排序算法 —— 归并排序

归并排序算法

1.划分问题:把序列分成元素个数尽量相等的两半。

2.递归求解:把两半元素分别排序。

3.合并问题:把两个有序表合并成一个。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

算法代码

void merge_sort(int *A, int x, int y, int *T) {
    if (y - x > 1) //等价于x+1<y,即左半部分的右边界还在右半部分的左边界左边
    {
        int m = x + (y - x) / 2;
        //划分
        int p = x, q = m, i = x;
        merge_sort(A, x, m, T);
        //递归求左解
        merge_sort(A, m, y, T);
        //递归求右解
        while (p < m || q < y)
        {
            if (q >= y || (p < m && A[p] <= A[q])) T[i++] = A[p++];
            //从左半数组复制到临时空间
            else T[i++] = A[q++];
            //从右半数组复制到临时空间
        }
        for (i = x; i < y; ++i) A[i] = T[i];
        //从辅助空间复制回A数组
    }
}

动图演示

在这里插入图片描述

实战演练

#include <iostream>

using namespace std;
int A0[15] = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
int T0[15];

void merge_sort(int *A, int x, int y, int *T) {
    if (y - x > 1) //等价于x+1<y,即左半部分的右边界还在右半部分的左边界左边
    {
        int m = x + (y - x) / 2;
        //划分
        int p = x, q = m, i = x;
        merge_sort(A, x, m, T);
        //递归求左解
        merge_sort(A, m, y, T);
        //递归求右解
        while (p < m || q < y)
        {
            if (q >= y || (p < m && A[p] <= A[q])) T[i++] = A[p++];
            //从左半数组复制到临时空间
            else T[i++] = A[q++];
            //从右半数组复制到临时空间
        }
        for (i = x; i < y; ++i) A[i] = T[i];
        //从辅助空间复制回A数组
    }
}

int main() {
    for (int m = 0; m < 15; ++m) {
        cout << A0[m] << ' ';
    }
    cout << endl;
    merge_sort(A0, 0, 15, T0);
    for (int n = 0; n < 15; ++n) {
        cout << A0[n] << ' ';
    }
    cout << endl;
    return 0;
}
3 44 38 5 47 15 36 26 27 2 46 4 19 50 48
2 3 4 5 15 19 26 27 36 38 44 46 47 48 50

算法精讲

首先,只要有一个序列非空,就要继续合并(while (p < m || q < y)),因此在比较时不能直接比较A[p]和A[q],因为可能其中一个序列为空,从而A[p]或者A[q]代表的是一个实际不存在的元素。
~如果第二个排序为空(此时第一个序列一定非空),复制A[p];
~否则(第二个序列非空),当且仅当第一个序列也非空,且A[p]<=A[q]时,才复制A[p]。

算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

逆序对问题

给一列数a1,a2,……,an,求它的逆序对数,即有多少个有序对(i,j),使得i<j但ai>aj。n可以高达106

分析

分治三步法:
“划分问题”:把序列分成元素个数尽量相等的两半;
“递归求解”:统计i和j均在左边或者右边的逆序对个数;
“合并问题”:统计i在左边,但j在右边的逆序对个数。

关键在于合并:如何求出i在左边,而j在右边的逆序对数目?
统计的常见技巧是“分类”:只要对于右边的每个j,统计左边比它大的元素的个数f(i),则所有f(j)之和便是答案。

归并排序可以“顺便”完成f(j)的计算:由于合并操作是从小到大进行的,当右边的A[j]复制到T中时,左边还没来得及复制到T的那些数就是左边所有比A[j]大的数,此时在累加器中加上左边元素个数m-p即可(左边所剩的元素在区间[p,m)中,因此元素个数为m-p)。

代码

#include <iostream>
using namespace std;
int cnt=0,T0[15];
int A0[15] = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
void Reverse(int *A,int x,int y,int *T)
{
    if (y>x+1)
    {
        int m=x+(y-x)/2;
        int p=x,q=m,i=x;
        Reverse(A,x,m,T);
        Reverse(A,m,y,T);
        while (p<m||q<y)
        {
            if (q>=y||(p<m&&A[p]<=A[q]))
                T[i++]=A[p++];
            else
            {
                T[i++]=A[q++];
                cnt+=m-p;
            }
        }
        for (int i = x; i < y; ++i)
            A[i]=T[i];
    }
}
int main()
{
    Reverse(A0, 0, 15, T0);
    cout<<"cnt="<<cnt<<endl;
    return 0;
}
原文地址:https://www.cnblogs.com/AlexKing007/p/12338481.html