日撸 Java 三百行(18 天: 循环队列)
注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明
前言
17天到18天似乎过了不是24h,而是若干天…
这是怎么回事呢?
因为我过去几周准备复试去了,所以无预告地暂时搁置每日的记录…(>人<;)
但是!从今天开始我们进度恢复,继续每日的记录
一、面对顺序表的妥协:循环起来的队列
今天我们继续第17天论述的队列,当时我们使用的链表完成队列,这样的队列是最直观的,可以不用在意空间地对队列进行有效扩充,这样来看顺序表实现队列似乎就不是很方便了,但是并不是说不能实现,今天我们就来看下通过顺序表实现队列的过程。
单一顺序表实现循环队列的受限
顺序表最大的障碍就是空间局限。
入队的话,因为使用顺序表尾部增加方法可以保证O(1),所以不用特别改进;但是出队操作是删除顺序表的第一个元素,这个就很麻烦了,因为顺序表若要正经删除第一个元素,那么需要O(n)复杂度,这对于我们设置操作受限的线性结构的初衷相悖。
故我们选择 逻辑上的删除 ,即利用双指针控制,令我们的首选元素的下标指向+1,从而在逻辑上屏蔽掉之前的位置的元素。
但是这个逻辑删除与添加方法有个弊端就是:假设我们不断对队列进行入队、出队、入队、出队…那么我们首尾指针位置最终会变得非常大,但是队列内的数据却还是非常少,而且之前出队后空余的位置无法被重复使用,照成极大浪费。
于是考虑用一种方法能够重用之前空余出来的无法被重复使用的空间。于是乎,我们使用了一种灵活的策略:循环队列:
循环队列与注意事项
这个方案是在逻辑上将我们的线性表的首位合并构造一个环,具体上来看,只要通过i = (i + 1) % N
就可以非常方便实现这个功能设想。
一方面,这样我们不断增大的指针就可以重复利用之前释放的空间,避免浪费。另一方面,这个方案允许了尾指针位置在数组上大于头指针的情况,保证了N个空间能完全使用。
但是我们也要注意一些细节,首先是队列元素有效范围,为了方便后续特例情况,我们定义头指针指向head,尾指针指向tail,其中数据有效范围为[h , t),如图:
· 因此,队空我们用head == tail
表示
那么队满怎么表示呢?按照这种表示,我们试着填满可以发现:
好像队满的表示也是head == tail
,那么这样自然会发生冲突,于是我们需要一些特别的策略来规避这种冲突,主要又两种策略:
- 使用标记:这个方法顾名思义,使用一个标记说明导致
head == tail
的操作是由入队操作导致的出队操作导致的。例:初始设置flag=0,若入队则设置flag=1,若出队则设置flag=0。这样的话,当判定出现head == tail
时,若flag是0,说明是当前状态为空,因为之前发生的是出队操作和什么都没干的操作;若flag是1,说明是当前状态为满,因为之前发生的是入队操作。 - 非满扩充法:此方法是比较常用的方法,因为其逻辑最简单。简单说,通过减少一个空间的使用来体现出 满 与 空 的差异,或者说,将认为数据N-1就是满,从而规避满与空的判断重叠。
我们主要用第二个方法就好:
· 因此,队满的表示为:(tail + 1) % N == head
满与空皆已知,那么接下来就代码实现了!
二、代码实现
类的属性、构造方法、遍历方法
Code:
/**
* The total space. One space can never can never be used
*/
public static final int TOTAL_SPACE = 10;
/*
* The data;
*/
int[] data;
/**
* The index for calculating the head. The actual head is head % TOTAL_SPACE.
*/
int head;
/**
* The index for caluculating the tail.
*/
int tail;
/**
*******************
* The constructor
*******************
*/
public CircleIntQueue() {
data = new int[TOTAL_SPACE];
head = 0;
tail = 0;
}// Of the first constructor
/**
*********************
* Overrides the method claimed in Object, the superclass of any class.
*********************
*/
public String toString() {
String resultString = "";
if (head == tail) {
return "empty";
} // Of if
for (int i = head; i < tail; i++) {
resultString += data[i % TOTAL_SPACE] + ", ";
} // Of for i
return resultString;
}// Of toString
如同顺序表的特性那般构造即可,不过要额外声明两个指针head和tail罢了
入队
Code:
/**
*********************
* Enqueue.
*
* @param paraValue The value of the new node.
*********************
*/
public void enqueue(int paraValue) {
if((tail + 1)% TOTAL_SPACE == head ) {
System.out.println("Queue full.");
return;
}// Of if
data[tail % TOTAL_SPACE] = paraValue;
tail++;
}//Of enqueue
对于非链表的结构,添加元素判断满自然是常规操作,这里按照之前分析的利用(tail + 1) % N == head
判断队列是否满
队列增加元素使用语句
data[tail % TOTAL_SPACE] = paraValue;
tail++;
其实就是基于我们之前定义的循环顺序表递增操作i = (i + 1) % N
;`而确定的,只不过因为我们尾指针默认在无数据的位置,故是先赋值在递增。
而且,我们的代码操作中,将取余操作从递增后立马取余推迟到了下次赋值时
出队:
Code:
/**
*********************
* Dequeue.
*
* @return The value at the head.
*********************
*/
public int dequeue() {
if(head == tail) {
System.out.println("No element in the queue");
return -1;
}//Of if
int resultValue = data[head % TOTAL_SPACE];
head++;
return resultValue;
}//Of dequeue
出队只需要判断队空就好了,其余出队方式也是秉承(tail + 1) % N == head
的讨论,细节上与入队颇具类似。
数据模拟
Code:
/**
*********************
* The entrance of the program.
*
* @param args Not used now.
*********************
*/
public static void main(String args[]) {
CircleIntQueue tempQueue = new CircleIntQueue();
System.out.println("Initialized, the list is: " + tempQueue.toString());
for (int i = 0; i < 5; i++) {
tempQueue.enqueue(i + 1);
} // Of for i
System.out.println("Enqueue, the queue is: " + tempQueue.toString());
int tempValue = tempQueue.dequeue();
System.out.println("Dequeue " + tempValue + ", the queue is: " + tempQueue.toString());
for (int i = 0; i < 6; i++) {
tempQueue.enqueue(i + 10);
System.out.println("Enqueue, the queue is: " + tempQueue.toString());
} // Of for i
for (int i = 0; i < 3; i++) {
tempValue = tempQueue.dequeue();
System.out.println("Dequeue " + tempValue + ", the queue is: " + tempQueue.toString());
} // Of for i
for (int i = 0; i < 6; i++) {
tempQueue.enqueue(i + 100);
System.out.println("Enqueue, the queue is: " + tempQueue.toString());
} // Of for i
}// Of main
Program execution results:
总结
虽然在现实生活中,循环队列的使用并不像普通链表那样队列方便和普遍,但是其在空间受限的环境的使用确实是非常方便的,只要掌握规律,很快就可以通过一般语言中的静态数组来实现。
但是相比于使用循环链表的使用,一方面,我们应当学习更多的是 " 对于同一个逻辑结构可以用不同的物理结构来表示 " 的这种体会,确实,虽然顺序表实现队列似乎显得有些笨重,但是尝试在这种受限的环境下完成一种逻辑功能也可以非常好锻炼我们对于这个逻辑功能的理解。
另一方面,我们要深刻体会这种程序中的 “环结构” 的特性,循环队列就体现其对于环对空间最大化利用的特性,弥补了单一顺序表使用队列时前端空间的浪费。虽然链表环也是一种不错的思维,可以方便解决一些具有增删效应的环问题(比如约瑟夫环问题),但是顺序表的这种取余循环更能应用更多场合,比如,取余环能很好压缩到for循环中,完成周期性的遍历,或者一些滑动窗口(例如计算机网络中的ARQ协议)机制中序号的重现,避免了花费过多空间存储序号位数的麻烦,等等。