【每日算法】归并排序及其应用(逆序对&合并有序链表)

归并排序是建立在归并操作上的排序算法,是采用分治法(Divide and Conquer)的一个非常典型的应用,它常用来做外排序。

若将两个有序表合并成一个有序表,称为二路归并。

外排序

外排序(External sorting)是指能够处理极大量数据的排序算法。通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。之后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。常见的有外归并排序。

(参考:百度百科)

分治模式

分解:将n个元素分成各含n/2个元素的子序列;
解决:用归并排序法对两个子序列递归地排序;
合并:合并两个已排序的子序列以得到排序结果。

当子序列长度为1时,递归结束,单个元素被视为是已排好序的。

基于以上思路,我们写出归并排序的代码:

void mergeSort(int arr[], int l, int r)
{
    if (NULL == arr || l >= r)
        return ;
    int m = l + (r-l)/2;
    mergeSort(arr, l, m);
    mergeSort(arr, m+1, r);
    merge(arr, l, m, r);
}

接下来的重点就是merge()函数了。

二路归并

merge()函数将两个有序的数组合并成一个有序的数组。

为理解这个过程,我们以摸扑克牌为例:

桌面上有两堆已排好序的牌(比如最上面的牌最小),牌面朝上。我们的任务是将两堆牌合并成一堆有序的牌。

下面开始取牌:

从两堆牌顶上的两张牌中选取较小的一张,将其取出,面朝下放到输出堆中。如果反复取牌直到其中一堆牌为空,接下来只要把剩下那堆牌(如果有的话)的所有牌都取出并放到输出堆中即可。

如果有n张牌那么我至多只需要比较n次,所以合并的时间为O(n)。

我们将上面的想法用代码来实现:

void merge(int arr[], int l, int m, int r)
{
    int index_l = l, index_r = m+1, index_tmp = 0;
    int *tmp = new int[r-l+1];
    if (!tmp)
    {
        cerr << "bad new" << endl;
        return ;
    }
    while (index_l <= m && index_r <= r)
    {
        if (arr[index_l] <= arr[index_r])
            tmp[index_tmp++] = arr[index_l++];
        else
            tmp[index_tmp++] = arr[index_r++];
    }

    while (index_l <= m)
        tmp[index_tmp++] = arr[index_l++];

    while (index_r <= r)
        tmp[index_tmp++] = arr[index_r++];

    for (int i = l; i <=r; ++i)
        arr[i] = tmp[i-l];

    delete [] tmp; //易漏,需细心(●'◡'●)
}

归并排序的时间复杂度为O(n logn),空间复杂度为O(n)。

归并排序是稳定的(当前后有两个相等的元素时,我们优先拿出前面的元素,因此排序后相对顺序仍不变)。

关于时间复杂度的计算,涉及到递归算法的复杂度计算,这里不展开讨论(可能需要用到master theorem),有兴趣的读者可参考《算法导论》第二章。

数组中的逆序对

在数组中的两个数,如果前面一个数字大于后面的数字,则两数构成一个逆序对,比如{7, 5, 6, 4}中,一共有5个逆序对:{7,5}, {7,6}, {7,4}, {5,4}, {6,4}。输入一个数组,求该数组中逆序对的总数。

最简单粗暴的做法:两个for循环,逐个遍历,时间复杂度O(n^2)。

如果这样子就交卷,未免令人失望。

逆序对是两个数之间关系的体现,所以最开始我们可以先看看两个相邻数字之间的关系:

比如{7, 5, 6, 4}中,{7,5}, {6,4}首先构成逆序对。找出这两对逆序对之后,两个相邻数是否构成逆序对的问题就解决了,接下来考虑4个数,则只需要考虑前两个数与后两个数之间的关系了(两个子序列内部的逆序对已经在前面计算了)。为避免重复统计,我们将两个子序列内部排好序。

目前序列变为{5,7,4,6},由于内部的逆序对已经计算过了,所以剩下的逆序对只有以下情况:第一个子序列中的元素大于第二个子序列中的元素,则构成逆序对。

到了这一步,如果你还是想着遍历第一个子序列中的每个元素来跟第二个子序列的每个元素比较的话……神仙也救不了你了!

我们考虑另外一个重要的性质:子序列是已排好序的了。所以,假如你发现第一个子序列中有一个元素m大于第二个子序列中的元素n,那么元素m后面的所有元素必然也将大于元素n。

因此可以想到:如果子序列的元素a <= 子序列的元素b,直接拿掉a,否则,a以及它后面的元素跟b构成逆序对,记录下来,然后b已经没有利用价值了,拿掉,继续比较……

是不是有那么一点点熟悉?我们每一步都拿掉两个序列中较小的一个,这不就是刚刚讲的二路归并吗!而我们的子序列划分、排序,不正是归并排序吗!

所以这个问题归根到底,可以用归并排序轻松解决,只需要增加一步来统计逆序对的个数,就直接把算法复杂度降到O(n logn)了(以牺牲O(n)的空间为代价)。

代码如下:

int inversePairs(int arr[], int l, int r)
{
    if (NULL == arr || l >= r)
        return 0;
    int m = l + (r-l)/2;
    int left = inversePairs(arr, l, m);
    int right = inversePairs(arr, m+1, r);
    int bet = merge(arr, l, m, r);
    return left+right+bet;
}

int merge(int arr[], int l, int m, int r)
{
    int index_l = l, index_r = m+1, index_tmp = 0;
    int cnt = 0;
    int *tmp = new int[r-l+1];

    while (index_l <= m && index_r <= r)
    {
        if (arr[index_l] <= arr[index_r])
        {
            tmp[index_tmp++] = arr[index_l++];
        }
        else
        {
            tmp[index_tmp++] = arr[index_r++];
            cnt += m-index_l+1; //add this line
        }
    }

    while (index_l <= m)
        tmp[index_tmp++] = arr[index_l++];

    while (index_r <= r)
        tmp[index_tmp++] = arr[index_r++];

    for (int i = l; i <=r; ++i)
        arr[i] = tmp[i-l];

    delete [] tmp;

    return cnt;
}

合并两个有序的链表

给定两升序链表,合并两个链表使得新链表仍升序,链表节点定义如下:

struct ListNode
{
    int value;
    ListNode* next;
};

本题很明显可以使用二路归并,不过操作的是指针,所以需要特别注意代码的鲁棒性。

    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) 
    {
        if (NULL == l1) return l2;
        if (NULL == l2) return l1;

        ListNode *head, *cur;

        if (l2->val < l1->val) 
        {
            head = l2;
            l2 = l2->next;
        }
        else 
        {
            head = l1;
            l1 = l1->next;
        }

        cur = head;
        while (l1 && l2) 
        {
            if (l1->val <= l2->val) 
            {
                cur->next = l1;
                cur = cur->next;
                l1 = l1->next;
            }
            else 
            {
                cur->next = l2;
                cur = cur->next;
                l2 = l2->next;
            }
        }

        if (l1) 
        {
            cur->next = l1;
        }

        if (l2) 
        {
            cur->next = l2;
        }

        return head;
    }

以上就是关于归并排序的介绍,下一次将介绍堆排序~

每天进步一点点,Come on!

(●’◡’●)

本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢!

你可能感兴趣的:(归并排序,排序算法,逆序对,合并链表,外排序)