Day27-贪心算法,leetcode455,376,53
贪心算法
理论
-
贪心算法的本质:选择每一阶段的局部最优,从而达到全局最优。
-
贪心一般解题步骤,理论四步骤:
-
- 将问题分解为若干个子问题
-
- 找出适合的贪心策略
-
- 求解每一个子问题的最优解
-
- 将局部最优解堆叠成全局最优解
-
-
感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
-
只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了
题目
455. 分发饼干
- 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
- 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。
- 思路
- 饼干、小孩胃口都从小到大排序,先从后向前遍历饼干,然后去小孩胃口中去匹配,找到了再将饼干和小孩胃口同时向前移动,继续寻找下一组,即局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。也可以先小饼干满足胃口小的。
// 用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。
// 先遍历的胃口,再遍历的饼干,外面的 for 里的下标 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。
var findContentChildren = function (g, s) {
g = g.sort((a, b) => a - b); // g胃口从小到大排序
s = s.sort((a, b) => a - b); // s饼干尺寸从小到大排序
let result = 0;
let index = s.length - 1; // 饼干数组的末尾(最大尺寸)
// 先满足胃口大的,先遍历胃口,再寻找饼干
for (let i = g.length - 1; i >= 0; i--) { // 从胃口最大的小孩开始
if (index >= 0 && s[index] >= g[i]) { // 如果最大饼干能满足当前孩子
result++;// 满足一个孩子
index--; // 饼干往前移(用掉了)
}
// 如果不能满足,i--,尝试下一个胃口小一点的孩子
}
return result;
};
/**
* 贪心策略
局部最优:用最大的饼干去喂胃口最大的孩子,只要能满足就分配。
全局最优:尽可能多地满足孩子。
* 总结
先排序,双指针从后往前遍历。
每次用最大饼干优先满足胃口大的孩子,能满足就分配,不能满足就跳过。
最终返回能满足的孩子数量。
* 一句话总结:
用最大饼干优先喂胃口大的孩子,能满足就分配,统计最多能满足多少孩子。
*/
// 小饼干先喂饱小胃口,先遍历饼干,再遍历胃口
var findContentChildren = function(g, s) {
// s 饼干
// g 胃口
// 先满足胃口小的,先遍历饼干,再寻找胃口
g = g.sort((a, b) => a - b) // 胃口从小到大
s = s.sort((a, b) => a - b) // 饼干从小到大
let index = 0 // 指向当前要满足的孩子
let result = 0
for (let i = 0; i <= s.length - 1; i++) {// 遍历所有饼干
if(index >= 0 && g[index] <= s[i]) {// 当前饼干能满足当前孩子
result++
index++
}
}
return result
};
// 双指针写法,遇到满足的就两个指针都前进,否则只前进饼干指针,效率更高,逻辑更清晰。
var findContentChildren = function(g, s) {
g.sort((a, b) => a - b);
s.sort((a, b) => a - b);
let i = 0, j = 0;
while (i < g.length && j < s.length) {
if (g[i] <= s[j]) {
i++; // 满足一个孩子
}
j++; // 每块饼干都要用掉
}
return i; // 满足的孩子数量
};
376. 摆动序列
-
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
-
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
-
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
-
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
-
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
- 思路
- 序列按照一上一下或者一下一上一下这种形式画图,看一下哪些属于峰值,哪些属于坡道上的数值,对于平坡,删除某几个相同的值,
- 最长摆动子序列的长度,只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)
- 在计算是否有峰值的时,计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),如果prediff < 0 && curdiff > 0 或者 prediff > 0 && curdiff < 0 此时就有波动就需要统计。
// 贪心
/**
preDiff 记录上一次的差值,初始为 0。
curDiff 记录当前相邻两个数的差值。
只要当前差值和上一次差值异号(正负交替),就说明出现了一个“波峰”或“波谷”,result++。
如果差值为 0(平坡),不计入波动。
最终 result 就是最长摆动子序列的长度。
总结
这段代码用贪心思想,只统计“拐点”(波峰/波谷),每遇到一次正负交替就加一,最终得到最长摆动子序列长度。
时间复杂度 O(n),空间复杂度 O(1)。
一句话总结:
每遇到一次正负交替的差值就统计一次,最终得到最长摆动子序列长度。
*/
var wiggleMaxLength = function(nums) {
if(nums.length <= 1) return nums.length // 特判,空或单元素直接返回
let result = 1 // 至少有一个元素
let preDiff = 0 // 上一个差值
let curDiff = 0 // 当前差值
for(let i = 0; i < nums.length - 1; i++) {
curDiff = nums[i + 1] - nums[i] // 计算当前差值
// 如果当前差值和上一个差值异号(正负交替),就统计一个峰值
if((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
result++
preDiff = curDiff // 更新上一个差值
}
}
return result
};
// 动态规划
/**
关键点说明
up:以当前元素为“峰顶”结尾的最长摆动子序列长度。
down:以当前元素为“谷底”结尾的最长摆动子序列长度。
每遇到上升,up = down + 1;每遇到下降,down = up + 1。
相等时不更新。
最终返回 up 和 down 的最大值。
总结
这段代码用动态规划思想,分别记录以“峰顶”和“谷底”结尾的最长长度,遍历一遍即可得到答案。
时间复杂度 O(n),空间复杂度 O(1)。
一句话总结:
用 up/down 两个变量分别记录以峰顶/谷底结尾的最长长度,遍历更新,最终取最大值。
*/
var wiggleMaxLength = function(nums) {
if (nums.length === 1) return 1;
// 考虑前i个数,当第i个值作为峰谷时的情况(则第i-1是峰顶)
let down = 1;
// 考虑前i个数,当第i个值作为峰顶时的情况(则第i-1是峰谷)
let up = 1;
for (let i = 1; i < nums.length; i++) {
if (nums[i] < nums[i - 1]) {
// 当前是下降,前面必须是上升,down = up + 1
down = Math.max(up + 1, down);
}
if (nums[i] > nums[i - 1]) {
// 当前是上升,前面必须是下降,up = down + 1
up = Math.max(down + 1, up)
}
}
return Math.max(down, up);
};
53. 最大子数组和
- 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
- 子数组是数组中的一个连续部分。
- 思路
- 暴力解法的思路,第一层 for 就是设置起始位置,第二层 for 循环遍历数组寻找最大值
- 贪心算法:负数只会拉低总和,连续和不是负数,继续加,否则从下一个元素重新开始选择起始位置,重新计算连续和,因为负数加上下一个元素连续和只会越来越小
/**
count:记录当前连续子数组的和。如果加上当前元素后变成负数,说明这段子数组对后续没有贡献,直接舍弃,从下一个元素重新开始。
result:记录目前为止遇到的最大子数组和。
每次累加后,更新最大和;如果当前和小于0,则重置为0。
总结
这段代码用贪心思想,遍历一遍数组,动态维护当前连续和与最大和,时间复杂度 O(n),空间复杂度 O(1)。
适用于有正有负的数组,且能正确处理全负数的情况。
一句话总结:
每次累加当前元素,遇到负数和就重置,动态维护最大和,最终返回最大连续子数组和。
*/
var maxSubArray = function(nums) {
// 初始化最大和为负无穷,防止全负数时出错
let result = -Infinity
// 当前连续子数组的和
let count = 0
for(let i = 0; i < nums.length; i++) {
// 累加当前元素
count += nums[i]
if(count > result) {
// 更新最大和
result = count
}
// 如果当前和为负,舍弃,重新开始
if(count < 0) {
count = 0
}
}
return result
};
// 暴力解法可能会超时
/**
外层 for 循环枚举子数组的起点 i。
内层 for 循环枚举终点 j,并累加区间和。
每次更新最大和 result。
**时间复杂度:**O(n²),适合理解暴力解法原理。
*/
var maxSubArray = function(nums) {
let result = -Infinity;
for (let i = 0; i < nums.length; i++) { // 枚举起点
let sum = 0;
for (let j = i; j < nums.length; j++) { // 枚举终点
sum += nums[j];
if (sum > result) {
result = sum;
}
}
}
return result;
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。

浙公网安备 33010602011771号