title: 11排序(上)
date: 2019/11/28
tags: 数据结构与算法
categories:
- 算法
为什么插入排序比冒泡排序更受欢迎?
常用的排序:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。
思考
插入排序和冒泡排序的时间复杂度相同,都是O(n*n),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?
如何分析一个“排序算法”?
- 最好情况、最坏情况、平均情况下的时间复杂度
- 对于要排序的数据,好的接近有序,有的完全无序。有序度不同数据,对于排序的执行时间有影响。
- 时间复杂度的系数、常数、低阶
- 时间复杂度反应的是数据规模n很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。
- 但是实际的软件开发中,我们的排序可能是10个、100个、1000个这样规模很小的数据,所以在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
- 比较次数和交换(或移动)次数
排序算法的内存消耗
- 算法的复杂度可以通过空间复杂度来衡量,排序算法也不例外。
- 这里还引入一个新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是O(1)的排序算法。
排序算法的稳定性
- 如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
冒泡排序
- 冒泡排序只会操作相邻的两个数据。
- 每次冒泡操作都会对相邻两个元素进行比较,看是否满足大小关系的要求。如果不能满足就让他两互换。
- 一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
冒泡排序实现
//冒泡排序,a表示数组,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; //没有数据交换,提前退出
}
}
冒泡排序分析
- 冒泡排序是原地排序算法吗?
- 冒泡的过程只涉及相邻数据的交换操作,只需要常量级别的临时空间,空间复杂度是O(1),是一个原地排序算法。
- 冒泡排序是稳定的算法吗?
- 为了保证排序算法的稳定性,当有相邻两个元素大小相等时,不做交换,大小相同的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。
- 冒泡排序的时间复杂度
- 最好情况,需要排序的数据已经是有序的,只需要一次冒泡操作,时间复杂度是O(n)
- 最坏情况,需要排序的数据是倒序排列的,需要n次冒泡,O(n*n)
- 平均时间复杂度,用概率论方法分析会很复杂,可以通过“有序度”和“逆序度”这两个概念来进行分析。
- 有序度是数组中具有有序关系的元素对的个数,有序元素对:a[i]<=a[j],如果i < j。
- 完全有序的数组的有序度,n*(n-1)/2,叫做满有序度。
- 逆序度=满有序度-有序度
- 根据交换的次数、有序度算出平均时间复杂度O(n*n)
- 这种方法不严格,但实用。
插入排序
- 首先,将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。
- 插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。
- 重复这个过程,直到未排序区间中元素为空,算法结束。
插入排序实现
- 插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。
- 当我们需要将一个数据a插入到已排序区间时,需要拿a与已排序区间的元素依次比较大小,找到合适的插入位置。
- 找到插入点后,我们还需要将插入到之后的元素往后移动一位,这样才能腾出位置给元素a插入。
- 对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。
//插入排序,a表示数组,n表示数组大小
public voif insertionSort(int[] a,int n){
if(n<=1) return;
for(int i=1;i<n;++i){
int value=a[i];
int j=i-1;
//查找插入的位置
fot(;j>=0;--j){
if(a[j]>value){
a[j+1]=a[j];//数据移动
}else{
break;
}
}
a[j+1]=value;//插入数据
}
}
插入排序分析
- 插入排序是原地排序算法吗?
- 插入排序算法运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法。
- 插入排序是稳定的排序算法吗?
- 可以在插入的过程中,对于值相同的元素,选择将后面出现的芫荽,插入到前面出现的元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。
- 插入排序的时间复杂度?
- 如果要排序的数据已经是有序的,我们并不需要搬移任何数据。
- 如果我们从尾到头在有序数组里面查找插入的位置,每次只需要比较一个数据就能确定插入的位置。这种情况下,最好的时间复杂度为O(n)。
- 如果数组是倒序的,每次插入第一相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为O(n*n)
- 在数组中插入一个数据的平均时间复杂度是O(n),所以对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环n次插入操作,所以平均复杂度为O(n*n)
选择排序
- 选择排序算法的实现思路有点类似插入排序,分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放在已排序区间的末尾。
选择排序分析
- 原地排序算法吗?
- 选择排序的空间复杂度为O(1),是一种原地排序算法。
- 选择排序是稳定的吗?
- 不稳定
- 选择排序的时间复杂度?
- 最好最坏和平均时间复杂度都是O(n*n)
为什么插入排序比冒泡排序更受欢迎?
- 从代码实现上看,冒泡排序的数据交换比插入排序的数据移动要复杂,冒泡排序需要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;
}
- 所以虽然冒泡排序和插入排序在时间复杂度上是一样的,都是O(n*n),但是如果我们希望把性能优化做到极致,肯定首选插入排序。
- 插入排序的算法思路也有很大的优化空间,我们只讲了最基础的一种,感兴趣的话可以自行学习一下希尔排序。
希尔排序
- 希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
课后思考
- 特定的算法是依靠特定的数据结构的,上述几种排序算法都是基于数组实现的。如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
- 应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置。
- 冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。
- 综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。