「重新点亮算法之光-贪心算法」

172 阅读7分钟

前言 :

本人是刚毕业不到一年的前端菜鸟,算法层面过于废物(就是面试算法就一问三不知,面试官听了直摇头)。
本文需要leetcode进行练习哦,没有的话赶紧去注册一个!

image.png

注意:

本章只是方便自己学习下,所以大佬请跳过,菜鸟可以学习顺带一提关于我Apex四转菜鸟那件事
本章内容大部分借鉴于下图的书,一位以前来自谷歌的大佬写的刷题书(羡慕,写的太好了)。
image.png image.png

正文:

最简单的算法:贪心算法 (其实我觉得也不简单,难点就是如何处理局部最优)

算法解释:

贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优的,从而使最后得到的结果是全局最优的。概念谁都知道,赶紧给我上菜(刷题),我要实践!!!

前餐:

先来一道很简单的题目,试试水。

前餐1:605. 种花问题 - 力扣(LeetCode)

image.png 这题都不会的话,额...(那你比我还差hhhh)

题解: 这道题很简单,确保每个花左右两侧没有花就行了问题在于花坛边界的特殊判断(因为边界的花只需要一边没花就行了)。所以在 flowerbed 数组两端各增加一个 0,然后从i=1开始循环就行。

总结下:

  1. 在 flowerbed 数组两端各增加一个 0
  2. 从i=1开始循环,到i<flower.length - 1结束。
    • 如果左右两侧没有花,且自己可以插花的情况下,则记录下可以插入。
    • 如果左右两侧任意一侧有花,则跳过不记录。
var canPlaceFlowers = function(flowerbed, n) {
  //在 flowerbed 数组两端各增加一个 0
  let flower = [0, ...flowerbed, 0];
  let num = 0;
  //从i=1开始循环,到i<flower.length - 1结束
  for (let i = 1; i < flower.length - 1; i++) {
    //如果左右两侧没有花,且自己可以插花的情况下
    if (flower[i - 1] === 0 && flower[i + 1] === 0 && flower[i] === 0) {
      //插入花,并记录数字  
      flower[i] = 1;
      num++;
    }
  }
  return num >= n;
};

这题其实就是用了局部最优的思想,只要考虑当前位置能不能插花就行

如果消化完了,我们开始第二道前菜了。请放心食用!

前餐2:455. 分发饼干 - 力扣(LeetCode)

image.png

题解: 一句话总结:每次都让胃口最小的孩子分配到能吃饱的最小的饼干。那么我们一步步解决。

  • 如何找到胃口最小的孩子?排序
  • 如何找到能吃饱的最小的饼干?我自己想了两个方法。
    • 方法1:每次都去遍历饼干数组,然后找到刚好能吃饱的饼干(反正我一开始是这么想的,但是每次都要跑一遍很麻烦)
    • 方法2:将饼干从小到大进行排序,然后依次遍历,找到第一个满足的就停止,再做个记号下次直接从这开始。(推荐)
var findContentChildren = function (g, s) {
  let kids = g.sort((a, b) => a - b); //按照胃口从小到大排序孩子
  let cookies = s.sort((a, b) => a - b); //按照饼干从小到大排序
  let kidNum = 0, //记录能吃的孩子个数
    cookieNum = 0; //记录吃掉的饼干个数
  //如果孩子都吃了或者饼干没了,就停止
  while (kidNum < kids.length && cookieNum < cookies.length) {
    //如果孩子能吃掉饼干
    if (kids[kidNum] <= cookies[cookieNum]) kidNum++;
    cookieNum++;
  }
  return kidNum;
};

//有个小地方要特殊处理,js中数组使用sort(),数组元素首先会被转换为字符,之后会根据`Unicode`编码的顺序来进行排序。
//所以 [10, 9, 8, 7].sort() ,结果是 [ 10, 7, 8, 9 ] ,规定下规则就行了!

怎么样,两道前菜吃的下吗,反正我是勉强吃下,嘿嘿嘿

如果没问题的话,那么副菜要上咯!

副菜:

副菜1:435. 无重叠区间 - 力扣(LeetCode)

image.png 题解: 一句话总结:要求最多有几个互不重叠的区间?局部最优:两个区间重叠的话,留下小的区间

  • 先将区间的左边界从小到大排序,确保区间都是从小到大一个方向上摆放。
  • 依次摆放,如果有重叠的话,就要看两个区间的右边界哪个小(越小说明该区间越小,才能存下尽可能多的区间)
var eraseOverlapIntervals = function(intervals) {
  //按左边界从小到大进行排序
  intervals.sort((a, b) => a[0] - b[0]);
  let temp = intervals[0];//重叠区间
  let num = 0;
  //依次摆放
  for (let i = 1; i < intervals.length; i++) {
    //如果有重叠(下一个的左边界小于上一个的右边界)
    if (intervals[i][0] < temp[1]) {
      //选择右边界小的那个
      if (intervals[i][1] < temp[1]) {
        temp = intervals[i];
      }
      num++;
    } else {
      temp = intervals[i];
    }
  }
  return num;
};

副菜2:406. 根据身高重建队列 - 力扣(LeetCode)

image.png 题解: 说实话,我觉得这题好简单,排队谁不会,因为我通常都是站在最后 来自190的自信,不过正常来说不都是矮的排前面吗?

局部最优:每个人只需要看前面有几个人比他高就行

  • 先按身高从高到低排序,如果身高一样,再按前面有几个人的数量从小到大排。
  • 然后循环,看前面有几个人比他高,插到相应的位置就行。
var reconstructQueue = function (people) {
  let res = [];
  //先按身高从高到低排序,如果身高一样,再按前面有几个人的数量从小到大排。
  people.sort((pre, cur) => cur[0] - pre[0]).sort((pre, cur) => pre[1] -cur[1] );
  //循环
  for (let i = 0; i < people.length; i++) {
    const ele = people[i];
    //如果前面没有比他高的人,直接排到第一个
    if (ele[1] === 0) {
      res.unshift(ele);
    } else {
      //满足前面有几个人比他高就行
      let n = 0; //记录下比他高的人
      //循环队伍
      for (let j = 0; j < res.length; j++) {
        //如果有比他高的人
        if (res[j][0] >= ele[0]) {
          ++n;
          //找到该站的位置
          if (n === ele[1]) {
            res.splice(j + 1, 0, ele);
            break;
          }
        }
      }
    }
  }
  return res;
};

主菜:

    孩子够不够,孩子够不够,孩子够不够,主菜来咯!

主菜1:135. 分发糖果 - 力扣(LeetCode)

image.png 题解: 这题乍一看,好简单,先全部给一个,只要每次找到评分最低的孩子,然后左右判断给糖果就行了。每次怎么找到评分最低的孩子,每次都循环有点过于麻烦了。
再想想,局部最优:如果两侧一起考虑一定会顾此失彼,那我只考虑一侧,然后跑两次不就行了

var candy = function (ratings) {
  //先全部给一个
  const candy = new Array(ratings.length).fill(1);
  //只考虑左侧
  for (let i = 1; i < ratings.length; i++) {
      if (ratings[i] > ratings[i - 1]) candy[i] = candy[i - 1] + 1;
  }
  //只考虑右侧
  for (let i = ratings.length - 1; i >= 0; i--) {
      if (ratings[i] > ratings[i + 1])
          candy[i] = Math.max(candy[i + 1] + 1, candy[i]);
  }
  return candy.reduce((a, b) => a + b);
};

主菜2:763. 划分字母区间 - 力扣(LeetCode)

image.png 题解: 分割成包含字符不同的字符串,数量要尽可能的多,长度要尽可能的长(其实我想骂人,又要多又要长,这不是欺负老实人吗?)
这里的难点就是怎么判断分割?分割后的字符串内的每个字母种类在后面的字符串不会出现了
解法:

  • 暴力解法:循环整个字符串,然后都判断下标之前的每个字符有没有在后面的字符串出现,若有则继续,若无则分割(痛点:每次都要记录字符串,而且每次要遍历后面的字符串,我要的是面试官喜欢,可不是你喜欢)
  • 换个思路,局部最优:确保字符没有在后面的字符串出现
    • 先循环一遍记录下每个字母 第一次出现的下标最后出现的下标得到了每个字母的第一次和最后一次的位置
    • 循环映射关系表,得到了第一个字母的 第一次出现的下标最后出现的下标,如果下一个的字母的 第一次出现的下标 在这整个区间内,且 最后出现的下标 超过上一个最后出现的下标则更新,如果不在区间,就地分手(分割)。
var partitionLabels = function (s) {
  let obj = {};
  //获取映射对象
  for (let i = 0; i < s.length; i++) {
    const element = s[i];
    if (obj.hasOwnProperty(element)) {
      obj[element][1] = i;
    } else {
      obj[element] = [i, i];
    }
  }

  let firseIndex = 0,
    lastIndex = 0,
    res = [];

  for (let key in obj) {
    //如果下一个的字母的第一次出现的下标在这整个区间内
    if (obj[key][0] > firseIndex && obj[key][0] < lastIndex) {
      //如果下一个字母的最后出现的下标超过上一个最后出现的下标则更新
      if (obj[key][1] > lastIndex) lastIndex = obj[key][1];
    } else {
      //如果不在,就分割
      obj[key][0] - firseIndex !==0 && res.push(obj[key][0] - firseIndex);
      firseIndex = obj[key][0];
      lastIndex = obj[key][1];
    }
  }
  res.push(s.length - firseIndex)
  return res;
};

甜品:

tip: 甜品得用合适的餐具吃才舒服。

甜品1:122. 买卖股票的最佳时机 II - 力扣(LeetCode)

image.png 题解: 这道题,第一眼看到,属实给我震惊到了,有点迷惑我了,要获得最大的利润。
冷静下来发现,其实简单的,获得最大利润,只要保证每次都能赚钱(价格高低差)的部分。废话,这谁不知道,现实中赚钱可没那么简单。
局部最优:保证每次都能赚钱,累计赚钱的金额就是最大利润。

var maxProfit = function (prices) {
  if (prices.length <= 1) return 0;
  let res = 0;
  for (let i = 0; i < prices.length - 1; i++) {
    const cur = prices[i];//今天的价格
    const next = prices[i + 1];//明天的价格
    //保证每次都能赚钱
    if (next > cur) res += next - cur;
  }
  return res;
};

打包带走(自己练去,懒狗):

    光看不做,食之无味。又看又做,歪瑞够得。

小菜1:665. 非递减数列 - 力扣(LeetCode)

小菜2:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)

总结:

贪心策略要无后向性,也就是说 某状态以后的过程不会影响以前的状态。在每一步贪心选择中,只考虑当前对自己最有利的选择,而不去考虑在后面看来这种选择是否合理。

人生也是一样,每天都比昨天进步一点,一直往前看,总会爬到高峰!

再次说明:作者只是前端菜鸟,分享的也是些比较普通的题目罢了,大佬勿骂,本文更多的是练习自己的算法。

下一章 玩转双指针 ,敬请期待。暗示点赞!