当前位置: 首页 > article >正文

算法基础 -- 字符串哈希的原理与数值选定的剖析

字符串哈希的原理与数值选定的剖析

1. 什么是字符串哈希

字符串哈希(String Hashing)是一种用于快速比较字符串的技术,核心思想是:
将一个字符串 S S S 通过某种哈希函数转换为一个固定长度的整数(通常是一个较大的数模一个素数),然后用于字符串比较、搜索等场景。

应用场景:

  • 字符串比较(例如,在字典或哈希表中查找字符串)
  • 字符串去重(例如,检测重复子串)
  • 子串匹配(例如,Rolling Hash 用于 Rabin-Karp 算法)
  • 字符串相似性检测(例如,用于数据去重或查找相似文本)

2. 基本哈希函数:多项式哈希

最常用的字符串哈希方法是多项式哈希(Polynomial Hashing),公式如下:

H ( S ) = ( S 0 ⋅ P 0 + S 1 ⋅ P 1 + S 2 ⋅ P 2 + . . . + S n − 1 ⋅ P n − 1 ) m o d    M H(S) = (S_0 \cdot P^0 + S_1 \cdot P^1 + S_2 \cdot P^2 + ... + S_{n-1} \cdot P^{n-1}) \mod M H(S)=(S0P0+S1P1+S2P2+...+Sn1Pn1)modM

其中:

  • S i S_i Si 是字符串第 i i i 个字符的 ASCII 码值(或其他数值映射)
  • P P P进制基数(Base)
  • M M M取模素数(Modulus)
  • n n n 是字符串长度

示例:
对于字符串 "abcd",使用:

  • P = 31 P = 31 P=31
  • M = 1 0 9 + 7 M = 10^9+7 M=109+7 (一个大素数)

计算哈希值:
H ( " a b c d " ) = ( 97 ⋅ 3 1 0 + 98 ⋅ 3 1 1 + 99 ⋅ 3 1 2 + 100 ⋅ 3 1 3 ) m o d    1 0 9 + 7 H("abcd") = (97 \cdot 31^0 + 98 \cdot 31^1 + 99 \cdot 31^2 + 100 \cdot 31^3) \mod 10^9+7 H("abcd")=(97310+98311+99312+100313)mod109+7


3. 进制基数 P P P 和模数 M M M 的选择原则

哈希算法的核心是选择合适的基数 P P P模数 M M M,以降低哈希冲突,提高计算效率。

(1) 进制基数 P P P 的选择
  • 要求是一个质数(Prime Number)
    • 质数能减少哈希碰撞,因为它们的因子较少,能让哈希值分布更均匀。
    • 常见的选择有 31、37、53、101、131
  • 选择比字符集大小略大的质数
    • 例如,ASCII 码字符(最大 127),可以选 3137 作为 P P P
    • Unicode 字符(最大 65535),可以选 101、131
(2) 模数 M M M 的选择
  • 要求是一个大质数(通常接近 2 31 2^{31} 231 2 63 2^{63} 263
    • 避免哈希碰撞,提高安全性。
    • 常见的取值:
      • 1 0 9 + 7 10^9+7 109+7 2 30 2^{30} 230 量级)
      • 998244353 998244353 998244353(适用于模运算优化)
      • 2 61 − 1 = 2305843009213693951 2^{61} - 1 = 2305843009213693951 2611=2305843009213693951(更大范围,减少冲突)
  • 避免与计算机整数溢出冲突
    • 选择 1 0 9 + 7 10^9+7 109+7 之类的素数,可以确保 32-bit 系统不会溢出。

4. 滚动哈希(Rolling Hash)

子串搜索(如 Rabin-Karp 算法)中,常常需要快速计算子串的哈希值。
朴素方法的时间复杂度是 O ( n m ) O(nm) O(nm)(其中 n n n 是文本长度, m m m 是模式串长度),而滚动哈希能将其优化到 O ( n ) O(n) O(n)

滚动哈希公式(假设使用 P = 31 P=31 P=31 M = 1 0 9 + 7 M=10^9+7 M=109+7):

给定字符串 “abcdef”,如果已知 “abc” 的哈希值 H ( " a b c " ) H("abc") H("abc"),计算 “bcd” 的哈希值:

H ( b c d ) = ( H ( a b c ) − S 0 ⋅ P 2 ) ⋅ P + S 3 m o d    M H(bcd) = (H(abc) - S_0 \cdot P^2) \cdot P + S_3 \mod M H(bcd)=(H(abc)S0P2)P+S3modM

实现:

long long rolling_hash(long long prev_hash, char out_char, char in_char, long long P, long long M, long long highest_pow) {
    return ((prev_hash - (out_char * highest_pow) % M + M) * P + in_char) % M;
}

其中:

  • p r e v _ h a s h prev\_hash prev_hash 是前一个子串的哈希值
  • o u t _ c h a r out\_char out_char 是被移出的字符
  • i n _ c h a r in\_char in_char 是新加入的字符
  • h i g h e s t _ p o w highest\_pow highest_pow P m − 1 P^{m-1} Pm1(最高次幂预计算)

5. 哈希碰撞的处理

由于哈希函数不是 单射(Injective),不同的字符串可能映射到同一个哈希值,称为碰撞(Collision)

(1) 使用多个哈希值(双哈希、多哈希)
  • 采用不同的 P P P M M M 计算多个哈希值,提高唯一性:
    H 1 ( S ) = ( S 0 ⋅ 3 1 0 + S 1 ⋅ 3 1 1 + S 2 ⋅ 3 1 2 ) m o d    1 0 9 + 7 H_1(S) = (S_0 \cdot 31^0 + S_1 \cdot 31^1 + S_2 \cdot 31^2) \mod 10^9+7 H1(S)=(S0310+S1311+S2312)mod109+7
    H 2 ( S ) = ( S 0 ⋅ 3 7 0 + S 1 ⋅ 3 7 1 + S 2 ⋅ 3 7 2 ) m o d    998244353 H_2(S) = (S_0 \cdot 37^0 + S_1 \cdot 37^1 + S_2 \cdot 37^2) \mod 998244353 H2(S)=(S0370+S1371+S2372)mod998244353
  • 只有当 H 1 ( S ) H_1(S) H1(S) H 2 ( S ) H_2(S) H2(S) 都相等,才认为字符串匹配。
(2) 选择更大的 M M M
  • 2 61 − 1 2^{61}-1 2611 这样的超大素数能减少碰撞的可能性。
(3) 使用拉比诺验算法(Rabin Test)
  • 在哈希匹配后,使用额外的字符串比较来确认字符串是否完全相同。

6. 总结

  • 多项式哈希 是最常用的字符串哈希方法,基于 P i P^i Pi 计算。
  • P P P 选择质数(如 31, 37, 53),避免哈希碰撞。
  • M M M 选择大质数(如 1 0 9 + 7 10^9+7 109+7),提高哈希分布的均匀性。
  • 滚动哈希 让子串匹配更高效,时间复杂度从 O ( n m ) O(nm) O(nm) 降到 O ( n ) O(n) O(n)
  • 多哈希/大素数哈希 进一步减少哈希冲突。

7. 示例代码(C 实现)

#include <stdio.h>

#define P 31
#define M 1000000007

// 计算字符串的哈希值
unsigned long long compute_hash(const char *s) {
    unsigned long long hash_value = 0;
    unsigned long long p_pow = 1;
    
    while (*s) {
        hash_value = (hash_value + (*s - 'a' + 1) * p_pow) % M;
        p_pow = (p_pow * P) % M;
        s++;
    }
    return hash_value;
}

int main() {
    const char *s = "hello";
    printf("Hash Value: %llu\n", compute_hash(s));
    return 0;
}

输出

Hash Value: 99162322

希望这篇讲解能让你彻底理解字符串哈希的原理与数值选定策略!如果有进一步的问题,欢迎继续讨论!


http://www.kler.cn/a/571328.html

相关文章:

  • C++发展
  • 使用 Spring Boot 实现前后端分离的海康威视 SDK 视频监控
  • 低成本、高效率的物理驱动数据生成框架,助力接触丰富的机器人操作任务
  • sentinel集成nacos
  • 04 高效HarmonyOS NEXT编程:ArkTS数据结构优化与属性访问最佳实践
  • C++小课堂——构造函数与析构函数
  • Spring Cloud — 消息驱动 Stream
  • [python] del
  • 字节旗下两款AI编程工具
  • MySQL——DDL、DML
  • 从 ISO 到 GMT+8:Vue 前端时间格式的奇妙之旅!
  • 软件接口(API)自动化测试 顶级框架 封装
  • Spark 中分区相关设置
  • 拉格朗日对偶性(Lagrangian Duality)详解
  • 国产编辑器EverEdit - 优化性能的一些设置项
  • 74道高级Java面试合集,java开发模式面试题
  • 【http://noi.openjudge.cn/】4.3算法之图论——1538:Gopher II
  • 14天 -- Redis 的持久化机制有哪些?Redis 主从复制的实现原理是什么? Redis 数据过期后的删除策略是什么?
  • DeepSeek开源周-汇总
  • VB6网络通信软件开发,上位机开发,TCP网络通信,读写数据并处理,完整源码下载