目录:
1. 队列(Queue)
与栈类似,队列也是最基本的数据结构之一。顾名思义,队列中数据的存储与排队相类似,遵循“先进先出”(First-In-First-Out, FIFO)的原则(都是文明人,插队肯定是不允许的,手动狗头)。
2. 队列的抽象数据类型(队列ADT)
方法 | 功能描述 |
---|---|
enqueue(x) | 若队列未满,则将元素x加到队列末尾,否则报错 |
dequeue() | 若队列非空,则将队列首元素移除,并将其返回,否则报错 |
getSize() | 返回当前队列中元素的数目 |
isEmpty() | 用于检测队列是否为空 |
front() | 若队列非空,则返回队列首元素(但并不移除),否则报错 |
3. 队列接口
/**
* 队列接口
*/
public interface Queue {
void enqueue(Object obj);
Object dequeue() throws ExceptionQueueEmpty;
int getSize();
boolean isEmpty();
Object front() throws ExceptionQueueEmpty;
}
4. 利用数组实现队列
4.1 队列的实现
借助一个定长数组Q来存放对象,即可简单地实现队列。一种自然的办法就是仿照栈的实现,以 Q[0]作为队首,其它对象顺序往后存放。然而如此一来, 每次首元素出队之后,都需要将后续的所有元素向前顺移一个单元⎯⎯若队长为 n,这项工作需要O(n)时间,因此效率很低。
为了避免数组的整体移动,可以引入两个变量 front 和 rear,分别指向队列的头和尾,使数组构成一个循环结构。
public class ExceptionQueueEmpty extends RuntimeException {
public ExceptionQueueEmpty(String error) {
super(error);
}
}
public class ExceptionQueueFull extends RuntimeException {
public ExceptionQueueFull(String error) {
super(error);
}
}
/**
* 定长数组实现队列
*/
public class ArrayQueue implements Queue {
private static final int CAPACITY = 1024;
private int capacity;
private Object[] Q;
private int front;
private int rear;
public ArrayQueue() {
this(CAPACITY);
}
public ArrayQueue(int capacity) {
this.capacity = capacity;
Q = new Object[this.capacity];
front = 0;
rear = 0;
}
@Override
public void enqueue(Object obj) throws ExceptionQueueFull {
// 数组只剩下一个空闲单元时就判定队列已满(如数组满则front与rear相等),(front == rear) is true用于判定队列空
if ((capacity - 1) == getSize()) {
// 对于容量有限的队列需要考虑队列溢出的情况
throw new ExceptionQueueFull("意外:队列溢出");
}
Q[rear] = obj;
// 利用取余数实现数组的循环
rear = (rear + 1) % capacity;
}
@Override
public Object dequeue() throws ExceptionQueueEmpty {
if (isEmpty()) {
throw new ExceptionQueueEmpty("意外:队列空");
}
Object returnElement = Q[front];
Q[front] = null;
front = (front + 1) % capacity;
return returnElement;
}
@Override
public int getSize() {
return ((capacity + rear - front) % capacity);
}
@Override
public boolean isEmpty() {
return (front == rear);
}
@Override
public Object front() throws ExceptionQueueEmpty {
if (isEmpty()) {
throw new ExceptionQueueEmpty("意外:队列空");
}
return Q[front];
}
}
4.2 利用数组实现队列的优势与缺点
- 优势:
1) 实现简单
2) 各方法的时间复杂度均为O(1) - 缺点:
1) 若数组容量为N,则空间复杂度为O(N),在队列中的元素较少时会造成空间的浪费
2) 入队操作会有溢出的风险
5. 利用单链表实现队列
由于队列只在队首才有出队(需删除元素)操作,因此只需要让单链表的头节点为队首,末节点为队尾,即可高效的实现队列这一数据结构(单链表删除末节点的时间复杂度是O(n))。
5.1 队列的实现
/**
* 单链表节点类
*/
public class Node {
private Object element; // 数据对象
private Node next; // 指向后继节点
public Node() {
this(null, null);
}
public Node(Object element, Node next) {
this.element = element;
this.next = next;
}
public Object getElement() {
return element;
}
public Object setElement(Object element) {
Object oldElememt = element;
this.element = element;
return oldElememt;
}
public Node getNext() {
return next;
}
public void setNext(Node newNext) {
next = newNext;
}
}
/**
* 利用单链表实现队列
*/
public class LinkedListQueue implements Queue {
private Node head;
private Node tail;
private int size; // 队列中元素的数目
public LinkedListQueue() {
head = null;
tail = null;
size = 0;
}
@Override
public void enqueue(Object obj) {
Node tailNode = new Node(obj, null);
// 若此前队列为空,则直接插入
if (0 == size) {
head = tailNode;
tail = tailNode;
} else {
tail.setNext(tailNode);
tail = tailNode;
}
size++;
}
@Override
public Object dequeue() throws ExceptionQueueEmpty {
if (isEmpty()) {
throw new ExceptionQueueEmpty("意外:队列空");
}
Object returnElement = head.getElement();
head = head.getNext();
size--;
// 如果出队后,队列已空,需将末节点置空
if (isEmpty()) {
tail = null;
}
return returnElement;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return (size == 0);
}
@Override
public Object front() throws ExceptionQueueEmpty {
if (isEmpty()) {
throw new ExceptionQueueEmpty("意外:队列空");
}
return head.getElement();
}
}
5.2 利用单链表实现队列的优势与缺点
- 优势:
1) 队列中实际有n个元素,空间复杂度即为O(n),空间利用效率较高
2) 入队操作不会有溢出的风险 - 缺点:
1) 较数组实现来说,再实现的复杂度上略有增加
2) 需要有一个变量由于记录队列中元素的数目
6.队列的应用
队列的应用十分广泛,无论是商店、剧院、机场还是银行、医院,凡是按照“先到的客户优先接受服务”原则的场合,都可以利用队列这一数据结构来编排相应的服务。
7. 双端队列
双端队列是队列的一种变型,顾名思义,就是前端与后端都支持插入和删除操作的队列,它可以利用链表来实现。但由于双端队列两端都有删除操作,如果使用单链表来实现的话,总会有一端的删除操作的时间复杂度为O(n)。
造成此现象的原因是,只有找到末节点的直接前驱节点(末节点的前一个节点)之后,才能对末节点实施删除操作。对于单链表,一节点并不能直接访问它的前一个节点,所以想要找到末节点的直接前驱节点必须进行一次遍历,即时间复杂度为O(n)。
解决此问题的一个有效的方法是选用双向链表来实现。双向链表的节点都可以直接访问到它的前驱节点和后驱节点,故对于两端点的删除操作,时间复杂度也仅为O(1)。
利用双向链表实现双端队列与单链表实现队列类似,此处不再赘述。