【数据结构】《动图演示》单链表实现

前言

数据结构的常见线性表,分别是顺序表,链表,栈,队列

本篇给大家带来单向无头链表的实现和讲解

为什么会有单链表?

  • 动态顺序表储存数据存会造成一定的消耗
  • 链表存储数据是按需扩容,不会造成空间浪费
  • 通常做复杂数据结构的子部分

等单链表的相关文章写完,之后就出一篇详细顺序表和单链表的区别


一、链表的概念和结构

1.1 概念

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

1.2 结构和存储

  • 逻辑结构
    在这里插入图片描述

  • 物理结构
    在这里插入图片描述

  1. 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续,他们这种连续的结构主要是其指针域指向了下一个结点的地址。
  2. 现实中的结点一般都是从(heap)申请出来的。
  3. 从堆上申请空间,是按照一定的策略来分配的,两次申请的空间可以能连续,也可能不连续。比如连续:第一个结点地址是0x0023b11,下一个地址就是0x0023b15

二、链表的分类

2.1 单项和双向
在这里插入图片描述

2.2 带头和不带头
在这里插入图片描述

2.3 循环和非循环
在这里插入图片描述

综上所述以有8种链表,但是我们实际用的就**2种**

在这里插入图片描述

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

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

三、单链表的实现

一般我们写一个项目的时候,一共包含3个文件。

  • 头文件.h:存放该项目所有需要的头文件,结构定义,函数声明。
  • 函数定义文件.c:函数的定义和实现
  • 测试文件.c:测试函数的功能

3.1 定义单链表的结构

一般定义结构前,我们会先重命名该结构的数据类型,因为这个结构可能会存放不止一种数据,可能会存放int、float、double等等,所以为了方便我们代码后期维护,重定义后只需要改变一处就可以了。

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

单个节点包含两个部分:数据域(data)指针域(next)
在这里插入图片描述

  • data:存放结点的数据部分
  • next:存放下一个结点(结构体)的地址
// 单链表结点的定义
typedef struct SListNode	
{
	SListDataType data;			// 数据域
	struct SListNode* next;		// 指针域
}SListNode; 

在这里插入图片描述

这里的意思就是重命名,把sturct SListNode 命名为SListNode,大家可以看我们后面的代码我定义变量的使用,类型是SListNode,正常情况在c语言里是不行,必须要加
关键字struct+结构体名才可以定义,c++里面就不要加关键字。

3.2 创建一个结点

每次插入的时候,我们都需要写很多重复的代码,所以可以把创建节点写成一个函数,后面方便调用。

思路:

  1. 申请一块结点结构体大小的空间
  2. 对他进行判断,是否为NULL
  3. 把数据放到data
  4. next指向NULL
  5. 最后返回申请成功的结点
// 创建一个节点
SListNode* CreateSListNode(SListDataType x)
{
	// 申请一块空间
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));	
	// 判断是否为空
	if (newNode == NULL)
	{
		printf("new node falied!");
		exit(-1);
	}
	// 需要插入的数据放到data里
	newNode->data = x;
	// 该结点的next指向空
	newNode->next = NULL;
	// 返回申请的结点
	return newNode;
}

3.3 尾插

在这里插入图片描述
思路:

  1. 利用assert,防止实参传过来NULL,这是不是说链表地址,而是直接传NULL
  2. 调用刚才创建结点的函数
  3. 判断链表是否为空,分两种情况,1. 链表为空 2. 链表不为空
    为空就直接把创建的结点设为头节点,不为空就找尾,把尾结点的next指向新创建的结点
    在这里插入图片描述
// 尾插
void SListPushBack(SListNode** pphead, SListDataType x)
{	
	// 1.防止传入NULL
	assert(pphead);
	
	// 2.创建一个结点
	SListNode* newNode = CreateSListNode(x);
	// 3.判断是否为链表
	if (*pphead == NULL)
	{
		// 把创建的结点设为头节点
		*pphead = newNode;
	}
	else
	{
		// 找尾
		SListNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newNode;
	}
}

这里大家想必有疑问,尾插函数的形参为什么是二级指针呢?
正常情况的下的尾插

如果尾插函数接收链表指针形参是SListNote *phead,这时候phead是一级指针,tail被赋值成为phead,tail此时指向的是链表的头结点,当链表不为空的时候,tail通过while循环找到链表的尾,将新的结点链接到链表的尾部。

特殊情况下的尾插:

如果尾插函数接收链表地址形参是SListNote *phead,当尾插的链表是空链表的时候,这是newNode就没有变化,还是那个老生常谈的问题,这个赋值只是临时的拷贝而已,大家可以看看我们创建结构体的那段代码,创建next变量的时候类型是指针类型,所以我们创建链表的时候也要用指针,但是你要通过函数来改变普通变量的话,需要传地址(指针)才能改变,如果传过来的指针变量,就需要用指针的指针,就是二级指针

所以我们在测试文件的时,只要对链表的头结点进行修改时,就需要传二级指针

在这里插入图片描述

3.4 打印

尾插完成后,我们可以对插入的数据进行下输出,是否插入成功

思路:用我们创建一个临时变量cur来保存该链表的头部地址,因为我们一般不会利用链表的头部来遍历,头部是我们访问链表的唯一途径,修改后数据不完整。

// 打印
void SListPrint(SListNode* phead)
{
	SListNode* cur = phead;
	while (cur)
	{
		// 打印数据
		printf("%d -> ", cur->data);
		// 迭代往下走
		cur = cur->next;
	}
	printf("NULL\n");
}

注意:这里没有改变链表的头,所以不需要传二级指针
在这里插入图片描述

3.5 头插

在这里插入图片描述

思路:

  1. 调用创建结点函数
  2. 让新节点的next指向链表的头
  3. 把新节点设为头节点
    在这里插入图片描述
// 头插
void SListPushFront(SListNode** pphead, SListDataType x)
{
	// 防止实参传入NULL
	assert(pphead);
	
	SListNode* newNode = CreateSListNode(x);
	newNode->next = *pphead;
	*pphead = newNode;
}

3.5 尾删

在这里插入图片描述

思路:

  1. 空链表不能删除
  2. 删除分为两种情况:
  • 正常情况:有两个以上结点,找尾结点的前一个,把尾结点前一个的next置为NULL
  • 特殊情况:只有一个头结点,对头节点置NULL
  1. 释放该结点的空间。把结点置为NULL,只是他们之间没有连接了,但是原本申请的空间还在,不处理会造成内存泄漏
    在这里插入图片描述
// 尾删
void SListPopBack(SListNode** pphead)
{
	assert(pphead);
	assert(*pphead);	// 空链表不能删除
	
	// 特殊情况:只有一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 正常情况:找尾结点的前一个
		SListNode* tail = *pphead;
		SListNode* tailPrev = NULL;
		while (tail->next)
		{
			tailPrev = tail;
			tail = tail->next;
		}
		tailPrev->next = NULL;
		// 释放空间
		free(tail);
	}
}

这里想必大家有疑问,为什么我free掉没有置NULL,没有置空就会变成野指针,这一般是配套使用啊,有free就要有置空,这是一个好习惯,但是也分情况,因为这里的free是我在函数体内定义的,所以我们对它置空是没有意义的,只是临时变量而已,出了函数体就会被销毁。

3.6 头删

在这里插入图片描述

思路:

  1. 空链表不能删除
  2. 创建一个临时指针变量,用来存储头下一个节点(headNext)
  3. 把头节点释放
  4. 存储头下一个的节点(headNext)设为头节点
    在这里插入图片描述
    头尾相关操作的测试:
    在这里插入图片描述
// 头删
void SListPopFront(SListNode** pphead)
{
	// 空链表不能删除
	assert(*pphead);

	SListNode* headNext = (*pphead)->next;
	free(*pphead);
	*pphead = headNext;
}

3.7 查找

查找返回值分为两种,1.返回结点地址 2.返回结点的下标,我这里演示的返回地址
返回结点地址更方便,可以直接修改的地址和数据,比如配合pos后面插入删除,下标需要遍历,地址的话直接操作。

// 查找,返回节点的地址
SListNode* SListFind(SListNode* phead, SListDataType x)
{	
	assert(phead);

	SListNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

3.8 在pos处插入

在这里插入图片描述

思路:

  1. 调用创建结点函数
  2. 插入分为两种情况
  • 正常情况:找到pos结点的前一个(posPrev),posPrev的next设为新节点,新节点的next设为pos
  • 特殊情况:pos为头结点,等于头插
    在这里插入图片描述
// 在pos处插入
void SListInsert(SListNode** pphead, SListNode* pos, SListDataType x)
{
	SListNode* newNode = CreateSListNode(x);
	// 头插
	if (pos == *pphead)
	{
		newNode->next = *pphead;
		*pphead = newNode;
	}
	else
	{
		// 找pos的前一个结点
		SListNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = newNode;
		newNode->next = pos;
	}
}

3.9 在pos后面插入

思路:

  1. 调用创建结点函数
  2. 新节点的next设为pos的next,pos的next设为新结点,顺序不要乱
    在这里插入图片描述
// 在pos后面插入
void SListInsertAfter(SListNode* pos, SListDataType x)
{
	assert(pos);

	SListNode* newNode = CreateSListNode(x);
	newNode->next = pos->next;
	pos->next = newNode;
}

3.10 在pos处删除

在这里插入图片描述

思路:

  1. 空链表不能删除
  2. 删除分为两种情况
  • 正常情况:找到pos结点的前一个(posPrev),posPrev的next指向pos的next
  • 特殊情况:pos为头结点,等于头删
  1. 释放pos
    在这里插入图片描述
// 在pos处删除
void SListErase(SListNode** pphead, SListNode* pos)
{
	// 特殊情况:头删
	if (pos == *pphead)
	{
		SListNode* headNext = (*pphead)->next;
		free(*pphead);
		*pphead = headNext;
	}
	else
	{
		// 正常情况:找pos的前一个结点
		SListNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
	}
}

3.11 在pos后面删除

思路:

  1. 禁止删除pos后面是NULL的情况,如果是NULL,会造成对空指针的引用(NULL->next)
  2. 存储pos的后一个结点(posNext)
  3. pos的next指向posNext的next(pos后第二个结点)
  4. 释放posNext
    在这里插入图片描述
// 在pos后面删除
// 在pos后面删除
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);	// pos后面禁止是NULL,会造成空指针的引用

	SListNode* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
}

pos处和查找配合相关测试
在这里插入图片描述
在这里插入图片描述

3.12 pos处相关操作的特点

pos处操作:

  • 时间复杂度是O(N):需要找pos的前一个
  • 需要二级指针:可以对头结点修改

pos后面操作:

  • 时间复杂度是O(1):不用找结点。
  • 一级指针:不能对头节点修改

3.13 销毁

这里的销毁不像顺序表,直接free掉链表的地址就可以,因为链表之间是通过指针域连接起来的,这里free掉链表的地址就等于删除了一个节点而已,并且你再也找不到剩下的结点了,我们需要迭代着依次free所有结点,才算销毁成功。

在这里插入图片描述

思路:

  1. 空链表不能销毁
  2. 存储当前节点的下个节点(curNext)
  3. 删除当前节点
  4. 利用下个节点迭代,依次这里操作,直到所有结点删除完毕
  5. 把头结点置为NULL
// 销毁  
void SListDestroy(SListNode** pphead)
{
	assert(pphead);
	// 链表为空不用销毁
	assert(*pphead);
	
	SListNode* cur = *pphead;	
	SListNode* curNext = NULL;	// 存放当前节点的下个节点

	// 迭代销毁
	while (cur)
	{
		curNext = cur->next;
		free(cur);
		cur = curNext;
	}
	*pphead = NULL;
}
该资源包含了几乎所有的数据结构动画视频,帮助我们更好的理解数据结构与算法的编程思路。 目录如下: 'B树的删除.swf', 'B树的生长过程.swf', '三元组表的转置.swf', '中序线索化二叉树.swf', '串的顺序存储.swf', '二分查找.swf', '二叉排序树的删除.swf', '二叉排序树的生成.swf', '二叉树的建立.swf', '克鲁斯卡尔算法构造最小生成树.swf', '冒泡排序.swf', '分块查找.swf', '单链表结点的删除.swf', '单链表结点的插入.swf', '图的深度优先遍历.swf', '基数排序.swf', '堆排序.swf', '头插法建单链表.swf', '寻找中序线索化二叉树指定结点的前驱.swf', '寻找中序线索化二叉树指定结点的后继.swf', '尾插法建表.swf', '希儿排序.swf', '开放定址法建立散列表.swf', '归并排序.swf', '循环队列操作演示.swf', '快速排序.swf', '拉链法创建散列表.swf', '拓扑排序.swf', '最短路径.swf', '朴素串匹配算法过程示意.swf', '构造哈夫曼树的算法模拟.swf', '构造哈夫曼树过程.swf', '栈与递归.swf', '树、森林和二叉树的转换.swf', '桶式排序法.swf', '直接插入排序.swf', '直接选择排序.swf', '邻接表表示的图的广度优先遍历.swf', '邻接表表示的图的深度优先遍历.swf', '顺序查找.swf', '顺序栈(4个存储空间).swf', '顺序栈(8个存储空间).swf', '顺序表的删除运算.swf', '顺序表的插入.swf', '顺序队列操作.swf'。 (注:.swf动画格式可直接使用播放器打开。)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值