Swift 中实现 Knuth-Morris-Pratt 字符串匹配:从 Z 数组到 suffixPrefix 移位表的线性时间模式搜索

发布时间:2026/9/19 22:54:41

Swift 中实现 Knuth-Morris-Pratt 字符串匹配:从 Z 数组到 suffixPrefix 移位表的线性时间模式搜索 Swift 中实现 Knuth-Morris-Pratt 字符串匹配从 Z 数组到 suffixPrefix 移位表的线性时间模式搜索【免费下载链接】swift-algorithm-clubAlgorithms and data structures in Swift, with explanations!项目地址: https://gitcode.com/gh_mirrors/sw/swift-algorithm-club本文以 swift-algorithm-club 仓库中 Knuth-Morris-Pratt 模块 为核心讲解如何用 Swift 实现一个线性时间的字符串模式匹配算法先基于 Z-Algorithm 为模式串构建suffixPrefix移位表再在搜索阶段利用该表实现大于单字符的“跳跃式”右移从而避免冗余比较。读完本文你将理解indexesOf(ptnr:)扩展的完整实现原理、每个关键分支的作用以及该算法O(n m)时间复杂度的来源。1. 算法目标与 API 设计模块的目标Goal是用 Swift 编写一个线性时间的字符串匹配算法返回给定模式串在文本中所有出现位置的索引原文见 Knuth-Morris-Pratt/README.markdown。具体形式是实现String上的一个扩展方法indexesOf(ptnr: String) - [Int]?返回值[Int]中每个整数代表模式串一次出现的起始下标若模式串未在文本中找到或模式串为空返回nil。原文给出的两个典型示例如下也同时出现在 KnuthMorrisPratt.playground 的 Contents.swift 末尾let dna ACCCGGTTTTAAAGAACCACCATAAGATATAGACAGATATAGGACAGATATAGAGACAAAACCCCATACCCCAATATTTTTTTGGGGAGAAAAACACCACAGATAGATACACAGACTACACGAGATACGACATACAGCAGCATAACGACAACAGCAGATAGACGATCATAACAGCAATCAGACCGAGCGCAGCAGCTTTTAAGCACCAGCCCCACAAAAAACGACAATFATCATCATATACAGACGACGACACGACATATCACACGACAGCATA dna.indexesOf(ptnr: CATA) // Output: [20, 64, 130, 140, 166, 234, 255, 270] let concert concert.indexesOf(ptnr: ) // Output: [6]第二个示例展示了该实现在多字节 emoji 场景下依然正确——因为实现中先用Array(self)把字符串转成Character数组再按索引比较避免了 Swift 字符串按UTF-16/Unicode.Scalar索引比较的陷阱。KMP 算法在理论上是解决模式匹配问题的最佳算法之一。文档同时指出实践中 Boyer-Moore 系列 往往更受青睐但 KMP 概念更简单且具有相同的线性时间复杂度。与最朴素的 暴力字符串搜索 相比KMP 的差别只在于当比较发生失配mismatch时不是简单地只右移一个字符而是根据预处理得到的信息执行更大步长的移动。这种移动能力的来源就是下一节讲的suffixPrefix数组。2. 核心数据结构suffixPrefix 移位表2.1 定义KMP 包含一个只针对模式串的预处理阶段它产出一个整数数组代码中命名为suffixPrefix。设模式串为P则suffixPrefix[i]记录的是P[0...i]的最长真后缀proper suffix中与P的前缀相匹配的那个后缀的长度。换句话说suffixPrefix[i]是以位置i结尾、且同时是P的前缀的最长子串的长度。文档给出的例子取P abadfryaabsabadffg则suffixPrefix[4] 0suffixPrefix[9] 2suffixPrefix[14] 42.2 用 Z-Algorithm 构建移位表suffixPrefix有多种求法本仓库采用的是基于 Z-Algorithm 的路线。Z-数组的定义是Z[i]表示P中从位置i开始、与P前缀相匹配的最长子串的长度实现见 ZAlgorithm.swift。可以发现Z[i]与suffixPrefix[i]记录的是同一份信息只是记录的位置不同Z[i]从子串的起点i处记录suffixPrefix从子串的终点处记录。因此只需把Z[i]映射到suffixPrefix的正确位置即可。仓库 KnuthMorrisPratt.swift 中的映射代码只有三行for patternIndex in (1 .. patternLength).reversed() { textIndex patternIndex zeta![patternIndex] - 1 suffixPrefix[textIndex] zeta![patternIndex] }其思路是从位置i开始、长度为Z[i]的子串其结束下标正好是i Z[i] - 1把Z[i]写入suffixPrefix的这个结束位置即可。从源码结构看使用.reversed()从大下标往小下标遍历有一层保护作用若多个起点位置映射到同一个结束位置即存在嵌套的重叠前缀匹配先写入的是较短的匹配随后较短匹配之外的更长匹配起点更小、长度更大会覆盖它最终表中保留的是最长的匹配长度这正好符合suffixPrefix的定义。3. 完整实现逐段解析以下是 KnuthMorrisPratt.swift 的完整实现文件头部注明其基于 Dan Gusfield 的著作Algorithms on String, Trees and Sequencesextension String { func indexesOf(ptnr: String) - [Int]? { let text Array(self) let pattern Array(ptnr) let textLength: Int text.count let patternLength: Int pattern.count guard patternLength 0 else { return nil } var suffixPrefix: [Int] Int var textIndex: Int 0 var patternIndex: Int 0 var indexes: [Int] [Int]() /* Pre-processing stage: computing the table for the shifts (through Z-Algorithm) */ let zeta ZetaAlgorithm(ptnr: ptnr) for patternIndex in (1 .. patternLength).reversed() { textIndex patternIndex zeta![patternIndex] - 1 suffixPrefix[textIndex] zeta![patternIndex] } /* Search stage: scanning the text for pattern matching */ textIndex 0 patternIndex 0 while textIndex (patternLength - patternIndex - 1) textLength { while patternIndex patternLength text[textIndex] pattern[patternIndex] { textIndex textIndex 1 patternIndex patternIndex 1 } if patternIndex patternLength { indexes.append(textIndex - patternIndex) } if patternIndex 0 { textIndex textIndex 1 } else { patternIndex suffixPrefix[patternIndex - 1] } } guard !indexes.isEmpty else { return nil } return indexes } }各部分的职责如下参数与边界检查KnuthMorrisPratt.swift#L15-L23把文本与模式都转为Character数组空模式直接返回nil。预处理阶段KnuthMorrisPratt.swift#L30-L36调用ZetaAlgorithm(ptnr:)得到zeta数组再按 2.2 节的映射规则填出suffixPrefix。注意zeta[0]未被使用循环从下标 1 开始。搜索阶段KnuthMorrisPratt.swift#L38-L58外层while条件textIndex (patternLength - patternIndex - 1) textLength是一个边界不变式它保证文本中从textIndex起剩余的字符数足够容纳模式串中尚未比较的部分patternLength - patternIndex - 1个从而让内层循环可以安全地做下标访问而不会越界内层while执行从左到右的逐字符比较两个游标同步前进若patternIndex patternLength说明整串匹配成功记录起点textIndex - patternIndex失配后的移动分两种情况若本次一次比较都没做成patternIndex 0文本游标textIndex右移一位从头再比否则执行 KMP 的关键一步——patternIndex suffixPrefix[patternIndex - 1]即保持textIndex不动仅把模式游标回退到suffixPrefix给出的位置。这一步的含义是P[0...suffixPrefix[i]]这个前缀与文本中刚匹配到的一段子串的后缀天然相等无需重新比较因此模式串可以一次性右移超过一个字符。这种“模式串内部回退”的写法与常见的“KMP 失配函数表”写法在数学上等价是从源码结构看可以得出的结论它利用的是suffixPrefix保证的“前缀—后缀自重合”性质把每次失配后的比较浪费压到最少。4. 一次完整匹配的推演文档用一个小例子完整走了一遍搜索阶段这里保留原推演。取模式串P ACTGACTA长度 8由预处理得到的suffixPrefix为[0, 0, 0, 0, 0, 0, 3, 1]文本T GCACTGACTGACTGACTAG。第 1 步对齐起点比较T[0]与P[0]GvsA失配。此时没有完整匹配且因为一次成功比较都没有patternIndex 0分支前的判断为suffixPrefix[1 - 1] 0模式串右移一位从T[1]与P[0]重新开始比较继续失配再移动到T[2]1 0123456789012345678 text: GCACTGACTGACTGACTAG textIndex: ^ pattern: ACTGACTA patternIndex: ^ suffixPrefix: 00000031第 2 步T[2]起开始连续匹配一直比到位置 8。但匹配长度 7 不等于模式长度 8不能报告出现。此时suffixPrefix发挥作用匹配长度为 7查suffixPrefix[7 - 1]得3意味着P的长度为 3 的前缀ACT与刚匹配的文本子串T[2...8]的后缀必然相等无需重新比较——模式串可以整体右移多于一位比较从T[9]与P[3]处直接恢复1 0123456789012345678 text: GCACTGACTGACTGACTAG textIndex: ^ pattern: ACTGACTA patternIndex: ^ suffixPrefix: 00000031第 3 步继续比较直到位置 13G与A失配。再次查表移位1 0123456789012345678 text: GCACTGACTGACTGACTAG textIndex: ^ pattern: ACTGACTA patternIndex: ^ suffixPrefix: 00000031第 4 步重新比较这次终于走到一次完整出现出现在文本下标17 - 7 101 0123456789012345678 text: GCACTGACTGACTGACTAG textIndex: ^ pattern: ACTGACTA patternIndex: ^ suffixPrefix: 00000031第 5 步报告出现后算法尝试比较T[18]与P[1]因为使用了suffixPrefix[8 - 1] 1比较失败下一次外层循环的条件不满足算法结束。整个过程中suffixPrefix两次帮助避免了逐字符重比这正是 KMP 线性时间的微观来源。5. 复杂度分析原文给出的复杂度结论与推导要点预处理阶段只涉及模式串Z-Algorithm 的运行时间为线性即O(n)n为模式串P的长度搜索阶段不会“越过”文本长度m并且可以证明搜索阶段的比较次数上界为2 * m——每次比较要么让textIndex前进要么让匹配前缀回退到suffixPrefix的更短位置两者都无法无限消耗因此 KMP 的总运行时间为O(n m)。对比之下暴力搜索在最坏情况下如文本全是A、模式为AAA...B退化到O(n * m)量级的比较次数KMP 通过suffixPrefix表把这种最坏情况消除掉了。6. 如何运行与使用仓库提供了两种运行方式与原文 Note 一致Playground 方式推荐用 Xcode 打开 KnuthMorrisPratt.playground。该 Playground 的Contents.swift已内置ZetaAlgorithm函数定义Knuth-Morris-Pratt/KnuthMorrisPratt.playground/Contents.swift#L3-L57以及indexesOf(ptnr:)扩展末尾还附带 DNA 与 emoji 两组示例直接执行即可看到输出。独立文件方式若单独使用 KnuthMorrisPratt.swift必须把 Z-Algorithm 文件夹下的 ZAlgorithm.swift 一并复制到同一模块中因为它依赖其中的ZetaAlgorithm函数。使用时的几个细节方法签名为indexesOf(ptnr:)参数标签写作ptnr返回[Int]?注意它与 Z-Algorithm 模块 中的indexesOf(pattern:)参数标签不同两者是同一仓库内两个独立的线性时间方案前者是 KMP后者是“Z-函数拼接P$T后扫描”的变体见 ZetaAlgorithm.swift预处理函数ZetaAlgorithm的参数标签为ptrn见 ZAlgorithm.swift#L11仓库中 KnuthMorrisPratt.swift 使用Array(self)Swift 5 的Character序列而 Playground 副本中保留了较早期的Array(self.characters)写法两者在现代 Swift 中均可工作但移植时建议统一为Array(self)。7. 小结与延伸阅读本文实现的 KMP 方案可概括为三步用 Z-Algorithm 得到Z数组 → 按i Z[i] - 1映射为suffixPrefix移位表 → 搜索阶段用“文本游标只前进、模式游标按表回退”的策略完成线性扫描。suffixPrefix表是连接“失配信息”与“大跨步移位”的桥梁也是理解 KMP 正确性的关键。仓库中可继续深入的相关实现Z-Algorithm/README.markdownZetaAlgorithm的完整原理与 Z-box 推演以及“Z-函数 分隔符拼接”这一更简单的线性匹配方案Boyer-Moore-Horspool/README.markdown实践中更常用的右到左匹配算法Brute-Force String Search/BruteForceStringSearch.swift作为 KMP 改进起点的朴素实现。本模块代码基于 Dan Gusfield 的著作Algorithms on String, Trees and Sequences: Computer Science and Computational BiologyCambridge University Press, 1997由 Matteo Dunnhofer 为 Swift Algorithm Club 编写。【免费下载链接】swift-algorithm-clubAlgorithms and data structures in Swift, with explanations!项目地址: https://gitcode.com/gh_mirrors/sw/swift-algorithm-club创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/19 22:49:41

Office LTSC 2024 离线安装 ISO 镜像制作与部署实战指南

1. 为什么离线部署 Office 依然是个刚需聊到 Microsoft Office LTSC 2024 的离线安装 ISO 镜像,很多人第一反应是"现在都什么年代了,直接点开官网下载安装器不就行了"。这话放在家里自己用的电脑上没毛病,但只要你干过企业 IT 运维…

2026/9/19 22:49:41

Python测试框架Pytest核心优势与实战指南

1. 为什么需要更优雅的测试框架在软件开发领域,测试代码的质量往往决定了项目的长期可维护性。传统unittest模块虽然能满足基本需求,但随着项目规模扩大,其局限性逐渐显现:繁琐的样板代码、不够直观的断言方式、难以扩展的测试组织…

2026/9/19 22:49:41

open-code-review:可验证的开源代码评审协议

1. “open-code-review”不是工具名,而是新一代代码评审范式的命名起点最近在几个技术社区里频繁看到“open-code-review”这个词,它不像 Git、Docker 或 ESLint 那样有明确的官网、安装命令或 GitHub star 数——它没有统一发行版,没有中心化…

2026/9/19 23:49:48

代码审查自动化:从Git Diff到AI辅助审查的工程实践

说到 code review,我最早开始做 open-code-review 这个项目,其实是被一次很尴尬的现场逼出来的。当时团队里一个老哥提了一个上千行变更的合并请求,我坐在屏幕前啃了两个小时,最后只抓住了两个变量命名问题,真正会导致…

2026/9/19 23:49:48

Meteor 1.10.2 迁移指南:Flow 语法移除与自定义 Babel 配置方案

后端前端开发工具移动开发 【免费下载链接】meteor Meteor, the JavaScript App Platform 项目地址: https://gitcode.com/gh_mirrors/me/meteor 点击查看 免费下载 本文基于 Meteor 官方迁移文档《Migrating to Meteor 1.10.2》展开,聚焦 1.10.2 版本中…

2026/9/19 23:44:48

Claude Code报错排查:401、404、超时的根因与解决方案

1. 先给报错定性:404、401、超时分别是什么信号这几年做AI工具链集成,我见过太多人栽在Claude Code配置这一步。明明安装很顺利,结果一执行任务就弹出各种报错,其中404、401、超时这三类占了八成以上。很多人一慌就开始瞎试&#…

2026/9/19 20:17:34

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/19 0:03:10

验证 OpenSpec 兼容性,Cursor 的 Token 从 TaoToken 出

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 0:03:10

书桌角落的 Mac mini,OpenClaw 通过 TaoToken 跑任务。

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/19 0:03:10

oh-my-hermes:打造跨工具的命令编排与插件化工作流

1. 项目概述与设计初衷1.1 它到底是什么先说结论:oh-my-hermes 是一个面向开发者日常终端操作的效率工具套件,核心定位是“把分散在各类命令行工具里的高频操作,统一收拢成一套插件化、可编排的工作流”。项目灵感来源很明显——oh-my-zsh 重…

2026/9/18 14:13:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/18 14:13:02

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/18 14:13:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码