代码随想录训练营 Day10打卡 栈与队列 part01
一、 理论基础
在 Python 中,栈和队列是用来存储和管理数据的两种重要的数据结构,虽然 Python 标准库中没有专门的栈或队列类,但可以很容易地用列表(list)或 collections.deque 来实现它们。这两种结构的理论和实际应用非常丰富,尤其在解决算法问题和系统设计中扮演关键角色。
栈 (Stack)
概念:
栈是一种后进先出(Last In, First Out,LIFO)的数据结构。最后添加进栈的元素将是第一个被移除的元素。
操作:
- push:将一个元素添加到栈顶。
- pop:移除栈顶元素。
- peek 或 top:查看栈顶元素而不移除它。
- isEmpty:检查栈是否为空。
Python 实现:
使用列表实现栈,利用列表的 append() 方法来 push,pop() 方法来移除和返回栈顶元素。
class Stack:
def __init__(self):
self.elements = []
def push(self, value):
self.elements.append(value)
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self.elements.pop()
def peek(self):
if self.is_empty():
raise IndexError("peek from empty stack")
return self.elements[-1]
def is_empty(self):
return len(self.elements) == 0
队列 (Queue)
概念:队列是一种先进先出(First In, First Out,FIFO)的数据结构。最先添加的元素将是第一个被移除的元素。
操作:
- enqueue:在队列的末尾添加一个元素。
- dequeue:移除队列的第一个元素。
- front:查看队列的第一个元素。
- isEmpty:检查队列是否为空。
Python 实现:
使用 collections.deque 实现队列,因为它提供了从两端快速添加和删除元素的功能。
from collections import deque
class Queue:
def __init__(self):
self.elements = deque()
def enqueue(self, value):
self.elements.append(value)
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue from empty queue")
return self.elements.popleft()
def front(self):
if self.is_empty():
raise IndexError("front from empty queue")
return self.elements[0]
def is_empty(self):
return len(self.elements) == 0
应用
栈和队列广泛应用于各种算法和系统设计问题中,包括但不限于:
栈:
- 解析表达式(如括号匹配和后缀表达式计算)
- 页面访问历史(浏览器后退功能)
- 函数调用(调用栈)
队列:
- 数据缓冲区(如打印队列)
- 资源共享(如 CPU 调度)
- 广度优先搜索(BFS)算法中使用队列来追踪待访问的节点
理解和掌握这两种基本的数据结构对于开发高效和有效的算法至关重要,能够帮助解决实际问题中的数据管理和访问序列问题。
一、 力扣232 . 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
示例 1:
输入:
[“MyQueue”, “push”, “push”, “peek”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
实现思路
- 初始化: 创建两个栈,stack_in 用于处理入队操作,stack_out 用于处理出队操作。
- 入队: 所有新元素都直接推入 stack_in。
- 出队:
如果 stack_out 为空,则将 stack_in 中的所有元素逐个弹出并推入 stack_out。这样,原本在stack_in 底部的元素(即最早入队的元素)会在 stack_out 的顶部,可以直接弹出。
如果 stack_out不为空,直接从 stack_out 弹出顶部元素。 - 查看队列前端元素: 通过 pop 方法获取队列前端的元素,然后将该元素再次放回 stack_out。
- 判断队列是否为空: 当两个栈都为空时,队列为空。
这种使用两个栈模拟队列的方法在执行出队操作时,只有在 stack_out 为空的情况下才需要将 stack_in 中的元素转移到 stack_out,这样可以减少整体操作的复杂度,使得每个元素平均只被移动两次(一次入 stack_in,一次转入 stack_out)。
代码实现
class MyQueue:
def __init__(self):
"""
初始化两个栈,stack_in用于入队操作,stack_out用于出队操作。
"""
self.stack_in = []
self.stack_out = []
def push(self, x: int) -> None:
"""
入队操作,新元素直接放入stack_in。
:param x: 要入队的元素。
"""
self.stack_in.append(x)
def pop(self) -> int:
"""
出队操作,返回队列前端的元素。
"""
if self.empty():
return None
if not self.stack_out:
# 如果stack_out为空,将stack_in中的所有元素逐一弹出并压入stack_out,
# 这样stack_in的栈底元素(队列的首元素)就移动到了stack_out的栈顶。
while self.stack_in:
self.stack_out.append(self.stack_in.pop())
return self.stack_out.pop()
def peek(self) -> int:
"""
获取队列前端的元素但不出队。
"""
# 使用pop方法获取队列前端的元素后,再将该元素重新压入stack_out。
ans = self.pop()
self.stack_out.append(ans)
return ans
def empty(self) -> bool:
"""
判断队列是否为空。如果两个栈都为空,则队列为空。
"""
return not (self.stack_in or self.stack_out)
二、 力扣 225 . 用队列实现栈
请你仅使用两个队列实现一个后入先出(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]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
使用队列来模拟栈的行为可以通过一个或两个队列实现。队列通常遵循先进先出(FIFO)原则,而栈则是后进先出(LIFO)。
版本一:使用两个队列实现栈
通过两个队列,可以在执行 pop 操作时模拟栈的行为。一个队列用于存储数据 (queue_in),另一个队列 (queue_out) 在执行 pop 操作时使用。
from collections import deque
class MyStack:
def __init__(self):
"""
使用双向队列,可以有效地从两端插入或删除元素。
"""
self.queue_in = deque()
self.queue_out = deque()
def push(self, x: int) -> None:
"""
元素入栈,直接加入到queue_in队列。
"""
self.queue_in.append(x)
def pop(self) -> int:
"""
出栈操作,需要模拟栈的LIFO行为。
先将queue_in中的元素(除最后一个)转移到queue_out,然后交换两个队列的身份,保证下一次操作时,新的queue_in仍然为空。
"""
if self.empty():
return None
while 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:
"""
获取栈顶元素但不弹出。
执行与pop相似的操作,但将最后一个元素重新放入队列。
"""
res = self.pop() # 获取栈顶元素
self.queue_in.append(res) # 将其再次放回,模拟栈顶操作
return res
def empty(self) -> bool:
"""
判断栈是否为空,只需检查queue_in是否为空。
"""
return not self.queue_in
版本二:使用一个队列实现栈
通过一个队列也能实现栈的行为。每次执行 push 操作后,将队列头部的元素(除了最新添加的元素)移动到队列尾部,从而使最新添加的元素始终位于队列的头部。
class MyStack:
def __init__(self):
self.que = deque()
def push(self, x: int) -> None:
"""
入栈操作,在添加新元素后,通过旋转队列的方式,将新元素移动到队列头部。
"""
n = len(self.que)
self.que.append(x)
for _ in range(n):
self.que.append(self.que.popleft())
def pop(self) -> int:
"""
出栈操作,直接从队列头部弹出元素,符合栈的LIFO特性。
"""
if self.empty():
return None
return self.que.popleft()
def top(self) -> int:
"""
查看栈顶元素但不移除。
直接返回队列头部的元素。
"""
if self.empty():
return None
return self.que[0]
def empty(self) -> bool:
"""
判断栈是否为空。
"""
return not self.que
这两种方法各有特点:
- 两个队列实现: 在 pop 操作时转移元素,保证 queue_in 总是用于添加新元素。
- 一个队列实现: 通过循环移动元素确保最后插入的元素总是在队列的头部,以此来模拟栈的行为。
三、 力扣 20. 有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
括号匹配问题是一种常见的编程问题,它可以有效地通过栈这种数据结构来解决。栈的后进先出(LIFO)特性使其非常适合处理与嵌套结构和对称性相关的问题,例如编程语言中的括号匹配。在实际应用中,这不仅适用于编译原理中的词法分析,也适用于操作系统路径解析等多种场景。
括号匹配问题的分析
在解决括号匹配问题之前,重要的是先理解可能导致括号不匹配的几种情况:
- 左括号多余: 如果字符串解析完成后,栈中仍然有剩余的左括号,则表示有未被匹配的左括号。
- 括号类型不匹配: 如果当前字符是一个右括号,它应该与栈顶的左括号匹配。如果类型不匹配(如{]或(}),则说明括号不匹配。
- 右括号多余: 如果在尝试匹配右括号时栈已经为空,这表明没有相对应的左括号与之匹配,因此右括号多余。
其动画如下:
版本一:使用栈
这种方法使用栈来直接存储期待匹配的右括号。当遇到一个左括号时,它就推入相对应的右括号到栈中。这样,在遇到右括号时,可以直接与栈顶元素进行比较。
实现思路:
-
使用栈来存储期待遇到的右括号。
-
遍历字符串中的每个字符。
如果是左括号,将对应的右括号入栈。
如果是右括号,检查栈是否为空或者栈顶元素是否与之匹配。如果不匹配,返回 False。
如果匹配,将栈顶元素出栈。 -
如果遍历结束后栈为空,则说明所有括号正确匹配,返回 True;否则,返回 False。
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 # 不匹配或栈空时返回False
else:
stack.pop() # 匹配则出栈
# 检查是否所有的左括号都被匹配了
return True if not stack else False
版本二:使用字典
实现思路:
- 使用栈来存储期待遇到的右括号,字典来映射左括号到对应的右括号。
- 遍历字符串,利用字典判断当前字符是否为左括号,并相应地处理。
- 对遇到的右括号,检查栈顶元素是否匹配。
- 如果匹配,出栈;否则,返回 False。
- 遍历结束后,如果栈为空,返回 True;否则,表示有未匹配的左括号,返回 False。
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 # 栈为空或不匹配时返回False
else:
stack.pop() # 匹配则出栈
# 栈为空说明全部匹配完毕
return True if not stack else False
四、 力扣 1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S, 重复项删除操作 会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:“abbaca”
输出:“ca”
解释:
例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。
问题解析
给定一个字符串,要求删除所有相邻且相同的字符对,直到没有任何相邻重复字符为止。这个过程可以通过使用栈来有效完成,栈可以帮助我们追踪之前遍历过的元素,并在找到重复项时快速回退。
解决思路
-
初始化栈: 创建一个空栈来保存字符。
-
遍历字符串: 逐个检查字符串中的字符。
如果栈非空且栈顶元素与当前字符 相同 ,说明发现了一对相邻的重复字符,此时应将栈顶元素弹出。
如果栈顶元素与当前字符 不同 ,或者栈为空,则将当前字符推入栈中。 -
构建最终结果: 遍历完成后,栈中剩余的元素就是删除相邻重复项后的结果。由于栈的特性,这些元素的顺序是反向的,因此需要将栈中的元素逆序拼接成最终的字符串。
版本一:使用栈
这种方法使用一个列表来模拟栈的行为,非常适合处理连续相邻的字符删除问题。
class Solution:
def removeDuplicates(self, s: str) -> str:
res = list() # 使用列表模拟栈
for item in s:
if res and res[-1] == item:
res.pop() # 如果栈不空且当前字符与栈顶字符相同,则弹出栈顶字符
else:
res.append(item) # 否则,将当前字符推入栈中
return "".join(res) # 将栈中的字符合并为字符串返回
版本二:使用双指针模拟栈
如果不允许直接使用栈结构,可以使用双指针技术在数组上模拟栈的操作。
class Solution:
def removeDuplicates(self, s: str) -> str:
res = list(s) # 将字符串转换为列表,方便原地修改
slow = fast = 0 # 初始化双指针,slow为模拟栈顶,fast为遍历指针
while fast < len(res):
res[slow] = res[fast] # 将fast指向的元素复制到slow位置
# 如果slow大于0且slow位置的元素与前一个元素相同
if slow > 0 and res[slow] == res[slow - 1]:
slow -= 1 # 相同则回退一个位置,模拟弹栈操作
else:
slow += 1 # 不相同则移动到下一个位置,模拟压栈操作
fast += 1 # fast指针总是向前移动
return ''.join(res[0: slow]) # 返回栈内元素组成的字符串