數據結構篇——隊列擴展(環形鏈表解決約瑟夫問題)

上一篇我們詳細的講了包,棧,隊列的高性能實現方案。本篇博客,我們一起來討論一下隊列的擴展應用。

循環隊列

顧名思義,循環隊列就是一個首尾相連的隊列。我們可以在寫之前總結出它的一些特點。

  1. 根據首尾相連,則在邏輯上必有兩個指針分別指向,且尾的下一個就是頭;
  2. 根據首位相連,則該隊列必定是長度固定的;
  3. 根據我們前篇博客,鏈表相較於數組在隊列的實現上優勢較爲明顯。如果使用鏈表來實現,則在這裏鏈表也變成了循環鏈表,也可稱作是
    大概的特點就總結出這麼多,在實際動手的時候應該會有更多的細節。

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());
    }
}

該有的註釋都已經寫在代碼中了。其實與一般隊列的差一點主要在於

  1. enqueue()的時候,如果超過最大長度,則會發生覆蓋。被覆蓋的數據就從隊列頭部開始。
  2. 迭代器的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指針需要維護。但是帶來的卻是刪除的直接方便。

總結

  1. 環形鏈表需要注意時刻維護last指針與first指針的關係,不能破壞環狀結構;
  2. 當添加的節點超過maxSize大小時,會發生覆蓋情況(從邏輯隊列頭開始覆蓋,邏輯上導致first後移);
  3. 在解決約瑟夫問題時,需要添加刪除節點的方法。此時使用雙向鏈表實現的隊列會更加方便。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章