1. 程式人生 > >【 C 】在單鏈表中插入一個新節點的嘗試(一)

【 C 】在單鏈表中插入一個新節點的嘗試(一)

根據《C和指標》中講解連結串列的知識,記錄最終寫一個在單鏈表中插入一個新節點的函式的過程,這個分析過程十分的有趣,準備了兩篇博文,用於記錄這個過程。

連結串列是以結構體和指標為基礎的,所以結構體和指標是需要首先掌握的知識,掌握之後,最後要明白這個問題:結構體的自引用

這時候就可以嘗試連結串列的學習了。記得去年學習連結串列的時候覺得特別新奇,並使用之寫了一個蹩腳的學生資訊管理系統,當然不值一提,可惜沒有趁熱打鐵繼續下去,也許不是壞事,醒悟的時候還不算晚!

先介紹下連結串列吧:

連結串列(linked list)就是一些包含資料的節點的集合。這些節點是用結構體定義的,如下:

typedef struct NODE{
    struct NODE *link;
    int value;

} Node;

使用typedef定義了一個結構型別的別稱Node,稱為節點。

連結串列中的每個節點通過指標(也叫鏈)連結一起。程式通過指標訪問連結串列中的節點。通常節點是動態分配的,但是有時你也能看到由節點陣列建立的連結串列。即使在這種情況下,程式也是通過指標來遍歷連結串列的。我們關注的是動態記憶體分配來建立節點。

在單鏈表中,每個節點包含一個指向連結串列下一個節點的指標。連結串列最後一個節點的指標欄位的值為NULL,提示連結串列後面不再有其他節點。在你找到連結串列的第一個節點後,指標就可以帶你訪問剩餘的所有節點。為了記住連結串列的起始位置,可以使用一個根指標(root pointer)。根指標指向連結串列的第一個節點。注意根指標只是一個指標,它不包含任何資料。

下面是一個單鏈表的圖:

從上面的圖中可以看出,這些節點相鄰在一起,這是為了顯示連結串列所提供的邏輯順序。事實上,連結串列中的節點可能分佈於記憶體中的各個地方。對於一個處理連結串列的程式而言,各節點在物理上是否相鄰並沒有什麼區別,因為程式始終用指標(鏈)從一個節點移動到另一個節點。

但連結串列可以通過鏈從開始位置遍歷連結串列直到結束位置,但連結串列無法從相反的方向進行遍歷。

上面顯示的連結串列中,節點根據資料的值按升序連結在一起。對於有些應用程式而言,這種順序非常重要,比如根據一天的時間安排約會。對於那些不要求排序的應用程式,當然也可以建立無序的單鏈表。

下面正式進入正題:在單鏈表中插入一個新節點的第一次嘗試:

如何才能把一個新節點插入到一個有序的單鏈表中呢?

假定我們有一個新值,比如12,想把它插入到上面提到的那個連結串列中。從概念上講,是很容易做到的,從連結串列的起始位置開始,跟隨指標直到找到第1個值大於12的節點,然後把這個新值插入到那個節點之前的位置。

實際的演算法比較有趣:我們按順序訪問連結串列,當到達內容為15的節點(第一個值大於12的節點)時就停下來。我們知道這個新值應該新增到這個節點之前,但前一個節點的指標必須進行修改以實現這個插入。但是我們已經越過了這個節點,無法返回。解決這個問題的辦法是始終儲存一個指向連結串列當前節點之前的那個節點的指標。

瞭解這麼多,我們就可以開始實踐了:

首先定義一個頭檔案:

typedef struct NODE{
    struct NODE *link;
    int value;

} Node;

存放於sll_node.h的標頭檔案中。

下面開發一個函式,把一個節點插入到一個有序的單鏈表中,後面並做出詳細分析:

//插入到一個有序的單鏈表。函式的引數是一個指向連結串列第一個節點的指標以及需要插入的值
#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"   //這個標頭檔案是前面自己建立的

#define FALSE 0
#define TRUE 1

int sll_insert( Node *current, int new_value )
{
    Node *previous;
    Node *new;                //需要插入的新節點
    
    //尋找正確的插入位置,方法是順序訪問連結串列,直到到達其值大於或等於新插入的節點的值
    
    while( current->value < new_value )
    {
        previous = current; //始終儲存當前節點之前的那個節點
        current = current->link; //當前節點移動到下一個節點
    }
    
    //為新節點分配記憶體,並把新值儲存到新節點中,如果記憶體分配失敗,函式返回FALSE
    
    new = ( Node *)malloc( sizeof( Node ) );
    if( new == NULL )
    {
        return FALSE;
    }
    new->value = new_value;
    
    //把新節點插入到連結串列中,並返回TRUE
    new->link = current; //新節點的指標指向當前節點
    previous->link = new; //前一個節點的指標指向新節點
    return TRUE;

}

我們用下面的方法呼叫這個函式:

result = sll_insert( root, 12 );

下面跟蹤程式碼的執行過程:

首先傳遞給函式的引數是root變數的值,它是指向連結串列的第一個節點的指標。當函式剛剛執行時,連結串列的狀態如下:

這張圖沒有顯示root變數,因為函式不能訪問它。它的值的一份拷貝作為形參current傳遞給函式,但函式不能訪問root。現在current->value是5,小於12,所以迴圈再次執行。

當我們回到迴圈的頂部時,current和previous指標都向前移動了一個節點。

現在current->value的值是10,小於12,因此迴圈體繼續執行,結果如下:

現在current->value的值是15大於12,所以退出迴圈。

此時,重要的是previous指標,因為它指向我們必須加以修改以插入新值的那個節點。但首先,我們必須得到一個新節點,用於容納新值。下面這張圖顯示了新值被賦值到新節點之後連結串列的狀態。

把這個新節點連結到連結串列中需要兩個步驟。

首先,new->link = current;

也就是使新節點指向將稱為連結串列下一個節點的節點,也就是我們找到的第一個值大於12的那個節點。在這個步驟之後,連結串列的內容為:

第二個步驟是讓previous指標所指向的節點修改為指向這個新節點。下面這個語句執行這個任務:

previous->link = new;

這個步驟之後,連結串列的狀態如下:

然後函式返回,連結串列最終的樣子如下:

從根節點開始,隨各個節點的link欄位逐個訪問連結串列,我們可以發現這個新節點已被正確地插入連結串列中。

最後,不得不提的是現實是否就是這麼如意?

可以思考一下,如果試圖把20插入連結串列,也就是new_value = 20,這個程式還能正常工作嗎?

這裡把迴圈提出來:

 while( current->value < new_value )
    {
        previous = current; //始終儲存當前節點之前的那個節點
        current = current->link; //當前節點移動到下一個節點
    }

你會發現,while迴圈會越過連結串列的尾部,並對一個NULL指標執行間接訪問操作。這是不合法的。

為了解決這個問題, 我們必須對current的值進行測試,在執行current->value之前確保它不是一個NULL指標:

將while語句的中條件換成如下:

while( current != NULL & current->value < new_value )

{

...

}

就解決了上述問題。

最後呢?提出下一篇博文要講的內容,就是試試把3這個值插入連結串列,看看會發生什麼?下篇博文見!