KMP 알고리즘이란? (상세 설명)

KMP 알고리즘은 무엇입니까?

KMP는 DEKnuth, JHMorris 및 VRPratt라는 세 마리의 큰 소에 의해 동시에 발견되었습니다. 첫 번째는 "컴퓨터 프로그래밍 기술"의 저자입니다! !

KMP 알고리즘으로 해결해야 할 문제는 문자열 (기본 문자열이라고도 함)에서 패턴을 찾는 것입니다. 간단히 말해서 우리가 일반적으로 말하는 키워드 검색입니다. 패턴 문자열은 키워드 (이하 P)이며, 기본 문자열 (이하 T)에 나타나면 특정 위치를 반환하고 그렇지 않으면 -1 (일반적으로 사용되는 의미)을 반환합니다.
여기에 사진 설명 삽입

우선이 문제에 대해 아주 간단한 생각을 가지고 있습니다 : 왼쪽에서 오른쪽으로 하나씩 일치하는 과정에서 일치하지 않는 문자가 있으면 뒤로 건너 뛰고 패턴 문자열을 오른쪽으로 이동합니다. 이것에 대해 무엇이 그렇게 어렵습니까?

다음과 같이 초기화 할 수 있습니다.
여기에 사진 설명 삽입

그 후에는 i 포인터가 가리키는 문자가 j 포인터가 가리키는 문자와 일치하는지 여부 만 비교하면됩니다. 일관성이있는 경우 아래와 같이 일관성이없는 경우 뒤로 이동합니다.

여기에 사진 설명 삽입

A와 E가 같지 않은 경우 i 포인터를 첫 번째 위치로 다시 이동하고 (아래 첨자가 0에서 시작한다고 가정) j를 패턴 문자열의 0 번째 위치로 이동 한 다음이 단계를 다시 시작합니다.
여기에 사진 설명 삽입

이 아이디어를 바탕으로 다음 프로그램을 얻을 수 있습니다.

/**

 * 暴力破解法

 * @param ts 主串

 * @param ps 模式串

 * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1

 */

public static int bf(String ts, String ps) {
    
    

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    while (i < t.length && j < p.length) {
    
    

       if (t[i] == p[j]) {
    
     // 当两个字符相同,就比较下一个

           i++;

           j++;

       } else {
    
    

           i = i - j + 1; // 一旦不匹配,i后退

           j = 0; // j归0

       }

    }

    if (j == p.length) {
    
    

       return i - j;

    } else {
    
    

       return -1;

    }

}

위의 프로그램은 괜찮지 만 충분하지 않습니다!

인위적으로 검색하면 첫 번째 A를 제외하고는 실패 위치와 일치하는 메인 스트링 앞에 A가 없기 때문에 반드시 처음으로 돌아 가지 않을 것입니다. 메인 스트링 앞에 A가 하나만 있다는 것을 왜 알 수 있습니까? ? 처음 세 문자가 일치한다는 것을 이미 알고 있기 때문입니다! (이건 매우 중요합니다). 과거의 이사는 확실히 일치하지 않습니다! 아이디어가 있습니다. 나는 움직일 수 없습니다. 아래 그림과 같이 j 만 움직이면됩니다.

여기에 사진 설명 삽입

위의 상황은 여전히 ​​상대적으로 이상적이며 기껏해야 다시 비교할 것입니다. 그러나 메인 문자열 "SSSSSSSSSSSSSA"에서 "SSSSB"를 검색하면 마지막 문자열을 비교 한 다음 역 추적 할 때까지 일치하지 않는다는 것을 알게 될 것입니다. 그리고 나서 역 추적하면 효율성이 가장 낮습니다.

큰 젖소는 "무력 균열"의 비효율적 인 방법을 견딜 수 없었기 때문에 세 마리는 KMP 알고리즘을 개발했습니다. 아이디어는 위에서 본 것과 같습니다. "부분적으로 일치 된 유효한 정보를 사용하여 i 포인터가 역 추적되지 않도록하고 j 포인터를 수정하면 패턴 문자열이 가능한 한 유효한 위치로 이동됩니다."

따라서 전체 KMP의 핵심은 특정 문자가 기본 문자열과 일치하지 않을 때 j 포인터를 어디로 이동해야하는지 알아야한다는 것입니다.

다음으로 j의 운동 법칙을 스스로 알아 봅시다.
여기에 사진 설명 삽입

그림과 같이 C와 D가 일치하지 않습니다. j를 어디로 이동 하시겠습니까? 분명히 1 위. 왜? 앞의 A가 동일하기 때문에 :
여기에 사진 설명 삽입

동일한 상황이 아래 그림에 나와 있습니다.

여기에 사진 설명 삽입

앞에있는 두 글자가 동일하기 때문에 j 포인터를 두 번째 위치로 이동할 수 있습니다.

여기에 사진 설명 삽입

지금까지 우리는 매치가 실패하면 j가 이동할 다음 위치 k에 대한 단서를 볼 수 있습니다. 이러한 속성이 있습니다. 처음 k 개의 문자는 j 이전의 마지막 k 개의 문자와 동일합니다.

수학 공식을 사용해서 이렇게 표현하면

P[0 ~ k-1] == P[j-k ~ j-1]

이것은 매우 중요합니다. 기억하기 힘들다면 다음 그림을 통해 이해할 수 있습니다.
여기에 사진 설명 삽입

이것을 이해하면 j가 k 위치로 직접 이동할 수있는 이유를 이해할 수있을 것입니다.

때문에:

当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]

공식은 지루합니다. 읽고 이해할 수 있습니다. 기억할 필요가 없습니다.
여기에 사진 설명 삽입

이 단락은 이전 k 문자를 비교하지 않고 j를 k로 직접 이동할 수있는 이유를 증명하기위한 것입니다.

좋아요, 다음 단계는 요점입니다이 (이) k를 어떻게 찾을 수 있을까요? P의 각 위치에서 불일치가 발생할 수 있기 때문에 즉, 각 위치 j에 해당하는 k를 계산해야하므로 저장하기 위해 next [j] = k 배열을 사용합니다. 이는 T [i]! = 일 때를 의미합니다. P [j], j 포인터의 다음 위치.

많은 교과서 나 블로그 게시물이이 곳에서 다소 모호하거나 한 번에 언급되거나 심지어 코드가 게시되어 있는데 왜 요청을합니까? 이것을 어떻게 요청할 수 있습니까? 전혀 명확하지 않습니다. 그리고 여기에 전체 알고리즘에서 가장 중요한 부분이 있습니다.

public static int[] getNext(String ps) {
    
    

    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {
    
    

       if (k == -1 || p[j] == p[k]) {
    
    

           next[++j] = ++k;

       } else {
    
    

           k = next[k];

       }

    }

    return next;

}

다음 배열을 찾는 알고리즘의이 버전은 가장 널리 퍼져 있어야하며 코드는 매우 간결합니다. 하지만 정말 혼란 스럽습니다.이 계산의 근거는 무엇입니까?

자, 이건 제쳐두고 우리 자신의 아이디어를 도출해 봅시다. 이제 우리는 next [j] (즉, k)의 값이 P [j]! = T [i] 일 때 j 포인터를 의미한다는 것을 항상 기억해야합니다. 다음으로 위치를 이동합니다.

첫 번째를 먼저 살펴 보겠습니다. j가 0 일 때 지금 일치하는 항목이 없으면 어떻게됩니까?

여기에 사진 설명 삽입

위 그림의 경우 j는 이미 맨 왼쪽에있어 이동할 수 없으며 이때 i 포인터가 뒤로 이동해야합니다. 따라서 next [0] = -1,이 초기화 코드가 있습니다.

j가 1이면 어떨까요?
여기에 사진 설명 삽입

분명히 j 포인터는 0 위치로 다시 이동해야합니다. 앞에는이 자리 밖에 없으니까 ~~~

다음이 가장 중요합니다. 아래 그림을 참조하십시오.

여기에 사진 설명 삽입
여기에 사진 설명 삽입

이 두 수치를주의 깊게 비교하십시오.

규칙을 찾았습니다.

当P[k] == P[j]时,
有next[j+1] == next[j] + 1

실제로 이것은 증명할 수 있습니다.

因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1

여기의 공식은 이해하기 쉽지 않지만 그림을 보면 이해하기 더 쉬울 것입니다.

만약 P [k]! = P [j]? 예를 들어, 아래 그림과 같이 :
여기에 사진 설명 삽입

이 경우 코드를 보면 다음과 같은 문장이어야합니다 : k = next [k]; 왜 이런가요? 아래를 볼 수 있습니다.
여기에 사진 설명 삽입

이제 왜 k = next [k]인지 알아야합니다! 위의 예와 같이 더 이상 가장 긴 접미사 문자열 [A, B, A, B]를 찾을 수 없지만 [A, B] 및 [B]와 같은 접두사 문자열은 여전히 ​​찾을 수 있습니다. 그래서이 과정은 C가 메인 문자열과 다를 때 (즉, k의 위치가 다를 때) 문자열 [A, B, A, C]의 위치를 ​​지정하는 것 같습니다. 물론 포인터는 next [k]로 이동합니다. .

다음 배열을 사용하면 모든 것이 쉬워지며 KMP 알고리즘을 작성할 수 있습니다.

public static int KMP(String ts, String ps) {
    
    

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    int[] next = getNext(ps);

    while (i < t.length && j < p.length) {
    
    

       if (j == -1 || t[i] == p[j]) {
    
     // 当j为-1时,要移动的是i,当然j也要归0

           i++;

           j++;

       } else {
    
    

           // i不需要回溯了

           // i = i - j + 1;

           j = next[j]; // j回到指定位置

       }

    }

    if (j == p.length) {
    
    

       return i - j;

    } else {
    
    

       return -1;

    }

}

무차별 대입 크래킹과 비교하여 4 곳이 변경되었습니다. 요점은 내가 역 추적 할 필요가 없다는 것입니다.

마지막으로 위 알고리즘의 결함을 살펴 보겠습니다. 첫 번째 예를보십시오.
여기에 사진 설명 삽입

당연히 위의 알고리즘으로 얻은 다음 배열은 [-1, 0, 0, 1]

따라서 다음 단계는 j를 첫 번째 요소로 이동하는 것입니다.
여기에 사진 설명 삽입

이 단계가 완전히 무의미하다는 것을 찾는 것은 어렵지 않습니다. 후자 B가 더 이상 일치하지 않기 때문에 전자 B도 일치하지 않아야합니다. 실제로 두 번째 요소 A에서 동일한 상황이 발생합니다.

분명히 문제의 원인은 P [j] == P [next [j]] 때문입니다.

따라서 판단 조건 만 추가하면됩니다.

public static int[] getNext(String ps) {
    
    
    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {
    
    

       if (k == -1 || p[j] == p[k]) {
    
    

           if (p[++j] == p[++k]) {
    
     // 当两个字符相等时要跳过

              next[j] = next[k];

           } else {
    
    

              next[j] = k;

           }

       } else {
    
    

           k = next[k];

       }

    }

    return next;

}

추천

출처blog.csdn.net/weixin_52622200/article/details/110563434