挑戰408——數據結構(14)——線性結構(5)

前篇討論了一下具體的鏈表結構以及常規操作。現在我們趁熱打鐵,感覺再來熟悉一下具體的操作。此時我們用鏈表的形式來實現之前的順序棧。

鏈棧

棧結構這裏不多說,我們直接進入正題。先考慮,當棧爲空的時候,如何表示?當然就是一個空指針了
在這裏插入圖片描述
將新元素推入堆棧時,該元素只會添加到鏈表的前端。因此,如果將元素e1 push到空棧上,該元素將存儲在一個新單元格中,該單元格將成爲鏈中唯一的鏈接:
在這裏插入圖片描述
將一個新元素push棧將該元素添加到鏈的開頭。所涉及的步驟與將字符插入鏈表列表緩衝區所需的步驟相同。 首先分配一個新的單元格,然後輸入數據,最後更新鏈接指針,使新單元格成爲鏈中的第一個元素。 因此,如果將元素e2推到棧上,將得到以下配置:
在這裏插入圖片描述
在鏈表表示中,pop操作包括刪除鏈中的第一個單元格並返回存儲在該列中的值。因此,來自上圖所示的棧的pop操作返回e2並恢復棧的先前狀態,如下所示:
在這裏插入圖片描述

對鏈棧中的pop和push的分析

push

我們以一個最基本的數字棧爲基礎分析,下面是需要的一些接口內容:

class StackInt {          // in StackInt.h
public:
    StackInt ();          // constructor

    void push(int value); // append a value 
    int pop();            // return the first-in value

private:
    struct Node {
      int value;
      Node * link;
    };
    Node * data;     // member variables
};

接下來看代碼分析:

void StackInt::push(int v) {
    Node * temp = new Node;
    temp->value = v;
    temp->link = data;  //這一步必不可少!
    data = temp;
}

老辦法,圖解push過程,假設此時我們的鏈棧如下:
在這裏插入圖片描述
我們的目的是push(7),將7放入棧頂,也就是要這樣的結果:
在這裏插入圖片描述
下面解釋整個過程:

  1. 新建一個臨時節點,並用創建臨時指針指向該節點,接着向該節點寫入數字7,就像這樣:
    在這裏插入圖片描述
  2. 然後,將頭指針data裏面的指針值,賦值給新節點的link字段,以便新節點可以連接到8這個節點:
    在這裏插入圖片描述
  3. 將temp裏面的值,賦值給首指針data處。將data的指向正確改變:
    在這裏插入圖片描述
  4. 函數返回,得出正確的push後的結構:在這裏插入圖片描述
pop

先看代碼:

int StackInt::pop() {
    int toReturn = data->value;
    Node * temp = data;
    data = temp->link;
    delete temp;
    return toReturn;
}

圖解過程如下:

  1. 先將要pop出去的值進行保存
    在這裏插入圖片描述
  2. 我們這邊創建一個臨時的指針變量Temp,然後將其指向首指針data指向的節點(即棧頂元素),就像這樣:
    在這裏插入圖片描述
  3. 然後改變首指針(即棧頂)的指向:
    在這裏插入圖片描述
  4. 最後刪除temp指向的節點
    在這裏插入圖片描述
  5. 此時函數返回。

注意,我們第二步不可以直接改變指針的指向而不使用臨時節點,因爲直接改變指針的指向會像下圖一樣:
在這裏插入圖片描述
這顯然不是我們希望看到的。

鏈隊列

隊列類也使用鏈表結構進行簡單的表示。爲了說明基本的方法,隊列的元素存儲在從隊列頭部開始並以尾部結尾的列表中。並且爲了保證入隊和出隊正確運行,Queue對象必須含有指向隊列兩端的指針。 因此,私有實例變量的定義在queuepriv.h的修改版本如下:

/*
 *這個文件保存的是Queue.h的私有部分
 *我們這個時候用鏈隊列。也就是用鏈表來實現我們的隊列
 *這樣的隊列我們稱爲鏈隊列
 */
private:
    /*隊列的鏈式結構*/
    struct Cell{
        ValueType data;
        Cell *link;
    };
    /*實例化變量*/
    Cell *head; //頭指針
    Cell *tail; //尾指針
    int count; //記錄隊列元素的總數


    /* Make it illegal to copy queues */
    Queue(const Queue & value) { }
    const Queue & operator=(const Queue & rhs) { return *this; }

那麼爲什麼我們要設置一個尾指針呢?
首先我們假設一下我們初始化的隊列如下所示:
在這裏插入圖片描述
這個時候我們執行後入隊操作:
在這裏插入圖片描述
顯然,這個時候我們相當於在鏈表中插入一個元素,且位於頭結點的第一個,自然我們的複雜度爲O(1)。但是當我們執行出隊操作時,情況就不同了,我們必須驗證頭指針遍歷到最後一個節點,再執行delet操作,此時算法複雜度爲O(N)。

入隊(enqueue)

那麼是否有更好的辦法呢?因此我們考慮一下添加一個尾指針(因爲隊列多數在鏈表的兩端操作)。所以我們看下面一段代碼:

void QueueInt::enqueue(int v) {
    Node * temp = new Node;
    temp->value = v;
    tail->link = temp;
    tail = temp;
}

圖解代碼執行過程:

  1. 假設我們初始的隊列值爲123,並且我們添加的尾指針指向的是我們的3的節點,如圖:
    在這裏插入圖片描述
  2. 此時我們在隊列尾部(3),這裏加入數值4,此時,我們的操作應該是,建立一個指針,指向新建立的節點,然後往這個新節點寫入數值:
    在這裏插入圖片描述
  3. 接下來我們改變指針的指向,也就是將cp的內容複製到3下面的link字段(也就是tail指向的節點的link字段)。此時情況如下(分兩步):
    在這裏插入圖片描述
    在這裏插入圖片描述
  4. 最終我們返回我們的隊列就可以了,因此複雜度爲O(1)
    在這裏插入圖片描述
出隊(dequeue)

我們就拿上面完成的例子爲例,進行我們的dequeue操作,我們先看2 3行代碼,建立一個臨時節點,指向與我們的head指針的指向相同,如下:
首先假設我們有完整的隊列
在這裏插入圖片描述
現在我們要進行的queue操作,於是將隊頭移除,以達到下圖的結果:
在這裏插入圖片描述
同理,看代碼:

int QueueInt::dequeue() {
    int toReturn = head->value;
    Node * temp = head;
    head = temp->link;
    delete temp;
    return toReturn;
}

  1. 我們先將此時隊頭指向的數字給存儲起來,然後新建一個指針,跟隊頭指針一同指向首元素1.
    在這裏插入圖片描述
  2. 然後改變頭指針的指向
    在這裏插入圖片描述
  3. 釋放被dequeue掉的那個節點
    在這裏插入圖片描述

算法複雜度分析

在這裏插入圖片描述

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