Rabin-Karp 字符串查找算法
和一般的比较字符串的方式不同,Rabin-Karp 查找算法通过对子字符串进行 hash,如果在原有字符串中找到了 hash 值相同的字符串,那么继续比较是否是需要查找的字串,一般来讲,如果 hash 操作做的很好的话,那么一般一次匹配就是待查找的子串
基本思想
长度为 \(M\) 的字符串对应着一个 \(R\) 进制的 \(M\) 位数。为了能够使用一张大小为 \(Q\) 的散列表来保存这种类型的键,需要一个能够将 \(R\) 进制的 \(M\) 位数值转换为一个 \(0\) 到 \(Q - 1\) 的整数,在实际中,\(Q\) 会是一个比较大的素数。
例如,假设现在要搜索的目标字符串为 1234,假设现在将 \(Q\) 取为 \(10007\),这里由于目标字符串都是数字,因此可以考虑将其直接对 \(Q\) 进行取模操作,得到 \(mod=1234\)。为了简单起见,假设待搜索的字符串的所有字符都是数字,为 011122123456,那么查找的过程如下所示:

当然,实际使用的过程中不能直接将字符串转换为对应的整数,一般会通过某种方式将字符串转换为对应的整数,如下面的 hash 函数:
private long hash(String key, int M) {
long h = 0;
for (int i = 0; i < M; ++i)
h = (R*h + key.charAt(i)) % Q;
return h;
}i
事实上,由于这种 hash 的存在,会使得搜索的时间复杂度在最坏的情况下为 \(O(NM)\),相比较一般的暴力搜索,该方式没有任何性能上的改进。
Rabin-Karp 则通过某种方式减少了每个子串的 hash 操作,具体为:
对于原字符串所有的位置 \(i\),高效地计算文本中 \(i + 1\) 位置的子字符串的
hash值使用 \(t_{i}\) 表示
txt.charAt(i),那么文本txt中起始位置为 \(i\),含有 \(M\) 个字符的子串对应的数为:\[x_{i} = t_{i}R^{M - 1} + t_{i + 1}R^{M - 2} + …… + t_{i + M -1}R^0
\]假设现在的
hash函数为一般的 \(h(x_{i}) = x_{i} \mod Q\),那么将模式字符串右移一位等价于将 \(x_{i}\) 替换为:\[x_{i + 1} = (x_{i} - t_{i}R^{M -1})R + t_{i + M}
\]即:\(i + 1\) 位置的子字符串的散列值为当前处理的子串的散列值减去子串第一个字符的
hash值,然后再乘以 \(R\) 再加上最后一个字符的散列值
这是 Rabin-Karp 算法的核心思想,该方式可以保证在搜索的过程中以常数的时间复杂度进行搜索操作
实现
import java.math.BigInteger;
import java.util.concurrent.ThreadLocalRandom;
public class RabinKarp {
private final String pat; // 待查找的模式字符串
private final long patHash; // 模式字符串的 hash 值
private final int M; // 模式字符串的长度
private final long Q; // 大素数
private final int R; // 进制数,默认为 256
private final long RM; // R^{M - 1}
public RabinKarp(String pat) {
this.pat = pat;
this.R = 256;
M = pat.length();
Q = longRandomPrime();
long rm = 1;
for (int i = 1; i < M; i++) {
rm = (R * rm) % Q;
}
RM = rm;
patHash = hash(pat, M);
}
// 在 txt 中搜索是 pat,如果不存在,返回 txt 的长度
public int search(String txt) {
int N = txt.length();
if (N < M) return N;
long txtHash = hash(txt, M);
if (txtHash == patHash && check(txt, 0)) return 0;
for (int i = M; i < N; ++i) {
// 带入公式,假设这里不会出现 long 整数溢出
txtHash = txtHash - RM*txt.charAt(i - M);
txtHash = txtHash*R + txt.charAt(i);
if (txtHash == patHash && check(txt, i - M + 1)) {
return i - M + 1;
}
}
return N;
}
// 检查 hash 匹配的两个字符串是否相等
private boolean check(String txt, int i) {
for (int j = 0; j < M; ++j) {
if (txt.charAt(i + j) != pat.charAt(j)) {
System.out.println("check false"); // 理论上来讲会执行的概率特别低
return false;
}
}
return true;
}
// 生成子串对应的 hash 值
private long hash(String key, int len) {
long h = 0;
for (int j = 0; j < len; j++) {
h = (R*h + key.charAt(j)) % Q;
}
return h;
}
// 随机生成一个大的素数
private long longRandomPrime() {
BigInteger prime = BigInteger.probablePrime(31,
ThreadLocalRandom.current()
);
return prime.longValue();
}
}
参考:
[1] 《算法(第四版)》
Rabin-Karp 字符串查找算法的更多相关文章
- Rabin-Karp字符串查找算法
1.简介 暴力字符串匹配(brute force string matching)是子串匹配算法中最基本的一种,它确实有自己的优点,比如它并不需要对文本(text)或模式串(pattern)进行预处理 ...
- KMP 算法 & 字符串查找算法
KMP算法 Knuth–Morris–Pratt algorithm 克努斯-莫里斯-普拉特 算法 algorithm kmp_search: input: an array of character ...
- Rabin-Karp指纹字符串查找算法
首先计算模式字符串的散列函数, 如果找到一个和模式字符串散列值相同的子字符串, 那么继续验证两者是否匹配. 这个过程等价于将模式保存在一个散列表中, 然后在文本中的所有子字符串查找. 但不需要为散列表 ...
- 字符串查找算法的改进-hash查找算法
字符串查找即为特征查找: 特征即位hash: 1.将待查找的字符串hash: 2.在容器字符串中找头字符匹配的字符串,并进行hash: 3.比较hash的结果:相同即位匹配: hash算法的设计为其中 ...
- 字符串查找算法总结(暴力匹配、KMP 算法、Boyer-Moore 算法和 Sunday 算法)
字符串匹配是字符串的一种基本操作:给定一个长度为 M 的文本和一个长度为 N 的模式串,在文本中找到一个和该模式相符的子字符串,并返回该字字符串在文本中的位置. KMP 算法,全称是 Knuth-Mo ...
- KMP字符串查找算法
#include <iostream> #include <windows.h> using namespace std; void get_next(char *str,in ...
- Sunday算法(字符串查找、匹配)
字符串查找算法中,最著名的两个是KMP算法(Knuth-Morris-Pratt)和BM算法(Boyer-Moore).两个算法在最坏情况下均具有线性的查找时间.但是在实用上,KMP算法并不比最简单的 ...
- 数据结构与算法--Boyer-Moore和Rabin-Karp子字符串查找
数据结构与算法--Boyer-Moore和Rabin-Karp子字符串查找 Boyer-Moore字符串查找算法 注意,<算法4>上将这个版本的实现称为Broyer-Moore算法,我看了 ...
- 字符串类——KMP子串查找算法
1, 如何在目标字符串 s 中,查找是否存在子串 p(本文代码已集成到字符串类——字符串类的创建(上)中,这里讲述KMP实现原理) ? 1,朴素算法: 2,朴素解法的问题: 1,问题:有时候右移一位是 ...
- 字符串查找String.IndexOf
String.indexOf的模拟实现,没想象中有多么高深的查找算法,就是最普通的遍历查找 思路:先找到第一个相同的字符,然后依次比较后面的字符,若都相等则表示查找成功 /** * 查找字符串patt ...
随机推荐
- BS系统的登录鉴权流程演变
1 基础知识 用户登录是使用指定用户名和密码登录到系统,以对用户的私密数据进行访问和操作.在一个有登录鉴权的BS系统中,通常用户访问数据时,后端拦截请求,对用户进行鉴权,以验证用户身份和权限.用户名. ...
- 文心一言 VS 讯飞星火 VS chatgpt (110)-- 算法导论10.2 1题
一.用go语言,单链表上的动态集合操作 INSERT 能否在 O(1)时间内实现?DELETE 操作呢? 文心一言: 在单链表中,INSERT 和 DELETE 操作的时间复杂度通常是 O(n),其中 ...
- 今天的第二道tarjan:受欢迎的牛
原题来自:USACO 2003 Fall 题目描述 每头奶牛都梦想成为牛棚里的明星.被所有奶牛喜欢的奶牛就是一头明星奶牛.所有奶牛都是自恋狂,每头奶牛总是喜欢自己的.奶牛之间的"喜欢&quo ...
- P8684 [蓝桥杯 2019 省 B] 灵能传输 题解
P8684 [蓝桥杯 2019 省 B] 灵能传输 题解 Part 1 提示 题目传送门 欢迎大家指出错误并私信这个蒟蒻 欢迎大家在下方评论区写出自己的疑问(记得 @ 这个蒟蒻) Part 2 更新日 ...
- linux 批量替换文件内容
1.批量查找某个目下文件的包含的内容,例如: # grep -rn "要找查找的文本" ./ 2.批量查找并替换文件内容. # sed -i "s/要找查找的文本 ...
- P3870 [TJOI2009] 开关(线段树)
P3870 [TJOI2009] 开关 思路:可以用线段树来维护区间中亮灯的个数,区间修改用加上懒标记就好 #include <bits/stdc++.h> #define LL long ...
- 夯实JAVA基本之一 —— 泛型详解(1):基本使用(转)
一.引入1.泛型是什么首先告诉大家ArrayList就是泛型.那ArrayList能完成哪些想不到的功能呢?先看看下面这段代码:ArrayList<String> strList = ne ...
- CSS属性 Position的几种定位方式
作者:WangMin 格言:努力做好自己喜欢的每一件事 在讲几种定位方式之前,我们先来了解一下什么是普通流(normal flow)? 除非专门指定,否则所有框都在普通流中定位.普通流中元素框的位置由 ...
- 2023-11-22:用go语言,给你一个长度为 n 下标从 0 开始的整数数组 nums。 它包含 1 到 n 的所有数字,请你返回上升四元组的数目。 如果一个四元组 (i, j, k, l) 满足
2023-11-22:用go语言,给你一个长度为 n 下标从 0 开始的整数数组 nums. 它包含 1 到 n 的所有数字,请你返回上升四元组的数目. 如果一个四元组 (i, j, k, l) 满足 ...
- 从一个 Demo 说起 Dubbo3
简介 2017年的9月份,阿里宣布重启Dubbo的开发维护,并且后续又将Dubbo捐献给了Apache,经过多年的发展已经发布到3.X版本了,Dubbo重启维护之后是否有值得我们期待的功能呢,下面就来 ...