hackerrank: Insertion Sort Advanced Analysis

Insertion Sort Advanced Analysis

Insertion Sort is a simple sorting technique which was covered in
previous challenges. Sometimes, arrays may be too large for us to wait
around for insertion sort to finish. Is there some other way we can
calculate the number of times Insertion Sort shifts each elements when
sorting an array?

If ki is the number of elements over which ith element of the array
has to shift then total number of shift will be k1 + k2 + … + kN.

Input: The first line contains the number of test cases T. T test
cases follow. The first line for each case contains N, the number of
elements to be sorted. The next line contains N integers
a[1],a[2]…,a[N].

Output: Output T lines, containing the required answer for each test
case.

Constraints: 1 <= T <= 5 1 <= N <= 100000 1 <= a[i] <= 1000000

Sample Input:

2 5 1 1 1 2 2 5 2 1 3 1 2

Sample Output:

0 4

Explanation First test case is already sorted therefore there’s no
need to shift any element. In second case it will proceed in following
way.

Array: 2 1 3 1 2 -> 1 2 3 1 2 -> 1 1 2 3 2 -> 1 1 2 2 3 Moves: —
1 — 2 — 1 = 4

通俗易懂的 O(N^2) 算法

这道题其实就是求一个数组中的逆序对的数量(做题时还不知道这叫逆序对呢,-_-!!!)。

最直白的解法就包含在题目标题里——插入排序 (O(N^2)):

int solve(vector<int> &v) {
    int c = 0;
    // 插入排序
    for (int i = 1; i < v.size(); ++i) {
        int tmp = v[i];
        int j = i - 1;
        for (; j >= 0 && v[j] > tmp; --j) {
            v[j + 1] = v[j];
            // 在这里加一个计数变量就行了
            ++c;
        }
        v[j + 1] = tmp;
    }

    return c;
}

这种方法的好处是简单明了,直接模拟人脑数数过程。但缺点也很明显——效率太低。
在本题里,N 的上限是 100000,而插入排序的时间复杂度是 O(N^2),显然会超时。

那如何提高“数数”效率呢?

我们先在脑海里模拟一下插入排序的数数过程:

假如有这样一个数组,

8 7 2 1 3 1 2

先来计算 8 的逆序对数,简单,数一下它前面有几个大于它的数就行了:

嗯,它前面没有数,也就没有逆序对数了

下面计算 7 的:

8,有一个

然后是 2:

8、7,有两个

1 的:

8、7、2,三个

一直继续……

发现没有,8、7 组成的区间(以及其他之前计算时出现过的区间)被反复数了好几遍,这太浪费时间了吧?!

如果能把之前区间的计算结果保存下来,用到时直接查询,消除重复“劳动”,“数数”效率自然会大大提高。

O(NlogN) 算法隆重登场

通过前面的分析,我们知道,如果能够保存之前工作的状态,就能比较高效地解决这个问题了。从划分区间入手,很容易想到归并排序。它的时间复杂度是 O(NlogN),解决本题绰绰有余。

归并解法:

long long mergeSort(vector<int> &v) {
    // 归并程序
    if (v.size() <= 1) {
        return 0;
    }

    long long ans = 0;
    int m = v.size() / 2;
    vector<int> v1(m);
    copy(v.begin(), v.begin() + m, v1.begin());
    vector<int> v2(v.size() - m);
    copy(v.begin() + m, v.end(), v2.begin());
    // 将分两段处理的结果加起来
    ans += mergeSort(v1);
    ans += mergeSort(v2);

    auto it = v.begin();
    auto it1 = v1.begin();
    auto it2 = v2.begin();
    while (it1 != v1.end() && it2 != v2.end()) {
        if (*it1 <= *it2) {
            *it++ = *it1++;
        } else {
            // *it1 及至分段结束的值都大于 *it2,它们都可以跟 *it2 组成逆序对
            ans += v1.end() - it1;
            *it++ = *it2++;
        }
    }
    while (it1 != v1.end()) {
        *it++ = *it1++;
    }
    while (it2 != v2.end()) {
        *it++ = *it2++;
    }

    return ans;
}

提交,OK,问题得解。

后记

这是我在做这道题时的思考过程。超时后想了很久怎么优化,最终解决了问题真的很开心。在这个过程中,自己对分治思想和备忘录法也有了更深的理解,还学到了线段树、树状数组等新的数据结构,觉得有必要写下来提醒自己,希望对大家也有所帮助。

你可能感兴趣的:(算法,归并,逆序对)