算法题解: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)。