冒泡排序
原理
冒泡排序在C语言程序设计篇已经讲解过了,冒泡排序的核心就是交换,通过不断地进行交换,一点一点将大的元素推向一端,每一轮都会有一个最大的元素排到对应的位置上,最后形成有序。算法演示网站:点我打开♪(∇*)
设数组长度为N,详细过程为:
- 共进行N轮排序。
- 每一轮排序从数组的最左边开始,两两元素进行比较,如果左边元素大于右边的元素,那么就交换两个元素的位置,否则不变。
- 每轮排序都会将剩余元素中最大的一个推到最右边,下次排序就不再考虑这些已经在对应位置的元素。
比如下面的数组:

那么在第一轮排序时,首先比较前两个元素:

我们发现前者更大,那么此时就需要交换,交换之后,继续向后比较后面的两个元素:

我们发现后者更大,不变,继续看后两个:

此时前者更大,交换,继续比较后续元素:

同理得,最后,当前数组中最大的元素就被丢到最前面去了,这一轮排序结束,因为最大的已经排到对应的位置上了,所以说第二轮我们只需要考虑其之前的这些元素即可:

代码
这样,我们就可以不断将最大的丢到最右边了,最后N轮排序之后,就是一个有序的数组了。
程序代码如下:
1 | void bubbleSort(int arr[], int size){ |
只不过这种代码还是最原始的冒泡排序,我们可以对其进行优化:
- 实际上排序并不需要N轮,而是N-1轮即可,因为最后一轮只有一个元素未排序了,相当于已经排序了,所以说不需要再考虑了。
- 如果整轮排序中都没有出现任何的交换,那么说明数组已经是有序的了,不存在前一个比后一个大的情况。
所以,我们来改进一下:
1 | void bubbleSort(int arr[], int size){ |
这样,我们才算编写完了一个优化版的冒泡排序。
排序的稳定性
当然,最后我们还需要介绍一个额外的概念:排序的稳定性,那么什么是稳定性呢?如果说大小相同的两个元素在排序之前和排序之后的先后顺序不变,这个排序算法就是稳定的。我们刚刚介绍的冒泡排序只会在前者大于后者的情况下才会进行交换,所以说不会影响到原本相等的两个元素顺序,因此冒泡排序是稳定的排序算法。
插入排序
原理
类似于扑克牌一样 ,后面小的一直往前插入,直到前面的数比自己小。
而已经插入完毕的数不再考虑。
设数组长度为N,详细过程为:
- 共进行N轮排序。
- 每轮排序会从后面依次选择一个元素,与前面已经处于有序的元素,从后往前进行比较,直到遇到一个不大于当前元素的的元素,将当前元素插入到此元素的前面。
- 插入元素后,后续元素则全部后移一位。
- 当后面的所有元素全部遍历完成,全部插入到对应的位置之后,排序完成。
比如下面的数组:

此时我们默认第一个元素已经是处于有序状态,我们从第二个元素开始看:

将其取出,从后往前,与前面的有序序列依次进行比较,首先比较的是4,发现比4小,继续向前,发现已经到头了,所以说直接放到最前面即可,注意在放到最前面之前,先将后续元素后移,腾出空间:

接着插入即可:

目前前面两个元素都是有序的状态了,我们继续来看第三个元素:

依然是从后往前看,我们发现上来就遇到了7小的4,所以说直接放到这个位置:

现在前面三个元素都是有序状态了,同样的,我们继续来看第四个元素:

依次向前比较,发现到头了都没找到比1还小的元素,所以说将前面三个元素全部后移:

将1插入到对应的位置上去:

代码
现在前四个元素都是有序的状态了,我们只需要按照同样的方式完成后续元素的遍历,最后得到的就是有序的数组了,我们来尝试编写一下代码:
1 | void insertSort(int arr[], int size){ |
当然,这个代码也是可以改进的,因为我们在寻找插入位置上逐个比较,花费了太多的时间,因为前面一部分元素已经是有序状态了,我们可以考虑使用二分搜索算法来查找对应的插入位置,这样就可以节省查找插入点的时间了:
1 | int binarySearch(int arr[], int left, int right, int target){ |
我们最后还是来讨论一下,插入排序算法的稳定性。那么没有经过优化的插入排序,实际上是不断向前寻找到一个不大于待插入元素的元素,所以说遇到相等的元素时只会插入到其后面,并没有更改相同元素原本的顺序,所以说插入排序也是稳定的排序算法(不过后面使用了二分搜索优化之后就不稳定了,比如有序数组中连续两个相等的元素,现在又来了一个相等的元素,此时中间的正好找到的是排在最前面的相等元素,返回其后一个位置,新插入的元素会将原本排在第二个的相等元素挤到后面去了)
选择排序
原理
每次都去后面找一个最小的放到前面即可
设数组长度为N,详细过程为:
- 共进行N轮排序。
- 每轮排序会从后面的所有元素中寻找一个最小的元素出来,然后与已经排序好的下一个位置进行交换。
- 进行N轮交换之后,得到有序数组
比如下面的数组:

第一次排序需要从整个数组中寻找一个最小的元素,并将其与第一个元素进行交换:

交换之后,第一个元素已经是有序状态了,我们继续从剩下的元素中寻找一个最小的:

此时2正好在第二个位置,假装交换一下,这样前面两个元素都已经是有序的状态了,我们接着来看剩余的:

此时发现3是最小的,所以说直接将其交换到第三个元素位置上:

代码
这样,前三个元素都是有序的了,通过不断这样交换,最后我们得到的数组就是一个有序的了,我们来尝试编写一下代码:
1 | void selectSort(int arr[], int size){ |
当然,对于选择排序,我们也可以进行优化,因为每次都需要选一个最小的出来,我们不妨再顺手选个最大的出来,小的往左边丢,大的往右边丢,这样就能够有双倍的效率完成了。
1 | void swap(int * a, int * b){ |
最后我们来分析一下选择排序的稳定性,首先选择排序是每次选择最小的那一个,在向前插入时,会直接进行交换操作,比如原序列为 3,3,1,此时选择出1是最小的元素,与最前面的3进行交换,交换之后,原本排在第一个的3跑到最后去了,破坏了原有的顺序,所以说选择排序是不稳定的排序算法。
总结
- 冒泡排序(优化版):
- 最好情况时间复杂度: O(n)O(n),如果本身就是有序的,那么我们只需要一次遍历,当标记检测到没有发生交换,直接就结束了,所以说一遍就搞定。
- 最坏情况时间复杂度: O(n2)O(n2),也就是硬生生把每一轮都吃满了,比如完全倒序的数组就会这样。
- 空间复杂度: 因为只需要一个变量来暂存一下需要交换的变量,所以说空间复杂度为 O(1)O(1)
- 稳定性: 稳定
- 插入排序:
- 最好情况时间复杂度: O(n)O(n),如果本身就是有序的,因为插入的位置也是同样的位置,当数组本身就是有序的情况下时,每一轮我们不需要变动任何其他元素。
- 最坏情况时间复杂度: O(n2)O(n2),比如完全倒序的数组就会这样,每一轮都得完完整整找到最前面插入。
- 空间复杂度:同样只需一个变量来存一下抽出来的元素,所以说空间复杂度为 O(1)O(1)
- 稳定性: 稳定
- 选择排序:
- 最好情况时间复杂度: O(n2)O(n2),即使数组本身就是有序的,每一轮还是得将剩余部分挨个找完之后才能确定最小的元素,所以说依然需要平方阶。
- 最坏情况时间复杂度: O(n2)O(n2),不用多说了吧。
- 空间复杂度:每一轮只需要记录最小的元素位置即可,所以说空间复杂度为 O(1)O(1)
- 稳定性: 不稳定
表格如下,建议记住:
| 排序算法 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | $O(n)$ | $O(n^2)$ | $O(1)$ | 稳定 |
| 插入排序 | $O(n)$ | $O(n^2)$ | $O(1)$ | 稳定 |
| 选择排序 | $O(n2)$ | $O(n^2)$ | $O(1)$ | 不稳定 |
说些什么吧!