【数据结构】3.1单链表(C语言)

【数据结构】——3.1单链表

一、链表的概念

1. 链表的概念及结构

链表(Linked List):物理结构上非连续、非顺序,逻辑结构上顺序存储的一种存储结构。链表中的逻辑顺序是通过指针链接的

单链表结构

  1. 链表在逻辑上是连续的,物理上是非连续的
  2. 结点一般是在堆上申请的,如果在栈上,函数调用结束后就会释放,所有操作都在一个函数内才能使用,显然这是不行的

2. 链表的分类

​ 实际中的链表非常多样,以下情况组合起来就有8种链表结构

  1. 单向或双向

单向和双向链表

  1. 带头或不带头

带头和不带头链表

  1. 循环或非循环

循环和非循环链表

虽然链表结构很多,但是我们实际种最常用的就是两种:无头单向非循环链表带头双向循环链表

二、单链表的概念和结构

1. 单链表的概念

无头单向非循环链表:结构简单的链表,不会用来单独存储数据,实际中是作为其他数据结构的子结构,如哈希桶、图的邻接表等

单链表结构

2. 单链表结构

typedef int SLTDataType;

typedef struct SListNode
{
    SLTDataType data;		//数据
    struct SListNode* next;	//指向下一个结点的指针
} SListNode;

三、单链表的实现

1. 动态申请结点

  1. 必须使用动态申请空间,否则调用结束后栈空间被释放
  2. 空间申请完成后赋值数值,初始化指针NULL
SListNode* BuySListNode(SLTDataType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

2. 遍历打印单链表

​ 遍历单链表,依次打印结点的数据

void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur != NULL)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

3. 插入结点

3.1 头插

  1. 必须先将新结点next指向第一个结点,再将头指针指向新节点
  2. 若是先将头指针指向新结点,则头指针指向的第一个结点的地址就会丢失,造成内存泄漏
void SListPushFront(SListNode** pplist, SLTDataType x)
{
    assert(pplist != NULL);
    
	SListNode* newnode = BuySListNode(x);

	newnode->next = *pplist;
	*pplist = newnode;
}

3.2 尾插

  1. 结构体为空时,改变头指针的指向
  2. 结构体不为空时,改变结构体中next的指向
  3. 尾插之前要遍历链表,找尾
//尾插
void SListPushBack(SListNode** pplist, SLTDataType x)
{
	assert(pplist != NULL);

	SListNode* newnode = BuySListNode(x);
	
	if (*pplist == NULL)	//结构体为空
	{
		*pplist = newnode;
	}
	else	//结构体不为空
	{
		SListNode* cur = *pplist;
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

3.3 插入

  1. 链表的插入默认是前插,要遍历链表找到pos结点的前一个结点,再进行插入
  2. 插入函数的后插很容易实现,函数库里有后插函数,在此不做实现
  3. pos结点要检查是否为空
  4. pos为头结点时,它的next为NULL,循环中再次访问NULL的next时会报错,所以要单独处理,直接调用头插函数即可
  5. 若是pos不在链表内则使用断言中断程序
void SListInsert(SListNode** pplist, SListNode* pos, SLTDataType x)
{
	assert(pplist != NULL && pos != NULL);

	if (*pplist == pos)			//pos为头结点时,作为头插,直接调用函数
	{
		SListPushFront(pplist, x);
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next != pos)
		{
			cur = cur->next;
			assert(cur != NULL);	//pos不在链表中
		}

		SListNode* newnode = BuySListNode(x);
		newnode->next = pos;
		cur->next = newnode;
	}
}

4. 删除结点

4.1 头删

  1. 删除前检查链表是否有数据,链表没有数据时,可以使用断言处理(在这里不做处理)
  2. 释放第一个结点前要先用指针记录第二个结点,否则第一个结点后面的结点都失去指针指引,造成内存泄漏
void SListPopFront(SListNode** pplist)
{
	assert(pplist != NULL);

	if (*pplist == NULL)
	{
		return;
	}

	SListNode* cur = *pplist;
	*pplist = (*pplist)->next;
	free(cur);
	cur = NULL;
}

4.2 尾删

  1. 删除前检查链表是否有数据,链表没有数据时,可以使用断言处理(在这里不做处理)
  2. 删除时要找到删除结点的上一个结点,将上一个结点的next置为NULL
  3. 当只有一个结点时,第一个结点的next为NULL,遍历查找上一个结点时,要对当前结点的next的next进行访问。,访问其next时会报错,所以要检查是否只有一个结点,一个结点时只删除一个结点,将头指针置为NULL
void SListPopBack(SListNode** pplist)
{
	assert(pplist != NULL);

	if (*pplist == NULL)
	{
		return;
	}

	if ((*pplist)->next == NULL)	//只有一个结点
	{
		free(*pplist);
		*pplist = NULL;
	}
	else	//有多个结点
	{
		SListNode* cur = *pplist;
		while (cur->next->next != NULL)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

4.3 删除

  1. 删除第一个结点时,直接释放,头指针置为NULL
  2. 删除其他结点时,遍历找到上一个结点,再删除
void SListErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist != NULL && pos != NULL);

	if (*pplist == NULL)
	{
		return;
	}

	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next != pos)
		{
			cur = cur->next;
			assert(cur != NULL);
		}
		cur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

5. 查找数据

  1. 遍历链表,比较数值,返回值为指定数据的结构体指针
  2. 调用者可以通过调用该函数获取指定数据的结点指针来修改数据,达到修改数据的目的
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	SListNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

6. 销毁单链表

​ 销毁时从前往后释放,释放之前要先用临时变量保存下一个结点的地址,防止内存泄漏(这里使用头指针保存下一个结点地址)

void SListDetroy(SListNode** pplist)
{
	assert(pplist != NULL);

	SListNode* cur = *pplist;
	while (cur != NULL)
	{
		*pplist = cur->next;
		free(cur);
		cur = *pplist;
	}
}

四、单链表的相关问题

1. 单链表实现中的问题

1.1 初始化问题

单链表不需要初始化,单链表为空时头指针为NULL,不需要其他操作

1.2 二级指针问题

  1. 为什么要使用二级指针作为参数传递单链表
  1. 单链表修改头指针时要传入二级指针,因为头指针需要传址调用,一级指针是头指针的拷贝,修改时不改变调用者的一级指针
  2. 在进行插入(头插、尾插、插入) 、 删除(头删、尾删、删除) 和 销毁时需要传入二级指针,因为有链表为空的情况,需要对头指针进行修改
  3. 在进行打印、查询操作时不用传二级指针,不对头指针内容进行修改
  1. 不使用二级指针的解决方案(两种)
  1. 使用带头结点的链表,链表没有元素时头指针指向头结点,不需要对头指针修改
  2. 调用函数时以返回值的形式返回头指针,让头指针在调用时赋值(非常麻烦)

1.3 断言问题

  1. 参数为一级指针不需要断言,单链表为空时头指针为NULL,是合法的情况
  2. 参数为二级指针需要断言,二级指针存储着头指针的地址,若是为NULL,则头指针不存在,是非法情况

五、完整代码

代码保存在gitee中:点击完整代码参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值