这章讲解了双向链表和双向循环链表的创建,销毁,删除,头插法和尾插法,查找和替换,以及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内核链表用侵入式设计实现通用性 数组和链表各有优劣 根据场景选择 记住多画图理解指针变化 动手写代码调试 就能掌握精髓
希望这篇博客帮你理清思路 有问题随时交流 下次见