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,一经查实,立即删除!

相关文章

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

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

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

小程序官方有改版了&#xff0c;搞得石头哥不得不紧急的再新出一版&#xff0c;教大家开通最新版的cms网页管理后台 一&#xff0c;技术选型和技术点 1&#xff0c;小程序前端 wxml css JavaScript MINA原生小程序框架 2&#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分支合并至…

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…

自动驾驶光学校准反射板

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

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

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

慈航医疗“五进”实践活动第九场:范庄村爱心义诊,村民健康有“医”靠

为落实推进“健康中国”战略&#xff0c;提升居民健康意识和预防疾病能力&#xff0c;4月18日&#xff0c;慈航医疗健康前往范庄村开展健康义诊活动&#xff0c;为100余位村民送上了一份暖心的公益关爱。 义诊当天&#xff0c;在志愿服务队队员的引领下&#xff0c;慈航医疗健康…

数据结构|树形结构|并查集

数据结构|并查集 并查集 心有猛虎&#xff0c;细嗅蔷薇。你好朋友&#xff0c;这里是锅巴的C\C学习笔记&#xff0c;常言道&#xff0c;不积跬步无以至千里&#xff0c;希望有朝一日我们积累的滴水可以击穿顽石。 有趣的并查集剧情演绎&#xff1a;【算法与数据结构】—— 并…

JavaScript —— APIs(三)

一、事件流 &#xff08;一&#xff09;定义 &#xff08;二&#xff09;事件捕获 事件捕获&#xff0c;对话框从大到小弹出&#xff0c;先弹出爷爷&#xff0c;最后弹出儿子 &#xff08;三&#xff09;事件冒泡 冒泡事件&#xff0c;对话框从小到大弹出&#xff0c;先弹出…

详解QString与QByteArray使用对比

QString与QByteArray是Qt库中两种不同的字符串/字节序列容器&#xff0c;各自服务于特定的应用场景。本篇文章将详细解析它们的异同&#xff0c;帮助您在实际编程中准确选择和有效地使用这两种类型。 参考 QString类的使用 相同之处 构造与初始化&#xff1a; 两者都支持直接使…

Apache Answer 开源问答社区安装体验

Answer 是由 SegmentFault 思否团队打造的一款问答平台软件,后端使用 Go 语言编写,于2022年10月24日(程序员节)正式开源。你可以免费使用 Answer 高效地搭建一个问答社区,并用于产品技术问答、客户支持、用户交流等场景。 2023年10月9日,Answer 顺利通过投票,以全票通过…

华为sr-mpls policy配置案例

SR&#xff0d;MPLS POLICY在ensp上面做不了&#xff0c;这是官方上的配置

编译Milkv-duo固件

Milk-V Duo 是一款基于 CV1800B 芯片的超紧凑型嵌入式开发平台。它可以运行Linux和RTOS&#xff0c;为专业人士、工业ODM、AIoT爱好者、DIY爱好者和创作者提供可靠、低成本、高性能的平台。 硬件 CPU&#xff1a;CVITEK CV1800B (C9061Ghz C906700MHz)双 RV64 内核&#xff…

node+vue3的websocket前后端消息推送

nodevue3的websocket前后端消息推送 前期写web项目时&#xff0c;前端获取数据的方式一般是向后端发起数据请求&#xff0c;然后后端向前端发送数据&#xff0c;然后对数据进行渲染&#xff0c;这是最常规的一种数据通讯方式&#xff0c;适用于绝大部分前后端分离的项目 实际…

多项式和Bezier曲线拟合

目录 1. 多项式拟合2. Bezier曲线拟合3. 源码地址 1. 多项式拟合 在曲线拟合中&#xff0c;多项式拟合方法的性能受到三个主要因素的影响&#xff1a;采样点个数、多项式阶数和正则项。 采样点个数 N N N&#xff1a;从Figure 1中可以看出较少的采样点个数可能导致过拟合&…