链表(一)----关于单链表的一切细节这里都有

一.链表

1 链表的概念及结构

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

  • 现实中的链表结构
    链表(一)----关于单链表的一切细节这里都有_第1张图片

  • 数据结构中的链表结构
    在这里插入图片描述

1.链式结构在逻辑上是连续的,但在物理上不一定是连续的。
2.现实中的节点一般是在堆上申请出来的。
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,可能不连续。


链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
链表(一)----关于单链表的一切细节这里都有_第2张图片

二.实现单向链表

记住这个图,一会链表的逻辑会用到
链表(一)----关于单链表的一切细节这里都有_第3张图片

我们创建三个文件:
头文件LList.h用于调用库函数、声明结构体和函数。
源文件LList.c存储函数。
源文件text.c进行测试。
每个源文件都必须包含LList.h。


1.声明链表结构体

//以下声明在头文件LList.h当中
#include 
#include 
#include 
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType val;
	struct SListNode* next;
}SLNode;

2.打印链表

声明

void SLTPrint(SLNode* phead);

SList.c

void SLTPrint(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur != NULL)
	 {
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

3.创建新节点

当我们进行插入节点等一系列操作时,都需要创建新的节点,用到这个函数

声明

SLNode* SLCreateNode(SLNDataType x);

SList.c

 SLNode* CreateNode(SLNDataType x)
 {
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode = NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
 }
  • 为新节点开辟空间,返回值为新节点的地址,所以函数类型为 SLNode* 结构体指针类型。
  • malloc函数为newnode开辟结构体大小个字节。
  • 判断是否开辟成功,失败则打印错误信息,结束函数运行。
  • 将新节点的数据val赋值为传入参数 x。
  • next赋值为空。

4.尾插

链表(一)----关于单链表的一切细节这里都有_第4张图片
如果参数为一级指针,尾插传值只是临时拷贝,必须传地址

  • 声明
void SLTPushBack(SLNode** phead, SLNDataType x);

对结构体指针修改,要传地址,用二级指针接收
我们第一反应可能想当然敲出这样的经典错误:
链表(一)----关于单链表的一切细节这里都有_第5张图片

  • 尾插的本质是上一个节点存下一个节点的地址
  • 而这里的tail是局部变量,出了作用域还会销毁,存在newnode内存泄漏的问题
  • tail和新节点确实在此刻取得了联系,但是并没有和上一个节点链接起来哦

  • SList.c
void SLTPushBack (SLNode ** pphead ,SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
   //没有节点的情况
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else//有节点的情况
	{
		//找尾
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
  • assert判断传入头节点指针的地址是否合法,为空则报错。
  • 为新节点newnode开辟空间并将x储存其中。
  • 插入时分两种情况:空链表 非空链表
  1. 如果链表为空则直接将*pphead 指向新节点 newnode,使其成为新的链表的头节点。
  2. 如果链表不为空,则创建变量tail指向头结点,循环遍历链表使tail指向尾节点,将新节点地址赋值给tail的next,成功将新节点添加到链表尾部。
    链表(一)----关于单链表的一切细节这里都有_第6张图片
    plist、pphead和newnode的新空间关系如下
  • plist和phead都是在函数栈帧里面开辟
  • newnode是借助于malloc在堆上开辟的,是一个结构体指针,在开辟时数值域上放的其实就是尾插函数的第二个参数
    链表(一)----关于单链表的一切细节这里都有_第7张图片

5.头插

  • 声明
void SLTPushFront(SLNode** pphead, SLNDataType x);
  • SList.c
void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
  • 注意:头插的这段代码对于没有节点的情况,既(*pphead)为空的情况也适用,所以不需要分类讨论
  • 创造出新节点的指针后,赋给newnode,此时newnode的数值域存放的也就是头插函数的第二个参数x,指针域存放的是NULL
  • 紧接着将*pphead,也就是plist,也就是原来第一个第一个节点的地址赋给newnode,这样,newnode就和原链表的节点取得了联系
  • 最后将newnode地址赋给*pphead,链表就顺利头插了!
    单链表的头插是非常方便的,这也是一个单链表的优点

6.尾删

  • 声明
void SLTPopBack(SLNode** pphead);
  • 当节点数量大于1的时候,用一级指针也可以,而头删的参数选择二级指针是因为当节点数只剩一个的时候,既是头也是尾,删除后要将*pphead置空,这样考虑的话就要对指针进行修改,所以索性传成二级指针了。

分析一波:

  • 尾删注意:

  • 在删除前必须保存一下即将删除节点的地址,这样的话才能free掉对应的内存空间,避免内存泄露的问题出现,所以我们还需要定义一个结构体指针prev,用来记录即将删除节点的地址

  • 当只有一个节点直接释放掉即可

  • 逻辑图如下:

链表(一)----关于单链表的一切细节这里都有_第8张图片

  • 只有一个节点时,对应的代码这样写正确吗?
    链表(一)----关于单链表的一切细节这里都有_第9张图片

这段代码的问题在于当只有一个节点的时候,prev和tail指向的是同一块空间,free掉tail之后,prev就变成了野指针
一定要理解free掉的是指针指向的内存空间,并不是把指针销毁了
而perv->next相当于对野指针访问了,所以是存在问题的

还存在一个问题,当节点都被删除完后,只剩一个NULL,如果继续删除,此时*pphead就为空,所以在删除前要对指针进行检查(断言 或者 if判断后提前return)。

  • SList.c
void SLTPopBack(SLNode** pphead)
{
	assert(*pphead);
	//1.一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else//一个以上的节点
	{
		//找尾
		SLNode* prev = NULL;
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;//可有可无,因为出了tail作用域,tail也会自动销毁
		prev->next = NULL;//必须置空,否则内存泄漏
	}
}

或者也可以这样写(只有else后的部分修改了)

void SLTPopBack(SLNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}
  • 逻辑图如下:
    链表(一)----关于单链表的一切细节这里都有_第10张图片

  • assert判断链表头节点是否为空,为空则报错。

  • 链表只有一个节点时,直接释放头节点空间,然后置空。

  • 链表有多个节点时,通过循环使变量 tail->next 找到尾节点,然后释放tail后一个节点的空间,也就是尾节点的空间,同时将其置空。


7.头删

  • 声明
void SLTPopFront(SLNode** pphead);

直接free掉头节点可以吗?

不行,当存在多个节点时,如果直接free第一个,后续的所有链表都访问不到了,内存也就随之泄漏了

先看看这个代码错哪里了?

链表(一)----关于单链表的一切细节这里都有_第11张图片

链表(一)----关于单链表的一切细节这里都有_第12张图片

tmp和*pphead指向的是同一块空间,free(tmp)后,*pphead成为了野指针
不要误认为free 掉 tmp后对 * pphead没有影响
但是上述代码的后两行换一下位置就对了

  • SList.c
void SLTPopFront(SLNode** pphead)
{
	assert(*pphead);
	
	SLNode* tmp = *pphead;
	
	*pphead = (*pphead)->next;
	
	free(tmp);
}

8.查找元素

  • 声明
SLNode* SLTFind( SLNode*phead,SLNDataType x);

查找不需要修改指针,传一级指针就可以了,遍历链表即可

  • SList.c
SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}
  • 函数在单链表中查找包含特定数据值 x 的节点。
  • 变量cur通过循环找到数据 val 等于x的节点。
  • 找到则返回指向当前节点的指针 cur,否则返回值为空。

9.在pos位置前插入一个元素

这个操作是单链表的一个劣势,因为单链表不支持随机访问,找下一个节点方便,但上一个节点并不好找

  • 声明
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)

如果pos为第一个节点,相当于头插
直接调用头插函数 SLTPushFront(SLNode** pphead, SLNDataType x);

分析:

  • 需要找到pos的前一个节点地址,也就是pos前一个节点的指针,保存赋给prev,
  • 再在堆区上创建新的节点,将prev的指针域赋成新节点的地址,再将新节点的指针域赋成pos的地址,pos地址不需要提前保存,因为pos地址是参数提供好的。
    逻辑图如下:
    链表(一)----关于单链表的一切细节这里都有_第13张图片
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	// 严格限定pos一定是链表里面的一个有效节点
	assert(pphead);
	assert(pos);
	assert(*pphead);

	if (*pphead == pos)
	{
		// 头插
		SLTPushFront(pphead, x);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SLNode* newnode = CreateNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。

  • 第二个assert判断传入指向链表中某个节点的指针pos是否合法,不存在则报错。

  • 如果在头节点位置之前插入,则调用头插解决。

  • 如果不是头节点位置,则创建一个指向链表头节点的指针 prev,然后使用循环找到要插入位置 pos 前面的节点。

  • 创建一个新的节点 newnode 并将数据值 x 存储在其中。

  • 修改 prev 节点的 next 指针,使其指向新节点 newnode,从而将新节点插入到 pos 前面


10.指定位置之后插入

  • 声明
void SLTInsertAfter(SLNode** pphead, SLNode* pos, SLNDataType x);

逻辑图如下:

链表(一)----关于单链表的一切细节这里都有_第14张图片

  • SList.c
void SLInsertAfter(SLNode* pos, SLNDataType x)
{
	assert(pos);
	SLNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
  • 先创建好新的节点newnode
  • 将newnode的指针域赋成下一节点的地址,也就是原来pos的指针域
  • 将pos指针域赋值为newnode地址,完美插入。

11.删除pos位置的值

  • 声明
void SLTErase(SLNode** pphead, SLNode* pos);

逻辑图如下
链表(一)----关于单链表的一切细节这里都有_第15张图片

  • SList.c
void SLTErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (*pphead == pos)
	{
		// 头删
		SLTPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

分析:

  • 第一个assert判断传入头节点指针的地址是否合法,为空则报错。
  • 第二个assert判断链表头节点是否为空,为空则报错。
  • pos节点为头节点,则调用头删解决。
  • pos不为头节点,则创建变量prev指向头节点,通过循环找到pos节点的前一个节点。
  • 将prev的next指向要删除的pos节点的下一个节点。
  • 释放pos空间

12.删除pos位置之后的值

  • 声明
void SLTEraseAfter(SLNode* pos);

逻辑图如下:
链表(一)----关于单链表的一切细节这里都有_第16张图片

  • Slist.c
void SLTEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLNode* tmp = pos->next;
	pos->next = pos->next->next;
-
	free(tmp);
	tmp = NULL;
}

13.销毁链表

  • 声明
void SLTDestroy(SLNode** pphead);

逻辑图如下:

链表(一)----关于单链表的一切细节这里都有_第17张图片

  • SList.c
void SLTDestroy(SLNode** pphead)
{
	assert(pphead);

	SLNode* cur = *pphead;
	while (cur)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}
  • 定义了两个指针,cur 和 tmp,用于遍历链表并释放内存。开始时,cur 被初始化为链表的头节点指针 pphead。

  • 这是一个循环,它会一直执行,直到 cur 变为 NULL,遍历到链表的末尾。

  • 在循环中,首先将 cur 赋值给 tmp,以便稍后释放 cur 指向的节点的内存。
    然后,将 cur 移动到下一个节点,即 cur = cur->next;

  • 最后,使用 free 函数释放 tmp 指向的节点的内存,即释放链表中的一个节点,接着进行循环依次释放节点直到链表最后。

所有完整版已经上传至我的gitte账户,

链接在这:gitee单链表

你可能感兴趣的:(数据结构,链表,数据结构,开发语言,c语言)