本文通过围绕三个问题来比较冒泡、插入和选择排序这三个排序算法,加深对这三个排序算法的理解。
时间复杂度如何(执行效率)?
是否是原地排序算法(内存消耗)?排序是否需要申请额外的内存空间。
是否是稳定的排序算法(稳定性)?是否会改变相同值元素的前后位置。
public int[] bubbleSort(int[] a, int n) {
if(n <= 1) return a;
for (int i = 0; i < n - 1; i++) {
boolean flag = false;//提前退出标志位
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {//进行交换
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;//有数据交换则置标志位为true
}
}
if (!flag) break;
}
return a;
}
冒泡排序的时间复杂度如何?
最好情况:最好的情况下,数组有序,此时只需进行一次冒泡且无需数据交换则可排序完成,时间复杂度为O(n)(进行了n-1次比较)。
最坏情况:最坏情况下,数组刚好是倒序的,此时需要进行n-1次冒泡,时间复杂度为O(n²)。
平均情况:平均情况的时间复杂度需要用到有序度和概率论的分析方法,我就不展开来讲,平均时间复杂度也为O(n²)。
是否是原地排序算法?
因为冒泡排序是在自身数组上进行排序,无需申请额外的内存空间,所以空间复杂度为O(1),是原地排序算法。
是否是稳定的排序算法?
我们可以发现,冒泡排序交换元素的前提是大小不同,所以相同元素的前后顺序不会改变,所以是稳定的排序算法。
public int[] insertionSort(int[] a, int n) {
if (n <= 1) return a;
for (int i = 1; i < n; i++) {
int value = a[i];//需要插入的值
int j = i - 1;
for (; j >= 0; j--) {//查找插入的位置
if (a[j] > value) {
a[j + 1] = a[j];//数据移动
} else {
break;
}
}
a[j + 1] = value;//插入
}
return a;
}
插入排序的时间复杂度如何?
最好情况:最好的情况下,数组有序,此时每次只需比较一次即可跳出查找插入的循环,换言之只需遍历一次数组即可,时间复杂度为O(n)。
最坏情况:最坏情况下,数组刚好是倒序的,此时每次都需将数组移动到数组开头,时间复杂度为O(n²)。
平均情况:插入排序的平均复杂度可以理解为每一次循环都往数组中插入一个元素,向数组中插入一个元素的平均复杂度为O(n),所以插入排序的平均时间复杂度为O(n²)。
是否是原地排序算法?
因为插入排序也是在自身数组上进行排序,无需申请额外的内存空间,所以空间复杂度为O(1),是原地排序算法。
是否是稳定的排序算法?
我们运用插入排序时,可以选择将后面出现的元素插入到前面出现的元素的后面,这样就可以保持前后不变,所以插入排序也为稳定的排序算法。
public int[] selectionSort(int[] a, int n) {
for (int i = 0; i < a.length - 1; i++) {
int min = i;
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[min]) {//找出未排序部分的最小值的坐标
min = j;
}
}
if (min != i) {
//交换
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
return a;
}
选择排序的时间复杂度如何?
最好、最坏和平均情况:选择排序无论何种情况每个大循环内都需要比较未排序部分找出最小元素的坐标,所以时间复杂度都为O(n²)。
是否是原地排序算法?
因为选择排序也是在自身数组上进行排序,无需申请额外的内存空间,所以空间复杂度为O(1),是原地排序算法。
是否是稳定的排序算法?
我们可以举个例子,一个数组:3,4,3,2,1。当找到1的时候,需要与第一个3交换,此时两个3的前后顺序翻转,所以选择排序不是稳定的排序算法。
原地排序? | 稳定排序? | 最好 最坏 平均 | |
冒泡排序 | √ | √ | O(n) O(n²) O(n²) |
插入排序 | √ | √ | O(n) O(n²) O(n²) |
选择排序 | √ | × | O(n²) O(n²) O(n²) |
总结这三种排序,你会发现,冒泡排序和插入排序稳定性和时间复杂度要优于选择排序,那么应该选择冒泡排序还是插入排序呢?
实际上,插入排序要优于冒泡排序,可以比较两者数据交换和数据移动的部分:
//冒泡排序
if(a[j] > a[j+1]){//数据交换
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = true;
}
//插入排序
if (a[j] > value) {
a[j + 1] = a[j];//数据移动
}
从上面这两部分的代码我们可以发现,冒泡排序的数据交换部分需要三次交换操作,而插入排序需要的移动次数只需一次。
经实验也证明,在大的数据量下的排序操作时,插入排序要明显优于冒泡排序,这也是插入排序在这三种排序算法中应用最广的原因。
本博客是博主在学习王争老师的《数据结构与算法之美》时做的学习笔记,如有纰漏请在评论区指出,我们共同学习算法之美。