860 柠檬水找零
同样第一题是简单题,本题情况较少 [5 10 20] 可分类讨论把情况都罗列出来
思路
这道题目刚一看,可能会有点懵,这要怎么找零才能保证完成全部账单的找零呢?
但仔细一琢磨就会发现,可供我们做判断的空间非常少!
只需要维护三种金额的数量,5,10和20。
有如下三种情况:
- 情况一:账单是5,直接收下。
- 情况二:账单是10,消耗一个5,增加一个10
- 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三,涉及到【优先】【如果不够】,因此采用贪心思想。
账单是20的情况,为什么要优先消耗一个10和一个5呢?
因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。
全局最优:完成全部账单的找零。
局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法!
整体代码如下:
public boolean lemonadeChange(int[] bills) {
// 初始值
int five = 0,ten = 0;
// 遍历bills
for (int bill : bills) {
// 支付5元
if (bill == 5) five++;
// 支付10元
else if (bill == 10) {
if (five == 0) return false;
else {
five--;
ten++;
}
}
// 支付20元
else if (bill == 20) {
if (ten > 0 && five > 0){
ten--;
five--;
// 此处没必要记录20,20不可找零
} else if (five >= 3) {
five -= 3;
// 同样没必要记录20
}else return false;
}
}
// 遍历完,能找玩零钱则true
return true;
}
- 时间复杂度: O(n)
- 空间复杂度: O(1)
总结
咋眼一看好像很复杂,分析清楚之后,会发现逻辑其实非常固定。
这道题目可以告诉大家,遇到感觉没有思路的题目,可以静下心来把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。
如果一直陷入想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。
460 根据身高重建队列
和135. 分发糖果一样需要考虑两个维度
思路
本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后再按照另一个维度重新排列。
就像135. 分发糖果一样,如果两头都考虑只会顾此失彼。遇到两个维度权衡的时候,应该先确定一个维度,再确定另一个维度。
对于本题困惑的点是先确定k还是先确定h呢,也就是究竟先按h排序呢,还是先按照k排序呢?
那就都试试~
如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。
如果按照身高h来排序,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。
此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!
那么只需要按照k为下标重新插入队列就可以了,为什么呢?
以图中{5,2} 为例:
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
所以在按照身高从大到小排序后:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
局部最优可推出全局最优,找不出反例,那就试试贪心。
整个插入过程如下:
根据身高排序完(身高一样的k小的在前面)的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]
插入的过程:(根据k值作为坐标插入)
- 插入[7,0]:[[7,0]]
- 插入[7,1]:[[7,0],[7,1]]
- 插入[6,1]:[[7,0],[6,1],[7,1]]
- 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
- 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
- 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
此时就按照题目的要求完成了重新排列。
整体代码如下:
public int[][] reconstructQueue(int[][] people) {
// 按身高[0]排序 如果身高相同按[1]排序
Arrays.sort(people,(a,b) -> {
if (a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0];
});
// 设置队列 or 数组方便插入
LinkedList<int[]> que = new LinkedList<>();
// ArrayList<int[]> que = new ArrayList<>();
// 遍历数组 元素向前插入
for (int[] person : people) {
// [1]即为要插入的下标(不需要考虑身高)
que.add(person[1],person);
}
// 队列转换成数组
return que.toArray(new int[people.length][]);
}
- 时间复杂度:O(nlog n + n^2)
- 空间复杂度:O(n)
在Java中,ArrayList底层是数组实现的,类似于C++的vector。当插入元素导致容量不足时,ArrayList会进行自动扩容,但扩容方法略有不同。ArrayList会创建一个更大的数组,然后将原数组内容拷贝到新数组中,同样涉及元素的复制过程。
所以使用ArrayList扩容的话,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。
相比之下,LinkedList在Java中使用链表的数据结构实现。它不需要进行数组的扩容操作,而是通过节点之间的链接来存储和操作元素。因此,在插入操作时,LinkedList只需要修改节点的链接,不需要移动和复制元素,所以插入操作的时间复杂度为O(1)。本题涉及元素较少,使用链表实现效率会更快
综上所述,使用LinkedList在插入操作频繁的情况下效率较高,因为插入操作的时间复杂度为O(1),但在访问和查找特定位置的元素时效率较低。而ArrayList在访问和查找元素时由于可以通过索引直接访问元素,效率较高,但在插入操作频繁的情况下效率较低。
具体分析见这里👈(C++版)
总结
这种需要考虑两个维度的题目,其技巧都是确定一边然后贪心另一边。[两边一起考虑,就会顾此失彼]
这道题目可以说比135. 分发糖果难不少,其贪心的策略也是比较巧妙。
最后根据数据结构特点优化算法,链表的效率比数组高的。
452 用最少数量的箭引爆气球
又是一题需要考虑两个维度
思路
如何使用最少的弓箭呢?
直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢?
尝试一下举反例,发现没有这种情况。那么就试一试贪心吧!
局部最优:当气球出现重叠,一起射,所用弓箭最少。
全局最优:把所有气球射爆所用弓箭最少。
算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?
如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。
但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remove气球,只要记录一下箭的数量就可以了。
以上为思考过程,已经确定下来使用贪心了,那么开始解题。
为了让气球尽可能的重叠,需要对数组进行排序。
那么按照气球起始位置排序,还是按照气球终止位置排序呢?
其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。
既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。
从前向后遍历遇到重叠的气球了怎么办?
如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)
- 如果当前气球和上一个气球有重叠(体现在上一个球右边界大于下一个球左边界)则更新当前球的右边界(为两个有边界最小值以便判断下一个球)
- 如果当前气球和上一个气球没重叠(反之体现在上一个球右边界小于下一个球左边界)则增加一支箭
- 注意:更新当前球的右边界就是为了判断下一个球,是否可以三连发
可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。
代码如下:
public int findMinArrowShots(int[][] points) {
// 按左边界排序
Arrays.sort(points,(a,b) -> Integer.compare(a[0],b[0]));
int count = 1; // points 不为空至少需要一支箭
for (int i = 1; i < points.length; i++) {
// 从第二个气球开始判断
// 根据当前球左边界与上一个球右边界比较判断。没有重合 - 消耗一支箭
if (points[i][0] > points[i - 1][1]) count++;
// 否则则为points[i][0] <= points[i - 1][1] 有重合(上一个球范围覆盖当前气球) - 更新右边界
else points[i][1] = Math.min(points[i][1],points[i - 1][1]);
}
return count;
}
- 时间复杂度:O(nlog n),因为有一个快速排序
- 空间复杂度:O(1),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
可以看出代码并不复杂。
注意事项
注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起(右边界刚好等于左边界)不重叠也可以一起射爆,
所以代码中 if (points[i][0] > points[i - 1][1]) 不能是>=
总结
这道题目贪心的思路很简单也很直接,就是重复的一起射了,但本题我认为是有难度的。
就算思路都想好了,模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了。
而且寻找重复的气球,寻找重叠气球最小右边界,其实都有代码技巧。
贪心题目有时候就是这样,看起来很简单,思路很直接,但是一写代码就感觉贼复杂无从下手。
*这里其实是需要代码功底的,那代码功底怎么练?多看多写多总结
学习资料: