【Leetcode】-代码随想录算法训练营Day10|栈与队列(Java),232.用栈实现队列,225. 用队列实现栈

栈与队列基础

  • 栈:LIFO(last-in-first-out)的数据结构,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。其仅允许在同一端插入/删除。
  • 队列:FIFO(first-in-first-out)的数据结构,先进入的数据会位于队头,最后的数据在队尾,需要读数据的时候从队头开始读取。它只允许在表的前端进行删除操作,而在表的后端进行插入操作。

Java中的栈与队列

Java中常见的栈容器有Stack和Deque,这两者有什么区别和联系呢?

Stack类

首先Stack是一个类,它继承自Vector类,底层是用数组实现的线程安全的栈。具体可见Java的官方文档:
【Leetcode】-代码随想录算法训练营Day10|栈与队列(Java),232.用栈实现队列,225. 用队列实现栈_第1张图片
但是在官方文档中我们可以看到一段话:

A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.
For example:

Deque<Integer> stack = new ArrayDeque<Integer>();

也就是说Java官方也不推荐用Stack进行LIFO stack的操作了,并在此推荐了Deque来替代Stack。
不推荐使用Stack主要是以下两个原因:

  • 因为Stack继承自的Vector,其使用了较多Synchronized来修饰保证线程安全,相应的继承自它的Stack也是如此,所以这种设计不是很灵活,导致Stack在多数情况下性能会不如非线程安全的数据结构。
  • Vector是动态数组,继承自它的Stack也相应地继承了它的大量方法如add,remove这样的方法。

public void add(int index, E element)
Inserts the specified element at the specified position in this Vector. Shifts the element currently at that position (if any) and any subsequent elements to the right (adds one to their indices).

public E remove(int index)
Removes the element at the specified position in this Vector. Shifts any subsequent elements to the left (subtracts one from their indices). Returns the element that was removed from the Vector.

这样的方法显然会破坏栈的结构,导致使用Stack的数据结构可能是个潜在的危险分子。(例如,某天一位新来的同事在代码中偷偷调用了它们)

Deque

Deque是一个接口,继承自Queue,它实际上是双端队列类型,支持FIFO也支持LIFO功能。所以可以作为栈使用,也可以作为队列使用。
【Leetcode】-代码随想录算法训练营Day10|栈与队列(Java),232.用栈实现队列,225. 用队列实现栈_第2张图片
它可以由ArrayDeque或者LinkedList实现。

ArrayDeque

ArrayDeque类是Deque接口的可变数组实现,它没有容量的限制,会根据使用的增长需求扩容。不支持null元素。它是线程不安全的,所以它在作为栈使用时,可能比 Stack 快。因为其底层是数组结构,所以其作为队列使用时比 LinkedList 快。
JavaDoc: 链接

LinkedList

LinkedList是Deque和List接口的双向链表实现,相应地它允许所有链表操作。支持null元素。这个实现是不同步的,所以如果有多线程同时操作情况,需要实现同步,通常用封装LinkedList的对象来实现,或者使用Collections.synchronizedList来包装。因为其底层是链表,所以其在随机访问多的情况下性能可能逊于ArrayDeque,而在频繁增删的情况下,性能可能优于ArrayDeque。
JavaDoc: 链接

总结

总结一下,Java中栈与队列的几种容器和其相应的实现的关系如下图。【Leetcode】-代码随想录算法训练营Day10|栈与队列(Java),232.用栈实现队列,225. 用队列实现栈_第3张图片

Leetcode题目-232. Implement Queue using Stacks

链接: 232. Implement Queue using Stacks

思路

因为栈仅允许在其中一端操作,所以要实现队列,需要维护两个栈分别用于出队和进队。可以想象是两个栈的栈底相连,就是一个一进一出的队列了。当然,实际上的栈内数据不可能直接在两个栈底传输,需要出栈再入栈,而这一过程中,后进入【进队栈】的数据,会先进入【出队栈】,先进入【进队栈】的数据,会后进入【出队栈】,这样我们从【出队栈】取数据的时候,就刚好能去到先进入【进队栈】的数据,符合队列FIFO的规则。当出队栈没有数据的时候,就再从入队栈获取数据即可。如果两个栈都没有数据了,说明当前队列也是空的。

代码实现

class MyQueue {
    Deque<Integer> stackIn;
    Deque<Integer> stackOut;

    public MyQueue() {
        stackIn = new LinkedList<>();
        stackOut = new LinkedList<>();
    }

    public void push(int x) {
        stackIn.push(x);
    }

    public int pop() {
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.poll());
            }
        }
        return stackOut.poll();
    }

    public int peek() {
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.poll());
            }
        }

        // 这里的逻辑和上述pop比较类似,也可以复用上面的pop函数
        // 并将pop出去的元素再添加回去
        // int x = pop();
        // stackIn.push(x);
        // return x;
        return stackOut.peek();

    }

    public boolean empty() {
        return stackOut.isEmpty() && stackIn.isEmpty();
    }
}

总结

这道题在书写peak和pop函数时,会发现两者的代码有重复之处。这一块可以进行代码的整合,避免重复书写相同的代码。这一点在工程中也非常重要,我刚开始开发的时候就一直被leader说要做重复代码的整合和抽象,减少代码的冗余度,提升代码的可读性。

Leetcode题目-225. Implement Stack using Queues

链接: 225. Implement Stack using Queues

思路

因为队列是一个一端读操作一端写操作的线性表,它可以从两端进行操作,其先进入的元素可以出队再入队从而调整到队尾,要实现LIFO的栈,只需要在入栈的时候将队列中最后一个数据之前的数据都出队入队后调整到队尾就可以保证出栈最后一个数据。新入队的数据顺序也不变。

代码实现

class MyStack {
    Queue<Integer> queue;

    public MyStack() {
        queue = new LinkedList<>();
    }

    public void push(int x) {
        // offer和add的区别:
        // offer若插入失败返回false,add插入失败时会抛出异常
        // 在处理限定容量的Queue时,使用offer会更好
        queue.offer(x);
        for (int i = 0; i < queue.size() - 1; i++) {
            // poll和remove的区别:
            // 当队列为空的时候,poll返回null,而remove会抛出异常
            queue.offer(queue.poll());
        }
    }

    public int pop() {
        return queue.poll();
    }

    public int top() {
        return queue.peek();
    }

    public boolean empty() {
        return queue.isEmpty();
    }
}

总结

这两道题都不难,主要是理解好栈和队列的特性和操作。另外在队列和stack的实现ArrayDeque和LinkedList中,关于pop,poll,remove这几个方法,主要的区别就是栈/队列为空时是否抛出异常;而push,add,offer这几个方法主要的区别是栈/队列为限制长度时,如果超出长度限制是否抛出异常。

你可能感兴趣的:(Leetcode-代码随想录,java,算法,leetcode,数据结构)