冒泡排序、插入排序和选择排序的算法思想可以通过引入“已排序区”和“未排序区”概念来分解逻辑,这三种排序算法从这两个分区的求解思路可以让人更清晰的辨识它们各自的命名缘由,以及不同点。
冒泡排序的算法求解思路是每次从未排序区中以相邻元素按照比较关系两两间可能交换位置,每次遍历下来能够找出该区间最小(求降序)或者最大(求升序)的元素,并放置到已排序区有序列入口端。
比如要排序如下数值(假定为升序排序):6,5,8,1
第一次冒泡遍历前,所有元素都在未排序区,已排序区目前为空,被放入已排序区的元素在后续的冒泡遍历过程中不需要拿它作比较。
未排序区 | 已排序区 |
6,5,8,1 |
第一次遍历过程:
相邻元素检查过程 | 检查调整后数值排列 |
6比5大,需要交换位置 | 5,6,8,1 |
8比6大,不需要交换位置 | 5,6,8,1 |
8比1大,需要交换位置 | 5,6,1,8 |
一次遍历后,找出当前未排序区内最大值8,放入已排序区:
未排序区 | 已排序区 |
5,6,1 | 8 |
第二次遍历过程:
相邻元素检查过程 | 检查调整后数值排列 |
5比6小,不需要交换位置 | 5,6,1,8 |
6比1大,需要交换位置 | 5,1,6,8 |
注:因为8在已排序区,所以这次遍历不用拿它作相邻元素检查。
二次遍历后,找出当前未排序区内最大值6,放入已排序区:
未排序区 | 已排序区 |
5,1 | 6,8 |
第三次遍历过程:
相邻元素检查过程 | 检查调整后数值排列 |
5比1大,需要交换位置 | 1,5,6,8 |
注:因为6、8在已排序区,所以这次遍历不用拿它们作相邻元素检查。
三次遍历后,找出当前未排序区内最大值5,放入已排序区:
未排序区 | 已排序区 |
1 | 5,6,8 |
因未排序区仅剩一个元素,该值毋庸置疑就是本次排序的最小值,所以可直接编排进已排序区,至此,排序过程结束:
未排序区 | 已排序区 |
1,5,6,8 |
选择排序的算法求解思路是每次遍历从未排序区找到最大(升序)或最小(降序)的元素,然后放置到已排序区有序列入口端。从求解思路来看,好像和冒泡排序差不多。
比如要排序如下数值(假定为升序排序):6,5,8,1
第一次遍历前,所有元素都在未排序区,已排序区目前为空。
未排序区 | 已排序区 |
6,5,8,1 |
第一次遍历过程:
查找未排序区最大值过程 | 查找调整后数值列 |
遍历发现8最大,和未排序区第一个元素6交换位置 | 8,5,6,1 |
一次遍历后,找出当前未排序区内最大值8,放入已排序区:
未排序区 | 已排序区 |
5,6,1 | 8 |
第二次遍历过程:
查找未排序区最大值过程 | 查找调整后数值列 |
遍历未排序区元素,发现6最大,和未排序区第一个元素5交换位置 | 8,6,5,1 |
二次遍历后,找出当前未排序区内最大值6,放入已排序区:
未排序区 | 已排序区 |
5,1 | 6,8 |
第三次遍历过程:
查找未排序区最大值过程 | 查找调整后数值列 |
遍历未排序区元素,发现5最大,和未排序区第一个元素(还是自己,等于没动)交换位置 | 8,6,5,1 |
三次遍历后,找出当前未排序区内最大值5,放入已排序区:
未排序区 | 已排序区 |
1 | 5,6,8 |
因未排序区仅剩一个元素,该值毋庸置疑就是本次排序的最大值,所以可直接编排进已排序区,至此,排序过程结束:
未排序区 | 已排序区 |
1,5,6,8 |
插入排序的算法求解思路是每次取未排序区的一个元素去已排序区查找一个合适的位置插入。
比如要排序如下数值(假定为升序排序):6,5,8,1
第一次遍历前,所有元素都在未排序区,已排序区目前为空。
未排序区 | 已排序区 |
6,5,8,1 |
第一次遍历过程:
取一个元素到已排序区查找合适的位置插入过程 | 插入调整后数值列 |
取未排序区的第一个元素6,去已排序区查找合适的位置,因为目前已排序区未空,所以直接插入 | 6,5,8,1 |
一次遍历后,6找到了合适的位置,放入已排序区:
未排序区 | 已排序区 |
5,8,1 | 6 |
第二次遍历过程:
取一个元素到已排序区查找合适的位置插入过程 | 插入调整后数值列 |
取未排序区的第一个元素5,去已排序区查找合适的位置,遍历发现5小于6,5目前的位置设置成值6,最后,6的位置设置成5 | 5,6,8,1 |
二次遍历后,5找到了合适位置,放入已排序区:
未排序区 | 已排序区 |
8,1 | 5,6 |
第三次遍历过程:
取一个元素到已排序区查找合适的位置插入过程 | 插入调整后数值列 |
取未排序区的第一个元素8,去已排序区查找合适的位置,遍历发现8大于6,直接插到尾端,实际是不用变化 | 5,6,8,1 |
三次遍历后,8找到了合适的位置,放入已排序区:
未排序区 | 已排序区 |
1 | 5,6,8 |
第四次遍历过程:
取一个元素到已排序区查找合适的位置插入过程 | 插入调整后数值列 |
取未排序区的第一个元素1,去已排序区查找合适的位置,遍历发现1小于8,1目前的位置设置成8,继续遍历发现1小于6,8之前的位置设置成6,再继续遍历发现1也小于5,6之前的位置设置成5,最后把5之前的位置设置成1,查找合适位置插入过程结束 | 1,5,6,8 |
四次遍历后,1找到了合适的位置,放入已排序区:
未排序区 | 已排序区 |
1,5,6,8 |
未排序区为空,至此,排序过程结束。
插入排序的遍历查找操作的消耗都集中在已排序区(算法核心就是查找合适的序列插入点,所以叫做插入排序),冒泡排序和选择排序都是集中在在未排序区。
至于冒泡排序和选择排序的区别则是冒泡排序在未排序区遍历时相邻元素会进行比较交换,最后间接找出最大或最小元素放入已排序区;选择排序是在未排序区遍历时相邻元素仅比较大小不交换位置,直接找出最大或最小元素放入到已排序区。一个是间接,一个是直接,那这有什么具体区别呢?
冒泡排序每两两相邻元素比较时,在比较关系合适的情况下会交换位置,在一定程度上,也在让未排序区的数列趋于有序(这就好像冒水泡一样,冒泡排序名字由来),当某次遍历未排序区元素后发现没有元素位置发生交换,那么就可以判定未排序区数列已经是有序的,也就可以提前退出排序过程了。假设要排序的数列本身就是有序(假设是升序的)的,然后通过冒泡排序也是求升序排序,第一次遍历后发现未排序区元素都没有发生位置交换,那么说明已经是有序的,就可以直接结束排序,而本次排序使用的时间复杂度就是在O(n)下完成了。
而选择排序每两两相邻元素比较时只通过比较关系查找未排序区里最大或者最小的元素(单纯选择最大最小,所以叫选择排序),那么就无法判定未排序区的数列是否有序,只有把所有未排序区的元素移动到已排序区后才能判定排序结束,也就是说,这个算法的最好情况时间复杂度和最坏情况时间复杂度都是O(n^2)。
插入排序如果未排序区的数列是有序的,且刚好是想要的排序向,因为如果是有序的,那么直接从未排序区一个一个尾插到已有序区即可,省去了遍历合适位置的时间,那么最好情况的时间复杂度也可以达到O(n)。
下面总结一下他们的区别点:
最好情况时间复杂度 | 最坏情况时间复杂度 | 平均情况时间复杂度 | 空间复杂度 | 是否为稳定排序 | |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不是 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
什么是稳定排序?
数列内值相同的元素不会因为排序的流程颠倒它们在数列内的前后顺序,那么就说明这个排序是稳定的,反之就是不稳定的。
为什么推荐使用稳定排序比不稳定排序更受欢迎?
因为值一样的元素排序前后在数列内的前后顺序发生变化的话,在大部分业务里是不希望发生的,比如排行榜信息,玩家AB的分数一样,第一次拉取排行榜单信息A排在前,B排在后,结果二次拉取时,其他玩家分数发生变化,因为使用的是不稳定排序,导致AB的分数在数列里经过排序后可能B排在前,A排在后,这样会让玩家看起来好怪。
选择排序为什么是不稳定的排序?
假设要排序的数列为:9,7,9,11,5
第一次从未排序区查找最大的值为11,于是和区内第一个元素交换位置,数列变成:11,7,9,9,5
之前第一个9就变到了第二个9后面了,相同值在排序前后在数列里的前后顺序发生了变化,所以是不稳定排序。
这三种排序算法因为都是原地排序(也就是不用额外开辟内存空间),所以空间复杂度都是O(1) 。
在选择排序算法时,我们主要关注相关排序算法的这三个要素:时间复杂度(时间消耗)、空间复杂度(内存消耗)、是否为稳定排序。
冒泡排序算法C语言版:
void boubleSort(int arr[], unsigned int size){
int i = 0;
int j, tmp, swapTag;
if(size < 2){
return;
}
for(; i < size; i++){
swapTag = 0;
for(j = 0; j < size - i - 1; j++){
if(arr[j] > arr[j + 1]){
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
swapTag = 1;
}
}
if(swapTag == 0){
break;
}
}
}
选择排序算法C语言版:
void selectionSort(int arr[], unsigned int size){
int i = 0;
int j, tmp, tmp1;
if(size < 2){
return;
}
for(; i < size; i++){
tmp = i;
for(j = i + 1; j < size; j++){
if(arr[j] > arr[tmp]){
tmp = j;
}
}
tmp1 = arr[i];
arr[i] = arr[tmp];
arr[tmp] = tmp1;
}
}
插入排序算法C语言版:
void insertionSort(int arr[], unsigned int size){
int i = 1;
int j,tmp;
if(size < 2){
return;
}
for(; i < size; i++){
tmp = arr[i];
for(j = i - 1; j >= 0; j--){
if(arr[j] < tmp){
arr[j + 1] = arr[j];
}
else{
break;
}
}
arr[j + 1] = tmp;
}
}