数据结构:单链表

我们已经学习过了数据结构中的顺序表,虽然动态顺序表能够自主判断空间是否可用并且在空间不足时自动扩容,但是每一次扩容都要申请两倍的内存,如果只是为了插入几个数据而对内存两倍的扩容会对内存造成很多的浪费。所以我们要利用到另外一种数据结构——单链表。链表的优势在于需要一个数据就申请一块空间,插入一个数据。同时,在实现顺序时我们发现头插或者头删一个数据要移动后面的所有数据,运行起来比较繁琐,而单链表就不会出现这种缺点。

目录

1.链表的概念和结构

2.单链表的实现

2.1单链表的定义(STList.h)

2.2单链表增删查改工作的实现(STList.c)

2.2.1单链表的初始化函数(头节点的创建函数)

2.2.2链表的销毁函数

2.2.3链表的打印函数

2.2.4尾插函数

2.2.5头插函数

2.2.6尾删函数

2.2.7头删函数

2.2.8查找函数

 2.2.9在指定位置之前插入数据  

2.2.10删除指定位置的节点

3.对链表的测试(test.c)

3.链表函数完整代码


1.链表的概念和结构

链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的。用更加精炼的话来说就是: 物理顺序不连续,逻辑顺序是连续的
链表类似于火车,数据就像车厢。车厢的增加或减少都是很容易的,每一个车厢都是独立的存在。既然是独立的存在,那么各个车厢如何联系呢?根据现实生活的经验知道车厢连接处可以联通所有车厢。那么链表联通所有数据的法宝就类似于车厢连接处—— 指针。每个独立的车厢在链表中叫做节点(或结点)。
如下图:链表结构的示意图
由图得知节点之间是通过指针相互连接的,指针变量Plist指向第一个节点,第一个节点再指向第二个节点......以此类推。也就是说, 一个节点包含了要存储的数据以及存储了下一个节点的地址
那么结合我们学过的结构体知识我们可以把节点的代码表示出来:
struct SListNode
{
 int data; //节点存储的数据
 struct SListNode* next; //保存下⼀个节点的地址
};

那么我们如何访问到链表的每一个元素呢?

创建一个新的变量pcur,通过pcur来访问每一个节点。pcur->data来访问数据,pcur=pcur->next来找到并访问下一个节点。当pcur->next=NULL的时候停止访问,这样就能遍历整个链表了。

2.单链表的实现

2.1单链表的定义(STList.h)

首先我们要在头文件(STList.h)里对链表进行定义:

typedef int SLDataType;

typedef struct SList 
{
	SLDataType data;
	struct SList* next;
}SL;

链表由两部分组成:存储的数据以及指向下一个节点的指针

定义和重命名完成之后,就要开始在.c文件中操作链表进行增删查改的工作了。

2.2单链表增删查改工作的实现(STList.c)

2.2.1单链表的初始化函数(头节点的创建函数)

对标我们前面所讲的顺序表,实现链表增删查改的第一步肯定是对链表进行初始化。但是与顺序表有区别的地方是:链表是输入一个数据,开辟一块空间,无需自动扩容所以我们使用malloc函数进行内存的申请。

SL* SLBuyNode(SLDataType x)
{
	SL* NewNode = (SL*)malloc(sizeof(SL));
	NewNode->data = x;
	NewNode->next = NULL;
	return NewNode;
}
初始化原理就是:创建一个结构体指针,并且给结构体里的元素赋值(链表的最后一个元素的指针默认指向NULL,链表只有一个元素的时候默认是最后一个元素)。
2.2.2链表的销毁函数

链表的销毁核心就是释放每次为链表节点申请的空间,并且释放指针,防止其变成野指针。

需要注意的一点是:因为我们需要改变结构体里指针的指向,指针本身就是一个变量。而我们在函数里修改一个变量需要传递一个变量的地址,而指针的地址就是指针的指针(也就是二级指针),所以函数的形参是要用二级指针来接收

void SLDestroy(SL**ppHead)
{
	SL* pcur = *ppHead;
	while (pcur)
	{
		SL* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*ppHead = NULL;
}

代码的示意图如下:

创建两个指针,pcur和next.初始时pcur指向头节点,next在pcur的下一个节点处。每一次循环pcur所在节点申请的内存空间都被释放,同时next指针能保证pcur指针能一直找到下一个节点所在的位置。

2.2.3链表的打印函数

链表的打印实际上就是链表的遍历。根据头节点一个一个向后寻找直到节点中指针指向的NULL(也就是没有下一个节点了)。

void PrintSL(SL* Pcur)
{
	SL* Phead = Pcur;
	while (Phead)
	{
		printf("%d ", Phead->data);
		Phead = Phead->next;
	}
	
}
2.2.4尾插函数

尾插分为两种情况:(1)链表中没有元素(创建一个头节点),(2)链表中已经有元素了(找到最后一个节点,并在最后一个节点后面加上要头插的节点)。

void SLPushBehind(SL**ppHead,SLDataType x)
{
	SL*NewNode=SLBuyNode(x);
	if (*ppHead==NULL)
	{
		*ppHead = NewNode;
	}
	else
	{
		SL* ptail = *ppHead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = NewNode;
	}
}

注意事项:

1.像打印函数所说的一样:要在函数中修改指针的指向就必须用二级指针来接收。

2.while循环的判断条件是 ptail->next==NULL时跳出,此时ptail指向的恰好是最后一个节点。再将ptail->next用新创建的节点赋值,这样就完成了尾插工作。

2.2.5头插函数

头插依旧分为两种情况:(1)链表中没有元素。(此时情况与尾插情况类似,省去了头插的分析),(2)链表中已经含有元素了。(此时通过指针交换的一种巧妙方式,不动链表中的其他元素,轻松头插一个节点)

第二种情况的画图分析如下:

Step1:使新节点指向原头节点

Step2:使头节点指向头插的元素

void SLPushFront(SL**ppHead,SLDataType x)
{
	SL* NewNode = SLBuyNode(x);
	SL* pHead = *ppHead;
	if (*ppHead==NULL)
	{
		*ppHead = NewNode;
	}
	else
	{
		NewNode->next = pHead;
		*ppHead = NewNode;
	}

}

2.2.6尾删函数
void SLPopBehind(SL**ppHead)
{
	assert(*ppHead);
	if ((*ppHead)->next==NULL)
	{
		(*ppHead) = NULL;
	}
	else
	{
		//找尾
		SL* pTail = *ppHead;
		SL* prev = *ppHead;
		while (pTail->next)
		{
			prev = pTail;
			pTail = pTail->next;
		}
		//释放尾节点
		free(pTail);
		pTail = NULL;
        //让倒数第二个节点指向NULL
		prev->next = NULL;
	}
}

注意事项:

(1)尾删工作分为4步:assert断言、找到尾节点、释放尾节点、让倒数第二个节点指向NULL(倒数第二个节点正式成为尾节点)

(2)要对数据进行删除的前提是有数据,所以我们一开始要对传进来的指针进行assert断言处理,保证传进来的指针不是空指针。

(3)当只有一个元素时(即代码中的(*ppHead)->next==NULL),直接释放该节点。

2.2.7头删函数

头删函数的示意图如下: 

(1)一开始时*pphead,phead正常指向头元素

(2)第二步*pphead移动到下一个节点为头删做准备(头删之后下一个节点就是头节点)。

(3)利用phead将头节点释放。同时使phead指针指向NULL。

void SLPopFront(SL**ppHead)
{
	assert(ppHead && *ppHead);
	if ((*ppHead)->next == NULL)
	{
		*ppHead = NULL;
	}
	else
	{
		SL* pHead = *ppHead;
		*ppHead = (*ppHead)->next;
		free(pHead);
		pHead = NULL;
	}
}
2.2.8查找函数

这个查找函数是根据输入的数据进行查找的,通过遍历链表,如果有该数据就打印“找到了”,没有就输出“没找到”。查找函数的底层逻辑就是遍历,没有什么难度。

void* SLFind(SL*pcur,SLDataType x)
{
	SL* pHead = pcur;
	assert(pHead);
	while (pHead)
	{
		if (pHead->data == x)
		{
			printf("找到了\n");
			return;
		}
		pHead = pHead->next;
	}
	printf("没找到\n");
	return;
}

 2.2.9在指定位置之前插入数据  

在指定位置前插入数据分两种情况:在第一个元素前插入(实际上就是头插),以及再后面的指定位置前插入。第二种情况的具体思路如下图:

第一步:首先假设绿色的是我们指定的位置,要在绿色元素前插入一个数据。那么我们的思路就是将1号指针(图中标明的)指向新的元素,同时将新元素的指针指向绿色的指定元素。

第二步:先找到指定元素的前一个元素并将其指针pcur->next指向新元素。新元素指针的next指向绿色指定元素。

void SLPushPointFront(SL**ppHead,int pos,SLDataType x)
{
	if (pos == 1)
	{
		SLPushFront(ppHead, x);
	}
	else
	{
		SL* NewNode = SLBuyNode(x);
		SL* pHead = *ppHead;
		SL* pTail = *ppHead;
		//找到指定元素的前一个元素
		for (int count = 1; count < pos-1; count++)
		{
			pHead = pHead->next;
			pTail = pTail->next;
		}
		pTail = pTail->next;
		pHead->next= NewNode;
		NewNode->next = pTail;
	}
}
2.2.10删除指定位置的节点
void SLPointDelete(SL**ppHead,int pos)
{
	SL* pcur = *ppHead;
	assert(ppHead&&*ppHead);
	//尾删情况
	if (pcur->next == NULL)
	{
		SLPopBehind(ppHead);
		return;
	}
	//其他情况
	for (int i=1;i<pos-1;i++)
	{
		pcur = pcur->next;
	}
	SL*pNext=pcur->next;
	pcur->next = pNext->next;
	free(pNext);
	pNext = NULL;
}

3.对链表的测试(test.c)

我们可以在主函数里写一个test函数来测试链表的效果。

#include"Slist.h"

SL link;
SL* ps=NULL;

void SLtest01()
{
	SLPushBehind(&ps, 1);
	SLPushBehind(&ps, 2);
	SLPushBehind(&ps, 3);
	SLPushBehind(&ps, 4);
	PrintSL(ps);
}


int main()
{
	SLtest01();
	return 0;
}

输出效果:

后续还可以进行一些其他功能的测试:

3.链表函数完整代码

#include"Slist.h"

SL link;

//申请空间函数
SL* SLBuyNode(SLDataType x)
{
	SL* NewNode = (SL*)malloc(sizeof(SL));
	NewNode->data = x;
	NewNode->next = NULL;
	return NewNode;
}

//打印函数
void PrintSL(SL* Pcur)
{
	SL* Phead = Pcur;
	while (Phead)
	{
		printf("%d ", Phead->data);
		Phead = Phead->next;
	}
	
}

//尾插函数
void SLPushBehind(SL**ppHead,SLDataType x)
{
	SL*NewNode=SLBuyNode(x);
	if (*ppHead==NULL)
	{
		*ppHead = NewNode;
	}
	else
	{
		SL* ptail = *ppHead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = NewNode;
	}
}

//头插函数
void SLPushFront(SL**ppHead,SLDataType x)
{
	SL* NewNode = SLBuyNode(x);
	SL* pHead = *ppHead;
	if (*ppHead==NULL)
	{
		*ppHead = NewNode;
	}
	else
	{
		NewNode->next = pHead;
		*ppHead = NewNode;
	}

}

//尾删函数
void SLPopBehind(SL**ppHead)
{
	assert(ppHead&&*ppHead);
	if ((*ppHead)->next==NULL)
	{
		(*ppHead) = NULL;
	}
	else
	{
		//找尾
		SL* pTail = *ppHead;
		SL* prev = *ppHead;
		while (pTail->next)
		{
			prev = pTail;
			pTail = pTail->next;
		}
		free(pTail);
		pTail = NULL;
		prev->next = NULL;
	}
}

//头删函数
void SLPopFront(SL**ppHead)
{
	assert(ppHead && *ppHead);
	if ((*ppHead)->next == NULL)
	{
		*ppHead = NULL;
	}
	else
	{
		SL* pHead = *ppHead;
		*ppHead = (*ppHead)->next;
		free(pHead);
		pHead = NULL;
	}
}

//指在指定位置之前插入数据
void SLPushPointFront(SL**ppHead,int pos,SLDataType x)
{
	if (pos == 1)
	{
		SLPushFront(ppHead, x);
	}
	else
	{
		SL* NewNode = SLBuyNode(x);
		SL* pHead = *ppHead;
		SL* pTail = *ppHead;
		//找到指定元素的前一个元素
		for (int count = 1; count < pos-1; count++)
		{
			pHead = pHead->next;
			pTail = pTail->next;
		}
		pTail = pTail->next;
		pHead->next= NewNode;
		NewNode->next = pTail;
	}
}

//删除指定的数据(节点)函数
void SLPointDelete(SL**ppHead,int pos)
{
	SL* pcur = *ppHead;
	assert(ppHead&&*ppHead);
	//尾删情况
	if (pcur->next == NULL)
	{
		SLPopBehind(ppHead);
		return;
	}
	//其他情况
	for (int i=1;i<pos-1;i++)
	{
		pcur = pcur->next;
	}
	SL*pNext=pcur->next;
	pcur->next = pNext->next;
	free(pNext);
	pNext = NULL;
}

//查找数据函数
void* SLFind(SL*pcur,SLDataType x)
{
	SL* pHead = pcur;
	assert(pHead);
	while (pHead)
	{
		if (pHead->data == x)
		{
			printf("找到了\n");
			return;
		}
		pHead = pHead->next;
	}
	printf("没找到\n");
	return;
}

//销毁链表
void SLDestroy(SL**ppHead)
{
	SL* pcur = *ppHead;
	while (pcur)
	{
		SL* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*ppHead = NULL;
}

  • 36
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值