佇列也是一種操作受限的線性資料結構,與棧很相似。
01、定義
棧的操作受限表現為只允許在佇列的一端進行元素插入操作,在佇列的另一端只允許刪除操作。這一特性可以總結為先進先出(First In First Out,簡稱FIFO)。這意味著在佇列中第一個加入的元素將第一個被移除。
入隊:向佇列中新增新元素的行為叫做入隊;
出隊:從佇列中移除元素的行為叫做出隊;
隊頭:在佇列中允許進行元素移除行為的一端稱為隊頭;
隊尾:在佇列中執行進行元素新增行為的一端稱為隊尾;
空佇列:當佇列中沒有元素時稱為空佇列。
滿佇列:當佇列是有限容量,並且容量已用完,則稱為滿佇列。
佇列容量:當佇列是有限容量,佇列容量表示佇列可以容納的最大元素數量。
佇列大小:表示當前佇列中的元素數量。
02、分類
佇列根據儲存方式和功能特性有兩種分類方式。
1、根據儲存方式分類
佇列是邏輯結構,因此以儲存方式的不同可以分為順序佇列和鏈式佇列。
順序佇列就是所有的佇列元素都儲存在連續的地址空間中,因此可以透過陣列來實現順序佇列,因為陣列的特性也導致順序佇列容量是固定的,不易擴容,這也導致容易浪費空間,同時還要注意元素溢位等問題。
鏈式佇列顧名思義就是採用鏈式方式儲存,可以透過連結串列實現,因此鏈式佇列可以做到無限擴容,大大的提高了記憶體利用率。
2、根據功能特性分類
根據功能特性可以分類出很多專有佇列,下面我們列舉幾個簡單介紹一下。
阻塞佇列:當空佇列時會阻塞出隊操作,當滿佇列時會阻塞入隊操作。
優先佇列:佇列中每個元素都有一個優先順序別屬性,而元素的出隊操作取決於這個優先順序別屬性,即優先順序別高則優先出隊。
延遲佇列:佇列中每個元素都標記一個時間記錄,元素只有在指定的延時時間後才會觸發出隊操作。
迴圈佇列:當使用陣列實現佇列時,可以透過把隊頭和隊尾相連線,即當隊尾到達陣列的尾端可以“繞回”陣列的開頭,透過這種巧妙的設計來提高陣列空間利用率。
雙端佇列:是一種兩端都可以進行入隊和出隊操作的資料結構。
根據這些佇列特性,在不同的場景中可以起到意想不到的效果。
下面我們將順序佇列和鏈式佇列的實現進行詳細講解。
03、實現(順序佇列)
下面我們藉助陣列來實現順序佇列,其核心思想是把陣列的起始位置作為隊頭,把陣列尾方向作為隊尾。當發生出隊行為時,需要把剩餘所有資料向隊頭方向移動一位,為什麼要做這步操作呢?
首先順序佇列內部是陣列,假設陣列內可以存放7個元素,此時陣列已存滿,因此不可以再進行新增新元素入隊操作了,然後我們對陣列頭元素進行出隊操作,此時陣列因為出隊會留下一個空位,如下圖。
那麼此時是否可以進行入隊操作呢?直接告訴我們應該可以,因為陣列頭已經有空位了。但是我們約定了佇列只能從陣列尾進行入隊操作,而此時陣列尾並沒有空位提供給新入隊的元素,因此實際上無法進行入隊操作。
那要如何處理呢?最簡單的方法就是當發生出隊操作時,後面所有的元素都向著隊頭方向移動一位,把隊尾空出一位,這每出一個元素就可以入一個元素。
當然這也不是唯一方案,還是透過迴圈佇列解決這一問題,有興趣的可以研究一下。
1、ADT定義
我們首先來定義順序佇列的ADT。
ADT Queue{
資料物件:D 是一個非空的元素集合,D = {a1, a2, ..., an},其中 ai 表示佇列中的第i個元素,n是佇列的長度。
資料關係:D中的元素透過它們的索引(位置)進行組織,索引是從0到n-1的整數,並且遵循元素先進先出的原則。
基本操作:[
Init(n) :初始化一個指定容量的空佇列。
Capacity:返回佇列容量。
Length:返回佇列長度。
Head:返回隊頭元素,當為空佇列則報異常。
Tail:返回隊尾元素,當為空佇列則報異常。
IsEmpty():返回是否為空佇列。
IsFull():返回是否為滿佇列。
Enqueue():入隊即新增元素,當為滿佇列則報異常。
Dequeue():出隊即返回隊頭元素並把其從佇列中移除,當為空佇列則報異常。
]
}
定義好佇列ADT,下面我們就可以開始自己實現的佇列。
2、初始化Init
首先定義3個變數用於存放佇列元素陣列、佇列容量以及隊尾索引,而沒有定義隊頭索引是因為隊頭索引永遠等於0。
初始化結構主要做幾件事。
-
初始化佇列的容量;
-
初始化存放佇列元素陣列;
-
初始化隊尾索引;
具體實現程式碼如下:
//存放佇列元素
private T[] _array;
//佇列容量
private int _capacity;
//隊尾索引,為-1表示空佇列
private int _tail;
//初始化佇列為指定容量
public MyselfQueueArray<T> Init(int capacity)
{
//初始化佇列容量為capacity
_capacity = capacity;
//初始化指定長度陣列用於存放佇列元素
_array = new T[_capacity];
_tail = -1;
//返回佇列
return this;
}
3、獲取佇列容量 Capacity
這個比較簡單直接把佇列容量私有欄位返回即可。
//佇列容量
public int Capacity
{
get
{
return _capacity;
}
}
4、獲取佇列長度 Length
我們並沒有定義佇列長度的私有欄位,因為隊尾索引即表示陣列最後一個元素索引,即可以代表佇列長度,因此只需用隊尾索引加1即可得到佇列長度,同時需要注意判斷佇列是否為空,如果為空則報錯。
//佇列長度
public int Length
{
get
{
if (IsEmpty())
{
return 0;
}
//佇列長度等於隊尾索引加1
return _tail + 1;
}
}
5、獲取隊頭元素 Head
基於我們上面的約定,隊頭元素永遠對應陣列的第一個元素,因此可以直接獲取索引為0的陣列元素。空佇列則報錯。具體程式碼如下:
//獲取隊頭元素
public T Head
{
get
{
if (IsEmpty())
{
//空佇列,不可以進行獲取隊頭元素操作
throw new InvalidOperationException("空佇列");
}
return _array[0];
}
}
6、獲取隊尾元素 Tail
因為我們定義了隊尾索引私有變數,因此可以直接透過隊尾索引獲取。具體程式碼如下:
//獲取隊尾元素
public T Tail
{
get
{
if (IsEmpty())
{
//空佇列,不可以進行獲取隊頭元素操作
throw new InvalidOperationException("空佇列");
}
return _array[_tail];
}
}
7、獲取是否空佇列 IsEmpty
是否空佇列只需判斷隊尾索引是否小於0即可。
//是否空佇列
public bool IsEmpty()
{
//隊尾索引小於0表示空佇列
return _tail < 0;
}
8、獲取是否滿佇列 IsFull
是否滿佇列只需判斷隊尾索引是否與佇列容量減1相等,程式碼如下:
//是否滿佇列
public bool IsFull()
{
//隊頭索引等於容量大小減1表示滿佇列
return _tail == _capacity - 1;
}
9、入隊 Enqueue
入隊只需向佇列內部陣列尾新增一個新元素即可,因此先把隊尾索引先後移動一位,然後再把新元素賦值給隊尾元素,同時還需要檢查是否為滿佇列,如果是滿佇列則報錯,具體實現程式碼如下:
//入隊
public void Enqueue(T value)
{
if (IsFull())
{
//滿佇列,不可以進行入佇列操作
throw new InvalidOperationException("滿佇列");
}
//隊尾索引向後移動1位
_tail++;
//給隊尾元素賦值新值
_array[_tail] = value;
}
10、出隊 Dequeue
出隊則大致分為以下幾步:
-
判斷是否為空佇列,空佇列則報錯;
-
取出隊頭元素暫存,重置隊頭元素為預設值;
-
把隊頭後面所有元素向隊頭方向移動一位;
-
重置隊尾元素為預設值;
-
隊尾索引向隊頭方向移動一位,即隊尾索引減1;
-
返回暫存的隊頭元素;
具體實現程式碼如下:
//出隊
public T Dequeue()
{
if (IsEmpty())
{
//空佇列,不可以進行出佇列操作
throw new InvalidOperationException("空佇列");
}
//取出隊頭元素
var value = _array[0];
//對頭元素重置為預設值
_array[0] = default;
//隊頭元素後面所有元素都向隊頭移動一位
for (int i = 0; i < _tail; i++)
{
_array[i] = _array[i + 1];
}
//隊尾元素重置為預設值
_array[_tail] = default;
//隊尾索引向隊頭方向移動一位
_tail--;
//返回隊頭元素
return value;
}
04、實現(鏈式佇列)
我們藉助連結串列來實現鏈式佇列,其核心思想是把連結串列尾節點作為隊尾,把連結串列首元節點作為隊頭。
1、ADT定義
相對於順序佇列的ADT來說,鏈式佇列的ADT少了兩個方法即獲取佇列容量和是否滿佇列,這也是連結串列特性帶來的好處。
2、初始化Init
首先需要定義連結串列節點類,包含兩個屬性資料域和指標域。
然後需要定義3個變數用於存放隊頭節點、隊尾節點以及佇列長度。
而初始化結構主要初始化3個變數初始值,具體實現如下:
public class MyselfQueueNode<T>
{
//資料域
public T Data;
//指標域,即下一個節點
public MyselfQueueNode<T> Next;
public MyselfQueueNode(T data)
{
Data = data;
Next = null;
}
}
public class MyselfQueueLinkedList<T>
{
//隊頭節點即首元節點
private MyselfQueueNode<T> _head;
//隊尾節點即尾節點
private MyselfQueueNode<T> _tail;
//佇列長度
private int _length;
//初始化佇列
public MyselfQueueLinkedList<T> Init()
{
//初始化隊頭節點為空
_head = null;
//初始化隊尾節點為空
_tail = null;
//初始化佇列長度為0
_length = 0;
//返回佇列
return this;
}
}
3、獲取佇列長度 Length
這個比較簡單直接把佇列長度私有欄位返回即可。
//佇列長度
public int Length
{
get
{
return _length;
}
}
4、獲取隊頭元素 Head
獲取隊頭元素可以透過隊頭節點資料域直接返回,但是要注意判斷佇列是否為空佇列,如果為空佇列則報異常。具體程式碼如下:
//獲取隊頭元素
public T Head
{
get
{
if (IsEmpty())
{
//空佇列,不可以進行獲取隊頭元素操作
throw new InvalidOperationException("空佇列");
}
//返回隊頭節點資料域
return _head.Data;
}
}
5、獲取隊尾元素 Tail
獲取隊尾元素可以透過隊尾節點資料域直接返回,但是要注意空棧則報異常。具體程式碼如下:
//獲取隊尾元素
public T Tail
{
get
{
if (IsEmpty())
{
//空佇列,不可以進行獲取隊尾元素操作
throw new InvalidOperationException("空佇列");
}
//返回隊尾節點資料域
return _tail.Data;
}
}
6、獲取是否空佇列 IsEmpty
是否空佇列只需判斷隊頭節點和隊尾節點是否都為空即可。
//是否空佇列
public bool IsEmpty()
{
//隊頭節點為null和隊尾節點都為空表示空佇列
return _head == null && _tail == null;
}
7、入隊 Enqueue
入隊大致分為以下幾步:
-
需要先建立一個新節點;
-
如果原隊尾節點不為空,則把原隊尾節點指標域指向新節點;
-
把原隊尾節點更新為新節點;
-
如果隊頭節點為空,則說明這是第一個元素,所以隊頭和隊尾都是同一個節點,因此要把隊尾節點賦值給隊頭節點;
-
佇列長度加1;
具體實現程式碼如下:
//入隊
public void Enqueue(T value)
{
//建立新的隊尾節點
var node = new MyselfQueueNode<T>(value);
//如果隊尾節點不為空,則把新的隊尾節點連線到尾節點後面
if (_tail != null)
{
_tail.Next = node;
}
//隊尾節點變更為新的隊尾節點
_tail = node;
//如果隊頭節點為空,則為其賦值為隊尾節點
if (_head == null)
{
_head = _tail;
}
//佇列長度加1
_length++;
}
8、出隊 Dequeue
出隊則大致分為以下幾步:
-
判斷是否空佇列,空佇列則報錯;
-
獲取隊頭節點資料域暫存;
-
更新頭節點為原隊頭節點對應的下一個節點;
-
如果隊頭節點為空,則說明為空佇列,隊尾節點也要置空;
-
佇列長度減1;
-
返回暫存的隊頭節點資料;
具體實現程式碼如下:
//出隊
public T Dequeue()
{
if (IsEmpty())
{
//空佇列,不可以進行出佇列操作
throw new InvalidOperationException("空佇列");
}
//獲取隊頭節點資料
var data = _head.Data;
//把隊頭節點變更為原隊頭節點對應的下一個節點
_head = _head.Next;
//如果佇列為空,表明為空佇列,同時更新隊尾為空
if (_head == null)
{
_tail = null;
}
//佇列長度減1
_length--;
//返回隊頭節點資料
return data;
}
注:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner