버블정렬

개요

인접한 배열의 두 수를 선택해, 그 두 수가 정렬되어 있지 않다면 두 수를 정렬하는 방식

 

개념

루틴: 인덱스 0부터 n-1까지 순차적으로 n번 비교하고, 만약 정렬이 필요하다면 Swapping함

루틴 1번이 돌 때 1개의 숫자가 정렬됨 (가장 큰 수가 n(가장 마지막)에 고정되고, 그 다음으로 큰 수가 n-2 자리에...)

 

시간 복잡도

O(n^2)

-> 배열의 크기가 n일 때 n번 비교(&스와핑)

 


BubbleSort.java
|-- main(String[] args)
|-- void bubbleSort()
public class BubbleSort {
    public static void main(String[] args) {
        bubbleSort();
    }

    static void bubbleSort() {
        int[] arr = createIntArray(10000);

        long startTime = System.currentTimeMillis();
        // 현재 시간 기준의 Milli seconds

        for (int i=0; i<arr.length; i++) {
            for (int j=0; j<arr.length-1; j++) {
                if (arr[j] > arr[j+1]) {    // 부등호 뒤집으면? 내림차순
                    swap(arr, j, j+1);
                }
            }
        }

        long endTime = System.currentTimeMillis();

        System.out.println(Arrays.toString(arr));
        System.out.printf("소요시간: %s 밀리초\n", endTime - startTime);
    }
}

 

유틸

TestUtil.java
|-- int[] createIntArray(int size)
|-- void swap(int[] arr, int i, int j)
import java.util.Random;

public class TestUtil {
    
    public static int[] createIntArray(int size) {
        int[] arr = new int[size];
        Random random = new Random();

        for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(size);
        }
        return arr;
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

 

이진 탐색 (Binary Search)

개요

  • 정렬된 데이터를 대상으로만 사용할 수 있다 ★
  • 반복적 접근: 배열의 중간 위치(값 기준X)를 기준으로 탐색을 진행하고, 목표값(검색 타겟)이 중간 값보다 작거나 큰지에 따라 탐색 범위를 줄임.

개념

투 포인터 방식을 사용함 ★

  1. 초기 설정: 시작 인덱스(left)와 끝 인덱스(right)를 설정합니다. 
    처음에는 left는 배열의 첫 번째 인덱스, right는 마지막 인덱스입니다.
  2. 중간 값 계산: 중간 인덱스(mid)를 계산합니다. 이는 mid = (left + right) / 2로 구할 수 있습니다.
  3. 비교:
    - 만약 arr[mid]가 찾고자 하는 값(target)과 같다면, 해당 인덱스를 반환합니다.
    - 만약 arr[mid]가 target보다 크다면, 탐색 범위를 왼쪽 절반으로 줄입니다 (right = mid - 1).
    - 만약 arr[mid]가 target보다 작다면, 탐색 범위를 오른쪽 절반으로 줄입니다 (left = mid + 1).
  4. 반복: 위 과정을 left가 right보다 작거나 같은 동안 반복합니다.
  5. 결과: target을 찾지 못한 경우 -1을 반환합니다.

시간복잡도

  • 최악의 경우 : O(log n) - 배열의 크기가 n일 때, 탐색 범위가 매번 반으로 줄어들기 때문에 로그 시간 복잡도를 가짐.
  • 최선의 경우 : O(1) - 중간 값이 바로 찾고자 하는 값인 경우
  • 평균의 경우 : O(log n)

코드 구현
static int binarySearch(int[] arr, int target) {
    // 투 포인터
    int left = 0;
    int right = arr.length - 1;

    // 종료 조건: 포인터끼리 만날 때까지 (모두 탐색함)
    while (left <= right) {
        int mid = (left + right) / 2;       // 중간 위치 설정
        if (arr[mid] == target) return mid; // 반환 조건: mid가 target이 될 때 반환함

        // mid의 위치를 기준으로 포인터를 옮김
        if (target < arr[mid]) {
            right = mid -1;
        } else if (target > arr[mid]) {
            left = mid + 1;
        }
    }

    // 디폴트 반환: 모든 데이터를 탐색했음에도 배열 내에 target이 존재하지 않음
    return -1;
}

 

테스트 코드
// Test Run
public static void main(String[] args) {
    int[] arr = {1,3,5,9,10,11,14,29};
    System.out.println(binarySearch(arr, 3));
}

 

선형 탐색 (Linear Search)

개요

배열이나 리스트와 같은 데이터 구조에서 특정 값을 찾기 위해 각 요소를 순차적으로 비교하는 간단한 탐색 알고리즘.

(어레이 안의 모든 요소를 탐색)

 

선형 탐색의 개념

배열의 첫번째 요소부터 시작해서 마지막까지 순차적으로 각 요소를 확인함.

찾고자 하는 값과 일치하는 요소를 발견하면 해당 인덱스를 반환.

만약 배열의 모든 요소를 확인했음에도 불구하고 값을 찾지 못하면 -1을 반환.

 

시간 복잡도

최악의 경우 : O(n) - 배열의 모든 요소를 탐색해야 할 수도 있음.

최선의 경우 : O(1) - 첫 번째 요소가 찾고자 하는 값일 경우.

평균의 경우 : O(n) - 찾고자 하는 값이 배열의 중간에 있을 경우.


기본 구현 코드
// 선형 탐색 (기본)
static int seqSearch(int[] arr, int target) {
    int idx = 0;    // 반환할 인덱스 변수 (0부터 탐색 시작)

    // 반복문 1회 -> 2개의 조건문 연산
    while (true) {
        // 종료조건: idx가 arr의 끝(=arr.length-1)을 초과하면 
        if ( idx == arr.length ) return -1;
        // 반환조건: arr의 값이 target과 일치하면 idx를 반환
        if ( arr[idx] == target ) return idx;
        idx++;
    }
}

 

보초법 (개선된 버전)
더보기

~ 보초법을 사용했을 때의 이점 ~

 

1. 루프 종료 조건 단순화

  • 보초법을 사용하면, 마지막 요소에 검색할 값을 미리 설정하여 루프 종료 조건을 단순화할 수 있습니다.
    일반적인 선형 탐색에서는 배열의 범위를 초과하지 않도록 인덱스를 체크해야 하지만, 보초법을 사용하면 이러한 체크가 필요 없어집니다.
  • 예를 들어, while (true) 루프를 사용하여 target을 찾을 때까지 계속 탐색할 수 있습니다.

2. 성능 향상

  • 보초법을 사용하면, 루프 내에서 인덱스의 유효성을 체크하는 조건문이 필요 없으므로, 조건문에 대한 오버헤드가 줄어듭니다. 이는 특히 반복 횟수가 많을 때 성능 향상으로 이어질 수 있습니다.

3. 코드 가독성 향상

  • 보초법은 코드의 가독성을 높여 줍니다. 루프가 종료되는 조건을 명시적으로 체크할 필요가 없어지므로, 코드가 더 간결해지고 이해하기 쉬워집니다.

4. 모든 요소 탐색 보장

  • 보초법을 사용하면, 배열의 마지막 요소까지 탐색하게 되므로, target이 배열의 마지막에 있을 때도 이를 찾을 수 있습니다. 일반적인 선형 탐색에서는 루프가 종료되기 전에 target을 찾지 못할 수 있지만, 보초법은 이 문제를 해결합니다.

5. 특별한 경우 처리 용이

  • 보초법을 사용하면, 배열의 모든 요소를 탐색한 후에도 target이 존재하지 않을 경우, return -1과 같은 처리를 간단하게 할 수 있습니다. 이로 인해 예외적인 상황을 다루기 쉬워집니다.
// 선형 탐색 (보초법)
static int seqSearchSentinel(int[] arr, int target) {
    // 반환조건: arr의 마지막 요소가 target이라면, 바로 lastIdx를 반환
    int lastIdx = arr.length - 1;
    if (arr[lastIdx] == target) return lastIdx;

    // 위의 조건에 맞지 않는다면, 이하의 과정을 진행
    // 배열의 마지막 인덱스 위치에 target을 넣음 (보초)
    arr[lastIdx] = target;

    // 종료 조건 1번(인덱스 범위 검사)이 필요 없어지고, 탐색할 데이터의 크기가 1 줄어듦
    // 즉, 반복문 1회 -> 조건문 비교 연산 1회
    int idx = 0;
    while (true) {
        if (arr[idx] == target) {
            // 반환조건: 전체 탐색에서 딱 1번만 실행됨
            return (idx < lastIdx) ? idx : -1;
        }
        idx++;
    }
}

 

테스트 코드
// Test Run
public static void main(String[] args) {
    int index = seqSearch(new int[]{1,6,12,4,76,23,411}, 4);
    System.out.println(index);
}

Recursion (재귀) (Chapter 5)
Recurrence equation for recursion (재귀를 위한 점화식)

별 찍기

Big O 표기법

O(1)  데이터 크기에 무관하게 항상 일정한 시간이 걸리는 경우
-> 상수(constant)
배열 인덱스 검색
O(log n) 데이터 크기가 2배 증가할 때 연산 수가 1단계씩 늘어나는 형태
-> 로그(logarithmic)
이진검색
O(n)  입력(탐색할 데이터) 개수와 수행시간이 비례하는 경우
-> 선형 (linear)
반복문, 선형 검색
O(n log n) N * log2 N 번만큼의 수행시간을 가짐
-> 선형로그(linear-logarithmic)
증가 스텝이 2인 반복문, 병합정렬
O(n^c) 문제 해결 단계 수가 입력값 n제곱
-> 다차(polynomial) 형태
이중 for문, 버블정렬, 삽입정렬, 선택정렬
O(C^n) n이 증가할 때마다 걸리는 시간이 C배 증가
-> 지수(exponential) 형태
 
O(n!) 팩토리얼 형태 존재해서는 안됨

 

+ Recent posts