鏈表練習:使用遞歸重寫單鏈表

本次的練習是使用遞歸來實現單鏈表的各種操作,本文目錄如下

目錄

定義鏈表結點

遞歸銷燬

遞歸輸出

遞歸尾插

尾部插入(Java寫法)

尾部插入(C++寫法)

遞歸在指定位置插入

遞歸刪除值爲x的結點

本文完整代碼

結束語


 

定義鏈表結點

首先要定義數據結構,定義一下構造函數方便後面寫碼:

struct Node
{
    int data;
    Node *next;
    Node(int d = -1, Node *nn = NULL) : data(d), next(nn) {}
};

這裏有個技巧,構造函數的參數我一般會把next結點放在後面,因爲這樣可以直接只傳一個值data來構造新結點,next默認爲NULL, 而據我的經驗,直接使用一個data值構造新Node的情況比較多,同時因爲設置了默認參數的緣故,也可以直接無參new Node()。

 

遞歸銷燬

遞歸銷燬和二叉樹的後序遍歷的思想差不多,都是先遞歸調用再銷燬自己,這樣會在遞歸調用到底部之後,從尾到頭回溯地delete每個結點。實現代碼如下:

void destroy(Node *p)
{
    if (!p)
        return;
    destroy(p->next);
    delete p;
}

順帶一提,delete一個NULL的指針是沒什麼問題的,C++標準有規定,這裏開頭需要判NULL的原因是後面需要訪問p->next,如果此時p爲NULL的話,p->next會直接段錯誤。

 

遞歸輸出

void print(Node *p)
{
    if (!p)
        return;
    cout << p->data << " ";
    print(p->next);
}

與遞歸銷燬不同的是,這裏需要先輸出再遞歸調用纔是正序輸出(即從頭到尾),否則將會逆序輸出(即從尾到頭輸出),除此之外,如果鏈表有頭結點的話,需要傳頭結點的下一個結點進去才能正常輸出,否則會一併輸出頭節點。

輸出後不帶換行。

 

遞歸尾插

這裏分兩種插入,一是插入到尾部,二是插入到指定順序。遺憾的是,好像並不能統一成一個函數

 

尾部插入(Java寫法)

Java沒有指針的引用這種東西,但是C++可以用指針的引用來簡化這個遞歸寫法,所以我在這裏分成兩個寫法。但是不管是Java寫法還是C++寫法,其實都是使用的C++,只不過Java寫法將指針改爲Java裏的引用就可以變成真正的Java寫法,而C++寫法則是使用了一些C++的特性。

代碼:

Node *TailInsertion(Node *p, int data)
{
    if (!p)
        return new Node(data);
    else
        p->next = TailInsertion(p->next, data);
    return p;
}

分三個步驟理解:

  1. 遞歸的終止條件就是遇到一個傳進來是NULL的指針,意味着遞歸到了尾結點的next位置,那麼此時就是要添加結點的時候了,Java版的寫法是利用返回值,此時返回一個新的結點作爲插入的尾結點。
  2. 但是新插入的尾結點怎麼掛到鏈表上去?這裏就要看函數裏的else分支了,else分支裏接收遞歸調用的返回值複製給p->next,這樣假設現在遞歸到最後一個結點了,p指向最後一個結點,那麼else分支裏遞歸調用傳進去的p->next就是NULL(尾結點的next肯定爲NULL),傳進去NULL根據我們的遞歸終止條件,此時會返回一個新的結點,那麼就剛好接收作爲尾結點的next。
  3. 那現在不是遞歸到尾結點,而是中間結點,怎麼保證這種修改p->next的寫法不會斷鏈表?這個時候就得看函數的底部的返回值,在處理完函數頭部的if分支後直接原樣得返回p,這樣就會返回到上一級遞歸調用的else分支,而else分支裏是這樣寫的:p->next = TailInsertion(p->next, data),對於中間結點來說,就等於p->next = p->next,當然不會斷鏈。

 

TailInsertion的函數作用是傳進去一個鏈表的頭結點,返回插入尾結點後的鏈表,聽起來比較難理解,但是當你結合這個函數的作用來理解函數裏面遞歸調用的地方就不難理解了。可以說這種寫法是一種比較 “正統” 的遞歸寫法,因爲按遞歸定義來說,其實這個函數符合遞歸的定義:鏈表的一部分也是一個鏈表,即是以結點p爲首的鏈表是一個鏈表,同樣以p->next結點爲首的鏈表也同樣是個鏈表。

不管怎麼樣,初學者還是要多寫多練多思考才能徹底掌握遞歸。

 

尾部插入(C++寫法)

利用C++裏指針的引用,可以寫出更加簡化的代碼,但是也比較難以理解,需要讀者對C++指針、引用理解得很透徹才能掌握。

代碼:

void TailInsertion2(Node *&p, int data)
{
    if (!p)
        p = new Node(data);
    else
        TailInsertion2(p->next, data);
}

注意,函數參數p是一個指針的引用,利用此特性可以免去返回值,直接定義成void返回值。

說說爲什麼可以這樣寫。其實這個指針的引用,就相當於是個二級指針,假設現在有個鏈表:

header-> 1 -> 2 -> 3 -> 4 -> 5 -> NULL

假設當前函數遞歸到結點3的位置,也就是函數參數p傳進來的是指向結點3的指針,此時對指針p解引用會得到結點3,但是如果我們修改p,將會修改結點2的next指針,因爲在函數遞歸到結點2的時候,在else分支將結點2的next作爲參數遞歸調用,所以遞歸到結點3的時候,參數傳進來的引用其實是結點2的next指針,根據引用的特性,修改它自然也就修改了上一個結點的next。按照這個規則,當函數遞歸到NULL的時候,p這個指針本身是結點5的next指針,只是它指向NULL而已。

這個寫法確實難理解得多,還是得多寫多練多思考才能掌握。

 

遞歸在指定位置插入

Java寫法的實現代碼:

Node *insert(Node *p, int index, int data)
{
    if (!p)
        return NULL;
    if (index == 0)
        return new Node(data, p);
    else
        p->next = insert(p->next, index - 1, data);
    return p;
}

每次遞歸的時候將index減1,並判斷index爲0時插入,即可實現指定遞歸位置插入且不需要外部變量輔助。要注意的是在index爲0的時候new的結點是要把p作爲該結點的next傳進去構造的,否則會斷掉後面的鏈,並且這種插入方法只支持在頭部和中間插入,不能在尾部插入。但是這個算法的魯棒性還是很好的,即使你index傳的是負數或是超出鏈表長度的數,這個函數就會在遞歸遍歷完鏈表後返回,什麼都不會發生。

寫個main函數測試一下:

int main()
{
    Node *header = new Node;

    for (int i = 1; i <= 10; ++i)
        TailInsertion(header, i);

    cout << "origin linkedlist:" << endl;
    print(header->next);
    cout << endl;

    cout << "mid insertion:" << endl;
    insert(header, 5, 99);
    print(header->next);
    cout << endl;

    cout << "head insertion:" << endl;
    insert(header, 1, 88);
    print(header->next);
    cout << endl;

    cout << "illeage insertion:" << endl;
    insert(header, 50, 77);
    print(header->next);
    cout << endl;

    cout << "illeage insertion2:" << endl;
    insert(header, -5, 77);
    print(header->next);
    cout << endl;
    
    destroy(header);
    return 0;
}

輸出:

origin linkedlist:
1 2 3 4 5 6 7 8 9 10
mid insertion:
1 2 3 4 99 5 6 7 8 9 10
head insertion:
88 1 2 3 4 99 5 6 7 8 9 10
illeage insertion:
88 1 2 3 4 99 5 6 7 8 9 10
illeage insertion2:
88 1 2 3 4 99 5 6 7 8 9 10

附一個C++指針引用的版本:

void insert2(Node *&p, int index, int data)
{
    if (!p)
        return;
    if (index == 0)
        p = new Node(data, p);
    else
        insert2(p->next, index - 1, data);
}

 

遞歸刪除值爲x的結點

原理和上文指定位置插入差不多,只不過new結點變成了delete結點,C++指針引用的實現:

void delNode2(Node *&p, int x)
{
    if (!p)
        return;
    if (p->data == x)
    {
        Node *tmp = p->next;
        delete p;
        p = tmp;
    }
    else
        delNode2(p->next, x);
}

當匹配到當前結點的值與x相同時,直接刪除當前結點並返回當前結點的下一個結點。

Java版:

Node *delNode(Node *p, int x)
{
    if (!p)
        return NULL;
    if (p->data == x)
    {
        Node *ret = p->next;
        delete p;
        return ret;
    }
    else
        p->next = delNode(p->next, x);
    return p;
}

要注意不管哪個版本,如果鏈表帶有頭結點的話一定得傳頭結點的下一個結點,像這樣 delNode(header->next,5) ,否則頭結點的值也會被判斷,如果頭結點的值剛好與x相同頭結點就會被刪掉。

 

本文完整代碼

測試代碼並沒寫完整,就不給出了,可以自己寫點代碼測試測試。

#include <iostream>
#include <algorithm>

using namespace std;

struct Node
{
    int data;
    Node *next;
    Node(int d = -1, Node *nn = NULL) : data(d), next(nn) {}
};

void destroy(Node *p)
{
    if (!p)
        return;
    destroy(p->next);
    delete p;
}

void print(Node *p)
{
    if (!p)
        return;
    cout << p->data << " ";
    print(p->next);
}

Node *TailInsertion(Node *p, int data)
{
    if (!p)
        return new Node(data);
    else
        p->next = TailInsertion(p->next, data);
    return p;
}

void TailInsertion2(Node *&p, int data)
{
    if (!p)
        p = new Node(data);
    else
        TailInsertion2(p->next, data);
}

Node *insert(Node *p, int index, int data)
{
    if (!p)
        return NULL;
    if (index == 0)
        return new Node(data, p);
    else
        p->next = insert(p->next, index - 1, data);
    return p;
}

void insert2(Node *&p, int index, int data)
{
    if (!p)
        return;
    if (index == 0)
        p = new Node(data, p);
    else
        insert2(p->next, index - 1, data);
}

void delNode2(Node *&p, int x)
{
    if (!p)
        return;
    if (p->data == x)
    {
        Node *tmp = p->next;
        delete p;
        p = tmp;
    }
    else
        delNode2(p->next, x);
}

Node *delNode(Node *p, int x)
{
    if (!p)
        return NULL;
    if (p->data == x)
    {
        Node *ret = p->next;
        delete p;
        return ret;
    }
    else
        p->next = delNode(p->next, x);
    return p;
}

int main()
{

    Node *header = new Node;

    for (int i = 1; i <= 10; ++i)
        TailInsertion(header, i);

    cout << "origin linkedlist:" << endl;
    print(header->next);
    cout << endl;

    //test here..

    destroy(header);
    return 0;
}

 

結束語

遞歸確實是一個比較難的內容,特別是利用指針引用這種思路更是難以理解,但是隻要我們多寫多練多思考,慢慢地總會熟練掌握的,共勉。

 

 

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