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


【系列文章推薦閱讀】

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

如有錯誤,還請指正。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM