(三)链表分类

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_headNULL。则新增的节点就是头节点,修改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;
}

由于用户需要初始化headNULL,且遍历时需要操作每个节点的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;
}
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值