-1

勘误


在本文内容正式开始之前,先对上一篇推文《Python数据结构——栈与队列》的一个表述不当之处进行校正。

不当之处原文如下。

python判断不为noll Python判断不为空_python 判断字符串为空

此处前后语句内容完全衔接不上,“链式存储结构”是一种存储结构,“线性结构”是数据的一种逻辑结构,二者之间不应当使用“而”字作为转折,容易误导读者认为“存储结构”是一种“非线性结构”。

上篇推文中简单放了张“线性结构”的百度百科词条截图,里面就有谈到“常用的线性结构有:线性表、栈、队列、双队列、串(一维数组)”,这个线性表里面就包含了顺序表与链表。

python判断不为noll Python判断不为空_python判断不为noll_02

00

前言


在已经了解过栈与队列特点的基础上,接下来将介绍几道简单的栈与队列的例题,主要还是充分利用栈与队列的特点求解。

另外,本文中使用的 Stack 类与 Queue 类在《Python数据结构——栈与队列》中已经讲过如何实现,这里不再赘述,而且由于这两个数据结构都是使用 Python 列表进行模拟的,所以本文中的例题也可以直接使用列表实现,使用 Stack 与 Queue 仅是为便于描述和方便读者理解。

01

栈与队列例题设计带最小值的栈


请设计一个栈,除 pop 与 push 方法外,还支持 min 方法,可返回栈元素中的最小值。

补充:push、pop、min 三个方法的时间复杂度必须为 O(1)



python判断不为noll Python判断不为空_python判断不为noll_03

在原来 Stack 类的实现中,我们将列表末尾作为栈顶,其实就是因为对列表使用 append 和 pop 进行增删的复杂度是 O(1),而现在新加了一个要求,需要添加 min 方法,复杂度也要求 O(1) 就有点麻烦了。

可能有人会想到,Python 不是有个内置函数是 min() 吗,直接调用这个内置函数,返回结果不就行了。然而,内置函数的时间复杂度也是需要考虑进去的,min() 方法应该是遍历一遍,找出最小值,那么复杂度就应当为 O(n)。就像你不能直接调用自带的 sorted() 函数排序,然后认为时间复杂度是 O(1),比所有排序方法都更好一样,内置函数的复杂度也在考虑范围。

这里我们不妨多设置一个栈,这个栈专门用于维护当前栈中的最小值,原栈每次 push 新元素,这个最小值栈的栈顶都和新元素比较一下,如果新元素更小,那么最小值栈中也 push 保存一份,否则将最小值栈的栈顶再次 push。原栈 pop 时最小值栈同样 pop。这样维护的最小值栈和原栈具有相同的 length,而且栈顶始终记录着当前的最小值。

python判断不为noll Python判断不为空_python 判断字符串为空_04

代码实现如下:

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 操作。



python判断不为noll Python判断不为空_python判断不为noll_03

这一题比较简单,由于 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。



python判断不为noll Python判断不为空_python判断不为noll_03

这一题就完全是应用栈与队列的特点求解了,栈是先进后出,队列是先进先出,要用栈实现队列,需要先将元素放入一个栈内,出队时将栈翻转一次即可,翻转就需要用到第二个栈来存储。翻转后栈底变栈顶,正好可以最先出队。如果还有元素要入队,就翻转回去,保持队尾在栈顶。

代码实现如下:

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())

两个栈的栈顶分别表示队首和队尾,同一时刻只有一个栈中有元素,即保证了同一时刻只能操作队列的入队或者出队,而且能够保证两个栈的元素一定相同。

有效的括号


给定一个只包括“{”“}”“[”“]”“(”“)”的字符串,判断字符串是否有效。

有效的字符串需满足:

左括号必须用相同类型的右括号闭合

左括号必须以正确的顺序闭合

注意空字符串可能认为是有效字符串。



python判断不为noll Python判断不为空_python判断不为noll_03

这一题可以在 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

本文介绍了栈与队列的几道简单例题,主要是栈的例题,题目都挺简单的,感兴趣的读者可以尝试使用代码实现,主要是练习使用代码将自己的思考过程表述出来。