KMP 알고리즘

2020. 7. 17. 02:59Algorithm/이론

KMP 알고리즘을 배우기전 엄청 큰 문자열 더미에서 특정 문자열을 찾기 위해서는 아래와 같이 O(N*H)의 시간복잡도를 사용하여 찾았을 것이다.  N은 찾을려는 문자열 H는 문자열 더미이다.

for(int begin = 0; begin + N.size() <= H.size(); ++begin){
	bool matched = true;
    for(int i = 0; i < N.size(); i++){
    	if(H[begin + i] != N[i]){
        	matched = false;
            break;
        }
    }
    if(matched) ret.push_back(begin);
}

예를 들어 H = "avadakedavra"에서 N = "aked"를 찾는 과정을 아래 사진으로 보여줍니다. 모든 상황을 비교하는 비효율적인 장면을 볼 수 있습니다.

이럴때 입력이 크면 꽤나 비효율적이지만 이런경우 흔치 않게 구현이 단순하기 때문에 표준라이브러리에 사용됩니다. C의 strstr(), C++ 문자열의 string::find(), 자바 문자열의 indexOf() 등이 이와같은 알고리즘을 사용합니다.

 

KMP 알고리즘

위와 다르게 검색 과정 중 얻는 정보를 버리지 않고 잘 활용하면 많은 시간을 절약할 수 있습니다. 

 

어떤 문자열에서 N="aabaabac"를 찾는 경우를 예로 생각해봅시다. 만일 aabaaba까지는 일치했지만 마지막 c가 일치하지 않는 경우 위와 같은 경우에는 한글자 옆으로 이동해서 반복하여 검색했습니다. 하지만 지금까지 일치하는 일곱 글자가 대응되었다는 사실을 이용하면 시작 위치 중 일부는 답이 될 수 없음을 보지 않아도 알 수 있기 때문입니다. 

위치 i에서 일곱 글자가 일치하기 위해서는 그림에서 볼 수 있듯이 H[i : i + 6]의 문자열이  aabaaba여야 합니다. i+1의 경우에는 한글자는 동일하지만 뒤에 글자가 틀려서 탈락. 결국 되는 것은 i+3, i+6 뿐입니다. 따라서 다음 확인 위치는 i+1이 아닌 i+3입니다. 

 

이와 같이 검색 과정에서 정보들을 미리 계산해 저장해 둘 수 있습니다. 이와 같은 문자열 알고리즘을 KMP 알고리즘이라고 합니다. 

 

다음 시작 위치 찾기

위 부분에서 검색 과정 중의 정보를 저장하여 한번 검색 후 빠르게 다음 검색으로 이동해야하는 데 다음 시작 위치는 어떻게 찾아야 하는지 알아 봅시다. 

 

시작점 i에서 matched까지 일치하고 마지막 글자에서는 틀렸습니다. matched 글자가 일치 했기 때문에 N의 접두사 N[matched-1]가 H[i:i+matched-1]와 일치했음을 알 수 있습니다. 

 

두 회색 부분의 글자가 같기 때문에 A와 B는 서로 일치함을 알 수 있습니다. 그러고 i + k가 답이 될 수 있으려면 B와 C도 서로 같아야 하므로 결과적으로 A와 C가 같아야합니다. 즉 결과적으로 C는 N[matched-1]의 접두사, A는 N[matched-1]의 접미사입니다. 

 

정리하자면 시작 위치 i+k에서 답을 찾기 위해서는 N[matched-1]의 길이 matched-k인 접두사와 접미사가 같아야합니다. 

그래서 빠르게 다음 위치를 찾기 위해서는 N의 각 접두사도 되고 접미사도 되는 문자열의 최대 길이를 구하면 됩니다. 

 

그래서 aabaaba의 경우에는 3만큼 옮김 aaba가 될 것입니다.

 

그래서 전처리 과정으로 pi[i] = N[...i]의 접두사도 되고 접미사도 되는 문자열의 최대 길이를 구하면 됩니다. 

 

KMP 알고리즘 구현

KMP 알고리즘은 시작위치 0에서 시작해서 H와 N의 글자를 비교합니다. 만약 matched 글자가 일치한 후 불일치가 발생했다면 시작점에  matched - pi[matched-1] 만큼 더해주면 됩니다. 

 

쉽게 설명하면 위 사진의 경우 현재 일치한 글자 수가 6개 입니다. 6개인데 접미사와 접두사가 동일한 가장 큰 길이는 4입니다. 

즉 matched - pi[matched-1]는 6  - 4가 되어서 시작점 i에 2만큼 추가하여 검색하면 됩니다. 그러고 다음 matched는 B와 C가 동일하기 때문에 4가되는 겁니다.

 

또한 하나도 일치하지 않는 경우에는 다음 글자부터 검색을 시작하면 됩니다. 

 

vector<int> KMPSearch(string base, string hook){
	int baseSize = base.size();
	int hookSize = hook.size();
	vector<int> ret;
	vector<int> pi = getPartialMatch(hook);

	int begin = 0, matched = 0; // 첫 글자부터 검색을 시작한다.
	while(begin <= baseSize-hookSize){
		if(matched < hookSize && base[begin+matched] == hook[matched]){
			matched++;
			if(matched == hookSize)
				ret.push_back(begin);
		}else{
			if(matched == 0) // matched가 0이면 다음 칸부터
				begin++;
			else{
				begin += matched - pi[matched-1];
				matched = pi[matched-1];	
			}
		}
	}

	return ret;
}

 

부분 일치 테이블 만들기 

접미사와 접두사가 동일한 최대길이 테이블 pi를 만들어보겠습니다. 여기서도 kmp 코드를 활용하여 pi를 구하겠습니다.

vector<int> getPartialMatch(string N){
	int m = N.size();
	vector<int> pi(m, 0);

	int begin = 1, matched = 0; // begin이 0이면 자기 자신을 찾는다.

	while(begin + matched < m){
		if(N[begin + matched] == N[matched]){ // begin에서 + matched뒤 matched는 앞 일치할 경우 
        //일치한 크기 삽입
			matched++;
			pi[begin+matched-1] = matched; 
		}else{
			if(matched == 0)
				++begin;
			else{
				begin += matched - pi[matched-1];
				matched = pi[matched-1];
			}
		}
	}

	return pi;
}

 

시간 복잡도 

위 KMPSearch의 경우 반복문이 base의 사이즈 만큼 한번 돌기 때문에 O(baseString) 부분 일치 테이블도 O(hookSize만큼 한번 돌기 때문에) 

 

O(base+hook)이된다.

'Algorithm > 이론' 카테고리의 다른 글

올바른 이진 탐색 트리인지 확인하기  (0) 2021.01.19
트라이  (0) 2020.07.18
union-find(Disjoint-set)  (0) 2020.06.25
투포인터(two pointer)  (4) 2020.05.10
최단 경로(3) 벨만 포드  (0) 2020.04.11