• 【C语言】链表详解(无头单向非循环)


    📒博客主页:要早起的杨同学的博客
    🎉欢迎关注🔎点赞👍收藏⭐️留言📝
    📌本文所属专栏:【数据结构
    ✉️坚持和努力从早起开始!
    💬参考在线编程网站:🌐牛客网🌐力扣
    🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!

    在这里插入图片描述
    学习链表之前,建议先学习下顺序表哦:

    【C语言】详解顺序表(SeqList)

    一. 链表介绍

    1.1 为什么引入链表

    • 学习链表之前,先让我们来思考一个问题:

    为什么有了顺序表,还需要有链表这样的数据结构呢?

    • 顺序表存在的一些问题:
    1. 顺序表在中间/头部的插入删除,要挪动很多数据,时间复杂度为O(N),效率太低了。
    2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
    3. 增容一般是一次增长2倍,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
      再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
    • 为了更好的解决上述问题,引入了链表。

    1.2 链表的概念及结构

    • 概念

    前面学习的顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,而链表是一种物理存储结构上不连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,可以实现更加灵活的动态内存管理。

    • 链表的组成

    链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

    每个结点包括两个部分:

    1、数据域:存储数据元素

    2、指针域:存储下一个结点地址

    • 链表的物理结构

    可以看到,4个节点的地址并不是连续的,链表在物理结构上不一定是线性的,而在逻辑结构上是线性的

    image-20210824110616542

    • 链表的逻辑结构(想象出来的)

    image-20210822225322883

    • 注意

    1、链式结构在逻辑上是连续的,但在物理上不一定连续

    2、链表的节点是在堆上申请出来的

    1.3 链表的分类

    链表的结构非常多样化

    • 单向、双向

    image-20210827235931988

    • 带头结点、不带头节点(哨兵位的头节点,不存储有效数据)

    image-20210828002001494

    • 非循环、循环

    image-20210827235906305

    • 常用的两种结构

    image-20210828000023080

    二. 无头单向非循环链表的实现

    首先新建一个工程( 博主使用的是 VS2019 )

    • SList.h(单链表的类型定义、接口函数声明、引用的头文件)
    • SList.c(单链表接口函数的实现)
    • Test.c(主函数、测试顺序表各个接口功能)

    如图:

    image-20210824114457095


    2.1 单链表的定义

    image-20210831212953194

    typedef int SLTDataType;
    
    //定义单链表节点
    typedef struct SListNode
    {
    	SLTDataType data;        //数据域
    	struct SListNode* next;  //指针域
    }SListNode;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.2 动态申请一个节点

    //动态申请一个节点
    SListNode* BuySListNode(SLTDataType x)
    {
    	SListNode* node = (SListNode*)malloc(sizeof(SListNode));
    	if (node == NULL)  //检查是否开辟成功
    	{
    		perror("malloc");
    		return;
    	}
    	node->data = x;
    	node->next = NULL;
    
    	return node;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.3销毁(释放)所有节点

    //销毁单链表中所有节点
    void SListDestory(SListNode** pphead)
    {
    	assert(pphead);
    
    	SListNode* cur = *pphead;
    	while (cur != NULL)  //遍历链表
    	{
    		SListNode* next = cur->next;  //保存cur的下一个节点
    		free(cur);  //释放节点
    		cur = next;
    	}
    	*pphead = NULL;  //z
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 测试其功能

    销毁单链表中所有节点,指向头节点的指针置空


    2.4 打印单链表

    //打印单链表
    void SListPrint(SListNode* phead)
    {
    	SListNode* ptr = phead;
    	while (ptr != NULL)
    	{
    		printf("%d->", ptr->data);
    		ptr = ptr->next;
    	}
    	printf("NULL\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.5 单链表尾插

    • 先来看一种错误写法:这个也是弄懂单链表的关键

    传一级指针的值,用一级指针接收

    image-20210824160321570

    • 这种写法会导致一个问题:

    因为当链表为空时,我们需要改变 plist 的指向,使其指向第一个节点。

    而初始 plist 和 phead 都指向 NULL,调用函数后,phead 指向了新的节点,而 plist 还是指向 NULL 的。

    image-20210824161326431

    • 如何解决呢:

    plist 是指向第一个节点的指针,想要在函数中改变 plist 的值(指向),必须要把 plist指针的地址 作为实参传过去,形参用 二级指针 接收,这样在函数中对二级指针解引用得到 plist 的值,就可以改变 plist 的值(指向)了

    记住:在函数里面要改变 int,则要传 int* ,要改变 int* ,则要传 int**

    • 注意区分开这几个代表的意思

    平时一般不用解引用两层,没啥意义,一般是解引用一层,来改变外面 plist 的指向。

    image-20210824234548430

    • 正确写法:通过二级指针改变外面 plist 的指向

    单链表为空时,plist 直接指向新节点;

    单链表不为空时,先找到单链表的尾节点,然后将尾节点的next指向新节点

    //单链表尾插
    void SListPushBack(SListNode** pphead, SLTDataType x)
    {
    	assert(pphead);  //检查参数是否传错
    
    	SListNode* newnode = BuySListNode(x);  //动态申请一个节点
    
    	if (*pphead == NULL)  //单链表中没有节点时
    	{
    		*pphead = newnode;  //plist指向新节点
    	}
    	else if (*pphead != NULL)  //单链表中已经有节点时
    	{
    		SListNode* tail = *pphead;
    		while (tail->next != NULL)  //找到单链表中的最后一个节点
    		{
    			tail = tail->next;
    		}
    		tail->next = newnode;  //最后一个节点的next指向新节点
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 测试其功能

    image-20210824163618525


    2.6 单链表头插

    • 图解头插操作

    image-20210824230705865

    • 代码如下

    因为要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址

    //单链表头插
    void SListPushFront(SListNode** pphead, SLTDataType x)
    {
    	assert(pphead);  //检查参数是否传错
    
    	SListNode* newnode = BuySListNode(x);  //动态申请一个节点
    
    	newnode->next = *pphead;  //新节点的next指针指向plist指向的位置
    	*pphead = newnode;  //plist指向头插的新节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 测试其功能

    image-20210824210318844


    2.7 单链表尾删

    • 图解尾删操作

    image-20210824224625868

    • 代码如下

    单链表只有一个节点时,删除节点,plist 指向 NULL;

    单链表有多个节点时,先找到单链表尾节点的上一个节点,删除尾节点,然后将该节点的next指向 NULL;

    因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。

    //单链表尾删
    void SListPopBack(SListNode** pphead)
    {
    	assert(pphead);   //检查参数是否传错
    	assert(*pphead);  //断言,链表不能为空
    
    	SListNode* tail = *pphead;
    
    	if ((*pphead)->next == NULL)  //链表只有一个节点
    	{
    		free(*pphead);   //删除节点
    		*pphead = NULL;  //plist置空
    	}
    	else  //链表中有多个节点
    	{
    		while (tail->next->next != NULL)  //找到链表的尾节点的上一个节点
    		{
    			tail = tail->next;
    		}
    		free(tail->next);   //删除尾节点
    		tail->next = NULL;  //置空
            
            /*思路2:
            SListNode* prev = *pphead;
    		while (tail->next)  //找到链表的尾节点和它的上一个节点
    		{
    			prev = tail;
    			tail = tail->next;
    		}
    		free(tail);         //删除尾节点
    		prev->next = NULL;  //置空
    		*/
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 测试其功能

    image-20210824224133868


    2.8 单链表头删

    • 图解头删操作

    image-20210825001225011

    • 代码如下

    因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。

    //单链表头删
    void SListPopFront(SListNode** pphead)
    {
    	assert(pphead);   //检查参数是否传错
    	assert(*pphead);  //链表不能为空
    
    	SListNode* cur = *pphead;  //保存头节点的地址
    
    	*pphead = cur->next;  //plist指向头节点的下一个节点
    	free(cur);  //删除头节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 测试其功能

    image-20210827153856471


    2.9 在单链表中查找指定值的节点

    如果查找到,返回该节点的地址;没有查找到,返回 NULL。

    //在单链表中查找指定值节点
    SListNode* SListFind(SListNode* phead, SLTDataType x)
    {
    	SListNode* cur = phead;
    	//遍历链表
    	while (cur != NULL)
    	{
    		if (cur->data == x)
    		{
    			return cur;  //找到了,返回该节点的地址
    		}
    		cur = cur->next;
    	}
    	return NULL;  //未找到,返回NULL
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.10 单链表在pos位置之后插入

    • 图解插入操作

    image-20210827234957091

    • 分析思考为什么不在pos位置之前插入?

    单链表不适合在pos位置之前插入,因为需要遍历链表找到pos位置的前一个节点,

    单链表更适合在pos位置之后插入,如果在后面插入,只需要知道pos位置就行,会简单很多

    C++官方库里面单链表给的也是在之后插入

    • 代码如下
    //单链表在指定pos位置之后插入
    void SListInsertAfter(SListNode* pos, SLTDataType x)
    {
    	assert(pos);  //给的pos位置不能为空
    
    	SListNode* newnode = BuySListNode(x);  //动态申请一个节点
    
    	newnode->next = pos->next; //新节点的next指针指向pos位置后一个节点
    	pos->next = newnode;       //pos位置的next指向新节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 测试其功能

    先调用 SListFind 函数在单链表中查找指定值的节点,查找到了,返回该节点的地址,也即是我们指定的 pos 位置,然后将其传给 SListInsertAfter 函数。


    2.11 单链表删除指定pos位置的节点

    • 图解删除操作

    image-20210827235619437

    • 代码如下

    要考虑到两种情况:pos位置为单链表的第一个节点,pos位置为中间节点;

    因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。

    //单链表删除指定pos位置的节点
    void SListErase(SListNode** pphead, SListNode* pos)
    {
    	assert(pphead);
    	assert(*pphead); //链表不能为空
    	assert(pos);     //给的pos位置不能为空
    
    	//pos位置为第一个节点,相当于头删
    	if (pos == *pphead)
    	{
    		SListPopFront(pphead);
    	}
    	//pos位置为中间节点
    	else
    	{
    		SListNode* prev = *pphead;
    		while (prev->next != pos)  //找到pos位置的前一个节点
    		{
    			prev = prev->next;
    		}
    		prev->next = pos->next;  //pos位置的前一个节点指向pos位置的后一个节点
    		free(pos);  //释放pos节点
    		pos = NULL; //置空
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 补充一下,assert 断言

    assert 放在函数里面检查参数,一方面是为了安全,另一方面也是为了防止有人调用该函数时,不正确的使用,错误传入参数,好及时提醒到他,写代码时一定要考虑到有人不正确的使用该函数时的场景,来进行避免

    • 测试其功能

    先调用 SListFind 函数在单链表中查找指定值的节点,查找到了,返回该节点的地址,也即是我们指定的 pos 位置,然后将其传给 SListErase 函数。


    2.12 单链表删除指定pos位置之后的节点

    • 图解删除操作

    image-20210827161734966

    • 代码如下
    //单链表删除指定pos位置之后的节点
    void SListEraseAfter(SListNode* pos)
    {
    	assert(pos);       //给的pos位置不能为空
    	assert(pos->next); //给的pos位置不能是尾节点
    
    	SListNode* posnext = pos->next;  //保存pos位置的后一个节点
    	pos->next = pos->next->next;
    	free(posnext);  //释放pos位置的后一个节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 测试其功能

    image-20210827162051063


    2.13 求单链表长度

    //求单链表长度
    int SListSize(SListNode* phead)
    {
    	int size = 0;
    	SListNode* cur = phead;
    	while (cur != NULL)  //遍历链表
    	{
    		size++;
    		cur = cur->next;
    	}
    	return size;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.14 判断单链表是否为空

    plist为空,返回 1 (true),非空,返回 0 (false)

    //单链表判空
    bool SListEmpty(SListNode* phead)
    {
    	//plist为空,返回1(true),非空,返回0(false)
    	return phead == NULL;
    	
    	/*写法2:
    	return phead == NULL ? true : false;
    	*/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    【AWS】在EC2上创建root用户,并使用root用户登录
    Go语言常用命令详解(三)
    【数据库迁移系列】从Oracle迁移到openGauss实战分享
    SAP oracle 复制新实例后数据库远程连接报错 ora-01031
    ping no reply
    中国钛合金自行车出口海外营销策略-大舍传媒
    umi项目本地开发环境远程打开的问题
    egg-jwt的使用
    多表操作(外键)
    25 Python的collections模块
  • 原文地址:https://blog.csdn.net/Y673789476/article/details/126057977