C语言自定义数据类型(四)用指针处理链表

C语言自定义数据类型系列文章:

C语言自定义数据类型(一)定义和使用结构体变量

C语言自定义数据类型(二)使用结构体数组

C语言自定义数据类型(三)结构体指针

C语言自定义数据类型(四)用指针处理链表

C语言自定义数据类型(五)共用体类型

C语言自定义数据类型(六)使用枚举类型

目录

一、什么是链表

1.1引入

1.2定义

二、建立简单的静态链表

2.1举例说明

三、建立动态链表

3.1举例说明

四、输出链表

4.1举例说明


一、什么是链表

1.1引入

链表是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。由前面的介绍中已知:用数组存放数据时,必须事先定义固定的数组长度(即元素个数)。如果有的班级有 100 人,而有的班级只有 30 人,若用同一个数组先后存放不同班级的学生数据,则必须定义长度为 100 的数组。如果事先难以确定一个班的最多人数,则必须把数组定得足够大,以便能存放任何班级的学生数据,显然这将会浪费内存。链表则没有这种缺点,它根据需要开辟内存单元。

链表有一个 “ 头指针 ” 变量,图中以 head 表示,它存放一个地址,该地址指向一个元素。链表中每一个元素称为 “ 结点 ”,每个结点都应包括两个部分:(1)用户需要用的实际数据;(2)下一个结点的地址。可以看出, head 指向第 1 个元素,第 1 个元素又指向第 2 个元素……直到最后一个元素,该元素不再指向其他元素,它称为 “ 表尾 ”,它的地址部分放一个 “ NULL ” (表示 “ 空地址 ”),链表到此结束。

可以看到链表中各元素在内存中的地址可以是不连续的。要找某一元素,必须先找到上一个元素,根据它提供的下一元素地址才能找到下一个元素。如果不提供 “ 头指针 ”(head),则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。

1.2定义

前面介绍了结构体变量,用它去建立链表是最合适的。一个结构体变量包含若干成员,这些成员可以是数值类型、字符类型、数组类型,也可以是指针类型。用指针类型成员来存放下一个结点的地址。

例如,可以设计这样一个结构体类型:

struct Student
{
	int num;
	float score;
	struct Student* next;		//next是指针变量,指向结构体变量
};

其中,成员 num 和 score 用来存放结点中的有用数据(用户需要用到的数据)。相当于上图结点中的A,B,C,D。next 是指针类型的成员,它指向 struct Student 类型数据(就是 next 所在的结构体类型)。一个指针类型的成员既可以指向其他类型的结构体数据,也可以指向自己所在的结构体类型的数据。现在,next 是 struct Student 类型中的一个成员,它又指向 struct Student 类型的数据。用这种方法就可以建立链表。

图中每一个结点都属于struct Student 类型,它的成员 next 用来存放下一结点的地址,程序设计人员可以不必知道各结点的具体地址,只要保证将下一个结点的地址放到前一结点的成员 next 中即可。

注意:上面只是定义了一个 struct Student 类型,并未实际分配存储空间,只有定义了变量才分配存储单元。

二、建立简单的静态链表

2.1举例说明

下面通过一个例子来说明怎样建立和输出一个简单链表。

举例:建立一个如 1.2 所示的简单链表,它由 3 个学生数据的结点组成,要求输出各结点中的数据。

解题思路:声明一个结构体类型,其成员包括num(学号),score(成绩)和next(指针变量)。将第 1 个结点的起始地址赋给头指针 head,将第 2 个结点的起始地址赋给第 1 个结点的 next 成员,将第 3 个结点的起始地址赋给第 ⒉个结点的 next 成员。第 3 个结点的 next 成员赋予 NULL。这就形成了链表。

#include<stdio.h>
struct Student					//声明结构体类型
{
	int num;
	float score;
	struct Student* next;		//next是指针变量,指向结构体变量
};

int main()
{
	struct Student a, b, c;				//定义结构体变量a,b,c作为链表的节点
	struct Student* head, * p;			//定义头指针和作为移动用的指针
	a.num = 1001; a.score = 90;			//对节点a的num和score成员赋值
	b.num = 1002; b.score = 92;			//对节点b的num和score成员赋值
	c.num = 1003; c.score = 94;			//对节点c的num和score成员赋值
	head = &a;							//将节点a的起始地址赋给头指针
	a.next = &b;						//将节点b的起始地址赋给a节点的next成员
	b.next = &c;						//将节点c的起始地址赋给b节点的next成员
	c.next = NULL;						//c节点的next成员不存放其他节点的地址
	p = head;							//使p指向a节点
	while (p != NULL)					//当p的值为NULL时,循环终止
	{
		printf("%ld %5.1f\n", p->num, p->score);	//输出p节点指向节点的数据
		p = p->next;								//使p指向下一节点
	}
	return 0;
}

运行结果:

程序分析:

为了建立链表,使 head 指向  a结点,a.next 指向 b 结点,b.next 指向 c 结点,这就构成链表关系。“ c.next = NULL ” 的作用是使 c.next 不指向任何有用的存储单元。

在输出链表时要借助 p,先使 p 指向 a 结点,然后输出 a 结点中的数据,“ p = p->next ” 是为输出下一个结点作准备。p->next 的值是 b 结点的地址,因此执行 “p = p>next” 后 p 就指向 b 结点,所以在下一次循环时输出的是 b 结点中的数据。

本例是比较简单的,所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为 “ 静态链表 ”。

三、建立动态链表

所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。

3.1举例说明

写一函数建立一个有 3 名学生数据的单向动态链表。

解题思路:先考虑实现此要求的算法。在用程序处理时要用到动态内存分配的知识和有关函数(malloc,calloc,realloc 和 free 函数)。

定义 3 个指针变量:head,p1 和 p2,它们都是用来指向 struct Student 类型数据的。先用 malloc 函数开辟第 1 个结点,并使 p1 和 p2 指向它。然后从键盘读入一个学生的数据给 p1 所指的第 1 个结点。在此约定学号不会为零,如果输入的学号为0,则表示建立链表的过程完成,该结点不应连接到链表中。先使 head 的值为 NULL (即等于0),这是链表为 “空” 时的情况(即 head 不指向任何结点,即链表中无结点),当建立第 1 个结点就使 head 指向该结点。

如果输入的 p1->num 不等于0,则输入的是第 1 个结点数据(n=1),令 head = p1,即把 p1 的值赋给 head,也就是使 head 也指向新开辟的结点。p1 所指向的新开辟的结点就成为链表中第 1 个结点。然后再开辟另一个结点并使 p1 指向它,接着输人该结点的数据。

如果输入的 p1->num ≠ 0,则应链入第 2 个结点(n=2),由于 n ≠ 1,则将 p1 的值赋给 p2->next,此时 p2 指向第1个结点,因此执行“ p2->next = p1” 就将新结点的地址赋给第 1 个结点的 next 成员,使第 1 个结点的 next 成员指向第 2 个结点。接着使 p2 = p1,也就是使 p2 指向刚才建立的结点。

接着再开辟 1 个结点并使 p1 指向它,并输入该结点的数据。在第 3 次循环中,由于 n = 3(n≠1),又将 p1 的值赋给 p2->next,也就是将第 3 个结点连接到第 2 个结点之后,并使 p2 = p1,使 p2 指向最后一个结点。

再开辟一个新结点,并使 p1 指向它,输入该结点的数据。由于p1->num 的值为0,不再执行循环,此新结点不应被连接到链表中。此时将 NULL 赋给 p2->next。建立链表过程至此结束,p1 最后所指的结点未链入链表中,第 3 个结点的 next 成员的值为 NULL,它不指向任何结点。虽然 p1 指向新开辟的结点,但从链表中无法找到该结点。

#include<stdio.h>
#include<stdlib.h>
#define LEN sizeof(struct Student)
struct Student
{
	long num;
	float score;
	struct Student* next;
};
int n;								//n为全局变量
struct Student* create()			//定义函数,此函数返回一个指向链表头的指针
{
	struct Student* head, * p1, * p2;
	n = 0;
	p1 = p2 = (struct Student*)malloc(LEN);		//开辟一个新单元
	scanf_s("%ld %f", &p1->num, &p1->score);	//输入第一个新单元
	head = NULL;
	while (p1->num != 0)
	{
		n = n + 1;
		if (n == 1) head = p1;
		else
		{
			p2->next = p1;
			p2 = p1;
		}
		p1 = (struct Student*)malloc(LEN);		//开辟动态存储区,把起始地址赋给p1
		scanf_s("%ld %f", &p1->num, &p1->score);//输入其他学生的学号和成绩
	}
	p2->next = NULL;
	return(head);
}

int main()
{
	struct Student* head;
	head = create();			//函数返回链表的第一个结点的地址
	printf("\nnum:%d\nscore:%5.1f\n", head->num, head->score);	//输出第一个节点的成员值
	return 0;
}

运行结果:

程序分析:

(1)调用 create 函数后,先后输入所有学生的数据,若输入“0 0”,表示结束。函数的返回值是所建立的链表的第 1 个结点的地址(请查看 return 语句),在主函数中把它赋给指针变量 pt。为了验证各结点中的数据,在 main 函数中输出了第 1 个结点中的信息。

(2)第 3 行令 LEN 代表 struct Student 类型数据的长度,sizeof 是 “ 求字节数运算符 ”。

(3)第 10 行定义一个 create 函数,它是指针类型,即此函数带回一个指针值,它指向一个 struct Student 类型数据。实际上此creat函数带回一个链表起始地址。

(4)第 15 行 malloc(LEN) 的作用是开辟一个长度为 LEN 的内存区,LEN 已定义为 sizeof( struct Student ),即结构体 struct Student 的长度。malloc 带回的是不指向任何类型数据的指针(void *类型)。而 p1,p2是指向 struct Student 类型数据的指针变量,可以用强制类型转换的方法使指针的基类型改变为 struct Student 类型,在 malloc(LEN) 之前加了 “( struct Student* )”,它的作用是使 malloc 返回的指针转换为 struct Student 类型数据的指针。注意括号中的 “ * ” 号不可省略,否则变成转换成 struct Student 类型了,而不是指针类型了。

(5)create 函数最后一行 return 后面的参数是 head( head 已定义为指针变量,指向  struct Student 类型数据)。因此函数返回的是 head 的值,也就是链表中第 1 个结点的起始地址。

(6)n 是结点个数。

(7)这个算法的思路是让 p1 指向新开辟的结点,p2 指向链表中最后一个结点,把 p1 所指的结点连接在 p2 所指的结点后面,用 “ p2->next = p1”来实现。

四、输出链表

将链表中各结点的数据依次输出。这个问题比较容易处理。

4.1举例说明

举例:编写一个输出链表的函数 print。

解题思路:从例 3.1 已经初步了解输出链表的方法。首先要知道链表第 1 个结点的地址,也就是要知道 head 的值。然后设一个指针变量 p,先指向第 1 个结点,输出 p 所指的结点,然后使 p 后移一个结点,再输出,直到链表的尾结点。

#include<stdio.h>
#include<stdlib.h>
#define LEN sizeof(struct Student)
struct Student
{
	long num;
	float score;
	struct Student* next;
};
int n;								//n为全局变量
struct Student* create()			//定义函数,此函数返回一个指向链表头的指针
{
	struct Student* head, * p1, * p2;
	n = 0;
	p1 = p2 = (struct Student*)malloc(LEN);		//开辟一个新单元
	scanf_s("%ld %f", &p1->num, &p1->score);	//输入第一个新单元
	head = NULL;
	while (p1->num != 0)
	{
		n = n + 1;
		if (n == 1) head = p1;
		else
		{
			p2->next = p1;
			p2 = p1;
		}
		p1 = (struct Student*)malloc(LEN);		//开辟动态存储区,把起始地址赋给p1
		scanf_s("%ld %f", &p1->num, &p1->score);//输入其他学生的学号和成绩
	}
	p2->next = NULL;
	return(head);
}

void print(struct Student* head)
{
	struct Student* p;
	printf("\n所有的记录为:\n");
	p = head;
	if (head != NULL)
	{
		while (p != NULL)
		{
			printf("%ld %5.1f\n", p->num, p->score);
			p = p->next;
		}
	}
}

int main()
{
	struct Student* head;
	head = create();			//函数返回链表的第一个结点的地址
	print(head);				//输出第一个节点的成员值
	return 0;
}

运行结果:

说明:

链表是一个比较深入的内容,初学者有一定难度,计算机专业人员是应该掌握的,非专业的初学者对此有一定了解即可,在以后需要用到时再进一步学习。

对链表中结点的删除和结点的插入等操作,在此不作详细介绍,如读者有需要或感兴趣,可以自己完成。如果想详细了解,可参考严蔚敏《数据结构》一书。

结构体和指针的应用领域很宽广,除了单向链表之外,还有环形链表和双向链表。此外还有队列、树、栈、图等数据结构。有关这些问题的算法可以学习 “ 数据结构 ” 课程。

  • 2
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

juechen333

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值