본문 바로가기
--------------------****/알고리즘

KMP 알고리즘/개선된 KMP 알고리즘 & next[]의 FSM 다이어그램

by kk님 2019. 12. 21.

KMP알고리즘

 

코드를 간단히 확인 후, 어떻게 next[]를 채우는지 알아보자.

우선은 어떻게 KMP에 접근할 것인지 표로 관찰해보고,

그 다음에는 코드로 익혀보도록 하자.

 

int KMP_Search(char *p, char *a)

{

    int i, j, M, N;



    M = strlen(p); N = strlen(a);

    InitNext(p); //패턴(p)로 next[] 초기화하는 함수

    for(i = 0, j = 0; j < M && i < N; i++, j++)

       while(j >= 0 && a[i] != p[j])

        j = next[j]; 

    if(j == M) return(i-M);

    else return(N); 

}

 


 

InitNext(char *p)

{

    int i, j, M;



    M = strlen(p);

    next[0] = -1;

    for(i = 0,j = -1; i < M; i++, j++, next[i] = j)

        while(j >= 0 && p[i] != p[j])

        j = next[j];

}

 

비교할 문장(A): abcaababaababbaaa

패턴 예제: ababbaaa 

여기서는, 비교할 문장(A)과 대조하는 것이 아니라, 패턴(ababbaaa)과 패턴(ababbaaa)을 비교한다.

위의 KMP알고리즘 코드를 살펴보면

next[0] = -1 로 나와있다. 따라서 next배열의 맨 처음은 -1로 시작한다.

next배열은, 패턴(ababbaaa)과 비교할 문장(A)를 비교할 때, 만약 패턴(ababbaaa)과 비교할 문장(A)중 다른 부분이 있다면, 다음 순서는 패턴의 어느 위치에서 시작할 것인지를 알려주는 것을 의미한다.

 

즉, 내가 틀리면 패턴의 어느 위치에서 다시 비교해? = next 배열의 역할인 것.

이렇게 대략적으로 next[]의 역할을 알아보았으니,

이제 next[]을 채워보자.

 

위에서 설명한 바와 같이, next[0] = -1로 채운다.

( 이해를 돕기 위해, 표에서 윗줄에 있는 패턴은 P라 하고, 표에서 아랫줄에 있는 패턴은 P' 이라고 하자.

 단, 표의 윗줄에 있는 패턴 P는 이동하지 않는다. 표의 아랫줄에 있는 페턴 P'만 이동한다. )

 

 

 이제부터 차근차근 시작해보자.

여기서는 코드 설명보다 직관적인 이해를 위해 그림으로 먼저 이해해보기로 한다.    

패턴의 맨 처음 a는 next[0] = -1이기 때문에, 비교하지 않는다.

 

 


next[1]을 생각해보면,

b    a    는 일치하지 않는다. a   의 인덱스는 0 이므로 next[1] = 0을 넣는다.

 

 


그리고 b와 a가 불일치했기 때문에 next[2]를 구하기 위해 패턴(P')을 한 칸 이동시킨다.

 

 

여기서 점검해야하는 것은,

불일치한 경우

패턴(P)의 해당 위치 next[]를 채운 뒤, 패턴을 이동시키고 더이상 next[]를 수정하지 않는다는 점이다.

위에서 본 것처럼, 패턴을 이동하더라도 next[1] = -1로 다시 수정하지 않는다는 것.

여기서 본다면 당연한 것처럼 느껴지지만 하단의 설명에서 헷갈릴 수 있는 부분이니 반드시 짚고 넘어가야 한다.

 

패턴(P')을 옆으로 한 칸 밀었을 때, 첫 부분에서 P와 P'이 일치하는 부분이 있다.

 

 


앗 그런데 우연히도 또 일치하는 부분이 있다.

 

 

흠 그렇지만 a와b까지만 일치하고 그 다음은 일치하지 않는다는 것을 확인헀다.

 

 


따라서,패턴(P)의 a와 b 위치에 해당하는 next[]는, 패턴(P)와 패턴(P')가 일치하는 부분의 패턴(P')의 인덱스를 각각 넣어준다.

패턴(P')에서 의 인덱스는 0이고 b   의 인덱스는 1이기 때문에, next[2]와 next[3]에 각각 0과 1을 넣어준다.

 

 


그리고 궁금해지는 부분. 그러면 next[4]는?

아까 언급한 내용을 다시 떠올려보자.

불일치한 경우

패턴(P)의 해당 위치 next[]를 채운 뒤, 패턴을 이동시키고 더이상 next[]를 수정하지 않는다는 점.

그렇다면 불일치한 부분의 next[4]를 채워보자.

패턴(P')에서 a    의 인덱스는 2 이기 때문에

next[4] =2를 넣어준다.

 

 


그리고 패턴(P')을 한 칸 뒤로 이동시켜준다.

 

 

 

여기서 또 궁금한 부분이 생긴다.

next[4]까지 채웠다. 그런데 지금 한 칸 밀었으면 next[4]는 채워져있는데 어떻게 되는거지?

next[]가 채워져있는 부분은 그대로 두자. (수정하지 않는다.)

우리가 원칙적으로 해야할 일은, 패턴(P')의 인덱스가 0인 곳과 일치하는 부분의 패턴(P)를 만날때까지 한 칸씩 뒤로 밀어준다는 것. 그래서 P'의 첫 문자와 일치하지 않는다면 next[]에 0을 넣어준 것이었지만, 이미 next[4]가 채워져있기 때문에 우리는 원칙대로 한 칸을 밀어서 다음을 비교한다.

 

 

그런데, P의 b와 P'의 a가 불일치한다. 하지만 next4]는 채워져있기 때문에 다시 P'을 한 칸 밀어준다.

 

 

P[5]위치에 P'[0]을 위치시킨다.

 

 


이제 패턴(P)와 패턴(P')을 비교하면

a    a    는 일치하지만 a    b    가 불일치한다는 것을 알 수 있다.

 

 


next[5]위치에는, 패턴(P')의 a   에 해당하는 인덱스인 0,

next[6]위치에는, 패턴(P')의 b   에 해당하는 인덱스인 1을 채워준다.

즉, next[5]=0 , next[6]=1 을 채워준다.

 

 

그리고 패턴(P')을 한 칸 밀어준다.

그러면 패턴(P)의 와 패턴(P')가 일치하는 부분을 찾았다.

 

 

a    에 해당하는 패턴(P)의 인덱스가 6이므로 next[6]을 확인하면 next[6]=1로 이미 채워져있다.

따라서 이미 next[]가 채워져있는것은 수정하지 않고, a    다음을 확인한다.

 

 

가 불일치한다는 것을 확인했다. 


next[6]은 이미 채워져있으므로, 불일치하는 부분인 next[7]을 채워주기 위해서는

패턴(P')의 인덱스에 해당하는 1을 next[7]에 넣어준다.

 

 

요약해보면,

1) next[0]=-1을 넣어주고(코드에 나온것처럼)

2) P[i]와 P'[0]을 지속적으로 비교한다. 만약 P'[0]에서부터 불일치 한다면 next[i]에 0을 넣어주고 한칸 씩 뒤로 밀어준다.

   이미 next[i]가 채워져 있다면 다시 수정하지 않는다.

3) 만약 P[j]와 P'[0]이 일치한다면 next[j]=0을 넣고, 그리고 P[j+1]과 P'[1]을 비교한다. 단, 이미 next[i]가 채워져 있다면 다시 수정하지 않는다.

4) P[j+1]과 P'[1]가 일치한다면 next[j+1]=1을 넣는다. P[j+k]와 P'[k]가 불일치 하는 지점까지 next[j+k]=k를 넣어준다.

5) 불일치 하는 지점이 있다면 next[]를 채워주고 한 칸 이동한다. 만약 이동했는데 next[]가 채워져 있다면 불일치 지점     까지 next[]가 채워져있는지 확인하고, 채워져있다면 다시 한 칸 이동한다.

6) 하지만 불일치 지점까지 next[]가 채워져 있지 않다면 위의 3)~5) 과정을 반복한다.

 

 

이것을 FSM으로 그려보면,

 

 

 

KMP의 FSM이 어떤것을 의미하는지 알려면 next[]와 비교해보면 된다.

 

 

FSM에서 화살표가 시작하는 지점과, 화살표가 가리키는 지점이 있는데

 

화살표가 시작하는 지점을 next[]배열이라고 생각하고, 화살표가 가리키는 지점을 next[]배열이 가진 값이라고 생각하자.

 

KMP 알고리즘의 FSM에서 a를 주목하자.

 

여기서 FSM의 첫번째 문자인 a를 살펴보자.

 

 

표에서 P가 가진 첫문자 'a' 의 인덱스인 0 위치에 해당하는 next[0]의 값을 보면 -1이 들어있다.

FMS을 다시 보면,

화살표가 시작하는 지점인 'a'에서,

화살표가 가리키는 지점인 '시작'이라고 적힌 네모칸을 가리킨다.

a 의 next인 next[0]에는 -1이 들어있다는 것을 의미한다.

 

위와같은 방식으로 P에 해당하는 next[]를 보면 FSM을 쉽게 이해할 수 있다.

 

이제는 개선된 KMP알고리즘의 FSM에 대해 알아보자.

 

개선된 KMP알고리즘의 FSM을 만들기 위해서는

위에서 만들었던 KMP알고리즘의 FSM을 이용하는게 가장 빠르다.

 

개선된 KMP는 P의 next[]가 어떤것을 가리키는지 알아야 한다.

 

 

 

 

개선된 KMP알고리즘의 코드중, Init() 부분의 for문을 주의해서 봐야한다

 

int KMP_Search2(char *p, char *a)

{

    int i, j, M, N;

 

    M = strlen(p); N = strlen(a);

    InitNext2(p); /* next[] 초기화 */

    for(i = 0, j = 0; j < M && i < N; i++, j++)

        while(j >= 0 && a[i] != p[j])

    j = next[j];

    if(j == M) return(i-M); /* 일치 문자열 시작 위치 */

    else return(N); /* return (-1), 불일치 */

}

 


 

InitNext2(char *p)

{

    int i, j, M;

 

    M = strlen(p);

    next[0] = -1; /* 첫 문자 불일치시 한칸 이동 */

    for(i = 0, j = -1; i < M; i++, j++, next[i] = (p[i]==p[j]) ? next[j] : j)

        while(j >= 0 && p[i] != p[j])

    j = next[j];

}


 

 

개선된 KMP알고리즘과 KMP알고리즘의 차이점은, next[i]=(p[i]==p[j])? next[j] : j 부분이라는 점.

KMP알고리즘에서는 next[i]=j를 하는 것으로 next[i]를 채우고 다음으로 넘어갔다.

하지만, 개선된 KMP알고리즘에서는 조건식을 사용해서 한번의 검사를 더 하게 된다. 

 

next[i]=(p[i]==p[j])? next[j] : j

위의 문장을 if-else문으로 바꾼다면

 

if(p[i]==p[j]){

   next[i]=next[j];

}else{

   next[i]=j;

}

 

즉, 화살표 시작점의 원소와 화살표가 가리키는 지점의 원소가 같다면, next[i]에 대입하는 것은 화살표가 가리키는 지점의 원소가 가리키는 지점의 원소가 가진 next[j]를 넣으라는 것이 된다. 

 

위의 FSM에서 화살표의 시작점과 화살표가 가리키는 지점을 보자.

P[0]인 'a'는 -1을 가리키고,

P[1]인 'b'의 next[1]은 P[0]인 'a'를 가리킨다. 화살표 시작점=b, 화살표가 가리키는 지점=a (시작점과 가리키는 점이 다르기 때문에 다음으로 넘어가 P[2]를 확인한다.)

P[2]인 'a'의 next[2]는 P[0]인 'a'를 가리킨다. 화살표 시작점=a, 화살표가 가리키는 지점=a (a와 a가 같다)

 

 

P[3]인 'b'의 next[3]P[1]인 'b'를 가리킨다. 화살표 시작점=b, 화살표가 가리키는 지점=b (b와 b가 같다)

 

 

P[4]인 'b'의 next[4]는 P[2]인 'a'를 가리킨다. 화살표 시작점=b, 화살표가 가리키는 지점=a (시작점과 가리키는 점이 다르기 때문에 다음으로 넘어가 P[5]를 확인한다.)

P[5]인 'a'의 next[5]는 P[0]인 'a'를 가리킨다. 화살표 시작점=a, 화살표가 가리키는 지점=a (a와 a가 같다)

 

 

P[6]인 'a'의 next[6]P[1]인 'b'를 가리킨다. 화살표 시작점=a, 화살표가 가리키는 지점=b (시작점과 가리키는 점이 다르기 때문에 다음으로 넘어가 P[7]을 확인한다.)

P[7]인 'a'의 next[7]P[1]인 'b'를 가리킨다. 화살표 시작점=a, 화살표가 가리키는 지점=b (시작점과 가리키는 점이 다르다는 것을 확인했다.)

 

이렇게 봤을 때,

 

 

next[2]에 next[0]인 -1이 대입
대입 결과
next[3]에 next[1]인 0이 대입
대입 결과
next[5]에 next[0]인 -1이 대입
대입 결과

 

이렇게 개선된 KMP도 FSM으로 확인할 수 있다.