数据结构-单链表

一、链表的定义

链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) +指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

而为了便于理解,我们可以将链表以图形的形式来展现,可以思考两种,一种是逻辑结构,也就是我们想象出来的,更形象,方便理解,逻辑结构图如下:

这里是引用

还有一种是物理结构,也就是在内存中实实在在如何存储的,物理结构图如下:

这里是引用

二、顺序表与单链表的比较

2.1顺序表的缺陷

(1)空间不够了,需要扩容,而扩容是有消耗的。
(2)头部或中间位置的插入删除都需要挪动,挪动数据页数有消耗的。
(3)避免频繁扩容,一次一般都是按倍数扩容(一般是2倍),可能存在一定空间浪费。

2.2顺序表的优点

支持随机访问。有些算法,需要结构支持随机访问。比如:二分查找、优化的快排…

2.3单链表的缺陷

每存一个数据,都要存一个指针去链接后面的数据节点。不支持随机访问(用下标直接访问第i个)

2.4单链表的优点

(1)按需要申请空间,不用了就释放空间(更合理的使用了空间)。
(2)头部中间插入删除数,不需要挪动数据,不存在空间浪费

三、函数实现

3.1打印

代码如下:

void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead; // cur和phead指向同一个位置
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next; 
	}
}

在这个代码里面,cur是结构体指针,可以通过->访问结构体成员,结构体有两个成员。一个是data存放数据,还有一个是next(指针)cur->next就是取右边的值,右边的值存的就是第二个节点的地址
换句话说,表面上看见cur在不断移动,实际上cur存的是不同节点的地址,看上去像是移动了的。

3.2尾插

在实现尾插前,我们首先都知道,形参是实参的一份临时拷贝,因而,我们要想改变实参的值传的应当将实参的地址传给形参,这里的形参是一级指针,而如果实参是一个指针,要想改变它的值,我们要传一个指针的指针也就是二级指针。

这里是引用
大致就是这个意思

👀我们第一步操作是找到链表的尾端,首先我们先定义一个变量tail作用类似于打印链表中的cur
代码如下:

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;//开辟一个新的节点newnode

	if (*pphead == NULL)
	{
		*pphead = newnode;   // 链接起来
	}
	else
	{
		// 找到尾节点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newnode;
	}
	
}

然后画个图我们更好理解

这里是引用

我们一样通过改变tail存放的地址来使它存放下一个节点的地址实现尾插。

3.3头插

头插单链表代码如下,我们将开辟空间malloc那一部分的代码用一个新的函数BuyListNode统一写,这一部分后面同样将会多次用到。代码如下:

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);

	newnode->next = *pphead; //*pphead就是plist
	*pphead = newnode;
}

3.4尾删

尾删函数因为删到最后可能为空,所以我们也要传二级指针。同时,尾删我们要考虑两种情况,链表只有一个节点以及链表有两个及以上节点的情况。如果只有一个节点的话我们直接free为空就好了。为了避免出现针的情况发生。我们定义了tail这个变量如图:

这里是引用

而单链表有个缺陷就是只能找到下一个位置但是不能找到上一个位置,如图:

这里是引用

考虑到链表并不确定有一个或两个节点,我们分开实现,代码如下:

void SListPopBack(SLTNode** pphead) 
{
	// 法一
	/*if (*pphead == NULL)
	{
		return;
	}*/
	assert(*pphead != NULL);
	// 1、一个节点
	// 2、两个及以上节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		//while(tail->next != NULL) 比较表达式返回值都是一个逻辑值(也就是真或者假),这里空为假
		while (tail->next)// 转换成逻辑条件判断。0为假,两者写法都可以
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;

		prev->next = NULL;
}

尾删还有一种写法是这样子的:

SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
		

这一种方法非常的好理解,如图:

这里是引用

我们通过tail->next->next找到了后面的后面的节点进行判断是否为空,然后再释放就会简便不少。

3.5头删

头删单链表的实现并不复杂。直接上代码;

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

3.6查找

查找的实现,我们传参的时候是不需要传二级指针的。我们只是找到某个数而对它并不做修改,因此我们只要传一级指针就可以了。代码如下:

SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;// 到最后都没找到就置为空
}

3.7插入数据

我们先假设要在3的位置插上一个30这样的数字,我们先开辟出newnode新的节点,然后posprev指向这个节点,再next指向pos从而实现中间pos位置的插入

这里是引用

代码如下:

//Insert是在pos之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x); //如果说pos在第一个节点,相当于头插
	if (*pphead == pos)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		//找到pos的前一个位置
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = newnode;
		newnode->next = pos;
	}
}

但是事实上单链表是不适合再pos的前面插入的,我们应当选择在pos的后面位置插入,如图:

这里是引用

代码如下:


// 在pos的后面插入,这个更适合,也更简单
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

3.8删除数据

啊,删除数据非常之奇妙,相信大家肯定早就学会了,我就不多说了,怕大家局的我烦(其实懒病发作,写不下去了🤢。时间太晚了,肝不动了)直接上代码好伐。
删除:

void SListErase(SLTNode** pphead, SLTNode* pos)
{
	if (*pphead == pos)
	{
		/**pphead = pos->next;
		free(pos);*/
		SListPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}

删除后一个:

void SListEraseAfter(SLTNode* pos)
{
	assert(pos->next);
	assert(pos);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
	//next = NULL;
}

3.9销毁数据

这里是引用
啊(bushi)

革命已经到了最后一刻,销毁单链表。我们还是画一个逻辑结构图这样好理解

这里是引用

在必不可少的断言惯例后,我们就free,free。上代码走起:

void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

这样,走到这里相信你已经完全学会了单链表。没错,你一定能看懂学会。

四、全部代码

test.c

#include "SList.h"

void TestSList1()
{
	SLTNode* plist = NULL;
	/*SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);*/

	/*SListPrint(plist);*/

	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);
	/*SListPopBack(&plist, 1);*/
	SListPopFront(&plist, 1);
	SListPrint(plist);
	SListPopFront(&plist, 2);
	SListPrint(plist);
}

void TestSList2()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 4);

	SLTNode* pos = SListFind(plist, 2);
	int i = 1;
	while (pos)
	{
		printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data);
		pos = SListFind(pos->next, 2);//我们找到第一个2了可以在第一个2后面的节点接着找
	}

	// 修改 3->30
	pos = SListFind(plist, 3);
	if (pos)
	{
		pos->data = 30;
	}
	SListPrint(plist);
}


void TestSList3()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SLTNode* pos = SListFind(&plist, 3);
	if (pos)
	{
		SListInsert(&plist, pos, 30);
	}
	SListPrint(plist);


}

int main()
{
	//TestSList1();
	//TestSList2();
	TestSList3();
	return 0;
}

SList.c

#include "SList.h"

SLTNode* BuyListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead; // cur和phead指向同一个位置
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next; // cur是结构体指针,可以通过->访问结构体成员,结构体有两个成员
// 一个是data存放数据,还有一个是next(指针)cur->next就是取右边的值,右边的值存的就是第二个的地址
	}
	printf("NULL\n");
}

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	//SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//newnode->data = x;
	//newnode->next = NULL;//开辟一个新的节点newnode

	if (*pphead == NULL)
	{
		*pphead = newnode;   // 链接起来
	}
	else
	{
		// 找到尾节点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newnode;
	}
	
}

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);

	newnode->next = *pphead; //*pphead就是plist
	*pphead = newnode;
}

void SListPopBack(SLTNode** pphead) 
{
	// 法一
	/*if (*pphead == NULL)
	{
		return;
	}*/
	assert(*pphead != NULL);
	// 1、一个节点
	// 2、两个及以上节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		//while(tail->next != NULL) 比较表达式返回值都是一个逻辑值(也就是真或者假),这里空为假
		while (tail->next)// 转换成逻辑条件判断。0为假,两者写法都可以
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;

		prev->next = NULL;

		// 方法二
		/*SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
		*/
	}
	
}

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;// 到最后都没找到就置为空
}

// 在pos的后面插入,这个更适合,也更简单
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

//Insert是在pos之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x); //如果说pos在第一个节点,相当于头插
	if (*pphead == pos)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		//找到pos的前一个位置
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = newnode;
		newnode->next = pos;
	}
}
	
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	if (*pphead == pos)
	{
		/**pphead = pos->next;
		free(pos);*/
		SListPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}

void SListEraseAfter(SLTNode* pos)
{
	assert(pos->next);
	assert(pos);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
	//next = NULL;
}

void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

SList.h

#pragma once
#include <stdio.h>
#include <stdlib.h> 
#include <assert.h>

typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SLTNode;

void SListPrint(SLTNode* phead); // phead是一个指针,指向第一个节点
void SListPushBack(SLTNode** pphead, SLTDateType x);// 尾插
void SListPushFront(SLTNode** pphead, SLTDateType x);// 头插
void SListPopBack(SLTNode** pphead);//尾删
void SListPopFront(SLTNode** pphead);//头删

SLTNode* SListFind(SLTNode* phead, SLTDateType x);//查找也可实现修改
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);//插入(配合find一起用)
void SListErase(SLTNode** pphead, SLTNode* pos);//删除
void SListEraseAfter(SLTNode* pos);// 删除后一个
void SListDestroy(SLTNode** pphead);// 链表销毁

五、总结


单链表到此为止就结束了
在这里插入图片描述链表我们不光有单链表,还有双向链表等,数据结构同样还有很多知识等待我们去学习探索,在这里给各位大佬们磕一个,感谢大家阅读我的文章,文章中还有很多不足的地方欢迎大家在评论区给我留言,我会不定期不定时有缘回复
最后依然还是我们的“只因”🐔哥镇图
在这里插入图片描述

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值