最经典、最常用的排序算法有:冒泡排序、插入排序、选择排序、归并排序、快速排序、计算排序、基数排序、桶排序等。按照时间复杂度,将其划分为三类:
思考题:插入排序跟冒泡排序的时间复杂度都是O(),在实际的软件开发当中,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?
1.如何评价分析一个“排序算法”?
1.1 排序算法的执行效率
对于排序算法的执行效率,一般从如下几个方面来衡量:
- 最好、最坏、平均情况时间复杂度
- 时间复杂度的系数、常数、低阶
- 比较次数和交换移动的次数
1.2 排序算法的内存消耗
算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。针对排序算法的空间复杂度,我们把空间复杂度是O(1)的排序算法叫做原地排序。
1.3 排序算法的稳定性
仅仅利用执行效率和内存消耗来判断排序算法的好坏还不够,所以还有一个重要的指标:稳定性。这个概念的意思是在待排序的序列中存在值相等的元素,经过排序之后,相等的元素之间原有的先后顺序不变。
举例说明:假如这里有个待排序序列:2,9,3,4,8,3,排序后是:2,3,3,4,8,9。这个数组里面有两个3,如果经过某种排序算法以后,两个3的前后顺序没有改变,那我们就把这种排序算法叫做稳定的排序算法;如果前后顺序发生了改变,那对应的算法就是不稳定的排序算法。
比如说,现在要给电商交易系统中的“订单”排序。订单有两个属性:下单时间、订单金额。订单数据有10万条。排序需求是:1.按照订单金额从小到大排。2.对于值相同的订单金额,希望按时间从早到晚排。
传统做法:
- 按照订单金额对订单数据从小到大排。
- 遍历排序之后的订单数据,对于每个订单金额相同的小区间,再按照下单时间来排序。
这种排序思想很简单,但是实现起来会比较复杂。
借助稳定排序算法的方法:
- 按照下单时间排序。
- 对排序以后的数据,利用稳定排序算法,按照订单金额重新排序。
稳定排序算法可以保证金额相同的两个对象,在排序之后的前后顺序不变。这样经过两次排序以后,就可以达到最初的排序需求。
2.冒泡排序
2.1 冒泡排序的原理
冒泡排序只会操作相邻的两个数据。每次冒泡排序都会对相邻的两个元素进行比较,看是否满足大小关系,如果不满足,就交换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
举个例子来展示一次冒泡排序的过程:对一组数据4,5,6,3,2,1从小到大排序。
可以看到,经过一次冒泡排序,6这个元素已经存储在正确的位置上了。想要完成所有数据的排序,只需要进行6次这样的冒泡排序即可。
当某次冒泡操作已经没有数据交换的时候,说明已经达到完全有序,不用再继续执行后面的操作。另外一个例子,6个元素只需要4次冒泡操作。
冒泡排序的代码:
// 冒泡排序,表示数组,n表示数组的大小
public void bubbleSort(int[] a, int n) {
if (n<= 1) return;
for (int i = 0;i < n ;++i){
// 提取退出冒泡排序的标志位
boolean flag = false;
for (int j = 0; j < n - i - 1;++j){
if (a[j] > a[j+1]){ // 交换
int tmp = a[j];
a[j] = a[j+1]
a[j+1] = tmp;
flag = true;//表示有数据交换
}
}
if (!flag) break;//没有数据交换
}
}
2.2 冒泡排序的评价分析
2.2.1 冒泡排序是原地排序算法吗?
冒泡排序只涉及相邻数据的交换,需要的临时空间是常数量级,所以空间复杂度是O(1),是一个原地排序算法。
2.2.2 冒泡排序是稳定的排序算法吗?
在冒泡排序中,只有交换才能改变两个元素的前后顺序。为了保证排序算法的稳定性,当两个相邻数据值相等时,我们不做交换,这样就可以保证在相同大小的数据在排序前后不会改变顺序。所以冒泡排序是稳定的排序算法。
2.2.3 冒泡排序的时间复杂度是多少?
最好情况,序列是有序的,只需要进行一次冒泡操作就结束,时间复杂度是O(n)。
最坏情况,序列是倒序的,需要进行n次冒泡排序才结束,所以时间复杂度是O()。
通过“有序度”和“逆序度”来进行平均情况时间复杂度的分析。
有序度是指数组中具有有序关系的元素对的个数。有序元素对用数学表达式表示就是:
//有序元素对:
a[i] <= a[j] //如果 i<j。
同理,对于一个倒排序列的数组,比如6,5,4,3,2,1的有序度是0的数组;比如1,2,3,4,5,6是有序度为n*(n-1)/2,也就是15的。我们把这种完全有序的数组的有序度叫做满有序度。
逆序度跟有序度恰恰相反,得到一个公式是:逆序度 = 满有序度 - 有序度。我们排序就是一种增加有序度,减少逆序度的过程,达到满有序度时,说明排序结束。
冒泡排序包含两个操作:比较和交换。每交换一次,有序度加1。不管算法怎么改进,交换的总次数是确定的,即为逆序度,也就是n*(n-1)/2 - 初始有序度。
最坏情况下,初始状态有序度是0,所以要进行n*(n-1)/2次交换;最好情况下,进行0次交换。可以取中间值n*(n-1)/4作为平均情况下的交换次数。比较次数肯定比交换次数多,而时间复杂度的上限是O(),所以平均情况下的时间复杂度是O(
)。
3.插入排序
对于一个有序的数组,往里面添加一个新数据,如何保持数据有序?只需要遍历数组,找到数据应该插入的位置将其插入即可。
上面是属于动态排序的过程,对于静态排序,可以借鉴这个思路,所以得到了插入排序算法。
3.1 插入排序的原理
将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间的元素,在已排序的区间站到合适的位置将其插入,并保证已排序的区间一直有序。重复这个过程,直到未排序区间中的元素为空,算法结束。
举个例子:要排序的数据是:4,5,6,1,2,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://插入数据
}
3.2 插入排序的评价分析
3.2.1 插入排序是原地排序算法吗?
插入排序不需要额外的存储空间,所以空间复杂度是O(1),是一个原地排序算法。
3.2.2 插入排序是稳定的排序算法吗?
对于值相同的元素,可以将后出现的插入到前面出现的值的后面,这样就保持原有的前后顺序不改变,所以插入排序也是稳定的排序算法。
3.2.3 插入排序的时间复杂度是多少?
最好情况下是O(n),最坏情况下是O(),平均情况是O(
)。
3.选择排序
选择排序的思路跟插入排序的思路类似,也是分为已排序区间和未排序区间。但是选择排序每次会在未排序区间找到最小的元素,将其放到已排序区间的末尾。
选择排序的空间复杂度是O(1),是一种原地排序算法。选择排序的最好情况、最坏情况、平均情况的时间复杂度都是:O()。
但是选择排序不是稳定的排序算法。
比如5,8,5,2,9这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素是2,与5交换位置,那第一个5与第二个5的顺序就改变了,所以就是不稳定了。
4.解答开篇的问题
是因为冒泡排序的数据交换要比插入排序的数据移动复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个。
// 冒泡排序中的数据交换操作:
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;
}