一、什么是队列
队列是一种基本且广泛应用的数据结构,它模拟了现实生活中排队等候服务的情景。在计算机科学中,队列遵循“先进先出”(First-In-First-Out, FIFO)原则,这意味着第一个进入队列的元素也将是第一个离开队列的元素。.
二、定义
队列是一个线性数据结构,可以想象成一个两端开口的管道或实际生活中的排队场景。一端用于添加元素(称为enqueue或入队),另一端用于移除元素(称为dequeue或出队)。新元素只能从队尾添加,而删除或访问元素则总是从队头进行。
三、特性
- 遵循FIFO(先进先出)顺序。
- 可能有最大容量限制,在满载时不允许再加入元素,除非有元素被移除腾出空间(取决于具体实现)。
- 常见操作包括:
enqueue(添加元素到队尾)
dequeue(移除并返回队头元素)
peek 或 front(查看队头元素而不移除)
is_empty(检查队列是否为空)
四、解决的问题与应用场景
队列主要用来解决和优化那些需要按照一定顺序处理任务或数据流的问题,常见于以下几个方面:
- 资源管理:操作系统中进程调度、打印机任务分配等,新到达的任务通常会被放入队列等待执行,最先到达的任务会优先获得系统资源。
- 消息传递: 在多线程编程中,消息队列常用于线程间通信,确保信息按发送顺序得到处理。
- 缓冲区管理: 网络传输中,接收方可能会使用队列暂存接收到的数据包,保证数据有序地被处理。
- 任务队列: 后台任务调度,如定时任务、异步任务等,任务提交后按顺序执行。
- 银行、医院叫号系统: 现实生活中的取号排队系统就是一个典型的队列应用实例,顾客按照到达顺序依次办理业务。
五、实现方式
1、链表实现队列
-
优点:
灵活性: 链表不需要预先分配固定的内存空间,可以在需要时动态地增加或减少节点,插入和删除操作都只需要改变几个指针指向即
可,因此enqueue和dequeue操作通常都可以在O(1)时间内完成。
空间利用率: 链表不会因为队列未充满而浪费大量空间。 -
缺点:
内存不连续: 由于链表的节点分散在内存的不同位置,所以对元素的随机访问不如数组高效,通常需要从头节点开始遍历到目标位置。
额外开销: 每个节点都需要额外存储指向下一个节点的指针,相比数组增加了存储空间的开销。
缓存不友好: 链表访问的局部性较差,不利于CPU缓存命中,可能影响整体性能。
2、数组实现队列
-
优点:
内存连续性: 数组在内存中是连续存储的,因此如果需要顺序访问队列中的元素(如dequeue时),可以利用这个特性快速访问。
空间利用率: 当队列大小固定且已知时,静态数组可以预先分配足够的空间,避免了频繁的内存分配和释放操作,提高效率。
随机访问: 理论上讲,对于非满队列,数组队列可以直接通过索引O(1)时间复杂度访问队头或队尾附近的元素。 -
缺点:
扩容问题: 如果是静态数组,一旦分配的空间用完,若要添加新元素,则可能需要创建一个更大的数组,并将所有元素复制过去,这在
实际操作中会消耗较大的时间成本。动态数组虽然可以自动扩容,但每次扩容也会带来一定的性能损失。
删除和插入: 在队列头部进行dequeue操作时,通常需要将后续元素向前移动一位以填补空位,这需要O(n)的时间复杂度,尤其是当队
列接近满的时候,效率较低。
空间浪费: 如果队列未充满整个数组,可能会有部分空间闲置无法使用。
六、代码实现
创建一个队列的公共接口
/**
* 队列接口
*
* @param <E> 队列数据类型。
*/
public interface Queue<E> {
/**
* 向队列尾插入值
*
* @param value 添加的值
* @return 插入成功返回 true, 插入失败返回 false
*/
boolean enqueue(E value);
/**
* 从队列头获取值, 并从队列中移除获取的值
*
* @return 如果队列非空返回队列头值, 否则返回 null
*/
E dequeue();
/**
* 从队列头获取值,不移除获取的值
*
* @return 如果队列非空返回对头值, 否则返回 null
*/
E peek();
/**
* 检查队列是否为空
*
* @return 空返回 true, 否则返回 false
*/
boolean isEmpty();
/**
* 检查队列是否已满
*
* @return 满返回 true, 否则返回 false
*/
boolean isFull();
/**
* 遍历打印队列中的值。
*/
void circulate();
}
1、链表方式
package queue;
/**
* 队列 单向循环链表实现
*/
public class LinkedListQueue<E> implements Queue<E> {
//头部节点
private Node<E> head = new Node(null, null);//初始化头部节点
private Node<E> tail = head;//初始化尾部节点,默认情况下,头部节点就是尾部节点。
public LinkedListQueue() {
head.next = tail;//设置头部节点的next节点
tail.next = head;//设置尾部节点的next节点
}
private static class Node<E> {
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
/**
* 向队列尾插入值
*
* @param value 添加的值
* @return 插入成功返回 true, 插入失败返回 false
*/
@Override
public boolean enqueue(E value) {
Node add = new Node(value, head);//新增节点,新增节点的下一个节点必须是头节点,因为是尾插法。
tail.next = add;//把最后一个的下一个节点赋值为新增节点。
tail = add;//然后更新尾节点为新增的节点。
return true;
}
/**
* 从队列头获取值, 并从队列中移除获取的值
*
* @return 如果队列非空返回队列头值, 否则返回 null
*/
@Override
public E dequeue() {
//队列不能为空
if (isEmpty()) {
return null;
}
Node<E> first = head.next;
head.next = first.next;//去掉已经取出的值。
//如果取出的first是最后一个元素,必须把尾节点再次指向头节点
if (first == tail) {
tail = head;
}
return first.value;
}
/**
* 从队列头获取值,不移除获取的值
*
* @return 如果队列非空返回对头值, 否则返回 null
*/
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return head.next.value;
}
/**
* 检查队列是否为空
*
* @return 空返回 true, 否则返回 false
*/
@Override
public boolean isEmpty() {
//如果头部节点等于尾部节点。那么说明是空队列。
return head == tail;
}
/**
* 检查队列是否已满
*
* @return 满返回 true, 否则返回 false
*/
@Override
public boolean isFull() {
//链表结构不存在队列会满的情况。
return false;
}
/**
* 遍历打印队列中的值。
*/
@Override
public void circulate() {
Node next = head.next;
while (next != head) {
System.out.println("打印队列值:" + next.value);
next = next.next;
}
}
}
2、数组方式(环形数组)
2.1、什么是环形数组
环形数组(Circular Array)是一种特殊的数组,它模拟了一个可以循环利用其空间的数据结构。其逻辑结构表现为一个首尾相连的“环”或“圈”。在物理上,它仍然是一个线性存储结构,即普通的数组,但它的读写操作遵循循环规则。在实现队列时,环形数组允许数组的尾部索引“绕回”到数组头部,从而有效地创建一个没有物理结尾的队列,克服了普通数组作为队列实现时可能出现的假溢出问题。
2.2、下表计算案例
例如,数组长度是 5,当前位置是 3 ,向前走 2 步 ,请问下标是多少
下标计算公式: (当前位置下标+前进步数) % 数组长度 --> 及:(3+2)%5 = 0
2.3、实现思路
-
初始化环形数组: 创建固定大小的数组,并设置两个指针,一个是头指针(head),另一个是尾指针(tail)。初始时,头指针和尾指针都指向数组的第一个位置。
-
入队操作(enqueue):
检查队列是否已满,使用公式(尾指针索引位置+1)取余 数组长度== 头指针索引位置 表示数组已满。
//tail:尾指针索引
//length:数组长度
//head:头指针索引
(tail+ 1) % length== head //为true则是满了.
如果未满,将新元素添加到尾指针所指向的位置,然后使尾指针加1并取模容量(取模公式为:如下),这样尾指针就可以在数组范围内循环移动。
//tail:尾指针索引
//length:数组长度
tail= (tail + 1) % length
- 出队操作(dequeue):
检查队列是否为空。
如果非空,则获取头指针所指向的元素并返回,然后更新头指针使其加1并取模容量(取模公式为:如下),同样地,头指针也能够循环回到数组起始位置。
//length:数组长度
//head:头指针索引
head = (head + 1) % length
2.4、具体代码如下
package queue;
import java.util.NoSuchElementException;
/**
* 队列 单向循环链表实现
*/
public class ArrayQueue<E> implements Queue {
private E[] queue;//用于存储元素的固定大小数组。
private int head;//指向队列头部元素的索引(初始值为0)。
private int tail;//指向队列尾部下一个可用位置的索引(初始值为-1,当入队第一个元素时变为0)。
private int length;//数组的容量,即队列的最大容量。
private int size;//当前队列中元素的数量(初始值为0)。
/**
* @param length 这是队列容量
*/
public ArrayQueue(int length) {
this.length = length + 1;
this.queue = (E[]) new Object[length];//多创建一个值,用来标注尾指针
this.head = 0;
this.tail = 0;
this.size = 0;
}
/**
* 向队列尾插入值
*
* @param value 添加的值
* @return 插入成功返回 true, 插入失败返回 false
*/
@Override
public boolean enqueue(Object value) {
//判断队列是否已满
if (isFull()) {
throw new IllegalStateException("队列已满");
}
queue[tail] = (E) value; // 存储元素
tail = (tail + 1) % length; // 更新尾指针
size++;
return true;
}
/**
* 从队列头获取值, 并从队列中移除获取的值
*
* @return 如果队列非空返回队列头值, 否则返回 null
*/
@Override
public Object dequeue() {
if (isEmpty()) {
throw new NoSuchElementException("队列为空,没有值!");
}
E data = queue[head];// 获取队头元素
queue[head] = null;//清空取出的元素。手动垃圾回收
head = (head + 1) % length; // 更新头指针
size--;
return data;
}
/**
* 从队列头获取值,不移除获取的值
*
* @return 如果队列非空返回对头值, 否则返回 null
*/
@Override
public Object peek() {
E data = queue[head];// 获取队头元素
return data;
}
/**
* 检查队列是否为空
*
* @return 空返回 true, 否则返回 false
*/
@Override
public boolean isEmpty() {
//如果头部节点等于尾部节点。那么说明是空队列。
return head == tail;
}
/**
* 检查队列是否已满
*
* @return 满返回 true, 否则返回 false
*/
@Override
public boolean isFull() {
return (tail + 1) % length == head;
}
/**
* 遍历打印队列中的值。
*/
@Override
public void circulate() {
while (head != tail) {
System.out.println(queue[head]);
head = (head + 1) % length; // 更新头指针
}
}
}