This commit is contained in:
krahets
2023-08-20 23:28:04 +08:00
parent 26a2e7f171
commit 47b7d6fd44
49 changed files with 161 additions and 162 deletions
+1 -1
View File
@@ -3426,7 +3426,7 @@
<h1 id="113">11.3 &nbsp; 冒泡排序<a class="headerlink" href="#113" title="Permanent link">&para;</a></h1>
<p>「冒泡排序 Bubble Sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。</p>
<p>「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。</p>
<p>我们可以利用元素交换操作模拟上述过程:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 &gt; 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。</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">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label><label for="__tabbed_1_6">&lt;6&gt;</label><label for="__tabbed_1_7">&lt;7&gt;</label></div>
<div class="tabbed-content">
+1 -1
View File
@@ -3427,7 +3427,7 @@
<h1 id="118">11.8 &nbsp; 桶排序<a class="headerlink" href="#118" title="Permanent link">&para;</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 &nbsp; 算法流程<a class="headerlink" href="#1181" title="Permanent link">&para;</a></h2>
<p>考虑一个长度为 <span class="arithmatex">\(n\)</span> 的数组,元素是范围 <span class="arithmatex">\([0, 1)\)</span> 的浮点数。桶排序的流程如下:</p>
<ol>
+2 -2
View File
@@ -3440,7 +3440,7 @@
<h1 id="119">11.9 &nbsp; 计数排序<a class="headerlink" href="#119" title="Permanent link">&para;</a></h1>
<p>「计数排序 Counting Sort」通过统计元素数量来实现排序,通常应用于整数数组。</p>
<p>「计数排序 counting sort」通过统计元素数量来实现排序,通常应用于整数数组。</p>
<h2 id="1191">11.9.1 &nbsp; 简单实现<a class="headerlink" href="#1191" title="Permanent link">&para;</a></h2>
<p>先来看一个简单的例子。给定一个长度为 <span class="arithmatex">\(n\)</span> 的数组 <code>nums</code> ,其中的元素都是“非负整数”。计数排序的整体流程如下:</p>
<ol>
@@ -3736,7 +3736,7 @@
</div>
<h2 id="1192">11.9.2 &nbsp; 完整实现<a class="headerlink" href="#1192" title="Permanent link">&para;</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>
+2 -2
View File
@@ -3414,9 +3414,9 @@
<h1 id="117">11.7 &nbsp; 堆排序<a class="headerlink" href="#117" title="Permanent link">&para;</a></h1>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>阅读本节前,请确保已学完「堆」章节。</p>
<p>阅读本节前,请确保已学完“堆“章节。</p>
</div>
<p>「堆排序 Heap Sort」是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序:</p>
<p>「堆排序 heap sort」是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序:</p>
<ol>
<li>输入数组并建立小顶堆,此时最小元素位于堆顶。</li>
<li>不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。</li>
+1 -1
View File
@@ -3426,7 +3426,7 @@
<h1 id="114">11.4 &nbsp; 插入排序<a class="headerlink" href="#114" title="Permanent link">&para;</a></h1>
<p>「插入排序 Insertion Sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。</p>
<p>「插入排序 insertion sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。</p>
<p>具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。</p>
<p>回忆数组的元素插入操作,设基准元素为 <code>base</code> ,我们需要将从目标索引到 <code>base</code> 之间的所有元素向右移动一位,然后再将 <code>base</code> 赋值给目标索引。</p>
<p><img alt="单次插入操作" src="../insertion_sort.assets/insertion_operation.png" /></p>
+1 -1
View File
@@ -3426,7 +3426,7 @@
<h1 id="116">11.6 &nbsp; 归并排序<a class="headerlink" href="#116" title="Permanent link">&para;</a></h1>
<p>「归并排序 Merge Sort」基于分治思想实现排序,包含“划分”和“合并”两个阶段:</p>
<p>「归并排序 merge sort」基于分治思想实现排序,包含“划分”和“合并”两个阶段:</p>
<ol>
<li><strong>划分阶段</strong>:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。</li>
<li><strong>合并阶段</strong>:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。</li>
+8 -8
View File
@@ -3454,8 +3454,8 @@
<h1 id="115">11.5 &nbsp; 快速排序<a class="headerlink" href="#115" title="Permanent link">&para;</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>
@@ -3794,8 +3794,8 @@
</div>
<h2 id="1151">11.5.1 &nbsp; 算法流程<a class="headerlink" href="#1151" title="Permanent link">&para;</a></h2>
<ol>
<li>首先,对原数组执行一次哨兵划分,得到未排序的左子数组和右子数组。</li>
<li>然后,对左子数组和右子数组分别递归执行哨兵划分</li>
<li>首先,对原数组执行一次哨兵划分,得到未排序的左子数组和右子数组。</li>
<li>然后,对左子数组和右子数组分别递归执行哨兵划分</li>
<li>持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序。</li>
</ol>
<p><img alt="快速排序流程" src="../quick_sort.assets/quick_sort_overview.png" /></p>
@@ -4004,14 +4004,14 @@
<li><strong>非稳定排序</strong>:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。</li>
</ul>
<h2 id="1153">11.5.3 &nbsp; 快排为什么快?<a class="headerlink" href="#1153" title="Permanent link">&para;</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>
<li><strong>缓存使用效率高</strong>:在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像堆排序这类算法需要跳跃式访问元素,从而缺乏这一特性。</li>
<li><strong>复杂度的常数系数低</strong>:在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与插入排序”比“冒泡排序更快的原因类似。</li>
</ul>
<h2 id="1154">11.5.4 &nbsp; 基准数优化<a class="headerlink" href="#1154" title="Permanent link">&para;</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>
+2 -2
View File
@@ -3413,12 +3413,12 @@
<h1 id="1110">11.10 &nbsp; 基数排序<a class="headerlink" href="#1110" title="Permanent link">&para;</a></h1>
<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>
<p>「基数排序 radix sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。</p>
<h2 id="11101">11.10.1 &nbsp; 算法流程<a class="headerlink" href="#11101" title="Permanent link">&para;</a></h2>
<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>
<li>对学号的第 <span class="arithmatex">\(k\)</span> 位执行计数排序。完成后,数据会根据第 <span class="arithmatex">\(k\)</span> 位从小到大排序。</li>
<li><span class="arithmatex">\(k\)</span> 增加 <span class="arithmatex">\(1\)</span> ,然后返回步骤 <code>2.</code> 继续迭代,直到所有位都排序完成后结束。</li>
</ol>
<p><img alt="基数排序算法流程" src="../radix_sort.assets/radix_sort_overview.png" /></p>
+1 -1
View File
@@ -3398,7 +3398,7 @@
<h1 id="112">11.2 &nbsp; 选择排序<a class="headerlink" href="#112" title="Permanent link">&para;</a></h1>
<p>「选择排序 Selection Sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。</p>
<p>「选择排序 selection sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。</p>
<p>设数组的长度为 <span class="arithmatex">\(n\)</span> ,选择排序的算法流程如下:</p>
<ol>
<li>初始状态下,所有元素未排序,即未排序(索引)区间为 <span class="arithmatex">\([0, n-1]\)</span></li>
+3 -3
View File
@@ -3412,7 +3412,7 @@
<h1 id="111">11.1 &nbsp; 排序算法<a class="headerlink" href="#111" title="Permanent link">&para;</a></h1>
<p>「排序算法 Sorting Algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更有效地查找、分析和处理。</p>
<p>「排序算法 sorting algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更有效地查找、分析和处理。</p>
<p>在排序算法中,数据类型可以是整数、浮点数、字符或字符串等;顺序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。</p>
<p><img alt="数据类型和判断规则示例" src="../sorting_algorithm.assets/sorting_examples.png" /></p>
<p align="center"> 图:数据类型和判断规则示例 </p>
@@ -3420,8 +3420,8 @@
<h2 id="1111">11.1.1 &nbsp; 评价维度<a class="headerlink" href="#1111" title="Permanent link">&para;</a></h2>
<p><strong>运行效率</strong>:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(即时间复杂度中的常数项降低)。对于大数据量情况,运行效率显得尤为重要。</p>
<p><strong>就地性</strong>:顾名思义,「原地排序」通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。</p>
<p><strong>稳定性</strong>:「稳定排序」在完成排序后,相等元素在数组中的相对顺序不发生改变。稳定排序是优良特性,也是多级排序场景的必要条件。</p>
<p>假设我们有一个存储学生信息的表格,第 1, 2 列分别是姓名和年龄。在这种情况下,「非稳定排序」可能导致输入数据的有序性丧失。</p>
<p><strong>稳定性</strong>:「稳定排序」在完成排序后,相等元素在数组中的相对顺序不发生改变。</p>
<p>稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,「非稳定排序」可能导致输入数据的有序性丧失。</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># 输入数据是按照姓名排序好的</span>
<a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a><span class="c1"># (name, age)</span>
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a><span class="w"> </span><span class="o">(</span><span class="s1">&#39;A&#39;</span>,<span class="w"> </span><span class="m">19</span><span class="o">)</span>