算法题解:Count of Smaller Numbers After Self (归并排序的妙用)

题目分析

题目链接:https://leetcode.com/problems...

上一篇题解中,我们介绍了如何通过在扫描输入的过程中维护一个有序的数据结构来为新输入的计算提供信息。
但是我们同时也发现,支持相关操作的容器(既能够进行二分查找又能够在常数时间内插入元素)似乎没有;如果自己构造一棵二叉搜索树,也会烦恼于树的平衡问题。

在这篇文章中我们介绍另一种方案,虽然它同样利用了排序,但是它并不维护某种数据结构。

归并排序体现了一种经典的分治策略,它不仅是一种稳定的排序(在下面的算法种就利用了这点),而且时间复杂度能稳稳地保证在O(nlogn)内(且不引入复杂的数据结构~)。

多亏了分治的思想,归并排序天然地就有“左”和“右”的概念:将要排序的数组分成左半部分右半部分,将它们分别排好序(递归),然后合并它们(由于已经排好序,合并将会非常高效)。

假设y在x的右边,且y<x,那么y应该什么时候使x的答案+1呢?

这是一种比较通用的思考方法,先假设符合的条件存在,然后追踪这些条件是如何对结果产生影响的。

按照上面的假设,必定存在某次合并,x在左半部分,y在右半部分。在这次合并中,当选择x进行合并的时候,y必定已经合并了。因此,当合并x的时候,右半部分已经合并的那些数字,都是在x右边、比x小的数字。

看下面的例子:x=5, y=4

针对链表的归并排序和针对数组的归并排序虽然思路相同,但在实现上有一些值得留意的区别,下面给出两种归并排序的实现。

代码实现(链表)

class Solution {
 public:
  vector<int> countSmaller(vector<int> &nums) {
    vector<int> right_smaller(nums.size(), 0);
    list<pair<int, int>>
        li;  // pair中first为这个数字在nums中的下标,second为这个数字
    for (int i = 0; i < nums.size(); i++) {
      li.push_back(make_pair(i, nums[i]));
    }
    merge_sort(li, right_smaller);
    return right_smaller;
  }

  void merge_sort(list<pair<int, int>> &li, vector<int> &right_smaller) {
    if (li.size() <= 1) return;
    // 将li分为左半部分和右半部分
    list<pair<int, int>> li_left;
    auto fast = li.begin();
    while (fast != li.end()) {
      // fast每前进2步,才将li最前面的一个元素移动到li_left
      fast++;
      if (fast != li.end()) {
        fast++;
        li_left.push_back(li.front());
        li.pop_front();
      }
    }
    // 循环结束时fast指向链表末尾,li_left包含原li的前一半,li包含原li的后一半
    list<pair<int, int>> li_right;
    // 将li的剩余部分移动到li_right(li同时被清空)
    li_right.splice(li_right.begin(), li);
    // 将左右部分分别排好序
    merge_sort(li_left, right_smaller);
    merge_sort(li_right, right_smaller);
    // 合并两个排好序的表
    merge(li_left, li_right, li, right_smaller);
  }

  void merge(list<pair<int, int>> &left, list<pair<int, int>> &right,
             list<pair<int, int>> &output, vector<int> &right_smaller) {
    while (!left.empty() && !right.empty()) {
      // 先合并较大的表头
      if (left.front().second > right.front().second) {
        // 如果左表头比右表头大,将左表头加入output
        // 右表剩余的所有元素都比左表头小
        right_smaller[left.front().first] += right.size();
        output.push_back(left.front());
        left.pop_front();
      } else {
        output.push_back(right.front());
        right.pop_front();
      }
    }
    if (!left.empty()) {
      output.splice(output.end(), left);
    }
    if (!right.empty()) {
      output.splice(output.end(), right);
    }
  }
};

上面的实现与说明中的略有不同,归并排序是从大到小的(大的数字先加入输出链表),这样,当合并x的时候,右半部分还剩下的那些数字,都是在x右边、比x小的数字。

代码实现(数组)

class Solution {
 public:
  vector<int> countSmaller(vector<int> &nums) {
    vector<int> right_smaller(nums.size(), 0);
    vector<pair<int, int>> vec(nums.size());
    for (int i = 0; i < nums.size(); i++) {
      // first是在nums中的下标,second是数字
      vec[i] = make_pair(i, nums[i]);
    }
    merge_sort(vec, 0, vec.size(), right_smaller);
    return right_smaller;
  }

  // 对vec的[start, end)范围进行归并排序
  void merge_sort(vector<pair<int, int>> &vec, int start, int end,
                  vector<int> &right_smaller) {
    if (end - start <= 1) return;
    int mid = (start + end) >> 1;
    merge_sort(vec, start, mid, right_smaller);
    merge_sort(vec, mid, end, right_smaller);
    merge(vec, start, mid, end, right_smaller);
  }

  void merge(vector<pair<int, int>> &vec, int start, int mid, int end,
             vector<int> &right_smaller) {
    auto it_start = vec.begin() + start;
    auto it_mid = vec.begin() + mid;
    auto it_end = vec.begin() + end;
    // 合并前,将左半部分复制到left,右半部分复制到right
    vector<pair<int, int>> left(it_start, it_mid), right(it_mid, it_end);
    // 记录左边已经合并的数量、右边已经合并的数量、总共已经合并的数量
    int left_merged = 0, right_merged = 0, total_merged = 0;
    while (left_merged < left.size() && right_merged < right.size()) {
      // 较小的表头先合并
      if (left[left_merged].second <= right[right_merged].second) {
        // 合并时,右表已经合并的元素都比左表头小
        right_smaller[left[left_merged].first] += right_merged;
        vec[start + total_merged] = left[left_merged];
        ++left_merged;
        ++total_merged;
      } else {
        vec[start + total_merged] = right[right_merged];
        ++right_merged;
        ++total_merged;
      }
    }
    while (left_merged < left.size()) {
      right_smaller[left[left_merged].first] += right_merged;
      vec[start + total_merged] = left[left_merged];
      ++left_merged;
      ++total_merged;
    }
    while (right_merged < right.size()) {
      vec[start + total_merged] = right[right_merged];
      ++right_merged;
      ++total_merged;
    }
  }
};

时间复杂度

该算法仅仅在归并排序的过程中增加了一些O(1)的记录性操作,因此时间复杂度与归并排序相同,O(nlogn)。

阅读资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值