惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
V
V2EX
博客园_首页
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Announcements
Recent Announcements
博客园 - 司徒正美
Microsoft Security Blog
Microsoft Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Latest news
Latest news
Vercel News
Vercel News
The Register - Security
The Register - Security
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
小众软件
小众软件
L
Lohrmann on Cybersecurity
GbyAI
GbyAI
P
Privacy & Cybersecurity Law Blog
T
Tor Project blog
AWS News Blog
AWS News Blog
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
Kaspersky official blog
B
Blog RSS Feed
G
Google Developers Blog
量子位
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
雷峰网
雷峰网
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
The Hacker News
The Hacker News
U
Unit 42
S
SegmentFault 最新的问题
I
InfoQ
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
罗磊的独立博客
Spread Privacy
Spread Privacy
C
CERT Recently Published Vulnerability Notes

BlogFinder

日常漫步 Vol.24 之漫步前山河 - 雅余 周报 #1-聊聊本周的收获 - Edwin's Blog 我的OpenCode必装插件与Skill Write Something 掌中之物未必在掌握之中 · CRIVU PiliNara,一个更顺手的 PiliPlus 分支 「NekoEcho」:做一个必有回响的猫娘主题博客 2026-05 书影音总结 简化博客主题 - 安迪 我第一次发布 npm 包 拾花小记#45:中考前的二三事 – 小改学习志 黛西花园5月游 #18 枇杷又熟了的五月月报 一些奇奇怪怪的需求?word仿方正书版的几个小操作 - Xiobb's Blog 0419 御温泉之旅 修复了一些bug,网站基本上趋于稳定了 - 新锐博客 又回到四十年前 如何定义成功 迷鹿屋2026已重新上线 科技冰火两重天+一周回顾 ${title} 热度退了,我反而用得更深了-咕咚同学 我到底该不该换个域名? 随身WIFI折腾记 - 安迪 博客撰写体验提升——hexo pro插件 为什么不用相机把屏幕上的接关密码拍下来? 国清寺与天台山 – Ouroboros ★★★★☆《挽救计划》——久违的经济上行感 - Davidの3号基地 删除右键“打开方式”里多余选项 第三周刊_No.53|一切都会被支付两次 安卓APP通话记录与录音上传踩坑记录 - 子舒的博客 天量下跌 inBox 笔记 2.3.8,把工具栏交给了你-咕咚同学 我把小龙虾搬到了微信-咕咚同学 安好 - 响石潭 Compound Engineering Plugin:让每个工程单元都比上一个更容易 MOSS-TTS Family:开源高质量语音与声音生成模型家族深度解析 Crawl4AI:专为 LLM 设计的开源 Web 爬虫与数据抓取工具 Build Your Own X:从零实现你最喜欢的技术——程序员进阶的终极资源清单 Anthropic Skills:用文件夹教 Claude 专业技能的开源框架 1年的去月球(下) - 梅之夏 欢迎回来。 简单讲讲 ASN.1 与 OID DTV - 直播聚合客户端 5.22-5.27 – 不兴江 还没去过鸭川 – 不兴江 张晶晶同学三刷林志颖 关于我 – 不兴江 爱与嫉妒 – 不兴江 港股被持续做空 备案码花了四百块-咕咚同学 一句话生成封面:我给公众号做了4种风格的AI封面生成技能 「官」方認證 再谈费曼学习法 2026-05-28T00:34:11+08:00 2026-05-28T00:28:45+08:00 离谱的英语学习指南:基于AI的英语进阶系统方法论 iii:零集成架构的后端统一运行时 Claude Code Harness:让 Claude Code 工作有迹可循的工程化框架 Heretic:全自动移除大语言模型审查机制的开源工具 MarkItDown:微软开源的万能文档转 Markdown 利器 Harness:让 Claude Code 秒变多智能体协作工厂 这段时间尽折腾AI Agent了,确实极大地提高了效率 近期动态:两个新站点正式上线啦 误判解除!zhouayuan.com 腾讯安全申诉成功 - 周阿源|玩具设计・插画日常・生活随笔 Ralph:让 AI 编码工具自主循环跑完所有 PRD 任务的量产神器 全都违法 – 个人工作记录 关于zhouayuan.com被误判 “含违规信息” 的说明与申诉记录 - 周阿源|玩具设计・插画日常・生活随笔 小米 MiMo v2.5 Pro 白嫖 最大的人间清醒,兜里有钱,但是不花。 夜晚靓歌(12):于文文现场solo - 王志勇的Blog 今日插画:风扬起的倔强 - 周阿源|玩具设计・插画日常・生活随笔 回门习俗 独立网卡 - 忘记了回忆 500亿入股人工智能企业 从命令行到桌面智能体-咕咚同学 第一性原理读书笔记 行者微评论223-加班の守株待兔-博客|政治与时事-风雨行者 ZOZO开源物理接触求解器:GPU加速的可扩展仿真引擎 OpenStock:开源股票市场交易平台技术深度解析 MoneyPrinterTurbo:基于AI的全自动短视频生成工具深度解析 Claude-Mem:为 Claude Code 构建的持久化记忆压缩系统 Twenty:可代码化定制的企业级开源 CRM 平台技术深度解析 2026-05-26T22:59:17+08:00 企业级开源大模型部署平台 GPUStack 实战教程 1年的去月球(上) - 梅之夏 Sevalla - 静态网站托管服务 不用翻墙、不用注册、不用月费,普通人也能用上 Claude Code 装修灯具要注意⚠️ 黄梅天先锋 - 游子微博 公安备案顺利办结,站点备案全部完成 - 周阿源|玩具设计・插画日常・生活随笔 第三次兑换天猫超市卡了宗宗酱-三维狐少儿编程 Don't think, feel. - Rolen's Blog 人这一辈子,到底图个什么 博客迁移 - Edwin's Blog 情感赛道写作模板 再现本轮行情的典型特征 裁员与平常心-咕咚同学 别让“偷懒”,成为隐私泄露的破绽 片刻 - Jdeal | Life is like a Design.
云风的 BLOG: 对基本有序的序列排序算法
云风的 BLOG · 2026-06-11 · via BlogFinder

quicksort 是基于比较的排序中表现最好的算法,它在大多数情况下都能接近时间复杂度 O(n log n) ,所以 qsort() 也是 C 标准库中排序的默认实现。但是,quicksort 是非稳定排序,即当原始序列中出现相同的元素时,经过 qsort ,这些相同元素的次序可能被调整。即两个 key 相同的元素,经过排序可能调换次序。

在某些场合,我们希望排序是稳定 stable 的。因为元素的 key 相同,但元素本身可以不相同。这些有着相同 key 的元素一开始以某种次序放入序列,我们不希望对 key 排序后,打乱原有的次序。当需要稳定性时,还有许多经典排序算法可供选择,例如插入排序,它非常简单,每次从无序序列中选择一个元素和有序序列的最后一个元素比较,要么追加在最后,要么向前依次比较,直到找到合适的位置插入。对已经有序的序列,插入排序的时间复杂度为 O(n) ,但对于逆序的序列,它会退化到 O(n^2) ,因为每个元素都要和之前所有元素比较一次,插入到最前。所以插入排序对于非特定场景(无特定规律的数据)的平均时间复杂度接近 O(n^2) 超过快速排序的 O(n log n) 。

但需要注意,光看大 O 时间复杂度是不够的。在 n 很小的时候,对实际开销影响更大的并非算法复杂度。因为复杂度是基于执行算法中每个单步操作的数量估算的,忽略了操作本身的时间差异。而 n 很小时,单个操作的开销占比就变大了。所以在 n 很小时,简单的算法每个操作都更短,对 CPU cache 也更友好,导致实际的总时间开销也越少。大部分排序算法的实现都选择在 n 很小时退化成插入排序。

对无规律数据集,基于比较的排序算法的理论极限是 O (n log n)。因为你需要至少对所有元素做一次比较,采用二分的形式分而治之是最好的方法,不断二分数据集的深度大致为 log n 。归并排序 merge sort 直观的体现了这个想法,它把数据不断对分,展开为一个完全平衡的二叉树,然后从叶节点逐级向根节点调整次序并合并,一路执行到根节点,就可以完成排序。所以它可以严格(即使在最坏情况)做到 O (n log n) 。或者这么看,合并两个有序序列只需要扫描两个序列各一遍,每次都挑出两个序列头部更考前的元素,插入新序列,就能得到合并后的有序序列。假设一共有 128 个元素,不断对分,就能分成 64 组元素每组 2 个。这两个元素调整位置合并成 32 组 4 元组,再继续为 16 组 8 元组,等等。

显然,merge sort 是很容易做到 stable ,但缺点是它难以在原地 in-place 排序,需要额外的空间。这导致实际实现时,需要额外的内存复制。在大多数要求 in-place 排序(大多数 sort api 都是这样)的场合,比 quicksort 要慢,且需要额外的运行空间。

但在真实世界的应用场合,大多数需要排序的数据并非毫无规律。针对有规律的数据排序就可以在工程上针对规律做出改进。一个普遍的规律是:往往需要排序的数据本身就是基本有序的,至少很多片段是局部有序的。因为你很难一次性获得海量的完全随机的数据集。数据都是逐步变多的,如果它们之前的片段是有序的,在整合到一起后,大部分还保持着次序;或者是原有的次序经过少量的调整,破坏了局部的次序。针对这点,就可以对 merge sort 做大幅改进。python 最早的版本直接封装 C 的 qsort() 实现排序,但随着 stable sort 的需求日益增加,在 2002 年 Tim Peters 针对这类情况改进了 merge sort 算法,最终成为 python 排序算法的标准实现。这是一个混合排序算法,一开始并未正式命名,但随着它作为一个比 merge sort 更能适应现实数据集的算法被引入 java 等其它语言的标准库,大家就用第一次工程实现者的名字命名为 timsort 。btw , 它的核心想法并不新,在 Knuth 的计算机算法艺术第三卷的排序算法中就有提到。

timsort 最重要的核心想法是:既然现实中的序列并非完全随机的,那么我们就先找出序列中的有序片段 (run) ,再对这些片段进行 merge sort ,这样就能大大的减少 N 。比如在极端情况下,一开始序列就是完全有序的,那么 N 就退化成了 1 。但是,如果我们一开始完全把序列的有序片段都标记出来,就需要等长甚至更大的内存空间记录这些 run 。原本 merge sort 的缺点就是需要更大的额外空间,但它需要的额外空间是固定的。如果在最坏情况下额外空间的上限更高 ,作为通用库,这是不可接受的。所以,需要一个有着固定上限的额外空间来处理这些 run ,且最坏情况也不能超过 merge sort 。

我们可以把 merge sort 的 run 看成是固定的 1, 2, 4, 8 ... 虽然算法描述是递归的,但由于我们不会处理超过 2^64 个元素,所以递归深度非常有限(低于 64 )。而且在这个固定规律下,很容易得到一个非递归的实现。当 run 的长度不固定时,Tim 认为用一个自适应的方法合并相邻 run 就可以了。应该尽量的将相邻的小 run 合并成更大的片段,而避免把已经很大的 run 上追加相邻的小 run 。至于为什么只能合并相邻的 run ,因为只有这样才能保证 stable 。

他提出顺着扫描序列,找到三个连起来的 run 就可以决定该合并哪两个更好。在 Wikipedia 上 timsort 页有简单的描述,还有一篇 blog 也有解释,cpython 的邮件列表里也有一篇叫 listsort.txt 的文档(链接以不可访问)。我全部读过后发现它们之间有细微的差别,这让我很疑惑。直到我读到 On the Worst-Case Complexity of TimSort 这篇 paper 才确定正确的描述。然后又看了 youtube 上 Quicksort, Timsort, Powersort - PyCon US 2023 talk 进一步印证了这点。

原始的算法是这样的:

用一个栈记录从头扫描每个 run 的长度。比如最后的三个 run 长度为 X Y Z ,其中 Z 是栈顶,也就是最后扫描到的 run 。

首先检查规则 A :当 Z 长度大于 X 时,把 X Y 合并起来,栈顶留下 (X+Y) 和 Z 。

如果不满足规则 A ,再检查规则 B :当 Z 大于等于 Y 时,合并 Y 和 Z ,即栈顶留下 X 和 (Y + Z) 。

若以上都不满足,但满足规则 C :若 Y+Z 大于 X ,则也合并 Y 和 Z ,结果和应用规则 B 一样。

若三条规则都不满足,就继续向栈顶添加后续的 run ,重复这个过程,直到整个序列扫描完,最后依次合并栈顶两个 run 。

这套规则的想法是:尽可能的把较小的相邻 run 合并在一起,但如果连着三个 run 长度类似,避免将仓促合并,而继续看下一个 run 。但实现上需要给栈设定一个最大的上限,因为它是利用 C 的 stack 实现的,需要避免 C stack 溢出。如果遵循以上规则,保留 run 长度的栈就会从大到小排列,因为一旦有更长的 run 入栈,就会引起合并。规则 C 则保证了最前面(栈底)的那一项长度超过后两项的和。这可以保证整个序列最坏情况类似斐波那契数列,大致上是指数分布的。这样,栈的最大容量在 64 位系统上也可以用一个较小的值就能保证不会溢出。

在很长时间里,没有人去验证这个假设是否是严格正确的。这个算法也被抄到了更多地方。直到有人试图用形式化证明 java 的 OpenJDK 正确性才发现有问题。

比如,我们构造这样一个序列,92 28 20 6 4 8 1 ,前 5 项(92 28 20 6 4 )依次入栈时,每一项都不会被合并,同时保证了依次递减,每一项都超过后两项之和。但接下来的 8 入栈就发生了变化,因为 8 > 6 ,所以 6 和 4 被合并位 10 ,序列变成了 92 28 20 10 8 ,这时,中间的 20 + 10 已经超过了前一项 28 。最后一项 1 入栈却并不会促使前面项的合并。这会导致算法根据算出的 2^64 项以内最坏情况下需要的栈容量上限偏小,精心构造一个序列就会导致栈溢出。Python 和 Java 在修复这个问题时采用了两种不同的方案。Python 增加了第四条规则:新入栈的 run 如果没有引起合并,要重复检查一次原有的栈顶三项,看前一轮合并适合做的彻底。这个方法后来被形式化证明是完备的,只不过每轮入栈可能需要多一次判断。Java 一开始的修复方案是重新计算了一个更大的理论上限。不过这个上限后来被证明又算小了,重新打了一次补丁。不过这只是理论值,构造一个超出错误上限造成栈溢出的序列,需要相当大的长度,所以实际上并没有发生过。


依赖形式化证明堆栈上限看起来过于隐晦,需要一个更简单明了的算法确定保存 run 的栈大小上限,这是 python 3.11 引入 Power sort 的动机。了解 power sort 的设计思路可以从回顾 merge sort 开始。merge sort 本质上是基于完全二叉树的逐层合并,所以,即使用递归实现,栈深度也严格保证在 log 2 之内,也就是 2^64 元素最多需要 64 层深度,这一目了然。当 run 的长度不确定,由原始序列中固有的有序片段长度决定,我们还是可以让合并策略尽可能的近似完全二叉树。由于我们假设了原始数据局部有序,所以可以看成是提前做了一些合并,那么理论上需要的栈深度不应超过 mergesort ,最坏情况也是和 mergesort 一致。

所以,可以先想象一个虚拟的完全二叉树,然后在遍历序列时,把每个 run 和虚拟完全二叉树的 run 对比,找到相邻的 run 对应到完全二叉树上是否应该合并,还是已经被合并过了。由于完全二叉树可以通过非常简单的公式算出每个分支片段,所以并不需要额外的储存空间。算法就变成了每次看栈顶两个相邻 run 的中点落在哪里,找到它和虚拟完全二叉树上最接近的节点,记录下它的层级(power),越靠叶子节点的 power 越小,越靠根的 power 越大。由于节点数量限制在 2^64 ,所以 power 最大就是 64 。而合并规则就可以简化成:让保存 run 的栈额外记录每连个相邻 run 的 power ,栈中元素的 power 必须依次增大,如果减少,就合并栈顶两个元素,并重新计算合并后的 power 。显然,栈的容量上限就是 64 ,不需要过多的形式化证明。

powersort 未必一定优于 timsort ,但在实践中它几乎总是略微好一点:更像 merge sort 表现的那样,尽可能的先合并较小的 run ,逐层合并成更大的 run 直到完全有序。当然,它虽然减少了每次合并前的条件判断,但增加了一个常量时间的 power 计算,使用 power 来拟合 merge sort 的完全二分也不总是正确,这些都有可能导致在某些条件下比 timsort 略慢。但关键在于,它运行需要的栈上限清晰明了。

如果想更进一步理解 powersort ,非常推荐上面提到的 PyCon US 2023 上的那个演讲,其中有非常清晰的算法视觉化演示。


除去改进了这个如何启发式合并 run 的算法,powersort 的其它部分和 timsort 是基本一致的。timsort 的其它对 mergesort 的改进几乎都是针对数据局部有序做的,长期实践也证明它的确非常有效。

其一,当数据基本有序时,合并两个 run 需要的额外空间可以大大减少。假设相邻的两个 run 原本就是基本有序的,只是很少的数据调换导致了分割成两片有序 run 。那么,我们可以用二分法找到前一个 run 中前一半不需要移动,以及后一个 run 中后一半不需要移动的两个端点。这两部分都不需要调整位置。剩下的两段,只需要更短的一半额外空间就可以整理成有序的序列。这是因为,我们只需要把其中一个 run 复制出去,然后在空出的位置上做合并。如果是前一半空出来,就从前到后合并;如果是后一半空出来,就从后向前合并。即使是最坏情况,也不会超过 N/2 的额外空间。

其二,在合并两个 run 时,如果发现连续取用其中一个 run 上的数据,就可以进入 Galloping 模式,即用二分法找到一段数据复制,而不是一个个比较。这里给 Galloping 设置了一个限界,逐个比较次数超过这个值时才启动 Galloping mode ,根据 Galloping 的成功率,动态调整这个值。

其三,针对逆序的数据做特别优化。因为把逆序序列倒转过来是很简单的,O(n) 就可以完成,但用传统 merge sort 的方法成本要高得多。在预扫描 run 时,检测前两个元素的次序就可以用来猜测这个 run 是正序还是逆序的。注:如果是逆序的,需要对相同元素做额外一点处理保证 stable 。

其四,由于 mergesort 是二分分治,所以在元素数量不足 2^n 时,复杂度其实是向上补足 2 的整数幂。即处理 1023 个元素和处理 1024 个元素近似,而处理 513 个元素也和处理 1024 元素类似。所以,分片的数量比 2 的整数幂略小一点最合适。timsort/powersort 解决这个问题的方法是设定最小的 run size ,它根据 n 的大小在 32 到 64 之间动态调整。在中间找到一个数 m ,让 n/m 最接近 2 的整数幂。这只需要取 n 的最高 6 位再加 1 就可以了。对于最小的无序 run ,采用插入排序。