目录
一、插入排序
1) 直接插入排序
优化: 折半插入排序
2)希尔排序
二、 交换排序
1)冒泡排序
2)快速排序——递归实现
三、选择排序
1)简单选择排序
2)堆排序
四、归并排序
五. 各个排序性能分析
一、插入排序
1) 直接插入排序
更适用于基本有序的排序表和数据量不大的排序表。
#include<bits/stdc++.h>
using namespace std;
typedef int ElemType;
//直接插入排序
//思路:分为两部分(已排序和未排序)
//每一趟将一个未排序的数插入到已排序的合适位置(要走n-1趟)
//A[0]不存放数据,作为哨兵
void InsertSort(ElemType A[],int n){int i,j;for(i=2;i<=n;i++){ //排序n-1趟,i表示待排序数的位置 if(A[i]<A[i-1]){A[0]=A[i]; //哨兵//因为稳定性,遇到相同数时循环就该停下来,也就是插入到相同的后面 for(j=i-1;A[j]>A[0];j--){ //从后往前查找插入位置 A[j+1]=A[j]; //向后移位 }A[j+1]=A[0]; }}
}
int main(void){int A[7]={0,1,455,-234,45,566,21}; //参与比较的只有A[1]-A[6] InsertSort(A,6);for(int i=1;i<7;i++)cout<<A[i]<<" ";return 0;
}
- 时间复杂度:O(n^2)
最好情况下:序列已经有序,每次向有序子表中插入元素时,只需要比较1次,总共比较n-1次,时间复杂度O(n)
最坏情况下: 序列整体逆序,总的比较次数和移动次数均达到最大, 时间复杂度为O(n^2)。
平均情况下:O(n^2)
tips:无特别说明下,时间复杂度指的是最坏时间复杂度
- 空间复杂度:O(1)
仅仅只是使用了常数个辅助空间(哨兵),故空间复杂度为O(1)
- 稳定性:稳定
从代码中我们可以看到,每次插入的时候从后往前比较,遇到相同的数时,总插在相同数的后面,即相同数的相对位置不会发生改变。
优化: 折半插入排序
与直接插入排序相比,在找插入排序位置的时候,使用折半插入减少了比较次数 O(nlogn),但移动次数并未发生改变 O(n^2),故时间复杂度还是O(n^2)。
#include<bits/stdc++.h>
using namespace std;
typedef int ElemType;
//折半插入排序
//思路:分为两部分(已排序和未排序)
//每一趟将一个未排序的数插入到已排序的合适位置(要走n-1趟)
//A[0]不存放数据,作为哨兵
void InsertSort(ElemType A[],int n){for(int i=2;i<=n;i++){A[0]=A[i]; //哨兵存放待排数 //1.找到插入位置int low=1,high=i-1; while(low<=high){int mid=(low+high)/2;if(A[0]<A[mid]) //左半子表 high=mid-1; else //右半子表(为保持稳定性=时候需要到右半子表) low=mid+1; }//当low>high时候退出循环,low所指位置就是要插入的地方 //2.[low,i]位置均向后移动一位for(int j=i-1;j>=low;j--)A[j+1]=A[j];A[low]=A[0]; //插入 }}
int main(void){int A[7]={0,1,455,-234,45,566,21}; //参与比较的只有A[1]-A[6] InsertSort(A,6);for(int i=1;i<7;i++)cout<<A[i]<<" ";return 0;
}
2)希尔排序
基本思想:把相隔某个“增量”的记录组成一个子表,对各个子表进行直接插入排序,当整个表中的元素呈现“基本有序”时,再对全体记录进行一次直接插入排序。
- 时间复杂度:依赖于增量序列的函数 (并不确定)
最坏情况下: 时间复杂度为O(n^2)。
- 空间复杂度:O(1)
仅仅只是使用了常数个辅助空间,故空间复杂度为O(1)
- 稳定性:不稳定
二、 交换排序
1)冒泡排序
#include<bits/stdc++.h>
using namespace std;
typedef int ElemType;//冒泡排序
//最多排序n-1趟,每趟将确定一个最小的放在队头
//第i趟比较n-i次(从后往前找最小)
//添加了一个flag标记位用来记录每趟是否存在交换,若不存在则说明排序结束
void BubbleSort(ElemType A[],int n){for(int i=0;i<n-1;i++){ //n-1趟排序 bool flag=false; //标记此趟是否存在交换for(int j=n-1;j>i;j--){ //每趟从后往前 if(A[j-1]>A[j]){ElemType t=A[j-1];A[j-1]=A[j];A[j]=t;flag=true;} }if(!flag) //如果没有发生交换,说明已经有序了 return;}
}int main(void){int a[5]={34,-13,24,324,21};BubbleSort(a,5); for(int i=0;i<5;i++)cout<<a[i]<<" ";return 0;
}
- 时间复杂度:O(n^2)
最好情况下:序列整体有序,第一趟比较n-1次之后即可完成排序,复杂度O(n)
最坏情况下: 序列整体逆序,需要比较n-1趟,第i趟排序比较n-i次关键字,复杂度O(n^2)
tips:无特别说明下,时间复杂度指的是最坏时间复杂度
- 空间复杂度:O(1)
仅仅只是使用了常数个辅助空间(两数交换的时候),故空间复杂度为O(1)
- 稳定性:稳定
从代码中,我们可以看到,当两数相等时,我们并不交换其位置,所以冒泡算法稳定。
2)快速排序——递归实现
快速排序是所有内部排序算法中平均性能最优的排序算法。
#include<bits/stdc++.h>
using namespace std;
typedef int ElemType;//选择排序(递归实现)
//每一次确定一个位置的主要代码
int partition(ElemType A[],int low,int high){ElemType pivot=A[low]; //将表中第一个需要排序的元素做为基准元素while(low<high){ //当low==high,该次结束 while(low<high&&A[high]>=pivot)high--;A[low]=A[high];while(low<high&&A[low]<=pivot)low++;A[high]=A[low]; }A[low]=pivot; //最终确认的位置return low; //返回该位置
}//快排主代码
void QuickSort(ElemType A[],int low,int high){if(low<high){ //排序并未结束 int mid=partition(A,low,high); //整个序列排序QuickSort(A,low,mid-1);QuickSort(A,mid+1,high); }
}
int main(void){int A[6]={23,-24,345,27,34,13};QuickSort(A,0,5);for(int i=0;i<6;i++){cout<<A[i]<<" ";}return 0;
}
- 时间复杂度:O(n*递归层数)
最好情况下:选取基准够好,刚好将其划分成了一个类似完全二叉树,时间复杂度O(nlogn)
最坏情况下: 选取基准最差,直接划分成了一个单支树,时间复杂度O(n^2)
- 空间复杂度:O(递归层数)
由于快速排序是递归的,所以需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大层数一致。
最坏情况下:O(n)
最好情况下:O(logn)
平均情况下:O(logn)
- 稳定性:不稳定
例如表{3,2 ,2},以1为基准的时候,经过一趟排序之后,就变成了{2,2,3},相对位置发生了改变。
三、选择排序
每一趟在待排序元素中选择关键字最小(或最大)的元素加入有序子序列。
1)简单选择排序
适用于顺序存储和链式存储的线性表,以及关键字较少的情况。
#include<bits/stdc++.h>
using namespace std;typedef int ElemType;//简单选择排序
//每次找到待排序中最小的,然后进行交换
void SelectSort(ElemType A[],int n){for(int i=0;i<n-1;i++){ //进行n-1趟即可int min=i; //最小值的下标 for(int j=i+1;j<n;j++)if(A[j]<A[min])min=j;//将min和i位置对应的值进行交换swap(A[min],A[i]);//swap也就是下面3句代码,需要移动3次
// ElemType t=A[min];
// A[min]=A[i];
// A[i]=t; }
}int main(void){int A[6]={1,455,-234,45,566,21}; //参与比较的是[0]-A[5] SelectSort(A,6);for(int i=0;i<6;i++)cout<<A[i]<<" ";return 0;
}
- 时间复杂度:O(n^2)
元素的移动次数:很少,不会超过 3*(n-1), 最好的情况0次
元素的比较次数: 与序列的初始状态无关,始终是 n(n-1)/2。
故时间复杂度为O(n^2)
- 空间复杂度:O(1)
仅使用常数个辅助单元。
- 稳定性:不稳定
例如表{5,6,5 ,2},经过经过一趟简单选择排序之后,就变成了{2,6,5 ,5},相对位置发生了改变。
2)堆排序
- 时间复杂度:O(nlogn)
堆排序分为两步:
1. 建立初始堆: 时间复杂度为O(n)
2. 排序(选择堆顶和最后一个待排元素交换,然后再调整剩余堆):
需要进行n-1趟(每次选择最大元素放在堆底),每趟最多向下调整h-1层,每层最多比较2次,故时间复杂度O(nh),也就是O(nlogn)
总时间复杂度 O(nlogn+n) 也就是O(nlogn)
- 空间复杂度:O(1)
仅使用常数个辅助单元。
- 稳定性:不稳定
例如堆{1,2 ,2},经过堆排序之后,就变成了{1,2 ,2},相对位置发生了改变。
四、归并排序
例子:
考点:对于两个长度分别为n和m的链表进行二路归并操作,最少的比较次数min(m,n),最多的比较次数m+n-1。
- 时间复杂度:O(nlogn)
二路归并的“归并树”可以看做是一个倒立的二叉树,
1. 需要归并h-1趟,时间复杂度O(logn) 。
因为二叉树第h层最多有2^(h-1)个元素 =》 n<=2^(h-1) =》 h-1=logn向上取整
2. 每趟归并遍历元素,时间复杂度O(n)
所以总的时间复杂度为O(nlogn),最好、最坏和平均情况下均为O(nlogn)。
- 空间复杂度:O(n)
1. 在归并过程中,借助了n个辅助空间来归并,时间复杂度为O(n)
2. 使用递归实现,时间复杂度O(h),也就是O(logn)
总的来看,空间复杂度也就是O(n+logn),取大头,也就是O(n)
- 稳定性:稳定
二路归并在Merge() 操作时,不会改变相同关键字记录的相对次序。
五. 各个排序性能分析
算法种类 | 时间复杂度 | 空间复杂度 | 是否稳定 | |||||
最好情况 | 平均情况 | 最坏情况 | 最好情况 | 平均情况 | 最坏情况 | |||
插入 排序 | 直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | O(1) | O(1) | 稳定 |
折半插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | O(1) | O(1) | 稳定 | |
希尔排序 | O(1) | O(1) | O(1) | 不稳定 | ||||
交换排序 | 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | O(1) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | O(logn) | O(n) | 不稳定 | |
选择排序 | 简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | O(1) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | O(1) | O(1) | 不稳定 | |
归并排序 | 二路归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | O(n) | O(n) | 稳定 |
这些都是自己总结的,有什么问题欢迎指正。
都看到这里了,点个赞再走吧!!!(*^_^*)