[Algorithm] Day.4 堆疊和佇列 (Stacks and Queues)

2023-03-12

#algorithm #note

在寫程式的時候,最常使用到的資料結構之一就是一個線性的陣列,而佇列和堆疊就是兩個非常基本的資料結構。

堆疊(Stacks)

堆疊是指執行順序為後進先出(Last-In-Frist-Out, LIFO) 的資料結構。實際操作起來的核心概念是只會在堆疊的最頂端進行操作,舉例來說如果今天依序在堆疊中加入 A, B, C, D, E 五個元素,那最先被移出堆疊的元素將會是 E

常見的應用場景包括瀏覽器的返回功能,以及編譯器編譯時的臨時記憶體。

佇列(Queue)

佇列則是類似排隊的先進先出(First-In-First-Out, FIFO)的資料結構,他的規則是只能在佇列的最後面插入和只能在最前面進行刪除。舉例來說如果今天依序在佇列中加入 A, B, C, D, E 五個元素,那最先被移出堆疊的元素將會是 A

常見的應用場景包括操作系統中的作業佇列,以及網絡中的數據包傳輸。

循環佇列 (Circular Queue)

那現在我們來講一個比較特別一點的佇列吧!

Circular Queue

Circular Queue(循環佇列)是一種特殊的佇列,與普通的佇列不同,它的佇列結構是環形的,即在一定的容量限制下,當佇列的尾指針到達了佇列的最後一個位置時,就會繞回到佇列的開頭,形成一個循環,從而可以有效地利用佇列的存儲空間。

在使用循環佇列的時候需要注意到兩個詞 front 他是一個指針,指向這個佇列中最前面那個元素的最前面一格,以及 rear 是另一個指針,他指向的是整個佇列的最後一格。

我一開始很好奇為什麼 front 指的不是佇列最前面的那個元素,原因是當今天如果 front 指向佇列中最前面那格的話,那 front == rear 的時候就會出現 佇列已滿佇列是空的 兩種情況,當這種模稜兩可的情況發生就會不利於我們判斷佇列的狀態。 但當我們把 front 指向最前面那個元素,front == rear 就只會發生在 佇列為空 的情況,這樣就可以解決上面所提到的模糊地帶。

雖然這樣做可以解決問題,但他也會產生另外一個浪費空間的問題!因為如果 front 指向的是佇列中最前面元素的前一格,就代表 front 指向的那個永遠會是空的,也就造成了記憶體的浪費。但相比於其他解決模稜兩可的辦法,這個微小的記憶體浪費是可以被接受的,所以才會被廣泛採用。

循環佇列的實現可以使用數組或鏈表等數據結構,其基本操作包括:入佇列(enqueue)、出佇列(dequeue)、查看佇列頭元素(front)、查看佇列尾元素(rear)等。其中,入佇列和出佇列的時間複雜度均為 O(1),因此循環佇列是一種高效的佇列實現方式。

// 刪除循環佇列 q 中的元素
Algorithm DeleteQ(item) {
  if(front = rear) then {
    write "Queue is empty!"; //因為繞一圈,所以如果頭尾相同的話就等於他是空的
    return false;
  } else {
    front = (front + 1) % q.size; // 把 front 的指針往後移一個位置(e.g. 0 -> 1)
    q[front] = none; // 把原本在 front 那個元素刪掉
    return true;
  }
}
// 在循環佇列 q 中插入元素
Alogrithm AddQ(item) {
  rear = (rear + 1) % q.size; //把 rear 往後移一格
  if(front == rear) then {
    write("Queue is full!"); //往後移一個之後就碰到 front 代表已經滿了
    if(front == 0) then rear = n - 1; //這個 if/else 是把剛剛移動的 rear 指針移回到原本的位子
    else rear = rear - 1;
    return false;
  } else {
    q[rear] = item;
    return true;
  }
}