队列
- 队列和栈一样是特殊的线性表。区别只是它能尾进头出而已
- 学习队列需要清楚的认识到
front
和rear
两指针什么情况下分别变动。
队列也分成两种:
- 静态队列(数组实现)
- 动态队列(链表实现)
需要注意一下:静态队列会遇到 “假溢出” 情况,即:当我们为了避免静态队列在进行元素删除操作性能差的时候(所有队列元素向前移位,来补数组删除元素后空出来的位置),我们通过移动front
指针移动来定位新的队头元素,而不是先前的移动剩余节点来确定数组下标为0的位置为队头。直到front
=rear
的时候认为该队列为空。但是这种优化方式会出现假溢出的现象,即我们front
一直向后移,而队尾rear
已经到数组尾了。当再插入元素的时候该存到哪里呢,此时就会出现数组越界异常。而此时我们的数组从下标为0的地方到front
的位置却是没有任何元素的。
解决办法:循环队列
解决思路:rear
原先的处理方式是指向尾节点的下一位置。当出现上述情况的时候rear
指向数组下标为0
的位置。形成循环队列,这样rear
就不会指向数组arrayLength
位置外的位置了。
新问题:当rear
=front
时,可能有队满和队空两种情况。rear
一直加元素向后走顶到front
处(队满),和front
一直删顶到rear
处(队空)。
解决办法:添加元素的时候rear
最多添加元素到front - 1
的索引处。这样front
和rear
就不可能重合了。我们要熟记这个关系。front = rear
即空队,满队即front + 1 = rear
。后续有很多操作会用到
队列常用三个判断因式
- 队满判断因式:
(rear + 1) % QueueSize == front
- 队空的判断因式:
rear == front
- 通用队长计算因式:
(QueueSize - front + rear) % QueueSize
联想图进行理解很好理解。
静态队列
循环队列的实现由于是基于数组实现,所以我们没有链表中尾节点next
指针指向头节点的操作。但是可以通过rear
字段来记录尾节点下标来达到循环队列的目的。
数组实现的方式在进行元素删除时的操作都是不对元素处理,通过移动“记录变量”的指向来无视该被删除的值。即便他在数组空间中任然真实存在,但是已然不会被该数据结构的索引方式所索引到。
- 需要注意的是静态队列我们一般都会做成循环队列,这样才能更好地使用内存空间规避掉“假溢出”的现象
静态队列的操作
队列
public class Queue {
private static final int QUEUE_MAX_SIZE = 100;
/**
* 队头指针
*/
public int front = 0;
/**
* 队尾指针
*/
public int rear = 0;
/**
* 队列元素容器
*/
public int[] queueData = new int[QUEUE_MAX_SIZE];
}
返回循环队列的队长
/**
* 返回循环队列的队长
*
* @param queue
* @return
*/
public static int queueLength(Queue queue) {
// | | rear | | | front | |
return (QUEUE_MAX_SIZE - queue.front + queue.rear) % QUEUE_MAX_SIZE;
}
只要记住这个逻辑图或按公式来,根据% QUEUE_MAX_SIZE
来适配全部情况即可。
循环队列入队
/**
* 循环队列入队
*
* @param queue
* @param value
* @return
*/
public static int insertQueue(Queue queue, int value) {
if ((queue.rear + 1) % QUEUE_MAX_SIZE == queue.front) {
return 0;
}
queue.queueData[queue.rear] = value;
/*这里需要注意`queue.rear++`后要对总数QUEUE_MAX_SIZE取余
这样才满足了循环队列的情况 否则可能会出现数组越界的异常*/
queue.rear = queue.rear++ % QUEUE_MAX_SIZE;
return 1;
}
这里也是根据那个逻辑图想逻辑就可,也可以直接套公式。
循环队列出队
/**
* 循环队列出队
*
* @param queue
* @return
*/
public static int removeQueue(Queue queue) {
if (queue.front == queue.rear) {
return 0;
}
// 不处理,移指针来规避
queue.front = queue.front++ % QUEUE_MAX_SIZE;
return 1;
}
需要特别提醒的一点就是我们在任何数组实现的数据结构中,在底层删除数据的时候的处理都是不做处理,只是移动“标识指针”,进而达到审查不到该元素的目的。其实最好的处理方式当然是根据数组的元素类型来直接置空或者为0来释放对象。
遍历循环队列
/**
* 遍历循环队列
*
* @param queue
* @return
*/
public static void traverse(Queue_ queue) {
int index = queue.front;
// 这里需要注意 rear 指向的是队尾节点的下一位置,而非队尾的位置,所以 rear 处不输出
while (index != queue.rear) {
int value = queue.queueData[index];
System.out.println(value);
index = index++ % queue.queueData.length;
}
}
这里需要注意rear
指向的是队尾节点的下一位置,而非队尾的位置,所以 rear
处不输出
判断该队列是否为空
/**
* 判断该队列是否为空
*
* @param queue
* @return
*/
public static int isEmpty(Queue_ queue) {
if (queue.front == queue.rear) {
return 1;
}
return 0;
}
只要清楚我们已经让rear
指向了队尾后一位的位置,以及当front
=rear
时队为空即可。
链式队列
链队列根本上还是链表。只是玩的是“节点标识“的把戏,并且被规定节点只能从队尾进队首出。我们凭借标识节点来确定这个数据结构
链式队列的操作
队列
public class Queue_ {
/**
* 队头指针
*/
public Node front;
/**
* 队尾指针
*/
public Node rear;
}
链队列元素入队
/**
* 链队列元素入队
*
* @param queue
* @param value
* @return
*/
public static int insert(Queue_ queue, int value) {
Node node = new Node(value, null);
queue.rear.nextNode = node;
queue.rear = node;
return 1;
}
链队列不存在数据越界的情况,给rear
尾节点加值即可。然后记得再移动尾节点的指针
链队列的出队
/**
* 链队列的出队
*
* @param queue
* @return
*/
public static int remove(Queue_ queue) {
// 空队
if (queue.front == queue.rear) {
return 0;
}
Node firstNode = queue.front.nextNode;
// 只剩一个节点的时候
if (firstNode.nextNode == null && queue.rear == firstNode) {
queue.front.nextNode = null;
// 队尾指针指向虚拟头结点
queue.rear = queue.front;
}
queue.front.nextNode = firstNode.nextNode;
return 1;
}
删除节点的时候需要判断一下,边界条件。考虑当该队列为空队的情况,以及队中只剩一个元素时rear
和front
的指向。因为front
一般指向头结点而非队头节点,所以在链队列中front
不会动,只是在删除元素时不断的变更front.nextNode
属性,而不会去动rear
。只剩一个元素的特殊情况时,我们就需要把rear
考虑进来了。由于rear
指向的是队尾元素,删除掉最后一个元素之后就不存在了,所以它只能到front
队头的位置。而front.nextNode = null
。