上一篇我們詳細的講了包,棧,隊列的高性能實現方案。本篇博客,我們一起來討論一下隊列的擴展應用。
循環隊列
顧名思義,循環隊列就是一個首尾相連的隊列。我們可以在寫之前總結出它的一些特點。
- 根據首尾相連,則在邏輯上必有兩個指針分別指向頭和尾,且尾的下一個就是頭;
- 根據首位相連,則該隊列必定是長度固定的;
- 根據我們前篇博客,鏈表相較於數組在隊列的實現上優勢較爲明顯。如果使用鏈表來實現,則在這裏鏈表也變成了循環鏈表,也可稱作是環。
大概的特點就總結出這麼多,在實際動手的時候應該會有更多的細節。
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後移);
- 在解決約瑟夫問題時,需要添加刪除節點的方法。此時使用雙向鏈表實現的隊列會更加方便。