“双哈希”是指在一个哈希(Hash)操作中使用两次哈希函数,通常目的是为了增加哈希操作的安全性、降低冲突的概率,或者提升哈希表的效率。
在计算机科学中,双哈希通常出现在以下几个场景:
1. 哈希表中的冲突解决
在哈希表中,当两个元素通过哈希函数映射到相同的槽(产生哈希冲突)时,需要一种策略来处理这个冲突。常见的处理方式包括:
- 开放地址法(Open Addressing):通过线性探测、二次探测或双哈希等方式,寻找下一个空槽来插入元素。
- 双哈希冲突解决:使用两个哈希函数来减少冲突的概率。每次出现冲突时,使用第二个哈希函数来计算探测步长。这样能有效避免多个键产生相同的探测步长,从而减少碰撞。
比如,假设第一个哈希函数为 h1(x),第二个哈希函数为 h2(x),则如果发生碰撞,下一次探测的位置可以是:
new_index=(h1(x)+i⋅h2(x))mod N
其中,i 是探测次数(从 0 开始递增),N 是哈希表的大小。
2. 双重哈希加密
在加密领域,有时会使用双重哈希来增强数据的安全性。比如,对同一个输入数据进行两次哈希操作:
hash_result=H(H(data))
这种方式可以有效防止一些攻击,比如通过预计算哈希表进行反向工程攻击。
3. 签名算法中的双哈希
在数字签名或密码学算法中,双哈希也常用于增强安全性。例如,在一些区块链协议中,数据可能会经过两次哈希处理,确保数据的完整性和不可篡改性。
总结:
双哈希的主要目的是通过使用两个哈希函数来增加哈希操作的复杂度和安全性。它可以用于哈希表冲突解决、加密保护以及数字签名等领域,帮助提高效率并降低冲突的发生概率。
小明的字符串:
import java.util.*;
public class std {
private static final int MOD = 998244353;
private static final int SEED = 233;
private static final int N = 1000005;
private static int[] h = new int[N];
private static int[] qp = new int[N];
private static int[] th = new int[N];
public static void initHash(String s) {
qp[0] = 1;
h[0] = s.charAt(0) % MOD;
for (int i = 1; i < s.length(); ++i) {
h[i] = (h[i - 1] * SEED % MOD + s.charAt(i)) % MOD;
qp[i] = (qp[i - 1] * SEED) % MOD;
}
}
public static int getHash(int l, int r) {
if (l == 0) return h[r];
return (h[r] - h[l - 1] * qp[r - l + 1] % MOD + MOD) % MOD;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
String T = scanner.next();
initHash(s);
th[0] = T.charAt(0) % MOD;
for (int i = 1; i < T.length(); ++i) {
th[i] = (th[i - 1] * SEED + T.charAt(i)) % MOD;
}
int ans = 0;
for (int i = 0; i < s.length(); ++i) {
int l = 1, r = Math.min(T.length(), s.length() - i), mid;
while (l <= r) {
mid = l + (r - l) / 2;
if (getHash(i, i + mid - 1) == th[mid - 1]) {
ans = Math.max(ans, mid);
l = mid + 1;
} else {
r = mid - 1;
}
}
}
System.out.println(ans);
scanner.close();
}
}
这段 Java 代码的目的是用哈希算法(滚动哈希)解决一个字符串匹配问题。下面是对代码的逐行解释:
1. 常量和数组的定义:
private static final int MOD = 998244353;
private static final int SEED = 233;
private static final int N = 1000005;
MOD:哈希值取模常量,用于防止哈希值过大。SEED:哈希函数的种子值,常用于滚动哈希。N:用于定义数组的大小,表示处理的字符串最大长度。
2. 数组定义:
private static int[] h = new int[N];
private static int[] qp = new int[N];
private static int[] th = new int[N];
h[]:用于存储字符串s的哈希值。qp[]:用于存储SEED^i % MOD的值,这对于滚动哈希非常重要。th[]:存储字符串T的哈希值。
3. 初始化哈希函数:
public static void initHash(String s) {
qp[0] = 1;
h[0] = s.charAt(0) % MOD;
for (int i = 1; i < s.length(); ++i) {
h[i] = (h[i - 1] * SEED % MOD + s.charAt(i)) % MOD;
qp[i] = (qp[i - 1] * SEED) % MOD;
}
}
initHash(s):初始化字符串s的哈希值。- 计算字符串中每个位置的哈希值,并存储在
h[]数组中。 qp[]数组存储每个位置的SEED^i % MOD,这对后续计算子串的哈希值至关重要。
- 计算字符串中每个位置的哈希值,并存储在
4. 获取子串哈希值:
public static int getHash(int l, int r) {
if (l == 0) return h[r];
return (h[r] - h[l - 1] * qp[r - l + 1] % MOD + MOD) % MOD;
}
getHash(l, r):获取字符串s中从位置l到r的子串的哈希值。- 使用滚动哈希的公式,利用
h[]和qp[]数组高效计算子串的哈希值。
- 使用滚动哈希的公式,利用
5. 主函数:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
String T = scanner.next();
initHash(s);
th[0] = T.charAt(0) % MOD;
for (int i = 1; i < T.length(); ++i) {
th[i] = (th[i - 1] * SEED + T.charAt(i)) % MOD;
}
- 读取输入的两个字符串
s和T。 - 使用
initHash(s)初始化字符串s的哈希值。 - 计算并存储字符串
T的哈希值到th[]数组。
6. 二分查找匹配最长子串:
int ans = 0;
for (int i = 0; i < s.length(); ++i) {
int l = 1, r = Math.min(T.length(), s.length() - i), mid;
while (l <= r) {
mid = l + (r - l) / 2;
if (getHash(i, i + mid - 1) == th[mid - 1]) {
ans = Math.max(ans, mid);
l = mid + 1;
} else {
r = mid - 1;
}
}
}
System.out.println(ans);
scanner.close();
}
ans记录匹配的最长子串的长度。- 遍历
s中的每一个位置,使用二分查找来找出与T最长匹配的子串长度。l和r分别表示二分查找的区间。- 每次使用
getHash计算当前子串的哈希值并与T的哈希值进行比较。 - 如果匹配,则更新
ans,并尝试查找更长的匹配;否则,缩小查找区间。
7. 输出结果:
最后,输出匹配到的最长子串的长度。
总结:
这段代码使用滚动哈希技术通过二分查找来高效地找到字符串 s 中与字符串 T 最长匹配的子串的长度。通过预先计算字符串的哈希值,避免了重复计算,提高了效率。
这段 Java 代码主要解决的是一个字符串匹配问题,通过哈希算法来进行高效的字符串比较。目标是判断一个字符串 T 是否是另一个字符串 s 的循环子串,且找到最小的循环偏移量。如果是循环子串,输出 Yes 并给出最小偏移量;否则,输出 No。
代码的主要思路:
- 双重循环子串:将字符串
s扩展为s + s,这保证了所有可能的循环子串都可以在新的字符串中找到。 - 哈希值计算:通过滚动哈希算法计算字符串的哈希值,使得字符串比较的时间复杂度降到 O(1)。
- 比较哈希值:判断字符串
T是否为s的循环子串,通过比较T和s + s中的所有子串的哈希值。
契合匹配:
代码详解:
import java.util.Scanner;
public class std {
private static final int MOD = 998244353; // 哈希值取模常量
private static final int SEED = 233; // 哈希种子
private static final int N = 2000005; // 最大字符数
private static long[] h = new long[N]; // 存储字符串的哈希值
private static long[] qp = new long[N]; // 存储 SEED^i % MOD,用于滚动哈希
private static long hashT; // 存储字符串 T 的哈希值
// 初始化哈希值计算
public static void initHash(String s) {
qp[0] = 1; // SEED^0 % MOD = 1
h[0] = s.charAt(0) % MOD; // 第一个字符的哈希值
for (int i = 1; i < s.length(); i++) {
// 计算当前位置的哈希值
h[i] = (h[i - 1] * SEED % MOD + s.charAt(i)) % MOD;
// 计算 SEED^i % MOD
qp[i] = (qp[i - 1] * SEED) % MOD;
}
}
// 获取字符串 s 从位置 l 到 r 的子串的哈希值
public static long getHash(int l, int r) {
if (l == 0) return h[r]; // 如果 l 是 0,直接返回 h[r]
return (h[r] + MOD - (h[l - 1] * qp[r - l + 1] % MOD)) % MOD; // 使用滚动哈希公式计算
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(); // 输入字符串 s 的长度
String s = scanner.next(); // 输入字符串 s
s = s + s; // 将字符串 s 扩展为 s + s
String T = scanner.next(); // 输入字符串 T
// 初始化哈希值计算
initHash(s);
// 计算字符串 T 的哈希值
char c = T.charAt(0);
// 处理大小写,使得 T 中的字符都统一为小写或大写
if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; // 如果是大写字母转小写
else c += 'A' - 'a'; // 如果是小写字母转大写
// 计算 T 的哈希值
hashT = c % MOD;
for (int i = 1; i < T.length(); i++) {
c = T.charAt(i);
if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
else c += 'A' - 'a';
// 更新 T 的哈希值
hashT = (hashT * SEED + c) % MOD;
}
// 寻找最小的匹配位置
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
// 如果 s + s 中从 i 到 i + n - 1 的子串与 T 的哈希值相同
if (getHash(i, i + n - 1) == hashT) {
// 更新最小匹配的位置
ans = Math.min(ans, i);
ans = Math.min(ans, n - i); // 比较环绕的部分
}
}
// 输出结果
if (ans <= n) {
System.out.println("Yes");
System.out.println(ans);
} else {
System.out.println("No");
}
scanner.close();
}
}
关键部分注释:
-
initHash(String s):初始化哈希数组h[]和预计算数组qp[],其中h[i]存储字符串s的前i个字符的哈希值,而qp[i]存储SEED^i % MOD。这样可以高效地计算子串的哈希值。 -
getHash(int l, int r):通过滚动哈希技术,计算字符串s从位置l到r的子串的哈希值。使用h[]和qp[]数组高效计算,时间复杂度为 O(1)。 -
主函数逻辑:
- 首先将
s扩展为s + s,这样可以保证所有可能的循环子串都能在扩展后的字符串中找到。 - 接着,计算字符串
T的哈希值。 - 最后,遍历
s + s中的所有子串,查找与T的哈希值相同的子串,并计算最小的匹配位置(即偏移量)。
- 首先将
-
ans的更新:当发现一个匹配的子串时,更新ans,并通过Math.min(ans, n - i)考虑环绕的情况,确保输出的是最小的偏移量。
代码的时间复杂度:
- 初始化哈希值的时间复杂度是 O(n)。
- 获取哈希值的时间复杂度是 O(1)。
- 遍历
s + s中的子串并比较哈希值的时间复杂度是 O(n)。 因此,整个算法的时间复杂度是 O(n),非常高效。
总结:
该代码通过使用滚动哈希算法,避免了直接比较字符串的方法,从而在大规模输入的情况下仍然能够高效地解决问题。通过扩展字符串 s 为 s + s,它能够检测所有的循环子串,并输出是否匹配及其最小偏移量。
浙公网安备 33010602011771号