Update bubble sort and insertion sort.

This commit is contained in:
krahets 2023-05-23 21:20:14 +08:00
parent abecea9ab6
commit eb8df49993
17 changed files with 60 additions and 57 deletions

View file

@ -103,10 +103,10 @@ void quickSortTailCall(int nums[], int left, int right) {
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
quickSortTailCall(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
quickSortTailCall(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -135,10 +135,10 @@ class QuickSortTailCall {
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -128,10 +128,10 @@ class QuickSortTailCall {
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -119,10 +119,10 @@ class QuickSortTailCall {
//
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); //
left = pivot + 1; // [pivot + 1, right]
left = pivot + 1; // [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); //
right = pivot - 1; // [left, pivot - 1]
right = pivot - 1; // [left, pivot - 1]
}
}
}

View file

@ -121,10 +121,10 @@ func (q *quickSortTailCall) quickSort(nums []int, left, right int) {
// 对两个子数组中较短的那个执行快排
if pivot-left < right-pivot {
q.quickSort(nums, left, pivot-1) // 递归排序左子数组
left = pivot + 1 // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1 // 剩余排序区间为 [pivot + 1, right]
} else {
q.quickSort(nums, pivot+1, right) // 递归排序右子数组
right = pivot - 1 // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1 // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -131,10 +131,10 @@ class QuickSortTailCall {
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -130,10 +130,10 @@ class QuickSortTailCall {
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
this.quickSort(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
this.quickSort(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -106,10 +106,10 @@ class QuickSortTailCall:
# 对两个子数组中较短的那个执行快排
if pivot - left < right - pivot:
self.quick_sort(nums, left, pivot - 1) # 递归排序左子数组
left = pivot + 1 # 剩余排序区间为 [pivot + 1, right]
left = pivot + 1 # 剩余排序区间为 [pivot + 1, right]
else:
self.quick_sort(nums, pivot + 1, right) # 递归排序右子数组
right = pivot - 1 # 剩余排序区间为 [left, pivot - 1]
right = pivot - 1 # 剩余排序区间为 [left, pivot - 1]
"""Driver Code"""

View file

@ -119,10 +119,10 @@ impl QuickSortTailCall {
// 对两个子数组中较短的那个执行快排
if pivot - left < right - pivot {
Self::quick_sort(left, pivot - 1, nums); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
Self::quick_sort(pivot + 1, right, nums); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -90,10 +90,10 @@ func quickSortTailCall(nums: inout [Int], left: Int, right: Int) {
//
if (pivot - left) < (right - pivot) {
quickSortTailCall(nums: &nums, left: left, right: pivot - 1) //
left = pivot + 1 // [pivot + 1, right]
left = pivot + 1 // [pivot + 1, right]
} else {
quickSortTailCall(nums: &nums, left: pivot + 1, right: right) //
right = pivot - 1 // [left, pivot - 1]
right = pivot - 1 // [left, pivot - 1]
}
}
}

View file

@ -152,10 +152,10 @@ class QuickSortTailCall {
// 对两个子数组中较短的那个执行快排
if (pivot - left < right - pivot) {
this.quickSort(nums, left, pivot - 1); // 递归排序左子数组
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
left = pivot + 1; // 剩余排序区间为 [pivot + 1, right]
} else {
this.quickSort(nums, pivot + 1, right); // 递归排序右子数组
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
right = pivot - 1; // 剩余排序区间为 [left, pivot - 1]
}
}
}

View file

@ -130,10 +130,10 @@ const QuickSortTailCall = struct {
//
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); //
left = pivot + 1; // [pivot + 1, right]
left = pivot + 1; // [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); //
right = pivot - 1; // [left, pivot - 1]
right = pivot - 1; // [left, pivot - 1]
}
}
}

View file

@ -1,4 +1,4 @@
# 搜索算法
# 重识搜索算法
「搜索算法 Searching Algorithm」用于在数据结构例如数组、链表、树或图中搜索一个或一组满足特定条件的元素。

View file

@ -1,13 +1,11 @@
# 冒泡排序
「冒泡排序 Bubble Sort」的工作原理类似于泡泡在水中的浮动。在水中,较大的泡泡会最先浮到水面
「冒泡排序 Bubble Sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序
「冒泡操作」利用元素交换操作模拟了上述过程,具体做法为:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
**在完成一次冒泡操作后,数组的最大元素已位于正确位置,接下来只需对剩余 $n - 1$ 个元素进行排序**。
我们可以利用元素交换操作模拟上述过程:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。
=== "<1>"
![冒泡操作步骤](bubble_sort.assets/bubble_operation_step1.png)
![利用元素交换操作模拟冒泡](bubble_sort.assets/bubble_operation_step1.png)
=== "<2>"
![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png)
@ -29,11 +27,12 @@
## 算法流程
设输入数组长度为 $n$ 整个冒泡排序的步骤为:
设输入数组长度为 $n$ ,冒泡排序的步骤为:
1. 完成第一轮「冒泡」后,数组的最大元素已位于正确位置,接下来只需对剩余 $n - 1$ 个元素进行排序;
2. 对剩余 $n - 1$ 个元素执行冒泡操作,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个;
3. 如此类推,经过 $n - 1$ 轮冒泡操作,整个数组便完成排序;
1. 首先,对 $n$ 个元素执行“冒泡”,**将数组的最大元素交换至正确位置**
2. 接下来,对剩余 $n - 1$ 个元素执行“冒泡”,**将第二大元素交换至正确位置**。
3. 以此类推,经过 $n - 1$ 轮“冒泡”后,**前 $n - 1$ 大的元素都被交换至正确位置**。
4. 仅剩的一个元素必定是最小元素,无需排序,因此数组排序完成。
![冒泡排序流程](bubble_sort.assets/bubble_sort_overview.png)
@ -97,17 +96,9 @@
[class]{}-[func]{bubbleSort}
```
## 算法特性
**时间复杂度 $O(n^2)$** :各轮冒泡遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。在引入下文的 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ ,所以它是“自适应排序”。
**空间复杂度 $O(1)$** :指针 $i$ , $j$ 使用常数大小的额外空间,因此是“原地排序”。
由于冒泡操作中遇到相等元素不交换,因此冒泡排序是“稳定排序”。
## 效率优化
我们发现,如果某轮冒泡操作中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。
我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。
经过优化,冒泡排序的最差和平均时间复杂度仍为 $O(n^2)$ ;但当输入数组完全有序时,可达到最佳时间复杂度 $O(n)$ 。
@ -170,3 +161,11 @@
```zig title="bubble_sort.zig"
[class]{}-[func]{bubbleSortWithFlag}
```
## 算法特性
各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ **因此时间复杂度为 $O(n^2)$** 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ **是“自适应排序”**。
指针 $i$ , $j$ 使用常数大小的额外空间,**因此空间复杂度为 $O(1)$ ,是“原地排序”**。
由于在“冒泡”中遇到相等元素不交换,**因此冒泡排序是“稳定排序”**。

View file

@ -1,8 +1,10 @@
# 插入排序
「插入排序 Insertion Sort」是一种基于数组插入操作的排序算法。具体来说,选择一个待排序的元素作为基准值 `base` ,将 `base` 与其左侧已排序区间的元素逐一比较大小,并将其插入到正确的位置
「插入排序 Insertion Sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似
回顾数组插入操作,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。
回忆数组的元素插入操作,设基准元素为 `base` ,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。
![单次插入操作](insertion_sort.assets/insertion_operation.png)
@ -10,9 +12,10 @@
插入排序的整体流程如下:
1. 首先,选取数组的第 2 个元素作为 `base` ,执行插入操作后,**数组的前 2 个元素已排序**。
2. 接着,选取第 3 个元素作为 `base` ,执行插入操作后,**数组的前 3 个元素已排序**。
3. 以此类推,在最后一轮中,选取数组尾元素作为 `base` ,执行插入操作后,**所有元素均已排序**。
1. 初始状态下,数组的第 1 个元素已完成排序。
2. 选取数组的第 2 个元素作为 `base` ,将其插入到正确位置后,**数组的前 2 个元素已排序**。
3. 选取第 3 个元素作为 `base` ,将其插入到正确位置后,**数组的前 3 个元素已排序**。
4. 以此类推,在最后一轮中,选取最后一个元素作为 `base` ,将其插入到正确位置后,**所有元素均已排序**。
![插入排序流程](insertion_sort.assets/insertion_sort_overview.png)
@ -86,14 +89,15 @@
## 插入排序优势
回顾冒泡排序和插入排序的复杂度分析,两者的循环轮数都是 $\frac{(n - 1) n}{2}$ 。然而,它们之间存在以下差异:
插入排序的时间复杂度为 $O(n^2)$ ,而我们即将学习的快速排序的时间复杂度为 $O(n \log n)$ 。尽管插入排序的时间复杂度相比快速排序更高,**但在数据量较小的情况下,插入排序通常更快**。这是因为快速排序属于基于分治的排序算法,包含更多单元计算操作。而在数据量较小时,复杂度中的常数项起主导作用。这个现象与线性查找和二分查找的适用情况相似。
- 冒泡操作基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;
- 插入操作基于元素赋值实现,仅需 1 个单元操作;
实际上,许多编程语言(例如 Java的内置排序函数都采用了插入排序大致思路为
粗略估计下来,冒泡排序的计算开销约为插入排序的 3 倍,因此插入排序更受欢迎。实际上,许多编程语言(如 Java的内置排序函数都采用了插入排序大致思路为
- 对于长数组,采用基于分治的排序算法,例如快速排序,时间复杂度为 $O(n \log n)$
- 对于短数组,直接使用插入排序,时间复杂度为 $O(n^2)$
- 对于长数组,采用基于分治的排序算法,例如「快速排序」,时间复杂度为 $O(n \log n)$
- 对于短数组,直接使用「插入排序」,时间复杂度为 $O(n^2)$
虽然冒泡排序、选择排序和插入排序的时间复杂度都为 $O(n^2)$ ,但在实际情况中,插入排序出现的频率远远高于冒泡排序和选择排序。这是因为:
尽管插入排序的时间复杂度高于快速排序,**但在数据量较小的情况下,插入排序实际上更快**。这是因为在数据量较小时,复杂度中的常数项(即每轮中的单元操作数量)起主导作用。这个现象与「线性查找」和「二分查找」的情况相似。
- 冒泡排序基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;插入排序基于元素赋值实现,仅需 1 个单元操作。因此,**冒泡排序的计算开销通常比插入排序更高**。
- 选择排序在任何情况下的时间复杂度都为 $O(n^2)$ 。**如果给定一组部分有序的数据,插入排序通常比选择排序效率更高**。
- 选择排序不稳定,无法应用于多级排序。

View file

@ -119,7 +119,7 @@
## 算法流程
1. 首先,对原数组执行一次「哨兵划分」,得到排序的左子数组和右子数组;
1. 首先,对原数组执行一次「哨兵划分」,得到排序的左子数组和右子数组;
2. 然后,对左子数组和右子数组分别递归执行「哨兵划分」;
3. 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序;

View file

@ -1,7 +1,7 @@
# 小结
- 冒泡排序通过交换相邻元素来实现排序。通过添加一个标志位来实现提前返回,我们可以将冒泡排序的最佳时间复杂度优化到 $O(n)$ 。
- 插入排序每轮将排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。
- 插入排序每轮将排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。
- 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,导致时间复杂度劣化至 $O(n^2)$ 。引入中位数基准数或随机基准数可以降低这种劣化的概率。尾递归方法可以有效地减少递归深度,将空间复杂度优化到 $O(\log n)$ 。
- 归并排序包括划分和合并两个阶段,典型地体现了分治策略。在归并排序中,排序数组需要创建辅助数组,空间复杂度为 $O(n)$ ;然而排序链表的空间复杂度可以优化至 $O(1)$ 。
- 桶排序包含三个步骤:数据分桶、桶内排序和合并结果。它同样体现了分治策略,适用于数据体量很大的情况。桶排序的关键在于对数据进行平均分配。