탐색: 선형 탐색과 이진 탐색
정렬된 이름 100만 개 가운데 하나를 찾을 때, 이진 탐색은 20번 넘게 확인할 일이 없습니다.
값 하나 찾기
탐색은 모음 안에서 어떤 값이 어디 있는지 찾거나, 없다는 것을 알아내는 문제입니다. 휴대전화에 연락처 이름을 칠 때, 가게가 물건 재고를 확인할 때, 맞춤법 검사기가 낱말 하나하나를 확인할 때 탐색이 일어납니다. 입력은 목록과 찾을 값이고, 출력은 그 값의 위치이거나 「없음」을 뜻하는 -1 같은 표시입니다.
선형 탐색: 하나씩 차례로
가장 쉬운 방법은 왼쪽부터 값을 하나씩 확인하다가 찾는 값을 만나거나 목록이 끝나면 멈추는 것입니다. 이것이 선형 탐색입니다. 정렬되어 있든 아니든 어떤 목록에서나 쓸 수 있습니다. 드는 수고는 운에 달려 있습니다. 찾는 값이 맨 앞이면 한 번으로 끝나고, 맨 뒤에 있거나 아예 없으면 모든 값을 확인해야 합니다. 값이 n개면 최악의 경우 n번입니다. 목록 길이가 두 배면 확인도 두 배가 될 수 있어서, 일의 양이 크기에 따라 직선으로 늘어난다는 뜻으로 「선형」이라고 부릅니다.
function linearSearch(list, target) {
for (let i = 0; i < list.length; i++) {
if (list[i] === target) return i; // found it
}
return -1; // checked everything
}이진 탐색: 남은 범위를 반으로
목록이 정렬되어 있으면 훨씬 잘할 수 있습니다. 숫자 맞히기 놀이를 떠올려 보세요. 한 사람이 1부터 100 사이의 수를 정하고 「더 크다」 또는 「더 작다」로만 대답합니다. 요령 있는 사람은 먼저 50을 부릅니다. 대답이 무엇이든 가능성의 절반이 사라집니다. 다음에는 남은 범위의 가운데를 부르고, 이렇게 계속합니다. 이진 탐색은 정렬된 목록에서 바로 이 일을 합니다. 찾는 값이 아직 있을 수 있는 구간 양 끝에 lo와 hi라는 표시를 둡니다. 가운데 값을 확인해서 찾는 값이면 끝냅니다. 가운데 값이 찾는 값보다 작으면 그 왼쪽은 모두 더 작으므로 lo를 가운데 다음 칸으로 옮깁니다. 크면 hi를 가운데 앞 칸으로 옮깁니다. lo가 hi를 넘어서면 남은 칸이 없으니 목록에 그 값이 없는 것입니다.
function binarySearch(list, target) {
let lo = 0, hi = list.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
if (list[mid] === target) return mid;
if (list[mid] < target) lo = mid + 1; // target is to the right
else hi = mid - 1; // target is to the left
}
return -1;
}끌어서 돌리고, 휠이나 두 손가락으로 확대하세요.
몇 번 확인할까요?
이진 탐색은 한 번 확인할 때마다 남은 구간을 적어도 절반으로 줄입니다. 값 n개에서 시작하면 한 번 뒤에는 많아야 약 n/2개, 두 번 뒤에는 약 n/4개가 남고, 이렇게 하나가 남을 때까지 갑니다. n을 1까지 줄이는 데 필요한 반 가르기 횟수를 밑이 2인 로그라고 하고 log₂ n으로 씁니다. 마지막 확인까지 세면 이진 탐색은 많아야 ⌊log₂ n⌋ + 1번 확인합니다. ⌊ ⌋는 내림을 뜻합니다. 값 100개라면 7번이어서, 숫자 맞히기 놀이는 언제나 7번 안에 이길 수 있습니다. 1,000개면 10번, 100만 개면 20번, 10억 개면 30번입니다.
| 목록 크기 n | 선형 탐색 최악 | 이진 탐색 최악 |
|---|---|---|
| 7 | 7 | 3 |
| 100 | 100 | 7 |
| 1,000 | 1,000 | 10 |
| 1,000,000 | 1,000,000 | 20 |
| 1,000,000,000 | 1,000,000,000 | 30 |
조건: 목록이 정렬되어 있어야 합니다
이진 탐색이 통하는 까닭은 정렬된 목록에서는 비교 한 번으로 찾는 값이 어느 쪽에 있어야 하는지 알 수 있기 때문입니다. 정렬되지 않은 목록에서는 가운데 값이 이웃에 대해 아무것도 알려 주지 않으므로 다시 선형 탐색으로 돌아가야 합니다. 정렬 자체에도 수고가 들기 때문에(다음 두 단원의 주제입니다) 어떻게 쓰느냐에 따라 고릅니다. 목록을 한 번만 찾는다면 선형 탐색이 가장 쌀 수 있습니다. 같은 목록을 수천 번 찾는다면 한 번 정렬해 두고 이진 탐색을 쓰는 편이 금방 이득입니다. 데이터를 준비하는 수고와 질문에 답하는 수고를 맞바꾸는 일은 이 분야 전체에 걸쳐 나옵니다.
lo = mid + 1 대신 lo = mid라고 쓰면 값이 두 개 남았을 때 mid가 바뀌지 않아 반복문이 끝나지 않을 수 있습니다. 가운데 값은 이미 확인했으므로 다음 범위에서 빼도 됩니다.| 선형 탐색 | 처음부터 값을 하나씩 확인 |
| 이진 탐색 | 정렬된 구간의 가운데를 확인하고 찾는 값이 있을 수 없는 절반을 버림 |
| 최악의 경우 | 알고리즘이 가장 많은 일을 하게 되는 입력 |
| log₂ n | n을 1이 될 때까지 반으로 가를 수 있는 횟수 |
| lo와 hi | 찾는 값이 아직 있을 수 있는 구간의 양 끝 |
| -1 | 「찾지 못함」을 뜻하는 흔한 반환값 |
- 정렬된 값 1,000개에서 이진 탐색은 많아야 몇 번 확인할까요?
- 값 1,000개에서 선형 탐색은 많아야 몇 번 확인할까요?
- 이진 탐색을 쓰려면 목록이 어때야 할까요?
- 정렬되어 있어야 함
- 짧아야 함
- 정수로만 되어 있어야 함
- 같은 값이 없어야 함
- [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]에서 이진 탐색으로 23을 찾을 때 몇 번 확인할까요?
- 같은 목록에서 선형 탐색으로 23을 찾으면 몇 번 확인할까요?
- 이진 탐색이 25를 찾다가 가운데 값 40을 확인했습니다. 다음에는?
- hi를 가운데 바로 앞 칸으로 옮긴다
- lo를 가운데 바로 다음 칸으로 옮긴다
- 25가 없으므로 멈춘다
- 오른쪽 다음 값을 확인한다
- 1부터 100 사이의 수를 맞히는데 「더 크다」 「더 작다」만 알려 줍니다. 가장 좋은 방법으로 몇 번이면 언제나 충분할까요?
- 목록 크기가 두 배가 되면 이진 탐색의 최악 확인 횟수는?
- 두 배가 된다
- 약 하나 늘어난다
- 정확히 그대로다
- 제곱이 된다
- 정렬되지 않은 목록에서 값 하나를 한 번만 찾습니다. 어느 쪽이 쌀까요?
- 선형 탐색
- 정렬한 뒤 이진 탐색
lo = mid + 1대신lo = mid로 쓰면 왜 끝나지 않을 수 있을까요?- 값이 두 개 남으면 mid가 계속 같을 수 있어서
- mid가 음수가 되어서
- 자바스크립트가 허용하지 않아서
- 목록이 길어져서
- 값 1,000,000개에서 이진 탐색은 많아야 몇 번 확인할까요?
- 선형 탐색은 값을 하나씩 확인합니다. 많으면 n번입니다.
- 이진 탐색은 남은 범위를 반으로 줄입니다. 많아야 ⌊log₂ n⌋ + 1번입니다.
- 이진 탐색은 정렬된 목록에서만 통합니다.
- 값 100만 개라도 많아야 20번입니다.
- 범위는 mid + 1, mid − 1로 고쳐야 늘 줄어듭니다.