数据结构:后缀数组

后缀数组

资料:https://pan.quark.cn/s/43d906ddfa1bhttps://pan.quark.cn/s/90ad8fba8347https://pan.quark.cn/s/d9d72152d3cf

一、后缀数组的定义

后缀数组(Suffix Array,简称 SA)是一种针对字符串 的高效数据结构,它将字符串的所有后缀字典序排序 后,存储这些后缀的起始索引

给定一个长度为 n 的字符串 S = s₀s₁...sₙ₋₁,其第 i 个后缀为 S[i:] = sᵢsᵢ₊₁...sₙ₋₁。后缀数组 sa 是一个长度为 n 的数组,满足 sa[k] = i 表示k 小的后缀是 S[i:] ,且字典序满足 S[sa[0]:] < S[sa[1]:] < ... < S[sa[n-1]:]

辅助数组

为了高效处理后缀相关问题,通常会搭配两个辅助数组:

  1. 排名数组 rkrk[i] = k 表示后缀 S[i:] 在排序后的后缀数组中排名为 k,与 sa 互为逆数组,即 sa[rk[i]] = irk[sa[k]] = k
  2. 高度数组 heightheight[k] 表示排名为 k 的后缀与排名为 k-1 的后缀的**最长公共前缀(LCP)**长度,即 height[k] = LCP(S[sa[k]:], S[sa[k-1]:]),规定 height[0] = 0

二、后缀数组的核心特性

  1. 字典序有序性:后缀数组中的后缀按字典序升序排列,这是解决字符串匹配、重复子串等问题的基础。
  2. 排名与后缀的双向映射 :通过 sark 可以快速查询后缀的排名,或排名对应的后缀起始索引。
  3. 最长公共前缀的传递性 :利用 height 数组可快速计算任意两个后缀的最长公共前缀长度:LCP(i,j) = min{height[rk[i]+1 ... rk[j]]}(假设 rk[i] < rk[j])。

三、后缀数组的构建算法

构建后缀数组的核心是对所有后缀进行高效排序,直接排序的时间复杂度为 O(n² log n)(比较两个后缀的时间为 O(n)),对于长字符串效率极低。因此需要更优的算法,常用的有:

1. 倍增算法(主流算法)

核心思想 :通过倍增长度的方式,逐步确定每个后缀的排名,避免直接比较长后缀。

  • 步骤
    1. 初始化 :先对每个字符(长度为 1 的子串)排序,得到初始的 sark
    2. 倍增排序 :对于长度 len = 2,4,8,...,将每个后缀的前 len 个字符拆分为len/2 字符len/2 字符 ,以 (rk[i], rk[i+len/2]) 为关键字进行排序,更新 sark
    3. 终止条件 :当 len ≥ n 时,所有后缀的排名已确定。
  • 时间复杂度O(n log n),实现简单且效率较高,是工程中常用的方法。

2. DC3 算法

核心思想 :基于基数排序的分治算法,将后缀分为三类进行排序,进一步优化时间复杂度。

  • 时间复杂度O(n),但实现复杂,适合对时间要求极高的场景。

四、后缀数组的实现示例(倍增算法)

python 复制代码
def build_sa(s):
    n = len(s)
    sa = list(range(n))
    rk = [ord(c) for c in s]  # 初始排名为字符的ASCII码
    tmp = [0] * n  # 临时数组,用于排序
    k = 1  # 倍增长度
    
    while k < n:
        # 排序关键字:(rk[i], rk[i+k]),i+k超出范围则为-1
        def cmp(i):
            return (rk[i], rk[i + k] if i + k < n else -1)
        
        # 对sa数组按新关键字排序
        sa.sort(key=cmp)
        
        # 更新tmp数组为新的排名
        tmp[sa[0]] = 0
        p = 0  # 排名计数器
        for i in range(1, n):
            # 若当前后缀与前一个后缀的关键字不同,排名+1
            if cmp(sa[i]) != cmp(sa[i-1]):
                p += 1
            tmp[sa[i]] = p
        
        # 更新rk数组
        rk[:] = tmp[:]
        k *= 2  # 倍增长度
    
    return sa, rk

def build_height(s, sa, rk):
    n = len(s)
    height = [0] * n
    k = 0  # 公共前缀长度
    for i in range(n):
        if rk[i] == 0:
            continue
        if k > 0:
            k -= 1
        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
    return height

使用示例

python 复制代码
s = "abracadabra"
n = len(s)
sa, rk = build_sa(s)
height = build_height(s, sa, rk)

print("字符串:", s)
print("后缀数组 sa:", sa)
print("排名数组 rk:", rk)
print("高度数组 height:", height)

# 输出解释:
# sa[0] = 10 表示排名0的后缀是 s[10:] = "a"
# rk[10] = 0 表示后缀 s[10:] 排名为0
# height[1] 表示排名1的后缀与排名0的后缀的最长公共前缀长度

五、后缀数组的时间复杂度

  • 构建(倍增算法)O(n log n),其中排序的时间为 O(n log n),倍增的次数为 log n
  • 高度数组构建O(n),利用公共前缀的传递性,避免重复比较。
  • 查询任意两后缀的 LCP :若搭配**区间最小值查询(RMQ)**预处理 height 数组,查询时间为 O(1),预处理时间为 O(n log n)

六、后缀数组的典型应用

后缀数组是处理字符串问题的"万能工具",常用于以下场景:

  1. 字符串匹配 :在主串 S 中匹配模式串 P,可将 PS 的后缀数组中的后缀进行二分查找,时间复杂度 O(|P| log |S|)
  2. 最长重复子串 :字符串中出现至少两次的最长子串,其长度等于 height 数组的最大值。
  3. 最长公共子串 :给定两个字符串 ST,拼接为 S + '#' + T 后构建后缀数组,找到分别来自 ST 的后缀的最大 height 值。
  4. 不同子串计数 :字符串中不同子串的总数为 n(n+1)/2 - sum(height[1...n-1])(总子串数减去重复子串数)。
  5. 后缀排序与字典序相关问题:如求字符串的最小表示、按后缀字典序输出子串等。

七、后缀数组与其他字符串结构的对比

数据结构 核心优势 适用场景 时间复杂度(构建)
后缀数组 处理 LCP 问题高效,功能全面 重复子串、公共子串、匹配 O(n log n)
字典树(Trie) 前缀匹配高效 前缀查询、词频统计 O(n)
后缀自动机(SAM) 空间效率极高,支持动态添加 海量字符串的子串问题 O(n)

后缀数组的优势在于直观易懂功能全面 ,缺点是空间复杂度较高(需存储 sarkheight 三个数组),而后缀自动机在空间和时间上更优,但理解和实现难度更大。

相关推荐
近津薪荼17 小时前
优选算法——双指针8(单调性)
数据结构·c++·学习·算法
松☆17 小时前
Dart 中的常用数据类型详解(含 String、数字类型、List、Map 与 dynamic) ------(2)
数据结构·list
历程里程碑17 小时前
Linux15 进程二
linux·运维·服务器·开发语言·数据结构·c++·笔记
嵌入小生00718 小时前
双向链表、双向循环链表之间的异同---嵌入式入门---Linux
linux·c语言·数据结构·链表·嵌入式·小白
独自破碎E18 小时前
【滑动窗口+计数】LCR015找到字符串中所有字母异位词
数据结构·算法
BoJerry77719 小时前
数据结构——单链表(不带头)【C】
c语言·开发语言·数据结构
-Try hard-19 小时前
数据结构 | 双向链表、双向循环链表、栈
数据结构·链表·vim
想进个大厂19 小时前
代码随想录day31 贪心05
数据结构·算法·leetcode
yyy(十一月限定版)19 小时前
寒假集训1——暴力和枚举
数据结构·算法
橘颂TA19 小时前
【剑斩OFFER】算法的暴力美学——力扣 207 题:课程表
数据结构·c++·算法·leetcode·职场和发展