455. 分发饼干
解题思路
贪心是从局部最优解出发得到全局最优解,贪心的题目的出发都是解出局部最优然后试着推出全局最优解且没有反例(这里其实是数学的思想,数学归纳法啥的,我也不太会),然后就可以按照贪心策略解题了,本题中对于大胃口的孩子我们喂大饼干就可以满足全局最优解
核心代码
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
int start = 0;
//i指向饼干
//start指向孩子
for(int i = 0; i < s.length && start < g.length; i++) {
if(s[i] >= g[start]) {
count++;
start++;
}
}
return count;
}
}
376. 摆动序列
解题思路
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值,借用一下卡哥的图(我画图太丑QAQ),还是强烈安利一波代码随想录,解决你力扣不知道从何刷起的难题,克服看见算法就头疼的情况,所以我们可以使用一个pre来记录上一次波峰或者波谷的情况,如果连续的子序列和pre的符号相同那么就"删除"这个结点,如果符号不同那么就记录下这个节点
核心代码
class Solution {
public int wiggleMaxLength(int[] nums) {
if(nums.length == 1) {
return 1;
}
//当前节点的值
int cur = 0;
//上一个节点的值
int pre = 0;
int count = 1;
for(int i = 0; i < nums.length - 1; i++) {
cur = nums[i + 1] - nums[i];
if((cur > 0 && pre <= 0) || (cur < 0 && pre >= 0)) {
count++;
pre = cur;
}
}
return count;
}
}
53. 最大子数组和
解题思路
对于这道题如果开头是负数那么无论后面的值有多大都不行(这是常识…也是一种贪心策略),所以我们要选的开头的数值必须为正数,如果没有正数也没关系,每次添加一个元素我们都和最大值进行比较,如果sum为负数就将sum置为0
核心代码
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
int sum = 0;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
if(sum > max) {
max = sum;
}
if(sum < 0) {
sum = 0;
}
}
return max;
}
}
122. 买卖股票的最佳时机 II
解题思路
这道题我们要读取中一天内只要买卖是一对的就是合法的,也就是说我们可以卖出这一天的股票然后再卖出,所以我们只需要求取相隔两天的正利润之和
核心代码
class Solution {
public int maxProfit(int[] prices) {
int result = 0;
for (int i = 1; i < prices.length; i++) {
result += Math.max(prices[i] - prices[i - 1], 0);
}
return result;
}
}
55. 跳跃游戏
解题思路
这道题不要考虑怎么跳,我们只需要一步一步的往前跳,然后更新当前所能达到的最大区间,这道题和跳跃游戏Ⅱ还是有点不同的
核心代码
class Solution {
//想象成一下下的往前跳获取能量,每跳一下能量就-1看能否跳到终点
//不用管跳多少下
public boolean canJump(int[] nums) {
//覆盖范围
int jump = 0;
//范围是可变的每跳动一下就更新最大范围
for(int i = 0; i <= jump; i++) {
//i + nums[i]表示从当前位置可达的范围
jump = Math.max(jump,i + nums[i]);
if(jump >= nums.length - 1) {
return true;
}
}
return false;
}
}
45. 跳跃游戏 II
解题思路
这道题和55不同之处在于我们要求解最少可以跳几步!!因为题目说我们总是可以达到终点的,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最小步数
核心代码
class Solution {
public int jump(int[] nums) {
if(nums.length == 1) {
return 0;
}
int jump = 0;
int cur = 0;
int count = 0;
//在它所能跳的范围内一步一步的跳
for(int i = 0; i < nums.length; i++) {
//每走一步更新所能覆盖的最大值
jump = Math.max(jump, i + nums[i]);
if(jump >= nums.length - 1) {
count++;
break;
}
//跳到了能跳到的极限也就是贪心的跳了一步
if (i == cur){
cur = jump;
count++;
}
}
return count;
}
}
1005. K 次取反后最大化的数组和
解题思路
如何能够获得最大化的数组和呢??按照贪心的策略我们只需要将所有的负数取反,然后如果k还有剩余我们就将最小的数重复取反,这样的数组就是最大化数组,所以我们选择排序的方法来挑选负数和最小的正数
核心代码
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
//排序
Arrays.sort(nums);
int sum = 0;
//这一步是将所有的负数取反
for(int i = 0; k > 0 && i < nums.length && nums[i] < 0; i++) {
nums[i] = -nums[i];
k--;
}
Arrays.sort(nums);
//如果k>0,那就将剩余的k的次数就将最小数重复取反操作
while(k-- > 0) {
nums[0] = -nums[0];
}
//最后将剩余的值相加
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
134. 加油站
解题思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ty5R3fsN-1640052686127)(力扣周结07——贪心/20201213162821958.png)]
gas数组代表的是可以从该加油站获取的油的数量,而cost数组代表的是从当前位置到下一个位置所需要花费的油,所以gas - cost数组代表的就是从这一站到下一站剩余的油,如果我们遍历一边数组发现数组的总和为负数那么无论从哪个位置出发都不能够绕一圈,我们必须要从正数的位置开始移动(贪心策略也是常规的思维因为负数的值肯定到不了),所以我们首先选择一个正数的值开始出发如果在遍历数组的过程中和的数量发现为负数那么代表不能从那个位置开始出发。
核心代码
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int index = 0;
int sum = 0;
int total = 0;
for(int i = 0; i < gas.length; i++) {
sum += gas[i] - cost[i];
//因为我们要遍历一边数组,使用total变量记录数组所有元素的总和
total += gas[i] - cost[i];
//如果在累加过程中sum的值出现了负数那么就代表从那个起点到现在这个终点所有的值都不能够满足出发点的条件
if(sum < 0) {
//我们让sum置0为重新开始计数
sum = 0;
//让下标值记作下一个节点
index = i + 1;
}
}
if(total < 0) {
return -1;
}
return index;
}
}
135. 分发糖果
解题思路
这道题的思路比较精妙,因为要确保每一个孩子的左右节点的关系所以我们选择先定一个右边孩子只比较左边孩子和当前的孩子的关系,然后我们定左孩子,只比较当前孩子和右孩子的关系,这样就可以确保当前孩子和左右孩子的关系,值得注意的是因为我们是先定一边再去遍历另一边所以我们再处理另一边关系的时候要注意因为之前的遍历关系导致现在数组的关系是当前的孩子如果比左孩子积分大那么就满足当前孩子的糖果数大于左边孩子的糖果如果我们在从右往左遍历确认当前孩子和右孩子的关系的时候可能当前节点的糖果数量已经大于右边孩子而由于和之前一样的处理导致左孩子和当前孩子的关系出现了破坏例如**[1,3,4,5,2]**这个测试用例中我们从左往右遍历已经确认了5孩子比4孩子的糖果数量大了,但我们从右往左遍历的时候不可以再使用之前的处理节点的方式了,而是要 Math.max(candy[i], candy[i + 1] + 1)
核心代码
class Solution {
public int candy(int[] ratings) {
//统计每位小朋友糖果的数量
int[] candy = new int[ratings.length];
int sum = 0;
//先给每个孩子一块糖
for(int i = 0; i < candy.length; i++) {
candy[i] = 1;
}
//从左往右遍历,只比较节点和左孩子的关系
for(int i = 1; i < ratings.length; i++) {
if(ratings[i] > ratings[i - 1]) {
candy[i] = candy[i - 1] + 1;
}
}
//从右往左遍历,比较节点和右孩子的关系
for(int i = ratings.length - 2; i >= 0; i--) {
if(ratings[i] > ratings[i + 1]) {
//由于每个节点已经处理过了,当前糖果的数量可能已经比右边的糖果数量大了,所以要比较一下
candy[i] = Math.max(candy[i], candy[i + 1] + 1);
}
}
for(int i = 0; i < candy.length; i++) {
sum += candy[i];
}
return sum;
}
}
860. 柠檬水找零
解题思路
贪心策略,每次找零钱都给最大的因为5美元的零钱是最有用的可以找20美元也可以找10美元但是10美元只能找20美元所以要注意遇到客人给20美元的情况要分情况
- 如果有10美元那么我们就再给一张5美元即可
- 如果没有10么远那么我们就需要给三张5美元
核心代码
class Solution {
public boolean lemonadeChange(int[] bills) {
boolean flag = true;
//记录钞票面额为5和10的数量
int[] count = new int[2];
//贪心策略每次找钱都找最大的
for(int i = 0; i < bills.length; i++) {
//如果面额为5直接存入即可不需要找零
if(bills[i] == 5) {
count[0]++;
}
//如果面额为10需要找零5
if(bills[i] == 10) {
count[0]--;
count[1]++;
}
//如果面额为20需要找零10和5,如果10的数量不够那么就将5减少3
if(bills[i] == 20) {
count[0]--;
if(count[1] == 0) {
count[0] -= 2;
}else {
count[1]--;
}
}
//交易完毕一次查看零钱的数量如果有一项为负数代表刚刚的交易零钱不够
if(count[0] < 0 || count[1] < 0) {
flag = false;
break;
}
}
return flag;
}
}
406. 根据身高重建队列
解题思路
这道题和分发糖果如出一辙,先定一个,我们选择定h也就是身高,然后按照key插入,只是这道题API属实把我调麻了
- 首先是Array.sort的重写比较器
- 随后是ArrayList的add()的重写方法
- list的toArray()方法
核心代码
class Solution {
//这道题难点好多API都调不明白,lambda表达式还不清楚,Arrays.sort()的重写比较器方法,queue的add方法在指定位置插入
//和最后queue.toArray()从一维数组转换为二维数组,这道题明白贪心策略之后并不难这错综复杂的API把我调麻了
public int[][] reconstructQueue(int[][] people) {
//先按照身高从大到小排,两边兼顾只会顾此失彼
//重写比较器
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0];
});
LinkedList<int[]> queue = new LinkedList<>();
//对插入元素进行贪心策略
//按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列
for(int[] p : people) {
queue.add(p[1], p);
}
return queue.toArray(new int[people.length][]);
}
}
452. 用最少数量的箭引爆气球
解题思路
452是贪心策略的区间覆盖类型的题目,首先是按照起点进行排序,然后从第二个节点开始遍历,如果当前节点的起点小于上一个起点的终点那么就代表当前节点和上一个节点只需要用一只箭即可
核心代码
class Solution {
public int findMinArrowShots(int[][] points) {
//按照右端口从小到大排序
Arrays.sort(points, (a,b) -> Integer.compare(a[0],b[0]));
int count = 1;
for(int i = 1; i < points.length; i++) {
//如果说当前节点的起始节点大于上一个节点的终节点那么就肯定需要一支箭
if(points[i][0] > points[i - 1][1]) {
count++;
}else {//如果能够覆盖那么就需要将本次的终结点改为重叠终节点的最小值
points[i][1] = Math.min(points[i][1],points[i - 1][1]);
}
}
return count;
}
}
763. 划分字母区间
解题思路
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
可以分为如下两步:
- 统计每一个字符最后出现的位置
- 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
核心代码
class Solution {
public List<Integer> partitionLabels(String s) {
//结果集
List<Integer> result = new ArrayList<Integer>();
int[] hash = new int[26];
//统计每个字符最后出现的位置(会带有更新操作)
for(int i = 0; i < s.length(); i++) {
hash[s.charAt(i) - 'a'] = i;
}
int left = 0;
int right = 0;
for(int i = 0; i < s.length(); i++) {
//找到字符出现的最远边界
right = Math.max(right, hash[s.charAt(i) - 'a']);
if(i == right) {
result.add(right - left + 1);
left = i + 1;
}
}
return result;
}
}
种一棵树最好的时机是10年前 其次是现在。 努力!!