字符串匹配算法之 KMP 极简动画教程

实现 strStr() 函数

给你两个字符串 haystack 和 needle ,请你在 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

提示:

0 <= haystack.length, needle.length <= 5 * 104
haystack 和 needle 仅由小写英文字符组成

本题是经典的字符串单模匹配的模型,因此可以使用字符串匹配算法解决,常见的字符串匹配算法包括暴力匹配、Knuth-Morris-Pratt 算法、Boyer-Moore 算法、Sunday 算法等,本文将讲解 KMP (Knuth-Morris-Pratt )算法。

朴素暴力破解法

简单的循环判断即可。
这里用一个 flagArray , 记录 needle 每个字符是否在 haystack 中。

代码

class Solution {
fun strStr(s: String, x: String): Int {
    if (x.isEmpty())
        return 0

    val sArray = s.toCharArray()
    val xArray = x.toCharArray()
    val width = x.length

    for (i in 0..(s.length - 1)) {

        if (i + width > s.length) {
            break
        }

        // 针对每一个 i, 遍历 x
        val flagArray = BooleanArray(width, { false })

        // 注意: 这地方的区间 [i, i + width - 1]
        (i..(i + width - 1)).forEachIndexed { index, k ->
            if (sArray[k] == xArray[index]) {
                flagArray[index] = true
            }
        }

        if (flagAllTrue(flagArray)) {
            return i
        }
    }

    return -1
}

fun flagAllTrue(flagArray: BooleanArray): Boolean {
    var flag = true
    flagArray.forEach {
        flag = flag && it
    }
    return flag
}
}

时间复杂度: O (m * n)
空间复杂度: O (m)

KMP 算法

KMP 算法的核心思想是在遍历模式串的过程中,把模式串的对称前后缀(等缀)部分记录下来,下一轮比对模式串的时候,直接把上一次已经比对的前缀(=后缀)部分跳过,直接来到 next(j)。

动画图示:

当下标为 i=5, j=5 的时候,发现 s[i]!=x[j], 接下来,要开始回溯。

首先,检查之前已经匹配成功的部分:aabaa( 此时,要看next(4)= 2 ),判断是否存在相同的「前缀」和「后缀」;

if 存在( 这里是 aa ),则跳转到「前缀」的下一个位置( aabaaf )继续往下匹配。

前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。

定义:滑动步长 π(i) 函数如下:

s[0..i] 前后等缀最大长度。

e.g. 模式串:x = aabaaf

π (0) = a = 0
π (1) = aa = 1
π (2) = aab = 0
π (3) = a ab a = 1
π (4) = aabaa = 2

说明一下:比如说,当模式串x 跟 s 的第 0..4 个字符均相等,那么如果第 5 个字符不相等,下一轮比对就可以直接从 aabaa 中的 baa 处开始比对了。 因为,前面的aabaa跟后面的aabaa, 在上一轮比对的过程中已经匹配过,是相等的。)

π (5) = aabaaf = 0

这个 π(i) 函数也叫前缀表(prefix table)。前缀表有什么作用呢? 前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

实现 π(i) 函数 之后,按照上面动画演示的算法步骤实现源代码即可。

如果输入的模式串为 aabaaf,对应的 next 为 0 1 0 1 2 0,(其实这就是前缀表的数值了)。

那么用这样的next数组也可以用来做匹配。实现代码如下:

class Solution {
    fun strStr(s: String, x: String): Int {
        val m = x.length
        val n = s.length

        if (x.isEmpty()) {
            return 0
        }

        var j = 0
        // 模式串的最大等缀长度表
        val next = getNext(x)
        // 遍历 s[n]
        (0..n - 1).forEach {
            val i = it

            while (j > 0 && s[i] != x[j]) {
                j = next[j - 1]
            }

            if (s[i] == x[j]) {
                j++
            }

            if (j == m) {
                return i - m + 1
            }


        }

        return -1

    }


    fun getNext(s: String): IntArray {
        val next = IntArray(s.length)
        var j = 0
        next[0] = j
        for (i in 1 until s.length) {
            while (j > 0 && s[i] != s[j]) {
                j = next[j - 1]
            }
            if (s[i] == s[j]) {
                j++
            }
            next[i] = j
        }
        return next
    }
}

如何求解前缀函数?

上面的 getNext(s: String) 函数的算法思想可以说是“艺术”了(一般人真的想不到,想必这就是大师为什么是大师了吧!):

时间复杂度: O (m + n)
空间复杂度: O (m)

KMP 算法源代码

/**
 * pi(k) 函数: 递归计算字符串的最大公共前后缀的长度 max common prefix suffix length
 */
fun pi(j: Int, pattern: String): Int {
    if (j == 0) return 0

    for (k in 1 until pattern.length) {
        while (pattern[j] != pattern[pi(j - 1, pattern)] && pi(j - 1, pattern) > 0) {
            return pi(pi(j - 1, pattern) - 1, pattern)
        }

        if (pattern[j] == pattern[pi(j - 1, pattern)]) {
            return pi(j - 1, pattern) + 1
        }
    }
    
    return 0
}

fun kmp(text: String, pattern: String): Int {
    val m = pattern.length
    val n = text.length
    if (pattern.isEmpty()) {
        return 0
    }
    // j: the current index of pattern
    var j = 0
    (0 until n).forEach {
        // i: the current index of text
        val i = it
        while (j > 0 && text[i] != pattern[j]) {
            j = pi(j - 1, pattern = pattern)
        }
        if (text[i] == pattern[j]) {
            j++
        }
        if (j == m) {
            return i - m + 1
        }
    }
    return -1
}

fun main() {
    var text = "addaabbcaabffffggghhddabcdaaabbbaab"
    var pattern = "aabbcaab"

    for (i in 0 until pattern.length) {
        print("${pi(i, pattern)} ")
    }
    println("\nKMP subtring search algorithm:")
    var index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

    text = "hello"
    pattern = "ll"
    for (i in 0 until pattern.length) {
        print("${pi(i, pattern)} ")
    }
    println("\nKMP subtring search algorithm:")
    index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

    text = "aaaaa"
    pattern = "bba"
    for (i in 0 until pattern.length) {
        print("${pi(i, pattern)} ")
    }
    println("\nKMP subtring search algorithm:")
    index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

}

参考资料

https://leetcode-cn.com/problems/implement-strstr/solution/shi-xian-strstr-han-shu-kotlin-by-chengu-sic2/

https://leetcode-cn.com/problems/implement-strstr/solution/shi-xian-strstr-by-leetcode-solution-ds6y/

https://leetcode-cn.com/problems/implement-strstr/solution/dai-ma-sui-xiang-lu-kmpsuan-fa-xiang-jie-mfbs/

https://leetcode-cn.com/problems/implement-strstr/solution/shua-chuan-lc-shuang-bai-po-su-jie-fa-km-tb86/

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

推荐阅读更多精彩内容