보이어-무어 문자열 검색 알고리즘
크래프톤 정글 8기 - 36일차 TIL
Boyer-Moore 문자열 검색 알고리즘 정리
Boyer–Moore 알고리즘은 문자열 검색 알고리즘 중에서도 가장 빠르고 실용적인 알고리즘 중 하나다. 이 글에서는 Boyer-Moore 알고리즘의 기본 개념, 핵심 규칙(Bad Character Rule, Good Suffix Rule), 그리고 실제 탐색 방식에 대해 정리한다.
1. Boyer-Moore 알고리즘의 핵심 아이디어
"비교는 오른쪽 끝부터 시작하며, 실패하면 패턴을 최대한 멀리 점프시킨다."
- 일반적인 문자열 검색은 왼쪽부터 비교하지만,
- Boyer–Moore는 오른쪽부터 왼쪽으로 비교한다.
- 실패 시, 두 가지 규칙 중 더 먼 쪽으로 점프하여 검색 속도를 높인다.
보이어-무어 알고리즘에서는 한국어로 "건너뛰기 표(skip table)"를 사용하는데 건너뛰기 표는 알고리즘이 패턴을 얼마나 점프할 수 있는지 계산할 수 있도록 도와주는 전처리 테이블들을 말한다. 다른 말로는 이동 거리 테이블 또는 점프 테이블이라고 부른다.
2. 주요 전처리: 두 가지 점프 규칙
🔹 1. Bad Character Rule
- 틀린 문자를 기준으로 패턴을 이동
- 현재 틀린 문자가 패턴 내 어디에 다시 등장하는지 확인
- 없으면 → 패턴 전체를 넘기기
예시:
Text: ... A B C D E ...
Pattern: A B C D F
↑ ↑
| 틀림 (E ≠ F)
→ E는 패턴 내 없음 → 패턴 전체 점프
🔹 2. Good Suffix Rule
- 이미 맞은 접미사를 기준으로 이동
- 해당 접미사가 패턴 내 어디서 다시 등장하는지 확인
- 등장하면 → 해당 위치로 정렬하여 점프
- 없다면 → 접미사의 일부 또는 전체 이동
3. 실제 탐색 시 동작 방식
보이어-무어 알고리즘에서 말하는 skip table은 이러한 이동 거리를 미리 계산해서 저장해 놓은 표를 의미한다.
Skip Table 종류 | 핵심 키 | 값 |
Bad Character Table | 문자 | 마지막 등장 위치 |
Good Suffix Table | 접미사 길이 | 이동 거리 |
이 두 가지 skip table을 미리 만들어두면, 실제 비교 도중에는:
- 둘 중 더 많이 이동할 수 있는 쪽을 선택해서 패턴을 효율적으로 이동시킴
4. 전처리 테이블 만들기
▶ Bad Character Table
- 크기: 일반적으로 256 (ASCII)
- 각 문자에 대해 가장 마지막 등장 인덱스 저장
예시 - 패턴이 "ABAC" 일 때
패턴 길이는 4이다. 이때 아스키코드 크기만큼의 배열을 만든 뒤 초가 값을 전부 4로 채워 넣는다. 이후 패턴 내에서의 마지막 등장 위치를 기준으로 각 문자의 이동거리를 계산한다.
badChar[char] = m - 1 - last_index_in_pattern
--> 패턴 끝에서 부터 얼마나 떨어져 있는지를 기준으로 점프 거리 설정
예: 패턴 "ABAC"
문자 | 인덱스 | 이동 거리 = m - 1 인덱스 |
A | 2 | 4 - 1 - 2 = 1 |
B | 1 | 4 - 1 - 1 = 2 |
C | 3 | 4 - 1 - 3 = 0 |
▶ Good Suffix Table
- 각 접미사에 대해:
- 패턴 내에서 다시 사용할 수 있는 위치 계산
- 없으면 → 더 짧은 접미사 탐색
- 최종적으로 없으면 → 패턴 전체 이동
Good Suffix Shift 테이블은 두 가지 정보를 조합해서 만들어진 구조다:
테이블 | 설명 |
border_pos[i] | 접미사 P[i:] 가 패턴 내 어디와 다시 일치하는지 나타냄 (재정렬 기준) |
shift[i] | P[i:]가 일치했을 때 패턴을 얼마나 이동시켜야 하는지 |
이때 shift[i]는 실질적으로 아래의 두 단계 점화식과 유사하게 구성된다.
Step 1: 접미사 재정렬용 테이블 border_pos[i]
border_pos[i] = j
의 의미는:
접미사 P[i:]와 일치하는 다른 부분 문자열 P[j:] 가 있다는 뜻
이건 KMP의 failure table과 유사한 재귀 구조이다.
if P[i - 1] == P[j - 1]:
border_pos[i - 1] = j - 1
else:
j = border_pos[j]
이게 재귀적으로 호출되므로 일종의 suffix-based DP로 작동한다.
Step 2: 점프 거리 계산 shift[i]
if pattern[i - 1] != pattern[j - 1]:
shift[j] = j - i
이 조건이 성립될 때:
- P[i:]는 일치하고,
- 그 앞 P[i - 1] ≠ P[j - 1] → 불일치 발생
- 이때 shift는 j - i만큼 패턴을 이동
여기서도 j = border_pos[j]로 재귀적으로 이동하며,
이미 채워진 shift[j]가 없을 경우에만 갱신하는 방식으로 작동한다.
- border_pos[i]는 KMP에서 접두사/접미사 비교로 실패 시 어디로 점프할지 계산하는 것과 같고
- shift[i]는 Good Suffix가 등장했을 때, 재정렬이 가능한 위치를 기반으로 얼마를 점프할지 나타냄
예시로 설명
예시 문자열
- 텍스트: T = C G T G C C T A C T T A C T T A C T T A C T T A C G C G A A
- 패턴: P = C T T A C T T A C
Step 1
일부 문자들은 성공적으로 매치됨
t = C T T A C T T A C → 불일치 발생
이제 이 일치된 접미사 t를 기준으로 다음 중 가장 빠르게 일치할 가능성이 있는 곳으로 패턴을 이동시킵니다:
Good Suffix Rule에서의 세 가지 이동 기준
- (a) t와 같은 문자열이 P 내에서 다른 위치에 또 존재 → 그 위치에 정렬
- (b) t의 접미사 중 일부가 P의 접두사(prefix)와 일치 → 그만큼 이동
- (c) 위 둘 다 해당 안 되면 P를 통째로 t 오른쪽으로 넘김 (t를 벗어남)
예시 :
패턴: C T T A C T T A C
우리가 방금 맞았던 부분이 T T A C 라고 해 보겠다.
Good Suffix Rule에 따라서 다음과 같이 생각해 보겠다:
- "혹시 T T A C라는 패턴이 내 안에 또 있는가?"
- 있다면 → 그 위치로 점프한다.
- 없다면 → 혹시 T A C, A C, C처럼 짧은 부분은 내 앞쪽에 있는가?
- 그마저도 없다면 → 아예 t를 피해서 통째로 패턴을 점프시킨다.
예시에서는 8칸을 한 번에 이동한다. 왜 그럴까?
C G T G C C T A C T T A C T T A C T T A C T T A C G C G A A 라는 문자열에서 우리가 찾고자 하는 패턴은 C T T A C T T A C 이다.
- 8번 인덱스부터 앞으로 비교하기 시작해서 TAC 까지는 맞았지만 그 앞의 T 즉 TTAC 에서 틀렸다.
- 그럼 t는 지금까지 맞았던 값 TAC 로 지정한다.
- 이제 자신 내부 즉 패턴 안에 TAC 값이 또 있는지 확인한다. 인덱스 2번부터 TAC가 또 등장한다.
- 그럼 바로 TAC로 이동을 하면 되는 것이 아닌가? 아니다.
- 앞에 발견된 TAC의 바로 앞 문자를 봐라 T이다. 그럼 TTAC 가 된다. 즉 우리가 이전에 틀렸던 상황이 또 발생하는 것이다.
- 그래서 TAC로 이동하는 것이 아니라 그냥 8칸을 이동하겠다고 결정한 것이다.
그렇다면 언제 t의 일부 접미사가 패턴의 접두사와 일치할 때 이동하는 것일까?
패턴에서 찾은 접두사의 앞 글자가 접미사의 틀렸던 위치 앞 값과 다를 때 이다.
다음 예시를 보자
T = C G T G C C T A C T T A C T T A C T T A C T T A C G C G A A
P = A C T T G A G A
P[7] 과 T[7]의 비교 결과 A가 맞다. 그다음 T와 G 가 틀린 상황이다. 이때 A가 어디에 위치했는지 보면 0번 인덱스와 5번 인덱스이다. 이때 5번 인덱스를 선택했을 시 A의 앞은 G로 이전에 틀렸던 값과 같다. 그렇다면 결국 0번 인덱스인 A를 선택해야 한다.
그럼 0번 인덱스의 A를 7번 인덱스에 위치하도록 점프를 한다.
T = C G T G C C T A C T T A C T T A C T T A C T T A C G C G A A
P = A C T T G A G A
자 이동을 마쳤는데 이제 다시 Good Suffix Rule에 의해 오른쪽부터 비교를 할 것인데 보면 일치하는 문자가 없다. 그렇다면 Good suffix Rule에 의해서는 이동할 수 없을 것이다. 이때 우리가 처음 봤던 Bad Character Rule을 이용하게 된다.
즉 두 가지 방법의 테이블을 완성한 상태에서 어떤 값이 더 멀리 점프할 수 있는지 비교를 하는 것 일종의 다이내믹 프로그램일 수 있다.
5. Boyer-Moore의 장점과 활용
장점 | 설명 |
---|---|
매우 빠름 | 평균적으로 가장 빠른 문자열 검색 알고리즘 중 하나 |
멀리 점프 가능 | 비교 실패 시 최대한 멀리 이동 가능 |
실전 활용도 높음 | grep, IDE 검색, 컴파일러 등에서 사용 |
6. KMP와 비교
항목 | KMP | Boyer-Moore |
---|---|---|
비교 방향 | 왼쪽 → 오른쪽 | 오른쪽 → 왼쪽 |
점프 방식 | LPS 배열 기반 점프 | Bad Character + Good Suffix 기반 |
평균 속도 | 빠름 | 더 빠름 |
구현 난이도 | 쉬움 | 어려움 |
결론
Boyer–Moore는 매우 빠르며 실전에서도 널리 활용되는 문자열 검색 알고리즘이다.
두 가지 전처리 규칙만 잘 이해하면, 매우 효율적인 검색이 가능하다.
강력한 검색 성능이 필요할 때 꼭 고려해 볼 만한 알고리즘이다.