数据结构:后缀数组

后缀数组

资料: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 三个数组),而后缀自动机在空间和时间上更优,但理解和实现难度更大。

相关推荐
玉树临风ives27 分钟前
atcoder ABC 453 题解
数据结构·c++·算法·图论·atcoder
琪伦的工具库42 分钟前
批量PDF合并工具使用说明:批量合并与直接合并两种模式,拖拽排序/页面范围/遍历子目录/重名自动处理
数据结构·pdf·排序算法
山甫aa1 小时前
哈希集合-----从零开始的数据结构学习
数据结构·算法·哈希算法
say_fall1 小时前
有关算法的简单数学问题
数据结构·c++·算法·职场和发展·蓝桥杯
小杰帅气1 小时前
算法的时间和空间复杂度
数据结构
阿Y加油吧1 小时前
二分查找进阶:旋转排序数组的两道经典题深度解析
数据结构·算法
想带你从多云到转晴1 小时前
05、数据结构与算法---栈与队列
java·数据结构·算法
m0_716765231 小时前
数据结构--顺序表的插入、删除、查找详解
c语言·开发语言·数据结构·c++·学习·算法·visual studio
say_fall2 小时前
滑动窗口算法
数据结构·c++·算法
qq_454245032 小时前
图数据标准化与智能去重框架:设计与实现解析
数据结构·架构·c#·图论