【算法】棧與隊列

1 棧與隊列理論基礎

隊列先進先出,棧先進後出;不允許有遍歷行爲,不提供迭代器

2 用棧實現隊列

題目:請你僅使用兩個棧實現先入先出隊列。隊列應當支持一般隊列支持的所有操作(pushpoppeekempty):

實現 MyQueue 類:

  • void push(int x) 將元素 x 推到隊列的末尾
  • int pop() 從隊列的開頭移除並返回元素
  • int peek() 返回隊列開頭的元素
  • boolean empty() 如果隊列爲空,返回 true ;否則,返回 false

說明:

  • 你 只能 使用標準的棧操作 —— 也就是隻有 push to toppeek/pop from topsize, 和 is empty 操作是合法的。
  • 你所使用的語言也許不支持棧。你可以使用 list 或者 deque(雙端隊列)來模擬一個棧,只要是標準的棧操作即可。

示例 1:

輸入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
輸出:
[null, null, null, 1, 1, false]

思路:使用棧來模式隊列的行爲,如果僅僅用一個棧,是一定不行的,所以需要兩個棧:一個輸入棧,一個輸出棧,這裏要注意輸入棧和輸出棧的關係。

在push數據的時候,只要數據放進輸入棧就好,但在pop的時候,操作就複雜一些,輸出棧如果爲空,就把進棧數據全部導入進來(注意是全部導入),再從出棧彈出數據,如果輸出棧不爲空,則直接從出棧彈出數據就可以了。

最後如何判斷隊列爲空呢?如果進棧和出棧都爲空的話,說明模擬的隊列爲空了。

Python中可以使用列表(list)來實現棧。可以使用append()方法來將元素添加到棧的頂部,並使用pop()方法來從棧的頂部移除元素。

class MyQueue:
    def __init__(self):
        # in主要負責push,out主要負責pop
        self.stack_in = []
        self.stack_out = []
    def push(self, x: int) -> None:
        self.stack_in.append(x) # 有新元素進來,就往in裏面push
    def pop(self) -> int:
        if self.empty():
            return None
        if not self.stack_out: # 輸出棧爲空
            while len(self.stack_in) != 0:
                self.stack_out.append(self.stack_in.pop())
        return self.stack_out.pop()
    def peek(self) -> int:
        res = self.pop()
        self.stack_out.append(res)
        return res
    def empty(self) -> bool:
        # 只要in或者out有元素,說明隊列不爲空
        return not (self.stack_in or self.stack_out)
        # return (len(self.stack_in) + len(self.stack_out)) == 0

3 用隊列實現棧

題目:請你僅使用兩個隊列實現一個後入先出(LIFO)的棧,並支持普通棧的全部四種操作(pushtoppop 和 empty)。

實現 MyStack 類:

  • void push(int x) 將元素 x 壓入棧頂。
  • int pop() 移除並返回棧頂元素。
  • int top() 返回棧頂元素。
  • boolean empty() 如果棧是空的,返回 true ;否則,返回 false 。

示例:

輸入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
輸出:
[null, null, null, 2, 2, false]

思路:用兩個隊列que1和que2實現棧的功能,que2其實完全就是一個備份的作用,把que1最後面的元素以外的元素都備份到que2,然後彈出最後面的元素,再把其他元素從que2導回que1。

Python普通的Queue或SimpleQueue沒有類似於peek的功能也無法用索引訪問,在實現top的時候較爲困難。因此這裏使用雙向隊列,我們保證只執行popleft()和append(),因爲deque可以用索引訪問,可以實現和peek相似的功能。

class MyStack:
    def __init__(self):
        self.queue_in = deque()
        self.queue_out = deque()
    def push(self, x: int) -> None:
        self.queue_in.append(x)
    def pop(self) -> int:
        if self.empty():
            return None
        for i in range(len(self.queue_in) - 1):
            self.queue_out.append(self.queue_in.popleft())
        self.queue_in, self.queue_out = self.queue_out, self.queue_in
        return self.queue_out.popleft()
    def top(self) -> int:
        ans = self.pop()
        self.queue_in.append(ans)
        return ans
    def empty(self) -> bool:
        return not self.queue_in

其實這道題目用一個隊列就夠了。

一個隊列在模擬棧彈出元素的時候只要將隊列頭部的元素(除了最後一個元素外) 重新添加到隊列尾部,此時再去彈出元素就是棧的順序了。

class MyStack:
    def __init__(self):
        self.queue = deque()
    def push(self, x: int) -> None:
        self.queue.append(x)
    def pop(self) -> int:
        if self.empty():
            return None
        for i in range(len(self.queue) - 1):
            self.queue.append(self.queue.popleft())
        return self.queue.popleft()
    def top(self) -> int:
        return self.queue[-1]
    def empty(self) -> bool:
        return len(self.queue) == 0

4 有效的括號*

題目:給定一個只包括 '('')''{''}''['']' 的字符串 s ,判斷字符串是否有效。有效字符串需滿足:

  1. 左括號必須用相同類型的右括號閉合。
  2. 左括號必須以正確的順序閉合。
  3. 每個右括號都有一個對應的相同類型的左括號。

示例 1:

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

示例 2:

輸入:s = "(]"
輸出:false

思路:先來分析一下這裏有三種不匹配的情況:

  1. 第一種情況,字符串裏左方向的括號多餘了 ,所以不匹配。
  2. 第二種情況,括號沒有多餘,但是括號的類型沒有匹配上。
  3. 第三種情況,字符串裏右方向的括號多餘了,所以不匹配。

第一種情況:已經遍歷完了字符串,但是棧不爲空,說明有相應的左括號沒有匹配,return false

第二種情況:遍歷字符串匹配的過程中,發現棧裏沒有要匹配的字符,所以return false

第三種情況:遍歷字符串匹配的過程中,棧已經爲空了,沒有匹配的字符了,說明右括號沒有找到對應的左括號return false

1. 在遍歷到左括號的時候讓右括號入棧,就只需要比較當前元素和棧頂是否相等就可以
class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        for item in s:
            if item == '(':
                stack.append(')')
            elif item == '[':
                stack.append(']')
            elif item == '{':
                stack.append('}')
            elif not stack or stack[-1] != item: # 棧已經爲空(右括號多)或遇到不匹配括號
                return False
            else:
                stack.pop()      
        return not stack # 棧不爲空說明左括號多
2. 使用字典
class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        mapping = {
            '(': ')',
            '[': ']',
            '{': '}'
        }
        for item in s:
            if item in mapping.keys():
                stack.append(mapping[item])
            elif not stack or stack[-1] != item: 
                return False
            else: 
                stack.pop()
        return True if not stack else False

5 刪除字符串中的所有相鄰重複項

題目:給出由小寫字母組成的字符串 S重複項刪除操作會選擇兩個相鄰且相同的字母,並刪除它們。在 S 上反覆執行重複項刪除操作,直到無法繼續刪除。

在完成所有重複項刪除操作後返回最終的字符串。答案保證唯一。

示例:

輸入:"abbaca"
輸出:"ca"

思路:用棧存放遍歷過的元素,當遍歷當前的這個元素的時候,去棧裏看一下我們是不是遍歷過相同數值的相鄰元素。

class Solution:
    def removeDuplicates(self, s: str) -> str:
        stack = []
        for i in s:
            if stack and stack[-1] == i:
                stack.pop()
            else:
                stack.append(i)
        return "".join(stack)
  • 用雙指針模擬棧***
class Solution:
    def removeDuplicates(self, s: str) -> str:
        fast = slow = 0
        res = list(s)
        while fast < len(s):
            res[slow] = res[fast]
            if slow > 0 and res[slow] == res[slow - 1]: # 如果相同,回退一格
                slow -= 1
            else:
                slow += 1
            fast += 1
        return "".join(res[:slow])

6 逆波蘭表達式

題目: 給你一個字符串數組 tokens ,表示一個根據 逆波蘭表示法 表示的算術表達式。

請你計算該表達式。返回一個表示表達式值的整數。

注意:

  • 有效的算符爲 '+''-''*' 和 '/' 。
  • 每個操作數(運算對象)都可以是一個整數或者另一個表達式。
  • 兩個整數之間的除法總是 向零截斷 。
  • 表達式中不含除零運算。

示例 1:

輸入:tokens = ["2","1","+","3","*"]
輸出:9
解釋:該算式轉化爲常見的中綴算術表達式爲:((2 + 1) * 3) = 9

示例 2:

輸入:tokens = ["4","13","5","/","+"]
輸出:6
解釋:該算式轉化爲常見的中綴算術表達式爲:(4 + (13 / 5)) = 6

題解

注意除法要寫成int(num1 / num2)

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        stack = []
        for i in tokens:
            if i not in {'+', '-', '*', '/'}: # 說明是數字
                stack.append(int(i))
            else:
                num2 = stack.pop()
                num1 = stack.pop()
                if i == '+':
                    stack.append(num1 + num2)
                elif i == '-':
                    stack.append(num1 - num2)
                elif i == '*':
                    stack.append(num1 * num2)
                elif i == '/':
                    stack.append(int(num1 / num2))
        return stack[0]

還看到兩種寫法,用了map和eval()

from operator import add, sub, mul
class Solution:
    op_map = {'+': add, '-': sub, '*': mul, '/': lambda x, y: int(x / y)}
    def evalRPN(self, tokens: List[str]) -> int:
        stack = []
        for token in tokens:
            if token not in {'+', '-', '*', '/'}:
                stack.append(int(token))
            else:
                op2 = stack.pop()
                op1 = stack.pop()
                stack.append(self.op_map[token](op1, op2))  # 第一個出來的在運算符後面
        return stack.pop()
class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        stack = []
        for item in tokens:
            if item not in {"+", "-", "*", "/"}:
                stack.append(item)
            else:
                first_num, second_num = stack.pop(), stack.pop()
                stack.append(
                    int(eval(f'{second_num} {item} {first_num}'))   # 第一個出來的在運算符後面
                )
        return int(stack.pop()) # 如果一開始只有一個數,那麼會是字符串形式的

7 滑動窗口最大值**

題目:給你一個整數數組 nums,有一個大小爲 k **的滑動窗口從數組的最左側移動到數組的最右側。你只可以看到在滑動窗口內的 k 個數字。滑動窗口每次只向右移動一位。

返回滑動窗口中的最大值 。

示例 1:

輸入:nums = [1,3,-1,-3,5,3,6,7], k = 3
輸出:[3,3,5,5,6,7]

思路:這是使用單調隊列的經典題目。

隊列裏的元素一定是要排序的,而且要最大值放在出隊口,要不然怎麼知道最大值呢。

但如果把窗口裏的元素都放進隊列裏,窗口移動的時候,隊列需要彈出元素。那麼問題來了,已經排序之後的隊列怎麼能把窗口要移除的元素(這個元素可不一定是最大值)彈出呢。

其實隊列沒有必要維護窗口裏的所有元素,只需要維護有可能成爲窗口裏最大值的元素就可以了,同時保證隊列裏的元素數值是由大到小的。

那麼這個維護元素單調遞減的隊列就叫做單調隊列,即單調遞減或單調遞增的隊列。

設計單調隊列的時候,pop,和push操作要保持如下規則:

  1. pop(value):如果窗口移除的元素value等於單調隊列的出口元素,那麼隊列彈出元素,否則不用任何操作
  2. push(value):如果push的元素value大於入口元素的數值,那麼就將隊列入口的元素彈出,直到push元素的數值小於等於隊列入口元素的數值爲止

保持如上規則,每次窗口移動的時候,只要問que.front()就可以返回當前窗口的最大值。

❗踩了個坑,push操作最開始設計成value大於等於入口元素時都會把入口元素覆蓋。出現問題:當value=入口元素=最大值,也就是新來的這個value一下子跑到單調隊列最前面去了,那麼下一次窗口滑動需要pop時,如果原先最大的元素正好就是要被移出窗口的元素,剛進來的value會代替那個最大值被移出去,隊列裏失去了應有的最大值!所以不能加等號!


class MyQueue:
    def __init__(self):
        self.que = deque() # 雙向
    def pop(self, val):
        if self.que and self.que[0] == val: # 要彈出的元素就是當前最大值
            self.que.popleft()
    def push(self, val):
        while self.que and val > self.que[-1]: # 要放入的元素比入口元素大
            self.que.pop()
        self.que.append(val)
    def top(self):
        return self.que[0]
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        que = MyQueue()
        res = []
        for i in range(k):
            que.push(nums[i])
        res.append(que.top())
        for i in range(k, len(nums)):
            que.pop(nums[i - k])
            que.push(nums[i])
            res.append(que.top())
        return res

8 前k個高頻元素*

題目:給你一個整數數組 nums 和一個整數 k ,請你返回其中出現頻率前 k 高的元素。你可以按 任意順序 返回答案。

示例 1:

輸入:nums = [1,1,1,2,2,3], k = 2
輸出:[1,2]

思路:這道題目主要涉及到如下三塊內容:

  1. 要統計元素出現頻率
  2. 對頻率排序
  3. 找出前K個高頻元素
  • 用字典自己寫了個解
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        freq = defaultdict(int)
        for i in nums:
            freq[i] += 1
        sorted_items = sorted(freq.items(), key = lambda x: x[1]) # 根據頻率排序,得到元組列表
        res = sorted_items[-k:] # 獲取頻率最高的k項,每一項爲(val, freq)元組
        return [item[0] for item in res] # 返回頻率最高的k個val組成的列表
  • 優先級隊列 - 小頂堆

對頻率進行排序,可以使用優先級隊列priority_queue。

優先級隊列就是一個披着隊列外衣的堆,內部元素自動依照元素的權值排列。

堆是一棵完全二叉樹,樹中每個結點的值都不小於(或不大於)其左右孩子的值。 如果父親結點是大於等於左右孩子就是大頂堆(堆頭是最大元素),小於等於左右孩子就是小頂堆。

我們要用小頂堆而不是大頂堆,因爲要統計最大前k個元素,只有小頂堆每次將最小的元素彈出,最後小頂堆裏積累的纔是前k個最大元素。存放元組時,小頂堆會根據元組的第一個元素(整數)進行排序。

import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        freq = defaultdict(int)
        for i in nums:
            freq[i] += 1   
        min_heap = [] # 小頂堆
        for val, freq in freq.items():
            heapq.heappush(min_heap, (freq, val))
            if len(min_heap) > k: # 固定大小爲k
                heapq.heappop(min_heap)
        res = []
        for i in range(k):
            res.append(heapq.heappop(min_heap)[1]) # 此時小頂堆裏一定是最大的k個
        return res
  • 優先級隊列 - 我直接用大頂堆也不是不行啊!
import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        freq = defaultdict(int)
        for i in nums:
            freq[i] += 1    
        max_heap = [] 
        for val, freq in freq.items():
            heapq.heappush(max_heap, (-freq, val)) # 負頻率實現大頂堆     
        res = []
        for i in range(k):
            res.append(heapq.heappop(max_heap)[1])
        return res

❗一開始用了切片結果踩坑了,注意大頂堆的列表不是整體有序的,還是得用heappop

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章