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

news2025/1/16 1:36:51

阅读目录

    • 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);
            // 更新c
            update(idx, n);
        }

        return counts;
    }
};

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

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

相关文章

Multitouch 1.27.28 免激活版 mac电脑多点触控手势增强工具

Multitouch 应用程序可让您将自定义操作绑定到特定的魔术触控板或鼠标手势。例如&#xff0c;三指单击可以执行粘贴。通过执行键盘快捷键、控制浏览器的选项卡、单击鼠标中键等来改进您的工作流程。 Multitouch 1.27.28 免激活版下载 强大的手势引擎 精心打造的触控板和 Magic …

js 模拟鼠标移动事件,并监听鼠标移动

代码实现 function simulateClick( x, y) {// 获取目标元素 const element document.querySelector("xxxxx"); // 创建一个鼠标移动事件 var mouseMoveEvent new MouseEvent(mousemove, {screenX: x window.screenX, screenY: y window.screenY, clientX: x,…

使用gdb调试遇到No symbol table is loaded. Use the “file“ command.怎么办?

问题排查 出现下面问题&#xff0c;通常是没有处于调式模式环境下&#xff0c;所以我们需要在gcc指令后加 【-g】。 因为&#xff0c;我么的gcc编辑器默认是动态链接&#xff0c;而且是realese发布版本。 想要解决也很简单 主要思路就是在gcc -g。 在makefile文件如下进行修改即…

cpp-tbox 之 RPC 通信服务

jsonrpc 序列化与反序列化基于的是json Proto 该类设计用于处理网络或传输层数据接收和发送&#xff0c;同时提供一些回调函数接口来定义如何处理接收到的数据和发送数据&#xff0c;也就是整个RPC的框架类 对于接收请求的回调函数&#xff0c;参数是请求id&#xff0c;方法…

科技云报道:AIGC掀算力需求革命,边缘计算将不再“边缘”

科技云报道原创。 随着以大模型为代表的AIGC时代拉开序幕&#xff0c;算力需求持续爆发&#xff0c;AI与边缘深度融合已是大势所趋&#xff0c;越来越多的企业开始积极布局GenAI。 GenAI技术的商用化部署和应用成为企业竞逐的新阵地&#xff0c;勾勒出大模型从“技术力”转向…

测试开发高频面试题(持续更新)

什么是测试开发以及其在软件开发流程中的作用。在过去项目中的测试策略和方法。是如何确保测试的全面性和质量的&#xff1f;讲解一下测试金字塔&#xff08;Test Pyramid&#xff09;模型&#xff0c;并解释各个层级的测试类型和其重要性。描述一下持续集成&#xff08;CI&…

AI:165-Coze自定义赛博风格Bot-图片生成操作指南

Coze是由字节跳动推出的一个AI聊天机器人和应用程序编辑开发平台&#xff0c;旨在帮助用户快速创建各种类型的聊天机器人、智能体、AI应用和插件&#xff0c;并将其部署在社交平台和即时聊天应用程序中&#xff0c;如Discord、WhatsApp、Twitter、飞书、微信公众号等。 这个平…

02、java语言为什么要配置环境变量?配置环境变量的两种方案?遇到问题的几种解决办法

探讨笔记 1、java 语言为什么要配置环境变量&#xff1f;其一、未配置环境变量之前&#xff0c;能否执行 java、javac 的命令&#xff1f;其二、未配置环境变量之前&#xff0c;在 Windows 命令行窗口&#xff0c;如何执行相关命令&#xff1f;其三、未配置环境变量之前&#x…

1小时学会SpringBoot3+Vue3前后端分离开发

首发于Enaium的个人博客 引言 大家可能刚学会Java和Vue之后都会想下一步是什么&#xff1f;那么就先把SpringBoot和Vue结合起来&#xff0c;做一个前后端分离的项目吧。 准备工作 首先你需要懂得Java和Vue的基础知识&#xff0c;环境这里就不多说了&#xff0c;直接开始。 …

Spring Boot-基础操作,常用工具,配置文件

lombok工具 首先将lombok的依赖引入 Lombok是一个实用的Java类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率。 日志操作 自定义日志打印 有以下两步&#xff1a; 在一个类中先获…

Vue3 实现 Three.js粒子特效

效果 <template><div id"waves" /> </template><script setup> import { ref, onMounted, onUnmounted } from "vue"; import * as THREE from "three";const amountX ref(50); const amountY ref(50); const color …

MySQL统计一个表的行数,使用count(1), count(字段), 还是count(*)?

为什么要使用count函数&#xff1f; 在开发系统的时候&#xff0c;我们经常要计算一个表的行数。比如我最近开发的牛客社区系统&#xff0c;有一个帖子表&#xff0c;其中一个功能就是要统计帖子的数量&#xff0c;便于分页显示计算总页数。 CREATE TABLE discuss_post (id i…

线性模型算法-完结总结篇

简介 该篇文章就是在CSDN上更新的最终版本。 本文章将介绍&#xff1a;机器学习中的线性模型有关内容&#xff0c;我将尽可能做到 详细地介绍线性模型的所有相关内容,模块如下&#xff0c;希望这些将有助于读者了解这种最初步但却强大的算法&#xff1a; 线性回归逻辑回归 S…

【ENSP】VRRP配置方法

VRRP配置步骤 1.配置虚拟ip地址作为网关&#xff0c;进行切换路由器 2.配置vrrp优先级&#xff0c;越大越优先 3.配置延迟抢占时间 4.配置备份组监视接口 AR1路由器配置 u t m #关闭提示 sys …

Zilliz Cloud 助力 AI 在线教育:智慧树的创新之路

在信息技术飞速发展的今天&#xff0c;教育行业正经历着一场深刻的变革。智慧树&#xff0c;作为全球领先的学分课程运营服务平台&#xff0c;始终站在教育创新的前沿。 为了进一步提升教育质量和效率&#xff0c;智慧树携手 Zilliz Cloud&#xff0c;共同开启了一场教育与技术…

Linux——(grep指令及zip/tar压缩指令)

1.grep指令 语法&#xff1a; grep【选项】查找字符串 文件 功能&#xff1a; 在文件中搜索字符串&#xff0c;将找到的行打印出来 常用选项&#xff1a; -i &#xff1a;忽略大小写&#xff0c;所以大小写视为相同 -n &#xff1a; 顺便输出行号 -v &#xff1a;反向选择&…

综合大实验

题目&#xff1a; 1、R4为ISP&#xff0c;其上只配置IP地址&#xff1b;R4与其他所直连设备间均使用公有IP&#xff1b; 2、R3-R5、R6、R7为MGRE环境&#xff0c;R3为中心站点&#xff1b; 3、整个OSPF环境IP基于172.16.0.0/16划分&#xff1b;除了R12有两个环回&#xff0c;其…

未来五十年,智能科技将如何改变传统行业格局?

未来五十年内&#xff0c;随着人工智能&#xff08;AI&#xff09;和智能科技的不断发展&#xff0c;许多行业将面临被取代的风险。虽然这种趋势可能会带来一些担忧&#xff0c;但也将为人类社会带来巨大的变革。下面将详细探讨哪些行业可能会在未来被智能科技所取代。 ▶ 制造…

【ruoyi-vue】登录解析(前端)

登录代码 1、登录之后做了什么&#xff1f; 执行登陆方法&#xff0c;成功之后&#xff0c;路由跳转到指定路径或者根目录 2、this.$store.dispatch是什么意思&#xff1f; this.$store.dispatch(‘Login’, this.loginForm) 来调取store里的user.js的login方法3、this.$r…

【Go语言】接口类型(一)接口类型与接口的值

本文是介绍golang接口类型的第一篇&#xff0c;主要介绍接口类型与接口类型的值的相关概念。 1. 静态类型、动态类型、动态值 所谓的静态类型&#xff08;即 static type&#xff09;&#xff0c;就是变量声明的时候的类型。 var age int // int 是静态类型 var name strin…