算法合集:滚动哈希——干脆叫前缀和得了

说到滚动哈希,相信没学过的大部分人都被劝退过,但其实高级名词都是用来吓唬新手的。滚动哈希无非就是前缀和,同时滚动哈希,字典树外加dp几乎横扫所有有难度的字符串算法。本节通过例题来讲透Rabin-Karp的滚动哈希。

一、滚动哈希讲解 + 例题讲解

1、hashCode

第一步要计算一个字符串的hashCode,用过HashMap应该了解到,HashMap就是利用hashCode来将key-value存储到数组中的
我们以一下方式来定义hashCode

字母a ~ z用数字1 ~ 26来计算hashCode
我们利用一个数b,来作为各个位数上的区分,一般b1311331等质数

例如:对于单词apple

  • 有a = 1,p = 16,l = 12,e = 5
  • hashCode(apple) = 1 * pow(131, 0) + 16 * pow(131, 1) + 16 * pow(131, 2) + 12 * pow(131, 3) + 5 * pow(131, 4)

但hashCode的值可能非常大,所以

找到一个数p,每次求hashCode时,都需要对p取模
p是用来规避哈希冲突的,所以p要比较大,一般根据题目要求来界定

举个例子,若要对banana求hashCode

  • 有a = 1,b = 2,n = 14
String s = "banana";
long[] hash = new long[s.length() + 1];
for (int i = 0; i < s.length(); i++)
	hash[i + 1] = (hash[i] * b + (s.charAt(i) - 'a' + 1)) % p;

当然,为什么要用数组?数组长度为什么要加一?下面来解释

2、hashCode与前缀和

要想求出任意一段连续子串的hashCode,其想法类似于前缀和

  • 对于前缀和数组s[]:欲求从lr的和
  • sum(l, r) = s[r] - s[l - 1]
  • 同理若求出了整个字符串的hashCode
  • hash[r]hash[l - 1]存在一定关系

hash[r]hash[l - 1]中间相差了一定长度的字符串,其长度为r - l + 1
所以通过求出pow(b, r - l + 1),就可以求出lr的hashCode
hashCode(l, r) = hash[r] - hash[l - 1] * pow(b, r - l + 1)
但注意的是,还需要对p取模,并且由于是hashCode之间相减,得到的hashCode有可能是负数
所以hashCode(l, r) = ((hash[r] - hash[l - 1] * pow(b, r - l + 1)) % p + p) % p

1)为了保证一般性,前缀和数组长度要加一
2)对于式子pow(b, r - l + 1)需要另开一个数组long[]来计算b的各个指数的值

3、例题讲解

LeetCode 1044 最长重复子串
1)初始化
首先看到字符串s的长度比较长,所以这里我使得b取1331,p取2的48次方

int len = s.length();
String ans = "";
long p = (long)Math.pow(2, 48);
long[] hash = new long[len + 1];
long[] p1331 = new long[len + 1]; // 计算pow(b, r - l + 1)
p1331[0] = 1;
for (int i = 0; i < len; i++) {
	hash[i + 1] = (hash[i] * 1331 + (s.charAt(i) - 'a' + 1)) % p;
	p1331[i + 1] = p1331[i] * 1331;
}

2)二分答案法求得最大长度
二分答案法的思路可以看我另一篇博客:算法合集:二分
先定义l = 0r = len - 1来界定答案的长度,而mid就成了当前需要计算的长度

int l = 0, r = len - 1;
while (l < r) {
	int mid = (l + r) >> 1;
}

接下来我们需要一个HashSet,对于长度为mid的子串,从左到右遍历字符串s,并保存hashCode,若遇到了相同的hashCode则记录答案

int l = 0, r = len - 1;
while (l < r) {
	int mid = (l + r) >> 1;
	HashSet<Long> set = new HashSet<>();
	for (int i = mid; i <= len; i++) {
		long h = ((hash[i] - hash[i - mid] * p1331[mid]) % p + p) % p;
		if (set.contains(h)) {
			ans = s.substring(i - mid, i);
			break;
		}
		set.add(h);
	}
}

还需要一个boolean find用来记录:当前长度为mid时,是否找到了答案,若找到了,则移动l = mid去试一个更大的mid,若没找到,令r = mid - 1,去试一个更小的mid

int l = 0, r = len - 1;
while (l < r) {
	boolean find = false;
	int mid = (l + r + 1) >> 1; // 若不+1,l = mid会造成死循环
	HashSet<Long> set = new HashSet<>();
	for (int i = mid; i <= len; i++) {
		long h = ((hash[i] - hash[i - mid] * p1331[mid]) % p + p) % p;
		if (set.contains(h)) {
			find = true;
			ans = s.substring(i - mid, i);
			break;
		}
		set.add(h);
	}
	if (find) l = mid;
	else r = mid - 1;
}

最后返回ans即可

二、实战题目

LeetCode 28 实现strStr()
LeetCode 187 重复的DNA序列
LeetCode 718 最长重复子数组

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值