|
| 1 | +--- |
| 2 | +title: "[파이썬 알고리즘] Ch 6-3. 문자열 매칭 (1) (Horspool Algorithm)" |
| 3 | +date: 2025-12-14 00:08:00 +0900 |
| 4 | +categories: [algorithm] |
| 5 | +tags: [python, programming, coding, counting, sort, mapping, horspool, shift table, Boyer-Moore, heuristic] |
| 6 | +comments: true |
| 7 | +math: true |
| 8 | +toc: true |
| 9 | +#pin: true |
| 10 | +#image: |
| 11 | + #path: thumbnail.png |
| 12 | + #alt: image alternative text |
| 13 | +--- |
| 14 | + |
| 15 | +참고 도서: 최영규, ⌜파이썬 알고리즘⌟ 생능츨판, 2021 |
| 16 | +<br> |
| 17 | + |
| 18 | + |
| 19 | +**문자열 매칭 문제**는 길이가 $n$인 텍스트 속에서 길이가 $m$인 패턴의 위치를 찾는 문제이다. |
| 20 | +<br> |
| 21 | + |
| 22 | + |
| 23 | +**억지 기법(brute force)**을 적용하면 최악의 경우 텍스트의 $n-m+1$개 위치에서 각각 $m$번의 비교를 해야 하므로 |
| 24 | +시간 복잡도는 $O(nm)$이지만, 평균적으로는 이보다 훨씬 좋은 $O(n+m)$의 성능을 보이는 것으로 알려져 있다. |
| 25 | + |
| 26 | +<img src="/assets/images/python-algorithm/6-7.png" alt="그림 6.6 억지 기법을 이용한 문자열 매칭"> |
| 27 | +<br> |
| 28 | + |
| 29 | + |
| 30 | +**< 효율적인 문자열 매칭을 위한 기본 전략 >** |
| 31 | +- 패턴을 빠르게 검색할 수 있도록 **전처리(preprocessing)**을 통해 필요 정보를 미리 저장해두고, |
| 32 | +- 패턴을 검사할 때 이를 활용한다. |
| 33 | +<br> |
| 34 | +<br> |
| 35 | +<br> |
| 36 | +<br> |
| 37 | + |
| 38 | +## **1. 호스풀 알고리즘(Horspool algorithm)** |
| 39 | +호스풀 알고리즘(Horspool algorithm)은 문자열 패턴의 마지막 문자를 기준으로 비교를 수행하며, 불일치가 발생하면 해당 문자에 대해 미리 계산된 이동 거리를 이용해 패턴을 이동시키는 **휴리스틱** 기반의 문자열 검색 알고리즘이다. |
| 40 | + |
| 41 | + |
| 42 | +> **휴리스틱(heuristic)** |
| 43 | +> - 모든 경우에 대해 최적의 성능을 보장하지는 않지만, 대부분의 경우에서 꾸준히 좋은 성능을 내는 방법 |
| 44 | +
|
| 45 | +<br> |
| 46 | + |
| 47 | + |
| 48 | +호스풀 알고리즘의 기본 전략은 다음과 같다. |
| 49 | +<br> |
| 50 | + |
| 51 | + |
| 52 | +## **2. 전처리(preprocessing)** |
| 53 | +- 패턴 불일치가 발생할 경우 이동 거리를 계산하기 위한 **시프트 테이블(shift table)** 생성 |
| 54 | +<br> |
| 55 | +<br> |
| 56 | +<br> |
| 57 | +<br> |
| 58 | + |
| 59 | +## **3. 문자열 패턴 검사** |
| 60 | +- <mark>항상 패턴의 마지막 문자부터 비교 (오른쪽 → 왼쪽)</mark> |
| 61 | +- 이 방식은 불일치 발생 시 여러 위치를 한 번에 건너뛸 수 있어서 <u>불필요한 비교를 줄이는 데 효과적이다.</u> |
| 62 | + |
| 63 | + > **< 앞에서 부터 비교 >** |
| 64 | + > - 첫 글자가 우연히 일치할 수 있음 |
| 65 | + > - 이후 비교 과정에서 뒤쪽 문자에서 불일치가 발생할 수 있음 |
| 66 | + > - 따라서, 여러 글자를 비교한 뒤에 불일치를 판단하게 됨 |
| 67 | + > <br> |
| 68 | + > |
| 69 | + > **< 뒤에서 부터 비교 >** |
| 70 | + > - 첫 비교에 바로 일치 or 불일치를 판단할 수 있음 |
| 71 | + > - 불일치 시 해당 문자를 기준으로 이동 거리 계산 가능 |
| 72 | +
|
| 73 | +<img src="/assets/images/python-algorithm/6-8.png" alt="그림 6.8 패턴 맨 뒤 문자부터 앞으로 비교함"> |
| 74 | +<br> |
| 75 | + |
| 76 | + |
| 77 | +다음으로 문자열 패턴 검사 시 발생할 수 있는 상황에 대해 알아보자. |
| 78 | +<br> |
| 79 | + |
| 80 | + |
| 81 | +**< $Case \ 1. \ $ 불일치가 발생한 문자가 패턴에 없는 문자라면? >** |
| 82 | +- 문자 M은 패턴(BANANA)에 없는 문자이므로 패턴의 위치를 인덱스 1 ~ 5로 한 자리씩 옮기면서 비교하더라도 절대 일치할 수가 없다. |
| 83 | +- 따라서, 패턴의 위치를 패턴의 길이(6)만큼 바로 넘겨 검사를 진행하면 된다. |
| 84 | +<img src="/assets/images/python-algorithm/6-9.png" alt="그림 6.9 문자가 패턴에 존재하지 않는 경우"> |
| 85 | +<br> |
| 86 | + |
| 87 | + |
| 88 | +**< $Case \ 2. \ $ 불일치가 발생한 문자가 패턴에 있는 문자라면? >** |
| 89 | +- 패턴의 맨 뒤 글자인 A는 일치하고, 다음 글자인 N은 텍스트 B와 일치하지 않는다. |
| 90 | +- 이러한 경우는 텍스트의 B는 패턴에 존재하는 문자이므로 패턴의 B 위치가 텍스트의 B 위치와 일치하도록 건너뛰면 된다. |
| 91 | +<img src="/assets/images/python-algorithm/6-10.png" alt="그림 6.10 불일치가 발생한 문자가 패턴에 있는 문자일 경우"> |
| 92 | +<br> |
| 93 | +<br> |
| 94 | +<br> |
| 95 | +<br> |
| 96 | + |
| 97 | +## **4. 시프트 테이블 생성** |
| 98 | +$e.g.$ |
| 99 | +- 패턴: `BANANA` |
| 100 | +- 패턴 길이: `m = 6` |
| 101 | +- 문자 집합: `ASCII (0 ~ 127)` |
| 102 | +- 기본 규칙: |
| 103 | + - 불일치 발생 시 패턴에 없는 문자 → `이동 거리 = m` |
| 104 | + - 불일치 발생 시 패턴에 있는 문자 → `마지막 문자 기준 거리` |
| 105 | +<br> |
| 106 | + |
| 107 | +**< 1. 패턴 인덱스 확인 >** |
| 108 | +- 마지막 인덱스 = `5` |
| 109 | +- 마지막 문자 = `A` |
| 110 | + |
| 111 | +|**인덱스**|0|1|2|3|4|5| |
| 112 | +|**문자**|B|A|N|A|N|A| |
| 113 | + |
| 114 | +<br> |
| 115 | + |
| 116 | +**< 2. 이동 거리 계산 >** |
| 117 | + |
| 118 | +$이동 거리 = (m-1)-i$ |
| 119 | + |
| 120 | +| **문자** | **인덱스($i$)** | **계산식** | **이동거리** | |
| 121 | +| :------------------------------: | :-------------: | :----------------: | :----------------------------------: | |
| 122 | +| B | 0 | 5 - 0 | 5 | |
| 123 | +| <span style="color:red">A</span> | 1 | 5 - 1 | 4 (<span style="color:red">X</span>) | |
| 124 | +| N | 2 | 5 - 2 | 3 | |
| 125 | +| <span style="color:red">A</span> | 3 | 5 - 3 | 2 (<span style="color:red">X</span>) | |
| 126 | +| N | 4 | 5 - 4 | 1 | |
| 127 | +| <span style="color:red">A</span> | 5 | 마지막 문자는 제외 | <span style="color:red">X</span> | |
| 128 | + |
| 129 | +<br> |
| 130 | + |
| 131 | +**< 3. 시프트 테이블 작성 >** |
| 132 | + |
| 133 | +| **문자** | **이동거리** | **설명** | |
| 134 | +| :------: | :----------: | :----------------------- | |
| 135 | +| A | 2 | 마지막 A 기준 (인덱스 3) | |
| 136 | +| N | 1 | 마지막 N 기준 (인덱스 1) | |
| 137 | +| B | 5 | 마지막 B 기준 (인덱스 0) | |
| 138 | +| 나머지 | 6 | 패턴에 없음 | |
| 139 | + |
| 140 | +<br> |
| 141 | + |
| 142 | +위 과정을 통해 아래의 그림과 같은 시프트 테이블(shift table)이 만들어진다. |
| 143 | +<img src="/assets/images/python-algorithm/6-11.png" alt="그림 6.11 시프트 테이블 작성"> |
| 144 | +<br> |
| 145 | +<br> |
| 146 | +<br> |
| 147 | +<br> |
| 148 | + |
| 149 | +## **5. 소스코드** |
| 150 | +```python |
| 151 | +# 시프트 테이블 만들기 |
| 152 | +NO_OF_CHARS = 128 # 아스키 문자 개수 |
| 153 | + |
| 154 | +def shift_table(pat): |
| 155 | + m = len(pat) # 패턴의 길이 |
| 156 | + tbl = [m] * NO_OF_CHARS # 시프트 테이블 초기화(기본 이동 거리 = m) |
| 157 | + |
| 158 | + for i in range(m-1): # 패턴의 마지막 문자를 제외한 모든 문자에 대해 |
| 159 | + tbl[ord(pat[i])] = m-i-1 # 불일치 발생 시 이동할 거리 계산 |
| 160 | + |
| 161 | + return tbl |
| 162 | +``` |
| 163 | +```python |
| 164 | +# 호스풀 알고리즘 |
| 165 | +def search_horspool(T, P): |
| 166 | + m = len(P) # 패턴 문자열 길이 |
| 167 | + n = len(T) # 텍스트 문자열 길이 |
| 168 | + t = shift_table(P) # 패턴 기반으로 시프트 테이블 생성 |
| 169 | + i = m-1 # 텍스트에서 패턴의 마지막 문자가 맞춰질 위치 인덱스 |
| 170 | + |
| 171 | + while(i <= n-1): # 텍스트 범위를 벗어나지 않는 동안 반복 |
| 172 | + k = 0 |
| 173 | + |
| 174 | + while k <= m-1 and P[m-1-k]==T[i-k]: # 패턴의 끝에서 왼쪽으로 이동하며 문자 비교 |
| 175 | + k += 1 # 문자가 일치하면 다음 문자로 이동 |
| 176 | + |
| 177 | + if k == m: # 패턴의 모든 문자가 일치할 경우 |
| 178 | + return i-m+1 # 패턴이 시작된 텍스트의 인덱스 반환 |
| 179 | + else: |
| 180 | + tc = t[ord(T[i-k])] # 불일치가 발생한 텍스트 문자에 대한 시프트 값 조회 |
| 181 | + i += max(1, (tc-k)) # 시프트 거리 계산 |
| 182 | + |
| 183 | + return -1 # 패턴을 찾지 못한 경우 |
| 184 | + |
| 185 | +# 호스풀 알고리즘 테스트 |
| 186 | +print("패턴의 위치:", search_horspool("APPLEMANGOBANANAGRAPE", "BANANA")) |
| 187 | +``` |
| 188 | +```python |
| 189 | +# 출력 결과 |
| 190 | +패턴의 위치: 10 |
| 191 | +``` |
| 192 | +<br> |
| 193 | +<br> |
| 194 | +<br> |
| 195 | +<br> |
| 196 | + |
| 197 | +## **6. 복잡도 분석** |
| 198 | +**최악의 경우 (worst case): <mark>$O(mn)$</mark>** |
| 199 | +- 8행의 외부 루프는 $n-m$번 반복한다. |
| 200 | +- 또한, 하나의 위치에서 패턴을 비교하는데 패턴의 길이($m$) 만큼의 비교가 필요하다. |
| 201 | +<br> |
| 202 | + |
| 203 | +**평균의 경우 (average case): <mark>$O(n)$</mark>** |
| 204 | +- 무작위 텍스트에 대해서는 거의 $O(n)$의 성능을 나타낸다. |
| 205 | +- <u>이는 억지 기법보다 훨씬 효울적이다.</u> |
0 commit comments