后缀数组【原理+python代码】
后缀数组
参考:https://blog.csdn.net/a1035719430/article/details/80217267
https://blog.csdn.net/YxuanwKeith/article/details/50636898
主要由三个数组组成,一部分是sa
和rank
,另一部分是height
sa
与rank
的产生:倍增法
还有其它方法,但是倍增法虽然时间复杂度低,但是已经算好理解的了……
当i
在[0,n)
之间变化时,s[i:]
就代表着数组的n个后缀。
现在我们要对这n个后缀按字典序进行排序,比如对于"banana$"
,排好序之后长这样:
现在我们用两个数组来记录这种排序,那就是sa
和rank
sa[i]
表示排名为i
的后缀的起始位置,它的下标是排名,值是位置,意思就是【s[sa[i]:]
排在第i个】。rank[i]
表示以i
起始的后缀的排名,它的下标是位置,值是排名,意思就是【s[i:]
的排名为rank[i]
】。
那么显然会有这样的等量关系:
rank[sa[i]] = i
,这里的i表示的是排名sa[rank[i]] = i
,这里的i表示的是位置
接下来看看这两数组怎么求。
想象一下这么一个过程:
首先,字符串里的每个字符都代表着它的对应后缀的第一个字母,假如我们只按这个首字母进行排序(先不管如何排序),我们能得到一种排序结果。我们把用来排序的长度叫“有效长度”,现在它为1。
我们如果能不断扩展这个有效长度,直到它超过
n
,或者在扩展的过程中发现rank
数组已经是0~n-1
的一种排列(每个排名都不相同了),这时候我们就排好了接下来,我们要把有效长度扩展到2,这时候我们要利用后缀的一个很重要的性质:
后缀
s[i:]
的前l个字符,恰好是后缀s[i-l:]
的[l:2l]
部分的字符假如从
s[l:]
到s[n-1:]
都已经按有效长度l
排好序了,那么后缀s[0:]
到s[n-1-l:]
的[l:2l]
部分的字符的排序就已知了
所以倍增法就是这么一个过程:
当所有后缀已经按有效长度l
排好序之后,把所有后缀的[0:l]
部分字符当作第一关键字,把[l:2l]
部分的字符当作第二关键字,再度进行排序,就能得到有效长度为2l
的排序。
而基数排序就特别适合这种模式,按第一关键字的rank值相当于十位,按第二关键字的rank值相当于个位,然后排序。
具体看看如何实现:
初始化
# sa[i]:排名为i的后缀的起始位置
# rk[i]:起始位置为i的后缀的排名
n = len(s)
sa = []
rk = []
for i in xrange(n):
rk.append(ord(s[i])-ord('a')) # 刚开始时,每个后缀的排名按照它们首字母的排序
sa.append(i) # 而排名第i的后缀就是从i开始的后缀
倍增循环
l = 0 # l是已经排好序的长度,现在要按2l长度排序
sig = 26 # sig是unique的排名的个数,初始是字符集的大小
while True:
p = []
# 对于长度小于l的后缀来说,它们的第二关键字排名肯定是最小的,因为都是空的
for i in xrange(n-l,n):
p.append(i)
# 对于其它长度的后缀来说,起始位置在`sa[i]`的后缀排名第i,而它的前l个字符恰好是起始位置为`sa[i]-l`的后缀的第二关键字
for i in xrange(n):
if sa[i]>=l:
p.append(sa[i]-l)
# 然后开始基数排序,先对第一关键字进行统计
# 先统计每个值都有多少
cnt = [0]*sig
for i in xrange(n):
cnt[rk[i]] += 1
# 做个前缀和,方便基数排序
for i in xrange(1,sig):
cnt[i] += cnt[i-1]
# 然后利用基数排序计算新sa
for i in xrange(n-1,-1,-1):
cnt[rk[p[i]]] -= 1
sa[cnt[rk[p[i]]]] = p[i]
# 然后利用新sa计算新rk
def equal(i,j,l):
if rk[i]!=rk[j]:return False
if i+l>=n and j+n>=n:
return True
if i+l<n and j+l<n:
return rk[i+l]==rk[j+l]
return False
sig = -1
tmp = [None]*n
for i in xrange(n):
# 直接通过判断第一关键字的排名和第二关键字的排名来确定它们的前2l个字符是否相同
if i==0 or not equal(sa[i],sa[i-1],l):
sig += 1
tmp[sa[i]] = sig
rk = tmp
sig += 1
if sig==n:
break
# 更新有效长度
l = l << 1 if l > 0 else 1
然后开始倍增法的循环,直到排好序为止,也就是rank
数组里的unique排名数达到n为止
每个循环大概就是这样的思路:
- 基于老sa数组计算出按第二关键字排序的新
sa
数组(把它叫做p
) - 基于
p
数组和老rank
数组,计算出新sa
数组 - 基于新
sa
数组得到新rank
数组,并统计出unique排名数量
p数组的产生
p = []
# 对于长度小于l的后缀来说,它们的第二关键字排名肯定是最小的,因为都是空的
for i in xrange(n-l,n):
p.append(i)
# 对于其它长度的后缀来说,起始位置在`sa[i]`的后缀排名第i,而它的前l个字符恰好是起始位置为`sa[i]-l`的后缀的第二关键字
for i in xrange(n):
if sa[i]>=l:
p.append(sa[i]-l)
基数排序的cnt数组
# 先统计每个值都有多少
cnt = [0]*sig
for i in xrange(n):
cnt[rk[i]] += 1
# 做个前缀和,方便基数排序
for i in xrange(1,sig):
cnt[i] += cnt[i-1]
利用基数排序更新排名
# 然后利用基数排序计算新sa
for i in xrange(n-1,-1,-1):
cnt[rk[p[i]]] -= 1
sa[cnt[rk[p[i]]]] = p[i]
从后往前更新,按第二关键字排序排在最后的后缀,肯定在它的第一关键字的组里排最后,也就是前缀和cnt[rk[p[i]]]
的值
接着每排一个,我们都将对应的cnt数组-1,相当于指针指向了前一位。
现在我们就把新sa
数组计算出来了
根据新sa
计算新rank
def equal(i,j,l):
if rk[i]!=rk[j]:return False
if i+l>=n and j+n>=n:
return True
if i+l<n and j+l<n:
return rk[i+l]==rk[j+l]
return False
sig = -1
tmp = [None]*n
for i in xrange(n):
# 直接通过判断第一关键字的排名和第二关键字的排名来确定它们的前2l个字符是否相同
if i==0 or not equal(sa[i],sa[i-1],l):
sig += 1
tmp[sa[i]] = sig
rk = tmp
sig += 1
if sig==n:
break
这一步需要判断一下排名相邻的两个数组在有效长度范围内是不是相等的,即它们的[0:2l]
部分是不是相等的,如果是相等的那么排名不变,这有助于统计unique排名数量sig,目的是为了跳出循环
为了保证判断相等的时间复杂度为O(1)
,需要建立一个新的rk
的临时数组,同时利用老rk
的值来判断是否相等
综上所述,倍增法的所有代码如下:
def doubling(s):
# sa[i]:排名为i的后缀的起始位置
# rk[i]:起始位置为i的后缀的排名
n = len(s)
sa = []
rk = []
for i in xrange(n):
rk.append(ord(s[i])-ord('a')) # 刚开始时,每个后缀的排名按照它们首字母的排序
sa.append(i) # 而排名第i的后缀就是从i开始的后缀
l = 0 # l是已经排好序的长度,现在要按2l长度排序
sig = 26 # sig是unique的排名的个数,初始是字符集的大小
while True:
p = []
# 对于长度小于l的后缀来说,它们的第二关键字排名肯定是最小的,因为都是空的
for i in xrange(n-l,n):
p.append(i)
# 对于其它长度的后缀来说,起始位置在`sa[i]`的后缀排名第i,而它的前l个字符恰好是起始位置为`sa[i]-l`的后缀的第二关键字
for i in xrange(n):
if sa[i]>=l:
p.append(sa[i]-l)
# 然后开始基数排序,先对第一关键字进行统计
# 先统计每个值都有多少
cnt = [0]*sig
for i in xrange(n):
cnt[rk[i]] += 1
# 做个前缀和,方便基数排序
for i in xrange(1,sig):
cnt[i] += cnt[i-1]
# 然后利用基数排序计算新sa
for i in xrange(n-1,-1,-1):
cnt[rk[p[i]]] -= 1
sa[cnt[rk[p[i]]]] = p[i]
# 然后利用新sa计算新rk
def equal(i,j,l):
if rk[i]!=rk[j]:return False
if i+l>=n and j+n>=n:
return True
if i+l<n and j+l<n:
return rk[i+l]==rk[j+l]
return False
sig = -1
tmp = [None]*n
for i in xrange(n):
# 直接通过判断第一关键字的排名和第二关键字的排名来确定它们的前2l个字符是否相同
if i==0 or not equal(sa[i],sa[i-1],l):
sig += 1
tmp[sa[i]] = sig
rk = tmp
sig += 1
if sig==n:
break
# 更新有效长度
l = l << 1 if l > 0 else 1
return sa,rk
height数组
我们现在令height[i]
为s[sa[i-1]:]
和s[sa[i]:]
的最长公共前缀长度,即排名相邻的两个后缀的最长公共前缀长度。
比如:
那么如果rank[j]<rank[k]
,则后缀S[j:]
和S[k:]
的最长公共前缀为min(height[rank[j]+1],height[rank[j]+2]...height[rank[k]])
。
证明转载自:https://blog.csdn.net/a1035719430/article/details/80217267,补充了加粗部分的话。
同时,我们还有一个结论:height[rank[i]]≥height[rank[i−1]]−1
证明:
设suffix(k)
是排在suffix(i−1)
前一名的后缀,则它们的最长公共前缀是height[rank[i−1]]
那么suffix(k+1)
将排在suffix(i)
的前面
并且suffix(k+1)
和suffix(i)
的最长公共前缀至少是height[rank[i−1]]−1
那么由于suffix(k+1)
和suffix(i)
的前height[rank[i−1]]−1
位都一样,那么排名在它们中间的后缀,这前height[rank[i−1]]−1
位也都得一样,不然它们肯定不会排在中间。
所以suffix(i)
和在它前一名的后缀的最长公共前缀至少是height[rank[i−1]]−1
证毕。
这样我们按照height[rank[1]],height[rank[2]]...height[rank[n]]
的顺序计算,利用height
数组的性质,就可以将时间复杂度可以降为O(n)
。这是因为height
数组的值最多不超过n
,每次计算结束我们只会减1,所以总的运算不会超过2n
次。
# 计算height数组
k = 0
height = [0]*n
for i in xrange(n):
if rk[i]>0:
j = sa[rk[i]-1]
while i+k<n and j+k<n and s[i+k]==s[j+k]:
k += 1
height[rk[i]] = k
k = max(0,k-1) # 下一个height的值至少从max(0,k-1)开始
总的代码
def doubling(s):
# sa[i]:排名为i的后缀的起始位置
# rk[i]:起始位置为i的后缀的排名
n = len(s)
sa = []
rk = []
for i in xrange(n):
rk.append(ord(s[i])-ord('a')) # 刚开始时,每个后缀的排名按照它们首字母的排序
sa.append(i) # 而排名第i的后缀就是从i开始的后缀
l = 0 # l是已经排好序的长度,现在要按2l长度排序
sig = 26 # sig是unique的排名的个数,初始是字符集的大小
while True:
p = []
# 对于长度小于l的后缀来说,它们的第二关键字排名肯定是最小的,因为都是空的
for i in xrange(n-l,n):
p.append(i)
# 对于其它长度的后缀来说,起始位置在`sa[i]`的后缀排名第i,而它的前l个字符恰好是起始位置为`sa[i]-l`的后缀的第二关键字
for i in xrange(n):
if sa[i]>=l:
p.append(sa[i]-l)
# 然后开始基数排序,先对第一关键字进行统计
# 先统计每个值都有多少
cnt = [0]*sig
for i in xrange(n):
cnt[rk[i]] += 1
# 做个前缀和,方便基数排序
for i in xrange(1,sig):
cnt[i] += cnt[i-1]
# 然后利用基数排序计算新sa
for i in xrange(n-1,-1,-1):
cnt[rk[p[i]]] -= 1
sa[cnt[rk[p[i]]]] = p[i]
# 然后利用新sa计算新rk
def equal(i,j,l):
if rk[i]!=rk[j]:return False
if i+l>=n and j+n>=n:
return True
if i+l<n and j+l<n:
return rk[i+l]==rk[j+l]
return False
sig = -1
tmp = [None]*n
for i in xrange(n):
# 直接通过判断第一关键字的排名和第二关键字的排名来确定它们的前2l个字符是否相同
if i==0 or not equal(sa[i],sa[i-1],l):
sig += 1
tmp[sa[i]] = sig
rk = tmp
sig += 1
if sig==n:
break
# 更新有效长度
l = l << 1 if l > 0 else 1
# 计算height数组
k = 0
height = [0]*n
for i in xrange(n):
if rk[i]>0:
j = sa[rk[i]-1]
while i+k<n and j+k<n and s[i+k]==s[j+k]:
k += 1
height[rk[i]] = k
k = max(0,k-1) # 下一个height的值至少从max(0,k-1)开始
return sa,rk,height
后缀数组【原理+python代码】的更多相关文章
- 用最复杂的方式学会数组(Python实现动态数组)
Python序列类型 在本博客中,我们将学习探讨Python的各种"序列"类,内置的三大常用数据结构--列表类(list).元组类(tuple)和字符串类(str). 不知道你发现 ...
- paip.输入法编程--英文ati化By音标原理与中文atiEn处理流程 python 代码为例
paip.输入法编程--英文ati化By音标原理与中文atiEn处理流程 python 代码为例 #---目标 1. en vs enPHati 2.en vs enPhAtiSmp 3.cn vs ...
- catboost原理以及Python代码
原论文: http://learningsys.org/nips17/assets/papers/paper_11.pdf catboost原理: One-hot编码可以在预处理阶段或在训练期间 ...
- lightgbm原理以及Python代码
原论文: http://papers.nips.cc/paper/6907-lightgbm-a-highly-efficient-gradient-boosting-decision-tree.pd ...
- 决策树ID3原理及R语言python代码实现(西瓜书)
决策树ID3原理及R语言python代码实现(西瓜书) 摘要: 决策树是机器学习中一种非常常见的分类与回归方法,可以认为是if-else结构的规则.分类决策树是由节点和有向边组成的树形结构,节点表示特 ...
- Python代码阅读(第12篇):初始化二维数组
Python 代码阅读合集介绍:为什么不推荐Python初学者直接看项目源码 本篇阅读的代码实现了二维数组的初始化功能,根据给定的宽高初始化二维数组. 本篇阅读的代码片段来自于30-seconds-o ...
- MD5( 信息摘要算法)的概念原理及python代码的实现
简述: message-digest algorithm 5(信息-摘要算法).经常说的“MD5加密”,就是它→信息-摘要算法. md5,其实就是一种算法.可以将一个字符串,或文件,或压缩包,执行md ...
- KNN算法原理(python代码实现)
kNN(k-nearest neighbor algorithm)算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性 ...
- 决策树原理实例(python代码实现)
决策数(Decision Tree)在机器学习中也是比较常见的一种算法,属于监督学习中的一种.看字面意思应该也比较容易理解,相比其他算法比如支持向量机(SVM)或神经网络,似乎决策树感觉“亲切”许多. ...
随机推荐
- 【JavaWeb安全】RMI-Remote Method Invocator
RMI-Remote Method Invocator 什么是RMI?RMI有什么用? RMI允许用户通过数据传输,调用远程方法,在远程服务器处理数据.例如将1,3传到远程服务器的加法运算器,加法运算 ...
- 解决springboot序列化 json数据到前端中文乱码问题
前言 关于springboot乱码的问题,之前有文章已经介绍过了,这一篇算是作为补充,重点解决对象在序列化过程中出现的中文乱码的问题,以及后台报500的错误. 问题描述 spring Boot 中文返 ...
- spring注解-组件注册
一.@Configuration+@Bean @Configuration:配置类==配置文件 @Bean:给容器中注册一个Bean:类型为返回值的类型,默认是用方法名作为id @Bean(" ...
- java输入/输出流的基本知识
通过流可以读写文件,流是一组有序列的数据序列,以先进先出方式发送信息的通道. 输入/输出流抽象类有两种:InputStream/OutputStream字节输入流和Reader/Writer字符输入流 ...
- 【MySQL】学生成绩
统计每个人的总成绩排名 select stu.`name`,sum(stu.score) as totalscore from stu GROUP BY `name` order by totalsc ...
- [笔记] Informer: Beyond Efficient Transformer for Long Sequence Time-Series Forecasting
原文地址:https://arxiv.org/abs/2012.07436 源码地址:https://github.com/zhouhaoyi/Informer2020
- vm16虚拟机安装win11
vm16虚拟机安装win11 参考https://baijiahao.baidu.com/s?id=1712702900207158969&wfr=spider&for=pc win1 ...
- TCP链接请求的10种状态
一.状态显示 SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的 ...
- Nginx中指令
Rewrite模块 1 return指令 Syntax: return code [text]; return code URL; return URL; Default: - Context: se ...
- Linux 文件权限、系统优化
目录 Linux 文件权限.系统优化 1.文件权限的详细操作 1.简介: 2.命令及归属: 3.权限对于用户和目录的意义 权限对于用户的意义: 权限对于目录的意义: 4.创建文件/文件夹的默认权限来源 ...