Java中常见的栈容器有Stack和Deque,这两者有什么区别和联系呢?
首先Stack是一个类,它继承自Vector类,底层是用数组实现的线程安全的栈。具体可见Java的官方文档:
但是在官方文档中我们可以看到一段话:
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主要是以下两个原因:
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是一个接口,继承自Queue,它实际上是双端队列类型,支持FIFO也支持LIFO功能。所以可以作为栈使用,也可以作为队列使用。
它可以由ArrayDeque或者LinkedList实现。
ArrayDeque类是Deque接口的可变数组实现,它没有容量的限制,会根据使用的增长需求扩容。不支持null元素。它是线程不安全的,所以它在作为栈使用时,可能比 Stack 快。因为其底层是数组结构,所以其作为队列使用时比 LinkedList 快。
JavaDoc: 链接
LinkedList是Deque和List接口的双向链表实现,相应地它允许所有链表操作。支持null元素。这个实现是不同步的,所以如果有多线程同时操作情况,需要实现同步,通常用封装LinkedList的对象来实现,或者使用Collections.synchronizedList来包装。因为其底层是链表,所以其在随机访问多的情况下性能可能逊于ArrayDeque,而在频繁增删的情况下,性能可能优于ArrayDeque。
JavaDoc: 链接
总结一下,Java中栈与队列的几种容器和其相应的实现的关系如下图。
链接: 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说要做重复代码的整合和抽象,减少代码的冗余度,提升代码的可读性。
链接: 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这几个方法主要的区别是栈/队列为限制长度时,如果超出长度限制是否抛出异常。