【DS】单链表的实现(创建过程详细讲解)

介绍

链表的概念

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

链表图片介绍

  • 也就是说链表是由多个节点来连结而成的,而这些节点是不连续的,需要我们保存好下一个节点的地址,从而达到在逻辑结构上是连续的
  • 就如同图中的的火车一样,D,A,C,E,Z,X,W就是节点,而将火车连接起来就是在当前节点存放下一个节点的地址,这样我们就能顺利找到下一个节点

链表的种类

  • 链表的种类多样,分为:带头或不带头,单向或双向,循环或不循环,组合起来一共有八种链表
  • 这里的带头是指这种链表的第一个节点不存储有效数据,只存储节点的地址,该节点也常称为哨兵位
    链表组合介绍

链表种类介绍

  • 虽然链表种类多样,但是我们用的最多的还是:不带头单向非循环链表(也就是我们常说的单链表)和带头双向循环链表

常用链表

  • 对链表种类有基本了解后,我们对单链表和带头双向循环链表进行实现

单链表的实现

链表作为一种数据结构,要对数据进行管理,那就肯定少不了增删查改这些操作。因此对于链表的实现也就是对以上四种功能的实现

增删查改

单链表节点结构定义

  • 单链表节点结构简单,一个是我们需要管理的数据,另一个就是保存下一个节点的指针
  • 这数据的类型我们先用typedef重命名一下,测试时我们用整型来测试,这样容易理解,等单链表实现之后我们只要将int改成我们需要的类型即可,达到一键替换的效果
typedef int SLDataType;

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

打印数据

  • 为了方便我们观察数据,我们先实现一个打印数据的函数
  • 这个函数简单,只需要遍历链表并将数据打印即可
void SListPrint(SList** pphead)
{
	assert(pphead);
	SList* pcur = *pphead;
	while (pcur)//当链表为空即完成打印,停止循环
	{
		printf("%d->", pcur->data);//打印
		pcur = pcur->next;//迭代
	}
}

插入数据

  • 增加数据一般有两种方式:头插,尾插
  • 由于需要经常申请节点,所以先封装一个ListBuyNode函数,方便申请新节点

头插

  • 头插即将数据插入到链表的头部。实现这个功能时,我们可能会写成下图这样,但跑起来却不是我们想要的结果,这是为什么呢?
    头插错误演示

  • 我们仔细观察可以发现,我们将list传给SLPushFront这个函数,用SList*phead接收。这里涉及了指针相关的内容,细心的这时便能看出这是传值调用,但是传值调用是不能改变list的啊,形参只是实参的临时拷贝,也就是phead的改变并不会改变list,而在栈区创建的phead一出SLPushFront这个函数便会销毁,那在堆区malloc出来的节点也就找不到了,无法将节点连接起来
    连接失败

  • 所以要想改变list加需要将list的地址传过去,通过传址调用这样就能改变list,但这里的list已经是指针了,传指针的地址就需要用二级指针接收了
    头插

  • 通过传址调用,链表已经头插成功了

  • 头插的操作也十分简单,只需要将申请的新节点与原本的头节点连接,再将新节点置为新的头节点即可

尾插

  • 尾插即在链表尾部插入数据
    尾插
  • 有了前车之鉴,我们增加一个assert断言,来防止我们没有传址调用导致错误
  • 尾插需要注意的是当链表为空时,需要单独处理,操作和头插一致。当链表有数据的时候,只需要用pcur->next!=NULL来找到尾节点,再将新节点连接即可。
  • 这里的SList*pcur=*pphead是当链表不为空的时候只需要使用一级指针即可。因为当链表不为空的时候,尾插新节点需要改动的是结构体里面的成员,而不是list
    尾插一级指针
  • 如上图所示,在有头节点后进行尾插,我们不传list的地址过去也能完成尾插。虽然这种方式也可以尾插,但是不能这样写,因为当链表为空时直接尾插就会出现和头插传值调用一样的问题。
  • 所以下面代码才是正确的写法
void SLPushBack(SList** pphead, SLDataType x)
{
	assert(pphead);
	SList* node = ListBuyNode(x);
	if (*pphead == NULL)//说明还没有节点
	{
		*pphead = node;
		return;//这种情况无需以下操作
	}
	SList* pcur = *pphead;
	while (pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = node;
	
}

传二级指针的原因

  • 上面的操作一会一级指针,一会二级指针,让人难免犯糊涂,因此我们详细讲解一下
    二级指针解释
    一二级指针使用

  • 我们只有在改变list这个指针的时候才需要二级指针,改变链表节点成员中next的指向只需要一级指针就可以

删除数据

  • 和插入数据一样,删除操作也有头删尾删

尾删

  • 尾删即删除链表中最后一个节点。
void SLPopBack(SList** pphead)
{
	assert(pphead);//防止传值调用
	assert(*pphead);//检查链表是否为空

	if ((*pphead)->next == NULL)//只有一个的情况
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	SList* pcur = *pphead;
	SList* prev = NULL;//尾节点的前一个节点
	while (pcur->next)
	{
		prev = pcur;
		pcur = pcur->next;
	}
	free(prev->next);
	prev->next = NULL;

}
  • 尾删操作需要保证链表不为空,使用assert断言即可。
  • 尾删释放节点需要分辨是否只有一个节点。free(prev->next)的操作不适用只有一个节点情况
  • 尾删的思路就是找到尾节点的前一个节点prev,再将尾节点free掉,若直接释放尾节点,会导致前一个节点的next指针变为野指针

头删

  • 头删即将头节点释放
void SLPopFront(SList** pphead)
{
	assert(pphead);
	assert(*pphead);
	SList* newhead = NULL;
	newhead = (*pphead)->next;
	free(*pphead);
	*pphead = newhead;
}
  • 头删也需要保证链表不为空,先用newhead保存新的头节点,再释放头节点,最后将*pphead更新。

任意位置插入数据

  • 有了前面的头插尾插,还可以继续丰富一下插入功能.该功能需要查找指定数据,所以封装一个查找函数SLFind
  • 这个函数的返回类型为SList*,会返回一个地址
  • 该函数只是为了测试当前数据类型为整型而写的,当类型替换成结构体之后这个函数是不能使用的,需要重新写,但逻辑是一样的
SList* SLFind(SList** pphead, SLDataType find)
{
	assert(pphead);
	assert(*pphead);
	SList* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == find)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;

}
  • 查找指定数据需遍历链表,一一比对数据即可

任意位置之前插入数据

void SLInserBefore(SList** pphead, SList* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	SList* node = ListBuyNode(x);
	if (*pphead == pos)//第一个结点之前
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}
	SList* pcur = *pphead;
	while (pcur->next!=pos)
	{
		pcur = pcur->next;
	}
	node->next = pos;
	pcur->next = node;

}
  • 任意位置之前插入数据需要查看是否是第一个节点之前,这样就变成了头插
  • 若不是第一个节点之前,只需用pcur->next!=pos找到指定节点的前一个节点,再将三个节点连接

任意位置之后插入数据

  • 本质就是尾插
void SLInserAfter(SList** pphead, SList* pos, SLDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SList* node = ListBuyNode(x);
	SList* pcur = *pphead;
	while (pcur != pos)
	{
		pcur = pcur->next;
	}
	node->next = pos->next;
	pos->next = node;

}
  • 需要注意连接的顺序,要将新节点与指定节点的后一节点先连接,再将指定节点与新节点连接,否则将找不到指定节点的下一个节点

删除指定数据

void SLErase(SList** pphead, SList* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (*pphead == pos)
	{
		*pphead = (*pphead)->next;
		free(pos);
		pos = NULL;
		return;
	}
	SList* pcur = *pphead;
	while (pcur->next != pos)//找前一个节点
	{
		pcur = pcur->next;
	}
	pcur->next = pos->next;
	free(pos);
}
  • 该删除需要判断是否为头节点,是的话就是头删操作,否则就需要找到该节点的前一个节点,将要删除节点的前后节点连接起来

销毁链表

void SLDestroy(SList** pphead)
{
	assert(pphead);
	assert(*pphead);
	SList* pcur = *pphead;
	while (pcur)
	{
		SList* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	*pphead = NULL;
}
  • 由于链表节点是在堆区开辟的,为防止内存泄漏,需要及时释放。
  • 用next指针保存下一个节点,再释放当前节点,不断循环直到释放完,最后要将链表list(*pphead)置空

单链表完整代码

  • 完整代码分为三个文件
    SList.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>

typedef int SLDataType;

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

SList* ListBuyNode(SLDataType x);//节点申请

void SLPushBack(SList** pphead, SLDataType x);//尾插
void SLPushFront(SList** pphead, SLDataType x);//头插

void SLPopBack(SList** pphead);//尾删
void SLPopFront(SList** pphead);//头删

void SLInserBefore(SList** pphead, SList* pos, SLDataType x);//任意位置之前插入
void SLInserAfter(SList** pphead, SList* pos, SLDataType x);//任意位置之后插入

SList* SLFind(SList** pphead, SLDataType find);//数据查找
void SLErase(SList** pphead, SList* pos);//删除指定数据

void SLDestroy(SList** pphead);//销毁链表

SList.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include"SList.h"

void SListPrint(SList** pphead)
{
	assert(pphead);
	SList* pcur = *pphead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL");
}

SList* ListBuyNode(SLDataType x)
{
	SList* node = (SList*)malloc(sizeof(SList));
	if (node == NULL)
	{
		perror("malloc");
		return;
	}
	node->data = x;
	node->next = NULL;

	return node;

}


void SLPushBack(SList** pphead, SLDataType x)
{
	assert(pphead);
	SList* node = ListBuyNode(x);
	if (*pphead == NULL)//说明还没有节点
	{
		*pphead = node;
		return;//这种情况无需以下操作
	}
	SList* pcur = *pphead;
	while (pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = node;
	
}
void SLPushFront(SList** pphead, SLDataType x)
{
	assert(pphead);
	SList* node = ListBuyNode(x);
	node->next = *pphead;
	*pphead = node;

}

void SLPopBack(SList** pphead)
{
	assert(pphead);
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	SList* pcur = *pphead;
	SList* prev = NULL;
	while (pcur->next)
	{
		prev = pcur;
		pcur = pcur->next;
	}
	free(prev->next);
	prev->next = NULL;

}
void SLPopFront(SList** pphead)
{
	assert(pphead);
	assert(*pphead);
	SList* newhead = NULL;
	newhead = (*pphead)->next;
	free(*pphead);
	*pphead = newhead;
}

SList* SLFind(SList** pphead, SLDataType find)
{
	assert(pphead);
	assert(*pphead);
	SList* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == find)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;

}

void SLInserBefore(SList** pphead, SList* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	SList* node = ListBuyNode(x);
	if (*pphead == pos)//第一个结点之前
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}
	SList* pcur = *pphead;
	while (pcur->next!=pos)
	{
		pcur = pcur->next;
	}
	node->next = pos;
	pcur->next = node;

}
void SLInserAfter(SList** pphead, SList* pos, SLDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SList* node = ListBuyNode(x);
	SList* pcur = *pphead;
	while (pcur != pos)
	{
		pcur = pcur->next;
	}
	node->next = pos->next;
	pos->next = node;

}

void SLErase(SList** pphead, SList* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (*pphead == pos)
	{
		*pphead = (*pphead)->next;
		free(pos);
		pos = NULL;
		return;
	}
	SList* pcur = *pphead;
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	pcur->next = pos->next;
	free(pos);
}

void SLDestroy(SList** pphead)
{
	assert(pphead);
	assert(*pphead);
	SList* pcur = *pphead;
	while (pcur)
	{
		SList* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	*pphead = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include"SList.h"

void SListTest()
{
	SList* list = { 0 };//直接初始化
	//SLPushBack(&list, 1);
	//SLPushBack(&list, 2);
	//SLPushBack(&list, 3);
	//SLPushBack(&list, 4);
	/*SLPopBack(&list);
	SLPopBack(&list);
	SLPopFront(&list);
	SLPopFront(&list);*/
	//SList* find = SLFind(&list, 1);
	//SLInserBefore(&list, find, 0);
	//SLInserAfter(&list, find, 5);
	//SLErase(&list, find);
	

	SListPrint(&list);
	SLDestroy(&list);
}


int main()
{
	SListTest();

	return 0;
}

至此,单链表的实现就完成了,其难点主要有对传址调用,二级指针的理解。
下面是对带头双向循环链表的实现

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值