说在前面
- 以下默认从小到大为有序,基于数组
- 原地排序:是指空间复杂度为O(1)的排序算法;
- 稳定排序(排序的稳定性):是指在待排序序列中存在相等值,经过排序后,相等值之间的原有顺序不变,即是稳定排序;
- 有序度是数组中具有有序关系的元素对的个数
- 逆序度的定义正好跟有序度相反
比如1,2,3,4,5,6
有序度就是 n*(n-1)/2,也就是 15。我们把这种完全有序的数组的有序度叫作满有序度 - 逆序度 = 满有序度 - 有序度,
- 排序算法的操作就是增加有序度的操作,
- 关于排序算法的复杂度要考虑复杂度的系数、常数、低阶以及比较次数和交换(移动)次数
冒泡排序
冒泡(每次冒泡【比较交换相邻元素】至少移动一个元素到应该在的位置)
思路
- 包含两个操作原子,比较和交换,可以理解一次冒泡就是一轮比较和交换;
- 一次冒泡至少移动一个元素到应该在的位置,每次冒泡结束,下次冒泡的元素个数减1;
- 冒泡只操作相邻的两个数据,每次冒泡不停地对相邻的两个元素进行比较,满足大小要求就进行交换,重复n次完成n个数据的排序;
- **优化:**当某次冒泡过程中没有发过数据交换,说明序列已经达到完全有序,不需要再继续后续的冒泡操作,重复次数就小于n;
实现
public void bubbleSort(int[] a,int n){
if(n<=1){
return;
}
for(int i=0;i<n;i++){
//是否提前完成 默认为true,当冒泡过程中出现交换则设为false
boolean isFinished=true;
//冒泡
for(int j=0;j<n-1-i;j++){
if(a[j]>a[j+1]){
//符合大小关系,从小到大,交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
//有交换操作说明未完成排序
isFinished = false;
}
if(isFinished){
//某次冒泡过程中没有交换操作,则提前退出不再执行后续冒泡
break;
}
}
}
冒泡排序
- 是稳定排序
- 是原地排序
- 最好O(1),最坏O(n2),平均O(n2)
插入排序
插入(逐个将未排序区间的元素插入已排序区间合适的位置)
思路
-
分已排序区间和未排序区间,初始已排序区间只有一个元素,就是数组的第一个元素,逐个将为排序区间的元素插入已经排序区间合适的位置
-
插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
-
从第二个元素开始,当需要将某个元素a插入到已排序区间时,需要拿a与已排序区间的元素(倒着)依次进行比较,
-
每次插入操作:
- 先将带插入的元素暂存,
- 再用已排序区间元素(倒着)依次和带插入元素进行比较,满足大小关系,则将当前比较元素向后移一位,直到不满足大小关系或者已排序区间比较完,将待插入元素插入当前比较元素后面(n<0时n=-1+1=0,插入位置则是已排序区间开头) 。
实现
public void insertionSort(int[] a,int n){
if(n<=1){
return;
}
for(int i=1;i<n;i++){
int curInsertItem = a[i];
int j=i-1;
for(;j>=0;j--){
if(a[j]>curInsertItem){
//满足大小关系,移动数据
a[j+1] = a[j];
}else{
//不满足,结束此轮插入前比较
break;
}
}
//将待插入元素插入当前比较元素后面
a[j+1]=curInsertItem;
}
}
插入排序
- 是稳定排序
- 是原地排序
- 最好O(n),最坏O(n2),平均O(n2)
选择排序
选择(从未排序区间选择最小的放到已排序区间最后)
思路
- 选择排序的实现思路有点类似于插入排序,也分已排序区间和未排序区间,但是选择排序每次会从未排序区间选择最小(或最大)的元素将其放入已排序区间的末尾。
- 排序开始前 已排序区间元素为0个,未排序区间元素为n个,每次选择未排序区间的最小元素则是已排序区间的最大元素放在已排序区间尾部,重复n次这样的选择完成排序。
实现
public void selectionSort(int[] a,int n){
if(n<=1){
return;
}
for(int i=0;i<n;i++){
//从第一个元素开始到n结束
//每次从未排序区间选择假设当前元素为最小
int min = a[i];
for(int j=i+1;j<n;j++){
//从当前元素后一个开始到结束
if(a[j]<min){
//遇到比假设最小元素小的则交换假设最小元素和比较元素
int tmp = a[j];
a[j]=min;
min=tmp;
}
}
//内循环结束,得到未排序区间最小元素
a[i]=min;
}
}
相比上面,内循环找到最小值下标替代找到具体的最小值,将交换操作放到外循环操作,可以减少多次交换
public void selectionSort(int[] a,int n){
if(n<=1){
return;
}
for(int i=0;i<n;i++){
//从第一个元素开始到n结束
//每次从未排序区间选择假设当前元素为最小
int minIdx = i;
for(int j=i+1;j<n;j++){
//从当前元素后一个开始到结束
if(a[j]<a[minIdx]){
//遇到比假设最小元素小的则将当前比较元素下标作为假设最小值下标
minIdx = j;
}
}
//内循环结束,得到未排序区间最小元素下标,交换最小值和当前带插入位置的值
if(minIdx!=i){
int tmp =a[i];
a[i]=a[minIdx];
a[minIdx]=tmp;
}
}
}
选择排序
- 原地排序
- 是不稳定排序
比较
插入和冒泡作为原地、稳定排序,算法复杂度(O(n^2))以及交换次数(逆序度)都一样,
但首选插入排序的原因在于交换操作,插入排序要优于冒泡排序,
冒泡排序每次冒泡需要执行3行代码,
插入排序数据移动只要执行一行,
冒泡排序、选择排序,实际开发中应用并不多,相较之下插入排序用的比较多。
以上三种排序算法,都是基于数组实现的。
如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
对于以上问题,应该有个前提:是否允许修改链表的节点value值,还是只能改变节点的位置。
一般而言,如果考虑只能改变节点位置,
冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;
插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;
选择排序比较次数一致,交换操作同样比较麻烦。
综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。
熄灯
由于插入排序对小数据量或者基本有序的列表排序效率较高,
当数据量特别大,可以考虑希尔排序
但是希尔排序 原地,不稳定排序
基本思想是将大的数组按增量gap逻辑划分成n-1个长度为增量gap+1的小数组,分别进行插入排序,
并逐渐缩小增量gap至1的时候,整个数组基本有序,再直接插入排序,效率突破O(n^2)
但是是非稳定排序,所以了解就行。
参考希尔排序–简单易懂图解