如何实现队列
这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战
笔者最近在学习《数据结构与算法之美》,正好借着这个机会边练习边记录一下自己学习的知识点。今天带来的是队列。
一、什么是队列
- 队列和栈类似,也是操作受限的线性表数据结构。队列只允许在一端添加数据,另一端移除数据,也造就队列先进先出的特点。可以把队列想象成排队检票,且不能插队,排在前面的先检查通过离开,后面来的只能排在队尾。
- 队列的基本操作也和栈类似,栈的基本操作时入栈 push() 和出栈 pop(),类似的队列也有两个基本操作,入队 enQueue () 和 出队 deQueue() 。
二、队列的常见类型
队列有两种实现方式,一是使用数组实现称为顺序队列,二是使用链表实现称为链式队列。
2.1 顺序队列
数组实现队列,首先需要 head 记录队头索引和 tail 记录队尾索引。出队时将数组 head 位置的数据移除,同时 head 向后移动一位。入队操作类似,将数据添加到数组 tail 位置,同时 tail 向后移动一位。
因为数组大小是有限的,当数据添加到数组的最后一个位置后,再添加一个新数据,就存在两种情况:
一是队列所有位置都满了,无法再添加数据,此时告诉用户添加失败即可。
二是队列的头部还存在空余位置,此时需要进行数据搬移到队列头部,再添加新数据。
下面是具体实现代码:
/**
* @author xuls
* @date 2021/11/20 21:45
* 数组实现顺序队列
*/
public class ArrayQueue <T>{
//存储数组
private Object[] dataArray;
//容量
private int capacity;
//队头索引
private int head;
//队尾索引
private int tail;
public ArrayQueue(int capacity) {
dataArray = new Object[capacity];
head=0;
tail=0;
this.capacity = capacity;
}
//入队
public boolean enQueue(T data){
//如果 tail 索引超过数组最大长度
if (tail == capacity){
//队满 tail - head = capacity
if (head ==0){
return false;
}
//数据搬移
int length = tail - head;
for (int i = 0; i < length; i++) {
dataArray[i] = dataArray[head+i];
}
head=0;
tail=length;
}
//入队的同时 , tail ++ 向后移动一位
dataArray[tail++]=data;
return true;
}
//出队
public T deQueue(){
if (isEmpty()){
throw new IndexOutOfBoundsException("queue is empty");
}
//出队的同时 head ++ 向后移动一位
return (T)dataArray[head++];
}
public boolean isEmpty(){
return head == tail;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
for (int i = head; i < tail; i++) {
builder.append(dataArray[i]).append(",");
}
return "ArrayQueue={"+builder.toString()+"}";
}
}
2.2 链式队列
队列使用链表实现,同样的需要一个 head 引用指向队头,一个 tail 引用指向队尾。入队将 tail.next 指向新结点,再把新结点设置为 tail;出队将 head.next 设置为 head。
下面是具体实现代码:
/**
* @author xuls
* @date 2021/11/20 23:35
* 链式队列
*/
public class LinkedListQueue<T> {
private Node<T> head;
private Node<T> tail;
public LinkedListQueue() {
head=tail=null;
}
//入队
public void enQueue(T data){
Node<T> node = new Node<>(data,null);
if (head == null){
head = tail =node;
}else {
tail.next = node;
tail = node;
}
}
//出队
public T deQueue(){
if (head==null){
throw new IndexOutOfBoundsException("queue is empty");
}
T data = head.data;
head = head.next;
//出队之后,队空时将 tail 置为 null
if (isEmpty()){
tail = null;
}
return data;
}
public boolean isEmpty(){
return head == null;
}
private class Node<T>{
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
Node<T> tempNode = head;
while (tempNode != null){
builder.append(tempNode.data).append(",");
tempNode = tempNode.next;
}
return "LinkedListQueue{"+builder.toString()+"}";
}
}
2.3 循环队列
在实现顺序队列时,当数据添加到数组的最后一个位置后,再添加一个新数据时,可能需要进行数据搬移。大量的数据搬移是非常耗时的,循环队列可以避免数据搬移操作,循环队列把队列的队头和队尾连接在一起,循环队列的实现思路和顺序队列基本相同,最大的不同点是循环队列如何判断队满?下图列举了一些情况,可以试着从里面总结一下规律。
从图里可以总结出来:循环队列队满时 (tail+1) % capaticy = head ,也可以看过循环队列是浪费了一个存储空间的。
下面是具体实现代码:
/**
* @author xuls
* @date 2021/11/21 11:03
* 循环队列
*/
public class CircleQueue<T> {
private Object[] dataArray;
private int capacity;
private int head;
private int tail;
public CircleQueue(int capacity) {
dataArray = new Object[capacity];
this.capacity = capacity;
head=tail=0;
}
public boolean enQueue(T data){
if ((tail+1)%capacity == head){
return false;
}
dataArray[tail]=data;
//取模 % ,可以避免数组访问越界
tail = (tail+1) % capacity;
return true;
}
public T deQueue(){
if (isEmpty()){
throw new IndexOutOfBoundsException("queue is empty");
}
T t = (T) dataArray[head];
//取模 % ,可以避免数组访问越界
head = (head+1) % capacity;
return t;
}
public boolean isEmpty(){
return tail == head;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
int temp = head;
while (temp != tail){
builder.append(dataArray[temp]).append(",");
temp = (temp+1) % capacity;
}
return "CircleQueue={"+builder.toString()+"}";
}
}
三、总结
- 队列的特点是先进先出。
- 队列的基本操作的是入队 enQueue() 和 出队 deQueue() 。
- 使用数组实现的叫顺序队列,使用链表实现的叫链式队列。顺序队列的数据搬移问题可以使用循环队列解决,循环队列和顺序队列最大的不同点是队满的判断条件,循环队列会浪费一个存储空间。