当然队列和栈拥有的操作一样,只有入队列和出队列两个操作:
- 入队列:在队尾加入一个元素
- 出队列:在队头取出一个元素
就像链表可以自由在中间任意某个位置插入和删除,而栈和队列只能在固定的位置操作。这其实就是创造者们所定义的,我们只有给予其一定的限制,才能获得我们想要的特性。
具体实现
顺序队列
当然,用数组实现的队列叫顺序队列,接下上完整代码吧。
public class Queue {
private String[] datas;//数组
private int n;//数组大小
private int head;//队头下标
private int tail;//队尾下标
//初始化一个队列
public Queue(int capacity) {
this.datas = new String[capacity];
this.n = capacity;
}
//入队操作
public void enQueue(String data) {
if (n == tail) {
System.out.println("队列已满");
} else {
datas[tail] = data;
tail++;
}
}
//出队操作
public String deQueue() {
if (head == tail) {
System.out.println("队列为空");
return null;
} else {
String temp = datas[head];
head++;
return temp;
}
}
//判断队列是否为空
public boolean isEmpty() {
if (head == tail)
return true;
return false;
}
public static void main(String[] args) {
Queue queue = new Queue(5);
queue.enQueue("1");
queue.enQueue("2");
queue.enQueue("3");
while (!queue.isEmpty()) {
System.out.println(queue.deQueue());
}
}
}
数组实现的队列代码如上,示意图如下:
我们看源码的时候,有时候总说底层是啥实现的,其实就是要主要他们的特性,比如我这里底层是「数组」实现的,**那么我们在写代码的时候就要遵循底层(数组)的规律。**而不是像示意图那样,队尾插入就真的是从右往左添加数据。
我们对这个图片的操作进行截图:
初始化:没啥好说的,就是申请了一个空数组。
enQueue(入队操作):tail++:表示一个一个往后面加,
而不是像这张图中最上面的示意图,直接从右边往左边加,这就是示意图和具体代码的具体。tip:我们使用数组实现的哦,数组就是从0开始往后加
deQueue(出队操作):head++:表示一个一个出队列(数据还存在在数组中,只是我们访问不到了)
Tip:当 head=tail 时,就表示数组为空,此时不能出队列tip:那当 tail 指向数组最后一个位置了呢?即 n=tail 那就是数组用完了,不能插入
数组有空闲但是不能用
呃呃,这个问题就是上面的当我的 head=tail,其实此时 head 以前的位置都已经“出队列”了,所以以前的位置还可以使用,那么该怎么办呢。
对,就是数据搬移,将此时head后面位置的元素移动从下标为 0 开始的位置。如下图:
因为我们只需要在入队的时候判断是否有空间,所以我们只要改造 enQueue() 函数即可。当没有空间的时候(即:tail = n ),我们再一次性的搬移。
n:是初始化数组的长度。
//入队操作
public void enQueue(String data) {
if (n == tail) {
if (head == 0) {//就是整个队列满了,没法搬移
System.out.println("队列已满");
} else {
for (int i = head; i < tail; i++) {
//此时数组剩下的所以元素:即有 head到tail 个
datas[i - head] = datas[i];
}
//数据搬移完,记得改变“指针”位置哦
tail = tail - head;
head = 0;
}
}
datas[tail] = data;
tail++;
}
public static void main(String[] args) {
Queue queue = new Queue(5);
queue.enQueue("1");
queue.enQueue("2");
queue.enQueue("3");
queue.enQueue("4");
queue.enQueue("5");
queue.deQueue();
queue.deQueue();
for (int i = queue.head; i < 5; i++) {
System.out.println(queue.datas[i]);
}
}
上面我只贴出了enQueue() 函数,和测试代码
enQueue() :
- 判断(n == tail && head == 0)是否成立;
- 搬移元素,并修改 head/tail 指针指向的位置
测试代码:
- 将数组插满
- 删除两个元素
- 从此时的 head 指向的位置看是遍历: for (int i = queue.head; i < 5; i++);而不是 for (int i = 0; i < 5; i++);后者会打印出12345;why?我们不是将数组移除队列了吗?
- 其实我们在内存中分配的数据还在这个数组里面没有变,也没有移动。我们只是通过移动 “指针” 去访问指定的位置,因为这是队列,我们是无法访问 head 以前的数据,所以我们把它看成是被移除队列,但它其实还真实的存在这个数组里面。
链式队列
class Node {
String datas;
Node next;
public Node() {
}
public Node(String data) {
this.datas = data;
}
}
public class Queue {
private Node head = null;
private Node tail = null;
//入队列
public void enQueue(String data) {
Node dataNode = new Node(data);
if (head == null) {//当队列为空时,把 head 和 tail 都同时赋给第一个节点
//这里就保证了遍历此链表时:head 是第一个节点
head = dataNode;
tail = dataNode;
} else {
tail.next = dataNode;
tail = dataNode;
}
}
//出队列
public String deQueue() {
String temp;
if (head != null) {//有元素
temp = head.datas;
head = head.next;
return temp;
} else {
return null;
}
}
public void print() {
Node p = head; //在验证代码时,防止 print 一次,链表中“没数据”了
while (p != null) {
System.out.print(p.datas + "->");
p = p.next;
}
System.out.println("null");
}
public static void main(String[] args) {
Queue queue = new Queue();
queue.enQueue("1");
queue.enQueue("2");
queue.enQueue("3");
queue.print();
queue.deQueue();
queue.print();//如果 print 不用临时变量替代 head,那么下面都会输出位 null
queue.deQueue();
queue.print();
}
}
分析:
当为空的时候,我们把 head/tail 节点都赋给第一个节点。
入队:
只需要移动 tail 即可,因为第一次赋值就保证了 head 和 tail 在同一个链表上
出队:
head 往后移即可
扩展
这里主要是先先实现基本的 Java队列功能,饭要一口一口吃,路呀一步一步走嘛。
对于扩展呢,主要是 Java 中集合库中使用了队列这种数据结构,比如:JDK 线程池中的 BlockingQueue
后续再补充诸如 循环队列、阻塞队列并发队列等,还可以加上一波对 JDK 底层源码解析。
总结
- 学会用画图辅助自己思考,可能有时候只靠想,脑子会不够用哦。画图是真香啊
- 不要觉得自己看懂了,我要我觉得你应该去自己独立的写出来。
- 如果觉得自己代码没有问题,但是结果并不是自己想象那样的,那么请 Debug。IDEA Debug 了解一下。