数组中的逆序对

数组中的逆序对

http://www.nowcoder.com/questionTerminal/96bd6684e04a44eb80e6a68efc0ec6c5

描述

这是一篇针对初学者的题解。讲述了如何从归并排序的思想到解决本题。
知识点:递归
难度:二星


题解

题目描述:给定一个数组arr, 数组元素各不相同,求arr[i] > arr[j] 且 i < j的个数。

首先还是提出两个问题,带着问题来看题解,我觉得效率更好。
Q1:为什么归并排序需要额外的空间?
Q2:为什么此题的最优解法可以借助归并排序的思想?

方法一:暴力方法

对于此题,按住一个arr[i], 依次判断{i+1 ... n-1]是否满足条件。n为数组的大小。
代码如下:

class Solution {
private:
    const int kmod = 1000000007;
public:
    int InversePairs(vector<int> data) {
        int ret = 0;
        int n = data.size();
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                if (data[i] > data[j]) {
                    ret += 1;
                    ret %= kmod;
                }
            }
        }

        return ret;
    }
};

对于10^5数据,O(N^2)算法显然超时。
时间复杂度:O(N^2)
空间复杂度:O(1)

方法二:归并排序思想

A1: 首先回答一下第一个问题,为什么归并排序需要额外空间?
显然我们知道,归并排序的过程就是,递归划分整个区间为基本相等的左右区间,之间左右区间各只有一个数字,然后就合并两个有序区间。
问题就出在了合并两个有序区间上,需要额外的空间。
为什么呢?
这里我举个例子,比如需要合并的两个有序区间为[3 4] 和 [1 2]
我们需要得到最后的结果为[1 2 3 4], 如果不需要额外的空间的话,是做不到的,
当比较1 和 3 的时候, 1 比 3 小,就会覆盖原来的位置。

A2:回答第二个问题之前,先了解一下归并排序的过程,主要有以下两个操作:

  • 递归划分整个区间为基本相等的左右两个区间
  • 合并两个有序区间

可能看了代码,更好理解:

// 合并过程
void merge__(vector<int> &arr, int l, int mid, int r) {
    // 在这个地方创建额外空间,是一种不好的做法,更好的做法,等下讲
    vector<int> tmp(r - l + 1);
    int i = l, j = mid + 1, k = 0;

    while (i <= mid && j <= r) {
        if (arr[i] >= arr[j]) {
            tmp[k++] = arr[j++];
        }
        else {
            tmp[k++] = arr[i++];
        }
    }

    while (i <= mid) {
        tmp[k++] = arr[i++];
    }
    while (j <= r) {
        tmp[k++] = arr[j++];
    }

    for (k = 0, i = l; i <= r; ++i, ++k) {
        arr[i] = tmp[k];
    }
}

// 递归划分过程
void merge_sort__(vector<int> &arr, int l, int r) {
    // 只有一个数字,则停止划分
    if (l >= r) {
        return;
    }

    int mid = l + ((r - l) >> 1);
    merge_sort__(arr, l, mid);
    merge_sort__(arr, mid + 1, r);
    // 合并两个有序区间
    merge__(arr, l, mid, r);
}
// 要排序的数组 arr
void merge_sort(vector<int>& arr) {
    merge_sort__(arr, 0, arr.size() - 1);
}

明白了归并排序的过程,那么回答问题2.
如果两个区间为[4, 3] 和[1, 2]
那么逆序数为(4,1),(4,2),(3,1),(3,2),同样的如果区间变为有序,比如[3,4] 和 [1,2]的结果是一样的,也就是说区间有序和无序结果是一样的。
但是如果区间有序会有什么好处吗?当然,如果区间有序,比如[3,4] 和 [1,2]
如果3 > 1, 显然3后面的所有数都是大于1, 这里为 4 > 1, 明白其中的奥秘了吧。所以我们可以在合并的时候利用这个规则。

直接上代码:

class Solution {
private:
    const int kmod = 1000000007;
public:
    int InversePairs(vector<int> data) {
        int ret = 0;
        merge_sort__(data, 0, data.size() - 1, ret);
        return ret;
    }


    void merge_sort__(vector<int> &arr, int l, int r, int &ret) {
        if (l >= r) {
            return;
        }

        int mid = l + ((r - l) >> 1);
        merge_sort__(arr, l, mid, ret);
        merge_sort__(arr, mid + 1, r, ret);
        merge__(arr, l, mid, r, ret);
    }

    void merge__(vector<int> &arr, int l, int mid, int r, int &ret) {
        vector<int> tmp(r - l + 1);
        int i = l, j = mid + 1, k = 0;

        while (i <= mid && j <= r) {
            if (arr[i] > arr[j]) {
                tmp[k++] = arr[j++];
                // 奥妙之处
                ret += (mid - i + 1);
                ret %= kmod;
            }
            else {
                tmp[k++] = arr[i++];
            }
        }

        while (i <= mid) {
            tmp[k++] = arr[i++];
        }
        while (j <= r) {
            tmp[k++] = arr[j++];
        }

        for (k = 0, i = l; i <= r; ++i, ++k) {
            arr[i] = tmp[k];
        }
    }
};

刚才提到在函数内部开辟额外空间的做法很不好。因为这样会涉及到频繁的构建 vector 和析构vector,所以比较好的做法是:直接在最外层开辟一个足够大的数组,然后传引用到函数。
代码如下:

class Solution {
private:
    const int kmod = 1000000007;
public:
    int InversePairs(vector<int> data) {
        int ret = 0;
        // 在最外层开辟数组
        vector<int> tmp(data.size());
        merge_sort__(data, tmp, 0, data.size() - 1, ret);
        return ret;
    }

    void merge_sort__(vector<int> &arr, vector<int> &tmp, int l, int r, int &ret) {
        if (l >= r) {
            return;
        }

        int mid = l + ((r - l) >> 1);
        merge_sort__(arr, tmp, l, mid, ret);
        merge_sort__(arr, tmp, mid + 1, r, ret);
        merge__(arr, tmp, l, mid, r, ret);
    }

    void merge__(vector<int> &arr, vector<int> &tmp, int l, int mid, int r, int &ret) {
        int i = l, j = mid + 1, k = 0;

        while (i <= mid && j <= r) {
            if (arr[i] > arr[j]) {
                tmp[k++] = arr[j++];
                ret += (mid - i + 1);
                ret %= kmod;
            }
            else {
                tmp[k++] = arr[i++];
            }
        }

        while (i <= mid) {
            tmp[k++] = arr[i++];
        }
        while (j <= r) {
            tmp[k++] = arr[j++];
        }

        for (k = 0, i = l; i <= r; ++i, ++k) {
            arr[i] = tmp[k];
        }
    }

};

时间复杂度:O(NlogN)
空间复杂度:O(N)

全部评论
楼上的同学,因为(4,3)在递归归并 [4] 、[3]的时候算过一次了
17 回复 分享
发布于 2020-12-09 21:04
ret=ret+(mid-i+1) 为什么就是逆序对数了 没明白
10 回复 分享
发布于 2021-06-16 10:58
为什么两个区间为[4,3]、[1,2]的时候没有逆序对(4,3)呢
7 回复 分享
发布于 2020-10-06 20:33
这个所谓的官方解答内存超限了啊!不行
4 回复 分享
发布于 2021-09-10 11:55
递归是每两个数组的两个元素比较,比较完之后将这个两个数组重新排序成一个有序数组; // 奥妙之处 ret += (mid - i + 1); 这一句是因为两个排序好的数组进行比较,(前一个数组是A,后一个数组是B)A的一个元素比B的一个元素大,就默认A数组该元素后面的元素也比B的这个元素大,B数组下标就+1,可以提高效率。
4 回复 分享
发布于 2022-09-01 20:17 广东
为什么在每一次都mod100007 不是最后就好了嘛
3 回复 分享
发布于 2021-04-19 19:37
为什么要把临时数组复制回原数组?
2 回复 分享
发布于 2022-03-09 11:29
妙啊
2 回复 分享
发布于 2022-09-23 20:55 浙江
为什么每次都要把k置为0重新开始合并,不应该继续上次的下标吗
点赞 回复 分享
发布于 2021-07-12 10:32
r - l 看成 r - 1(数字),调试了好久 [汗~]
点赞 回复 分享
发布于 2021-07-20 23:34
嗯,当时还没注意到这个细节,忘了运算符优先级了
点赞 回复 分享
发布于 2021-10-25 22:37
// 奥妙之处 ret += (mid - i + 1); ret %= kmod; 没搞明白,这个是啥意思
点赞 回复 分享
发布于 2022-07-24 13:21
ret %= kmod;这玩意为啥要写到循环里面? 不就是取余吗,最后返回的时候再取余不行嘛?
点赞 回复 分享
发布于 2022-08-15 13:13
这道题还可以用插入排序加二分查找来写,比归并稍慢些
点赞 回复 分享
发布于 2022-09-21 08:37 河南
我的ret传值每次都会清0
点赞 回复 分享
发布于 2023-03-24 12:00 北京
这个题解,会超出最大值,需要在merge函数的最后对ret再取一次mod。
点赞 回复 分享
发布于 2023-09-13 14:51 北京
可以使用全局变量
点赞 回复 分享
发布于 2023-09-13 14:51 北京
[1,2],[3,4],如果是这种情况,是不是比较的次数和没有归并前是一样的?
点赞 回复 分享
发布于 07-09 12:14 江苏
几点感悟: 1.归并法,因即是果,果即是因,想要什么因,就造什么果。感觉与佛学契合。 2.只传arr和同时传arr,l,r的区别是:前者数组是变的,l和r从数组实时获得,后者数组固定,l和r是变的。前者是真切数组,后者是模拟切,是分段
点赞 回复 分享
发布于 09-22 19:39 湖北

相关推荐

10-30 22:18
已编辑
毛坦厂中学 C++
点赞 评论 收藏
分享
Noob1024:一笔传三代,人走笔还在
点赞 评论 收藏
分享
156 40 评论
分享
牛客网
牛客企业服务