資料結構與演算法——佇列(環形佇列)

天然呆dull 發表於 2021-08-26
演算法 資料結構

一個使用場景

銀行辦理業務的排隊叫號

資料結構與演算法——佇列(環形佇列)

辦理業務的人先拿號,然後視窗叫號處理,沒有叫到的,則排隊等待。

基本介紹

佇列:是一個 有序列表,可以用 陣列連結串列 實現。

特點:遵循 先入先出 原則。即:先存入的資料,先取出。

示意圖:

資料結構與演算法——佇列(環形佇列)

  • front:隊首,佇列頭部
  • rear:隊尾,佇列尾部
  • 左 1 圖:佇列初始化的兩個變數值
  • 中圖:存入資料後,的首位變化
  • 右圖:取資料時,從隊首取,隊首的變數指向也在發生變化

陣列模擬佇列

佇列本身是 有序列表,使用陣列結構來儲存佇列的資料,則如前面基本介紹中的示意圖一樣。

宣告 4 個變數:

  • arr:用來儲存資料的陣列
  • maxSize:該佇列的最大容量
  • front:隊首下標,隨著資料輸出而改變
  • rear:隊尾下標,隨著資料輸入而改變

佇列中常用操作分析,以 add,把資料存入佇列為例,思路分析:

  1. 將尾指標往後移:rear + 1,前提是當 front == rear 時,佇列是空的
  2. 若尾指標 rear < maxSize -1
    • 則將資料存入 rear 所指的陣列元素中,
    • 否則無法存入資料。rear = maxSize -1 表示佇列滿了

以上思路是一個最基本的實現(不是完美的,看完程式碼就明白了)。程式碼實現如下

/**
 * 陣列模擬佇列
 */
public class ArrayQueueDemo {
    public static void main(String[] args) {
        ArrayQueue queue = new ArrayQueue(3);
        queue.add(1);
        queue.add(2);
        queue.add(3);
        System.out.println("檢視佇列中的資料");
        queue.show();
        System.out.println("檢視佇列頭資料:" + queue.head());
        System.out.println("檢視佇列尾資料:" + queue.tail());
//        queue.add(4);
        System.out.println("獲取佇列資料:" + queue.get());
        System.out.println("檢視佇列中的資料");
        queue.show();

    }
}

class ArrayQueue {
    private int maxSize; // 佇列最大容量
    private int front; // 佇列頭,指向佇列頭的前一個位置
    private int rear; // 佇列尾,指向佇列尾的資料(及最後一個資料)
    private int arr[]; // 用於儲存資料,模擬佇列

    public ArrayQueue(int arrMaxSize) {
        maxSize = arrMaxSize;
        arr = new int[maxSize];
        front = -1;
        rear = -1;
    }

    /**
     * 取出佇列資料
     */
    public int get() {
        if (isEmpty()) {
            throw new RuntimeException("佇列空");
        }
        return arr[++front];
    }

    /**
     * 往佇列儲存資料
     */
    public void add(int n) {
        if (isFull()) {
            System.out.println("佇列已滿");
            return;
        }
        arr[++rear] = n;
    }

    /**
     * 顯示佇列中的資料
     */
    public void show() {
        if (isEmpty()) {
            System.out.println("佇列為空");
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.printf("arr[%d] = %d \n", i, arr[i]);
        }
    }

    /**
     * 檢視佇列的頭部資料,注意:不是取出資料,只是檢視
     *
     * @return
     */
    public int head() {
        if (isEmpty()) {
            throw new RuntimeException("佇列空");
        }
        return arr[front + 1]; // front 指向佇列頭前一個元素,取頭要 +1
    }

    /**
     * 檢視隊尾資料
     *
     * @return
     */
    public int tail() {
        if (isEmpty()) {
            throw new RuntimeException("佇列空");
        }
        return arr[rear];
    }

    // 佇列是否已滿
    private boolean isFull() {
        return rear == maxSize - 1;
    }

    // 佇列是否為空
    private boolean isEmpty() {
        return rear == front;
    }
}

執行測試

檢視佇列中的資料
arr[0] = 1 
arr[1] = 2 
arr[2] = 3 
檢視佇列頭資料:1
檢視佇列尾資料:3
獲取佇列資料:1
檢視佇列中的資料
arr[0] = 1 
arr[1] = 2 
arr[2] = 3 

分析

目前實現了一個 一次性的佇列(不能複用),因為可以往佇列中新增資料,基本功能也是可以的,當佇列滿之後,再新增就加不進去了,獲取資料也不能清空原佇列中的資料。

優化方向:使用演算法將這個陣列改進成一個環形佇列。

陣列模擬環形佇列

思路分析

資料結構與演算法——佇列(環形佇列)

  1. front:含義調整

    表示:佇列的第一個元素,也就是說 arr[front] 就是佇列的第一個元素

    初始值:0

  2. rear:含義調整

    表示:佇列的最後一個元素的下一個位置

    初始值(只是初始值):0

    這個很重要,是一個小演算法,能更方便的實現我們的環形佇列。

  3. 佇列 滿 計算公式:(rear + 1) % maxSize == front

  4. 佇列 計算公式:rear == front

  5. 佇列中 有效元素個數 計算公式:(rear + maxSize - front) % maxSize

為了能更清晰這個演算法,下面畫圖來演示佇列中元素個數,關鍵變數的值

資料結構與演算法——佇列(環形佇列)

該演算法取巧的地方在於 rear 的位置,注意看上圖,rear 所在的位置 永遠是空的,實現環形佇列的演算法也有多種,這裡空出來一個位置,是這裡演算法的核心。所以,迴圈佇列會浪費一個陣列的儲存空間。

程式碼實現

/**
 * 陣列擬環形佇列
 */
public class CircleQueueDemo {
    public static void main(String[] args) {
        CircleQueue queue = new CircleQueue(3);

        // 為了測試方便,寫一個控制檯輸入的小程式
        Scanner scanner = new Scanner(System.in);
        boolean loop = true;
        char key = ' '; // 接受使用者輸入指令
        System.out.println("s(show): 顯示佇列");
        System.out.println("e(exit): 退出程式");
        System.out.println("a(add): 新增資料到佇列");
        System.out.println("g(get): 從佇列取出資料");
        System.out.println("h(head): 檢視佇列頭的資料");
        System.out.println("t(tail): 檢視佇列尾的資料");
        System.out.println("p(isEmpty): 佇列是否為空");
        while (loop) {
            key = scanner.next().charAt(0);
            switch (key) {
                case 's':
                    queue.show();
                    break;
                case 'e':
                    loop = false;
                    break;
                case 'a':
                    System.out.println("請輸入要新增到佇列的整數:");
                    int value = scanner.nextInt();
                    queue.add(value);
                    break;
                case 'g':
                    try {
                        int res = queue.get();
                        System.out.printf("取出的資料是:%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h':
                    try {
                        int res = queue.head();
                        System.out.printf("隊首資料:%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 't':
                    try {
                        int res = queue.tail();
                        System.out.printf("隊尾資料:%d\n", res);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'p':
                    System.out.printf("佇列是否為空:%s", queue.isEmpty());
                    break;
            }
        }
    }
}

class CircleQueue {
    private int maxSize; // 佇列最大容量
    private int front; // 佇列頭,指向 隊頭 的元素
    private int rear; // 佇列尾,指向 隊尾 的下一個元素
    private int arr[]; // 用於儲存資料,模擬佇列

    public CircleQueue(int arrMaxSize) {
        maxSize = arrMaxSize + 1;
        arr = new int[maxSize];
        front = 0;
        rear = 0;
    }

    /**
     * 取出佇列資料
     */
    public int get() {
        if (isEmpty()) {
            throw new RuntimeException("佇列空");
        }
        // front 指向的是隊首的位置
        int value = arr[front];
        // 需要向後移動,但是由於是環形,同樣需要使用取模的方式來計算
        front = (front + 1) % maxSize;
        return value;
    }

    /**
     * 往佇列儲存資料
     */
    public void add(int n) {
        if (isFull()) {
            System.out.println("佇列已滿");
            return;
        }
        arr[rear] = n;
        // rear 指向的是下一個位置
        // 由於是環形佇列,需要使用取模的形式來喚醒他的下一個位置
        rear = (rear + 1) % maxSize;
    }

    /**
     * 顯示佇列中的資料
     */
    public void show() {
        if (isEmpty()) {
            System.out.println("佇列為空");
            return;
        }
        // 列印的時候,需要從隊首開始列印
        // 列印的次數則是:有效的元素個數
        // 獲取資料的下標:由於是環形的,需要使用取模的方式來獲取
        for (int i = front; i < front + size(); i++) {
            int index = i % maxSize;
            System.out.printf("arr[%d] = %d \n", index, arr[index]);
        }
    }

    /**
     * 檢視佇列的頭部資料,注意:不是取出資料,只是檢視
     *
     * @return
     */
    public int head() {
        if (isEmpty()) {
            throw new RuntimeException("佇列空");
        }
        return arr[front];
    }

    /**
     * 檢視隊尾資料
     *
     * @return
     */
    public int tail() {
        if (isEmpty()) {
            throw new RuntimeException("佇列空");
        }
        // rear - 1 是隊尾資料,但是如果是環形收尾相接的時候
        // 那麼 0 -1 就是 -1 了,負數時,則是陣列的最後一個元素
        return rear - 1 < 0 ? arr[maxSize - 1] : arr[rear - 1];
    }

    // 佇列是否已滿
    private boolean isFull() {
        return (rear + 1) % maxSize == front;
    }

    // 佇列是否為空
    public boolean isEmpty() {
        return rear == front;
    }

    // 有效個數
    public int size() {
        return (rear + maxSize - front) % maxSize;
    }
}
    

執行測試功能輸出

s(show): 顯示佇列
e(exit): 退出程式
a(add): 新增資料到佇列
g(get): 從佇列取出資料
h(head): 檢視佇列頭的資料
t(tail): 檢視佇列尾的資料
p(isEmpty): 佇列是否為空
a
請輸入要新增到佇列的整數:
10
a
請輸入要新增到佇列的整數:
20
a
請輸入要新增到佇列的整數:
30
s
arr[0] = 10 
arr[1] = 20 
arr[2] = 30 
a
請輸入要新增到佇列的整數:
40
佇列已滿
h
隊首資料:10
t
隊尾資料:30
g
取出的資料是:10
s
arr[1] = 20 
arr[2] = 30 
a
請輸入要新增到佇列的整數:
40
s
arr[1] = 20 
arr[2] = 30 
arr[3] = 40 
h
隊首資料:20
t
隊尾資料:40
g
取出的資料是:20
s
arr[2] = 30 
arr[3] = 40 
a
請輸入要新增到佇列的整數:
50
s
arr[2] = 30 
arr[3] = 40 
arr[0] = 50 
h
隊首資料:30
t
隊尾資料:50
    

可以看到上面的表現,和那個圖解分析是一致的, rear 所在的位置沒有元素,是動態的。