Rabin-Karp 算法(字符串快速查找)

  Go 语言的 strings 包(strings.go)中用到了 Rabin-Karp 算法。Rabin-Karp 算法是基于这样的思路:即把字符串看作是字符集长度进制的数,由数值的比较结果得出字符串的比较结果。

  朴素的字符串匹配算法为什么慢?因为它太健忘了,前一次匹配的信息其实有部分可以应用到后一次匹配中去,而朴素的字符串匹配算法只是简单的把这个信息扔掉,从头再来,因此,浪费了时间。好好的利用这些信息,自然可以提高运行速度。

  由于完成两个字符串的比较需要对其中包含的字符进行逐个比较,所需的时间较长,而数值比较则一次就可以完成,那么我们首先把“搜索词”中各个字符的“码点值”通过计算,得出一个数值(这个数值必须可以表示出字符的前后顺序,而且可以随时去掉某个字符的值,可以随时添加一个新字符的值),然后对“源串”中要比较的部分进行计算,也得出一个数值,对这两个数值进行比较,就能判断字符串是否匹配。对两个数值进行比较,速度比简单的字符串比较快很多。

  比如我们要在源串 "9876543210520" 中查找 "520",因为这些字符串中只有数字,所以我们可以使用字符集 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 来表示字符串中的所有元素,并且将各个字符映射到数字 0~9,然后用 M 表示字符集中字符的总个数,这里是 10,那么我们就可以将搜索词 "520" 转化为下面的数值:

("5"的映射值 * M + "2"的映射值) * M + "0"的映射值 = (5 * 10 + 2) * 10 + 0 = 520

  当然,如果“搜索词”很长,那么计算出来的这个数值就会很大,这时我们可以选一个较大的素数对其取模,用取模后的值作为“搜索词”的值。

  分析一下这个数值:520,它可以代表字符串 "520",其中:

代表字符 "5" 的部分是“ "5"的映射值 * (M 的 n - 1 次方) = 5 * (10 的 2 次方) = 500”
代表字符 "2" 的部分是“ "2"的映射值 * (M 的 n - 2 次方) = 2 * (10 的 1 次方) = 20”
代表字符 "0" 的部分是“ "0"的映射值 * (M 的 n - 3 次方) = 0 * (10 的 0 次方) = 0”
(n 代表字符串的长度)   我们可以随时减去其中一个字符的值,也可以随时添加一个字符的值。   “搜索词”计算好了,那么接下来计算“源串”,取“源串”的前 n 个字符(n 为“搜索词”的长度)"987",按照同样的方法计算其数值: ("9"的映射值 * M + "8"的映射值) * M + "7"的映射值 = (9 * 10 + 8) * 10 + 7 = 987   然后将该值与搜索词的值进行比较即可。   比较发现 520 与 987 不相等,则说明 "520" 与 "987" 不匹配,则继续向下寻找,这时候该如何做呢?下一步应该比较 "520" 跟 "876" 了,那么我们如何利用前一步的信息呢?首先我们把 987 减去代表字符 "9" 的部分: 987 - ("9"的映射值 * (M 的 n - 1 次方)) = 987 - (9 * (10 的 2 次方)) = 987 - 900 = 87   然后再乘以 M(这里是 10),再加上 "6" 的映射值,不就成了 876 了么: 87 * M + "6"的映射值 = 87 * 10 + 6 = 876   当然了,由于采用了取模操作,当两个数值相等时,未必是真正的相等,我们需要进行一次细致的检查(再进行一次朴素的字符串比较)。若不匹配,则可以排除掉。继续下一步。   如果我们要在 ASCII 字符集范围内查找“搜索词”,由于 ASCII 字符集中有 128 个字符,那么 M 就等于 128,比如我们要在字符串 "abcdefg" 中查找 "cde",那么我们就可以将搜索词 "cde" 转化为“("c"的码点 * M + "d"的码点) * M + "e"的码点 = (99 * 128 + 100) * 128 + 101 = 1634917”这样一个数值。   分析一下这个数值:1634917,它可以代表字符串 "cde",其中: 代表字符 "c" 的部分是“ "c"的码点 * (M 的 n - 1 次方) = 99 * (128 的 2 次方) = 1622016”
代表字符 "d" 的部分是“ "d"的码点 * (M 的 n - 2 次方) = 100 * (128 的 1 次方) = 12800”
代表字符 "e" 的部分是“ "e"的码点 * (M 的 n - 3 次方) = 101 * (128 的 0 次方) = 101”
(n 代表字符串的长度)   我们可以随时减去其中一个字符的值,也可以随时添加一个字符的值。   “搜索词”计算好了,那么接下来计算“源串”,取“源串”的前 n 个字符(n 为“搜索词”的长度)"abc",按照同样的方法计算其数值: ("a"的码点 * M + "b"的码点) * M + "c"的码点 = (97 * 128 + 98) * 128 + 99 = 1601891   然后将该值与“搜索词”的值进行比较即可。   比较发现 1634917 与 1601891 不相等,则说明 "cde" 与 "abc" 不匹配,则继续向下寻找,下一步应该比较 "cde" 跟 "bcd" 了,那么我们如何利用前一步的信息呢?首先去掉 "abc" 的数值中代表 a 的部分: (1601891 - "a"的码点 * (M 的 n - 1 次方)) = (1601891 - 97 * (128 的 2 次方)) = 12643   然后再将结果乘以 M(这里是 128),再加上 "d" 的码点值不就成了 "bcd" 的值了吗: 12643 * 128 + "d"的码点 = 1618304 + 100 = 1618404   这样就可以继续比较 "cde" 和 "bcd" 是否匹配,以此类推。   如果我们要在 Unicode 字符集范围内查找“搜索词”,由于 Unicode 字符集中有 1114112 个字符,那么 M 就等于 1114112,而 Go 语言中使用 16777619 作为 M 的值,16777619 比 1114112 大(更大的 M 值可以容纳更多的字符,这是可以的),而且 16777619 是一个素数。这样就可以使用上面的方法计算 Unicode 字符串的数值了。进而可以对 Unicode 字符串进行比较了。   其实 M 可以理解为进位值,比如 10 进制就是 10,128 进制就是 128,16777619 进制就是 16777619。   下面是 Go 语言中字符串匹配函数的源码,使用 Rabin-Karp 算法进行字符串比较: // primeRK 是用于 Rabin-Karp 算法中的素数,也就是上面说的 M
const primeRK = 16777619 // 返回 Rabin-Karp 算法中“搜索词” sep 的“哈希值”及相应的“乘数因子(权值)”
func hashstr(sep string) (uint32, uint32) {
// 计算 sep 的 hash 值
hash := uint32(0)
for i := 0; i < len(sep); i++ {
hash = hash*primeRK + uint32(sep[i])
}
// 计算 sep 最高位 + 1 位的权值 pow(乘数因子)
// 也就是上面说的 M 的 n 次方
// 这里通过遍历 len(sep) 的二进制位来计算,减少计算次数
var pow, sq uint32 = 1, primeRK
for i := len(sep); i > 0; i >>= 1 {
if i&1 != 0 { // 如果二进制最低位不是 0
pow *= sq
}
sq *= sq
}
return hash, pow
} // Count 计算字符串 sep 在 s 中的非重叠个数
// 如果 sep 为空字符串,则返回 s 中的字符(非字节)个数 + 1
// 使用 Rabin-Karp 算法实现
func Count(s, sep string) int {
n := 0
// 特殊情况判断
switch {
case len(sep) == 0: // 空字符,返回字符个数 + 1
return utf8.RuneCountInString(s) + 1
case len(sep) == 1: // 单个字符,可以用快速方法
c := sep[0]
for i := 0; i < len(s); i++ {
if s[i] == c {
n++
}
}
return n
case len(sep) > len(s):
return 0
case len(sep) == len(s):
if sep == s {
return 1
}
return 0
}
// 计算 sep 的 hash 值和乘数因子
hashsep, pow := hashstr(sep)
// 计算 s 中要进行比较的字符串的 hash 值
h := uint32(0)
for i := 0; i < len(sep); i++ {
h = h*primeRK + uint32(s[i])
}
lastmatch := 0 // 下一次查找的起始位置,用于确保找到的字符串不重叠
// 找到一个匹配项(进行一次朴素比较)
if h == hashsep && s[:len(sep)] == sep {
n++
lastmatch = len(sep)
}
// 滚动 s 的 hash 值并与 sep 的 hash 值进行比较
for i := len(sep); i < len(s); {
// 加上下一个字符的 hash 值
h *= primeRK
h += uint32(s[i])
// 去掉第一个字符的 hash 值
h -= pow * uint32(s[i-len(sep)])
i++
// 开始比较
// lastmatch <= i-len(sep) 确保不重叠
if h == hashsep && lastmatch <= i-len(sep) && s[i-len(sep):i] == sep {
n++
lastmatch = i
}
}
return n
}   我是初学者,这些学习笔记参考了网络上的一些资料,由于参考的内容比较多杂,所以不一一列出了,感谢各位网络朋友的无私奉献!

基础知识 - Rabin-Karp 算法的更多相关文章

  1. C++笔记(2)——一些语法基础知识以及基本算法知识

    今天和PAT无直接相关的关系,主要是关于一些语法/算法的笔记,因为我发现自己的基础还没有打扎实,有些时候看别人的代码还会觉得一头雾水,不明白代码的含义. 一些C/C++语法 先从语法开始吧.这部分很琐 ...

  2. LeetCode刷题--基础知识篇--KMP算法

    KMP算法 关于字符串匹配的算法,最知名的莫过于KMP算法了,尽管我们日常搬砖几乎不可能去亲手实现一个KMP算法,但作为一种算法学习的锻炼也是很好的,所以记录一下. KMP算法是根据三位作者(D.E. ...

  3. Python:基础知识

    python是一种解释型.面向对象的.带有动态语义的高级程序语言. 一.下载安装 官网下载地址:https://www.python.org/downloads 下载后执行安装文件,按照默认安装顺序安 ...

  4. Levenberg-Marquardt算法基础知识

    Levenberg-Marquardt算法基础知识 (2013-01-07 16:56:17) 转载▼   什么是最优化?Levenberg-Marquardt算法是最优化算法中的一种.最优化是寻找使 ...

  5. 数据结构&&算法基础知识

    写本篇主要是为了将基础知识梳理一遍,天天加一些基本东西,以后复习时可以返回来看看. 数据结构&&基础算法: 基本算法: 二分查找 二叉树: 二叉树的各种遍历 位操作: 排序: 排序算法 ...

  6. PHP面试(二):程序设计、框架基础知识、算法与数据结构、高并发解决方案类

    一.程序设计 1.设计功能系统——数据表设计.数据表创建语句.连接数据库的方式.编码能力 二.框架基础知识 1.MVC框架基本原理——原理.常见框架.单一入口的工作原理.模板引擎的理解 2.常见框架的 ...

  7. 任何国家都无法限制数字货币。为什么呢? 要想明白这个问题需要具备一点区块链的基础知识: 区块链使用的大致技术包括以下几种: a.点对点网络设计 b.加密技术应用  c.分布式算法的实现 d.数据存储技术 e.拜占庭算法 f.权益证明POW,POS,DPOS 原因一: 点对点网络设计 其中点对点的P2P网络是bittorent ,由于是点对点的网络,没有中心化,因此在全球分布式的网

    任何国家都无法限制数字货币.为什么呢? 要想明白这个问题需要具备一点区块链的基础知识: 区块链使用的大致技术包括以下几种: a.点对点网络设计 b.加密技术应用  c.分布式算法的实现 d.数据存储技 ...

  8. 数据结构和算法(Golang实现)(8.1)基础知识-前言

    基础知识 学习数据结构和算法.我们要知道一些基础的知识. 一.什么是算法 算法(英文algorithm)这个词在中文里面博大精深,表示算账的方法,也可以表示运筹帷幄的计谋等.在计算机科技里,它表示什么 ...

  9. 数据结构和算法(Golang实现)(8.2)基础知识-分治法和递归

    分治法和递归 在计算机科学中,分治法是一种很重要的算法. 字面上的解释是分而治之,就是把一个复杂的问题分成两个或更多的相同或相似的子问题. 直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合 ...

  10. 数据结构和算法(Golang实现)(9)基础知识-算法复杂度及渐进符号

    算法复杂度及渐进符号 一.算法复杂度 首先每个程序运行过程中,都要占用一定的计算机资源,比如内存,磁盘等,这些是空间,计算过程中需要判断,循环执行某些逻辑,周而反复,这些是时间. 那么一个算法有多好, ...

随机推荐

  1. Myeclipse8.5 svn插件安装两种方式

    第一种方式:(亲测成功)第一步:准备插件包:site-1.6.18.zip解压该包里面有features和plugins文件夹,删除该包里面的xml结尾的文件. 第二:我的Myeclipse8.5安装 ...

  2. Sql中判断“数据库"、"表"、"临时表"、"存储过程"和列”是否存在

    --判断数据库是否存在   IF EXISTS (SELECT * FROM MASTER..sysdatabases WHERE NAME = ''库名'')      PRINT ''exists ...

  3. login:用户登陆的意思

    login:用户登陆的意思 在思科的设备上有两种登录方式: 一种是本地方式,使用console口: 一种是远程方式(或者叫做网络方式):使用的是telnet等 1.默认情况下,思科的远程访问是禁止的. ...

  4. linux下配置双网卡及RAC规划——1

    使用背景: 操作系统:centos 虚拟机:virtualbox RAC系统中需要双网卡,一个为公共的网络环境,一个为私有的网络环境,从而需要搭建双网络. 在菜单FILE中选择preferences, ...

  5. 你今天Python了吗?(下)

    在体验了wxPython的强大之后,让我们把注意力集中到Twisted上来.在C++的世界里,你会发现一个很棒的网络应用框架,那就是ACE了:在Python的地盘,Twisted则是在网络应用框架中当 ...

  6. 线段相交 poj 1066

    // 线段相交 poj 1066 // 思路:直接枚举每个端点和终点连成线段,判断和剩下的线段相交个数 // #include <bits/stdc++.h> #include <i ...

  7. Azure支持docker简介以及使用指南

    Docker 是一个开源的项目,主要的特点是能将应用程序包装在一个 LXC (Linux Container) 容器中,当这些应用被包装进容器后,部署.迁移都变得更为简单.与传统的虚拟化技术相比,虚拟 ...

  8. wxPython安装错误问题:No module named wx

    今天心血来潮安装wxPython,本机win7,且已经安装Python,版本为2.7.3,然后IDE使用的PyCharm,然后wxPython下载的版本为:wxPython2.8-win32-unic ...

  9. 《Java数据结构与算法》笔记-CH1

    /** * 数据结构的特性 ************************************************************************************** ...

  10. Argument 'xxx' is not a function, got undefined,初学Angular的第一个坑

    终于考完试了,在没更新的这一段时间里,一直都在忙于应付考试.不过在期间也是接触到不少好玩的东西,比如Html5的Canvas,我用lufylegend的Html5引擎做了个<看你有所色>的 ...