排序 之 堆排序 归并排序
堆的概念
堆是具有下列性质的完全二叉树:每个节点的值都大于或等于其左右孩子结点的值,称为大顶堆;或着每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序
堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是对顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,就可以得到一个有序序列了。
有上述思想可知,堆排序的两个要点:
- 1.如何由一个无序序列构建成一个堆?
- 2.如何在输出堆顶元素后,调整剩余元素称为一个新的堆?
/* 堆排序********************************** */ /* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, */ /* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */ void HeapAdjust(SqList *L,int s,int m) { int temp,j; temp=L->r[s]; for(j=2*s;j<=m;j*=2) /* 沿关键字较大的孩子结点向下筛选 */ { if(j<m && L->r[j]<L->r[j+1]) ++j; /* j为关键字中较大的记录的下标 */ if(temp>=L->r[j]) break; /* rc应插入在位置s上 */ L->r[s]=L->r[j]; s=j; } L->r[s]=temp; /* 插入 */ } /* 对顺序表L进行堆排序 */ void HeapSort(SqList *L) { int i; for(i=L->length/2;i>0;i--) /* 把L中的r构建成一个大根堆 */ HeapAdjust(L,i,L->length); for(i=L->length;i>1;i--) { swap(L,1,i); /* 将堆顶记录和当前未经排序子序列的最后一个记录交换 */ HeapAdjust(L,1,i-1); /* 将L->r[1..i-1]重新调整为大根堆 */ } }
时间复杂度
构建堆的时间复杂度为O(n)。在正式排序过程中,第i次取堆顶记录重建堆需要用O(logi)的时间,需要取n-1此堆顶记录。因此重建堆的时间复杂度为O(nlogn).
不过由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
归并排序
分治法
分治法(divide-and-conquer)是一种算法设计策略,其思想是将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。归并排序,希尔排序和快排都是分治思想的典型应用。
分治模式在每一层递归上都有三个步骤:
- 分解(Divide):将原问题分解成一系列子问题;
- 解决(Conquer):递归地解各子问题。若子问题足够小,则直接求解;
- 合并(Combine):将子问题的结果合并成原问题的解。
归并排序(merge sort)算法完全依照了上述模式,直观地操作如下:
分解:将n个元素分成各含n/2个元素的子序列;
解决:用合并排序法对两个子序列递归地排序;
合并:合并两个已排序的子序列以得到排序结果。
/* 归并排序********************************** */ /* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */ void Merge(int SR[],int TR[],int i,int m,int n) { int j,k,l; for(j=m+1,k=i;i<=m && j<=n;k++) /* 将SR中记录由小到大地并入TR */ { if (SR[i]<SR[j]) TR[k]=SR[i++]; else TR[k]=SR[j++]; } if(i<=m) { for(l=0;l<=m-i;l++) TR[k+l]=SR[i+l]; /* 将剩余的SR[i..m]复制到TR */ } if(j<=n) { for(l=0;l<=n-j;l++) TR[k+l]=SR[j+l]; /* 将剩余的SR[j..n]复制到TR */ } } /* 递归法 */ /* 将SR[s..t]归并排序为TR1[s..t] */ void MSort(int SR[],int TR1[],int s, int t) { int m; int TR2[MAXSIZE+1]; if(s==t) TR1[s]=SR[s]; else { m=(s+t)/2; /* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */ MSort(SR,TR2,s,m); /* 递归地将SR[s..m]归并为有序的TR2[s..m] */ MSort(SR,TR2,m+1,t); /* 递归地将SR[m+1..t]归并为有序的TR2[m+1..t] */ Merge(TR2,TR1,s,m,t); /* 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] */ } } /* 对顺序表L作归并排序 */ void MergeSort(SqList *L) { MSort(L->r,L->r,1,L->length); } /* 非递归法 */ /* 将SR[]中相邻长度为s的子序列两两归并到TR[] */ void MergePass(int SR[],int TR[],int s,int n) { int i=1; int j; while(i <= n-2*s+1) {/* 两两归并 */ Merge(SR,TR,i,i+s-1,i+2*s-1); i=i+2*s; } if(i<n-s+1) /* 归并最后两个序列 */ Merge(SR,TR,i,i+s-1,n); else /* 若最后只剩下单个子序列 */ for(j =i;j <= n;j++) TR[j] = SR[j]; } /* 对顺序表L作归并非递归排序 */ void MergeSort2(SqList *L) { int* TR=(int*)malloc(L->length * sizeof(int));/* 申请额外空间 */ int k=1; while(k<L->length) { MergePass(L->r,TR,k,L->length); k=2*k;/* 子序列长度加倍 */ MergePass(TR,L->r,k,L->length); k=2*k;/* 子序列长度加倍 */ } }
复杂度
时间复杂度为O(nlogn),
空间复杂度为O(n+logn),由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间。
归并排序是一种比较占用内存,但却效率高且稳定的算法。
非递归的迭代方法,避免了递归时深度为log2n的栈空间,因此空间复杂度为O(n).并且避免递归在时间性能上有一定的提升(函数调用的开销)。所以,使用归并排序时,尽量考虑使用非递归方法。