字符串匹配算法之 KMP 極簡動畫教程

實現(xiàn) strStr() 函數(shù)

給你兩個字符串 haystack 和 needle ,請你在 haystack 字符串中找出 needle 字符串出現(xiàn)的第一個位置(下標(biāo)從 0 開始)。如果不存在,則返回 -1 。

說明:

當(dāng) needle 是空字符串時,我們應(yīng)當(dāng)返回什么值呢?這是一個在面試中很好的問題。

對于本題而言,當(dāng) needle 是空字符串時我們應(yīng)當(dāng)返回 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 僅由小寫英文字符組成

本題是經(jīng)典的字符串單模匹配的模型,因此可以使用字符串匹配算法解決,常見的字符串匹配算法包括暴力匹配、Knuth-Morris-Pratt 算法、Boyer-Moore 算法、Sunday 算法等,本文將講解 KMP (Knuth-Morris-Pratt )算法。

樸素暴力破解法

簡單的循環(huán)判斷即可。
這里用一個 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 })

        // 注意: 這地方的區(qū)間 [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
}
}

時間復(fù)雜度: O (m * n)
空間復(fù)雜度: O (m)

KMP 算法

KMP 算法的核心思想是在遍歷模式串的過程中,把模式串的對稱前后綴(等綴)部分記錄下來,下一輪比對模式串的時候,直接把上一次已經(jīng)比對的前綴(=后綴)部分跳過,直接來到 next(j)。

動畫圖示:

當(dāng)下標(biāo)為 i=5, j=5 的時候,發(fā)現(xiàn) s[i]!=x[j], 接下來,要開始回溯。

首先,檢查之前已經(jīng)匹配成功的部分:aabaa( 此時,要看next(4)= 2 ),判斷是否存在相同的「前綴」和「后綴」;

if 存在( 這里是 aa ),則跳轉(zhuǎn)到「前綴」的下一個位置( aabaaf )繼續(xù)往下匹配。

前一個字符的前綴表的數(shù)值是2, 所有把下標(biāo)移動到下標(biāo)2的位置繼續(xù)比配。 可以再反復(fù)看一下上面的動畫。

定義:滑動步長 π(i) 函數(shù)如下:

s[0..i] 前后等綴最大長度。

e.g. 模式串:x = aabaaf

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

說明一下:比如說,當(dāng)模式串x 跟 s 的第 0..4 個字符均相等,那么如果第 5 個字符不相等,下一輪比對就可以直接從 aabaa 中的 baa 處開始比對了。 因為,前面的aabaa跟后面的aabaa, 在上一輪比對的過程中已經(jīng)匹配過,是相等的。)

π (5) = aabaaf = 0

這個 π(i) 函數(shù)也叫前綴表(prefix table)。前綴表有什么作用呢? 前綴表是用來回退的,它記錄了模式串與主串(文本串)不匹配的時候,模式串應(yīng)該從哪里開始重新匹配。

實現(xiàn) π(i) 函數(shù) 之后,按照上面動畫演示的算法步驟實現(xiàn)源代碼即可。

如果輸入的模式串為 aabaaf,對應(yīng)的 next 為 0 1 0 1 2 0,(其實這就是前綴表的數(shù)值了)。

那么用這樣的next數(shù)組也可以用來做匹配。實現(xiàn)代碼如下:

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
    }
}

如何求解前綴函數(shù)?

上面的 getNext(s: String) 函數(shù)的算法思想可以說是“藝術(shù)”了(一般人真的想不到,想必這就是大師為什么是大師了吧?。?/p>

時間復(fù)雜度: O (m + n)
空間復(fù)雜度: O (m)

KMP 算法源代碼

/**
 * pi(k) 函數(shù): 遞歸計算字符串的最大公共前后綴的長度 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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容