數據結構與算法專題之線性表——隊列及其應用

  本章內容是數據結構與算法第三彈——隊列及其應用。與前一章棧的講解一樣,本章對於隊列的講解也會首先介紹棧的基本概念及結構和代碼實現,然後再引入幾個經典的隊列問題幫助大家理解隊列的應用。

  隊列與棧一樣,也是一個簡單但相當重要的數據結構,重點也應該落在對於隊列的理解應用而非代碼實現上,在今後的數據結構與算法的學習中也會學到多種依賴於隊列的算法,同樣我們在那時候會使用C++ STL的queue泛型容器,本文前半部分介紹的隊列也將使用泛型,實現STL queue裏的大部分方法。

隊列的概念與實現

  我們先來認識一下隊列。

  隊列,顧名思義,是一個“排隊的序列”,它與棧一樣,是一個操作受限的線性表,只不過棧的插入刪除都限制在同一端(也就是棧頂),而隊列的插入和刪除分別限制在兩端。也就是說,隊列只能從一端插入數據,從另一端刪除數據,就像餐廳排隊一樣,新來的人只能排在隊伍最後面,隊伍裏最前面打完飯的人離開。我們把隊列插入數據的一端稱爲隊尾刪除數據的一端稱謂隊首,想想排隊,是一個原理。

  我們假設一個隊列左邊是隊首,右邊是隊尾,一個基本的隊列示意圖如下:


  可見,插入數據總是在隊尾操作,刪除數據總是在隊首操作,當然,我們只能訪問到隊首元素,所以,隊列是一個先進先出(FIFO)結構。

  我們爲隊列定義下面幾個方法:

   (1) 入隊:Push
   (2) 出隊:Pop
   (3) 取隊首:Front
   (4) 獲取大小:Size
   (5) 隊列是否空:Empty

  看這些方法,是不是跟棧很像呢?下面我們會依次來介紹並實現這些方法。

  由於隊列也是線性表,所以與棧、鏈表一樣,也有順序結構和鏈式結構,在前一章關於棧的順序結構已經說了,它是比較耗費內存的,但是隊列與棧又有所不同,我們這裏簡單介紹一下順序結構,主要內容還是講鏈式結構。

隊列的順序結構

順序結構概述

  所謂順序結構,其實就是一個大小固定的數組,擁有隊首指針front和隊尾指針back,初始隊列爲空時,front與back相等且都爲0,如圖:


  容易看出,我們的數組構造成多大,隊列的極限容量就是多大,上圖中,隊列最大容量爲8,這也是順序結構的侷限性,無法動態擴展空間。

  當我們插入元素時,實際上就是把元素賦值到back指針所指的地方,然後back後移,假設我們插入了整數6,如下圖:


  可以看出,現在我們隊首元素就是front指針所指位置,隊列元素數量(size)是back-front=1,我們假設依次插入了1和8,此時隊列變成下圖:


  此時,我們執行pop,刪除隊首元素,事實上我們不需要真正的刪除front所指的那個6,只需要將front指針後移一位即可,這樣我們獲取到的隊首元素就變成front所指的元素1了,如圖:


  此時隊列大小爲back-front=2。聰明的你或許已經看出問題了,不管是刪除還是添加,兩個指針永遠都只會向後移,這樣總會超出數組的範圍,出現“上溢出”現象(也叫假溢出,由於存儲區未滿但指針超出界限發生溢出,故稱爲假溢出),而且執行刪除操作以後,front指針之前的元素位置就會浪費掉,再也不會被訪問。沒錯,這樣的隊列實用性極低,所以對於順序結構的隊列,我們需要將其改造成循環隊列

循環隊列概述

  那麼何爲循環隊列?循環隊列就是當其中一個指針超出數組時,返回數組的首元素,參照循環鏈表,也就是將數組首尾相連變成一個圈兒,這樣指針就能在數組範圍內循環起來,不至於發生溢出,如圖所示,我們將上圖直的數組“掰彎”,使其變成循環的(粉色數字是原數組下標):


  這樣的話,指針就不會溢出了。當然,我們不可能把順序表從內存中的邏輯順序變成環狀,但是我們觀察下標值,可以發現,我們要的是指針在下標7時,移動一下會變成下標0,想到了沒?對,就是模運算。我們已知數組大小是8,所以怎樣使7+1=0?答案就是(7+1)%8=0,溢出歸零。

  所以在不改變順序表結構的前提下,只需要把指針移動的操作由back+1改爲(back+1)%size即可(size爲數組總長度,front同理)。變成循環隊列以後,求隊列元素個數就不能簡單地使用back-front了,而應該使用(back-front+size)%size,爲什麼要+size呢?因爲back-front可能出現負數,所以我們要加上模,然後再取模,就可以得到答案了,可以自己簡單地舉幾個例子試驗一下。

  接下來我們分析一下這front和back兩個指針,回到上圖的圈圈裏,添加元素實際上是back指針順時針移動,刪除元素事實上是front指針順時針移動,也就是說,添加和刪除是兩個指針“互相追趕”的過程。如果back追趕front,說明是插入元素,追上了的話,說明隊列滿;如果front追趕back,說明是刪除元素,追上了就說明隊列空。

  由於指針始終是順時針方向移動,而back指針總是比front指針超前(也就是說front指針無論怎麼追趕,只會趕上back而不會超過back,因爲添加的元素始終要比刪除的元素多),所以由front指針開始,按照順時針方向到back指針所經過的所有元素,就是隊列中的元素(思考一下爲什麼)。

  但是這裏出來了一個特殊情況,如果back指針和front指針重合了,那麼算是隊列空,還是算隊列滿呢?

  這裏就不太好確定了,因爲你不清楚是back追上了front還是front追上了back,所以我們要避開這種特殊情況,怎麼辦呢?就是始終在back後面留一個空位置,使back永遠不會追上front,但front依然可以追上back,這樣當兩指針重合時,就可以確定是front追上back導致的重合,也就是刪除導致的,也就是隊空的情況。

  所以對於隊空和隊滿的判定:

  ☆隊列空:指針front==back

  ★隊列滿:當(back+1)%size==front時(+1的原因是因爲預留空位了)

順序隊列的實現

  這裏直接給出順序循環隊列的實現代碼,留作大家自己思考:

#include<bits/stdc++.h>

using namespace std;

template<class T>
class CQueue
{
private:
    T* arr; // 順序表
    int _front, _back; // 兩指針
    int sz; // 隊列最大容量
public:
    CQueue(int sz) // 構造一個順序隊列,聲明其最大容量
    {
        arr = new T[sz + 1]; // 因爲要back指針預留一個元素的位置
        this->sz = sz;
        _front = _back = 0;
    }
    void push(T elem); // 入隊操作
    void pop(); // 出隊操作
    T front(); // 獲取隊首元素
    int size(); // 獲取隊內元素數量
    bool empty(); // 判斷隊空
};
template<class T>
void CQueue<T>::push(T elem) // 入隊操作
{
    if((_back + 1) % sz == _front) // 隊列滿,忽略
        return;
    arr[_back] = elem;
    _back = (_back + 1) % sz;
}
template<class T>
void CQueue<T>::pop() // 出隊操作
{
    if(_front == _back) // 隊空,忽略
        return;
    _front = (_front + 1) % sz;
}
template<class T>
T CQueue<T>::front() // 獲取隊首元素
{
    if(_front == _back) // 隊空,返回默認值
        return *new T;
    return arr[_front];
}
template<class T>
int CQueue<T>::size() // 獲取隊內元素數量
{
    return (_back - _front + sz ) % sz;
}
template<class T>
bool CQueue<T>::empty() // 判斷隊空
{
    return _front == _back;
}

int main()
{
    CQueue<int> q(10);
    q.push(1);
    printf("%d\n", q.front());
    q.push(2);
    q.push(3);
    printf("%d\n", q.front());
    q.pop();
    printf("%d\n", q.front());
    q.pop();
    printf("%d\n", q.front());

    return 0;
}

隊列的鏈式結構

基本結構定義

  上面講了隊列的順序結構,實現起來比較簡單,就是理解起來稍微有那麼一點點困難。可以看出順序結構的侷限性還是相當大的,所以我們在不確定隊列最大值的情況下,一般使用鏈式結構,可以動態地管理空間,既不會出現空間不足,也不會出現空間浪費的情況。同鏈式棧一樣,鏈式隊列也是一個單鏈表,結構上與單鏈表一模一樣,我們來看一下單鏈表變成鏈式棧和鏈式隊列的區別:

  鏈式棧:無需尾指針,插入、刪除和查詢均在head結點後操作。

  鏈式隊列:需要尾指針,插入在tail指針上操作,查詢和刪除在head結點後操作。

  所以一個基本的鏈式隊列如下圖:

  (其實這個圖就是我把單鏈表的圖拿來改了改……)可以看到,結構與單鏈表一致,push操作相當於單鏈表的push_back,而pop和front都是操作第一個元素,實現起來也很簡單,所以,結點的結構代碼:

template<class T>
struct Node
{
    T data;
    Node<T> *next;
};
  隊列的類定義與棧基本一致,只是私有字段多了個尾指針,如下:

template<class T>
class Queue
{
private:
    Node<T> *head, *tail;
    int cnt;
public:
    Queue()
    {
        head = new Node<T>;
        head->next = NULL;
        tail = head;
        cnt = 0;
    }
    void push(T elem); // 將elem元素入隊
    void pop(); // 彈出隊首元素
    T front(); // 獲取隊首元素值
    int size(); // 獲取隊內元素數量
    bool empty(); // 判斷是否爲空隊列
};
  是不是跟棧相似?下面的各方法的實現也是很像的,有的我直接拷貝的棧的代碼,下面的講解也不會涉及圖例,不明白的請移步單鏈表章節進行全面系統的學習,傳送門>>

1. 入隊操作(push)

  入隊操作,由於新元素的添加是在隊尾進行的,所以相當於單鏈表的push_back操作,所以步驟如下:

  ① 構造一個新結點p並賦值,並且將p的指針域置爲NULL

  ② 將tail的指針域置爲p

  ③ 修改tail的指向爲新節點p

  代碼如下:

template<class T>
void Queue<T>::push(T elem) // 將elem元素入隊
{
    // 此操作與單鏈表push_back一致
    Node<T> *p = new Node<T>;
    p->data = elem;
    p->next = tail->next;
    tail->next = p;
    tail = p;
    cnt++;
}

2. 出隊操作(pop)

  這裏的出隊操作與棧的出棧操作是一致的,都是在鏈表頭部進行的,我們首先要獲取首元素,也就是head->next,賦值給指針p

  ① 若p爲NULL,則說明隊列內沒有元素,直接返回;否則將head的指針域指向p->next。
  ② 釋放p指向的結點的內存,即delete p;
  ③ 計數器-1
  同樣需要注意的是,如果p爲NULL,說明隊列空,此時請求pop操作是非法的,可以根據實際情況拋出異常或者返回特殊值,這裏方法直接返回。

  還有一點與棧不同,由於隊列含有尾指針,所以當隊列內只有一個元素時,刪除該元素的同時也需要將tail尾指針重置,即tail=head。

  實現代碼如下:

template<class T>
void Queue<T>::pop() // 彈出隊首元素
{
    Node<T> *p = head->next;
    if(p == NULL)
        return;
    head->next = p->next;
    delete p;
    if(cnt == 1) // 只有一個元素,移動尾指針
        tail = head;
    cnt--;
}

3. 獲取隊首元素(front)

  同樣地,直接返回head->next指向的元素的值,若指向爲空,則拋出異常或返回特殊值。

  代碼如下:

template<class T>
T Queue<T>::front() // 獲取隊首元素值
{
    Node<T> *p = head->next;
    if(p == NULL) // 如果隊內沒有元素,則返回一個新T類型默認值
        return *(new T);
    return p->data;
}

4. 獲取隊列元素個數(size)

  直接返回內部計數器,代碼:

template<class T>
int Queue<T>::size() // 獲取隊內元素數量
{
    return cnt;
}

5. 判斷隊列是否爲空(empty)

  若隊空,返回true,否則返回false,代碼:

template<class T>
bool Queue<T>::empty() // 判斷是否爲空隊列
{
    return (cnt == 0);
}

**下面是完整的隊列類代碼

#include <bits/stdc++.h>

using namespace std;

template<class T>
struct Node
{
    T data;
    Node<T> *next;
};

template<class T>
class Queue
{
private:
    Node<T> *head, *tail;
    int cnt;
public:
    Queue()
    {
        head = new Node<T>;
        head->next = NULL;
        tail = head;
        cnt = 0;
    }
    void push(T elem); // 將elem元素入隊
    void pop(); // 彈出隊首元素
    T front(); // 獲取隊首元素值
    int size(); // 獲取隊內元素數量
    bool empty(); // 判斷是否爲空隊列
};

template<class T>
void Queue<T>::push(T elem) // 將elem元素入隊
{
    // 此操作與單鏈表push_back一致
    Node<T> *p = new Node<T>;
    p->data = elem;
    p->next = tail->next;
    tail->next = p;
    tail = p;
    cnt++;
}
template<class T>
void Queue<T>::pop() // 彈出隊首元素
{
    Node<T> *p = head->next;
    if(p == NULL)
        return;
    head->next = p->next;
    delete p;
    if(cnt == 1) // 只有一個元素,移動尾指針
        tail = head;
    cnt--;
}
template<class T>
T Queue<T>::front() // 獲取隊首元素值
{
    Node<T> *p = head->next;
    if(p == NULL) // 如果隊內沒有元素,則返回一個新T類型默認值
        return *(new T);
    return p->data;
}
template<class T>
int Queue<T>::size() // 獲取隊內元素數量
{
    return cnt;
}
template<class T>
bool Queue<T>::empty() // 判斷是否爲空隊列
{
    return (cnt == 0);
}

int main()
{


    return 0;
}

  

  以上就是隊列的實現及概念的全部內容,附個練習題的傳送門:

  SDUT OJ 2135 數據結構實驗之隊列一:排隊買飯




  下集預告&傳送門:數據結構與算法專題之串——字符串及KMP算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章