String_matching

Table of contents


문자열 탐색 (String Matching)

정의

  • 문자열에서 찾고자 하는 특정 문자열(pattern)의 위치를 찾는 알고리즘

종류

  • 단순 비교
  • 라빈-카프 알고리즘
  • KMP 알고리즘
  • 보이어-무어 알고리즘
  • 아호-코라식 알고리즘

단순 비교 (Naive Pattern Searching)

정의

  • 단순히 문자열을 처음부터 끝까지 하나하나 비교하며 패턴을 찾는 알고리즘

    • 작은 문자열에서 유용

원리

  • 길이가 n인 텍스트 T안에서 길이가 m인 패턴 P의 존재 여부를 검토 (m<=n)

    • T[ 0.. n-1 ]
    • P[ 0.. m-1 ]
  • 패턴 P는 텍스트 T의 처음부터 n-m+1까지 이동하며 매치가 되는지 검토

  • 패턴이 발견되면 패턴의 위치를 출력하고 다음 일치 위치 탐색

코드


void naivePatternSearch (char T[], char P[]) {

  int n = strlen(T);
  int m = strlen(P);

  for (int i = 0; i <= n-m; i++) {

    for (int j = 0; j < m; j++) { 

      if (T[i+j] != P[j])
        break;

      if (j == m-1) {

        printf("Pattern found at index %d \n", i);
        break;

      }
    }
  }
}

장점

  • 추가 공간이 요구되지 않음

단점

  • 하나하나 비교하기에 비효율적
  • 이미 검사를 마친 위치를 기억하지 못함

시간복잡도

  • Best Case

    • 패턴의 첫문자가 텍스트에 없을 때

      • T [] = “AABBAAB”
      • P [] = “FAA”
    • 텍스트의 길이가 n 이면 O(n)

  • Worst Case

    • 텍스트와 패턴의 모든 문자가 같을 경우

      • T [] = “AAAAAAA”
      • P [] = “AA”
    • 패턴의 마지막 문자만 다른 경우

      • T [] = “AAAAAAA”
      • P [] = “AAB”
    • 패턴의 길이가 m 이면 O(m * (n-m+1))

#

라빈-카프 알고리즘(Rabin-Karp)

정의

  • 문자열에 해싱 기법을 사용하여 해시 값으로 비교하는 알고리즘

원리

  • 각 문자의 아스키 코드 값에 특정 제곱수를 차례대로 곱하고 모두 더하여 해시 값을 만든다.
    • 차례대로 라는 의미는 자리마다(흔히 인덱스) 다른 수를 곱해주는 것

예시

  • 특정 제곱수를 2로 설정 시, abacaaba의 해시 값 ( a : 97 , b : 98 )
    • 97 * 2^7 +

      98 * 2^6 +

      97 * 2^5 + … (생략)

  • 문자열 AABDCDAB에서 패턴 ABD 가 발견되는 지 탐색하는 예제

| 문자열 | A | A | B | D | C | D | A | B | | — | — | — | — | — | — | — | — | — | | 패턴 | A | B | D | | | | | |

  • 검색 대상 문자열 해시 값(AAB) : 846
  • 문자열 패턴 해시 값(ABD) : 851
    • 탐색 실패

| 문자열 | A | A | B | D | C | D | A | B | | — | — | — | — | — | — | — | — | — | | 패턴 | A | B | D | | | | | |

  • ABD 해시 값이 같아 탐색 성공

이후로도 있는 지 같은 방식으로 계속 탐색

코드

def find_string(parent, pattern):
	  parent_len = len(parent)    # 검색 대상 문자열
    pattern_len = len(pattern)  # 찾으려는 패턴 문자열
    parent_hash = 0
    pattern_hash = 0
    power = 1
    for i in range(parent_len - pattern_len + 1):
        if i == 0:
            for j in range(pattern_len):
                parent_hash += ord(parent[pattern_len - 1 - j]) * power  # ord : 아스키코드 값 반환
                pattern_hash += ord(pattern[pattern_len - 1 - j]) * power
                if j < pattern_len - 1:
                    power *= 2
        else:
            parent_hash = 2 * (parent_hash - ord(parent[i - 1]) * power) + ord(parent[pattern_len - 1 + i])

        if parent_hash == pattern_hash:
            found = True
            for j in range(pattern_len):
                if parent[i + j] != pattern[j]:
                    found = False
                    break
            if found:
                print(f'{i + 1}번째에서 발견')
  • 특정 제곱수를 2로 설정 시의 코드

장점

  • 일반적인 경우 빠른 속도
  • 비교적 간단한 구조

단점

  • 패턴 등이 길어질 수록 연산이 증가
    • 호너의 법칙 등을 사용하여 보완
  • 해시 충돌

시간복잡도

  • O(n)

#

KMP 알고리즘(Knuth Morris Partt Algorithm)

정의

  • 문자열이 주어졌을때 그 문자열 안에서 어떠한 패턴을 효율적으로 찾는 알고리즘

특징

  • pi배열을 사용한다.(pi[i]는 주어진 문자열의 0~i 까지의 접두사==접미사가 될 수 있는 부분 문자열 중에서 가장 긴 것의 길이)
  • ex) “AABAA”의 pi배열

| i | 부분 문자열 | pi[i] | | — | — | — | | 0 | A | 0 | | 1 | AA | 1 | | 2 | AAB | 0 | | 3 | AABA | 1 | | 4 | AABAA | 2 |

  • 상황에 따라 비교할 대상을 건너뛰면서 탐색을 진행하기 때문에 효율적으로 패턴을 찾을 수 있다.

원리

  1. 문자열과 패턴을 비교한다.
  2. 일부가 일치했다면 조금이라도 일치했었다는 정보에 주목하고 미리 전처리 해둔 pi배열을 이용해서 많은 중간 시도를 건너뛴다.

예시

-문자열 “ABCDABCDABEE”에서 패턴 “ABCDABE”를 찾는 상황

| 인덱스 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | — | — | — | — | — | — | — | — | — | — | — | — | — | | 텍스트 | A | B | C | D | A | B | C | D | A | B | E | E | | 패턴 | A | B | C | D | A | B | E | | | | | |

  • 첫 번째 시도에서는 패턴의 0~5부분 문자열은 텍스트와 일치했지만 6번째 인덱스의 E가 텍스트와 일치하지 않았다.

| 인덱스 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | — | — | — | — | — | — | — | — | — | — | — | — | — | | 텍스트 | A | B | C | D | A | B | C | D | A | B | E | E | | 패턴 | A | B | C | D | A | B | E | | | | | |

  • 접두사 AB와 접미사 AB가 일치하고, 이는 접두사와 접미사가 일치하는 최대 길이이다.(pi[5]=2)

| 인덱스 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | — | — | — | — | — | — | — | — | — | — | — | — | — | | 텍스트 | A | B | C | D | A | B | C(i) | D | A | B | E | E | | 패턴 | | | | | A | B | C(j) | D | A | B | E | |

  • 접두사 AB와 접미사 AB가 같음을 보여주었으므로(pi[5]=2) 패턴 “ABCDABE”에서 “AB”는 이미 텍스트와 일치한다.
  • 그러므로 비교를 6번째 인덱스부터 시작한다.(i: 텍스트의 현재 비교위치, j: 패턴의 현재 비교위치)
  • 텍스트와 패턴이 모두 일치함을 확인했다.

코드

  • pi함수
    # KMP 알고리즘을 수행하기 전, 패턴을 처리하는 함수
    # 패턴의 테이블 생성
    def KMP_table(pattern):
      lp = len(pattern)
      tb = [0 for _ in range(lp)] # 정보 저장용 테이블
        
      pidx = 0 # 테이블의 값을 불러오고, 패턴의 인덱스에 접근
      for idx in range(1, lp): # 테이블에 값 저장하기 위해 활용하는 인덱스
          # pidx가 0이 되거나, idx와 pidx의 pattern 접근 값이 같아질때까지 진행
          while pidx > 0 and pattern[idx] != pattern[pidx]:
              pidx = tb[pidx-1]
            
          # 값이 일치하는 경우, pidx 1 증가시키고 그 값을 tb에 저장
          if pattern[idx] == pattern[pidx] :
              pidx += 1
              tb[idx] = pidx
        
      return tb
    
  • KMP 검색 ```python def KMP(word, pattern): # KMP_table 통해 전처리된 테이블 불러오기 table = KMP_table(pattern)

    results = [] # 결과를 만족하는 인덱스 시점을 기록하는 리스트 pidx = 0

    for idx in range(len(word)): # 단어와 패턴이 일치하지 않는 경우, pidx를 table을 활용해 값 변경 while pidx > 0 and word[idx] != pattern[pidx] : pidx = table[pidx-1] # 해당 인덱스에서 값이 일치한다면, pidx를 1 증가시킴 # 만약 pidx가 패턴의 끝까지 도달하였다면, 시작 인덱스 값을 계산하여 추가 후 pidx 값 table의 인덱스에 접근하여 변경 if word[idx] == pattern[pidx]: if pidx == len(pattern)-1 : results.append(idx-len(pattern)+2) pidx = table[pidx] else: pidx += 1

    return results



### 장점
- 문자열을 거의 한 번만 읽고도 원하는 패턴을 찾아낼 수 있다.

### 단점
- 검색할 대상의 크기가 현저히 큰 경 우에는 실행시간이 많이 증가한다.

### 시간복잡도
- O(m+n)

#
## 보이어 무어 알고리즘 (Boyer Moore)

### 정의
- 패턴 문자열의 오른쪽 끝에서부터 왼쪽으로 진행하며 비교하는 알고리즘

### 특징
- 보통 상황에서 문자열 앞부분보다 뒷부분에서 불일치가 일어날 확률이 높다는 성질을 이용했다.
- 패턴 문자열에 대한 **skip 테이블**을 만들어 놓고, 불일치 시 적절한 이동 거리만큼 오른쪽으로 이동한다.

### 원리
1. 문자열 끝부분부터 비교한다.
2. 문자가 일치하면 **skip**하지 않고 계속해서 비교한다.
3. 불일치 문자가 검색 문자열에 없으면 검색 문자열의 길이 만큼 **skip**한다.
4. 불일치 문자가 검색 문자열에 있을 경우, 다음과 같이 **skip**한다.
   - 뒤에서 k번째에 있다면 (k-1)만큼 **skip**한다.
5. 비교 과정의 중간에서 불일치 문자를 만나면 skip 테이블의 값에서 앞서 이리한 수만큼 뺀 값 만큼 **skip**한다.

### 예시
- **heyhibye**라는 문자열에서 **bye**라는 문자열을 검색한다.
- 이 때 skip 테이블과 비교 과정은 다음과 같다.
  - skip 테이블의 경우, 위 원리에 따라 만들어진다.

| b | y | e | 다른 문자 |
| --- | --- | --- | ---|
| 2 | 1 | 0 | 3 |

![보이어 무어](sample/string_search/image/boyer_moore.png)

1. 불일치 문자 **y**가 테이블에 있으므로 오른쪽으로 1만큼 **skip**한다.
2. 불일치 문자 **h**(다른 문자)이므로 오른쪽으로 3만큼 **skip**한다.
3. 불일치 문자 **y**가 테이블에 있으므로 오른쪽으로 1만큼 **skip**한다.
4. **e**가 일치한다.
5. **y**가 일치한다.
6. **b**가 일치한다.
7. 검색을 성공했다.

### 코드
```python
# 보이어 무어 알고리즘 
def boyer_moore(pattern, text):
    pattern_length = len(pattern)   # 패턴 문자열의 길이
    text_length = len(text)         # 텍스트 문자열의 길이
    i = 0

    # while문을 이용해 텍스트 내의 패턴과 매칭될 때까지 skip 과정을 반복한다.
    # 최대 반복 횟수는 (텍스트 길이-패턴 길이)만큼한다.
    while (i<=text_length-pattern_length):
        # 패턴 문자열의 인덱스를 받을 idx로 보이어 무어 알고리즘은 뒤에서부터 접근하므로 (패턴 길이-1)로 선언 및 초기화
        idx = pattern_length -1 

        while idx >= 0:
            if pattern[idx] != text[i+idx]:                         # 패턴의 글자와 텍스트의 글자가 다른 경우
                move = get_skip(pattern, text[i+pattern_length-1])  # 해당 불일치 문자의 skip 칸을 알기 위해 get_skip() 호출
                break
            
            idx -= 1

       
        if idx == -1:   # 문자열 탐색에 성공한 경우
            return True
        else:           # 문자열 탐색에 실패한 경우  
            i += move   # move만큼 skip

    return False    

# skip 테이블의 역할을 하는 메소드 get_skip         
def get_skip(pattern, char):
    # for문을 이용해 불일치 문자 char가 패턴 문자열에 있는지 검사
    # 패턴의 마지막 문자와 불일치 함을 확인 후 호출되는 함수이기 때문에 마지막 문자 바로 전부터 검사 
    for i in range(len(pattern)-2, -1, -1):
        if pattern[i] == char:      # 불일치 문자가 패턴 문자열에 있는 경우
            return len(pattern)-i-1 # 뒤에서 i번째 있으므로 (i-1)만큼 skip
    
    return len(pattern)             # 패턴에 없는 문자이므로 패턴 길이만큼 skip

장점

  • KMP 알고리즘보다 성능이 좋다.
  • 문자열이 길수록 더 효과가 있다.
  • 실무에서 자주 사용된다.

단점

  • 최악의 경우, 시간복잡도가 O(MN)이 될 수 있다.
  • 실제로 검색어가 긴 경우가 많지 않다.

시간복잡도

  • 최악의 경우: O(MN)
  • 보통의 경우: O(N)보다 작음

아호-코라식 알고리즘 (Aho-Corasick)

정의

  • 패턴 집단에 대해 수행하는 문자열 탐색 알고리즘들의 속도가 느려 고안된 문자열 탐색 알고리즘

특징

  • KMP 👉 텍스트에서 패턴 하나를 찾아낼 때 사용 (1:1)
  • 아호 코라식 👉 텍스트에서 패턴 여러개를 동시에 찾아낼 때 사용 (1:N)

    • KMP의 확장이라고 볼 수 있음
    • KMP는 문자열과 문자열 매칭
    • 아호-코라식은 문자열과 트라이 간 매칭

    • 트라이란?

      • 트라이(Trie)는 문자열의 집합을 표현하는 ‘트리 자료구조’
      • S = 문자열
      • W = 패턴 문자열 집합

  • 실패 함수(Failure Function)

    • 매칭이 중간에 실패하였을 때 지금까지 사용한 데이터를 그대로 활용하여 효율적으로 탐색하기 위한 함수

장점

  • 수 많은 페이지에서 여러 검색어들이 부분적으로 포함된 각각의 위치들을 빠르게 찾을 수 있음

단점

  • 트라이를 사용하기에 메모리가 많이 필요

시간복잡도

  • O(T+L+P)

    • T = 텍스트 문자열 크기
    • L = 패턴 문자열들의 길이 합
    • P = 텍스트 내의 패턴 발생 수