8. 贪心算法
什么是贪心算法
代码随想录
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。
455. 分发饼干
- 455. 分发饼干
- 0601,easy,quick
- 贪心
只需要对 🍪 g,和 🧒 s 进行 sort,然后确保每个饼干 s[j]
是大于小孩 g[i]
的,且是最小值。
var findContentChildren = function (g, s) {
s.sort((x, y) => x - y);
g.sort((x, y) => x - y);
// 饼干 s[j] 是大于 g[i] 的最小值
let i = 0, j = 0;
while (s[j] && g[i]) {
while (s[j] && s[j] < g[i]) j++;
if (!s[j]) break;
i++, j++;
}
return i;
};
- 376. 摆动序列
- 0602,mid,answer
- 贪心,动态规划
方法一:贪心
对这种问题,自己能画出一个图清晰的图,可以快速的找到解题思路。
下图可以看到,当出现相等、连续递增、连续递减的情况时,我们跳过中间数,去判断下一个数即可。
复杂度:时间(O(n)),空间(O(1))。
- preDiff 统计上一个可参考的差值;
- curDiff 统计当前的差值,只有当前差值和上一个差值不相同时,结果 count 才 +1;否则就跳过,进行判断下一个数字。
- 因为至少长度为 2 以上,才可以有差值的记录,所以 index 从 1 开始、count 最小值为 1。
var wiggleMaxLength = function (nums) {
if (nums.length === 1) return 1; // 特例
let preDiff = 0; // 只有在初始化时,preDiff = 0,除此之外一定是大于/小于0的
let count = 1;
for (let index = 1; index < nums.length; index++) {
// 当前差值
const curDiff = nums[index] - nums[index - 1];
// 如果没有差值为0、出现递增 / 递减序列,则跳过。
if (curDiff > 0 && preDiff <= 0 || curDiff < 0 && preDiff >= 0) {
count++;
preDiff = curDiff;
}
}
return count;
};
方法二:动态规划
可以发现,对于我们当前考虑的这个数,要么是作为山峰(即nums[i] > nums[i-1]),要么是作为山谷(即nums[i] < nums[i - 1]):
- 时间复杂度:O(n), 空间复杂度:O(1)
如果一切顺利,没有连续相等、连续递增、连续递减的情况:
- 如果当前是山峰,当前山峰的最长序列值就是山谷 + 1:
up = down + 1
; - 如果当前是山谷,当前山谷的最长序列值就是山峰 + 1:
down = up + 1
;
但如果发生不顺的时候,及产生了连续相等、连续递增、连续递减的情况:
- 山谷 down 和山峰 up 的值就不发生改变。
- 比如当前是山峰,
up = Math.max( down + 1, up)
,因为在连续递增的最初,up 已经被赋值为down + 1
,此时后续发生连续递增 / 连续相等,up 的值就不会再增加了,而是一直维持down + 1
不变。
var wiggleMaxLength = function (nums) {
if (nums.length === 1) return 1;
let down = 1; // 考虑前i个数,当第i个值作为峰谷时的情况(则第i-1是峰顶)
let up = 1; // 考虑前i个数,当第i个值作为峰顶时的情况(则第i-1是峰谷)
for (let i = 1; i < nums.length; i++) {
// 当前是峰谷:如果之前 经历过连续递减 / 连续相等,这里就会用 down 值;
if (nums[i - 1] > nums[i])
down = Math.max(up + 1, down);
// 当前是峰顶:如果之前经历过连续递增 / 连续相等,这里就会用 up 值;
if (nums[i - 1] < nums[i])
up = Math.max(down + 1, up)
// console.log(Math.max(nums[i] - nums[i - 1]), down, up);
}
return Math.max(down, up);
};
53. 最大子数组和
- 53. 最大子数组和
- 0603,easy,quick
- 贪心,动态规划
方法一:贪心
时间复杂度:O(n),空间复杂度:O(1)
- 注意我当时总弄错的地方:max 的初始化一定是
-Infinity
;
var maxSubArray = function (nums) {
let max = -Infinity; // 初始化的和一定是 -Inifinity
let count = 0;
for (let i = 0; i < nums.length; i++) {
count += nums[i];
max = Math.max(count, max);
// 如果当前和为负数,则丢弃
if (count < 0) count = 0;
}
return max;
};
另一种思路,两个不同之处:
- 初始化值为
nums[0]
,for
循环从 1 开始; - 丢弃原有和的实际放在下一次循环的开头。
var maxSubArray = function (nums) {
let max = nums[0];
let count = nums[0];
for (let i = 1; i < nums.length; i++) {
// 如果当前和为负数,且下一个数字大于当前和,丢弃原有和
if (count < 0 && nums[i] > count) count = nums[i];
else count += nums[i];
max = Math.max(count, max);
}
return max;
};
方法二:动态规划
时间复杂度:O(n),空间复杂度:O(n)
dp[i]
:当前的最大连续子序列和(包括i
本身)。- 状态转移方程:
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
- 上一个最大和 + 当前
nums[i]
,与当前数字nums[i]
哪一个更大?
- 上一个最大和 + 当前
var maxSubArray = function (nums) {
const dp = new Array(nums.length);
dp[0] = nums[0];
let max = dp[0];
for (let i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式
max = Math.max(max, dp[i]); // max: 保存dp[i]的最大值
}
return max;
};
122. 买卖股票的最佳时机 II
- 122. 买卖股票的最佳时机 II
- 0603,mid,quick
- 贪心,动态规划
方法一:贪心算法
时间复杂度:O(n);空间复杂度:O(1)
贪心: 统计每一天和上一天之间的股票差值,找局部最优。只要 prices[i] - prices[i-1] > 0
,就 把当前利润加入 profit 中;
var maxProfit = function (prices) {
let profit = 0;
for (let i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0)
profit += prices[i] - prices[i - 1]
}
return profit;
};
方法二:动态规划
速度慢:时间复杂度:O(n);空间复杂度:O(n)。
动态规划对每一天对股票 持有 / 不持有 两个状态进行模拟,得出每一天的最大收益。
定义 dp
:
dp[i][0]
:表示第 i 天交易完后,不持有股票的最大利润;dp[i][1]
:表示第 i 天交易完后,持有股票的最大利润。
分析买卖股票的两个动作:
- 股票买入。股票买入后,当前现金 = 之前现金 - 股票价格;
- 股票卖出。股票卖出后,当前现金 = 之前现金 + 股票价格;
得出两个状态转移方程:
-
第
i
天持有股票 即dp[i][0]
, 可以由两个状态推出来:-
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
-
(不卖出)第
i-1
天就持有股票,当前先进和昨天持有股票的所得现金一样,即:dp[i - 1][0]
。- 因为这里没有任何卖出、买入的操作,所以自己手里的现金是没发生变化的。
-
(没买过)第
i
天买入股票,所得现金 = [昨天不持有股票的所得现金] - [今天股票价格],即:dp[i - 1][1] - prices[i]
;- 因为今天买入了股票,现金肯定要减去买股票的价格。
-
-
第
i
天不持有股票 即dp[i][1]
, 可以由两个状态推出来:- (没买过)第
i-1
天就没持有股票,那么所得现金和昨天不持有股票的所得现金相同 即:dp[i - 1][1]
; - (今天卖)第
i
天没持有股票,按照今天股票价格卖出后,剩余的现金就是所得现金:prices[i] + dp[i - 1][0]
。
- (没买过)第
const maxProfit = (prices) => {
const dp = Array.from(new Array(prices.length), () => new Array(2).fill(0));
dp[0][0] = 0 - prices[0]; // 持有
dp[0][1] = 0; // 不持有
for (let 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][0] + prices[i], dp[i - 1][1]);
}
// 返回最后一天卖出所有股票,即不持有的情况
return dp[prices.length - 1][1];
};
55. 跳跃游戏
- 55. 跳跃游戏
- 0603,mid,normal
- 贪心
方法一:贪心
题目关键点:不要拘泥于每次究竟怎么跳跳几步,而是看覆盖范围,范围内的位置是一定可以跳到的,不用管是怎么跳到的这个位置。
贪心的思路,我们定义一个变量 jump,表示当前我们可以达到的最远范围。也就是说,[0, jump]
内的所有位置我们都可以访问。
- 然后通过 for 循环遍历
[0, jump]
这个区间,一旦jump >= nums.length - 1
表明此时 jump 的范围已经包括了数组的终点。返回 true;反之,如果 for 遍历完毕,都没有达到终点,那就返回 false。
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
var canJump = function (nums) {
if (nums.length === 1) return true;
let jump = 0; // 可以跳到的范围
for (let i = 0; i <= jump; i++) {
jump = Math.max(jump, i + nums[i]);
if (jump >= nums.length - 1) return true;
}
return false;
};
方法二:贪心(倒着分析)
我想到的方法,但更复杂。考虑自己当时为什么没想从正面直接解决?两个原因:
- 好多题的分析都是倒着来,这里有了思维惯性,想到倒着能做,就直接做了。
- 看到需要两层 for 循环略复杂,但没仔细考虑是否可以有另一种方法,应该考虑更全面。
定义一个数组 dp,dp[i] 表示当前位置是否可以达到终点如果可以为 true,不可以为 false。
- 从最后一步
nums[nums.length - 2]
,倒着往前分析:如果可以达到,就置为 true,不可以达到就置为 false。分析步骤:- 计算当前位置可以达到的范围 jump。如果跳跃范围大于终点
jump > lastIndex
,则为 true; - 利用 for 循环,寻找 jump 的范围内是否有一个点可以达到终点,有则为 true;
- 计算当前位置可以达到的范围 jump。如果跳跃范围大于终点
复杂度分析:
- 时间复杂度:O(n x n!) (最坏,嵌套的 for 循环次数无法保证);
- 空间复杂度:O(n^2);
var canJump = function (nums) {
if (nums.length === 1) return true;
const lastIndex = nums.length - 1;
// 从最后往前,如果能到达就为true,不能到达就为false
const dp = Array(lastIndex).fill(false);
for (let i = lastIndex - 1; i >= 0; i--) {
const jump = i + nums[i];
// 直接跳到结尾
if (jump >= lastIndex) dp[i] = true;
else {
// 查找 jump 范围内有没有 true:
for (let j = i + 1; j <= jump; j++) {
if (dp[j]) {
dp[i] = true;
break;
}
}
}
}
return dp[0];
};
45. 跳跃游戏 II
- 45. 跳跃游戏 II
- 0604,mid,answer