Day27-贪心算法,leetcode455,376,53

贪心算法

理论

  • 贪心算法的本质:选择每一阶段的局部最优,从而达到全局最优。

  • 贪心一般解题步骤,理论四步骤:

      1. 将问题分解为若干个子问题
      1. 找出适合的贪心策略
      1. 求解每一个子问题的最优解
      1. 将局部最优解堆叠成全局最优解
  • 感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。

  • 只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了



题目

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;
};



参考&感谢各路大神

posted @ 2025-06-23 09:03  安静的嘶吼  阅读(3)  评论(0)    收藏  举报