鏈表知識點總結(一):鏈表的常見操作以及複雜度分析

前言

鏈表是數據結構中最基礎的鏈式結構,也是後面構成圖、樹的基礎。爲此,我覺得有必要專門開幾篇文章寫寫鏈表相關的內容,但是如果從零開始寫起太過於枯燥,文章也會變得冗長,所以本文只寫一些總結性的內容,對其中的原理不深究。

 

另外,本文默認使用節點Node的C++定義爲:

class Node
{
public:
    int data;
    Node *next;
    Node(int dd = -999, Node *nn = NULL) : data(dd), next(nn) {}
};

下文中出現的checkIndex函數爲檢查索引參數是否合法的函數,本來是用於我的封裝鏈表中的(封裝的鏈表中還有一個size成員,可以知道鏈表的長度,以此來判斷索引是否合法),具體實現這裏就不給出了,這裏默認傳入的參數都是合法的、且鏈表爲非空鏈表,忽略這個函數調用即可。

 

單鏈表的常見操作

下面總結一些我個人的鏈表常見操作的實現,並且會帶上簡單的複雜度分析。以下實現默認是對帶頭節點的單鏈表進行的操作

 

單鏈表的銷燬

void delLL(Node *header)
{
    Node *pre = NULL;
    Node *cur = header;
    while (cur != NULL)
    {
        pre = cur;
        cur = cur->next;
        delete pre;
    }
}

以一前一後兩個指針pre和cur掃鏈表,不斷讓兩個指針後移,同時刪除pre所指的節點,直到cur指向NULL爲止。凡是需要用兩個指針掃鏈表的操作都建議根據實際用途命名爲pre 和 cur,代表前一個節點和當前指向節點。時間複雜度爲O(n),由於需要對鏈表進行遍歷,所以是On。

 

單鏈表的插入

void add(Node* header, int data, int index)
    {
        checkIndex(index);//判斷一下索引是否合法

        Node *pre = header;
        for (int i = 0; i < index-1; i++)
        {
            pre = pre->next;
        }
        pre->next = new Node(data, pre->next);
        
    }

這裏我默認傳進來的index爲從1開始的索引,所以for的中止條件需要-1,實現思路是用pre指針掃鏈表,先找到要插入位置的前一個個節點,然後再讓新節點成爲pre指向節點的下一個節點,注意,此時pre後面可能還有節點,要把pre節點的next賦值給新節點的next,就把要插入的節點插入進去了,這裏使用了Node類的一個構造函數,讓代碼變得簡潔。

另外,給鏈表添加頭節點的好處也在這裏體現,試想一下,如果沒有頭節點的話,那麼add函數就得判斷一下當前要插入的位置是否是第一個位置,是的話就得修改header指針,不是的話就進入上面add函數裏的循環,去找待插入位置的前一個節點。但是我們給鏈表加入頭結點後,不管插入的是不是第一個位置,我們都不需要修改header指針,也就是不需要額外的if來判斷,這樣就簡化了代碼。

時間複雜度爲O(n),從頭部插入的話是O1的複雜度,但是插入到其他地方的話就就是On,所以不管是從最壞的角度考慮,還是從綜合的角度考慮,其時間複雜度都爲On。

 

上面講的是在任意位置的插入,其實就是教科書裏說的尾插法,下面說說頭插法。

頭插法,顧名思義,就是在鏈表的頭部插入一個元素,如果對一個鏈表一直使用頭插法插入元素的話,將會得到跟插入順序倒序的鏈表。代碼:

void HeadInsertion(Node *header, int data)
{
    Node *tmp = new Node;
    tmp->data = data;
    tmp->next = header->next;
    header->next = tmp;
}

如果是按照我上面節點定義的那樣定義了構造函數的話,其實可以這樣寫:

void HeadInsertion(Node *header, int data)
{
    header->next = new Node(data,header->next);
}

對頭插法來說,時間複雜度是O(1)的,因爲它不需要遍歷整個鏈表。

 

單鏈表的刪除

    int remove(Node* header, int index)
    {
        checkIndex(index);//保證索引合法

        Node *pre = header;
        for (int i = 0; i < index-1; i++)
        {
            pre = pre->next;
        }

        Node *tmp = pre->next;
        pre->next = tmp->next;

        int res = tmp->data;
        delete tmp;

        return res;
    }

這裏也是跟插入一樣,先找到要刪除節點的上一個節點,因爲要返回這個要刪除節點的值,所以得先用臨時變量res把data存起來後面返回,又因爲要把這個要刪除節點的內存釋放,所以還得用一個臨時指針tmp保存這個要刪除節點。時間複雜度爲O(n),分析同單鏈表的插入。

 

單鏈表的遍歷

void print(Node* header)
    {
        Node *cur = header->next;
        while (cur != NULL)
        {
            cout << cur->data << " ";
            cur = cur->next;
        }
        cout << endl;
    }

這裏以輸出整個鏈表爲例,用cur指針掃一遍鏈表即可,要注意,cur初始值應爲第一個數據節點,而不是頭節點。時間複雜度O(n),遍歷嘛,肯定是和鏈表的長度n有關。

 

反轉單鏈表

void reverse(Node *header)
{
    Node *cur = header->next;
    Node *tmp = NULL;
    while (cur != NULL)
    {
        tmp = cur->next;
        cur->next = (header->next == cur ? NULL : header->next);//將反轉後最後一個節點的next置空,其實就是反轉前的第一個數據節點
        header->next = cur;
        cur = tmp;
    }
}

這裏其實就是從第一個數據節點開始,將節點逐個用頭插法插入到原鏈表中(頭節點),就完成了反轉操作,顯然,這個操作的時間複雜度也是O(n)

 

 

總結

單鏈表的各種操作都不難理解,對初學者而言,需要多理解各種操作的實現原理,必要時在草稿紙上畫草圖梳理下思路,在理解各種操作的原理後最好動手打幾遍單鏈表的完整實現,這樣才能真正掌握學到的知識點。總之,多看,多想,多寫。

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