2022-05-11 Rabin-Karp字符串查找算法,易于理解,需要一点中学数学基础

万物皆数,字符串也是数

在计算机眼里,字符是数,字符串是一串数,反正都是数。

如此一来,就真的好办了,查找字符串可以转变为查找一个特定的数。

比如“ABAC”,‘A’是65,’B‘是66,’C’是67,如果按照百进制的玩法,这个字符串就是65666567,不停的比数就行了,易于理解。

但是,有个小问题,从字符串转换为数字需要开销,如果每移动一个字符都要重新计算,那还不如暴力解法来的省时省力。同时,仅仅4个字符组成的字符串就有8位,40个字符组成的字符串岂不是要有80位,难道还要搞个高精度int类,那成本实在扛不住啊。

幸亏我们有数学,可以帮我们化解这个难题,通过取 hash 值实现为O(1)即可。

简单的数学公式,发现算法本质是数学

x i = t i R M − 1 + t i + 1 R M − 2 + … + t i + M − 1 R 0 x_{i}=t_{i}R^{M-1}+t_{i+1}R^{M-2}+…+t_{i+M-1}R^0 xi=tiRM1+ti+1RM2++ti+M1R0
我们刚才是如何得到字符串对应的数字的?

根据上面的公式,x是我们要得到的数字,t是字符值,R是进制,65666567构造方式即:
65666567 = 65 × 10 0 4 − 1 + 66 × 10 0 3 − 1 + 65 × 10 0 2 − 1 + 67 × 10 0 1 − 1 65666567 = 65 \times 100^{4-1} + 66\times 100^{3-1}+65\times 100^{2-1}+67\times 100^{1-1} 65666567=65×10041+66×10031+65×10021+67×10011
到这里应该还好,下一步可能需要琢磨琢磨:

如果比较的字符串“ABAC”向后移动一位,变成“BACB”,我们怎么变?
x i + 1 = ( x i − t i R M − 1 ) R + t i + M x_{i+1}=(x_i-t_iR^{M-1})R+t_{i+M} xi+1=(xitiRM1)R+ti+M
66656766 = ( 65666567 − 65 × 10 0 4 − 1 ) × 100 + 66 66656766=(65666567-65\times100^{4-1}) \times 100 + 66 66656766=(6566656765×10041)×100+66
有了这个公式,我们貌似可以在任意字符串长度 M 情况,以常数范围(需要一些技巧)获得字符串对应的值。

下一个要解决的问题是如果有100个字符怎么办,没有这么高精度的int,就算有,比较成本恐怕也远大于字符串比较本身。

OK,我们的老朋友hash函数来了,把一个极度分散的元素集合收集到一个有限的区间,正是我们想要的。

而且我们依然使用散列表中的哈希计算方法,除留余法,连续的对每个字符进行除大质数取得余数,迭代取得整个字符串哈希值。

    auto hash(const std::string &key, int patSize) const -> int
    {
        int hashNum = 0;
        for (int j = 0; j != patSize; ++j)
        {
            hashNum = ((hashNum << logAlphSize) + key.at(j)) % Prime;
        }
        return hashNum;
    }

重点理解一句话:

取余操作的一个基本性质是:

如果在每次算术操作之后都将结果除以大质数Q并取余,这等价于在完成了所有算术操作之后再将最后的结果除以Q并取余。

5 % 91 + 6 % 91 + 92 % 91 = (5 + 6 + 92) % 91

虽然是中学知识,但大多数人都忘了。

x i + 1 = ( x i − t i R M − 1 ) R + t i + M x_{i+1}=(x_i-t_iR^{M-1})R+t_{i+M} xi+1=(xitiRM1)R+ti+M

这个公式两边 %Q 得到的值是一样的,于是,我们可以利用前面字符串的哈希值,在常数时间内计算出移动一位后的字符串的哈希值。

h a s h ( x i + 1 ) = x i + 1   m o d   Q = [ ( x i − t i R M − 1 ) R + t i + M ]   m o d   Q hash(x_{i+1})= x_{i+1}\bmod Q = [(x_i-t_iR^{M-1})R+t_{i+M} ]\bmod Q hash(xi+1)=xi+1modQ=[(xitiRM1)R+ti+M]modQ

( x i − t i R M − 1 )   m o d   Q (x_i-t_iR^{M-1})\bmod Q (xitiRM1)modQ
提出,为
h a s h ( x i ) − t i R M − 1   m o d   Q hash(x_i)-t_iR^{M-1}\bmod Q hash(xi)tiRM1modQ
为确保得到正数,需要进行转换
( h a s h ( x i ) + Q − t i R M − 1   m o d   Q )   m o d   Q (hash(x_i)+Q-t_iR^{M-1}\bmod Q)\bmod Q (hash(xi)+QtiRM1modQ)modQ
当字母表R比较大的时候,构造如下的值可以通过迭代除 Q 留余得到,其中 M = patSize 模式字符长度。
R M − 1 R^{M-1} RM1

        RM = 1;
        for (int i = 1; i != patSize; ++i)
        {
            RM = (RM << logAlphSize) % Prime;
        }

我们分步计算:
h a s h ( t e m p ) = ( h a s h ( x i ) + Q − t i R M − 1   m o d   Q )   m o d   Q hash(temp)=(hash(x_i)+Q-t_iR^{M-1}\bmod Q)\bmod Q hash(temp)=(hash(xi)+QtiRM1modQ)modQ
h a s h ( x i + 1 ) = x i + 1   m o d   Q = [ ( x i − t i R M − 1 ) R + t i + M ]   m o d   Q = ( h a s h ( t e m p ) R + t i + M )   m o d   Q hash(x_{i+1})= x_{i+1}\bmod Q = [(x_i-t_iR^{M-1})R+t_{i+M} ]\bmod Q=(hash(temp)R+t_{i+M})\bmod Q hash(xi+1)=xi+1modQ=[(xitiRM1)R+ti+M]modQ=(hash(temp)R+ti+M)modQ
数学公式是枯燥的,让我们用个简单例子辅助思考:

十进制的两个数:58,86,查看哈希构造

        hash("58") = ((5 % 11) * 10 + (8 % 11) * 1) % 11 = 3; // 可以直接用 58 % 11 = 3
        
        hash("86") = (hash(temp) * 10 + 6) % 11;
        
        hash(temp) = (hash("58") + 11 - 5 * 10 % 11) % 11 = (3 + 11 - 6) % 11 = 8;
        
        hash("86") = (8 * 10 + 6) % 11 = 86 % 11 = 9;

上述例子只是阐述哈希构造,我知道太简单,直接求余数更简单,但如果是100位字符串呢,那可是 256^100 的数字,你能直接算么。

最后说一下大质数,我们可以设一个大质数,也可以临时计算,我比较倾向设大质数表,随机抽,实现中是写死在初始值中的。

由于字母表 R 通常是 256 或 256 * 256 ,所以有关 R 的乘法,一律实现为位运算:

        65 * 256 = 65 << 8;
        
        66 * 65536 = 66 << 16; 

为了确保匹配出错的概率极小,也就是哈希碰撞概率近乎零,我们选用 int64_t 进行计算,获取极大的质数而不溢出,这不一定可以移植,注意一下。

以下是代码的实现:

struct RabinKarp
{
    explicit RabinKarp(std::string patStr) : pat(std::move(patStr))
    {
        patSize = static_cast<int>(pat.size());
        RM = 1;
        for (int i = 1; i != patSize; ++i)
        {
            RM = (RM << logAlphaSize) % Prime;
        }
        patHash = hash(pat, patSize);
    }
    static auto check(int /*i*/) -> bool
    {
        return true;
    }
    auto search(const std::string &txt) const -> int
    {
        int txtSize = static_cast<int>(txt.size());
        int64_t txtHash = hash(txt, patSize);
        if (patHash == txtHash && check(0))
        {
            return 0;
        }
        for (int i = patSize; i != txtSize; ++i)
        {
            txtHash =
                (txtHash + Prime - RM * txt.at(i - patSize) % Prime) % Prime;
            txtHash = ((txtHash << logAlphaSize) + txt.at(i)) % Prime;
            if (patHash == txtHash)
            {
                if (check(i - patSize + 1))
                {
                    return i - patSize + 1;
                }
            }
        }
        return txtSize;
    }

  private:
    auto hash(const std::string &key, int patSize) const -> int64_t
    {
        int64_t hashNum = 0;
        for (int j = 0; j != patSize; ++j)
        {
            hashNum = ((hashNum << logAlphaSize) + key.at(j)) % Prime;
        }
        return hashNum;
    }
    // 哈希表,除留余取值,没有使用。
    auto longRandomPrime() const -> int
    {
        const std::vector<int> Prime{
            12289,    24593,     49157,     98317,     196613,    393241,
            786433,   1572869,   3145739,   6291469,   12582917,  25165843,
            50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};
        return Prime[patSize % Prime.size()];
    }
    // 模式字符串
    std::string pat;
    // 模式字符哈希值
    int64_t patHash = 0;
    // 模式字符size
    int patSize = 0;
    // 大质数,用于除余留法计算哈希值
    const int64_t Prime = 1610612741;
    // AlphaSize = 256 字母表大小
    const int logAlphaSize = 8;
    // AlphaSize ^ (patSize - 1)
    int64_t RM = 0;
};

测试用例:

inline void testRabinKarp(char *argv[])
{
    std::string pat = argv[1];
    std::string txt = argv[2];
    subStr::RabinKarp RK(pat);
    std::cout << "text:    " << txt << std::endl;
    int offset = RK.search(txt);
    std::cout << "pattern: ";
    for (int i = 0; i != offset; ++i)
    {
        std::cout << ' ';
    }
    std::cout << pat << std::endl;
}

// text:    AAAAFDDFSDFHBGHJKJVCAACCBBBABBCAABABABAC
// pattern:                           BABBCAA

你可能感兴趣的:(笔记,算法,哈希算法,散列表,Rabin-Karp指纹,字符串查找)