对常见的排序记录一下,后面忘记了再回头看,代码是用Java写的 项目地址
最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
冒泡排序 | 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) | 稳定 |
希尔排序 | O(n) | O(n^2) | O(n^ 3/2) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
注:希尔排序的时间复杂度没有定论,上面的写的时间复杂度具有争议。
冒泡排序和快速排序属于交换排序,选择排序和堆排序属于选择排序,插入排序和希尔排序属于插入排序。三个O(n^2) 级别的排序中插入排序是最优的,插入排序是稳定的排序,而且数据有序度较高时用时会比较短。三个O(nlogn)级别的排序中快速排序是最快的,用的比较多,O(logn)的空间复杂度主要是递归创建变量的开销基本可以忽略,可以看作是原地排序,快排通过选合适的基准值几乎可以避免时间复杂度到最坏的情况。希尔排序作为普通排序打破了O(n^2) 的时间复杂度,会比O(n^2) 好很多,但是数据量很大的情况还是使用高级排序。
下面排序默认按照从小到大排序,data为int数组
// 交换数组中两个数
swap (int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
冒泡排序
public void sort(int[] data) {
sort(data, data.length);
}
private void sort(int[] data, int n) {
for (int i = 1; i < n; i++) {
for (int j = 0; j < n - i; j++) {
if (data[j] > data[j + 1]) {
swap(data, j, j + 1);
}
}
}
}
原理:内层循环每次比较相邻的两个数大小,大的向后移动,内层循环完后最后一个数便是最大的数了。外层循环控制次数,总共进行n-1趟,则全部有序了。内层循环每次j<n-i,因为每次比较完后数组中最后一个已经是最大的了,不需要再比较了。
选择排序
public void sort(int[] data) {
sort(data, data.length);
}
private void sort(int[] data, int n) {
for (int i = 0; i < n - 1; i++) {
int k = i;
for (int j = k + 1; j < n; j++) {
if (data[j] < data[k]) {
k = j;
}
}
if (i != k) {
swap(data, i, k);
}
}
}
原理:内层循环寻找最小值然后记录找到最小值的下标,在内层循环结束后交换使得最小值放到正确的位置。外层循环控制次数,总共进行n-1趟,则全部有序了。内存循环每次j=i+1,因为每次比较完后数组中第一个数已经是最小的了,不需要再比较了。
插入排序
public void sort(int[] data) {
sort(data, data.length);
}
private void sort(int[] data, int n) {
for (int rightShift = 1; rightShift < n; rightShift++) {
int compareData = data[rightShift];
int leftShift = rightShift - 1;
while (leftShift >= 0 && data[leftShift] > compareData) {
data[leftShift + 1] = data[leftShift];
leftShift--;
}
data[leftShift + 1] = compareData;
}
}
原理:插入排序看成是将原来数组分成两个数组,前面一个是一个有序的数组,后面是无序的,然后将后面无序中每次取出一个插入到前面有序数组中合适的位置,循环n-1次后面数组中数据都被取出插入到前面有序数组中了,则整个数组有序了。
过程:上面代码默认数组中第一个数为有序的,然后取数组中第2个数(compareData)向前和第1个数(data[leftShift])比较,如果第2个数比第1个数小就将第1个数移到第2个数所在的位置,此时数组中第1个数和第2个数则都是第1个数的值,leftShift减1后为-1小于0内层循环结束,然后执行data[leftShift + 1] = compareData将数组第2个数赋值给第1个数,此时数组中前两个数有序了。rightShift加1,compareData取第3个数向前和前面已经有序的数组比较,如果leftShift没到最左边且compareData比前一个小则将前一个数向后一位赋值(最开始比较的则赋值到compareData所在位置),然后继续向前比较,符合上面条件则再向后赋值,只要有一个数碰到一个数比compareData小则内层循环终止(因为前面都是有序的,只要最后一个数比compareData小则前面的数比compareData都要小),然后执行data[leftShift + 1] = compareData,将compareData赋值到最后比较数的那个位置,此时前面有序数组中又插入一个数,一直循环结束则有序了。
特点:1、插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。因为有序时内层循环data[leftShift] 一直小于 compareData,所以只有外层循环的遍历时间,时间复杂度为O(n)。
2、当数据较少时,插入排序速度比较快
3、插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序
public void sort(int[] data) {
sort(data, data.length);
}
private void sort(int[] data, int n) {
for (int step = n / 2; step > 0; step /= 2) {
for (int rightShift = step; rightShift < n; rightShift++) {
int compareData = data[rightShift];
int leftShift = rightShift;
while (leftShift - step >= 0 && data[leftShift - step] > compareData) {
data[leftShift] = data[leftShift - step];
leftShift = leftShift - step;
}
data[leftShift] = compareData;
}
}
}
原理:通过将原数组分解为多个较小的子数组,每个子数组使用插入排序进行排序。接着依次缩小增量继续进行排序,待整个序列基本有序时,再对全体元素进行插入排序。
希尔排序又称缩小增量排序,主要是针对插入排序上面三点特点对插入排序的优化。主要是将较大的数据分为多个小数组进行插入排序,而且几次下来数据也基本有序,最后一次进行全量插入排序就非常快了。
过程:以一组数据大体看上面代码步骤,数组[5, 3, 8, 4, 2, 6, 3, 1],8个数据,步长step=n/2=4,外层循环第一遍过程可以看作通过步长4将原数据分为4个小数组[5,2],[3,6],[8,3],[4,1],每个数组进行插入排序后为[2,5],[3,6],[3,8],[1,4],即整个数组变为[2, 3, 3, 1, 5, 6, 8, 4]。然后步长除以2,外层循环执行第二遍,将数据分为2个小数组[2,3,5,8],[3,1,6,4],这两个数组再进行插入排序后为[2,3,5,8],[1,3,4,6],即整个数组变为[2, 1, 3, 3, 5, 4, 8, 6]。然后步长除以2,此时步长为1了,外层循环执行最后一遍,此时数据不用分了也可以说分为1个数组了,此时就是上面的插入排序了,执行完后数据有序[1, 2, 3, 3, 4, 5, 6, 8]。