news 2026/9/5 8:14:05

每日leetcode(这题真的很有意思,第一次搞半天搞错瓶颈在哪)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
每日leetcode(这题真的很有意思,第一次搞半天搞错瓶颈在哪)

2948. 交换得到字典序最小的数组 - 力扣(LeetCode)

代码参考:

并查集【C++数据结构进阶】玩转并查集:从原理到实战,C++ 实现与高频面试题全解析_并查集c++-CSDN博客

题目

给你一个下标从0开始的正整数数组nums和一个正整数limit

在一次操作中,你可以选择任意两个下标ij如果满足|nums[i] - nums[j]| <= limit,则交换nums[i]nums[j]

返回执行任意次操作后能得到的字典序最小的数组

如果在数组a和数组b第一个不同的位置上,数组a中的对应元素比数组b中的对应元素的字典序更小,则认为数组a就比数组b字典序更小。例如,数组[2,10,3]比数组[10,2,3]字典序更小,下标0处是两个数组第一个不同的位置,且2 < 10

示例 1:

输入:nums = [1,5,3,9,8], limit = 2输出:[1,3,5,8,9]解释:执行 2 次操作: - 交换 nums[1] 和 nums[2] 。数组变为 [1,3,5,9,8] 。 - 交换 nums[3] 和 nums[4] 。数组变为 [1,3,5,8,9] 。 即便执行更多次操作,也无法得到字典序更小的数组。 注意,执行不同的操作也可能会得到相同的结果。

示例 2:

输入:nums = [1,7,6,18,2,1], limit = 3输出:[1,6,7,18,1,2]解释:执行 3 次操作: - 交换 nums[1] 和 nums[2] 。数组变为 [1,6,7,18,2,1] 。 - 交换 nums[0] 和 nums[4] 。数组变为 [2,6,7,18,1,1] 。 - 交换 nums[0] 和 nums[5] 。数组变为 [1,6,7,18,1,2] 。 即便执行更多次操作,也无法得到字典序更小的数组。

示例 3:

输入:nums = [1,7,28,19,10], limit = 3输出:[1,7,28,19,10]解释:[1,7,28,19,10] 是字典序最小的数组,因为不管怎么选择下标都无法执行操作。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 1 <= limit <= 109

思路

  1. 目标是得到字典序最小的数组,即最小的数,也就是尽可能让更小值放在更前面。那么是否可以考虑分别维护不同的可交换小组(因为可交换元素之间也符合交换率),然后小组中用某种方法维护“值”和“下标”两个属性,那么就可以将对应的数据由小到大重新排入对应的下标之中,得出的结果自然也就是最小的了。

  2. 那么该怎么构造这样的数据结构就是主要问题了。

  3. 没想出来比较好的解决方案偷偷看了一下提示——并查集,又得开始补习了(感谢【C++数据结构进阶】玩转并查集:从原理到实战,C++ 实现与高频面试题全解析_并查集c++-CSDN博客,非常清楚地解释了并查集的原理和构造方法,我基本就是纯copy)。这是一个维护“相关”关系的数据结构,时空复杂度都是O(n)的。参照这个思路,我们可以维护一个和输入数组等长的一维数组,初始化为-1,利用双重循环遍历数组,将满足条件的数组通过下标连接起来,连接的方式为,若两者满足限制条件,则下标较小的节点为下标较大的节点的父节点,这样我们就可以通过双重循环分出不同的独立的可交换小组。

  4. 新的问题又出现了,现在得到了几个独立的可交换小组,怎么按小到大的顺序将他们交换呢?本来想自定义sort的比较函数的,但是排序结果一直不符合预期——因为sort逻辑主要是基于归并排序的逻辑,相等的元素不是位置保持不变,而是被集体往后挪动,只把更小的元素往前放,所以单纯的sort无法实现不同组不比较的逻辑。

  5. 那么只能通过哈希表分别对不同的组维护一个数组再排序了,但是还是超时了,因为出现了超多分组,显然如果小组内有多个元素,那么肯定需要排序,如果没有多个元素,那就不必构造哈希表排序了。

  6. 既然不行,估计是排序上开销太大了,因为插入的同时就可以排序了,而不用插完再排序一次,所以不妨直接建立小顶堆而不是vector,这样插入的同时就做好了排序了。

  7. 好的,一系列优化之后,还是超时,说明瓶颈在前面建立并查集的过程中。

  8. class Solution { public: int FindRoot(vector<int> &ufs, int index) { if(index < 0 || index >= ufs.size()) { throw invalid_argument("index out of range."); } // 找到根节点 int root = index; while(ufs[root] >= 0) { root = ufs[root]; } // 路径压缩:将路径上所有节点挂靠到根节点上 while(ufs[index] >= 0) { int parent = ufs[index]; ufs[index] = root; index = parent; } return root; } bool Union(vector<int> &ufs, int x, int y) { int root1 = FindRoot(ufs, x); int root2 = FindRoot(ufs, y); if(root1 == root2) return false; // 将较小集合合并到较大集合中,令root1为较大集合(这里两个root的都是存的负数,所以越小越大) if(root1 > root2) swap(root1, root2); ufs[root1] += ufs[root2]; ufs[root2] = root1; return true; } vector<int> lexicographicallySmallestArray(vector<int>& nums, int limit) { // 目标是得到字典序最小的数组,即最小的数,也就是尽可能让更小值放在更前面 int n = nums.size(); vector<int> ufs(n, -1); for(int i = 0; i < n; i++) { for(int j = i+1; j < n; j++) { if(abs(nums[i]-nums[j]) <= limit) { Union(ufs, i, j); } } } unordered_map<int, priority_queue<int, vector<int>, greater<int>>> groups; unordered_map<int, priority_queue<int, vector<int>, greater<int>>>::iterator iter; // 建立小组集合 for(int i = 0; i < n; i++) { if(ufs[i] < 0) { if(abs(ufs[i]) > 1) groups[i].push(nums[i]); } else groups[FindRoot(ufs, i)].push(nums[i]); } for(int i = 0; i < n; i++) { if(ufs[i] == -1) continue; int groupNo = FindRoot(ufs, i); nums[i] = groups[groupNo].top(); groups[groupNo].pop(); } return nums; } };
  9. 我太菜了,还是看了分析,因为现在的并查集构造部分是O(n^2α(n))的,还是太大了,目标是再进行一次降维。那么只能考虑把输入数据构造成一个新的能够更方便做并查集的数据,因为看了分析,我们可以直接知道,要把数据先排序,这样就只用看相邻元素就可以知道是不是同一组了,这样就可以达到O(nα(n))的复杂度构造并查集了。

  10. 所以不妨构造一个形如pair<value, index>的数组,然后对根据value,index进行排序,这样我们就可以得到一个按value升序,同value按index升序的数组。再基于这个数组遍历构造并查集,剩下的部分沿用原来的方案即可。

  11. 只能说太巧妙了,这题中等难度感觉屈才了,感觉蛮考验数据结构的设计的,相比于算法上困难的题目,这个题目感觉更考验巧思或者工程能力。

代码实现

class Solution { public: int FindRoot(vector<int> &ufs, int index) { if(index < 0 || index >= ufs.size()) { throw invalid_argument("index out of range."); } // 找到根节点 int root = index; while(ufs[root] >= 0) { root = ufs[root]; } // 路径压缩:将路径上所有节点挂靠到根节点上 while(ufs[index] >= 0) { int parent = ufs[index]; ufs[index] = root; index = parent; } return root; } bool Union(vector<int> &ufs, int x, int y) { int root1 = FindRoot(ufs, x); int root2 = FindRoot(ufs, y); if(root1 == root2) return false; // 将较小集合合并到较大集合中,令root1为较大集合(这里两个root的都是存的负数,所以越小越大) if(root1 > root2) swap(root1, root2); ufs[root1] += ufs[root2]; ufs[root2] = root1; return true; } vector<int> lexicographicallySmallestArray(vector<int>& nums, int limit) { // 目标是得到字典序最小的数组,即最小的数,也就是尽可能让更小值放在更前面 int n = nums.size(); vector<int> ufs(n, -1); vector<pair<int, int>> sortedNums(n); for(int i = 0; i < n; i++) { sortedNums[i] = make_pair(nums[i], i); } sort(sortedNums.begin(), sortedNums.end(), [](pair<int,int> a, pair<int,int> b) { if(a.first == b.first) return a.second < b.second; return a.first < b.first; }); // 构造并查集 for(int i = 1; i < n; i++) { if(abs(sortedNums[i].first-sortedNums[i-1].first) <= limit) { Union(ufs, sortedNums[i].second, sortedNums[i-1].second); } } unordered_map<int, priority_queue<int, vector<int>, greater<int>>> groups; unordered_map<int, priority_queue<int, vector<int>, greater<int>>>::iterator iter; // 建立小组集合 for(int i = 0; i < n; i++) { if(ufs[i] < 0) { if(abs(ufs[i]) > 1) groups[i].push(nums[i]); } else groups[FindRoot(ufs, i)].push(nums[i]); } for(int i = 0; i < n; i++) { if(ufs[i] == -1) continue; int groupNo = FindRoot(ufs, i); nums[i] = groups[groupNo].top(); groups[groupNo].pop(); } return nums; } };

复杂度分析

  • 时间复杂度:排序数组的时间复杂度是构造的O(n)+排序的O(nlogn),构造并查集的时间复杂度是O(nα(n))(路径压缩后的find和union操作的均摊时间复杂度为O(α(n))≈O(1)),priority_queue的插入和删除的时间复杂度是O(logn),弹出堆顶元素的时间复杂度是O(1)的,并查集查找,所以建堆和构建答案的时间复杂度都是O(nlogn)的。——所以总的时间复杂度是O(nlogn)。
  • 空间复杂度:因为不涉及很深的栈开销,所以空间复杂度即是主函数中定义的数据结构——O(n)。

官方题解

  • 官解利用了并查集的思维但并没有实际构造一个并查集,而是将sortedNums排序后直接分别对每个小组调整位置,更巧妙了,这样既省了构造并查集的开销,也将堆的插入和删除这样的2次O(nlogn)操作转化为一次O(nlogn)加一次O(n)的赋值操作,规模上没变,但显然效率更高了,代价是需要额外的几个将近O(n)复杂度的辅助数组来做中间处理。复刻一下(稍微做了点优化)。
  • ps.top1的大神方法过于高级,先不为难自己了。
  • class Solution { public: vector<int> lexicographicallySmallestArray(vector<int>& nums, int limit) { int n = nums.size(); vector<pair<int, int>> sortedNums(n); for(int i = 0; i < n; i++) { sortedNums[i] = {nums[i], i}; } // 让sortedNums升序排列 sort(sortedNums.begin(), sortedNums.end(), [](pair<int,int> a, pair<int,int> b) { if(a.first == b.first) return a.second < b.second; return a.first < b.first; }); int i = 0; while(i < n) { int start = i; // 以小组为单位单独处理 vector<int> groupValues, groupIndices; while(i < n && (i == start || sortedNums[i].first-sortedNums[i-1].first <= limit)) {// 注意这里没有用abs(),因为数组已经是非递减数组,不会小于0 groupValues.push_back(sortedNums[i].first); groupIndices.push_back(sortedNums[i].second); i++; } // 小组内的value已经是升序的了,只要把乱了的index重新排一下序,就能把对应的元素填到对应的index上了 sort(groupIndices.begin(), groupIndices.end()); for(int j = 0; j < groupIndices.size(); j++) { nums[groupIndices[j]] = groupValues[j]; } } return nums; } };
  • 时间复杂度:O(nlogn)。
  • 空间复杂度:O(n)。

知识积累

  • 并查集:是一种构建“相关”的小组的方法,将离散的小组成员分到一个小组中,通过路径压缩算法可以达到构建和查询的均摊时间复杂度达到O(α(n))的一种算法。
  • priority_queue<type(数据类型), container(存数据的容器), fucntion(比较算法)>:堆,插入(push(num))和删除(pop())的时间复杂度都是O(logn)的,查询堆顶元素的时间复杂度是O(1)。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 6:34:05

收藏 | 医疗AI大模型:从辅助到独立写报告,小白也能看懂的技术变革

本文介绍了医疗AI从辅助功能向完整工作流转变的过程&#xff0c;重点讲述了一场医学影像AI大会上的挑战赛。比赛中&#xff0c;AI智能体首次尝试独立承担任务&#xff0c;编写胸部CT影像报告。比赛设置了四种工作模式&#xff1a;医生单独写报告、医生搭配传统AI模型、医生协同…

作者头像 李华
网站建设 2026/9/2 7:22:58

电商AI客服软件选型与落地:从消息限流到知识库维护的实操指南

做客服系统实测这些年&#xff0c;我观察到一个很常见的现象&#xff1a;很多商家一听到“AI客服软件”&#xff0c;第一反应就是问价格&#xff0c;第二反应是问能不能自动回复。一旦继续追问“每天到底能处理多少条消息”“拼多多、淘宝、抖音的客服接口都能接吗”“机器人回…

作者头像 李华
网站建设 2026/9/2 7:21:38

PESQ语音质量评估全解析:原理、实践与避坑指南

简介&#xff1a;国际电信联盟P.862标准即感知语音质量评价&#xff08;PESQ&#xff09;的MATLAB实现与配套测试资源&#xff0c;面向语音处理、通信系统及网络优化领域的开发者和研究人员&#xff0c;用于客观评估语音信号质量。压缩包共8个文件&#xff0c;涵盖MATLAB实现函…

作者头像 李华
网站建设 2026/9/5 4:24:52

基于YOLOv8的考古文物识别系统:从数据标注到桌面应用全流程实践

简介&#xff1a;本资源是一套面向计算机、人工智能及相关专业在校生与初学者的考古文物目标检测实践项目&#xff0c;基于YOLOv8框架构建端到端识别系统&#xff0c;解决文物图像中多类别器物&#xff08;如陶器、青铜器、玉器等&#xff09;的自动定位与分类问题&#xff0c;…

作者头像 李华
网站建设 2026/9/2 7:20:52

UE5近战平A排坑:解决武器挂载报错与动画切换异常

平时做 UE5 近战玩法&#xff0c;最烦的不是写逻辑&#xff0c;而是武器挂上去报错、动画切不过来、特效又不显示。这次看的是《UE5 虚幻入门到就业 全套 Niagara 游戏特效》课程第 217 集&#xff0c;对应 18.5.5 小节&#xff0c;主题就是近战平A的排坑&#xff0c;重点解决两…

作者头像 李华