1.栈与队列
文章目录
写在前面
本系列笔记主要作为笔者刷题的题解,所用的语言为Python3
,若于您有助,不胜荣幸。
1.1栈与队列理论基础
栈[stack]是一种先进后出逻辑的线性数据结构。栈的常用操作如表所示
方法 | 描述 | 时间复杂度 |
---|---|---|
push() | 元素入栈 | O ( 1 ) \mathcal{O}(1) O(1) |
pop() | 栈顶元素出栈 | O ( 1 ) \mathcal{O}(1) O(1) |
peek() | 访问栈顶元素 | O ( 1 ) \mathcal{O}(1) O(1) |
队列[queue]是一种先进先出逻辑的线性数据结构。我们将队列的头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。为了统一我们采用和栈相同的命名方式
方法 | 描述 | 时间复杂度 |
---|---|---|
push() | 元素入队,即将元素添加到队尾 | O ( 1 ) \mathcal{O}(1) O(1) |
pop() | 队首元素出队 | O ( 1 ) \mathcal{O}(1) O(1) |
peek() | 访问队首元素 | O ( 1 ) \mathcal{O}(1) O(1) |
1.2用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
思路:使用两个栈来实现一个队列,我们需要一个栈充当in
,负责接收元素,另一个栈充当out
,负责弹出元素,每当我们需要弹出元素的时候,我们就将in
中的所有元素弹出并加入到out
中,然后再从out
中弹出栈顶元素。
class MyQueue:
def __init__(self):
self.stack_in: List = []
self.stack_out: List = []
def push(self, x: int) -> None:
while self.stack_out: # 将out栈中的元素恢复到in栈中
self.stack_in.append(self.stack_out.pop())
self.stack_in.append(x) # 添加新元素
def pop(self) -> int:
if self.empty():
return None
if self.stack_out:
return self.stack_out.pop()
else:
while self.stack_in:
self.stack_out.append(self.stack_in.pop())
return self.stack_out.pop()
def peek(self) -> int:
res: int = self.pop()
self.stack_out.append(res)
return res
def empty(self) -> bool:
return not self.stack_in and not self.stack_out # 只要in或out含有元素,说明队列不为空
1.3用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
思路:我们可以使用两个队列来实现一个栈,或者使用一个队列来实现一个栈。两种方法都是可以的
解法一:使用一个队列
# 使用一个队列来实现栈
from collections import deque
class MyStack:
def __init__(self):
self.queue: deque = deque()
self.size: int = 0
def push(self, x: int) -> None:
self.queue.append(x)
self.size += 1
def pop(self) -> int:
if self.empty():
return None
for _ in range(self.size-1):
self.queue.append(self.queue.popleft())
self.size -= 1
return self.queue.popleft()
def top(self) -> int:
if self.empty():
return None
ans: int = self.pop()
self.push(ans)
return ans
def empty(self) -> bool:
return not self.queue
解法二:使用两个队列
from collections import deque
class MyStack:
def __init__(self):
self.que_in: deque = deque()
self.que_out: deque = deque()
def push(self, x: int) -> None:
while self.que_out: # 将out中的元素恢复到in中
self.que_in.append(self.que_out.popleft())
self.que_in.append(x) # 添加新的元素
def pop(self) -> int:
"""
1. 首先确认不空
2. 因为队列的特殊性,FIFO,所以我们只有在pop()的时候才会使用queue_out
3. 先把queue_in中的所有元素(除了最后一个),依次出列放进queue_out
4. 交换in和out,此时out里只有一个元素
5. 把out中的pop出来,即是原队列的最后一个
tip:这不能像栈实现队列一样,因为另一个queue也是FIFO,如果执行pop()它不能像
stack一样从另一个pop(),所以干脆in只用来存数据,pop()的时候两个进行交换
"""
if self.empty():
return None
for _ in range(len(self.que_in)-1): # 保存前n-1个元素
self.que_out.append(self.que_in.popleft())
self.que_out, self.que_in = self.que_in, self.que_out # 这里很重要
return self.que_out.popleft()
def top(self) -> int:
if self.empty():
return None
for _ in range(len(self.que_in)-1): # 保存前n-1个元素
self.que_out.append(self.que_in.popleft())
self.que_out, self.que_in = self.que_in, self.que_out
ans: int = self.que_out.popleft()
self.que_in.append(ans)
return ans
def empty(self) -> bool:
return not self.que_in and not self.que_out
使用两个队列,用存储空间来换取时间,明显看出这样的执行速度更快。
1.4有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
思路:首先我们需要找到有哪些不匹配的情况,其实这道题目一共只有三种不匹配的情况,分别是:
- 情况1:字符串左边存在多余的括号
([{}]()
- 情况2:字符串中间存在不匹配的括号
[{(]}]
- 情况3:字符串右边存在多余的括号
[{}]()))
针对这三种情况我们分别来进行处理即可。
class Solution:
def isValid(self, s: str) -> bool:
stack: List = []
if len(s) % 2 != 0: # 剪枝(可省略)
return False
for char in s:
if char == '(':
stack.append(')')
elif char == '[':
stack.append(']')
elif char == '{':
stack.append('}')
elif not stack or char != stack[-1]: # 处理情况二中间不匹配,或者情况三右边有多余的元素
return False
else:
stack.pop()
return True if not stack else False # 处理情况一左边有多余的元素
1.5删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
思路:这是一道典型的使用栈来完成的题目,我们只需要判断入栈的元素是否等于栈顶元素,如果等于则表明这是一对需要删除的元素,我们只需要移除栈顶元素即可,最后返回由栈中所有元素构成的字符串即可。
class Solution:
def removeDuplicates(self, s: str) -> str:
stack: List[str] = []
for c in s:
if stack and c == stack[-1]: # 如果当前入栈的元素等于栈顶元素,且栈不为空,则删除栈顶元素
stack.pop()
else:
stack.append(c)
return ''.join(stack)
1.6逆波兰表达式求值
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
思路:什么是逆波兰表达式呢?这其实是一种方便计算机运算的存储方式,逆波兰表达式其实是相当于二叉树的后序遍历,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,这就非常像一个字符串匹配的问题,我们就可以用栈这种数据结构来进行处理。注意:第一个弹出的数字应该在运算符的后面。
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
op_map = {'+': add, '-': sub, '*': mul, '/': lambda x, y: int(x/y)}
stack: List[int] = []
for c in tokens:
if c in ['+', '-', '*', '/']:
num2 = stack.pop()
num1 = stack.pop()
stack.append(op_map[c](num1, num2)) # 第一个出来的在运算符后面
else:
stack.append(int(c))
return stack[-1]
1.7滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路:维护一个单调队列,如果push()
进的值大于当前队列的队首位置的,则将队列清空,再将这个值push()
进来,这样就维护了一个单调递减的队列,并且每次访问队首位置就能够访问队列中的最大值,那我们应该如何做pop()
呢?通常来说做pop()
不需要传入任何的值,但是这里我们可以判断我们当前需要pop()
的值是否和队首的位置相等,如果相等则表明这是需要弹出的元素:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
from collections import deque
class MyQueue:
def __init__(self): # 单调队列,从大到小
self.que: deque = deque()
#每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
#同时pop之前判断队列当前是否为空。
def pop(self, value):
if self.que and self.que[0] == value:
self.que.popleft()
#如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
#这样就保持了队列里的数值是单调从大到小的了。
def push(self, value):
while self.que and value > self.que[-1]:
self.que.pop()
self.que.append(value)
def front(self):
return self.que[0]
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
result: List[int] = []
queue: MyQueue = MyQueue()
for i in range(k):
queue.push(nums[i])
result.append(queue.front())
for i in range(k, len(nums)):
queue.pop(nums[i-k]) # 判断进入上个队列的首个元素还存在吗?如果存在就表明这个元素是最大的值,并且没有被pop掉,就手动popleft掉这个元素
queue.push(nums[i])
result.append(queue.front())
return result
1.8前K个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
思路:先使用哈希法得到一个可供查询的map,然后获取这个map中value值的前k
个即可,但是我们无需对整个map都进行排序,而是我们只维护一个大小为k
的有序区间。这就涉及到了大顶堆和小顶堆这样的数据结构。这里涉及到选择小顶堆还是大顶堆的问题,如果我们选择大顶堆的话,每次弹出的元素都是最大的元素,这样我们就把最大的元素都弹出了,只保留了较小的元素,相反如果我们选择小顶堆的话,每次弹出都是弹出最小的元素,这样就保留了加大的元素。
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
elem_map: dict = {}
for elem in nums:
elem_map[elem] = elem_map.get(elem, 0) + 1
# 对频率进行排序,定义一个小顶堆
pri_que = []
for key, value in elem_map.items():
heapq.heappush(pri_que, (value, key)) # heapq按照tuple中的第一个元素来进行排序
if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
heapq.heappop(pri_que)
return [key for value, key in pri_que] # 对返回的顺序没有要求,我们可以不用每次都进行弹出
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
elem_map: dict = {}
for elem in nums:
elem_map[elem] = elem_map.get(elem, 0) + 1
# 对频率进行排序,定义一个小顶堆
pri_que: List[int] = []
for key, value in elem_map.items():
heapq.heappush(pri_que, (value, key)) # heapq按照tuple中的第一个元素来进行排序
if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
heapq.heappop(pri_que)
res: List[int] = [0] * k
for i in range(k): # 按照频率顺序进行弹出
res[k-i-1] = heapq.heappop(pri_que)[1]
return res