이진 탐색의 불변식: 탐색 구간·중간값·종료 조건

··설명 약 9분·작성: 해비·블로그 소개

읽기 전에 알아두면 좋아요

아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.

개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.

과정 위치와 선택 복습 안내

과정 위치: 구간과 탐색 · 알고리즘 개념

과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.

JavaScript로 문법·실행 복습하기 (선택)

개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

이진 탐색은 탐색 구간과 종료 조건을 어떻게 일관되게 정하나요?

이진 탐색은 정렬된 배열에서 답이 있을 구간을 절반씩 줄이는 방법입니다. 구간의 오른쪽 끝을 포함할지부터 정하면 이동과 종료 조건을 일관되게 쓸 수 있습니다.

정렬된 1·3·3·7에서 첫 3 이상인 인덱스 1과 뒤의 같은 값을 구분한 그림
target이 3일 때 lower bound는 같은 값 중 아무 위치가 아닌 첫 후보 인덱스 1을 찾습니다. 원본 보기 (새 탭)

[1,3,3,7]에서 3 이상인 첫 위치는 인덱스 1입니다. 아무 3 하나를 찾는 것과 첫 위치를 찾는 것은 서로 다른 목표입니다.

적용 조건·범위: 원본은 정렬을 수행하거나 정렬 여부를 검사하지 않습니다. 예를 들어 [3,1,2]에서 3을 찾으면 중간 1보다 크다는 이유로 오른쪽만 보고 null을 반환할 수 있습니다. 오름차순으로 정렬된 숫자 배열을 준비해야 합니다. 문자열 순서가 적용되는 기본 sort()와 혼동하지 말고, 숫자 정렬에는 sort((a,b) => a-b)처럼 같은 순서 기준을 사용합니다. 탐색 중 배열도 바꾸지 않습니다.

본문 근거: C++ 표준 초안 — lower_bound

이진 탐색 개념을 표현한 일러스트 표지

이 글에서 할 일: 반열린 구간으로 lower bound를 구현하고 존재 여부 검사에 연결합니다. 선수 지식: 정렬된 배열·인덱스·반복문.

반열린 구간으로 첫 위치를 찾습니다

JavaScript 예제: 찾는 값 이상인 첫 위치와 값의 존재 여부를 구합니다. 아래 코드와 실행 결과를 읽고, 이어지는 구간 변화 표로 후보 범위가 줄어드는 과정을 따라가 보세요. 직접 실행하지 않아도 설명을 이해할 수 있습니다.

왼쪽을 포함하고 오른쪽을 제외하는 구간과 lower bound의 두 경계 갱신 규칙
[left,right)는 왼쪽 경계 0을 포함하고 오른쪽 경계 4를 제외합니다. 중간값이 작으면 left를 mid+1로, 그렇지 않으면 right를 mid로 옮깁니다. 원본 보기 (새 탭)
function lowerBound(values, target) {
  let left = 0;
  let right = values.length; // 후보 구간 [left, right)
  while (left < right) {
    const mid = left + Math.floor((right - left) / 2);
    if (values[mid] < target) left = mid + 1;
    else right = mid;
  }
  return left;
}

function contains(values, target) {
  const index = lowerBound(values, target);
  return index < values.length && values[index] === target;
}

console.log(lowerBound([1, 3, 3, 7], 3));
console.log(contains([1, 3, 3, 7], 5));
console.log(lowerBound([], 3));

실행 결과

1
false
0

[left,right)는 left를 포함하고 right는 포함하지 않는 구간입니다. 시작은 [0,N)이며 N은 읽을 인덱스가 아니라 구간의 끝입니다. 중간값이 target보다 작으면 그 위치까지 답이 아니므로 left = mid + 1로 옮깁니다. 그렇지 않으면 mid가 첫 후보일 수 있으므로 right = mid로 남깁니다.

구간 바깥에 대해 알고 있는 사실을 유지합니다

항상 left 앞의 값은 모두 target보다 작고, right부터 뒤의 값은 모두 target 이상입니다. 아직 판정하지 않은 부분만 [left,right)에 남습니다. 구간 길이는 매번 줄어들며 left와 right가 같아지면 그 경계가 첫 후보 위치입니다.

1·3·3·7에서 target 3의 lower bound를 찾는 세 번의 반열린 구간 축소
중간값이 목표 이상이면 오른쪽 경계를 mid로 남깁니다. [0,4)에서 [1,1)까지 줄어든 경계 1이 첫 후보입니다. 원본 보기 (새 탭)
[1,3,3,7]에서 target=3 찾기
구간 mid와 값 다음 구간
[0,4) 2 → 3 [0,2)
[0,2) 1 → 3 [0,1)
[0,1) 0 → 1 [1,1)
종료 left=1 첫 3의 위치

찾는 값보다 모두 작으면 N을 반환합니다. 따라서 존재 여부는 인덱스가 N보다 작은지 확인한 뒤 실제 값도 비교해야 합니다. lower bound 자체는 값이 없더라도 삽입할 경계를 돌려줍니다.

내 컴퓨터에서 직접 실행하기 — 이진 탐색 예제 (선택)

코드를 직접 바꿔보고 싶은 분을 위한 안내입니다. 이 과정은 방문자 자신의 컴퓨터에서 진행하며, 글을 읽기 위해 반드시 실행할 필요는 없습니다.

준비물: Node.js와 코드 편집기인 Visual Studio Code(VS Code). Node.js는 JavaScript 파일을 컴퓨터에서 실행하는 프로그램입니다. 아직 설치하지 않았다면 두 프로그램의 공식 사이트에서 설치한 뒤 진행하세요.

  1. 컴퓨터에 연습용 폴더를 하나 만들고, VS Code에서 파일 → 폴더 열기로 그 폴더를 엽니다.
  2. 그 폴더 안에 lower-bound.js 파일을 만듭니다. 위의 JavaScript 예제 전체를 복사해 붙여넣고 저장하세요. 마지막 console.log 줄도 포함하며, 파일 이름 끝이 .js.txt가 되지 않도록 확인합니다.
  3. VS Code의 터미널 → 새 터미널을 선택합니다. 아래쪽에 열린 터미널은 명령어를 입력하는 창입니다. 다음 명령으로 Node.js 설치 여부를 확인합니다.
node --version

v로 시작하는 버전 번호가 나오면 실행할 수 있습니다. 명령을 찾을 수 없다는 메시지가 나오면 Node.js를 설치하고 VS Code를 완전히 닫았다가 다시 여세요.

터미널의 현재 위치가 방금 파일을 저장한 폴더인지 확인한 뒤, 다음 명령을 입력하고 Enter를 누릅니다. 이 명령은 JavaScript 파일 안이 아니라 터미널에 입력합니다.

node lower-bound.js

터미널에 나온 값을 위의 실행 결과와 비교하세요. Cannot find module이 나오면 파일 이름과 저장 여부, 터미널에서 연 폴더가 같은지 확인합니다.

구간 규칙과 정렬 비용을 섞지 않습니다

닫힌 구간 [left,right]으로 존재 여부를 찾는 방식도 있습니다. 그 방식의 초기 right=N-1, 반복 left≤right, 제외 시 right=mid-1을 반열린 코드에 부분적으로 섞으면 누락이나 무한 반복이 생깁니다. 한 방식을 끝까지 유지하세요.

반열린 구간과 닫힌 구간의 초기 경계·반복 조건·왼쪽 갱신을 비교한 표
초기 right, 반복 조건, mid를 남기거나 제외하는 규칙은 한 방식으로 유지합니다. 사전 정렬이 필요하면 탐색 비용과 별도로 더합니다. 원본 보기 (새 탭)

정렬된 배열을 한 번 찾는 시간은 O(log N), 추가 공간은 O(1)입니다. 정렬되지 않았다면 정렬 비용도 필요합니다. 비교 정렬 O(N log N) 후 Q번 찾으면 총 O(N log N + Q log N)입니다. 질의가 한 번뿐이면 O(N) 선형 탐색이 더 단순할 수 있습니다.

이 코드는 랜덤 접근이 가능한 배열의 이진 탐색입니다. 노드와 연결로 값을 저장하는 이진 탐색 트리의 삽입·삭제 연산과는 구분합니다. 입력은 오름차순이며 NaN처럼 순서 비교가 성립하지 않는 값은 제외합니다.

직접 확인하고 문제에 적용하기

[2,2,5]에서 target을 1, 2, 4, 9로 바꾸면 lower bound는 각각 얼마일까요?

배열 2·2·5에서 네 목표값의 lower bound를 직접 적는 빈 표
목표를 1, 2, 4, 9로 바꾸며 첫 경계를 직접 찾아 보세요. 반환 위치에 실제 목표값이 있는지도 별도로 확인합니다. 원본 보기 (새 탭)
답과 이유 확인

0, 0, 2, 3입니다. target=4의 반환 위치 2에는 5가 있으므로 contains는 false입니다. 빈 배열에서는 lower bound 0, contains false입니다.

참고 자료

자료 확인일: 2026년 10월 19일.

첨부 재귀 이진 탐색: 인덱스 반환과 닫힌 구간 읽기

첨부 binarySearch.mjs는 정렬된 배열에서 값 하나를 찾아 인덱스 또는 null을 반환합니다. 앞에서 배운 lowerBound와 구간을 절반씩 줄이는 원리는 같지만 반환값과 경계 규칙이 다릅니다. 원본은 binarySearch(arr, target, 0, arr.length - 1)로 시작하며 start와 end를 모두 포함하는 닫힌 구간을 사용합니다.

1부터 10 배열에서 3을 찾는 닫힌 구간 탐색과 세 반환 계약 비교
닫힌 구간은 [0,9]→[0,3]→[2,3]으로 줄어 인덱스 2를 반환합니다. 인덱스 0도 성공이므로 원본 계약은 null과 직접 비교합니다. 원본 보기 (새 탭)

같은 “탐색”이어도 반환값은 세 가지 계약입니다

함수 성공·실패 결과 중복과 빈 배열
첨부 binarySearch 발견 인덱스 / null 일치한 어느 위치든 반환; 빈 배열은 null
기존 lowerBound target 이상인 첫 위치 0…N 같은 값의 첫 위치; 빈 배열은 0
기존 contains true / false lowerBound 결과의 범위와 값까지 검사; 빈 배열은 false

원본 함수는 실패에 false나 undefined를 반환하지 않습니다. 특히 인덱스 0은 성공인데 조건문에서 거짓으로 취급되므로 if (index)로 발견 여부를 검사하면 첫 원소를 놓칩니다. 원본 계약에서는 index !== null로 검사합니다. lowerBound의 반환 위치는 값이 없을 때도 유효한 삽입 경계일 수 있으므로 같은 검사를 적용하면 안 됩니다.

원본의 [1,2,3,4,5,6,7,8,9,10]에서 3 찾기

닫힌 구간 mid / arr[mid] 다음 행동
[0,9] 4 / 5 3이 작으므로 [0,3]
[0,3] 1 / 2 3이 크므로 [2,3]
[2,3] 2 / 3 일치하여 인덱스 2 반환

원본의 Math.floor((start + end) / 2)는 중간 인덱스를 정수로 만듭니다. 배열에 담긴 숫자를 내림하는 것이 아닙니다. 값이 다르면 방금 확인한 mid는 후보에서 제외되므로 오른쪽 검색은 mid + 1, 왼쪽 검색은 mid - 1입니다. start > end가 되면 후보가 없어서 null로 끝납니다. 빈 배열의 첫 구간은 [0,-1]이므로 원소를 읽지 않고 바로 끝납니다.

기존 반열린 lowerBound는 [left,right)·right=N·left<right·right=mid를 한 묶음으로 사용합니다. 원본 닫힌 구간의 end=N-1·start≤end·end=mid-1와 섞지 마세요. 중복 배열 [1,3,3,3,7]의 원본 탐색은 첫 mid=2에서 3을 만나 2를 반환합니다. 첫 3의 위치 1이 필요하다면 기존 lowerBound를 사용합니다.

정렬 조건과 실제 숫자 비교를 구분합니다

원본은 정렬을 수행하거나 정렬 여부를 검사하지 않습니다. 예를 들어 [3,1,2]에서 3을 찾으면 중간 1보다 크다는 이유로 오른쪽만 보고 null을 반환할 수 있습니다. 오름차순으로 정렬된 숫자 배열을 준비해야 합니다. 문자열 순서가 적용되는 기본 sort()와 혼동하지 말고, 숫자 정렬에는 sort((a,b) => a-b)처럼 같은 순서 기준을 사용합니다. 탐색 중 배열도 바꾸지 않습니다.

인덱스와 달리 값이 반드시 정수일 필요는 없습니다. [-1.5,0.25,2.75]에서 0.25도 찾을 수 있습니다. 다만 숫자의 직접 비교가 성립해야 하므로 이번 예제는 유한한 숫자를 전제로 하고 NaN·문자열·빈 칸을 제외합니다. 원본의 ==는 문자열 ‘3’과 숫자 3도 같게 취급할 수 있지만 입력 타입을 확인해 주지는 않습니다.

실수 계산값의 정확한 동등성은 별도 문제입니다. JavaScript에서 0.1 + 0.2 === 0.3은 false여서 [0.3]에서 그 합을 정확히 찾으면 실패합니다. 임의의 오차 허용 비교를 추가하면 정렬·경계 계약도 함께 설계해야 합니다. 아래 예제는 근삿값 검색을 추가하지 않습니다.

아래 독립 예제는 원본과 같은 닫힌 구간과 인덱스/null 반환을 유지하면서 경계를 함수 안에서 초기화하고, 숫자 비교를 ===로 통일한 반복문입니다. mid도 start + floor((end-start)/2)로 씁니다. 이는 별도 학습 예제이며 기존 lowerBound/contains를 교체하지 않습니다. 원본 재귀의 호출 공간은 O(log N), 이 반복문의 추가 공간은 O(1)입니다. 정렬된 배열에서 탐색 시간은 둘 다 O(log N)이며 사전 정렬 비용은 별도입니다.

추가 예제 코드와 실행 방법 펼치기

직접 실행할 때만 펼쳐 보세요. Node.js 시작 안내를 확인하고 closed-interval-binary-search.mjs로 저장합니다.

// 오름차순의 유한한 숫자 배열과 유한한 숫자 target을 받는 독립 예제입니다.
// 정렬과 입력 검사는 호출 전에 합니다. 성공은 인덱스, 실패는 null입니다.
function binarySearchIndex(values, target) {
  let start = 0;
  let end = values.length - 1;
  while (start <= end) {
    const mid = start + Math.floor((end - start) / 2);
    if (values[mid] === target) return mid;
    if (target > values[mid]) start = mid + 1;
    else end = mid - 1;
  }
  return null;
}

console.log(JSON.stringify({
  found: binarySearchIndex([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3),
  missing: binarySearchIndex([1, 3, 7], 5),
  empty: binarySearchIndex([], 3),
  zero: binarySearchIndex([3], 3),
  duplicate: binarySearchIndex([1, 3, 3, 3, 7], 3),
  decimal: binarySearchIndex([-1.5, 0.25, 2.75], 0.25),
  roundedSum: binarySearchIndex([0.3], 0.1 + 0.2)
}));

파일이 있는 폴더의 터미널에서 실행합니다.

node closed-interval-binary-search.mjs

기대 출력

{"found":2,"missing":null,"empty":null,"zero":0,"duplicate":2,"decimal":1,"roundedSum":null}

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 포인터를 움직여도 답을 버리지 않는 이유와 양수·0·음수의 차이를 설명하세요.

다음 추천 · 이 개념으로 풀 문제: 이진 탐색 연습: 숫자 카드 존재 여부 확인

같은 주제의 문제에 적용하며 방금 배운 개념을 확인합니다.

권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.

원래 과정 목록 순서 (선택)

읽음 기록

읽음 — / 전체 30개 · 필수 0개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기