【排序】排序算法之---归并排序

来源:互联网 发布:电子竞技 奥运会 知乎 编辑:程序博客网 时间:2024/06/09 21:57

所谓排序,就是要将一堆记录,使之按关键字递增(或递减)次序排列起来。根据排序所采用的策略,可以分为如下五种:

1、插入排序(直接插入排序、希尔排序)

2、交换排序(冒泡排序、快速排序)

3、选择排序(直接选择排序、堆排序)
    4、归并排序;

5、分配排序(桶排序,基数排序);


---------------------------------------------------------------------------------


归并排序(Merge Sort)是利用"归并"技术来进行排序。归并是指将若干个已排序的子文件合并成一个有序的文件。


归并算法:

基本思想:设两个有序的子序列(相当于输入序列)放在同一序列中相邻的位置上:array[low..m],array[m + 1..high],先将它们合并到一个局部的暂存序列 temp (相当于输出序列)中,待合并完成后将 temp 复制回 array[low..high]中,从而完成排序。


在具体的合并过程中,设置 i,j 和 p 三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较 array[i] 和 array[j] 的关键字,取关键字较小(或较大)的记录复制到 temp[p] 中,然后将被复制记录的指针 i 或 j 加 1,以及指向复制位置的指针 p 加 1。重复这一过程直至两个输入的子序列有一个已全部复制完毕(不妨称其为空),此时将另一非空的子序列中剩余记录依次复制到 array 中即可。


下面是合并过程的 C 代码实现:

void merge(int* array, int low, int mid, int high) //这里的mid不一定是在中间,只是表示分割点{    assert(array && low >= 0 && low <= mid && mid <= high);    int* temp = (int*)malloc((high - low + 1) * sizeof(int));//因为归并算法本身要用到o(N)大小的临时数组,可能会很大,所以一定要动态申请成功才能执行    if (!temp) {        printf("Error:out of memory!");        return;    }    int i = low;//归并排序的子序列1的开头    int j = mid + 1;//归并排列的子序列2的开头    int index = 0;//临时数组的开头    while (i <= mid && j <= high) {        if (array[i] <= array[j]) {            temp[index++] = array[i++];        }        else {            temp[index++] = array[j++];        }    }//执行完上面的while循环,说明必有一个子序列已经插入完毕了,剩下只要一口气将另一个子序列插入即可    while (i <= mid) {        temp[index++] = array[i++];    }    while (j <= high) {        temp[index++] = array[j++];    }    memcpy((void*)(array + low), (void*)temp, (high - low + 1) * sizeof(int)) ;//将临时数组的内容copy回原数组array,并释放临时数组    free(temp);}


归并排序 就是要利用到归并算法,将原本无序的记录变为有序(归并算法本身要求归并的两个记录对象均为有序),这就要用到分治或者递归的思想来实现了
归并排序有两种实现方法:自底向上和自顶向下。


自底向上方法,也就是常说的二路归并排序,其基本思想是:第 1 趟排序将长度为 n 的待排序记录看作 n 个长度为 1 的有序子序列,然后将这些子序列两两合并。完成第 1 趟排序之后,将得到 lgn 个长度为 2 的有序子序列(如果 n 为奇数,则最后还有一个长度为 1 的子序列)。第 2 趟排序是在第 1 趟的排序的基础上,将这 lgn 个长度为 2 的子序列两两合并。如此反复,直到最后得到一个长度为n的有序文件为止。从这个排序过程来看,二路归并排序是从将长度为 1 的子序列排序变化到长度为 n 的有序序列,因而是自底向上的。


下面是二路归并排序的 C 代码实现:

// 对 [0, length - 1] 做一趟归并长度为 n  的归并排序,n为要归并的子序列的长度void merge_pass(int* array, int length, int n){    assert(array && length >= 1 && n >= 1);    int i;    int sortLength = 2 * n;    // 归并长度为 n 的两个相邻子序列    for(i = 0; i + sortLength - 1 < length; i = i + sortLength) {        merge(array, i, i + n - 1, i + sortLength - 1);    }//以2n长度为部分进行归并,最后往往会剩余一些不够2n长度的子序列//如果length - 1<i + n - 1,则说明剩的不到n个,即在上次的长度为n/2归并排序中已经成为有序子序列    // 尚有两个子序列,其中后一个长度小于 n, 归并最后两个子序列。    if (length - 1 > i + n - 1) {        merge(array, i, i + n - 1, length - 1);    }}// 用分治法自下向上进行二路归并排序//void merge_sort(int* array, int length){    assert(array && length >= 0);    int n;    for(n = 1; n < length; n = (n << 1)) {//每趟归并的子序列的长度以2倍变长        merge_pass(array, length, n);    }}


自底向上的二路归并排序算法虽然效率较高,但可读性较差(循环实现比递归实现一般效率都要高些)。下面来看看自上而下的递归实现,其可读性要好得多。自上而下的方法是采用分治法思想,具体排序过程分成三个过程:

(1)分解:将当前区间一分为二,即求分裂点 mid = (low + high)/2

(2)求解:递归地对两个子区间 array[low..mid] 和 array[mid + 1..high] 进行归并排序;递归的终结条件:子区间长度为 1(一个记录自然有序)。

(3)合并:将已排序的两个子区间R[low..mid]和R[mid + 1..high]归并为一个有序的区间 array[low..high]。

  

下面即是自上而下方法的 C 代码实现:

void merge_sort_dc_impl(int* array, int low, int high){    assert(array && low >= 0);    int mid;    if (low < high) {        mid = (low + high) >> 1;        merge_sort_dc_impl(array, low, mid);        merge_sort_dc_impl(array, mid + 1, high);        merge(array, low, mid, high);//能执行到这一步说明子序列都已经有序    }}// 用分治法自上向下进行排序void merge_sort_dc(int* array, int length){    assert(array && length >= 0);    merge_sort_dc_impl(array, 0, length - 1);}

时间复杂度分析:
   对长度为 n 的序列进行 lgn 趟二路归并,而每一趟归并的时间复杂度为 O(n),因此归并排序的平均时间复杂度为 nlgn。


空间复杂度分析:

需要与待排记录等大的空间来存储中间变量,因为其空间复杂度为 O(n)。因此,归并排序肯定就不是就地排序了。


补充:

归并排序是稳定排序。若归并排序采用链表存储结构的话,实现起来更加高效。


转载自http://www.cppblog.com/kesalin




原创粉丝点击