前端算法面试必刷题系列[35]

248 阅读5分钟

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

61. 下一个更大元素 I (next-greater-element-i)

标签

  • 单调栈
  • 简单

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

给你两个 没有重复元素数组 nums1 和 nums2 ,其中nums1 是nums2 的子集

请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
    对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
    对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
    对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

相关知识

单调栈

栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈

单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序单调递增或单调递减)。

单调栈用途不太广泛,只处理一种典型的问题,O(n)复杂度解决NGE问题Next Greater Element。简单来说,对序列中每个元素,找到下一个比它大的元素。(当然,“下一个”可以换成“上一个”,“比它大”也可以换成“比他小”)

我们维护一个,表示待确定NGE的元素,然后遍历序列。当我们碰上一个新元素,我们知道,越靠近栈顶的元素离新元素位置越近。所以不断比较新元素栈顶,如果新元素比栈顶大,则可断定新元素就是栈顶的NGE,于是弹出栈顶并继续比较。直到新元素不比栈顶大,再将新元素压入栈。显然,这样形成的栈是单调递减的

单调栈的插入操作

将一个元素插入单调栈时,为了维护栈的单调性,需要在保证将该元素插入到栈顶后整个栈满足单调性的前提下弹出最少的元素

例如,栈中自顶向下的元素为 [1, 3, 5, 10, 20, 30],插入元素 15 时为了保证单调性需要依次弹出元素 1,2,5,10 ,操作后栈变为 [15, 20, 30]

用伪代码描述如下:

// insert x
while !stack.empty() && stack.top()<x
    stack.pop()
stack.push(x)

我们用下面几个例子深刻了解下这个单调栈。

基本思路

这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 O(n^2),会超时。

基本步骤

要找对应数字的下一个更大,其实就是把 nums2每个元素对应的 下一个更大元素 (NGE) 找到,再依次遍历nums1,输出每个数对应值就行。我们也容易想到用 map 来存储 nums2的每个元素对应的 NGE。

那么下面的问题就是如何建立这个Map 也就是如何找到每个元素对应的 NGE (next Greater Element)

举例 stack = [], map = {}, nums2 = [1, 3, 4, 2]

  1. 1 入栈 stack = [1] 此时栈顶元素是 1

  2. 3 插入 单调栈,3大于栈顶元素 1,要使栈单调增:1 为栈顶元素首先要出栈,那么作为最靠近的元素,就是1的NGE,存入map,此时map = {1 => 3},然后 3 再入栈 此时 stack = [3]

  3. 4 插入 单调栈,4大于栈顶元素 3,要使栈单调增:3 为栈顶元素首先要出栈,那么作为最靠近的元素,就是3的NGE,存入map,此时map = {1 => 3,3 => 4},然后 4 再入栈 此时 stack = [4]

  4. 2 插入 单调栈,2小于栈顶元素 4,保持了单调性,直接入栈,此时 stack = [4, 2]

  5. 遍历结束,栈中还剩下几个数,也就是找不到临近的NGE了,依次出栈,并把他们都至为 -1,此时 map = {1 => 3,3 => 4, 2 => -1, 4 => -1}

  6. 下面就遍历 nums1输出就行,非常简单的操作。

写法实现

var nextGreaterElement = function(nums1, nums2) {
  let setMap = (nums) => {
    // 这个 map 初始化映射都是 -1
    let [map, stack] = [new Map(nums2.map(it => [it, -1])), []]
    for (let i = 0; i < nums.length; i++) {
      while (stack.length !== 0 && nums[i] > stack[stack.length - 1]) {
        map.set(stack.pop(), nums[i])
      }
      stack.push(nums[i])
    }
    return map;
  }
  let numsMap = setMap(nums2);
  return nums1.map(item => numsMap.get(item))
};

let nums1 = [4,1,2], nums2 = [1,3,4,2]
console.log(nextGreaterElement(nums1, nums2))

这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 O(n^2),但是实际上这个算法的复杂度只有 O(n)。

分析它的时间复杂度,要从整体来看:总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以总的计算规模是和元素规模 n 成正比的,也就是 O(n) 的复杂度。

62. 下一个更大元素 II (next-greater-element-ii)

标签

  • 单调栈
  • 简单

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1

输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数; 
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

基本步骤

这题跟上面的变化是循环,使用取模运算 % 可以把下标 i 映射到数组 nums 长度的 0 - N 内。

注意 stack 中放置的是下标

接下来跟上面一模一样的单调栈方法解决问题。

写法实现

var nextGreaterElements = function(nums) {
  let [len, stack, res] = [nums.length, [], new Array(nums.length).fill(-1)]
  // 首先把nums假设扩展成2倍长度,stack 中存放下标
  for (let i = 0 ; i < 2 * len - 1; i++) {
    while (stack.length && nums[i % len] > nums[stack[stack.length - 1]]) {
      res[stack.pop()] = nums[i % len]
    }
    stack.push(i % len)
  }
  return res
};

console.log(nextGreaterElements([1,2,1]))

另外向大家着重推荐下这位大哥的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,暗号对不上不加哈,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考