【数据结构初阶】:无头单向不循环链表(用C语言实现,附图详解)

在这里插入图片描述

一、链表的概念

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
注意: 链表在逻辑上是连续的,但在物理上不一定连续,链表不像顺序表那样在物理上是一点连续的,我们要特别注意这一点。
在这里插入图片描述

二、链表的分类

实际中链表的结构非常多样,我们有以下分类:
1、单向或者双向 在这里插入图片描述
2、带头或者不带头
在这里插入图片描述
3、循环或者非循环
在这里插入图片描述

而根据上面的3种情况组合起来就有8种链表结构,分别是:

1.无头单向不循环链表
2.无头单向循环链表
3.无头双向不循环链表
4.无头双向循环链表
5.有头单向不循环链表
6.有头单向循环链表
7.有头双向不循环链表
8.有头双向循环链表

但是虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
在这里插入图片描述

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
  3. 下面我们主要来实现无头单向不循环链表。

三、无头单向不循环链表的实现思路

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成,所以我们要创建一个结点型,这个可以用结构体实现,在结构体里面有指针域和数据域,数据域用来存放数据,指针域存放下一个结点的地址。
创建类型如下:

typedef int SLNDataType;  //对int类型重新起个名字叫SLNDataType

struct SListNode
{
	SLNDataType data;        //数据域
	struct SListNode* next;  //指针域
};

//对链表类型struct SListNode重新起个名字叫SLNode
typedef struct SListNode SLNode;

然后有了类型之后我们就在可以创建一个链表类型的指针plist,这个指针用来维护链表的第一个结点,所以我们有一点要非常注意:
如果想要改变第一个链表的第一个结点就要传指针plist的地址,然后要用二级指针来接收plist的地址,从而解引用来改变指针plist所存储的第一个结点地址,这样才会改变指针plist维护的第一个结点。
如果传指针plist的值,那么只是对plist内容的拷贝,不会改变plist里面存储的第一个结点地址,从而第一个结点仍然没有被改变。

四、无头单向不循环链表内存布局图

在这里插入图片描述

五、无头单向不循环链表接口实现:

1.尾部插入数据

在这里插入图片描述
代码实现如下:


//新开一个结点
SLNode* BuyNewNode(SLNDataType x)
{
	SLNode* tmp = (SLNode*)malloc(sizeof(SLNode));
	if (tmp == NULL)
	{
		perror("erron ");
		exit(-1);
	}
	tmp->data = x;
	tmp->next = NULL;
	//把新开结点的地址返回去
	return tmp;
}

//尾插
void SListPushBack(SLNode** pphead, SLNDataType x)
{
	assert(pphead);
	SLNode* tmp = BuyNewNode(x);  //新开一个结点
	//1.链表为空的情况
	if (*pphead == NULL)
	{
		*pphead = tmp;
	}
	//2.链表为多个节点的情况,要找到最后一个结点
	else
	{
		SLNode* cur = *pphead;
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		//把新结点的地址给最后一个结点的指针域
		cur->next = tmp;
	}
}

2.头部插入数据

在这里插入图片描述
代码实现如下:

//头插
void SListPushFront(SLNode** pphead, SLNDataType x)
{
	assert(pphead);
	SLNode* tmp = BuyNewNode(x);
	tmp->next = *pphead;
	//新的结点成为第一个结点
	//就算链表为空也能实现
	*pphead = tmp;
}

3.尾部删除数据

尾删分为三种情况:
1、链表为空就不能再删除了,直接报错。
2、链表为多个结点,如果删除的不是第一个结点我们可以传指针plist的值即可。
3、链表只有一个结点,但是链表要删除第一个结点我们还得传二级指针来改变指针plist存的地址,所以我们统一传二级指针来实现。
4、注意:删除尾部的数据还要把尾部的前一个结点的指针域置为NULL空指针,否则我们下次删除就找不到尾了。
在这里插入图片描述
代码实现如下:

//尾删
void SListPopBack(SLNode** pphead)
{
	assert(pphead);
	//空链表就不能再删除了
	assert(*pphead);

	//链表为一个结点的情况
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表为多个结点的情况
	else
	{
		SLNode* cur = *pphead;
		SLNode* prev = NULL;
		while (cur->next != NULL)
		{
			prev = cur;
			cur = cur->next;
		}
		free(cur); //释放结点
		prev->next = NULL;  //把倒数第二个结点指针域置为空
	}

}

4.头部删除数据

在这里插入图片描述

//头删
void SListPopFront(SLNode** pphead)
{
	assert(pphead);
	//空链表就不能再删除了
	assert(*pphead);
	
	SLNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

5.显示数据

显示数据即把结点里面数据域的内容打印出来,我们在这个过程中没有改变第一个结点的地址,所以我们没有不必要传二级指针,直接传指针plist的值过去即可。然后根据指针plist里面存的地址一个接着一个访问结点就可以了,直到链表结束。
代码实现如下:

//显示数据
void SListPrint(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

6.查找数据

这个接口和显示数据类似,我们在查找过程中一定不会改变第一个结点的地址,所以我们
没有不必要传二级指针,直接传指针plist的值过去即可。然后根据指针plist里面存的地址一个接着一个访问结点里面的数据域,如果找到了就返回这个结点的地址,找不到就返回空指针NULL。
代码如下:

//查找数据
SLNode* SListFind(SLNode* phead,SLNDataType x)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			//找到了就返回结点的地址
			return cur;
		}
		cur = cur->next;
	}
	//找不到就返回空指针
	return NULL;
}

7.在结点前面插入数据

这个接口有两种情况:
1、在其它结点前面插入
2、在第一个结点前面插入
如果是第一种情况我们可以传指针plist的值过去即可,但是如果是第二种情况,就要改变第一个结点的地址要传指针plist的地址过去才能修改plist里面的内容;所以我们还是统一传二级指针过去。
在这里插入图片描述
代码如下:

//在结点前面插入数据
void SListInsertPrev(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	assert(pphead);
	assert(pos);
	SLNode* tmp = BuyNewNode(x);
	//1.如果要插入的位置在头部前面就头插
	if (pos == *pphead)
	{
		tmp->next = *pphead;
		*pphead = tmp;
	}
	//2.在其它位置就找前一个结点
	else
	{
		SLNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = tmp;
		tmp->next = pos;
	}
}

8.在结点后面插入数据

这个比要在结点前面插入简单很多,不必要找前面一个结点的地址了。
因为不会改变链表第一个结点的地址,所以我们可以不用传二级指针也能实现。
在这里插入图片描述
代码如下:

//在结点后面插入数据
void SListInsertAfter(SLNode* pos, SLNDataType x)
{
	assert(pos);
	SLNode* tmp = BuyNewNode(x);
	tmp->next = pos->next;
	pos->next = tmp;
}

9.删除当前位置数据

这个有2种情况:
1、删除的是不是第一个结点
2、删除的是第一个结点
因为删除当前位置数据有可能是第一个结点,所以我们统一传二级指针来实现。
在这里插入图片描述
代码如下:

//删除当前位置数据
void SListErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(pos);
	//要删除的是第一个结点
	if (*pphead == pos)
	{
		*pphead = pos->next;
		free(pos);
	}
	//删除的是其它结点,要找到前一个结点
	else
	{
		SLNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = pos->next;
		free(pos);
	}
}

10.删除当前位置后一个数据

这个接口不可能删除第一个结点,所以我们不用传二级指针来改变,直接指针plist存的地址即可。
不够要注意的是不能传最后一个结点的地址过来,因为最后一个结点后面没有数据可以删除了。
在这里插入图片描述
代码如下:

void SListEraseAfter(SLNode* pos)
{
	assert(pos);
	//不能传最后一个结点的地址过来
	assert(pos->next != NULL);
	pos->next = pos->next->next;
	free(pos->next);
}

11.内存释放

要注意的是不能一下子就释放第一个结点,要保存好下一个结点地址,一个接一个释放。
因为要释放第一个结点所以还要传二级指针来改变plist里面的地址。
代码如下:

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

六、内容总结(附源码)

以上就是无头单向不循环链表各种接口的实现了,总的来说实现有几点要注意:
1、要改变指针plist所存储第一个结点的地址必须要传址实现
2、要把握好结点之间存放的信息
下面是我的测试代码:

#include "SList.h"

//测试头插和尾插
void TestPush()
{
	SLNode* plist = NULL;
	
	printf("尾插4个数据:\n\n");
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPrint(plist);

	printf("头插两个数据:\n");
	SListPushFront(&plist, 8);
	SListPushFront(&plist, 9);
	SListPushFront(&plist, 10);
	SListPrint(plist);
	SListDestroy(&plist);
}

//测试头删和尾删
void TestPop()
{
	SLNode* plist = NULL;

	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPrint(plist);

	printf("尾删两个数据:\n");
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPrint(plist);

	printf("头删两个数据:\n");
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPrint(plist);
	SListDestroy(&plist);
}

//测试插入
void TestInsert()
{
	SLNode* plist = NULL;

	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);
	SListPrint(plist);

	SLNode* pos = SListFind(plist, 1);
	if (pos != NULL)
	{
		printf("在1的前面插入99:\n");
		SListInsertPrev(&plist, pos, 99);
		SListPrint(plist);
	}
	pos = SListFind(plist, 4);
	if (pos != NULL)
	{
		printf("在4的前面插入88:\n");
		SListInsertPrev(&plist, pos, 88);
		SListPrint(plist);
	}

	pos = SListFind(plist, 5);
	if (pos != NULL)
	{
		printf("在5的后面插入99:\n");
		SListInsertAfter(pos, 55);
		SListPrint(plist);

	}
	SListDestroy(&plist);
}

//测试删除
void TestErase()
{
	SLNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);
	SListPrint(plist);
	SLNode* pos = SListFind(plist, 2);
	if (pos != NULL)
	{
		printf("把2删除掉:\n");
		SListErase(&plist, pos);
		SListPrint(plist);
	}
	
     pos = SListFind(plist, 4);
	if (pos != NULL)
	{
		printf("把4删除掉:\n");
		SListErase(&plist, pos);
		SListPrint(plist);
	}

	pos = SListFind(plist, 3);
	if (pos != NULL)
	{
		printf("删除掉2后面的下一个数据:\n");
		SListEraseAfter(pos);
		SListPrint(plist);
	}
	SListDestroy(&plist);

}

int main()
{
	TestPush();
	TestPop();
	TestInsert();
	TestErase();
	return 0;
}

以上就是我的链表实现内容了,其中前面我只有把源文件SList.c 和测试文件test.c拆开来分析了。如果想要无头单向不循环链表的全部内容,阔以移步到gitee上获取。
在这里插入图片描述
内容链接:https://gitee.com/fait-juyuan/data-structure/tree/master/test_10_22_%E5%8D%95%E9%93%BE%E8%A1%A8/test_10_22_%E5%8D%95%E9%93%BE%E8%A1%A8

  • 17
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值