1一、冒泡排序 Bubble Sort
21. 前提条件
3
4冒泡排序是最基础的交换排序。
5
6它的思想是:相邻两个数比较
7
8如果顺序错了,就交换
9
10一轮下来,最大值会被“冒泡”到最后
11
12例如升序排序:小的在前,大的在后
13
142. 核心概念
15
16数组:[5, 3, 8, 2]
17
18第一轮:
19
205 和 3 比,交换 → [3,5,8,2]
21
225 和 8 比,不换 → [3,5,8,2]
23
248 和 2 比,交换 → [3,5,2,8]
25
26这一轮结束后:8 已经到最后
27
28所以每一轮都会确定一个最大值的位置。
29
303. 固定模板
31void bubbleSort(int arr[], int n)
32{
33 for(int i = 0; i < n - 1; i++)
34 {
35 for(int j = 0; j < n - 1 - i; j++)
36 {
37 if(arr[j] > arr[j + 1])
38 {
39 int temp = arr[j];
40 arr[j] = arr[j + 1];
41 arr[j + 1] = temp;
42 }
43 }
44 }
45}
46其实就是每次换两个之后确定最后一个最大值的位置,然后这样每次从头开始两两比较交换
474. 优化就是:如果某一轮没有发生交换,说明已经有序,可以提前结束。
48
49void bubbleSort(int arr[], int n)
50{
51 for(int i = 0; i < n - 1; i++)
52 {
53 int swapped = 0;
54
55 for(int j = 0; j < n - 1 - i; j++)
56 {
57 if(arr[j] > arr[j + 1])
58 {
59 int temp = arr[j];
60 arr[j] = arr[j + 1];
61 arr[j + 1] = temp;
62
63 swapped = 1;
64 }
65 }
66
67 if(swapped == 0)
68 {
69 break;
70 }
71 }
72}
73
745. 复杂度
75最好:O(n)
76平均:O(n²)
77最坏:O(n²)
78空间:O(1)
79稳定性:稳定
80
81稳定的意思是:相等元素的相对顺序不变
82
83因为冒泡只在:
84
85arr[j] > arr[j + 1]
86
87时交换,相等不交换。
88
89二、希尔排序 Shell Sort
90
911. 前提条件
92
93希尔排序是插入排序的改进版。
94
95普通插入排序每次只能让元素移动一步。
96
97希尔排序先让元素“大步移动”,再逐渐缩小步长。
98
992. 核心概念
100
101希尔排序有一个关键词:gap 间隔
102
103例如:
104
105arr = [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
106
107如果:
108
109gap = 5
110
111分组就是:
112
113下标 0,5
114下标 1,6
115下标 2,7
116下标 3,8
117下标 4,9
118
119每组内部做插入排序。
120
121然后:gap = gap / 2
122
123继续。直到:gap = 1
124
125最后就是普通插入排序,但这时数组已经基本有序,所以会快很多。
126
1273. 固定模板
128void shellSort(int arr[], int n)
129{
130 for(int gap = n / 2; gap > 0; gap = gap / 2)
131 {
132 for(int i = gap; i < n; i++)
133 {
134 int temp = arr[i];
135 int j = i;
136
137 while(j >= gap && arr[j - gap] > temp)
138 {
139 arr[j] = arr[j - gap];
140 j = j - gap;
141 }
142
143 arr[j] = temp;
144 }
145 }
146}
147希尔排序其实按照我的理解是,先分组大排序,这时候排序的次数少,步幅比较大,一个gap里面的循环对应每个j只交换一次,就是先提前保留下来,如果大于就交换,小于就保留,后面等到gap变小的时候,循环次数就变多了,比如说gap等于2,等到j加到比如说4的时候,他就要换两次,是步幅减短,增多小间隔的交换次数,逐渐使数组有序。
148
1494. 代码理解
150
151这段:for(int gap = n / 2; gap > 0; gap = gap / 2)
152
153表示:
154
155先大间隔排序
156
157再小间隔排序
158
159最后 gap = 1
160这段:for(int i = gap; i < n; i++)
161
162表示:从每组的第二个元素开始做插入排序
163
164这段:
165
166while(j >= gap && arr[j - gap] > temp)
167{
168 arr[j] = arr[j - gap];
169 j = j - gap;
170}
171
172表示:如果前面同组元素比 temp 大
173
174就往后挪
175
176直到找到 temp 应该插入的位置
177
1785. 例子
179arr = [9, 8, 3, 7, 5, 6, 4, 1]
180n = 8
181
182第一轮:gap = 4
183
184分组:
185
186下标 0,4 → 9,5
187下标 1,5 → 8,6
188下标 2,6 → 3,4
189下标 3,7 → 7,1
190
191组内排序后大概变成:
192
193[5,6,3,1,9,8,4,7]
194
195第二轮:gap = 2
196
197继续让元素更接近正确位置。
198
199最后:gap = 1
200
201普通插入排序收尾。
202
2036. 复杂度
204
205时间复杂度:和 gap 序列有关
206常见简单写法:大概介于 O(nlogn) 和 O(n²) 之间
207最坏可到 O(n²)
208空间:O(1)
209稳定性:不稳定
210因为相同元素可能在不同 gap 分组中跨越移动,导致相对顺序改变。
211
212三、归并排序 Merge Sort
213
2141. 前提条件
215
216归并排序是典型的:分治算法
217
218分治就是:
219
220把大问题拆成小问题
221小问题解决后再合并
222
2232. 核心概念
224
225归并排序分两步:
226
2271. 分:把数组不断分成两半
2282. 合:把两个有序数组合并成一个有序数组
229
230例如:[5, 2, 8, 1]
231
232先分:[5,2] 和 [8,1]
233
234继续分:[5] [2] [8] [1]
235
236单个元素天然有序。
237
238然后合并:
239
240[5] + [2] → [2,5]
241
242[8] + [1] → [1,8]
243
244[2,5] + [1,8] → [1,2,5,8]
245
2463. 固定模板
247
248#include <stdlib.h>
249
250void merge(int arr[], int temp[], int left, int mid, int right)
251{
252 //这里是把一个数组从mid割开成两部分排序之后合并,奇数偶数的情况不需要考虑长度不同也可以排序
253 int i = left;
254 int j = mid + 1;
255 int k = left;
256
257 while(i <= mid && j <= right)
258 {
259 if(arr[i] <= arr[j])
260 {
261 temp[k] = arr[i];
262 i++;
263 }
264 else
265 {
266 temp[k] = arr[j];
267 j++;
268 }
269 //这里是弄了一个暂时数组存放比较i和j的结果之后选择小的放进left的位置里面,从left的位置开始,往后排序
270 //本质上是数组的两部分,比如说 1 3 4 | 2 5 6 排序,然后mid对应数值2,1和2比较1小,把1放进去,之后不断移位循环,3比2大,把2放进去,之后移位
271 k++;
272 }
273 while(i <= mid)
274 {
275 temp[k] = arr[i];
276 i++;
277 k++;
278 }
279 //这里是排序了就是右边的部分已经到了尽头,左边还有部分,所以就是把左边直接拉进temp来
280 while(j <= right)
281 {
282 temp[k] = arr[j];
283 j++;
284 k++;
285 }
286 //这里是排序了左边的部分已经到了尽头,右边还有一部分,把右边的拉进temp来
287 for(int p = left; p <= right; p++)
288 {
289 arr[p] = temp[p];
290 }
291}
292
293void mergeSortHelper(int arr[], int temp[], int left, int right)
294{
295 if(left >= right)
296 {
297 return;
298 }
299
300 int mid = left + (right - left) / 2;
301
302 mergeSortHelper(arr, temp, left, mid);
303
304 mergeSortHelper(arr, temp, mid + 1, right);
305
306 merge(arr, temp, left, mid, right);
307}
308
309这个函数是逐渐分拆数组,直到分拆数组left=right之后返回,左右各自分拆,最后左右合并排序
310理解还是和之前一样先写出口之后逐渐递归回拆排序,
311
312void mergeSort(int arr[], int n)
313{
314 int* temp = malloc(n * sizeof(int));
315
316 mergeSortHelper(arr, temp, 0, n - 1);
317
318 free(temp);
319}
320这里是分配空间之后,进行排序
321
3224. 代码理解
323
324递归出口:
325
326if(left >= right)
327{
328 return;
329}
330
331表示:区间里只有 0 个或 1 个元素天然有序
332
333递归拆分:
334
335mergeSortHelper(arr, temp, left, mid);
336mergeSortHelper(arr, temp, mid + 1, right);
337
338表示:
339
340左半边排序
341右半边排序
342
343最后合并:
344
345merge(arr, temp, left, mid, right);
346
347表示:两个有序区间合并成一个有序区间
348
3495. merge 函数怎么理解
350
351假设:
352
353左边:[2,5,8]
354右边:[1,3,7]
355
356三个指针:
357
358i 指向左边
359j 指向右边
360k 指向 temp
361
362比较:
363
3642 和 1 → 放 1
3652 和 3 → 放 2
3665 和 3 → 放 3
3675 和 7 → 放 5
3688 和 7 → 放 7
369左边剩 8 → 放 8
370
371结果:[1,2,3,5,7,8]
372
3736. 为什么归并稳定?
374
375代码里:
376
377if(arr[i] <= arr[j])
378
379相等时先取左边。
380
381所以相等元素的相对顺序不变。
382
383因此归并排序是稳定排序。
384
3857. 复杂度
386时间复杂度:O(nlogn)
387空间复杂度:O(n)
388稳定性:稳定
389
390为什么是 O(nlogn)?
391
392每一层合并总共 O(n)
393
394一共有 logn 层
395所以 O(nlogn)
396
397四、三种排序对比总结
398排序 思想 时间复杂度 空间 稳定性
399冒泡排序 相邻交换 O(n²) O(1) 稳定
400希尔排序 分组插入 和 gap 有关,最坏 O(n²) O(1) 不稳定
401归并排序 分治合并 O(nlogn) O(n) 稳定
402五、总结
403冒泡排序:
404
405相邻元素比较
406顺序错就交换
407每一轮确定一个最大值
408
409核心:
410 arr[j] > arr[j+1] 就交换
411
412特点:
413 简单
414 稳定
415 O(n²)
416
417--------------------------------
418
419希尔排序:
420
421插入排序的改进版
422
423核心:
424 gap 分组
425 每组做插入排序
426 gap 不断变小
427 最后 gap = 1
428
429特点:
430 比普通插入快
431 不稳定
432 O(1)空间
433
434--------------------------------
435
436归并排序:
437
438分治算法
439
440核心:
441 先分成两半
442 左边排好
443 右边排好
444 再合并
445
446特点:
447 O(nlogn)
448 稳定
449 需要 O(n) 辅助数组
450
451冒泡靠交换,希尔靠分组插入,归并靠分治合并。
一、冒泡排序 Bubble Sort
- 前提条件
冒泡排序是最基础的交换排序。
它的思想是:相邻两个数比较
如果顺序错了,就交换
一轮下来,最大值会被“冒泡”到最后
例如升序排序:小的在前,大的在后
- 核心概念
数组:[5, 3, 8, 2]
第一轮:
5 和 3 比,交换 → [3,5,8,2]
5 和 8 比,不换 → [3,5,8,2]
8 和 2 比,交换 → [3,5,2,8]
这一轮结束后:8 已经到最后
所以每一轮都会确定一个最大值的位置。
- 固定模板
void bubbleSort(int arr[], int n)
{
for(int i = 0; i < n - 1; i++)
{
for(int j = 0; j < n - 1 - i; j++)
{
if(arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
其实就是每次换两个之后确定最后一个最大值的位置,然后这样每次从头开始两两比较交换
- 优化就是:如果某一轮没有发生交换,说明已经有序,可以提前结束。
void bubbleSort(int arr[], int n)
{
for(int i = 0; i < n - 1; i++)
{
int swapped = 0;
for(int j = 0; j < n - 1 - i; j++)
{
if(arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}
if(swapped == 0)
{
break;
}
}
}
- 复杂度
最好:O(n)
平均:O(n²)
最坏:O(n²)
空间:O(1)
稳定性:稳定
稳定的意思是:相等元素的相对顺序不变
因为冒泡只在:
arr[j] > arr[j + 1]
时交换,相等不交换。
二、希尔排序 Shell Sort
- 前提条件
希尔排序是插入排序的改进版。
普通插入排序每次只能让元素移动一步。
希尔排序先让元素“大步移动”,再逐渐缩小步长。
- 核心概念
希尔排序有一个关键词:gap 间隔
例如:
arr = [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
如果:
gap = 5
分组就是:
下标 0,5
下标 1,6
下标 2,7
下标 3,8
下标 4,9
每组内部做插入排序。
然后:gap = gap / 2
继续。直到:gap = 1
最后就是普通插入排序,但这时数组已经基本有序,所以会快很多。
-
固定模板
void shellSort(int arr[], int n)
{
for(int gap = n / 2; gap > 0; gap = gap / 2)
{
for(int i = gap; i < n; i++)
{
int temp = arr[i];
int j = i;
while(j >= gap && arr[j - gap] > temp)
{
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = temp;
}
}
}
希尔排序其实按照我的理解是,先分组大排序,这时候排序的次数少,步幅比较大,一个gap里面的循环对应每个j只交换一次,就是先提前保留下来,如果大于就交换,小于就保留,后面等到gap变小的时候,循环次数就变多了,比如说gap等于2,等到j加到比如说4的时候,他就要换两次,是步幅减短,增多小间隔的交换次数,逐渐使数组有序。
-
代码理解
这段:for(int gap = n / 2; gap > 0; gap = gap / 2)
表示:
先大间隔排序
再小间隔排序
最后 gap = 1
这段:for(int i = gap; i < n; i++)
表示:从每组的第二个元素开始做插入排序
这段:
while(j >= gap && arr[j - gap] > temp)
{
arr[j] = arr[j - gap];
j = j - gap;
}
表示:如果前面同组元素比 temp 大
就往后挪
直到找到 temp 应该插入的位置
- 例子
arr = [9, 8, 3, 7, 5, 6, 4, 1]
n = 8
第一轮:gap = 4
分组:
下标 0,4 → 9,5
下标 1,5 → 8,6
下标 2,6 → 3,4
下标 3,7 → 7,1
组内排序后大概变成:
[5,6,3,1,9,8,4,7]
第二轮:gap = 2
继续让元素更接近正确位置。
最后:gap = 1
普通插入排序收尾。
- 复杂度
时间复杂度:和 gap 序列有关
常见简单写法:大概介于 O(nlogn) 和 O(n²) 之间
最坏可到 O(n²)
空间:O(1)
稳定性:不稳定
因为相同元素可能在不同 gap 分组中跨越移动,导致相对顺序改变。
三、归并排序 Merge Sort
- 前提条件
归并排序是典型的:分治算法
分治就是:
把大问题拆成小问题
小问题解决后再合并
- 核心概念
归并排序分两步:
- 分:把数组不断分成两半
- 合:把两个有序数组合并成一个有序数组
例如:[5, 2, 8, 1]
先分:[5,2] 和 [8,1]
继续分:[5] [2] [8] [1]
单个元素天然有序。
然后合并:
[5] + [2] → [2,5]
[8] + [1] → [1,8]
[2,5] + [1,8] → [1,2,5,8]
- 固定模板
#include <stdlib.h>
void merge(int arr[], int temp[], int left, int mid, int right)
{
//这里是把一个数组从mid割开成两部分排序之后合并,奇数偶数的情况不需要考虑长度不同也可以排序
int i = left;
int j = mid + 1;
int k = left;
while(i <= mid && j <= right)
{
if(arr[i] <= arr[j])
{
temp[k] = arr[i];
i++;
}
else
{
temp[k] = arr[j];
j++;
}
//这里是弄了一个暂时数组存放比较i和j的结果之后选择小的放进left的位置里面,从left的位置开始,往后排序
//本质上是数组的两部分,比如说 1 3 4 | 2 5 6 排序,然后mid对应数值2,1和2比较1小,把1放进去,之后不断移位循环,3比2大,把2放进去,之后移位
k++;
}
while(i <= mid)
{
temp[k] = arr[i];
i++;
k++;
}
//这里是排序了就是右边的部分已经到了尽头,左边还有部分,所以就是把左边直接拉进temp来
while(j <= right)
{
temp[k] = arr[j];
j++;
k++;
}
//这里是排序了左边的部分已经到了尽头,右边还有一部分,把右边的拉进temp来
for(int p = left; p <= right; p++)
{
arr[p] = temp[p];
}
}
void mergeSortHelper(int arr[], int temp[], int left, int right)
{
if(left >= right)
{
return;
}
int mid = left + (right - left) / 2;
mergeSortHelper(arr, temp, left, mid);
mergeSortHelper(arr, temp, mid + 1, right);
merge(arr, temp, left, mid, right);
}
这个函数是逐渐分拆数组,直到分拆数组left=right之后返回,左右各自分拆,最后左右合并排序
理解还是和之前一样先写出口之后逐渐递归回拆排序,
void mergeSort(int arr[], int n)
{
int* temp = malloc(n * sizeof(int));
mergeSortHelper(arr, temp, 0, n - 1);
free(temp);
}
这里是分配空间之后,进行排序
- 代码理解
递归出口:
if(left >= right)
{
return;
}
表示:区间里只有 0 个或 1 个元素天然有序
递归拆分:
mergeSortHelper(arr, temp, left, mid);
mergeSortHelper(arr, temp, mid + 1, right);
表示:
左半边排序
右半边排序
最后合并:
merge(arr, temp, left, mid, right);
表示:两个有序区间合并成一个有序区间
- merge 函数怎么理解
假设:
左边:[2,5,8]
右边:[1,3,7]
三个指针:
i 指向左边
j 指向右边
k 指向 temp
比较:
2 和 1 → 放 1
2 和 3 → 放 2
5 和 3 → 放 3
5 和 7 → 放 5
8 和 7 → 放 7
左边剩 8 → 放 8
结果:[1,2,3,5,7,8]
- 为什么归并稳定?
代码里:
if(arr[i] <= arr[j])
相等时先取左边。
所以相等元素的相对顺序不变。
因此归并排序是稳定排序。
- 复杂度
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定
为什么是 O(nlogn)?
每一层合并总共 O(n)
一共有 logn 层
所以 O(nlogn)
四、三种排序对比总结
排序 思想 时间复杂度 空间 稳定性
冒泡排序 相邻交换 O(n²) O(1) 稳定
希尔排序 分组插入 和 gap 有关,最坏 O(n²) O(1) 不稳定
归并排序 分治合并 O(nlogn) O(n) 稳定
五、总结
冒泡排序:
相邻元素比较
顺序错就交换
每一轮确定一个最大值
核心:
arr[j] > arr[j+1] 就交换
特点:
简单
稳定
O(n²)
希尔排序:
插入排序的改进版
核心:
gap 分组
每组做插入排序
gap 不断变小
最后 gap = 1
特点:
比普通插入快
不稳定
O(1)空间
归并排序:
分治算法
核心:
先分成两半
左边排好
右边排好
再合并
特点:
O(nlogn)
稳定
需要 O(n) 辅助数组
冒泡靠交换,希尔靠分组插入,归并靠分治合并。