数据结构3-双向链表、循环链表
摘要:本文详细介绍了三种链表结构的实现:双向链表、循环链表和内核链表。主要内容包括:1. 双向链表的节点定义、创建、头插/尾插法、遍历、查找、修改、删除和销毁操作的C语言实现;2. 循环链表的特殊处理(首尾相连)及各操作的实现差异;3. 内核链表的特点(节点嵌入数据结构)及其与传统链表的区别。通过代码示例和操作步骤说明,系统讲解了每种链表的基本操作原理和实现要点,包括指针处理、内存管理等关键技术细节。
一、双向链表
(一)链表节点的定义
1、定义的代码实现:
/* 节点存放数据的类型 */
typedef int datatype;
/* 节点类型 */
typedef struct node
{
datatype data; //存放数据空间
struct node *ppre; //存放前一个节点地址
struct node *pnext; //存放下一个节点地址
}linknode;
(二)空白链表的创建
1、步骤:
- 申请节点空间
- 对pnext和ppre赋值为NULL
- 返回空白节点地址即可
图例
2、代码实现:
/* 创建一个空链表 */
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、步骤:
- 申请节点
- 存放数据
- pnext赋值为phead->pnext
- ppre赋值为phead的地址
- phead->pnext赋值为新申请节点地址
- 如果有后一个节点,需要让后一个节点的ppre指向该节点
2、代码实现:
/* 链表头插法 */
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;}
3、注意事项:
与单向链表思想上的不同是:要判断ptmpnode->pnext;后面还有没有节点。
(四)链表尾插法
1、步骤:
- 申请节点
- 将节点的pnext赋值为NULL
- 找到链表最后一个节点
- 将节点的ppre赋值为最后一个节点地址
- 将最后一个节点的pnext赋值为新申请节
2、代码实现:
/* 链表尾插法 */
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、步骤:与单向链表类似
2、代码实现:
/* 链表的遍历 */
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、步骤:与单向链表类似
2、代码实现:
/* 链表的查询 */
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、步骤:与单向链表的类似
2、代码实现:
/* 链表的修改 */
int update_linklist(linknode *phead, datatype olddata, datatype newdata)
{
linknode *ptmpnode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != NULL)
{
if (ptmpnode->data == olddata)
{
ptmpnode->data = newdata;
}
ptmpnode = ptmpnode->pnext;
}
return 0;
}
(八)链表的删除
1、步骤:
- 找到要删除的节点
- 让删除节点的前一个节点的pnext指向要删除节点的后一个节点
- 让伤处节点的后一个节点的ppre指向要删除节点的前一个节点
- 将要删除的节点释掉
<细节版>
- 定义两个指针ptmpnode、pfreenode,ptmpnode用来遍历寻找要删除的数据
- 将ptmpnode->pnext->ppre = ptmpnode->ppre;
- 将ptmpnode->ppre->pnext = ptmpnode->pnext;
- 令pfreenode = ptmpnode,ptmpnode往前进一步
- 释放pfreenode空间
图例
2、代码实现:
/* 链表的删除 */
int delete_linklist(linknode *phead, datatype tmpdata)
{
linknode *ptmpnode = NULL;
linknode *pfreenode = NULL;
ptmpnode = phead->pnext;
while (ptmpnode != NULL)
{
if (ptmpnode->data == tmpdata)
{
ptmpnode->ppre->pnext = ptmpnode->pnext;
if (ptmpnode->pnext != NULL)
{
ptmpnode->pnext->ppre = ptmpnode->ppre;
}
pfreenode = ptmpnode;
ptmpnode = ptmpnode->pnext;
free(pfreenode);
}
else
{
ptmpnode = ptmpnode->pnext;
}
}
return 0;
}
3、注意事项:
1)设置两个指针的作用:
① 删除链表元素完要释放空间,所以需要定于一个指针(pfreenode后续称为A)指向此时的位置(ptmpnode后续称为B)。
② 若没有A,直接释放掉B之后我们就找不到被删除的下一个节点位置。
③ 所以需要A指向B此时的位置,然后让B往前再走一个节点到达下一个节点。然后释放掉A指向节点的空间。
2)解析释放空间的操作:
free(pfreenode); 释放的是该指针指向的空间,而不是指针本身。该操作之后此指针就为悬垂指针。pfreenode仍然可以重新指向别的空间。所以为了误用,规范使用最好释放后立即置空。
(九)链表的销毁
1、步骤:与单向链表类似
2、代码实现:
/* 链表的销毁 */
int destroy_linklist(linknode **pphead)
{
linknode *ptmpnode = NULL;
linknode *pfreenode = NULL;
ptmpnode = *pphead;
pfreenode = ptmpnode;
while (ptmpnode != NULL)
{
ptmpnode = ptmpnode->pnext;
free(pfreenode);
pfreenode = ptmpnode;
}
*pphead = NULL;
return 0;
}
二、循环链表
(一)节点定义
1、步骤:与双向链表类似
2、代码实现:
/* 节点存放数据的类型 */
typedef int datatype;
/* 节点类型 */
typedef struct node
{
datatype data; //存放数据空间
struct node *ppre; //存放前一个节点地址
struct node *pnext; //存放下一个节点地址
}linknode
(二)链表创建
1、步骤:
- 申请节点
- pnext和ppre都指向自己
2、代码实现:
/* 创建空链表 */
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;
}
(三)链表头插法
1、步骤:
- 申请节点空间
- 存放数据
- 将pnext指向空白节点的pnext
- 将ppre指向空白节点
- 将空白节点的pnext指向新申请节点
- 将后续节点的ppre指向新申请节点
2、代码实现:
/* 头插法 */
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;
}
(四)链表尾插法
1、步骤:
- 申请节点空间
- 将数据存放到节点中
- 将pnext赋值为空白节点
- 将ppre赋值为之前的最后一个节点地址
- 将之前最后一个节点的pnext指向新申请节点
- 将空白节点的ppre指向新申请节点
2、代码实现:
/* 尾插法 */
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->pnext->ppre = ptmpnode;
ptmpnode->ppre->pnext = ptmpnode;
return 0;
}
(六)链表的遍历
1、步骤:与双向链表类似,循环链表最终循环条件由 != NULL修改为 != phea
2、代码实现:
/* 链表节点遍历 */
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;
}
(七)链表的查找
1、步骤:与双向链表类似
2、代码实现:
/* 链表的查找 */
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;
}
(八)链表的修改
1、步骤:与双向链表类似
2、代码实现:
/* 链表的修改 */
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;
}
(九)链表的删除
1、步骤:与双向链表类似
2、代码实现:
/* 链表的删除 */
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);pfreenode = NULL;
}
else
{
ptmpnode = ptmpnode->pnext;
}}
return 0;
}
(十)链表的销毁
1、步骤:与双向链表类似
1、代码实现:
/* 链表的销毁 */
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;
}
三、内核链表
1、概念:
- Linux内核中所使用到的一种链表结构
- 使用同一种结构可以存储不同类型的链表
- 普通链表:链表节点中包含数据
- 内核链表:数据中包含链表节点
2、结构:
3、 内核链表、单向链表、双向链表和循环链表 区别与联系
- 单向链表
- 由一系列节点组成,每个节点包含数据域和一个指向下一个节点的指针。
- 节点之间的连接是单向的,只能从链表头开始,沿着指针依次访问后续节点,无法反向遍历。
- 双向链表
- 同样由节点构成,每个节点除了包含数据域和指向下一个节点的指针外,还包含一个指向前一个节点的指针。
- 可以从链表头向链表尾遍历,也可以从链表尾向链表头遍历,增加了遍历的灵活性。
- 循环链表
- 可以是单向循环链表或双向循环链表。在单向循环链表中,最后一个节点的指针指向链表头节点;在双向循环链表中,头节点的前驱指针指向尾节点,尾节点的后继指针指向头节点。
- 形成一个封闭的环,使得遍历可以无限循环下去。
- 内核链表
- 是 Linux 内核中使用的一种链表实现方式,它将链表节点嵌入到其他数据结构中。节点只包含两个指针(前驱和后继),不包含数据域。
- 通过这些指针将不同的数据结构连接成链表,使得链表操作与具体的数据结构解耦。