返回 LeetCode 刷题

Markdown File

冒泡排序 希尔排序 归并排序

冒泡排序 希尔排序 归并排序.md

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冒泡靠交换,希尔靠分组插入,归并靠分治合并。
Rendered Preview

一、冒泡排序 Bubble Sort

  1. 前提条件

冒泡排序是最基础的交换排序。

它的思想是:相邻两个数比较

如果顺序错了,就交换

一轮下来,最大值会被“冒泡”到最后

例如升序排序:小的在前,大的在后

  1. 核心概念

数组:[5, 3, 8, 2]

第一轮:

5 和 3 比,交换 → [3,5,8,2]

5 和 8 比,不换 → [3,5,8,2]

8 和 2 比,交换 → [3,5,2,8]

这一轮结束后:8 已经到最后

所以每一轮都会确定一个最大值的位置。

  1. 固定模板
    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;
    }
    }
    }
    }
    其实就是每次换两个之后确定最后一个最大值的位置,然后这样每次从头开始两两比较交换
  2. 优化就是:如果某一轮没有发生交换,说明已经有序,可以提前结束。

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;
    }
}

}

  1. 复杂度
    最好:O(n)
    平均:O(n²)
    最坏:O(n²)
    空间:O(1)
    稳定性:稳定

稳定的意思是:相等元素的相对顺序不变

因为冒泡只在:

arr[j] > arr[j + 1]

时交换,相等不交换。

二、希尔排序 Shell Sort

  1. 前提条件

希尔排序是插入排序的改进版。

普通插入排序每次只能让元素移动一步。

希尔排序先让元素“大步移动”,再逐渐缩小步长。

  1. 核心概念

希尔排序有一个关键词: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

最后就是普通插入排序,但这时数组已经基本有序,所以会快很多。

  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的时候,他就要换两次,是步幅减短,增多小间隔的交换次数,逐渐使数组有序。

  2. 代码理解

这段: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 应该插入的位置

  1. 例子
    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

普通插入排序收尾。

  1. 复杂度

时间复杂度:和 gap 序列有关
常见简单写法:大概介于 O(nlogn) 和 O(n²) 之间
最坏可到 O(n²)
空间:O(1)
稳定性:不稳定
因为相同元素可能在不同 gap 分组中跨越移动,导致相对顺序改变。

三、归并排序 Merge Sort

  1. 前提条件

归并排序是典型的:分治算法

分治就是:

把大问题拆成小问题
小问题解决后再合并

  1. 核心概念

归并排序分两步:

  1. 分:把数组不断分成两半
  2. 合:把两个有序数组合并成一个有序数组

例如:[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]

  1. 固定模板

#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);

}
这里是分配空间之后,进行排序

  1. 代码理解

递归出口:

if(left >= right)
{
return;
}

表示:区间里只有 0 个或 1 个元素天然有序

递归拆分:

mergeSortHelper(arr, temp, left, mid);
mergeSortHelper(arr, temp, mid + 1, right);

表示:

左半边排序
右半边排序

最后合并:

merge(arr, temp, left, mid, right);

表示:两个有序区间合并成一个有序区间

  1. 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]

  1. 为什么归并稳定?

代码里:

if(arr[i] <= arr[j])

相等时先取左边。

所以相等元素的相对顺序不变。

因此归并排序是稳定排序。

  1. 复杂度
    时间复杂度: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) 辅助数组

冒泡靠交换,希尔靠分组插入,归并靠分治合并。