代码随想录之贪心算法

贪心算法

分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。

贪心算法适合用在每一步都能做出局部最优,并且全局最优解可以由局部最优推出的场景,且每次做的选择不会影响后面的全局最优。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
// 大饼干喂给胃口大的,避免浪费的同时形成最优解,考虑胃口
// 或者小饼干喂给小胃口的,考虑饼干
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int start = s.length - 1;
int count = 0;
// 遍历大胃口,将大饼干喂给恰好小于饼干值的大胃口,即寻找每一个大饼干合适的小孩
// 局部最优:大饼干喂给大胃口
// 全局最优:喂饱尽可能多的孩子
for(int i = g.length - 1; i >= 0 && start >= 0; i--){
if(g[i] <= s[start]){
start--;
count++;
}
}
return count;
}
}

摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

注意平坡问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
// 在坡度变化的时候修改prediff,避免平坡问题
// 局部最优:考虑峰值
// 全局最优:保证摆动序列
public int wiggleMaxLength(int[] nums) {
if(nums.length <= 1){
return nums.length;
}

int curDiff = 0;
int preDiff = 0;
int count = 1;
for(int i = 1; i < nums.length; i++){
curDiff = nums[i] - nums[i - 1];
if((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)){
count++;
preDiff = curDiff;
}
}
return count;
}
}

最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。

使用贪心算法时理清局部最优和全局最优。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
// 局部最优:连续和为负数时放弃,保证局部为正数
// 全局最优:选取最大连续和
public int maxSubArray(int[] nums) {
if(nums.length == 1){
return nums[0];
}
int sum = Integer.MIN_VALUE;
int count = 0;
for(int i = 0; i < nums.length; i++){
count += nums[i];
sum = Math.max(sum, count); // 必须实时
if(count <= 0){
count = 0;
}
}
return sum;
}
}

买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
// 局部最优:收集每天的正利润
// 全局最优:获得最大利润
public int maxProfit(int[] prices) {
int res = 0;
for(int i = 1; i < prices.length; i++){
res += Math.max(prices[i] - prices[i - 1], 0);
}
return res;
}
}

跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public boolean canJump(int[] nums) {
// 局部最优:跳最远
// 全局最优:能不能覆盖到终点
if(nums.length == 1){
return true;
}
int coverRange = 0;
// 之所以用coverRange终止,是因为只能跳那么远
for(int i = 0; i <= coverRange; i++){
coverRange = Math.max(coverRange, i + nums[i]);
if(coverRange >= nums.length - 1){
return true;
}
}
return false;
}
}

跳跃游戏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]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
// 局部最优:尽可能跳得更远
// 全局最优:最少步数
public int jump(int[] nums) {
int res = 0;
int curDis = 0;
int nextDis = 0;
for(int i = 0; i <= curDis && curDis < nums.length - 1; i++){
nextDis = Math.max(nextDis, i + nums[i]);
if(i == curDis){
// 到达当前最大覆盖范围,准备跳跃
curDis = nextDis; // 更新为下次的最大覆盖范围
res++; //跳跃
}
}
return res;
}
}

加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
// 局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行
// 全局最优:找到可以跑一圈的起始位置
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0;
int totalSum = 0;
int start = 0;
for(int i = 0; i < gas.length; i++){
totalSum += gas[i] - cost[i];
curSum += gas[i] - cost[i];
if(curSum < 0){
curSum = 0;
start = i + 1;
}
}
if(totalSum < 0) return -1;
return start;
}
}

分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
// 贪心两次,防止顾此失彼
public int candy(int[] ratings) {
int len = ratings.length;
int[] candy = new int[len];
candy[0] = 1;
// 第一次贪心,局部最优:保证右边的比左边多
// 故正向遍历
for(int i = 1; i < len; i++){
candy[i] = (ratings[i] > ratings[i - 1]) ? candy[i - 1] + 1 : 1;
}

// 第二次贪心,局部最优:保证左边的比右边多
// 故反向遍历
for(int i = len - 2; i >= 0; i--){
if(ratings[i] > ratings[i + 1]){
candy[i] = Math.max(candy[i], candy[i + 1] + 1);
}
}

int res = 0;
for(int num : candy){
res += num;
}
return res;
}
}

根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
// 先按照身高从大到小排序
// 局部最优:身高高的优先插入正确位置,因为后续身高矮的插入不会影响结果
// 全局最优:正确队列
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<>();

for(int[] p : people){
queue.add(p[1], p);
}
return queue.toArray(new int[people.length][]);
}
}

用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
// 局部最优:射击重叠的气球
// 全局最优:用的弓箭最少
public int findMinArrowShots(int[][] points) {
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0])); // 避免溢出

int count = 1; // 第0个气球
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;
}
}

无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
注意 只在一点上接触的区间是 不重叠的。例如 [1, 2] 和 [2, 3] 是不重叠的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
// 类似于气球题
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

int count = 1;
for(int i = 1; i < intervals.length; i++){
if(intervals[i][0] >= intervals[i - 1][1]){
count++;
} else {
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]); // 用min是为了防止气球遗漏
}
}
return intervals.length - count;
}
}

合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int[][] merge(int[][] intervals) {
LinkedList<int[]> res = new LinkedList<>();
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
res.add(intervals[0]);
for(int i = 1; i < intervals.length; i++){
if(intervals[i][0] <= res.getLast()[1]){
// 重叠区间
int start = res.getLast()[0];
int end = Math.max(intervals[i][1], res.getLast()[1]);
res.removeLast();
res.add(new int[]{start, end});
} else {
res.add(intervals[i]);
}
}
return res.toArray(new int[res.size()][]);
}
}

单调递增的数字

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int monotoneIncreasingDigits(int n) {
String s = String.valueOf(n);
char[] chars = s.toCharArray();
int start = s.length();
for(int i = s.length() - 2; i >= 0; i--){
if(chars[i] > chars[i + 1]){
chars[i]--;
start = i + 1;
}
}
for(int i = start; i < s.length(); i++){
chars[i] = '9';
}
return Integer.parseInt(String.valueOf(chars));
}
}

监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
// 局部最优:叶子结点的父节点安装摄像头
// 全局最优:总共用的摄像头最少
static int res;
public int minCameraCover(TreeNode root) {
res = 0;
if(recursive(root) == 0) res++;
return res;
}

// 0:未被监控
// 1:监控但不是摄像头
// 2:自身是摄像头
private static int recursive(TreeNode root){
if(root == null) return 1; // 便于叶子节点的判断
int left = recursive(root.left);
int right = recursive(root.right);
if(left == 0 || right == 0){
// 需要在自己身上安装摄像头
res++;
return 2;
}
// 左右节点都被监控,自身不安装摄像头,减少数量
if(left == 1 && right == 1){
return 0;
}
// 左右节点有至少一个摄像头,已被监控
return 1;
}
}

代码随想录之贪心算法
http://example.com/2025/06/17/代码随想录之贪心算法/
作者
jietiDdd
发布于
2025年6月17日
许可协议