20-字符串匹配

字符串匹配

这章节,我们会讲到几大典型的字符串匹配算法

1. BF算法
2. RK算法
3. Sunday算法
4. KMP算法(后续尽力更新)

BF算法

BF算法是最最符合正常人逻辑思维的一种匹配模式,它就是采取穷举的方式,一步步进行主串与模式串的比较,匹配成功返回主串匹配开始的位置。

接下来我们一起通过例子来看看这种让人喜欢用的算法吧,假设现在我们有主串abcdefg,待匹配的模式串efg,我们来找出模式串在主串出现的位置。

准备工作

表格1是主串在数组的存储格式,表格2是模式串在数组的存储格式,我们设置两个游标i,j
其中i游标始终指向主串,j游标指向模式串。

a e f d e f g
0 1 2 3 4 5 6
e f g
0 1 2
  1. 首先i和j游标每次指向开始待匹配的字符,第一次i指向了a,j指向了e,结果开始就发现不匹配。继续下一次
string1.jpg

2.上次匹配失败,所以模式串的j游标回退到起点e,主串的i向前进1位来到了下标是1的位置。如图所示,此时我们发现主串的i游标和模式串的j游标指向的相等,所以i和j同时前进,此时发现i和j游标指向的又相等,两个继续前进,i指向了d,j指向了给g,所以匹配失败。

string2.jpg

3.上次匹配失败,所以模式串的j游标回退到起点e,主串的i向前进1位来到了下标是2的位置。如图所示,此时我们继续匹配,发现游标i和游标j开始指向的都不匹配,匹配失败

string3.jpg

4.上次匹配失败,所以模式串的j游标回退到起点e,主串的i向前进1位来到了下标是3的位置。如图所示,此时发现游标i和游标j开始指向的都不匹配,匹配失败

string5.jpg

5.上次匹配失败,所以模式串的j游标回退到起点e,主串的i向前进1位来到了下标是4的位置。如图所示,此时我们的游标i指向了e,j也指向了e,二者继续前进,又相等,继续前进,又相等,此时模式串匹配成功,返回i开始指向的位置4.

string4.jpg

上面就是完成的一个BF匹配过程,看起来还是比较容易理解的算法,我们发现BF算法每次都会从主串遍历匹配,而模式串一旦失败,又得回退到起点。所以实践复杂度就是0(m*n),m是主串的长度,n是模式串的长度。

代码

int String::BF()
{
    int i, j, index,begin,l2;
    if (this->s1.length() < s2.length())
    {
        return -1;
    }
    //开始BF算法
    for (i = 0; i < s1.length(); i++)
    {
        begin = i;//主串起点位置
        j = 0;//模式串起点位置
        while (s1[begin] == s2[j])
        {
            begin++;
            j++;
            if (j == s2.length())
            {
                break;
            }
        }
        if (j == s2.length())
        {
            return i;
        }
        else
        {
            begin++;
        }
    }
    return -1;
}

RK算法

RK匹配算法是对BF算法的升级,它将匹配串的思想了转换成了数值的简单比较,相比较其它类匹配算法,这个算法可谓是真牛逼啊。那么快来欣赏下这位大神之作吧。

对比BF算法,我们看看RK算法如何将串转化成了数值,在BF算法中,假设现在有主串长度是len1,有待匹配的模式串长度是len2,现在我们通过某种计算方式计算出模式串的哈希值,通过比较完善的哈希函数求出对应哈希值,前面的BF算法,我们知道主串有len1-len2+1个子串(看下图),我们依次对这些子串求出对应哈希值。假设子串长度是3,下图显示的是主串的len-len2+1个子串的哈希值.

string6.jpg

然后我们只需要逐个的将这len1-len2+1个子串与模式串的哈希值进行比较,每个串对应自己的哈希值,所以每次不用移动这移动那,回退啥的,直接数值比较,相等的表示该位置与模式串暂时匹配好了,但是如果不行等,肯定表示这个子串与模式串不匹配啊。好了,此时应该有人要问了,啥叫暂时匹配好了,嗯嗯嗯,你的疑问是正确的,这个我们就要追溯到哈希表那块了,上面我们讲过,每个子串和模式串都通过某种哈希函数计算对应的哈希值,可以别忘了,哈希函数的设计必然产生哈希冲突(忘了的,去看我的哈希表那一章),只是你设计的哈希函数可以让这个冲突减少,但是不可避免。所以啊,当子串的哈希值和模式串的哈希值一样时,我们还得进行二次检测,就是逐个的比较模式串和当前子串,看它们是否相等,如果相等,匹配成功,反之则失败了。

哈希函数的选择和哈希值计算

好了,是时侯讨论下此处匹配算法的哈希函数的选择了,此处的哈希值计算方式是按照进制数来算的,根据你待匹配串的标准来选择,假设现在你的串只包含有0-9这10个字符,那对应的就是10进制数字,假设你的只包含a-z这26个字母,那对应的就是26进制数字,然后不管哪种,都最后将它们转化成10进制数字,如下图,分别是只包含0-9和只包含a-z字符的哈希值算法。

string7.jpg

好了,上面就是每个串哈希值的计算,哦,我忘记了两个重要的哈希值计算问题。

1.哈希值过大问题

因为上面的进制如果很大,如果串很长,假设是26进制的,后面会是26的串长度-1的幂次方,那么这个数字很大很大,这时long long都保存不下了,所以,我们必须对每次求得哈希值都做取余数处理,至于这个余数是多少?当时我网上查到了外国两哥们说设计人员建议是一个质数,且尽可能大一些,假设你设计的小,那么每个串对应哈希值冲突的概率极大提升,那此时该算法的时间复杂度就和BF没区别了。

2.主串中每个子串的哈希值计算

看了上面的哈希值计算,许多人可能会感到很烦,如果去写代码,此处就是最大的痛点,每个子串的哈希值计算这么长,咋写么。嗯嗯嗯,其实它是有规律的,好了,我不啰嗦了直接看下面图中的推导。

现在我们准备一个主串和一个模式串如下图,主串是"yzadbe",模式串是"dbe",假设我们已经计算出了待匹配的模式串的哈希值是data4

string10.jpg

所以说,我们只需要完整的计算出第一个子串的哈希值,后面字串的哈希值都可以由上一层推导来。

讲解完上面所有的理论后,我们就可以开始模拟整个匹配过程了,其实很简单,就是准备的工作多,首先下图是准备的主串和模式串,并且我们已经计算出模式串的哈希值了。

string8.jpg
  1. 将主串每个对应的子串的哈希值都计算出来,如下图。
string9.jpg

2.逐个的将这些子串的哈希值与模式串的哈希值data4比较,发现主串的子串"dbe"和模式串的哈希值相等,然后可以结束了?no no no,此处还没结束,现在你还不知道是不是真正的匹配了,接着你需要逐个检测子串"dbe"和模式串"dbe"是否匹配,发现匹配着,这时才表示真正匹配成功,返回下标。

好了RK算法就讲完了,我们分析下它的时间复杂度,计算每个子串的哈希值时间复杂度是o(n-m+1),接着挨个对比是也是0(n-m+1),加起来就是o(n)时间复杂度,当然前提是你设计的哈希函数到位。

代码

int String::RK()
{
    //准备一个素数和一个进制数(进制数按照你的字符串范围进行)
    int q = 144451,d = 26;//假设我的字符串全是a-z组成,范围是26,进制用26进制。
    int main_Str = s1[0] - 'a';//主串第一个字符下标a就是0,b就是2
    int pat_Str = s2[0] - 'a';//模式串第一个字符下标
    int h = 1,begin,j,i;//26的多少幂次方
    for (i = 1; i < s2.length(); i++)
    {
        main_Str = (main_Str * 26 + s1[i] - 'a') % q;
        pat_Str = (pat_Str * 26 + s2[i] - 'a')% q;
        h = h*d;
    }
    //开始逐个串的比较
    for (i = 0; i < s1.length() - s2.length() + 1; i++)
    {
        //先判断code值是否相等
        if (main_Str == pat_Str)
        {
            begin = i;//主串开始位置
            j = 0;
            //继续逐个字符的判断
            while (s1[begin]==s2[j])
            {
                begin++;
                j++;
                if (j == s2.length())
                {
                    return i;//匹配ok
                }
            }
        }
        //code不等或者匹配失败,就更新main_Str的值是下一个
        //code更新办法是减去最高位的进制,剩余的数字*26 +最新进来的数字
        main_Str = (   (main_Str - h*(s1[i]-'a')) * d    +    s1[i + s2.length()] - 'a'   )%q;
    }
    return -1;
}

Sunday算法

Sunday算法可以说是最快的男人了,不仅快,还被程序界视为最好用,最时尚,最好理解的,我猜测,他们肯定和我一样,被KMP匹配算法折磨透了,我现在还没会啊。此算法是90年代被发明的,距离现在也就不到30年,这个算法也是牛啊,我就不吹了,还是看它的牛逼之处吧。

抛开RK算法不说,相对于其它类匹配算法,它们都是对模式串和主串比较中,每次主串移动位数是1,虽然KMP也可以跳着匹配,但是难的不是一点点啊,维护起来够你受的。所以真正好用的就是今天的主角了,Sunday算法,它实现了真正意义上的跳跃式匹配,让我们一起见识见识它的威力。

这一块,我尝试直接画图举例子分析,然后总结出规律。我太难了

  1. 准备我们的主串"abcadcdzp"和模式串"dzp",如下图
string11.jpg

2.游标i指向主串开始位置,游标j指向模式串开始位置,第一次匹配开始,游标i指向的a和游标j指向的d不匹配,然后,主串立马去找它的末尾,这个末尾是指相对于模式串的后面一个,就是主串下标是3的位置找到了a,接着游标j开始从模式串的头遍历到尾,看模式串是否有这个元素a,发现没有,接着主串游标i就直接跳到了下标是3的下一个位置,就是下标是4的位置,模式串的游标j回退到模式串起点,等待下一次匹配。结果如下图

string12.jpg

3.主串与模式串游标继续匹配比较,发现游标i对应的和游标j对应的相等,所以i和j同时前进,此时i指向了下标是5的位置,此时又发现不对应了,完了,主串又生气了,直接去找相对于模式串的末尾的下一个位置,就是下标是7的位置,此元素是z,然后,让游标j从头开始找有没有一个等于z的值,j在模式串第二个位置找到了这个值,然后啊,主串的游标i从当前位置4跳到了6的位置,跳动长度=(模式串末尾位置2-模式串找到该值的位置1)+1,就是数字2,所以从游标i从4直接跳到了6上,最终结果如下图。

string13.jpg

4.此时游标i和j继续匹配,发现i和j指向的相等,同时向前进1,又相等,继续前进,又相等,模式串结束了,所以匹配成功。

好了,上面就是一个完整的Sunday算法匹配过程,你可能觉的好复杂啊,可能是我讲的不好,我的原因,其实我这个例子讲这么长,因为这个例子把Sunday算法所有跳转情况都用到了,为了给您分析一个面面俱到的例子嘛。

有了上面的分析过程,我来给您总结下Sunday算法的过程,您根据这个过程再去看上面的例子,就一定恍然大雾了,哦写错了,是恍然大悟了,千万别雾啊。

  1. 主串和模式串左边对齐,逐个的比较
  2. 当发现某个位置不匹配时,主串去查看当前对齐后未参与匹配的位置index1的值(相对于模式串最后位置的下一个),然后模式串去从头到尾遍历,看自己是否存在这个值
    • 如果不存在这个值,好,主串直接从index1+1位置开始于模式串进行新一轮匹配
    • 如果存在这个值,这里需要注意,存在的话,还得向后继续找,找模式串最后一次出现这个值的位置index2,计算出模式串最后一个元素位置-index2+1的值,这个值就是
      主串直接从开始匹配的位置跳过的长度到下一个待匹配的位置。
  3. 循环执行步骤1和2,直到匹配结束。

好了,总结完成。哦,忘记了一个问题,总结里面有句话,匹配不成功时,主串拿末尾未参与匹配的值,模式串从头找这个值,当模式串找到了这个值,还得继续找后面有没有这个值,最后返回的是最后一次出现这个值的位置。这句话是重点,我必须解释下,为什么要选择最后一次出现这个值的位置?当模式串存在这个值时,主串最后跳的距离是这个值到模式串末尾距离再加1的长度,所以如果,你选择靠前面出现的,那跳到这个距离就会变大,有可能错过了第一次就匹配上的串,是不是这个道理啊。

接下来,我们用这个算法再来模拟下开始BF算法解决的那个案例。

  1. 游标i和j分别指向主串和模式串的开头位置
string1.jpg

2.发现第一个位置就不匹配,所以主串直接去相对于模式串的尾部的下一个位置就是下标是3的位置找到元素d,然后模式串从头到尾遍历,看是否有这个值,发现模式串无该值,所以,主串直接跳到下标是4的位置开始和模式串匹配了。如下图

string14.jpg

3.继续进行,发现匹配成功了,是不是很牛逼。之所以展示这个,就是给你说这个算法跳的有多快。

代码

//Sunday算法
int String::Sunday()
{
    int i, j,  temp, index[100] = {0};
    char ch;
    //记录模式串每个字符最后出现的位置(相对于模式串)
    for (i = 0; i < s2.length(); i++)
    {
        j = i;
        index[i] = j;
        while (j != s2.length())
        {
            if (s2[j] == s2[i])
            {
                index[i] = j;
            }
            j++;
        }
        cout << i<<" "<<index[i] << "\n";
    }
    /*
    1.每次先在主串里和模式串逐个字符匹配
    2.当遇到不匹配时,立马去主串对应下一个起点处找一个字符位置是index
    3.此时将该字符在模式串里面从头到尾对应的找
    4.如果没找到,就将主串的游标直接移动到index+1,模式串回归到0
    5.如果找到了,先确定模式串找到的位置x,然后查看此元素在模式串最后出现位置y,让主串的游标直接加上模式串长度-最后位置
    */


    
    for (i = 0; i < s1.length(); )
    {
        temp = i;//temp主串游标
        j = 0;//j模式串游标
        while (s1[temp]==s2[j])
        {
            temp++;
            j++;
            if (j == s2.length())
            {
                return i;
            }
        }
        //此时主串下标temp,模式串下标j不匹配了,来到主串下一个起点ch
        ch = s1[i + s2.length()];
        //在模式串,从头找,是否存在与ch相等的值
        j = 0;
        while (s2[j]!=ch)
        {
            j++;
            if (j == s2.length())
            {
                break;
            }
        }
        //判断是否找到了这个值ch
        if (j == s2.length())//没有找到
        {
            i += s2.length() + 1;
        }
        else//找到了
        {
            i += s2.length() - index[j];
        }
    }
    return -1;
}

获取完整代码

我分别用C,C++,JAVA三种主流语言编写了完整代码,请大家指导批正,一起学习。

点击查看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358