Rabin-Karp 算法(字符串快速查找,基于go语言)

  之前在领扣做题目的时候,遇到一个字符串匹配的问题,想到了Rabin-Karp算法,但是具体细节已经忘了。所以写一篇文章来温故而知新一下。
  普通的字符串子串匹配时耗时又耗力,最主要的原因是匹配一个串时不能利用上一次匹配的结果,就导致有些串需要重新计算匹配。所以只要好好利用上一次匹配的信息,就能够提升查询效率。
  我们在比较字符串的时候可能回想为什么我们要一个一个比较,而不能一次性比较,就像数字那样。Rabin-Karp算法告诉我们,我们可以将每一个字符串都对应一个数字(比如ASCII码),然后将每个字符对应的数字经过运算得到一个这个字符串对应的数字,比较两个字符串时只需要比较这个数字即可。
  接下来展示一下怎么计算这个数字(这里以ASCII为例)。
  由于ASCII的长度有128,所以我们这里记L=128。假设我们要在sfefgse中查找fgs,那么就可将fgs转化为一个数字: ('f’对应的值*L+'g’对应的值)*M + ‘s’对应的值 = (102 * 128 + 103 ) * 128 + 115 = 1684467 。我们可以分析一下这个数字:(其中n表示字符串长度)
    代表字符 “f” 的部分是“ "f"对应的值 * (L 的 n - 1 次方) = 102 * (128 ^2) = 1671168”
    代表字符 "g"的部分是“ "g"对应的值 * (L 的 n - 2 次方) = 103 * (128^1) = 13184”
    代表字符 “s” 的部分是“ "s"的码点 * (M 的 n - 3 次方) = 115 * (128^0) = 115”
  这里的f、g、s对应的数值的和就是fgs所对应的数字1684467。所以这里我们就可以对这三个字符进行加减。
  得到了这个字符串对应的数字后,我们就可以计算原字符串里对应长度的子串的数字。
  我们先计算sfe:
     Num(‘sfe’) = (115 * 128 + 102) * 128 + 101 = 1897317。
  然后与fgs进行比较发现并不相等,所以这时我们就需要去掉开头的字符’s’,得到
Num(‘fe’) = (1897317 - 115 * 128^2) * 128 = 1684096,所以Num(‘fef’) = Num(‘fe’) + Num(‘f’) = 1684096 + 102 = 1684198。然后再用’fef’字符对应的数字与‘fgs’的比较,以此类推可以找到相同的子串。

  上面所使用的字符范围是ASII码内的字符,go里面源码使用的是Unicode范围内的字符。
  如果我们要在 Unicode 字符集范围内查找“搜索词”,由于 Unicode 字符集中有 1114112 个字符,那么 L 就等于 1114112,而 Go 语言中使用 16777619 作为 L 的值,16777619 比 1114112 大(更大的 L值可以容纳更多的字符,这是可以的),而且 16777619 是一个素数。这样就可以使用上面的方法计算 Unicode 字符串的数值了。进而可以对 Unicode 字符串进行比较了。

  下面是 Go 语言中字符串匹配函数的源码,使用 Rabin-Karp 算法进行字符串比较:

// primeRK 是用于 Rabin-Karp 算法中的素数,也就是上面说的 L
const primeRK = 16777619

// 返回 Rabin-Karp 算法中“搜索词” sep 的“哈希值”及相应的“乘数因子(权值)”
func hashstr(sep string) (uint32, uint32) {
	// 计算 sep 的 hash 值
	hash := uint32(0)
	for i := 0; i < len(sep); i++ {
		hash = hash*primeRK + uint32(sep[i])
	}
	// 计算 sep 最高位 + 1 位的权值 pow(乘数因子)
	// 也就是上面说的 M 的 n 次方
	// 这里通过遍历 len(sep) 的二进制位来计算,减少计算次数
	var pow, sq uint32 = 1, primeRK
	for i := len(sep); i > 0; i >>= 1 {
		if i&1 != 0 { // 如果二进制最低位不是 0
			pow *= sq
		}
		sq *= sq
	}
	return hash, pow
}

// Count 计算字符串 sep 在 s 中的非重叠个数
// 如果 sep 为空字符串,则返回 s 中的字符(非字节)个数 + 1
// 使用 Rabin-Karp 算法实现
func Count(s, sep string) int {
	n := 0
	// 特殊情况判断
	switch {
	case len(sep) == 0: // 空字符,返回字符个数 + 1
		return utf8.RuneCountInString(s) + 1
	case len(sep) == 1: // 单个字符,可以用快速方法
		c := sep[0]
		for i := 0; i < len(s); i++ {
			if s[i] == c {
				n++
			}
		}
		return n
	case len(sep) > len(s):
		return 0
	case len(sep) == len(s):
		if sep == s {
			return 1
		}
		return 0
	}
	// 计算 sep 的 hash 值和乘数因子
	hashsep, pow := hashstr(sep)
	// 计算 s 中要进行比较的字符串的 hash 值
	h := uint32(0)
	for i := 0; i < len(sep); i++ {
		h = h*primeRK + uint32(s[i])
	}
	lastmatch := 0 // 下一次查找的起始位置,用于确保找到的字符串不重叠
	// 找到一个匹配项(进行一次朴素比较)
	if h == hashsep && s[:len(sep)] == sep {
		n++
		lastmatch = len(sep)
	}
	// 滚动 s 的 hash 值并与 sep 的 hash 值进行比较
	for i := len(sep); i < len(s); {
		// 加上下一个字符的 hash 值
		h *= primeRK
		h += uint32(s[i])
		// 去掉第一个字符的 hash 值
		h -= pow * uint32(s[i-len(sep)])
		i++
		// 开始比较
		// lastmatch <= i-len(sep) 确保不重叠
		if h == hashsep && lastmatch <= i-len(sep) && s[i-len(sep):i] == sep {
			n++
			lastmatch = i
		}
	}
	return n
}

你可能感兴趣的:(算法)