算法-贪心
贪心法能够成立的条件是:通过局部最优能够得到全局最优。
1. 分发饼干(LeetCode 455)
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;
并且每块饼干 j,都有一个尺寸 s[j] 。
如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。
你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
思路: 先对两个数组排序,用满足孩子的胃口的最小饼干进行投喂。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
for(int i = 0, j = 0; i<g.length && j<s.length; ++j) {
if(s[j] >= g[i]) {
count++;
i++;
}
}
return count;
}
}
2. 摆动序列(LeetCode 376)
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
思路:将序列连成一条线,观察其上升和下降,取转折点(也就是摆动序列中的正负交替)作为子序列。
class Solution {
// 观察序列的上升和下降,取波峰和波谷的转折点作为子序列
public int wiggleMaxLength(int[] nums) {
int count = 1;
int prediff = 0;
int curdiff = 0;
for(int i = 1; i<nums.length; ++i) {
curdiff = nums[i] - nums[i-1];
// 出现摆动的时候才更新prediff
// 避免不减序列中出现连续相等的情况,如1 2 2 3
if((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0)) {
count++;
prediff = curdiff;
}
}
return count;
}
}
3. 最大子数组和(LeetCode 53)
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组
是数组中的一个连续部分。
class Solution {
public int maxSubArray(int[] nums) {
int result = Integer.MIN_VALUE;
int sum = 0;
for(int i = 0; i<nums.length; ++i) {
sum += nums[i];
if(sum > result)
result = sum;
// 如果sum<0了,说明需要从下一个起点重新开始了
if(sum < 0)
sum = 0;
}
return result;
}
}
4. 买卖股票的最佳时机II (LeetCode 122)
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
思路:收集每天的正利润,累加作为最大利润。
class Solution {
public int maxProfit(int[] prices) {
int result = 0;
int curdiff = 0;
for(int i = 1; i<prices.length; ++i) {
curdiff = prices[i] - prices[i-1];
if(curdiff > 0) {
result += curdiff;
}
}
return result;
}
}
5. 跳跃游戏(LeetCode 55)
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
思路:贪心法,每次更新最大覆盖范围,如果最大覆盖范围大于等于最后一个下标,则返回true。
class Solution {
public boolean canJump(int[] nums) {
// 最大覆盖范围
int cover = nums[0];
for(int i = 0; i<=cover; ++i) {
// 更新最大覆盖距离
if(i + nums[i] > cover)
cover = i + nums[i];
if(cover >= nums.length-1)
return true;
}
return false;
}
}
6. 跳跃游戏 II(LeetCode 45)
给定一个长度为 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]。
思路:
- 用双指针
precover
和curcover
记录当前一步和下一步能够覆盖的最大距离 - 如果i到达了precover,但还没有到数组的末尾,则需要再跳一步
jump++
class Solution {
public int jump(int[] nums) {
if(nums.length == 1) return 0;
int precover = nums[0];
int curcover = precover;
int jump = 1;
for(int i = 0; i<=precover; ++i) {
if(precover >= nums.length-1) {
break;
}
if(i + nums[i] > curcover) {
curcover = i + nums[i];
}
if(i == precover) {
precover = curcover;
jump++;
}
}
return jump;
}
}
7. K次取反后最大化的数组和(LeetCode 1005)
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
思路:
- 有负数的时候,先翻转绝对值大的负数
- 当所有负数均翻转完成,仍有盈余翻转次数,则对最小的非负数进行翻转。
- 盈余次数为偶数,则抵消;为奇数时,需要进行实质性的翻转
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
int sum = 0;
Arrays.sort(nums);
for(int i = 0; i<nums.length && k>0; ++i) {
if(nums[i] < 0) {
nums[i] *= -1;
k--;
}
else
break;
}
// 所有的负数已经变为正数,还剩奇数次
// 将当前最小的非负数取反
if(k%2 == 1) {
Arrays.sort(nums);
nums[0] *= -1;
}
for(int i = 0; i<nums.length; ++i){
sum += nums[i];
}
return sum;
}
}
8. 加油站(LeetCode 134)
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。
如果存在解,则保证它是唯一
的。
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
// 从当前start出发的累计剩余油量
int curRest = 0;
int totalRest = 0;
int start = 0;
for(int i = 0; i<gas.length; ++i) {
totalRest += (gas[i] - cost[i]);
curRest += (gas[i] - cost[i]);
// 说明从之前的start出发不可行,至少要从i+1开始
if(curRest < 0) {
start = i+1;
curRest = 0;
}
}
// 不可能环绕一周
if(totalRest < 0)
return -1;
return start;
}
}
9. 分发糖果(LeetCode 135)
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
思路:
- 一次遍历比较一种情况:左边>右边(正向遍历),右边>左边(反向遍历)
- 从右向左遍历的中,取较大值,是为了同时满足该位置与左孩子、右孩子的大小关系
class Solution {
public int candy(int[] ratings) {
int sum = 0;
int[] arr = new int[ratings.length];
for(int i = 0; i<ratings.length; ++i) {
arr[i] = 1;
}
// 右孩子比左孩子评分高,从左向右遍历
for(int i = 1; i<ratings.length; ++i) {
if(ratings[i] > ratings[i-1])
arr[i] = arr[i-1] + 1;
}
// 左孩子比右孩子评分高,从右向左遍历
for(int i = ratings.length - 2; i>=0; --i) {
if(ratings[i] > ratings[i+1])
// 取正向遍历和右孩子+1二者中的较大值
arr[i] = Math.max(arr[i], arr[i+1] + 1);
}
for(int i = 0; i<ratings.length; ++i) {
sum += arr[i];
}
return sum;
}
}
10. 柠檬水找零(LeetCode 860)
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
class Solution {
public boolean lemonadeChange(int[] bills) {
// 分别代表5,10,20美元的数量
int[] money = new int[3];
for(int i = 0; i<money.length; ++i) {
money[i] = 0;
}
for(int i = 0; i<bills.length; ++i) {
if(bills[i] == 5){
money[0]++;
}
else if(bills[i] == 10) {
if(money[0] > 0){
money[0]--;
money[1]++;
}
else
return false;
}
else {
if(money[1] > 0 && money[0] > 0) {
money[2]++;
money[1]--;
money[0]--;
} else if(money[0] >= 3) {
money[2]++;
money[0]-=3;
}
else
return false;
}
}
return true;
}
}
11. 根据身高重建队列(LeetCode 406)(有难度)
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。
每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面正好有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。
返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
思路:
- 按照身高降序排列,然后的for循环类似于插牌,将根据k将每个元素插入到对应位置
- 降序排列的目的:后面人的身高小于前面人,所以插入后不会破坏
排在 前面人 之前的 大于等于其身高的人数
class Solution {
public int[][] reconstructQueue(int[][] people) {
LinkedList<int[]> que = new LinkedList<>();
// 先按身高降序排列,若身高相同则按k升序排列
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0];
});
for(int i = 0; i<people.length; ++i) {
// 参数1:插入位置;参数2:插入元素
que.add(people[i][1], people[i]);
}
return que.toArray(new int[people.length][2]);
}
}
12. 用最少的箭引爆气球(LeetCode 452)
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。
可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
思路:
- 问题抽象:用多少个点可以覆盖所有的区间
- 对于重叠的区间,只需要射出一只箭,箭的射出位置为重叠区间的
最小右边界
class Solution {
public int findMinArrowShots(int[][] points) {
// points不为空,至少需要一只箭
int count = 1;
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
for(int i = 1; i<points.length; ++i) {
if(points[i][0] > points[i-1][1])
count++;
else
// 更新为重叠气球的最小右边界
// 因为end大于这个值的、在当前重叠区间的气球,已经被引爆了
points[i][1] = Math.min(points[i-1][1], points[i][1]);
}
return count;
}
}
13. 删除区间(LeetCode 435)
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
思路:
- 和上一题射箭的思路相似,保留
重叠区间中的最小右边界
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
int count = 0;
Arrays.sort(intervals, (a,b)->Integer.compare(a[0], b[0]));
for(int i = 1; i<intervals.length; ++i) {
if(intervals[i][0] < intervals[i-1][1]) {
count++;
intervals[i][1] = Math.min(intervals[i-1][1], intervals[i][1]);
}
}
return count;
}
}
14. 划分字母区间(LeetCode 763)
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
思路:
- 用一个map,存储每个字母出现的最大数组下标
- 注意结果收集的位置,这里的实现是在每次更新start前,以及for循环接收后收集最后一个区间
class Solution {
public List<Integer> partitionLabels(String s) {
// 用于存储每个字母最晚出现的下标
int[] map = new int[26];
for(int i = 0; i<s.length(); ++i) {
map[s.charAt(i) - 'a'] = i;
}
List<Integer> result = new ArrayList<>();
int start = 0;
int end = map[s.charAt(0) - 'a'];
for(int i = 0; i<s.length(); ++i) {
if(i <= end) {
if(map[s.charAt(i) - 'a'] > end){
end = map[s.charAt(i) - 'a'];
}
} else {
result.add(end - start + 1);
start = i;
end = map[s.charAt(i) - 'a'];
}
}
result.add(end - start + 1);
return result;
}
}
15. 合并区间(LeetCode 56)
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。
请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> result = new ArrayList<>();
Arrays.sort(intervals, (a,b) -> Integer.compare(a[0], b[0]));
int start = intervals[0][0];
int end = intervals[0][1];
for(int i = 1; i<intervals.length; ++i) {
if(intervals[i][0] <= end) {
end = Math.max(end, intervals[i][1]); // 取重叠区间的最大右边界
} else {
result.add(new int[]{start, end});
start = intervals[i][0];
end = intervals[i][1];
}
}
result.add(new int[]{start, end});
return result.toArray(new int[result.size()][2]);
}
}
16. 单调递增的数字(LeetCode 738)
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
输入: n = 332
输出: 299
class Solution {
public int monotoneIncreasingDigits(int n) {
String s = String.valueOf(n);
char[] chars = s.toCharArray();
// 9开始的下标
int nineStart = s.length();
// 从后往前遍历
for(int i = s.length() - 1; i>0; --i) {
// 发现不符合递增的,将后一位置为9,前一位减一
if(chars[i] < chars[i-1]) {
chars[i-1]--;
nineStart = i;
}
}
// 将末尾的数字置为9
for(int i = nineStart; i<s.length(); ++i) {
chars[i] = '9';
}
return Integer.parseInt(String.valueOf(chars));
}
}
17. 监控二叉树(LeetCode 968)(有难度)
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
思路:
- 从叶子节点的父节点开始设置摄像头,自底向上地设置(后序遍历)
- 为每个节点维护一个状态
state
,分别表示该节点{有摄像头,被覆盖,未覆盖}
- if-else的设置顺序需要仔细考虑
class Solution {
int count = 0;
public int minCameraCover(TreeNode root) {
if(postOrderTraverse(root) == 3)
count++;
return count;
}
public int postOrderTraverse(TreeNode root) {
// 1表示有摄像头,2表示被覆盖,3表示未覆盖
int state;
if(root == null) {
state = 2;
return state;
}
int left = postOrderTraverse(root.left);
int right = postOrderTraverse(root.right);
if(left == 3 || right == 3) {
// 5种情况 13,23,33,31,32
state = 1;
count++;
} else if(left == 1 || right == 1){
// 3种情况 11,12,21
state = 2;
} else {
// 1种情况 22
state = 3;
}
return state;
}
}