目录
一、队列(Queue)
1.1 队列(Queue)概述
• 队列(Queue)是一种经常使用的集合,是实现了一个先进先出(FIFO:First In First Out)的特殊线性表,因此又称为FIFO 队列。
• 队列(Queue)只允许在一端进行入队列操作,用于插入数据;在另一端进行出队列操作,用于删除数据。
入队列:进行插入操作的一端称为队尾(Tail/Rear);
出队列:进行删除操作的一端称为队头(Head/Front);
1.2 队列(Queue)的实现
• 队列(Queue)的底层实现可以通过链表或数组两种方式。
(1) 链表实现:链表是一种动态数据结构,可以动态地分配内存。队列的每个元素都是一个节点,节点包含数据和指向下一个节点的指针。当队列满时,只需要添加一个新节点到链表尾部,而不需要像数组一样重新分配内存。因此,链表实现的队列具有较高的空间利用率,但查找操作则开销较大。
(2) 数组实现:数组是一种静态数据结构,在创建时必须确定长度。队列的每个元素都存储在数组的指定位置。数组可以进行随机查找操作,但在增删元素时开销较大,需要移动其他元素以保持队列的顺序。
1.3 队列(Queue)的方法
• 队列(Queue)中,有两组功能一样,但执行失败时给出的反馈不一样的方法,如下表展示:
方法功能 | 抛出异常的方法 | 返回false或null的方法 |
添加尾元素 | add(E e) | boolean offer(E e) |
删除首元素 | E remove() | E poll() |
查看首元素 | E element() | E peek() |
• 应注意尽量避免将null添加到队列中。因为poll()方法在删除元素失败时,将会返回null,如果队列中存在null,此时将无法判断是否成功删除元素;
1.4 队列的应用场景
• 列举队列的部分应用场景:
1. 线程池中的任务队列:线程池通过队列来管理待执行的任务。
2. 消息队列:消息队列用于在不同的进程或线程之间传递消息。
3. 缓冲区:缓冲区通过队列来实现等待或缓冲数据。
二、双端队列(Deque)
2.1 双端队列(Deque)概述
• 双端队列(Deque)是一种可以在队列的两端进行插入和删除操作的数据结构,deque 是 “double ended queue” 的简称。与普通队列(Queue)不同,双端队列允许在队列的两端进行操作。
• 双端队列(Deque)继承了接口 Queue 。
public interface Deque<E> extends Queue<E>
• ArrayDeque 和 LinkedList 是双端队列(Deque)的实现类。
Deque<E> arrayDeque = new ArrayDeque<>();
Deque<E> linkedList = new LinkedList<>();
2.2 双端队列(Deque)的方法
• 双端队列(Deque)中,同样有两组功能一样,但执行失败时给出的反馈不一样的方法,如下表展示:
方法功能 | 抛出异常的方法 | 返回false或null的方法 |
添加首元素 | addFirst(E e) | boolean offerFirst(E e) |
添加尾元素 | addLast(E e) | boolean offerLast(E e) |
删除首元素 | E removeFirst() | E pollFirst() |
删除尾元素 | E removeLast() | E pollLast() |
查看首元素 | E getFirst() | E peekFirst() |
查看尾元素 | E getLast() | E peekLast() |
• 双端队列(Deque)中,也包含了自己的 push(E e) 和 E pop() 方法,这两个方法的命名和使用与栈(Stack)类相同。当前Java官方推荐使用双端队列(Deque)来代替栈,栈(Stack)类被认为已过时。
三、循环队列
3.1 循环队列概述
• 循环队列并不是一个单独的类(class)或接口(interface),只是一种数据的组织形式,是一种特殊的队列。
• 循环队列通常使用数组实现,具有头指针(front)和尾指针(rear),分别指向队头元素和队尾元素。
• 循环队列通常只在队列的两端进行操作,是实现了一个先进先出(FIFO:First In First Out)的特殊队列。
• 最大下标的后继是最小下标,最小下标的前驱是最大下标,形成一个环状队列。
• 循环队列的优点:
(1)节省空间:循环队列可以重复利用队列空间。
(2)提高效率:循环队列的删除和添加操作的时间复杂度均为O(1),操作效率高。
• 循环队列经常被应用于队列缓冲区,在数据传输、数据处理等场景中,经常需要使用队列来缓冲数据。循环队列可以节省空间,提高效率,因此常常被用来作为队列缓冲区。
3.2 实现数组下标的循环
• 循环队列通常使用数组实现,要实现队列的循环,就必须实现数组下标最大值和最小值之间的转换,如下图:
• 数组下标循环的技巧:
(1)下标最大值向最小值转换:偏移后下标 = (当前下标 + 偏移量) % 数组长度 ;
(2)下标最小值向最大值转换: 偏移后下标 = (当前下标 + 数组长度 - 偏移量) % 数组长度 ;
偏移量:是指每次增加或减少元素后,下标指针移动的幅度;
例:假设偏移量为1;
由下标 7 向下标 0 转换;
偏移后下标 = (当前下标 + 偏移量) % 数组长度
偏移后下标 = (7 + 1) % 8
偏移后下标 = 0
由下标 0 向下标 7 转换;
偏移后下标 = (当前下标 + 数组长度 - 偏移量) % 数组长度
偏移后下标 = (0 + 8 - 1) % 8
偏移后下标 = 7
3.3 判断循环队列的状态
• 循环队列可以通过以下三种方式判断队列为空或已满:
(1)在类中设置 size 属性记录,每增删一个元素,size 对应增加或减少。当 size 为 0 时,队列为空;当 size 与数组的长度一样时,队列为满。
(2)设置一个标志,假设当出队列时,标志为 false ,当入队列时,标志为 ture 。每次出入队列后,判断头尾指针是否重合,如果重合且标志为 false ,则表示出队列后,头尾指针重合,此时队列为空;如果重合且标志为 ture ,则表示入队列后,头尾指针重合,此时队列为满。
(3)保留一个空间不放元素,用于表示数组是否已满。在队列中保留一个空的空间,尾指针永远指向该空间。根据头尾指针是否重合,判断队列是否为空;根据尾指针的后继是否为头指针,判断队列是否为满。
• 保留一个空间不放元素,判断队列状态的代码实现:
//判断空队列方法;
public boolean isEmpty() {
return this.front == this.rear;
}
//判断满队列方法;
public boolean isFull() {
return this.front == (this.rear+1) % this.elem.length;
}
(下文中,循环队列其他操作的实现代码都沿用保留一个空间不放元素的方式)
3.4 循环队列的增删查操作
//入队列方法;
public boolean enQueue(int value) {
/*判断数组是否已满,
已满则返回false;
没满赋值,移动rear指针到下一下标;
返回true;*/
if(isFull()){
return false;
}
this.elem[this.rear] = value;
this.rear = (this.rear+1) % this.elem.length;
return true;
}
//出队列方法;
public boolean deQueue() {
/*判断数组是否为空,
为空则返回false;
否则将elem[front]置空,
移动front到下一下标;
返回true;*/
if(isEmpty()){
return false;
}
this.elem[this.front] = null;
this.front = (this.front+1) % this.elem.length;
return true;
}
//查看头元素方法;
public int getFront() {
if(isEmpty()){
//抛出异常,队列为空;
}
return this.elem[this.front];
}
//查看尾元素方法;
public int getRear() {
if(isEmpty()){
//抛出异常,队列为空;
}
//尾指针永远指向空的元素,如果尾指针位于0下标,则尾元素下标为最大下标;否则尾元素下标为当前空元素的下标-1;
int index = (this.rear == 0 ? this.elem.length-1 : this.rear-1) ;
return this.elem[index];
}
3.5 获取循环队列存储的元素个数
• 由于 尾指针rear 指向数组尾元素的后继,因此数组求有效元素个数size时,可以使用 rear - front 的计算方式得到。
例:front为0,rear为3:
size = rear - front
size = 3 - 0
size = 3
• 但是由于当前队列是一个循环队列,那么将可能出现头指针下标大于尾指针前驱的下标的情况。此时再使用 rear - front 的计算方式求有效元素个数,将得到一个负数。
例:front为3,rear为1:
size = rear - front
size = 1 - 3
size = -2
• 得到的有效元素个数为负数,显然是不正确的。
• 此时假设数组最大容量为MAXSIZE,将 这个值为负数的差 + MAXSIZE 就可以得到正确的有效元素个数。
例:front为3,rear为1,MAXSIZE为8:
size = rear - front + MAXSIZE
size = 1 - 3 + 8
size = 6
• 由此可以得出结论,当 front <= rear 时,有效元素个数等于 rear - front ;当 front > rear 时,有效元素个数等于 rear - front + MAXSIZE ;
• 通过if else语句实现代码:
//获取队列中存储的有效元素个数;
public int getSize() {
if (this.front <= this.rear) {
return this.rear - this.front;
} else {
return this.rear - this.front + this.MAXSIZE;
}
}
• 上述代码中, rear - front 为负数的情况下,使用公式 rear - front + MAXSIZE 进行处理,返回一个正确反映有效元素个数的正数;而使用 MAXSIZE 对 rear - front + MAXSIZE 进行取模,得到公式 (rear - front + MAXSIZE) % MAXSIZ ,则可以有一条公式正确处理正负两种情况的效果,代码更为简洁。
• 通过公式 (rear - front + MAXSIZE) % MAXSIZ 实现代码:
//获取队列中存储的有效元素个数
public int size() {
return (this.rear - this.front + this.MAXSIZE) % this.MAXSIZE;
}
( 哈哈哈~~ 文章结束!)
( 看到这里,如果有为各位帅哥美女提供一点点灵感,请点一个小小的赞哦,比心💖💖💖 )