链表(二):删除节点、查找节点、修改节点、双向链表、循环链表

好,咱们接着聊链表。上一章我们把单链表的基本操作——创建、插入、遍历——都过了一遍。今天要讲的这几个操作,说实话,才是真正让你在项目里「用得上」的东西。

删除、查找、修改,这三个动作,说白了就是链表的「增删改查」里的后三个。我当年刚学的时候,觉得删除节点特别绕,指针指来指去的,一不小心就丢了节点。后来写多了才发现,其实就那么几步,关键是要画图。

另外,双向链表和循环链表,这两个是单链表的「升级版」。我个人习惯是,只要数据量不大、不需要频繁逆序遍历,我就用单链表。但一旦涉及到「从后往前找」或者「环形数据」,那双向链表和循环链表就是你的不二之选。

删除节点

删除节点,核心就一句话:让前一个节点跳过当前节点,直接指向后一个节点。然后释放当前节点的内存。

嗯,这里要注意:删除头节点和删除中间节点,处理方式不一样。我见过不少新手在这栽跟头。

删除节点的三种情况:

  • 删除头节点:直接把 head 指向第二个节点,释放原头节点。
  • 删除中间节点:找到待删节点的前一个节点,让它指向待删节点的下一个节点。
  • 删除尾节点:让倒数第二个节点的 next 指向 NULL,释放尾节点。
// 删除指定值的节点(单链表)
Node* deleteNode(Node* head, int value) {
    if (head == NULL) return NULL;

    // 如果要删的是头节点
    if (head->data == value) {
        Node* temp = head;
        head = head->next;
        free(temp);
        return head;
    }

    // 查找待删节点的前一个节点
    Node* current = head;
    while (current->next != NULL && current->next->data != value) {
        current = current->next;
    }

    // 如果找到了
    if (current->next != NULL) {
        Node* temp = current->next;
        current->next = temp->next;
        free(temp);
    }

    return head;
}

我曾经犯过的错: 删除节点后忘了 free(),导致内存泄漏。在嵌入式项目中,内存泄漏是致命的。后来我养成了习惯:每次 free 之后,顺手把指针置为 NULL,防止野指针。

查找节点

查找其实最简单。单链表不支持随机访问,只能从头开始,一个一个往后找。时间复杂度 O(n),没什么好说的。

但我想分享一个经验:查找操作往往不是单独存在的。比如你要删除一个节点,得先找到它;你要修改一个节点,也得先找到它。所以查找函数写好了,后面两个操作就省事多了。

// 查找第一个值为 value 的节点
Node* findNode(Node* head, int value) {
    Node* current = head;
    while (current != NULL) {
        if (current->data == value) {
            return current;
        }
        current = current->next;
    }
    return NULL;  // 没找到
}

小技巧: 如果你需要频繁查找,可以考虑把链表改成「有序链表」,查找时可以提前终止。或者干脆用哈希表 + 链表组合,比如 LRU 缓存就是这么干的。

修改节点

修改节点,说白了就是「先查找,再赋值」。找到节点后,直接改它的 data 字段就行。

你想想看,修改操作本身不涉及指针变动,所以比删除简单多了。但有一个坑:如果你修改的是节点的「关键字段」(比如链表是按 data 排序的),那修改后可能会破坏链表的有序性。这时候你得重新调整节点位置,甚至重新插入。

// 修改第一个值为 oldVal 的节点,改为 newVal
void modifyNode(Node* head, int oldVal, int newVal) {
    Node* target = findNode(head, oldVal);
    if (target != NULL) {
        target->data = newVal;
    }
}

我在项目中遇到过: 有一次做学生成绩管理系统,链表按学号排序。有个功能要修改学生成绩,结果我直接改了 data 里的成绩字段,没动学号,所以没问题。但如果要改学号,那就得先删除再重新插入了。

双向链表

双向链表,每个节点有两个指针:一个指向前一个节点(prev),一个指向后一个节点(next)。

为什么要用双向链表?说白了,就是为了「反向遍历」。单链表只能从头走到尾,双向链表可以从尾走到头。我当年做文本编辑器的时候,用双向链表来管理「撤销/重做」操作,非常顺手。

// 双向链表节点定义
typedef struct DNode {
    int data;
    struct DNode* prev;
    struct DNode* next;
} DNode;

// 在双向链表尾部插入
DNode* insertAtEnd(DNode* head, int value) {
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = value;
    newNode->next = NULL;

    if (head == NULL) {
        newNode->prev = NULL;
        return newNode;
    }

    DNode* current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = newNode;
    newNode->prev = current;

    return head;
}

注意: 双向链表删除节点时,要同时更新 prev 和 next 两个指针。我曾经漏掉了 prev 的更新,结果链表出现了「断链」,调试了半天才发现。

循环链表

循环链表,就是让尾节点的 next 指向头节点,形成一个环。它没有真正的「尾节点」,遍历时要注意防止死循环。

循环链表最适合处理「环形数据」,比如约瑟夫环问题、操作系统的进程调度(时间片轮转)。我记得有一次做音乐播放器的「循环播放」功能,就是用循环链表实现的。

// 循环链表遍历(需要记录起始点)
void traverseCircular(Node* head) {
    if (head == NULL) return;

    Node* current = head;
    do {
        printf("%d ", current->data);
        current = current->next;
    } while (current != head);  // 回到头节点时停止
    printf("\n");
}

避坑指南: 循环链表的插入和删除,要特别注意「头节点」的处理。如果你删除了头节点,记得更新 head 指针,否则整个链表就丢了。

知识体系总览

下面这张图,把今天讲的内容串起来了。你可以看到,删除、查找、修改是单链表的三大操作,而双向链表和循环链表是两种重要的变体。

链表操作 单链表核心操作 删除节点 查找节点 修改节点 链表变体 双向链表 循环链表 ⚠️ 避坑要点 • 删除节点后记得 free(),防止内存泄漏 • 双向链表删除要同时更新 prev 和 next • 循环链表遍历要防止死循环,用 do-while 更安全

总结

今天的内容,说白了就是三件事:

  • 删除、查找、修改——单链表的三大基本操作,查找是基础,删除最复杂。
  • 双向链表——多了一个 prev 指针,换来的是双向遍历的能力。
  • 循环链表——首尾相连,适合处理环形数据。

我个人觉得,链表这东西,光看代码是学不会的。你得在纸上画图,把每个指针的指向画清楚,然后再写代码。我当年就是这么过来的,画了上百张图之后,链表就再也没出过 bug。

好,今天就到这儿。下一章我们讲栈和队列,这两个东西其实可以用链表来实现,到时候你会发现,链表真的是「万能积木」。


公众号:蓝海资料掘金营,微信deep3321