串的模式匹配算法

来源:互联网 发布:查询qqip地址软件 编辑:程序博客网 时间:2024/06/06 08:44

串的模式匹配算法又称为字串的定位,是指在主串中找给定字串。规范地说:

int Index(String S, String T, int pos);

初始条件:串S、T存在,T非空,1≤pos≤S.length。

操作结果:若主串S中存在与字串T相等的字符串,则返回T在S中pos位置后第一次出现的位置,否则返回0。


串的朴素模式匹配算法。

串的模式匹配有很多算法,最简单的朴素算法就是简单地遍历,利用两个游标遍历两字符串,匹配的话两个游标同时后移,不匹配的话一个回到起点,一个回到之前遍历起点的下一个位置。即:

<span style="font-size:14px;">int Index(SString S, SString T, int pos){    int s_len = S[0];                   //S[0]为主串的长度        if (pos > s_len || pos <= 0) {      //如果位置不对则返回错误信息        return -1;    }        int i = pos, j = 1;                 //i从给定位置开始找起,j从字串的头部开始    while (i <= S[0] && j <= T[0]) {    //当i和j都没有走到字符串尾部时        if (S[i] == T[j]) {             //如果相等则说明当前位置是匹配的,i和j同时后移            i++;            j++;        } else {                        //否则j回到字串头部,i回到本次检查初始位置的下一个位置继续比较(低效的回溯)            i = i - j + 2;            j = 1;        }    }        if (j > T[0]) {                     //如果j大于字串的长度,说明找到了,返回起始位置即i当前下标减去字串的长度        return i - T[0];    } else {                            //没找到        return 0;    }}</span>

正如注释中说的,最主要的部分在于else块中的回溯。如果当前位置没有匹配,这种算法将无视之前的比较结果,回头重新比较。那么先看一下时间复杂度。

较好情况(最好情况一开始就成功,O(1)就不提了):如下图


此时要么首字母就不匹配,要么整体匹配。此时时间复杂度易得为O(S.length + T.length)。

最坏情况:试想如果每次都在最后一位不同,那么


每次都要走一遍T.length长度,一共走(S.length - T.length + 1)次(原因:最后一次匹配是成功的,所以S串中后T.length个长度是不用走的,这个有种小时候的益智题蜗牛爬井,朝进暮退的感觉)。所以时间复杂度为O(T.length * (S.length - T.length + 1))。即乘法级别的。

然而,实际上遇到的更多地是下面的情况:


考虑这样比较,第一次在最后一此比较时失败,则按照朴素算法的思想,T整体后移一位继续比较。然而直观地看就知道这样是没有意义的,因为bcd已经在本轮比较中确定匹配了,而且他们本身又不相等,所以后移一位必定没有意义。


造成朴素算法低效的原因正是因为回溯。


KMP模式匹配算法

谈到对之前结果的运用,大家一定能想到动态规划。其实KMP正是这种思想,以达到不回溯的效果。还是上面的情况,既然后移一位没有必要,那么到底要后移几位呢?

刚才在证明比较没有必要的时候,我们谈到,因为T串中a与bcd不相等,而bcd又已经匹配过了,所以没有必要。也就是说,下一次比较后移的位数next关键在于字串T中字符的构造。也就是T串中字符的重复性。

考虑T = “abcabd"假设前五位已经匹配上,而d没有匹配,那么下一次应该移动几位呢?

凭我们的直觉来看,abc已经匹配并且互不相等了,而后面ab与前面ab一样,不能简单跳过,所以T串应该往后移动到后一个a的位置(略拗口。。),也就是后移3位

现在来结合T串的构造看看


既然是在d上失配的,那么将d拿掉,可以看到剩余串abcab中前缀ab和后缀ab相等,长度为2。所以要后移三位。也就是说,移动的位数为相等的最大的前后缀的位数+1。

把T串的各个位置失配情况下移动的位数定义为一个数组next,则next的长度为T的长度,并且next的值为:


这是书上的“官话”,我来给那些和我一样看到数学式就头疼的人解释一下。

第一个元素因为除去它以外就没有串了,所以特殊处理,取0.

中间的那个“。。。” = “。。。”其实就是我们说的去掉当前字符以后剩余字符串的最大前缀和后缀的长度 + 1(最大长度为k - 1,而取的是max(k))。

最后,如果上述集合为空,即没有相等的最大前后缀,则取1.

例如,T = “abcabd”

1)对a,满足条件1,next[1] = 0

2)对b,它的前面的字符串为“a”,满足条件三,next[2] = 1

3)对c,字符串为“ab”,满足条件三,next[3] = 1

4)对a同上

5)对b,字符串为"abca",此时最大相等的前后缀为a,长度为1,所以next[5] = 2

6)同上,最大相等的前后缀为"ab",长度为2,所以next[6] = 3


代码经过n多人优化后已经做到了很简:

void GetNext(SString T, int *next){    int i, j;    i = 1;    j = 0;        next[1] = 0;                    //case 1    while (i < T[0]) {              //next数组的长度为T.length,即T[0]                if (j == 0 || T[i] == T[j]) {            //j = 0                 指case3            //j通过之后的j=next[j]可以回溯到一个比较小的数,代表前缀            //i通过不断自增,代表后缀            //i与j的比较代表寻找最大前后缀的长度            //每次找到后将长度+1(就是j)记录到next[i]中,这里涵盖了case2和case3                        i++;            j++;            next[i] = j;        } else {            j = next[j];            //最复杂的地方        }    }}

最难理解的地方就是else块的回溯。这里用的是动态规划的思想,如果把整体的流程写一下就会发现每次比较都是在上一次比较的基础上进行的。

下面以T = “ababaaaba”的next计算流程来说明。


比较过程中可以发现while中每次之比较一个字符,但是已经保证了前面的字符都相等,而且不等的时候j通过回溯回到了合适的位置。至于j为什么能通过next[j]回溯,当前前后缀的相等性已经在j处断掉了,如果还想找相等的前后缀,第一、只能更短,第二、只能在之前的一大块前后缀中找包括j的子块,即通过next[j]找前一位的最大相等前后缀,然后在此基础上进行查找。


有了next数组,KMP算法还是比较容易实现的。

<span style="font-size:14px;">int Inext_KMP(SString S, SString T, int pos){    int i = pos;    int j = 1;    int next[255];        GetNext(T, next);           //得到next数组        while (i <= S[0] && j <= T[0]) {        if (j == 0 || S[i] == T[j]) {   //同朴素算法,只不过要考虑到j = 0的情况,即j被回溯到0得情况下            i++;            j++;        } else {            j = next[j];        //j回到合适的位置        }    }        if (j > T[0]) {        return i - T[0];    } else {        return 0;    }}</span>

实际上,KMP的优势只有在字串中存在“部分匹配”时才能体现出来。否则可能与朴素算法差不多。

0 0
原创粉丝点击