算法导论——字符串搜索Rabin-Karp算法实现
Rabin-Karp算法是基于指纹的字符串匹配算法,这里就不得不提到一些听歌识曲的算法——Shazam,也是通过音乐指纹来进行乐曲的匹配。思路很简单,不过坑却很多。
理论
R a b i n − K a r p Rabin-Karp Rabin−Karp算法的理论很简单,要点有二:
- 其一,Hash函数求子字符串的指纹;
- 其二,已知前一个子字符串的指纹求下一个子字符串的指纹;
其中,
H
a
s
h
Hash
Hash函数的定义通常如下:
H
a
s
h
(
′
a
0
a
1
.
.
.
a
n
′
)
=
a
n
∗
b
a
s
e
0
+
a
n
−
1
∗
b
a
s
e
1
+
.
.
.
+
a
0
∗
b
a
s
e
n
(
1
)
Hash('a_0a_1...a_n')=a_n*base^0+a_{n-1}*base^1+...+a_0*base^n (1)
Hash(′a0a1...an′)=an∗base0+an−1∗base1+...+a0∗basen(1)
b
a
s
e
base
base是基数,通常取字符集的个数,如字符集合
{
a
,
b
,
c
,
d
.
.
.
x
,
y
,
z
}
\{ a,b,c,d...x,y,z\}
{a,b,c,d...x,y,z}就是小写英文字母集合,共26个。我们的字符串的元素应该全部取自这个集合,那么有
b
a
s
e
=
26
base=26
base=26。
至于已知前一个子字符串的指纹求下一个子字符串的指纹的过程可由一张图概括:
即
f
t
i
+
1
=
b
a
s
e
∗
(
f
t
i
−
T
s
∗
1
0
m
−
1
)
+
T
s
+
m
(
2
)
ft_{i+1}=base*(ft_i-T_s*10^{m-1})+T_{s+m} (2)
fti+1=base∗(fti−Ts∗10m−1)+Ts+m(2)
Rabin-Karp算法求模改进
如果说
′
a
0
a
1
.
.
.
a
n
′
'a_0a_1...a_n'
′a0a1...an′过长,那么计算机就难以求解
H
a
s
h
(
′
a
0
a
1
.
.
.
a
n
′
)
Hash('a_0a_1...a_n')
Hash(′a0a1...an′),或者说结果很大可能会溢出,为了解决这个问题,我们使用求余。改变(1)式和(2)式,得到:
具体证明可以看我上几次的博客
算法导论——Rabin-Karp算法求模证明(1)
算法导论——Rabin-Karp算法求模证明(2)
不多BB了,开始实践
C#实践
Program.cs
using System;
using System.Collections.Generic;
namespace StringMatch
{
class Program
{
static void Main(string[] args)
{
string T = "Star, I Want to Love with U, I'm so in Love with U";
string P = "Love with U";
//Base表明字符集一共有多少个元素,q为大于T[i]值的元素
Console.WriteLine("Rabin-Karp算法");
RabinKarp solver = new RabinKarp();
//ASCII码共128个码元,设置Base为128,q为大于T[i]的数,这里选择129即可。另外,算法导论上面要求q为质数,这样可以进一步减少时间复杂度,具体为何,博主由于知识有限,并没有做详细论证。
List<int> result = solver.RabinKarpStategy(T, P, 128, 129);
GetResult(result, P, T);
}
}
}
RabinKarp.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace StringMatch
{
public class RabinKarp
{
public List<int> RabinKarpStategy(string T, string P, int Base, int q)
{
List<int> result = new List<int>();
int n = T.Length;
int m = P.Length;
double ft = Hash(T.Substring(0, m), Base, q);
double fp = Hash(P, Base, q);
double c = 1;
//double c = Math.Pow(Base, m - 1) % q;,直接算易越界,结果会出错,我们用下面的算法
//此时字符串相当于"10000...00",注意我的意思不是ASCII码的1、0,而是真正的1、0,大家带入公式即可得到下面的结果
for(int i = 0; i < m - 1; i++)
{
c = (c * Base) % q;
}
//演算结果
for (int s = 0; s <= n - m; s++)
{
if (IsEqualByMod(ft,fp,q))
{
//Hash相同,不一定匹配,进一步考虑,如果选择q为质数,或许可以减少Hash相同的情况
if (T.Substring(s, m) == P)
{
result.Add(s);
}
}
if (s < n - m)
{
//可能为负数,我们需要充分考虑
ft = (((ft - (T[s] * c)) * Base) + T[s + m] ) % q;
}
}
return result;
}
//防止负数情况
private bool IsEqualByMod(double ft, double fp,int q)
{
if (ft < 0)
{
if (ft + q == fp)
{
return true;
}
}
else
{
if (fp + q == ft)
{
return true;
}
}
return false;
}
private double Hash(string p, int Base, int q)
{
double hashValue = 0;
for(int i = 0; i < p.Length; i++)
{
hashValue = (hashValue * Base + p[i]) % q;
}
return hashValue;
}
}
}