字符串哈希原理

        试想一下,如果对两个字符串进行是否为相同字符串的比较,如果是按照从前到后一个一个字符进行比较的话,如果字符串很长,那么消耗的时间就比较多。那么如果我们将不同的字符串转化为不同的有限位的数字,那么不就很好了吗,这就是今天要引出的哈希函数。哈希函数就是把复杂样本变成数字,以后复杂样本的对比变成数字之间的对比。

        基本性质:

  1. 输入的参数可能性是无限的,输出的值范围相对有限。
  2. 输入同样样本的值一定得到相同的输出值,也就是哈希函数没有任何随机限制。
  3. 输入不同样本的值可能得到相同的输出值,此时称为哈希碰撞。
  4. 输入大量不同的样本,得到大量的输出值,几乎均匀的分布在整个输出域上。

如何理解性质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;
	}

 重复叠加串的匹配

686. 重复叠加字符串匹配 - 力扣(LeetCode)

拼接情况只有以下两种:

 因此先拼接最大的次数,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;
	}

 串联单词所有字串

30. 串联所有单词的子串 - 力扣(LeetCode)

 思路,将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;
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值