【了解链表的适用场景;掌握单向链表、双向链表的使用】(学习笔记18--链表)


在这里插入图片描述

链表中的节点和火车的车厢一样,是一个连着一个的,通常将当前节点的前一个或上一个节点称为它的前驱节点,将后一个或者下一个节点称之为它的后继节点。链表中的第一个节点被称为头节点,最后一个节点被称为尾节点。在链表中,头节点是没有前驱的节点,尾节点是没有后继的节点,其它的所有节点都称为中间节点,中间节点都会拥有一个前驱节点和一个后继节点。

单向链表

所谓单向链表,就是每个节点的指针域中只含有一个指向其后续节点指针的链表
在这里插入图片描述
在单向链表中,只能从头节点开始,不断地通过指向后续节点的指针,来逐个地向后访问遍历其它链表节点,最后一个节点的指针域为一个空指针,表示其没有后继节点了

为了使用单向链表必须定义相应的节点类型,由于节点具有数据域和指针域两个部分,因此,应该选用结构体来进行定义

struct node
{
	int data;
	struct node *next;
}

定义了结构体node,它有两个成员:int类型的成员data和struct node*类型的成员next
在struct node和next之间有个星号,表示所定义出的成员next是struct node类型的指针。这是允许的
因为指针的大小是固定的,并不会随着指针的类型不同而改变。反之,如果在struct node和next之间没有这个星号就是错误的,C语言中,不允许在结构体的定义中,出现和结构体相同类型的成员,因为要想得知结构体的大小,就得知道其所有成员的大小,而成员的类型又和结构体类型相同,那么若想知道成员的大小,就必须得先知道结构体的大小。这就出现了死锁现象,变成了无限的递归定义,就好像遇见了世界上是先有鸡还是先有蛋的亘古难题

可以将结构体node看成是一个链表的节点类型,数据域部分是成员data,而指针域部分是成员next。为了方便,定义一个创建节点的函数createNode

/*
作者:是北豼不太皮吖
时间:2022年3月22日10:54:45
函数功能:创建节点
参数:int val
返回值:struct node*
		成功:pnode
		失败:NULL
	说明:
	createNode函数只有一个int类型的参数val,用于表示新创建节点数据域
	部分的值,函数返回值为指向新创建节点的指针。在函数体中,首先通过malloc
	函数在堆中申请分配一块节点大小的内存空间,并让指针pnode指向这块内存。
	然后通过if语句判断申请分配内存是否成功,若成功则将这块内存作为新创建节
	点的存储区域,并通过指针pnode来给节点的数据域和指针域部分赋值,即将参
	数val赋值给成员data,由于此时所创建的是一个单独的节点,并不知道是否会
	有后续节点,因此,将成员next的值赋为NULL。最后,通过return语句返回指向
	堆中所创建的节点的指针
*/
struct node *createNode(int val)
{
	struct node *pnode = (struct node*)mslloc(sizeof(struct node));	
	if(pnode != NULL)		//判断内存分配是否成功
	{
		pnode -> data = val;
		pnode -> next = NULL;
	}
	return pnode;
}

下面定义将节点加到链表的函数addNode

/*
作者:是北豼不太皮吖
时间:2022年3月22日10:55:09
函数功能:将节点加到链表
参数:struct node **pheadptr,int val
返回值:int
	成功:1
	失败:0
	说明:
	addNode函数有两个参数,其中参数pheadptr是一个二级指针,表示指向链表头
	节点指针的指针,使用二级指针的目的是为了能在函数中修改实参,而这个实参
	应为指向链表头节点的指针。另一个参数val是所添加节点的值,即节点的数据
	域部分。函数的返回值为int类型,值为1表示添加节点成功,值为0表示添加节
	点失败
*/
int addNode(struct node **pheadptr,int val)
{
	struct node *p = createNode(val);
	if(p == NULL)		//如果创建节点失败
		return 0;
	if(*pheadptr == NULL)
		*pheadptr = p;
	else
	{
		struct node *ptmp  = *pheadptr;
		while(ptmp->next)
			ptmp = ptmp->next;
		ptmp->next = p;	
	}
	return 1;
}

在这里插入图片描述

在函数体中,首先以val作为参数,调用createNode函数在堆中创建新节点,并由指针p指向该节点,然后,通过if语句检查新节点是否创建成功,若指针p为空指针,则表示节点创建失败,通过return语句返回0,表示向链表添加节点失败,接着,再通过if…else语句判断指向头节点的指针是否为空指针,由于pheadptr是一个二级指针,因此,对它进行解引用即可访问到实参指针(即指向链表头节点的指针,简称头指针)。若实参指针是空指针,则表示此时的链表是一个空链表,就会将指针p赋值给实参指针,即将新节点作为链表的头节点,并让实参指针指针指向这个新节点,若实参指针不是空指针,即将新节点作为链表并非空链表,就会执行else部分,定义一个临时指针ptmp,并让它指向链表头节点,然后通过while循环语句找到链表的尾节点(即成员next的值为NULL的节点),再将尾节点的成员next赋值为指针p,即表示将原来尾节点作为新创建节点的前驱节点,而新创建节点则成为链表的尾节点了。需要注意的是,在while循环的条件判断处,是通过检查指针ptmp所指向节点的成员next的值是否为空来决定是否执行循环体的,如果指针ptmp所指向节点的成员next的值为空,则表示指针ptmp已经指向了链表的尾节点,此时会终止while循环。如果指针ptmp所指向节点的成员next的值不为空,则会将其成员next的值重新赋值给指针ptmp,即让指针ptmp指向原先节点的后续节点(即下一个节点),并再次执行下轮循环,直到指针ptmp所指向节点的成员next的值为空时止。由于指针ptmp是一个临时指针,因此,在while循环中,对指针ptmp的修改是不会影响到实参指针的,所以要注意它和if部分的指针使用方式的区别;最后,函数通过return语句返回1,表示向链表添加节点成功。由此可见,每次调用addNode函数,都是将新创建的节点添加到链表的尾部

下面定义一个统计链表节点数量的countOfNodes函数

/*
作者:是北豼不太皮吖
时间:2022年3月22日14:39:04
函数功能:统计链表节点数量
参数:struct node *headptr
返回值:unsigned
	说明:
	countOfNodes函数中的形式参数heaptr只是一个普通指针,因此,在while循环
	体中,对指针headptr的修改不会影响到实参指针
*/
unsigned countOfNodes(struct node *headptr)
{
	unsigned c = 0;
	while(headptr)
	{
		++c;
		headptr = headptr->next;
	}
	return c;
}

在这里插入图片描述

知道了如何统计节点数量,那么想要遍历并打印链表节点就很容易了,可以定义专门用于遍历打印链表节点的函数printAllNodes

/*
作者:是北豼不太皮吖
时间:2022年3月22日15:32:46
函数功能:遍历并打印链表节点
参数:struct node *headptr
返回值:void
	说明:
	和countOfNodes函数的不同之处在于while循环中将变量自增换成了printf函数
	调用,将指针headptr所指向节点的数据域部分打印输出,并在所有节点打印完
	毕之后,进行一个执行的操作
*/
void printAllNodes(struct node *headptr)
{
	while(headptr)
	{
		printf("%d",headptr->data);
		headptr = headptr->next;
	}
	printf("\n");
}

在这里插入图片描述

对于节点的删除,算是链表中最为复杂的操作之一了。因为链表的节点可能是分散存储于内存的不同地方,靠着节点的指针域来进行节点间的联系,就可能会使链表的整体或部分断裂,造成数据的丢失和内存的泄露。因此,在删除链表节点的同时,要精心维护好节点间的关系

(1)头节点的删除

由于头指针是指向链表头节点的,因此,要想删除头节点,应该先用一个临时指针指向头节点,然后将头指针修改为指向链表第二个节点,最后再通过临时指针将原头节点所占用的内存空间释放回收即可
在这里插入图片描述
删除1号节点步骤:
1.用临时指针指向1号节点
2.使头指针指向2号节点
3.通过临时指针释放回收1号节点的内存空间

(2)非头节点的删除

对于非头节点的删除,首先要找到欲删除节点的前驱节点,将其成员next的指针指向欲删除节点的后续节点,然后再将节点删除并释放回收内存空间
在这里插入图片描述
删除2号节点步骤:
1.用临时指针指向2号节点
2.使1号节点的next指针指向3号节点
3.通过临时指针释放回收2号节点内存空间

删除链表节点的函数deleteNode

/*
作者:是北豼不太皮吖
时间:2022年3月22日15:50:52
函数功能:删除链表节点(非头节点)
参数:struct node **pheadptr,unsigned loc
返回值:int
	成功:1
	失败:0
	说明:
	deleteNode函数有两个参数,由于在头节点的删除中需要修改头指针,因此,参
	数pheadptr被定义为二级指针,即一个指向头指针的指针,参数loc是unsigned
	类型的变量,它用来表示欲删除节点的位置,即删除链表中的哪一个节点。函数
	返回值为int类型,如果节点删除成功返回1,节点删除失败返回0
*/
int deleteNode(struct node **pheadptr,unsigned loc)
{
	unsigned c = countOfNodes(*pheadptr);
	if(c < loc)
		return 0;
	struct node *p = *pheadptr;
	if(loc == 1)
	{
		//头节点的删除
		*pheadptr = (*pheadptr)->next;
		free(p);
	}
	else
	{
		//非头节点的删除
		for(int i = 2;i < loc;++i)
			p = p->next;
		struct node *pdel = p->next;
		p->next  = pdel->next;
		free(pdel);
	}
	return 1;
}

在函数体中,首先调用countOfNode函数获取当前链表中的节点数量,并将节点数量保存至变量c中,然后通过if语句检查参数loc的值是否合法,如果loc的值大于链表节点数量,则直接通过return语句返回0,表示删除节点失败;接着定义一个指针p并让其指向链表头节点;再通过if语句检查要删除的节点是否是头节点,若loc的值等于1,则表示对链表头节点的删除,执行if部分,即让头指针指向头节点的后续节点,并调用free函数对原头节点的内存进行回收。若loc的值不等于1,则表示对链表非头节点的删除,会执行else部分,即通过for循环让指针p指向欲删除节点的前驱节点,其中循环变量i是从2开始循环的,例如想删除链表中第2个节点,则该循环的循环体不会被执行,指针p还是指向链表头节点的,而第2个节点的前驱节点就是头节点。接下来再定义一个指针的后续节点,即将欲删除的节点从链表中脱离,并让其前驱节点和后续节点建立联系;最后再调用free函数,释放、回收被删除节点的空间;函数体的最后,通过return语句返回1,表示删除节点成功

有了删除某一位置节点的deleteNode函数后,就可以通过它来实现链表所有节点的删除

/*
作者:是北豼不太皮吖
时间:2022年3月22日15:53:39
函数功能:删除链表所有节点
参数:struct node **pheadptr
返回值:void
	说明:
	deleteAllNode函数没有返回值,只有一个参数pheadptr,由于需要修改头指
	针,因此,也被定义为一个二级指针。在函数体中只有一个while循环,并将调
	用countOfNode函数的结果作为循环的条件,即链表中的头节点数量不为0时执行
	循环体,为0时终止循环。在循环体中通过调用deleteNode函数,并将整数1作为
	其第2个参数,这表示将链表中的头节点删除。该函数会依次将链表中的头节点
	删除,直至成为空链表
*/
void deleteAllNode(struct node **pheadptr)
{
	while(countOfNodes(*pheadptr))
		deleteNode(pheadptr,1);
}

最后在主函数中测试这些函数,检查链表节点的创建、添加、统计和删除是否正确

int main()
{
	//定义链表头指针,并将其初始化为空指针
	struct node *headPtr = NULL;
	//向链表添加5个节点
	for(int i = 1;i <= 5;++i)
		addNode(&headPtr,i * 10);
	//打印链表节点数量和各节点的值
	printf("The number of linked list nodes is :%u\n",countOfNodes(headPtr));
	printAllNode(headPtr);
	//删除链表中第2个节点
	printf("Delete the node at location 2.\n");
	deleteNode(&headPtr,2);
	//再次打印链表节点数量和各节点的值
	printf("The number of linked list nodes is:%u\n",countOfNodes(headPtr));
	printAllNodes(&headPtr);
	//删除链表所有节点
	deleteAllNode(&headPtr)
	return 0;
}

结果如下

The number of linked list nodes is :5
10 20 30 40 50
Delete the node at location 2.
The number of linked list nodes is:4
10 30 40 50

双向链表

链表节点的指针域中既含有指向前驱节点的指针,也含有指向后继节点的指针
在这里插入图片描述
在双向链表中,可以非常方便地通过一个节点来访问它的前驱节点与后继节点,其中头节点指向前驱节点的指针为空,尾节点指向后继节点的指针为空。我们既可以从头节点开始,一顺序方式遍历链表所有节点,也可以从尾节点开始,以逆序方式遍历链表所有节点

下面用一个程序案例来展示双向链表的使用

#include <stdio.h>
#include <stdlib.h>

/*定义双向链表的节点类型*/
typedef struct node
{
	//数据域
	int data;	
	//指针域			
	struct node *prev;			//指向前驱节点的指针			
	struct node *next;			//指向后续节点的指针
}NODE,*PNODE;

/*定义双向链表类型*/
typedef struct dblLinklist
{
	PNODE head;				//链表的头指针
	PNODE tail;				//链表的尾指针
	unsigned count;			//链表节点数量
}DLinkList,*PDLinkList;

/*初始化双向链表*/
void InitDLinkList(PDLinkList pdll)
{
	pdll->head = pdlll->tail = NULL;		//将头指针与尾指针设置为空指针
	pdll->count = 0;						//链表节点数量为0
}

/*在堆中创建双向链表节点,并返回该节点的指针*/
PNODE createDLLNode(int val)
{
	//在堆中为新节点申请分配内存空间
	PNODE p = (PNODE)malloc(sizeof(NODE));
	if(p)
	{
		//内存分配堆内存成功,对节点各成员赋值
		p->data = val;
		p->prev = p->next = NULL;			//将指向前驱与后续的指针设置为空指针
	}
	return p;
}

/*将节点添加到双向链表的首端,成功返回1,失败返回0*/
int addNodeToHead(PDLinkList pdll,PNODE pnode)
{
	if(pnode == NULL)			//若新节点为空,则添加节点失败,返回0
		return 0;
	if(pdll->head)				//若之前有头节点,则让其成为新节点的后续节点,新节点变为链表头节点
	{
		pdll->head->prev = pnode;		//让原头节点的前驱为新节点
		pnode->next = pdll->head;		//让新节点的后续为原头节点
		pdll->head = pnode;				//让新节点成为链表的头节点
	}
	else			//若之前没有头节点,则链表为空链表,让新节点成为链表的头节点和尾节点
		pdll->head = pdll->tail = pnode;
	++pdll->count;		//自增链表节点数量
	return 1;			//添加节点成功,返回1
}

/*将节点添加到双向链表的尾端,成功返回1,失败返回0*/
int addNodeToTail(PDLinkList pdll,PNODE pnode)
{
	if(pnode == NULL)
		return 0;
	if(pdll->tail)		//若之前有尾节点,则让其成为新节点的前驱节点,新节点变为链表尾节点
	{
		pdll->tail->next = pnode;			//让原尾节点的后续为新节点
		pnode->prev = pdll->tail;			//让新节点的前驱为原尾节点
		pdll->tail = pnode;					//让新节点成为链表尾节点
	}
	else
		pdll->head = pdll->tail = pnode;		//若之前没有尾节点,则链表为空链表,让新节点成为链表的头节点和尾节点
	++pdll->count;		//自增链表节点数量
	return 1;
}

/*获取双向链表节点数量*/
unsigned countOfDLinkList(PDLinkList pdll)
{
	return pdll->count;
}

/*顺序打印输出链表所有节点*/
void printDLinkList(PDLinkList pdll)
{
	PNODE p = pdll->head;		//指针p指向头节点
	while(p)					//指针p不为空,则执行循环体
	{
		printf("%d ",p->data);		//打印节点数据
		p = p->next;				//修改指针p,让其指向后继节点
	}
	printf("\n");
}

/*逆序打印输出链表所有节点*/
void printDLinkListByReverse(PDLinkList pdll)
{
	PNODE p = pdll->head;		//指针p指向尾指针
	while(p)					//指针p不为空,则执行循环体
	{
		printf("%d ",p->data);		//打印节点数据
		p = p->next;				//修改指针p,让其指向前驱节点
	}
	printf("\n");
}

/*删除指定位置处的节点,成功返回1,失败返回0*/
int deldteNodeByPosition(PDLinkList pdll,unsigned loc)
{
	if(pdll->count < loc)
		return 0;
	PNODE p;
	if(loc == 1)
	{
		//删除头节点
		p = pdll->head;
		pdll->head = pdll->head->next;
		if(pdll->head)
			pdll->head->prev = NULL;
	}
	else if(loc == pdll->count)
	{
		//删除尾节点
		p = pdll->tail;
		pdll->tail = pall->tail->prev;
		if(pdll->tail)
			pdll->tail->next = NULL;
	}
	else
	{
		//删除中间节点
		p = pdll->head;
		for(int i = 1;i < loc;++i)
			p = p->next;
		p->prev->next = p->next;
		p->next->prev = p->prev;
	}
	free(p);
	--pdll->count;
	return 1;
}

/*清空链表,即删除双向链表所有节点*/
void EmptyDLinkList(PDLinkList pdll)
{
	while(pdll->count)
		deleteNodeByPosition(pdll,1);
	InitDLinkList(pdll);
}

int main()
{
	//定义双向链表
	DLinkList list;
	//初始化双向链表
	InotDLinkList(&list);
	int i;
	//以首行添加方式,向双向链表添加3个节点
	for(i = 1;i <= 3;++i)
		addNodeToHead(&list,createDLLNode(i));
	//以尾端添加方式,向双向链表添加3个节点
	for(i = 4;i <= 6;++i)
		addNodeToHead(&list,createDLLNode(i*10));
	//以顺序方式打印链表
	printf("Print all nodes of the linked list sequentially:\n");
	printf(&list);
	//以逆序方式打印链表
	printf("Print all nodes in the linked list in reverse order:\n");
	printf(&list);
	//删除链表第3个节点
	printf("Delete the node at position 3:\n");
	deleteNodeByPosition(&list,3);
	//以顺序方式打印链表
	printf("Print all nodes of the linked list sequentially:\n");
	printf(&list);
	//清空链表
	EmptyDLinkList(&list);
	return 0;
}

大家可以比较一下双向链表与单链表在使用上的细微差别,尤其在删除链表节点时,要充分考虑到除了释放节点本身,还要喔维护好它的前驱节点与后继节点之间的关系,以及在建立节点关系时,对各指针进行修改的先后顺序

运行结果

Print all nodes of the linked list sequentially:
3 2 1 40 50 60
Print all nodes in the linked list in reverse order:
60 50 40 1 2 3
Delete the node at position 3:
Print all nodes of the linked list sequentially:
3 2 40 50 60

在这里插入图片描述

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是北豼不太皮吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值