<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Interview on Neil的自留地</title><link>https://neilmin.com/zh/tags/interview/</link><description>Recent content in Interview on Neil的自留地</description><image><title>Neil的自留地</title><url>https://neilmin.com/images/papermod-cover.png</url><link>https://neilmin.com/images/papermod-cover.png</link></image><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Sat, 13 Jun 2026 07:00:00 -0700</lastBuildDate><atom:link href="https://neilmin.com/zh/tags/interview/index.xml" rel="self" type="application/rss+xml"/><item><title>RocksDB 是怎么工作的：一份 LSM-Tree 的极简笔记</title><link>https://neilmin.com/zh/posts/how-rocksdb-works/</link><pubDate>Sat, 13 Jun 2026 07:00:00 -0700</pubDate><guid>https://neilmin.com/zh/posts/how-rocksdb-works/</guid><description>准备面试时我把 RocksDB 的工作原理好好学了一遍，这是我整理的一份笔记：RocksDB 是什么、数据怎么写进去、怎么读出来、后台的 compaction 在忙什么，以及绕不开的「三种放大」权衡。不求多专业，只想把 LSM-Tree 的核心思路讲清楚，分享给同样想搞懂它的人。</description><content:encoded><![CDATA[<p>准备面试的时候，我花了点时间，把 RocksDB 的工作原理从头到尾学了一遍——它的存储引擎到底是怎么设计的，数据是怎么写进去、又怎么读出来的。RocksDB（以及它背后的 LSM-Tree）是那种很多人听过、但真要讲清楚就容易卡壳的东西，我自己以前也是。等真的搞懂了，就把里面的核心思路整理成这份笔记，分享给同样想弄明白它的人。</p>
<p>我不敢说讲得有多专业、多全面，但希望读完，你（还有未来的我）能对「RocksDB 大概是怎么转起来的」有一个清楚的整体印象。下面这些英文词我尽量保留原样（LSM-Tree、MemTable、SST、WAL、compaction……），因为面试和文档里大家就是这么说的，硬翻成中文反而别扭。</p>
<h2 id="rocksdb-是什么">RocksDB 是什么</h2>
<p>一句话：<strong>一个可嵌入的、持久化的键值（key-value）存储引擎</strong>。</p>
<ul>
<li><strong>可嵌入（embedded）</strong>：它不是一个像 MySQL 那样单独跑的服务器，而是一个库，直接编进你的程序里，省掉了进程间通信的开销。</li>
<li><strong>持久化</strong>：数据落在磁盘上，崩了也不丢。</li>
<li>2012 年从 Google 的 <strong>LevelDB</strong> fork 出来，用 C++ 写的，专门为 <strong>SSD</strong> 和<strong>写多</strong>的场景做了优化。Meta、Microsoft、Netflix、Uber 都在用。</li>
<li>它<strong>不是分布式的</strong>——副本、分片这些得你自己在上层做。</li>
</ul>
<p>它对外提供的操作很朴素：<code>put(key, value)</code> 写、<code>get(key)</code> 读、<code>delete(key)</code> 删、<code>merge(key, value)</code> 合并、还有 <code>iterator.seek()</code> 做范围扫描。</p>
<h2 id="核心思想lsm-tree">核心思想：LSM-Tree</h2>
<p>RocksDB 的一切都建立在 <strong>LSM-Tree（Log-Structured Merge-Tree，日志结构合并树）</strong> 上。</p>
<p>它要解决的核心矛盾是：<strong>磁盘最怕随机写，最喜欢顺序写</strong>。LSM 树的思路就是——先把写攒在内存里排好序，再一次性顺序地刷到磁盘。换句话说，它<strong>把大量随机写「攒」成了顺序写</strong>，这就是它写得快的根本原因。</p>
<p>结构上，数据分成很多层：最上面一层在内存里，下面一层层在磁盘上，编号 L0、L1、L2……越往下的数据越老、容量越大（通常下一层是上一层的约 10 倍）。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>内存   ┌──────────────────────────────┐
</span></span><span style="display:flex;"><span>       │  MemTable（可写，内部有序）    │  ← 新数据先进这里
</span></span><span style="display:flex;"><span>       └──────────────────────────────┘
</span></span><span style="display:flex;"><span>- - - - - - - - - - - - - - - - - - - - - -  flush（刷盘）
</span></span><span style="display:flex;"><span>磁盘   L0   [SST] [SST] [SST]      ← 最新；文件之间 key 范围可能重叠
</span></span><span style="display:flex;"><span>       L1   [SST][SST][SST][SST]   ← 每层内部 key 不重叠，且更大
</span></span><span style="display:flex;"><span>       L2   [SST][SST] ......      ← 越往下越老、越大（约 ×10）
</span></span><span style="display:flex;"><span>       ...
</span></span></code></pre></div><p>这套结构 1996 年就提出来了，专门优化写密集的负载。除了 RocksDB，Bigtable、HBase、Cassandra、MongoDB 的 WiredTiger 引擎用的也都是 LSM 树。</p>
<h2 id="写数据是怎么进去的">写：数据是怎么进去的</h2>
<p>一次写入，会<strong>同时</strong>落到两个地方：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>put(key, value)
</span></span><span style="display:flex;"><span>      │
</span></span><span style="display:flex;"><span>      ├──► WAL    （顺序追加到磁盘，防崩溃）
</span></span><span style="display:flex;"><span>      │
</span></span><span style="display:flex;"><span>      └──► MemTable（在内存里排好序）
</span></span><span style="display:flex;"><span>                  │  写满约 64MB
</span></span><span style="display:flex;"><span>                  ▼
</span></span><span style="display:flex;"><span>            转为只读，后台线程刷成一个 SST 文件 → 落到 L0
</span></span></code></pre></div><p><strong>MemTable</strong>：内存里的写缓冲，所有增删改都先到这。它内部是<strong>按 key 排好序</strong>的（默认用<strong>跳表 skip list</strong> 实现），这样后面刷盘和范围查询才高效。一个细节：删除不是真的把数据抹掉，而是写一条<strong>墓碑</strong>（tombstone）记录，表示「这个 key 删了」——真正的清理留给后面的 compaction。</p>
<p><strong>WAL（Write-Ahead Log，预写日志）</strong>：MemTable 在内存里，断电就没了。所以每次写也会<strong>顺序追加</strong>一条记录到磁盘上的 WAL 文件，里面有 key、value、操作类型和校验和（checksum）。崩溃重启后，靠重放 WAL 把 MemTable 恢复出来。注意 WAL 是<strong>按写入顺序追加的，不排序</strong>——它图的就是个快。</p>
<p><strong>Flush（刷盘）</strong>：MemTable 写满后会变成只读，换一个新的继续接客；后台线程把这个只读的 MemTable 刷成一个 <strong>SST 文件</strong>，落在 L0。刷完，对应的 WAL 就可以丢了。因为 MemTable 本来就是有序的，这一刷就是一次<strong>顺序写</strong>——LSM 树的精髓就在这儿。</p>
<h2 id="sst-文件长什么样">SST 文件长什么样</h2>
<p><strong>SST（Static Sorted Table，静态有序表）</strong> 是磁盘上真正存数据的文件，一旦写好就不再修改。它里面是一堆<strong>排好序的 key-value</strong>，并且为了查询方便做了精心的分块设计（block，默认 4KB，可以用 Snappy、LZ4、ZSTD 等压缩）。</p>
<p>一个 SST 大致分几部分：</p>
<ul>
<li><strong>数据块（data）</strong>：有序的 key-value。因为相邻 key 很像，可以只存差异（delta encoding）省空间。</li>
<li><strong>索引（index）</strong>：记录每个数据块「最后一个 key → 在文件里的位置」，这样查的时候能<strong>二分</strong>直接定位到某个块，而不用扫整个文件。</li>
<li><strong>布隆过滤器（Bloom filter，可选）</strong>：一个概率型结构，能极快地回答「这个 key <strong>一定不在</strong>这个文件里」。它可能误报「在」，但绝不漏报「不在」——所以特别适合在读的时候先挡掉一大批根本不用查的文件。</li>
</ul>
<h2 id="读数据是怎么找出来的">读：数据是怎么找出来的</h2>
<p>读一个 key，要<strong>从新到老</strong>一层层找，因为新写的值在上层、旧值在下层，第一个找到的就是最新的：</p>
<ol>
<li>先查正在写的 MemTable；</li>
<li>再查还没刷完的只读 MemTable；</li>
<li>再查 L0 的每个 SST 文件（L0 文件之间 key 范围会重叠，所以得挨个看，从新到旧）；</li>
<li>L1 及以下，每层内部 key 不重叠，所以<strong>每层只需定位并查一个文件</strong>。</li>
</ol>
<p>而在<strong>单个 SST 文件</strong>里找，又是三步走：先用 <strong>Bloom filter</strong> 问一句「key 在不在」，不在就直接跳过这个文件；在的话，用 <strong>index 二分</strong>定位到对应的数据块；最后把那个块读出来，在块内找到 key。</p>
<p>所以读的代价，关键看要翻多少层、多少文件——这也引出了下一节要解决的问题。</p>
<h2 id="compaction后台一直在打扫">Compaction：后台一直在「打扫」</h2>
<p>前面说了，删除只是写墓碑、更新也只是写新值盖在旧值上面。时间一长，磁盘上就会堆满<strong>过期的旧版本和墓碑</strong>：既白占空间，又让读的时候要翻越更多文件。</p>
<p><strong>Compaction（压缩合并）</strong> 就是后台干这个清理活的：把某一层的若干 SST 文件，和下一层有重叠的文件合在一起，<strong>丢掉那些被覆盖的旧值和被删的 key</strong>，再写成新的、干净的 SST 放到下一层。因为每个文件本来都有序，合并用的是<strong>多路归并（k-way merge）</strong>，就是归并排序里「合并」那一步的放大版。整个过程在后台线程跑，不挡前台的读写。</p>
<p>RocksDB 默认用 <strong>leveled compaction（分层合并）</strong>：</p>
<ul>
<li><strong>L0</strong> 比较特殊，文件之间 key 范围<strong>允许重叠</strong>（因为它们是 MemTable 直接刷下来的）；当 L0 文件数攒到阈值（默认 4 个）就触发合并。</li>
<li><strong>L1 及以下</strong>，每一层内部所有文件的 key 范围<strong>互不重叠</strong>、整体有序；当某层的总大小超过设定值，就把超出的部分往下一层合并，有时会一路连锁往下压好几层。</li>
</ul>
<h2 id="一切都是权衡三种放大">一切都是权衡：三种「放大」</h2>
<p>理解 RocksDB（其实是所有 LSM 引擎）调优的关键，是三个<strong>放大</strong>（amplification）指标：</p>
<ul>
<li><strong>空间放大（space amplification）</strong>：实际占的磁盘空间 ÷ 数据本身的大小。旧版本和墓碑越多，空间放大越大。</li>
<li><strong>读放大（read amplification）</strong>：读一条数据，底层实际要做几次 I/O。要翻的层和文件越多，读放大越大。</li>
<li><strong>写放大（write amplification）</strong>：写一条数据，底层实际写了几次。同一条数据会在 compaction 里被反复重写到更下面的层，所以写放大会很大。</li>
</ul>
<p>这三者按下葫芦浮起瓢：<strong>compaction 越勤，空间和读放大越小，但写放大越大</strong>；反之亦然。怎么平衡全看你的负载，参数极多且互相影响——连 RocksDB 官方都坦言很难讲清每个参数的确切效果，建议<strong>多做 benchmark，盯着这三个放大指标调</strong>。</p>
<blockquote>
<p><strong>顺带一提：merge 操作</strong></p>
<p>除了 put 和 delete，RocksDB 还有个 <code>merge</code>。当你要对一个值做大量「增量更新」（比如不停往一个计数器或列表上追加）时，传统做法是 read-modify-write：读出来、改、再写回去，很费劲。<code>merge</code> 让你只写「增量」本身，把怎么合并交给一个你自定义的 merge 函数，到读取或 compaction 时再真正算出最终值。<strong>好处</strong>是省写放大、还线程安全；<strong>代价</strong>是读变贵了——没合并之前，每次读都得把这些增量重新算一遍。</p>
</blockquote>
<h2 id="记住这几点就够了">记住这几点就够了</h2>
<p>如果只留一张「脑图」，是这样的：</p>
<ul>
<li><strong>RocksDB</strong> = 可嵌入的持久化 KV 存储，源自 LevelDB，核心是 <strong>LSM-Tree</strong>；</li>
<li><strong>写</strong>：先进内存的 <strong>MemTable</strong>（有序）+ 顺序写 <strong>WAL</strong>（防崩溃）→ 攒满后刷成 <strong>SST</strong> 文件落到 L0 → 后台 <strong>compaction</strong> 慢慢往下整理；</li>
<li><strong>读</strong>：从新到老一层层找，靠 <strong>Bloom filter</strong> + <strong>index</strong> 跳过和定位，少读冤枉文件；</li>
<li><strong>本质</strong>：用「写放大」换「把随机写变成顺序写」的高吞吐——<strong>空间、读、写三种放大之间，永远是权衡，没有免费的午餐</strong>。</li>
</ul>
<p>把这几句记住，RocksDB 的大框架就立起来了。更细的东西——skip list、delta encoding、各种 compaction 策略、参数到底怎么调——等真正用到的时候再往里钻也不迟。</p>
<blockquote>
<p>这份笔记里不少理解，来自 Artem Krylysov 的 <a href="https://artem.krylysov.com/blog/2023/04/19/how-rocksdb-works/">How RocksDB Works</a>——原文讲得非常细致，想往深里走的话很推荐读一读。</p>
</blockquote>
]]></content:encoded></item><item><title>排序算法面试复盘：一份从冒泡到 Timsort 的 Python 参考</title><link>https://neilmin.com/zh/posts/sorting-algorithms-interview-reference/</link><pubDate>Sat, 13 Jun 2026 00:00:00 -0700</pubDate><guid>https://neilmin.com/zh/posts/sorting-algorithms-interview-reference/</guid><description>为准备 coding 面试重新复习排序算法时整理的一份参考：11 种排序的 Python 实现、时间与空间复杂度、稳定性和适用场景，外加快排的多种 partition 写法和容易被忽略的非比较排序。可以照着自检——哪些我会，哪些忘了。</description><content:encoded><![CDATA[<p>最近在准备 coding 面试，把排序算法又从头捋了一遍。捋的过程里我有点被自己吓到：很多东西五年前我是真的记得的，比如快排的 partition 到底怎么写、为什么会退化，现在却要愣一下才能想起来。等翻到非比较排序那一块——计数排序、基数排序、桶排序——我发现那已经基本是一片空白了。</p>
<p>于是干脆把这一遍复习写下来，一方面给后来准备面试的人当个 reference，另一方面也是给未来的自己留个存档：等下次再要面试，我可以回到这里，照着扫一遍，很快就知道「这个我还会、这个我忘了，重点看一下」。</p>
<p>这篇文章的用法很简单：</p>
<ul>
<li>先看下面那张<strong>速查表</strong>，扫一眼就能自检哪些算法你已经忘了；</li>
<li>想细看哪个，用右侧的目录（TOC）直接跳过去；</li>
<li>每个算法都按同一个模板写：<strong>一句话思路 → Python 实现 → 复杂度 → 稳定性和是否原地 → 面试要点</strong>，方便对照。</li>
</ul>
<p>代码全部用 Python 写，因为它最接近伪代码、最容易看清逻辑。</p>
<h2 id="一页速查表">一页速查表</h2>
<p>先把结论摆出来。下面这张表覆盖了本文的全部 11 种排序，面试里被问到复杂度、稳定性的时候，脑子里应该能立刻浮现出这张表。</p>
<table>
  <thead>
      <tr>
          <th>算法</th>
          <th>最好</th>
          <th>平均</th>
          <th>最坏</th>
          <th>空间</th>
          <th style="text-align: center">稳定</th>
          <th style="text-align: center">原地</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>冒泡 Bubble</td>
          <td>O(n)</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>选择 Selection</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>插入 Insertion</td>
          <td>O(n)</td>
          <td>O(n²)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>希尔 Shell</td>
          <td>O(n log n)</td>
          <td>≈O(n^1.3)</td>
          <td>O(n²)</td>
          <td>O(1)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>归并 Merge</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>快排 Quick</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n²)</td>
          <td>O(log n)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>堆排 Heap</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(1)</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>计数 Counting</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>基数 Radix</td>
          <td>O(d·(n+k))</td>
          <td>O(d·(n+k))</td>
          <td>O(d·(n+k))</td>
          <td>O(n+k)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>桶 Bucket</td>
          <td>O(n+k)</td>
          <td>O(n+k)</td>
          <td>O(n²)</td>
          <td>O(n+k)</td>
          <td style="text-align: center">✅*</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Timsort</td>
          <td>O(n)</td>
          <td>O(n log n)</td>
          <td>O(n log n)</td>
          <td>O(n)</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
  </tbody>
</table>
<p>几点说明，免得这张表骗到你：</p>
<ul>
<li><strong>希尔排序</strong>的复杂度取决于「增量序列」，最好情况随用的序列不同而变，所以这里的数字只是常见量级。</li>
<li><strong>快排</strong>标的空间是平均情况下的递归栈深度 O(log n)，最坏会退化到 O(n)；它是原地分区，但递归本身要占栈。</li>
<li><strong>桶排序</strong>的稳定性打了星号：桶内用稳定排序（比如插入排序）它才稳定。</li>
<li><strong>k</strong> 是数据的取值范围，<strong>d</strong> 是数字的位数——非比较排序的复杂度都和数据本身的特征绑在一起，这点后面会细说。</li>
</ul>
<h2 id="写在前面几个绕不开的概念">写在前面：几个绕不开的概念</h2>
<p>在逐个看算法之前，有四个概念几乎每道排序面试题都会用到。把它们先讲清楚，后面就不用反复解释了。</p>
<h3 id="比较排序-vs-非比较排序">比较排序 vs 非比较排序</h3>
<p><strong>比较排序</strong>只通过「两个元素谁大谁小」这一种操作来决定顺序——冒泡、插入、归并、快排、堆排都是。它们的共同点是：理论下界就是 O(n log n)，谁也快不过（原因见下面）。</p>
<p><strong>非比较排序</strong>不靠比较，而是利用元素本身的值去「算」出它该放的位置——计数、基数、桶都是。正因为绕开了比较，它们能做到线性时间 O(n)，但代价是对数据有额外要求（比如必须是范围有限的整数）。</p>
<h3 id="稳定性stability">稳定性（stability）</h3>
<p>如果两个元素的排序键相等，排完序后它们的<strong>相对先后顺序</strong>不变，这个排序就是<strong>稳定的</strong>。</p>
<p>举个具体例子。有一批订单，已经按下单时间排好了，现在要再按金额排序：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>排序前（已按时间）： (100元, 9:00)  (50元, 9:01)  (100元, 9:02)
</span></span><span style="display:flex;"><span>稳定排序后（按金额）：(50元, 9:01)  (100元, 9:00)  (100元, 9:02)   ← 两个 100 元仍保持时间先后
</span></span><span style="display:flex;"><span>不稳定排序后：       (50元, 9:01)  (100元, 9:02)  (100元, 9:00)   ← 两个 100 元的顺序被打乱
</span></span></code></pre></div><p>为什么面试爱问？因为<strong>多关键字排序</strong>依赖它：先按次要键排，再用一个稳定排序按主要键排，次要键的顺序就被保留下来了。记住哪些稳定（冒泡、插入、归并、计数、基数、Timsort）、哪些不稳定（选择、希尔、快排、堆排）几乎是必考点。</p>
<h3 id="原地排序in-place">原地排序（in-place）</h3>
<p>只用 O(1) 或 O(log n) 的额外空间就能完成排序，叫<strong>原地</strong>。归并排序要额外开一个 O(n) 的数组，所以不是原地；快排、堆排只在原数组上倒腾，是原地的。当面试官追问「内存很紧张怎么办」，问的往往就是这个。</p>
<h3 id="复杂度为什么分最好平均最坏">复杂度为什么分最好/平均/最坏</h3>
<p>同一个算法，面对不同输入表现可能天差地别。最典型的是快排：输入随机时是 O(n log n)，但输入已经有序、又恰好每次都挑到最差的 pivot 时，会退化成 O(n²)。面试里报复杂度，最好顺带说清是哪种情况——这恰恰是体现你理解深度的地方。</p>
<blockquote>
<p><strong>为什么比较排序快不过 O(n log n)？</strong>
任何比较排序都可以画成一棵「决策树」：每个内部节点是一次比较，每个叶子是一种可能的最终排列。n 个元素共有 n! 种排列，所以树至少要有 n! 个叶子。一棵高度为 h 的二叉树最多有 2ʰ 个叶子，于是 2ʰ ≥ n!，即 h ≥ log₂(n!)。由斯特林公式，log₂(n!) ≈ n log n。树的高度就是最坏情况下的比较次数，所以下界是 Ω(n log n)。这也解释了为什么想更快，就只能绕开「比较」这件事——也就是非比较排序。</p>
</blockquote>
<p>好，概念铺垫完了，开始逐个看。</p>
<h2 id="比较类排序">比较类排序</h2>
<h3 id="冒泡排序-bubble-sort">冒泡排序 Bubble Sort</h3>
<p><strong>一句话思路</strong>：相邻两个元素两两比较，逆序就交换，每一轮都把当前最大的「冒泡」到末尾。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">bubble_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        swapped <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 每一轮把未排序区间里最大的元素冒泡到右端</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">-</span> i):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&gt;</span> arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]:
</span></span><span style="display:flex;"><span>                arr[j], arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>], arr[j]
</span></span><span style="display:flex;"><span>                swapped <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> swapped:          <span style="color:#75715e"># 一整轮都没交换，说明已经有序，提前收工</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：最坏、平均都是 O(n²)；加了 <code>swapped</code> 提前退出后，对已经有序的输入是 O(n)。空间 O(1)。</li>
<li><strong>稳定性 / 原地</strong>：稳定（只在严格大于时才交换），原地。</li>
<li><strong>面试要点</strong>：实战里基本不用，但它是「稳定 + 提前退出能到 O(n)」的经典例子。注意那个 <code>swapped</code> 优化，常被拿来考。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/sort-an-array/">912. 排序数组</a> —— LeetCode 没有专门考冒泡的题，这道通用排序题可以拿来练手实现（纯 O(n²) 在大数据下会超时，仅作练习）。</li>
</ul>
<h3 id="选择排序-selection-sort">选择排序 Selection Sort</h3>
<p><strong>一句话思路</strong>：每一轮从未排序区间里挑出最小的，放到已排序区间的末尾。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">selection_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        min_idx <span style="color:#f92672">=</span> i
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 在未排序区间 [i+1, n) 里找最小值的下标</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, n):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&lt;</span> arr[min_idx]:
</span></span><span style="display:flex;"><span>                min_idx <span style="color:#f92672">=</span> j
</span></span><span style="display:flex;"><span>        arr[i], arr[min_idx] <span style="color:#f92672">=</span> arr[min_idx], arr[i]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：无论输入长什么样都是 O(n²)——它不会因为数据有序而变快。空间 O(1)。</li>
<li><strong>稳定性 / 原地</strong>：<strong>不稳定</strong>，原地。比如 <code>[5a, 5b, 2]</code>，第一轮把 <code>2</code> 和 <code>5a</code> 交换，两个 5 的相对顺序就反了。</li>
<li><strong>面试要点</strong>：唯一的亮点是<strong>交换次数最少</strong>（最多 n−1 次），在「写操作很贵」的场景下有意义。另外它是「最好情况也救不回来」的反例，常和插入排序对比着考。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/sort-an-array/">912. 排序数组</a> —— 同样用这道通用题练手实现，体会它「交换少、但比较一点不少」的特点。</li>
</ul>
<h3 id="插入排序-insertion-sort">插入排序 Insertion Sort</h3>
<p><strong>一句话思路</strong>：像理扑克牌——从左到右，把每张新牌插到左边已经排好的牌中正确的位置。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">insertion_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, len(arr)):
</span></span><span style="display:flex;"><span>        key <span style="color:#f92672">=</span> arr[i]
</span></span><span style="display:flex;"><span>        j <span style="color:#f92672">=</span> i <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 把比 key 大的元素整体右移一位，给 key 腾出位置</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">while</span> j <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">and</span> arr[j] <span style="color:#f92672">&gt;</span> key:
</span></span><span style="display:flex;"><span>            arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> arr[j]
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        arr[j <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>] <span style="color:#f92672">=</span> key
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：最坏、平均 O(n²)；对<strong>几乎有序</strong>的输入接近 O(n)。空间 O(1)。</li>
<li><strong>稳定性 / 原地</strong>：稳定（<code>while</code> 条件用 <code>&gt;</code> 不是 <code>&gt;=</code>），原地。</li>
<li><strong>面试要点</strong>：别小看它。<strong>数据量小或几乎有序时，插入排序比快排还快</strong>，所以它正是 Timsort、Introsort 这些工业级排序在「小块」上回退使用的算法。基础三件套里，它是最有实用价值的一个。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/insertion-sort-list/">147. 对链表进行插入排序</a> —— 专门为插入排序准备的题：在链表上原地插入。</li>
</ul>
<h3 id="希尔排序-shell-sort">希尔排序 Shell Sort</h3>
<p><strong>一句话思路</strong>：插入排序的升级版。先按一个较大的「间隔」分组做插入排序，再逐步缩小间隔，最后一轮间隔为 1（就是普通插入排序），但此时数组已经「大致有序」，所以很快。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">shell_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    gap <span style="color:#f92672">=</span> n <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> gap <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 对每个间隔为 gap 的子序列做插入排序</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(gap, n):
</span></span><span style="display:flex;"><span>            key <span style="color:#f92672">=</span> arr[i]
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">=</span> i <span style="color:#f92672">-</span> gap
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">while</span> j <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">and</span> arr[j] <span style="color:#f92672">&gt;</span> key:
</span></span><span style="display:flex;"><span>                arr[j <span style="color:#f92672">+</span> gap] <span style="color:#f92672">=</span> arr[j]
</span></span><span style="display:flex;"><span>                j <span style="color:#f92672">-=</span> gap
</span></span><span style="display:flex;"><span>            arr[j <span style="color:#f92672">+</span> gap] <span style="color:#f92672">=</span> key
</span></span><span style="display:flex;"><span>        gap <span style="color:#f92672">//=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：取决于增量序列。上面这种 <code>n//2</code> 折半序列最坏是 O(n²)；换更好的序列（如 Knuth 的 <code>3k+1</code>、Sedgewick 序列）能到 O(n^1.5) 甚至更好。空间 O(1)。</li>
<li><strong>稳定性 / 原地</strong>：<strong>不稳定</strong>（跨间隔交换会打乱相等元素的相对位置），原地。</li>
<li><strong>面试要点</strong>：它是「为什么让数据先变得大致有序，能让插入排序提速」这个思想的代表。面试不常直接考，但作为承上启下的一环值得知道——它把简单的 O(n²) 排序往 O(n log n) 的方向推了一把。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/sort-an-array/">912. 排序数组</a> —— 拿它练手实现希尔排序，可以试试不同增量序列对耗时的影响。</li>
</ul>
<h3 id="归并排序-merge-sort">归并排序 Merge Sort</h3>
<p><strong>一句话思路</strong>：分治。把数组对半切到不能再切，再把两个<strong>已排序</strong>的小数组合并成一个大的有序数组。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">merge_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(arr) <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    mid <span style="color:#f92672">=</span> len(arr) <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    left <span style="color:#f92672">=</span> merge_sort(arr[:mid])
</span></span><span style="display:flex;"><span>    right <span style="color:#f92672">=</span> merge_sort(arr[mid:])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> merge(left, right)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">merge</span>(left, right):
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    i <span style="color:#f92672">=</span> j <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 双指针，每次取两边较小的那个；用 &lt;= 保证稳定</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> i <span style="color:#f92672">&lt;</span> len(left) <span style="color:#f92672">and</span> j <span style="color:#f92672">&lt;</span> len(right):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> left[i] <span style="color:#f92672">&lt;=</span> right[j]:
</span></span><span style="display:flex;"><span>            result<span style="color:#f92672">.</span>append(left[i])
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            result<span style="color:#f92672">.</span>append(right[j])
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>extend(left[i:])   <span style="color:#75715e"># 剩下的直接接上</span>
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>extend(right[j:])
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：最好、平均、最坏<strong>全是 O(n log n)</strong>——非常稳定，不会因为输入退化。空间 O(n)（合并时要额外数组）。</li>
<li><strong>稳定性 / 原地</strong>：稳定，<strong>不是原地</strong>。</li>
<li><strong>面试要点</strong>：复杂度稳如磐石、天然稳定，是「需要稳定 + 最坏也要 O(n log n)」时的首选。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/sort-list/">148. 排序链表</a> —— 排序链表的最优解就是归并；想练数组版可用 <a href="https://leetcode.cn/problems/sort-an-array/">912. 排序数组</a>。</li>
</ul>
<p>下面是两个高频延伸：</p>
<blockquote>
<p><strong>概念补充：链表排序和外部排序</strong></p>
<p><strong>链表排序</strong>：归并排序对链表特别友好——合并链表只要改指针，不需要额外数组，能做到 O(1) 额外空间（不算递归栈）。这也是为什么「O(n log n) 排序一个链表」的标准答案是归并而不是快排。</p>
<p><strong>外部排序（external sort）</strong>：当数据大到内存装不下（经典面试题：「1G 内存怎么排 10G 文件？」），答案就是<strong>外部归并排序</strong>——把大文件切成内存能装下的小块，逐块读进来排好序写回磁盘，再用「多路归并」把这些有序小文件合并成最终结果。归并的精髓「合并多个有序序列」在这里被用到了极致。</p>
</blockquote>
<h3 id="快速排序-quick-sort">快速排序 Quick Sort</h3>
<p>这是我自己复习时最该重点捡起来的一节，partition 的细节五年没碰就模糊了。慢慢来。</p>
<p><strong>一句话思路</strong>：分治。选一个基准值（pivot），通过一次<strong>分区</strong>（partition）把数组拆成「比 pivot 小的」和「比 pivot 大的」两部分，pivot 归位后，再对两边递归。</p>
<h4 id="1-lomuto-分区最好背的版本">1. Lomuto 分区（最好背的版本）</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quick_sort</span>(arr, low<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, high<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> high <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        high <span style="color:#f92672">=</span> len(arr) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> low <span style="color:#f92672">&lt;</span> high:
</span></span><span style="display:flex;"><span>        p <span style="color:#f92672">=</span> partition(arr, low, high)
</span></span><span style="display:flex;"><span>        quick_sort(arr, low, p <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)    <span style="color:#75715e"># 递归左半</span>
</span></span><span style="display:flex;"><span>        quick_sort(arr, p <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, high)   <span style="color:#75715e"># 递归右半</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">partition</span>(arr, low, high):
</span></span><span style="display:flex;"><span>    pivot <span style="color:#f92672">=</span> arr[high]            <span style="color:#75715e"># Lomuto：固定取最右元素作 pivot</span>
</span></span><span style="display:flex;"><span>    i <span style="color:#f92672">=</span> low <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>                  <span style="color:#75715e"># i 是「小于 pivot 区」的右边界</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(low, high):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&lt;</span> pivot:
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            arr[i], arr[j] <span style="color:#f92672">=</span> arr[j], arr[i]
</span></span><span style="display:flex;"><span>    arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>], arr[high] <span style="color:#f92672">=</span> arr[high], arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]   <span style="color:#75715e"># pivot 归位</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Lomuto 的好处是只用一个指针 <code>i</code> 推进，逻辑直观、最好记。面试里手写快排，默认写这个版本就行。</p>
<h4 id="2-为什么会退化以及怎么救">2. 为什么会退化，以及怎么救</h4>
<p>固定取最右元素当 pivot 有个致命问题：<strong>输入已经有序（或逆序）时，每次分区都把数组切成 0 和 n−1 两块</strong>，递归深度变成 n，复杂度退化到 O(n²)，还可能爆栈。</p>
<p>救法是<strong>别让 pivot 的选择被输入「预测」</strong>——随机选一个，或者取「头、中、尾」三个数的中位数（median-of-three）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">partition</span>(arr, low, high):
</span></span><span style="display:flex;"><span>    rand <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>randint(low, high)
</span></span><span style="display:flex;"><span>    arr[rand], arr[high] <span style="color:#f92672">=</span> arr[high], arr[rand]   <span style="color:#75715e"># 随机选 pivot，再换到最右，复用上面的逻辑</span>
</span></span><span style="display:flex;"><span>    pivot <span style="color:#f92672">=</span> arr[high]
</span></span><span style="display:flex;"><span>    i <span style="color:#f92672">=</span> low <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> j <span style="color:#f92672">in</span> range(low, high):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> arr[j] <span style="color:#f92672">&lt;</span> pivot:
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            arr[i], arr[j] <span style="color:#f92672">=</span> arr[j], arr[i]
</span></span><span style="display:flex;"><span>    arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>], arr[high] <span style="color:#f92672">=</span> arr[high], arr[i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>只加两行，就把「有序输入退化」这个最常见的坑堵上了。面试官追问「快排最坏情况怎么办」，这就是标准答案。</p>
<h4 id="3-三路快排对付大量重复元素">3. 三路快排：对付大量重复元素</h4>
<p>如果数组里有<strong>大量重复值</strong>（比如全是 0 和 1），普通快排还是会做很多无谓的递归。三路快排（基于「荷兰国旗问题」）把数组分成 <code>&lt; pivot</code>、<code>== pivot</code>、<code>&gt; pivot</code> 三段，等于 pivot 的那一整段直接跳过：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quick_sort_3way</span>(arr, low<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, high<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> high <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        high <span style="color:#f92672">=</span> len(arr) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> low <span style="color:#f92672">&gt;=</span> high:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    pivot <span style="color:#f92672">=</span> arr[low]
</span></span><span style="display:flex;"><span>    lt, i, gt <span style="color:#f92672">=</span> low, low, high   <span style="color:#75715e"># [low,lt)&lt;pivot  [lt,i)==pivot  (gt,high]&gt;pivot</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> i <span style="color:#f92672">&lt;=</span> gt:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> arr[i] <span style="color:#f92672">&lt;</span> pivot:
</span></span><span style="display:flex;"><span>            arr[lt], arr[i] <span style="color:#f92672">=</span> arr[i], arr[lt]
</span></span><span style="display:flex;"><span>            lt <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> arr[i] <span style="color:#f92672">&gt;</span> pivot:
</span></span><span style="display:flex;"><span>            arr[gt], arr[i] <span style="color:#f92672">=</span> arr[i], arr[gt]
</span></span><span style="display:flex;"><span>            gt <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>               <span style="color:#75715e"># 换过来的元素还没检查，i 不动</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            i <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    quick_sort_3way(arr, low, lt <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    quick_sort_3way(arr, gt <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, high)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：平均 O(n log n)，最坏 O(n²)（用随机 pivot 后几乎遇不到）。空间 O(log n)，是递归栈的开销。</li>
<li><strong>稳定性 / 原地</strong>：<strong>不稳定</strong>（分区里的远距离交换会打乱相等元素），<strong>原地</strong>。</li>
<li><strong>面试要点</strong>：手写默认 Lomuto；被追问最坏情况就讲随机/三数取中；被追问大量重复就讲三路快排。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/sort-an-array/">912. 排序数组</a> —— 提交时记得用随机 pivot，否则遇到有序或大量重复的数据会超时、甚至递归栈溢出。</li>
</ul>
<p>再加一个高频延伸——</p>
<blockquote>
<p><strong>概念补充：Quickselect（快速选择）</strong></p>
<p>「求第 k 大/小的元素」是面试常客。如果只是要第 k 个，没必要全排序：用快排的 partition，每次分区后看 pivot 落在哪，<strong>只递归 k 所在的那一边</strong>。平均 O(n)，比先排序再取（O(n log n)）更快。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">quickselect</span>(arr, k):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;返回第 k 小的元素，k 从 1 开始计数&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    low, high, target <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>, len(arr) <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, k <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> low <span style="color:#f92672">&lt;=</span> high:
</span></span><span style="display:flex;"><span>        p <span style="color:#f92672">=</span> partition(arr, low, high)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> p <span style="color:#f92672">==</span> target:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> arr[p]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> p <span style="color:#f92672">&lt;</span> target:
</span></span><span style="display:flex;"><span>            low <span style="color:#f92672">=</span> p <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>        <span style="color:#75715e"># 目标在右边</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            high <span style="color:#f92672">=</span> p <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>       <span style="color:#75715e"># 目标在左边</span>
</span></span></code></pre></div><p><strong>练手</strong>：<a href="https://leetcode.cn/problems/kth-largest-element-in-an-array/">215. 数组中的第 K 个最大元素</a> —— 用快速选择平均 O(n) 解，正好和堆的解法对照。</p>
</blockquote>
<h3 id="堆排序-heap-sort">堆排序 Heap Sort</h3>
<p><strong>一句话思路</strong>：先把数组建成一个<strong>最大堆</strong>（每个父节点都 ≥ 子节点），堆顶就是最大值；把堆顶换到末尾，堆缩小 1，再调整堆顶下沉，重复直到排完。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">heap_sort</span>(arr):
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 1. 建堆：从最后一个非叶子节点开始，逐个向下调整</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        sift_down(arr, i, n)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 2. 反复把堆顶（最大值）换到末尾，再修复剩下的堆</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> end <span style="color:#f92672">in</span> range(n <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">0</span>, <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        arr[<span style="color:#ae81ff">0</span>], arr[end] <span style="color:#f92672">=</span> arr[end], arr[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>        sift_down(arr, <span style="color:#ae81ff">0</span>, end)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">sift_down</span>(arr, root, size):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        largest <span style="color:#f92672">=</span> root
</span></span><span style="display:flex;"><span>        left, right <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> root <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span> <span style="color:#f92672">*</span> root <span style="color:#f92672">+</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> left <span style="color:#f92672">&lt;</span> size <span style="color:#f92672">and</span> arr[left] <span style="color:#f92672">&gt;</span> arr[largest]:
</span></span><span style="display:flex;"><span>            largest <span style="color:#f92672">=</span> left
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> right <span style="color:#f92672">&lt;</span> size <span style="color:#f92672">and</span> arr[right] <span style="color:#f92672">&gt;</span> arr[largest]:
</span></span><span style="display:flex;"><span>            largest <span style="color:#f92672">=</span> right
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> largest <span style="color:#f92672">==</span> root:      <span style="color:#75715e"># 父节点已经最大，下沉结束</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        arr[root], arr[largest] <span style="color:#f92672">=</span> arr[largest], arr[root]
</span></span><span style="display:flex;"><span>        root <span style="color:#f92672">=</span> largest
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：最好、平均、最坏<strong>全是 O(n log n)</strong>。建堆是 O(n)（不是 O(n log n)，这是个常考的反直觉点），之后 n 次下沉每次 O(log n)。空间 O(1)。</li>
<li><strong>稳定性 / 原地</strong>：<strong>不稳定</strong>，<strong>原地</strong>。</li>
<li><strong>面试要点</strong>：它是<strong>唯一</strong>既保证最坏 O(n log n)、又只用 O(1) 空间的排序——「内存极紧又不能退化」时选它。注意它和<strong>优先队列 / 堆</strong>（heap）是同一套机制：<code>heapq</code>、Top-K 问题、Dijkstra 里的堆，全是这个 <code>sift_down</code> 的变体。用最小堆维护一个大小为 k 的堆求 Top-K，是堆这块的连环考点。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/kth-largest-element-in-an-array/">215. 数组中的第 K 个最大元素</a> —— 堆的经典题（维护一个大小为 k 的最小堆）；它也能用快速选择解，正好对照两种思路。</li>
</ul>
<h2 id="非比较类排序">非比较类排序</h2>
<p>前面所有算法都靠「比较」，所以卡在 O(n log n) 这条线上。接下来三个绕开了比较，用<strong>元素的值本身</strong>当索引去定位，从而做到线性时间——代价是对数据有要求。这一块也是我复习时空白最大的地方，写得细一点。</p>
<h3 id="计数排序-counting-sort">计数排序 Counting Sort</h3>
<p><strong>一句话思路</strong>：统计每个值出现了几次，再用「前缀和」算出每个值在结果里该放的位置，直接放进去。适合<strong>取值范围 k 不大的整数</strong>。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">counting_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> arr:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    lo, hi <span style="color:#f92672">=</span> min(arr), max(arr)
</span></span><span style="display:flex;"><span>    k <span style="color:#f92672">=</span> hi <span style="color:#f92672">-</span> lo <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> k
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> arr:                <span style="color:#75715e"># 1. 计数</span>
</span></span><span style="display:flex;"><span>        count[x <span style="color:#f92672">-</span> lo] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, k):        <span style="color:#75715e"># 2. 前缀和：count[i] 变成「值 ≤ i 的元素个数」</span>
</span></span><span style="display:flex;"><span>        count[i] <span style="color:#f92672">+=</span> count[i <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> reversed(arr):      <span style="color:#75715e"># 3. 从后往前填，保证稳定</span>
</span></span><span style="display:flex;"><span>        count[x <span style="color:#f92672">-</span> lo] <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        result[count[x <span style="color:#f92672">-</span> lo]] <span style="color:#f92672">=</span> x
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：O(n + k)，n 是元素个数，k 是取值范围。空间 O(n + k)。</li>
<li><strong>稳定性 / 原地</strong>：稳定（关键就在第 3 步<strong>从后往前</strong>遍历），不是原地。</li>
<li><strong>面试要点</strong>：当 k 远小于 n（比如给十万个 0–100 的分数排序）时，它吊打任何 O(n log n) 排序。但 k 一旦很大（比如要排任意 32 位整数），空间就爆了——这正是它的适用边界，也是基数排序要解决的问题。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/sort-colors/">75. 颜色分类</a> —— 只有 0、1、2 三种值，计数排序（或三路快排）一趟搞定。</li>
</ul>
<h3 id="基数排序-radix-sort">基数排序 Radix Sort</h3>
<p><strong>一句话思路</strong>：按位排序。从最低位（个位）开始，对每一位用一次<strong>稳定的</strong>计数排序，一直排到最高位。因为每一轮都稳定，排完最高位时整体就有序了。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">radix_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> arr:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    max_val <span style="color:#f92672">=</span> max(arr)
</span></span><span style="display:flex;"><span>    exp <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>                           <span style="color:#75715e"># 当前处理的位：1=个位, 10=十位, 100=百位…</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> max_val <span style="color:#f92672">//</span> exp <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>        arr <span style="color:#f92672">=</span> counting_sort_by_digit(arr, exp)
</span></span><span style="display:flex;"><span>        exp <span style="color:#f92672">*=</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">counting_sort_by_digit</span>(arr, exp):
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> <span style="color:#ae81ff">10</span>                  <span style="color:#75715e"># 十进制，每位只有 0–9</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> arr:
</span></span><span style="display:flex;"><span>        count[(x <span style="color:#f92672">//</span> exp) <span style="color:#f92672">%</span> <span style="color:#ae81ff">10</span>] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">10</span>):
</span></span><span style="display:flex;"><span>        count[i] <span style="color:#f92672">+=</span> count[i <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>] <span style="color:#f92672">*</span> len(arr)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> reversed(arr):           <span style="color:#75715e"># 从后往前，保持这一位排序的稳定性（基数排序正确性的关键）</span>
</span></span><span style="display:flex;"><span>        digit <span style="color:#f92672">=</span> (x <span style="color:#f92672">//</span> exp) <span style="color:#f92672">%</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>        count[digit] <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        result[count[digit]] <span style="color:#f92672">=</span> x
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：O(d·(n + k))，d 是最大数字的位数，k 是基数（这里十进制 k=10）。空间 O(n + k)。</li>
<li><strong>稳定性 / 原地</strong>：稳定，不是原地。</li>
<li><strong>面试要点</strong>：它解决了计数排序「取值范围一大空间就爆」的问题——把一个大整数拆成几位小数字来排。上面的版本只处理非负整数；要支持负数，可以先整体平移成非负，或对正负分别处理。面试常考的点：<strong>为什么必须从低位排到高位？为什么每一位的排序必须稳定？</strong>（因为高位排序时要靠稳定性保留低位已经排好的顺序。）</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/maximum-gap/">164. 最大间距</a> —— 要求线性时间和空间，标准解法正是基数排序或桶排序。</li>
</ul>
<h3 id="桶排序-bucket-sort">桶排序 Bucket Sort</h3>
<p><strong>一句话思路</strong>：把数据按值域均匀分到若干个「桶」里，每个桶内部各自排序，最后按桶的顺序拼起来。适合<strong>均匀分布</strong>的数据。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">bucket_sort</span>(arr):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> arr:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> arr
</span></span><span style="display:flex;"><span>    n <span style="color:#f92672">=</span> len(arr)
</span></span><span style="display:flex;"><span>    buckets <span style="color:#f92672">=</span> [[] <span style="color:#66d9ef">for</span> _ <span style="color:#f92672">in</span> range(n)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> x <span style="color:#f92672">in</span> arr:                     <span style="color:#75715e"># 假设元素均匀分布在 [0, 1)</span>
</span></span><span style="display:flex;"><span>        buckets[int(n <span style="color:#f92672">*</span> x)]<span style="color:#f92672">.</span>append(x)
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> bucket <span style="color:#f92672">in</span> buckets:
</span></span><span style="display:flex;"><span>        insertion_sort(bucket)        <span style="color:#75715e"># 桶内用稳定排序，整体才稳定</span>
</span></span><span style="display:flex;"><span>        result<span style="color:#f92672">.</span>extend(bucket)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></div><ul>
<li><strong>复杂度</strong>：数据均匀分布时平均 O(n + k)；最坏情况（所有元素挤进同一个桶）退化到 O(n²)。空间 O(n + k)。</li>
<li><strong>稳定性 / 原地</strong>：取决于桶内排序——用插入排序就稳定；不是原地。</li>
<li><strong>面试要点</strong>：它的性能完全押在「数据分布是否均匀」上，这是它和计数/基数最大的区别。计数和基数对数据形态不敏感，桶排序敏感。经典适用场景：把一批均匀分布在 [0, 1) 的浮点数排序。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/top-k-frequent-elements/">347. 前 K 个高频元素</a> —— 按出现频次分桶，是这道题最漂亮的解法。</li>
</ul>
<h2 id="现实世界里用的是什么timsort">现实世界里用的是什么：Timsort</h2>
<p>前面这些都是「教科书算法」。但你每天 <code>sorted()</code> 一下，Python 背后跑的其实是 <strong>Timsort</strong>——一个为真实世界数据精心调校过的混合算法。值得单独说，因为面试里能讲出这个，往往是加分项。</p>
<p><strong>核心思想</strong>：真实数据很少是完全随机的，往往<strong>局部已经有序</strong>。Timsort 抓住这一点：</p>
<ol>
<li>先扫描数组，找出已经天然有序的连续片段，叫 <strong>run</strong>；</li>
<li>太短的 run 用<strong>插入排序</strong>补齐到一个最小长度（<code>minrun</code>，通常 32–64）——前面说过，小数组上插入排序最快；</li>
<li>再用<strong>归并排序</strong>的方式，按一套规则把这些 run 两两合并，合并时还有「galloping（飞奔）」模式来加速。</li>
</ol>
<p>所以 Timsort = <strong>归并排序的骨架 + 插入排序的小块优化 + 对已有序数据的特判</strong>。</p>
<ul>
<li><strong>复杂度</strong>：最坏 O(n log n)，但对几乎有序的数据能到 O(n)。空间 O(n)。</li>
<li><strong>稳定性</strong>：稳定。这也是为什么 Python 的 <code>sorted()</code> 和 <code>list.sort()</code> 保证稳定。</li>
<li><strong>冷知识</strong>：Java 给对象排序（<code>Arrays.sort(Object[])</code>）用的也是 Timsort 的变体；而 C++ 的 <code>std::sort</code> 用的是另一个混合算法 <strong>Introsort</strong>（快排打底，递归太深时切换堆排避免退化，小块用插入排序）。「快排 + 堆排 + 插入排序」三合一，思路和 Timsort 异曲同工：<strong>没有银弹，工业级排序都是混合的</strong>。</li>
<li><strong>LeetCode 例题</strong>：<a href="https://leetcode.cn/problems/merge-intervals/">56. 合并区间</a> —— 先排序再扫描合并；在 Python 里那句 <code>sorted()</code> 跑的就是 Timsort，正好体会「真实数据局部有序」带来的加速。</li>
</ul>
<h2 id="面试里到底怎么选--怎么答">面试里到底怎么选 / 怎么答</h2>
<p>把上面的东西浓缩成一张「该用哪个」的决策清单：</p>
<ul>
<li><strong>没有特殊要求，就要快</strong> → 快排（随机 pivot）。这是大多数场景的默认选择。</li>
<li><strong>要稳定，且最坏也得 O(n log n)</strong> → 归并排序。</li>
<li><strong>内存极紧（要 O(1) 空间）又不能退化</strong> → 堆排序。</li>
<li><strong>数据量很小（几十个）或几乎已经有序</strong> → 插入排序。</li>
<li><strong>排链表</strong> → 归并排序。</li>
<li><strong>整数且取值范围不大</strong> → 计数排序。</li>
<li><strong>整数但范围很大（如定长整数 / 字符串）</strong> → 基数排序。</li>
<li><strong>数据均匀分布在一个区间</strong> → 桶排序。</li>
<li><strong>只要第 k 大 / 中位数，不用全排序</strong> → Quickselect。</li>
<li><strong>数据大到内存装不下</strong> → 外部归并排序。</li>
</ul>
<p>几个常见的连环追问，提前想好答案：</p>
<ul>
<li><strong>「哪些排序是稳定的？」</strong> → 冒泡、插入、归并、计数、基数、桶（桶内稳定时）、Timsort。</li>
<li><strong>「快排最坏情况和怎么避免？」</strong> → 有序输入 + 坏 pivot 退化成 O(n²)；用随机 pivot 或三数取中。</li>
<li><strong>「能不能比 O(n log n) 更快？」</strong> → 比较排序不能（决策树下界）；但如果数据是范围有限的整数，可以用非比较排序到 O(n)。</li>
<li><strong>「O(n log n) 又稳定又原地的排序存在吗？」</strong> → 一般实现里不存在；归并稳定但不原地，堆排原地但不稳定，快排原地但不稳定。这是个很好的「理解权衡」的考点。</li>
</ul>
<h2 id="几个常见易错点">几个常见易错点</h2>
<p>复习时我自己踩过或者差点踩的坑，列在这里：</p>
<ul>
<li><strong>选择排序对有序输入也不会变快</strong>——它没有「提前退出」，永远 O(n²)。别和冒泡/插入搞混。</li>
<li><strong>建堆是 O(n) 不是 O(n log n)</strong>。直觉上是 n 个元素各 O(log n)，但仔细算（大部分节点离底很近）是 O(n)。这是个高频反直觉考点。</li>
<li><strong>计数 / 基数排序「从后往前」填结果</strong>——这一步是稳定性的来源，反过来写就不稳定了，而基数排序一旦不稳定就直接错了。</li>
<li><strong>快排的空间不是 O(1)</strong>——虽然原地分区，但递归栈平均 O(log n)、最坏 O(n)。</li>
<li><strong>桶排序的最坏是 O(n²)</strong>——别只记平均 O(n)，分布不均时会退化。</li>
<li><strong>稳定 ≠ 原地</strong>，这是两个独立的维度，面试里经常被一起问，别混为一谈。</li>
</ul>
<h2 id="小结">小结</h2>
<p>排序这块知识，框架其实很清楚：</p>
<ul>
<li><strong>比较排序</strong>卡在 O(n log n)，里面快排最快但会退化、归并稳定但费空间、堆排原地但不稳定——<strong>没有全能选手，全是权衡</strong>；</li>
<li><strong>非比较排序</strong>靠数据特征换来线性时间，但对数据有要求；</li>
<li><strong>工业级排序（Timsort、Introsort）都是混合的</strong>，把几种算法的长处缝在一起。</li>
</ul>
<p>如果你和我一样是隔了几年重新捡起来，建议照着最上面那张速查表过一遍：能默写出实现、说清复杂度和稳定性的就跳过，卡壳的就回到对应章节细看。等下次再准备面试，回到这里再扫一遍就好。</p>
<p>祝面试顺利。</p>
]]></content:encoded></item></channel></rss>