【數據結構之鏈表】詳細圖文教你花樣玩鏈表

【系列文章推薦閱讀】

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 獲取。

如有錯誤,還請指正。

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