贪心0
一、简单题目
1.分发饼干
题目描述
解题思路
-
为了满足更多的小孩,就不要造成饼干尺寸的浪费。
-
小的饼干先喂胃口小的
代码
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g); //胃口
Arrays.sort(s); //饼干
int i = 0;
for(int j=0; j<s.length && i<g.length; j++) {
if(s[j] < g[i]) {
continue;
}
i++;
}
return i;
}
}
2.k次取反后的最大化数组和
题目描述
代码
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
Arrays.sort(nums);
//先利用k将最小的小于0的数字转换为正数
int i = 0;
for(; i<nums.length && k>0; i++) {
if(nums[i] <= 0) {
nums[i] = -nums[i];
k--;
} else {
break;
}
}
//剩余的k可以抵消
if(k == 0 || k % 2 == 0) {
return Arrays.stream(nums).sum();
}
//此时nums全是非负数
Arrays.sort(nums);
nums[0] = -nums[0];
return Arrays.stream(nums).sum();
}
}
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
//Arrays.sort(nums, (n1, n2) -> Math.abs(n1) - Math.abs(n2));//需要是Integer
nums = IntStream.of(nums) //绝对值从大到小排
.boxed()
.sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
.mapToInt(Integer::intValue).toArray();
for(int i=0; i<nums.length && k>0; i++) {
if(nums[i] < 0) {
nums[i] = -nums[i];
k--;
}
}
//此时有两种可能
//1.k == 0
//2.k > 0,并且全是非负数
if(k % 2 == 1) {
nums[nums.length-1] = -nums[nums.length-1];
}
return Arrays.stream(nums).sum();
}
}
3.柠檬水找零
这题没必要再做了
题目描述
解题思路
- 当用户支付20时,优先找零10
代码
class Solution {
public boolean lemonadeChange(int[] bills) {
int n5 = 0;
int n10 = 0;
for(int bill : bills) {
if(bill == 5) {
n5++;
} else if(bill == 10) {
if(n5 > 0) {
n5--;
n10++;
} else {
return false;
}
} else {
//如果用户支付20,优先找零10
if(n10 > 0 && n5 > 0) {
n10--;
n5--;
} else if(n10 == 0 && n5 >= 3) {
n5 -= 3;
} else {
return false;
}
}
}
return true;
}
}
二、中等题目
1.摆动序列
题目描述
题目链接
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
进阶:你能否用 O(n) 时间复杂度完成此题?
解题思路
- diff = nums[i] - nums[i-1]
- flag = 0/-1/1 分别表示上一个状态是:初始状态/下坡/上坡
- 跳过平坡和上坡过程中的节点,只关注可以导致摆动的坡度变化
class Solution {
/*
case1:先增后减是摆动
/\
case2:先减后增是摆动
\/
case3:有平坡
--
/ \
case4:有平坡
/
--
/
*/
//何时有摆动?
//先增后减
//先减后增
//diff = nums[i] - nums[i-1]
public int wiggleMaxLength(int[] nums) {
if(nums.length <= 1) return nums.length;
int ans = 1;
int flag = 0; //0:上一个是初始状态,-1:上一个是下坡,1:上一个是上坡
for(int i=1; i<nums.length; i++) {
int diff = nums[i] - nums[i-1];
if(diff > 0 && (flag == 0 || flag == -1)) {
ans++;
flag = 1;
} else if(diff < 0 && (flag == 0 || flag == 1)) {
ans++;
flag = -1;
}
// else if(diff == 0) 跳过平坡;
// else 跳过上/下坡过程中的点
}
return ans;
}
}
2.买卖股票的最佳时机
题目描述
题目链接
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
解题思路
法1:贪心
- 利润可以拆解,只收集正利润
法2:dp
- 第i天的状态:持有股票/不持有股票
- dp[i][0] = 第i天不持有股票获得的最大利润
- dp[i][1] = 第i天持有股票获得的最大利润
代码
class Solution {
public int maxProfit(int[] prices) {
int ans = 0;
for(int i=1; i<prices.length; i++) {
ans += Math.max(0, prices[i] - prices[i-1]);
}
return ans;
}
}
class Solution {
//dp[i][0] = 第i天不持有股票获得的最大利润
//dp[i][1] = 第i天持有股票获得的最大利润
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = - prices[0];
for(int i=1; i<prices.length; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
return dp[prices.length-1][0];
}
}
3.分发糖果
题目描述
题目链接
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
解题思路
- 需要从两个维度权衡
- 从前往后遍历一次
- 从后往前遍历一次
代码
class Solution {
public int candy(int[] ratings) {
int[] candys = new int[ratings.length];
//从前往后遍历,保证比自己左边邻居评分高时糖果也更多
for(int i=1; i<ratings.length; i++) {
if(ratings[i] > ratings[i-1] && candys[i] <= candys[i-1]) {
candys[i] = candys[i-1] + 1;
}
}
//从后往前遍历,保证比自己右边邻居评分高时糖果也更多
for(int i=ratings.length-2; i>=0; i--) {
if(ratings[i] > ratings[i+1] && candys[i] <= candys[i+1]) {
candys[i] = candys[i+1] + 1;
}
}
return Arrays.stream(candys).sum() + ratings.length;
}
}
4.根据身高重建队列
题目描述
题目描述
数组people[i] = [hi, ki],表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。但是这个数组是被打乱的,恢复这个数组的顺序。
解题思路
- 很明显这道题有两个维度,分别是身高和前面的比自己高(或相等)的人的个数。因此要分别考虑。
前面 正好 有 ki 个身高大于或等于 hi 的人。
因此,hi降序,ki升序。所以先按hi排序,再按ki排序。- 然后在新的list,在位置k插入。因为比i高的都在i前面,因此一定可以插进去。
代码
class Solution {
public int[][] reconstructQueue(int[][] people) {
//先按照h降序排序
//再按照k升序排序
Arrays.sort(people, (p1, p2) -> {
if(p1[0] == p2[0]) {
return p1[1] - p2[1];
} else {
return p2[0] - p1[0];
}
});
List<int[]> queue = new LinkedList<>();
for(int[] person : people) {
queue.add(person[1], person);
}
return queue.toArray(people);
}
}
5.单调递增的数字
题目描述
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
解题思路
-
本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。
-
想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。
-
最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。
代码
class Solution {
public int monotoneIncreasingDigits(int n) {
StringBuilder sb = new StringBuilder(Integer.toString(n));
int flag = sb.length(); //-1的位数后面的位数都要变成9
for(int i=sb.length()-1; i>0; i--) {
if(sb.charAt(i-1) > sb.charAt(i)) {
sb.setCharAt(i-1, (char)(sb.charAt(i-1) - 1));
flag = i;
}
}
for(int i=flag; i<sb.length(); i++) {
sb.setCharAt(i, '9');
}
return Integer.parseInt(sb.toString()); //会自动去掉前导0
}
}
//示例(过程):
// 332
// 329
// 299
// 4321
// 4319
// 4299
// 3999
三、有点难度
1. 最大子数组和
题目描述
题目链接
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
解题思路
法1:贪心
- 一旦当前子数组的和已经小于0了,那么就没必要继续追加当前子数组了,因为它已经为负数了,只会拖累后面的。就可以重新开始记录新的子数组了
- 用ans及时和curSum比较,记录最大值
法2:动态规划
- dp[i] = 以i结尾的连续子数组的最大和
代码
class Solution {
public int maxSubArray(int[] nums) {
int ans = -10001;
int curSum = 0; //当前子数组的累计和
for(int num : nums) {
curSum += num;
if(curSum > ans) {
ans = curSum; //及时记录过程中的最大值
}
if(curSum < 0) {//如果当前子数组的累计和<0了,它只会拖累后面的,所以果断抛弃重新开始
curSum = 0;
}
}
return ans;
}
}
class Solution {
//dp[i] = 以i结尾的连续子数组的最大和
public int maxSubArray(int[] nums) {
int dp0 = nums[0];
int ans = nums[0];
for(int i=1; i<nums.length; i++) {
dp0 = Math.max(dp0 + nums[i], nums[i]);
ans = Math.max(ans, dp0);
}
return ans;
}
}
2.跳跃游戏
题目描述
题目链接
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
解题思路
- 关键在于最大覆盖范围能否覆盖终点,不必关心怎么走过去的
- 只有被cover到的地方才有资格被判断,cover是不断变化的
代码
class Solution {
public boolean canJump(int[] nums) {
int cover = 0; //
for(int i=0; i<=cover; i++) { //只能走到被cover住的位置,cover是不断更新的
cover = Math.max(cover, i + nums[i]);
if(cover >= nums.length-1) {
return true;
}
}
return false;
}
}
3.跳跃游戏II
题目描述
题目链接
和上一题的区别在于,只能跳规定的距离,而不是最多跳规定的距离
解题思路
- 关键在于使用最小的步数达到最大的覆盖距离
- 在走到当前覆盖范围的尽头时,需要再走一步才能到达更大的覆盖范围,这一步要尽可能的去最远的地方,因此要记录下当前覆盖范围的起点之后的最大覆盖范围next
代码
class Solution {
public int jump(int[] nums) {
int ans = 0;
int cover = 0; //当前覆盖范围
//比当前覆盖范围更大的最大覆盖范围,
//是在当前覆盖范围后得到的,所以走到这个位置需要比当前多一步,
//所以只有在走到当前最大覆盖范围的尽头时,如果还没到终点,就需要走这一步
int next = 0;
for(int i=0; i<nums.length-1; i++) {
next = Math.max(next, i + nums[i]); //更新下一步可以走到的最大范围
if(i == cover) { //走到当前覆盖范围的尽头,需要再走一步才能到达下一个最大覆盖范围
cover = next;
ans++;
}
if(cover >= nums.length-1) {
break;
}
}
return ans;
}
}
4.用最少数量的箭引爆气球
题目描述
题目链接
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
解题思路
- 先按照每个区间的左边界升序排序,然后遍历右边界。
- 注意考虑
(s1, s2, e1, e2)
这种情况会导致当前覆盖范围缩小。 - 注意
-2^31 <= xstart < xend <= 2^31 - 1
,Arrays.sort(points, (p1, p2) -> p1[0] - p2[0]);
会溢出,使用Integer.compare(p1[0], p2[0])
防止溢出。
代码
class Solution {
public int findMinArrowShots(int[][] points) {
Arrays.sort(points, (p1, p2) -> Integer.compare(p1[0], p2[0])); //按照start升序排序
int ans = 1;
int curEnd = points[0][1];
for(int i=1; i<points.length; i++) {
if(points[i][0] <= curEnd) {
curEnd = Math.min(curEnd, points[i][1]); // (s1, s2, e2, e1)这种情况会导致覆盖范围缩小
} else {
ans++;
curEnd = points[i][1];
}
}
return ans;
}
}
5.无重叠区间
题目描述
题目链接
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
解题思路
- 先按照左边界排序
- 若两个排序后的相邻区间有重叠,则删除往后涉及范围更大的那个区间(也就是忽略这个被删除的区间,将没被删除的区间的右边界视作新的边界
代码
class Solution {
//先按照左边界排序
//若两个排序后的相邻区间有重叠,则删除往后涉及范围更大的那个区间(也就是忽略这个被删除的区间,将没被删除的区间的右边界视作新的边界
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (i1, i2) -> Integer.compare(i1[0], i2[0]));
int ans = 0;
int curEnd = intervals[0][1];
for(int i=1; i<intervals.length; i++) {
if(intervals[i][0] < curEnd) { //有重叠
ans++;
curEnd = Math.min(curEnd, intervals[i][1]);
} else {
curEnd = intervals[i][1];
}
}
return ans;
}
}
6.划分字母区间
题目描述
解题思路
- 先找到每种字符的最远边界。
- 设置一个变量curEnd更新当前片段的所有字符的最远边界。
- 当
当前字符的下标 = 当前片段的所有字符的最远边界
时(后面不会再出现当前片段的字符了),得到一段最小划分。
代码
class Solution {
public List<Integer> partitionLabels(String s) {
int[] endForChar = new int[26]; //记录每种字符出现的最后位置
for(int i=0; i<s.length(); i++) {
endForChar[s.charAt(i)-'a'] = i;
}
List<Integer> ans = new ArrayList<>();
int curStart = 0;
int curEnd = 0;
for(int i=0; i<s.length(); i++) {
curEnd = Math.max(curEnd, endForChar[s.charAt(i)-'a']); //更新当前片段的所有字符的最远边界
if(i == curEnd) {
ans.add(curEnd - curStart + 1);
curStart = curEnd + 1;
}
}
return ans;
}
}
7.合并区间
题目描述
题目链接
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
解题思路
- 喵喵喵
代码
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (i1, i2) -> Integer.compare(i1[0], i2[0]));
List<int[]> ans = new ArrayList<>();
int curStart = intervals[0][0];
int curEnd = intervals[0][1];
for(int i=1; i<intervals.length; i++) {
if(intervals[i][0] <= curEnd) {
curEnd = Math.max(curEnd, intervals[i][1]);
} else {
ans.add(new int[]{curStart, curEnd});
curStart = intervals[i][0];
curEnd = intervals[i][1];
}
}
ans.add(new int[]{curStart, curEnd});
return ans.toArray(new int[ans.size()][2]);
}
}
8.监控二叉树
题目描述
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
解题思路
-
尽量把摄像头安装在父节点上
-
如何判断父节点是否需要安装?-> 根据子节点的状态 -> 后序遍历,且需要返回子节点的状态
-
递归函数返回子节点状态:有摄像头/被覆盖/无覆盖
-
null节点返回有覆盖状态,才能让叶节点的父结点放摄像头
代码
class Solution {
//0 有摄像头
//1 有覆盖
//-1 无覆盖
int ans;
public int minCameraCover(TreeNode root) {
ans = 0;
if(backtrack(root) == -1) {
ans++;
}
return ans;
}
int backtrack(TreeNode root) {
if(root == null) return 1; //空节点设置为有覆盖状态!
int left = backtrack(root.left);
int right = backtrack(root.right);
//root的子树中有没有被覆盖的 -> 必须在root放摄像头
if(left == -1 || right == -1) {
ans++;
return 0;
}
//此时子树都不是未覆盖状态(上下代码不能颠倒)
//root的子树有摄像头 -> 不用在root放摄像头
if(left == 0 || right == 0) {
return 1;
}
//root需要被覆盖
return -1;
}
}
9.加油站
题目描述
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
解题思路
- 设
rest[i] = gas[i] - cost[i]
- 如果
总油量 - 总消耗 >= 0
一定可以跑完一圈,说明rest[i]相加一定是大于等于0的 - 从 i = 0 开始累加
rest[i]
,记为curSum
。一旦curSum < 0
,说明区间[0, i]
都不能作为起始位置,因为在这个区间中选择任何一个位置作为起点,到i这里都会断油(因为前面的累计和preSum只会让curSum更大,因为是正数,如果是正数早就提前断油了)。那么只能从i+1
位置开始,重新从0累计curSum。
代码
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int totalSum = 0;
int curSum = 0;
int start = 0;
for(int i=0; i<gas.length; i++) {
curSum += (gas[i] - cost[i]);
totalSum += (gas[i] - cost[i]);
if(curSum < 0) {
curSum = 0;
start = i + 1;
}
}
if(totalSum < 0) return -1;
return start;
}
}