[알고리즘] 그림으로 알아보는 문자열 검색 알고리즘 - 보이어-무어 알고리즘, 라빈-카프 알고리즘

emplam27·2020년 12월 16일
3
post-thumbnail

저번글에 이어 문자열 검색 알고리즘 중 보이어-무어 알고리즘, 라빈-카프 알고리즘의 구동원리에 대해 알아보겠습니다.

부르트포스, KMP 알고리즘보다 향상된 알고리즘이라고 할 수 있으며 실전에서 더 자주 쓰인다고 합니다. 구동 원리에 대해 알아보겠습니다.


보이어-무어 알고리즘

KMP 알고리즘의 개선판이라고 할 수 있습니다. 시간복잡도는 최악의 경우 O(N)O(N)이지만 평균 O(N/M)O(N/M)의 시간복잡도를 가진다고 합니다. O(N+M)O(N+M)인 KMP 알고리즘보다 향상된 성능을 기대할 수 있습니다.

보이어-무어 알고리즘의 작동은 KMP 알고리즘과 유사하게 불필요한 비교는 건너뛰고 유의미한 비교만 진행하겠다는 것입니다. KMP 알고리즘의 경우에는 접두사와 접미사가 같은 최대길이를 저장하는 배열을 만들어 앞에서부터 검사를 진행하였다면, 보이어-무어 알고리즘은 문자열을 검색할때 주로 뒤에서 부터 확인합니다.

보이어-무어 알고리즘은 두가지 이동 방법이 존재하지만 주로 사용하는 bad match table 방법으로 작성해보겟습니다.

구현과정

KMP 알고리즘과 같이 건너뛰는 경우를 저장하는 배열(skip_table)을 만들어야 합니다. 이때 배열은 본문 문자열이 비교 문자열에 존재하는지, 존재한다면 불일치 시 얼만큼 건너뛰는지를 판단하는 정보를 가지고 만들게 됩니다.

비교 문자열의 각 문자마다 건너뛰는 value 값을 가지는 skip_table이 만들어져야 합니다. value의 의미는 '문자열에 불일치가 일어나면 마지막 문자를 기준으로 skip_table의 value만큼 뒤로 이동시키겠다' 입니다. 각 문자의 value비교문자열 길이(length) - 문자열 index - 1 중 0을 제외한 최솟값으로 계산할 수 있습니다. 이외 모든 문자는 비교문자열의 길이(length)로 계산합니다.

보이어-무어 skip_table

skip_table을 구했다면 문자열 검색을 시작합니다.

보이어-무어 시뮬레이션



라빈-카프 알고리즘

문자열 검색에 hash값을 이용하는 알고리즘입니다. 문자열의 hash값을 한칸씩 옆으로 이동하면서 구하고, 해당 값을 비교 문자열의 hash값과 같은지 확인합니다. hash값이 똑같다면 두 문자열을 직접 비교하여 문자열을 검색합니다.

다만 의문점이 드는건 '매번 문자열의 hash값을 만들게 되면, O(NM)O(NM)과 같은 시간복잡도를 만들게 되는 것 아닌가?' 입니다. 이런 부분을 Rabin-Karp fingerprint라는 해쉬함수를 사용하여 해결합니다. Rabin-Karp fingerprint 해쉬함수는 다음 문자열의 hash값을 O(1)O(1)에 계산할 수 있습니다.

Rabin-Karp fingerprint

Hash는 hash값, S는 문자열의 아스키코드, i는 인덱스, n은 문자열의 길이라고 하면 Rabin-Karp fingerprint의 해쉬 함수는 아래와 같습니다.


H(i)=S[i]2n1+S[i+1]2n2+...+S[i+n2]21+S[i+n1]20H(i)={S[i]\,2^{n-1}}+{S[i+1]\,2^{n-2}}+...+{S[i+n-2]\,2^{1}}+{S[i+n-1]\,2^{0}}


간단히 말하면 문자열의 아스키코드 값마다 2n1,2n2,...,21,202^{n-1}, 2^{n-2}, ..., 2^{1}, 2^{0}을 순서대로 곱하여 hash값을 구해주는 방식입니다. 여기서 O(1)로 다음 hash값을 구하는 방법은 다음과 같습니다.


H(i)=S[i]2n1+S[i+1]2n2+...+S[i+n2]21+S[i+n1]20H(i+1)=S[i+1]2n1+...+S[i+n2]22+S[i+n1]21+S[i+n]20                =2(S[i+1]2n2+...+S[i+n2]21+S[i+n1]20)+S[i+n]20                =2(H(i)S[i]2n1)+S[i+n]20H(i)={S[i]\,2^{n-1}}+{S[i+1]\,2^{n-2}}+...+{S[i+n-2]\,2^{1}}+{S[i+n-1]\,2^{0}}\\ H(i+1)={S[i+1]\,2^{n-1}}+...+{S[i+n-2]\,2^{2}}+{S[i+n-1]\,2^{1}}+{S[i+n]\,2^{0}}\\ \space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space=2({S[i+1]\,2^{n-2}}+...+{S[i+n-2]\,2^{1}}+{S[i+n-1]\,2^{0}})+{S[i+n]\,2^{0}}\\ \space\space\space\space\space\space\space\space\space\space\space\space\space\space\space\space=2(H(i)-{S[i]\,2^{n-1}})+{S[i+n]\,2^{0}}


아래 그림으로 보면 치환되는 부분을 더 직관적으로 알 수 있습니다.

Rabin-Karp fingerprint

이렇게 맨 앞 문자의 아스키코드값, 바로 다음 문자의 아스키코드값만 알고있으면 O(1)O(1)로 다음 hash값을 구할 수 있습니다.

대표적으로 밑을 2로 사용한다고 합니다. 경우에 따라서 밑을 문자의 종류에 따라 바꿀 수 있다고 합니다. 아스키코드 모든 문자열을 포함한다면 256 등으로 설정이 가능하다고 합니다. 단 매우 큰 수 가 나외기 때문에 큰 수를 처리하는 연산이 필요하다고 합니다.

구현과정

해쉬값을 구했다면 작동원리는 부르트포스와 다르지 않습니다. 먼저 비교 문자열의 hash값을 구해줍니다.

라빈-카프 hash값 구하기

이후 기준 문자열의 가장 처음 hash값을 구해주고, 차례대로 hash값을 구하면서 비교합니다.

라빈-카프 시뮬레이션

profile
내가 다시 보고 싶은 글이어야 남들도 보고 싶은 글이라 생각하며 작성합니다. 공부한 내용들을 건강하게 공유하며 함께 성장하고자 합니다😊😊

0개의 댓글