LeetCode 315—— 计算右侧小于当前元素的个数

阅读目录

    • 1. 题目
    • 2. 解题思路一
    • 3. 代码实现一
    • 4. 解题思路二
    • 5. 代码实现二

1. 题目

2. 解题思路一

参考 剑指 Offer——数组中的逆序对,我们依然借助于归并排序中的合并操作来计算某个元素右侧小于它的元素个数。

如上图最左边所示,第五行开始进行第一次合并过程。因为 11 > 8 11>8 11>8,所以, 11 11 11 右边小于它的元素个数加 1 1 1,也就是上图最右侧第二行的数组(我们记为 c o u n t s counts counts 数组)第 0 0 0 个元素变为 1 1 1。同理,数字 7 7 7 所在的位置,数组的第 4 4 4 个元素也要加 1 1 1

这次合并后,有一些元素的位置更改了,比如 11 11 11 变到了第 1 1 1 个位置,后面合并过程如果再遇到有比 11 11 11 小的元素,我们需要对 c o u n t s [ 0 ] counts[0] counts[0] 的值进行更改(也就是 11 11 11 在原数组中的位置 0 0 0),而不是修改 c o u n t s [ 1 ] counts[1] counts[1] (也就是 11 11 11 在当前合并后的位置 1 1 1

所以,我们还需要一个索引数组来记录每个数字在原始数组中的索引位置,这里我们记为 s r c _ i n d e x src\_index src_index 数组,也就是上图中间的数组,这样,不管数字 11 11 11 在排序后变到了哪个位置,我们都可以获取到它的原始索引。

继续往后看,下一步,我们需要合并左边的有序数组 [ 8 , 11 ] [8, 11] [8,11] 和右边的有序数组 [ 3 , 9 ] [3, 9] [3,9],首先,我们判断出 8 > 3 8>3 8>3,那么 8 8 8 以及它后面的元素对应的 c o u n t s counts counts 数组的值都需要增 1 1 1。然后 8 < 9 8<9 8<9,不需要做什么。最后, 11 > 9 11>9 11>9,那么 11 11 11 对应的 c o u n t s [ 0 ] counts[0] counts[0] 继续增 1 1 1 变为 3 3 3

这样理解起来没什么问题,但若是实现代码,那么每次遇到右边元素比左边小,我们都需要循环左边有序区间还未进行归并的元素,来对它们的 c o u n t s counts counts 数组进行修改,这样每次归并过程就不是只访问所有待合并元素一遍,那么总的算法时间复杂度也就不是 O ( n l o g n ) O(nlogn) O(nlogn) 了, LeetCode 上部分测试用例也会通不过,会报一个超出时间限制的错误,如下图所示。

我们换个思路,只在归并到左边区间元素的时候更新 c o u n t s counts counts 数组即可。还是上面的过程,首先,我们判断出 8 > 3 8>3 8>3,这时候需要放置右边的元素 3 3 3,所以我们什么都不做。然后 8 < 9 8<9 8<9,我们需要放置左边的元素 8 8 8,这时候我们去检查右边区间的元素已经放置了多少个,那么 c o u n t s counts counts 数组就需要增加多少,发现右边只放了一个 3 3 3,所以 c o u n t s [ 1 ] + = 1 counts[1]+=1 counts[1]+=1 。继续判断 11 > 9 11>9 11>9,需要把 9 9 9 放置过去, c o u n t s counts counts 数组不变。最后,我们需要放置左边的元素 11 11 11,这时候发现右边区间放置了 3 , 9 3, 9 3,9 两个元素 ,所以 c o u n t s [ 0 ] + = 2 counts[0]+=2 counts[0]+=2

3. 代码实现一

class Solution {
public:vector<int> sorted_nums; // 合并过程中存放元素的临时数组vector<int> counts; // 存放结果vector<int> src_idxs; // 存放排序后元素对应的原始索引vector<int> sorted_idxs; // 合并过程中存放元素原始索引的临时数组void MergeArray(vector<int>& nums, int left, int mid, int right) {int i = left;int j = mid + 1;int st = 0;while (i <= mid && j <= right) {if (nums[i] <= nums[j]) {// 正确做法是在放置左边元素的时候,一次性更新counts数组// 这时候,右边区间[mid+1, j)位置的元素都比左边当前元素小counts[src_idxs[i]] += j - mid - 1;sorted_idxs[st] = src_idxs[i];sorted_nums[st++] = nums[i++];} else {// 这里,每次遇到右边元素比左边元素小// 就更新左边区间当前元素及后面元素的counts数组// 时间复杂度不满足O(nlogn),会超时// for (int m = i; m <= mid; ++m) {//     counts[src_idxs[m]]++;// }sorted_idxs[st] = src_idxs[j];sorted_nums[st++] = nums[j++];}}while (i <= mid) {counts[src_idxs[i]] += j - mid - 1;sorted_idxs[st] = src_idxs[i];sorted_nums[st++] = nums[i++];}while (j <= right) {sorted_idxs[st] = src_idxs[j];sorted_nums[st++] = nums[j++];}for (int i = 0; i < right-left+1; ++i) {src_idxs[left+i] = sorted_idxs[i];nums[left+i] = sorted_nums[i];}}void MergeSort(vector<int>& nums, int left, int right) {int mid = left + (right - left) / 2;if (left < right) {MergeSort(nums, left, mid);MergeSort(nums, mid+1, right);MergeArray(nums, left, mid, right);}}vector<int> countSmaller(vector<int>& nums) {sorted_nums.reserve(nums.size());sorted_idxs.reserve(nums.size());counts = vector<int>(nums.size(), 0);for (int i = 0; i < nums.size(); ++i) {src_idxs.push_back(i);}MergeSort(nums, 0, nums.size()-1);return counts;}
};

4. 解题思路二

假设数据依然为 [ 11 , 8 , 3 , 9 , 7 , 1 , 2 , 5 ] [11, 8, 3, 9, 7, 1, 2, 5] [11,8,3,9,7,1,2,5],同时我们准备了 8 8 8 个桶来分别放置每个元素,然后我们从后往前开始遍历元素并将它们放入对应的桶内。当放置到元素 7 7 7 时,前面四个桶内的元素个数也就是右侧小于 7 7 7 的元素个数。

但是,如果我们计算右侧小于 7 7 7 的元素个数时要对前 4 4 4 个桶求和,而计算右侧小于 11 11 11 的元素个数时要对前 7 7 7 个桶求和,那么这时候的算法复杂度是 O ( n 2 ) O(n^2) O(n2),就和暴力求解法没有区别了。

所以,如果我们寻找到一种能在 O ( l o g n ) O(logn) O(logn) 复杂度下计算出前 n n n 个桶的和的算法,那么总的时间复杂度就变成了 O ( n l o g n ) O(nlogn) O(nlogn),问题也就迎刃而解了 。

这时候,我们只需要借助于一种数据结构——树状数组即可。


上图中的 a [ 1 ] − a [ 8 ] a[1]-a[8] a[1]a[8] 就代表我们上面说的每个桶里面的元素个数,然后数组 c c c 有:

c [ 1 ] = a [ 1 ] c [ 2 ] = a [ 1 ] + a [ 2 ] c [ 3 ] = a [ 3 ] c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c [ 5 ] = a [ 5 ] c [ 6 ] = a [ 5 ] + a [ 6 ] c [ 7 ] = a [ 7 ] c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] \begin{gather*} c[1] &=& a[1] \\ c[2] &=& a[1]+a[2] \\ c[3]&=&a[3] \\ c[4]&=&a[1]+a[2]+a[3]+a[4] \\ c[5]&=&a[5] \\ c[6]&=&a[5]+a[6] \\ c[7]&=&a[7] \\ c[8]&=&a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] \\ \end{gather*} c[1]c[2]c[3]c[4]c[5]c[6]c[7]c[8]========a[1]a[1]+a[2]a[3]a[1]+a[2]+a[3]+a[4]a[5]a[5]+a[6]a[7]a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

这样,比如我们要求前 5 5 5 个桶内的和时,只需要求 c [ 4 ] + c [ 5 ] c[4]+c[5] c[4]+c[5] 即可;求前 7 7 7 个桶内的和时,只需要求 c [ 4 ] + c [ 6 ] + c [ 7 ] c[4]+c[6]+c[7] c[4]+c[6]+c[7] 即可。

而当某个桶内元素增加的时候,我们需要同步更新对应的数组 c c c。比如,第二个桶内元素也即 a [ 2 ] a[2] a[2] 更新的时候, c [ 2 ] , c [ 4 ] , c [ 8 ] c[2], c[4], c[8] c[2],c[4],c[8] 都需要同步进行更新。

c [ k ] c[k] c[k] 管理的元素个数为区间 a [ k − l o w b i t ( k ) + 1 , k ] a[k-lowbit(k)+1, k] a[klowbit(k)+1,k],其中 l o w b i t ( k ) lowbit(k) lowbit(k) 代表 k k k 的二级制中从最低位的一个 1 1 1 开始的后面所有位所表示的数字。比如, 3 3 3 的二进制为 011 011 011,最低位的一个 1 1 1 在倒数第一位,所以 l o w b i t ( 1 ) = 1 lowbit(1)=1 lowbit(1)=1 c [ 3 ] c[3] c[3] 管理的元素个数为区间 a [ 3 , 3 ] a[3, 3] a[3,3] 6 6 6 的二进制为 110 110 110,最低位的一个 1 1 1 在倒数第二位,所以 l o w b i t ( 6 ) = 1 0 2 进制 = 2 lowbit(6)=10_{2进制}=2 lowbit(6)=102进制=2 c [ 6 ] c[6] c[6] 管理的元素个数为区间 a [ 5 , 6 ] a[5, 6] a[5,6] 8 8 8 的二进制为 1000 1000 1000,最低位的一个 1 1 1 在倒数第四位,所以 l o w b i t ( 8 ) = 100 0 2 进制 = 8 lowbit(8)=1000_{2进制}=8 lowbit(8)=10002进制=8 c [ 8 ] c[8] c[8] 管理的元素个数为区间 a [ 1 , 8 ] a[1, 8] a[1,8]

求和过程:

  • 所以,如果我们要求前 k k k 个元素的和时,第一步我们先拿到 c [ k ] c[k] c[k] 也即是 a [ k − l o w b i t ( k ) + 1 , k ] a[k-lowbit(k)+1, k] a[klowbit(k)+1,k] 的和。

  • 下一步我们跳到 k 1 = k − l o w b i t ( k ) k_1=k-lowbit(k) k1=klowbit(k),这时候我们可以拿到 c [ k 1 ] c[k_1] c[k1] 也即是 a [ k 1 − l o w b i t ( k 1 ) + 1 , k − l o w b i t ( k ) ] a[k_1-lowbit(k_1)+1, k-lowbit(k)] a[k1lowbit(k1)+1,klowbit(k)] 的和。

  • 依次往下,直到最后一个区间的左边界为 1 1 1,我们可以拿到 c [ k n − 1 ] c[k_{n-1}] c[kn1] 也即是 a [ 1 , k n − 1 ] a[1, k_{n-1}] a[1,kn1] 的和。

  • 再往下就到了第零个元素,循环结束,我们也就得到了前 k k k 个元素的和。

同理,某一个 a [ k ] a[k] a[k] 更新的时候,我们需要找到所有管理区间包含 a [ k ] a[k] a[k] c c c 进行更新。

更新过程:

  • 首先, c [ k ] c[k] c[k] 管理的元素个数为区间 a [ k − l o w b i t ( k ) + 1 , k ] a[k-lowbit(k)+1, k] a[klowbit(k)+1,k],所以,我们第一个先更新 c [ k ] c[k] c[k]
  • 然后,跳到 k 1 = k + l o w b i t ( k ) k_1=k+lowbit(k) k1=k+lowbit(k),更新 c [ k 1 ] c[k_1] c[k1],其中, c ( k , k + l o w b i t ( k ) ) c(k, k+lowbit(k)) c(k,k+lowbit(k)) 都不包含 a [ k ] a[k] a[k],具体证明可参考树状数组中的性质3 ;
  • 依次往上,直到 k n − 1 k_{n-1} kn1 大于数组 a a a 的大小,我们停止更新。

那么,现在还有最后一个问题,怎么求 l o w b i t ( k ) lowbit(k) lowbit(k) 呢?

负数的补码等于其对应的正数,符号位取反,其余位取反再加 1 1 1

k = 0 … … ⏞ 任意个 0 , 1 1 … … ⏞ 任意个 0 k = 0\overbrace{……}^{任意个0,1}1\overbrace{……}^{任意个0} k=0…… 任意个0,11…… 任意个0
− k = 1 … … ⏞ x 的对应位取反 1 … … ⏞ 任意个 1 -k = 1\overbrace{……}^{x的对应位取反}1\overbrace{……}^{任意个1} k=1…… x的对应位取反1…… 任意个1
k & ( − k ) = … … ⏞ 任意个 0 1 … … ⏞ 任意个 0 k \& (-k) = \overbrace{……}^{任意个0}1\overbrace{……}^{任意个0} k&(k)=…… 任意个01…… 任意个0

所以,正数与相反数按位取反就正好得到了我们想要的 l o w b i t ( k ) lowbit(k) lowbit(k),完美!

5. 代码实现二

class Solution {
public:vector<int> c;int lowbit(int k) {return k & (-k);}void update(int k, int n) {while (k < n) {c[k] += 1;k += lowbit(k);}}int query(int k) {int sum = 0;while (k > 0) {sum += c[k];k -= lowbit(k);}return sum;}vector<int> countSmaller(vector<int>& nums) {// 排序,去重,看看需要几个桶vector<int> bucket = nums;sort(bucket.begin(), bucket.end());bucket.erase(unique(bucket.begin(), bucket.end()), bucket.end());// 初始化树状数组,第0个位置不使用c = vector<int>(bucket.size()+1, 0);int n = c.size();vector<int> counts(nums.size(), 0);// 从后往前把每个元素放入桶内for (int i = int(nums.size())-1; i >= 0; --i) {// 当前元素应该放进哪个桶内,第0个桶对应a[1]int idx = lower_bound(bucket.begin(), bucket.end(), nums[i]) - bucket.begin() + 1;cout << nums[i] << ", " << idx << endl;// 求前idx-1个桶的元素和counts[i] = query(idx-1);// 更新cupdate(idx, n);}return counts;}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/2211.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uthash哈希库使用详解(增删改查和遍历,示例代码)

在C语言中&#xff0c;标准库并没有提供哈希表的实现&#xff0c;因此很多开发者需要自己实现哈希表&#xff0c;这通常是一个复杂且容易出错的过程。幸运的是&#xff0c;有像uthash这样的开源库可以帮助我们简化这一过程。本文将对uthash的使用进行详尽的讲解&#xff0c;包括…

国内首个48小时大模型极限挑战赛落幕,四位“天才程序员”共同夺冠

4月21日晚&#xff0c;第四届ATEC科技精英赛&#xff08;ATEC2023&#xff09;线下赛落幕。本届赛事以大模型为技术基座&#xff0c;围绕“科技助老”命题&#xff0c;是国内首个基于真实场景的大模型全链路应用竞赛。ATEC2023线下赛采用48小时极限挑战的形式&#xff0c;来自东…

【Linux开发 第十一篇】rpm和yum

rpm rpm用于互联网下载包的打包及安装工具&#xff0c;它包含在某些Linux分发版中&#xff0c;就是一种Linux中软件包的管理工具 rpm包指令 rpm -qa&#xff1a;查询所安装的所有的rpm软件包 rpm -qa | more rom -qa | grep X rpm -q 软件包名:查询软件包是否安装 rpm -qi 软…

2024年最新版云开发cms开通步骤,开始开发微信小程序前的准备工作,认真看完奥!

小程序官方有改版了&#xff0c;搞得石头哥不得不紧急的再新出一版&#xff0c;教大家开通最新版的cms网页管理后台 一&#xff0c;技术选型和技术点 1&#xff0c;小程序前端 wxml css JavaScript MINA原生小程序框架 2&#xff0c;数据库 云开发 云数据库 云…

一般转行嵌入式应该怎么做?

转行嵌入式可以考虑以下方向&#xff1a; 嵌入式软件开发&#xff1a;包括嵌入式操作系统的开发、应用软件开发等。 嵌入式硬件设计&#xff1a;涉及电路设计、微处理器应用等。 物联网应用开发&#xff1a;利用嵌入式技术实现物联网设备的连接、控制和数据处理。 华清远见的…

options表的service

目录 1、 * options表的service 1.1、 insertOption 1.2、 saveOptions 1.3、 getOptions package com.my.blog.website.service.impl; import com.my.blog.website.dao.OptionVoMapper;

Mysql优化之分区分表

为什么要分区分表 分区和分表是两种用于优化大型数据集查询性能的技术&#xff0c;它们有不同的应用场景和优势。随着数据库数据越来越大&#xff0c;单个表中数据太多。以至于查询速度变慢&#xff0c;而且由于表的锁机制导致应用操作也受到严重影响&#xff0c;就出现了数据…

springboot 批量下载文件, zip压缩下载

一、使用hutool 工具类 效果&#xff1a;下载速度可以 1、依赖&#xff1a;hutool <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version> </dependency>2、调用方式 im…

Android Studio开发工具学习之Git分支操作

这里写目录标题 2.1 查看、创建本地分支2.1.1 命令行查看与创建2.1.2 Git窗口查看与创建 2.2 切换分支&#xff1a;Checkout2.3.1 通过命令行切换 2.3.2 通过Git窗口切换 2.3 合并分支&#xff1a;Merge2.3.1 操作command_new、gui-new分支2.3.1 通过命令行将gui-new分支合并至…

Day35代码随想录贪心part04:860.柠檬水找零、406.根据身高重建队列、452. 用最少数量的箭引爆气球

Day35 贪心part04 860.柠檬水找零 leetcode题目链接&#xff1a;860. 柠檬水找零 - 力扣&#xff08;LeetCode&#xff09; **复习一下dict的语法&#xff1a;**wallet[i] wallet.get(i, 0)1 本题一开始尝试用逐层判断去做&#xff0c;但这样好像并不合理 思路&#xff1…

深度学习网络训练,Loss出现Nan的解决办法

文章目录 前言 一、原因 二、典型实例 1. 梯度爆炸 2. 不当的损失函数 3. 不当的输入 前言 模型的训练不是单纯的调参&#xff0c;重要的是能针对出现的各种问题提出正确的解决方案。本文就训练网络loss出现Nan的原因做了具体分析&#xff0c;并给出了详细的解决方案&#xff…

LT8711UXD助力新款Swtich游戏机底座《4K/60HZ投屏方案》

Nintendo Switch&#xff08;OLED版&#xff09;正面搭载了一块分辨率为720P的7.0英寸OLED屏幕&#xff1b;具有白色和电光蓝电光红2种颜色&#xff1b;机身长度102毫米&#xff0c;宽度242毫米&#xff0c;厚度13.9毫米&#xff0c;重量约420克。 [2]Nintendo Switch&#xff…

举个栗子!Tableau 技巧(271):同时筛选不同年份的 TopN 数据

零售企业的销售数据分析中&#xff0c;经常用排序来查看过去一年或者几年的数据 TopN 情况。如果可以在同一视图中&#xff0c;呈现很多年的数据排名&#xff0c;且通过筛选能灵活调整 TopN 的 N 值&#xff0c;岂不是更方便&#xff1f; 如下示例&#xff1a;图表呈现了各品牌…

centos7上搭建mongodb数据库

1.添加MongoDB的YUM仓库&#xff1a; 打开终端&#xff0c;执行以下命令来添加MongoDB的YUM仓库&#xff1a; sudo vi /etc/yum.repos.d/mongodb-org-4.4.repo 在打开的文件中&#xff0c;输入以下内容&#xff1a; [mongodb-org-4.4] nameMongoDB Repository baseurlh…

【sping】在logback-spring.xml 获取项目名称

在日志文件中我们想根据spring.application.name 创建出的文件夹。 也不想死在XML文件中。 application.yml spring:application:name: my-demo logback-spring.xml <springProperty name"application_name" scope"context" source"spring.app…

如何用微信小程序实现远程控制无人售货柜

如何用微信小程序实现远程控制无人售货柜呢&#xff1f; 本文描述了使用微信小程序调用HTTP接口&#xff0c;实现控制无人售货柜&#xff0c;独立控制售货柜、格子柜的柜门。 可选用产品&#xff1a;可根据实际场景需求&#xff0c;选择对应的规格 序号设备名称厂商1智能WiFi…

Java基础:设计模式之简单工厂模式

简单工厂模式是一种创建型设计模式&#xff0c;它通过一个专门的类&#xff08;即工厂类&#xff09;负责创建对象&#xff0c;从而将对象的创建过程与客户端代码解耦。简单工厂模式的核心在于提供一个统一的入口&#xff0c;接收外界请求并根据请求参数返回相应的对象实例&…

Linux系统上C++使用alsa库播放声音文件

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、命令行1.ffmpeg2.aplay 二、代码实现总结 前言 平常读麦克风的场景居多&#xff0c;有时候也需要播放一个声音文件&#xff0c;这里就介绍怎么处理。 一、…

自动驾驶光学校准反射板

光学校准反射板是一种用于光学系统校准的重要工具。它以其高反射率和精确的几何特性&#xff0c;为光学仪器、光学系统和光学元件的校准提供了可靠的参考。在现代光学领域&#xff0c;光学校准反射板的应用已经深入到各个领域&#xff0c;从科学研究到工业生产&#xff0c;都离…

# IDEA2019 如何打开 Run Dashboard 运行仪表面板

IDEA2019 如何打开 Run Dashboard 运行仪表面板 段子手168 1、依次点击 IDEA 上面工具栏 —> 【View】 视图。 —> 【Tool Windows】 工具。 —> 【Run Dashboard】 运行仪表面板。 2、如果 【Tool Windows 】工具包 没有 【Run Dashboard】 运行仪表面板 项 依次…