1.如何分析一个排序算法?
排序算法的执行效率
- 最好情况、最坏情况、平均情况时间复杂度
- 时间复杂度的系数、常数 、低阶
- 比较次数和交换(或移动)次数
排序算法的内存消耗
也就是空间复杂度,特别的,空间复杂度为O(1)的排序算法也叫原地排序算法
排序算法的稳定性
如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
2.冒泡排序
算法代码
public static void sort(int[] array, int n){
for (int i = n - 1;i > 0;i--) {
boolean isChange = false;
for (int j = n - 1; j > n - i - 1; j--) {
if (array[j] < array[j - 1]) {
int tmp = array[j - 1];
array[j - 1] = array[j];
array[j] = tmp;
isChange = true;
}
}
if (!isChange) break;
}
}
分析:
冒泡排序是原地排序算法,只用了常量级的空间
冒泡排序是稳定的排序算法
时间复杂度:最好O(n) 最坏O(n2) 平均怎么算?
有序度和逆序度
概念:
有序度
有序元素对:a[i] <= a[j], 如果i < j
逆序度
逆序元素对:a[i] > a[j], 如果i < j
满有序度为n*(n-1)/2
逆序度 = 满有序度 - 有序度
由于每交换一次,有序度就加1,交换次数就和逆序度相对应
最坏情况下,初始有序度为0,要进行n*(n-1)/2次交换;最坏情况下,初始有序度为n*(n-1)/2,不需要交换,我们取个中间值n*(n-1)/4
也就是说,平均情况下,需要n*(n-1)/4次交换操作,比较操作肯定更多,而复杂度上限为O(n2),故平均时间复杂度就是O(n2)
3.插入排序
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
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; // 插入数据
}
}
分析:
插入排序是原地算法
插入排序是稳定算法
时间复杂度分析 最好O(n) 最坏O(n2)
平均时间复杂度 数组中插入一个数据的平均时间复杂度为O(n),对插入排序来说,每次插入都相当于在数组中插入一个数据,执行n次操作,故平均时间复杂度为O(n2)
4.选择排序
public static void selectSort(int[] array, int n) {
for (int i = 0; i < n - 1; i++) {
int min = i; //min记录最小值的下标
//从i+1开始,搜索最小值下标
for (int j = i + 1; j < n; j++)
if (array[j] < array[min]) min = j;
//交换
if (i != min){
int tmp = array[i];
array[i] = array[min];
array[min] = tmp;
}
}
}
选择排序是原地排序算法
选择排序是不稳定算法
最好时间复杂度O(n) 最坏时间复杂度O(n2) 平均时间复杂度O(n2)
5.为什么插入比冒泡更受欢迎?
冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个
我们把执行一个赋值语句的时间粗略地计为单位时间(unit_time),然后分别用冒泡排序和插入排序对同一个逆序度是 K 的数组进行排序。用冒泡排序,需要 K 次交换操作,每次需要 3 个赋值语句,所以交换操作总耗时就是 3*K 单位时间。而插入排序中数据移动操作只需要 K 个单位时间。
冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
插入排序中数据的移动操作:
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
参考课程:极客时间王争老师的《数据结构与算法之美》