diff --git a/codes/c/chapter_sorting/quick_sort.c b/codes/c/chapter_sorting/quick_sort.c index b1648eba3..1ecf6f7e8 100644 --- a/codes/c/chapter_sorting/quick_sort.c +++ b/codes/c/chapter_sorting/quick_sort.c @@ -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] } } } diff --git a/codes/cpp/chapter_sorting/quick_sort.cpp b/codes/cpp/chapter_sorting/quick_sort.cpp index 327600f0a..8b6ebb038 100644 --- a/codes/cpp/chapter_sorting/quick_sort.cpp +++ b/codes/cpp/chapter_sorting/quick_sort.cpp @@ -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] } } } diff --git a/codes/csharp/chapter_sorting/quick_sort.cs b/codes/csharp/chapter_sorting/quick_sort.cs index 60fd8119a..f00198070 100644 --- a/codes/csharp/chapter_sorting/quick_sort.cs +++ b/codes/csharp/chapter_sorting/quick_sort.cs @@ -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] } } } diff --git a/codes/dart/chapter_sorting/quick_sort.dart b/codes/dart/chapter_sorting/quick_sort.dart index 4dbcd5ad5..1b039c481 100644 --- a/codes/dart/chapter_sorting/quick_sort.dart +++ b/codes/dart/chapter_sorting/quick_sort.dart @@ -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] } } } diff --git a/codes/go/chapter_sorting/quick_sort.go b/codes/go/chapter_sorting/quick_sort.go index 66ea91dde..5b7412584 100644 --- a/codes/go/chapter_sorting/quick_sort.go +++ b/codes/go/chapter_sorting/quick_sort.go @@ -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] } } } diff --git a/codes/java/chapter_sorting/quick_sort.java b/codes/java/chapter_sorting/quick_sort.java index 2bc1634fd..74d707ea2 100644 --- a/codes/java/chapter_sorting/quick_sort.java +++ b/codes/java/chapter_sorting/quick_sort.java @@ -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] } } } diff --git a/codes/javascript/chapter_sorting/quick_sort.js b/codes/javascript/chapter_sorting/quick_sort.js index 90f5ed21f..7cb9816bf 100644 --- a/codes/javascript/chapter_sorting/quick_sort.js +++ b/codes/javascript/chapter_sorting/quick_sort.js @@ -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] } } } diff --git a/codes/python/chapter_sorting/quick_sort.py b/codes/python/chapter_sorting/quick_sort.py index 2dd04934e..ec36db72a 100644 --- a/codes/python/chapter_sorting/quick_sort.py +++ b/codes/python/chapter_sorting/quick_sort.py @@ -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""" diff --git a/codes/rust/chapter_sorting/quick_sort.rs b/codes/rust/chapter_sorting/quick_sort.rs index d09652d45..9ed35058c 100644 --- a/codes/rust/chapter_sorting/quick_sort.rs +++ b/codes/rust/chapter_sorting/quick_sort.rs @@ -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] } } } diff --git a/codes/swift/chapter_sorting/quick_sort.swift b/codes/swift/chapter_sorting/quick_sort.swift index 88cb0a36e..8a94eef45 100644 --- a/codes/swift/chapter_sorting/quick_sort.swift +++ b/codes/swift/chapter_sorting/quick_sort.swift @@ -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] } } } diff --git a/codes/typescript/chapter_sorting/quick_sort.ts b/codes/typescript/chapter_sorting/quick_sort.ts index 1fe975907..f36611c92 100644 --- a/codes/typescript/chapter_sorting/quick_sort.ts +++ b/codes/typescript/chapter_sorting/quick_sort.ts @@ -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] } } } diff --git a/codes/zig/chapter_sorting/quick_sort.zig b/codes/zig/chapter_sorting/quick_sort.zig index dee931da1..587887a4f 100644 --- a/codes/zig/chapter_sorting/quick_sort.zig +++ b/codes/zig/chapter_sorting/quick_sort.zig @@ -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] } } } diff --git a/docs/chapter_searching/searching_algorithm_revisited.md b/docs/chapter_searching/searching_algorithm_revisited.md index 5e9fc8c31..25edf1510 100644 --- a/docs/chapter_searching/searching_algorithm_revisited.md +++ b/docs/chapter_searching/searching_algorithm_revisited.md @@ -1,4 +1,4 @@ -# 搜索算法 +# 重识搜索算法 「搜索算法 Searching Algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 9bf788f18..afb5cdd1d 100755 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -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)$ ,是“原地排序”**。 + +由于在“冒泡”中遇到相等元素不交换,**因此冒泡排序是“稳定排序”**。 diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index c5cf8084c..9435ef4fd 100755 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -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)$ 。**如果给定一组部分有序的数据,插入排序通常比选择排序效率更高**。 +- 选择排序不稳定,无法应用于多级排序。 diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index fa042b468..7bd5d60a7 100755 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -119,7 +119,7 @@ ## 算法流程 -1. 首先,对原数组执行一次「哨兵划分」,得到待排序的左子数组和右子数组; +1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组; 2. 然后,对左子数组和右子数组分别递归执行「哨兵划分」; 3. 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序; diff --git a/docs/chapter_sorting/summary.md b/docs/chapter_sorting/summary.md index d1888e5e1..423b43730 100644 --- a/docs/chapter_sorting/summary.md +++ b/docs/chapter_sorting/summary.md @@ -1,7 +1,7 @@ # 小结 - 冒泡排序通过交换相邻元素来实现排序。通过添加一个标志位来实现提前返回,我们可以将冒泡排序的最佳时间复杂度优化到 $O(n)$ 。 -- 插入排序每轮将待排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。 +- 插入排序每轮将未排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,它在小数据量的排序任务中非常受欢迎。 - 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,导致时间复杂度劣化至 $O(n^2)$ 。引入中位数基准数或随机基准数可以降低这种劣化的概率。尾递归方法可以有效地减少递归深度,将空间复杂度优化到 $O(\log n)$ 。 - 归并排序包括划分和合并两个阶段,典型地体现了分治策略。在归并排序中,排序数组需要创建辅助数组,空间复杂度为 $O(n)$ ;然而排序链表的空间复杂度可以优化至 $O(1)$ 。 - 桶排序包含三个步骤:数据分桶、桶内排序和合并结果。它同样体现了分治策略,适用于数据体量很大的情况。桶排序的关键在于对数据进行平均分配。