链表学习
1.简介
链表作为C语言中的一种基础数据结构,在操作系统里面是随处可见。链表好比一个圆型的晾衣架,上面有很多钩子,钩子首尾相连。链表也是,链表由节点(node)组成,节点之间首尾相连。
2.链表分类
2.1单向链表
前一个结点都有一个箭头指向后一个节点,首尾相连,组成一个圈。节点本身必须包含一个节点指针,用于指向下一个节点,同时还可以携带数据
2.2双向链表
2.3循环链表
2.4双向循环链表
3.链表使用
1.一般很少按照这种方法使用(书本上一般都是这样的)
2.通常这样做(后文说的数据与指针分离)
4.链表定义
4.1常用定义
typedef struct DList_Node{
struct DList_Node * prev;
struct DList_Node * next;
int data;//可以考虑自定义类型
}List_Node;
这种定义不够通用,只能携带int类型数据,如果要存放其他类型的数据需要改代码。用union也无法表示所有类型的数据,数据类型太多了(自定义类型)
4.2存值
typedef struct DList_Node{
struct DList_Node * prev;
struct DList_Node * next;
void* data;
size_t length;
}List_Node;
存入数据的指针和长度,但是复制数据会带来开销,很少用
4.2存指针
typedef struct DList_Node{
struct DList_Node * prev;
struct DList_Node * next;
void* data;
}List_Node;
效率最高,使用时候强制转换就行,浪费一个指针空间
5.链表代码&原理详解
5.1单向链表
typedef int element_type_t
typedef struct _list_node{
element_type_t data;//dtat由调用者(APP)提供
struct _list_node *p_next;//存放下一个节点的地址
}slist_node_t;
通常需要定义一个指向链表头节点的指针,便于从链表头节点开始,顺序访问链表的所有节点。比如 slist_node_t *p_head;
此时只要获取p_head
的值就可以遍历链表的所有节点
while (p_head != NULL) //从头开始遍历全部节点
{
//访问节点
p_head = p_head->p_next;
}
如果直接使用p_head
访问各个节点,遍历结束之后,p_head
的值为NULL
,它不再指向第一个节点,从而丢失了整个链表,所以需要通过一个临时指针变量访问链表
slist_node_t *p_tmp = p_head;
while (p_tmp != NULL) //从头开始遍历全部节点
{
//访问节点
p_tmp = p_tmp->p_next;
}
接下来考虑把节点添加到链表的尾部。初始状态下,链表是一个不包含任何节点的空表,此时p_head
为NULL
。则新增的节点就是头节点,修改p_head
的值,使其指向新节点
#include<stdio.h>
typedef int element_type_t
typedef struct _list_node{
element_type_t data;
struct _list_node *p_next;
}slist_node_t;
int slist_add_tail(slist_node_t **p_head,slist_node_t *p_node)
{
if(*p_head==NULL)//空链表添加第一个节点
{
*p_head=p_node;
p_node->p_next=NULL;
}
else //后续添加节点,更改链表节点p_next的值
{
slist_node_t *p_tmp=*p_head;
while(p_tmp->p_next!=NULL)
{
p_tmp=p_tmp->p_next;
}//遍历完毕
p_tmp->p_next=p_node;
p_node->p_next=NULL;
}
return 0;
}
int main()
{
slist_node_t *p_head=NULL;
slist_node_t node1,node2,node3;
slist_node_t *p_tmp;
node1.data=1;
node2.data=2;
node3.data=3;
slist_add_tail(&p_head,&node1);
slist_add_tail(&p_head,&node2);
slist_add_tail(&p_head,&node3);
p_tmp=p_head;
while(p_tmp!=NULL)
{
printf("%d\r\n",p_tmp->data);
p_tmp=p_tmp->p_next;
}
return 0;
}
以上程序可以添加成功,需要先判断当前链表是否为空。
如果开始就存在一个节点slist_node_t head
,这样链表就不会为空
对于这种类型的链表,始终存在一个无需有效数据的头节点,对于空链表,至少应该包含头节点。由于初始化时候不包含任何其他节点,因此p_next=NULL
int slist_add_tail(slist_node_t **p_head,slist_node_t *p_node)
{
slist_node_t *p_tmp=p_head;//这里的p_head始终指向存在的头节点
while(p_tmp->p_next!=NULL)
{
p_tmp=p_tmp->p_next;
}
p_tmp->p_next=p_node;
p_node->p_next=NULL;
return 0;
}
int main()
{
slist_node_t head={0,NULL};
slist_node_t node1,node2,node3;
slist_node_t *p_tmp;
node1.data=1;
node2.data=2;
node3.data=3;
slist_add_tail(&p_head,&node1);
slist_add_tail(&p_head,&node2);
slist_add_tail(&p_head,&node3);
p_tmp=head.p_next;
while(p_tmp!=NULL)
{
printf("%d\r\n",p_tmp->data);
p_tmp=p_tmp->p_next;
}
return 0;
}
5.2数据与p_next分离
如上所示,添加头节点之后,带来新的问题,头节点的data
区域浪费了
typedef struct _list_node{
struct _list_node *p_next;
}slist_node_t; //链表结构体
typedef struct _list_int{
slist_node_t node;//链表节点
int data;//数据节点
}slist_int_t;
int slist_add_tail(slist_node_t *p_head,slist_node_t *p_node);//可以继续使用
int main()
{
slist_node_t head={NULL};
slist_int_t node1,node2,node3;
slist_node_t *p_tmp;
node1.data=1;
node2.data=2;
node3.data=3;
slist_add_tail(&p_head,&node1.node);
slist_add_tail(&p_head,&node2.node);
slist_add_tail(&p_head,&node3.node);
p_tmp=head.p_next;
while(p_tmp!=NULL)
{
printf("%d\r\n",((slist_int_t*)p_tmp)->data);
p_tmp=p_tmp->p_next;
}
return 0;
}
由于用户需要初始化head
为NULL
,且遍历时需要操作每个节点的p_next
指针,而将数据与指针分离就是为了分离功能职责,链表只需关系p_next
的处理,用户只关心数据的处理。因此,对用户来说,链表的定义就是一个黑盒子,只能通过链表相关的接口来操作,而不能访问其成员
为了完成头节点的初始赋值,应该提供一个初始化函数,本质就是将头节点的p_next
设置为NULL
,原型为int slist_init (slist_node_t *p_head);
由于头节点与其他普通节点类型一样,因此很容易让人误以为这是初始化所有节点的函数。实际上头节点与普通节点含义不一样,只要获取头节点就可遍历整个链表。为了避免混淆,定义头节点如下typedef slist_node_t slist_head_t;
初始化函数如下int slist_init (slist_head_t *p_head);
,p_head指向待初始化的链表头节点
int slist_init (slist_head_t *p_head)
{
if (p_head == NULL){
return -1;
}
p_head -> p_next = NULL;
return 0;
}
在向链表添加节点前,需要初始化头节点
slist_node_t head;
slist_init(&head);
由于重新定义了头节点的类型,因此添加节点的函数原型也应该修改为int slist_add_tail (slist_head_t *p_head, slist_node_t *p_node);
int slist_add_tail (slist_head_t *p_head, slist_node_t *p_node)
{
slist_node_t *p_tmp;
if ((p_head == NULL) || (p_node == NULL)){
return -1;
}
p_tmp = p_head;
while (p_tmp -> p_next != NULL){
p_tmp = p_tmp -> p_next;
}
p_tmp -> p_next = p_node;
p_node -> p_next = NULL;
return 0;
}
当前链表的遍历还是采用访问节点成员的方式,核心代码如下
slist_node_t *p_tmp = head.p_next;
while (p_tmp != NULL){
printf("%d ", ((slist_int_t *)p_tmp)->data);
p_tmp = p_tmp->p_next;
}
主要包括得到第一个节点,得到下一个节点,判空。基于此可以提供3个接口函数
slist_node_t *slist_begin_get (slist_head_t *p_head); // 获取开始位置第一个用户节点
slist_node_t *slist_next_get (slist_head_t *p_head, slist_node_t *p_pos);// 获得某节点的后一个节点
slist_node_t *slist_end_get (slist_head_t *p_head); 、、结束位置,尾节点的下一个位置
slist_node_t *slist_next_get (slist_head_t *p_head, slist_node_t *p_pos)
{
if (p_pos) {
return p_pos->p_next;
}
return NULL;
}
slist_node_t *slist_begin_get (slist_head_t *p_head)
{
return slist_next_get(p_head, p_head);
}
slist_node_t *slist_end_get (slist_head_t *p_head)
{
return NULL;
}
//使用接口遍历
slist_node_t *p_tmp = slist_begin_get(&head);
slist_node_t *p_end = slist_end_get(&head);
while (p_tmp != p_end){
printf("%d ", ((slist_int_t *)p_tmp)->data);
p_tmp = slist_next_get(&head, p_tmp);
}
//上面printf才是用户关心的数据,可以封装成一个回调函数,由用户实现,然后传递给遍历函数
typedef int (*slist_node_process_t) (void *p_arg, slist_node_t *p_node);
int slist_foreach(slist_head_t *p_head,
slist_node_process_t pfn_node_process,
void *p_arg);
int slist_foreach( slist_head_t *p_head,
slist_node_process_t pfn_node_process,
void *p_arg);
{
slist_node_t *p_tmp, *p_end;
int ret;
if ((p_head == NULL) || (pfn_node_process == NULL)){
return -1; }
p_tmp = slist_begin_get(p_head);
p_end = slist_end_get(p_head);
while (p_tmp != p_end){
ret = pfn_node_process(p_arg, p_tmp);
if (ret < 0) return ret; // 返回
p_tmp = slist_next_get(p_head, p_tmp); // 继续下一个节点
}
return 0;
}
1 #include <stdio.h>
2 #include "slist.h"
3
4 typedef struct _slist_int {
5 slist_node_t node; //
6 int data; // int
7 }slist_int_t;
8
9 int list_node_process (void *p_arg, slist_node_t *p_node)
10 {
11 printf("%d ", ((slist_int_t *)p_node)->data);
12 return 0;
13 }
14
15 int main(void)
16 {
17 slist_head_t head; //
18 slist_int_t nodel, node2, node3;
19 slist_init(&head);
20
21 node1.data = 1;
22 slist_add_tail(&head, &(node1.node));
23 node2.data = 2;
24 slist_add_tail(&head, &(node2.node));
25 node3.data = 3;
26 slist_add_tail(&head, &(node3.node));
27 slist_foreach(&head, list_node_process, NULL); //
28 return 0;
29 }
6.双向链表
1.普通双向链表
2.环形链表
typedef struct _dlist_node{
struct _dlist_node *p_next;
struct _dlist_node *p_prev;
}dlist_node_t;
typedef dlist_node_t dlist_head_t;//头节点定义
头节点定义dlist_head_t head;
由于此时没有添加任何其他节点,仅仅存在一个头节点,所以头节点既是第一个节点,也是最后一个节点,按照循环链表的定义,尾节点的p_next指向头节点,头节点的p_prev指向尾节点,如图所示
显然,仅仅有头节点时候,p_next ,p_prev都指向本身
head.p_next = &head;
head.p_prev = &head;
如何获取尾节点?头节点的p_prev就是指向了尾节点
1.链表
静态链表
动态链表
建立动态链表指的是在程序执行过程中从无到有建立一个链表,也就是一个一个的开辟节点和输入节点数据,并建立连接关系
先建立一个空链表,之后在尾部插入
struct Student {
int num;//编号
float score;//成绩
struct Student* next;//存储下一个节点的地址
}a,b,c;
typedef struct Student ST;
void add(ST** phead, int inum, float iscore)//传入头节点的地址,插入数据
{
if (*phead == NULL)//判断链表是否为空
{
ST* newnode = malloc(sizeof(ST));
if (newnode == NULL)
{
printf("内存分配失败");
return;
}
newnode->num = inum; //节点初始化
newnode->score = iscore;
newnode->next = NULL;
*phead = newnode;//让头指针指向这个节点
}
else
{
ST* p = *phead;
while (p->next != NULL)//遍历到最后1个节点,尾部插入
{
p = p->next;
}
ST* newnode = malloc(sizeof(ST));
if (newnode == NULL)
{
printf("内存分配失败");
return;
}
newnode->num = inum; //节点初始化
newnode->score = iscore;
newnode->next = NULL;
p->next = newnode;//让头指针指向这个节点
}
}
void showall(ST* phead)//传入头节点,显示所有数据
{
ST* head = phead;
while (head != NULL)
{
printf("%d,%f\n", head->num, head->score);//访问数据
head = head->next;
}
}
int main()
{
struct Student* head = NULL;//头指针,指向节点,从而访问节点
add(&head, 1, 70);
add(&head, 2, 80);
add(&head, 3, 90);
add(&head, 4, 91);
add(&head, 5, 92);
//printf("%d,%f\n", head->num, head->score);//访问数据
//printf("%d,%f\n", head->next->num, head->next->score);
//printf("%d,%f\n", head->next->next->num, head->next->next->score);
showall(head);
system("pause");
return 0;
}