高效排序(归并排序)
这里开始分析比初级排序更高效的方法——归并排序。归并排序的核心思想为将两个有序的数组合并成一个更大的有序数组。对一个数组进行排序,可以将它分成两部分分别进行排序,然后将结果归并起来。此时不难发现,分割的子数组同样可以利用切分再归并的方式进行排序。这是一个递归调用的过程。
归并方法
实现归并的一种直截了当的办法是将两个不同的有序数组归并到第三个数组,创建一个适当大小的数组然后将两个数组中的元素一个个从小到大放入这个数组中。(后面会说明这种方式不可取,但不影响拿来抽象分析)
开始需要两个标记位
i,j
i
,
j
,分别表示两个待合并数组
A,B
A
,
B
的当前访问元素位置,然后比较这两个元素的大小将最小元素存入到第三个数组
C
C
中。此外,最小元素对应数组中的标记位后移一位。然后重复上述步骤。我们会发现需要判断4种情况:
① 数组用完了,此时将 B B 后面元素依次存入即可;
② B B 数组用完了,此时将后面元素依次存入 C C 即可;
③ , 此时将 B[j] B [ j ] 存入 C C 中,并且将加1;
④ A[i]<B[j] A [ i ] < B [ j ] , 此时将 A[i] A [ i ] 存入 C C 中,并且将加1。当用归并排序一个大数组时,需要调用很多次归并,因此每次归并都创建一个新数组存储排序结果显然不可取。这里可以通过创建一个共享的辅助数组解决。
//声明 template < class T > class SortMethod { private: void merge(T a[], int lo, int mid , int hi); T * aux; } // 定义 template<class T> void SortMethod<T>::merge(T a[], int lo, int mid , int hi) { assert(aux != NULL); // auxiliary array int i = lo; j = mid+1; for(int k = lo; k <= hi; k++ ) // copy source data aux[k] = a[k]; for(int k = lo; k <= hi; k++ ) { if (i > mid) a[k] = aux[j++]; // ① else if (j > hi) a[k] = aux[i++]; // ② else if (less(aux[i],aux[j])) a[k] = aux[i++]; // ④ else a[k] = aux[j++]; // ③ } }
数组归并轨迹如图:
自顶向下归并
自定向下即是分治思想的一种典型应用,将长度为N的大数组一分为二,二分为四,循环往复,直至分到1为止。此时,单元素的数组即可认为是已排序数组,需要强调的是在每次切分排序后都需要对子数组归并。
// 声明 template < class T> class SortMethod { public: void merge_sort(T a[], int N); private: void merge_sort_Up2Down(T a[], int lo, int hi); } // 定义 template<class T> void SortMethod<T>::merge_sort_Up2Down(T a[], int lo, int hi) { if(lo >= hi) return; int mid = (lo + hi)/2; // partition merge_sort_Up2Down(a,lo,mid); // sort left part merge_sort_Up2Down(a,mid+1,hi); // sort right part // merge merge(a,lo,mid,hi); } template<class T> void SortMethod<T>::merge_sort(T a[], int N) { aux = new T[N]; merge_sort_Up2Down(a,0,N-1); delete [] aux; } // 实例化 string str = "MERGESORTEXAMPLE"; char* data = (char*)str.c_str(); int N = str.length(); SortMethod<char>::print(data,N); SortMethod<char> merger; merger.merge_sort(data,N); SortMethod<char>::print(data,N); bool sorted = SortMethod<char>::isSortd(data,N); cout<<"sorted: "<<sorted<<endl; >> M E R G E S O R T E X A M P L E A E E E E G L M M O P R R S T X sorted: 1
自顶向下的归并排序中归并结果轨迹如图。
结论: 对于长度为N的数组,自顶向下的归并排序需要 1/2N∗lgN 1 / 2 N ∗ l g N 至 N∗lgN N ∗ l g N 次比较,最多需要访问数组 6N∗lgN 6 N ∗ l g N 次。
此外,可通过以下三种方式优化自顶向下归并排序的效率。
1. 小规模子数组使用插入排序。
2. 对子数组排序前先判断是否有序。
3. 辅助数组与输入排序数组循环交换角色。自底向上归并
实现归并的另外一种方法是,首先归并微型数组,然后在归并得到的数组,如此重复,直到将整个数组归并到一起。简单来说,对长度为N的数组,先两两归并,然后四四归并……直到前一半N/2与剩余归并。
// 声明 // 声明 template < class T> class SortMethod { private: void merge_sort_Down2Up(T a[], int N); } // 定义 template<class T> void SortMethod<T>::merge_sort_Down2Up(T a[], int N ) { for(int sz = 1; sz < N; sz *= 2 ) for (int lo = 0; lo < N - sz; lo += 2*sz ) { int mid = lo + sz-1; int hi = MIN(lo + 2*sz -1 , N-1); merge(a,lo,mid,hi); } } template<class T> void SortMethod<T>::merge_sort(T a[], int N) { aux = new T[N]; //merge_sort_Up2Down(a,0,N-1); merge_sort_Down2Up(a,N); delete [] aux; } >>> M E R G E S O R T E X A M P L E A E E E E G L M M O P R R S T X sorted: 1
自底向上归并的过程如图。
结论:对于长度为N的数组,自底向上的归并排序需要的比较次数与自顶向下归并排序具有相同范围,最多访问数组次数也相同。当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和访问数组次数正好相同,只是顺序不同。
归并排序局限性
归并排序是一种渐进最优的基于比较排序的算法,因为归并排序在最坏情况下的比较次数和任意基于比较的排序算法所需的最少比较次数都是 N∗lgN N ∗ l g N 。但是归并排序同样具有一定局限性。
- 归并排序的空间复杂度不是最优;
- 在实践中不一定会遇到最坏情况,此时归并排序不一定更优;
- 算法中的访问数组次数同样制约算法效率。
- 不通过比较 也能将某些数据排序。