各种排序方法及其比较
排序方法 | 平均时间 | 最坏情况 | 辅助存储 |
简单排序 | O(n^2) | O(n^2) | O(1) |
快速排序 | O(nlogn) | O(n^2) | O(logn) |
堆排序 | O(nlogn) | O(nlogn) | O(1) |
归并排序 | O(nlogn) | O(nlogn) | O(n) |
基数排序 | O(d(n+rd)) | O(d(n+rd)) | O(rd) |
1.简单排序
直接插入排序
平均复杂度为O(N^2);最坏情况=(n-1)(n+2)/2 (次);最好情况下为O(n);少量中间变量,为常量级O(1);稳定的排序方法。
//直接插入排序
void InsertSort(int r[],int n)
{
//对数组r[1..n]的n个记录作排序
int i,j;
for(i=2;i<=n;i++)
{
r[0]=r[i]; //记录r[i]存入监视哨
j=i-1; //从r[i-1]开始向左扫描
while(r[0]<r[j])
{
r[j+1]=r[j]; //记录后移
j--; //继续向左扫描. 当r[0]比r[1]还小时,j减到0.再与哨兵比较则相等,插入记录。
}
r[j+1]=r[0]; //插入记录r[0](原r[i])
}
}
希尔排序
在序列基本有序时,直接插入的效率比较高;直接插入比较简单,在记录比较少的时候效率比较高。希尔排序就是基于这2点,其基本思想是将整个代拍记录序列分割成为若干字序列分别进行直接茶树排序,待其基本有序之后对全体记录进行一次直接插入排序。
其时间与所取的“增量”有关。比直接排序效率高。
2.选择排序
(1)树形选择排序
思想:将一个数组中的数两两一组,更大的数(或者更小的数)作为这两个数的父节点,依次向上构建一个完全二叉树。树的根结点即为最大的数。输出该数字之后,对应的叶子结点换成负无穷。然后在此基础上重新构建二叉树,直到所有结点均为负无穷为止。
复杂度:第一个值的复杂度为n,其他值的复杂度为logn的,平均复杂度为nlogn。(二叉树的删除与插入操作的复杂度为O(logk)?)
辅助空间O(n)。
(2)堆排序
最大堆的定义:一棵完全二叉树,每一个结点都比其子节点的值大。
思想:首先将数组简历成为一棵完全二叉树,然后调整成为初始堆。将根节点和最后一个元素交换,然后对前面的树进行堆调整。循环该步骤n-1次,完成排序。
复杂度分析:n个结点的完全二叉树的深度为(logn+1)。对深度为k的堆,HeapAdjust的比较次数最多为2(k-1)。调整新堆时调用HeapAdjust过程n-1次,其复杂度为:2(log(n-1)+log(n-2)+...+log2)<2nlogn。
建立初始堆时,总共进行的比较次数不超过4n。
因此堆排序的复杂度为O(nlogn)。
优势:适用于n较大的文件。且最坏情况下的复杂度也是O(nlogn),相比快排的优势。并且需要少量的辅助存储空间。
//堆排序 //若r[1,2...n]中以s为根的二叉树的左右子树是大堆,将以r[s]为根的子树调整为大堆 void HeapAdjust(int r[],int s,int m) { int j; //r[s]的左孩子 int temp=r[s]; //根r[s]的中间变量 for(j=2*s;j<=m;j=2*s) //左孩子存在(依次向下层比较) { if(j<m&&r[j]<r[j+1]) //右孩子存在,并且右孩子大于左孩子时 j++; //选r[j+1]为大孩子 if(temp>=r[j]) break; //如果根结点比大孩子的值大,则退出 r[s]=r[j]; //否则大孩子上移到双亲 s=j; //大孩子为新的双亲 } r[s]=temp; //最初的根下移 } //堆排序 void HeapSort(int r[],int n) { int i; int temp; for(i=n/2;i>0;i--) //第一次调整为初始堆的方法:从r[n/2]开始,自底向上调整堆 HeapAdjust(r,i,n); for(int n;i<1;i--) //共n-1趟,交换首尾结点,把最大值放到数列的尾部。以首结点为根节点调整堆,对前i-1个数排序 { temp=r[1]; r[1]=r[i]; r[i]=temp; HeapAdjust(r,1,i-1); } }
3.二分归并排序
思想:将一个数组分成左右两个数组,分别对其进行排序之后,再将左右2个相邻的有序文件归并为一个有序文件。将两个有序文件归并为一个有序文件的方法:按从小到大的顺序分别从左右文件中取出数字来放到排序后的文件中,直到其中的一个文件中的数字取完之后把剩余的数字都复制到另一个文件中。
算法复杂度:每次都要将原文件分成2个大小为k/2的文件,进行merge。所以整个归并排序要进行logn趟。且每一次merge的最大比较次数都是k次,因此算法复杂度为O(nlogn);最坏情况下的算法复杂度也是O(nlogn)。
空间复杂度:O(n);
优劣分析:是一种稳定的排序方法,递归形式的实用性较差。
题目:求数组中的逆序对数目。如{7,5,6,4}中共有{7,5}/{7,6}/{7,4}/{5,4}/{6,4}共5组逆序。——剑指offer 面试题36
思想:把数组拆分成小的子数组,统计子数组中的逆序对个数之后,把子数组排序,使得逆序对不会重复排序。然后合并子数组,统计逆序对数目。这就是一个归并排序的过程。
//逆序问题:归并排序 #include<stdlib.h> #include<stdio.h> #include<iostream> using namespace std; //假设Array[low]..Array[mid]和Array[mid+1]..Array[high]分别为相邻的有序子文件,将这2个子文件归并为有序文件rArray(允许使用n个大小的辅助空间) void Merge(int* r,int low,int high,int mid,int &sum) //注意要在原数组上面排序 { int *temp=(int *)malloc((high-low+1)*sizeof(int)); int i=low; int j=mid+1; int k=0; while(i<=mid&&j<=high) { if(r[i]<=r[j]) { temp[k++]=r[i++]; sum+=j-mid-1; //每次插入一个前面文件的数时,都说明由该数导致了j-mid-1个逆序 } else temp[k++]=r[j++]; } while(j<=high) //归并后一个子文件的记录 temp[k++]=r[j++]; while(i<=mid) //归并前一个子文件的记录 { temp[k++]=r[i++]; sum+=high-mid; //每次插入一个前面文件的数时,都说明由该数导致了j-mid-1个逆序,此时j==high+1. } k=0; for(i=low;i<=high;++i) r[i]=temp[k++]; free(temp); } //二分归并排序的递归算法 void MergeSort(int *r,int low,int high,int &sum) { if(low==high) return; int mid=(low+high)/2; MergeSort(r,low,mid,sum); MergeSort(r,mid+1,high,sum); Merge(r,low,high,mid,sum); } void main() { int r[5]={7,5,6,4,3}; int sum=0; MergeSort(r,0,4,sum); int i=0; while(i<5) cout<<r[i++]<<endl; cout<<sum; }
4.冒泡排序
思想:依次比较相邻的数,将较大的数移动到右边。通过一次排序,将最大的数移动到最右边。也就是每一次冒泡排序之后,都将第k大的值移到了n-k+1的位置。
普通冒泡排序需要比较n-1次。改进的冒泡排序,只要在某一次排序中没有发生交换,说明该排序已经完成。
复杂度分析:最坏情况下(逆序),需要比较1+2+..n-1=O(n^2)次,交换O(n^2)次。
最好情况下(正序),只需要比较n-1次,不需要交换。
辅助空间:少量的辅助空间,O(1)。
稳定的排序方法。
//普通冒泡排序。对任何情况作n-1次排序 void bubble1(int r[],int n) { int i,j; int temp; for(i=1;i<n;i++) //作n-1次排序 for(j=1;j<n-i;j++) //每次排序针对前n-i个数 { if(r[j]>r[j+1]) { temp=r[j]; r[j]=r[j+1]; r[j+1]=temp; } } } //改进的冒泡排序.如果在某一趟排序中没有发生过交换,说明该排序已经完成。 void bubble2(int r[],int n) { int i,j,swap; int temp; j=1; //比较趟数 do { swap=0; //交换标志为0 for(i=1;i<n-j;i++) if(r[i]>r[i+1]) { temp=r[i]; r[i]=r[i+1]; r[i+1]=temp; swap=1; //交换标志置1 } j++; } while (j<n&&swap); //比较趟数小于n,且前一趟排序发生过交换。 }