剑指 Offer 09. 用两个栈实现队列 (Java实现)

128 阅读2分钟

剑指 Offer 09. 用两个栈实现队列

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

来源:力扣(LeetCode) 链接:leetcode.cn/problems/yo… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路

这道题的需求就是要使用双栈结构来实现一个队列的增删操作。

  • 的特点是:FILO,先进后出,即只能在栈的一端操作数据。

  • 队列的特点是:FIFO,先进先出,即在队列头部删除,队列尾部插入。

  • 对于插入操作,栈和队列是一致的,只需要直接插入即可,数据都会存在尾部(栈顶)。

  • 但是队列的头部删除操作,相当于要删除栈底的数据,一个栈结构是无法完成的。想要删除栈底的数据,那么就需要把栈的所有数据按顺序取出来,删除栈底的数据后,再把所有数据按原来的顺序放回去。

初步实现

逻辑

这里我使用一个栈stack来存储数据,添加到队列尾直接push进栈即可,删除队列头数据,我用另一个栈tempStack把数据从stack取出来,pop掉栈顶元素就完成了队列头元素的删除。

代码

class CQueue {
    //栈
    Stack<Integer> stack; 

    public CQueue() {
		stack = new Stack<>(); 
    }
    
    public void appendTail(int value) {
        //直接进栈
        stack.push(value);
    }
    
    public int deleteHead() {
        //如果队列为空,直接返回-1
        if (stack.empty()) return -1;
        //思路:用另一个stack把队列头元素取出来删掉,再装回去
        //暂时储存元素的栈
        Stack<Integer> tempStack = new Stack<>();
        //当栈不为空时
        while(!stack.empty()){
            //把stack栈的元素移到temp stack栈中
            tempStack.push(stack.pop());
        }
        //删掉表头元素
        int res = tempStack.pop();
        //再装回去
        while(!tempStack.empty()){
            stack.push(tempStack.pop());
        }
        //返回删掉的值
        return res;
    }
}

复杂度分析

  • appendTail方法:直接插入即可,复杂度应该为O(1)
  • deleteHead方法:需要先移动n个元素到另一个栈,再删除栈顶元素,复杂度为O(n)

性能分析

image-20220716154146742.png

实际性能表现很慢,主要是因为Java自带的Stack是Vector的子实现,Vector是线程安全的,所以很慢,后续优化可以换成LinkedList类来实现栈结构,这也是Java官方推荐的Stack实现方式。

此外,代码逻辑还有优化空间。

优化实现

优化有两方面,一是把Stack使用Deque来实现,二是对具体的代码逻辑进行优化。

逻辑优化分析

  1. 在先前的代码中,我的代码实现逻辑没有考虑到多次频繁存取,实际上可以把删除栈声明在类中,省去了频繁创建栈对象的步骤。

  2. 此外,并不需要每次删除都要来回装取元素,可以用两个栈共同存储元素,一个栈负责队列头,一个栈负责队列尾,把两个栈的整体看出一个队列。

image-20220716161255039.png

    • 添加操作:元素直接压入队尾栈即可。
    • 删除操作:
      • 第一次删除时:把队尾栈的元素移动到队头栈中,队头栈直接出栈即完成了删除操作。
      • 不是第一次删除:就需要判断队头栈中是否还有元素?如果有,直接出栈完成删除,如果没有,就再去移动队尾栈中的元素到队头栈中来,如果队尾栈也没有元素,就说明整个队列都是空的,返回-1即可。

代码实现

class CQueue {
    //队头栈、队尾栈分别表示队列的头部和尾部
    Deque<Integer> stackHead;
    Deque<Integer> stackTail;

    public CQueue() {
        //初始化
        stackHead = new LinkedList<>();
        stackTail = new LinkedList<>();
    }
    
    public void appendTail(int value) {
        //添加操作直接加到队尾上
        stackTail.push(value);
    }
    
    public int deleteHead() {
        //先判断队头栈中是否有元素
        if(!stackHead.isEmpty()){
            //如果有,直接返回栈顶元素完成删除操作
            return stackHead.pop();
        }
        //如果队头栈为空,那么就要判断队尾栈中是否有元素
        if(stackTail.isEmpty()){
            //如果队尾栈也为空,说明整个队列都为空,返回-1
            return -1;
        }
        //如果队尾栈不为空,那么就把队尾栈中的元素移动到队头栈中去
        while(!stackTail.isEmpty()){
            stackHead.push(stackTail.pop());
        }
        //再返回队头栈的栈顶元素,完成删除操作
        return stackHead.pop();
    }
}

复杂度分析

  • 添加操作:O(1)
  • 删除操作:O(1)或者O(n)

性能分析

image-20220716162919614.png

可以看出,远远优于第一次实现。

总结

  • 如果要使用Stack结构,如非有特别的线程安全需求,优先使用LinkedList类作为栈的实现,不要使用Vector下的Stack类。
  • 第一次实现性能低下的原因主要是自己没有考虑清楚底层数据结构的元素流向过程,要多考虑、多模拟,思路清晰了在开始写代码。