【LeetCode分享】KMP算法

1. 题目描述 28. 实现 strStr()

实现 strStr()函数。

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

说明:

needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的strstr()以及 Java 的indexOf()定义相符。

示例 1:
输入:haystack = "hello", needle = "ll"
输出:2

示例 2:
输入:haystack = "aaaaa", needle = "bba"
输出:-1

示例 3:
输入:haystack = "", needle = ""
输出:0

2. 思路1 暴力求解

暴力求解思路比较简单,即在模式串和主串进行匹配的过程中,如果发现某个字符不匹配,则模式串从头开始,主串的指针回退到上一次开始位置的下一个位置,直到主串遍历完毕,或者提前匹配成功。

遍历主串所用的时间复杂度是O(n),而每次从一个字符开始的时候,都需要依次和模式串的每个字符进行比较,故总体时间复杂度为O(n*m),但是通常m << n所以该方法的时间复杂度其实也不是很高,因而效率依然还是不错。

代码如下

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function (haystack, needle) {
    let needle_len = needle.length
    if (needle_len === 0) return 0
    let res = -1
    for (let i = 0; i < haystack.length; i++) {
        // 如果第一个字符匹配上,则判断接下来的几个字符是不是相同
        if (haystack[i] === needle[0]) {
            if (haystack.substring(i, i + needle_len) === needle) {
                res = i
                break
            }
        }
    }
    return res
}

3. 思路2 KMP算法求解

KMP算法相比于暴力求解方法的优点在于,暴力求解每次匹配失败都会使得主串的指针回溯,而KMP算法则不会使得主串指针回溯,我们来通过例子理解一下。

1.png

从头开始匹配,当匹配到[d, e]的时候,发现该字符匹配不上,主串的指针则会回退到b,与模式串重新开始匹配,具体过程如下

2.png
3.png
4.png
5.png

我们可以看到,暴力匹配的方式需要这样老实巴交的执行,才能发现匹配不成功。但是在①的时候,即[d, e]匹配不成功时,我们发现之前的都是匹配上的,且匹配上的部分有前缀和后缀均为ab的子串,在KMP算法中,我们不需要回溯主串指针,直接将模式串从真前缀位置ab移动到真后缀ab位置,即一下可以跳过步骤②和③直接到达④进行后续匹配。

6.png

为什么可以这么做呢?

因为我们在发现当[d, e]匹配不上的时候,我们希望模式串有一个字符能够替代e使得匹配继续进行,这时候我们发现,如果前面有真前缀和真后缀配对的话,那么我们可以用真前缀替换真后缀,使得模式串向后移动,真前缀的下一个字符则可以替代真后缀的下一个字符,也就是当前没有匹配上的e从而使得匹配继续进行。不过需要注意的是,如果我们每次都去寻找真前缀和真后缀的话,实际上是很慢的!

这时候我们又发现了一个规律,当出现匹配不上字符的时候,模式串和主串在这个字符之前是完全匹配上的,那么说明寻找真前缀和真后缀压根和主串没有啥关系,只需要找模式串即可。我们定义一个数组\Pi,其中\Pi[i]表示s[0:i]最长真前缀的长度

以本例的模式串为例,我们来计算一下\Pi数组的值

pi[0] = 0, a没有真前缀
pi[1] = 0, ab没有真前缀
pi[2] = 0, abc没有真前缀
pi[3] = 1, abca有真前缀a
pi[4] = 2, abcab有真前缀ab
pi[5] = 0, abcabe没有真前缀

不难发现,数组\Pi有几个重要的性质:

  1. \Pi[i] \leq \Pi[i-1]+1,即\Pi [i]的下一位的数值最多比当前值递增1。
  2. s[0:j-1]=s[i-j-1:i-1]s[i]=s[j]时,有\Pi[i] = \Pi[i-1]+1

那么我们在构造数组\Pi的时候,只需要执行如下操作:

1. 定义一个数组,长度等于模式串的长度,且pi[0] = 0
2. 令i = 1,j = 0开始遍历,如果pattern[i] != pattern[j]则说明未匹配成功,
     我们需要将j指针回退到一个可以使得pattern[i] == pattern[j]的位置,根据性质2,
     令j = pi[j - 1]
     若j回退到0的位置,则表示未找到这样一个合适位置,pi[i] = 0,i从下一个字符开始
3. 如果pattern[i] == pattern[j],说明匹配成功,根据性质2,pi[i] = j+1

具体构造数组\Pi的代码如下

/**
 * 构建pi数组
 * @param {string} pattern 模式串
 */
const getPi = (pattern) => {
    let i = 1,
        j = 0,
        m = pattern.length
    const pi = new Array(m).fill(0)
    while (i < m) {
        // 未匹配成功,j进行回溯,若回溯到j=0则表示不存在这样的真前缀
        // 回溯取j=pi[j-1]一步步向前试探
        while (j > 0 && pattern[i] !== pattern[j]) {
            j = pi[j - 1]
        }
        // 如果匹配上,则 pi[i] = j+1, 且i和j后移
        if (pattern[i] === pattern[j]) {
            pi[i++] = ++j
            // 如果j为0,则表示没真前缀,此时pi[i] = j, i和j后移
        } else if (j === 0) {
            pi[i++] = j
        }
    }
    return pi
}

有了预定义的数组\Pi我们就可以开始进行KMP算法了。

1. 让主串和模式串的指针i和j分别从下标0开始匹配
2. 如果主串和模式串出现了字符不匹配,那么j需要进行回退到一个合适位置
     即j = pi[j-1],如果j=0了,说明i从该字符开始无法找到匹配子串,i++
3. 如果匹配上,则i和j都向后移动
4. 如果j的值等于模式串长度,则说明找到了,返回此时子串的起始位置
5. 其他情况为未找到,返回-1

代码如下

/**
 * KMP算法实现
 * @param {string} s 主串
 * @param {string} pattern 模式串
 */
const strStr = (s, pattern) => {
    const n = s.length,
        m = pattern.length
    if (m === 0) return 0 // c语言的结果
    const pi = getPi(pattern)
    // 执行Kmp匹配过程
    for (let i = 0, j = 0; i < n; i++) {
        // 出现了某个字符不匹配的情况,需要j进行回溯
        while (j > 0 && s[i] !== pattern[j]) {
            j = pi[j - 1]
        }
        // 匹配成功,此时j++,循环结束后i++
        if (s[i] === pattern[j]) {
            j++
        }
        if (j === m) {
            // 匹配成功
            return i - j + 1
        }
    }
    return -1
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容