【資料結構之連結串列】詳細圖文教你花樣玩連結串列

二十二畫程式設計師發表於2021-04-12

【系列文章推薦閱讀】

0. 提要鉤玄

文章【資料結構之連結串列】看完這篇文章我終於搞懂連結串列了已經介紹了鏈式儲存結構,介紹了鏈式儲存結構的最基本(簡單)實現——單向連結串列。

單向連結串列,顧名思義,它是單向的。

因為單連結串列的每個結點只有一個資料域和一個指標域,而該指標域只儲存了下一個結點的地址,所以我們只能通過某結點找到其直接後繼結點,卻不能通過某節點找到其直接前驅結點。

此外,由於單連結串列到尾結點(連結串列的最後一個結點)結束,所以尾結點的指標域是 NULL,以此來表示連結串列的終止,這就導致我們遍歷到尾結點的時候,如果想再次遍歷,只能手動回到頭結點再開始遍歷。

為了彌補單連結串列的上面兩個缺點,下面介紹兩種連結串列,它們都是單連結串列的變形,如果你理解了單連結串列,那麼會很容易理解這兩種變形。

目錄

1. 單向迴圈連結串列

1.1. 結構

單連結串列的尾結點的指標域是 NULL,所以單連結串列到此終止。如果我們使用單連結串列的尾結點的指標域儲存頭結點的地址,即尾結點的直接後繼結點為頭結點,如此一來,單連結串列就構成了一個環(迴圈),稱之為單項迴圈連結串列。

1.2. 實現思路

單向迴圈連結串列是由單連結串列進化而來的,算是單連結串列的“兒子”,所以單連結串列的那一套結構對於單向迴圈連結串列來說完全適用,從上圖你也可以看出,結構並無較大改變,二者所不同只在尾結點,所以我們只需要在尾結點和與尾結點相關的操作上下功夫就行了。

因此,單向迴圈連結串列的結構體和單連結串列的結構體相同。

/*單向迴圈連結串列的結點的結構體*/
typedef struct _Node {
    int data; //資料域:儲存資料
    struct _Node *next; //指標域:儲存直接後繼結點的地址
} Node;

為了統一對空連結串列和非空連結串列的操作,我們使用帶頭結點的連結串列來實現它。

1.3. 空連結串列及初始化

一個空連結串列如圖所示,只有一個頭指標和頭結點:

空連結串列

頭結點的指標域指向其本身構成一個迴圈,我們可以藉此來判斷連結串列是否為空。

if (head->next == head) {
    printf("空連結串列。\n");
}

想要初始化一個空連結串列很簡單,創造頭結點,使頭結點的 next 指標指向其自身即可:

Node *create_node(int elem)
{
    Node *new = (Node *) malloc(sizeof(Node));
    new->data = elem;
    new->next = NULL;
    return new;
}

/**
 * 初始化連結串列
 * p_head: 指向頭指標的指標
 */
void init(Node **p_head)
{
    //建立頭結點
    Node *head_node = create_node(0);
    //頭指標指向頭結點
    *p_head = head_node;
    //頭結點的next指標指向其本身,構成環
    head_node->next = head_node;
}

1.4. 插入操作

這裡只演示頭插法和尾插法

【頭插法】

因為帶頭結點,所以不需要考慮是否為空連結串列。下圖是向空連結串列中頭插兩個元素的過程:

單向迴圈連結串列頭插法過程

/**
 * 頭插法,新結點為頭結點的直接後繼
 * p_head: 指向頭指標的指標
 * elem: 新結點的資料
 */
void insert_at_head(Node **p_head, int elem)
{
    Node *new = create_node(elem);
    Node *head_node = *p_head; //頭結點
    //新結點插入頭結點之後
    new->next = head_node->next;
    head_node->next = new;
}

【尾插法】

因為為了儘量簡單,所以我們並沒有設定指向尾結點的尾指標,所以在尾插之前,需要先借助某個指標,遍歷至尾結點,然後再插入。

/**
 * 尾插法:新插入的結點始終在連結串列尾
 * p_head: 指向頭指標的指標
 * elem: 新結點的資料
 */
void insert_at_tail(Node **p_head, int elem)
{
    Node *new = create_node(elem);
    Node *head_node = *p_head; //頭結點
    Node *tail = head_node; //tail指標指向頭結點
    while (tail->next != head_node) { //tail遍歷至連結串列尾
        tail = tail->next;
    }
    //尾插
    new->next = tail->next;
    tail->next = new;
}

1.5. 刪除操作

刪除的本質是“跳過”待刪除的結點,所以我們要找到待刪除結點的直接前驅結點,然後讓其直接前驅結點的 next 指標指向其直接後繼結點,以此來“跳過”待刪除結點,最後儲存其資料域,釋放結點,即完成刪除。

這裡只演示頭刪法。

因為刪除的是頭結點的直接後繼結點,所以我們不必再費力尋找待刪除結點的直接前驅結點了。

單向迴圈連結串列頭刪法過程

/**
 * 頭刪法:刪除頭結點之後的結點
 * p_head: 指向頭指標的指標
 * elem: 指向儲存資料變數的指標
 */
void delete_from_head(Node **p_head, int *elem)
{
    Node *head_node = *p_head; //頭結點
    if (head_node->next == head_node) {
        printf("空連結串列,無元素可刪。\n");
        return;
    }
    Node *first_node = head_node->next; //首結點:頭結點的下一個結點
    *elem = first_node->data; //儲存被刪除結點的資料
    head_node->next = first_node->next; //刪除結點
    free(first_node); //釋放
}

1.6. 遍歷操作

我們可以一圈又一圈地迴圈遍歷連結串列,下面是迴圈列印 20 次結點地程式碼:

/**
 * 迴圈列印20次結點
 */
void output_20(Node *head)
{
    if (head->next == head) {
        printf("空連結串列。\n");
        return;
    }
    Node *p = head->next;
    for (int i = 0; i <= 20; i++) {
        if (p != head) { //不列印頭結點
            printf("%d ", p->data);
        }
        p = p->next;
    }
    printf("\n");
}

2. 雙向連結串列

2.1. 結構

顧名思義,雙向連結串列,就是有兩個方向,一個指向前,一個指向後。這樣我們就彌補了單連結串列的某個結點只能找到其直接後繼的缺陷。如圖所示:

雙向連結串列

2.2. 實現思路

為了實現能指前和指後的效果,只靠 next 指標肯定是不夠的,所以我們需要再新增一個指標 —— prev,該指標指向某結點的直接前驅結點。

/*雙向連結串列的結點結構體*/
typedef struct _Node {
    int data; //資料域
    struct _Node *prev; //指向直接前驅結點的指標
    struct _Node *next; //指向直接後繼結點的指標
} Node;

2.3. 空連結串列及初始化

雙向連結串列的空連結串列如圖所示:

雙向空連結串列

要初始化一個這樣的空連結串列,需要創造出頭結點,然後將兩個指標域置空即可:

Node *create_node(int elem)
{
    Node *new = (Node *)malloc(sizeof(Node));
    new->data = elem;
    new->prev = NULL;
    new->next = NULL;
    return new;
}

void init(Node **p_head)
{
    //建立頭結點
    Node *head_node = create_node(0);
    //頭指標指向頭結點
    *p_head = head_node;
}

2.4. 插入操作

這裡只演示頭插法,過程如下:

雙向連結串列頭插法過程

程式碼如下:

/**
 * 頭插法,新結點為頭結點的直接後繼
 * p_head: 指向頭指標的指標
 * elem: 新結點的資料
 */
void insert_at_head(Node **p_head, int elem)
{
    Node *new = create_node(elem);
    Node *head_node = *p_head; //頭結點
    if (head_node->next != NULL) { //不為空連結串列
        Node *first_node = head_node->next; //首結點:頭結點的下一個結點
        //首結點的prev指標指向new結點
        first_node->prev = new;
        //new結點的next指標指向首結點
        new->next = first_node;
    }
    //new結點的prev指標指向頭結點
    new->prev = head_node;
    //頭結點的next指標指向new結點
    head_node->next = new;
}

2.5. 刪除操作

這裡只演示頭刪法。下圖是將一個有兩個元素結點的雙向連結串列頭刪為空連結串列的過程:

雙向連結串列頭刪法過程

程式碼如下:

/**
 * 頭刪法
 * p_head: 指向頭指標的指標
 * elem: 指向儲存變數的指標
 */
void delete_from_head(Node **p_head, int *elem)
{
    Node *head_node = *p_head; //頭結點
    Node *first_node = head_node->next; //待刪除的首結點:頭結點的下一個結點
    if (head_node->next == NULL) { //判空
        printf("空連結串列,無元素可刪。\n");
        return;
    }
    *elem = first_node->data; //儲存資料
    
    if (first_node->next != NULL) {
        first_node->next->prev = first_node->prev;
    }
    first_node->prev->next = first_node->next;
    free(first_node);
}

2.6. 遍歷操作

有了 next 指標域,我們可以一路向後遍歷;有了 prev 指標域,我們可以一路向前遍歷。

這裡不再展示程式碼了。

3. 總結

瞭解了單向迴圈連結串列和雙向連結串列,就像拿搭積木一樣,我們還可以創造出來雙向迴圈連結串列。這裡就不再演示了,讀者可以自行嘗試。只要你搞懂上面三種連結串列,這絕非難事。

以上就是連結串列的花樣玩法部分內容,以後還會繼續更新。

完整程式碼請移步至 GitHub | Gitee 獲取。

如有錯誤,還請指正。

如果覺得寫的不錯,可以點個贊和關注。後續會有更多資料結構和演算法相關文章。

相關文章