根据浙大数据结构慕课和《算法设计与分析基础》整理
规定按照从小到大顺序排序。
前提:
void X_Sort(int A[ ], int N)
主函数:
int main(){
int N;
cin>>N;
int a[N];
for(int i = 0;i<N;i++){
cin>>a[i];
}
X_Sort(a,N);//排序函数
cout<<a[0];
for(int i = 1;i<N;i++){
cout<<' '<<a[i];
}
}
一、选择排序
扫描整个列表,找到它的最小元素,和第一个元素交换,将最小元素放到它在有序表的最终位置。之后从第二个元素开始扫描,找到第二小的元素,和第二个元素交换位置……在进行(n-1)遍扫描后,列表排好序。
代码如下:
void Selection_Sort(int A[],int N)
{
int i,j;
int minDex;//最小元素的索引
for(i = 0;i<N-1;i++){
minDex = i;
for(j = i+1;j<N;j++){
if(A[j]<A[minDex]){
minDex = j;
}
}
int t = A[i];
A[i] = A[minDex];
A[minDex] = t;
}
}
对于任何输入,选择排序都要进行一样的双重循环,其时间复杂度为O(n2)
但是,其键的交换次数少,仅交换(n-1)次便能完成排序。
选择排序并不稳定。
二、冒泡排序
比较表中相邻元素,如果它们是逆序就交换它们的位置,重复多次后,最大的元素就沉到列表的最后一个位置。第二篇将列表的第二大元素沉下去,一直这样做,到第n-1遍后,列表排好序。
代码如下:
void Bubble_Sort(int A[],int N)
{
for(int i = N-1;i > 0;i--){//i是一趟冒泡排序最后比到的那个数
int flag = 0;
for(int j = 0;j<i;j++){
if(A[j]>A[j+1]){
int t = A[j];
A[j] = A[j+1];
A[j+1] = t;
flag = 1;
}
}
if(flag==0){
return;//如果一趟冒泡排序全程无交换,则已经有序了
}
}
}
分析可知,设置flag后的最好情况:序列一开始即是有序的,时间复杂度为O(N);
最坏情况:逆序,时间复杂度O(N2)
平均时间复杂度:O(N2)
冒泡排序是稳定的。
三、插入排序
类似于玩家在玩牌时将抽到的牌插入到之前抽到的牌中,插入排序从A[1]开始到A[n-1]为止,A[i]被插入到数组的前i个有序元素的适当位置上(此位置一般并不是它们的最终位置)。
代码如下:
void Insertion_Sort(int A[],int N)
{
int i,j;
for(i = 1;i<N;i++){
int t = A[i];
for(j = i-1;j>=0;j--){
if(t<A[j]){
A[j+1] = A[j];
}else{
break;
}
}
A[j+1] = t;
}
}
插入排序是稳定的。
对算法平均效率的精确分析主要基于对逆序对的研究。
冒泡排序和插入排序每次交换2个元素都正好消除一个逆序对,若数列中有N个逆序对,则交换N次。
I 为逆序对的个数,插入排序的时间复杂度T(N,I)= O(N+I),所以插入排序在遇到基本有序数组时表现出优越的性能(如果逆序对个数和N同一数量级,则时间复杂度为线性),使得插入排序领先于冒泡排序和选择排序。
但是定理:任意N个不同元素组成的序列平均具有
N(N-1)/4个逆序对。所以任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为Ω(N2)
所以,插入排序:
最坏情况:逆序,O(N2);
最好情况:顺序:O(N)
平均情况:O(N2)
四、希尔排序
相较于插入排序,为了提高算法效率,每次交换相隔较远的两个元素。
希尔排序中要注意选取增量元素。若增量元素不互质,则小增量可能根本不起作用。
复杂度问题很复杂,不好讨论。
希尔排序不稳定。
void Shell_Sort(int A[],int N)
{
int S_array[] = {121,40,13,4,1,0};
int i,j,k,Tmp;
int D;//D是间隔
//初始增量S_array[k]的长度不能超过待排序列的长度
for(k=0;S_array[k]>=N;k++);
for(D = S_array[k];D>0;D = S_array[k++]){//首先规定好此次排序的间隔
//这里是k++不是k+1。。。k变量要自增,k+1没有改变k!除非写成K = K+1
for(i = D;i<N;i++){//注意是i++ ,插入排序(间隔为D)
Tmp = A[i];
for(j = i-D;j>=0;j = j-D){
if(Tmp<A[j]){
A[j+D] = A[j];
}else{
break;
}
}
A[j+D] = Tmp;
}
}
}
五、堆排序
选择排序的改进。选择排序中键的交换次数少,但是选择最小值的过程复杂,所以利用最小堆去选择最小值可以改进选择排序。
堆排序过程:首先建立一个最大堆,之后一个个删除堆顶的最小值元素(将堆顶元素移到最后面,即排好序的数组的最终位置),之后重新调整剩下的堆为最大堆。
时间复杂度无论最好最坏都是O(N*logN)
堆排序不稳定。
代码如下:
void Heap_Sort(int A[],int N)
{
int i;
for(i = N/2-1;i>=0;i--){
PercDown(A,i,N);//调整成最大堆
}
for(i=N-1;i>0;i--){//i即删除前最后一个元素的下标 ,删除后的堆元素的个数
int tmp = A[0];
A[0] = A[i];
A[i] = tmp;//交换最大值(即A[0])和数组最后一个元素
PercDown(A,0,i);//把剩下的元素调整成最大堆
}
}
void PercDown(int A[],int p,int N)
{//将N个元素数组中以A[p]为跟的堆调整成最大堆
int Parent,Child;
int X = A[p];
for(Parent = p;Parent*2+1<N;Parent = Child){
Child = Parent*2+1;
if((Child!=(N-1))&&A[Child]<A[Child+1]){
Child++;
}
if(X<A[Child]){
A[Parent] = A[Child];
}else{
break;
}
}
A[Parent] = X;
}
六、归并排序
对于一个需要排序的数组A,归并排序将其一分为二,并对每个子数组归并排序,然后把两个排好序的子数组合并为一个有序数组存在临时数组TmpA里面,然后把TmpA导回A当中,一步步向上递归。(空间上此算法多开辟了一个数组长度的空间)
归并排序的难点在于合并成一个有序数组,其时间复杂度为O(N)。所以归并排序的时间复杂度公式为T(N) = 2*T(N/2) + O(N),计算得平均时间复杂度为O(NlogN)。最坏情况:最小元素轮流来自于不同的数组。但即使是最坏情况也是O(NlogN) (无论最好最坏都是O(NlogN))
归并排序具有稳定性。
void Merge_Sort(int A[],int N){
int *TmpA = new int(N);
M_Sort(A,TmpA,0,N-1);
}
void M_Sort(int A[],int TmpA[],int L,int RightEnd)
{
int Center;
if(L<RightEnd){
Center = (L+RightEnd)/2;
M_Sort(A,TmpA,L,Center);
M_Sort(A,TmpA,Center+1,RightEnd);
Merge(A,TmpA,L,Center+1,RightEnd);
}
}
void Merge(int A[],int TmpA[],int L,int R,int RightEnd)
{
int LeftEnd = R-1;
int i = L;
int Num = RightEnd - L +1;
while((L<=LeftEnd)&&(R<=RightEnd)){
if(A[L]<=A[R]){
TmpA[i] = A[L];
L++;
i++;
}else{
TmpA[i] = A[R];
R++;
i++;
}
}
while(R<=RightEnd){
TmpA[i] = A[R];
R++;i++;
}
while(L<=LeftEnd){
TmpA[i] = A[L];
L++;i++;
}
for(i = 0;i<Num;i++,RightEnd--){
A[RightEnd] = TmpA[RightEnd];
}//将TmpA中数值导入A中
}
七、快速排序
和归并排序一样,也是分治法。但是它不是按照元素位置划分,而是按照元素的值进行划分。归并排序的重点在于归并,快速排序的重点在于划分。即找到一个元素,通过划分令该元素左边的值都小于此元素,元素右边的值都大于此元素,这时就确定了此元素的最终位置(因此快速排序的交换次数也少,所以比较快)。
快速排序最好的情况:所有的分裂点都位于相应子数组的中点。
快速排序最坏的情况:所有的分裂点都趋于极端(顺序或逆序),此时的时间复杂度:O(N2)
当问题规模较小(N<100)时,快排可能不如插入排序快,因此可以在规模较小时将快排转化成插入排序。
快速排序不稳定。
void Quick_Sort(int A[],int N){//接口
Q_Sort(A,0,N-1);
}
void Q_Sort(int A[],int L,int R)
{
int Cutoff = 50;//阈值
int Pivot;
if((R-L)>Cutoff){
Pivot = Median3(A,L,R);
int i = L;int j = R-1;
for(;;){
//Median3的好处不仅令主元选取更科学,
//还在边界设置了哨兵,不用担心i,j越界
while(A[++i]<Pivot){
}
while(A[--j]>Pivot){
}
if(i<j){
Swap(A[i],A[j]);
}else{
break;
}
}
Swap(A[i],A[R-1]);
Q_Sort(A,L,i-1);
Q_Sort(A,i+1,R);
}
else{
Insertion_Sort(A+L,R-L+1);
}
}
void Insertion_Sort(int A[],int N){
int i;int j;
int tmp;
for(i = 1;i<N;i++){
tmp = A[i];
for(j = i-1;j>=0;j--){
if(tmp<A[j]){
A[j+1] = A[j];
}else{
break;
}
}
A[j+1] = tmp;
}
}
int Median3(int A[],int L,int R)
{
int tmp;
int Center = (L+R)/2;
if(A[L]>A[Center]){
Swap(A[L],A[Center]);
}
if(A[L]>A[R]){
Swap(A[L],A[R]);
}
if(A[Center]>A[R]){
Swap(A[Center],A[R]);
}
//此时左边数最小,右边数最大
Swap(A[Center],A[R-1]);
//只需考虑A[l+1]与A[R-1]之间的数
return A[R-1];
}
void Swap(int &a,int&b){
int tmp = a;
a = b;
b = tmp;
}