字符串算法详解
字符串算法在文本处理、搜索引擎、生物信息学等领域应用广泛。预计学习时间 2-3 周。
一、字符串算法的重要性 🔤
字符串是计算机中最基本的数据类型之一,从简单的文本编辑到复杂的搜索引擎,都离不开高效的字符串算法。
应用领域
二、基础字符串操作 📝
1. 字符串匹配(暴力法)
最简单的方法:逐个位置尝试匹配。
def brute_force_search(text, pattern):
"""
暴力字符串匹配
:param text: 文本
:param pattern: 模式串
:return: 所有匹配位置的列表
"""
n = len(text)
m = len(pattern)
positions = []
# 遍历文本的每个可能起始位置
for i in range(n - m + 1):
# 检查从位置 i 开始是否匹配
match = True
for j in range(m):
if text[i + j] != pattern[j]:
match = False
break
if match:
positions.append(i)
return positions
# 测试
text = "AABAACAADAABAABA"
pattern = "AABA"
print(brute_force_search(text, pattern)) # [0, 9, 12]
时间复杂度:O(n × m),n 为文本长度,m 为模式长度
暴力法在最坏情况下效率很低,特别是当文本和模式有很多部分匹配时。
2. KMP 算法
KMP(Knuth-Morris-Pratt)算法通过预处理模式串,避免不必要的回溯,将时间复杂度降到 O(n + m)。
def kmp_search(text, pattern):
"""
KMP 字符串匹配算法
:param text: 文本
:param pattern: 模式串
:return: 所有匹配位置的列表
"""
n = len(text)
m = len(pattern)
if m == 0 or n < m:
return []
# 构建 next 数组(部分匹配表)
next_array = build_next_array(pattern)
positions = []
i = j = 0
while i < n:
if text[i] == pattern[j]:
i += 1
j += 1
# 找到完整匹配
if j == m:
positions.append(i - j)
j = next_array[j - 1]
else:
if j > 0:
j = next_array[j - 1]
else:
i += 1
return positions
def build_next_array(pattern):
"""构建 KMP 的 next 数组"""
m = len(pattern)
next_array = [0] * m
length = 0 # 前一个最长前后缀的长度
i = 1
while i < m:
if pattern[i] == pattern[length]:
length += 1
next_array[i] = length
i += 1
else:
if length > 0:
length = next_array[length - 1]
else:
next_array[i] = 0
i += 1
return next_array
# 测试
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
print(kmp_search(text, pattern)) # [10]
模式串: A B A B C A B A B
next[]: 0 0 1 2 0 1 2 3 4核心思想:利用已匹配的信息,避免重复比较
next 数组示例:
3. Rabin-Karp 算法
Rabin-Karp 使用哈希函数进行快速匹配,特别适合多模式匹配。
def rabin_karp_search(text, pattern, prime=101):
"""
Rabin-Karp 字符串匹配
:param text: 文本
:param pattern: 模式串
:param prime: 质数(用于哈希)
:return: 所有匹配位置的列表
"""
n = len(text)
m = len(pattern)
d = 256 # 字符集大小
if m == 0 or n < m:
return []
# 计算 d^(m-1) % prime
h = pow(d, m - 1, prime)
# 计算模式和文本第一个窗口的哈希值
pattern_hash = 0
text_hash = 0
for i in range(m):
pattern_hash = (d * pattern_hash + ord(pattern[i])) % prime
text_hash = (d * text_hash + ord(text[i])) % prime
positions = []
# 滑动窗口
for i in range(n - m + 1):
# 哈希值匹配,进一步验证(避免哈希冲突)
if pattern_hash == text_hash:
if text[i:i+m] == pattern:
positions.append(i)
# 计算下一个窗口的哈希值
if i < n - m:
text_hash = (d * (text_hash - ord(text[i]) * h) +
ord(text[i + m])) % prime
# 处理负数
if text_hash < 0:
text_hash += prime
return positions
# 测试
text = "AABAACAADAABAABA"
pattern = "AABA"
print(rabin_karp_search(text, pattern)) # [0, 9, 12]
优势:
- ✅ 平均情况 O(n + m)
- ✅ 适合多模式匹配
- ✅ 可以并行计算哈希
劣势:
- ❌ 最坏情况 O(n × m)(哈希冲突多时)
- ❌ 需要额外的哈希计算
三、高级字符串算法 🚀
1. Trie 树(前缀树)
Trie 树是一种树形数据结构,用于高效存储和检索字符串集合。
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
"""插入单词"""
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word):
"""搜索单词是否存在"""
node = self._find_node(word)
return node is not None and node.is_end_of_word
def starts_with(self, prefix):
"""判断是否有以 prefix 为前缀的单词"""
return self._find_node(prefix) is not None
def _find_node(self, prefix):
"""查找前缀对应的节点"""
node = self.root
for char in prefix:
if char not in node.children:
return None
node = node.children[char]
return node
# 测试
trie = Trie()
trie.insert("apple")
trie.insert("app")
trie.insert("application")
print(trie.search("apple")) # True
print(trie.search("app")) # True
print(trie.search("appl")) # False
print(trie.starts_with("app")) # True
应用场景:
- 自动补全
- 拼写检查
- IP 路由表
- 词典实现
时间复杂度:
- 插入/搜索:O(m),m 为字符串长度
- 空间复杂度:O(n × m),n 为字符串数量
2. 后缀数组
后缀数组用于快速解决字符串的子串查询问题。
def build_suffix_array(text):
"""
构建后缀数组
:param text: 输入字符串
:return: 后缀数组(排序后的起始位置)
"""
n = len(text)
# 生成所有后缀及其起始位置
suffixes = [(text[i:], i) for i in range(n)]
# 按字典序排序
suffixes.sort()
# 返回起始位置数组
return [suffix[1] for suffix in suffixes]
def longest_common_prefix(text, suffix_array):
"""计算相邻后缀的最长公共前缀"""
n = len(text)
sa = suffix_array
lcp = [0] * n
for i in range(1, n):
# 计算 sa[i-1] 和 sa[i] 对应后缀的 LCP
k = 0
while (sa[i-1] + k < n and sa[i] + k < n and
text[sa[i-1] + k] == text[sa[i] + k]):
k += 1
lcp[i] = k
return lcp
# 测试
text = "banana"
sa = build_suffix_array(text)
print("后缀数组:", sa)
# [5, 3, 1, 0, 4, 2]
# 对应后缀: a, ana, anana, banana, na, nana
应用:
- 最长重复子串
- 最长回文子串
- 字符串周期性检测
3. Manacher 算法
Manacher 算法用于在 O(n) 时间内找到最长回文子串。
def manacher(s):
"""
Manacher 算法求最长回文子串
:param s: 输入字符串
:return: 最长回文子串
"""
# 预处理:插入特殊字符,统一奇偶长度
t = '#' + '#'.join(s) + '#'
n = len(t)
# p[i] 表示以 t[i] 为中心的最长回文半径
p = [0] * n
center = right = 0
max_len = max_center = 0
for i in range(n):
# 利用对称性
if i < right:
mirror = 2 * center - i
p[i] = min(right - i, p[mirror])
# 尝试扩展
while (i + p[i] + 1 < n and i - p[i] - 1 >= 0 and
t[i + p[i] + 1] == t[i - p[i] - 1]):
p[i] += 1
# 更新中心和右边界
if i + p[i] > right:
center = i
right = i + p[i]
# 更新最长回文
if p[i] > max_len:
max_len = p[i]
max_center = i
# 提取最长回文子串
start = (max_center - max_len) // 2
return s[start:start + max_len]
# 测试
print(manacher("babad")) # "bab" 或 "aba"
print(manacher("cbbd")) # "bb"
print(manacher("racecar")) # "racecar"
时间复杂度:O(n),线性时间
四、字符串算法对比 📊
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力匹配 | O(n×m) | O(1) | 短模式串 |
| KMP | O(n+m) | O(m) | 单模式匹配 |
| Rabin-Karp | O(n+m) 平均 | O(1) | 多模式匹配 |
| Trie | O(m) 搜索 | O(n×m) | 前缀查询、自动补全 |
| 后缀数组 | O(n log n) 构建 | O(n) | 子串查询 |
| Manacher | O(n) | O(n) | 最长回文子串 |
选择指南
- 单次匹配 → KMP
- 多次匹配不同模式 → Rabin-Karp
- 前缀查询/自动补全 → Trie
- 回文相关问题 → Manacher
- 复杂子串查询 → 后缀数组
五、实际应用场景 💼
场景一:搜索引擎
搜索引擎使用多种字符串算法:
- ✅ 倒排索引(类似 Trie)
- ✅ 模糊匹配(编辑距离)
- ✅ 关键词高亮(字符串匹配)
- ✅ 自动补全(Trie 树)
场景二:DNA 序列分析
生物信息学中大量使用字符串算法:
def find_dna_motif(dna_sequence, motif):
"""在 DNA 序列中查找基序"""
return kmp_search(dna_sequence, motif)
# 测试
dna = "ATCGATCGATCGATCG"
motif = "ATCG"
positions = find_dna_motif(dna, motif)
print(f"基序 '{motif}' 出现在位置: {positions}")
场景三:代码编辑器
现代代码编辑器使用字符串算法实现:
- 语法高亮:正则表达式匹配
- 代码补全:Trie 树
- 查找替换:KMP/Rabin-Karp
- 括号匹配:栈 + 字符串扫描
场景四: plagiarism 检测
检测文本相似度:
六、字符串算法的优化技巧 ⚡
许多字符串算法通过预处理提升效率:
- KMP:构建 next 数组
- Rabin-Karp:预计算哈希
- Trie:批量插入构建
原则:如果多次查询,值得花时间预处理。
Trie 和后缀数组都使用了额外空间来加速查询。
权衡:
- 内存充足 → 用空间换时间
- 内存受限 → 考虑在线算法
对于大规模文本,可以并行处理:
- 分块后并行匹配
- GPU 加速哈希计算
- 分布式 Trie 构建
处理多语言文本时注意:
- UTF-8 编码的字符可能占多个字节
- 某些语言的字符有组合形式
- 大小写转换要考虑语言特性
建议:使用成熟的库(如 Python 的 re 模块)
七、练习与挑战 🏆
- ✅ 实现 KMP 算法
- ✅ 构建 Trie 树
- ✅ 解决最长回文子串
- ✅ 完成 LeetCode 字符串专题
- ✅ 尝试实现后缀数组
推荐练习题
LeetCode 28. 实现 strStr()
实现 strStr() 函数,返回 needle 在 haystack 中第一次出现的位置。 难度:⭐ 简单 可使用 KMP 或暴力法
LeetCode 208. 实现 Trie
实现一个 Trie(前缀树),包含 insert、search 和 startsWith 操作。 难度:⭐⭐ 中等 经典 Trie 实现
LeetCode 5. 最长回文子串
给定一个字符串,找到其中最长的回文子串。 难度:⭐⭐ 中等 可使用 Manacher 或 DP
LeetCode 76. 最小覆盖子串
给你一个字符串 s 和一个字符串 t,返回 s 中涵盖 t 所有字符的最小子串。 难度:⭐⭐ 困难 滑动窗口 + 字符串处理
八、学习路径建议 🗺️
九、总结 🎯
字符串算法是计算机科学的基础,掌握它们能大幅提升你处理文本数据的能力。