이진 탐색은 정렬된 배열에서 특정 값을 찾기 위한 효율적인 알고리즘이다.
일반적인 선형 탐색보다 훨씬 빠르며, 특히 대규모 데이터셋에서 그 효율성이 두드러진다.

이진 탐색은 간단하면서도 강력한 알고리즘으로, 정렬된 데이터에서 매우 효율적인 검색을 가능하게 한다.
O(log n)의 시간 복잡도는 대규모 데이터셋에서 특히 중요하다.
이 알고리즘을 마스터하면 다양한 문제 해결과 시스템 최적화에 적용할 수 있다.

이진 검색은 정렬된 리스트에서 특정 값을 찾는 효율적인 알고리즘이다.
이 알고리즘은 리스트의 중간 값을 선택하고, 찾고자 하는 값과 비교하여 탐색 범위를 반으로 줄여가며 검색을 수행한다.

이진 탐색의 기본 원리

이진 탐색은 ‘분할 정복(divide and conquer)’ 전략을 사용한다.
정렬된 배열에서 중간 요소를 확인하고, 찾는 값이 중간 요소보다 작으면 왼쪽 절반을, 크면 오른쪽 절반을 검색한다. 이 과정을 값을 찾거나 더 이상 검색할 범위가 없을 때까지 반복한다.

동작 과정

  1. 정렬된 배열의 중간 요소를 찾는다.
  2. 중간 요소와 대상 값을 비교한다.
  3. 중간 요소가 대상 값과 일치하면 검색을 종료한다.
  4. 대상 값이 중간 요소보다 작으면 왼쪽 반을 검색한다.
  5. 대상 값이 중간 요소보다 크면 오른쪽 반을 검색한다.
  6. 검색 범위가 단일 요소로 줄어들 때까지 이 과정을 반복한다.

Binary Search
Source: https://jojozhuang.github.io/algorithm/algorithm-binary-search/

이진 탐색의 구현

Python으로 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def binary_search(arr, target):
    """
    정렬된 배열에서 대상 값의 인덱스를 찾는 이진 탐색 함수
    
    Args:
        arr: 정렬된 배열
        target: 찾으려는 값
        
    Returns:
        대상 값의 인덱스 또는 찾지 못한 경우 -1
    """
    left = 0
    right = len(arr) - 1
    
    while left <= right:
        # 중간 인덱스 계산 (오버플로우 방지를 위한 최적화 방법)
        mid = left + (right - left) // 2
        
        # 중간 요소가 대상 값인 경우
        if arr[mid] == target:
            return mid
        
        # 대상 값이 중간 요소보다 작은 경우, 왼쪽 부분 배열 검색
        elif arr[mid] > target:
            right = mid - 1
            
        # 대상 값이 중간 요소보다 큰 경우, 오른쪽 부분 배열 검색
        else:
            left = mid + 1
            
    # 값을 찾지 못한 경우
    return -1

JavaScript로 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * 정렬된 배열에서 대상 값의 인덱스를 찾는 이진 탐색 함수
 * @param {Array} arr - 정렬된 배열
 * @param {number} target - 찾으려는 값
 * @returns {number} - 대상 값의 인덱스 또는 찾지 못한 경우 -1
 */
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    
    while (left <= right) {
        // 중간 인덱스 계산 (오버플로우 방지를 위한 방법)
        const mid = Math.floor(left + (right - left) / 2);
        
        // 중간 요소가 대상 값인 경우
        if (arr[mid] === target) {
            return mid;
        }
        
        // 대상 값이 중간 요소보다 작은 경우, 왼쪽 부분 배열 검색
        else if (arr[mid] > target) {
            right = mid - 1;
        }
        
        // 대상 값이 중간 요소보다 큰 경우, 오른쪽 부분 배열 검색
        else {
            left = mid + 1;
        }
    }
    
    // 값을 찾지 못한 경우
    return -1;
}

재귀적 이진 탐색 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def binary_search_recursive(arr, target, left, right):
    """
    재귀적 이진 탐색 구현
    
    Args:
        arr: 정렬된 배열
        target: 찾으려는 값
        left: 왼쪽 경계 인덱스
        right: 오른쪽 경계 인덱스
        
    Returns:
        대상 값의 인덱스 또는 찾지 못한 경우 -1
    """
    # 기저 사례: 검색 범위가 없는 경우
    if left > right:
        return -1
    
    # 중간 인덱스 계산
    mid = left + (right - left) // 2
    
    # 중간 요소가 대상 값인 경우
    if arr[mid] == target:
        return mid
    
    # 대상 값이 중간 요소보다 작은 경우, 왼쪽 부분 배열 검색
    elif arr[mid] > target:
        return binary_search_recursive(arr, target, left, mid - 1)
    
    # 대상 값이 중간 요소보다 큰 경우, 오른쪽 부분 배열 검색
    else:
        return binary_search_recursive(arr, target, mid + 1, right)

# 사용 예:
# result = binary_search_recursive(sorted_array, target_value, 0, len(sorted_array) - 1)

이진 탐색의 시간 복잡도와 공간 복잡도

  1. 시간 복잡도

    • 최선의 경우: O(1) - 첫 번째 중간 요소가 대상 값인 경우
    • 평균 및 최악의 경우: O(log n) - 각 단계마다 검색 범위가 절반으로 줄어들기 때문.
      이 로그 시간 복잡도는 이진 탐색의 가장 큰 장점이다.
      10억 개의 요소가 있는 정렬된 배열에서도 최대 약 30번의 비교만으로 값을 찾을 수 있다.
  2. 공간 복잡도

    • 반복적 구현: O(1) - 고정된 추가 메모리만 사용.
    • 재귀적 구현: O(log n) - 재귀 호출 스택에 필요한 공간.

이진 탐색의 실용적 응용

  1. 데이터베이스 인덱싱: B-트리와 같은 데이터베이스 인덱스는 이진 탐색의 원리를 확장하여 사용.
  2. 라이브러리 함수: 많은 언어의 표준 라이브러리에는 이진 탐색 기능이 있다(예: Python의 bisect 모듈).
  3. 디버깅: 버그가 처음 발생한 코드 변경을 찾는 “git bisect"와 같은 도구.
  4. 최적화 알고리즘: 최적화 문제에서 특정 값을 찾기 위해 이진 탐색을 사용.

이진 탐색의 변형과 응용

하한과 상한 찾기

정렬된 배열에서 특정 값보다 작은(또는 큰) 첫 번째 요소를 찾는 데 사용된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def lower_bound(arr, target):
    """배열에서 target 이상인 첫 번째 요소의 인덱스를 찾습니다."""
    left, right = 0, len(arr)
    
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid
            
    return left

def upper_bound(arr, target):
    """배열에서 target보다 큰 첫 번째 요소의 인덱스를 찾습니다."""
    left, right = 0, len(arr)
    
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] <= target:
            left = mid + 1
        else:
            right = mid
            
    return left

회전된 정렬 배열에서의 이진 탐색

배열이 회전되어 있을 때도 이진 탐색을 적용할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def search_in_rotated_array(arr, target):
    """회전된 정렬 배열에서 대상 값을 찾습니다."""
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = left + (right - left) // 2
        
        if arr[mid] == target:
            return mid
            
        # 왼쪽 절반이 정렬되어 있는 경우
        if arr[left] <= arr[mid]:
            if arr[left] <= target < arr[mid]:
                right = mid - 1
            else:
                left = mid + 1
        # 오른쪽 절반이 정렬되어 있는 경우
        else:
            if arr[mid] < target <= arr[right]:
                left = mid + 1
            else:
                right = mid - 1
                
    return -1

이진 탐색 관련 문제 유형과 접근 방법

  1. 범위 검색: 특정 범위 내의 모든 요소를 찾을 때 이진 탐색으로 범위의 시작과 끝을 빠르게 찾을 수 있다.
  2. 첫 번째/마지막 출현 찾기: 중복 요소가 있는 배열에서 특정 값의 첫 번째 또는 마지막 인덱스를 찾는다.
  3. 이차원 배열에서의 검색: 행과 열이 정렬된 2D 배열에서 값을 검색하는 방법.
  4. 이진 탐색을 이용한 최적화: 특정 조건을 만족하는 최소/최대 값을 찾는 문제.

이진 탐색 구현 시 주의사항

  1. 오버플로우 방지: mid = (left + right) / 2 대신 mid = left + (right - left) // 2를 사용하여 정수 오버플로우를 방지.
  2. 무한 루프 방지: 종료 조건과 포인터 업데이트를 올바르게 설정해야 한다.
  3. 경계 조건 처리: 빈 배열, 단일 요소 배열, 또는 대상 값이 배열의 범위를 벗어나는 경우를 고려한다.
  4. 사전 조건 확인: 이진 탐색은 정렬된 배열에서만 작동한다는 것을 명심한다.

참고 및 출처