mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-08 21:46:06 +00:00
deploy
This commit is contained in:
@@ -3970,8 +3970,8 @@
|
||||
</div>
|
||||
<h2 id="1133">11.3.3 算法特性<a class="headerlink" href="#1133" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> 、自适应排序</strong> :各轮“冒泡”遍历的数组长度依次为 <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(n - 2\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> ,总和为 <span class="arithmatex">\((n - 1) n / 2\)</span> 。在引入 <code>flag</code> 优化后,最佳时间复杂度可达到 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度为 <span class="arithmatex">\(O(1)\)</span> 、原地排序</strong>:指针 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> 使用常数大小的额外空间。</li>
|
||||
<li><strong>时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span>、自适应排序</strong>:各轮“冒泡”遍历的数组长度依次为 <span class="arithmatex">\(n - 1\)</span>、<span class="arithmatex">\(n - 2\)</span>、<span class="arithmatex">\(\dots\)</span>、<span class="arithmatex">\(2\)</span>、<span class="arithmatex">\(1\)</span> ,总和为 <span class="arithmatex">\((n - 1) n / 2\)</span> 。在引入 <code>flag</code> 优化后,最佳时间复杂度可达到 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度为 <span class="arithmatex">\(O(1)\)</span>、原地排序</strong>:指针 <span class="arithmatex">\(i\)</span> 和 <span class="arithmatex">\(j\)</span> 使用常数大小的额外空间。</li>
|
||||
<li><strong>稳定排序</strong>:由于在“冒泡”中遇到相等元素不交换。</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -3828,7 +3828,7 @@
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n + k)\)</span></strong> :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 <span class="arithmatex">\(\frac{n}{k}\)</span> 。假设排序单个桶使用 <span class="arithmatex">\(O(\frac{n}{k} \log\frac{n}{k})\)</span> 时间,则排序所有桶使用 <span class="arithmatex">\(O(n \log\frac{n}{k})\)</span> 时间。<strong>当桶数量 <span class="arithmatex">\(k\)</span> 比较大时,时间复杂度则趋向于 <span class="arithmatex">\(O(n)\)</span></strong> 。合并结果时需要遍历所有桶和元素,花费 <span class="arithmatex">\(O(n + k)\)</span> 时间。</li>
|
||||
<li><strong>自适应排序</strong>:在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 <span class="arithmatex">\(O(n^2)\)</span> 时间。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n + k)\)</span> 、非原地排序</strong> :需要借助 <span class="arithmatex">\(k\)</span> 个桶和总共 <span class="arithmatex">\(n\)</span> 个元素的额外空间。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n + k)\)</span>、非原地排序</strong>:需要借助 <span class="arithmatex">\(k\)</span> 个桶和总共 <span class="arithmatex">\(n\)</span> 个元素的额外空间。</li>
|
||||
<li>桶排序是否稳定取决于排序桶内元素的算法是否稳定。</li>
|
||||
</ul>
|
||||
<h2 id="1183">11.8.3 如何实现平均分配<a class="headerlink" href="#1183" title="Permanent link">¶</a></h2>
|
||||
|
||||
@@ -3766,11 +3766,11 @@
|
||||
</div>
|
||||
<h2 id="1192">11.9.2 完整实现<a class="headerlink" href="#1192" title="Permanent link">¶</a></h2>
|
||||
<p>细心的同学可能发现,<strong>如果输入数据是对象,上述步骤 <code>3.</code> 就失效了</strong>。假设输入数据是商品对象,我们想要按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。</p>
|
||||
<p>那么如何才能得到原数据的排序结果呢?我们首先计算 <code>counter</code> 的“前缀和”。顾名思义,索引 <code>i</code> 处的前缀和 <code>prefix[i]</code> 等于数组前 <code>i</code> 个元素之和,即:</p>
|
||||
<p>那么如何才能得到原数据的排序结果呢?我们首先计算 <code>counter</code> 的“前缀和”。顾名思义,索引 <code>i</code> 处的前缀和 <code>prefix[i]</code> 等于数组前 <code>i</code> 个元素之和:</p>
|
||||
<div class="arithmatex">\[
|
||||
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
|
||||
\]</div>
|
||||
<p><strong>前缀和具有明确的意义,<code>prefix[num] - 1</code> 代表元素 <code>num</code> 在结果数组 <code>res</code> 中最后一次出现的索引</strong>。这个信息非常关键,因为它告诉我们各个元素应该出现在结果数组的哪个位置。接下来,我们倒序遍历原数组 <code>nums</code> 的每个元素 <code>num</code> ,在每轮迭代中执行:</p>
|
||||
<p><strong>前缀和具有明确的意义,<code>prefix[num] - 1</code> 代表元素 <code>num</code> 在结果数组 <code>res</code> 中最后一次出现的索引</strong>。这个信息非常关键,因为它告诉我们各个元素应该出现在结果数组的哪个位置。接下来,我们倒序遍历原数组 <code>nums</code> 的每个元素 <code>num</code> ,在每轮迭代中执行以下两步。</p>
|
||||
<ol>
|
||||
<li>将 <code>num</code> 填入数组 <code>res</code> 的索引 <code>prefix[num] - 1</code> 处。</li>
|
||||
<li>令前缀和 <code>prefix[num]</code> 减小 <span class="arithmatex">\(1\)</span> ,从而得到下次放置 <code>num</code> 的索引。</li>
|
||||
@@ -4196,7 +4196,7 @@
|
||||
<h2 id="1193">11.9.3 算法特性<a class="headerlink" href="#1193" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n + m)\)</span></strong> :涉及遍历 <code>nums</code> 和遍历 <code>counter</code> ,都使用线性时间。一般情况下 <span class="arithmatex">\(n \gg m\)</span> ,时间复杂度趋于 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n + m)\)</span> 、非原地排序</strong> :借助了长度分别为 <span class="arithmatex">\(n\)</span> 和 <span class="arithmatex">\(m\)</span> 的数组 <code>res</code> 和 <code>counter</code> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n + m)\)</span>、非原地排序</strong>:借助了长度分别为 <span class="arithmatex">\(n\)</span> 和 <span class="arithmatex">\(m\)</span> 的数组 <code>res</code> 和 <code>counter</code> 。</li>
|
||||
<li><strong>稳定排序</strong>:由于向 <code>res</code> 中填充元素的顺序是“从右向左”的,因此倒序遍历 <code>nums</code> 可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 <code>nums</code> 也可以得到正确的排序结果,但结果是非稳定的。</li>
|
||||
</ul>
|
||||
<h2 id="1194">11.9.4 局限性<a class="headerlink" href="#1194" title="Permanent link">¶</a></h2>
|
||||
|
||||
@@ -3446,7 +3446,7 @@
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>阅读本节前,请确保已学完“堆“章节。</p>
|
||||
</div>
|
||||
<p>「堆排序 heap sort」是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序:</p>
|
||||
<p>「堆排序 heap sort」是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序。</p>
|
||||
<ol>
|
||||
<li>输入数组并建立小顶堆,此时最小元素位于堆顶。</li>
|
||||
<li>不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。</li>
|
||||
@@ -3963,8 +3963,8 @@
|
||||
</div>
|
||||
<h2 id="1172">11.7.2 算法特性<a class="headerlink" href="#1172" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span> 、非自适应排序</strong> :建堆操作使用 <span class="arithmatex">\(O(n)\)</span> 时间。从堆中提取最大元素的时间复杂度为 <span class="arithmatex">\(O(\log n)\)</span> ,共循环 <span class="arithmatex">\(n - 1\)</span> 轮。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(1)\)</span> 、原地排序</strong> :几个指针变量使用 <span class="arithmatex">\(O(1)\)</span> 空间。元素交换和堆化操作都是在原数组上进行的。</li>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span>、非自适应排序</strong>:建堆操作使用 <span class="arithmatex">\(O(n)\)</span> 时间。从堆中提取最大元素的时间复杂度为 <span class="arithmatex">\(O(\log n)\)</span> ,共循环 <span class="arithmatex">\(n - 1\)</span> 轮。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(1)\)</span>、原地排序</strong>:几个指针变量使用 <span class="arithmatex">\(O(1)\)</span> 空间。元素交换和堆化操作都是在原数组上进行的。</li>
|
||||
<li><strong>非稳定排序</strong>:在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -3676,15 +3676,15 @@
|
||||
</div>
|
||||
<h2 id="1142">11.4.2 算法特性<a class="headerlink" href="#1142" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n^2)\)</span> 、自适应排序</strong> :最差情况下,每次插入操作分别需要循环 <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(n-2\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> 次,求和得到 <span class="arithmatex">\((n - 1) n / 2\)</span> ,因此时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(1)\)</span> 、原地排序</strong> :指针 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> 使用常数大小的额外空间。</li>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n^2)\)</span>、自适应排序</strong>:最差情况下,每次插入操作分别需要循环 <span class="arithmatex">\(n - 1\)</span>、<span class="arithmatex">\(n-2\)</span>、<span class="arithmatex">\(\dots\)</span>、<span class="arithmatex">\(2\)</span>、<span class="arithmatex">\(1\)</span> 次,求和得到 <span class="arithmatex">\((n - 1) n / 2\)</span> ,因此时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(1)\)</span>、原地排序</strong>:指针 <span class="arithmatex">\(i\)</span> 和 <span class="arithmatex">\(j\)</span> 使用常数大小的额外空间。</li>
|
||||
<li><strong>稳定排序</strong>:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。</li>
|
||||
</ul>
|
||||
<h2 id="1143">11.4.3 插入排序优势<a class="headerlink" href="#1143" title="Permanent link">¶</a></h2>
|
||||
<p>插入排序的时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> ,而我们即将学习的快速排序的时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> 。尽管插入排序的时间复杂度相比快速排序更高,<strong>但在数据量较小的情况下,插入排序通常更快</strong>。</p>
|
||||
<p>这个结论与线性查找和二分查找的适用情况的结论类似。快速排序这类 <span class="arithmatex">\(O(n \log n)\)</span> 的算法属于基于分治的排序算法,往往包含更多单元计算操作。而在数据量较小时,<span class="arithmatex">\(n^2\)</span> 和 <span class="arithmatex">\(n \log n\)</span> 的数值比较接近,复杂度不占主导作用;每轮中的单元操作数量起到决定性因素。</p>
|
||||
<p>实际上,许多编程语言(例如 Java)的内置排序函数都采用了插入排序,大致思路为:对于长数组,采用基于分治的排序算法,例如快速排序;对于短数组,直接使用插入排序。</p>
|
||||
<p>虽然冒泡排序、选择排序和插入排序的时间复杂度都为 <span class="arithmatex">\(O(n^2)\)</span> ,但在实际情况中,<strong>插入排序的使用频率显著高于冒泡排序和选择排序</strong>。这是因为:</p>
|
||||
<p>虽然冒泡排序、选择排序和插入排序的时间复杂度都为 <span class="arithmatex">\(O(n^2)\)</span> ,但在实际情况中,<strong>插入排序的使用频率显著高于冒泡排序和选择排序</strong>,主要有以下原因。</p>
|
||||
<ul>
|
||||
<li>冒泡排序基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;插入排序基于元素赋值实现,仅需 1 个单元操作。因此,<strong>冒泡排序的计算开销通常比插入排序更高</strong>。</li>
|
||||
<li>选择排序在任何情况下的时间复杂度都为 <span class="arithmatex">\(O(n^2)\)</span> 。<strong>如果给定一组部分有序的数据,插入排序通常比选择排序效率更高</strong>。</li>
|
||||
|
||||
@@ -3456,7 +3456,7 @@
|
||||
|
||||
|
||||
<h1 id="116">11.6 归并排序<a class="headerlink" href="#116" title="Permanent link">¶</a></h1>
|
||||
<p>「归并排序 merge sort」是一种基于分治策略的排序算法,包含图 11-10 所示的“划分”和“合并”阶段:</p>
|
||||
<p>「归并排序 merge sort」是一种基于分治策略的排序算法,包含图 11-10 所示的“划分”和“合并”阶段。</p>
|
||||
<ol>
|
||||
<li><strong>划分阶段</strong>:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。</li>
|
||||
<li><strong>合并阶段</strong>:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。</li>
|
||||
@@ -3465,7 +3465,7 @@
|
||||
<p align="center"> 图 11-10 归并排序的划分与合并阶段 </p>
|
||||
|
||||
<h2 id="1161">11.6.1 算法流程<a class="headerlink" href="#1161" title="Permanent link">¶</a></h2>
|
||||
<p>如图 11-11 所示,“划分阶段”从顶至底递归地将数组从中点切为两个子数组:</p>
|
||||
<p>如图 11-11 所示,“划分阶段”从顶至底递归地将数组从中点切为两个子数组。</p>
|
||||
<ol>
|
||||
<li>计算数组中点 <code>mid</code> ,递归划分左子数组(区间 <code>[left, mid]</code> )和右子数组(区间 <code>[mid + 1, right]</code> )。</li>
|
||||
<li>递归执行步骤 <code>1.</code> ,直至子数组区间长度为 1 时,终止递归划分。</li>
|
||||
@@ -3507,7 +3507,7 @@
|
||||
</div>
|
||||
<p align="center"> 图 11-11 归并排序步骤 </p>
|
||||
|
||||
<p>观察发现,归并排序的递归顺序与二叉树的后序遍历相同,对比来看:</p>
|
||||
<p>观察发现,归并排序与二叉树后序遍历的递归顺序是一致的。</p>
|
||||
<ul>
|
||||
<li><strong>后序遍历</strong>:先递归左子树,再递归右子树,最后处理根节点。</li>
|
||||
<li><strong>归并排序</strong>:先递归左子数组,再递归右子数组,最后处理合并。</li>
|
||||
@@ -4053,22 +4053,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>合并方法 <code>merge()</code> 代码中的难点包括:</p>
|
||||
<p>实现合并函数 <code>merge()</code> 存在以下难点。</p>
|
||||
<ul>
|
||||
<li><strong>在阅读代码时,需要特别注意各个变量的含义</strong>。<code>nums</code> 的待合并区间为 <code>[left, right]</code> ,但由于 <code>tmp</code> 仅复制了 <code>nums</code> 该区间的元素,因此 <code>tmp</code> 对应区间为 <code>[0, right - left]</code> 。</li>
|
||||
<li><strong>需要特别注意各个变量的含义</strong>。<code>nums</code> 的待合并区间为 <code>[left, right]</code> ,但由于 <code>tmp</code> 仅复制了 <code>nums</code> 该区间的元素,因此 <code>tmp</code> 对应区间为 <code>[0, right - left]</code> 。</li>
|
||||
<li>在比较 <code>tmp[i]</code> 和 <code>tmp[j]</code> 的大小时,<strong>还需考虑子数组遍历完成后的索引越界问题</strong>,即 <code>i > leftEnd</code> 和 <code>j > rightEnd</code> 的情况。索引越界的优先级是最高的,如果左子数组已经被合并完了,那么不需要继续比较,直接合并右子数组元素即可。</li>
|
||||
</ul>
|
||||
<h2 id="1162">11.6.2 算法特性<a class="headerlink" href="#1162" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span> 、非自适应排序</strong> :划分产生高度为 <span class="arithmatex">\(\log n\)</span> 的递归树,每层合并的总操作数量为 <span class="arithmatex">\(n\)</span> ,因此总体时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n)\)</span> 、非原地排序</strong> :递归深度为 <span class="arithmatex">\(\log n\)</span> ,使用 <span class="arithmatex">\(O(\log n)\)</span> 大小的栈帧空间。合并操作需要借助辅助数组实现,使用 <span class="arithmatex">\(O(n)\)</span> 大小的额外空间。</li>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span>、非自适应排序</strong>:划分产生高度为 <span class="arithmatex">\(\log n\)</span> 的递归树,每层合并的总操作数量为 <span class="arithmatex">\(n\)</span> ,因此总体时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n)\)</span>、非原地排序</strong>:递归深度为 <span class="arithmatex">\(\log n\)</span> ,使用 <span class="arithmatex">\(O(\log n)\)</span> 大小的栈帧空间。合并操作需要借助辅助数组实现,使用 <span class="arithmatex">\(O(n)\)</span> 大小的额外空间。</li>
|
||||
<li><strong>稳定排序</strong>:在合并过程中,相等元素的次序保持不变。</li>
|
||||
</ul>
|
||||
<h2 id="1163">11.6.3 链表排序 *<a class="headerlink" href="#1163" title="Permanent link">¶</a></h2>
|
||||
<p>归并排序在排序链表时具有显著优势,空间复杂度可以优化至 <span class="arithmatex">\(O(1)\)</span> ,原因如下:</p>
|
||||
<p>对于链表,归并排序相较于其他排序算法具有显著优势,<strong>可以将链表排序任务的空间复杂度优化至 <span class="arithmatex">\(O(1)\)</span></strong> 。</p>
|
||||
<ul>
|
||||
<li>由于链表仅需改变指针就可实现节点的增删操作,因此合并阶段(将两个短有序链表合并为一个长有序链表)无须创建辅助链表。</li>
|
||||
<li>通过使用“迭代划分”替代“递归划分”,可省去递归使用的栈帧空间。</li>
|
||||
<li><strong>划分阶段</strong>:可以通过使用“迭代”替代“递归”来实现链表划分工作,从而省去递归使用的栈帧空间。</li>
|
||||
<li><strong>合并阶段</strong>:在链表中,节点增删操作仅需改变引用(指针)即可实现,因此合并阶段(将两个短有序链表合并为一个长有序链表)无须创建额外链表。</li>
|
||||
</ul>
|
||||
<p>具体实现细节比较复杂,有兴趣的同学可以查阅相关资料进行学习。</p>
|
||||
|
||||
|
||||
@@ -4030,19 +4030,19 @@
|
||||
</div>
|
||||
<h2 id="1152">11.5.2 算法特性<a class="headerlink" href="#1152" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span> 、自适应排序</strong> :在平均情况下,哨兵划分的递归层数为 <span class="arithmatex">\(\log n\)</span> ,每层中的总循环数为 <span class="arithmatex">\(n\)</span> ,总体使用 <span class="arithmatex">\(O(n \log n)\)</span> 时间。在最差情况下,每轮哨兵划分操作都将长度为 <span class="arithmatex">\(n\)</span> 的数组划分为长度为 <span class="arithmatex">\(0\)</span> 和 <span class="arithmatex">\(n - 1\)</span> 的两个子数组,此时递归层数达到 <span class="arithmatex">\(n\)</span> 层,每层中的循环数为 <span class="arithmatex">\(n\)</span> ,总体使用 <span class="arithmatex">\(O(n^2)\)</span> 时间。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n)\)</span> 、原地排序</strong> :在输入数组完全倒序的情况下,达到最差递归深度 <span class="arithmatex">\(n\)</span> ,使用 <span class="arithmatex">\(O(n)\)</span> 栈帧空间。排序操作是在原数组上进行的,未借助额外数组。</li>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span>、自适应排序</strong>:在平均情况下,哨兵划分的递归层数为 <span class="arithmatex">\(\log n\)</span> ,每层中的总循环数为 <span class="arithmatex">\(n\)</span> ,总体使用 <span class="arithmatex">\(O(n \log n)\)</span> 时间。在最差情况下,每轮哨兵划分操作都将长度为 <span class="arithmatex">\(n\)</span> 的数组划分为长度为 <span class="arithmatex">\(0\)</span> 和 <span class="arithmatex">\(n - 1\)</span> 的两个子数组,此时递归层数达到 <span class="arithmatex">\(n\)</span> 层,每层中的循环数为 <span class="arithmatex">\(n\)</span> ,总体使用 <span class="arithmatex">\(O(n^2)\)</span> 时间。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n)\)</span>、原地排序</strong>:在输入数组完全倒序的情况下,达到最差递归深度 <span class="arithmatex">\(n\)</span> ,使用 <span class="arithmatex">\(O(n)\)</span> 栈帧空间。排序操作是在原数组上进行的,未借助额外数组。</li>
|
||||
<li><strong>非稳定排序</strong>:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。</li>
|
||||
</ul>
|
||||
<h2 id="1153">11.5.3 快排为什么快?<a class="headerlink" href="#1153" title="Permanent link">¶</a></h2>
|
||||
<p>从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与“归并排序”和“堆排序”相同,但通常快速排序的效率更高,原因如下:</p>
|
||||
<p>从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与“归并排序”和“堆排序”相同,但通常快速排序的效率更高,主要有以下原因。</p>
|
||||
<ul>
|
||||
<li><strong>出现最差情况的概率很低</strong>:虽然快速排序的最差时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> ,没有归并排序稳定,但在绝大多数情况下,快速排序能在 <span class="arithmatex">\(O(n \log n)\)</span> 的时间复杂度下运行。</li>
|
||||
<li><strong>缓存使用效率高</strong>:在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像“堆排序”这类算法需要跳跃式访问元素,从而缺乏这一特性。</li>
|
||||
<li><strong>复杂度的常数系数低</strong>:在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与“插入排序”比“冒泡排序”更快的原因类似。</li>
|
||||
</ul>
|
||||
<h2 id="1154">11.5.4 基准数优化<a class="headerlink" href="#1154" title="Permanent link">¶</a></h2>
|
||||
<p><strong>快速排序在某些输入下的时间效率可能降低</strong>。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 <span class="arithmatex">\(n - 1\)</span> 、右子数组长度为 <span class="arithmatex">\(0\)</span> 。如此递归下去,每轮哨兵划分后的右子数组长度都为 <span class="arithmatex">\(0\)</span> ,分治策略失效,快速排序退化为“冒泡排序”。</p>
|
||||
<p><strong>快速排序在某些输入下的时间效率可能降低</strong>。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 <span class="arithmatex">\(n - 1\)</span>、右子数组长度为 <span class="arithmatex">\(0\)</span> 。如此递归下去,每轮哨兵划分后的右子数组长度都为 <span class="arithmatex">\(0\)</span> ,分治策略失效,快速排序退化为“冒泡排序”。</p>
|
||||
<p>为了尽量避免这种情况发生,<strong>我们可以优化哨兵划分中的基准数的选取策略</strong>。例如,我们可以随机选取一个元素作为基准数。然而,如果运气不佳,每次都选到不理想的基准数,效率仍然不尽如人意。</p>
|
||||
<p>需要注意的是,编程语言通常生成的是“伪随机数”。如果我们针对伪随机数序列构建一个特定的测试样例,那么快速排序的效率仍然可能劣化。</p>
|
||||
<p>为了进一步改进,我们可以在数组中选取三个候选元素(通常为数组的首、尾、中点元素),<strong>并将这三个候选元素的中位数作为基准数</strong>。这样一来,基准数“既不太小也不太大”的概率将大幅提升。当然,我们还可以选取更多候选元素,以进一步提高算法的稳健性。采用这种方法后,时间复杂度劣化至 <span class="arithmatex">\(O(n^2)\)</span> 的概率大大降低。</p>
|
||||
|
||||
@@ -4101,8 +4101,8 @@ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
|
||||
<h2 id="11102">11.10.2 算法特性<a class="headerlink" href="#11102" title="Permanent link">¶</a></h2>
|
||||
<p>相较于计数排序,基数排序适用于数值范围较大的情况,<strong>但前提是数据必须可以表示为固定位数的格式,且位数不能过大</strong>。例如,浮点数不适合使用基数排序,因为其位数 <span class="arithmatex">\(k\)</span> 过大,可能导致时间复杂度 <span class="arithmatex">\(O(nk) \gg O(n^2)\)</span> 。</p>
|
||||
<ul>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(nk)\)</span></strong> :设数据量为 <span class="arithmatex">\(n\)</span> 、数据为 <span class="arithmatex">\(d\)</span> 进制、最大位数为 <span class="arithmatex">\(k\)</span> ,则对某一位执行计数排序使用 <span class="arithmatex">\(O(n + d)\)</span> 时间,排序所有 <span class="arithmatex">\(k\)</span> 位使用 <span class="arithmatex">\(O((n + d)k)\)</span> 时间。通常情况下,<span class="arithmatex">\(d\)</span> 和 <span class="arithmatex">\(k\)</span> 都相对较小,时间复杂度趋向 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n + d)\)</span> 、非原地排序</strong> :与计数排序相同,基数排序需要借助长度为 <span class="arithmatex">\(n\)</span> 和 <span class="arithmatex">\(d\)</span> 的数组 <code>res</code> 和 <code>counter</code> 。</li>
|
||||
<li><strong>时间复杂度 <span class="arithmatex">\(O(nk)\)</span></strong>:设数据量为 <span class="arithmatex">\(n\)</span>、数据为 <span class="arithmatex">\(d\)</span> 进制、最大位数为 <span class="arithmatex">\(k\)</span> ,则对某一位执行计数排序使用 <span class="arithmatex">\(O(n + d)\)</span> 时间,排序所有 <span class="arithmatex">\(k\)</span> 位使用 <span class="arithmatex">\(O((n + d)k)\)</span> 时间。通常情况下,<span class="arithmatex">\(d\)</span> 和 <span class="arithmatex">\(k\)</span> 都相对较小,时间复杂度趋向 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(n + d)\)</span>、非原地排序</strong>:与计数排序相同,基数排序需要借助长度为 <span class="arithmatex">\(n\)</span> 和 <span class="arithmatex">\(d\)</span> 的数组 <code>res</code> 和 <code>counter</code> 。</li>
|
||||
<li><strong>稳定排序</strong>:与计数排序相同。</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -3692,8 +3692,8 @@
|
||||
</div>
|
||||
<h2 id="1121">11.2.1 算法特性<a class="headerlink" href="#1121" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li><strong>时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> 、非自适应排序</strong>:外循环共 <span class="arithmatex">\(n - 1\)</span> 轮,第一轮的未排序区间长度为 <span class="arithmatex">\(n\)</span> ,最后一轮的未排序区间长度为 <span class="arithmatex">\(2\)</span> ,即各轮外循环分别包含 <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> 轮内循环,求和为 <span class="arithmatex">\(\frac{(n - 1)(n + 2)}{2}\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(1)\)</span> 、原地排序</strong>:指针 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> 使用常数大小的额外空间。</li>
|
||||
<li><strong>时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span>、非自适应排序</strong>:外循环共 <span class="arithmatex">\(n - 1\)</span> 轮,第一轮的未排序区间长度为 <span class="arithmatex">\(n\)</span> ,最后一轮的未排序区间长度为 <span class="arithmatex">\(2\)</span> ,即各轮外循环分别包含 <span class="arithmatex">\(n\)</span>、<span class="arithmatex">\(n - 1\)</span>、<span class="arithmatex">\(\dots\)</span>、<span class="arithmatex">\(3\)</span>、<span class="arithmatex">\(2\)</span> 轮内循环,求和为 <span class="arithmatex">\(\frac{(n - 1)(n + 2)}{2}\)</span> 。</li>
|
||||
<li><strong>空间复杂度 <span class="arithmatex">\(O(1)\)</span>、原地排序</strong>:指针 <span class="arithmatex">\(i\)</span> 和 <span class="arithmatex">\(j\)</span> 使用常数大小的额外空间。</li>
|
||||
<li><strong>非稳定排序</strong>:如图 11-3 所示,元素 <code>nums[i]</code> 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。</li>
|
||||
</ul>
|
||||
<p><img alt="选择排序非稳定示例" src="../selection_sort.assets/selection_sort_instability.png" /></p>
|
||||
|
||||
@@ -3471,7 +3471,7 @@
|
||||
</code></pre></div>
|
||||
<p><strong>自适应性</strong>:「自适应排序」的时间复杂度会受输入数据的影响,即最佳、最差、平均时间复杂度并不完全相等。</p>
|
||||
<p>自适应性需要根据具体情况来评估。如果最差时间复杂度差于平均时间复杂度,说明排序算法在某些数据下性能可能劣化,因此被视为负面属性;而如果最佳时间复杂度优于平均时间复杂度,则被视为正面属性。</p>
|
||||
<p><strong>是否基于比较</strong>:「基于比较的排序」依赖于比较运算符(<span class="arithmatex">\(<\)</span> , <span class="arithmatex">\(=\)</span> , <span class="arithmatex">\(>\)</span>)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> 。而「非比较排序」不使用比较运算符,时间复杂度可达 <span class="arithmatex">\(O(n)\)</span> ,但其通用性相对较差。</p>
|
||||
<p><strong>是否基于比较</strong>:「基于比较的排序」依赖于比较运算符(<span class="arithmatex">\(<\)</span>、<span class="arithmatex">\(=\)</span>、<span class="arithmatex">\(>\)</span>)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> 。而「非比较排序」不使用比较运算符,时间复杂度可达 <span class="arithmatex">\(O(n)\)</span> ,但其通用性相对较差。</p>
|
||||
<h2 id="1112">11.1.2 理想排序算法<a class="headerlink" href="#1112" title="Permanent link">¶</a></h2>
|
||||
<p><strong>运行快、原地、稳定、正向自适应、通用性好</strong>。显然,迄今为止尚未发现兼具以上所有特性的排序算法。因此,在选择排序算法时,需要根据具体的数据特点和问题需求来决定。</p>
|
||||
<p>接下来,我们将共同学习各种排序算法,并基于上述评价维度对各个排序算法的优缺点进行分析。</p>
|
||||
|
||||
@@ -3459,7 +3459,7 @@
|
||||
<div class="admonition question">
|
||||
<p class="admonition-title">关于尾递归优化,为什么选短的数组能保证递归深度不超过 <span class="arithmatex">\(\log n\)</span> ?</p>
|
||||
<p>递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在尾递归优化后,向下递归的子数组长度最大为原数组的一半长度。假设最差情况,一直为一半长度,那么最终的递归深度就是 <span class="arithmatex">\(\log n\)</span> 。</p>
|
||||
<p>回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 <span class="arithmatex">\(n, n - 1, n - 2, ..., 2, 1\)</span> ,从而递归深度为 <span class="arithmatex">\(n\)</span> 。尾递归优化可以避免这种情况的出现。</p>
|
||||
<p>回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 <span class="arithmatex">\(n\)</span>、<span class="arithmatex">\(n - 1\)</span>、<span class="arithmatex">\(\dots\)</span>、<span class="arithmatex">\(2\)</span>、<span class="arithmatex">\(1\)</span> ,递归深度为 <span class="arithmatex">\(n\)</span> 。尾递归优化可以避免这种情况的出现。</p>
|
||||
</div>
|
||||
<div class="admonition question">
|
||||
<p class="admonition-title">当数组中所有元素都相等时,快速排序的时间复杂度是 <span class="arithmatex">\(O(n^2)\)</span> 吗?该如何处理这种退化情况?</p>
|
||||
|
||||
Reference in New Issue
Block a user