题目难度: 简单

原题链接

今天继续更新剑指 offer 系列, 这道题依然很经典, 考察了两个基础数据结构和它们的特性. 题目思考部分加了一些进阶问题, 感兴趣的小伙伴可以先想想看, 最后有答案哦

若无意外, 每天晚上 6 点 45 分准时更新, 中间可能会穿插一些周赛题解. 大家在我的公众号"每日精选算法题"中的聊天框中回复 offer 就能看到该系列当前已经更新的文章了

大家有什么想法建议和反馈的话欢迎随时交流, 包括但不限于公众号聊天框/知乎私信评论等等~

题目描述

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTaildeleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

  • 1 <= values <= 10000
  • 最多会对 appendTaildeleteHead 进行 10000 次调用

题目样例

示例 1

输入

  • ["CQueue","appendTail","deleteHead","deleteHead"]
  • [[],[3],[],[]]

输出

[null,null,3,-1]

示例 2

输入

  • ["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
  • [[],[],[5],[2],[],[]]

输出

[null,-1,null,null,5,2]

题目思考

(思考题答案在最后)

  1. 如何用到两个栈?
  2. 可以做到插入和删除都只需要常数时间吗?
  3. 假如题目变为使用两个队列来实现栈, 又该怎么做? (队列只能尾部插入或者头部删除)
  4. 可以使用一个队列来实现栈吗?

解决方案

思路

分析

  • 栈的特性是后进先出, 队列的特性是先进先出
  • 队列尾部插入很简单, 由于队列尾对应就是最新的元素, 尾部插入就直接压入栈中即可
  • 而队列头部删除相对麻烦, 因为单就一个栈而言, 此时队列头是在栈的最下面
  • 因此我们需要第二个栈, 逆序存放第一个栈中比较老的数据, 这样其栈顶就对应队列头了
  • 每次遇到需要删除队列头时
    • 如果第二个栈中还有数字, 就把其栈顶弹出即可
    • 否则就把第一个栈的所有数字都逆序导入第二个栈中, 然后再弹出第二个栈的栈顶
    • 如果两个栈都没有数字, 就返回-1

实现

  • 定义两个栈 stackIn 和 stackOut: 前者对应上面分析的第一个栈, 只用于尾部插入; 后者对应第二个栈, 只用于头部删除
  • 尾部插入
    • 无脑压入新数字到 stackIn
  • 头部删除
    • 如果 stackOut 不是空, 弹出栈顶
    • 如果 stackOut 是空, 分为两种情况:
      • 如果 stackIn 也是空, 代表队列为空, 返回-1
      • 否则就将 stackIn 的数字倒入 stackOut 中, 再弹出栈顶

复杂度

  • 时间复杂度
    • 尾部插入, 时间复杂度 O(1)
    • 头部删除, 均摊复杂度也是 O(1). 因为第一次删除导入一批后, 之后该批的删除都只需要常数时间的弹出, 分摊下来每个数字的删除都只需要 O(1). 当然如果只删除一次的话, 那还是需要把之前所以插入的都倒过来, 这样就是 O(N) 了..
  • 空间复杂度 O(N)
    • 使用了两个栈

代码

class CQueue:

    def __init__(self):
        # 存较新的尾部插入数字
        self.stackIn = []
        # 存较老的逆序数字
        self.stackOut = []

    def appendTail(self, value: int) -> None:
        # 直接压入stackIn
        self.stackIn.append(value)

    def deleteHead(self) -> int:
        if self.stackOut:
            # stackOut还有数字, 直接pop
            return self.stackOut.pop()
        if not self.stackIn:
            # stackIn也没有数字, 队列为空
            return -1
        while self.stackIn:
            # 将stackIn的数字倒序导入stackOut中
            self.stackOut.append(self.stackIn.pop())

        # 弹出stackOut
        return self.stackOut.pop()

思考题答案

  1. 如何用到两个栈?
    1. 参考分析部分
  2. 可以做到插入和删除都只需要常数时间吗?
    1. 参考复杂度部分
  3. 假如题目变为使用两个队列来实现栈, 又该怎么做? (队列只能尾部插入或者头部删除)
    1. 定义两个队列, 保证任何时刻都只有一个队列中有数字
    2. 栈压入操作, 就是将当前有数字的队列(如果两个队列都没有, 就随便选一个)加入新数字到队尾
    3. 栈弹出操作, 需要将有数字的队列的除了最后一个数字都放到另一个队列中, 然后删除并返回最后那个数字, 与此同时另一个队列就成新的有数字队列了
    4. 压入 O(1), 弹出 O(N) (由于队列无法倒置导出, 所以无法做到一部分弹出只使用常数时间, 而是每次都要导出除了最后一个数字到另一个队列, 均摊复杂度无法达到 O(1))
  4. 可以使用一个队列来实现栈吗?
    1. 考虑队列和栈的顺序差异, 我们如果能保证每次队列头都对应栈顶元素的话, 就可以 O(1) 弹出了
    2. 所以思路就是每次压入新数字, 将新数字放在队尾, 然后将队列前面的数字从头开始依次插入队尾即可, 这样新的队列头就是刚刚压入的数字, 即栈顶元素. 这个过程等效于将新数字前面的所有数字原封不动按顺序搬到新数字后面, 假设当前队列是[1,2,3] (根据翻转操作, 该队列真实的插入时序是 3=>2=>1), 新数字是 4, 那么刚进队的时候是[1,2,3,4], 搬过去后就是[4,1,2,3], 队列头就是栈顶了 (加上数字后的插入时序是3=>2=>1=>4, 栈顶到栈底依次是4=>1=>2=>3, 正是对应现在队列的顺序)
    3. 显然压入 O(N), 弹出 O(1)

大家可以在下面这些地方找到我~😊

我的知乎专栏

我的 CSDN

我的 Leetcode

我的牛客网博客

我的公众号: 每日精选算法题, 欢迎大家扫码关注~😊

每日精选算法题 - 微信扫一扫关注我