Trie树

mac2025-06-12  29

相关概念

主串

模式串

模式串<=主串

常用算法

BF

原理

我们在主串中,分别检查从0,1,2,....,到n-m位置的,n-m+1个字符串是否匹配。

特点

暴力匹配算法(最简单,最暴力的匹配算法),也叫朴素匹配算法。因为简单,不容易出错,容易实现,所以比较常用,而且现实场景字符串长度并不会很长,而且比较过程中发现不匹配就可以推到主串下一个字符继续匹配了,所以算法的时间复杂度远高于最坏O(m*n)的情况。在实际的开发过程中,绝大多数场景下,尤其针对小规模的字符串匹配,朴素的字符串匹配算法就够用了。

时间复杂度

O(m*n)

代码实现

package main func Bf(ss, s string) (b bool, at int) { n := len(ss) m := len(s) if m == 0 || n == 0 || m > n { return b, at } for i := 0; i < n-m+1; i++ { for j := i; j < i+m-1; j++ { if ss[j] != s[j-i] { break } b = true at = j return } } return b, at } func Bf2(ss, s string) (b bool, at int) { n := len(ss) m := len(s) if m == 0 || n == 0 || m > n { return b, at } for i := 0; i < n-m+1; i++ { if ss[i:i+m] == s { b = true at = i return } } return } func main() { ss := "abcdjsfjsr" s := "jsf" b, at := Bf(ss, s) println(b) println(at) b, at = Bf2(ss, s) println(b) println(at) }

RK

原理

是对BF算法的一种优化,把每个子串用hash值来代替,模式串也计算出hash值,仅对hash值进行比较,当模式串与子串的hash值相等时,为避免hash冲突而相等时,在针对此子串跟模式串逐个对比一下即可。

难点

hash算法的设计,减少hash冲突。

HASH算法设计

a-z转化为26进制,转化为数字值,然后针对数字比较,这样比较快速。把每个元素的26进制,相加,这样比较可以有效避免计算机整数越界问题,但会出现大量hash冲突,比较模式串和子串hash相同后,要在详细逐一对比子串和模式串的每个元素,这样来解决hash冲突的问题。

时间复杂度

整个 RK 算法包含两部分,计算子串哈希值和模式串哈希值与子串哈希值之间的比较。第一部分,我们前面也分析了,可以通过设计特殊的哈希算法(复杂部分可以略过,就用简单的hash字符即可),只需要扫描一遍主串就能计算出所有子串的哈希值了,所以这部分的时间复杂度是 O(n)。(个人觉得此处缺少了计算模式串hash的时间。如果模式串很长的话,也会是一个O(n),但不影响整体时间复杂度的统计。)

模式串哈希值与每个子串哈希值之间的比较的时间复杂度是 O(1),总共需要比较 n-m+1 个子串的哈希值,所以,这部分的时间复杂度也是 O(n)。所以,RK 算法整体的时间复杂度就是 O(n)。

BM

算法思想

我们把模式串与主串匹配的过程,看成是模式串在主串上向后滑动比较的过程,当遇到不匹配的字符,bf和rk的做法是向后滑动一位,然后在从模式串头部开始重新比较,而BM主要是利用一些策略,计算出肯定不匹配的位数,让模式串尽最大位数的向后滑动,这样比较次数大幅减小,效率提高。

如:

主串:g t c d g t v y u 

模式串:g t v

bf和rk做法:

第一次比较:c,v不等,向后滑动一位

g t c d g t v y u 

g t v

第二次比较:t,g不等,向后滑动一位

g t c d g t v y u 

   g t v

第三次比较:c,gt,g不等,向后滑动一位

g t c d g t v y u 

     g t v

BM做法:

第一次比较:c,v不等,而且c不在模式串中出现,所以c的位置,跟子串都不匹配,子串移动到c后一位

g t c d g t v y u 

g t v

第二次比较:

g t c d g t v y u 

        g t v

BM算法解析

坏字符规则

比较顺序,从后往前比较。没有好后缀的情况,最后一位就不匹配si,而坏字符在子串的前面xi(如果出现多个去最靠后的那个)。模式串相对主串的滑动位数为:si-xi 当前面没有匹配时候,xi取-1,(如果出现多个去最靠后的那个,及xi最大的那个,这样往后移动的位数最小,可以防止错过应有的匹配)。但是当有好后缀时候,xi反而比si数值还要大,si-xi 为负值,所以,单有坏字符策略还不够,还要有好后缀策略。

好后缀规则

在模式串中,查找跟好后缀匹配的另一个子串;在好后缀的后缀子串中,查找最长的、能跟模式串前缀子串匹配的后缀子串;

当既有坏字符,也有好后缀时候,取二者最大值,来向后滑动模式串,这就避免了si-xi为负数的场景。

KMP

原理

todo

代码实现

package main import "fmt" func KMP(ss, s string) (b bool, at int) { n := len(ss) m := len(s) nexts := Nexts(s) j := 0 // 模式串指针 for i:=0; i<n; i++ { for j > 0 && ss[i] != s[j] { j = nexts[j-1]+1 } if ss[i] == s[j] { if j == m-1 { b = true at = i-m+1 return } j = j+1 } } return b, at } func Nexts(s string) []int { l := len(s) next := make([]int, l) for k := range next { next[k] = -1 } for i:=1;i<l;i++ { j := next[i-1] for j > -1 && s[j+1] != s[i] { j = next[j] } if s[j+1] == s[i] { j = j+1 } next[i] = j } return next } func main() { ss := "aacabdfghaacabcdefg" s := "aacabc" b, at := KMP(ss, s) fmt.Println(b) fmt.Println(at) }

复杂度

空间复杂度

O(m) m为模式串长度

时间复杂度

O(m+n) 

 

 

 

最新回复(0)