day11 棧與佇列

haohaoscnblogs發表於2024-07-27

任務

150. 逆波蘭表示式求值

思路

用棧儲存運算元,遇到運算子,將運算元彈出並處理。注意除法的邏輯

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        s = []
        from operator import add, sub, mul
        op_dic = {'+':add,'-':sub }
        for i in range(len(tokens)):
            if tokens[i] not in {'+','-','*','/'}:
                s.append(int(tokens[i]))
            elif tokens[i] == '+':
                op2 = s.pop()
                op1 = s.pop()
                s.append(add(op1,op2))
            elif tokens[i] == '-':
                op2 = s.pop()
                op1 = s.pop()
                s.append(sub(op1,op2))
            elif tokens[i] == '*':
                op2 = s.pop()
                op1 = s.pop()
                s.append(mul(op1,op2))
            elif tokens[i] == '/':
                op2 = s.pop()
                op1 = s.pop()
                val =  op1//op2 if op1 * op2 > 0 else -(abs(op1) // abs(op2))# 向0取整
                s.append(val)
        return s[0]

239. 滑動視窗最大值

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

思路

額外維護這樣一個滑動視窗(雙端佇列結構),因此該結構需要保證:某一時刻,雙端佇列中頭結點的值為最大值。
邏輯上,雙端佇列維護的是什麼資訊呢?

  1. 如果依次擴充視窗,誰會成為最大值的資訊。
  2. 如果不再擴充而是依次縮減滑動視窗,誰會成為最大值的資訊。

當陣列中的視窗往右擴時,為了維護上述結構,需要佇列中將它之前比它小的數都移除出去。這個是比較好想到的。
更深入的思考:為什麼新的數進來後可以讓之前比它小的數(或者相等的數)彈出呢?因為這個彈出的數再也不可能成為最大值了,我的新數比你大還比你晚過期,那麼就不需要你這個舊的小值了。也即是,滑動視窗實際上維護的資訊是兩個維度,數字越新越大越優先。按照上述邏輯,實際上,雙端佇列每一時刻都是單調遞減的。那麼縮減呢?只有當前要縮減的元素沒有在之前擴充時處理過,才縮減,否則已經被處理過了。
完整的演算法如下

  • 擴充:只要比當前要入隊的數小,則從視窗中彈出
  • 縮減:如果和頭節點的數一樣(舊的最大值),則彈出。否則當前需要處理的數就是又舊又不是之前的最大值的數,擴充時已經被處理過了。
  • 最大值:隊首元素
class MyWindow:
    def __init__(self):
        self.queue = deque()
    def push(self,val):
        while(self.queue and val > self.queue[-1]): #只要比當前要入隊的數小,則從視窗中彈出
            self.queue.pop()
        self.queue.append(val) #將當前值放入滑動視窗
    
    def pop(self,val):
        if(self.queue and self.queue[0] == val):
            self.queue.popleft()

    def getMaxValue(self)->int: 
        return self.queue[0]

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        res = []
        window = MyWindow()
        for i in range(k):
            window.push(nums[i]) 
        res.append(window.getMaxValue()) 
        size = len(nums)
        for i in range(k,size):
            window.pop(nums[i-k])
            window.push(nums[i])
            maxV = window.getMaxValue()
            res.append(maxV)
        return res

347.前 K 個高頻元素

思路

堆(優先順序佇列)
這裡只考慮優先順序佇列的功能,暫時不考慮實現(上濾下濾等),以小頂堆為例,即在加入和刪除元素的過程中,堆頂元素保持最小值。
我們用dic來統計頻率,然後利用小頂堆依次新增,達到k個後,再依次新增和彈出最小元素,保證最大的k個元素均在小頂堆中,即可完成。

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        import heapq
        import collections
        dic = collections.defaultdict(int) 
        for i in range(len(nums)):
            dic[nums[i]] += 1
        
        pq = [] #預設小頂堆
        for key,v in dic.items():
            heapq.heappush(pq,(v,key))
            if len(pq) > k:       #後續每次新加入元素,滿足堆的結構後,彈出堆頂元素(最小值)
                heapq.heappop(pq)
        
        #此時pq中的元素為最大的k個元素,pq的堆頂為這k個內最小的元素
        res = [0]*k
        
        for i in range(k-1,-1,-1): #逆序賦值,保證次數單調遞減,val的次數越多的越靠前
            val = heapq.heappop(pq)[1]
            res[i] = val
        return res

心得體會

複習了棧的應用,學習了滑動視窗(雙端佇列)和優先順序佇列。注意後兩種結構的思路和應用。

  • 棧的應用 相鄰項之間的消除
  • 滑動視窗 與之前的陣列中那道題不同,本題是自定義了一種特殊功能的資料結構,保證了可以很快取出極值,並隨著視窗的滑動,這個性質保持不變。
  • 優先順序佇列 同樣利用元素的插入刪除,堆結構性質的一致性,保證棧頂為極值,解決topK問題。

相關文章