深入浅出双向链表与Linux内核链表 附数组链表核心区别解析

这章讲解了双向链表和双向循环链表的创建,销毁,删除,头插法和尾插法,查找和替换,以及linux中c语言的内核链表,最后简单讲解了数组和链表的区别。

大家好, 今天我们一起深入学习双向链表和双向循环链表 还会聊聊Linux内核链表的巧妙设计 最后对比数组和链表的区别 我会用最通俗的语言 配合详细注释的代码 让任何级别的学习者都能看懂

一 双向链表基础 什么是双向链表

想象一列火车 每节车厢不仅能看到后面的车厢 还能看到前面的车厢 双向链表就像这样的火车 每个节点 Node 包含三部分

1 数据域 data 存放实际数据

2 前驱指针 prev 指向前面一个节点

3 后继指针 next 指向后面一个节点

这样的结构让我们既能从头往后遍历 也能从尾往前遍历 比单向链表更灵活

双向链表节点定义
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 双向链表节点结构体
typedef struct DNode {
    int data;           // 数据域 存储整数示例
    struct DNode* prev; // 前驱指针 指向前一个节点
    struct DNode* next; // 后继指针 指向后一个节点
} DNode;

二 双向链表的核心操作 从创建到销毁

1 创建节点 初始化一个孤立节点

创建一个新节点 数据设为指定值 前后指针先置为空

cpp 复制代码
// 创建新节点 返回节点指针 失败返回NULL
DNode* createDNode(int data) {
    DNode* newNode = (DNode*)malloc(sizeof(DNode)); // 申请内存
    if (newNode == NULL) { // 内存申请失败处理
        printf("内存分配失败\n");
        return NULL;
    }
    newNode->data = data;  // 设置数据
    newNode->prev = NULL;  // 初始无前驱
    newNode->next = NULL;  // 初始无后继
    return newNode;
}
2 销毁链表 释放所有节点内存

遍历链表 逐个释放节点 注意释放顺序避免野指针

cpp 复制代码
// 销毁整个双向链表 传入头节点指针的地址 释放后头指针置空
void destroyDList(DNode** head) {
    if (head == NULL || *head == NULL) return; // 空链表直接返回
    DNode* current = *head;
    DNode* temp;
    while (current != NULL) { // 遍历所有节点
        temp = current;       // 暂存当前节点
        current = current->next; // 移动到下一个
        free(temp);           // 释放暂存节点
    }
    *head = NULL; // 头指针置空 避免悬垂指针
}
3 头插法 在链表头部插入新节点

新节点成为新的头节点 原头节点变成它的后继 注意处理空链表情况

cpp 复制代码
// 头插法 在链表头部插入节点 返回新的头节点
DNode* insertAtHead(DNode* head, int data) {
    DNode* newNode = createDNode(data); // 创建新节点
    if (newNode == NULL) return head;   // 创建失败返回原头
    
    if (head == NULL) { // 空链表 新节点就是头节点
        return newNode;
    }
    
    // 非空链表 新节点next指向原头 原头prev指向新节点
    newNode->next = head;
    head->prev = newNode;
    return newNode; // 新节点成为新头
}
4 尾插法 在链表尾部插入新节点

找到当前尾节点 新节点接在后面 注意空链表时直接作为头节点

cpp 复制代码
// 尾插法 在链表尾部插入节点 返回头节点
DNode* insertAtTail(DNode* head, int data) {
    DNode* newNode = createDNode(data);
    if (newNode == NULL) return head;
    
    if (head == NULL) { // 空链表 新节点就是头
        return newNode;
    }
    
    // 找尾节点 尾节点next为NULL
    DNode* tail = head;
    while (tail->next != NULL) {
        tail = tail->next;
    }
    // 尾节点next指向新节点 新节点prev指向尾节点
    tail->next = newNode;
    newNode->prev = tail;
    return head; // 头节点不变
}
5 删除节点 按值删除第一个匹配的节点

分三种情况 删头节点 删尾节点 删中间节点 注意指针重新连接

cpp 复制代码
// 删除第一个值为data的节点 返回头节点 不存在则不变
DNode* deleteNode(DNode* head, int data) {
    if (head == NULL) return NULL; // 空链表
    
    DNode* current = head;
    // 遍历找目标节点
    while (current != NULL && current->data != data) {
        current = current->next;
    }
    if (current == NULL) return head; // 没找到
    
    // 情况1 删头节点 current是头且prev为NULL
    if (current->prev == NULL) {
        head = current->next; // 新头是原头的next
        if (head != NULL) {   // 如果新头存在 新头prev置空
            head->prev = NULL;
        }
    } 
    // 情况2 删尾节点 current是尾且next为NULL
    else if (current->next == NULL) {
        current->prev->next = NULL; // 前驱的next置空
    } 
    // 情况3 删中间节点
    else {
        current->prev->next = current->next; // 前驱next指向后继
        current->next->prev = current->prev; // 后继prev指向前驱
    }
    
    free(current); // 释放节点内存
    return head;
}
6 查找与替换 遍历找节点并修改数据

查找返回节点指针 替换直接修改找到节点的数据

cpp 复制代码
// 查找值为data的节点 返回节点指针 没找到返回NULL
DNode* findNode(DNode* head, int data) {
    DNode* current = head;
    while (current != NULL) {
        if (current->data == data) {
            return current; // 找到返回节点
        }
        current = current->next;
    }
    return NULL; // 没找到
}

// 替换第一个值为oldData的节点数据为newData 成功返回1 失败0
int replaceNode(DNode* head, int oldData, int newData) {
    DNode* node = findNode(head, oldData);
    if (node != NULL) {
        node->data = newData; // 修改数据
        return 1;
    }
    return 0;
}

三 双向循环链表 首尾相连的双向链表

双向循环链表和普通双向链表类似 只是尾节点的next指向头节点 头节点的prev指向尾节点 形成环

双向循环链表节点定义 和普通双向链表一样
复制代码
// 双向循环链表节点 结构体定义同上 复用DNode
双向循环链表头插法示例

和普通头插法区别 插入后新节点的prev指向尾节点 尾节点的next指向新节点

cpp 复制代码
// 双向循环链表头插法 返回新的头节点
DNode* insertAtHeadCircle(DNode* head, int data) {
    DNode* newNode = createDNode(data);
    if (newNode == NULL) return head;
    
    if (head == NULL) { // 空链表 自己成环
        newNode->next = newNode;
        newNode->prev = newNode;
        return newNode;
    }
    
    // 非空链表 找到尾节点 尾节点是head->prev
    DNode* tail = head->prev;
    // 新节点next指向原头 新节点prev指向尾
    newNode->next = head;
    newNode->prev = tail;
    // 原头prev指向新节点 尾节点next指向新节点
    head->prev = newNode;
    tail->next = newNode;
    return newNode; // 新节点成为新头
}

四 Linux内核链表 嵌入式设计的典范

Linux内核链表不是把数据和指针放一起 而是用一个通用的list_head结构体只存指针 然后把list_head嵌入到你的数据结构中 这样链表操作和数据无关 超级通用

内核链表核心结构体
cpp 复制代码
// Linux内核链表节点 仅含前后指针
struct list_head {
    struct list_head* next;
    struct list_head* prev;
};

// 自定义数据结构 嵌入list_head
struct student {
    int id;
    char name[20];
    struct list_head list; // 嵌入内核链表节点
};
内核链表常用宏 简化操作

内核提供了LIST_HEAD_INIT LIST_HEAD container_of等宏 我们模拟实现核心逻辑

cpp 复制代码
// 初始化链表头
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

// 添加节点到链表头 模拟内核list_add
void list_add(struct list_head* newNode, struct list_head* head) {
    newNode->next = head->next;
    newNode->prev = head;
    head->next->prev = newNode;
    head->next = newNode;
}

// 通过list_head指针获取整个结构体地址 container_of宏原理
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
    const typeof(((type*)0)->member)* __mptr = (ptr); \
    (type*)((char*)__mptr - offsetof(type, member)); })

// 示例使用 遍历内核链表
void traverseKernelList(struct list_head* head) {
    struct list_head* pos;
    struct student* s;
    // 内核遍历宏for_each_entry简化版
    for (pos = head->next; pos != head; pos = pos->next) {
        // 通过pos获取student结构体地址
        s = container_of(pos, struct student, list);
        printf("id=%d name=%s\n", s->id, s->name);
    }
}

五 数组和链表的区别 一张表说清

对比项 数组 双向链表
内存分配 连续空间 非连续空间
大小 固定 编译时确定 动态 运行时增减
插入删除效率 低 需移动大量元素 高 仅改指针
访问方式 随机访问 O1 顺序访问 On
内存开销 小 仅存数据 大 每个节点多两个指针

六 示例 双向链表完整操作流程

cpp 复制代码
int main() {
    DNode* head = NULL; // 初始化空链表
    
    // 头插三个节点 数据10 20 30 头插后顺序是30 20 10
    head = insertAtHead(head, 10);
    head = insertAtHead(head, 20);
    head = insertAtHead(head, 30);
    
    // 尾插两个节点 数据40 50 链表变为30 20 10 40 50
    head = insertAtTail(head, 40);
    head = insertAtTail(head, 50);
    
    // 查找数据20的节点
    DNode* found = findNode(head, 20);
    if (found) printf("找到节点 数据=%d\n", found->data); // 输出20
    
    // 替换数据10为100
    replaceNode(head, 10, 100);
    
    // 删除数据30的节点 链表变为20 100 40 50
    head = deleteNode(head, 30);
    
    // 销毁链表
    destroyDList(&head);
    return 0;
}

总结

双向链表通过双指针实现双向遍历 头插尾插灵活高效 双向循环链表首尾相连适合环形场景 Linux内核链表用侵入式设计实现通用性 数组和链表各有优劣 根据场景选择 记住多画图理解指针变化 动手写代码调试 就能掌握精髓

希望这篇博客帮你理清思路 有问题随时交流 下次见

相关推荐
wWYy.4 小时前
指针与引用区别
数据结构
wanghu20244 小时前
AT_abc443_C~E题题解
c语言·算法
梵刹古音4 小时前
【C语言】 浮点型(实型)变量
c语言·开发语言·嵌入式
历程里程碑4 小时前
Linux 17 程序地址空间
linux·运维·服务器·开发语言·数据结构·笔记·排序算法
-dzk-5 小时前
【代码随想录】LC 203.移除链表元素
c语言·数据结构·c++·算法·链表
齐落山大勇5 小时前
数据结构——栈与队列
数据结构
进击的小头5 小时前
陷波器实现(针对性滤除特定频率噪声)
c语言·python·算法
毅炼5 小时前
hot100打卡——day17
java·数据结构·算法·leetcode·深度优先
404未精通的狗5 小时前
(数据结构)二叉树(上)
数据结构