数据结构与算法基础四:字符串与KMP算法

一:串

串就是字符有限序列,即字符串.
1.字符串比较大小
两个字符串s1(a1a2a3..an)和s2(b1b2b3...bm),当满足下面两个条件之一时,s1<s2;

  • n<m并且a1=b1,a2=b2...(也就是s1是s2的字串的时候)
  • 存在k<=min(n,m),使a1=b1,a2=b2...ak-1=bk-1,ak<bk(也就是前面都一样,遇到了小于s2的字符时,s1更小)

3.串的顺序存储
字符串与线性表很相似.但是线性表关注单个元素的查找插入删除,字符串出了这些,还得关注字串的操作.
自然也是存在数组中,有时候会在下标0的位置存上串的长度,也有放最后的,也有的像C语言那样,在最后一个字符后面存一个"\0",这个是ASCII码的一个字符,表示串到这里结束.

4.串的链式存储
字符串使用链式存储在各个方面上都不如顺序存储,经常被存在静态区,不会动态的管理,也有特殊的结构会使用链式存储,每个节点可以存1个字符,也可以存多个字符,具体看情况.

5.朴素模式匹配
在一个字符串中搜索指定字串的位置,叫做模式匹配.
最简单的匹配思路,就是从头开始对比,遇到不一样的,就把整个字串往后移动一位,重新对比.

d和g匹配不上

向后移

算法

接上图

二:KMP模式匹配

总结自此文
首先定义一些概念,文本字符串S,模式字符串P,S中字符的下标为i,P中字符的下标为j.
1.最大公共元素长度
对于一个模式串ABCDABD,从左到右一位一位增加,有7种子串,A;AB;ABC...
引入两个概念叫前缀和后缀,A只有一位,没有前缀后缀;AB前缀是A,后缀是B;ABC前缀是A,AB,后缀是C,BC;ABCD的前缀是A,AB,ABC以此类推
再引入前缀后缀的最大重复长度的概念,A;AB;ABC;ABCD都没有重复,ABCDA重复长度是1;ABCDAB重复度是2;ABCDABD重复长度是0,因为前缀是A后缀是D.

举例

最大长度表

2.使用最大长度表
使用方法:失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值.

案例

i的前4位都是第一个就匹配不上,因此都是右移一位.
右移到第一个字符匹配上

当S[i]=p[j]的时候,i和j都增加,也就是对比下一字符是否相同.
遇到失配

现在遇到第7位字符失配,那么如果是根据朴素模式匹配,则是模式串P右移一位,实际上能看出来一位肯定是没用的;
此时P应该向右移动6-2=4,因为已经匹配上了6位,这6位的最大长度是2.
移动4位

又匹配两位之后,再次适配,再移动2-0=2位.
移动2位

然后发现第一位就匹配不上,继续移动.
继续上面的流程

需要移动6-2=4位.
匹配成功

3.引入next数组
下面是刚才的最大长度表,刚才说到应用方法是(已匹配字符数 - 失配字符的上一位字符所对应的最大长度值)

最大长度表

也就是每次都得看j上一位的的最大长度,那为了对齐下标,干脆把这个表往右移动一位,形成数组,之后根据j去找next[j]就是需要右移的位数,这就是next数组.
右移之后把next数组的第一位填充为-1.
image.png

现在,使用方法是:失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值.

// 计算 arr[] 的 next 数组
public static int[] getNextArray(char[] arr) {
    if (arr.length == 1) {
        return new int[]{-1};
    }
    int[] next = new int[arr.length];
 
    // 根据定义初始化next数组,0位置为-1,1位置为0.
    next[0] = -1;
    next[1] = 0;
    int pos = 2;    // 当前位置
    int cn = 0;     // 当前位置前一个字符的 next[] 值(最长相等前后缀的长度)
    while (pos < next.length) {
        if (arr[pos - 1] == arr[cn]) {
            // 当字符串的 pos-1 位置与 pos-1 位置字符所对应的最长相同前后缀的下一个字符 arr[next[pos-1]] 相等时
            // 我们就能确定 next[pos] 的值为 pos-1 位置所对应的 next[pos-1] + 1,即 ++cn.
            next[pos++] = ++cn;
        } else if (cn > 0) {
            // 当着两个字符 不相等 时,cn向前跳跃到 next[cn] 的位置,去寻找长度更短的相同前后缀。
            cn = next[cn];
        } else {
            // cn<=0; 此时说明前面已经没有相同前后缀了,即 cn 已经没办法再跳跃了,
            // 此时 pos 对应的 next[pos] 值为 0 (无相同前后缀)
            next[pos++] = 0;
        }
    }
 
    return next;
}

上面是获取一个模式串的next数组的demo.

4.优化KMP算法
next数组仍然可以优化;
比如下面这个例子,得到next数组是-1,0,0,1;因此右移j-next[j] = 3-1=2位.

图一

移动2位之后,这时可以看到j=1时失配,需要移动j-next[j] = 1-0=1位,但是看一眼发现移动一位也还是一样的失配,那这一步岂不是白走了,因此这里可以优化.
右移2位

问题在于出现了p[j]=p[next[j]],也就是图一里的情形.
当p[j]!=s[i]时,p会右移,移动之后一定是从p[next[j]]和s[i]开始比较.从上面两张图可以就看出来,这也是KMP的核心.
因此当p[j]=p[next[j]]时,下面这一步就白走了,必然是适配的.

解决方法就是当p[j]=p[next[j]]时,不再移动next[j]位,而是next[next[j]]


新旧next数组对比
使用新的next数组

使用新的next数组,next[3]=0,移动3位.


image.png

next[0]=-1,i++,j++.


匹配成功

修改一下获取next数组的函数

public static int[] next(String p) {
        int[] next = new int[p.length()];
        int k = -1, j = 0;
        next[0] = -1;        //    初值为-1    
        
        while(j < p.length() - 1) { 
            //    p[k]表示字符串的前缀,p[j]表示字符串的后缀
            if(k == -1 || p.charAt(k) == p.charAt(j)) {  // 判断的先后顺序不能调换
                k++;
                j++;
                //    后面即是求next[j+1]的过程
                if(p.charAt(k) == p.charAt(j))             //  此处等价于if(p[j] == p[ next[j] ])
                    //    因为不能出现p[j] = p[ next[j] ],所以当出现时需要继续递归,k = next[k] = next[next[k]]
                    next[j] = next[k];                    //  此处等价于next[j] = next[ next[j] ]
                else    
                    next[j] = k;
            }
            else {
                k = next[k];         
            }
        }
    
        return next;
    }

贴出KMP完整的代码,是从原文复制的,具体看前面贴出来的地址,讲的非常详细.

public class KMP {
 
    public static void main(String[] args) {
        String s = "abacababc";
        String p = "abab";
        System.out.println(Index_KMP(s, p));
    }
    
    //优化过后的next数组求法 
    public static int[] next(String p) {
        int[] next = new int[p.length()];
        int k = -1, j = 0;
        next[0] = -1;        //    初值为-1    
        
        while(j < p.length() - 1) { 
            //    p[k]表示字符串的前缀,p[j]表示字符串的后缀
            if(k == -1 || p.charAt(k) == p.charAt(j)) {  // 判断的先后顺序不能调换
                k++;
                j++;
                //    后面即是求next[j+1]的过程
                if(p.charAt(k) == p.charAt(j))             //  此处等价于if(p[j] == p[ next[j] ])
                    //    因为不能出现p[j] = p[ next[j] ],所以当出现时需要继续递归,k = next[k] = next[next[k]]
                    next[j] = next[k];                    //  此处等价于next[j] = next[ next[j] ]
                else    
                    next[j] = k;
            }
            else {
                k = next[k];         
            }
        }
    
        return next;
    }
    
    public static int Index_KMP(String S, String P) {
        int i = 0, j = 0;
        int[] next = next(P);
        
        while(i < S.length() && j < P.length()) {      
            if(j == -1 || S.charAt(i) == P.charAt(j)) {        //    如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++. 注意:这里判断顺序不能调换! 
                i++;
                j++;
            }
            else
                //    如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
                //    next[j]即为j所对应的next值,效果为进行回溯        
                j = next[j];
        }
        
        if(j == P.length())
            return i - j;
        else 
            return -1;
    }
    
}

5.KMP算法的时间复杂度
因为不像朴素模式那样i会回溯,因此S这一层是n的复杂度,另外求next数组可以从代码上看出来,是另一个规模参数m的,因此整体复杂度是O(n+m).

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

推荐阅读更多精彩内容