代码随想录算法训练营Day58 | 单调栈 | 739. 每日温度 | 496. 下一个更大元素 I

文章目录

  • 单调栈
    • 单调栈的使用
    • 时间复杂度
    • 单调栈的原理
    • 单调栈的思路
  • 739. 每日温度
    • 暴力解法
  • 496. 下一个更大元素 I
    • 暴力解法

单调栈

单调栈的使用

通常是一维数组,要寻找任一个元素的 左边/右边 第一个 比自己大/小的元素的位置。此时我们就要想到可以用单调栈了。

  • 栈口到栈底是递增的:寻找当前元素后面第一个比它大的元素的位置
  • 栈口到栈底是递减的:寻找当前元素后面第一个比它小的元素的位置

寻找当前元素左侧第一个比它大/小的元素直接在栈里内侧寻找即可。

时间复杂度

O ( n ) O(n) O(n)

单调栈的原理

单调栈的本质是空间换时间,用一个栈来记录遍历过的元素,但只需要遍历一边即可。

在遍历到当前元素的时候,需要把当前元素和栈口的元素进行比较,从而得到当前元素的某些性质。由于只能比较当前元素和栈口元素,所以对栈内的元素性质有要求:(从 stack top 到 stack bottom)单调增或者单调减。

单调栈的思路

  1. 单调栈内部存放的元素是什么?
    • 存放元素的下标即可,因为需要元素值也可以直接访问原数组的对应下标
  2. 单调栈内,从 top 到 bottom,是递增还是递减?
    • 根据题目的要求来决定
    • top-bottom 递增,说明如果当前元素比 stack top 大,则这个元素会成为栈内某些元素右侧的第一个更大值
    • top-bottom 递减,说明如果当前元素比 stack top 小,则这个元素会成为栈内某些元素右侧的第一个更小值
  3. 遍历时,当前元素和 stack top 的比较:
    • array[i] > array[stack.top()]
    • array[i] = array[stack.top()]
    • array[i] < array[stack.top()]

739. 每日温度

题目链接 | 解题思路

本题要求当前元素和后面第一个比当前元素大的元素之间的距离。最直观的想法就是暴力两层 for loop,但时间复杂度 O ( n 2 ) O(n^2) O(n2) 太高了。

这里我们选择单调栈,本题是一道单调栈的基本应用。
由于要计算元素间的距离,我们在单调栈中选择储存下标。单调栈的意义是帮助我们存放已经遍历过的元素,从而能够比较当前元素和之前遍历过的元素(其实只能和栈口的元素比较,所以需要栈内保持递增或者递减的性质):<, >, ==,得到当前元素是否满足某些要求的性质。

以本题的一个样例来演示:temperatures = [73,74,75,71,69,69,72,76,73](栈的开口朝右)

  1. 当前元素是 73,idx=0 | 当前栈为空 | stack = [0]
  2. 当前元素是 74,idx=1 | 当前栈为 stack=[0]t[1] > t[0] | result[0] = 1 - 0, stack = [1](这样才能保持单调递增的栈)
  3. 当前元素是 75,idx=2 | 当前栈为 stack=[1]t[2] > t[1] | result[1] = 2 - 1, stack = [2]
  4. 当前元素是 71,idx=3 | 当前栈为 stack=[2]t[3] < t[2] | stack = [2, 3]
  5. 当前元素是 69,idx=4 | 当前栈为 stack=[2, 3]t[4] < t[3] | stack = [2, 3, 4]
  6. 当前元素是 69,idx=5 | 当前栈为 stack=[2, 3, 4]t[5] == t[4] | stack = [2, 3, 4, 5]
  7. 当前元素是 72,idx=6 | 当前栈为 stack=[2, 3, 4, 5]t[6] > t[5], t[4], t[3], t[6] < t[2] | result[5] = 6 - 5, result[4] = 6 - 4, result[3] = 6 - 3, stack = [2, 6]
  8. 当前元素是 76,idx=7 | 当前栈为 stack=[2, 6]t[7] > t[6], t[2] | result[6] = 7 - 6, result[2] = 7 - 2, stack = [7]
  9. 当前元素是 73,idx=8 | 当前栈为 stack=[7]t[8] < t[7] | stack = [7, 8]
  10. 最终结果:result = [1, 1, 5, 3, 2, 1, 1, 0, 0] (最后两个没有弹出栈,说明右边没有比他们更大的值了)

所以本题对于单调栈的使用:

  1. 储存元素的下标
  2. 栈内 top-bottom 单调递增,保证遇到一个更大值时能够更新结果与栈内元素
  3. 单调栈的遍历:
    • array[i] > array[stack.top()],则从栈中不断去除下标进行更新,直到当前值大于等于 stack top 或者栈为空,然后将当前元素压入栈中
    • 否则,直接将当前下表压入栈中

注意,答案中比较的是元素的值,但是栈中维护的只是下标,使用的时候不要搞混。

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        result = [0] * len(temperatures)
        stack = [0]

        for i in range(1, len(temperatures)):
            if temperatures[i] <= temperatures[stack[-1]]:
                stack.append(i)
            else:
                while (len(stack) > 0 and temperatures[i] > temperatures[stack[-1]]):
                    result[stack[-1]] = i - stack[-1]
                    stack.pop()
                stack.append(i)

        return result
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

暴力解法

直接两层遍历, O ( n 2 ) O(n^2) O(n2) 不出意外超时了。

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        result = []
        for i in range(len(temperatures)):
            curr_wait = 0
            for j in range(i+1, len(temperatures)):
                if temperatures[j] > temperatures[i]:
                    curr_wait = j - i
                    break
            result.append(curr_wait)
        return result

496. 下一个更大元素 I

题目链接 | 解题思路

题目要求找到 nums1[i]nums2 中下一个比当前元素大的元素,所以结果应该是一个长度和 nums1 相等的数组。

暴力解法是比较直观的,类似于双指针,时间复杂度为 O ( n 2 ) O(n^2) O(n2),可以看到这里面最大的时间复杂度来源于“在 nums2 中搜索每一个 nums1[i] 的位置”。乍一看这个搜索的时间是不可避免的成本,但在本题描述中,nums1nums2 的一个子集,并且其中没有重复的元素。这样的条件可以通过合适的 hash mapping 来进行优化。具体如下:

val2idx = {}
for i in range(len(nums1)):
	val2idx[nums1[i]] = i

这个 mapping 记录了 nums1(nums1[i], i):对于在 nums1 内出现的 nums2[j],可以通过 val2idx[nums2[j]] 来得到在 nums1 的下标;否则,可以知道该 nums2[j] 没有在 nums1 内出现。

单调栈:本题的单调栈使用和上一题完全一样,依然需要找到当前元素右侧的第一个更大值。题目中虽然问的是“nums1[i]nums2 中下一个比当前元素大的元素”,但可以通过拆分问题让其变得不这么复杂,只要得到 nums2 内部的右侧第一个更大值记录即可。

在得到了单调栈记录和**(值、下标)记录**之后,很容易就能得到最后结果。

  1. 通过 top-bottom 递增的单调栈,得到 nums2 中每个元素右侧第一个更大的元素下标。
  2. 通过 hash mapping,记录 nums1 中的元素的 (nums1[i], i)
  3. 遍历 nums2,如果当前值在 nums1 中出现过,就在 result 对应的位置记录 nums2_record[i]
class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        # first find the index of the next greater number in nums2 for each element, -1 if no next greater 
        nums2_record = [-1] * len(nums2)
        stack = [0]
        for i in range(1, len(nums2)):
            if nums2[i] <= nums2[stack[-1]]:
                stack.append(i)
            else:
                while (len(stack) > 0 and nums2[i] > nums2[stack[-1]]):
                    nums2_record[stack[-1]] = i
                    stack.pop()
                stack.append(i)
        
        # find the mapping between index and value in nums1, since nums1 is a subset of nums2
        val2idx = {}
        for i in range(len(nums1)):
            val2idx[nums1[i]] = i

        result = [-1] * len(nums1)
        for i in range(len(nums2)):
            if nums2[i] in val2idx and nums2_record[i] != -1:
                result[val2idx[nums2[i]]] = nums2[nums2_record[i]]
        return result

虽然是一道简单题,但是想要高效完成就必须意识到hash + 单调栈的组合,还是很考验基本功的。

暴力解法

两层 for loop 循环, O ( n 2 ) O(n^2) O(n2) 的时间复杂度。

class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        result = [-1] * len(nums1)

        for i in range(len(nums1)):
            find_duplicate = False
            for j in range(len(nums2)):
                if nums2[j] == nums1[i]:
                    find_duplicate = True
                if nums2[j] > nums1[i] and find_duplicate:
                    result[i] = nums2[j]
                    break
        return result

你可能感兴趣的:(代码随想录算法训练营一刷,算法)