Skip to content

Commit aae7772

Browse files
committed
更新 01.03 数组排序 相关图片、图片标题
1 parent 49b3a45 commit aae7772

13 files changed

+52
-134
lines changed

docs/ch01/01.03/01.03.01-Array-Bubble-Sort.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@
6060
2. 第 $2$ 趟「冒泡」:对前 $n - 1$ 个元素执行「冒泡」,从而使第 $2$ 个值最大的元素放置在正确位置上。
6161
1. 先将序列中第 $1$ 个元素与第 $2$ 个元素进行比较,若前者大于后者,则两者交换位置,否则不交换。
6262
2. 然后将第 $2$ 个元素与第 $3$ 个元素比较,若前者大于后者,则两者交换位置,否则不交换。
63-
3. 依次类推,直到第 $n - 2$ 个元素与第 $n - 1$ 个元素比较(或交换)为止。
63+
3. 依次类推,直到对 $n - 2$ 个元素与第 $n - 1$ 个元素比较(或交换)为止。
6464
4. 经过第 $2$ 趟排序,使得数组中第 $2$ 个值最大元素被安置在第 $n$ 个位置上。
6565
3. 依次类推,重复上述「冒泡」过程,直到某一趟排序过程中不出现元素交换位置的动作,则排序结束。
6666

67-
我们以 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下冒泡排序的整个过程
67+
我们以 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下冒泡排序算法的整个步骤
6868

69-
![冒泡排序](../../images/20230816154510.png)
69+
![冒泡排序算法步骤](../../images/20230816154510.png)
7070

7171
## 3. 冒泡排序代码实现
7272

@@ -76,7 +76,7 @@ class Solution:
7676
# 第 i 趟「冒泡」
7777
for i in range(len(nums) - 1):
7878
flag = False # 是否发生交换的标志位
79-
# 对数组未排序区间 [0, n - i - 1] 的元素执行「冒泡」
79+
# 从数组中前 n - i + 1 个元素的第 1 个元素开始,相邻两个元素进行比较
8080
for j in range(len(nums) - i - 1):
8181
# 相邻两个元素进行比较,如果前者大于后者,则交换位置
8282
if nums[j] > nums[j + 1]:
@@ -89,9 +89,6 @@ class Solution:
8989

9090
def sortArray(self, nums: [int]) -> [int]:
9191
return self.bubbleSort(nums)
92-
93-
94-
print(Solution().sortArray([5, 2, 3, 6, 1, 4]))
9592
```
9693

9794
## 4. 冒泡排序算法分析
@@ -104,4 +101,4 @@ print(Solution().sortArray([5, 2, 3, 6, 1, 4]))
104101

105102
## 参考资料
106103

107-
- 【文章】[11.3. 冒泡排序 - Hello 算法](https://www.hello-algo.com/chapter_sorting/bubble_sort/)
104+
- 【文章】[11.3. 冒泡排序 - Hello 算法](https://www.hello-algo.com/chapter_sorting/bubble_sort/)

docs/ch01/01.03/01.03.02-Array-Selection-Sort.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
3. 此时,$[0, 1]$ 为已排序区间,$[2, n - 1]$(总共 $n - 2$ 个元素)为未排序区间。
2424
4. 依次类推,对剩余未排序区间重复上述选择过程,直到所有元素都划分到已排序区间,排序结束。
2525

26-
我们以 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下选择排序的整个过程
26+
我们以 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下选择排序的整个步骤
2727

2828
<!-- tabs:start -->
2929

@@ -75,8 +75,6 @@ class Solution:
7575

7676
def sortArray(self, nums: [int]) -> [int]:
7777
return self.selectionSort(nums)
78-
79-
print(Solution().sortArray([5, 2, 3, 6, 1, 4]))
8078
```
8179

8280
## 4. 选择排序算法分析
@@ -85,4 +83,5 @@ print(Solution().sortArray([5, 2, 3, 6, 1, 4]))
8583
- 这是因为无论序列中元素的初始排列状态如何,第 $i$ 趟排序要找出值最小元素都需要进行 $n − i$ 次元素之间的比较。因此,整个排序过程需要进行的元素之间的比较次数都相同,为 $∑^n_{i=2}(i - 1) = \frac{n(n−1)}{2}$ 次。
8684
- **空间复杂度**:$O(1)$。选择排序算法为原地排序算法,只用到指针变量 $i$、$j$ 以及最小值位置 $min\underline{\hspace{0.5em}}i$ 等常数项的变量。
8785
- **选择排序适用情况**:选择排序方法在排序过程中需要移动较多次数的元素,并且排序时间效率比较低。因此,选择排序方法比较适合于参加排序序列的数据量较小的情况。选择排序的主要优点是仅需要原地操作无需占用其他空间就可以完成排序,因此在空间复杂度要求较高时,可以考虑选择排序。
88-
- **排序稳定性**:由于值最小元素与未排序区间第 $1$ 个元素的交换动作是在不相邻的元素之间进行的,因此很有可能会改变相等元素的相对顺序,因此,选择排序法是一种 **不稳定排序算法**
86+
87+
- **排序稳定性**:由于值最小元素与未排序区间第 $1$ 个元素的交换动作是在不相邻的元素之间进行的,因此很有可能会改变相等元素的相对顺序,因此,选择排序法是一种 **不稳定排序算法**

docs/ch01/01.03/01.03.03-Array-Insertion-Sort.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
4. 插入元素后有序区间变为 $[0, 2]$,无序区间变为 $[3, n - 1]$。
2727
4. 依次类推,对剩余无序区间中的元素重复上述插入过程,直到所有元素都插入到有序区间中,排序结束。
2828

29-
我们以 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下插入排序的整个过程
29+
我们以 $[5, 2, 3, 6, 1, 4]$ 为例,演示一下插入排序算法的整个步骤
3030

31-
![插入排序](../../images/20230816175619.png)
31+
![插入排序算法步骤](../../images/20230816175619.png)
3232

3333
## 3. 插入排序代码实现
3434

@@ -41,7 +41,7 @@ class Solution:
4141
j = i
4242
# 从右至左遍历有序区间
4343
while j > 0 and nums[j - 1] > temp:
44-
# 将有序区间中插入位置右侧的所有元素依次右移一位
44+
# 将有序区间中插入位置右侧的元素依次右移一位
4545
nums[j] = nums[j - 1]
4646
j -= 1
4747
# 将该元素插入到适当位置
@@ -51,8 +51,6 @@ class Solution:
5151

5252
def sortArray(self, nums: [int]) -> [int]:
5353
return self.insertionSort(nums)
54-
55-
print(Solution().sortArray([5, 2, 3, 6, 1, 4]))
5654
```
5755

5856
## 4. 插入排序算法分析
@@ -61,4 +59,4 @@ print(Solution().sortArray([5, 2, 3, 6, 1, 4]))
6159
- **最差时间复杂度**:$O(n^2)$。最差的情况下(初始时区间已经是降序排列),每个元素 $nums[i]$ 都要进行 $i - 1$ 次元素之间的比较,元素之间总的比较次数达到最大值,为 $∑^n_{i=2}(i − 1) = \frac{n(n−1)}{2}$。
6260
- **平均时间复杂度**:$O(n^2)$。如果区间的初始情况是随机的,即参加排序的区间中元素可能出现的各种排列的概率相同,则可取上述最小值和最大值的平均值作为插入排序时所进行的元素之间的比较次数,约为 $\frac{n^2}{4}$。由此得知,插入排序算法的平均时间复杂度为 $O(n^2)$。
6361
- **空间复杂度**:$O(1)$。插入排序算法为原地排序算法,只用到指针变量 $i$、$j$ 以及表示无序区间中第 $1$ 个元素的变量等常数项的变量。
64-
- **排序稳定性**:在插入操作过程中,每次都讲元素插入到相等元素的右侧,并不会改变相等元素的相对顺序。因此,插入排序方法是一种 **稳定排序算法**
62+
- **排序稳定性**:在插入操作过程中,每次都讲元素插入到相等元素的右侧,并不会改变相等元素的相对顺序。因此,插入排序方法是一种 **稳定排序算法**

docs/ch01/01.03/01.03.05-Array-Merge-Sort.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
5. 将另一个子数组中的剩余元素存入到结果数组 $nums$ 中。
2323
6. 返回合并后的有序数组 $nums$。
2424

25-
我们以 $[0, 5, 7, 3, 1, 6, 8, 4]$ 为例,演示一下归并排序的整个过程
25+
我们以 $[0, 5, 7, 3, 1, 6, 8, 4]$ 为例,演示一下归并排序算法的整个步骤
2626

27-
![归并排序](../../images/20230817103814.png)
27+
![归并排序算法步骤](../../images/20230817103814.png)
2828

2929
## 3. 归并排序代码实现
3030

@@ -69,12 +69,10 @@ class Solution:
6969

7070
def sortArray(self, nums: [int]) -> [int]:
7171
return self.mergeSort(nums)
72-
73-
print(Solution().sortArray([0, 5, 7, 3, 1, 6, 8, 4]))
7472
```
7573

7674
## 4. 归并排序算法分析
7775

7876
- **时间复杂度**:$O(n \times \log n)$。归并排序算法的时间复杂度等于归并趟数与每一趟归并的时间复杂度乘积。子算法 `merge(left_nums, right_nums):` 的时间复杂度是 $O(n)$,因此,归并排序算法总的时间复杂度为 $O(n \times \log n)$。
7977
- **空间复杂度**:$O(n)$。归并排序方法需要用到与参加排序的数组同样大小的辅助空间。因此,算法的空间复杂度为 $O(n)$。
80-
- **排序稳定性**:因为在两个有序子数组的归并过程中,如果两个有序数组中出现相等元素,`merge(left_nums, right_nums):` 算法能够使前一个数组中那个相等元素先被复制,从而确保这两个元素的相对顺序不发生改变。因此,归并排序算法是一种 **稳定排序算法**
78+
- **排序稳定性**:因为在两个有序子数组的归并过程中,如果两个有序数组中出现相等元素,`merge(left_nums, right_nums):` 算法能够使前一个数组中那个相等元素先被复制,从而确保这两个元素的相对顺序不发生改变。因此,归并排序算法是一种 **稳定排序算法**

docs/ch01/01.03/01.03.06-Array-Shell-Sort.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
4. 减少间隔数,并重新将整个数组按新的间隔数分成若干个子数组,再分别对各个子数组进行排序。
1818
5. 依次类推,直到间隔数 $gap$ 值为 $1$,最后进行一次排序,排序结束。
1919

20-
我们以 $[7, 2, 6, 8, 0, 4, 1, 5, 9, 3]$ 为例,演示一下希尔排序的整个过程
20+
我们以 $[7, 2, 6, 8, 0, 4, 1, 5, 9, 3]$ 为例,演示一下希尔排序的整个步骤
2121

2222
<!-- tabs:start -->
2323

@@ -78,8 +78,6 @@ class Solution:
7878

7979
def sortArray(self, nums: [int]) -> [int]:
8080
return self.shellSort(nums)
81-
82-
print(Solution().sortArray([7, 2, 6, 8, 0, 4, 1, 5, 9, 3]))
8381
```
8482

8583
## 4. 希尔排序算法分析
@@ -90,4 +88,4 @@ print(Solution().sortArray([7, 2, 6, 8, 0, 4, 1, 5, 9, 3]))
9088
- 从算法中也可以看到,外层 `while gap > 0` 的循环次数为 $\log n$ 数量级,内层插入排序算法循环次数为 $n$ 数量级。当子数组分得越多时,子数组内的元素就越少,内层循环的次数也就越少;反之,当所分的子数组个数减少时,子数组内的元素也随之增多,但整个数组也逐步接近有序,而循环次数却不会随之增加。因此,希尔排序算法的时间复杂度在 $O(n \times \log^2 n)$ 与 $O(n^2)$ 之间。
9189

9290
- **空间复杂度**:$O(1)$。希尔排序中用到的插入排序算法为原地排序算法,只用到指针变量 $i$、$j$ 以及表示无序区间中第 $1$ 个元素的变量、间隔数 $gap$ 等常数项的变量。
93-
- **排序稳定性**:在一次插入排序是稳定的,不会改变相等元素的相对顺序,但是在不同的插入排序中,相等元素可能在各自的插入排序中移动。因此,希尔排序方法是一种 **不稳定排序算法**
91+
- **排序稳定性**:在一次插入排序是稳定的,不会改变相等元素的相对顺序,但是在不同的插入排序中,相等元素可能在各自的插入排序中移动。因此,希尔排序方法是一种 **不稳定排序算法**

docs/ch01/01.03/01.03.08-Array-Quick-Sort.md

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# 01.03.08 快速排序(第 07 天)
22

3-
## 1. 快速排序算法思想
4-
53
> **快速排序(Quick Sort)基本思想**
64
>
75
> 采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。
@@ -26,7 +24,7 @@
2624

2725
我们先来看一下单次「哨兵划分」的过程。
2826

29-
<!-- tabs:start -->
27+
<!-- tabs:start -->
3028

3129
#### **<1>**
3230

@@ -56,11 +54,11 @@
5654

5755
![哨兵划分 7](../../images/20230818180027.png)
5856

59-
<!-- tabs:end -->
57+
<!-- tabs:end -->
6058

61-
在经过一次「哨兵划分」过程之后,数组就被划分为左子数组、基准数、右子树组三个独立部分。接下来只要对划分好的左右子数组分别进行递归排序即可完成排序。整个步骤如下
59+
在经过一次「哨兵划分」过程之后,数组就被划分为左子数组、基准数、右子树组三个独立部分。接下来只要对划分好的左右子数组分别进行递归排序即可完成排序。快速排序算法的整个步骤如下
6260

63-
![快速排序](../../images/20230818153642.png)
61+
![快速排序算法步骤](../../images/20230818153642.png)
6462

6563
## 3. 快速排序代码实现
6664

@@ -78,7 +76,7 @@ class Solution:
7876
return self.partition(nums, low, high)
7977

8078
# 哨兵划分:以第 1 位元素 nums[low] 为基准数,然后将比基准数小的元素移动到基准数左侧,将比基准数大的元素移动到基准数右侧,最后将基准数放到正确位置上
81-
def partition(self, nums: [int], low: int, high: int) -> int:
79+
def partition(self, nums: [int], low: int, high: int) -> int:
8280
# 以第 1 位元素为基准数
8381
pivot = nums[low]
8482

@@ -93,14 +91,15 @@ class Solution:
9391
# 交换元素
9492
nums[i], nums[j] = nums[j], nums[i]
9593

96-
# 将基准数放到正确位置上
97-
nums[j], nums[low] = nums[low], nums[j]
98-
return j
94+
# 将基准节点放到正确位置上
95+
nums[i], nums[low] = nums[low], nums[i]
96+
# 返回基准数的索引
97+
return i
9998

10099
def quickSort(self, nums: [int], low: int, high: int) -> [int]:
101100
if low < high:
102101
# 按照基准数的位置,将数组划分为左右两个子数组
103-
pivot_i = self.partition(nums, low, high)
102+
pivot_i = self.randomPartition(nums, low, high)
104103
# 对左右两个子数组分别进行递归快速排序
105104
self.quickSort(nums, low, pivot_i - 1)
106105
self.quickSort(nums, pivot_i + 1, high)
@@ -109,8 +108,6 @@ class Solution:
109108

110109
def sortArray(self, nums: [int]) -> [int]:
111110
return self.quickSort(nums, 0, len(nums) - 1)
112-
113-
print(Solution().sortArray([4, 7, 5, 2, 6, 1, 3]))
114111
```
115112

116113
## 4. 快速排序算法分析
@@ -139,4 +136,4 @@ print(Solution().sortArray([4, 7, 5, 2, 6, 1, 3]))
139136

140137
## 参考资料
141138

142-
- 【文章】[快速排序 - OI Wiki](https://oi-wiki.org/basic/quick-sort/)
139+
- 【文章】[快速排序 - OI Wiki](https://oi-wiki.org/basic/quick-sort/)

docs/ch01/01.03/01.03.09-Array-Heap-Sort.md

Lines changed: 14 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515

1616
### 1.2 堆的存储结构
1717

18-
堆的逻辑结构就是一颗完全二叉树。而我们在「07.树 - 01.二叉树 - 01.树与二叉树的基础知识」章节中学过,对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构(数组)的形式来表示完全二叉树,能够充分利用存储空间。
18+
堆的逻辑结构就是一颗完全二叉树。如下图所示:
19+
20+
![堆的逻辑结构](../../images/202405092006120.png)
21+
22+
而我们在「07.树 - 01.二叉树 - 01.树与二叉树的基础知识」章节中学过,对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构(数组)的形式来表示完全二叉树,能够充分利用存储空间。如下图所示:
23+
24+
![使用顺序存储结构(数组)表示堆](../../images/202405092007823.png)
1925

2026
当我们使用顺序存储结构(即数组)来表示堆时,堆中元素的节点编号与数组的索引关系为:
2127

@@ -28,8 +34,6 @@ class MaxHeap:
2834
self.max_heap = []
2935
```
3036

31-
![堆的存储结构](../../images/20230824154601.png)
32-
3337
### 1.3 访问堆顶元素
3438

3539
> **访问堆顶元素**:指的是从堆结构中获取位于堆顶的元素。
@@ -232,9 +236,7 @@ class MaxHeap:
232236
3. **重复交换和调整堆**
233237
1. 重复第 $2$ 步,直到堆的大小为 $1$ 时,此时大顶堆的数组已经完全有序。
234238

235-
#### 2.2.1 构建初始大顶堆
236-
237-
<!-- tabs:start -->
239+
<!-- tabs:start -->
238240

239241
#### **<1>**
240242

@@ -266,9 +268,7 @@ class MaxHeap:
266268

267269
<!-- tabs:end -->
268270

269-
#### 2.2.2 交换元素,调整堆
270-
271-
<!-- tabs:start -->
271+
<!-- tabs:start -->
272272

273273
#### **<1>**
274274

@@ -312,84 +312,19 @@ class MaxHeap:
312312

313313
#### **<11>**
314314

315-
![2. 交换元素,调整堆 11](../../images/20230831162505.png)
315+
![https://qcdn.](../../images/20230831162505.png)
316316

317317
#### **<12>**
318318

319319
![2. 交换元素,调整堆 12](../../images/20230831162512.png)
320320

321-
<!-- tabs:end -->
321+
<!-- tabs:end -->
322322

323-
### 2.3 堆排序完整代码实现
323+
### 2.3 堆排序代码实现
324324

325325
```python
326326
class MaxHeap:
327-
def __init__(self):
328-
self.max_heap = []
329-
330-
def peek(self) -> int:
331-
# 大顶堆为空
332-
if not self.max_heap:
333-
return None
334-
# 返回堆顶元素
335-
return self.max_heap[0]
336-
337-
def push(self, val: int):
338-
# 将新元素添加到堆的末尾
339-
self.max_heap.append(val)
340-
341-
size = len(self.max_heap)
342-
# 从新插入的元素节点开始,进行上移调整
343-
self.__shift_up(size - 1)
344-
345-
def __shift_up(self, i: int):
346-
while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2]:
347-
self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i]
348-
i = (i - 1) // 2
349-
350-
def pop(self) -> int:
351-
# 堆为空
352-
if not self.max_heap:
353-
raise IndexError("堆为空")
354-
355-
size = len(self.max_heap)
356-
self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0]
357-
# 删除堆顶元素
358-
val = self.max_heap.pop()
359-
# 节点数减 1
360-
size -= 1
361-
362-
self.__shift_down(0, size)
363-
364-
# 返回堆顶元素
365-
return val
366-
367-
368-
def __shift_down(self, i: int, n: int):
369-
while 2 * i + 1 < n:
370-
# 左右子节点编号
371-
left, right = 2 * i + 1, 2 * i + 2
372-
373-
# 找出左右子节点中的较大值节点编号
374-
if 2 * i + 2 >= n:
375-
# 右子节点编号超出范围(只有左子节点
376-
larger = left
377-
else:
378-
# 左子节点、右子节点都存在
379-
if self.max_heap[left] >= self.max_heap[right]:
380-
larger = left
381-
else:
382-
larger = right
383-
384-
# 将当前节点值与其较大的子节点进行比较
385-
if self.max_heap[i] < self.max_heap[larger]:
386-
# 如果当前节点值小于其较大的子节点,则将它们交换
387-
self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i]
388-
i = larger
389-
else:
390-
# 如果当前节点值大于等于于其较大的子节点,此时结束
391-
break
392-
327+
......
393328
def __buildMaxHeap(self, nums: [int]):
394329
size = len(nums)
395330
# 先将数组 nums 的元素按顺序添加到 max_heap 中
@@ -433,4 +368,4 @@ print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14]))
433368
2. 在第 $2$ 个循环中,每次调用调整堆算法一次,节点移动的最大距离为这棵完全二叉树的深度 $d = \lfloor \log_2(n) \rfloor + 1$,一共调用了 $n - 1$ 次调整堆算法,所以,第 $2$ 个循环的时间花费为 $(n-1)(\lfloor \log_2 (n)\rfloor + 1) = O(n \times \log n)$。
434369
- 因此,堆积排序的时间复杂度为 $O(n \times \log n)$。
435370
- **空间复杂度**:$O(1)$。由于在堆积排序中只需要一个记录大小的辅助空间,因此,堆积排序的空间复杂度为:$O(1)$。
436-
- **排序稳定性**:在进行「下移调整」时,相等元素的相对位置可能会发生变化。因此,堆排序是一种 **不稳定排序算法**
371+
- **排序稳定性**:在进行「下移调整」时,相等元素的相对位置可能会发生变化。因此,堆排序是一种 **不稳定排序算法**

0 commit comments

Comments
 (0)