试想一下,如果对两个字符串进行是否为相同字符串的比较,如果是按照从前到后一个一个字符进行比较的话,如果字符串很长,那么消耗的时间就比较多。那么如果我们将不同的字符串转化为不同的有限位的数字,那么不就很好了吗,这就是今天要引出的哈希函数。哈希函数就是把复杂样本变成数字,以后复杂样本的对比变成数字之间的对比。
基本性质:
- 输入的参数可能性是无限的,输出的值范围相对有限。
- 输入同样样本的值一定得到相同的输出值,也就是哈希函数没有任何随机限制。
- 输入不同样本的值可能得到相同的输出值,此时称为哈希碰撞。
- 输入大量不同的样本,得到大量的输出值,几乎均匀的分布在整个输出域上。
如何理解性质4,有一个100m的绳子,范围是0~2^32-1,1000个不同的输入值经过哈希函数计算后,拿一个10m的绳子在范围内随便套,几乎是接近100个散落在这个范围的数字。
字符串哈希
将一个字符串转换为以base进制的数字,并让其自然溢出,例如:字符串abdec转换为499进制,那么假设a=1,b=2,c=3,d=4,e=5,得到的数字为3*499的0次方+5*499的一次方+4*499的二次方+2*499的三次方+1*499的4次方,原来的12453转换为了3*499的0次方+5*499的一次方+4*499的二次方+2*499的三次方+1*499的4次方,如果有long类型的溢出,就让它自然溢出。
尽量转化时的base选择质数,不要选择一些经典值,否则会被刻意构造出哈希碰撞,数字转换时从1开始,不要从0开始,否则可能会出错。利用字符串转化为数字进行比较,可以大大降低复杂度。
题目
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
const int MAXN = 10001;
const int base = 499;
vector<long long> nums(MAXN);
int n;
int v(char c) {
if (c >= '0' && c <= '9') {
return c - '0' + 1;
} else if (c >= 'A' && c <= 'Z') {
return c - 'A' + 11;
} else {
return c - 'a' + 37;
}
}
long long value(const string& s) {
long long ans = v(s[0]);
for (size_t i = 1; i < s.length(); i++) {
ans = ans * base + v(s[i]);
}
return ans;
}
int cnt() {
sort(nums.begin(), nums.begin() + n);
int ans = 1;
for (int i = 1; i < n; i++) {
if (nums[i] != nums[i - 1]) {
ans++;
}
}
return ans;
}
int main() {
cin >> n;
string s;
for (int i = 0; i < n; i++) {
cin >> s;
nums[i] = value(s);
}
cout << cnt() << endl;
return 0;
}
其中v函数是将字符转化为数字,value方法是进行哈希值的计算,其中计算每一个值将之前的值*base是因为多了1位,为了正确的计算。
2168. 每个数字的频率都相同的独特子字符串的数量 - 力扣(LeetCode)
例如一个字符串是ababccabab:
可以发现,在遍历的过程已经时间复杂度变为O(n^2)了 ,因此本体最小的时间复杂度为O(n^2)
首先解决判断一个字串是否是每个字符都出现相同的次数:维护一个词频表,更新词频maxcnt,更新拥有最大词频的字符数maxcntkinds和更新总字符数allkinds,判断maxcntkinds是否等于allkinds,相等则是一个字串是否是每个字符都出现相同的次数,不等反之。举个例子:当子字符串是aabbbcc时,maxcnt==3,maxcntkinds==3,allkinds==7;子字符串是aabbcc,maxcnt==2,maxcntkinds==6,allkinds==6;
代码如下:
#include <iostream>
#include <unordered_set>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;
int equalDigitFrequency(const string& str) {
long base = 499;
int n = str.length();
unordered_set<long> set;
vector<int> cnt(10, 0); // 用于统计数字频率的数组
for (int i = 0; i < n; i++) {
fill(cnt.begin(), cnt.end(), 0); // 重置频率数组
long hashCode = 0;
int curVal = 0, maxCnt = 0, maxCntKinds = 0, allKinds = 0;
for (int j = i; j < n; j++) {
curVal = str[j] - '0'; // 当前字符对应的数字
// +1是为了从1开始
// '0' --> 1
// '1' --> 2
hashCode = hashCode * base + curVal + 1; // 计算哈希值
cnt[curVal]++; // 更新当前数字的频率
if (cnt[curVal] == 1) {
allKinds++; // 新增一种数字
}
// 10 --> 11 最大频率是11,数字种类为1种 进if
// 原来有最大频率10,现在有个字符从9-->10,数字种类变成了两种 进else if
if (cnt[curVal] > maxCnt) {
maxCnt = cnt[curVal]; // 更新最大频率
maxCntKinds = 1; // 重置最大频率的数字种类
} else if (cnt[curVal] == maxCnt) {
maxCntKinds++; // 增加最大频率的数字种类
}
// 如果所有数字的频率相同,则加入集合
if (maxCntKinds == allKinds) {
set.insert(hashCode);
}
}
}
return set.size(); // 返回集合的大小
}
int main() {
string str;
cin >> str; // 输入字符串
cout << equalDigitFrequency(str) << endl; // 输出结果
return 0;
}
首先unordered_set<long> set;为什么set里面存放long类型不是string类型,如果是存放string,在哈希表进行比对的话是和string的长度有关,这样他就不够快,因此转化为long类型进行哈希去重.
从一个大的字符串中快速得到子字符串的哈希值
作用:当计算一个字符串任意字串的哈希值时,可不可以用O(1)的时间快速计算出来。
利用以下几个进行辅助base:转换为base进制. pow数字来记录base的多少次方,例如pow[1]=base的0次方,pow[2]=base的一次方,pow[3]=base的二次方......用pow数组进行下记录。
pow[0] = 1;
for (int i = 1; i < n; i++) {
pow[i] = pow[i - 1] * base;
}
// 比如,base = 499, 就是说的选择的质数进制
// 再比如字符串s如下
// " c a b e f "
// 0 1 2 3 4
// hash[0] = 3 * base的0次方 对应c
// hash[1] = 3 * base的1次方 + 1 * base的0次方 对应a
// hash[2] = 3 * base的2次方 + 1 * base的1次方 + 2 * base的0次方 对应b
// hash[3] = 3 * base的3次方 + 1 * base的2次方 + 2 * base的1次方 + 5 * base的0次方
// hash[4] = 3 * base的4次方 + 1 * base的3次方 + 2 * base的2次方 + 5 * base的1次方 + 6 *
// base的0次方
// hash[i] = hash[i-1] * base + s[i] - 'a' + 1,就是上题说的意思
// 想计算子串"be"的哈希值 -> 2 * base的1次方 + 5 * base的0次方 (原来的方法)
// 子串"be"的哈希值 = hash[3] - hash[1] * base的2次方(子串"be"的长度次方)
// hash[1] = 3 * base的1次方 + 1 * base的0次方
// hash[1] * base的2次方 = 3 * base的3次方 + 1 * base的2次方
// hash[3] = 3 * base的3次方 + 1 * base的2次方 + 2 * base的1次方 + 5 * base的0次方
// hash[3] - hash[1] * base的2次方 = 2 * base的1次方 + 5 * base的0次方
// 这样就得到子串"be"的哈希值了
// 子串s[l...r]的哈希值 = hash[r] - hash[l-1] * base的(r-l+1)次方,就是上面说的意思
代码如下
public static int strStr(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int n = s1.length;
int m = s2.length;
build(s1, n);
long h2 = s2[0] - 'a' + 1;
for (int i = 1; i < m; i++) {
h2 = h2 * base + s2[i] - 'a' + 1;
}
for (int l = 0, r = m - 1; r < n; l++, r++) {
if (hash(l, r) == h2) {
return l;
}
}
return -1;
}
public static int MAXN = 100005;
public static int base = 499;
public static long[] pow = new long[MAXN];
public static long[] hash = new long[MAXN];
public static void build(char[] s, int n) {
pow[0] = 1;
for (int i = 1; i < n; i++) {
pow[i] = pow[i - 1] * base;
}
hash[0] = s[0] - 'a' + 1;
for (int i = 1; i < n; i++) {
hash[i] = hash[i - 1] * base + s[i] - 'a' + 1;
}
}
// 返回s[l...r]的哈希值
public static long hash(int l, int r) {
long ans = hash[r];
if (l > 0) {
ans -= hash[l - 1] * pow[r - l + 1];
}
return ans;
}
重复叠加串的匹配
拼接情况只有以下两种:
因此先拼接最大的次数,B串的长度/A串的长度向上取整+1次
int k = (m + n - 1) / n;
int len = 0;
for (int cnt = 0; cnt <= k; cnt++) {
for (int i = 0; i < n; i++) {
s[len++] = s1[i];
}
}
拼接好之后计算B的哈希值,然后从拼好的A串出发,数B的长度个字符,计算哈希值,如果可以对应,看是否进入到了最后一个拼接的字串,进入到返回拼最大次数,没有进入返回次数-1,如果不能够对应,从下一位开始继续进行哈希值计算.
public static int repeatedStringMatch(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int n = s1.length;
int m = s2.length;
// m / n 向上取整
int k = (m + n - 1) / n;
int len = 0;
for (int cnt = 0; cnt <= k; cnt++) {
for (int i = 0; i < n; i++) {
s[len++] = s1[i];
}
}
build(len);
long h2 = s2[0] - 'a' + 1;
for (int i = 1; i < m; i++) {
h2 = h2 * base + s2[i] - 'a' + 1;
}
for (int l = 0, r = m - 1; r < len; l++, r++) {
if (hash(l, r) == h2) {
return r < n * k ? k : (k + 1);
}
}
return -1;
}
public static int MAXN = 30001;
public static char[] s = new char[MAXN];
public static int base = 499;
public static long[] pow = new long[MAXN];
public static long[] hash = new long[MAXN];
public static void build(int n) {
pow[0] = 1;
for (int i = 1; i < n; i++) {
pow[i] = pow[i - 1] * base;
}
hash[0] = s[0] - 'a' + 1;
for (int i = 1; i < n; i++) {
hash[i] = hash[i - 1] * base + s[i] - 'a' + 1;
}
}
public static long hash(int l, int r) {
long ans = hash[r];
ans -= l == 0 ? 0 : (hash[l - 1] * pow[r - l + 1]);
return ans;
}
串联单词所有字串
思路,将words数组中的三个字符串进行字符串哈希得到三个long类型的值,然后将s字符串从0位置开始分割,每一段为words数组中字符串的长度,此时为3,然后每一段进行字符串哈希得到多个long类型的值,进行滑动窗口比对,窗口大小为words的大小,此时为3,如果窗口包含words数组得到的long类型的哈希值,进行记录,直到退出窗口;将s字符串从1位置分割,进行相同操作,不满足长度不进行记录,同理,将字符串从2位置分割进行相同的操作
public static List<Integer> findSubstring(String s, String[] words) {
List<Integer> ans = new ArrayList<>();
if (s == null || s.length() == 0 || words == null || words.length == 0) {
return ans;
}
// words的词频表
HashMap<Long, Integer> map = new HashMap<>();
for (String key : words) {
long v = hash(key);
map.put(v, map.getOrDefault(v, 0) + 1);
}
build(s);
int n = s.length();
int wordLen = words[0].length();
int wordNum = words.length;
int allLen = wordLen * wordNum;
// 窗口的词频表
HashMap<Long, Integer> window = new HashMap<>();
for (int init = 0; init < wordLen && init + allLen <= n; init++) { // 同余分组
// init是当前组的首个开头
int debt = wordNum;
// 建立起窗口
for (int l = init, r = init + wordLen, part = 0; part < wordNum; l += wordLen, r += wordLen, part++) {
long cur = hash(l, r);
window.put(cur, window.getOrDefault(cur, 0) + 1);
if (window.get(cur) <= map.getOrDefault(cur, 0)) {
debt--;
}
}
if (debt == 0) {
ans.add(init);
}
// 接下来窗口进一个、出一个
for (int l1 = init, r1 = init + wordLen, l2 = init + allLen,
r2 = init + allLen + wordLen; r2 <= n; l1 += wordLen, r1 += wordLen, l2 += wordLen, r2 += wordLen) {
long out = hash(l1, r1);
long in = hash(l2, r2);
window.put(out, window.get(out) - 1);
if (window.get(out) < map.getOrDefault(out, 0)) {
debt++;
}
window.put(in, window.getOrDefault(in, 0) + 1);
if (window.get(in) <= map.getOrDefault(in, 0)) {
debt--;
}
if (debt == 0) {
ans.add(r1);
}
}
window.clear();
}
return ans;
}
public static int MAXN = 10001;
public static int base = 499;
public static long[] pow = new long[MAXN];
public static long[] hash = new long[MAXN];
public static void build(String str) {
pow[0] = 1;
for (int j = 1; j < MAXN; j++) {
pow[j] = pow[j - 1] * base;
}
hash[0] = str.charAt(0) - 'a' + 1;
for (int j = 1; j < str.length(); j++) {
hash[j] = hash[j - 1] * base + str.charAt(j) - 'a' + 1;
}
}
// 范围是s[l,r),左闭右开
public static long hash(int l, int r) {
long ans = hash[r - 1];
ans -= l == 0 ? 0 : (hash[l - 1] * pow[r - l]);
return ans;
}
// 计算一个字符串的哈希值
public static long hash(String str) {
if (str.equals("")) {
return 0;
}
int n = str.length();
long ans = str.charAt(0) - 'a' + 1;
for (int j = 1; j < n; j++) {
ans = ans * base + str.charAt(j) - 'a' + 1;
}
return ans;
}