栈与队列(二)
栈和队列是两种重要的数据结构。从栈与队列的逻辑结构上来说,它们也是线性结构,与线性表不同的是它们所支持的基本操作是受到限制的,它们是操作受限的线性表,是一种限定性的数据结构。
二、队列
2.1队列的定义及抽象数据类型
队列(queue)简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。在队列中把插入数据元素的一端称为队尾(rear),删除数据元素的一端称为队首(front)。向队尾插入元素称为进队或入队,新元素入队后成为新的队尾元素;从队列中删除元素称为离队或出队,元素出队后,其后续元素成为新的队首元素。由于队列的插入和删除操作分别在队尾和队首进行,每个元素必然按照进入的次序离队,也就是说先进队的元素必然先离队,所以称队列为先进先出表(First In First Out,简称FIFO)。队列结构与日常生活中排队等候服务的模型是一致的,最早进入队列的人,最早得到服务并从队首离开;最后到来的人只能排在队列的最后,最后得到服务并最后离开。
Queue接口
package queue; //Queue 接口 public interface Queue { // 返回队列的大小 public int getSize(); // 判断队列是否为空 public boolean isEmpty(); // 数据元素e 入队 public void enqueue(Object e); // 队首元素出队 public Object dequeue() throws QueueEmptyException; // 取队首元素 public Object peek() throws QueueEmptyException; }
QueueEmptyException队列为空时出队或取队首元素抛出此异常
package queue; //QueueEmptyException 队列为空时出队或取队首元素抛出此异常 public class QueueEmptyException extends RuntimeException { public QueueEmptyException(String err) { super(err); } }
2.2队列的顺序存储实现
在队列的顺序存储实现中,我们可以将队列当作一般的表用数组加以实现,但这样做的效果并不好。尽管我们可以用一个指针last来指示队尾,使得enqueue运算可在Ο(1)时间内完成,但是在执行dequeue时,为了删除队首元素,必须将数组中其他所有元素都向前移动一个位置。这样,当队列中有n个元素时,执行dequeue就需要Ο(n)时间。
因为只有一个last指针,所以必须移动。如果也有front指针,不移动,只改变front的值的话。队列的操作是在两端进行的,因此,当为队列申请到一段连续空间后,插入元素在rear端进行,删除元素在front端进行,删除后的元素虽然从队列中出来,但是其空间还被占用,当rear达到最大值,其实整个队列的空间并未占满,剩余的空间在rear的左端,这将造成空间上的浪费,
理解循环队列的引入:第一种理解为了提高运算的效率,第二种理解是可充分利用存储空间。
,我们用另一种方法来表达数组中各单元的位置关系。设想数组A[0.. capacity-1]中的单元不是排成一行,而是围成一个圆环,即A[0]接在A[capacity-1]的后面。这种意义下的数组称为循环数组,如图所示。
用循环数组实现的队列称为循环队列,我们将循环队列中从队首到队尾的元素按逆时针方向存放在循环数组中一段连续的单元中。并且直接用队首指针front指向队首元素所在单元,用队尾指针rear指向队尾元素所在单元的后一个单元。用循环数组来实现队列可以在Ο(1)时间内完成enqueue和dequeue运算。执行一系列的入队与出队运算,将使整个队列在循环数组中按逆时针方向移动。
当然队首和队尾指针也可以有不同的指向,例如也可以用队首指针front指向队首元素所在单元的前一个单元,或者用队尾指针rear指向队尾元素所在单元的方法来表示队列在循环数组中的位置。但是不论使用哪一种方法来指示队首与队尾元素,我们都要解决如何表示满队列和空队列的问题。
e0、e1、e2、e3相继出队,则得到空队列,此时队首指针追上队尾指针,所以也有front = rear。如下图
当e4、e5、e6、e7相继进入队列后,队列空间被占满,此时队尾指针追上队首指针,有rear = front。如下图
可见仅凭front与rear是否相等无法判断
解决办法:
方法一:少使用一个存储空间,当队尾指针的下一个单元就是队首指针所指单元时,则停止入队。这样队尾指针就不会追上队首指针,所以在队列满时就不会有front = rear。这样一来,队列满的条件就变为(rear+1)% capacity = front,而队列判空的条件不变,仍然为front = rear。
方法二:是增设一个标志,以区别队列是“空”还是“满”,例如增设size变量表明队列中数据元素的个数,如果size = Max则队列满。
Queue的顺序存储实现
package queue; //Queue 的顺序存储实现 public class QueueArray implements Queue { private static final int CAP = 7;// 队列默认大小 private Object[] elements; // 数据元素数组 private int capacity; // 数组的大小elements.length private int front; // 队首指针,指向队首 private int rear; // 队尾指针,指向队尾后一个位置 public QueueArray() { this(CAP); } public QueueArray(int cap) { capacity = cap + 1; elements = new Object[capacity]; front = rear = 0; } // 返回队列的大小 public int getSize() { return (rear - front + capacity) % capacity; } // 判断队列是否为空 public boolean isEmpty() { return front == rear; } // 数据元素e 入队 public void enqueue(Object e) { if (getSize() == capacity - 1) expandSpace(); elements[rear] = e; rear = (rear + 1) % capacity; } private void expandSpace() { Object[] a = new Object[elements.length * 2]; int i = front; int j = 0; while (i != rear) { // 将从front 开始到rear 前一个存储单元的元素复制到新数组 a[j++] = elements[i]; i = (i + 1) % capacity; } elements = a; capacity = elements.length; front = 0; rear = j; // 设置新的队首、队尾指针 } // 队首元素出队 public Object dequeue() throws QueueEmptyException { if (isEmpty()) throw new QueueEmptyException("错误:队列为空"); Object obj = elements[front]; elements[front] = null; front = (front + 1) % capacity; return obj; } // 取队首元素 public Object peek() throws QueueEmptyException { if (isEmpty()) throw new QueueEmptyException("错误:队列为空"); return elements[front]; } }
2.3队列的链式存储实现
队列的链式存储可以使用单链表来实现。为了操作实现方便,这里采用带头结点的单链表结构。根据单链表的特点,选择链表的头部作为队首,链表的尾部作为队尾。除了链表头结点需要通过一个引用来指向之外,还需要一个对链表尾结点的引用,以方便队列的入队操作的实现。为此一共设置两个指针,一个队首指针和一个队尾指针,如图所示。队首指针指向队首元素的前一个结点,即始终指向链表空的头结点,队尾指针指向队列当前队尾元素所在的结点。当队列为空时,队首指针与队尾指针均指向空的头结点。
package queue; //Queue 的链式存储实现 public class QueueSLinked implements Queue { private SLNode front; private SLNode rear; private int size; public QueueSLinked() { front = new SLNode(); rear = front; size = 0; } // 返回队列的大小 public int getSize() { return size; } // 判断队列是否为空 public boolean isEmpty() { return size == 0; } // 数据元素e 入队 public void enqueue(Object e) { SLNode p = new SLNode(e, null); rear.setNext(p); rear = p; size++; } // 队首元素出队 public Object dequeue() throws QueueEmptyException { if (size < 1) throw new QueueEmptyException("错误:队列为空"); SLNode p = front.getNext(); front.setNext(p.getNext()); size--; if (size < 1) rear = front; // 如果队列为空,rear 指向头结点 return p.getData(); } // 取队首元素 public Object peek() throws QueueEmptyException { if (size < 1) throw new QueueEmptyException("错误:队列为空"); return front.getNext().getData(); } }