【算法总结】字符串哈希

简介: 【算法总结】字符串哈希

字符串哈希

理论基础

  • 字符串哈希/字符串编码是什么?
    字符串编码是一种常用的字符匹配方法。一般地,一个字符串 s 的编码值计算公式如下:

image.png

其核心是将字符串看成一个 k 进制的整数,其中 k 是字符串中可能出现的字符种类,本题中字符串只包含小写字母,即 k = 26(也可以取比 k 大的整数,一般来说可以取一个质数,

例如 29 或 31)。这样做的好处是绕开了字符串操作,将字符串看成整数进行比较,并可以在常数时间内将字符串加入哈希集合中。



  • 什么时候两个字符串相同?
    当两个字符串的长度相同并且编码值也相等时,这两个字符串相同
  • 如何预处理出不同长度字符的编码表示?


image.png

  • 在实际编码中,当字符串的长度很长时,对应的哈希编码值也会很大,甚至会超出整数类型的范围。对此,一般的解决方法是对字符的编码值进行取模(MOD),使其保持在整数类型的范围之内。


  • 哈希冲突:字符串哈希本身存在哈希冲突的可能,一般会在尝试131之后尝试使用13131 ,然后再尝试使用更大的质数。
  • base如何选择?
    将字符串表示为一个 P 进制的数,P 是一个经验值,P = 131 / 13331/ 131313 不会出现哈希值冲突(概率很小,所以不考虑冲突)


相关题目

重复的DNA序列【LC187】

DNA序列 由一系列核苷酸组成,缩写为 'A', 'C', 'G''T'.。

  • 例如,"ACGAATTCCG" 是一个 DNA序列

在研究 DNA 时,识别 DNA 中的重复序列非常有用。

给定一个表示 DNA序列 的字符串 s ,返回所有在 DNA 分子中出现不止一次的 长度为 10 的序列(子字符串)。你可以按 任意顺序 返回答案。



  • 思路计算每个长度为10的子字符串的字符串哈希编码值,当遇到相同的哈希编码值时,将该字符串放入结果集中
  • 注意:不严谨,未判断长度是否相同
  • 实现

字符串哈希的「构造 数组」和「计算哈希」的过程,不会溢出吗?

会溢出,溢出就会变为负数,当且仅当两个哈希值溢出程度与 Integer.MAX_VALUE 呈**不同【相同?】**的倍数关系时,会产生错误结果(哈希冲突),此时考虑修改 或者采用表示范围更大的 long 来代替 int。->不取余也是可以的

  • 溢出1->Integer.MIN_VALUE:-2147483648
class Solution {
    // private final static long MOD = 1000000007;
    public List<String> findRepeatedDnaSequences(String s) {
        int n = s.length();
        int base = 7;
        int[] h = new int[n + 1];
        int[] p = new int[n + 1];
        p[0] = 1;
        Map<Integer, Integer> count = new HashMap<>();
        List<String> res = new ArrayList<>();
        for (int i = 1; i <= n; i++){
            h[i] = h[i - 1] * base + s.charAt(i - 1);
            p[i] = p[i - 1] * base;
        }
        for (int i = 0; i <= n - 10; i++){
            int code = hash(h, p, i, i + 10 - 1);
            int cnt = count.getOrDefault(code, 0);
            if (cnt == 1){
                res.add(s.substring(i, i + 10));
            }
            count.put(code, cnt + 1);
        }
        return res;
    }
    private int hash(int[] h, int[] p, int l, int r){
        return h[r + 1] - h[l] * p[r - l + 1];
    }
}

image.png

class Solution {
    int N = (int)1e5+10, P = 131313;
    int[] h = new int[N], p = new int[N];
    public List<String> findRepeatedDnaSequences(String s) {
        int n = s.length();
        List<String> ans = new ArrayList<>();
        p[0] = 1;
        for (int i = 1; i <= n; i++) {
            h[i] = h[i - 1] * P + s.charAt(i - 1);
            p[i] = p[i - 1] * P;
        }
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 1; i + 10 - 1 <= n; i++) {
            int j = i + 10 - 1;
            int hash = h[j] - h[i - 1] * p[j - i + 1];
            int cnt = map.getOrDefault(hash, 0);
            if (cnt == 1) ans.add(s.substring(i - 1, i + 10 - 1));
            map.put(hash, cnt + 1);
        }
        return ans;
    }
}
作者:宫水三叶
链接:https://leetcode.cn/problems/repeated-dna-sequences/solutions/1035708/gong-shui-san-xie-yi-ti-shuang-jie-hua-d-30pg/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

不同的循环子字符串【LC1316】

给你一个字符串 text ,请你返回满足下述条件的 不同 非空子字符串的数目:

  • 可以写成某个字符串与其自身相连接的形式(即,可以写为 a + a,其中 a 是某个字符串)。

例如,abcabc 就是 abc 和它自身连接形成的。

暴力
  • 思路:
    暴力枚举每个子字符串,判断其后是否有相同字符串,如果有那么将字符串记录在哈希表中,最后返回哈希表的大小
  • 实现
class Solution {
    public int distinctEchoSubstrings(String text) {
        int n = text.length();
        Set<String> set = new HashSet<>();
        for (int i = 0; i < n; i++){
            for (int len = 1; 2 * len <= n - i; len++){
                if (text.substring(i, i + len).equals(text.substring(i + len, i + 2 * len))){
                    set.add(text.substring(i, i + len));
                }
            }
        }
        return set.size();
    }
}

image.png

字符串哈希
  • 思路
    通过字符串哈希将子字符串转化为long类型变量,降低放入哈希表的时间复杂度。

字符串哈希不再赘述,实现时只使用一个哈希表进行实现,未发生哈希冲突。保险起见可以使用n个哈希表实现


在「判断」这一步中,由于我们只对两个字符串进行比较,因此引入哈希冲突(在下方的注意事项中也有提及)的概率极小。然而在「去重」这一步中,最坏情况下字符串的数量为 O ( N 2 ) O(N^2)O(N

2

),大量的字符串造成哈希冲突的概率极大。为了减少字符串的数量以降低冲突的概率,我们可以使用 N 个哈希集合,分别存放不同长度的字符串,即第 m 个哈希集合存放长度为 m + 1 的字符串的哈希值。这样每个哈希集合只对某一固定长度的字符串进行去重操作,并且其中最多只有 N 个字符串,冲突概率非常低。


作者:力扣官方题解

链接:https://leetcode.cn/problems/distinct-echo-substrings/solutions/101305/bu-tong-de-xun-huan-zi-zi-fu-chuan-by-leetcode-sol/

来源:力扣(LeetCode)

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


实现

class Solution {
    private final static long MOD = 1000000007;
    public int distinctEchoSubstrings(String text) {       
        int n = text.length();
        int base = 31, res = 0;
        long[] h = new long[n + 1], p = new long[n + 1];
        p[0] = 1L;
        for (int i = 1; i <= n; i++){
            h[i] = (h[i - 1] * base + text.charAt(i - 1)) % MOD;
            p[i] = p[i - 1] * base % MOD;
        }
        Set<Long> seen = new HashSet<>();
        for (int i = 0; i < n; i++){
            for (int len = 1; 2 * len <= n - i; len++){
                long hashLeft = hash(h, p, i, i + len - 1);
                if (!seen.contains(hashLeft) && hashLeft == hash(h, p, i + len, i + 2 * len - 1)){
                    res++;
                    seen.add(hashLeft);
                }
            }
        }
        return res;
    }
    private long hash(long[] h, long[] p, int l, int r){
        return (h[r + 1] - h[l] * p[r - l + 1] % MOD + MOD) % MOD;
    }
}

image.png

最长重复子串【LC1044】

给你一个字符串 s ,考虑其所有 重复子串 :即 s 的(连续)子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。

返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 ""

字符串哈希【超时】
  • 字符串太长啦
class Solution {
    long[] h, p;
    public String longestDupSubstring(String s) {
        int n = s.length();
        this.h = new long[n + 1];
        this.p = new long[n + 1];
        p[0] = 1;
        int base = 1313131;
        Set<Long> set = new HashSet<>();
        int l = -1, r = -2;
        for (int i = 1; i <= n; i++){
            h[i] = h[i - 1] * base + s.charAt(i - 1);
            p[i] = p[i - 1] * base;
        }
        for (int i = 0; i < n; i++){
            for (int j = i; j < n; j++){
                long code = hash(i, j);
                if (set.contains(code) && j - i > r - l){
                    l = i;
                    r = j;
                }
                set.add(code);
            }
        }
        return l == -1 ? "" : s.substring(l, r + 1);
    }
    private long hash(int l, int r){
        return h[r + 1] - h[l] * p[r - l + 1];
    }
}

image.png

字符串哈希+二分答案
  • 思路
  • 二段性:题目要求得「能取得最大长度的任一方案」,首先以「最大长度」为分割点的数轴具有「二段性」:
  • 小于等于最大长度方案均存在(考虑在最大长度方案上做删减);
  • 大于最大长度的方案不存在。

image.png

class Solution {
    long[] h, p;
    public String longestDupSubstring(String s) {
        int P = 1313131, n = s.length();
        h = new long[n + 10]; p = new long[n + 10];
        p[0] = 1;
        for (int i = 0; i < n; i++) {
            p[i + 1] = p[i] * P;
            h[i + 1] = h[i] * P + s.charAt(i);
        }
        String ans = "";
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            String t = check(s, mid);
            if (t.length() != 0) l = mid;
            else r = mid - 1;
            ans = t.length() > ans.length() ? t : ans;
        }
        return ans;
    }
    String check(String s, int len) {
        int n = s.length();
        Set<Long> set = new HashSet<>();
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            long cur = h[j] - h[i - 1] * p[j - i + 1];
            if (set.contains(cur)) return s.substring(i - 1, j);
            set.add(cur);
        }
        return "";
    }
}
作者:宫水三叶
链接:https://leetcode.cn/problems/longest-duplicate-substring/solutions/1172474/gong-shui-san-xie-zi-fu-chuan-ha-xi-ying-hae9/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

image.png

最长快乐前缀【LC1392】

「快乐前缀」 是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。

给你一个字符串 s,请你返回它的 最长快乐前缀。如果不存在满足题意的前缀,则返回一个空字符串 ""

  • 思路
    将字符串使用字符串哈希算法进行编码,从大到小枚举长度判断前缀和后缀是否相同,如果相同直接返回该字符串
  • 实现
class Solution {
    public String longestPrefix(String s) {
        // 暴力O(n2)
        // 字符串哈希O(n)
        int n = s.length();
        long[] h = new long[n + 1];
        long[] p = new long[n + 1];
        int base = 13;
        p[0] = 1L;
        for (int i = 1; i <= n; i++){
            h[i] = h[i - 1] * base + s.charAt(i - 1);
            p[i] = p[i - 1] * base;
        }
        for (int len = n - 1; len >= 1 ; len--){
            if (code(h, p, 0, len - 1) == code(h, p, n - len, n - 1)){
                return s.substring(0, len);
            }
        }
        return "";
    }
    public long code(long[] h, long[] p, int l, int r){
        return h[r + 1] - h[l] * p[r - l + 1];
    }
}

image.png

相等行列对【LC2352】

给你一个下标从 0 开始、大小为 n x n 的整数矩阵 grid ,返回满足 Ri 行和 Cj 列相等的行列对 (Ri, Cj) 的数目*。*

如果行和列以相同的顺序包含相同的元素(即相等的数组),则认为二者是相等的。

映射+哈希表
  • 思路
    将每个数字映射为字符,那么可以将每行每列用字符串表示,然后求出每行对应的字符串及其出现次数,再求出每列对应的字符串,如果存在相同的行,那么累加行出现的次数
  • 实现
class Solution {
    public int equalPairs(int[][] grid) {
        int n = grid.length;
        int res = 0;
        HashMap<String, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++){
            StringBuilder row = new StringBuilder();
            for (int j = 0; j < n; j++){
                row.append((char)('0' + grid[i][j]));
            }
            map.put(row.toString(), map.getOrDefault(row.toString(), 0) + 1);           
        }
        for (int i = 0; i < n; i++){
            StringBuilder col = new StringBuilder();
            for (int j = 0; j < n; j++){
                col.append((char)('0' + grid[j][i]));
            }
            res += map.getOrDefault(col.toString(), 0);          
        }
        return res;
    }
}

image.png

字符串哈希
  • 思路将每行每列使用字符串哈希算法进行编码,其他同上
  • grid[i][j]相当于某个字符的整数形式,所以1-11和11-1所代表的值是不同的
  • 实现
class Solution {
    public int equalPairs(int[][] grid) {
        int n = grid.length;
        int base = 7;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++){
            int h = 0;
            for (int j = 0; j < n; j++){
                h = h * base + grid[i][j];
            }
            map.put(h, map.getOrDefault(h, 0) + 1);
        }
        int res = 0;
        for (int i = 0; i < n; i++){
            int h = 0;
            for (int j = 0; j < n; j++){
                h = h * base + grid[j][i];
            }
            res += map.getOrDefault(h, 0);          
        }
        return res;
    }
}


目录
相关文章
|
2月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
57 3
|
23天前
|
算法 安全
散列值使用相同的哈希算法
当使用相同的哈希算法对相同的数据进行散列时,所产生的散列值(也称为哈希值或摘要)总是相同的。这是因为哈希算法是一种确定性的函数,它对于给定的输入将始终产生相同的输出。 例如,如果你用SHA-256算法对字符串"hello world"进行哈希处理,无论何时何地,只要输入是完全一样的字符串,你都会得到相同的160位(40个十六进制字符)的SHA-256散列值。 但是,需要注意的是,即使是输入数据的微小变化也会导致产生的散列值完全不同。此外,不同的哈希算法(如MD5、SHA-1、SHA-256等)会对相同的数据产生不同的散列值。 哈希算法的一个关键特性是它们的“雪崩效应”,即输入中的一点小小
30 4
|
4月前
|
算法
【算法】滑动窗口——找到字符串中所有字母异位词
【算法】滑动窗口——找到字符串中所有字母异位词
|
2月前
|
算法
两个字符串匹配出最长公共子序列算法
本文介绍了最长公共子序列(LCS)问题的算法实现,通过动态规划方法求解两个字符串的最长公共子序列,并提供了具体的编程实现细节和示例。
104 1
两个字符串匹配出最长公共子序列算法
|
2月前
|
存储 算法 C#
C#哈希查找算法
C#哈希查找算法
|
2月前
|
算法 安全 Go
Python与Go语言中的哈希算法实现及对比分析
Python与Go语言中的哈希算法实现及对比分析
51 0
|
2月前
|
存储 算法 C++
【算法】哈希映射(C/C++)
【算法】哈希映射(C/C++)
|
4月前
|
算法 安全 JavaScript
安全哈希算法:SHA算法
安全哈希算法:SHA算法
103 1
安全哈希算法:SHA算法
|
4月前
|
算法 Java
掌握算法学习之字符串经典用法
文章总结了字符串在算法领域的经典用法,特别是通过双指针法来实现字符串的反转操作,并提供了LeetCode上相关题目的Java代码实现,强调了掌握这些技巧对于提升算法思维的重要性。
|
4月前
|
JavaScript 算法 前端开发
国标哈希算法基础:SHA1、SHA256、SHA512、MD5 和 HMAC,Python和JS实现、加盐、算法魔改
国标哈希算法基础:SHA1、SHA256、SHA512、MD5 和 HMAC,Python和JS实现、加盐、算法魔改
653 1

热门文章

最新文章