mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-12 23:36:06 +00:00
deploy
This commit is contained in:
@@ -3427,7 +3427,7 @@
|
||||
|
||||
<h1 id="113">11.3 冒泡排序<a class="headerlink" href="#113" title="Permanent link">¶</a></h1>
|
||||
<p>「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。</p>
|
||||
<p>我们可以利用元素交换操作模拟上述过程:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。</p>
|
||||
<p>如下图所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:7"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1"><1></label><label for="__tabbed_1_2"><2></label><label for="__tabbed_1_3"><3></label><label for="__tabbed_1_4"><4></label><label for="__tabbed_1_5"><5></label><label for="__tabbed_1_6"><6></label><label for="__tabbed_1_7"><7></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3456,7 +3456,7 @@
|
||||
<p align="center"> 图:利用元素交换操作模拟冒泡 </p>
|
||||
|
||||
<h2 id="1131">11.3.1 算法流程<a class="headerlink" href="#1131" title="Permanent link">¶</a></h2>
|
||||
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,冒泡排序的步骤为:</p>
|
||||
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,冒泡排序的步骤如下图所示。</p>
|
||||
<ol>
|
||||
<li>首先,对 <span class="arithmatex">\(n\)</span> 个元素执行“冒泡”,<strong>将数组的最大元素交换至正确位置</strong>,</li>
|
||||
<li>接下来,对剩余 <span class="arithmatex">\(n - 1\)</span> 个元素执行“冒泡”,<strong>将第二大元素交换至正确位置</strong>。</li>
|
||||
|
||||
@@ -3427,9 +3427,9 @@
|
||||
|
||||
<h1 id="118">11.8 桶排序<a class="headerlink" href="#118" title="Permanent link">¶</a></h1>
|
||||
<p>前述的几种排序算法都属于“基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 <span class="arithmatex">\(O(n \log n)\)</span> 。接下来,我们将探讨几种“非比较排序算法”,它们的时间复杂度可以达到线性阶。</p>
|
||||
<p>「桶排序 bucket sort」是分治思想的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。</p>
|
||||
<p>「桶排序 bucket sort」是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。</p>
|
||||
<h2 id="1181">11.8.1 算法流程<a class="headerlink" href="#1181" title="Permanent link">¶</a></h2>
|
||||
<p>考虑一个长度为 <span class="arithmatex">\(n\)</span> 的数组,元素是范围 <span class="arithmatex">\([0, 1)\)</span> 的浮点数。桶排序的流程如下:</p>
|
||||
<p>考虑一个长度为 <span class="arithmatex">\(n\)</span> 的数组,元素是范围 <span class="arithmatex">\([0, 1)\)</span> 的浮点数。桶排序的流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>初始化 <span class="arithmatex">\(k\)</span> 个桶,将 <span class="arithmatex">\(n\)</span> 个元素分配到 <span class="arithmatex">\(k\)</span> 个桶中。</li>
|
||||
<li>对每个桶分别执行排序(本文采用编程语言的内置排序函数)。</li>
|
||||
@@ -3803,11 +3803,13 @@
|
||||
</ul>
|
||||
<h2 id="1183">11.8.3 如何实现平均分配<a class="headerlink" href="#1183" title="Permanent link">¶</a></h2>
|
||||
<p>桶排序的时间复杂度理论上可以达到 <span class="arithmatex">\(O(n)\)</span> ,<strong>关键在于将元素均匀分配到各个桶中</strong>,因为实际数据往往不是均匀分布的。例如,我们想要将淘宝上的所有商品按价格范围平均分配到 10 个桶中,但商品价格分布不均,低于 100 元的非常多,高于 1000 元的非常少。若将价格区间平均划分为 10 份,各个桶中的商品数量差距会非常大。</p>
|
||||
<p>为实现平均分配,我们可以先设定一个大致的分界线,将数据粗略地分到 3 个桶中。<strong>分配完毕后,再将商品较多的桶继续划分为 3 个桶,直至所有桶中的元素数量大致相等</strong>。这种方法本质上是创建一个递归树,使叶节点的值尽可能平均。当然,不一定要每轮将数据划分为 3 个桶,具体划分方式可根据数据特点灵活选择。</p>
|
||||
<p>为实现平均分配,我们可以先设定一个大致的分界线,将数据粗略地分到 3 个桶中。<strong>分配完毕后,再将商品较多的桶继续划分为 3 个桶,直至所有桶中的元素数量大致相等</strong>。</p>
|
||||
<p>如下图所示,这种方法本质上是创建一个递归树,目标是让叶节点的值尽可能平均。当然,不一定要每轮将数据划分为 3 个桶,具体划分方式可根据数据特点灵活选择。</p>
|
||||
<p><img alt="递归划分桶" src="../bucket_sort.assets/scatter_in_buckets_recursively.png" /></p>
|
||||
<p align="center"> 图:递归划分桶 </p>
|
||||
|
||||
<p>如果我们提前知道商品价格的概率分布,<strong>则可以根据数据概率分布设置每个桶的价格分界线</strong>。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。如下图所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。</p>
|
||||
<p>如果我们提前知道商品价格的概率分布,<strong>则可以根据数据概率分布设置每个桶的价格分界线</strong>。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。</p>
|
||||
<p>如下图所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。</p>
|
||||
<p><img alt="根据概率分布划分桶" src="../bucket_sort.assets/scatter_in_buckets_distribution.png" /></p>
|
||||
<p align="center"> 图:根据概率分布划分桶 </p>
|
||||
|
||||
|
||||
@@ -3442,7 +3442,7 @@
|
||||
<h1 id="119">11.9 计数排序<a class="headerlink" href="#119" title="Permanent link">¶</a></h1>
|
||||
<p>「计数排序 counting sort」通过统计元素数量来实现排序,通常应用于整数数组。</p>
|
||||
<h2 id="1191">11.9.1 简单实现<a class="headerlink" href="#1191" title="Permanent link">¶</a></h2>
|
||||
<p>先来看一个简单的例子。给定一个长度为 <span class="arithmatex">\(n\)</span> 的数组 <code>nums</code> ,其中的元素都是“非负整数”。计数排序的整体流程如下:</p>
|
||||
<p>先来看一个简单的例子。给定一个长度为 <span class="arithmatex">\(n\)</span> 的数组 <code>nums</code> ,其中的元素都是“非负整数”,计数排序的整体流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>遍历数组,找出数组中的最大数字,记为 <span class="arithmatex">\(m\)</span> ,然后创建一个长度为 <span class="arithmatex">\(m + 1\)</span> 的辅助数组 <code>counter</code> 。</li>
|
||||
<li><strong>借助 <code>counter</code> 统计 <code>nums</code> 中各数字的出现次数</strong>,其中 <code>counter[num]</code> 对应数字 <code>num</code> 的出现次数。统计方法很简单,只需遍历 <code>nums</code>(设当前数字为 <code>num</code>),每轮将 <code>counter[num]</code> 增加 <span class="arithmatex">\(1\)</span> 即可。</li>
|
||||
@@ -3735,7 +3735,7 @@
|
||||
<p>从桶排序的角度看,我们可以将计数排序中的计数数组 <code>counter</code> 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。</p>
|
||||
</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>细心的同学可能发现,<strong>如果输入数据是对象,上述步骤 <code>3.</code> 就失效了</strong>。假设输入数据是商品对象,我们想要按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。</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]}
|
||||
@@ -3745,7 +3745,7 @@
|
||||
<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>
|
||||
</ol>
|
||||
<p>遍历完成后,数组 <code>res</code> 中就是排序好的结果,最后使用 <code>res</code> 覆盖原数组 <code>nums</code> 即可。</p>
|
||||
<p>遍历完成后,数组 <code>res</code> 中就是排序好的结果,最后使用 <code>res</code> 覆盖原数组 <code>nums</code> 即可。下图展示了完整的计数排序流程。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="2:8"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1"><1></label><label for="__tabbed_2_2"><2></label><label for="__tabbed_2_3"><3></label><label for="__tabbed_2_4"><4></label><label for="__tabbed_2_5"><5></label><label for="__tabbed_2_6"><6></label><label for="__tabbed_2_7"><7></label><label for="__tabbed_2_8"><8></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
|
||||
@@ -3423,14 +3423,17 @@
|
||||
</ol>
|
||||
<p>以上方法虽然可行,但需要借助一个额外数组来保存弹出的元素,比较浪费空间。在实际中,我们通常使用一种更加优雅的实现方式。</p>
|
||||
<h2 id="1171">11.7.1 算法流程<a class="headerlink" href="#1171" title="Permanent link">¶</a></h2>
|
||||
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,堆排序的流程如下:</p>
|
||||
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,堆排序的流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>输入数组并建立大顶堆。完成后,最大元素位于堆顶。</li>
|
||||
<li>将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 <span class="arithmatex">\(1\)</span> ,已排序元素数量加 <span class="arithmatex">\(1\)</span> 。</li>
|
||||
<li>从堆顶元素开始,从顶到底执行堆化操作(Sift Down)。完成堆化后,堆的性质得到修复。</li>
|
||||
<li>循环执行第 <code>2.</code> 和 <code>3.</code> 步。循环 <span class="arithmatex">\(n - 1\)</span> 轮后,即可完成数组排序。</li>
|
||||
</ol>
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>实际上,元素出堆操作中也包含第 <code>2.</code> 和 <code>3.</code> 步,只是多了一个弹出元素的步骤。</p>
|
||||
</div>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1"><1></label><label for="__tabbed_1_2"><2></label><label for="__tabbed_1_3"><3></label><label for="__tabbed_1_4"><4></label><label for="__tabbed_1_5"><5></label><label for="__tabbed_1_6"><6></label><label for="__tabbed_1_7"><7></label><label for="__tabbed_1_8"><8></label><label for="__tabbed_1_9"><9></label><label for="__tabbed_1_10"><10></label><label for="__tabbed_1_11"><11></label><label for="__tabbed_1_12"><12></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3473,7 +3476,7 @@
|
||||
</div>
|
||||
<p align="center"> 图:堆排序步骤 </p>
|
||||
|
||||
<p>在代码实现中,我们使用了与堆章节相同的从顶至底堆化(Sift Down)的函数。值得注意的是,由于堆的长度会随着提取最大元素而减小,因此我们需要给 Sift Down 函数添加一个长度参数 <span class="arithmatex">\(n\)</span> ,用于指定堆的当前有效长度。</p>
|
||||
<p>在代码实现中,我们使用了与堆章节相同的从顶至底堆化 <code>sift_down()</code> 函数。值得注意的是,由于堆的长度会随着提取最大元素而减小,因此我们需要给 <code>sift_down()</code> 函数添加一个长度参数 <span class="arithmatex">\(n\)</span> ,用于指定堆的当前有效长度。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="2:12"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JS</label><label for="__tabbed_2_6">TS</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label><label for="__tabbed_2_11">Dart</label><label for="__tabbed_2_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
|
||||
@@ -3428,12 +3428,12 @@
|
||||
<h1 id="114">11.4 插入排序<a class="headerlink" href="#114" title="Permanent link">¶</a></h1>
|
||||
<p>「插入排序 insertion sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。</p>
|
||||
<p>具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。</p>
|
||||
<p>回忆数组的元素插入操作,设基准元素为 <code>base</code> ,我们需要将从目标索引到 <code>base</code> 之间的所有元素向右移动一位,然后再将 <code>base</code> 赋值给目标索引。</p>
|
||||
<p>下图展示了数组插入元素的操作流程。设基准元素为 <code>base</code> ,我们需要将从目标索引到 <code>base</code> 之间的所有元素向右移动一位,然后再将 <code>base</code> 赋值给目标索引。</p>
|
||||
<p><img alt="单次插入操作" src="../insertion_sort.assets/insertion_operation.png" /></p>
|
||||
<p align="center"> 图:单次插入操作 </p>
|
||||
|
||||
<h2 id="1141">11.4.1 算法流程<a class="headerlink" href="#1141" title="Permanent link">¶</a></h2>
|
||||
<p>插入排序的整体流程如下:</p>
|
||||
<p>插入排序的整体流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>初始状态下,数组的第 1 个元素已完成排序。</li>
|
||||
<li>选取数组的第 2 个元素作为 <code>base</code> ,将其插入到正确位置后,<strong>数组的前 2 个元素已排序</strong>。</li>
|
||||
|
||||
@@ -3426,7 +3426,7 @@
|
||||
|
||||
|
||||
<h1 id="116">11.6 归并排序<a class="headerlink" href="#116" title="Permanent link">¶</a></h1>
|
||||
<p>「归并排序 merge sort」基于分治思想实现排序,包含“划分”和“合并”两个阶段:</p>
|
||||
<p>「归并排序 merge sort」是一种基于分治策略的排序算法,包含下图所示的“划分”和“合并”阶段:</p>
|
||||
<ol>
|
||||
<li><strong>划分阶段</strong>:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。</li>
|
||||
<li><strong>合并阶段</strong>:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。</li>
|
||||
@@ -3435,7 +3435,7 @@
|
||||
<p align="center"> 图:归并排序的划分与合并阶段 </p>
|
||||
|
||||
<h2 id="1161">11.6.1 算法流程<a class="headerlink" href="#1161" title="Permanent link">¶</a></h2>
|
||||
<p>“划分阶段”从顶至底递归地将数组从中点切为两个子数组:</p>
|
||||
<p>如下图所示,“划分阶段”从顶至底递归地将数组从中点切为两个子数组:</p>
|
||||
<ol>
|
||||
<li>计算数组中点 <code>mid</code> ,递归划分左子数组(区间 <code>[left, mid]</code> )和右子数组(区间 <code>[mid + 1, right]</code> )。</li>
|
||||
<li>递归执行步骤 <code>1.</code> ,直至子数组区间长度为 1 时,终止递归划分。</li>
|
||||
@@ -3477,7 +3477,7 @@
|
||||
</div>
|
||||
<p align="center"> 图:归并排序步骤 </p>
|
||||
|
||||
<p>观察发现,归并排序的递归顺序与二叉树的后序遍历相同,具体来看:</p>
|
||||
<p>观察发现,归并排序的递归顺序与二叉树的后序遍历相同,对比来看:</p>
|
||||
<ul>
|
||||
<li><strong>后序遍历</strong>:先递归左子树,再递归右子树,最后处理根节点。</li>
|
||||
<li><strong>归并排序</strong>:先递归左子数组,再递归右子数组,最后处理合并。</li>
|
||||
|
||||
@@ -3454,14 +3454,13 @@
|
||||
|
||||
|
||||
<h1 id="115">11.5 快速排序<a class="headerlink" href="#115" title="Permanent link">¶</a></h1>
|
||||
<p>「快速排序 quick sort」是一种基于分治思想的排序算法,运行高效,应用广泛。</p>
|
||||
<p>快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程为:</p>
|
||||
<p>「快速排序 quick sort」是一种基于分治策略的排序算法,运行高效,应用广泛。</p>
|
||||
<p>快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>选取数组最左端元素作为基准数,初始化两个指针 <code>i</code> 和 <code>j</code> 分别指向数组的两端。</li>
|
||||
<li>设置一个循环,在每轮中使用 <code>i</code>(<code>j</code>)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。</li>
|
||||
<li>循环执行步骤 <code>2.</code> ,直到 <code>i</code> 和 <code>j</code> 相遇时停止,最后将基准数交换至两个子数组的分界线。</li>
|
||||
</ol>
|
||||
<p>哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 <span class="arithmatex">\(\leq\)</span> 基准数 <span class="arithmatex">\(\leq\)</span> 右子数组任意元素”。因此,我们接下来只需对这两个子数组进行排序。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:9"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1"><1></label><label for="__tabbed_1_2"><2></label><label for="__tabbed_1_3"><3></label><label for="__tabbed_1_4"><4></label><label for="__tabbed_1_5"><5></label><label for="__tabbed_1_6"><6></label><label for="__tabbed_1_7"><7></label><label for="__tabbed_1_8"><8></label><label for="__tabbed_1_9"><9></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3495,8 +3494,9 @@
|
||||
</div>
|
||||
<p align="center"> 图:哨兵划分步骤 </p>
|
||||
|
||||
<p>哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 <span class="arithmatex">\(\leq\)</span> 基准数 <span class="arithmatex">\(\leq\)</span> 右子数组任意元素”。因此,我们接下来只需对这两个子数组进行排序。</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">快速排序的分治思想</p>
|
||||
<p class="admonition-title">快速排序的分治策略</p>
|
||||
<p>哨兵划分的实质是将一个较长数组的排序问题简化为两个较短数组的排序问题。</p>
|
||||
</div>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="2:12"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JS</label><label for="__tabbed_2_6">TS</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label><label for="__tabbed_2_11">Dart</label><label for="__tabbed_2_12">Rust</label></div>
|
||||
@@ -3793,6 +3793,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="1151">11.5.1 算法流程<a class="headerlink" href="#1151" title="Permanent link">¶</a></h2>
|
||||
<p>快速排序的整体流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。</li>
|
||||
<li>然后,对左子数组和右子数组分别递归执行“哨兵划分”。</li>
|
||||
|
||||
@@ -3415,7 +3415,7 @@
|
||||
<p>上一节我们介绍了计数排序,它适用于数据量 <span class="arithmatex">\(n\)</span> 较大但数据范围 <span class="arithmatex">\(m\)</span> 较小的情况。假设我们需要对 <span class="arithmatex">\(n = 10^6\)</span> 个学号进行排序,而学号是一个 <span class="arithmatex">\(8\)</span> 位数字,这意味着数据范围 <span class="arithmatex">\(m = 10^8\)</span> 非常大,使用计数排序需要分配大量内存空间,而基数排序可以避免这种情况。</p>
|
||||
<p>「基数排序 radix sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。</p>
|
||||
<h2 id="11101">11.10.1 算法流程<a class="headerlink" href="#11101" title="Permanent link">¶</a></h2>
|
||||
<p>以学号数据为例,假设数字的最低位是第 <span class="arithmatex">\(1\)</span> 位,最高位是第 <span class="arithmatex">\(8\)</span> 位,基数排序的步骤如下:</p>
|
||||
<p>以学号数据为例,假设数字的最低位是第 <span class="arithmatex">\(1\)</span> 位,最高位是第 <span class="arithmatex">\(8\)</span> 位,基数排序的流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>初始化位数 <span class="arithmatex">\(k = 1\)</span> 。</li>
|
||||
<li>对学号的第 <span class="arithmatex">\(k\)</span> 位执行“计数排序”。完成后,数据会根据第 <span class="arithmatex">\(k\)</span> 位从小到大排序。</li>
|
||||
|
||||
@@ -3399,7 +3399,7 @@
|
||||
|
||||
<h1 id="112">11.2 选择排序<a class="headerlink" href="#112" title="Permanent link">¶</a></h1>
|
||||
<p>「选择排序 selection sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。</p>
|
||||
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,选择排序的算法流程如下:</p>
|
||||
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,选择排序的算法流程如下图所示。</p>
|
||||
<ol>
|
||||
<li>初始状态下,所有元素未排序,即未排序(索引)区间为 <span class="arithmatex">\([0, n-1]\)</span> 。</li>
|
||||
<li>选取区间 <span class="arithmatex">\([0, n-1]\)</span> 中的最小元素,将其与索引 <span class="arithmatex">\(0\)</span> 处元素交换。完成后,数组前 1 个元素已排序。</li>
|
||||
@@ -3664,7 +3664,7 @@
|
||||
<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>非稳定排序</strong>:在交换元素时,有可能将 <code>nums[i]</code> 交换至其相等元素的右边,导致两者的相对顺序发生改变。</li>
|
||||
<li><strong>非稳定排序</strong>:如下图所示,元素 <code>nums[i]</code> 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。</li>
|
||||
</ul>
|
||||
<p><img alt="选择排序非稳定示例" src="../selection_sort.assets/selection_sort_instability.png" /></p>
|
||||
<p align="center"> 图:选择排序非稳定示例 </p>
|
||||
|
||||
@@ -3413,7 +3413,7 @@
|
||||
|
||||
<h1 id="111">11.1 排序算法<a class="headerlink" href="#111" title="Permanent link">¶</a></h1>
|
||||
<p>「排序算法 sorting algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更有效地查找、分析和处理。</p>
|
||||
<p>在排序算法中,数据类型可以是整数、浮点数、字符或字符串等;顺序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。</p>
|
||||
<p>如下图所示,排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。</p>
|
||||
<p><img alt="数据类型和判断规则示例" src="../sorting_algorithm.assets/sorting_examples.png" /></p>
|
||||
<p align="center"> 图:数据类型和判断规则示例 </p>
|
||||
|
||||
|
||||
@@ -3407,6 +3407,7 @@
|
||||
<li>计数排序是桶排序的一个特例,它通过统计数据出现的次数来实现排序。计数排序适用于数据量大但数据范围有限的情况,并且要求数据能够转换为正整数。</li>
|
||||
<li>基数排序通过逐位排序来实现数据排序,要求数据能够表示为固定位数的数字。</li>
|
||||
<li>总的来说,我们希望找到一种排序算法,具有高效率、稳定、原地以及正向自适应性等优点。然而,正如其他数据结构和算法一样,没有一种排序算法能够同时满足所有这些条件。在实际应用中,我们需要根据数据的特性来选择合适的排序算法。</li>
|
||||
<li>下图对比了主流排序算法的效率、稳定性、就地性和自适应性等。</li>
|
||||
</ul>
|
||||
<p><img alt="排序算法对比" src="../summary.assets/sorting_algorithms_comparison.png" /></p>
|
||||
<p align="center"> 图:排序算法对比 </p>
|
||||
|
||||
Reference in New Issue
Block a user