数据结构(2) —— 双向链表、循环链表与内核链表
1 双向链表
操作功能 | 函数定义 | 核心步骤 | 注意事项 |
---|---|---|---|
节点定义 | typedef int datatype; typedef struct node { datatype data; struct node *ppre; struct node *pnext; } linknode; | 定义数据类型datatype (int)和节点类型linknode ,包含数据域data 、前驱指针ppre 和后继指针pnext | 指针域需正确指向前后节点 |
创建空双向链表 | linknode *create_empty_linklist(void) | 1. 申请头节点空间2. 将头节点的ppre 和pnext 置为NULL 3. 返回头节点地址 | 内存分配失败时需处理错误 |
头插法 | int insert_head_linklist(linknode *phead, datatype tmpdata) | 1. 申请新节点并存储数据2. 新节点pnext 指向头节点pnext ,ppre 指向头节点3. 头节点pnext 指向新节点4. 若新节点后有节点,更新其后继节点的ppre | 处理新节点为第一个或非第一个节点的情况 |
尾插法 | int insert_tail_linklist(linknode *phead, datatype tmpdata) | 1. 申请新节点并存储数据2. 找到最后一个节点3. 最后节点pnext 指向新节点,新节点ppre 指向最后节点4. 新节点pnext 置为NULL | 遍历找到尾节点时需判断pnext 是否为NULL |
遍历链表 | int show_linklist(linknode *phead) | 1. 从首元节点(头节点pnext )开始2. 依次打印节点数据,直到节点为NULL | 避免从头节点开始遍历(头节点无数据) |
查找节点 | linknode *find_linklist(linknode *phead, datatype tmpdata) | 1. 从首元节点开始遍历2. 找到数据匹配节点返回该节点,否则返回NULL | 需完整遍历链表直至pnext 为NULL |
修改节点 | int update_linklist(linknode *phead, datatype olddata, datatype newdata) | 1. 遍历链表查找旧数据节点2. 找到后修改为新数据3. 返回 0(成功)或 - 1(失败) | 可修改所有匹配的节点 |
删除节点 | int delete_linklist(linknode *phead, datatype tmpdata) | 1. 找到目标节点2. 前一节点pnext 指向后一节点,后一节点ppre 指向前一节点3. 释放目标节点内存 | 需处理删除节点为尾节点的情况(无后一节点) |
销毁链表 | int destroy_linklist(linknode **pphead) | 1. 从头节点开始依次释放每个节点2. 最后将头节点指针置为NULL | 使用二级指针确保头节点在外部被正确置空,避免野指针 |
双向链表通过双指针实现前后节点访问,操作时需特别注意指针关系的维护,尤其是插入和删除操作
1.1 节点定义
- 定义节点结构体,包含数据域
data
、指向前驱节点的指针*ppre
和指向后继节点的指针*pnext
。 - 用
typedef
定义数据类型datatype
(这里为int
)和节点类型linknode
。
typedef int datatype;
typedef struct node {
datatype data; // 存放数据空间
struct node *ppre; // 指向前驱节点的指针
struct node *pnext; // 指向后继节点的指针
} linknode;
1.2 创建空双向链表
- 申请节点空间,将新节点的
ppre
和pnext
都置为NULL
,返回该空节点地址。
linknode *create_empty_linklist(void) {
linknode *ptmpnode = NULL;
ptmpnode = malloc(sizeof(linknode));
if (NULL == ptmpnode) {
perror("fail to malloc");
return NULL;
}
ptmpnode->ppre = NULL;
ptmpnode->pnext = NULL;
return ptmpnode;
}
1.3 链表的头插法
- 申请新节点,存储数据,将新节点的
pnext
赋值为头节点的pnext
,ppre
赋值为头节点地址。 - 再将头节点的
pnext
赋值为新节点地址,若新节点后有节点,需让后一节点的ppre
指向新节点。
int insert_head_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
ptmpnode = malloc(sizeof(linknode));
if (NULL == ptmpnode) {
perror("fail to malloc");
return -1;
}
ptmpnode->data = tmpdata;
ptmpnode->pnext = phead->pnext;
ptmpnode->ppre = phead;
phead->pnext = ptmpnode;
if (ptmpnode->pnext != NULL) {
ptmpnode->pnext->ppre = ptmpnode;
}
return 0;
}
1.4 链表尾插
- 申请新节点,存储数据,将新节点的
pnext
置为NULL
,找到链表最后一个节点,将其pnext
指向新节点,新节点的ppre
指向最后一个节点。
int insert_tail_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
linknode *plastnode = NULL;
ptmpnode = malloc(sizeof(linknode));
if (NULL == ptmpnode) {
perror("fail to malloc");
return -1;
}
plastnode = phead;
while (plastnode->pnext != NULL) {
plastnode = plastnode->pnext;
}
ptmpnode->data = tmpdata;
ptmpnode->pnext = NULL;
ptmpnode->ppre = plastnode;
plastnode->pnext = ptmpnode;
return 0;
}
1.5 链表的遍历
- 从首元节点(头节点的
pnext
指向的节点)开始,依次打印节点数据,直到节点为NULL
。
int show_linklist(linknode *phead) {
linknode *ptmpnode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != NULL) {
printf("%d ", ptmpnode->data);
ptmpnode = ptmpnode->pnext;
}
printf("\n");
return 0;
}
1.6 链表的查找
- 从首元节点开始遍历,若找到数据等于目标数据的节点,返回该节点;否则返回
NULL
。
linknode *find_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != NULL) {
if (ptmpnode->data == tmpdata) {
return ptmpnode;
}
ptmpnode = ptmpnode->pnext;
}
return NULL;
}
1.7 链表的修改
- 遍历链表,找到数据为旧数据的节点,将其数据修改为新数据。
int update_linklist(linknode *phead, datatype olddata, datatype newdata)
{
linknode *ptmpnode = NULL;
int found = 0; // 标记是否找到要修改的节点
ptmpnode = phead->pnext;
while (ptmpnode != NULL){
if (ptmpnode->data == olddata){
ptmpnode->data = newdata;
found = 1;
}
ptmpnode = ptmpnode->pnext;
}
return found ? 0 : -1; // 找到并修改返回0,未找到返回-1
}
1.8 链表的删除
- 找到要删除的节点,让该节点前一节点的
pnext
指向后一节点,后一节点的ppre
指向前一节点,释放该节点。
int delete_linklist(linknode *phead, datatype tmpdata)
{
linknode *ptmpnode = NULL;
linknode *pfreernode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != NULL){
if (ptmpnode->data == tmpdata){
pfreernode = ptmpnode;
// 前一个节点指向后一个节点
ptmpnode->ppre->pnext = ptmpnode->pnext;
// 如果有后一个节点,让它的前驱指向当前节点的前驱
if (ptmpnode->pnext != NULL){
ptmpnode->pnext->ppre = ptmpnode->ppre;
}
// 保存下一个节点地址
ptmpnode = ptmpnode->pnext;
// 释放当前节点
free(pfreernode);
}else
{
// 不匹配则移动到下一个节点
ptmpnode = ptmpnode->pnext;
}
}
return 0;
}
1.9 链表的销毁
- 从头节点开始,依次释放每个节点,最后将头节点置为
NULL
。
int destroy_linklist(linknode **pphead) {
linknode *ptmpnode = NULL;
linknode *pfreernode = NULL;
ptmpnode = *pphead;
pfreernode = ptmpnode;
while (ptmpnode != NULL) {
ptmpnode = ptmpnode->pnext;
free(pfreernode);
pfreernode = ptmpnode;
}
*pphead = NULL;
return 0;
}
2 循环链表
操作功能 | 函数定义 | 核心步骤 | 注意事项 |
---|---|---|---|
创建空链表 | linknode *create_empty_linklist(void) | 1. 申请头节点内存空间2. 头节点的pnext 和ppre 均指向自身 | 1. 需检查malloc 是否成功2. 初始循环结构的正确建立 |
头插法插入 | int insert_head_linklist(linknode *phead, datatype tmpdata) | 1. 申请新节点并赋值2. 新节点pnext 指向头节点的pnext 3. 新节点ppre 指向头节点4. 调整原相邻节点指针指向新节点 | 1. 确保头节点不为空2. 指针修改顺序避免断裂3. 检查内存分配结果 |
尾插法插入 | int insert_tail_linklist(linknode *phead, datatype tmpdata) | 1. 申请新节点并赋值2. 新节点pnext 指向头节点3. 新节点ppre 指向头节点的ppre 4. 调整原尾节点和头节点指针 | 1. 确保头节点不为空2. 正确找到尾节点(头节点的ppre )3. 检查内存分配结果 |
遍历链表 | int show_linklist(linknode *phead) | 1. 从phead->pnext 开始遍历2. 循环访问直到节点等于头节点3. 输出每个节点的数据 | 1. 处理空链表情况2. 循环终止条件正确(ptmpnode != phead ) |
按值查找 | linknode *find_linklist(linknode *phead, datatype tmpdata) | 1. 从首节点开始遍历2. 比较节点数据与目标值3. 找到则返回节点,否则返回NULL | 1. 空链表直接返回NULL 2. 只返回第一个匹配节点 |
按值修改 | int update_linklist(linknode *phead, datatype olddata, datatype newdata) | 1. 遍历所有节点2. 找到数据为olddata 的节点3. 将其数据更新为newdata | 1. 会修改所有匹配的节点2. 需处理空链表情况 |
按值删除 | int delete_linklist(linknode *phead, datatype tmpdata) | 1. 遍历查找目标节点2. 调整前后节点指针跳过目标节点3. 释放目标节点内存 | 1. 释放节点前先移动遍历指针2. 避免空指针操作3. 会删除所有匹配的节点 |
销毁链表 | int destroy_linklist(linknode **pphead) | 1. 遍历释放所有有效节点2. 释放头节点3. 将头指针置为NULL | 1. 使用二级指针确保头指针能被置空2. 释放顺序避免访问已释放内存3. 检查传入指针的有效性 |
2.1 特点
- 头结点的
ppre
指向链表最后一个节点。 - 最后节点的
pnext
指向第一个节点,形成循环结构。
2.2 节点定义
typedef int datatype; // 定义数据类型
typedef struct node {
datatype data; // 存储数据
struct node *ppre; // 指向前一个节点
struct node *pnext; // 指向后一个节点
} linknode;
2.3 链表创建
- 申请节点空间。
- 新节点的
pnext
和ppre
都指向自己,形成初始循环。
linknode *create_empty_linklist(void) {
linknode *ptmpnode = NULL;
ptmpnode = malloc(sizeof(linknode));
if (NULL == ptmpnode) {
perror("fail to malloc");
return NULL;
}
ptmpnode->pnext = ptmpnode->ppre = ptmpnode;
return ptmpnode;
}
2.4 链表头插法
- 申请节点空间,存储数据。
- 将新节点的
pnext
指向头节点的pnext
。 - 将新节点的
ppre
指向头节点。 - 更新头节点的
pnext
和原头节点下一个节点的ppre
,指向新节点。
int insert_head_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
ptmpnode = malloc(sizeof(linknode));
if (NULL == ptmpnode) {
perror("fail to malloc");
return -1;
}
ptmpnode->data = tmpdata;
ptmpnode->pnext = phead->pnext;
ptmpnode->ppre = phead;
ptmpnode->pnext->ppre = ptmpnode;
ptmpnode->ppre->pnext = ptmpnode;
return 0;
}
2.5 链表尾插法
- 申请节点空间,存储数据。
- 新节点的
pnext
指向头节点。 - 新节点的
ppre
指向头节点的ppre
(即原最后一个节点)。 - 更新原最后一个节点的
pnext
和头节点的ppre
,指向新节点。
int insert_tail_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
ptmpnode = malloc(sizeof(linknode));
if (NULL == ptmpnode) {
perror("fail to malloc");
return -1;
}
ptmpnode->data = tmpdata;
ptmpnode->pnext = phead;
ptmpnode->ppre = phead->ppre;
ptmpnode->ppre->pnext = ptmpnode;
ptmpnode->pnext->ppre = ptmpnode;
return 0;
}
2.6 链表的遍历
- 循环条件为当前节点不等于头节点,依次访问每个节点数据。
int show_linklist(linknode *phead) {
linknode *ptmpnode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != phead) {
printf("%d ", ptmpnode->data);
ptmpnode = ptmpnode->pnext;
}
printf("\n");
return 0;
}
2.7 链表查找
- 从首节点开始遍历,若找到数据匹配的节点则返回该节点,否则返回
NULL
。
linknode *find_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != phead) {
if (ptmpnode->data == tmpdata) {
return ptmpnode;
}
ptmpnode = ptmpnode->pnext;
}
return NULL;
}
2.8 链表的修改
- 遍历链表,找到数据为
olddata
的节点,将其数据修改为newdata
。
int update_linklist(linknode *phead, datatype olddata, datatype newdata) {
linknode *ptmpnode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != phead) {
if (ptmpnode->data == olddata) {
ptmpnode->data = newdata;
}
ptmpnode = ptmpnode->pnext;
}
return 0;
}
2.9 链表的删除
- 遍历链表,找到数据为
tmpdata
的节点,调整其前后节点的指针,断开该节点连接,释放节点空间。
int delete_linklist(linknode *phead, datatype tmpdata) {
linknode *ptmpnode = NULL;
linknode *pfreenode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != phead) {
if (ptmpnode->data == tmpdata) {
ptmpnode->ppre->pnext = ptmpnode->pnext;
ptmpnode->pnext->ppre = ptmpnode->ppre;
pfreenode = ptmpnode;
ptmpnode = ptmpnode->pnext;
free(pfreenode);
} else {
ptmpnode = ptmpnode->pnext;
}
}
return 0;
}
2.10 链表的销毁
- 从首节点开始,依次释放每个节点空间,最后释放头节点并将头节点指针置
NULL
。
int destroy_linklist(linknode **pphead) {
linknode *ptmpnode = NULL;
linknode *pfreenode = NULL;
ptmpnode = (*pphead)->pnext;
pfreenode = ptmpnode;
while (ptmpnode != *pphead) {
ptmpnode = ptmpnode->pnext;
free(pfreenode);
pfreenode = ptmpnode;
}
free(*pphead);
*pphead = NULL;
return 0;
}
3 内核链表
类别 | 内容 |
---|---|
概念 | 是 Linux 内核中所使用的一种链表结构,使用同一种结构可以保存不同类型的表,分为普通链表(链表节点中包含数据)和内核链表(数据中包含链表节点) |
结构描述 - 创建 | 创建空白节点 |
结构描述 - 插入 | 申请节点,并按照循环链表插入方式操作(插入的链表节点首地址即数据节点首地址) |
结构描述 - 访问节点 | 通过遍历找到每个节点的首地址,强制类型转换即可获得对应数据空间首地址 |
使用内核链表 - 基础结构与初始化 | - 定义 struct list_head 结构体,包含 next 和 prev 指针;- 用 INIT_LIST_HEAD 宏初始化空白头结点,使头结点的 next 和 prev 指针指向自身 |
使用内核链表 - 节点添加操作 | - list_add :将新节点添加到指定头结点之后;- list_add_tail :将新节点添加到指定头结点之前(尾部) |
使用内核链表 - 按序插入节点 | list_add_order 函数根据比较函数的返回值,将新节点按顺序插入链表 |
使用内核链表 - 节点删除操作 | - list_del :从链表中删除指定节点,并对节点指针做特殊赋值;- list_del_init :删除节点后,还会将节点重新初始化为头结点形式 |
使用内核链表 - 节点移动操作 | - list_move :将节点移动到另一个链表的头部;- list_move_tail :将节点移动到另一个链表的尾部 |
使用内核链表 - 链表状态判断 | - list_empty :判断链表是否为空,若头结点的 next 指针指向自身则为空;- list_is_singular :判断链表是否只有一个节点,即非空且头结点的 next 和 prev 指针指向同一节点 |
使用内核链表 - 链表拼接操作 | - __list_splice :将一个链表的所有元素移到另一个链表的前面;- list_splice_init :完成拼接后初始化原链表的头结点;- __list_append :将一个链表的所有元素添加到另一个链表的后面;- list_append_init :添加后初始化原链表头结点 |
使用内核链表 - 节点替换与链表旋转 | - list_replace :用新节点替换旧节点;- list_replace_init :替换后初始化旧节点;- list_rotate_left :实现链表左旋,将第一个节点移到尾部 |
使用内核链表 - 数据结构查找 | 通过 offsetof 和 container_of 宏,根据链表节点找到对应数据结构的首地址,还有 list_first_entry 、list_last_entry 等宏用于查找特定位置的数据元素地址 |
使用内核链表 - 链表遍历 | 提供多种遍历宏,如 list_for_each_entry 正向遍历所有数据元素,list_for_each_entry_safe 支持在遍历中修改指针,还有反向遍历及带安全判断的遍历宏 |
使用内核链表 - 节点空判断获取 | list_next_entry_or_null 和 list_prev_entry_or_null 宏用于获取下一个或上一个数据元素首地址(无则返回 NULL ) |