【算法总结】字符串哈希

字符串哈希

理论基础

  • 字符串哈希/字符串编码是什么?

    字符串编码是一种常用的字符匹配方法。一般地,一个字符串 s 的编码值计算公式如下:
    e n c o d e ( s ) = ∑ i = 0 ∣ s ∣ − 1 s [ i ] ∗ b a s e ∣ s ∣ − i − 1 encode(s)= \sum _{i=0}^{|s|-1}s[i]*base^{|s|-i-1} encode(s)=i=0s1s[i]basesi1
    下标从0开始, b a s e base base为选定的进制单位。例如,s 仅包含 26个小写字母,因此 b a s e base base可取一个大于 26 的数(如 b a s e = 31 base=31 base=31)。

  • 其核心是将字符串看成一个 k 进制的整数,其中 k 是字符串中可能出现的字符种类,本题中字符串只包含小写字母,即 k = 26(也可以取比 k 大的整数,一般来说可以取一个质数,例如 29 或 31)。这样做的好处是绕开了字符串操作,将字符串看成整数进行比较,并可以在常数时间内将字符串加入哈希集合中。

  • 什么时候两个字符串相同?

    当两个字符串的长度相同并且编码值也相等时,这两个字符串相同

  • 如何预处理出不同长度字符的编码表示?

    • 对于以 s[i] 结尾的前缀字符串 s[0,i],我们可以预处理出其编码值,记为 h[i+1]。特别地, h [ 0 ] = 0 h[0]=0 h[0]=0

    • 可以得到以下递推关系
      h [ i + 1 ] = h [ i ] ∗ b a s e + e n c o d e ( s [ i ] ) h[i+1]=h[i]*base+encode(s[i]) h[i+1]=h[i]base+encode(s[i])

    • 根据前缀和的思想,可快速得到任一区间字符的编码表示:
      e n c o d e ( s [ l , r ] ) = h [ r + 1 ] − h [ l ] ∗ b a s e r − l + 1 encode(s[l,r])= h[r+1]-h[l]*base^{r-l+1} encode(s[l,r])=h[r+1]h[l]baserl+1

    • 为了加速运算, b a s e r − l + 1 base^{r-l+1} baserl+1这一项可以预处理出一个表示base进制的次方数组p
      p [ i ] = b a s e i p[i]=base^i p[i]=basei

  • 在实际编码中,当字符串的长度很长时,对应的哈希编码值也会很大,甚至会超出整数类型的范围。对此,一般的解决方法是对字符的编码值进行取模(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];
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n)
      • 空间复杂度: O ( n ) O(n) O(n)
  • 三叶

    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();
    
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n 3 ) O(n^3) O(n3),双重循环 O ( n 2 ) O(n^2) O(n2)+字符串放入哈希表 O ( n ) O(n) O(n)
      • 空间复杂度: O ( n 3 ) O(n^3) O(n3),最坏情况
字符串哈希
  • 思路

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

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

    在「判断」这一步中,由于我们只对两个字符串进行比较,因此引入哈希冲突(在下方的注意事项中也有提及)的概率极小。然而在「去重」这一步中,最坏情况下字符串的数量为 O ( N 2 ) O(N^2) O(N2),大量的字符串造成哈希冲突的概率极大。为了减少字符串的数量以降低冲突的概率,我们可以使用 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;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n 2 ) O(n^2) O(n2),双重循环 O ( n 2 ) O(n^2) O(n2)+字符串放入哈希表 O ( 1 ) O(1) O(1)
      • 空间复杂度: O ( n ) O(n) O(n)

最长重复子串【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];
    
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n 2 ) O(n^2) O(n2),双重循环 O ( n 2 ) O(n^2) O(n2)+字符串放入哈希表 O ( 1 ) O(1) O(1)
      • 空间复杂度: O ( n ) O(n) O(n)
字符串哈希+二分答案
  • 思路

    • 二段性:题目要求得「能取得最大长度的任一方案」,首先以「最大长度」为分割点的数轴具有「二段性」:

      • 小于等于最大长度方案均存在(考虑在最大长度方案上做删减);

      • 大于最大长度的方案不存在。

    • 因此可以使用二分答案的方式优化:二分的范围为 [ 0 , n ] [0,n] [0,n]

    • check函数:枚举所有某一长度的所有字符串,求出其哈希编码,如果有重复值,那么记录答案,向右搜索;如果没有重复值,那么向左搜索

  • 实现

    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)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度
      • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),二分答案 O ( n l o g n ) O(nlogn) O(nlogn)+字符串放入哈希表 O ( 1 ) O(1) O(1)
      • 空间复杂度: O ( n ) O(n) O(n)

最长快乐前缀【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];
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ) \mathcal{O}(n) O(n)
      • 空间复杂度: O ( n ) \mathcal{O}(n) O(n)

相等行列对【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;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n 3 ) \mathcal{O}(n^3) O(n3),双重循环 O ( n 2 ) O(n^2) O(n2)+字符串放入哈希表 O ( n ) O(n) O(n)
      • 空间复杂度: O ( n 2 ) \mathcal{O}(n^2) O(n2)
字符串哈希
  • 思路

    将每行每列使用字符串哈希算法进行编码,其他同上

    • 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;
        }
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
字符串哈希算法是一种将字符串映射为数字的算法,常用于字符串的比较和匹配。在C++中,可以使用字符串哈希算法来加速字符串的比较操作。 引用\[1\]中的代码示例展示了一个使用字符串哈希算法的C++代码。该代码使用了前缀和数组和字符串数组来存储字符串,并通过计算哈希值来比较两个子串是否相等。其中,哈希值的计算使用了前缀和数组和幂运算。 引用\[2\]中的解释指出,使用字符串哈希的目的是为了比较字符串时不直接比较字符串本身,而是比较它们对应映射的数字。这样可以将子串的哈希值的时间复杂度降低到O(1),从而节省时间。 引用\[3\]中的代码示例也展示了一个使用字符串哈希算法的C++代码。该代码使用了前缀和数组和字符串数组来存储字符串,并通过计算哈希值来比较两个子串是否相等。与引用\[1\]中的代码类似,哈希值的计算也使用了前缀和数组和幂运算。 综上所述,字符串哈希算法是一种将字符串映射为数字的算法,常用于字符串的比较和匹配。在C++中,可以使用前缀和数组和幂运算来计算字符串哈希值,并通过比较哈希值来判断两个子串是否相等。 #### 引用[.reference_title] - *1* [C++算法题 # 33 字符串哈希](https://blog.csdn.net/weixin_44536804/article/details/123425533)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [字符串哈希(c++)](https://blog.csdn.net/qq_41829492/article/details/120980055)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [AcWing 841. 字符串哈希(C++算法)](https://blog.csdn.net/YSA__/article/details/108453403)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值