<?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>Python on Neil的自留地</title><link>https://neilmin.com/zh/tags/python/</link><description>Recent content in Python 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 00:00:00 -0700</lastBuildDate><atom:link href="https://neilmin.com/zh/tags/python/index.xml" rel="self" type="application/rss+xml"/><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>