1 棧與隊列理論基礎
隊列先進先出,棧先進後出;不允許有遍歷行爲,不提供迭代器
2 用棧實現隊列
題目:請你僅使用兩個棧實現先入先出隊列。隊列應當支持一般隊列支持的所有操作(push
、pop
、peek
、empty
):
實現 MyQueue
類:
void push(int x)
將元素 x 推到隊列的末尾int pop()
從隊列的開頭移除並返回元素int peek()
返回隊列開頭的元素boolean empty()
如果隊列爲空,返回true
;否則,返回false
說明:
- 你 只能 使用標準的棧操作 —— 也就是隻有
push to top
,peek/pop from top
,size
, 和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)的棧,並支持普通棧的全部四種操作(push
、top
、pop
和 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:
輸入:s = "()[]{}"
輸出:true
示例 2:
輸入:s = "(]"
輸出:false
思路:先來分析一下這裏有三種不匹配的情況:
- 第一種情況,字符串裏左方向的括號多餘了 ,所以不匹配。
- 第二種情況,括號沒有多餘,但是括號的類型沒有匹配上。
- 第三種情況,字符串裏右方向的括號多餘了,所以不匹配。
第一種情況:已經遍歷完了字符串,但是棧不爲空,說明有相應的左括號沒有匹配,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操作要保持如下規則:
- pop(value):如果窗口移除的元素value等於單調隊列的出口元素,那麼隊列彈出元素,否則不用任何操作
- 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]
思路:這道題目主要涉及到如下三塊內容:
- 要統計元素出現頻率
- 對頻率排序
- 找出前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