본문 바로가기

수업정리/자료구조

KMP (Knuth-Morris-Pratt) Algorithm 알고리즘

이 내용은 시간복잡도와 Big-O 를 알고 있어야 이해할 수 있다. 

KMP Algorithm : 문자열에서 특정 패턴 문자열을 찾는 방법이다. 


1. 일반적인 문자열에서 패턴 문자열 찾기

                      ↓                     

String A B C F A B D C
Pattern A B D          

                                     

String A B C F A B D C
Pattern   A B D        

                                                                     ↓                      

String A B C F A B D C
Pattern       A B D    

                                                                                      ↓          ↓           

String A B C F A B D C
Pattern         A B D  

위에 있는 문자열은 전체 문자열이다. --> String

아래에 있는 문자열은 우리가 찾고자 하는 패턴 문자열이다. --> Pattern

String 의 첫 글자부터 Pattern 문자열과 비교해서 같으면 다음 글자를 비교하고 ... 이를 반복한다.

만약 다르다면 String 의 다음 글자부터 다시 비교한다.

시간 복잡도를 계산해 본다면 

1. String 의 글자 수 인 m 번 만큼 반복한다.

2. 각각의 경우에 Pattern 의 글자 수 인 n 까지 일치하는지 확인한다.

따라서 O(mn) 이 나온다.

이 시간 복잡도를 줄이기 위해 KMP Algorithms 이 만들어 졌다.


2. 시간 복잡도를 줄이는 메커니즘

                      ↓                    

String A B C F A B D C
Pattern A B D          

                                     

String A B C F A B D C
Pattern   A B C        

위의 예시에서 A B 두 글자가 일치하고 D 글자가 불일치 한 다음에 다시 비교를 할 때 String 의 두번째 글자부터 비교를 시작했다. 즉 String 의 비교 화살표는 C까지 갔다가 다시 B로 되돌아 오는 상황이 된다.

만약 String 의 비교 화살표가 뒤로 되돌아 오지 않고 한 방향으로 쭉 가면 시간 복잡도는 줄어들 것이다.( O(m)+O(n) ) 

a. 시간 복잡도 줄이기

                      ↓                     

String A B C F A B D C
Pattern A B D          

                                           

String A B C F A B D C
Pattern   A B D        

우리는 첫번째 비교로 앞 두글자 A B 는 일치하고 C 부터 틀리다는 것을 알았다. 그렇다면 그냥 바로 틀렸던 글자인 C 부터 다시 비교하면 안될까? 그렇게 해도 된다면 시간 복잡도는 위에서 말한 이유처럼 줄어들게 된다. 

b. 고려해야 하는 것

                       ↓                    ↓           ↓           ↓            ↓ 

String A B C A B C A B D
Pattern A B C A B D      

                                                                                                            

String A B C A B C A B D
Pattern           A B C A

위 처럼 바로 넘어갔을 때 건너 뛰는 A B 가 패턴에 일치되는 문자열의 앞부분일 수 있다. 따라서 함부로 불일치하는 곳 까지 넘어가면 안된다.

c. 해결방법

                      ↓                       ↓          ↓           ↓            ↓ 

String A B C A B C A B D
Pattern A B C A B D      

                                                         X         X          ↓      

String A B C A B C A B D
Pattern       A B C A B D

만약 우리가 Pattern 의 앞부분이 String 의 건너뛰는 부분과 일치하는 곳을 안다면 그 다음 부분부터 비교를 진행하면 된다.

우리가 미리 Pattern 의 문자열을 분석하여 앞부분 글자와 겹치는 부분을 안다면 시간 복잡도가 줄어들 것이다.

d. 정리

위의 생각들을 정리하자면

1. 특정 패턴 문자열에서 불일치가 발생했을 때                                 A B C A B C D

2. 패턴 문자열의 앞부분(prefix) 과 일치하는 부분(suffix) 를 안다면        A B C A B C D

3. 일치하는 부분 다음 글자부터 비교를 하면 된다                             A B C A B C D

4. 이렇게 된다면 String 의 비교 화살표는 뒤로 돌아오지 않고 한 방향으로 쭉 비교를 진행한다.


3. KMP Algorithm 구현하기

위에서 다룬 내용을 다시 한 번 보고 가자.

1. 우리는 문자열에서 특정 패턴 문자열을 찾으려고 한다.

2. 일치된 문자열을 건너 뛰어 시간복잡도를 줄이려고 하는데 문자열을 건너 뛸 때 생기는 문제점이 있다.

3. 이를 해결하기 위해 패턴 문자열에서 미리 앞부분에 겹치는 글자가 '있는지, 얼마나 있는지' 를 미리 구하려고 한다.

 

1. 패턴 문자열에서 미리 정보를 구해놓기

Pattern A B C A B A B C  
T[n] -1 -1 -1 0 1 0 1 2  

위는 패턴 문자열과 T 인덱스이다.  T 인덱스는 현재 패턴 문자열에서 앞 글자와 지금 글자가 prefix 와 일치하는지에 대한 데이터를 담고 있다.

단순히 보았을때 처음 T 값이 0 이 된 부분을 보면 prefix 와 같기 때문에 T 값이 0 이 되었다.

다음 글자인 B 도 앞글자인 A가 prefix와 일치하고 B 또한 prefix 와 같기 때문에 T 값은 증가하여 1 이 된다.

즉 prefix 와 같은 글자라면 T 값은 0 이 아니고 연속적으로 같은 글자면 T 값은 1씩 증가하게 된다.

아래 다른 예시 몇 개를 본다면 대충 어떤 패턴인지 알 수 있을 것이다.

Pattern A B C A A A C D  
T[n] -1 -1 -1 0 0 0 -1 -1  
Pattern A B A C A B B C  
T[n] -1 -1 0 -1 0 1 -1 -1  
Pattern A B C A B C A B  
T[n] -1 -1 -1 0 1 2 3 4  

2. 패턴 문자열에서 미리 정보 구하는 코드

void fail(char *pat) {

  /* compute pattern's failure function */
  int i, j, n = strlen(pat);
  
  failure[0] = -1;                  // 첫번째 T=-1
  for(j=1; j<n; j++) {              // 두번째 글자부터 마지막 글자까지 비교
    i = failure[j-1];               // 이 전 글자의 T 값이 앞 글자의 i 값 

    // i 가 양수고 이번 글자와 prefix 다음 글자가 다를 경우
    while((i >= 0) && (pat[j] != pat[i + 1])) // i>=0 이라면 이 전 글자(들)이 일치한다는 거고 --> i 가 한번 거쳤는데도 양수면 현재 글자와 같은 글자가 앞 prefix 에 있다는 것?
        i = failure[i];                       // 이제 이번 글자와 prefix 를 비교
                                              // 만약 일치하지 않으면 
                                              // 지금 현재 글자 전까지는 prefix 와 일치하니까
                                              // 현재 글자와 prefix 다음 글자와 비교 반복
                                              // 지금 현재 글자랑


    if(pat[j] == pat[i+1])                    // 이번 글자와 prefix 다음 글자 비교
        failure[j] = i+1;                     // 일치하면 T 값을 하나씩 증가
    else failure[j] = -1;                     // 다르면 T = -1
  }
}

 

3. 미리 정보를 처리한 패턴 문자열과 문자열을 비교하는 함수

우리는 패턴 문자열에 앞부분에 겹치는 글자가 얼마나 있는지를 미리 구해놨습니다. ( T[n] )

이제 패턴 문자열과 문자열을 비교해봅시다.

String A B C F A B C D A B D
Pattern A B C D A B D        
T[n] -1 -1 -1 -1 0 1 -1        
String A B C F A B C D A B D
Pattern       A B C D A B D  
T[n]       -1 -1 -1 -1 0 1 -1  

위와 같은 경우 String 의 불일치한 F 부분 부터 Pattern 의 첫 부분인 A 부터 다시 비교를 시행합니다.

String A B C D A B F A C B D
Pattern A B C D A B D        
T[n] -1 -1 -1 -1 0 1 -1        
String A B C D A B F A C B D
Pattern         A B C D A B D
T[n]         -1 -1 -1 -1 0 1 -1

위와 같은 경우 String 의 불일치한 부분 F 부터 Pattern 의 세번째 부분인 C 부터 비교를 시행합니다. 앞 글자인 A B 는 prefix 와 일치하다는 것을 T[n] 을 통해 알 수 있기 때문입니다.

int pmatch(char *string, char *pat) {

  /* Knuth-Morris-Pratt string matching algorithm */
  int i=0, j=0;
  int lens = strlen(string);
  int lenp = strlen(pat);
  // i 길이랑 j 길이 둘 중 하나 끝날 때 까지
  while(i < lens && j < lenp) {
      // string i 글자 pattern j 글자 비교
      //같으면 비교화살표 둘 다 증가
    if(string[i] == pat[j]) 
    { i++; j++; }
    // 다르고 pattern 첫 글자면 String i 비교 화살표 증가
    else if(j == 0) 
        i++;
    // 다르고 pattern 비교화살표 가 첫 글자가 아니면 --> 전 글자가 prefix 와 같으면
    // pattern 의 비교화살표는 이 전 T[]값 ( 겹치는 글자 수) + 1 --> 겹치는 글자 수 다음 글자
    else 
        j = failure[j-1] + 1;
  }
  return ((j==lenp) ? (i-lenp) : -1);
}

위 코드가 미리 정보를 처리한 Pattern 과 String 을 비교하는 코드입니다.


4. KMP Algorithm 의 시간복잡도

이제까지 일반적인 문자열에서 패턴 문자열 찾기 의 시간복잡도를 줄이기 위한 KMP Algorithm 을 살펴 보았다.

원래의 시간복잡도는 String 의 길이인 m, Pattern 의 길이인 n 으로 O(mn) 이었다.

KMP Algorithm 의 시간 복잡도는 다음과 같다.

Pattern 의 정보 처리 함수 : 비교 화살표가 항상 1씩 증가하면서 앞부분 글자와 같은지 비교한다. --> n 번 시행

문자열 비교 함수 : String 의 비교 화살표는 항상 1씩 증가하며 뒤로 되돌아가지 않는다. --> m 번 시행

따라서 시간 복잡도는O(m + n) 이다.

원래의 시간 복잡도 O(mn) 과 비교했을 때 상당히 효율적이다.

 

참고자료 : 서강대학교 소정민 교수님 수업자료

'수업정리 > 자료구조' 카테고리의 다른 글

시간 복잡도 이해하기  (0) 2021.09.23