算法种类 | 最好 | 平均 | 最坏 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
直接插入排序 | O(n) | O(N2) | O(N2) | O(1) | 是 |
冒泡排序 | O(n) | O(N2) | O(N2) | O(1) | 是 |
简单选择排序 | O(N2) | O(N2) | O(N2) | O(1) | 否 |
希尔排序 | O(n1.3) | O(1) | 否 | ||
快速排序 | O(nlog2n) | O(nlog2n) | O(N2) | O(log2n) | 否 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 否 |
2-路归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 是 |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O( r ) | 是 |
排序
将数据元素的一个任意序列,重新排列成一个按关键字有序的序列。
若按照记录的主关键字排序,则排序结果唯一。
若按照记录的次关键字排序,则排序结果可以不唯一。
设 Ki = Kj(1≤i≤n, 1≤j≤n, i≠j ),且在排序前的序列中 R i领先于Rj(即 i < j)。若在排序后的序列中 Ri 仍领先于 Rj,则称所用的排序方法是稳定的;反之,则称所用的排序方法是不稳定的。
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序,内部排序的过程是一个逐步扩大记录的有序序列的过程;反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
若待排序记录的关键字顺序正好和要排序顺序相同,称此表中记录为正序;反之,若待排序记录的关键字顺序正好和要排序顺序相反,称为反序。基于比较的排序算法中有些算法是与初始序列的正序或反序相关,有些算法与初始序列的正序和反序无关
将无序子序列中的一个或几个记录“插入”到有序序列中。
void insertSort(int a[n+1]){
for(int i=2;i<=n;i++){
a[0]=a[i];
j=i;
while(j>1&&a[0]<a[j-1]){
a[j]=a[j-1];
j--;
}
a[j]=[0];
}
}
//
void insertSort(int a[n+1]){
//对n个元素的排序
for(int i=2;i<=n;i++){
if(a[i]<a[i-1]){
a[0]=a[i];//哨兵
}
for(j=i-1;a[0]<a[j];j--){
a[j+1]=a[j];
}
a[j+1]=a[0];
}
}
算法评价
时间复杂度:
最好情况(正序) :O(N)
最坏情况(逆序) :O(N2)
平均情况:O(N2)
空间复杂度:O(1)
稳定性:稳定排序
void InsertSort(int a[n+1]){
//对n个元素排序
int i,j,liw,high,mid;
for(i=2,i<n;i++){
a[0]=a[i];
low=1;high=i-1;
while(low<=high){
mid=(low+high)/2;
if(a[mid]>a[0]){
heigh=mid-1;
}else{
low=mid+1;
}
}
for(j=i-1;j>high+1;j--){
a[j+1]=a[j];
}
a[hight+1]=a[0];
}
}
算法评价
时间复杂度:
最好情况(正序) :
最坏情况(逆序) :
平均情况:O(n2)(元素的移动次数没有改变)
空间复杂度:
稳定性:稳定排序
void ShellSort(int a[n+1]){
//对n个元素排序
//前后记录位置的增量是dk不是1
//a[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
for(dk=n/2;dk>=1;dk=dk/2){
for(i=dk+1;i<=n;++i){
if(a[i]<a[i-dk]){
a[0]=a[i];
for(j=i-dk;j>0&&a[0]<a[j];j-=dk){
a[j+dk]=a[j];
}
a[j+dk]=a[0];
}
}
}
}
时间复杂度:
最好情况(正序) :
最坏情况(逆序) :O(N2)
平均情况:O(N1.3)
空间复杂度:O(1)
稳定性:稳定排序
通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中.
void BubbleSort(int a[n]){
//对n个元素的排序
for(int i=0;i<n-1;i++){
//整个过程执行n-1趟
for(int j=n-1;j>i;j--){
if(a[j]<a[j-1]){
swap(a[j],a[j+1]);
}
}
}
}
//优化
void BubbleSort(int a[n]){
for(int i=0;i<n-1;i++){
flag=false;
for(int j=n-1;j>i;j--){
if(a[j]<a[j-1]){
swap(a[j],a[j+1]);
flag=true;//本趟遍历后没有发生交换,说明表以有序
}
}
if(flag==false)return;
}
}
算法评价
时间复杂度:
最好情况(正序) :O(n)
最坏情况(逆序) :O(n2) 。
平均情况:O(n2)
空间复杂度:O(1)
稳定性:稳定排序
任选一个记录,以它的关键字作为“枢轴”,凡关键字小于枢轴的记录均移至枢轴之前,凡关键字大于枢轴的记录 均移至枢轴之后。
并不产生有序子序列,但每趟排序会将一个元素放到其最终位置上。
int Partition(int a[],int left,int right){
int temp=a[left];
while(left<right){
while(left<right&&a[right]>temp)right--;
a[left]=a[right];
while(left<right&&a[right]<=temp)left--;
a[right]=a[left];
}
a[right]=temp;
return left;
}
void quickSort(int a[],int left,int right){
if(left<right){
int pos=Partition(a,left,right);
quickSort(a,left,pos-1);
quickSort(a,pos+1,right);
}
}
算法评价
若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为 O(n2)。 所以快速排序适用于原始记录排列杂乱无章的情况。
时间复杂度:
最好情况:O(nlog2n)
最坏情况:O(n2)
平均情况:O(nlog2n)
空间复杂度:O(log2n)
稳定性:不稳定排序
随机快速排序
在a[left,right]中随机选取一个主元,生成一个在[left,right]内的随机数p,然后以a[p]为主元来划分。
具体做法:
将a[p]与a[left]交换,其他同上。
srand((unsigned)time(NULL));
int p=(round(1.0*rand()/RAND_MAX*(right-left)+left);
swap(a[p],a[left]);
优化
从记录的无序子序列中“选择” 关键字最小或最大的记录,并将它加入到有序子序列中。
void SelsetSort(int a[n+1]){
//对n个元素的排序
for(int i=1;i<=n;i++){
//n趟
min=i;
for(int j=i;j<=n;j++){
if(a[j]<a[min]){
min=j;
}
}
if(min!=i)swap(a[i],a[min]);
}
}
算法评价
时间复杂度:
最好情况:O(n2)
最坏情况:O(n2)
平均情况:O(n2)
空间复杂度:O(1)
稳定性:不稳定排序
23215->13225(不稳定)->12325->12235
//调整
void adjustDown (int low,int hight){
int i=low,j=i*2;
while(j<=height){
if(j+1<=height&&heap[j+1]>heap[j]){
j=j+1;
}
if(heap[j]>heap[i]){
swap(heap[j],heap[i]);
i=j;
j=i*2;
}else{
break;
}
}
}
//建堆
void createHeap(){
for(int i=n/2;i>=1;i++){
downAdjust(i,n);
}
}
void deleteTop()[
heap[1]=heap[n--];
downAdjust(1,n);
}
void upAdjust(int low,int height){
int i=height,j=i/2;
while(j>=low){
if(heap[j]<heap[i]){
swap(heap[j],heap[i]);
i=j;
j=i/2;
}else{
break;
}
}
}
void insert(int x){
heap[++n]=x;
upAdjust(1,n);
}
void heapSort(){
creatHeap();
for(int i=n;i>1;i--){
swap(heap[i],heap[1]);
downAdjust(1,i-1);
}
}
算法评价
时间复杂度:
最好情况:O(nlog2n)
最坏情况:O(nlog2n)
平均情况:O(nlog2n)
空间复杂度:O(1)
稳定性:不稳定排序
通过“归并”两个或两个以上的记录有序子序列。
//递归实现
void merge(int a[n+1],int l1,int r1,int l2,int r2){
int i=l1,j=l2;
int temp[n+1],index=0;
while(i<r1&&j<r2){
if(a[i]<=a[j]){
temp[index++]=a[i++];
}else{
temp[index++]=a[j++];
}
}
while(i<=r1)temp[index++]=a[i++];
while(j<=r2)temp[index++]=a[j++];
for(i=0;i<index;i++){
a[l1+1]=temp[i];
}
}
void mergeSort(int a[n+1],int left,int right){
while(left<right){
int mid=(left+rigth)/2;
mergeSort(a,left,mid);
mergeSort(a,mid,right);
merge(a,left,mid,mid+1,right);
}
}
//非递归实现
void mergeSort(int a[n+1]){
//step为组内元素个数,step/2为左子区间元素个数,等号可不取
for(int step=2;step/2<=n;step*=2){
//每step个元素为一组,组内前step/2和后step/2个元素进行合并
for(int i=1;i<=n;i+=step){
int mid=i+step/2-1;
if(mid+1<=n){
merge(a,i,mid,mid+1,min(i+step-1,n);
}
}
}
}
//一趟归并
void mergeSort(int a[n+1]){
//step为组内元素个数,step/2为左子区间元素个数,等号可不取
for(int step=2;step/2<=n;step*=2){
//每step个元素为一组,组内前step/2和后step/2个元素进行合并
for(int i=1;i<=n;i+=step){
merge(a,i,mid,mid+1,min(i+step-1,n);//可以用sort代替
}
}
}
算法评价
时间复杂度:
最好情况:O(nlog2n)
最坏情况:O(nlog2n)
平均情况:O(nlog2n) ,每一趟归并的时间复杂度为 O(n),总共需进行log2n(向上取整)趟。
空间复杂度:O(n)
稳定性:稳定排序
基数排序是一种基于多关键字排序的思想,采用多关键字的排序思想(即基于关键字各位的大小进行排序),借助分配和收集两种单逻辑关键字进行排序。
分为高位优先排序MSD和低位优先排序LSD。
以r为基数的最低位优先基数排序的过程:假设线性表由结点排序a0,a1,…,an-1构成,每个节点aj的关键字由d元组(kjd-1,kjd-2…kj1,kj0)组成,其中0≤kji≤r-1(0≤j
对i=0,1…d-1,依次做一次分配和收集。
分配:开始时,把Q0,Q1…Qr-1各个队列置成空队列,然后依次考察线性表中的每个节点aj(j=0,1…n-1),若aj的关键字kji=k,就把aj放进Qk中。
收集:把Q0,Q1…Qr-1各个队列中的节点依次首尾相接,得到新的节点序列从而组成新的线性表。
算法评价
时间复杂度:O(d(n+r))
平均情况:O(nlog2n) ,每一趟归并的时间复杂度为 O(n),总共需进行log2n(向上取整)趟。
空间复杂度:O( r)
稳定性:稳定排序
指排序文件较大,内存一次放不下,需要存放在外部介质的文件的排序。排序时把数据一部分一部分的调入内存进行排序。在排序过程中需要多次进行内存和外存之间的交换,对外存文件中的记录进行排序后的结果仍然被放到外存文件中。
在外排序过程中的时间代价主要考虑访问磁盘的次数。
首先根据内存缓冲区的大小,将外存上含n个记录的文件分成若干长度为h的子文件,依次读入内存并利用有效的内部排序方法对他们进行排序,然后将排序后得到的有序的子文件重新写回外存,通常称这些有序的子文件为归并段或顺串。然后对这些归并段进行逐趟归并,使归并段逐渐有小到大,直至得到整个有序文件为止。
外部排序的总时间=内部排序所需的时间+外存信息读写的时间+内部归并随需的时间。
败者树是树形选择排序的一种变体,可视为一棵完全二叉树,每个叶节点存放各归并段在归并过程中当前参加比较的记录,内部节点用来记忆左右子树中的失败者,而让胜者继续往上进行比较,一直到根节点。
使用败者树之后,内部归并的比较次数与归并路数m无关,因此,只要内存空间允许,增大m将有效减少归并树的高度,从而减少IO次数d,提高速度。
归并路数m并不是越大越好,m增大时,相应的需要增加输入缓冲区的个数。若可供使用的内存空间不变,势必要减少每个输入缓冲区的容量,使得内存,外存交换数据的次数增大,当m过大时,虽然归并趟数会减少,但读写外存的次数任然会增加。
对k个有序段进行k路平衡归并的方法如下:
1.取每个输入有序段的第一个记录作为败者树的叶子结点,建立初始败者树:两两叶子结点进行比较,在双亲结点中记录比赛的败者(关键字较大者),而让胜者去参加更高一层的比赛,如此在根结点之上胜出的“冠军”是关键字最小者。
2.胜出的记录写至输出归并段,在对应的叶子结点处,补充其输入有序段的下一个记录,若该有序段变空,则补充一个大关键字(比所有记录关键字都大,设为kmax)的虚记录。
3.调整败者树,选择新的关键字最小的记录:从补充记录的叶子结点向上和双亲结点的关键字比较,败者留在该双亲结点,胜者继续向上,直至树的根结点,最后将胜者放在根结点的双亲结点中。
4.若胜出的记录关键字等于kmax,则归并结束;否则转2继续。
减少初始归并段的个数也可以减少归并趟数。但若采用前面介绍的内部排序的方法,将得到长度都相同的初始归并段。
设初始待排文件为FI,初始归并段文件为FO,内存工作区为WA,内存工作区可容纳w个记录。
1.从待排文件FI中输入w个记录。
2.从内存工作区WA中选出关键字最小的记录MINMAX。以后再选出关键字比他大的记录归入本归并段,比他小的归入下一归并段。
3.将MINMAX记录输出到FO中。
4.若FI不空,则从FI中读入下一个记录x放在WA中。
5.在WA中所有关键字比MINMAX记录的关键字大的记录中选出最小的关键字记录,作为新的MINMAX。
6.重复3-5,直到在WA中选不出新的MINMAX记录为止。由此得到一个初始归并段,输出一个归并段的结束标志到FO中。
7.重复2-6,直到WA为空。
由于采用置换-选择排序的方法生成的初始归并段长度不等,在进行逐趟k路归并时对归并段的组合不同,会导致归并过程中对外存的读/写次数不同。为提高归并的时间效率,有必要对各归并段进行合理的搭配组合。按照最佳归并树的设计可以使归并过程中对外存的读/写次数最少。
m路归并排序可用一颗m叉树描述,因为每次做m路归并都需要有m个归并段参加,因此,归并树是一颗只有度为0和度为m的节点的严格m叉树。
各个叶节点表示参加归并的一个初始归并段,叶节点上的权值表示该归并段中的记录数,根节点表示最终生成的归并段,叶节点到根节点的路径长度表示在归并过程中的归并趟数,各非叶节点代表归并成的新归并段。
归并方案不同,所得的归并树不同,树的带权路径程度也不同,可以将哈夫曼树的思想推广到m叉树。让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立总共的IO次数最少的最佳归并树。
若初始归并段不足以构成一棵严格m叉树,需要添加长度为0的虚段。