双向循环链表的拆解理解

循环链表是一种特殊的链表,其中最后一个节点的指针不是指向NULL,而是指向第一个节点,形成一个环。

  • 普通双向链表:5 <-> 10 <-> 20 <-> NULL(到头就没了)
  • 双向循环链表:5 <-> 10 <-> 20 <-> 5 <-> 10...(绕圈走,永远走不完,除非主动停)
(1)定义循环链表的 "车厢"(节点)
cpp 复制代码
struct Node {
    int data;          // 车厢里的数字
    struct Node* next; // 拉后面车厢的手
    struct Node* prev; // 拉前面车厢的手
};
(2)造一节新车厢(createNode)
cpp 复制代码
struct Node* createNode(int data) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    newNode->prev = NULL;
    return newNode;
}
(3)给空链表插第一个车厢(insertInEmpty)
cpp 复制代码
struct Node* insertInEmpty(struct Node* head, int data) {
    struct Node* newNode = createNode(data);
    newNode->next = newNode; // 自己的后手拉自己
    newNode->prev = newNode; // 自己的前手拉自己
    return newNode;
}

空链表插第一个车厢时,因为要形成循环,这节车厢只能 "自己拉自己"(就像一个人站成圈,左手拉右手)。比如插 10,就变成「10 <-> 10 <-> 10...」。

(4)往车头插车厢(insertAtBeginning)
cpp 复制代码
struct Node* insertAtBeginning(struct Node* head, int data) {
    if (head == NULL) { // 空链表,走上面的insertInEmpty
        return insertInEmpty(head, data);
    }
    
    struct Node* newNode = createNode(data);
    struct Node* last = head->prev; // 找到最后一节车厢(头节点的前手就是最后一节)
    
    newNode->next = head;   // 新车厢的后手拉拉原来的车头
    newNode->prev = last;   // 新车厢的前手拉最后一节车厢
    
    last->next = newNode;   // 最后一节车厢的后手拉拉新车厢
    head->prev = newNode;   // 原来的车头的前手拉新车厢
    
    return newNode; // 新车厢变成新车头
}

原来循环链表是「10 <-> 10」(只有 10),插 5 到车头:

  1. 先找到最后一节车厢(还是 10);
  2. 5 的后手拉拉 10,5 的前手拉 10;
  3. 10 的后手拉拉 5,10 的前手拉 5;
  4. 最终变成「5 <-> 10 <-> 5 <-> 10...」,5 成了新车头。
(5)往车尾插车厢(insertAtEnd)
cpp 复制代码
struct Node* insertAtEnd(struct Node* head, int data) {
    if (head == NULL) {
        return insertInEmpty(head, data);
    }
    
    struct Node* newNode = createNode(data);
    struct Node* last = head->prev; // 找到最后一节车厢
    
    newNode->next = head;   // 新车厢的后手拉拉车头(形成循环)
    newNode->prev = last;   // 新车厢的前手拉原来的最后一节
    
    last->next = newNode;   // 原来的最后一节的后手拉拉新车厢
    head->prev = newNode;   // 车头的前手拉新车厢(新车厢变成新的最后一节)
    
    return head; // 车头没变
}

原来链表是「5 <-> 10 <-> 5」,插 20 到车尾:

  1. 找到最后一节车厢(10);
  2. 20 的后手拉拉车头 5,20 的前手拉 10;
  3. 10 的后手拉拉 20,5 的前手拉 20;
  4. 最终变成「5 <-> 10 <-> 20 <-> 5...」,车头还是 5。
(6)在指定车厢后插新车厢(insertAfter)
cpp 复制代码
struct Node* insertAfter(struct Node* head, int data, int key) {
    if (head == NULL) {
        printf("链表为空!\n");
        return NULL;
    }
    
    struct Node* current = head;
    
    do { // 注意:循环链表用do-while,先走一次再判断(避免漏了头节点)
        if (current->data == key) { // 找到要插在后面的车厢
            struct Node* newNode = createNode(data);
            
            newNode->next = current->next; // 新车厢的后手拉拉当前车厢的后手
            newNode->prev = current;       // 新车厢的前手拉当前车厢
            
            current->next->prev = newNode; // 当前车厢后手的前手拉新车厢
            current->next = newNode;       // 当前车厢的后手拉拉新车厢
            
            return head;
        }
        current = current->next;
    } while (current != head); // 回到车头就停(避免无限循环)
    
    printf("未找到值为%d的节点!\n", key);
    return head;
}

链表是「5 <-> 10 <-> 20 <-> 5」,要在 10 后面插 15:

  1. 找到 10 这个车厢;
  2. 15 的后手拉拉 20,15 的前手拉 10;
  3. 20 的前手拉 15,10 的后手拉拉 15;
  4. 最终变成「5 <-> 10 <-> 15 <-> 20 <-> 5...」。
(7)删除指定数字的车厢(deleteNode)
cpp 复制代码
struct Node* deleteNode(struct Node* head, int key) {
    if (head == NULL) {
        printf("链表为空!\n");
        return NULL;
    }
    
    // 如果只有1节车厢,且是要删的
    if (head->next == head && head->data == key) {
        free(head);
        return NULL; // 删完就空了
    }
    
    struct Node* current = head;
    struct Node* toDelete = NULL;
    
    // 找要删的车厢
    do {
        if (current->data == key) {
            toDelete = current;
            break;
        }
        current = current->next;
    } while (current != head);
    
    if (toDelete == NULL) {
        printf("未找到值为%d的节点!\n", key);
        return head;
    }
    
    // 如果要删的是车头,新车头是下一节
    if (toDelete == head) {
        head = head->next;
    }
    
    // 让要删的车厢的前后车厢直接拉手
    toDelete->prev->next = toDelete->next;
    toDelete->next->prev = toDelete->prev;
    
    free(toDelete); // 拆了这节车厢
    return head;
}

链表是「5 <-> 10 <-> 15 <-> 20 <-> 5」,删 10:

  1. 找到 10 这个车厢;
  2. 5 的后手直接拉拉 15,15 的前手直接拉拉 5;
  3. 拆了 10,最终变成「5 <-> 15 <-> 20 <-> 5...」。
(8)打印循环链表(printList)
cpp 复制代码
void printList(struct Node* head) {
    if (head == NULL) {
        printf("链表为空!\n");
        return;
    }
    
    struct Node* current = head;
    
    printf("双向循环链表内容: ");
    do { // 先打印头节点,再往后走
        printf("%d <-> ", current->data);
        current = current->next;
    } while (current != head); // 回到车头就停
    
    printf("(回到头部)\n");
}

循环链表不能用while (current != NULL)(因为永远不为 NULL),必须用do-while,走到回到车头就停,不然会无限打印。

(9)释放循环链表(freeList)
cpp 复制代码
void freeList(struct Node* head) {
    if (head == NULL) {
        return;
    }
    
    struct Node* current = head->next;
    struct Node* temp;
    
    // 第一步:先断开循环(把最后一节车厢的后手放空)
    head->prev->next = NULL;
    
    // 第二步:像普通链表一样一节节拆
    while (current != NULL) {
        temp = current->next;
        free(current);
        current = temp;
    }
    
    free(head); // 最后拆车头
}
(10)主函数(main):实际操作循环链表
cpp 复制代码
int main() {
    struct Node* head = NULL;
    
    // 插第一个节点10 → 「10 <-> 10」
    head = insertInEmpty(head, 10);
    printf("创建双向循环链表:\n");
    printList(head);
    
    // 车头插5 → 「5 <-> 10 <-> 5」
    head = insertAtBeginning(head, 5);
    printf("\n在头部插入5后:\n");
    printList(head);
    
    // 车尾插20 → 「5 <-> 10 <-> 20 <-> 5」
    head = insertAtEnd(head, 20);
    printf("\n在尾部插入20后:\n");
    printList(head);
    
    // 10后面插15 → 「5 <-> 10 <-> 15 <-> 20 <-> 5」
    head = insertAfter(head, 15, 10);
    printf("\n在值为10的节点后插入15:\n");
    printList(head);
    
    // 删10 → 「5 <-> 15 <-> 20 <-> 5」
    head = deleteNode(head, 10);
    printf("\n删除值为10的节点后:\n");
    printList(head);
    
    // 找15 → 能找到
    struct Node* found = searchNode(head, 15);
    if (found != NULL) {
        printf("\n找到值为15的节点\n");
    } else {
        printf("\n未找到值为15的节点\n");
    }
    
    // 查长度 → 3(5、15、20)
    printf("\n双向循环链表长度: %d\n", getLength(head));
    
    // 释放链表
    freeList(head);
    
    return 0;
}
相关推荐
一只小风华~2 小时前
Vue.js 核心知识点全面解析
前端·javascript·vue.js
2022.11.7始学前端2 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay2 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室2 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder2 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy2 小时前
Cursor 前端Global Cursor Rules
前端·cursor
红彤彤2 小时前
前端接入sse(EventSource)(@fortaine/fetch-event-source)
前端
L、2182 小时前
统一日志与埋点系统:在 Flutter + OpenHarmony 混合架构中实现全链路可观测性
javascript·华为·智能手机·electron·harmonyos