資料結構 - 佇列

IT规划师發表於2024-10-15

佇列也是一種操作受限的線性資料結構,與棧很相似。

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

相關文章