본문 바로가기

CS/알고리즘

[알고리즘] 라빈-카프 알고리즘

라빈-카프(Rabin-Karp) 알고리즘은 1987년 리처드 M. 카프와 마이클 O. 라빈에 의해 개발된 문자열 검색 알고리즘입니다.

이 알고리즘은 주어진 텍스트에서 특정 패턴 문자열을 효율적으로 찾기 위해 해싱(Hashing) 기법을 활용합니다.

특히, 롤링 해시(rolling hash)라는 기법을 사용하여 텍스트의 각 위치에서 패턴과의 일치 여부를 빠르게 확인할 수 있습니다.

 

라빈-카프 알고리즘의 기본 개념은 텍스트와 패턴을 비교할 때, 각 문자를 일일이 대조하는 대신,

텍스트의 부분 문자열과 패턴의 해시 값을 비교함으로써 비교 과정을 단순화하는 것입니다.

해시 값이 일치하는 경우에만 실제 문자열 비교를 수행하여, 불필요한 비교를 줄이고 속도를 높입니다.

이 과정에서 롤링 해시를 사용하여 이전에 계산된 해시 값을

바탕으로 다음 해시 값을 효율적으로 계산할 수 있습니다.

이 알고리즘의 가장 큰 의의는 문자열 검색 문제를 해싱 문제로 변환함으로써

비교적 간단한 연산을 통해 빠른 검색이 가능해졌다는 점입니다.

특히, 다중 패턴 검색에 유리하며, 특정 문자 열이나 패턴이 다수 포함된 텍스트에서

매우 효율적으로 작동할 수 있습니다. 따라서 표절 검사에 자주 사용됩니다.

 

라빈-카프 알고리의 특징


1. 해싱

라빈-카프 알고리즘의 핵심은 해시 함수를 이용하여 문자열을 숫자 값으로 변환하는 것입니다. 해시 함수는 텍스트 내에서 패턴과 동일한 길이의 부분 문자열을 빠르게 비교할 수 있도록 해줍니다. 이 과정에서, 텍스트와 패턴의 해시 값을 비교하여 일치 여부를 판단하고, 해시 값이 일치할 때만 실제 문자열 비교를 수행합니다.

2. 롤링 해시 기법

라빈-카프 알고리즘의 또 다른 중요한 특징은 롤링 해시 기법입니다. 롤링 해시는 텍스트의 각 위치에서 새로운 부분 문자열로 이동할 때, 기존 해시 값을 사용하여 새 해시 값을 빠르게 계산하는 방법입니다. 이를 통해 전체 문자열을 처음부터 끝까지 비교하는 대신, 각 문자 이동 시 해시 값을 효율적으로 업데이트할 수 있습니다.

롤링 해시를 사용함으로써, 텍스트의 길이가 길어질수록 효율성이 증가합니다. 텍스트 내의 모든 부분 문자열을 처음부터 해시 값을 재계산하지 않고, 이전 계산을 기반으로 새로운 값을 빠르게 구할 수 있기 때문입니다.

라빈-카프 알고리즘의 동작 방법


1. 초기 설정: 패턴의 해시 값 계산

  • 해시 함수 선택: 먼저, 문자열을 숫자로 변환하는 해시 함수를 정의합니다. 이 해시 함수는 주어진 문자열을 고유한 숫자 값으로 변환하며, 같은 문자열은 항상 동일한 해시 값을 가져야 합니다. 또한, 서로 다른 문자열은 가능하면 다른 해시 값을 가져야 합니다.
  • 패턴 해시 값 계산: 주어진 패턴 문자열의 해시 값을 계산합니다. 이 해시 값은 이후 텍스트의 부분 문자열들과 비교할 때 사용됩니다.

2. 텍스트의 첫 부분 문자열 해시 값 계산

  • 부분 문자열의 해시 값 계산: 텍스트에서 패턴의 길이와 동일한 첫 번째 부분 문자열을 추출하고, 이 부분 문자열의 해시 값을 계산합니다. 이 해시 값은 이후에 패턴의 해시 값과 비교됩니다.

3. 해시 값 비교 및 매칭

  • 해시 값 비교: 패턴의 해시 값과 텍스트의 부분 문자열 해시 값을 비교합니다.
    • 해시 값이 일치하지 않는 경우: 다음 부분 문자열로 이동하여 해시 값을 재계산하고 비교를 계속합니다.
    • 해시 값이 일치하는 경우: 해 시 값이 일치하더라도 실제 문자열이 다를 수 있기 때문에, 해당 부분에서 패턴과 문자열이 일치하는지 확인하기 위해 직접적인 문자열 비교를 수행합니다.

4. 롤링 해시를 사용한 다음 부분 문자열로 이동

  • 롤링 해시 계산: 텍스트에서 다음 부분 문자열로 이동할 때, 이전 해시 값을 바탕으로 새로 추가된 문자와 제거된 문자를 고려하여 해시 값을 빠르게 업데이트합니다. 
  • 해시 값 업데이트: 롤링 해시를 통해 새로 계산된 해시 값과 패턴의 해시 값을 다시 비교하고, 동일한 과정을 반복합니다.

5. 전체 텍스트에 대해 반복

  • 반복 과정: 텍스트의 마지막 부분 문자열까지 위의 과정을 반복합니다. 패턴과 일치하는 부분 문자열을 찾으면 그 위치를 기록합니다.

6. 결과 반환

  • 일치하는 위치 반환: 텍스트 내에서 패턴이 발견된 모든 위치를 반환합니다. 만약 패턴이 텍스트 내에 존재하지 않으면, 아무 위치도 반환하지 않습니다.

라빈-카프 알고리즘 구현


 

public class RabinKarp {
    // 문자열을 검색하는 메서드
    public static void search(String text, String pattern) {
        int d = 256; // 사용 가능한 문자 수 (ASCII 문자 집합)
        int q = 101; // 소수, 해시 충돌을 줄이기 위한 모듈로 사용
        int n = text.length();
        int m = pattern.length();
        int i, j;
        int p = 0; // 패턴의 해시 값
        int t = 0; // 텍스트의 현재 창의 해시 값
        int h = 1;

        // h = (d^(m-1)) % q 계산
        for (i = 0; i < m - 1; i++) {
            h = (h * d) % q;
        }

        // 패턴과 텍스트의 첫 번째 문자열의 해시 값 계산
        for (i = 0; i < m; i++) {
            p = (d * p + pattern.charAt(i)) % q;
            t = (d * t + text.charAt(i)) % q;
        }

        // 텍스트에서 패턴을 찾는 과정
        for (i = 0; i <= n - m; i++) {
            // 현재 문자열의 해시 값과 패턴의 해시 값이 일치하면
            if (p == t) {
                // 해시 값이 일치하는 경우 문자도 일치하는지 확인
                for (j = 0; j < m; j++) {
                    if (text.charAt(i + j) != pattern.charAt(j)) {
                        break;
                    }
                }

                // 패턴이 텍스트 내에 존재하는 경우
                if (j == m) {
                    System.out.println("패턴 위치: " + i);
                }
            }

            // 다음 문자열로 이동 (롤링 해시 사용)
            if (i < n - m) {
                t = (d * (t - text.charAt(i) * h) + text.charAt(i + m)) % q;

                // 음수 해시 값을 양수로 변환
                if (t < 0) {
                    t = (t + q);
                }
            }
        }
    }

    // 메인 메서드
    public static void main(String[] args) {
        String text = "ABCCDDAEFG";
        String pattern = "CDD";
        search(text, pattern);
    }
}

 

라빈-카프 알고리즘 시간복잡도


1. 평균 시간 복잡도

라빈-카프 알고리즘에서 해시 값을 계산하는 연산과 패턴을 비교하는 연산을 기준으로 시간 복잡도를 분석할 수 있습니다.

  • 해시 계산: 텍스트에서 패턴의 길이와 동일한 첫 번째 부분 문자열의 해시 값을 계산하는 데 걸리는 시간은 입니다. 여기서 m은 패턴의 길이입니다.
  • 롤링 해시: 이후 각 문자열에서 새로운 부분 문자열로 이동할 때, 롤링 해시를 사용하여 해시 값을 갱신하는 데 걸리는 시간은 O(1)입니다. 따라서 텍스트의 나머지 n−m+1개의 문자열에서 해시 값을 계산하는 데는 이 소요됩니다. 여기서은 텍스트의 길이입니다.
  • 해시 비교: 해시 값이 일치하는 경우에만 패턴과 해당 부분 문자열을 실제로 비교합니다. 해시 값이 일치하지 않는 경우, 문자열 비교 없이 바로 다음 창으로 넘어가므로 시간 복잡도에 큰 영향을 미치지 않습니다. 해시 충돌이 적은 경우 실제로 문자열 비교가 필요한 경우는 매우 드뭅니다.

따라서 평균 시간 복잡도O(n+m)입니다.

2. 최악의 경우 시간 복잡도

최악의 경우, 해시 충돌이 빈번하게 발생하면 모든 문자열에서 실제 문자열 비교를 해야 할 수 있습니다. 해시 충돌은 서로 다른 문자열이 동일한 해시 값을 가지는 경우로, 충돌이 발생하면 텍스트의 부분 문자열과 패턴을 문자 하나하나 비교해야 합니다.

  • 최악의 경우는 해시 값이 자주 충돌하여 텍스트의 각 문자열에서 패턴의 길이만큼 문자 비교가 필요하게 됩니다.
  • 이 경우 문자열 비교O(m)의 시간이 소요되며, 개의 문자열에 대해 비교해야 합니다.

따라서 최악의 경우 시간 복잡도O(n⋅m)입니다.

 

'CS > 알고리즘' 카테고리의 다른 글

[알고리즘] KMP 알고리즘  (0) 2024.09.01
[알고리즘] 다익스트라  (0) 2024.07.31
[알고리즘] 오일러 피  (0) 2024.06.26
[알고리즘] 소수 구하기  (0) 2024.06.18
[알고리즘] LCM  (0) 2024.06.13