上一篇我们详细的讲了包,栈,队列的高性能实现方案。本篇博客,我们一起来讨论一下队列的扩展应用。
循环队列
顾名思义,循环队列就是一个首尾相连的队列。我们可以在写之前总结出它的一些特点。
- 根据首尾相连,则在逻辑上必有两个指针分别指向头和尾,且尾的下一个就是头;
- 根据首位相连,则该队列必定是长度固定的;
- 根据我们前篇博客,链表相较于数组在队列的实现上优势较为明显。如果使用链表来实现,则在这里链表也变成了循环链表,也可称作是环。
大概的特点就总结出这么多,在实际动手的时候应该会有更多的细节。
CircleQueue
public class CircleQueue<T> implements Iterable<T> {
private Node first;
private Node last;
private int size;
/**
* 队列的总长度
*/
private int maxSize;
CircleQueue(int maxSize) {
this.maxSize = maxSize;
}
/**
* 循环队列的enqueue
* 一直移动last
* 然后要时刻保证last.next = first
* 当队列放满了 如果要继续添加就要往下移动一个first 再增加last 相当于覆盖了第一个节点
*/
public void enqueue(T value) {
boolean ifAddSize = false;
if (size + 1 <= maxSize) {
ifAddSize = true;
}
Node tempNode = new Node(value);
if (isEmpty()) {
first = tempNode;
last = tempNode;
first.next = last;
size++;
return;
}
if (isFull()) {
first = first.next;
}
last.next = tempNode;
last = tempNode;
last.next = first;
if (ifAddSize) {
size++;
}
}
private boolean isEmpty() {
return first == null;
}
private boolean isFull() {
return size == maxSize;
}
public T dequeue() {
//如果队列空了则返回null
if (isEmpty()) {
System.out.println("队列空了");
return null;
}
T topValue = first.item;
//如果只剩一个了 就全部变成null
if (first == last) {
first = last = null;
} else {
first = first.next;
last.next = first;
}
size--;
return topValue;
}
public int size() {
return this.size;
}
@Override
public Iterator<T> iterator() {
return new QueueIterator();
}
class QueueIterator implements Iterator<T> {
Node current = first;
int currentSize;
@Override
public boolean hasNext() {
return currentSize < maxSize;
}
@Override
public T next() {
T tempItem = current.item;
current = current.next;
currentSize++;
return tempItem;
}
}
class Node {
Node(T item) {
this.item = item;
}
Node next;
T item;
}
/**
* 用于测试泛型的自定义类
*/
private static class Fruit {
public Fruit(String name, Double price) {
this.name = name;
this.price = price;
}
String name;
Double price;
}
public static void main(String[] args) {
CircleQueue<Fruit> fruitQueue = new CircleQueue<>(3);
Fruit apple = new Fruit("苹果", 5.1);
Fruit grape = new Fruit("葡萄", 6.7);
Fruit orange = new Fruit("橘子", 3.2);
fruitQueue.enqueue(apple);
fruitQueue.enqueue(grape);
fruitQueue.enqueue(grape);
fruitQueue.enqueue(orange);
fruitQueue.enqueue(orange);
fruitQueue.enqueue(orange);
int size = fruitQueue.size();
System.out.println(size);
for (Fruit fruit : fruitQueue) {
System.out.println(fruit.name + " " + fruit.price);
}
System.out.println("开始dequeue数据");
for (int i = 0; i < size; i++) {
System.out.println(fruitQueue.dequeue().name);
}
//最后一次检查有没有空
System.out.println(fruitQueue.dequeue());
}
}
该有的注释都已经写在代码中了。其实与一般队列的差一点主要在于
- enqueue()的时候,如果超过最大长度,则会发生覆盖。被覆盖的数据就从队列头部开始。
- 迭代器的hasNext()方法,需要根据当前长度与maxSize进行比较
循环队列的应用——约瑟夫问题
约瑟夫问题简介
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
——摘自百度百科
通俗的讲,就是一个环形队列。从first开始,每间隔M个,就从队列中dequeue出来。直到size()==1为止。
单链表解决方法
/**
* 解决约瑟夫问题
*
* @param interval 间隔
*/
public void solveJosephusProblem(int interval) {
Node temp = first;
//单项链表删除节点需要辅助节点 要删除节点的前一个节点
Node pre = first;
while (temp.next != temp) {
int i = 1;
while (i <= interval) {
pre = temp;
temp = temp.next;
i++;
}
System.out.print(temp.item + " ");
pre.next = temp.next;
temp = pre.next;
}
System.out.println();
System.out.println("最后留在队列中的是:" + temp.item);
}
测试代码:
public static void main(String[] args) {
CircleQueue<Integer> josephusCircle = new CircleQueue<>(6);
for (int i = 1; i <= 6; i++) {
josephusCircle.enqueue(i);
}
//第5个被干掉 则说明是间隔4个
josephusCircle.solveJosephusProblem(4);
}
最后输出结果为:
5 4 6 2 3
最后留在队列中的是:1
双向链表实现环形队列
约瑟夫问题涉及到了队列的删除。从上面单向链表的实现中,我们发现删除一个节点必须要辅助节点的参与。其实解决删除更好的方式是双向链表,能够自己把自己从链表中删除。
/**
* 双向链表实现循环队列
*/
public class DoubleLinkedListCircleQueue<T> implements Iterable<T> {
public DoubleLinkedListCircleQueue(int maxSize) {
this.maxSize = maxSize;
}
private Node first;
private Node last;
private int size;
private int maxSize;
/**
* 双向链表需要额外维护一个pre指针
*/
public void enqueue(T value) {
Node tempNode = new Node(value);
if (isEmpty()) {
first = tempNode;
last = tempNode;
first.next = last;
first.pre = last;
size++;
return;
}
boolean ifAddSize = true;
if (isFull()) {
//先把当前节点指向下一个节点 再删除当前节点的上一个节点(也就是删除自己)
first = first.next;
dequeue(first.pre);
ifAddSize = false;
}
last.next = tempNode;
tempNode.pre = last;
last = tempNode;
last.next = first;
first.pre = last;
if (ifAddSize) {
size++;
}
}
/**
* 删除first
*/
public T dequeue() {
//如果队列空了则返回null
if (isEmpty()) {
System.out.println("队列空了");
return null;
}
T topValue = first.item;
//如果只剩一个了 就全部变成null
if (first == last) {
first = last = null;
} else {
first = first.next;
first.pre = last;
last.next = first;
}
size--;
return topValue;
}
/**
* 解决约瑟夫问题
*
* @param interval 间隔
*/
public void solveJosephusProblem(int interval) {
Node temp = first;
while (temp.next != temp) {
int i = 1;
while (i <= interval) {
temp = temp.next;
i++;
}
System.out.print(temp.item + " ");
//先把当前节点指向下一个节点 再删除当前节点的上一个节点(也就是删除自己)
temp = temp.next;
dequeue(temp.pre);
}
System.out.println();
System.out.println("最后留在队列中的是:" + temp.item);
}
/**
* 删除链表中的指定节点
*/
private T dequeue(Node aimNode) {
T result = aimNode.item;
aimNode.next.pre = aimNode.pre;
aimNode.pre.next = aimNode.next;
return result;
}
public boolean isEmpty() {
return first == null;
}
public boolean isFull() {
return size == maxSize;
}
public int size() {
return this.size;
}
@Override
public Iterator<T> iterator() {
return new DoubleLinkedListCircleQueueIterator();
}
private class DoubleLinkedListCircleQueueIterator implements Iterator<T> {
private int currentSize;
Node current = first;
@Override
public boolean hasNext() {
return currentSize < size;
}
@Override
public T next() {
T tempItem = current.item;
current = current.next;
currentSize++;
return tempItem;
}
}
private class Node {
public Node(T item) {
this.item = item;
}
Node pre;
Node next;
T item;
}
/**
* 用于测试泛型的自定义类
*/
private static class Fruit {
public Fruit(String name, Double price) {
this.name = name;
this.price = price;
}
String name;
Double price;
}
public static void main(String[] args) {
DoubleLinkedListCircleQueue<Fruit> fruitQueue = new DoubleLinkedListCircleQueue<>(3);
Fruit apple = new Fruit("苹果", 5.1);
Fruit grape = new Fruit("葡萄", 6.7);
Fruit orange = new Fruit("橘子", 3.2);
fruitQueue.enqueue(apple);
fruitQueue.enqueue(grape);
fruitQueue.enqueue(grape);
fruitQueue.enqueue(orange);
fruitQueue.enqueue(orange);
fruitQueue.enqueue(orange);
int size = fruitQueue.size();
System.out.println(size);
for (Fruit fruit : fruitQueue) {
System.out.println(fruit.name + " " + fruit.price);
}
System.out.println("开始dequeue数据");
for (int i = 0; i < size; i++) {
System.out.println(fruitQueue.dequeue().name);
}
//最后一次检查有没有空
System.out.println(fruitQueue.dequeue());
DoubleLinkedListCircleQueue<Integer> josephusCircle = new DoubleLinkedListCircleQueue<>(6);
for (int i = 1; i <= 6; i++) {
josephusCircle.enqueue(i);
}
//第5个被干掉 则说明是间隔4个
josephusCircle.solveJosephusProblem(4);
}
}
相比於单项链表,双向链表只是多了一个pre指针需要维护。但是带来的却是删除的直接方便。
总结
- 环形链表需要注意时刻维护last指针与first指针的关系,不能破坏环状结构;
- 当添加的节点超过maxSize大小时,会发生覆盖情况(从逻辑队列头开始覆盖,逻辑上导致first后移);
- 在解决约瑟夫问题时,需要添加删除节点的方法。此时使用双向链表实现的队列会更加方便。