一、概述
最经典、最常用的排序算法有:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、按照时间复杂度分为三类。如下图所示
二、如何分析一个排序算法
(1)排序算法的执行效率
1、最好情况、最坏情况、平均情况时间复杂度
2、时间复杂度的系数、常数、底阶
3、比较次数和交换(或移动)次数
(2)排序算法的内存消耗
算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。针对排序算法的空间复杂度,引入了新概念,原地排序。
原地排序算法,就是特指空间复杂度为O(1)的排序算法。
(3)排序算法的稳定性
针对排序算法,还有一个重要的衡量指标:稳定性。所谓稳定性就是说如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。经过排序算法之后,如果值相等的前后顺序没有改变,那我们把这种排序算法叫做稳定的排序算法;
三、冒泡排序
冒泡排序的英文Bubble Sort,是一种最基础的交换排序。冒泡排序的思想是,相邻的元素两两比较,根据大小来交换元素的位置。
原始的冒泡排序是稳当排序,冒泡排序在每一轮只把一个元素冒泡到数列的一端,也就是有序区。由于该排序算法的每一轮都要遍历所有元素。轮转的次数和元素数量相当,所以时间复杂度是O(N²)。
public class BubbleSort {
//从小到大排序
private static void sort(int array[]) {
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}
}
}
public static void main(String[] args) {
int [] array=new int[]{5,8,6,3,9,2,1,7};
sort(array);
System.out.println(Arrays.toString(array));
}
}
①使用双重循环来进行排序。外部循环控制所有的回合,内部循环代表每一轮的冒泡处理。先进行元素比较,再进行元素交换。
四、冒泡排序总结
1、第一,冒泡排序是原地排序算法吗?
只涉及相邻数据的交换操作,只需要常量级的临时空间,所以空间复杂度为O(1),是一个原地排序算法。
2、第二,冒泡排序是稳定的排序算法吗?
在冒泡排序中,只有交换才可以改变两个元素的先后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,不做交换。
3、第三,冒泡排序的时间复杂度是多少?
最好情况下,要排序的数据已经是有序的了,我们只需要进行1次冒泡操作,就可以结束了,所以最好情况时间复杂度是O(n)。而最坏情况是,要排序的数据是倒序排列的,我们需要进行n次冒泡操作,所以最坏情况时间复杂度是O(n²)。那么平均时间复杂度是多少呢?平均时间复杂度就是加权平均期望时间复杂度。
如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算很复杂。所以通过“有序度”和“逆序度”来进行分析。
有序度:数组中具有有序关系的元素对的个数。有序元素对用数学表达式就是这样:
有序元素对:a[i] <= a[j], 如果 i < j。
同理,对于一个倒序排序的数组,比如6,5,4,3,2,1,有序度是0;如果对于一个完全有序的数组,比如1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15。我们把这种完全有序的数组的有序度叫作满有序度。
逆序度的定义正好和有序度相反(默认从小到大为有序)
逆序度=满有序度-有序度。我们排序的过程就是一种增加有序度,减少逆序度,最终达到满有序度的过程。
要排序的数组初始状态是4,5,6,3,2,1。其中,有序元素对有(4,5)(4,6)(5,6)。所以有序度是3.n=6,所以满有序度为n*(n-1)/2=15。
冒泡排序包含两个原子操作:比较和交换。每交换一次,有序度就加1。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2-初始有序度。此例中就是15-3=12.要进行12次交换操作。
五、插入排序
插入排序的英文是Insertion Sort。首先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。示意图如下
插入排序如何借助上面的思想来实现排序的呢?
首先,我们将数组中的数据分为两个区间,有序区和无序区。初始有序区只有1个元素,就是数组的第一个元素。插入算法的核心思想是取无序区中的元素,在有序区中找到合适的插入位置将其插入,并保证有序区数据一直有序。重复这个过程,直到无序区中元素为空,算法结束。
如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为有序区,右侧为无序区。
插入排序代码如下:
public class InsertionSort {
//插入排序,a表示数组,n表示数组大小
public static void insertionSort(int[] a) {
int n = a.length;
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;//插入数据
}
}
public static void main(String[] args) {
int[] array = new int[]{5, 8, 6, 3, 9, 2, 1, 7};
//sort(array);
insertionSort(array);
System.out.println(Arrays.toString(array));
}
}
六、选择排序
选择排序的英文是Selection Sort
七、为什么插入排序比冒泡排序更受欢迎
冒泡排序和插入排序的时间复杂度都是O(n²),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?
上文分析冒泡排序和插入排序的时候讲到,冒泡排序不管怎么优化,元素交换的次数是一个固定值。是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。
但是,从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个。我们来看这段操作。
冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
}
插入排序中数据的移动操作:
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}