排序算法二
基本的声明及公用函数库在【算法导论】排序算法 零中介绍。
一、选择法排序、冒泡排序、插入法排序
二、快速排序、分治法排序、堆排序
三、计数排序、基数排序、桶排序
gtest介绍及测试用例如下:测试框架之GTest
MIT《算法导论》下载:hereorhttp://download.csdn.net/detail/ceofit/4212385
源码下载:here orhttp://download.csdn.net/detail/ceofit/4218488
选择法排序、冒泡排序、插入排序都比较简单,时间复杂度也比较大,都是O(n*n)。于是有了减小时间复杂度的排序算法,最常用的当属快速排序。
本节介绍几个时间复杂度为nlogn的排序算法:快速排序、堆排序、分治法排序。这几个算法其基础都是基于二分的思想,或说是基于有序二叉树,实现过程一般都是递归。使用递归一层一层的处理。
快速排序
快速排序使用很普遍,比较简单,而且是原地排序,所以性能要求严格的地方经常使用快排。
比如n个数据,快排将第1个数据m作为中间数据,将所有数据中按照大于m、小于m分开,即找到m的位置,所以一轮后,所有数据排列为<m,m,>m,然后对<m与>m递归这个过程即可。最优情况下时间复杂度nlogn,最坏情况下时间复杂度n*n。
具体查找m的位置的算法很多,下面介绍两种,很简单,不详细介绍,直接看代码。
例如
2 4 5 3 1
第一层快排后为:
1 2 5 3 4
第二层快排,2左边只有1个数据,已经牌号,右边5 3 4,排序后为:
1 2 3 4 5
ok,排序完成。
//快速排序,时间复杂度最大n*n,最优情况下为nlgn,原地排序 void QuickSort(int *pArray,int cnt);
#include "SK_Common.h" #include "SK_Sort.h" int get_middle(int *pArray,int start,int end) { if(start >= end) return start; int midvalue = pArray[start]; int save_ptr = start; int front_ptr = start; while(front_ptr <= end) { if(pArray[front_ptr] < midvalue) { pArray[save_ptr] = pArray[front_ptr]; pArray[front_ptr] = pArray[save_ptr+1]; save_ptr++; } front_ptr++; } pArray[save_ptr] = midvalue; return save_ptr; } void _quick_sort(int *pArray,int start,int end) { if(start >= end) return; int mid = get_middle(pArray,start,end); _quick_sort(pArray,start,mid-1); _quick_sort(pArray,mid+1,end); } void QuickSort(int *pArray,int cnt) { _quick_sort(pArray,0,cnt-1); }
其中get_middle还有另外一种实现方法:
int get_middle(int *pArray,int start,int end) { if(start >= end) return start; int midvalue = pArray[start]; int first_ptr = start; int second_ptr = end; while(first_ptr <= second_ptr) { while(pArray[first_ptr] < midvalue) first_ptr++; while(pArray[second_ptr] > midvalue) second_ptr--; if(first_ptr < second_ptr) { exchange(pArray+first_ptr,pArray+second_ptr); first_ptr++; second_ptr--; } else { break; } } return second_ptr; }
快排对乱序的数据排序效果很好,但对有序的数据排序效果很差,复杂度到n*n,所以每层排序获取的中间数据可以随机化选择,而不是用第1个元素,这样可以有效的提高有序序列的排序速度。
int get_middle_random(int *pArray,int start,int end) { int mid = get_random(start,end); exchange(pArray+start,pArray+mid); return get_middle(pArray,start,end); } void _quick_sort(int *pArray,int start,int end) { if(start >= end) return; int mid = get_middle_random(pArray,start,end); _quick_sort(pArray,start,mid-1); _quick_sort(pArray,mid+1,end); }
分治法排序
分治法排序也是二分思想,假设n个数据的前n/2个数据和后n/2个数据都是有序的,那将两段数据合并后即可得到一个有序序列。合并的复杂度为n。基于二分思想,所以分治法排序的算法复杂度也是nlogn.
例如
2 4 5 3 1
如此分:2 4 5 3 1 ,5 3 1 分为5 3 1
第一次合并后
2 4 ||5 | 1 3
第二次合并后为
2 4 |1 3 5
第三次合并后为
1 2 3 4 5
排序OK。
由于分治法排序需要存储临时数据,所以标准分治法排序不是原地排序,时间复杂度为nlogn,空间复杂度为n
书中给出的一个例子图解:
//分治排序,时间复杂度nlgn,非原地排序 void MergeSort(int *pArray,int cnt);
#include "SK_Common.h" #include "SK_Sort.h" void _merge(int *pArray,int start,int mid,int end) { if(!(mid >= start && mid <= end)) return; int len1 = mid-start+1; int len2 = end - mid; int *pArray1 = new int[len1+1];// +1防止最后一个for循环中, // 因为判断顺序不同机器不同而导致的数组下标越界 int *pArray2 = new int[len2+1]; int i = 0,j = 0,k = 0; for(i=0;i<len1;i++) { pArray1[i] = pArray[start+i]; } for(i=0;i<len2;i++) { pArray2[i] = pArray[mid+i+1]; } i = j = 0; for(k=start; k<=end; k++) { if(j>=len2 || (i < len1 && pArray1[i] <= pArray2[j])) pArray[k] = pArray1[i++]; else pArray[k] = pArray2[j++]; } delete[] pArray1; delete[] pArray2; } void _mergesort(int *pArray,int start,int end) { if(start >= end) return; int mid = (start+end)/2; _mergesort(pArray,start,mid); _mergesort(pArray,mid+1,end); _merge(pArray,start,mid,end); } void MergeSort(int *pArray,int cnt) { _mergesort(pArray,0,cnt-1); }
为防止多次new、delete性能降低,可预分配临时空间:
void _merge(int *pArray,int start,int mid,int end,int *tmpArray) { if(!(mid >= start && mid <= end)) return; int len1 = mid-start+1; int len2 = end - mid; int *pArray1 = tmpArray; int *pArray2 = tmpArray+len1; int i = 0,j = 0,k = 0; for(i=start;i<=end;i++) { tmpArray[j++] = pArray[i]; } i = j = 0; for(k=start; k<=end; k++) { if(j>=len2 || (i < len1 && pArray1[i] <= pArray2[j])) pArray[k] = pArray1[i++]; else pArray[k] = pArray2[j++]; } } void _mergesort(int *pArray,int start,int end,int *tmpArray) { if(start >= end) return; int mid = (start+end)/2; _mergesort(pArray,start,mid,tmpArray); _mergesort(pArray,mid+1,end,tmpArray); _merge(pArray,start,mid,end,tmpArray); } void MergeSort(int *pArray,int cnt) { int *tmpArray = new int[cnt+1]; _mergesort(pArray,0,cnt-1,tmpArray); delete[] tmpArray; }
堆排序
对排序是利用完全二叉树来实现的,复杂度nlogn且是原地排序。
将待排序数据表示为二叉树,例如 1 2 3 4 5 6 7,其中1为根节点,2 3为第一层子节点 4 5 6 7为叶子节点。其中4、5的父节点为2,6、7的父节点为3。
所以对于一个有序序列,假设n个元素,跟节点是1,深度为[log2(n)],节点m的子节点为2m与2m+1,父节点为[m/2]。
其中对于任一节点m,存在m>2m,且m>2*m+1,这称为最大堆,可利用最大堆排升序。由于最大堆的性质可知最大堆的根节点永远是此二叉树的最大值。
找到最大值后,将此值置入最后一个叶子节点,然后将此节点从二叉树删除,依次递归,将第二大的值插入倒数第二个位置....依次类推可得到升序排序的序列。
因此堆排序需要两个过程:建立最大堆、堆排序。
建立最大堆是从n/2开始到0遍历,将值与两个子节点比较,如果比两个孩子至少1个小,则下移,直到满足此节点>=任意子节点。并递归遍历此过程,使最终达到最大堆的条件。例如
2 4 5 3 1
2
4 5
3 1
建立最大堆从4开始,4比1、3大,此步不动。
然后到2,从4、5中找大者5,2下移,5上移得出:
5 4 2 3 1
5
4 2
3 1
排序:
第一次找出最大值后为:
4 3 2 1 5
4
3 2
1 5
第二次找到最大值后为:
3 1 2 4 5
3
1 2
4 5
第三次找到最大值后为:
2 1 3 4 5
2
1 3
4 5
第四次找到最大值后为:
1 2 3 4 5
1
2 3
4 5
OK,排序完成。
书中给出的一个例子:
建最大堆:
排序:
//堆排序,时间复杂度nlgn,原地排序 void HeapSort(int *pArray,int cnt);
#include "SK_Common.h" #include "SK_Sort.h" static inline int _Parent(int i) { return (i+1)/2 - 1; } static inline int _Left(int i) { return (i+1)*2 - 1; } static inline int _Right(int i) { return (i+1)*2; } void _Max_HeapFy(int *pArray,int cnt,int pos) { int maxpos = pos; int l = _Left(pos); int r = _Right(pos); if(l < cnt && pArray[l] > pArray[pos]) maxpos = l; if(r < cnt && pArray[r] > pArray[maxpos]) maxpos = r; if(maxpos != pos) { exchange(pArray+pos,pArray+maxpos); _Max_HeapFy(pArray,cnt,maxpos); } } void Make_Max_Heap(int *pArray,int cnt) { int i = 0; for(i=_Parent(cnt);i>=0;i--) { _Max_HeapFy(pArray,cnt,i); } } void HeapSort(int *pArray,int cnt) { if(cnt <= 1) return; int i = 0; Make_Max_Heap(pArray,cnt); for(i=cnt-1;i>0;i--) { exchange(pArray+i,pArray); _Max_HeapFy(pArray,i,0); } }
堆排序不容易理解,几个过程都比较复杂,实际应用比较少。如果想深入学习,最好还是看一下算法导论的 第6章 堆排序。
本文只做简单介绍,均手打,简单贴图,具体贴图可参考《算法导论》的介绍。