(f1)Karp-Rabin算法:串即是数

官官方信息技术老师·16 页·深入(追求细节与边界)·0 次浏览·38 天前
字符串匹配哈希算法

Karp-Rabin:串即是数

看完你能把字符串匹配彻底转化为数字比较,说清哈希滚动的每一步

按 空格/→ 演示下一步

问一句
1 / 16 页

全部页面点击任意一页,跳回舞台从这页播放

字符串匹配哈希算法

Karp-Rabin:串即是数

看完你能把字符串匹配彻底转化为数字比较,说清哈希滚动的每一步

1第 1 页 · Karp-Rabin:串即是数

暴力匹配慢在哪

在文本里找关键词,最直接的办法是逐位对齐、逐个字符比对。可一旦中途不匹配,就得往回退一格、重新开始——大部分时间都耗在重复比较上。能不能一次比较就判断整段匹配?

回溯代价
每失败一次就右移一位,最坏 O(mn),字符被反复比较
重复劳动
主串前缀信息其实早已看过,暴力法却丢弃不用
一个数代替一串
若把子串看作数,比较两个数即可判断整段是否匹配
条形码扫码对应 →串比较

扫一个码对应一整串商品,不必逐位核对每个数字

2第 2 页 · 暴力匹配慢在哪

化串为数:进制编码思想

前页结论:暴力匹配慢在逐字符比较。那有没有办法一次就比完?想想「314」这个数——我们从不逐位比较它,而是直接当成一个整数比大小。串能不能也这样变成一个数?

字符即数位
把每个字符的编码值当作一个数位
串即大整数
整段串按进制展开,就是一个大整数
进制选基数
取进制 d ≥ 字符集大小,保证编码不冲突
十进制数 314对应 →串的编码值

3、1、4 对应串中各字符,位置即权重

H("abc")=a⋅d2+b⋅d+cH("abc")=a\cdot d^{2}+b\cdot d+c
3第 3 页 · 化串为数:进制编码思想

如何把串变成数

把串当作进位数:选进制、定映射、按位展开、求和取值。

1
选定进制
字母共 26 种,取进制 d=26,每一位有 26 种取值
2
数位映射
把字母对应到数字:a→0,b→1,…,z→25
3
按位展开
串 cab:c 是最高位,按 d²、d¹、d⁰ 逐位写出权
4
求和取值
2×26²+0×26+1×26⁰=1353,串 cab 变成数 1353
4第 4 页 · 如何把串变成数

化串为数的实现

python

这段 Python 把「按位展开」写成代码:字符映射成数字,再累乘求和。

代码高亮加载中…

ord 把字符变成 0~255 的「数位」;h*BASE+val 正是十进制按位展开的进制版:左移一位、落新位。

5第 5 页 · 化串为数的实现

凡物皆数

前面几页我们用进制编码把字符串变成了一个大整数,匹配变成了比大小。其实字符串只是特例——任何能序列化的对象,本质都是一串符号,也就都能变成数。

串是符号序列
字符串只是最常见的一种序列,编码规则不关心内容
对象先序列化
图像、矩阵、音频都先转成字节流,再套进制编码
数化即哈希
大整数太长,Karp-Rabin 取模把它压成可比较的指纹
身份证号对应 →对象编码为数

身份证号把一个人的身份信息映射成一个数字

H(s)=(∑i=0m−1si⋅bm−1−i) mod pH(s)=\left(\sum_{i=0}^{m-1} s_i \cdot b^{m-1-i}\right) \bmod p
6第 6 页 · 凡物皆数

凡物皆数与化串为数

二者名字相近、常被混为一谈,但一个是具体算法技巧,一个是可以带走的思想原则。

化串为数
  • Karp-Rabin 的具体手法
  • 进制编码:串映射成整数
  • 配合滚动哈希,用在单点
凡物皆数
  • 可推广到万物的一般原则
  • 任意对象都可映射为数
  • 换场景可复用:去重、指纹
正确的理解是:化串为数是凡物皆数在字符串匹配上的一个实例;前者是战术,后者是战略。
7第 7 页 · 凡物皆数与化串为数

串亦是数:窗口数值的意义

上一页我们把子串按进制编码成了一个数。现在关键一步:既然串变成了数,判断两个子串是否相同,就不用逐字比对,直接比较两个数字是否相等——相等即匹配。

串的数值指纹
每个子串编码后对应一个数,数等则串等
比较降维
逐字符比较 O(m) 变成一次数字比较 O(1)
窗口滑动
在文本上逐位取长 m 的窗口,算出各自的数再比对
身份证号对应 →子串数值

验明身份不必查本人,比对号码即可

8第 8 页 · 串亦是数:窗口数值的意义

滚动:O(1) 更新指纹

相邻窗口数值的递推关系:减去最高位、乘进制、加新字符,常数时间滑动

滚动:O(1) 更新指纹
相邻窗口数值的递推关系:减去最高位、乘进制、加新字符,常数时间滑动
9第 9 页 · 滚动:O(1) 更新指纹

滑动窗口数值演化图

沿箭头看窗口右移一位:老数减去最高位贡献、乘进制、再加新字符,一步得到新指纹。

图解渲染中…
b1:减 H×R²,抹掉最高位的贡献c1:剩余 EL 整体左移一位,相当于乘进制 Rf1:每步取模 Q,指纹不超界
10第 10 页 · 滑动窗口数值演化图

Karp-Rabin 主流程

python

预处理哈希值,再逐窗口滚动更新,一次比对指纹即可判断匹配。

代码高亮加载中…

高亮行串起全流程:先化串为数得两枚指纹,主循环只比指纹;相等时回验防碰撞,随后滚动 O(1) 算出下一窗口,均摊 O(n)。

11第 11 页 · Karp-Rabin 主流程

数太大了怎么办

前一页我们能把整个窗口压成一个数,O(1) 滚动更新——但如果串长 1000,这个数就有上千位,内存根本存不下。怎么办?答案是:只留它的「余数」当指纹。

位宽爆炸
串越长,编码出的数越大,千位串即千位大整数
取模压缩
对一个大素数取余,把巨大数值压进固定宽度
指纹可比较
余数相同仍可快速判断,模运算保持同余关系
碰撞风险
余数相同原串未必相同,需回头比对一次
身份证后四位对应 →取模指纹

完整身份号太长,报后四位即可快速比对,但可能撞号

h(T)=(∑iTi b m−1−i) mod qh(T)=\Big(\sum_{i} T_i\, b^{\,m-1-i}\Big)\bmod q
12第 12 页 · 数太大了怎么办

指纹冲突与验证

前页我们给每个窗口算出一个指纹。可指纹只是数:两个人指纹相同,未必是同一个人。数值相等不等于串相等——碰撞必须想清楚怎么处理。

误报
指纹相同但串不同,散列碰撞导致假命中
必须验证
指纹相等时,再逐字符比对窗口与模式串
代价可控
碰撞罕见,验证只在命中时发生,摊还开销小
最坏情形
构造恶意输入可令每次都碰撞,退化为 O(nm)
火车站人脸闸机对应 →指纹验证

闸机先快速比对脸(指纹),存疑时才人工查证件(逐字符核验)

13第 13 页 · 指纹冲突与验证

自测:滚动更新

点击作答

已知窗口哈希值 h 和新进出的字符,如何 O(1) 得到下一个窗口的哈希值?

14第 14 页 · 自测:滚动更新

收束:串即是数

  • ✓把串编码成数,匹配变成数的比较
  • ✓预处理指纹后逐位滚动,窗口更新只需 O(1)
  • ✓指纹相等只代表可能相等,必须回原串验证
  • ✓思想可迁移:任何对象都能设计出可滚动的指纹
延伸主题:多模式匹配:Karp-Rabin 的强项双哈希进一步降低冲突概率对比 KMP/Z 算法的适用场景
15第 15 页 · 收束:串即是数

课后思考

先自己想一分钟,再看参考答案。开放问题没有唯一解,答案只是思考的起点。

1滚动更新为什么必须是「乘进制、加新位、减高位」三步,直接重算差在哪?

参考答案三步各管一事:乘 base 承接结构,加新位补增量,减高位去残留。直接重算是 O(m),滚动才保住 O(1)。可想想缺一步会怎样错位。

2如果指纹相等却不做逐字符验证,算法 worst case 会退化成什么样?

参考答案冲突时会把不匹配窗口当命中,正确性消失——这正是随机化的边界。可进一步想:取多个模数,冲突概率怎样下降?

3把「串即是数」推广到二维图像匹配,行、列两个方向的滚动还能同时 O(1) 吗?

参考答案先按行做一维哈希,再对指纹列纵向滚动,两遍一维嵌套仍均摊 O(1)。难点是移出元素要两层剥落。还能推广到树或图吗?

16第 16 页 · 课后思考
(f1)Karp-Rabin算法:串即是数 · 知识图解