题目1:860 柠檬水找零
题目链接:柠檬水找零
对题目的理解
一杯柠檬水5美元,顾客按照bills数组上的顺序一次购买一杯,给顾客找零,开始手头是没有零钱的,bills数组中至少有一个元素,数组中的元素只有5,10,20这3个数
只有3种情况,分情况讨论
情况1:bill是5,直接收下
情况2:bill是10,消耗一个5,增加一个10
情况3:bill是20,优先消耗一个10和一个5,如果不够,再消耗3个5
情况1和情况2是固定策略,
情况3这里有贪心算法:因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!
伪代码
代码
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five=0;//面值为5的钞票数量
int ten=0;//面值为10的钞票数量
int twenty=0;//面值为20的钞票数量
for(int bill:bills){
if(bill==5){
five++;
}
// if(bill==10){
// if(five==0) return false;
// five--;
// ten++;
// }这两种方式都可以
if(bill==10){
if(five>0){
five--;
ten++;
}
else return false;
}
if(bill==20){
//使用10+5这个方式还
if(ten>0 && five>0){
ten--;
five--;
twenty++;
}
else if(five>=3){
five-=3;
twenty++;
}
else return false;
}
}
return true;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
总结
本题是分情况讨论,分析具体情况
题目2:406 根据身高重建队列
题目链接:根据身高重建队列
对题目的理解
数组people中的元素,people[i] = [hi,ki],表示第i个人的身高为hi,前面有ki个身高大于等于hi的人
返回队列数组queue,queue中的元素满足ki符合
例 people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
queue = [[5,0],[7,0][5,2],[6,1],[4,4],[5,2]]
这个queue代表
0的身高为5,0前面没有比它高的人了
1的身高是7,1前面没有比他高的人了
2的身高是5,5前面有两个比他高的人(这两个人是0和1,身高分别是5和7)
3的身高是6,6前面只有1个比他高的人(这个人是2,身高是7)
........
自己的思路,但不会编程:
本题需要考虑两点:一是计算其他人的身高与本人身高的差,二是看本人身高排在第几
首先对身高进行排列,按照降序进行排列
然后按照个数进行排序
贪心算法
本题有两个维度:h和k,一定要确定一个维度,然后再按照另一个维度重新排列。
首先按照k从小到大排,因为k是代表前面有几个人身高比本人高,所以k应该是逐渐增大的,所以将k从小到大排(k相同的话,身高要从小到大排),发现这样排列不符合条件,身高h和k的维度都没有确定
再尝试按照身高h进行排序,因为比的是前面有几个人的身高大于本人,所以身高应该是逐渐减小的,降序排列(身高h相同的话,将k按照从小到大排列),这个可以确定一个维度:身高h,
Q:接下来只要以k为下标插入队列即可,为什么能够插入呢?
A:因为插入的元素的身高h一定比前面被插入的身高小,所以被插入的元素的身高相对顺序没有变
如图:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
局部最优可推出全局最优
使用vector数组
代码
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){//对二维数组里的一维数组进行引用
//a[0].b[0]代表身高h的维度,a[1],b[1]代表k的维度
if(a[0]==b[0]) return a[1]<b[1];//如果两个人的身高相同,就按照k进行升序排列
return a[0]>b[0];//两人身高不同,按照降序排列
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
vector<vector<int>> quque;
//将数组中的身高按照降序排列
sort(people.begin(),people.end(),cmp);
//按照k插入
for(int i=0;i<people.size();i++){
int position = people[i][1];//取k作为插入的位置
quque.insert(quque.begin()+position, people[i]);
}
return quque;
}
};
- 时间复杂度:O(nlog n + n^2)
- 空间复杂度:O(n)
注意!!!
使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。
vector的大小有两个维度一个是size一个是capicity,size就是平时用来遍历vector时候用的
而capicity是vector底层数组(就是普通数组)的大小,capicity可不一定就是size。
当insert数据的时候,如果已经大于capicity,capicity会成倍扩容,但对外暴漏的size其实仅仅是+1。
Q:vector底层实现是普通数组,怎么扩容的?
A:重新申请一个二倍于原数组大小的数组,然后把数据都拷贝过去,并释放原数组内存。
如上图所示,原vector中的size和capicity相同都是3,初始化为1 2 3,此时要push_back一个元素4,那么底层其实就要申请一个大小为6的普通数组,并且把原元素拷贝过去,释放原数组内存,注意图中底层数组的内存起始地址已经变了,同时也注意此时capicity和size的变化。
虽然表面上复杂度是O(n^2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n^2 + t × n)级别的,t是底层拷贝的次数。
所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。
使用链表
使用C++中的list(底层链表实现)比vector(数组)效率高得多。
代码
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){//对二维数组里的一维数组进行引用
//a[0].b[0]代表身高h的维度,a[1],b[1]代表k的维度
if(a[0]==b[0]) return a[1]<b[1];//如果两个人的身高相同,就按照k进行升序排列
return a[0]>b[0];//两人身高不同,按照降序排列
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
list<vector<int>> queue;//list底层实现是链表,插入效率比vector高很多
//将数组中的身高按照降序排列
sort(people.begin(),people.end(),cmp);
//按照k插入
for(int i=0;i<people.size();i++){
int position = people[i][1];//取k作为插入的位置
std::list<vector<int>>::iterator it=queue.begin();
while(position--) it++;
queue.insert(it,people[i]);
}
return vector<vector<int>>(queue.begin(),queue.end());
}
};
- 时间复杂度:O(nlog n + n^2)
- 空间复杂度:O(n)
题目3:452 用最少数量的箭引爆气球
题目链接:用最少数量的箭引爆气球
对题目的理解
球的水平直径是[xstart,xend],平面上的各个球的这个直径记录在数组points中,箭从x轴的x处垂直射出(一直向前),如果x在球的直径范围内(xstart<=x<=xend),那么箭可以射中该球,箭的数量没有限制,求解射中所有气球需要箭的最少数量。
贪心算法
局部最优:一支箭要尽可能多地射中重叠气球,全局最优,所有气球都被射中,且使用的箭最少。
如下图所示
为了让气球尽可能的重叠,需要对数组进行排序,因为数组有两个坐标,起始坐标start和终止坐标end,所以可以按照这两个顺序进行排序,那么按照起始坐标start进行排序
注意题目要求的一个很关键的点:xstart<=x<=xend 气球就会引爆,那么说明两个气球的区间分别为[2,3][3,4],这两个气球会被同一支箭射爆,所以判断两个气球是否重叠的时候使用如下代码,该段代码是整个代码的精华所在,,注意判断条件是大于,没有等号,这个原因就是这里的注意;
还有就是本题的难点就在于继续判断第三支箭是否和前两支箭重叠,一定注意更新最小右边界,拿第三支箭的左边界和最小边界去比较,这是这道题目的难点
伪代码
完整代码
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[0]<b[0];//将数组的第一个元素按照从小到大排序
}
int findMinArrowShots(vector<vector<int>>& points) {
//首先判断数组是否为空,如果为空,说明不需要箭,返回0,但是这一行没必要,因为数组要求长度大于1
if(points.size()==0) return 0;
sort(points.begin(), points.end(), cmp);//对数组的第一个位置的元素按照从小到大进行排序
int result = 1;//初始化为1,因为至少需要一支箭
for(int i=1;i<points.size();i++){
if(points[i][0]>points[i-1][1]) result++;//两个气球不重叠
//判断下一个气球是否和这两个气球重叠
else{
points[i][1] = min(points[i-1][1],points[i][1]);//更新最小右边界
}
}
return result;
}
};
- 时间复杂度:O(nlog n),因为有一个快排
- 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间