贪心算法
- 什么是贪心算法
- [122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/)
- 代码
- [455. 分发饼干](https://leetcode.cn/problems/assign-cookies/)
- 思路
- 代码
- [435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/)
- 1.动态规划
- 2.贪心
- [55. 跳跃游戏](https://leetcode-cn.com/problems/jump-game/) (medium)
- 1.动态规划
- 2.贪心
- [881. 救生艇](https://leetcode.cn/problems/boats-to-save-people/)
- 思路
- 代码
- [452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/)
- 代码
- [134. 加油站](https://leetcode.cn/problems/gas-station/)
- 思路
- 代码
- [860. 柠檬水找零](https://leetcode.cn/problems/lemonade-change/)
- 代码
什么是贪心算法
贪心法,又称贪心算法,贪婪算法,在对问题求解时,总是做出在当前看来最好的选择,期望通过每个阶段的局部最优选择达到全局最优,但结果不一定最优
适用场景:简单的说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解,就能用贪心算法的到最后的最优解,这种子问题最优解称为最优子结构
贪心算法与动态规划的不同点在于它对每个子问题的解决方案都做出当前的最优选择,不能回退,而动态规划会保留之前的运算结果,并根据之前的结果进行选择,有回退的功能,贪心是动态规划的理想化的情况。
122. 买卖股票的最佳时机 II
难度中等
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
- 思路:因为不限制交易次数,只要今天价格比昨天高,就交易,利润为正累加,最后的和就是最大的利润,注意第一天是没有利润的,这道题之所以可以用贪心是因为局部最优:收集每天的正利润,可以推导出,全局最优:求得最大利润。
- 复杂度分析:时间复杂度
O(n)
,n是数组的长度。空间复杂度是O(1)
代码
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int res = 0;
for (int i = 1; i < n; ++i) {
//今天价格和昨天的差值是否为正,如果为正累加进去,为负则加0
res += Math.max(0, prices[i] - prices[i - 1]);
}
return res;
}
}
455. 分发饼干
难度简单
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j
分配给孩子 i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
示例 2:
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
提示:
1 <= g.length <= 3 * 104
0 <= s.length <= 3 * 104
1 <= g[i], s[j] <= 231 - 1
思路
- 思路:大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。排序两个数组,从右到左遍历,用大饼干首先满足胃口大的小孩
- 复杂度:时间复杂度
O(mlogm + nlogn)
。空间复杂度O(logm + logn)
代码
class Solution {
public int findContentChildren(int[] g, int[] s) {
//排序
Arrays.sort(g);
Arrays.sort(s);
int nums = 0;
int index = s.length -1;//饼干的下标
for (int i = g.length - 1; i >= 0; i--) {
//如果当前饼干能够分给要求最大的孩子
if(index >= 0 && s[index] >= g[i]){
nums++;
index--;
}
}
return nums;
}
}
435. 无重叠区间
难度中等
给定一个区间的集合 intervals
,其中 intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
提示:
1 <= intervals.length <= 105
intervals[i].length == 2
-5 * 104 <= starti < endi <= 5 * 104
1.动态规划
- 思路:
dp[i]
表示前i个区间中最大不重合区间的个数,首先将区间数组按左边界排序,找出intervals中最多有多少个不重复的区间,动态规划方程dp[i] = Math.max(dp[i], dp[j] + 1)
。intervals的长度减去最多的不重复的区间 就是最少删除区间的个数 - 复杂度:时间复杂度
O(n^2)
,两层嵌套循环leetcode执行超时 复杂度过高。空间复杂度O(n)
,dp数组的空间
超时
public int eraseOverlapIntervals(int[][] intervals) {
int n = intervals.length;
int[] dp = new int[n];
//按照左边界排序
Arrays.sort(intervals,(o1,o2)->{
return o1[0] - o2[0];
});
Arrays.fill(dp, 1);
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (intervals[j][1] <= intervals[i][0]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return n - Arrays.stream(dp).max().getAsInt();
}
2.贪心
- 思路:intervals按右边界排序,然后从左往右遍历,右边界结束的越早,留给后面的区间的空间就越大,不重合的区间个数就越多,intervals的长度减去最多的不重复的区间 就是最少删除区间的个数
- 复杂度:时间复杂度
O(nlogn)
,数组排序O(nlogn)
,循环一次数组O(n)
。空间复杂度O(logn)
,排序需要的栈空间
https://leetcode.cn/problems/non-overlapping-intervals/solution/435-wu-zhong-die-qu-jian-tan-xin-jing-di-qze0/
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
//按照右边界排序
//按右边界排序,然后从左往右遍历,右边界结束的越早,
//留给后面的区间的空间就越大,不重合的区间个数就越多
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals,(o1,o2)->{
return Integer.compare(o1[1],o2[1]);
});
int n = intervals.length;
int nums = 0;
int right = intervals[0][1];
for (int i = 1; i < n; i++) {
if(intervals[i][0] < right){
//如果重叠就丢掉
nums++;
}else{
//不重叠就更新
right = intervals[i][1];
}
}
return nums;
}
}
能不能用贪心算法需要满足贪心选择性,贪心算法正确的的证明可以用反证法
以这一题为例:
- 我们的思路是保留最多的不重合的区间,所以按照区间结尾排序,区间结尾结束的越早且和前一个区间不重叠的,就加入最多不重复的区间中,我们称为算法a,假如算法a中的某一个步骤是选择区间
[a, b]
,我们称为区间A。 - 假设这个选择不正确,也就是说算法a得到的不是最优解。
- 我们假设存在另一个算法c能得到最优解,算法c中的一个步骤是选择区间
[c, d]
,我们称为区间C,使得它是最优解中的一个区间,其中d>b
,因为算法a选择的是结尾最先结束且不重合的区间,如果算法a不正确,又因为区间数组中的区间是固定的,则其他算法c肯定存在d>b
的情况。 - 我们用区间A替换区间C完全不影响算法c的结果,因为
b<d
,所以不影响区间C后面区间的结果。所以我们选择了区间A也构成了一个最优解。而我们假设的是选择区间A不是最优解,所以和之前的假设矛盾,所以算法a是正确的贪心算法
55. 跳跃游戏 (medium)
难度中等
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
1.动态规划
- 思路:
dp[i]
表示能否到达位置i,对每个位置i判断能否通过前面的位置跳跃过来,当前位置j能达到,并且当前位置j加上能到达的位置如果超过了i,那dp[i]
更新为ture,便是i位置也可以到达。 - 复杂度:时间复杂度
O(n^2)
,空间复杂度O(n)
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
boolean[] dp = new boolean[n];
dp[0] = true;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
//当前位置j能达到,并且当前位置j加上能到达的位置如果超过了i,那dp[i]更新为ture,便是i位置也可以到达
if(dp[j] == true && nums[j] + j >= i){
dp[i] = true;
break;
}
}
}
return dp[n - 1];
}
}
2.贪心
- 思路:不用考虑每一步跳跃到那个位置,而是尽可能的跳跃到最远的位置,看最多能覆盖的位置,不断更新能覆盖的距离。
- 复杂度:时间复杂度
O(n)
,遍历一边。空间复杂度O(1)
public boolean canJump(int[] nums) {
if (nums.length == 1) {
return true;
}
int cover = nums[0];//能覆盖的最远距离
for (int i = 0; i <= cover; i++) {
cover = Math.max(cover, i + nums[i]);//当前覆盖距离cover和当前位置加能跳跃的距离中取一个较大者
// 覆盖距离超过或等于nums.length - 1 说明能到达终点
if (cover >= nums.length - 1) {
return true;
}
}
return false;//循环完成之后 还没返回true 就是不能达到终点
}
881. 救生艇
难度中等
给定数组 people
。people[i]
表示第 i
个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit
。
每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit
。
返回 承载所有人所需的最小船数 。
示例 1:
输入:people = [1,2], limit = 3
输出:1
解释:1 艘船载 (1, 2)
示例 2:
输入:people = [3,2,2,1], limit = 3
输出:3
解释:3 艘船分别载 (1, 2), (2) 和 (3)
示例 3:
输入:people = [3,5,3,4], limit = 5
输出:4
解释:4 艘船分别载 (3), (3), (4), (5)
提示:
1 <= people.length <= 5 * 104
1 <= people[i] <= limit <= 3 * 104
思路
- 思路:题意是一条船只能坐2人,要求尽可能的用少的船装下这些人。所以可以用贪心策略。让更多的人组成2人组,而且这些2人组的两人重量加起来不超过船的载重。所以可以先排序people,用双指针从两边向中间遍历,让重的人和轻的人组成2人组,如果当前最重的人和最轻的人的重量和超过了载重,那只能让重的人先乘一条船。
- 复杂度:时间复杂度
O(nlogn)
,排序的复杂度。空间复杂度O(logn)
,排序的栈空间
代码
public int numRescueBoats(int[] people, int limit) {
int n = people.length;
Arrays.sort(people);
int left = 0;
int right = n-1;
int nums = 0;
//让最重的最轻的先走
while (left <= right){
if(people[right] + people[left] <= limit){
right--;
left++;
nums++;
}else{
//让最重的一个人走
right--;
nums++;
}
}
return nums;
}
452. 用最少数量的箭引爆气球
难度中等719收藏分享切换为英文接收动态反馈
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points
,其中points[i] = [xstart, xend]
表示水平直径在 xstart
和 xend
之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x
处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``start
,x``end
, 且满足 xstart ≤ x ≤ x``end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。
有点类似前面的最小区间,注意不能直接o1[1] - o2[1] 有个案例会溢出
- 思路:区间按照结尾从小到大排序,循环数组,如果后面一个区间的开始大于前一个区间的结尾 就需要新增一支箭。
- 复杂度:时间复杂度
O(nlogn)
,排序的复杂度O(nlogn)
,循环数组的复杂度O(n)
。空间复杂度O(logn)
,排序栈空间
代码
class Solution {
public int findMinArrowShots(int[][] points) {
//按照右边界升序排序
Arrays.sort(points, (o1, o2) -> {
return Integer.compare(o1[1], o2[1]);
});
int n = points.length;
// for (int[] point : points) {
// for (int i : point) {
// System.out.print(i + " ");
// }
// System.out.println();
// }
int nums = 1;
int right = points[0][1];
//如果下一个的左边比之前的右边大 nums+1
for (int i = 1; i < n; i++) {
if (points[i][0] > right) {
right = points[i][1];
nums++;
}
}
return nums;
}
}
134. 加油站
难度中等
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
提示:
gas.length == n
cost.length == n
1 <= n <= 105
0 <= gas[i], cost[i] <= 104
思路
- 思路:首先判断总油量是否小于总油耗,如果是则肯定不能走一圈。如果否,那肯定能跑一圈。接下来就是循环数组,从第一个站开始,计算每一站剩余的油量,如果油量为负了,就以这个站为起点从新计算。如果到达某一个点为负,说明起点到这个点中间的所有站点都不能到达该点。
- 复杂度:时间复杂度
O(n)
,空间复杂度O(1)
代码
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
int sumGas = 0;
int sumCost = 0;
for (int i = 0; i < n; i++) {
sumGas += gas[i];
sumCost += cost[i];
}
//如果总油量小于总距离,失败
if (sumGas < sumCost) {
return -1;
}
int currentGas = 0;
int start = 0;
for (int i = 0; i < n; i++) {
currentGas += gas[i] - cost[i];
//如果到达下一站的时候油量为负数 就以这个站为起点 从新计算
if (currentGas < 0) {
currentGas = 0;
start = i + 1;
}
}
return start;
}
}
860. 柠檬水找零
难度简单
在柠檬水摊上,每一杯柠檬水的售价为 5
美元。顾客排队购买你的产品,(按账单 bills
支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5
美元、10
美元或 20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5
美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills
,其中 bills[i]
是第 i
位顾客付的账。如果你能给每位顾客正确找零,返回 true
,否则返回 false
。
示例 1:
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
示例 2:
输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。
代码
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
if (five == 0) {
return false;
}
five--;
ten++;
} else {
if (five > 0 && ten > 0) {
five--;
ten--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
}