链表

本文深入探讨了链表的基本概念及其实现细节,包括链表的结构、动态内存分配、单链表的特点及其插入操作。并通过实例展示了如何优化链表插入函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

链表

       链表是一些包含独立数据结构(通常称为节点)的集合。链表中的每个节点通过指针连接在一起。

程序通过指针访问链表中的节点。

为什么要使用链表

       C语言中结构体可以包含多种不同数据类型。现实中的数据表示要求我们使用多种数据类型,因此

需要使用结构体。如影评,需要电影名(字符串)和评分(浮点数)。最直接的想法是申请一个结构体数组

来保存数据。但声明的数组大小太大,可能会浪费大量空间。如下面代码中显示,第一,大多数电影名

用不了40个字符,但有的电影名的确很长。第二,不同的人有不同的需求,指定数组大小为常量就不合

适了,100部电影对有的人仍然太小,但对其他人来说可能浪费大量的内存。

struct film{
    //电影名
    char title[40];
    //评分
    float rating; 
};
···
// 申请一个长度为 100 的 film 结构体数组
struct film movies[100];

       这里的根本问题是数据表示方法太不灵活。必须在编译时做出决定,而事实上在运行时做这些决定

会更好。这表明应该使用动态内存分配的数据表示。比如下列代码:

struct film{
    char title[40];
    int rating;
};
···
//n 代表 film 数组大小
int n;
//声明一个 film 型指针指向一个 film 数组
struct film * movies;
···
scanf("%d",&n);
movies = (struct film *)malloc(n * sizeof(stuct film));

       但这样仍有不足,尽管是在运行时输入,仍然限定了结构体数组的大小。你可能希望可以不确定地

添加数据,而不用事先指定你会输入多少项目,也不用让程序分配不必要的大块内存。而这一点可以通

过在输入每个项目之后调用 malloc()分配大小合适的空间以保存新的数据项来做到。如果输入300部

电影,就调用 malloc()函数300次。

       但这导致了新的问题。试比较调用 malloc()函数 1 次、请求保存300个 film 结构的空间,和调

用 malloc()函数300次、每次请求保存 1 个 film 结构的空间。第一种情况将分配一个连续的内存块,

用以跟踪这些内容的只是一个指向 struct film 的指针变量,它指向数组中的第一个结构体。使用简单的

数组符号允许这个指针访问块中每一个结构体。第二种方法的问题是不能保证连续的存储空间。因此,

需要存储300个指针,其中每个指针指向一个独立存储的结构体,而不是存储一个指向有300个结构的内

存块的指针。

       那么该如何管理这些指针呢?一种方法是声明一个大的指针数组,并在分配新的结构时逐个地对这

些指针赋值,但是无用指针占用的空间仍然会被浪费掉,并且仍有对结构数量的限制。

#define MAX 500
···
struct film * movies[MAX];
int i;
···
movies[i] = (struct film *)malloc(sizeof(struct film));

       另一种更好的方法,就是每次使用 malloc()函数为新结构分配空间时,也为新指针分配空间。对

新指针采取的分配空间的方式是重新定义结构。使得每个结构包含一个指向下一个结构的指针,这样在使

用 malloc()函数为新结构分配空间的同时,新的结构声明了一个指向同类型结构的指针,也就是为新

指针分配了空间。然后,每次创建新的结构时,就可以在前一个结构中存储它的地址。

struct film{
    char title[40];
    float rating;
    struct film * next;
};

       为了表示链表的结束,将最后一个结构的 next 成员指针设为 NULL,以说明这个结构后面没有别的

结构。同时,为跟踪第一个结构存储在哪里,可以将其地址赋给一个独立的称为头指针(或根指针)的

指针。头指针指向链表中的第一个结构。

启示

       寻找正确的数据表示方式常常不仅是选择一种数据类型。还必须考虑到哪些操作是必须的。也就是

说,必须确定如何存储数据,并且必须定义对数据类型来说哪些操作是有效的。简而言之,设计数据类

型包括确定如何存储数据以及设计一系列函数来管理数据。探究数据类型的过程,就是一个将算法(操

纵数据的方法)和数据表示方法相匹配的过程。有时,需要执行的动作影响到你对如何存储信息的决定。

因此在编写代码之前,需要做出许多设计上的决定。

单链表

       在单链表中,每个节点包含一个指向链表下一个节点的指针。链表最后一个节点的指针字段的值为

NULL,提示链表后面不再有其他节点。因此在找到链表第 1 个节点后,指针可以访问剩下的所有节点。

为了记住链表的起始位置,可以使用一个游离于结构外独立的根指针。根指针指向链表的第 1 个节点。注

意根指针只是一个指针,它不包含任何数据。

单链表特点

       单链表在逻辑上相连,而在物理上未必相邻。因为程序始终用指针从一个节点移动到另一个节点。

同时,单链表是单向的,单链表可以通过指针从开始位置遍历链表直到结束为止,但链表无法从相反的

方向进行遍历。也就是说,当程序到达链表最后一个节点时,如果想回到其他任何节点,只能从根指针

从头开始,当然你可以另外创建一个指针指向当前位置保存这个节点。

单链表插入函数

       以链表插入函数介绍使用链表的技巧,更多操作,如查找和删除等很容易类比实现。

功能

       根据输入的整数值,按从小到大顺序插入链表。

首先定义结点类型
typedef struct node{
    int value;
    node *link;
}Node;
函数调用
//root 是根指针
//new_value 是插入的整数
root = insert(root, new_value);
函数实现
Node * list_insert(Node *rootp, int new_value){
	Node * current; 
	Node * previous;
	Node * newnode;
	
	current = rootp;
	previous = NULL;
	
	while(current != NULL && new_value > current->value){
	    previous = current;
	    current = current->next;	
	}
	
	newnode = (Node *)malloc(sizeof(Node));
	if(newnode == NULL){
	    printf("Error"); 
	    return rootp;
	}
	newnode->value  = new_value;
	newnode->next = current;
	
	if(previous == NULL)
	    rootp = newnode;
	else
	    previous->next = newnode;
		
	return rootp;
}
函数分析

       操作链表的想法很简单。我们考虑谁指向节点,节点又指向谁。

       在两个节点中间插入新的节点,只需让前一个结点的成员指针从指向后一个节点转为指向新节点,新节点的成员指针指向后一个节点。

       插入在开始,只需要让根指针指向新节点,新节点成员指针指向后一个节点(也就是原来的第一个节点)。

       插入在最后,只需要让前一个结点(也就是原来的最后一个节点)的成员指针从 NULL 转为指向新节点,新节点的成员指针设为 NULL 表示链表结束。

 代码分析

       函数的形式参数创建两个变量:一个指向头节点的指针用于遍历链表,另一个是要插入到链表中的整数值。另外创建三个指针变量,current 、 previous 和 newnode。

       current 称为插入点后的节点。current 最开始被根指针赋值,开始遍历链表,判断是否插入,满足则停止,否则向后读。(1)在其值为 NULL 时有两种情况:①根指针为 NULL,赋给 current 同样为 NULL,代表链表为空。②最后一个节点的 next 成员指针为 NULL,表示后面没有其他节点,将其值赋给 current,代表链表结束。(2)不为 NULL 时 ,它指向插入点后的节点。因为 current 始终指向链表中未被访问过的最新节点,并判断当前节点是否满足插入条件,所以我把该指针变量记做 current。

       previous 称为插入点前的节点。(1)在其值为 NULL 时有两种情况:①链表为空。② current 指向第一个节点。这两种情况下都不存在前面的节点,所以 previous 值自然为 NULL。(2)当 previous 值不为 NULL 时,previous 指向插入点前的节点。

       newnode 指向新插入的节点。(1)如果 previous 和 current 节点存在(不为 NULL),就插入在它们中间,此时表示插入在有头有尾的链表中间。操作插入点前节点的 next 成员指针指向新节点,新节点的 next 成员指针指向插入点后节点。(2)如果 previous 节点不存在,current 节点存在,说明插入在链表最开始,不存在插入点前节点,成为新的头节点。操作根指针指向新节点,新节点的 next 成员指针指向插入点后节点。(3)如果 current 节点不存在,previous 节点存在,说明插入在链表最后,成为新的尾节点,插入点前结点 next 成员指针指向新节点,因为不再有后续节点,将新节点 next 成员指针设为 NULL,表示链表结束。(4)如果 previous 和 current 节点都不存在(均为 NULL),说明链表为空,插入新节点作为链表中的唯一节点,将根指针指向它,并将它的 next 成员指针设为 NULL。

       幸运的是,current 作为插入点后节点,时刻为新节点准备位置,如果新节点后还有节点,新节点的 next 成员指针尽管指向它,如果新节点后没有节点,current 为 NULL,新节点的 next 成员指针还是可以由 current 赋值。于是,我们只需要判断根指针是否有变化,如果插入在链表最开始,改变根指针指向新的头结点。否则只需要将插入点前结点的 next 成员指针指向新节点。

       最后,函数实参传递根指针,函数形参只是创建了一个与根指针有相同指向的指针变量,并不是根指针本身,所以我们返回函数中的形参根指针,赋给调用函数中的实参根指针,在头结点改变时保存链表的变化。

*优化插入函数

       首先,因为函数形参创建变量,接受实参赋值。因此,实参指针和形参指针相当于两个指针指向同一个节点,所以形参指针指向的改变不会影响到实参指针。有两种方法可以解决这个问题。第一种是函数返回形参指针赋值给实参指针;第二种是实参传递一个指向指针的指针,形参创建一个指向指针的指针并被实参赋值。它们都指向同一个指针,都可以修改这个被指向指针,这个被指向的指针则指向链表中的节点。

       其次,上面的函数中我们把一个节点插入到链表的起始位置当做一种特殊情况处理,因为此时需要修改根指针。对于其他任何节点,则是修改前一个节点的 next 成员指针。这两个看上去不同的操作实际上是一样的。

       消除特殊情况的关键在于:我们必须认识到,链表中的每个节点都有一个指向它的指针。对于第 1 个节点,这个指针是根指针;对于其他节点,这个指针是前一个节点的 next 成员指针。重点在于每个节点都有一个指针指向它,至于该指针是否位于一个节点内部则无关紧要。

       我们采用指向指针的指针来消除区别;这个被指向的指针是根指针,还是其他节点的 next 成员指针则无关紧要。利用这个指向指针的指针,我们既可以修改根指针或其他节点的 next 成员指针,也可以不断指向下一个节点的 next 成员指针遍历链表。我们已经拥有遍历链表的能力,为了方便操作,我们再创建一个指向当前节点的指针。这样我们拥有了一个指向当前节点的指针,和一个指向成员指针或根指针的指针(这个成员指针存在于上一个节点内部,指向当前节点)。

函数调用

       因为形参接受指向指针的指针。这个指针的值是它指向指针的地址,所以实参调用如下,其中 root 是根指针。

Node * root;
···
list_insert(&root, new_value);
函数实现
void list_insert(Node **linkp, int new_value){
	Node * current;
	Node * newnode;
	
	while((current = *linkp) != NULL && new_value > current->value)
		linkp = &current->next;
	
	newnode = (Node *)malloc(sizeof(Node));
	if(newnode == NULL)
		printf("Error"); 
	else{
		newnode->value = new_value;
		newnode->next = current;
		*linkp = newnode;
	}
}
启示

       消除特殊情况使这个函数更为简单。这个改进之所以可行是由于两方面的因素。第 1 个因素是我们正确解释问题的能力。在看上去不同的操作中总结出共性,否则你只能编写额外的代码来处理特殊情况。通常,这种知识在学习一阵数据结构并对其有进一步的理解之后才能获得。第 2 个因素是 C 语言提供了正确的工具帮助你归纳问题的共性,在代码编写中,我们要结合不同语言、编译器的特性因地制宜。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值