一、队列知识结构及面试题目分析
JAVA 中的队列也是比较常见的面试题,一般队列的面试题可以分为三类:
- 队列及类似结构的基础类题目,通常围绕着队列的数据结构特性展开,有时候会以编程题的形式考察;
- 队列的细分题目,考察阻塞队列、非阻塞队列、延迟队列、同步队列等不同队列的特性及应用场景;
- 队列和线程池、消息总线等结合着考察。
总体来讲,队列属于基础知识中的高阶部分,如果不是候选人简历中特意提及,通常不会考得太深入。但正因为如此,队列类题目具有一定的区分度和灵活性, 比如说第 1 类题目,还可考察候选人对数据结构的理解;第 3 类题目考察候选人对队列的实践情况,而不仅仅是死记硬背。
二、典型面试例题及思路分析
问题 1:" 栈(Stack)和队列(Queue)的相同点和不同点是什么?如何用两个栈实现队列(入队和出队)?”
- 相同点
- 栈和队列都是属于线性表;
- 栈和队列插入操作都是限定在线性表的头尾进行;
- 栈和队列插入与删除的时间复杂度都是 O (1);
- 不同点:
- 特性不同,栈后进先出(LIFO,Last In First Out),队列先进先出(FIFO,First In First Out);
- 栈只在表的一端进行插入和删除操作,队列只在表的一端进行插入操作,在表的另一端进行删除操作;
- JAVA 中的栈 (Stack) 继承自 Vector,再往上的接口是 List/Collection;而队列(Queue) 直接继承的是 Collection 接口。
用两个栈来实现队列:
/**
* 两个栈组成队列
*/
public class StackQueue<E> {
private Stack<E> stack1 =new Stack<>();//入队操作的栈
private Stack<E> stack2 =new Stack<>();//出队操作的栈
/**
* 压入队列元素,只使用stack1
* @param element
*/
public void push(E element) {
stack1.add(element);
}
/**
* 取出队列顶部元素
* @return
*/
public E poll() {
// stack2的数据为空时,才把stack1中的元素压入stack2(两种情况:1、初始化时两个栈的数据均为空;2、stack2数据出栈出完了
if (stack2.isEmpty()) {
while (stack1.size() > 0) {
stack2.add(stack1.pop());
}
}
//stack1的数据出栈完成后,stack2仍然为空,说明两个栈的数据都为空
if (stack2.isEmpty()) {
throw new RuntimeException("queue is Empty!");
}
E head = stack2.pop();
return head;
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
StackQueue<String> stackQueue = new StackQueue();
stackQueue.push("first");
stackQueue.push("second");
System.out.println("------first time poll in StackQueue---------");
System.out.println(stackQueue.poll());
System.out.println("------second time poll in StackQueue---------");
System.out.println(stackQueue.poll());
System.out.println("------third time poll in StackQueue---------");
stackQueue.push("third");
System.out.println(stackQueue.poll());
}
}
点评:
栈和队列都属于线性表(linear list),而线性表是 n 个具有相同特性的数据元素的有限序列:
- 栈,只能在表尾插入或删除的线性表。对栈来说,表尾称为栈顶、表头称为栈底。因此,最先入栈的元素最后被删除,最晚入栈的元素最先被删除,所以栈又称为后进先出的线性表;
- 队列,只能在表的一端进行插入(队尾),在表的另一端删除元素(队头),所以又称先进先出的线性表;
本质上,是各自不同的数据结构决定了栈和队列的特性和应用场景,比如说栈经常用于括号匹配检验、行编辑程序、表达式求值、递归实现等等,而队列常用于作业任务排队等。
再来看两个栈实现队列的过程(见下图):
stack1 用于存储元素,stack2 用于弹出元素。简单地说,就是把数据先压入 stack1,然后再从 stack1 中取出压入 stack2(后进先出),取数的时候直接从 stack2 出(后进先出),经过两次后进先出就符合队列先进先出的特性了。
这是一道比较典型的代码题,即需要手写代码或者伪代码的题目,类似的题目还包括:
- 用数组实现队列;
- 用队列实现栈;
- 用 Queue 实现生产者 / 消费者场景;
- 实现一个 LRU(Least Recently Used);
…
问题 2:"ArrayBlockingQueue 和 LinkedBlockingQueue 的区别是什么?"
ArrayBlockingQueue 和 LinkedBlockingQueue 都是阻塞队列(BlockingQueue),区别主要是:
- 内部存储结构不同,ArrayBlockingQueue 采用的是数组存储,而 LinkedBlockingQueue 采用的是 Node 节点;
- ArrayBlockingQueue 初始化时必须指定容量值,LinkedBlockingQueue 可以不用指定(默认容量为 Integer.MAX_VALUE);
点评:
这个题目虽然看起来比较简单,但它很有代表性,实际考察的是对 JDK 中 Queue 实现的熟悉程度,类似的题目,还包括:
- 常用的阻塞队列有哪些?
- 无锁队列 / 延迟队列的原理是什么?
- 有界队列和无界队列的差异是什么?
…
加分项:
这类题目回答时注意和工程实践相结合。比如说,在上述参考答案的后面可以加上 “因为 ArrayBlockingQueue 的有界的性质,对系统负载会有一定的阈值控制,所以在我们之前的 xxx 项目的 xxx 场景中使用的是 ArrayBlockingQueue"
Queue 及其常用子类可以用下面的类图来分为三类:
- 双端队列(Deque):头部和尾部都支持元素插入和获取。
- 阻塞队列(BlockingQueue):在元素的添加 / 删除操作时,如果没有成功,会阻塞等待执行。例如,当添加元素时,如果队列元素已满,队列会阻塞等待直到有空位时再插入。
- 非阻塞队列(Non BlockingQueue):在元素的添加 / 删除操作时,如果没有成功,会直接返回操作的结果(通常双端队列也属于非阻塞队列)
几种常见的 Queue 的特性:
- PriorityBlockingQueue:带优先级的无界阻塞队列,元素按优先级顺序被移除,而不是先进先出队列(FIFO)。此外,队列中的元素要具有比较能力(用于判定优先级)。PriorityBlockingQueue 不允许 null 元素;
- DelayQueue:存放 Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,DelayQueue 不允许使用 null 元素。
- SynchronousQueue:同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然。它的一个典型应用场景是 Executors.newCachedThreadPool () ,在新任务到来时创建新的线程,如果有空闲线程则会重复使用,SynchronousQueue 不允许 null 元素。
三、总结
本章节主要讲述了队列及其类似结构(比如说栈)的数据特性,并对常见的队列的进行了介绍。这是队列题目的常见套路,即询问某一类队列的特点,回答这类题目除了对各种队列的特性了解外,还可以结合自己的项目就队列在各业务场景下的最佳实践进行展开说明。而另一个比较常见的题目则是各种数据结构的转换,比如说文中的两个栈构成一个队列、或者两个队列构成一个栈等,这是面试中比较常见的一类编程题,在面试前可以常加练习以达成熟练掌握的程度。
四、扩展阅读及思考题
- 链接: 无锁队列的总结.
- 链接: 线性表 及 Java 实现 顺序表、链表、栈、队列.
问:LinkedHashMap和PriorityQueue的区别?
PriorityQueue 是一个优先级队列,保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。
问:BlockingQueue是什么?
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。
BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。
Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
问:队列和栈是什么,它们的区别是什么?
栈又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
问:BlockingQueue有哪些实现类?ArrayBlockingQueue: 由数组组成的有界阻塞队列?
LinkedBlockingQueue: 由链表组成的有界阻塞队列(默认大小为 Integer.MAX_VALUE)
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue: 不存储元素的阻塞队列,单个元素的队列,同步提交队列
LinkedTransferQueue:链表组成的无界阻塞队列
LinkedBlockingDeque:链表组成的双向阻塞队列
问:Queue中add/offer方法的区别?
add()和offer()都是向队列中添加一个元素。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,调用 add() 方法就会抛出IllegalStateException异常,而调用 offer() 方法会返回 false。
问:Queue中remove()/poll()方法的区别?
poll()/remove()方法都是从队列中删除第一个元素。如果队列元素为空,调用remove() 则会抛出NoSuchElementException,而poll() 方法在用空集合调用时只是返回 null。
问:Queue中element()/peek()方法的区别?
element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出NoSuchElementException异常,而 peek() 返回 null。