【数据结构】队列与实现分析
概念
(百度百科)队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
从概念上看,跟栈多少有几分相似之处,同样是特殊的线性代表,同样只能在两端操作,唯一的区别除了名字意外,就是线性结构头和尾操作的那点区别了。栈是一种先进后出的结构,那么队列就是一种先进先出的机构。队列在现实中还算是比较常见的,例如排队就是一种队列结构的“实现”,谁排第一位(先进),谁就先获得优先权(先出),当然,插队是这种不文明现象就不值得一提了。在栈的学习场景中我知道栈分别有循序栈和链表栈,主要的区别在于栈使用的实现结构数组和链表。同理,队列同样可以基于这两种基础结构分别实现静态队列和链表队列,此外还有一个叫循环队列的类别,说白了,跟循环链表的概念差不多,通过首尾相连达到一个循环状的结构。
在学习队列的特性过程中,结合现实的一些场景就可以很好理解了。接下莱看看队列结构的一些主要操作有哪些:
InitQueue() ——初始化队列
EnQueue() ——进队列
DeQueue() ——出队列
IsQueueEmpty() ——判断队列是否为空
IsQueueFull() ——判断队列是否已满
以上为队列结构的一些主要操作,再来结合Java语言中ArrayBlockingQueue类对这些操作的实现吧:
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable{
……
}
public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> {
……
}
public interface BlockingQueue<E> extends Queue<E> {
……
}
public interface Queue<E> extends Collection<E> {
……
}
从以上ArrayBlockingQueue类的名称和集成关系来看,ArrayBlockingQueue是一个基于数组实现的阻塞队列。再来看看各队列方法的实现:
1 public ArrayBlockingQueue(int capacity, boolean fair) {
2 if (capacity <= 0)
3 throw new IllegalArgumentException();
4 this.items = (E[]) new Object[capacity];
5 lock = new ReentrantLock(fair);
6 notEmpty = lock.newCondition();
7 notFull = lock.newCondition();
8 }
从ArrayBlockingQueue构造方法可以看出,它定义了一个Object数组和相关锁的实例,这些锁是确保ArrayBlockingQueue的线性安全的。
1 public boolean offer(E e) {
2 if (e == null) throw new NullPointerException();
3 final ReentrantLock lock = this.lock;
4 lock.lock();
5 try {
6 if (count == items.length)
7 return false;
8 else {
9 insert(e);
10 return true;
11 }
12 } finally {
13 lock.unlock();
14 }
15 }
以上为ArrayBlockingQueue的插入队列尾部元素的方法,它是通过Lock锁的机制实现线性安全的。
1 public E poll() {
2 final ReentrantLock lock = this.lock;
3 lock.lock();
4 try {
5 if (count == 0)
6 return null;
7 E x = extract();
8 return x;
9 } finally {
10 lock.unlock();
11 }
12 }
以上为ArrayBlockingQueue获取队列头部元素的方法,同样是通过Lock机制实现线性安全。接下来看看offer中的insert(e)和poll()中extract()方法:
1 private void insert(E x) {
2 items[putIndex] = x;
3 putIndex = inc(putIndex);
4 ++count;
5 notEmpty.signal();
6 }
7 private E extract() {
8 final E[] items = this.items;
9 E x = items[takeIndex];
10 items[takeIndex] = null;
11 takeIndex = inc(takeIndex);
12 --count;
13 notFull.signal();
14 return x;
15 }
从上面两个队列操作方法可以看到,插入是依赖于putIndex数组标签,而取出是依赖于takeIndex数组标签,每插入或取出元素都会对各自标签进行inc()自增,再来看看inc()方法:
1 final int inc(int i) {
2 return (++i == items.length)? 0 : i;
3 }
显而易见,这是一个循环数组队列,当数组索引达到数组长度最大值,又重新回到数组0位的开始重新循环存放元素,前提队列没有被填满。