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

催命判官
• 阅读 3985

题目分析

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

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

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

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

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

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

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

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

看下面的例子:x=5, y=4
算法题解:Count of Smaller Numbers After Self (归并排序的妙用)

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

代码实现(链表)

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)。

阅读资料

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
4年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
4年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
4年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这