📌 이진탐색 (Binary Search)

📢 오름차순으로 정렬되어 있는 데이터셋에서 특정 데이터를 찾아내는 가장 빠른 방법
🗓️ Last updated:

이진탐색

데이터셋이 있고, 그 안에서 원하는 값(target)을 찾고자 할 때는, 보통 데이터셋 제일 앞 데이터부터 순차적으로 검색을 한다. 그러나 이 방식은 운이 나쁘면 target을 찾는데 아주 오랜 시간이 걸릴 수 있다.

엑셀을 많이 이용해본 유저는 알겠지만, VLOOKUP 함수에 “정확히 일치” 옵션을 주고 사용하면, 범위에서 target을 순서대로 검사해가며 찾게 되는데, 엑셀 계산 속도가 체감이 될 정도로 느려진 경험을 해본 적이 한번쯤은 있을 것이다. 순차검색의 문제다.

하지만 데이터셋이 오름차순으로 정렬이 되어있다면, 특별한 알고리즘으로 원하는 데이터를 빠르게 찾을 수 있다. 이진탐색 알고리즘인데, 나무위키에 있는 그림만 봐도 어떤 알고리즘인지 쉽게 알 수 있을 것이다.

​마치 사전에서 어떤 단어를 찾을 때, 적당히 사전을 펴서 만일 그 단어보다 앞에 있는 단어라면 그 위치부터 뒤쪽으로 다시 적당한 위치를 어림잡아 펴보고하는 것과 같을 것이다.

그래서 왜 데이터셋이 사전에 오름차순으로 정렬이 되어있어야 하는지도 충분히 이해가 될 것이라 생각한다.

코드구현

찾고자 하는 값을 target, 데이터셋을 A라고 한다면, 대체로 아래와 같은 방식으로 찾을 수 있다.

  • 전체 A 범위에서 중간위치값 middle을 찾아서, target과 비교
  • middle < target이면 middle 포함 그 아래 데이터들을 모두 버리고, middle > target이면 middle 포함 그 위 데이터들을 모두 버림
  • 버리지않고 남은 데이터들을 다시 A로 정의하고, 위 단계를 middle == target이 될 때까지 계속 반복

알고리즘 이해는 간단하지만, 이를 실제 코드로 구현하려하면 생각보다 까다롭다. 여기저기 찾아봐서 개인적으로 가장 잘 구현되었다라고 느끼는 코드를 가져와서 아래 코딩테스트 문제에 적용해보았다.

leetcode: 35. Search Insert Position

https://leetcode.com/problems/search-insert-position

오름차순으로 주어진 데이터셋이 있을 때, 어떤 특정 값(target)이 위치한 인덱스를 찾거나, target이 없다면 오름차순을 위반하지 않고 삽입이 가능한 인덱스를 찾아 리턴하는 문제다.

위 문제는 target을 찾거나, 아니면 찾지 못했을 때 target이 들어갈 위치를 찾거나와 같이 두가지 케이스를 나눠서 해결해야하는 것처럼 보이는데 그렇게 문제를 풀 필요는 없다.

위 이진탐색 알고리즘을 계속 반복하다가 끝까지 target을 찾지 못해도, 마지막 반복까지 탐색한 그 자리가 target이 들어갈 인덱스가 되도록 짤 수 있다. 아래 코드는 그렇게 되도록 만들어져있는데, 누가 처음 고안했는지 참 대단하다고 느꼈다.

# python
class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        i, j = 0, len(nums)

        while i < j:
            m = i + (j-i) // 2
            if nums[m] < target:
                i = m + 1
            else:
                j = m

        return i

위에서 i와 j는 데이터셋에 해당하는 nums의 버려지지않은 채 아직 남아있는 데이터들의 인덱스를 가리키는 값으로, i이상 j미만으로 정의하였다. 알고리즘 풀이에서 종종 볼 수 있는 two pointers이지만, j가 “미만”으로 정의되어 있다는 점에 주의해야 한다.

m은 중간 인덱스를 구하고 있고, 중간값인 nums[m]과 target을 비교, 불필요한 데이터들을 버리는 것을 i와 j의 값을 조정하는 것으로 대신하고 있다. 참고로 i이상 j미만으로 정의했기 때문에 값조정 수식이 서로 다르다.

​위 코드가 훌륭한 점은, 어떠한 케이스에서도 반복을 계속하다보면 반드시 i == j가 되는 시점이 발생하며, 이때의 i 또는 j 값이 target이 오름차순을 유지한 채 삽입 가능한 인덱스가 된다는 점이다.

또한 반복문 중간에 target을 찾았더라도 그 인덱스를 리턴하지 않고 계속 반복을 진행하도록 되어있는데, 만일 아래와 같이 동일값이 여러개 있는 데이터셋 케이스에도 작동이 되어야 하기 때문이다.

target = 7
nums = [2, 3, 5, 7, 7, 7, 7, 7, 7, 11, 13]

코드를 보면, nums[m] == target인 경우 j를 조정하도록 되어있다. 즉 m 인덱스보다 뒤에 있는 데이터들을 버리는 셈이 되며, 바로 위 케이스 에서는 반복문 종료가 되었을 때 i 또는 j는 7 데이터들이 모여있는 구간의 제일 앞 인덱스인 3 이 된다. 만일 i를 조정하도록 했다면 결과는 7 데이터들이 모여있는 구간의 제일 뒤쪽의 다음칸 인덱스인 9 가 된다.

​참고로 python은 이진탐색을 하는 빌트인 라이브러리가 별도로 있으며, 이를 사용해서 문제를 풀 수도 있다.

# python
class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        return bisect_left(nums, target)

본래 bisect 라이브러리를 임포트해야 하나, leetcode 에서는 자동으로 임포트를 해준다. bisect 라이브러리 구체적 명세는 python 도움말을 참고하길 바란다.