1. 程式人生 > >三.從零寫雙鏈表到基本演算法的實現(終)

三.從零寫雙鏈表到基本演算法的實現(終)

一.雙鏈表的引入和基本實現

1.雙鏈表的結構

首先,我們要明白雙鏈表並不是有兩條鏈的連結串列,而是有兩個遍歷方向的連結串列,因此我們所說的雙鏈表其實就是雙向連結串列的簡稱。

2.有效資料+2個指標的節點(雙鏈表)

(1)單鏈表的節點 = 有效資料 + 指標(指標指向後一個節點)

(2)雙向連結串列的節點 = 有效資料 + 2個指標(一個指向後一個節點,另一個指向前一個節點)

(3)雙鏈表的結構圖如下:

這裡寫圖片描述

==可以看出,圖中的每一個節點都有一個有效資料和兩個指標(前向指標pPrev和後向指標pNext),分別指向該節點的前一個節點和後一個節點。頭結點的pPrev和尾節點的pNext都指向NULL==

3.建立一個雙鏈表節點的實現

根據上面的雙鏈表的結構圖,再結合前面學習的單鏈表,建立雙鏈表的節點無疑依葫蘆畫瓢!

①.實現一個連結串列的首要任務就是構造節點,在c語言中構構造節點的方法就是定義一個結構體:

// 構建一個雙鏈表的節點
struct node
{
    int data;               // 有效資料
    struct node *pPrev;     //指向上一個節點的指標
    struct node *pNext;     // 指向下一個節點的指標
};

②.使用堆記憶體建立一個節點

因為連結串列的記憶體要求比較靈活,不能用棧,也不能用data資料段。只能用堆記憶體
建立節點的過程:

①申請一個節點大小的堆記憶體

②檢查堆記憶體是否申請成功

③清理申請到的堆記憶體

④填充節點中的資料

⑤節點中的兩個指標域初始化為NULL;

4.程式碼的具體實現

// 作用:建立一個連結串列節點
// 返回值:指標,指標指向我們本函式新建立的一個節點的首地址
struct node * create_node(int data)
{
    struct node *p = (struct node *)malloc(sizeof(struct node));
    if (NULL == p)
    {
        printf("malloc error.\n"
); return NULL; } // 清理申請到的堆記憶體 bzero(p, sizeof(struct node)); // 填充節點 p->data = data; p->pPrev = NULL;//預設建立的節點的前向後向指標都指向NULL p->pNext = NULL; return p; }

二.雙鏈表的演算法之插入節點

1.尾部插入方式

==和單鏈表的插入方式基本一樣,所以,對單鏈表的插入 遍歷 刪除 掌握ok,這裡分析起來是很簡單的。==

①.直接上手尾插入節點的連結串列分析圖

這裡寫圖片描述
尾部插入節點的任務分析:
==思路:將從連結串列尾部插入節點的任務分為兩步:==

1.1.第一步.找到連結串列的尾節點

1.2.第二步.將新節點接到連結串列的尾節點後面成為新的尾節點

①.原來的尾節點的pNext指標指向新節點的首地址

②.新節點的pPrev指標指向原來的尾節點的首地址

1.3程式碼實現

// 將新節點new插入到連結串列pH的尾部
void insert_tail(struct node *pH, struct node *new)
{
    // 第一步先走到連結串列的尾節點
    struct node *p = pH;
    while (NULL != p->pNext)
    {
        p = p->pNext;           // 第一次迴圈走過了頭節點
    }
    // 迴圈結束後p就指向了原來的最後一個節點
    // 第二步:將新節點插入到原來的尾節點的後面
    p->pNext = new;             // 後向指標關聯好了。新節點的地址和前節點的next
    new->pPrev = p;             // 前向指標關聯好了。新節點的prev和前節點的地址
                                // 前節點的prev和新節點的next指標未變動
}
/**************雙鏈表插入節點***********************/
int main(void)
{
    struct node *pHeader = create_node(0);      // 頭指標

    insert_tail(pHeader, create_node(11));//尾巴插入雙鏈表節點
    insert_tail(pHeader, create_node(12));//尾巴插入雙鏈表節點
    insert_tail(pHeader, create_node(13));//尾巴插入雙鏈表節點

    // 遍歷
    printf("node 1 data: %d.\n", pHeader->pNext->data);//通過pNext指標訪問每個節點
    printf("node 2 data: %d.\n", pHeader->pNext->pNext->data);
    printf("node 3 data: %d.\n", pHeader->pNext->pNext->pNext->data);

    struct node *p = pHeader->pNext->pNext->pNext;      // p指向了最後一個節點
    printf("node 3 data: %d.\n", p->data);//通過pPrev指標逆向訪問每個節點
    printf("node 2 data: %d.\n", p->pPrev->data);
    printf("node 1 data: %d.\n", p->pPrev->pPrev->data);

    return 0;
}
}

2.頭部插入方式

這裡寫圖片描述

2.1.頭結點插入的重要四個步驟:

①.新節點的pNext指向原來的第一個節點的首地址,即圖中的新節點pNext和原來的第一個節點的首地址相連

②.原來第1個有效節點的prev指標指向新節點的首地址

③.頭節點的pNext指標指向新節點的首地址

④.頭節點的pNext指向新節點的首地址,即圖中頭結點的首地址和新節點的pPrev相連

2.2.思考:四個步驟①②③④是否可以交換一下順序?(可參考單鏈表頭部插入)

==答案顯然是不能的,因為由上圖可知,如果交換①和③的步驟,③是可以完成的,但是執行步驟①時就會發現,原來的第一個有效節點的地址已經丟失了,由圖中知道,我們原來第一個節點的首地址是在頭結點的pNext指標中儲存的,因為先執行了步驟③,故原來第一個節點的有效地址丟失了。且②必須放在①③中間,具體原因和上面一致。==

2.3.程式碼實現

// 將新節點new前頭插入連結串列pH中。
// 演算法參照圖示進行連線,一共有4個指標需要賦值。注意的是順序。
void insert_head(struct node *pH, struct node *new)
{
    // 新節點的next指標指向原來的第1個有效節點的地址
    new->pNext = pH->pNext;

    // 原來第1個有效節點的prev指標指向新節點的地址
    if (NULL != pH->pNext)
    //因為當只有頭結點和頭指標時,即ph->pNext=NULL;
   // 而pH->pNext->pPrev,則會報段錯誤
        pH->pNext->pPrev = new;

    // 頭節點的next指標指向新節點地址
    pH->pNext = new;

    // 新節點的prev指標指向頭節點的地址
    new->pPrev = pH;
}

需要注意的是,第二步中,當連結串列如果只有一個頭結點,即沒有有效節點時,

②.原來第1個有效節點的prev指標指向新節點的首地址

這個步驟則會報段錯誤,因為當只有頭結點和頭指標時,即ph->pNext=NULL;而pH->pNext->pPrev,則會報段錯誤

==故解決方法就是新增這條if (NULL != pH->pNext)語句,判斷是否為連結串列頭結點,如果是,則不做任何操作。==

三.雙鏈表的演算法之遍歷節點

==(1)雙鏈表是單鏈表的一個父集。雙鏈表中如何完全無視pPrev指標,則雙鏈表就變成了單鏈表。這就決定了雙鏈表的正向遍歷(後向遍歷)和單鏈表是完全相同的。==

==(2)雙鏈表中因為多了pPrev指標,因此雙鏈表還可以前向遍歷(從連結串列的尾節點向前面依次遍歷直到頭節點)。但是前向遍歷的意義並不大,主要是因為很少有當前當了尾節點需要前向遍歷的情況。==

1.正向遍歷

1.1因為正向遍歷和單鏈表的過程相同,這裡不再贅述。

遍歷方法:==從頭指標+頭節點開始,順著連結串列掛接指標依次訪問連結串列的各個節點,取出這個節點的資料,然後再往下一個節點,直到最後一個節點,結束返回。==

1.2正向遍歷雙鏈表遍歷節點過程分析圖(cp的單鏈表分析)

這裡寫圖片描述

1.3程式碼實現

//正向遍歷(後向遍歷)雙鏈表,ph為指向單鏈表的頭指標,將遍歷的節點資料打印出來
void bianli(struct node *ph)
{
    struct node *p=ph; //頭指標的後面是頭節點
    printf("---------正向遍歷----------\n");
    printf("---------start----------\n");
    while(NULL!=p->pNext)// 是不是最後一個節點
    {
        p=p->pNext;// 走到下一個節點,也就是迴圈增量
        printf("node data: %d.\n",p->data);
    }
    printf("------------end----------\n");
}

2.反向遍歷

==反向遍歷節點(即從尾節點開始前向遍歷)的邏輯和正向遍歷差不多,通過p=p->pPrev來向前移動,依次訪問節點。==

//反向遍歷(前向遍歷)雙鏈表,ph為指向單鏈表的頭指標,將遍歷的節點資料打印出來
void fanxiang_bianli(struct node *pTail)
{
    struct node *p=pTail; //頭指標的後面是頭節點
    printf("---------反向遍歷----------\n");
    printf("---------start----------\n");
    while(NULL!=p->pPrev)// 是不是最後一個節點
    {
        printf("node data: %d.\n",p->data);/*若printf與下面交換程式,則把尾節點的資料捨棄了,並沒有打印出來,所以,遍歷節點一定的注意順序!*/
        p=p->pPrev;// 走到下一個節點,也就是迴圈增量

    }
    printf("------------end----------\n");
}

/**************雙鏈表之遍歷節點***********************/
int main(void)
{
    struct node *pHeader = create_node(0);      // 頭指標

    insert_tail(pHeader, create_node(11));//尾插入雙鏈表節點
    insert_tail(pHeader, create_node(12));//尾插入雙鏈表節點
    insert_tail(pHeader, create_node(13));//尾插入雙鏈表節點


    bianli(pHeader);//正向遍歷雙鏈表
    struct node *p=pHeader->pNext->pNext->pNext;//p指向最後一個節點
    fanxiang_bianli(p);//反向遍歷雙鏈表

    /*// 手動遍歷
    printf("node 1 data: %d.\n", pHeader->pNext->data);//通過pNext指標訪問每個節點
    printf("node 2 data: %d.\n", pHeader->pNext->pNext->data);
    printf("node 3 data: %d.\n", pHeader->pNext->pNext->pNext->data);

    struct node *p = pHeader->pNext->pNext->pNext;      // p指向了最後一個節點
    printf("node 3 data: %d.\n", p->data);//通過pPrev指標逆向訪問每個節點
    printf("node 2 data: %d.\n", p->pPrev->data);
    printf("node 1 data: %d.\n", p->pPrev->pPrev->data);
    */
    return 0;
}

四.雙鏈表的演算法之刪除節點

1.為什麼要刪除節點?

(1)一直在強調,連結串列到底用來幹嘛的?==用來儲存資料==

(2)有時候連結串列節點中的資料不想要了,因此要刪掉這個節點。

2、刪除節點的2個步驟

(1)第一步:找到要刪除的節點;第二步:刪除這個節點。

3、如何找到待刪除的節點

(1)通過遍歷來查詢節點。從頭指標+頭節點開始,順著連結串列依次將各個節點拿
出來,按照一定的方法比對,找到我們要刪除的那個節點。

4、如何刪除一個節點(分兩種情況)(這裡就和單鏈表不同)

(1)待刪除的節點是尾節點的情況:

==這種情況要刪除節點2就需要斷開①.②.這兩條指標的連結,然後釋放free(p)==

步驟1.首先把把待刪除的尾節點的前一個節點的pNext指標存放的待刪除尾節點的首地址清除,然後把待刪除的尾節點的前一個節點的pNext指標指向null(這時候就相當於原來尾節點前面的一個節點變成了新的尾節點),即圖中①

步驟2.然後再將待刪除尾節點的pPrev指標存放的上一個節點的地址斷開連結,即圖②

步驟3.最後再將這個摘出來的節點free掉即可。。 (因為最終是要釋放尾節點的,所以,第②步可以省略)

這裡寫圖片描述

==p表示當前節點地址,p->pNext表示後一個節點地址,p->pPrev表示前一個節點的地址
故步驟①表示為:p->pPrev->pNext = NULL;
步驟②表示為p->pPrev = NULL;

(2)待刪除的節點不是尾節點的情況:

==這種情況要刪除節點1就需要斷開①.②.③.④這四條指標的連結,然後釋放free(p)==
這裡寫圖片描述
步驟如下:

步驟①.首先把待刪除節點的前一個節點的pNext指標指向待刪除節點的後一個節點的首地址(這樣就把這個節點從連結串列中摘出來了),

步驟②.當前待刪除的節點的prev和next指標置為NULL,但這裡可以不用管,因為後面會整體銷燬整個節點

步驟③.待刪除節點的後一個節點的prev指標指向待刪除節點的前一個節點的首地址

步驟④.最後再將這個摘出來的節點free掉即可。(因為最終是要釋放尾節點的,所以,第②步可以省略)

這裡寫圖片描述

==p表示當前節點地址,p->pNext表示後一個節點地址,p->pPrev表示前一個節點的地址==

故步驟①為前一個節點的next指標指向後一個節點的首地址

表示為:==p->pPrev->pNext = p->pNext;==

步驟②.③當前待刪除的節點的prev和next指標置為NULL

==p->pPrev = NULL;==
==p->pNext = NULL;==

步驟④為後一個節點的prev指標指向前一個節點的首地址

表示為==p->pNext->pPrev = p->pPrev;==

==和待刪除的節點是尾節點同樣,最終都要釋放free(p),所以第②步和第③步可以省略。==

5、設計一個刪除節點的演算法

(1)刪除節點的演算法流程框圖如下

這裡寫圖片描述

(2)總結總體步驟如下:

①通過遍歷連結串列來尋找需要刪除的節點

②找到該節點又分為兩種情況:一個是非尾節點,另一個是尾節點

③通過上面描述的待刪除的節點不同處理方法處理:

6.程式碼實現

// 從雙鏈表pH中刪除節點,待刪除的節點的特徵是資料區等於data
// 返回值:當找到並且成功刪除了節點則返回0,當未找到節點時返回-1
//struct node *ph:頭指標
//int data:待刪除節點的有效資料
int delete_node(struct node *ph,int data)
{
    struct node *p=ph; //頭指標的後面是頭節點
    printf("---------start----------\n");
    if(NULL==p)//這是為了防止沒有頭結點,報段錯誤
    {
        return -1;
    }
    while(NULL!=p->pNext)// 通過遍歷節點,判斷是不是尾節點
    {
        //p->pNext表示下一個節點的地址,即走到下一個節點
        p=p->pNext;

        //判斷這個節點是否為我們要刪除的節點
        if(p->data==data)
        {
            //處理找到的節點,分為兩種情況
            if(NULL==p->pNext)//待刪除的節點如果為尾節點,執行如下
            {
            //p表示當前節點地址,p->pNext表示後一個節點地址,
            //p->pPrev表示前一個節點的地址
            //把把待刪除的尾節點的前一個節點的pNext指標存放的待刪除尾節點的首地址清除,然後把待刪除的尾節點的前一個節點的pNext指標指向null
                p->pPrev->pNext = NULL;

                //p->pPrev = NULL;//待刪除的尾節點的前一個節點的pPrev指標指向null
                //這裡可以省略這部,因為下面整個待刪除節點都被銷燬了

                free(p);//釋放待刪除的尾節點的記憶體
            }
            else  //待刪除的節點如果為非尾節點,即普通節點,執行如下
            {
                // 待刪除的節點的前一個節點的next
                //指標指向待刪除的節點的後一個節點的首地址
                p->pPrev->pNext = p->pNext;

                // 當前節點的prev和next指標都不用管,因為後面會整體銷燬整個節點

                p->pNext->pPrev = p->pPrev;//待刪除的節點的後一個節點的prev指標指向前一個節點的首地址
                free(p);//釋放待刪除的尾節點的記憶體
            }   
            printf("------------end----------\n");
            return 0;//成功刪除該節點,退出程式返回0
        }

    }   
    printf("-----------沒有找到該待刪除的節點----------\n");
    return -1;
}
/**************雙鏈表之刪除節點***********************/
int main(void)
{
    struct node *pHeader = create_node(0);      // 頭指標

    insert_tail(pHeader, create_node(11));//尾插入雙鏈表節點
    insert_tail(pHeader, create_node(12));//尾插入雙鏈表節點
    insert_tail(pHeader, create_node(13));//尾插入雙鏈表節點


    bianli(pHeader);//正向遍歷雙鏈表

    delete_node(pHeader, 12);//從連結串列pH中刪除資料為33的節點
    printf("------------------刪除該節點後-------------\n");  

    //再次正向遍歷雙鏈表各個節點的有效資料
    bianli(pHeader);

    /*// 手動遍歷
    printf("node 1 data: %d.\n", pHeader->pNext->data);//通過pNext指標訪問每個節點
    printf("node 2 data: %d.\n", pHeader->pNext->pNext->data);
    printf("node 3 data: %d.\n", pHeader->pNext->pNext->pNext->data);

    struct node *p = pHeader->pNext->pNext->pNext;      // p指向了最後一個節點
    printf("node 3 data: %d.\n", p->data);//通過pPrev指標逆向訪問每個節點
    printf("node 2 data: %d.\n", p->pPrev->data);
    printf("node 1 data: %d.\n", p->pPrev->pPrev->data);
    */
    return 0;
}

這裡寫圖片描述