This commit is contained in:
krahets
2023-08-22 13:50:12 +08:00
parent 0c9bf14e20
commit 92a0853ab8
64 changed files with 478 additions and 479 deletions

View File

@@ -6,7 +6,7 @@ comments: true
「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样因此得名冒泡排序。
图所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
如图 11-4 所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
=== "<1>"
![利用元素交换操作模拟冒泡](bubble_sort.assets/bubble_operation_step1.png)
@@ -29,11 +29,11 @@ comments: true
=== "<7>"
![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png)
<p align="center"> 图利用元素交换操作模拟冒泡 </p>
<p align="center"> 图 11-4 &nbsp; 利用元素交换操作模拟冒泡 </p>
## 11.3.1 &nbsp; 算法流程
设数组的长度为 $n$ ,冒泡排序的步骤如图所示。
设数组的长度为 $n$ ,冒泡排序的步骤如图 11-5 所示。
1. 首先,对 $n$ 个元素执行“冒泡”,**将数组的最大元素交换至正确位置**
2. 接下来,对剩余 $n - 1$ 个元素执行“冒泡”,**将第二大元素交换至正确位置**。
@@ -42,7 +42,7 @@ comments: true
![冒泡排序流程](bubble_sort.assets/bubble_sort_overview.png)
<p align="center"> 图冒泡排序流程 </p>
<p align="center"> 图 11-5 &nbsp; 冒泡排序流程 </p>
=== "Java"

View File

@@ -10,7 +10,7 @@ comments: true
## 11.8.1 &nbsp; 算法流程
考虑一个长度为 $n$ 的数组,元素是范围 $[0, 1)$ 的浮点数。桶排序的流程如图所示。
考虑一个长度为 $n$ 的数组,元素是范围 $[0, 1)$ 的浮点数。桶排序的流程如图 11-13 所示。
1. 初始化 $k$ 个桶,将 $n$ 个元素分配到 $k$ 个桶中。
2. 对每个桶分别执行排序(本文采用编程语言的内置排序函数)。
@@ -18,7 +18,7 @@ comments: true
![桶排序算法流程](bucket_sort.assets/bucket_sort_overview.png)
<p align="center"> 图桶排序算法流程 </p>
<p align="center"> 图 11-13 &nbsp; 桶排序算法流程 </p>
=== "Java"
@@ -409,16 +409,16 @@ comments: true
为实现平均分配,我们可以先设定一个大致的分界线,将数据粗略地分到 3 个桶中。**分配完毕后,再将商品较多的桶继续划分为 3 个桶,直至所有桶中的元素数量大致相等**。
图所示,这种方法本质上是创建一个递归树,目标是让叶节点的值尽可能平均。当然,不一定要每轮将数据划分为 3 个桶,具体划分方式可根据数据特点灵活选择。
如图 11-14 所示,这种方法本质上是创建一个递归树,目标是让叶节点的值尽可能平均。当然,不一定要每轮将数据划分为 3 个桶,具体划分方式可根据数据特点灵活选择。
![递归划分桶](bucket_sort.assets/scatter_in_buckets_recursively.png)
<p align="center"> 图递归划分桶 </p>
<p align="center"> 图 11-14 &nbsp; 递归划分桶 </p>
如果我们提前知道商品价格的概率分布,**则可以根据数据概率分布设置每个桶的价格分界线**。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。
图所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。
如图 11-15 所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。
![根据概率分布划分桶](bucket_sort.assets/scatter_in_buckets_distribution.png)
<p align="center"> 图根据概率分布划分桶 </p>
<p align="center"> 图 11-15 &nbsp; 根据概率分布划分桶 </p>

View File

@@ -8,7 +8,7 @@ comments: true
## 11.9.1 &nbsp; 简单实现
先来看一个简单的例子。给定一个长度为 $n$ 的数组 `nums` ,其中的元素都是“非负整数”,计数排序的整体流程如图所示。
先来看一个简单的例子。给定一个长度为 $n$ 的数组 `nums` ,其中的元素都是“非负整数”,计数排序的整体流程如图 11-16 所示。
1. 遍历数组,找出数组中的最大数字,记为 $m$ ,然后创建一个长度为 $m + 1$ 的辅助数组 `counter`
2. **借助 `counter` 统计 `nums` 中各数字的出现次数**,其中 `counter[num]` 对应数字 `num` 的出现次数。统计方法很简单,只需遍历 `nums`(设当前数字为 `num`),每轮将 `counter[num]` 增加 $1$ 即可。
@@ -16,7 +16,7 @@ comments: true
![计数排序流程](counting_sort.assets/counting_sort_overview.png)
<p align="center"> 图计数排序流程 </p>
<p align="center"> 图 11-16 &nbsp; 计数排序流程 </p>
=== "Java"
@@ -336,7 +336,7 @@ $$
1. 将 `num` 填入数组 `res` 的索引 `prefix[num] - 1` 处。
2. 令前缀和 `prefix[num]` 减小 $1$ ,从而得到下次放置 `num` 的索引。
遍历完成后,数组 `res` 中就是排序好的结果,最后使用 `res` 覆盖原数组 `nums` 即可。图展示了完整的计数排序流程。
遍历完成后,数组 `res` 中就是排序好的结果,最后使用 `res` 覆盖原数组 `nums` 即可。图 11-17 展示了完整的计数排序流程。
=== "<1>"
![计数排序步骤](counting_sort.assets/counting_sort_step1.png)
@@ -362,7 +362,7 @@ $$
=== "<8>"
![counting_sort_step8](counting_sort.assets/counting_sort_step8.png)
<p align="center"> 图计数排序步骤 </p>
<p align="center"> 图 11-17 &nbsp; 计数排序步骤 </p>
计数排序的实现代码如下所示。

View File

@@ -17,7 +17,7 @@ comments: true
## 11.7.1 &nbsp; 算法流程
设数组的长度为 $n$ ,堆排序的流程如图所示。
设数组的长度为 $n$ ,堆排序的流程如图 11-12 所示。
1. 输入数组并建立大顶堆。完成后,最大元素位于堆顶。
2. 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 $1$ ,已排序元素数量加 $1$ 。
@@ -64,7 +64,7 @@ comments: true
=== "<12>"
![heap_sort_step12](heap_sort.assets/heap_sort_step12.png)
<p align="center"> 图堆排序步骤 </p>
<p align="center"> 图 11-12 &nbsp; 堆排序步骤 </p>
在代码实现中,我们使用了与堆章节相同的从顶至底堆化 `sift_down()` 函数。值得注意的是,由于堆的长度会随着提取最大元素而减小,因此我们需要给 `sift_down()` 函数添加一个长度参数 $n$ ,用于指定堆的当前有效长度。

View File

@@ -8,15 +8,15 @@ comments: true
具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。
图展示了数组插入元素的操作流程。设基准元素为 `base` ,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
11-6 展示了数组插入元素的操作流程。设基准元素为 `base` ,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
![单次插入操作](insertion_sort.assets/insertion_operation.png)
<p align="center"> 图单次插入操作 </p>
<p align="center"> 图 11-6 &nbsp; 单次插入操作 </p>
## 11.4.1 &nbsp; 算法流程
插入排序的整体流程如图所示。
插入排序的整体流程如图 11-7 所示。
1. 初始状态下,数组的第 1 个元素已完成排序。
2. 选取数组的第 2 个元素作为 `base` ,将其插入到正确位置后,**数组的前 2 个元素已排序**。
@@ -25,7 +25,7 @@ comments: true
![插入排序流程](insertion_sort.assets/insertion_sort_overview.png)
<p align="center"> 图插入排序流程 </p>
<p align="center"> 图 11-7 &nbsp; 插入排序流程 </p>
=== "Java"

View File

@@ -4,18 +4,18 @@ comments: true
# 11.6 &nbsp; 归并排序
「归并排序 merge sort」是一种基于分治策略的排序算法包含图所示的“划分”和“合并”阶段:
「归并排序 merge sort」是一种基于分治策略的排序算法包含图 11-10 所示的“划分”和“合并”阶段:
1. **划分阶段**:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。
2. **合并阶段**:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。
![归并排序的划分与合并阶段](merge_sort.assets/merge_sort_overview.png)
<p align="center"> 图归并排序的划分与合并阶段 </p>
<p align="center"> 图 11-10 &nbsp; 归并排序的划分与合并阶段 </p>
## 11.6.1 &nbsp; 算法流程
图所示,“划分阶段”从顶至底递归地将数组从中点切为两个子数组:
如图 11-11 所示,“划分阶段”从顶至底递归地将数组从中点切为两个子数组:
1. 计算数组中点 `mid` ,递归划分左子数组(区间 `[left, mid]` )和右子数组(区间 `[mid + 1, right]` )。
2. 递归执行步骤 `1.` ,直至子数组区间长度为 1 时,终止递归划分。
@@ -52,7 +52,7 @@ comments: true
=== "<10>"
![merge_sort_step10](merge_sort.assets/merge_sort_step10.png)
<p align="center"> 图归并排序步骤 </p>
<p align="center"> 图 11-11 &nbsp; 归并排序步骤 </p>
观察发现,归并排序的递归顺序与二叉树的后序遍历相同,对比来看:

View File

@@ -6,7 +6,7 @@ comments: true
「快速排序 quick sort」是一种基于分治策略的排序算法运行高效应用广泛。
快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程如图所示。
快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程如图 11-8 所示。
1. 选取数组最左端元素作为基准数,初始化两个指针 `i``j` 分别指向数组的两端。
2. 设置一个循环,在每轮中使用 `i``j`)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
@@ -39,7 +39,7 @@ comments: true
=== "<9>"
![pivot_division_step9](quick_sort.assets/pivot_division_step9.png)
<p align="center"> 图哨兵划分步骤 </p>
<p align="center"> 图 11-8 &nbsp; 哨兵划分步骤 </p>
哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 $\leq$ 基准数 $\leq$ 右子数组任意元素”。因此,我们接下来只需对这两个子数组进行排序。
@@ -362,7 +362,7 @@ comments: true
## 11.5.1 &nbsp; 算法流程
快速排序的整体流程如图所示。
快速排序的整体流程如图 11-9 所示。
1. 首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。
2. 然后,对左子数组和右子数组分别递归执行“哨兵划分”。
@@ -370,7 +370,7 @@ comments: true
![快速排序流程](quick_sort.assets/quick_sort_overview.png)
<p align="center"> 图快速排序流程 </p>
<p align="center"> 图 11-9 &nbsp; 快速排序流程 </p>
=== "Java"

View File

@@ -10,7 +10,7 @@ comments: true
## 11.10.1 &nbsp; 算法流程
以学号数据为例,假设数字的最低位是第 $1$ 位,最高位是第 $8$ 位,基数排序的流程如图所示。
以学号数据为例,假设数字的最低位是第 $1$ 位,最高位是第 $8$ 位,基数排序的流程如图 11-18 所示。
1. 初始化位数 $k = 1$ 。
2. 对学号的第 $k$ 位执行“计数排序”。完成后,数据会根据第 $k$ 位从小到大排序。
@@ -18,7 +18,7 @@ comments: true
![基数排序算法流程](radix_sort.assets/radix_sort_overview.png)
<p align="center"> 图基数排序算法流程 </p>
<p align="center"> 图 11-18 &nbsp; 基数排序算法流程 </p>
下面来剖析代码实现。对于一个 $d$ 进制的数字 $x$ ,要获取其第 $k$ 位 $x_k$ ,可以使用以下计算公式:

View File

@@ -6,7 +6,7 @@ comments: true
「选择排序 selection sort」的工作原理非常直接开启一个循环每轮从未排序区间选择最小的元素将其放到已排序区间的末尾。
设数组的长度为 $n$ ,选择排序的算法流程如图所示。
设数组的长度为 $n$ ,选择排序的算法流程如图 11-2 所示。
1. 初始状态下,所有元素未排序,即未排序(索引)区间为 $[0, n-1]$ 。
2. 选取区间 $[0, n-1]$ 中的最小元素,将其与索引 $0$ 处元素交换。完成后,数组前 1 个元素已排序。
@@ -47,7 +47,7 @@ comments: true
=== "<11>"
![selection_sort_step11](selection_sort.assets/selection_sort_step11.png)
<p align="center"> 图选择排序步骤 </p>
<p align="center"> 图 11-2 &nbsp; 选择排序步骤 </p>
在代码中,我们用 $k$ 来记录未排序区间内的最小元素。
@@ -288,8 +288,8 @@ comments: true
- **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\dots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。
- **空间复杂度 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。
- **非稳定排序**:如图所示,元素 `nums[i]` 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。
- **非稳定排序**:如图 11-3 所示,元素 `nums[i]` 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。
![选择排序非稳定示例](selection_sort.assets/selection_sort_instability.png)
<p align="center"> 图选择排序非稳定示例 </p>
<p align="center"> 图 11-3 &nbsp; 选择排序非稳定示例 </p>

View File

@@ -6,11 +6,11 @@ comments: true
「排序算法 sorting algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用因为有序数据通常能够被更有效地查找、分析和处理。
图所示,排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。
如图 11-1 所示,排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。
![数据类型和判断规则示例](sorting_algorithm.assets/sorting_examples.png)
<p align="center"> 图数据类型和判断规则示例 </p>
<p align="center"> 图 11-1 &nbsp; 数据类型和判断规则示例 </p>
## 11.1.1 &nbsp; 评价维度

View File

@@ -12,11 +12,11 @@ comments: true
- 计数排序是桶排序的一个特例,它通过统计数据出现的次数来实现排序。计数排序适用于数据量大但数据范围有限的情况,并且要求数据能够转换为正整数。
- 基数排序通过逐位排序来实现数据排序,要求数据能够表示为固定位数的数字。
- 总的来说,我们希望找到一种排序算法,具有高效率、稳定、原地以及正向自适应性等优点。然而,正如其他数据结构和算法一样,没有一种排序算法能够同时满足所有这些条件。在实际应用中,我们需要根据数据的特性来选择合适的排序算法。
- 图对比了主流排序算法的效率、稳定性、就地性和自适应性等。
- 11-19 对比了主流排序算法的效率、稳定性、就地性和自适应性等。
![排序算法对比](summary.assets/sorting_algorithms_comparison.png)
<p align="center"> 图排序算法对比 </p>
<p align="center"> 图 11-19 &nbsp; 排序算法对比 </p>
## 11.11.1 &nbsp; Q & A