1. 程式人生 > >資料結構--單向連結串列

資料結構--單向連結串列

C語言中,我們在使用陣列時,會需要對陣列進行插入和刪除的操作,這時就需要移動大量的陣列元素,但在C語言中,陣列屬於靜態記憶體分配,陣列在定義時就必須指定陣列的長度或者初始化。這樣程式一旦執行,陣列的長度就不能再改變,若想改變,就只能修改原始碼。實際使用中陣列元素的個數也不能超過陣列元素的最大長度,否則就會發生下標越界的錯誤(這是新手在初學C語言時肯定會遇到的問題,相信老師也會反覆強調!!!但這種問題肯定會遇到,找半天找不到錯誤在哪,怪我咯???)。另外如果陣列元素的使用低於最大長度,又會造成系統資源的浪費,會導致降低空間使用效率。

那有沒有更合理的使用系統資源的方法呢?比如,但需要新增一個元素時,程式就可以自動的申請記憶體空間並新增新的元素,而當需要減少一個元素時,程式又可以自動地釋放該元素佔用的記憶體空間。我們聰明的祖先早就意識到了這個問題,於是就有了動態資料結構--

連結串列結構(Linked list)。它主要是利用動態記憶體分配、使用結構體並配合指標來實現的一種資料結構。

連結串列有三種不同的型別:單向連結串列,雙向連結串列以及迴圈連結串列。今天我們只對單向連結串列做詳細的說明。

連結串列中最簡單的一種是單向連結串列,它包含兩個域,一個資訊域和一個指標域。這個連結指向列表中的下一個節點,而最後一個節點則指向一個空值(NULL)。

%e5%8d%95%e5%90%91%e9%93%be%e8%a1%a8%e5%ad%98%e5%82%a8%e7%bb%93%e6%9e%84

單向連結串列的儲存結構

/*單向連結串列的程式碼表示*/
struct node
{
    int data;  // 資料域
    struct node *next;  // 指向下一個節點的指標
};

接下來進入正題,分別詳細講一下單向連結串列的插入、刪除節點以及插入節點操作。

  1. 單向連結串列的建立

建立一個單向連結串列,我們可以使用向連結串列中新增節點的方式。首先,要為新建的節點動態申請記憶體空間,讓指標變數指向這個新建節點,然後將新建節點新增到連結串列中,這時,我們需要考慮以下兩種情況:

(1)若原連結串列為空,則將新建節點設定為頭節點

%e5%8d%95%e9%93%be%e8%a1%a8%e5%bb%ba%e7%ab%8b-1

(2)若原連結串列為非空,則將新建節點新增到表尾

%e5%8d%95%e9%93%be%e8%a1%a8%e5%bb%ba%e7%ab%8b-2

具體程式碼如下:

#include "stdio.h"
#include "stdlib.h"

struct link \*AppendNode(struct link \*head);
void DisplayNode(struct link *head);
void DeleteMemory(struct link *head);

struct link {
    int data;
    struct link *next;
};

int main(int argc, char const *argv\[\])
{
    int i = 0;
    char c;
    struct link *head = NULL;   //連結串列頭指標
    printf("Append a new node(y/n)?");
    scanf("%c", &c);

    while(c == 'Y' || c == 'y'){
        head = AppendNode(head);    //向head為頭指標的連結串列末尾新增節點
        DisplayNode(head);
        printf("Append a new node(y/n)?");
        scanf(" %c", &c);
        i++;
    }

    printf("%d new nodes have been appened!\\n");
    DeleteMemory(head);
    return 0;
}

// 新建一個節點並新增到連結串列末尾,返回新增節點後的連結串列的頭指標
struct link \*AppendNode(struct link \*head){
    struct link \*p = NULL, \*pr = head;
    int data;

    p = (struct link *)malloc(sizeof(struct link)); // 通過malloc函式動態的申請記憶體,注意結構體佔用記憶體的大小隻能用sizeof()獲取
    if (p == NULL){
        printf("No enough memory to allocate!\\n");
        exit(0);
    }
    if (head == NULL){  //原連結串列為空
        head = p;
    }else{              // 原連結串列為非空,則將新建節點新增到表尾
        while(pr->next != NULL){    // 如果pr指向的不是表尾,則移動pr直到指向表尾
            pr = pr->next;
        }
        pr->next = p;
    }
    printf("Input node data:");
    scanf("%d",&data);      // 輸入新建節點的資料
    p->data = data;
    p->next = NULL;         // 將新建節點置為表尾
    return head;
}

// 顯示連結串列中所有的節點
void DisplayNode(struct link *head){
    struct link *p = head;
    int j = 1;
    while(p != NULL){       // p不在表尾,迴圈列印節點的值
        printf("%5d%10d\\n", j, p->data);
        p = p->next;
        j++;
    }
}

//釋放head指向的連結串列中所有節點佔用的記憶體
void DeleteMemory(struct link *head){
    struct link \*p = head, \*pr = NULL;
    while(p != NULL){   // p不在表尾,釋放節點佔用的記憶體
        pr = p;         // 在pr中儲存當前節點的指標
        p = p->next;    // p指向下一個節點
        free(pr);       // 釋放pr指向的當前節點佔用的記憶體
    }
}

程式碼執行結果如下:%e5%8d%95%e5%90%91%e9%93%be%e8%a1%a8%e5%bb%ba%e7%ab%8b%e4%bb%a3%e7%a0%81%e8%bf%90%e8%a1%8c%e7%bb%93%e6%9e%9c1

2. 單向連結串列的刪除操作

連結串列的刪除操作就是將待刪除的節點從連結串列中斷開,那麼待刪除節點的上一個節點就成為尾節點。在刪除節點時,我們要考慮一下4種情況:

(1)若原連結串列為空,則不執行任何操作,直接退出程式

(2)若待刪除節點是頭節點,則將head指向當前節點的下一個節點,再刪除當前節點

%e5%8d%95%e9%93%be%e8%a1%a8%e5%88%a0%e9%99%a4-1

(3)若待刪除節點不是頭節點,則將前一節點的指標域指向當前節點的下一節點,即可刪除當前節點。當待刪除節點是尾節點時,由於p->next=NULL,因此執行pr->next = p->next後,pr->next的值也變為了NULL,從而使pr所指向的節點由倒數第二個節點變成尾節點。%e5%8d%95%e9%93%be%e8%a1%a8%e5%88%a0%e9%99%a4-2

(4)若待刪除的節點不存在,則退出程式

注意:節點被刪除後,只表示將它從連結串列中斷開而已,它仍佔用著記憶體,必須要釋放這個記憶體,否則會出現記憶體洩漏。

刪除一個節點的程式碼如下:

// 從head指向的連結串列中刪除一個節點,返回刪除節點後的連結串列的頭指標
struct link \*DeleteNode(struct link \*head, int nodeData)
{
    struct link \*p = head, \*pr = head;
    if (head == NULL)       // 若原連結串列為空,則退出程式
    {
        printf("Linked Table is empty!\\n");
        return head;
    }
    while(nodeData != p->data && p->next != NULL)   // 未找到待刪除節點,且沒有到表尾
    {
        pr = p;         // 在pr中儲存當前節點的指標
        p = p->next;    // p指向當前節點的下一節點
    }
    if (nodeData == p->data)    // 若當前節點就是待刪除節點
    {
        if (p == head)  // 若待刪除節點為頭節點
        {
            head = p->next;     // 將頭指標指向待刪除節點的下一節點
        }
        else        // 若待刪除節點不是頭節點
        {
            pr->next = p->next; // 讓前一節點的指標指向待刪除節點的下一節點
        }
        free(p);    // 釋放為已刪除節點分配的記憶體
    }
    else    // 沒有找到節點值為nodeData的節點
    {
        printf("This Node has not been found!\\n");
    }
    return head;        // 返回刪除節點後的連結串列頭指標
}

3. 單鏈表的插入操作

向一個連結串列中插入一個新節點時,首先要新建一個節點,並將新建節點的指標域初始化為空NULL,然後在連結串列中尋找適當的位置執行節點插入操作,此時需要考慮下面4種情況:

(1)若原連結串列為空,則將新建節點p作為頭節點,讓head指向新節點p

(2)若原連結串列為非空,折按新建節點的值的大小(假設原連結串列已按節點值升序排列)確定插入新節點的位置。若在頭結點前插入新節點,則將新節點的指標域指向原連結串列的頭結點,並且讓head指向新節點p

%e5%8d%95%e9%93%be%e8%a1%a8%e6%8f%92%e5%85%a5-1

(3)若在原連結串列中間插入新節點,則將新節點p的指標域指向下一節點,並且讓前一節點的指標域指向新建節點p

%e5%8d%95%e9%93%be%e8%a1%a8%e6%8f%92%e5%85%a5-2

(4)若在表尾插入新節點,則將尾節點的指標域指向新節點p

%e5%8d%95%e9%93%be%e8%a1%a8%e6%8f%92%e5%85%a5-3

具體程式碼如下:

// 在已按升序排列的連結串列中插入一個新節點,返回插入節點後的連結串列頭指標
struct link \*InsertNode(struct link \*head, int nodeData)
{
    struct link \*pr = head, \*p = head, *temp = NULL;
    p = (struct link *)malloc(sizeof(struct link)); // 給新建節點動態申請記憶體空間
    if (p == NULL)      // 若動態申請記憶體失敗,則退出程式
    {
        printf("No enough memory!\\n");
        exit(0);
    }

    p->next = NULL; // 將新建節點的指標域初始化為空
    p->data = nodeData; // 將新建節點的資料域初始化為nodeData

    if (head == NULL)   // 若原連結串列為空
    {
        head = p;   // 將新建節點作為頭節點
    }
    else    // 若原連結串列為非空
    {
        // 未找到新建節點的插入位置並且沒有到尾節點
        while(pr->data < nodeData && pr->next != NULL)
        {
            temp = pr;  // 在temp中儲存當前節點pr的指標
            pr = pr->next;  // pr跳到下一節點
        }
        // 找到需要插入的位置
        if (pr->data >= nodeData)
        {
            if (pr == head) // 若當前節點為頭節點,則將新建節點插入頭節點之前
            {
                p->next = head; // 將新節點的指標域指向原連結串列的頭節點
                head = p;   // head指向新建節點
            }
            else    // 在原連結串列中插入新節點
            {
                pr = temp;
                p->next = pr->next; // 新建節點的指標域指向當前節點的下一節點
                pr-next = p;        // 當前節點的下一節點指向新節點
            }
        }
        else    // 新建節點的值為最大值,插在原連結串列尾部
        {
            pr->next = p;   // 原連結串列的尾節點指向新節點
        }
    }
    return head;    // 返回插入新節點後的連結串列的頭指標
}

到此,對於單鏈表的操作已經介紹完了。通過寫這篇部落格,我也深刻學習了單鏈表的結構和一些主要操作,在寫作的過程中也翻閱了很多資料,讓我意識到資料結構的重要性,不懂資料結構,你永遠只能當一個碼農。