贪心
455.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j
分配给孩子 i
,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。
思路: 贪心, 让每一个大饼干配大胃口, 充分利用饼干
代码:
class Solution {// 孩子 饼干
public int findContentChildren(int[] g, int[] s) {
int res = 0;// 返回结果
Arrays.sort(g);// 让胃口增序
Arrays.sort(s);// 让饼干增序
int index = s.length - 1;// 控制饼干(从大到小)
for(int i = g.length - 1;i >= 0;i--) {// 遍历每一个较大胃口, 找符合的较大饼干
if(index >= 0 && g[i] <= s[index]) {
index--;
res++;
}
}
return res;
}
}
注: 不能外层for遍历s, 内层if遍历g, 因为外层一直在递减, 内层不一定会减, 如果外层的饼干最大值满足不了最大的胃口, 那么整个for都满足不了, 最终返回0. 但是如果外层的胃口不能被当前的饼干满足, 更小的胃也许可以, 要找到能被满足的胃
376.摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)
是正负交替出现的。 - 相反,
[1, 4, 7, 2, 5]
和[1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度 。
思路: 贪心
局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
本题要考虑三种情况:
- 情况一:数组首尾两端 (延长pre, 默认统计末尾)
- 情况二:上下坡中有平坡 (将平坡与转折点也纳入统计)
- 情况三:单调坡中有平坡 (只在单调方向改变时更新preDiff, 2中==的情况只用于判断被延长的开头)
具体细节: https://programmercarl.com/0376.%E6%91%86%E5%8A%A8%E5%BA%8F%E5%88%97.html#%E6%80%9D%E8%B7%AF
代码:
class Solution {
public int wiggleMaxLength(int[] nums) {
// 只有一个元素, 不符合下面代码逻辑(length >= 2), 直接返回
if(nums.length == 1) return 1;
int res = 1;// 默认统计最后一个数值
int preDiff = 0, curDiff = 0;// 延长preDiff,使length==2时可以操作
for(int i = 0;i < nums.length - 1;i++) {// 不再统计最后一个数值
curDiff = nums[i+1] - nums[i];// 每位i都要计算curDiff
if(preDiff <= 0 && curDiff > 0 || preDiff >= 0 && curDiff < 0) {
res++;
preDiff = curDiff;
// curDiff单调方向变化时才更新preDiff, 避免有平坡的递增的平坡被错误统计
}
}
return res;
}
}
53.最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
思路: 贪心, 保持sum >= 0, 使其可以递增, 用res记录sum的最大值
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length == 1) return nums[0];
// 保存结果, 初始化为最小值
int res = Integer.MIN_VALUE;
// 每个序列的总和
int sum = 0;
for(int i = 0;i < nums.length;i++) {
sum+=nums[i];// 累加该位的值
if(sum > res) res = sum;
// 先保存较大的值, 然后判断是否重置sum, 否则可能出现sum<0,被重置,但又>res, 从而使res=0
if(sum < 0) sum = 0;// 如果累加和<0, 初始化为0, 从下一个值开始重新统计
}
return res;
}
}
122.买卖股票的最佳时机 II
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
思路: 贪心, 每次累加正利润, 使res逐渐增大, 从而使最终结果为最大值
代码:
class Solution {
public int maxProfit(int[] prices) {
int res = 0;
for(int i = 1;i < prices.length;i++) {
res+=Math.max(0,prices[i] - prices[i - 1]);// 累加正利润
}
return res;
}
}
55.跳跃游戏
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
思路: 贪心, 依据可跳跃的长度的下标覆盖范围, 每次找最大的下标覆盖范围, 从而使整体下标覆盖范围最大, 判断最大覆盖范围能否到达数组末尾
代码:
class Solution {
public boolean canJump(int[] nums) {
// 只有一个值, 默认位于第一个下标, 直接返回true
if(nums.length == 1) return true;
int cover = 0;// 可跳位置的下标覆盖范围
for(int i = 0;i <= cover;i++) {
cover = Math.max(cover,i + nums[i]);// 不断更新cover为最大的覆盖范围, 使其更易走到末尾
if(cover >= nums.length - 1) return true;// 如果cover能够覆盖到最后一个下标, 直接返回true
}
return false;// for里没有返回true, cover覆盖不到最后一个下标, 返回false
}
}
45.跳跃游戏 II
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向后跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
思路: 贪心, 找数组所有值中最大的覆盖范围, 看能否覆盖到数组末尾.
代码:
class Solution {
public int jump(int[] nums) {
if(nums.length == 1) return 0;// 数组只有一个值的时候要特殊判断, 否则在for里会res++
int res = 0;
int curDistance = 0, nextDistance = 0;
// 数组当前覆盖范围, 下一个最大覆盖范围
for(int i = 0;i < nums.length;i++) {
nextDistance = Math.max(i + nums[i],nextDistance);// 每次尝试更新下一个覆盖范围
if(i == curDistance) {// 当前走到当前数组最大覆盖范围, 尝试更新覆盖范围
res++;// 每更新一次就是再跳一个覆盖范围更大的数值
curDistance = nextDistance;// 更新覆盖范围
if(curDistance >= nums.length - 1) break;// 如果更新后的能覆盖到末尾, 程序终止
}
}
return res;// 返回
}
}
1005.K 次取反后最大化的数组和
给你一个整数数组 nums
和一个整数 k
,按以下方法修改该数组:
- 选择某个下标
i
并将nums[i]
替换为-nums[i]
。
重复这个过程恰好 k
次。可以多次选择同一个下标 i
。
以这种方式修改数组后,返回数组 可能的最大和 。
思路:
贪心, 局部最大: 对绝对值最大的负数, 绝对值最小的正数取反, 从而实现全局总和最大, 有总和最大的正数+绝对值最小的负数
代码:
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
// 对num排序, 使其按照绝对值大小降序排序(绝对值最小的放在最后)
nums = IntStream.of(nums).boxed().sorted((a,b) -> Math.abs(b) - Math.abs(a)).mapToInt(Integer::intValue).toArray();
for(int i = 0;i < nums.length;i++) {
if(nums[i] < 0 && k > 0) {// 按绝对值的大小, 将负数取反
nums[i] = -nums[i];
k--;// 注意, 取反后要k--
}
}
if(k % 2 == 1) nums[nums.length - 1] *= -1;
// 由于for的控制, 此时k>=0, 如果k为奇数才将末尾取反
// 累加数组和
int res = 0;
for(int n : nums) res+=n;
return res;
}
}
134.加油站
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
思路: 贪心, 局部最优: 每个i开头的curSum, 总体最优: 最终确定的符合条件的i及curSum
代码:
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
// curSum: 从每个i开始到当前位置差值的sum
// totalSum: n个i的所有差值的sum
int curSum = 0,totalSum = 0;
int start = 0;// 符合条件的结果
// 1. 遍历数组
for(int i = 0;i < gas.length;i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if(curSum < 0) {// curSum的i不符合条件, 从i+1开始, curSum重新统计
start = i + 1;
curSum = 0;
}
}
// 2. 判断结果
// 如果totalSum < 0, 整个数组没有最终能成功的位置
if(totalSum < 0) return -1;
else return start;// >= 0, 则start就是最终结果
}
}
135.分发糖果
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
思路: 贪心. 局部最优: 从左往右分发最少, 从右往左分发最少. 总体最优: 两种条件都符合的情况下分发最少.
代码:
class Solution {
public int candy(int[] ratings) {
int len = ratings.length;
// 创建糖果数组存储每个孩子分发的糖果
int[] candyVal = new int[len];
candyVal[0] = 1;// 对0下标初始化
// 1. 满足右比左大
for(int i = 1;i < len;i++) {
candyVal[i] = ratings[i] > ratings[i - 1] ? (candyVal[i - 1] + 1) : 1;
}
// 2. 满足左比右大, 且与右比左大的结果取max
for(int i = len - 2;i >= 0;i--) {
if(ratings[i] > ratings[i + 1]) {
candyVal[i] = Math.max(candyVal[i],candyVal[i + 1] + 1);
}
}
// 3. 统计总糖果数
int res = 0;
for(int n : candyVal) {
res+=n;
}
return res;
}
}
860.柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5
美元。顾客排队购买你的产品,(按账单 bills
支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5
美元、10
美元或 20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5
美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills
,其中 bills[i]
是第 i
位顾客付的账。如果你能给每位顾客正确找零,返回 true
,否则返回 false
。
思路: 贪心. 局部最优: 每次找零的时候先用最大面值的10找20零, 以便留下更多5找10零. 全局最优: 所有需要找零的人都能成功找零.
代码:
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0, ten = 0, twenty = 0;
for(int bill : bills) {
if(bill == 5) {
// 1. 遇到5, 直接收下
five++;
}else if(bill == 10) {
// 2. 遇到10, 用5找零
if(five > 0) {// 够找零
five--;ten++;
}else return false;// 不够找零
}else{
// 3. 遇到20
if(ten > 0 && five > 0) {// 优先10 + 5找零
ten--;five--;twenty++;
}else if(five >= 3) {// 用3*5找零
five -= 3;twenty++;
}else return false;// 不够找零
}
}
return true;// 没有不够找零, 则全部找零成功
}
}
406.根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people
表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki]
表示第 i
个人的身高为 hi
,前面 正好 有 ki
个身高大于或等于 hi
的人。
请你重新构造并返回输入数组 people
所表示的队列。返回的队列应该格式化为数组 queue
,其中 queue[j] = [hj, kj]
是队列中第 j
个人的属性(queue[0]
是排在队列前面的人)。
思路: 贪心, 局部最优: 分别使数组排序满足身高要求, k值要求. 全局最优: 整个数组既满足h, 也满足k.
代码:
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 1. 按规则排序
Arrays.sort(people,(a,b) -> {
if(a[0] == b[0]) return a[1] - b[1];// 身高相同的按照k升序排列(k大的排在后面)
return b[0] - a[0];// 整体按照身高降序排序
});
// 2. 按照k调整(第i个前面的一定比它高, 所以只需要保证前面有k个就行, 自己刚好就排在第k位)
LinkedList<int[]> queue = new LinkedList<>();// 使用链表动态插入
for(int[] p : people) {
queue.add(p[1],p);// 将p插在第k(p[1])位
}
// 3. 按要求返回
return queue.toArray(new int[people.length][2]);// 使用LinkedList的toArray
}
}
452.用最少数量的箭引爆气球
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points
,其中points[i] = [xstart, xend]
表示水平直径在 xstart
和 xend
之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x
处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``start
,x``end
, 且满足 xstart ≤ x ≤ xend
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
思路: 贪心, 局部最优: 找出所有重叠的气球, 用一支箭射, 全局最优: 引爆所有气球使用的箭最少.
代码:
class Solution {
public int findMinArrowShots(int[][] points) {
// 1. 对没有气球的情况单独判断, 下面的逻辑至少需要1个气球
if(points.length == 0) return 0;
// 2. 将所有气球按照左边界进行排序
Arrays.sort(points,(a,b) -> Integer.compare(a[0], b[0]));// 否则会溢出
// 3. 统计重叠的气球, 更新res
int res = 1;// 默认至少要有一个去射击第一个气球(points[0])
for(int i = 1;i < points.length;i++) {// 从points[1]开始往后统计
if(points[i][0] > points[i-1][1]) {
// 3.1 如果 前气球的右边界<后气球的左边界(气球不重叠), res++
res++;
}else {
// 3.2 如果 前气球的右边界>=后气球的左边界(气球重叠)
// 更新当前气球的右边界为重叠气球右边界的最小值方便下轮循环使用, 看后序气球是否重叠
points[i][1] = Math.min(points[i-1][1],points[i][1]);
}
}
// 4. 返回
return res;
}
}
435.无重叠区间
给定一个区间的集合 intervals
,其中 intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
注意 只在一点上接触的区间是 不重叠的。例如 [1, 2]
和 [2, 3]
是不重叠的。
思路: 贪心. 局部最优: 找最多的共同重叠区间, 移除共同重叠区间. 全局最优: 移除的区间数最少.
代码:
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// if(intervals.length == 0) return 0;
// 1. 按照左边界排序
Arrays.sort(intervals,(a,b) -> a[0]-b[0]);
// 2. 遍历, 找重叠区间的个数
int res = 0;
for(int i = 1;i < intervals.length;i++) {
// 如果 前右 <= 后左, 不重叠, 不需要操作
// 如果 前右 > 后左, 重叠, (重叠数)res++
if(intervals[i-1][1] > intervals[i][0]) {
res++;
intervals[i][1] = Math.min(intervals[i-1][1],intervals[i][1]);
// 更新共同重叠区间的最小右边界, 查看下一个区间是否也重叠
}
}
return res;
}
}
763.划分字母区间
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc"
能够被分为 ["abab", "cc"]
,但类似 ["aba", "bcc"]
或 ["ab", "ab", "cc"]
的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
思路:
代码:
class Solution {
public List<Integer> partitionLabels(String s) {
// 1. 统计各个字符最远下标
char[] str = s.toCharArray();
int[] edge = new int[26];// map,k:字符对应的下标;v:字符所在的最远下标
for(int i = 0;i < str.length;i++) {
edge[str[i]-'a'] = i;// 不断更新字符的下标
}
// 2. 统计结果
List<Integer> res = new ArrayList<>();
int left = 0,right = 0;// lr用来记录一个区间的左右边界
for(int i = 0;i < str.length;i++) {
right = Math.max(right,edge[str[i]-'a']);// 在遍历一个区间的过程中不断更新右边界
if(i == right) {// 当前走到右边界, 说明该区间遍历完毕, 可以记录
res.add(right - left + 1);// 收集结果
left = i + 1;// 更新left
}
}
// 3. 返回
return res;
}
}
56.合并区间
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
思路: 收集区间, 根据区间重叠, 与被收集的区间合并, 若无法合并, 则收集新的区间, 并检查新区间接下来能否被合并.
代码:
class Solution {
public int[][] merge(int[][] intervals) {
// 1. 对0进行特殊判断, 下面的代码至少要有1个元素
if(intervals.length == 0) return new int[0][0];
// 2. 排序
Arrays.sort(intervals,(a,b) -> a[0] - b[0]);
// 3. 先将第一个元素加入res, 开始遍历区间集合
List<int[]> res = new ArrayList<>();
res.add(new int[]{intervals[0][0],intervals[0][1]});
for(int i = 1;i < intervals.length;i++) {
int[] lastInterval = res.get(res.size()-1);// 上次区间
int[] curInterval = intervals[i];// 本次区间
if(lastInterval[1] < curInterval[0]) {// 上右 < 当左
res.add(curInterval);
// 本次区间无法被合并,将本次区间加入res
}else {// 上右 >= 当左
lastInterval[1] = Math.max(lastInterval[1],curInterval[1]);
// 更新上右(与被收集的区间合并, 不需要再次收集)
}
}
// 4. 返回
return res.toArray(new int[res.size()][]);
}
}
738.单调递增的数字
当且仅当每个相邻位数上的数字 x
和 y
满足 x <= y
时,我们称这个整数是单调递增的。
给定一个整数 n
,返回 _小于或等于 n
的最大数字,且数字呈 _单调递增 。
思路: 从后往前(如果从前往后很可能后面的改为递增以后前面的成递减的了,因为改变后的数值不能大于起始数值, 如果从后往前可以用9保证>=) 遍历数字的每一位, 如果前一位>后一位, 前一位-1,后一位开始改为9
代码:
class Solution {
public int monotoneIncreasingDigits(int n) {
// 1. 将原数字转为字符数组
String num = String.valueOf(n);
char[] str = num.toCharArray();
// 2. 设置flag记录从哪个位置开始将数字改为'9'
int flag = str.length;
// 3. 遍历该数字
for(int i = str.length - 1;i > 0;i--) {
if(str[i-1] > str[i]) {// 当前一个数字>后一个数字
str[i-1]--;// 前一个数字-1
flag = i;// 从后一个数字开始更新为'9'
}
}
// 4. 根据flag更新数字
for(int i = flag;i < str.length;i++) {
str[i] = '9';
}
// 5. 将字符数组转为int返回
return Integer.parseInt(String.valueOf(str));
}
}
968.监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
思路: 贪心+状态转移. 局部最优: 只在叶节点的父节点安摄像头(中间, 既可以覆盖上, 又可以覆盖下), 从而使局部摄像头最少. 全局最优: 整个二叉树上安装的摄像头最少.
代码:
class Solution {
// 0.该节点无覆盖 1.本节点有摄像头 2. 本节点有覆盖
int res = 0;// 注意: 不能为static,保证每个类测试的独立性
public int minCameraCover(TreeNode root) {
// 1. 如果根节点无覆盖, 需要在根节点一个摄像头
if(traversal(root) == 0) res++;
return res;
}
public int traversal(TreeNode root) {
// 1. 递归出口: 遍历到null, 则null的状态为有覆盖, 以保证叶节点的上一个结点可以被放摄像头
if(root == null) return 2;
// 2. 递归主要逻辑: 后序遍历
// 2.1 左右
int left = traversal(root.left);
int right = traversal(root.right);
// 2.2 中
// 2.2.1 左右子节点都有覆盖(后序:子节点的子节点有摄像头), 则父节点无覆盖
if(left == 2 && right == 2) return 0;
// 2.2.2 左右子节点至少有一个无覆盖, 则父节点有摄像头
if(left == 0 || right ==0) {
res++;
return 1;
}
// 2.2.3 左右子节点至少有一个有摄像头, 则父节点有覆盖
if(left == 1 || right == 1) return 2;
// 3. 为了代码格式完整性, 实际不会走到这一步
return -1;
}
}