单链表的基本操作

本篇博客会介绍一下链表,然后来实现单链表的功能(增删查改)。篇幅较长,代码部分较多,建议按目录查看。

目录

一、链表的概念与结构

1.1 链表的概念

1.2 链表结构的存储方式

二、链表的分类

 三、单链表的实现

1. 定义单链表的结构

2. 新结点的创建

3. 链表的尾插

4. 链表的头插

5. 链表的打印

6. 链表的头删

7. 链表的尾删

8. 链表的头删

9.链表的查与改

10. 在pos位置之前插入

11. 删除pos位置

12. 在pos位置之后插入

13. 删除pos位置之后的结点

14. 链表的销毁


一、链表的概念与结构

1.1 链表的概念

概念:链表是一种物理存储结构上非连续、非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

1.2 链表结构的存储方式

链表存储结构可以用逻辑结构和物理结构来表示,这里我们看看这两种方式下链表的形式。

逻辑结构:


物理结构:

注意:

        1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续,他们这种连续的结构主要是其指针域指向了下一个结点的地址。

        2.现实中的结点一般都是从堆上申请出来的。

        3.从堆上申请空间,是按照一定的策略来分配的,两次申请的空间可以能连续,也可能不连续。

二、链表的分类

实际中链表的结构非常多样,一下情况组合起来共有8种情况。

分别为单向/双向、带头/不带头、循环/不循环这8种情况。

1.单向or双向

2. 不带头or带头

 3. 循环or不循环

虽然有这么多的链表结构,但是我们实际中常用的还是两种结构:

原因:

1.无头单向非循环链表:结构简单,一般不会单独用来存放数据。实际中更多是作为其他数据结构的子结构。如:哈希桶、图的领接表等等。另外这种结构在笔试面试中出现很多。

2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用链表的数据结构,都是带头双向循环链表。另外这个结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,这个结构后续我们会用代码来实现。

 三、单链表的实现

1. 定义单链表的结构

对于每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。

typedef struct SListNode
{
	SLTDateType date;               //用于存放数据
	struct SListNode* next;         //指向下一个结点的指针
}SLTNode;

这里的SLTDateType是链表中数据域存放的数据类型。因为我们使用单链表时,可能会存放整形数据、字符型数据、浮点型数据,所以我们使用SLTDateType来表示链表中存放的数据类型。如下:

//存放的数据类型
typedef int SLTDateType;

2. 新结点的创建

现在我们开始创建链表的结点,我们的思路是:调用这个函数时,我们malloc出一块空间,然后将数据放入数据域中,并将这个结点的下一个元素指向NULL,最后将开辟的这个节点的地址返回。

每条代码的意思我打上注释,具体实现大家可以看看。

//创建新节点
SLTNode* BuyListNode(SLTDateType x)
{
	//开辟一块空间,然NewNode的指向这块空间;
	SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
	//判断空间开辟是否成功
	assert(NewNode);
	//将指针域置为空,即该节点指向的下一个节点为空
	NewNode->next = NULL;
	//放入数据
	NewNode->date = x;
	//返回该空间的地址
	return NewNode;
}

3. 链表的尾插

现在我们创建了节点,但是节点的指向为空,我们无法做到将各节点间串起来,所以我们要再创建一个尾插接口来实现这个功能。

思路:

           1.复用BuyListNode创建一个新节点。

           2.判断该链表是不是空链表,如果是,则让新开辟的节点为第一个节点。

           3.如果不是,则用一个指针找到尾节点,让尾节点的指针域指向新开辟的节点。

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	//1.创建一个新节点
	SLTNode* NewNode = BuyListNode(x);

	//2.如果phead是空,则表示一个结点都没有
	//那就要将开辟的链表当作第一个节点
	if (*pphead == NULL)
	{
		*pphead = NewNode;
		return;
	}
	//3.找尾
	SLTNode* Tail = *pphead;
    while (Tail->next != NULL)
	{
    	Tail = Tail->next;
	}
	//此时Tail指向最后一个结点
	//则将新开辟的结点赋予Tail
    Tail->next = NewNode;
	//函数结束之后,NewNode和Tail会被销毁,但是开辟的空间依然在。
}

注意:

    这里函数参数我们传入的是二级指针,因为我们这里做了一个行为,即如果phead是空,我们就将新节点作为第一个节点。这里我们改变了外面第一个节点的指向,所以我们要使用二级指针。因为函数形参是实参的临时拷贝,所以如果我们要改变一个指针的指向,要传入它的地址,指针的地址,所以我们要传入二级指针。


4. 链表的头插

尾插实现完那我们来实现头插的。思路如下:

//链表的头插
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	//创建一个新节点
	SLTNode* NewNode = BuyListNode( x);
	//新节点指向第一个节点
	NewNode->next = *pphead;
	//改变了外面的实参,将NewNode改为第一个节点
	*pphead = NewNode;
}

注意:

同样,我们改变了外面指针的指向,将新结点变成了第一个结点。所以我们使用二级指针。


5. 链表的打印

实现了头插、尾插,我们再来实现一个打印函数来检验一下成果。这个函数的话,我们不用改变实参的值,所以传入一级指针即可。

//链表的打印
void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	//直到cur == NULL
	while (cur != NULL)
	{
		printf("%d->", cur->date);
		cur = cur->next;
	}
	printf("NULL\n");
}


6. 链表的头删

 这个功能因为要改变实参的指向,所以要传入二级指针。思路如下:

//头删的实现
void SListPopFront(SLTNode** pphead)
{
	//检查plist是否为空 
	assert(*pphead);
	//先保存plist的指向,即为plist的下一个结点的地址
	SLTNode* next = (*pphead)->next;
	//释放plist节点
	free(*pphead);
	//next置为第一个结点
	*pphead = next;
}

7. 链表的尾删

思路:

1. 判断传入的链表是否为空,为空则直接退出。

2. 如果传入的链表只有一个节点,那直接free掉这一个节点,返回NULL即可。表示链表已删空。

3.如果传入链表有2个或两个以上的节点。那遍历找到最后一个节点,然后free该节点,然后将最后一个节点的前一个结点的指针域指向置为空。

注意:

        这里传入的是二级指针,我们将如果有一个结点的情况下,将实参变为了空。

//尾删的实现
void SListPopBack(SLTNode** pphead)
{
	//去除空结点的请况
	assert(*pphead);
	//只有一个节点的情况
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个结点的情况
	//方法1:使用两个节点
	
	SLTNode* cur = *pphead;
	SLTNode* prev = NULL;
	//找到最后一个节点
	while (cur->next!= NULL)
	{
		prev = cur;
		cur = cur->next;
	}
	free(cur);
	cur->next = NULL;  
}

这里还可以使用方法二:

        因为在上面已经处理了,结点数为0和结点数为1的情况,所以找尾的这个遍历我们使用一个指针cur就可以了,其实思路相差不大。

//方法2:使用一个节点,判断Temp1->next->next!=NULL  让Temp1找倒数第二个
	SLTNode* cur2 = *pphead;
	while (cur2->next->next != NULL)
	{
		cur2 = cur2->next;
	}
	free(cur2->next);
	cur2->next = NULL;

8. 链表的头删

思路:

1. 检查链表是否为空

2. 保存下一个结点的地址

3. 释放第一个结点

4. 将下一个结点置为第一个结点。

//头删的实现
void SListPopFront(SLTNode** pphead)
{
	//检查plist是否为空
	assert(*pphead);
	//先保存plist的指向,即为plist的下一个结点的地址
	SLTNode* next = (*pphead)->next;
	//释放plist节点
	free(*pphead);
	//将next的地址给plist,则是将下一个节点链接起来
	*pphead = next;
}

功能检测:

还是使用上面的测试案例,我们头删、尾删各5次,结果应该是剩下两个1; 


9.链表的查与改

查和改通常是一起使用的,我们传入想要的值,找到那个值返回这个结点的地址,然后将新值放入该结点中。这个两个函数比较简单,就不多赘述了。

//查找的实现
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->date == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//修改的实现
void SListModify(SLTNode* phead, SLTDateType Data, SLTDateType NewData)
{
    assert(phead);
	//使用一个结点接受返回结点的地址
	SLTNode*ModifyNode= SListFind(phead, Data);
    assert(ModifyNode);
	ModifyNode->date = NewData;
}

查改数据有很多种方式,因为都比较简单,只需要遍历的问题而已,就不多介绍了,如果有兴趣的话可以将多种查改数据的方式给实现出来。


10. 在pos位置之前插入

实现了上面的头插、尾插,接下来我们实现一个传入pos位,那我们就在pos位之前插入一个结点。

//在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	//pos不能为空
	assert(pos&&pphead);
	//头插
	if (pos == *pphead)
	{
		SListPushFront(pphead, x);
	}
	SLTNode* prev = *pphead;
	//找到pos之前的位置
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//创建一个新节点
	SLTNode* newnode = BuyListNode(x);
	//将prev指向新节点
	prev->next = newnode;
	//新结点放在pos之前
	newnode->next=pos;
}

11. 删除pos位置

实现删除pos位置的值,不局限于头删和尾删。

//删除pos位置
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && pos);
    //如果删除的结点是链表的第一个结点
	if (*pphead == pos)
	{
		SListPopFront(pphead);
	}
	SLTNode* prev = *pphead;
    //找到pos位置之前的一个位置
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
}

12. 在pos位置之后插入

实现这个函数,我们可以在任意pos位置之后插入数据。

在pos位置之后插入数据可以做到不用遍历链表,时间复杂度低。

思路:

        1.创建一个新结点

        2.将该结点指向pos的下一个结点。

        3.然后将pos指向新结点。

//在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos);
	SLTNode* NewNode = BuyListNode(x);
	//顺序不能颠倒!!!
	NewNode->next = pos->next;
	pos->next = NewNode;
}

注意:

        这里我没有传入二级指针,因为我们没有将实参进行修改。


13. 删除pos位置之后的结点

思路:

        1.判断传入的pos是否为空

        2.判断pos的下一个位置是否为空

        3.保存pos后的下一个结点,将pos指向下一个结点所指向的结点。

        4.释放待删除的结点。

//删除pos位置之后的结点
void SListEraseAfter(SLTNode* pos)
{
	//判断pos是否为空
	assert(pos);
	//判断pos的下一个位置是否为空
	if (pos->next == NULL)
	{
		return;
	}
	//将pos下一个结点保存起来
	SLTNode* del = pos->next;
    //使pos指向del的下一个结点。
	pos->next = del->next;
	free(del);
}

14. 链表的销毁

//链表的销毁
void SListDestoy(SLTNode* phead)
{
	SLTNode* cur = phead;
	SLTNode* del = NULL;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		del->next = NULL;
		free(del);
	}
}

结语

本篇的介绍到此就结束了,下篇博客会实现带头双向循环链表,这种一种常见的链表结构,名字听着吓人,但是功能实现比单链表简单很多。

如果感觉还的不错的话可以关注一下留个赞呗,你们的鼓励是我最大的动力。我们下期再见。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Brant_zero2022

素材免费分享不求打赏,只求关注

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

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

打赏作者

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

抵扣说明:

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

余额充值