首页 > Go 语言实现字符串匹配算法 -- BF(Brute Force) 和 RK(Rabin Karp)

Go 语言实现字符串匹配算法 -- BF(Brute Force) 和 RK(Rabin Karp)

今天介绍两种基础的字符串匹配算法,当然核心还是熟悉一下Go的语法,巩固一下基础知识

  • BF(Brute Force)
  • RK(Rabin Karp)

源字符串:src, 目标字符串:dest; 确认dest是否是src 的一部分。

BF算法很简单暴力,维护两个下标i,j,i控制src的遍历顺序, j控制dest遍历顺序。

记录一下i的起始位置,当j和i所在的字符匹配的时候,j和i都移动,知道j达到末尾则直接返回匹配。

否则i 回到起始位置的下一个位置,j 回到起始位置,两者重新进行匹配搜索。

由于比较简单,直接看一下Go实现的代码即可:

func Brute_force(str1 string, str2 string) bool{ if len(str1) < len(str2){ return  false}var (begin int = 0 // 维护src的起始位置i intj int)for i = 0; i < len(str1); begin++ { for j = 0;j < len(str2); j++ { if i < len(str1) && str1[i] == str2[j] { i++continue} else { break}}if j == len(str2) { return  true}i = begini++}return false
}

以上最坏的情况下是 主串是“aaaaa…aaaaaa”(省略号表示有很多重复的字符a),模式串是“aaaaab”。我们每次都比 对m个字符,要比对n-m+1次,所以,这种算法的最坏情况时间复杂度是O(n*m)。

当然这种模式匹配在实际的软件开发中还是比较常用的,主要有如下两种原因:

  • 实际的软件开发中,大部分情况下,模式串和主串的长度都不会太长。而且每次模式串与主串中的子串匹配的时候,当中途遇到不能匹配的字符的时候, 就可以就停止了,不需要把m个字符都比对一下。所以,尽管理论上的最坏情况时间复杂度是O(n*m),但是,统计意义上,大部分情况下,算法执行效率要比这 个高很多。
  • 朴素字符串匹配算法思想简单,代码实现也非常简单。简单意味着不容易出错,如果有bug也容易暴露和修复。

接下来看一下第二种算法,Rabin Karp。

这个算法的大体思路是:

过哈希算法对主串中的n-m+1个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。如果某个子串的哈希值与模式串相 等,那就说明对应的子串和模式串匹配了。因为哈希值是一个数字,数字之间比较是否相等是非常快速的, 所以模式串和子串比较的效率就提高了。

在这里插入图片描述

为了提升效率,我们需要尽可能避免遍历字符串中的每一个字符,否则就是仅仅提高了比较效率而已(数值的比较效率)。

hash函数的设计需要精巧一些,假设我们设置的字符集只包含k个字符,我们可以使用一个K进制的数来表示一个字符串,将这个 K进制转化为10进制的值即可作为hash 值。

比如要处理的字符串包括a-z 26个字母,我们可以使用26进制表示一个字符串,同时将26进制转化为一个10进制的值作为这个字符串的hash值。

对于字符串:“hjk”

h: (‘h’- ‘a’) *26^0

j: (‘j’ - ‘a’) * 26^1

j: (‘k’ - ‘a’) * 26^2

hash(“hjk”) = (‘h’- ‘a’) *26^0 + (‘j’ - ‘a’) * 26^1 + (‘k’ - ‘a’) * 26^2

为了减少遍历源字符串中的字符 次数,我们可以看看如下规律:

源字符串“hjkl”,我们先取 “hjk”, 后取"jkl"

hash(“hjk”) = (‘h’- ‘a’) *26^0 +('j' - 'a') * 26^1 + ('k' - 'a') * 26^2

hash(“jkl”) = ('j' - 'a') * 26^0 + ('k' - 'a')* 26^1 + (‘l’ - ‘a’) * 26^2

依据此规律,我们可以总结出来两个公式:

hash(i-1) = 26^0 * (s[i-1] - ‘a’) + 26^1 * (s[i] - ‘a’) + … + 26^(m-1) * (s[i+m-2] -‘a’)

hash(i) = 26^0 * (s[i] - ‘a’) + … + 26^(m-2) * (s[i + m - 2] - ‘a’) + 26^(m-1) * (s[i+m-2] -‘a’)

i表示源字符串的起始遍历位置字符下标,m表示目标字符串的长度, s表示源字符串 src。

两个公式进行运算:hash(i) - hash(i-1) / 26 = 26^(m-1) * (s[i+m-2] -‘a’) - (26^0 * (s[i-1] - ‘a’)) / 26

最终可以得到: hash(i) = (hash(i-1) - s[i-1] -‘a’ ) / 26 + 26^(m-1) * (s[i+m-2] -‘a’) 这样的计算公式。

这个时候,我们只需要扫描一遍主串即能够完成目标字符串的匹配, 主串的长度为n, 也就是我们需要O(n)的扫描复杂度。模式串的hash值 和每一个子串的hash值之间的比较是O(1) , 总共需要比较n-m+1个子串的哈希值,所以,这部分的时间复杂度也是O(n)。

所以,RK算法整体的时间复杂度就是O(n),相比于BF的O(m*n)效率还是高了不少

Go语言的完整实现如下:

// Calculate a string's hash function
func Hash(str string, m [] int) int { if len(str) == 0 { return 0}var (t intres int = 0)for i := 0; i < len(str); i ++ { t = m[i] * int(str[i] - 'a')res = res + t}return res
}// match the substring with hash function
// we can calculate the string's hash value with below formula
//
// 's' is source string, m is the length of the substring
// h(i-1) = 26^0 * (s[i-1] - 'a') +
// 			26^1 * (s[i] - 'a') + ... +
// 			26^(m-1) * (s[i+m-2] -'a')
//
// h(i) = 26^0 * (s[i] - 'a') + ... +
// 		  26^(m-2) * (s[i + m - 2] - 'a') +
// 		  26^(m-1) * (s[i+m-2] -'a')
//
// so
// h(i) = (h(i-1) - s[i-1] -'a' ) / 26 + 26^(m-1) * (s[i+m-2] -'a')
// we can use the formula to reduce the cpu's calculation
func Rabin_Karp_Hash(str1 string, str2 string) bool { if len(str1) < len(str2) { return false}var m []intvar t int = 1m = append(m,1)for i := 1; i < len(str2) + 1; i ++ { t = t*26m = append(m,t) // m store with 26^0, 26^1, 26^2 ... 26^(len(str2))}str2_hash := Hash(str2, m)fmt.Println(str2_hash)str1_hash := Hash(string([]byte(str1)[:len(str2)]),m)if str2_hash == str1_hash { return  true}for i := 1; i < len(str1) - len(str2) + 1; i ++ { new_hash := (str1_hash - int(str1[i-1]-'a')) / 26 +m[len(str2)-1] * int(str1[i+len(str2) -1] - 'a')if new_hash == str2_hash { return  true} else { str1_hash = new_hash}}return  false
}

更多相关:

  • HMAC: Hash-based Message Authentication Code,即基于Hash的消息鉴别码 在各大开放平台大行其道的互联网开发潮流中,调用各平台的API接口过程中,无一例外都会用到计算签名值(sig值)。而在各种计算签名的方法中,经常被采用的就是HMAC-SHA1,现对HMAC-SHA1做一个简单的介绍:...

  • 总体的hash学习导图如下: 文章目录定义分类字符hash排序hash链式hash(解决hash冲突)创建链式hash查找指定数值STL map(hash)哈希分类 完整测试代码应用(常见题目)1. 回文字符串(Longest Palindrome)2. 词语模式(Word Pattern)3. 同字符词语分组(Group Ana...

  • 运算符一.算数运算:二.比较运算:三.赋值运算四.逻辑运算 五.成员运算基本数据类型一.Number(数字)Python3中支持int、float、bool、complex。使用内置的type()函数查询变量类型。int(整型)在python2中整数类型有两种一个是int,表示整型,一种是long,表示长整型。而在python3中整数...

  • 题目:面试题38. 字符串的排列 输入一个字符串,打印出该字符串中字符的所有排列。 你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。 示例: 输入:s = "abc" 输出:["abc","acb","bac","bca","cab","cba"] 限制: 1 <= s 的长度 <= 8 解题: clas...

  •      给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。 示例 1: 输入: "()" 输出: true 示例 2: 输入: "()[]{}" 输出:...

  • Redis没有使用C语言字符串的形式,通过’’作为结尾,而是使用了简单动态字符串(simple dynamic string)。 当Redis使用的字符串不需要修改字符串的内容的时候,可以使用C语言提供的字符串,当需要修改内容的时候就使用的是简单动态字符串。Redis键值对的操作中,都是使用的简单动态字符串的方式。 这里可以把简...

  • 设计思路:导入Scanner类输入字符串,再将输入的字符串转化为字符数组,然后从字符串左右两侧依次比较字符chu是否相同,若相同递归返回读取的字符个数,若返回字符的个数==输入字符串的长度,则输出该字符串是回文,否则输 出该字符串不是回文   import java.util.Scanner;public class test1...