链表(C语言)

目录

初步了解链表

链表的基本构成

单链表

单链表的创建

 链表的初始化

单链表的打印

1.无哨兵位的打印

2.有哨兵位的打印

第一种打印

 第二种方式打印

 单链表的增、删、查、改

 1.单链表的头插

2.单链表的尾插

 3.单链表删头

4.单链表尾删

 5.单链表查找

6.单链表定向插入

7.单链表定点删除

8.销毁链表

双向循环链表

双向循环链表的创建

双向循环链表的初始化

建立一个节点

双向循环链表的尾插

 双向循环链表的打印

 双向循环链表的头插


初步了解链表

        在介绍链表之前,让我们来回顾下数组。

        我们都知道数组是通过下标来访问数组内容的,下面是一维数组在内存中的分布。

         但是实际上,数组下标的访问是指针的后移,因为arr[ i ]<==>* (arr+i),即通过首元素的地址+i向后访问后面的内存

        这种访问必须满足数据是连续存储的。而今天我们要介绍的链表,则是逐个向后访问非连续的内存的一种结构。

链表的基本构成

        为了能够将分散的内存联系在一起,必不可少的,我们需要像“绳子”一样的东西将其一个个连接起来,而这个“绳子”就是指针。

        因此,链表的一个节点由两部分构成,数据域和指针域

         其中数据域用来存储数据,例如 int a=10;等,而指针域则是用来存储指针,该指针指向下一块需要访问的内存。

        真实结构

1.

        (此时创建的就是头指针+节点+表尾的单链表)

2.

        (此时创建的是头指针+哨兵位+节点+表尾的单链表)

         (哨兵位是指头指针指向一个节点,此节点数据域不存放任何东西)

        因此,各个不相连的内存就通过指针联系在一起,构成链表。

        链表的组成部分有,头指针,(哨兵位)节点和表尾。这样由前到后组成的结构为单链表。其中头指针就是指向这个链表的指针,用于找到并访问该链表,节点是构成链表的基本单位,表尾的指针域指向空,作为链表的结束。(注:链表有单、双向,带哨兵位和不带哨兵位,循环和非循环的分类,本篇仅介绍‘单链表非循环不带哨兵位’和‘双向链表循环带哨兵位’两种类型)

单链表

单链表的创建

以下为创建的函数的功能

SLTnode* BuylistNode(int x)            //创建一个新节点

SLTnde* Creat(int n)                       //初始化链表

void SlistPrint(SLTnode* Phead)     //打印链表

void SlistPushFront(SLTnode** pphead, int x)        //头插

void SlistPushBack(SLTnode*Phead, int x)        //尾插

void SlistPopfront(SLTnode** pphead)        //头删

void SlistPopBack(SLTnode*Phead)        //尾删

SLTnode* SlistFind(SLTnode* Phead,int x)        //查找

void SlistInsertFront(SLTnode** pphead, SLTnode* pos, int x)        //定点前插

void SlistInsertAfter(SLTnode* pos, int x)        //定点后插

void SlistErase(SLTnode** pphead,SLTnode* pos)        //定点删除

void SlistDestroy(SLTnode** pphead)        //销毁

        单链表可用自义定结构体创建。

struct slistnode
{
	//数据域
	int data;

	//指针域
	struct slistnode* next;
};

         为了方便写代码,一般将该结构体重命名,用起来更为方便。

typedef struct slistnode
{
    //数据域
    int data;

    //指针域
    struct slistnode* next;
}SLTnode;

 链表的初始化

        本质上就是创造链表的长度并赋给数据域内容。

SLTnode* BuylistNode(int x)
{
	SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
	//在堆区开辟一个节点的空间
	newnode->data = x;
	//为节点数据域赋值
	newnode->next = NULL;
	//该节点指针域指向空
	return newnode;
	//返回该节点的指针
}

         此时BuylistNode函数就创建了一个表尾,返回值用Phead接收。(此时创建的就是头指针+节点+表尾的单链表)

#include<stdio.h>
#include<stdlib.h>
int main()
{
	SLTnode* Phead = BuylistNode(1);
    return 0;
}

         此时的Phead就为次链表的头指针。后面还可以通过其他形式来加长此链表。

当然,也可以初始就创建一个较为长的链表。

SLTnode* Creat(int n) 
{
	SLTnode* head = (SLTnode*)malloc(sizeof(SLTnode));
	//创建一个单链表的头,并为其开辟空间
	SLTnode* Phead = head;
	//将该头的指针先保存起来,以防后面head改变时能正常返回链表的头指针
	for (int i = 0; i < n; i++) //创建n个节点
	{
		SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
		//为新节点开辟空间
		scanf("%d", &newnode->data);
		//为数据域赋值
		head->next = newnode;//将新建的节点与头连接起来
		head = newnode;//head指针向后移,经循环后继续向后增加节点
		//看不懂没关系,增加节点会单独拿出来讲
	}
	head->next = NULL;//经过循环后,此时的head指向表尾,为表尾的指针域赋上空
	return Phead;//返回链表的头指针
}

        (此时创建的是头指针+哨兵位+节点+表尾的单链表)

        (用此时的结构是因为一次性创建连续的单链表有哨兵位会更方便些)

单链表的打印

逐个访问单链表节点,并打印出其数据域存储的内容。

1.无哨兵位的打印

void SlistPrint1(SLTnode* Phead)
{
	while (Phead  != NULL)
	{	
		printf("%d", Phead->data);
		Phead = Phead->next ;
	}
}

         依次访问链表内容并打印,直到遇到表尾。

2.有哨兵位的打印

void SlistPrint2(SLTnode* Phead)
{
	while (Phead != NULL)
	{		
		Phead = Phead->next;
		printf("%d", Phead->data);
	}
}

         此种打印方式直接跳过了哨兵位,对后面的进行打印。

第一种打印

#include<stdio.h>
#include<stdlib.h>
int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPrint1(Phead);
	//SLTnode* Phead=creat(3);
	//SlistPrint2(Phead);
	return 0;
}

 结果

 从监视窗口中看,更容易看出链表结构。

 第二种方式打印

#include<stdio.h>
#include<stdlib.h>
int main()
{
	//SLTnode* Phead = BuylistNode(1);
	//SlistPrint1(Phead);
	SLTnode* Phead=creat(3);
	SlistPrint2(Phead);
	return 0;
}

 结果

监视窗口

 单链表的增、删、查、改

(本文章以无哨兵位的单链表为例)

 1.单链表的头插

        头插,即在单链表的最前面的节点前增加一个新节点,具体过程为:创建新节点,新节点指针域指向第一个节点,头指针指向新节点。

void SlistPushFront1(SLTnode* Phead, int x)
{
	SLTnode*newnode= BuylistNode(x);//设置新节点
	newnode->next = Phead;//新节点指针域指向第一个节点
	Phead = newnode;//头指针指向新节点
	return Phead;//返回新的头指针
}

        这样做必须要返回新的头指针,因为传头指针进入函数时,依然用一级指针接收,为同级传参,也就是说函数内部传过来的只是头指针的一份拷贝,无法改变头指针的指向,因此需要返回新的头指针,并在主函数接收。可以看出,这种方式调用函数非常繁琐、别扭。因此,可以用另一种方式在函数内部也能直接更改头指针的指向,即二级指针。

void SlistPushFront2(SLTnode** pphead, int x)
{
	SLTnode*newnode= BuylistNode(x);//设置新节点
	newnode->next = *pphead;//新节点指针域指向第一个节点
	*pphead = newnode;//头指针指向新节点
}

省去了主函数更新头指针的步骤。

int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPushFront2(&Phead, 2);
	SlistPrint1(Phead);
	return 0;
}

打印结果

2.单链表的尾插

        尾插,即在单链表的表尾后增加一个新节点,具体过程为:创建新节点,找到表尾地址,表尾指针域指向新节点。

void SlistPushBack(SLTnode*Phead, int x)
{
	SLTnode* newnode = BuylistNode(x);//设置新节点
	while (Phead->next != NULL)
	{
		Phead = Phead->next;
	}
	//通过循环找到表尾,因为Phead为拷贝,不会影响主函数的头指针
	Phead->next = newnode;//表尾指针域指向新节点
}

测试

int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPushFront2(&Phead, 2);
	SlistPushBack(Phead,3);
	SlistPrint1(Phead);
	return 0;
}

 3.单链表删头

        删除第一个节点,具体过程为:记录第二个节点的地址,释放第一个节点,最后让头指针指向第二个节点。(操作前须判断该节点是否已被删完,1、2均未考虑初始为空,有兴趣可自行加入断言或用其他方式处理)

void SlistPopFront(SLTnode** pphead)
{
		assert(*pphead != NULL);
		//断言,链表为空时,不可能再删了
		SLTnode* head = (*pphead)->next;
		//记录第二个节点的地址
		free(*pphead);
		//释放第一个节点
		*pphead = head;
		//让头指针指向第二个节点
}

测试

int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPushFront2(&Phead, 2);
	SlistPushBack(Phead,3);
	SlistPopFront(&Phead);
	SlistPrint1(Phead);
	return 0;
}

4.单链表尾删

        删除最后一个节点,具体过程为:找到最后一个节点,想办法记录倒数第二个节点的位置,将倒数第二个节点指针域置空,最后释放最后一个节点。

void SlistPopBack(SLTnode*Phead)
{
	assert(Phead != NULL);
	//断言,链表不能为空
	SLTnode* prev = NULL;
	//初始化prev,准备记录倒数第二个节点
	while (Phead->next != NULL)
	{
		prev = Phead;
		Phead = Phead->next;
	}
	prev->next = NULL;//倒数第二个指针域置空
	free(Phead);//释放最后一个节点
}

测试

int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPushFront2(&Phead, 2);
	SlistPushBack(Phead,3);
	SlistPopBack(Phead);
	SlistPrint1(Phead);
	return 0;
}

 5.单链表查找

        根据节点中数据域的值来查找,具体过程为:找到该节点,并返回该节点地址。

SLTnode* SlistFind(SLTnode* Phead,int x)
{
	while (Phead != NULL)//能够把全部节点都判断一遍
	{
		if (Phead->data == x)
		{
			return Phead;//找到该节点,并返回地址
		}
		else
		{
			Phead = Phead->next;//没找到则继续向后进行
		}
	}
    return NULL;
}

测试

int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPushBack(Phead,2);
	SlistPushBack(Phead,3);
	SlistPushBack(Phead,4);
	SlistPushBack(Phead,5);
	SlistPushBack(Phead,6);
	SlistPushBack(Phead,7);
	SlistPushBack(Phead,8);
	SlistPushBack(Phead,9);
	SlistPushBack(Phead,10);
	SlistPrint1(SlistFind(Phead,5));
	return 0;
}

        (虽然实现了查找,但是返回值只能有一个,故此程序只能找到第一个,如果后面还有则不能返回,当然,也可以通过多次调用查找函数来找到后面的地址。另外,为了防止为未找到打印有误,最好在打印函数内加上Phead不为空的断言)

多次调用示例

int main()
{
	SLTnode* Phead = BuylistNode(1);
	SlistPushBack(Phead,2);
	SlistPushBack(Phead,3);
	SlistPushBack(Phead,4);
	SlistPushBack(Phead,5);
	SlistPushBack(Phead,6);
	SlistPushBack(Phead,5);
	SlistPushBack(Phead,8);
	SlistPushBack(Phead,9);
	SlistPushBack(Phead,10);
	SlistPrint1(SlistFind(Phead,5));
	printf("\n");
	SlistPrint1(SlistFind(SlistFind(Phead, 5)->next ,5));
//注意:第二次调用函数时传的是第一次函数返回节点的后一个节点的地址
	return 0;
}

结果

6.单链表定向插入

(1)前插

        根据节点的地址来找到节点,在其前一个位置插入新的节点。(具体细节不再赘述)

void SlistInsertFront(SLTnode** pphead, SLTnode* pos, int x)
{
	//pos传的是想要插入节点的位置
	assert(*pphead!=NULL);
	if (*pphead == pos)
	{
		SlistPushFront2(pphead, 2);
		//若*pphead==pos,相当于头插,直接用头插的函数
	}
	else
	{
		SLTnode* head = *pphead;//拷贝头指针,不改动头指针
		SLTnode* prev = NULL;//准备记录pos的前一个节点
		SLTnode* newnode = BuylistNode(x);
		//创建一个新的节点
		while (head != pos)
		{
			assert(head != NULL);
			prev = head;//最终记录pos的前一个节点
			head = head->next;//不满足条件向后移
		}
			prev->next = newnode;//满足条件后pos前一个节点指向新节点
			newnode->next = pos;//新节点指向pos
	}
}

测试

int main()
{
	SLTnode* Phead = BuylistNode(1);
	//SlistPushFront2(&Phead, 2);
	SlistPushBack(Phead,2);
	SlistPushBack(Phead,3);
	SlistPushBack(Phead,4);
	SlistPushBack(Phead,5);
	SlistPushBack(Phead,6);
	SlistPushBack(Phead,7);
	SlistPushBack(Phead,8);
	SlistPushBack(Phead,9);
	SlistPushBack(Phead,10);
	SlistInsertFront(&Phead,SlistFind(Phead, 5),5);
	SlistPrint1(Phead);
	return 0;
}

结果

 (2)后插

void SlistInsertAfter(SLTnode* pos, int x)
{
	SLTnode* newnode = BuylistNode(x);
	SLTnode* tail = pos->next;
	pos->next = newnode;
	newnode->next = tail;
}

7.单链表定点删除

void SlistErase(SLTnode** pphead,SLTnode* pos)
{
	if (*pphead == pos)//若删除的是第一个节点,则只需调用头删
	{
		SlistPopFront(pphead);//头删函数
	}
	else
	{
		SLTnode* head = *pphead;//拷贝头指针,不改动头指针
		SLTnode* prev = NULL;//准备记录pos的前一个节点
		while (head != pos)
		{
			assert(head != NULL);
			prev = head;//最终记录pos的前一个节点
			head = head->next;//不满足条件向后移
		}
		SLTnode* tail = head->next;
		prev->next =tail ;//满足条件后pos前一个节点指向pos后一个节点
		free(head);
	}
}

8.销毁链表

逐个访问节点并释放

void SlistDestroy(SLTnode** pphead)
{		
	assert(*pphead != NULL);
	SLTnode* tail = NULL;
	while ( *pphead!= NULL)
	{
		tail = (*pphead)->next ;
		free(*pphead);
		*pphead = tail;
	}
}

双向循环链表

双向循环链表的创建

以下为创建的函数的功能

CSLTnode* CSlistInit()        //初始化链表

CSLTnode* CBuylistNode(int x)        //建立一个节点

void CSlistPushBack(CSLTnode* CPhead, int x)        //尾插

void CSlistPrint(CSLTnode* CPhead)        //打印

void CSlistPushFront(CSLTnode* CPhead, int x)        //头插

typedef struct cslistnode
{
	struct cslistnode* prev;
	int data;
	struct cslistnode* next;
//成员间顺序无所谓,这样写只是为了与图片更契合
}CSLTnode;

双向循环链表的初始化

CSLTnode* CSlistInit()
{
	CSLTnode* CPhead = (CSLTnode*)malloc(sizeof(CSLTnode));
	CPhead->next = CPhead;
	CPhead->prev = CPhead;//初始哨兵位,头尾指针都指向自身
	return CPhead;//返回创建哨兵位的指针
}

建立一个节点

CSLTnode* CBuylistNode(int x)
{
	CSLTnode* newhead = (CSLTnode*)malloc(sizeof(CSLTnode));
	newhead->data = x;
	newhead->next = newhead;
	newhead->prev = newhead;
	return newhead;
}

双向循环链表的尾插

        在链表后插入一个新节点,具体过程为:找到最后一个节点,创建新节点,最后一个节点next指向新节点,新节点的prev指向最后一个节点,新节点的next指向哨兵位,哨兵位的prev指向新节点。

void CSlistPushBack(CSLTnode* CPhead, int x)
{
	CSLTnode* newnode = CBuylistNode(x);
	//建立新节点
	CSLTnode* tail = CPhead;
	//记录头指针
	while (CPhead->next != tail)
	{
		CPhead = CPhead->next;
	}
	//尾结点存的是哨兵位的地址,通过该条件找到尾结点
	CPhead->next = newnode;
	//尾结点next指向新节点
	newnode->prev = CPhead;
	//新节点prev指向尾结点
	newnode->next = tail;
	//新节点next指点哨兵位
	tail->prev = newnode;
	//哨兵位prev指向新节点
}

测试

建立一个哨兵位后监视明显看出循环性

尾插成功

 双向循环链表的打印

void CSlistPrint(CSLTnode* CPhead)
{
	CSLTnode* tail = CPhead;
	//记录头指针
	while (CPhead->next != tail)
	{
		CPhead = CPhead->next;
		printf("%d ", CPhead->data);
	}
}

 双向循环链表的头插

        在链表前插入一个新节点,具体过程为:找到第一个节点节点,创建新节点,第一个节点prev指向新节点,新节点的next指向最后一个节点,新节点的prev指向哨兵位,哨兵位的next指向新节点

void CSlistPushFront(CSLTnode* CPhead, int x)
{
	assert(CPhead);
	CSLTnode* newnode = CBuylistNode(x);
	//创建新节点
	CSLTnode* tail = CPhead->next ;
	//记录第一个节点的地址
	newnode->next = tail;
	//新节点next指向第一个节点
	tail->prev = newnode;
	//第一个节点prev指向新节点
	CPhead->next = newnode;
	//哨兵位next指向新节点
	newnode->prev = CPhead;
	//新节点prev指向哨兵位
}

        双向循环链表一个节点记录了前后的地址,因此删除较为简单,可参考单链表的删除。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Soul&Spark

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

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

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

打赏作者

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

抵扣说明:

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

余额充值