Rabin-Karp字符串查找算法

1.简介
  暴力字符串匹配(brute force string matching)是子串匹配算法中最基本的一种,它确实有自己的优点,比如它并不需要对文本(text)或模式串(pattern)进行预处理。然而它最大的问题就是运行速度太慢,所以在很多场合下暴力字符串匹配算法并不是那么有用。我们需要一些更快的方法来完成模式匹配的工作,然而在此之前,我们还是回过头来再看一遍暴力法匹配,以便更好地理解其他子串匹配算法。
  如下图所示,在暴力字符串匹配里,我们将文本中的每一个字符和模式串的第一个字符进行比对。一旦我们找到了一个匹配,我们就将文本中下一个字符取出来和模式串的第二个字符进行比较。

  该算法运行速度慢的主要原因有二:一方面,我们需要对文本中的每个字符都进行比对;另一方面,即使我们发现模式串首字符和文本中的某个字符相匹配, 我们仍然需要对模式串中剩下的所有符号(字符)挨个进行比对,才知道它们是不是出现在接下来的文本中。那么,是否有别的方法可以用来判断文本是否包含模式串呢?

  暴力字符串匹配代码:

public class NaiveStringMatch {
    public static int Search(String pat, String txt) {
        int M = pat.length();
        int N = txt.length();

        for (int i = 0; i <= N - M; i++) {
            int j;
            for (j = 0; j < M; j++) {
                if (txt.charAt(i + j) != pat.charAt(j))
                    break;
            }
            if (j == M)
                return i;
        }
        return -1;
    }
    public static int canBackSearch(String pat, String txt) {
        int j, M = pat.length();
        int i, N = txt.length();

        for (i = 0, j = 0; i < N & j < M; i++) {
            if (txt.charAt(i) == pat.charAt(j))
                j++;
            else {
                i -= j;
                j = 0;
            }
        }
        if (j == M)
            return i - M;
        else
            return -1;
    }
}

  答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对文本和模式串进行比较,我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。通过哈希函数,我们可以算出模式串的哈希值,然后将它和文本中的子串的哈希值进行比较。这里有一个问题,我们必须保证该哈希函数能够对一个较长的字符串返回较短的哈希值。然而,我们又不能指望较长的模式串能得到较短的哈希值。但除此之外,这个新方法在速度上应该能比暴力法有显著提升。这种更快的方法就是Rabin-Karp算法。
概述
  Michael O. Rabin和Richard M. Karp在1987年提出一个想法,即可以对模式串进行哈希运算并将其哈希值与文本中子串的哈希值进行比对。总的来说这一想法非常浅显,唯一的问题在于我们需要找到一个哈希函数 ,它需要能够对不同的字符串返回不同的哈希值。例如,该哈希函数可能会对每个字符的ASCII码进行算,但同时我们也需要仔细考虑对多语种文本的支持。
  哈希算法可以有很多种不同的形式,它可能包含ASCII码字符以便对数字进行转化,但也可能是别的形式。我们唯一需要的就是将一个字符串(模式串) 转化成为能够快速进行比较的哈希值。以"hello world"为例,假设它的哈希值hash(‘hello world‘)=12345。那么如果hash(‘he‘)=1,那么我们就可以说模式串"he"包含在文本"hello world"中。由此,我们可以每次从文本中取出长度为m(m为模式串的长度)的子串,然后将该子串进行哈希,并将其哈希值与模式串的哈希值进行比较。
实现
  算法的基本思想是:长度为M的字符串对应着一个R进制的M位数。Rabin-Karp 算法(以下简称为 RK 算法),是基于这样的思路:即把串看作是字符集长度d进制的数,由数的比较得出字符串的比较结果。例如,给定字符集为∑ ={0,1,2,3,4,5,6,7,8,9} ,∑长度为d=10 ,那么任何以∑为字符集的串都可看作d(此处为 10)进制的数。记模式串P[0..n-1]对应的数值为P,T[0..n-1]所有长度为m的子串对应的数值为ts,设P 和T都是基于字符集长度为| ∑ |=d的字符串。那么, ts即为T[s..s+m]对应的数值,这里 0<=s<=n-m-1 。
  P= P[m]+d*(P[m-1]+d*(P[m-2]+..)))

  同样 t0 也可类似求得。最重要的是如何从ts求出 ts+1 。ts+1=T[s+m]+d*(ts -dm-1 *T[s])
  注:此处是该算法的关键,即在常数时间内能够计算出下一个 m 长度的字串对应的数值。初看比较抽象,举个例子就比较明白了,设 x=12345 ,现在是已知长度为 3 的数值 234 ,现在要求 345 对应的数值,可以这样来得到: 345 = 5 + 10*(234-102 *2) 0*(234-102 *2)

import java.math.BigInteger;
import java.util.Random;

public class RabinKarp {
    private String pat;
    private long patHash;
    private int M;
    private long Q;
    private int R = 256;
    private long RM;// R^(M-1)%Q

    public RabinKarp(String pat) {
        this.pat = pat;
        this.M = pat.length();
        Q = longRandomPrime();
        RM = 1;
        for (int i = 0; i <= M - 1; i++) {
            RM = (R * RM) % Q;
        }
        patHash = hash(pat, M);
    }

    public boolean check(String txt, int i) {
        for (int j = 0; j < M; j++)
            if (pat.charAt(j) != txt.charAt(i + j))
                return false;
        return true;
    }

    private long hash(String key, int M) {
        long h = 0;
        for (int i = 0; i < M; i++) {
            h = (R * h + key.charAt(i)) % Q;
        }
        return h;
    }

    private int serach(String txt) {
        int N = txt.length();
        long texHash = hash(txt, M);
        if (patHash == texHash && check(txt, 0))
            return 0;
        for (int i = M; i < N; i++) {
            texHash = (texHash + Q - RM * txt.charAt(i - M) % Q) % Q;
            texHash = (texHash * R + txt.charAt(i)) % Q;
            if (patHash == texHash) {
                if (check(txt, i - M + 1))
                    return i - M + 1;
            }
        }
        return N;
    }

    private static long longRandomPrime() {
        BigInteger prime = BigInteger.probablePrime(31, new Random());
        return prime.longValue();
    }
}

多模式匹配
  Rabin-Karp算法非常适用于多模式匹配(multiple pattern match)。事实上,它天生就是能够支持此类的操作,这也是它相对于其他字符串查找算法的优势。
算法复杂度
  Rabin-Karp算法的复杂度是O(nm),其中n和m分别是文本和模式串的长度。那么它到底比暴力匹配好在哪儿呢?暴力匹配法的算法复杂度同样是O(nm),这样看起来Rabin-Karp算法在性能上并没有多大提升。然后在实际使用过程中,Rabin-Karp的复杂度通常被认为是 O(n+m)。这就使得它比暴力匹配法要快一些,具体见下图。
  需要注意的是Rabin-Karp算法需要O(m)的预处理时间。
  译者注:事实上,由于哈希函数无法保证对不同的字符串产生不同的哈希值,有哈希冲突的现象存在,所以即使模 式串的哈希值和文本子串的哈希值相等,也需要对这两个长度为m的字符串进行额外的比对(当然,如果不相等也就不用比对了,其实大部分的时间省在这上面), 这时比对的开销是O(m)。最坏情况下,文本中所有长度为m的子串(一共n-m+1个)都和模式串匹配,所以算法复杂度为O((n-m+1)m)。然而实 际情况下,需要进一步比对的子串个数总是有限的(假设为c个),那么算法的期望匹配时间就变成O((n-m+1)+cm)=O(n+m)。
应用
  我们已经看到Rabin-Karp算法比暴力匹配法其实也快不了太多,那它的应用场景到底是哪里?
  译者注:原文没有给出具体答案。要回答这个问题,需要先了解Rabin-Karp算法被称道和诟病的原因。然后根据自己的具体应用需要来做判断。
  Rabin-Karp算法被称道的三个原因

  • 它可以用来检测抄袭,因为它能够处理多模式匹配
  • 虽然在理论上并不比暴力匹配法更优,但在实际应用中它的复杂度仅为O(n+m);
  • 如果能够选择一个好的哈希函数,它的效率将会很高,而且也易于实现。

  Rabin-Karp算法被诟病的两个原因

  1. 有许多字符串匹配算法的复杂度小于O(n+m);
  2. 有时候它和暴力匹配法一样慢,并且它需要额外空间。
时间: 2024-10-12 17:26:38

Rabin-Karp字符串查找算法的相关文章

字符串查找算法-KMP

/** *    KMP algorithm is a famous way to find a substring from a text. To understand its' capacity, we should acquaint onself with the normal algorithm. */ /** *    simple algorithm * *    workflow: *        (say,  @ct means for currently position o

Rabin-Karp指纹字符串查找算法

首先计算模式字符串的散列函数, 如果找到一个和模式字符串散列值相同的子字符串, 那么继续验证两者是否匹配. 这个过程等价于将模式保存在一个散列表中, 然后在文本中的所有子字符串查找. 但不需要为散列表预留任何空间, 因为它只有一个元素. 基本思想 长度为M的字符串对应着一个R进制的M位数, 为了用一张大小为Q的散列表来保存这种类型的键, 需要一个能够将R进制的M位数转化为一个0到Q-1之间的int值散列函数, 这里可以用除留取余法. 举个例子, 需要在文本 3 1 4 1 5 9 2 6 5 3

KMP字符串查找算法

#include <iostream> #include <windows.h> using namespace std; void get_next(char *str,int *num) { int idFront = 0; int len = strlen(str); int amount = 1; int flag = 0;//相等时一直往下循环 int flag2 = 0;//标记是否在循环过程中不匹配,如果在循环过程中不匹配,则要防止跳过这个数 for(int i =

字符串查找算法

#include<iostream> using namespace std; int BFMatch(char* s,char* p) { int i=0; int j=0; while(i<strlen(s)) { while(s[i]==p[j]&&j<strlen(p)) { j++; i++; } if(j==strlen(p)) return i-strlen(p); i=i-j+1; } return -1; } int main() { char*

字符串查找与匹配之BM算法

一.字符串查找:1.在Word. IntelliJ IDEA.Codeblocks等编辑器中都有字符串查找功能.2.字符串查找算法是一种搜索算法,目的是在一个长的字符串中找出是否包含某个子字符串. 二.字符串匹配:1.一个字符串是一个定义在有限字母表上的字符序列.例如,ATCTAGAGA是字母表 E ={A,C,G,T}上的一个字符串.2.字符串匹配算法就是在一个大的字符串T中搜索某个字符串P的所有出现位置.其中,T称为文本,P称为模式,T和P都定义在同一个字母表E上.3.字符串匹配的应用包括信

暴力子字符串查找

子字符串查找:给定一段长度为N的文本和一个长度为M的模式字符串,在文本中找到一个和该模式相符的子字符串 广泛使用的暴力算法,虽然在最坏情况下的运行时间与M*N成正比,但是在实际中,绝大多数比较在比较第一个字符时就会产生不匹配,它实际运行时间一般与M+N成正比 下面是暴力子字符串查找算法的Java实现: /** * 暴力字符串查找,如果找到,返回pat在txt中第一次出现的位置:没有找到则返回N的值 * @param txt * @param pat * @return */ public int

字符串查找与匹配算法

一.字符串查找:1.在Word. IntelliJ IDEA.Codeblocks等编辑器中都有字符串查找功能.2.字符串查找算法是一种搜索算法,目的是在一个长的字符串中找出是否包含某个子字符串. 二.字符串匹配:1.一个字符串是一个定义在有限字母表上的字符序列.例如,ATCTAGAGA是字母表 E ={A,C,G,T}上的一个字符串.2.字符串匹配算法就是在一个大的字符串T中搜索某个字符串P的所有出现位置.其中,T称为文本,P称为模式,T和P都定义在同一个字母表E上.3.字符串匹配的应用包括信

Rabin Karp 算法实战

关键字 Rabin karp 算法, C++, ubuntu 14.04, linux, big integer, gmp 为了计算冗余度, 我写出了如下算法 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

c数据结构的字符串查找的Brute-Force算法

#include<stdio.h> #include<malloc.h> #include<string.h> //定义字符串的结构体 typedef struct { char *str;//字符串 int maxLength;//最大可以存放字符的长度 int length;//目前的字符长度 }DString; //1.初始化操作 //初始化操作用来建立和存储串的动态数组空间以及给相关的数据域赋值 void Initiate(DString *s,int max,