1. 前言

栈和队列相关的题目是校招中出现频率一般,但是是属于相对基础的题型。我们要关注两类问题,栈和队列的添加和删除操作,以及栈和队列之间的区别和联系。

2. 栈和队列

2.1 数据结构

首先我们给出栈和队列的数据结构定义:

(1)栈(Stack):允许在某一端插入元素(即push操作),以及删除元素(即pop操作)的数据结构,必须满足后进先出(LIFO:Last In First Out)的运算规则。
一个典型的栈数据结构如下图所示:

图片描述

栈结构

(2)队列(Queue):允许在某一端插入元素(即enqueue操作),以及在另一端删除元素(即dequeue操作)的数据结构,必须满足先进先出(FIFO:First In First Out)的运算规则。
一个典型的队列数据结构如下图所示:

图片描述

队列结构

2.2 用两个栈实现一个队列

面试官提问:能否用两个栈实现一个队列的数据结构?

题目解析

反转链表是来源于算法网站LeetCode的经典题目,题目链接:https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/。

本题是一道非常经典的面试题,我们不能直接使用语言自带的Queue实现类,需要手动用两个栈来模拟队列的操作。

在解题之前,我们需要对栈和队列有个前置理解,栈支持入栈和出栈操作,队列支持入队和出队操作。并且从题目看,队列还需要实现peek函数(返回队列的头部元素)和empty函数(判断队列是否为空)。举例来说,我们假设实现的队列类名是MockQueue,基础的操作案例示例:

MockQueue queue = new MockQueue();
queue.push(100);  //将100放入队列
queue.push(200);  //将200放入队列
queue.empty();    //判断队列是否为空,return false
queue.pop();      //弹出队列的一个元素
queue.peek();     //返回队列的头部元素,return 100

我们用 stack1、stack2 来表示两个栈,还是以小规模数据作为案例开始分析,对于一个数组[1,2,3,4],我们将其存放到第一个栈stack1中,那么出栈的顺序是[4,3,2,1],明显不满足队列的要求。入栈和出栈的操作是将数组逆序排列了一遍,从几何的角度可以看成旋转了180度,如果连续两次旋转180度,那么数组就会回到原点,分析到这里解法就呼之欲出了。

从分工上看,stack1 负责 push,stack2 负责 pop。

(1)入队的操作:我们直接把值放到 stack1;
(2)出队的操作:首先判断 stack2 是否为空,如果为空就把 stack1 中的元素全部压入 stack2,最后弹出 stack2 的栈顶元素;
(3)返回队列头部元素的操作:peek 操作和 pop 操作类似,区别在于不会弹出 stack2 的栈顶元素;
(4)判断队列是否为空的操作:直接判断 stack1 和 stack2 是否同时为空就行。

下面给出Java源码的实现,示例:

class MockQueue {
    //首先我们需要定义两个Stack数据结构
    Stack<Integer> s1;
    Stack<Integer> s2;

    public MockQueue() {
        // 初始化对象
        s1 = new Stack<>();
        s2 = new Stack<>();
    }
    
    public void push(int x) {
        //入队操作
        s1.push(x);
    }
    
    public int pop() {
        //出队操作
        if(s2.isEmpty()){
            while(!s1.isEmpty()){
                s2.push(s1.pop());
            }
        }
        return s2.pop();
    }
    
    public int peek() {
        //从队列头部获取元素
        if(s2.isEmpty()){
            while(!s1.isEmpty()){
                s2.push(s1.pop());
            }
        }
        return s2.peek();
    }
    
    public boolean empty() {
        //判断队列是否为空
        return s1.isEmpty() && s2.isEmpty();
    }
}

3. 小结

本章节我们介绍了最基础的数据结构,栈和队列,其中栈在动态规划以及二叉树问题中也经常出现。在使用栈和队列解决算法问题时,需要特别注意容器为空的情况,防止抛出异常。这类边界问题的判断,在其他算法问题中也是注意点,例如二叉树首先要判断 root 是否为空,防止空指针访问异常。