开始前,会这些就够了
- 能按从0开始的下标读取字符串
- 可先读 最小旋转中的候选位置
- Python 3.10+;只使用标准库
在文本abababaca里找模式ababaca。已经对上ababa以后,接下来的文本字符是b,模式却想要c。这一刻如果把已经对上的长度清零,就把之前观察到的字符关系一起扔掉了。
匹配失败不表示所有已读字符都没有用。ababa的结尾aba恰好也是模式的开头;保留这个关系,才可能从文本下标2开始找到完整模式。下面逐步看保留了什么,以及当前这个b为什么还要再比较。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。Python 标准库的独立 KMP 实验;固定示例加 7650 组匹配与 126 个前缀表对照。仅接受短小写英文字母,不验证完整 Unicode 文本搜索。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
字符失配时,已经匹配的部分要全部扔掉吗?
正在准备实验控件…
固定小写英文字母,找出所有从 0 编号的匹配起点。错误模式在失配和完整匹配后都把长度清零;正确模式保留仍可作为新开头的后缀。这里不处理正则表达式、Unicode 字素或忽略大小写。
先只看模式:哪些结尾同时也是开头?
模式ababaca的前缀表是0、0、1、2、3、0、1。pi[i]表示:模式从开头到下标i这一段中,同时作为开头和结尾的最长真前后缀长度。真表示不能把整段自己也算进去。
例如ababa的开头aba和结尾aba相同,长度3;整段ababa长度5虽然也相同,却必须排除。因此对应的表项是3。再看ababac,结尾c连模式的第一个a都对不上,表项是0。
表项保存长度,不是文本下标。本文统一从0编号:已经匹配j个字符时,下一次想比较的是pattern[j];需要回退时读取pi[j−1]。混用“第几个字符”和“下标几”,很容易多减一次。
失败后保留aba,再比较刚才同一个b
文本abababaca的下标0到4先匹配了ababa,此时j=5。下标5的b与模式下标5的c不相等。查pi[4]=3,把j改为3,就表示前面读过的字符末尾还保留aba。
文本指针此刻没有前进,仍是下标5的b。现在拿它与模式下标3的b比较,就能把j推进到4。继续读取后面的aca,最终找到从文本下标2开始的完整ababaca。
演示里的错误模式在失败时直接把j置0,再让同一个文本字符与模式开头比较。它会丢掉这里的候选起点,输出空列表。并不是说所有从头比较的方法都错;朴素逐起点检查可以正确,只是不能一边丢掉部分状态、一边仍只让文本指针往前走。
完整匹配以后,也可能保留一段后缀
在abababa里找aba,正确起点是0、2、4。找到下标0开始的aba之后,末尾那个a仍然可以作为下一次匹配的开头,所以j应回到pi[2]=1,而不是0。
若找到完整模式后直接清零,接下来从后续字符重新积累,就会漏掉下标2的重叠匹配,只找到0和4。模式aaaa与一串a也会产生大量重叠,找到一次并不等于可以把所有已读信息丢掉。
这种行为取决于任务要不要返回重叠匹配。本文要求返回每一个匹配起点;如果产品只允许不重叠命中,需要明确改变输出规则,而不能把漏掉的重叠结果当作同一任务的正确答案。
文本指针只前进,不等于每个字符只比较一次
前缀表的构建也用同样的回退:新字符与当前候选后缀接不上,就尝试这个候选内部更短的真前后缀。它按长度逐渐缩短,不必把所有长度从头逐一验证。
搜索时,一个文本字符可能与多个模式位置比较。线性时间的理由是j只会因成功比较增加,且每读一个文本字符最多增加1;每次回退又让j严格减小。总下降次数受此前总增加次数限制,所以累计比较次数是O(n+m),n为文本长度,m为模式长度。
本文Python记录每次回退和命中,全部回退数量仍为线性,含轨迹和输出的空间是O(n+m)。浏览器为了来回翻步骤,会在每个字符后复制当前命中列表;重叠很多时这些快照可能占O(n²)空间和复制时间。交互演示不是性能基准。
先固定字母和空模式规则,避免把文本语义混进匹配机制
示例只接受最多200个小写英文字母;文本可以为空,模式必须非空。这是为了让一个字符的位置直观明确。它不负责大小写折叠、Unicode规范化、中文分词或正则表达式。
空模式在不同API中可能另有定义,本文选择拒绝,而不是暗中返回全部位置。正文中的相等就是字符相等;外观相同的复杂Unicode文本是否相等,需要另一个处理层决定。
完整程序:用独立方法核对结果
保存下方全部代码为 demo.py,执行 python demo.py。程序只使用 Python 标准库,本次实际运行环境是 Windows / CPython 3.14.0;另通过 Python 3.10 语法解析,未在 3.10 解释器中执行。
枚举a、b组成的长度0到7文本和长度1到4模式,共7650组,用逐个起点切片比较作独立参考;另枚举126个模式,用直接检查全部真前后缀核对前缀表,并拒绝5类无效输入。
检查使用显式 AssertionError,正常执行和 python -O 执行都通过。输出直接取自本次运行;小范围穷举核对实现,不能代替前文对一般情况的解释。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""KMP retains a matched suffix; resetting it can lose overlapping matches."""
from itertools import product
def require(ok, message):
if not ok:
raise AssertionError(message)
def valid_string(value, empty=True):
if type(value) is not str or len(value) > 200 or (not empty and not value):
raise ValueError("a string of at most 200 lowercase ASCII letters required")
if any(not "a" <= char <= "z" for char in value):
raise ValueError("lowercase ASCII only")
def prefix(pattern):
valid_string(pattern, False)
pi = [0] * len(pattern)
for i in range(1, len(pattern)):
j = pi[i - 1]
while j and pattern[i] != pattern[j]:
j = pi[j - 1]
if pattern[i] == pattern[j]:
j += 1
pi[i] = j
return pi
def search(text, pattern, reset=False):
valid_string(text)
pi = prefix(pattern)
if type(reset) is not bool:
raise ValueError("reset must be bool")
j, matches, trace = 0, [], []
for i, char in enumerate(text):
before, falls = j, []
while j and char != pattern[j]:
target = 0 if reset else pi[j - 1]
falls.append((j, target))
j = target
if char == pattern[j]:
j += 1
matched = None
if j == len(pattern):
matched = i - len(pattern) + 1
matches.append(matched)
j = 0 if reset else pi[-1]
trace.append((i, char, before, falls, j, matched))
return matches, trace
def naive(text, pattern):
return [i for i in range(len(text) - len(pattern) + 1)
if text[i:i + len(pattern)] == pattern]
def main():
for text, pattern in [("abababaca", "ababaca"), ("abababa", "aba")]:
good, _ = search(text, pattern)
bad, _ = search(text, pattern, True)
print(f"text={text}; pattern={pattern}; prefix={prefix(pattern)}")
print(f"keep_suffix={good}; reset_to_zero={bad}")
require(good == naive(text, pattern) and good != bad, "visible counterexample")
cases = 0
for n in range(8):
for letters in product("ab", repeat=n):
text = "".join(letters)
for m in range(1, 5):
for letters in product("ab", repeat=m):
pattern = "".join(letters)
actual, _ = search(text, pattern)
require(actual == naive(text, pattern), "all starting positions")
cases += 1
prefix_cases = 0
for m in range(1, 7):
for letters in product("ab", repeat=m):
pattern = "".join(letters)
expected = []
for end in range(1, m + 1):
word = pattern[:end]
expected.append(max(k for k in range(end) if word[:k] == word[end-k:]))
require(prefix(pattern) == expected, "proper prefix/suffix enumeration")
prefix_cases += 1
for args in [("abc", ""), ("A", "a"), ("a", "é"), (True, "a"), ("a" * 201, "a")]:
try:
search(*args)
except ValueError:
pass
else:
raise AssertionError("invalid input accepted")
print(f"search_cases={cases}; prefix_cases={prefix_cases}; invalid_inputs=5")
print("PASS: all-start-position search and independent proper-border enumeration")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。Python 标准库的独立 KMP 实验;固定示例加 7650 组匹配与 126 个前缀表对照。仅接受短小写英文字母,不验证完整 Unicode 文本搜索。
运行后对照结果
text=abababaca; pattern=ababaca; prefix=[0, 0, 1, 2, 3, 0, 1]
keep_suffix=[2]; reset_to_zero=[]
text=abababa; pattern=aba; prefix=[0, 0, 1]
keep_suffix=[0, 2, 4]; reset_to_zero=[0, 4]
search_cases=7650; prefix_cases=126; invalid_inputs=5
PASS: all-start-position search and independent proper-border enumeration换一组有重复前缀的字符串,自己画回退
练习不预填答案:在文本aaaaaa中找模式aaaa。先预测全部匹配起点,再写出每次完整命中后j应保留多少。比较直接清零会漏掉哪些位置。
再把文本改成ababababca、模式改成ababca,逐字符记录失败前的j、回退后的j,以及当前文本字符有没有前进。解释一次回退为什么仍可能失败,并继续回退。
记住这些要点
- 前缀表记录最长真前后缀的长度,不是文本中的跳转下标。
- 失配时先保留可复用的后缀,并重新比较同一个文本字符。
- 要求所有命中位置时,完整匹配后也要保留重叠候选。
- 线性时间依靠累计回退次数受限,不是每个字符只比较一次。
别把例子用过头
- 失配就清零,却不回头检查被丢弃的候选起点。
- 完整匹配后清零,漏掉重叠命中。
- 把pi[i]当作下标,或把整段自身算作真前后缀。
- 把ASCII小示例直接当作完整的Unicode搜索规则。
自己试一小步
文本ababababca、模式ababca发生失配时,如何保留候选并证明没有跳过当前字符?
- 独立列出模式每段的最长真前后缀。
- 记录每个文本字符对应的j与回退链。
- 用逐起点切片的参考方法核对全部命中。
做到这些,就算完成
- 回退不会提前推进文本指针。
- 完整匹配后保留所需后缀。
- 先预测再运行,练习不预填命中列表。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Cornell CS312:KMP字符串匹配与前缀回退 (新标签页)↗
失配后利用已匹配段的前后缀继续搜索,以及累计回退次数的线性界。
- Hawaii ICS 311 Topic 23:String Matching (新标签页)↗
前缀函数含义,以及完整匹配之后继续利用前缀关系寻找后续匹配。