刷力扣时,遇到关键词:下一个更大/小的数这类题目时,往往会采用单调栈的解法,如每日温度

刷题最常见的问题就是,看到题解,感觉很精妙,但下次遇到一模一样的题目时,往往知道思路,但写不出代码,有或者遇到类似的变体题目时,不会往这方面想。这两种情况在之前的文章(数据结构算法小结)中提到过,分别有两方面的原因:

  1. 对工具(如单调栈知识点)的特性(适用范围)不明朗
  2. 对工具的原理没有真正的理解

初学单调栈最容易搞错的一点是,获取下一个更大的数,往往需要用下降栈,下一个更小的数却要用上升栈。

本篇将会从单调栈的原理特性入手,分析单调栈原理和用法,并给出固定的方法模板。

单调栈场景分析

如果一上手就结合问题讲单调栈怎么用,注意力往往在解法上,而容易忽视单调栈的特性,这里先以单调上升栈为例,分析进出栈的特性:

假设原数组为[5,1,6,2,0,7],从左向右入栈,维护一个单调上升栈,过程如图所示:

单调栈.png

可以发现,单调上升栈更适合维护下一个更小值的数组,若让其维护下一个更大值的数组,过程中便会发现,无法对已出栈的数据进行比较,如果再加一个栈保存出栈数据,计算的复杂度又会变成O(n^2),变没有达到优化的效果。

小结

根据上面例子做个小结:

  • 单调栈适用问题

    • 获取某个值的下一个更大/更小值
  • 上升栈还是下降栈

    • 获取下一个更小值:维护一个上升栈
    • 获取下一个更大值:维护一个下降栈
  • 什么时候往下一个更大/更小数组中塞入值:

    • 数据出栈时

单调栈问题的模板

根据上面的例子,不难总结出单调栈问题的模板:

// 模板:
const fillArr = (arr, isNextLowerArr) => {
  const stack = [];
  const getTop = () => stack[stack.length - 1];
  // isNextLowerArr为true -> 维护[下一个更小]数组 -> 上升栈
  const validFunc = isNextLowerArr
      ? ((topVal, compareVal) => compareVal >= topVal)
      : ((topVal, compareVal) => compareVal <= topVal)
  const addData = (data) => {
      let top = getTop();
      // stack不为空,且顶部值不符合上升/下降规则时,顶部出栈,并填充数组
      while(top && !validFunc(top.val, data.val)) {
          arr[top.index] = data;
          stack.pop();
          top =  getTop();
      }
      stack.push(data);
  }
  addData.getStack = ()=> stack;
  return addData;
}
// test:
const testArr = [5,1,6,2,0,7];
const nextLowerArr = Array(testArr.length).fill(null);
const addDataToIncreaseStack = fillArr(nextLowerArr, true);
// 同理,获取下一个更大数组,获取添加数据函数就是fillArr(nextUpperArr, false)
testArr.forEach((val, index) => {
    const data = {val, index};
    addDataToIncreaseStack(data);
    console.log("stack:", [...addDataToIncreaseStack.getStack()]);
    console.log("nextLowerArr:", [...nextLowerArr]);
})

后话

明确模板之后,可自行尝试每日温度这一题。

很多时候,比起掌握方法,更进一步总结出模板无疑更有效率,下次遇到同样问题的时候,只需要将复杂问题拆解成多个子问题,分别套模板就行,就和做项目时调用第三方库一样;如果只是感性的掌握解法,解决问题时一点失误都会导致结果出错。


goblin_pitcher
590 声望30 粉丝

道阻且长