news 2026/10/8 1:19:27

Swift Algorithms 指南:`suffix(while:)` 从集合尾部按谓词提取后缀子序列

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Swift Algorithms 指南:`suffix(while:)` 从集合尾部按谓词提取后缀子序列
  • 开发工具

【免费下载链接】swift-algorithms

Commonly used sequence and collection algorithms for Swift

项目地址:https://gitcode.com/gh_mirrors/swi/swift-algorithms
点击查看免费下载

suffix(while:)是 Swift Algorithms 开源库(README.md)中提供的一个子集操作 API:它从集合的末尾开始反向扫描,返回连续满足给定谓词的所有元素组成的后缀子序列(SubSequence),一旦谓词返回false便立即停止,并跳过更靠前的其余元素。本文以 Guides/Suffix.md 为骨架,结合 Sources/Algorithms/Suffix.swift 的实现与 Tests/SwiftAlgorithmsTests/SuffixTests.swift 的测试用例,完整讲解它的用法、边界行为、底层原理、复杂度与命名由来。读完本文,你将掌握如何用一行代码取出集合末尾满足条件的连续元素,并理解它与prefix(while:)、drop家族的异同。

核心功能:什么是suffix(while:)

suffix(while:)返回一个子序列,其中包含从集合尾部起、所有连续通过给定谓词(predicate)检验的元素。扫描自后向前进行:从最后一个元素开始,逐个向前检验;一旦遇到某个元素使谓词返回false,扫描立即终止,该元素及其之前的全部元素都会被跳过。

用原文档中的示例来说明:对一个整数集合从尾部向前遍历,直到遇到$0 <= 5的元素为止,剩下的元素即为结果。

(0...10).suffix(while: { $0 > 5 }) // == [6, 7, 8, 9, 10]

这里0...10的尾部是10,10 > 5成立继续向前;9、8、7、6同样成立;遇到5时5 > 5为false,扫描停止,返回6...10这个子序列。

从源码注释(Sources/Algorithms/Suffix.swift)可以看到 API 语义的精确定义:

  • 参数predicate:一个以元素为参数、返回Bool的闭包。返回true表示该元素应被包含,返回false表示应被排除。一旦谓词返回过一次false,它就不会再被调用。
  • 返回值:集合的一个后缀子序列,其中所有元素都使predicate返回true。

这个"谓词只在前缀/后缀扫描期间被调用"的约定很重要——它意味着suffix(while:)是短路的,不会为整个集合的每个元素都执行谓词,这一点与标准库prefix(while:)的行为一致。

基本用法与边界行为

在 Tests/SwiftAlgorithmsTests/SuffixTests.swift 的testSuffix()用例中,覆盖了四种典型边界情况:

let a = 0...10 expectEqualSequences(a.suffix(while: { $0 > 5 }), (6...10)) expectEqualSequences(a.suffix(while: { $0 > 10 }), []) expectEqualSequences(a.suffix(while: { $0 > 9 }), [10]) expectEqualSequences(a.suffix(while: { $0 > -1 }), (0...10)) let empty: [Int] = [] expectEqualSequences(empty.suffix(while: { $0 > 10 }), [])

逐条解读这五个用例,正好涵盖了所有值得注意的边界场景:

调用结果说明
a.suffix(while: { $0 > 5 })6...10常规场景:尾部连续满足谓词的元素
a.suffix(while: { $0 > 10 })[]尾部第一个元素(10)就不满足,返回空子序列
a.suffix(while: { $0 > 9 })[10]只取到紧邻"首个失败元素"之后的一个元素
a.suffix(while: { $0 > -1 })0...10全部元素都满足,返回整个集合作为子序列
empty.suffix(while: { $0 > 10 })[]空集合上调用,安全返回空子序列,不会崩溃

这组测试还顺带验证了返回类型是子序列(SubSequence)而非新数组:(6...10)是对原Range的切片视图,而不是拷贝出来的Array,因此调用suffix(while:)不会产生额外的堆分配。

实战示例:清理日志中的"活跃尾段"

假设有一段日志数组,你想取出最近连续处于活跃状态的记录:

let log = ["idle", "idle", "active", "active", "active"] let activeTail = log.suffix(while: { $0 == "active" }) // ["active", "active", "active"]

或者提取一个数组末尾连续为偶数的一段:

let numbers = [1, 2, 3, 4, 8, 16, 7] let tailEven = numbers.suffix(while: { $0.isMultiple(of: 2) }) // [4, 8, 16]

注意第二个例子中4之后的8、16都满足,而3不满足,所以结果只包含4, 8, 16——中间一旦断裂,靠前的满足元素同样会被丢弃。

详细设计:API 签名与协议约束

根据 Guides/Suffix.md 的 Detailed Design 一节,suffix(while:)作为BidirectionalCollection的扩展方法加入:

extension BidirectionalCollection { public func suffix(while predicate: (Element) throws -> Bool) rethrows -> SubSequence }

方法声明为rethrows,意味着传入的谓词闭包可以抛出错误,此时错误会向上传递;这也符合 Swift 标准库对这类高阶遍历 API 的一贯约定。

为什么必须是BidirectionalCollection

该方法之所以要求BidirectionalCollection,是为了获得高效实现:它能从尾部向前尽量少地访问元素。Swift 的BidirectionalCollection协议允许对集合进行反向遍历,并且提供访问集合last属性(以及formIndex(before:))的能力。若退化为仅支持Collection的实现,只能从startIndex一路遍历到endIndex才能确定后缀的边界,代价是始终扫描整个集合。

关于这一设计取舍,可以参考同仓库的 Guides/Trim.md:其中明确指出,虽然理论上任何Collection都能实现"从尾部裁剪"的低效版本(总是遍历整个集合),但库有意不提供这种实现,以免编写泛型算法的开发者忘记添加BidirectionalCollection约束时,悄悄得到一个低效版本。suffix(while:)遵循同样的原则——把效率保证写进类型约束里。

可直接运行的安装前提

要在自己的 SwiftPM 工程中使用该 API,按 README.md 的说明,在Package.swift中添加依赖:

.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),

并在目标中声明.product(name: "Algorithms", package: "swift-algorithms"),随后在源码中import Algorithms即可调用suffix(while:)、prefix(while:)等全部算法。本仓库对应的完整清单见 Guides/README.md,suffix(while:)被归类在"Subsetting operations(子集操作)"之下。

底层实现剖析

Sources/Algorithms/Suffix.swift中suffix(while:)的实现极其简洁,只有一行:

@inlinable public func suffix( while predicate: (Element) throws -> Bool ) rethrows -> SubSequence { try self[startOfSuffix(while: predicate)...] }

它的真正逻辑被委托给配套的辅助方法startOfSuffix(while:),其完整实现如下:

extension BidirectionalCollection { @inlinable public func startOfSuffix( while predicate: (Element) throws -> Bool ) rethrows -> Index { var index = endIndex while index != startIndex { let after = index formIndex(before: &index) if try !predicate(self[index]) { return after } } return index } }

逐行走读这段核心逻辑:

  1. 从endIndex出发,把当前游标记为"下一个元素(after)";
  2. 通过formIndex(before: &index)回退一步,检验该元素是否满足谓词;
  3. 若不满足,说明此前已收集到的后缀到此为止,返回after(即第一个失败元素之后的位置),作为后缀子序列的包含下界(inclusive lower bound);
  4. 若满足,继续向前回退;
  5. 循环直到index == startIndex,说明全部元素都满足,返回startIndex。

于是suffix(while:)只需用self[lowerBound...]从找到的下界一路切到endIndex,形成切片返回。整个过程中,谓词一旦返回false就立刻结束,不会再被调用。

对称的姊妹 API:endOfPrefix(while:)

同一个源文件中还提供了Collection上的endOfPrefix(while:),它从前向后工作,返回前缀的排他上界(exclusive upper bound),即第一个不满足谓词的元素的索引;若全部满足则返回endIndex:

extension Collection { @inlinable public func endOfPrefix( while predicate: (Element) throws -> Bool ) rethrows -> Index { var index = startIndex while try index != endIndex && predicate(self[index]) { formIndex(after: &index) } return index } }

这两个辅助方法最初是trimming系列方法的内部实现细节,在 1.2.0 版本中因"本身独立有用"而被公开为公共 API(见 CHANGELOG.md 中 "TheendOfPrefix(while:)andstartOfSuffix(while)methods are now public" 的记载,以及 Guides/Trim.md 的 Supporting Methods 一节)。它们的公开也使得查找集合的"前缀边界 / 后缀边界"成为库的一等能力,文档入口见 Sources/Algorithms/Documentation.docc/Trimming.md 中的 "Finding Boundaries within a Collection" 与 "Finding the Suffix of a Collection" 两节。

endOfPrefix(while:)与startOfSuffix(while:)的行为同样有专门的测试覆盖(testEndOfPrefix、testStartOfSuffix,见 Tests/SwiftAlgorithmsTests/SuffixTests.swift),包括全部满足、全部不满足、空集合等情形,例如:

let array = Array(0..<10) XCTAssertEqual(array.endOfPrefix(while: { $0 < 3 }), 3) XCTAssertEqual(array.startOfSuffix(while: { $0 >= 3 }), 3) XCTAssertEqual(array.endOfPrefix(while: { _ in true }), array.endIndex) XCTAssertEqual(array.startOfSuffix(while: { _ in false }), array.endIndex)

注意startOfSuffix(while:)在全部不满足时返回endIndex(此时后缀为空),在全部满足时返回startIndex(此时后缀即整个集合)——这两个返回值方向相反,使用时可结合测试用例对照理解。

复杂度分析

原文档明确给出:调用suffix(while:)的时间复杂度为 O(n),其中n是集合长度(Sources/Algorithms/Suffix.swift 的文档注释同样标注了- Complexity: O(*n*))。

这里的 O(n) 是最坏情况上界——当所有元素都满足谓词时,需要从尾部一路回退到startIndex。但在实际使用中,由于算法从尾部短路返回,典型开销与被返回后缀之前"失败元素"的位置成正比:失败元素越靠近末尾,访问的元素越少。空间上,返回值是原集合的切片(SubSequence),不会复制元素,无额外存储开销。

命名考量:与prefix(while:)的对称

原文档的 Naming 一节解释了函数名的由来:Swift 标准库已有一个prefix(while:),它对集合做正向遍历,返回从头部开始连续满足谓词的前缀;suffix(while:)从集合末端反向遍历,做的是同一件事,因此命名为suffix(while:)与既有 API 形成清晰的镜像对称。标准库中还提供了返回子序列的drop(while:)(等价于"左侧裁剪"),以及dropFirst(Int)、dropLast(Int)等无谓词裁剪手段,但这些方法都不支持自定义谓词且方向语义各不相同(详见 Guides/Trim.md 对drop家族的讨论)。suffix(while:)与它们的区别在于:它同时具备"从尾部开始"与"谓词控制裁剪点"两个能力,正好补齐了标准库在"右端按条件裁剪"上的空缺。

小结

  • suffix(while:)从BidirectionalCollection的末端向前扫描,返回连续满足谓词的后缀子序列,谓词一遇false即停止;
  • 实现委托给公开的辅助方法startOfSuffix(while:),配合endOfPrefix(while:)共同构成"查找集合前后边界"的基础工具;
  • 时间复杂度最坏 O(n),实际开销取决于失败元素的位置;返回值为零拷贝切片;
  • 边界行为(空集合、全部满足、首元素即失败)均有测试用例佐证,可放心在泛型代码中直接使用。

如果想进一步了解以这两个辅助方法为地基的trimming系列(同时裁剪头部与尾部),建议继续阅读 Guides/Trim.md;完整的 API 分类索引见 Guides/README.md。

  • 开发工具

【免费下载链接】swift-algorithms

Commonly used sequence and collection algorithms for Swift

项目地址:https://gitcode.com/gh_mirrors/swi/swift-algorithms
点击查看免费下载
上一篇:Fluid 开源项目使用教程
下一篇:【开源宝藏】Glow.nvim:终端里的Markdown预览神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 1:17:41

题解:洛谷 P5729 【深基5.例7】工艺品制作

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/8 1:16:31

VC6+GDI横版过关游戏源码解析:仿超级玛丽实现与避坑指南

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

作者头像 李华
网站建设 2026/10/8 1:15:59

工业级电源路径保护:eFuse与8位MCU协同实现故障可追溯设计

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

作者头像 李华
网站建设 2026/10/8 1:15:59

RISC-V特权架构与CSR速查:M/S/U模式切换与中断委托详解

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

作者头像 李华
网站建设 2026/10/8 1:15:00

JSP+MySQL个人日记本源码全解析:从环境搭建到避坑指南

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

作者头像 李华
网站建设 2026/10/8 1:14:52

遥感影像滑坡场景分类:从特征提取到SVM的完整实现与避坑指南

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

作者头像 李华