이분탐색의 종료조건과 경계조건 정리
이분탐색의 종료조건과 경계조건, 즉 while문에 무엇이 들어가야 하는지, lo와 high를 각각 무슨 값으로 설정해야 하는지를 저를 포함해서 헷갈리시는 분들이 있는 거 같아서 한번 정리해 보려고 합니다.
기본 구조
우선 이분탐색의 기본 구조는 다음과 같습니다. (물론 이와 다르게 쓰셔도 상관없습니다)
int binary_search(int *arr, int key, int start, int end)
{
int lo = /* */;
int high = /* */;
while (/* 1 */)
{
int mid = /* 2 */;
if (arr[mid] == key)
return mid;
if (arr[mid] < key)
/* 3 */
else // arr[mid] > key
/* 4 */
}
return -1;
}
lo, high에 대한 생각은 뒤로 미루어놓고, 1~4번에 들어갈 내용을 구체적인 코드로 적기 전에 문장으로 먼저 생각해 보겠습니다.
각 빈칸을 문장으로 채우기
1번: while문은 언제까지 돌아야 하는가
만약 우리가 탐색하는 배열 범위의 길이가 0이 되면, 원하는 데이터를 찾지 못했다는 것이고 while문에서 빠져나와야 합니다.
→ 탐색하는 배열 범위의 길이가 1 이상일 때
2번: mid를 무엇으로 설정하는가
mid는 탐색하고자 하는 범위의 가운데를 가리킵니다.
→ (탐색 범위 시작점 + 탐색 범위 종점) / 2
여기서부터 나오는 "탐색 범위 시작점"과 "탐색 범위 종점"은 포함관계입니다. 즉, 탐색하고자 하는 배열의 인덱스를 i라 할 때 탐색 범위 시작점 <= i <= 탐색 범위 종점을 말합니다.
3번: arr[mid] < key일 때
mid에 있는 원소가 찾고자 하는 값보다 작다는 것은, 찾고자 하는 값이 mid 오른쪽에 있다는 뜻입니다.
→ 탐색 범위를
mid < i <= 탐색 범위 종점으로 설정
4번: arr[mid] > key일 때
반대로 찾고자 하는 값이 mid 왼쪽에 있다는 뜻입니다.
→ 탐색 범위를
탐색 범위 시작점 <= i < mid로 설정
문장을 대입한 중간 형태
int binary_search(int *arr, int key, int start, int end)
{
int lo = /* */;
int high = /* */;
while (탐색하는 배열 범위의 길이가 1 이상일 때)
{
int mid = (탐색 범위 시작점 + 탐색 범위 종점) / 2;
if (arr[mid] == key)
return mid;
if (arr[mid] < key)
탐색 범위를 "mid < i <= 탐색 범위 종점"으로 설정;
else
탐색 범위를 "탐색 범위 시작점 <= i < mid"로 설정;
}
return -1;
}
각 문장에 들어갈 실제 코드는 lo와 high를 어떻게 설정하느냐에 따라 달라집니다.
lo와 high를 설정하는 4가지 방법
- 양쪽 포함:
lo <= i <= high - lo 포함, high 미포함:
lo <= i < high - lo 미포함, high 포함:
lo < i <= high - 양쪽 미포함:
lo < i < high
케이스 1: lo <= i <= high (양쪽 포함)
초기값
탐색해야 하는 배열의 전체 범위는 start <= i <= end이고, 이것을 lo <= i <= high로 표현하면:
lo = start, high = end
mid
탐색 범위 시작점 = lo, 탐색 범위 종점 = high이므로:
mid = (lo + high) / 2
while 조건
탐색 범위의 길이가 1 이상일 때를 lo와 high로 표현하면 lo <= high입니다.
lo = 3, high = 3이면 → 범위는3 <= i <= 3→ i = 3 → 길이 1 ✅lo = 4, high = 3이면 → 범위는4 <= i <= 3→ 만족하는 i 없음 → 길이 0 ❌
while (lo <= high)
arr[mid] < key일 때
mid < i <= 종점으로 범위를 좁혀야 합니다.
lo는 포함관계이므로, mid < i를 만족시키려면 lo = mid + 1로 설정하면 됩니다.
→ mid + 1 <= i <= high, 즉 mid < i <= high
lo = mid + 1
arr[mid] > key일 때
시작점 <= i < mid로 범위를 좁혀야 합니다.
high는 포함관계이므로, i < mid를 만족시키려면 high = mid - 1로 설정하면 됩니다.
→ lo <= i <= mid - 1, 즉 lo <= i < mid
high = mid - 1
최종 코드
int binary_search(int *arr, int key, int start, int end)
{
int lo = start;
int high = end;
while (lo <= high)
{
int mid = (lo + high) / 2;
if (arr[mid] == key)
return mid;
if (arr[mid] < key)
lo = mid + 1;
else
high = mid - 1;
}
return -1;
}
케이스 2: lo <= i < high (lo 포함, high 미포함)
초기값
전체 범위 start <= i <= end를 lo <= i < high로 표현해야 합니다.
lo는 포함이므로 케이스 1과 동일하게 lo = start입니다. high는 미포함이므로, i < high가 i <= end와 같으려면 high = end + 1이어야 합니다.
lo = start, high = end + 1
mid
앞서 정한 문장은 (탐색 범위 시작점 + 탐색 범위 종점) / 2였습니다.
시작점 = lo는 케이스 1과 동일하지만, 종점은 high가 아니라 high - 1입니다 (high는 미포함이므로).
mid = (lo + high - 1) / 2
while 조건
lo = 3, high = 4이면 → 범위는3 <= i < 4→ i = 3 → 길이 1 ✅lo = 3, high = 3이면 → 범위는3 <= i < 3→ 만족하는 i 없음 → 길이 0 ❌
while (lo < high)
arr[mid] < key일 때
lo는 포함관계이므로 케이스 1과 동일하게 lo = mid + 1입니다.
lo = mid + 1
arr[mid] > key일 때
케이스 1에서는 high가 포함관계라서 high = mid - 1로 1을 빼야 했지만, 여기서는 high가 미포함관계이므로 high = mid만 하면 i < mid가 만족됩니다.
→ lo <= i < high, 즉 lo <= i < mid
high = mid
최종 코드
int binary_search(int *arr, int key, int start, int end)
{
int lo = start;
int high = end + 1;
while (lo < high)
{
int mid = (lo + high - 1) / 2;
if (arr[mid] == key)
return mid;
if (arr[mid] < key)
lo = mid + 1;
else
high = mid;
}
return -1;
}
케이스 3, 4
케이스 3과 4는 자세한 설명은 생략하고 코드만 올리겠습니다.
케이스 3: lo < i <= high (lo 미포함, high 포함)
int binary_search(int *arr, int key, int start, int end)
{
int lo = start - 1;
int high = end;
while (lo < high) // ex) 3 < i <= 4 -> i = 4
{
int mid = (lo + high + 1) / 2;
if (arr[mid] == key)
return mid;
if (arr[mid] < key)
lo = mid;
else
high = mid - 1;
}
return -1;
}
케이스 4: lo < i < high (양쪽 미포함)
int binary_search(int *arr, int key, int start, int end)
{
int lo = start - 1;
int high = end + 1;
while (lo + 1 < high) // ex) 3 < i < 5 -> i = 4
{
int mid = (lo + high) / 2; // (lo + 1 + high - 1) / 2
if (arr[mid] == key)
return mid;
if (arr[mid] < key)
lo = mid;
else
high = mid;
}
return -1;
}
unsigned 타입을 사용할 때
일반적으로는 어떤 케이스를 선택하든 문제가 없을겁니다. 다만 unsigned 타입(unsigned int, size_t 등)을 사용할 때는 케이스 2만 안전하게 사용할 수 있습니다.
케이스 3과 4는 lo = start - 1로 설정하기 때문에, start가 0이면 언더플로우가 발생합니다. 케이스 1도 high = mid - 1 때문에 언더플로우가 날 수 있습니다.
따라서 unsigned 타입을 쓰고 싶다면 케이스 2를 선택해야 합니다 (물론 다른 구현 방법도 있습니다). 실제로 libc의 bsearch 구현을 보면 케이스 2를 선택한 것을 알 수 있습니다. 소스코드
이런 식으로 빈칸에 들어갈 것들을 lo, high를 이용해서 코드를 넣기 전에 문장으로 먼저 생각해보고, lo와 high가 어떻게 설정되어 있는지에 따라 코드로 채워넣으면 헷갈리지 않고 이진탐색을 구현할 수 있지 않나 싶습니다.


댓글 댓글 쓰기