描述高頻題之佇列&棧

演算法推薦管發表於2021-11-30

棧和佇列

全文概覽

image-20211129163913713

基礎知識

棧是一種先進後出的資料結構。這裡有一個非常典型的例子,就是堆疊盤子。我們在放盤子的時候,只能從下往上一個一個的放;在取的時候,只能從上往下一個一個取,不能從中間隨意取出。

image-20211129094451883

棧是一種操作受限的線性表,只允許在一端處理資料。主要包括兩種操作,即入棧和出棧,也就是在棧頂插入一個資料和從棧頂刪除一個資料。

棧既可以用陣列實現,也可以用連結串列來實現。用陣列實現的棧,我們叫作順序棧,用連結串列實現的棧,我們叫作鏈式棧。

佇列

佇列是一種先進先出的資料結構。你可以把它想象成排隊買票,先來的先買,後來的人只能站末尾,不允許插隊。

image-20211129101242533

佇列跟棧一樣,也是一種操作受限的線性表資料結構。主要包括兩個操作,即出隊和入隊,也就是從隊首取一個元素和在隊尾插入一個元素。

佇列可以用陣列來實現,也可以用連結串列來實現。用陣列實現的佇列叫作順序佇列,用連結串列實現的佇列叫作鏈式佇列。

用兩個棧實現佇列

劍指 Offer 09. 用兩個棧實現佇列

問題描述

用兩個棧來實現一個佇列,完成 n 次在佇列尾部插入整數 (push) 和在佇列頭部刪除整數 (pop) 的功能。 佇列中的元素為 int 型別。保證操作合法,即保證pop操作時佇列內已有元素。

示例:

輸入:["PSH1","PSH2","POP","POP"]

返回值:1,2

說明:

"PSH1":代表將1插入佇列尾部
"PSH2":代表將2插入佇列尾部
"POP":代表刪除一個元素,先進先出 => 返回1
"POP":代表刪除一個元素,先進先出 => 返回2

分析問題

首先,我們需要知道佇列和棧的區別。

  1. 佇列是一種先進先出的資料結構。佇列中的元素是從後端入隊,從前端出隊。就和排隊買票一樣。
  2. 棧是一種後進先出的資料結構。棧中的元素是從棧頂壓入,從棧頂彈出。

為了使用兩個棧實現佇列的先進先出的特性,我們需要用一個棧來反轉元素的入隊順序。

入隊操作Push:

因為棧是後進先出的,而佇列是先進先出的,所以要想使用棧來實現佇列的先進先出功能,我們需要把新入棧的元素放入棧底。為了實現這個操作,我們需要先把棧S1中的元素移動到S2,接著再把新來的元素壓入S2,然後再把S2中的所有元素再彈出,壓入到S1。這樣就實現了把新入棧的元素放入棧底的功能。

image-20210928153029729

    def push(self, node):
        # write code here
        while self.stack1:
            self.stack2.append(self.stack1.pop())
        self.stack2.append(node)
        while self.stack2:
            self.stack1.append(self.stack2.pop())

出隊操作Pop:

我們直接從S1彈出就可以了,因為經過反轉後,S1中的棧頂元素就是最先入棧的元素,也就是隊首元素。

    def pop(self):
        if self.stack1:
            return self.stack1.pop()

我們來看完整的程式碼實現。

class Solution:
    def __init__(self):
        self.stack1 = []
        self.stack2 = []
    def push(self, node):
        # write code here
        while self.stack1:
            self.stack2.append(self.stack1.pop())
        self.stack2.append(node)
        while self.stack2:
            self.stack1.append(self.stack2.pop())

    def pop(self):
        if self.stack1:
            return self.stack1.pop()

我們可以看到入隊操作的時間複雜度是O(n),空間複雜度也是O(n)。出隊時間複雜度是O(1),空間複雜度也是O(1)。

優化

在上面的演算法中,不知道你有沒有發現,每次在push一個新元素時,我們都需要把S1中的元素移動到S2中,然後再從S2移回到S1中。這顯然是冗餘的。其實,我們在入隊時只需要插入到S1中即可。而出隊的時候,由於第一個元素被壓在了棧S1的底部,要想實現佇列的先進先出功能,我們就需要把S1的元素進行反轉。我們可以把棧S1的元素Pop出去,然後壓入S2。這樣就把S1的棧底元素放在了棧S2的棧頂,我們直接從S2將它彈出即可。一旦 S2 變空了,我們只需把 S1 中的元素再一次轉移到 S2 就可以了。

image-20210928153046516

下面我們來看一下程式碼實現。

class Solution:
    def __init__(self):
        self.stack1 = []
        self.stack2 = []
    def push(self, node):
        # write code here
        self.stack1.append(node)

    def pop(self):
        if not self.stack2:
            while self.stack1:
                self.stack2.append(self.stack1.pop())
        return self.stack2.pop()

有效的括號

LeetCode 20. 有效的括號

問題描述

給定一個只包括 '('')''{''}''['']' 的字串 s ,判斷字串是否有效。

有效字元滿足的條件是:

  • 左括號必須用相同型別的右括號閉合。
  • 左括號必須以正確的順序閉合。

示例:

輸入:s = "()[]{}"

輸出:true

分析問題

這個問題我們可以藉助 “棧” 這種資料結構來解決。在遍歷字串的過程中,當我們遇見一個左括號時,我們就入棧,當我們遇到一個右括號時,我們就取出棧頂元素去判斷他們是否是同型別的。如果不是的話,那就代表字串s不是有效串,我們直接返回False。如果是,接著去遍歷,直到遍歷結束為止。當遍歷完字串s後,如果棧為空,就代表字串是有效的。這裡需要注意一點,為了加快判斷左、右括號是否是同型別的,我們引入雜湊表儲存每一種括號。雜湊表的鍵為右括號,值為相同型別的左括號。

下面我們來看一下程式碼實現。

def isValid(s):
    #如果字串不是偶數,直接返回false
    #因為字元只包含括號,所以只有偶數時才有可能匹配上
    if len(s) % 2 == 1:
        return False

    dict = {
        ")": "(",
        "]": "[",
        "}": "{",
    }

    stack = list()

    for ch in s:
        #代表遍歷到右括號
        if ch in dict:
            #看棧頂元素是否能匹配上,如果沒有匹配上,返回false
            if not stack or stack[-1] != dict[ch]:
                return False
            #如果匹配上,彈出棧頂元素
            stack.pop()
        else:
            #匹配到左括號,入棧
            stack.append(ch)
    #棧為空,代表s是有效串,否則是無效串
    return not stack

包含min函式的棧

劍指 Offer 30. 包含min函式的棧

問題描述

定義棧的資料結構,請在該型別中實現一個能夠得到棧中所含最小元素的min函式,並且呼叫 min函式、push函式 及 pop函式 的時間複雜度都是 O(1)。

push(value):將value壓入棧中

pop():彈出棧頂元素

top():獲取棧頂元素

min():獲取棧中最小元素

示例:

minStack.push(-2); -->將-2入棧
minStack.push(0);  -->將0入棧
minStack.push(-3); -->將-3入棧
minStack.min();    -->返回棧中最小元素-3
minStack.pop();    -->彈出棧頂元素-3
minStack.top();    -->返回棧頂元素0
minStack.min();    -->返回棧中最小元素-2

分析問題

對於普通的棧來說,執行push和pop的時間複雜度是O(1),而執行min函式的時間複雜度是O(N),因為要想找到最小值,就需要遍歷整個棧,為了降低min函式的時間複雜度,我們引入了一個輔助棧。

  • 資料棧A:棧A用來儲存所有的元素,保證入棧push、出棧pop、獲取棧頂元素top的操作。
  • 輔助棧B:棧B用來儲存棧A中所有非嚴格遞減的元素,即棧A中的最小值始終在棧B的棧頂,這樣可以保證以O(1)的時間複雜度來返回棧中的最小值。

image-20211010122012798

image-20211010122025833

image-20211010122040550

下面我們來看一下程式碼實現。

class Solution:
    def __init__(self):
        #資料棧
        self.A = []
        #輔助棧
        self.B = []
    #push操作
    def push(self, x):
        self.A.append(x)
        #如果輔助棧B為空,或者棧頂元素大於x,則入棧
        if not self.B or self.B[-1] >= x:
            self.B.append(x)

    def pop(self):
        #彈出資料棧A中的元素
        s = self.A.pop()
        #如果彈出的元素和棧B的棧頂元素相同,則為了保持一致性
        #將棧B的棧頂元素彈出
        if s == self.B[-1]:
            self.B.pop()

    def top(self):
        #返回資料棧A中的棧頂元素
        return self.A[-1]

    def min(self):
        #返回輔助棧B中的棧頂元素
        return self.B[-1]

表示式求值

問題描述

請寫一個整數計算器,支援加減乘三種運算和括號。

示例:

輸入:"(2 * (3 - 4)))* 5"

返回值:-10

分析問題

因為只支援加、減、乘、括號,所以我們根據優先順序可以分為3類,即括號>乘>加、減,假設先把括號去掉,那麼就剩下乘和加減運算,根據運算規則,我們需要先計算乘、再計算加、減,因此我們可以這麼來考慮,我們先進行乘法運算,並將這些乘法運算後的整數值返回原表示式的相應位置,則隨後整個表示式的值,就等於一系列整數加減後的值。而對於被括號分割的表示式,我們可以遞迴的去求解,具體演算法如下。

遍歷字串s,並用變數preSign記錄每個數字之前的運算子,初始化為加號。

  1. 遇到空格時跳過。
  2. 遇到數字時,繼續遍歷求出這個完整的數字的值,儲存到num中。
  3. 遇到左括號時,需要遞迴的求出這個括號內的表示式的值。
  4. 遇到運算子或者表示式的末尾時,就根據上一個運算子的型別來決定計算方式。
    • 如果是加號,不需要進行計算,直接push到棧裡
    • 如果是減號,就去當前數的相反數,push到棧裡
    • 如果是乘號,就需要從棧內pop出一個數和當前數求乘法,再把計算結果push到棧中
  5. 最後把棧中的結果求和即可。

下面我們來看一下程式碼實現。

class Solution:
    def calculate(self, s):
        n = len(s)
        #存取部分資料和
        stack = []
        preSign = '+'
        num = 0
        i=0
        while i<n:
            c=s[i]
            if c==' ':
                i=i+1
                continue
            if c.isdigit():
                num = num * 10 + ord(c) - ord('0')

            #如果遇到左括號,遞迴求出括號內表示式的值
            if c=='(':
                j=i+1
                counts=1
                #擷取出括號表示式的值
                while counts>0:
                    if s[j]=="(":
                        counts=counts+1
                    if s[j]==")":
                        counts=counts-1
                    j=j+1
                #剝去一層括號,求括號內表示式的值
                num=self.calculate(s[i+1:j-1])
                i=j-1

            if not c.isdigit() or i==n-1:
                if preSign=="+":
                    stack.append(num)
                elif preSign=="-":
                    stack.append(-1*num)
                elif preSign=="*":
                    tmp=stack.pop()
                    stack.append(tmp*num)

                num=0
                preSign=c
            i=i+1
        return sum(stack)

s=Solution()
print(s.calculate("(3+4)*(5+(2-3))"))

滑動視窗的最大值

LeetCode 239. 滑動視窗最大值

問題描述

給你一個整數陣列 nums,有一個大小為 k 的滑動視窗從陣列的最左側移動到陣列的最右側。你只可以看到在滑動視窗內的 k 個數字。滑動視窗每次只向右移動一位。返回滑動視窗中的最大值。

示例:

輸入:[2,3,4,2,6,2,5,1],3

輸出:[4,4,6,6,6,5]

分析問題

這道題的關鍵點在於求滑動視窗中的最大值。大小為k的滑動視窗,我們可以通過遍歷的方式來求出其中的最大值,需要O(k)的時間複雜度。對於大小為n的陣列nums,一共有n-k+1個視窗,因此該演算法的時間複雜度是O(nk)。

image-20211028134716254

通過觀察,我們可以知道,對於兩個相鄰的滑動視窗,有k-1個元素是共用的,只有一個元素是變化的,因此我們可以利用此性質來優化我們的演算法。

image-20211028135405455

對於求最大值問題,我們可以使用優先順序佇列(大頂推)來求解。首先,我們將陣列的前k個元素放入優先順序佇列中。每當我們向右移動視窗時,我們就可以把一個新的元素放入佇列中,此時堆頂元素就是堆中所有元素的最大值,然而這個最大值有可能不屬於當前的滑動視窗中,我們需要將該元素進行移除處理(如果最大值不在當前滑動視窗中,它只能在滑動視窗的左邊界的左側,所以滑動視窗向右移動的過程中,該元素再也不會出現在滑動視窗中了,所以我們可以對其進行移除處理)。我們不斷地移除堆頂的元素,直到其確實出現在滑動視窗中。此時,堆頂元素就是滑動視窗中的最大值。

為了方便判斷堆頂元素與滑動視窗的位置關係,我們可以在優先佇列中儲存二元組 (num,index),表示元素num在陣列中的下標為index。

小trick:因為python中只提供了小頂堆,所以我們需要對元素進行取反處理,例如對於列表[1, -3],我們對元素進行取反,然後插入小頂堆中,此時堆中是這樣的[-1,3],我們取出堆頂元素-1,然後取反為1,正好可以得到列表中的最大值1。

我們nums=[2,3,4,2,6,2,5,1],k=3為例,來看一下具體的過程。

  1. 首先,我們將nums的前3個元素放入優先順序佇列中,隊首元素下標值index=2>0,在視窗中,所以加入結果中,此時res=[4]。

    image-20211028181619295

  2. 下一個元素2入隊,此時隊首元素下標index=2>1,在視窗中,所以加入結果中,此時res=[4,4]。

    image-20211028181644285

  3. 下一個元素6入隊,此時隊首元素下標index=4>2,在視窗中,所以加入結果中,此時res=[4,4,6]。

    image-20211028181720875

  4. 下一個元素2入隊,此時隊首元素下標index=4>3,在視窗中,所以加入結果中,此時res=[4,4,6,6]。

    image-20211028181754576

  5. 下一個元素5入隊,此時隊首元素下標index=4=4,在視窗中,所以加入結果中,此時res=[4,4,6,6,6]。

    image-20211028181811290

  6. 下一個元素1佇列,此時隊首元素下標index=4<5,不在視窗中,所以我們將其彈出,此時隊首元素的下標變為6,在視窗中,所以加入結果中,此時res=[4,4,6,6,6,5]。

    image-20211028181832592

進階

這道題我們也可以使用雙端佇列來求解。我們在遍歷陣列的過程中,不斷的對元素對應的下標進行出隊入隊操作,在出入隊的過程中,我們需要保證佇列中儲存的下標對應的元素是從大到小排序的。具體來說,當有一個新的元素對應的下標需要入隊時,如果該元素比隊尾對應的元素的值大,我們需要彈出隊尾,然後迴圈往復,直到佇列為空或者新的元素小於隊尾對應的元素。

由於佇列中下標對應的元素是嚴格單調遞減的,因此隊首下標對應的元素就是滑動視窗中的最大值。但是此時的最大值可能在滑動視窗左邊界的左側,並且隨著視窗向右移動,它永遠不可能出現在滑動視窗中了。因此我們還需要不斷從隊首彈出元素,直到隊首元素在視窗中為止。

我們還是以nums=[2,3,4,2,6,2,5,1],k=3為例,來看一下具體的過程。我們首先初始化一個空佇列que。

  1. 此時佇列為que空,元素2對應的下標0入隊。並且此時未形成視窗,不取值。

    image-20211028182724320

  2. 此時佇列que=[0],隊尾元素為0,它對應陣列中的元素是nums[0] < nums[1]的,所以我們把隊尾0彈出,此時佇列為空,我們將1入隊。並且此時未形成視窗,不取值。

    image-20211028182735893

  3. 此時佇列que=[1],隊尾元素為1,它對應的陣列中的元素是nums[1] < nums[2]的,所以我們把隊尾1彈出,此時佇列為空,我們將2入隊。並且此時隊首元素2在視窗[0,2]中,所以取出隊首元素。

    image-20211028182754689

  4. 此時佇列que=[2],隊尾元素為2,它對應的陣列中的元素是nums[2] > nums[3]的,所以我們將3入隊。並且此時隊首元素2在視窗[1,3]中,所以取出隊首元素。

    image-20211028182820316

  5. 此時佇列que=[2,3],隊尾元素為3,它對應的陣列中的元素是nums[3] < nums[4]的,所以我們把隊尾3彈出,並且此時隊尾元素對應的陣列中的元素是nums[2] < nums[4],所以我們把隊尾2彈出,此時佇列為空,我們將4入隊。並且此時隊首元素4在視窗[2,4]中,所以取出隊首元素。

    image-20211028182843010

  6. 此時佇列que=[4],隊尾元素為4,它對應的陣列中的元素是nums[4] > nums[5]的,所以我們將5入隊。並且此時隊首元素4在視窗[3,5]中,所以我們取出隊首元素。

    image-20211028182937255

  7. 此時佇列que=[4,5],隊尾元素為5,它對應的陣列中的元素是nums[5] < nums[6]的,所以我們把隊尾5彈出,此時隊尾元素對應的陣列中的元素時nums[4] > nums[6] ,所以我們將6入隊。並且此時隊首元素4在視窗[4,6]中,所以我們取出隊首元素。

    image-20211028183110577

  8. 此時佇列que=[4,6],隊尾元素為6,它對應的陣列中的元素是nums[6] > nums[7]的,所以我們將7入隊。而此時隊首元素4不在視窗[5,7]中,所以我們將其移除佇列,此時隊首元素6在視窗[5,7]中,所以我們將其取出。

    image-20211028183151850

下面我們來看一下程式碼實現。

import collections
class Solution:
    def maxSlidingWindow(self, nums, k):
        n = len(nums)
        #申請一個雙端佇列
        q = collections.deque()

        #初始化第一個視窗
        for i in range(k):
            #如果佇列不為空且比隊尾元素大,將隊尾出隊
            while q and nums[i] >= nums[q[-1]]:
                q.pop()
            #直到佇列為空,或者比隊尾元素小,入隊
            q.append(i)

        #將隊首元素加入結果中
        ans = [nums[q[0]]]

        #視窗逐步向右移動
        for i in range(k, n):
            #如果佇列不為空且比隊尾元素大,將隊尾出隊
            while q and nums[i] >= nums[q[-1]]:
                q.pop()
            #直到佇列為空,或者比隊尾元素小,入隊
            q.append(i)
            #如果隊首元素不在該視窗內,出隊操作
            while q[0] <= i - k:
                q.popleft()
            #將隊首元素加入結果中
            ans.append(nums[q[0]])

        return ans


s=Solution()
print(s.maxSlidingWindow([2,3,4,2,6,2,5,1],3))

棧和排序

問題描述

給你一個由1~n,n個數字組成的一個排列和一個棧,要求按照排列的順序入棧。如何在不打亂入棧順序的情況下,僅利用入棧和出棧兩種操作,輸出字典序最大的出棧序列。

排列:指 1 到 n 每個數字出現且僅出現一次。

示例:

輸入:[2,4,5,3,1]

輸出:[5,4,3,2,1]

分析問題

由於我們只能使用出棧和入棧兩種操作,要想使得出棧序列字典序最大,首先想到的就是令高位儘可能地大,我們出棧的時機就是:當前入棧元素若是大於之後將要入棧的元素,那麼就將其出棧。當元素出棧後,還需要判斷棧頂元素與之後將要入棧元素之間的大小關係,如果此時棧頂元素大於之後將要入棧的元素,那麼就將其出棧,不斷判斷直到棧為空或條件不滿足。

為了快速判斷“當前入棧元素是否大於之後將要入棧的元素”,我們需要建立一個輔助陣列temp,其中temp[i]表示i之後的最大元素。藉助輔助陣列,我們可以以O(1)的時間複雜度去判斷當前入棧元素是否大於之後將要入棧的元素。

image-20211109214153107

image-20211109215553532

image-20211109215610927

image-20211109215644288

image-20211109215700561

image-20211109215716016

image-20211109215736573

image-20211109215756969

image-20211109215816076

下面我們來看一下程式碼的實現。

import sys
class Solution:
    def solve(self , a):
        n=len(a)
        res=[]
        if n==0:
            return res
        stack=[]
        temp=[0]*n
        temp[n-1]=-sys.maxsize-1
        #從右往左遍歷陣列a,然後取填充temp
        #使得temp[i]表示i之後的最大元素
        for i in range(n-2,-1,-1):
            temp[i]=max(a[i+1],temp[i+1])

        #遍歷陣列a
        for i in range(0,n):
            if a[i] > temp[i]:  #若當前元素大於之後將要入棧的元素,將其加入結果中
                res.append(a[i])
                # 若棧不為空,且棧頂元素大於temp[i],
                # 棧頂出棧,加入結果中
                while stack and stack[-1] > temp[i]:
                    res.append(stack[-1])
                    stack.pop()
            else:
                stack.append(a[i])

        while stack:
            res.append(stack[-1])
            stack.pop()
        return res

該演算法的時間複雜度是O(n),空間複雜度也是O(n)。

單調棧

問題描述

給定一個長度為 n 的可能含有重複值的陣列 arr ,找到每一個 i 位置左邊和右邊離 i 位置最近且值比 arr[i] 小的位置。請設計演算法,返回一個二維陣列,表示所有位置相應的資訊。位置資訊包括:兩個數字 l 和 r,如果不存在,則值為 -1,下標從 0 開始。

示例:

輸入:[3,4,1,5,6,2,7]

輸出:[[-1,2],[0,2],[-1,-1],[2,5],[3,5],[2,-1],[5,-1]]

分析問題

這道題最簡單的解法就是暴力求解,即通過兩層for迴圈來求解。如下所示:

class Solution:
    def foundMonotoneStack(self , nums):
        n=len(nums)
        res=[]
        #遍歷一遍陣列
        for i in range(0,n):
            l=-1
            r=-1
            #從左往右尋找l,尋找比nums[i]小的最近的nums[l]
            for j in range(0,i):
                if nums[j] < nums[i]:
                    l=j

            #從右往左尋找l,尋找比nums[i]小的最近的nums[r]
            for j in range(n-1,i,-1):
                if nums[j] < nums[i]:
                    r=j

            res.append([l,r])
        return res

該演算法的時間複雜度是O(n^2),空間複雜度是O(1)。

顯然暴力求解的時間複雜度太高,那我們該如何優化呢?其實這道題我們可以使用單調棧來求解,首先我們來看一下單調棧的定義。

單調棧是指棧內元素是具有有單調性的棧,和普通棧相比,單調棧在入棧的時候,需要將待入棧的元素和棧頂元素進行對比,看待加入棧的元素入棧後是否會破壞棧的單調性,如果不會,直接入棧,否則一直彈出到滿足條件為止。

本題中,我們維護一個儲存陣列下標的單調棧。然後遍歷陣列,執行如下操作。

我們以求每一個i左邊離i最近且小於arr[i]的位置為例,來看一下演算法的執行流程。首先我們從左往右遍歷陣列。

  • 假設遍歷到的元素是 arr[i],棧頂元素 top 對應的陣列中的元素是 arr[top],然後我們拿 arr[i] 和 arr[top] 進行對比。
  • 如果 arr[top] > arr[i],就說明 top 不是第 i 個元素的解,也不會是 i 以後任何元素的解(因為 i 比 top 距離後面的數更近,同時arr[i] < arr[top]),所以我們就把top彈出,直到棧為空或者棧頂元素(陣列的下標)對應陣列中的元素小於 arr[i]。
  • 如果arr[top] < arr[i],就說明第 i 個數的解就是 top,因為棧內的元素都是單調遞增的,所以 top 是離 i 最近的數,即 top 就是所求值。然後因為 i 可能是 i 右側的候選解,所以把 i 加入棧中。

image-20211110161325006

image-20211110161410870

image-20211110161448715

image-20211110161517313

同理,我們從右往左遍歷陣列,就可以得到每一個i右邊離i最近且小於arr[i]的位置

下面我們來看一下程式碼的實現。

class Solution:
    def foundMonotoneStack(self , nums):
        n=len(nums)
        res=[]
        l=[0]*n
        r=[0]*n
        #單調棧
        stack=[]
        #從左往右遍歷陣列
        for i in range(0,n):
            #如果棧頂元素top對應的陣列中的元素num[top]>nums[i]
            #則出棧,直到棧為空或者棧頂元素對應的陣列中的元素比nums[i]小
            while stack and nums[stack[-1]] >=nums[i]:
                stack.pop()

            l[i]=stack[-1] if stack else -1
            #i入棧,因為i有可能成為i右邊的答案
            stack.append(i)

        stack=[]
        #從右往左遍歷陣列
        for i in range(n-1,-1,-1):
            # 如果棧頂元素top對應的陣列中的元素num[top]>nums[i]
            # 則出棧,直到棧為空或者棧頂元素對應的陣列中的元素比nums[i]小
            while stack and nums[stack[-1]] >= nums[i]:
                stack.pop()
            r[i] = stack[-1] if stack else -1
            # i入棧,因為i有可能成為i左邊的答案
            stack.append(i)

        for i in range(0,len(l)):
            res.append([l[i],r[i]])

        return res

s=Solution()
print(s.foundMonotoneStack([3,4,1,5,6,2,7]))

該演算法的時間複雜度是O(n),空間複雜度是O(n)。

每日溫度

739. 每日溫度

問題描述

請根據每日氣溫列表 temperatures ,計算在每一天需要等幾天才會有更高的溫度。如果氣溫在這之後都不會升高,請在該位置用 0 來代替。

示例:

輸入:temperatures = [73,74,75,71,69,72,76,73]

輸出:[1,1,4,2,1,1,0,0]

分析問題

既然是求每一天需要等幾天才會有更高的溫度,那麼最直觀的想法就是針對氣溫列表 temperatures 中的每個溫度值,向後依次進行搜尋,找到第一個比當前溫度更高的值的位置索引,然後減去當前溫度所在的位置索引,就是要求的結果。

下面我們來看一下程式碼的實現。

class Solution(object):
    def dailyTemperatures(self,temperatures):
        #求出溫度列表的長度
        n = len(temperatures)
        result=[0]*n
        #遍歷每一個溫度值
        for i in range(n):
            if temperatures[i]<100:
                #想後搜尋第一個大於當前溫度值的元素
                for j in range(i+1,n):
                    if temperatures[j] > temperatures[i]:
                        result[i]=j-i
                        break

        return result

該演算法的時間複雜度是O(n^2),空間複雜度是O(n)。

顯然該演算法的時間複雜度太高,那我們有什麼優化的方法嗎。

優化

這裡可以使用單調棧來優化,即維護一個溫度值下標的單調棧,使得從棧頂到棧底的的元素對應的溫度值依次遞減。具體來說,我們正向遍歷溫度列表 temperatures。對於溫度列表中的每個元素 temperatures[i]。如果棧為空,則直接將 i 進棧;如果棧不為空,則比較棧頂元素 prev 對應的溫度值 temperatures[prev] 和 當前溫度 temperatures[i]。

  • 如果 temperatures[prev] < temperatures[i],則將棧頂元素 prev 移除,此時 prev 對應的等待天數為 i - prev。重複上述操作直到棧為空或者棧頂元素對應的溫度大於等於當前溫度,然後將 i 進棧。

  • 如果 temperatures[prev] > temperatures[i],則直接將元素 i 入棧。

下面我們來思考一個問題,為什麼可以在出棧的時候更新等待天數 result[prev] 呢?因為即將進棧的元素 i 對應的溫度值 temperatures[i] 一定是 temperatures[prev] 右邊第一個比它大的元素。

下面我們來看一個具體的例子,假設溫度列表 temperatures = [73,74,75,71,69,72,76,73] 。

初始時,單調棧 stack 為空;等待天數 result 為 [0,0,0,0,0,0,0,0]。

image-20211129124347950

image-20211129124411253

image-20211129124507019

image-20211129124531093

image-20211129124608472

image-20211129124634335

image-20211129124752957

image-20211129124817867

image-20211129124851144

下面我們來看一下程式碼的實現。

n = len(temperatures)
        #初始化一個空的棧
        stack = []
        result = [0] * n
        for i in range(n):
            temperature = temperatures[i]
            #如果 temperatures[i] 大於棧頂元素對應的溫度值,則棧頂元素出棧。
            while stack and temperature > temperatures[stack[-1]]:
                prev = stack.pop()
                result[prev] = i - prev
            stack.append(i)
        return result

該演算法的時間複雜度是O(n),空間複雜度也是O(n)。

相關文章