队列(queue)是一种采用先进先出(FIFO)策略的抽象数据结构,即最早进队列的数据元素,一样要最早出队列。队列跟咱们排队买票同样,先来排队的确定先买票,后来排队的的后买到票。队列以下图所示: java
队列有两个重要的概念,一个叫队头,一个叫队尾,队头指向的是第一个元素,而队尾指向的是最后一个元素。队列跟栈同样也是访问受限制的,因此队列也只有两个主要的操做:入队(enqueue)操做 和 出队(dequeue)操做 。入队操做就是将一个元素添加到队尾,出队操做就是从队头取出一个元素。node
队列的底层实现能够用数组和链表,基于数组实现的队列叫做顺序队列,基于链表实现的队列叫做链式队列,下面咱们分别用数组和链表来简单的实现这两种队列。数组
无论使用那种方式来实现队列,都须要定义两个指针分别指向队头和队尾,本文中咱们用head
指向队头,tail
指向队尾,后面的示例中这将默认使用这个,有特殊的地方我会进行说明,先来看看顺序队列的入队、出队操做。bash
图中能够看出,入队时,队尾日后移动,队头保持不变,出队是队头日后移动,队尾保持不变。入队、出队操做的逻辑都比较简单,可能你有疑问的地方是:出队时为何队头要日后移动而不是一直指向数组下标为0
的位置? 为何呢?若是咱们保持队头一直指向数组下标为0
的位置,那每次出队操做后,后面的数据都须要往前挪一位,换句话说每次出队操做都须要进行数据迁移,而数据迁移的代价比较大,每次数据迁移的时间复杂度为O(n),这样会极大的影响队列的使用性能。若是咱们出队时,队头日后移动一位,这样咱们就避免每次出队都进行数据迁移,咱们只须要在只有在tail
等于数组大小且head
不等于0
时,进行一次数据迁移,将已经出队留下的空间继续供入队时使用。下图是数据迁移的过程:微信
数据迁移时,从head
位置开始的数据都须要往前移动head
位,这样就把出队后的空间腾出来,供后续入队操做使用。数据结构
/**
* 基于数组的队列
*/
public class ArrayQueue {
// 存放数据的数组
private String[] items;
// 容器的大小
private int size = 0;
// 第一个节点
private int head = 0;
// 最后一个节点
private int tail = 0;
// 构造函数
public ArrayQueue(int size){
this.size = size;
items = new String[size];
}
/**
* 入队操做
* @param data
* @return
*/
public int enqueue(String data){
// 若是最后一个节点等于容器大小,说明队列满了
/**
* 判断队列满了的条件,tail = size,head = 0,
*/
if (tail == size && head == 0) return -1;
/**
* 若是tail = size,可是head != 0,说明前有数据删除,队列未满,须要数据迁移
*/
if (tail == size){
// head 后面的数据都须要往前迁移 head 位
for (int i= head;i< size;i++){
items[i-head] = items[i];
}
// 将最后一个元素迁移 head 位
tail -=head;
// 第一个元素指向 0
head = 0;
}
// 向队列中添加元素
items[tail] = data;
tail++;
return 1;
}
/**
* 出队操做
* @return
*/
public String dequeue(){
// 第一个元素和最后一个元素相等时,队列为空
if (head == tail) return null;
String result = items[head];
// 第一个元素后移一次,这样作的好处是在出队时不须要数据迁移
head ++ ;
return result;
}
}
复制代码
链式队列实现起来相对顺序队列来讲要简单不少,咱们先来看看链式队列的入队、出队操做:函数
tail
的
next
指向新增的节点,而后将
tail
指向新增的节点,出队操做时,将
head
节点指向
head.next
节点。链式队列与顺序队列比起来不须要进行数据的迁移,可是链式队列增长了存储成本。
/** * 基于链表的队列 */
public class LinkQueue {
// 指向队首
private Node head;
// 指向队尾
private Node tail;
/** * 入队操做 * @param data * @return */
public int enqueue(String data){
Node node = new Node(data,null);
// 判断队列中是否有元素
if (tail == null) {
tail = node;
head = node;
}else {
tail.next = node;
tail = node;
}
return 1;
}
/** * 出队操做 * @return */
public String dequeue(){
if (head==null) return null;
String data = head.data;
head = head.next;
// 取出元素后,头指针为空,说明队列中没有元素,tail也须要制为空
if (head == null){
tail = null;
}
return data;
}
class Node{
private String data;
private Node next;
public Node(String data,Node node){
this.data = data;
next = node;
}
}
}
复制代码
循环队列是对顺序队列的改进,由于顺序队列不可避免的数据迁移操做,数据迁移操做会致使队列的性能降低,为了不这个问题,将队列改形成循环的,当tail
到达数组的最大下标时,从新指回数组下标为0
的位置,这样就避免了数据迁移。先来看看循环队列的出队、入队操做:性能
tail
、
head
进行简单的加
1
操做,咱们须要对
tail
、
head
加
1
后与数组的大小进行求余操做,来得出
tail
、
head
的值,这样才能进行循环操做。循环队列须要牺牲一个存储空间,对于一个存储空间为
n
的循环队列来讲只能存放
n-1
为数据,由于若是不牺牲一个存储空间的话,当
tail==head
时,就有可能存在队空或者队满的状况。
/** * 环形队列,不须要数据迁移,提升性能 */
public class CircularQueue {
// 存放数据的数组
private String[] items;
// 容器的大小
private int size = 0;
// 第一个节点
private int head = 0;
// 最后一个节点
private int tail = 0;
// 构造函数
public CircularQueue(int size){
this.size = size;
items = new String[size];
}
/** * 入队操做 * @param data * @return */
public int enqueue(String data){
/** * 判断环形队列满了的条件,(tail+1)求余等于head */
if ((tail+1)%size == head) return -1;
// 向队列中添加元素
items[tail] = data;
// 由于是环形队列,因此下边是数组长度的余数
tail= (tail+1)%size;
return 1;
}
/** * 出队操做 * @return */
public String dequeue(){
// 第一个元素和最后一个元素相等时,队列为空
if (head == tail) return null;
String result = items[head];
// 由于是环形队列,因此下边是数组长度的余数
head = (head+1)% size ;
return result;
}
}
复制代码
双端队列是一种队头、队尾均可以进行入队、出队操做的队列,双端队列采用双向链表来实现,先来看一下双端队列的入队、出队操做:ui
能够从动态图中看出,双端队列的每一端都是一个栈,都符合栈先进后出的特性,若是咱们对双端队列进行禁止队头入队和队尾出队操做的限制,双端队列又变成了一个链式队列,双端队列是一种多功能的数据结构,咱们可使用它来提供队列和栈两种功能。this
/** * 双端队列,使用双向链表实现 */
public class DoubleEndsQueue {
private static class Node {
String item;
Node next;
Node prev;
Node(Node prev, String element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
// 第一个节点
private Node first;
// 最后一个节点
private Node last;
/* * 在第一个节点前面入队 */
public void enqueueFirst(String e) {
final Node f = first;
final Node newNode = new Node(null, e, f);
// 第一个节点指向新节点
first = newNode;
if (f == null)
// 最后一个节点也指向该节点
last = newNode;
else
// 当前节点的前节点指向新节点
f.prev = newNode;
}
/** * 在最后一个元素后面入队 * @param e */
public void enqueueLast(String e) {
final Node l = last;
final Node newNode = new Node(l, e, null);
// 最后一个节点指向新节点
last = newNode;
if (l == null)
// 第一个节点指向新节点
first = newNode;
else
// 当前节点的下节点指向新节点
l.next = newNode;
}
/** * 从第一个节点出队 * @return */
public String dequeueFirst() {
if (first == null) return null;
final Node f = first;
String element = f.item;
Node next = f.next;
f.item = null;
f.next = null;
// 第一个节点指向当先节点的next节点
first = next;
if (next == null)
// 说明队列为空
last = null;
else
next.prev = null;
return element;
}
/** * 从最后节点出队 * @return */
public String dequeueLast() {
final Node l = last;
if (last == null) return null;
String element = l.item;
Node prev = l.prev;
l.item = null;
l.prev = null;
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
return element;
}
// 输出队列所有内容
public void displayAll() {
while (first !=null){
System.out.print(first.item+" ");
first = first.next;
}
System.out.println("===============");
}
}
复制代码
优先队列为一种没必要遵循队列先进先出(FIFO)特性的特殊队列,优先队列跟普通队列同样都只有一个队头和一个队尾而且也是从队头出队,队尾入队,不过在优先队列中,每次入队时,都会按照入队数据项的关键值进行排序(从大到小、从小到大),这样保证了关键字最小的或者最大的项始终在队头,出队的时候优先级最高的就最早出队,这个就像咱们医院就医同样,急救的病人要比普通的病人先就诊。一块儿来看看优先队列的出队、入队操做:
在示例中,咱们规定数值越小优先级越高。咱们每执行一次入队操做时,小的元素都会靠近头队,在出队的时候,元素小的也就先出队。
这里使用的数组实现优先队列,用数组实现主要缘由是更好理解优先队列的思想。通常都是使用堆来实现优先队列,由于数组实如今插入的时候对数据的排序代价比较大。
/** * 优先队列 */
public class PriorityQueue {
// 存放数据的数组
private Integer[] items;
// 容器的大小
private int size = 0;
// 第一个节点
private int head = 0;
// 构造函数
public PriorityQueue(int size){
this.size = size;
items = new Integer[size];
}
/** * 入队操做 * @param data * @return */
public int enqueue(Integer data){
int j;
if (head == 0){
items[head++] = data;
}
else {
for (j=head-1;j>=0;j--){
// 将小的数日后排
if (data > items[j]){
items[j+1] = items[j];
}else {
break;
}
}
items[j+1] = data;
head++;
}
return 1;
}
public Integer dequeue(){
return items[--head];
}
}
复制代码
打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一块儿进步吧。