2 분 소요

INPUT

src     : ababacabacaaba
search  : abacaaba 

PI

//pi[i] = str[..i] 의 접미사도 되고 접두사도 되는 문자열의 최대 길이
public static int[] getPartialMatch(String str) {
		int[] pi = new int[str.length()];
	 //N을 N에서 찾는다. begin=0 이면 자기 자신을 찾아버리니까 건너뛴다.
		int begin = 1, matched = 0;
	//비교할 문자가 N의 끝에 도달할 때까지 찾으면서 부분 일치를 모두 기록한다.
		while (begin + matched < str.length()) {
			if (str.charAt(begin + matched) == str.charAt(matched)) {
				++matched;
				pi[begin + matched - 1] = matched;
		//일치하면 접미사, 접두사 되는 문자열 최대 길이 + 1해서 쌓아준다.
			} else {
				if (matched == 0)
					++begin;
		//값이 다르고 matched가 0이면 begin을 다음 인덱스로 이동시킨다.
				else {
					begin += matched - pi[matched - 1];
					matched = pi[matched - 1];
				}
			}
		}
		return pi;
	}

abacaaba 예시 ⇒ [0, 0, 1, 0, 1, 1, 2, 3]

begin matched str.charAt(matched) str.charAt(begin + matched) pi 문자열 위치
1 0 a b pi[1]=0 abacaaba
2 0 a a pi[2]=1 abacaaba
2 1 b c   abacaaba
3 0 a b pi[3]=0 abacaaba
4 0 a a pi[4]=1 abacaaba
4 1 b a   abacaaba
5 0 a a pi[5]=1 abacaaba
5 1 b b pi[6]=2 abacaaba
5 2 a a pi[7]=3 abacaaba

KMP

vector<int> kmpSearch(string& src, string& search){
    int n = src.size(), m = search.size();
    vector<int> ret;
    // 탐색할 문자열의 접두사, 접미사 길이를 문자열의 처음부터 끝 까지 미리 계산
    vector<int> pi = getPartialMatch(search);    
    int begin = 0, matched = 0;
    
    while(begin <= n - m){
        // 탐색할 문자열과 원본 문자열에서 현재 위치의 문자가 동일한 경우
        if (matched < m && src[begin + matched] == search[matched]){
            ++matched;
            
            // 문자열이 모두 일치하는 경우 답에 추가한다.
            if (matched == m)
                ret.push_back(begin);
        }
        else{
            // 일치하는 부분이 없는 경우, 다음 위치의 문자 부터 탐색
            if(matched == 0)
                ++begin;
           
            else{
                begin += matched - pi[matched - 1];
               // begin을 옮겼다고 처음부터 다시 비교할 필요가 없다.
								// 옮긴 후에도 p[matched-1] 만큼은 항상 일치하기 때문이다.
                matched = pi[matched - 1];
            }
        }
    }

    return ret;
}

시간 복잡도 분석

  • 분기 결과마다 다른 동작을 하기에 분석하기 어렵지만

    if 문의 각 분기 내용이 최대 몇번 수행되는 지로 수행 시간을 분석

  • while 문 내에서 begin + matched 는 절대 감소하지 않는다.
  • 한번 match 가 증가하고 나면 H[begin+matched] 를 다시 참조할 일은 전혀 없으므로
  • 비교 성공은 짚더미의 각 문자당 최대 한 번씩만 일어나고

    결과적으로 이 분기는 최대 O( H )번 수행된다.

예제: 접두사/접미사

문자열 S가 주어질 때 이 문자열의 접미사도 되고 접두사도 되는 문자열들의 길이를 전부 출력

S=”ababbaba” 일 경우 “a”와 “aba”는 문자열의 접미사도 되고 접두사도 된다.

  • ababbaba
  • ababbaba
//s의 접두사도 되고 접미사도 되는 문자열들의 길이를 반환한다.
vector<int> getPrefixSuffix(const string$ s){
	vector<int> ret, pi = getPartialMatch(s);
	int k = s.size();
	while(k>0){
	  //s[..k-1]는 답이다.
		ret.push_back(k);
    k = pi [k-1];
}
return ret;
S k ret.push_back pi[k-1]
ababbaba 8 8 pi[7] = ababbaba → 3 ⇒ k
  3 3 pi[2] = ababbaba → 1 = k
  1 1 pi[0] = ababbaba → 0 = k
       

정답=8,3,1