-1
勘误
在本文内容正式开始之前,先对上一篇推文《Python数据结构——栈与队列》的一个表述不当之处进行校正。
不当之处原文如下。
此处前后语句内容完全衔接不上,“链式存储结构”是一种存储结构,“线性结构”是数据的一种逻辑结构,二者之间不应当使用“而”字作为转折,容易误导读者认为“存储结构”是一种“非线性结构”。
上篇推文中简单放了张“线性结构”的百度百科词条截图,里面就有谈到“常用的线性结构有:线性表、栈、队列、双队列、串(一维数组)”,这个线性表里面就包含了顺序表与链表。
00
前言
在已经了解过栈与队列特点的基础上,接下来将介绍几道简单的栈与队列的例题,主要还是充分利用栈与队列的特点求解。
另外,本文中使用的 Stack 类与 Queue 类在《Python数据结构——栈与队列》中已经讲过如何实现,这里不再赘述,而且由于这两个数据结构都是使用 Python 列表进行模拟的,所以本文中的例题也可以直接使用列表实现,使用 Stack 与 Queue 仅是为便于描述和方便读者理解。
01
栈与队列例题设计带最小值的栈
请设计一个栈,除 pop 与 push 方法外,还支持 min 方法,可返回栈元素中的最小值。
补充:push、pop、min 三个方法的时间复杂度必须为 O(1)
在原来 Stack 类的实现中,我们将列表末尾作为栈顶,其实就是因为对列表使用 append 和 pop 进行增删的复杂度是 O(1),而现在新加了一个要求,需要添加 min 方法,复杂度也要求 O(1) 就有点麻烦了。
可能有人会想到,Python 不是有个内置函数是 min() 吗,直接调用这个内置函数,返回结果不就行了。然而,内置函数的时间复杂度也是需要考虑进去的,min() 方法应该是遍历一遍,找出最小值,那么复杂度就应当为 O(n)。就像你不能直接调用自带的 sorted() 函数排序,然后认为时间复杂度是 O(1),比所有排序方法都更好一样,内置函数的复杂度也在考虑范围。
这里我们不妨多设置一个栈,这个栈专门用于维护当前栈中的最小值,原栈每次 push 新元素,这个最小值栈的栈顶都和新元素比较一下,如果新元素更小,那么最小值栈中也 push 保存一份,否则将最小值栈的栈顶再次 push。原栈 pop 时最小值栈同样 pop。这样维护的最小值栈和原栈具有相同的 length,而且栈顶始终记录着当前的最小值。
代码实现如下:
class MyStack(object):
def __init__(self, size):
self.stack = Stack(size) # 原栈
self.minStack = Stack(size) # 最小值栈
def push(self, x):
"""将元素 x 压入栈内"""
self.stack.push(x) # 压栈
if self.minStack.isEmpty(): # 当前最小值栈为空,直接压栈
self.minStack.push(x)
else:
current_min = self.minStack.peek() # 取出最小值栈栈顶
self.minStack.push(min(current_min, x)) # 压入二者较小值
def pop(self):
"""将栈顶元素弹出并返回"""
res = self.stack.pop()
self.minStack.pop() # 两个栈同时弹出栈顶元素,保持相同的 length 值
return res
def min(self):
"""返回栈中最小值"""
return self.minStack.peek()
这里的初始化方法里面实例化了之前实现过的 Stack 类,MyStack 类与 Stack 类并不是继承关系。
SetOfStacks
代码实现 SetOfStacks 这个数据结构,SetOfStacks 这个数据结构由多个栈组成,其中每个栈的大小均为 size,当前一个栈填满时,新建一个栈,该数据结构也应当有与普通栈相同的 push、pop 操作。
这一题比较简单,由于 SetOfStacks 是由多个栈组成,所以需要一个变量用于记录当前操作的栈,另外可以从 push 和 pop 两个方法上考虑。
push 前判断栈是否已满。
是:新建一个栈,元素 push 进新栈,将新栈添加到 SetOfStacks
否:push 进入当前操作的栈
pop 前判断栈是否为空。
是:抛弃空栈,从SetOfStacks 中取出上一个已满的栈,当前操作的栈改为该取出的栈,执行 pop
否:直接对当前操作的栈执行 pop
由于涉及到从 SetOfStacks 中取出上一个已满的栈这种操作,典型的 FILO,可以看成 SetOfStacks 是一个更大的栈,内部存储一些小点的栈。
代码如下
class SetOfStacks(object):
def __init__(self, size):
self.stacks = Stack(size) # 假定 SetOfStacks 最多由 size 个栈组成,这里可以修改成其他数值
self.current_stack = Stack(size)
self.stacks.push(self.current_stack) # 当前操作的栈添加到 stacks 中
self.size = size
def push(self, x):
"""将元素 x 压入栈内"""
if self.current_stack.isFull(): # 当前操作的栈已满
self.current_stack = Stack(self.size) # 新建一个栈
self.stacks.push(self.current_stack) # 加入到 stacks 中
self.current_stack.push(x) # 元素压栈
else:
self.current_stack.push(x) # 当前栈未满则直接压栈
def pop(self):
"""将栈顶元素弹出"""
if self.current_stack.isEmpty(): # 当前操作的栈为空
self.stacks.pop() # 抛弃当前操作的栈
self.current_stack = self.stacks.peek() # 改上一个已满的栈为当前操作的栈
return self.current_stack.pop() # 弹出元素
这里没有单独考虑 SetOfStacks 的栈空和栈满异常,直接用了 Stack 类中的方法,非法操作最终也会抛出异常。
使用两个栈实现队列
使用两个栈实现队列的入队 enqueue 和出队 dequeue。
这一题就完全是应用栈与队列的特点求解了,栈是先进后出,队列是先进先出,要用栈实现队列,需要先将元素放入一个栈内,出队时将栈翻转一次即可,翻转就需要用到第二个栈来存储。翻转后栈底变栈顶,正好可以最先出队。如果还有元素要入队,就翻转回去,保持队尾在栈顶。
代码实现如下:
class MyQueue(object):
def __init__(self, size):
self.stack1 = Stack(size) # stack1 栈顶作为队尾,主要用于入队
self.stack2 = Stack(size) # stack2 栈顶作为队首,主要用于出队
def enqueue(self, x):
"""将元素 x 入队"""
if self.stack1.isEmpty(): # stack1 为空,stack2 翻转填入 stack1
self.transform(self.stack2, self.stack1)
self.stack1.push(x) # stack1 栈顶作为队尾,添加元素
def dequeue(self):
"""队首元素出队并返回"""
if self.stack2.isEmpty(): # stack2 为空,stack1 翻转填入 stack2
self.transform(self.stack1, self.stack2)
return self.stack2.pop() # stack2 栈顶作为队首,元素出队
def transform(self, s1, s2):
"""将栈 s1 翻转填入栈 s2 中"""
while not s1.isEmpty():
s2.push(s1.pop())
两个栈的栈顶分别表示队首和队尾,同一时刻只有一个栈中有元素,即保证了同一时刻只能操作队列的入队或者出队,而且能够保证两个栈的元素一定相同。
有效的括号
给定一个只包括“{”“}”“[”“]”“(”“)”的字符串,判断字符串是否有效。
有效的字符串需满足:
左括号必须用相同类型的右括号闭合
左括号必须以正确的顺序闭合
注意空字符串可能认为是有效字符串。
这一题可以在 Leetcode 中找到,在“栈”分类中。
有效的括号可以是“(([[]]))”或者“{()[]}”这样类似的,我们可以尝试遍历这个字符串,如果遇到的是左括号就压入栈内,由于右括号必然要与左括号闭合,所以遇到右括号,就判断栈顶能否匹配成功,匹配成功则弹栈,代表有一对括号完成了匹配,如果匹配失败或者栈为空,则这个括号组成的字符串一定不是有效的括号组合。
另外,如果直到遍历字符串结束都没有问题,也不一定是有效的括号组合,我们还需要看下栈是否为空,如果是有效的括号组合,由于左右括号数目相等,遇到一次右括号就进行弹栈,会导致栈最终为空栈。
Python 可以直接使用列表模拟栈,此处的代码可以直接使用列表实现,使用 Stack 类创建对象仅是为与上述思路描述相统一。
def isValid(string):
stack = Stack(100) # 创建一个栈,此处假设输入数据中左括号不超过100个
dic = {")": "(", "]": "[", "}": "{"} # 创建一个记录对应关系的字典
flag = True
for s in string:
if s in "([{": # 左括号压栈
stack.push(s)
elif stack.isEmpty() or stack.pop() != dic[s]: # 栈为空或栈顶左括号与当前右括号不匹配
flag = False # 修改标记
break
return flag and stack.isEmpty() # 标记为 True 且最终栈要为空才会返回 True
循环内部的 elif 语句处使用了 or 语句,这里如果 stack.isEmpty() 判断为 True 将不会执行后面的内容,这是利用了逻辑语句的短路特性,也能够避免在栈为空情况下 pop 而引发异常。
在最后Last but not least
本文介绍了栈与队列的几道简单例题,主要是栈的例题,题目都挺简单的,感兴趣的读者可以尝试使用代码实现,主要是练习使用代码将自己的思考过程表述出来。