文章目录
- 406.根据身高重建队列(注意思路)
- 思路
- 两个维度
- 降序排序注意点
- 完整版
- vector容器插入相关复习
- 为什么能直接根据ki数值插入ki位置的下标
- 时间复杂度
- vector-insert操作存在的问题
- 链表优化版
- 时间复杂度
- list和vector的插入与访问操作区别
- 452.最少弓箭引爆气球(重叠区间)
- 思路
- 情况分析
- 完整版
- 时间复杂度
- 弓箭初值设置的原因
- 总结
406.根据身高重建队列(注意思路)
-
如果某元素前面有k个满足条件的元素,那么这个元素的下标就是k,而不是k-1。本题排序结束之后,如果想要>=hi的元素个数=ki,那么需要插入的位置下标就是ki!
-
本题的两个维度,和 135.分发糖果 类似,当遇到两个维度的问题的时候,一定不要两个维度同时考虑,需要先考虑一个再考虑另一个!
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki]
表示第 i 个人的身高为 hi ,前面 正好 有 ki
个身高大于或等于 hi
的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj]
是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
1 <= people.length <= 2000
0 <= hi <= 10^6
0 <= ki < people.length
题目数据确保队列可以被重建。
思路
本题首先要理解题意,是针对每个人属性第二栏的有多少个人比此人高,来对队列重新排列,使得队列前面h>=此人身高hi的人数=ki,如下图所示:
两个维度
本题有h和k两个维度,看到这种题目一定要想如何确定一个维度,然后再按照另一个维度重新排列。
本题的两个维度体现在同时有两个需要考虑的限制条件。朴素的想法应该是直接降序排序,然后降序排序的同时满足hi≥当前hi的数字个数=ki,但是不能同时满足。
所以,我们需要先把hi降序排序,ki放在降序之后考虑。因为如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。而当身高h定死的时候,k也就定死了(也就是降序排列之后,对于每个人,前面有多少个比他高的人就定死了),此时再去调整K,就会方便很多。
移动策略如下图所示:
hi先降序排完,再根据ki的情况去insert。降序排序结束之后,再从头开始遍历,对于每个组合{h,k},判断该数字前面>=h的数字个数,并且把该向量放到个数=k的下标位置。
例如{6,1}这个例子,从头遍历需要找到1个大于6的数字,找到的数字是第一个数字nums[0]=7>6,所以放在nums[1]的位置。遍历第一遍的策略情况如图粉色线条所示。
降序排序注意点
注意,在降序排序的过程中,同样hi的组合,应该是ki较小的放在前面。也就是说{5,0}{5,2}这个组合,应该是{5,0}放在前面。我们假设{5,2}放在前面,那么先遍历{5,2},再遍历{5,0},遍历到{5,0}的时候把{5,0}又放在了{5,2}的前面,这个时候{5,2}前面就又多了一个5!就会导致结果错误(因为大于/等于都算)。
完整版
- 降序排序需要满足两条,一条是降序,一条是相同的时候k从小到大排!注意cmp的写法,是比较了一维数组的两个元素!
- 降序排列结束之后,如果想要>=hi的元素个数=ki,那么需要插入的位置下标就是ki!
class Solution {
public:
//注意cmp接收的是两个一维数组,而不是二维数组
static bool cmp(vector<int>& P1,vector<int>& P2){
if(P1[0]>P2[0]) return true;//整体降序
if(P1[0]==P2[0]){
if(P1[1]<P2[1])
return true;//p1[0]相同的时候按照p1[1]升序
}
return false;
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
//先对所有的hi降序排序,因为本题的people中的变量是{a,b},所以需要自定义sort cmp
sort(people.begin(),people.end(),cmp);
//定义新的二维数组作为输出
vector<vector<int>>result;
//开始遍历排序后的people
for(int i=0;i<people.size();i++){
//因为此时已经排序完毕,所以[6,1]直接插入到下标为1的地方,[5,0]直接插入下标为0的地方
int position=people[i][1];//people[i][1]就代表着第i个集合people的第二个元素!
//元素放到对应的二维结果数组里
result.insert(result.begin()+position,people[i]);
}
return result;
}
};
vector容器插入相关复习
(1条消息) vector容器语法相关_大磕学家ZYX的博客-CSDN博客
为什么能直接根据ki数值插入ki位置的下标
这也是本题的一个思维问题,当我们降序排序结束之后,降序排序就是为了把大于这个元素的因素,全都放在这个元素的前面。因此,以[7,1]为例,当遍历到[7,1]的时候,[7,1]前面的元素一定是>=[7,1]的!
因此此时如果想要前面只有ki个>=[7,1]的元素,直接把[7,1]移动到ki的位置就行了!
(前面有k个满足条件的元素,下标又是从0开始,因此当前下标就是k而不是k-1)
时间复杂度
在C++中,std::vector::insert
的时间复杂度是O(n),其中n是从插入点到vector末尾的元素数量。这是因为插入新元素时,所有在插入点后的元素都需要移动以创建空间。因此,对于在vector的开头插入元素,需要移动所有的元素,这是最糟糕的情况,对应于O(n)的时间复杂度。对于在vector的末尾插入元素,不需要移动任何元素,这是最好的情况,对应于O(1)的时间复杂度。
代码中的循环体内使用了std::vector::insert
,因此,循环的每一次迭代都可能需要移动元素。在最糟糕的情况下,这个代码的时间复杂度是O(n^2),其中n是people
中的元素数量。
此外,还需要考虑到代码中的排序操作。std::sort
函数的时间复杂度通常为O(n log n),其中n是要排序的元素数量。
- 时间复杂度:O(nlog n + n^2)
- 空间复杂度:O(n)
vector-insert操作存在的问题
使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。
所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n2)了**,甚至**可能拷贝好几次,就不止O(n2)了。
因此我们这道题,在结果数组的数据结构选择上,可以选择把vector换成List。list底层是链表实现,链表不存在双倍扩容的问题。
链表优化版
- list不支持随机访问迭代器,因此result.begin()+position这种操作是不被允许的。
class Solution {
public:
static bool cmp(vector<int>& P1,vector<int>& P2){
if(P1[0]>P2[0]) return true;
if(P1[0]==P2[0]){
if(P1[1]<P2[1])
return true;
}
return false;
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(),people.end(),cmp);
//结果数组类型修改为list<vector<int>>
list<vector<int>>result;
//遍历排序后的people
for(int i=0;i<people.size();i++){
int position=people[i][1];
//找到position位置之后,定义迭代器再插入
list<vector<int>>::iterator it = result.begin();
//注意这里insert的写法,先寻找插入位置
while(position--){
it++;
}
//while结束之后找到插入位置
result.insert(it,people[i]);
}
//把结果转换为vector<vector<int>>,相当于构造新的二维vector
return vector<vector<int>>(result.begin(),result.end());
}
};
时间复杂度
区别讲解:代码随想录 (programmercarl.com)
链表的做法,时间复杂度也是O(nlog n + n^2)
首先,std::list
的插入操作的时间复杂度是O(1),但这只是指插入操作本身,即在已知要插入的位置的情况下的插入。然而,你需要找到要插入的位置,而在std::list
中找到一个位置的时间复杂度是O(n)。
在代码中有一个while
循环,用于找到每个元素应插入的位置。这个查找操作的时间复杂度是O(n)。因此,每次插入的总时间复杂度(查找+插入)是O(n)。由于你在循环中对每个元素都进行了这样的操作,因此,总的时间复杂度仍然是O(n^2)。
因此,链表做法的时间复杂度还是O(n log n + n^2)。其中,O(n log n)对应于排序操作,O(n^2)对应于插入操作。
而std::vector
的情况类似。std::vector
的插入操作的时间复杂度是O(n),但这已经包含了查找位置和插入两个步骤(对于std::vector
,查找位置的时间复杂度是O(1),但插入的时间复杂度是O(n))。所以,std::vector
的做法的时间复杂度也是O(n log n + n^2)。
vector的主要问题在Insert上,我们使用vector来做insert的操作,insert每一次插入都会动态扩容,虽然表面上复杂度是O(n2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n2 + t × n)级别的,t是底层拷贝的次数。
list和vector的插入与访问操作区别
博客整理:list和vector对比
std::list
和std::vector
是C++中的两种常见数据结构,它们在不同的使用场景下各有优势。
std::vector
的内部实现是动态数组,它在连续的内存块中存储数据。这使得std::vector
在访问元素时具有非常高的效率,因为可以直接通过索引来访问元素,时间复杂度为O(1)。然而,std::vector
在插入和删除元素时可能需要移动大量的元素,特别是在非尾部进行插入或删除操作时,时间复杂度为O(n)。std::list
的内部实现是双向链表,它在非连续的内存块中存储数据。这使得std::list
在插入和删除元素时具有非常高的效率,因为你只需要修改相关节点的指针,无需移动其他元素,时间复杂度为O(1)。然而,std::list
在访问元素时可能需要遍历整个链表,时间复杂度为O(n)。
如果主要的操作是插入元素insert操作,那么使用std::list
会比使用std::vector
更高效。
虽然插入操作的理论时间复杂度没有改变,但在实践中,由于std::list
不需要移动元素,所以实际运行时间会更短。这就是为什么使用std::list
后代码运行时间减少的原因。
452.最少弓箭引爆气球(重叠区间)
- 重叠区间题目需要注意元素初值的问题,包括计数变量的初值,以及有时候需要考虑数组i=0时候的初值(因为重叠判断大都是i=1开始的)
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend]
表示水平直径在 xstart
和 xend
之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 2处发射箭,击破气球[1,2]和[2,3]。
-在x = 4处射出箭,击破气球[3,4]和[4,5]。
提示:
- 1 <= points.length <= 10^5
points[i].length
== 2- -2^31 <= xstart < xend <= 2^31 - 1
思路
本题是一道经典的重叠区间类型题目,重点就是用一只弓箭尽量射重叠最多的气球。题意示意图如下。
本题关键在于代码的模拟。首先是记录重叠的气球,再引爆气球。
首先,想要得到重叠情况的统计,需要先对气球左边界进行排序。按照左边界对气球排序之后,才能得到大概类似上图,气球相邻的情况,方便处理气球。
情况分析
- 气球不重叠:第i个气球左边界>第i-1个气球的右边界,说明这两个气球一定不重合,此时一定要添加一个弓箭
//注意数组points第一个量是第几个气球,第二个量是左边界or右边界
//示例:points = [[10,16],[2,8],[1,6],[7,12]]
if(i>0&&points[i][0]>points[i-1][1]){
//不重叠一定要添加弓箭
arrow++;
}
- 气球重叠:判断右边界就可以判断相邻气球是不是重叠了,i左边界<=i-1右边界
- 但是气球重叠的情况存在一个问题,如果气球一直重叠,我们需要判断共有几个重叠的气球可以用一根箭。此时我们使用的方法是更新最小右边界,示意图如下所示。
- 更新的右边界是最新气球的右边界,是取最新气球右边界和上一个重叠气球右边界的最小值,注意是取最大值,因为
points[i-1][1]
可能大于points[i][1]
!
if(points[i][0]<=points[i-1][1]){
//此时,第i个气球和第i-1重合了,还需要判断是不是和下一个也重合
//方法:更新最小右边界,也就是把第i个气球的右边界,取为第i-1个气球的右边界和第i个的右边界的最大值
points[i][1]=min(points[i-1][1],points[i][1]);
}
完整版
- arrow初值设置为1,是因为这样遍历下去,到了最后一个就会缺失弓箭增加的逻辑,此时直接初值设置为1即可!
- 也可以考虑,当气球不为0的时候,至少需要一只箭,所以初值是1
class Solution {
public:
static bool cmp(vector<int>&a,vector<int>&b){
if(a[0]<b[0])
return true;
return false;
}
int findMinArrowShots(vector<vector<int>>& points) {
int arrow=1;
if(points.size()==0) return 0;
//先对气球左边界进行升序排序
sort(points.begin(),points.end(),cmp);
//排序完了进行遍历,先是不重叠
for(int i=1;i<points.size();i++){
if(points[i][0]>points[i-1][1]){
arrow++;
}
//其他情况就都是重叠
else{
//重叠,更新最小右边界
points[i][1]=min(points[i-1][1],points[i][1]);
}
}
return arrow;
}
};
时间复杂度
- 时间复杂度:O(nlog n),因为有一个快排(加法+n省略)
- 空间复杂度:O(1),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
弓箭初值设置的原因
依然以上面的图为例,我们按照更新最小右边界的逻辑,遇到重叠的就继续遍历,遇到不重叠才++,这种逻辑在遇到最后一个元素的时候,就缺失了弓箭++的操作。
但是,在只有最后一个元素是这样,其他元素不受影响的情况下,我们可以直接通过调整初值来实现逻辑,也就是直接把初值设置为1即可!这样就不需要在代码里单独加上处理最后一个数字的逻辑了。
总结
这道题目贪心的思路很简单也很直接,就是重复的一起射了,但是真正模拟引爆气球是有难度的。
我们需要注意的一点是,气球并不需要真的引爆,只需要累积弓箭数目+1就行了。
另外,气球的左右数值也不是不能改变的,我们可以通过更新最小右边界的形式,相当于"修改"这个气球,使得当前气球继续判断和下一个气球是否重合的逻辑。