字符串(灵神题单)

177 阅读5分钟

字符串题单

本题单是主要根据灵神的字符串题单来组织,用于记录字符串中常使用的思路和算法代码。

KMP算法

核心思想

KMP的核心思想就是计算出模板串的跳转数组(next数组),然后在接下来的对比中,匹配串和模式串发生冲突时,匹配串不动。模式串跳转到上一次最大匹配的位置,从这个位置接着比对。

next数组

在计算next数组之前,我们需要首先通过一个例子来理解字符串匹配的过程。例子如下

aaabbaabbzaaabb (匹配串) aaabbaa (模式串)

在移动模式串时,需要知道要移动哪里(即移动哪里可以最大的利用之前的比较过程,尽可能的让两个串之间比较的次数最少)。这个时候就出现前缀和后缀的概念。

模式串前缀如下: a aa aaa aaab aaabb aaabba aaabbaa

模式串后缀如下: a aa aab aabb aabba aabbaa aabbaaa

从上面的内容可以看出,模式串前后缀的最大匹配是在aa时,最长匹配数为2。次大匹配是在a时,最长匹配数为1。这里有一个规律就是次大匹配串一定是最大匹配串的子串。

因此,我们在使用next数组比较时,如果当前匹配不上就应该跳到次大匹配的位置(就是往前跳)。因此求next数组的算法已经清晰。具体算法如下:

此后,在进行KMP匹配时的代码逻辑与求next数组的逻辑相同。但是我们需要从位置0开始进行匹配。具体逻辑代码如下所示:

可以根据如下几个题目来分析KMP算法:

Z函数

基本思路

    Z 函数主要思想是通过计算字符串的每个后缀和字符串的最大公共前缀来判断是否能够满足模式串 ,即当z[m+i] >= m时(m等于模式串的长度),说明此时匹配的长度大于了模式串的长度。下面给出一个例子:

字符串: abcdcd   模式串: cd

此时我们将模式串和字符串进行拼接 -> cdabcdcd
并计算出这个字符串的`Z函数`

Z 0 1 2 3 4 5 6 7
V 0 0 0 0 2 0 2 0

Z函数求解说明

    Z 函数求解的过程主要是来计算每个后缀与当前字符串的最大公共前缀长度。

    方法一
    我们可以想到的最简单方法是通过暴力循环来求解Z函数,即如下方法

package main

import "fmt"

func main() {
    s := "abcdcd"
    pattern := "cd"
    zstr := pattern + s

    z := make([]int, len(zstr))

    for i := 1; i < len(zstr); i++ {
       for j := 0; j < len(zstr); j++ {
          if zstr[i+j] != zstr[j] {
             z[i] = j
             break
          }
       }
    }

    fmt.Println(z)
}

    但是暴力方法的时间复杂度达到了O(n^2)。显然,当我们的字符串很长时,会花费非常长的时间来求解 Z 函数。所以,我们可以尽可能的利用之前求解出来的 Z 函数的值来辅助求解后面的内容。

    因此,我们可以使用 l r来分别表示当前最靠右边的公共前缀。这样就可能出现下面几种情况:

  1. i > r,说明此时比较的位置已经超过了之前公共前缀的有边界,后面的内容都是未知的。因此需要每个字符都进行比较,比较如下:
package main

func zfunc(s string) []int {
    n := len(s)
    z := make([]int, n)

    // l,r 分别表示r最大的前缀匹配的边界
    l, r := 0, 0

    for i := 1; i < n; i++ {
       //.....
       // i + z[i] < n 保证比较的字符存在,不会出现越界的情况
       // s[z[i]] == s[i+z[i]] 表示两个字符串的比较,其中可以把z[i]看作字符串比较时的下标偏移。
       // 当匹配上时,要及时更新l和r,同时将z[i] + 1 表示下一次比较下一个下标的字符
       for i+z[i] < n && s[z[i]] == s[i+z[i]] {
          l, r = i, i+z[i]
          z[i]++
       }
       //....
    }
}

  1. i <= r,说明此时需要比较的字符处于上次前缀匹配的范围内,我们就可以利用之前的条件s[0:r-l]==s[l,r]的性质来帮助我们快速求解Z[i] 。 可知s[i-l,r-l]==s[i,r],所以我们可以通过Z[i-l]来判断Z[i]

    如果,从i开始匹配时,在r之内就匹配失败的话,则Z[i] = Z[i-l],因为后半段相同,所有可以直接复用。如果Z[i-l] > r - i + 1 此时r后面的还未进行比较,但是r自身及前面的字符已经比较完了。所以,可以先将Z[i]设置为r - i + 1,然后后面继续进行比较并及时更新lr和Z函数。

##最终版本Z函数求解过程

package main

func zfunc(str string) []int {
    n := len(str)
    z := make([]int, n)

    l, r := 0, 0

    for i := 1; i < n; i++ {
       if i <= r {
          z[i] = min(z[i-l], r-i+1)
       }

       for i+z[i] < n && str[z[i]] == str[i+z[i]] {
          l, r = i, i+z[i]
          z[i]++
       }
    }
    return z
}