–
队列概念
复习了一下队列这种数据结构,队列这种数据结构起始挺好理解的:将它想象成奶茶店排队买奶茶的人,先排队的,排在队伍前面,先买到奶茶喝,后来排队的都在队伍后边,最后买到奶茶。
先进先出,后进后出(FIFO)
所以,队列是一种操作受限的数据结构。
队列实现
-
- 顺序队列
顺序队列,顾名思义数据在队列中顺序存储。这样的特性,与数组的存储方式不谋而合,因此我们可以使用数组来实现顺序队列。
class ArrayQueue {
private Object[] mData; // 数组
private int mLength; // 数组大小
private int mHead;
private int mTail;
public ArrayQueue(int length) {
mLength = length;
mData = new Object[length];
}
/**
* 入队
*
* @param item
* @return
*/
boolean enqueue(Object item) {
if (mTail == mLength) // 队列满
return false;
mData[mTail] = item;
mTail++;
return true;
}
/**
* 出队
*
* @return
*/
Object dequeue() {
if (mHead == mTail) // 空队列
return null;
Object result = mData[mHead];
mHead++;
return result;
}
}
从上面的代码,来看队列这种数据结构实现的思路:
首先需要一个顺序存储容器-数组,来存储「队列」内容,然后定义两个下标指针,分别指向队列的头部和尾部。
可以根据下图来帮助理解。
入队的元素,添加到数组下标为「tail」的位置,同时「tail」移动到新的队伍尾部位置。所以,队尾下标指针「tail」总是在队列最后一个元素的下一个位置,因此我们可以使用「tail==mLength」来判断队列是否已满。
那么用上面的方法「tail==mLength」来判断队列是否已满,可能出现这种情况:
队头下标指针「mHead」前面空出的位置并没有填充数据,因而此时队列并没有满。
那么针对这种情况,就需要「搬移」数据:将「mHead」和「mTail」之间的数据,搬移到数组下标「0」到「mTail-mHead」的位置。
/**
* 入队
*
* @param item
* @return
*/
boolean enqueue(Object item) {
// tail == n 表示队列末尾没有空间了
if (mTail == mLength) {
// tail ==n && head==0,表示整个队列都占满了
if (mHead == 0) return false;
// 数据搬移
System.arraycopy(mData, mHead, mData, 0, mTail - mHead);
// 搬移完之后重新更新 head 和 tail
mTail -= mHead;
mHead = 0;
}
mData[mTail] = item;
++mTail;
return true;
}
-
- 链表队列
使用链表实现的队列,称之为链表队列。
链表队列的大小没有限制,因此不需要设置队列大小,只添加链表头部和尾部指针:head指针和tail指针。它们分别指向链表的第一个结点和最后一个结点。
入队:tail.next = newNode, tail = tail.next;
出队:head = head.next.
直接上代码:
class ListQueue {
private Node mHead;
private Node mTail;
/**
* 入队
*
* @param item
*/
void enqueue(Object item) {
if (null == mTail) { // 空队入队
Node newNode = new Node(item, null);
mHead = newNode;
mTail = newNode;
} else {
mTail.mNext = new Node(item, null);
mTail = mTail.mNext;
}
}
/**
* 出队
*
* @return
*/
Object dequeue() {
if (null == mHead)
return null;
Object result = mHead.mData;
mHead = mHead.mNext;
if (null == mHead) {
mTail = null;
}
return result;
}
/**
* 打印队列内容
*/
void printAll() {
Node node = mHead;
while (null != node) {
System.out.print(node.mData + " ");
node = node.mNext;
}
System.out.println();
}
class Node {
private Object mData;
private Node mNext;
public Node() {
this.mData = null;
this.mNext = null;
}
public Node(Object data) {
mData = data;
}
public Node(Object data, Node next) {
mData = data;
mNext = next;
}
}
}
-
- 循环队列
在第1节中,数组满的判断条件「tail==mLength」时,存在队列前半部分由于出队导致实际队列并不满的情况。我们的处理方式是搬移数据到数组头部位置。
这样的搬移操作,在数据量很大的情况下n,它的时间复杂度为O(n)。因此并不是一个十分有效的解决方法。这节使用另外一种解决思路——循环队列,来解决这个效率问题。
循环队列,也就是说队列是类似一个圆环一样的环形结构:队尾下标指针「tail」在判断队满的时候「tail==mLength」,此时「tail」下标指针的下一个位置为数组下标为0的位置,形成收尾相接的形状。
通过这样的方法,就可以避免搬移数据的操作。
循环链表实现的关键在于判断队空和队满的条件。
队空的判断条件还是「head==tail」
队满的判断条件:从循环队列的实现思路,可以看到,「tail」指针的变化就是基于数组长度mLength的一个循环,这在代码中通常会用取余「%」来实现。而队空的判断条件使用了「head==tail」,那么队满的判断则是「tail+1 == head」。再基于循环取余%的运算,得到判断条件:
(tail+1)%n=head
所以,最终的实现代码:
class CircleQueue {
private Object[] mData; // 数组
private int mLength; // 数组大小
private int mHead;
private int mTail;
public CircleQueue(int length) {
mLength = length;
mData = new Object[length];
}
/**
* 入队
*
* @param item
* @return
*/
boolean enqueue(Object item) {
int nextTail = (mTail + 1) % mLength; // 队尾的下一个位置
if (nextTail == mHead) // 队列满
return false;
mData[mTail] = item;
mTail = nextTail;
return true;
}
/**
* 出队
* @return
*/
Object dequeue() {
if (mHead == mTail) // 空队列
return null;
Object result = mData[mHead];
mHead = (mHead + 1) % mLength;
return result;
}
}
数据结构队列的复习完成。