You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何正确使用哨兵节点?关于双向链表改造的系列技术问询

双向链表改造:哨兵节点的常见问题与实现方案

嘿,我看你在改造带哨兵节点的双向链表时遇到了一些疑问,刚好我在这类问题上踩过不少坑,给你整理几个核心问题的解决方案,全是针对存储int类型的基础双向链表的:

1. 哨兵节点该怎么初始化?

对于双向链表来说,最常用的是头哨兵+尾哨兵的组合——这俩节点本身不存储有效数据,主要作用是把链表的“边界”变成普通节点的邻居,彻底消除空链表、头尾节点的特殊判断逻辑。

比如你可以这么定义结构并初始化:

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

typedef struct Node {
    int val;
    struct Node* prev;
    struct Node* next;
} Node;

typedef struct DoublyLinkedList {
    Node* head_sentinel; // 头哨兵,永远在链表最前面
    Node* tail_sentinel; // 尾哨兵,永远在链表最后面
} DoublyLinkedList;

// 初始化带哨兵的空链表
DoublyLinkedList* init_dll() {
    DoublyLinkedList* list = malloc(sizeof(DoublyLinkedList));
    list->head_sentinel = malloc(sizeof(Node));
    list->tail_sentinel = malloc(sizeof(Node));

    // 空链表时,两个哨兵互相指向
    list->head_sentinel->next = list->tail_sentinel;
    list->head_sentinel->prev = NULL; // 非循环链表的话,头哨兵的prev设为NULL
    list->tail_sentinel->prev = list->head_sentinel;
    list->tail_sentinel->next = NULL;

    // 给哨兵的val设个无效值,避免误访问时混淆有效数据
    list->head_sentinel->val = INT_MIN;
    list->tail_sentinel->val = INT_MIN;

    return list;
}

划重点:哨兵的val值只要是你业务里不会用到的无效值就行,比如INT_MIN或者INT_MAX,不用纠结具体是什么。

2. 插入节点时怎么简化逻辑?

有了哨兵,不管是插头部、尾部还是中间位置,逻辑都能统一,再也不用判断“链表是不是空”“是不是插在头/尾”这种边界情况了。

比如插尾部的代码:

// 向链表尾部插入元素
void insert_tail(DoublyLinkedList* list, int val) {
    Node* new_node = malloc(sizeof(Node));
    new_node->val = val;

    // 找到尾哨兵的前一个节点(空链表时就是头哨兵)
    Node* prev_node = list->tail_sentinel->prev;

    // 调整指针,把新节点插在prev_node和尾哨兵之间
    prev_node->next = new_node;
    new_node->prev = prev_node;
    new_node->next = list->tail_sentinel;
    list->tail_sentinel->prev = new_node;
}

插头部的逻辑也类似,直接跟头哨兵的next节点交互就行,完全不用考虑空链表的特殊情况——是不是比原来省了好多if判断?

3. 删除节点时怎么避免崩溃?

没有哨兵的时候,删除头节点要改链表的head指针,删除尾节点要改tail指针,还得判断节点是不是NULL;有了哨兵之后,这些麻烦都没了:

// 删除第一个值为val的节点
void delete_node(DoublyLinkedList* list, int val) {
    Node* curr = list->head_sentinel->next;

    // 遍历到尾哨兵就停止,永远不会碰到NULL
    while (curr != list->tail_sentinel) {
        if (curr->val == val) {
            // 不管curr是头节点还是中间节点,prev和next都一定存在(要么是哨兵,要么是普通节点)
            Node* prev_node = curr->prev;
            Node* next_node = curr->next;

            prev_node->next = next_node;
            next_node->prev = prev_node;
            free(curr);
            return;
        }
        curr = curr->next;
    }
    // 没找到目标节点的话,可以加个提示,比如printf("Node not found!\n");
}

划重点:遍历终止条件是curr != list->tail_sentinel,不用再写curr != NULL,彻底杜绝了空指针访问的风险。

4. 遍历链表的逻辑有啥变化?

原来的遍历是从head走到NULL,现在改成从头哨兵的next节点开始,走到尾哨兵结束就行,完美避开哨兵节点的无效数据:

// 遍历并打印所有有效元素
void traverse_dll(DoublyLinkedList* list) {
    Node* curr = list->head_sentinel->next;
    while (curr != list->tail_sentinel) {
        printf("%d ", curr->val);
        curr = curr->next;
    }
    printf("\n");
}

5. 释放内存时要注意啥?

释放链表的时候,记得先释放所有普通节点,最后再释放两个哨兵节点,别漏了哨兵导致内存泄漏:

// 释放整个链表的内存
void free_dll(DoublyLinkedList* list) {
    Node* curr = list->head_sentinel->next;
    while (curr != list->tail_sentinel) {
        Node* temp = curr;
        curr = curr->next;
        free(temp);
    }
    // 别忘了释放两个哨兵
    free(list->head_sentinel);
    free(list->tail_sentinel);
    free(list);
}

内容的提问来源于stack exchange,提问作者Sandwich

火山引擎 最新活动