C语言之数据结构初见篇(6):单链表的介绍(2)

目录

一:核心操作及代码要点

1.为创建新的节点申请空间

2.尾插

空链表情况

非空链表情况

3.头插

4.尾删

5.头删

6.查找


一:核心操作及代码要点

在上一篇文章中,我们简单的介绍了部分单链表的节点创建的内容与书写,这里我会把后面的全部讲解完,这里我们利用尾插和头插来进行创建节点,而不是我们之前那样手动创建的四个节点

1.为创建新的节点申请空间

代码如下:

由于我们在插入或者删除某一个数据的时候,我们都先必须确保数据的节点空间是足够的,所以这里我们必须先为创建新的节点申请新的空间位置

cpp 复制代码
//为创建新的节点申请空间
SLTNode* SLTBuyNode(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

解析:

代码整体功能

这个函数封装了创建新节点的所有操作:

  1. 申请内存空间

  2. 检查是否申请成功

  3. 初始化节点的数据域和指针域

  4. 返回创建好的节点指针

第1行:函数头

cpp 复制代码
SLTNode* SLTBuyNode(SLDataType x)
  • 返回值SLTNode* 返回创建好的节点指针

  • 参数SLDataType x 要存入节点的数据

  • 作用:创建一个值为x的新节点

第2行:申请内存

cpp 复制代码
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  • malloc(sizeof(SLTNode)):在堆上申请一块大小为节点结构体的内存

  • (SLTNode*):强制类型转换

  • newnode 指向这块新内存

错误处理

cpp 复制代码
if (newnode == NULL)
{
    perror("malloc fail");
    exit(1);
}
  • 检查内存是否申请成功

  • perror("malloc fail"):打印错误信息(会输出类似 "malloc fail: Cannot allocate memory")

  • exit(1):终止整个程序,返回1表示异常退出

  • 这是防御性编程 :内存申请失败如果不处理,后面 newnode->data = x 会直接崩溃

初始化节点

cpp 复制代码
newnode->data = x;
newnode->next = NULL;
  • data = x:将传入的数据存入节点

  • next = NULL非常重要------新节点的next必须置NULL,否则是随机值

  • 如果不置NULL,后续使用这个节点时无法判断它是不是最后一个

返回

cpp 复制代码
return newnode;
  • 返回创建好的节点指针

  • 调用者拿到这个指针后,可以将其链接到链表中

2.尾插

代码如下:

cpp 复制代码
void SLTPushBack(SLTNode** pphead, SLDataType x);
cpp 复制代码
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead); //不能传空指针,因为二级指针不能接收空指针

	//先调用申请节点空间的函数
	SLTNode* newnode = SLTBuyNode(x);
	//再判断是否是空列表还是非空链表
	if (*pphead == NULL) //空链表的情况  *pphead就是指向第一个节点的指针
	{
		*pphead = newnode;
	}
	else //非空链表的情况
	{
		//先找到尾部节点的位置
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//此时ptail指向的是尾节点
		ptail->next = newnode;
	}
	
}
cpp 复制代码
void SListTest02()
{
	SLTNode* plist = NULL;

	//测试尾插
	SLTPushBack(&plist, 1); 
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist); //打印链表
}

解析:

  • 参数1SLTNode** pphead ------ 二级指针,指向链表的头指针

  • 参数2SLDataType x ------ 要插入的数据

  • 返回值void

为什么用二级指针?

因为当链表为空时,需要修改头指针的指向(从NULL变成指向新节点)。如果传一级指针,修改只在函数内部生效。

函数头

cpp 复制代码
void SLTPushBack(SLTNode** pphead,SLDataType x)
  • 接收二级指针 pphead 和数据 x

断言检查

cpp 复制代码
assert(pphead);//不能传空指针,因为二级指针不能接收空指针
  • assert(pphead):如果 pphead == NULL,程序终止并报错

  • 注意 :这里检查的是二级指针本身是否为NULL,不是检查 *pphead

创建新节点

cpp 复制代码
SLTNode* newnode = SLTBuyNode(x);
  • 调用之前封装的函数创建新节点

  • newnode->data = xnewnode->next = N

空链表情况
cpp 复制代码
if (*pphead == NULL)  //空链表的情况
{
    //*pphead就是指向第一个节点的指针
    *pphead = newnode;
}
  • 如果 *pphead == NULL,说明链表为空

  • 直接将头指针指向新节点

  • 此时链表只有一个节点

示意图

pphead *pphead(NULL) newnode

│ │ │

└──→ [ 头指针 ] ──→ NULL ←── [1|NULL]

└── 修改后:*pphead = newnode

pphead *pphead newnode

│ │ │

└──→ [ 头指针 ] ──→ [1|NULL] ←──────┘

非空链表情况
cpp 复制代码
else  //非空链表的情况
{
    //先找到尾部节点的位置
    SLTNode* ptail = *pphead;
    while(ptail->next)
    {
        ptail = ptail->next;
    }
    //此时ptail指向的是尾节点
    ptail->next = newnode;
}

查找尾节点:

  • SLTNode* ptail = *pphead;:从头部开始

  • while(ptail->next):当当前节点的next不为NULL时继续移动

  • ptail = ptail->next;:移动到下一个节点

  • 循环结束时,ptail指向最后一个节点(它的next是NULL)

链接新节点:

  • ptail->next = newnode;:将尾节点的next指向新节点

输出结果如下:

3.头插

代码如下:

cpp 复制代码
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
cpp 复制代码
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead); //不能传空指针,因为二级指针不能接收空指针
	
	//先调用申请节点空间的函数
	SLTNode* newnode = SLTBuyNode(x);

    //此时我们需要将newnode指向我们的第一个节点
	newnode->next = *pphead;
	*pphead = newnode;
}
cpp 复制代码
 测试头插
	SLTPushFront(&plist, 20);
	SLTPrint(plist); //打印链表

输出结果如下:

解析如下:
函数声明

cpp 复制代码
//头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
  • 参数1SLTNode** pphead ------ 二级指针,指向链表的头指针

  • 参数2SLDataType x ------ 要插入的数据

  • 返回值void

为什么用二级指针? 因为需要修改头指针的指向(让头指针指向新节点)。

创建新节点

cpp 复制代码
SLTNode* newnode = SLTBuyNode(x);
  • 调用封装函数创建新节点

  • newnode->data = xnewnode->next = NULL

核心操作(两步)

cpp 复制代码
//此时我们需要将newnode指向我们的第一个节点
newnode->next = *pphead;
*pphead = newnode;

第一步:newnode->next = *pphead;

  • 让新节点的next指向原来的第一个节点

  • 无论原链表是否为空,这步都成立

    • 如果原链表非空:newnode 指向原来的头节点

    • 如果原链表为空:*pphead 是 NULL,newnode->next = NULL

第二步:*pphead = newnode;

  • 更新头指针,让它指向新节点

  • 现在 newnode 成为了新的头节点

图解头插过程

情况1:原链表非空(1->2->3->NULL)

初始状态:

pphead *pphead node1

│ │ │

└──→ [ 头指针 ] ──→ [1|●]──→[2|●]──→[3|NULL]

原来的第一个节点

第一步:newnode->next = *pphead

newnode *pphead

20\|●\]──────────────────→ \[1\|●\]──→\[2\|●\]──→\[3\|NULL

新节点指向原来的头节点

第二步:*pphead = newnode

pphead *pphead newnode

│ │ │

└──→ [ 头指针 ] ──→ [20|●]──→[1|●]──→[2|●]──→[3|NULL]

新节点成为头节点

情况2:原链表为空(NULL)

初始状态:

pphead *pphead

│ │

└──→ [ 头指针 ] ──→ NULL

第一步:newnode->next = *pphead(即 NULL)

newnode

20\|NULL

第二步:*pphead = newnode

pphead *pphead newnode

│ │ │

└──→ [ 头指针 ] ──→ [20|NULL]

4.尾删

全部代码如下:

cpp 复制代码
//尾删
void SLTPopBack(SLTNode** pphead);
cpp 复制代码
//尾删
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	//链表只有一个节点的情况
	if ((*pphead)->next == NULL) //(*pphead) 这里加了括号是因为-> 的优先级更高,但是我们先的解引用,所以加括号提升优先级
	{
		//此时只有一个节点,我们需要释放空间
		free(*pphead);
		*pphead = NULL;
	}
	//链表有多个节点的情况
	else
	{
		//先找到尾部节点和前一个节点
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = prev;
			ptail = ptail->next;
		}
		//此时全部找到了prev  prev 
		//此时运行完之后要释放空间
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}
cpp 复制代码
//测试尾删
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);

输出结果如下:

解析如下:

链表节点只有一个的情况下

cpp 复制代码
    // 链表只有一个节点的情况
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
  • (*pphead)->next == NULL:头节点的next是NULL,说明只有一个节点

  • free(*pphead):释放这个唯一的节点

  • *pphead = NULL:头指针置空,链表变为空链表

图解

删除前:head → [5|NULL]

删除后:head → NULL

链表节点有多个的情况下

cpp 复制代码
    // 链表有多个节点的情况
    else
    {
        // 找尾节点和前一个节点
        SLTNode* prev = *pphead;
        SLTNode* ptail = *pphead;
  • prevptail 都从头节点开始

  • prev 的目的是记录 ptail 的前一个节点

遍历找到尾节点和前一个节点

cpp 复制代码
        while (ptail->next)
        {
            prev = ptail;      // prev跟上ptail
            ptail = ptail->next;
        }

循环条件while (ptail->next)

  • 只要当前节点的next不为NULL,说明不是最后一个节点

  • 等价于 while (ptail->next != NULL)

循环体

  1. prev = ptail;:让 prev 移动到 ptail 的位置

  2. ptail = ptail->next;ptail 移动到下一个节点

执行过程示例(链表 1->2->3->4->NULL):

循环次数 prev指向 ptail指向 ptail->next 是否继续
初始 1 1 2(非NULL) 进入循环
第1次后 1 → 1 1 → 2 3(非NULL) 继续
第2次后 1 → 2 2 → 3 4(非NULL) 继续
第3次后 2 → 3 3 → 4 NULL 循环结束

循环结束时

  • ptail 指向尾节点(4)

  • prev 指向倒数第二个节点(3)

删除尾节点

cpp 复制代码
        // 循环结束:ptail指向尾节点,prev指向倒数第二个节点
        
        free(ptail);        // 释放尾节点
        prev->next = NULL;  // 新的尾节点next置NULL
    }
}
  • free(ptail);:释放尾节点的内存

  • prev->next = NULL;:将新的尾节点(原倒数第二个节点)的next置NULL

  • 不需要 ptail = NULL;,因为 ptail 是局部变量,函数结束就销毁了

5.头删

全部代码如下:

cpp 复制代码
//头删
void SLTPopFront(SLTNode** pphead);
cpp 复制代码
//头删 
void SLTPopFront(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next; //(*pphead) 这里加了括号是因为->的优先级更高

	free(*pphead);
	*pphead = next;
}
cpp 复制代码
//测试头删
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);

输出结果如下:

图解头删过程

情况1:链表有多个节点(1->2->3->NULL)

初始状态:

pphead *pphead

│ │

└──→ [ 头指针 ] ──→ [1|●]──→[2|●]──→[3|NULL]

要删除的节点

第一步:SLTNode* next = (*pphead)->next;

next 指向节点2

pphead *pphead next

│ │ │

└──→ [ 头指针 ] ──→ [1|●]──→[2|●]──→[3|NULL]

即将被释放

第二步:free(*pphead);

释放节点1的内存

pphead *pphead next

│ │ │

└──→ [ 头指针 ] ──→ (内存已释放) [2|●]──→[3|NULL]

这块内存还给系统

第三步:*pphead = next;

头指针指向节点2

pphead *pphead

│ │

└──→ [ 头指针 ] ──→ [2|●]──→[3|NULL]

情况2:链表只有一个节点(1->NULL)

初始状态:

pphead *pphead

│ │

└──→ [ 头指针 ] ──→ [1|NULL]

要删除的节点

第一步:SLTNode* next = (*pphead)->next;

next = NULL

pphead *pphead next(NULL)

│ │ │

└──→ [ 头指针 ] ──→ [1|NULL] ←─┘

即将被释放

第二步:free(*pphead);

释放节点1的内存

pphead *pphead next(NULL)

│ │ │

└──→ [ 头指针 ] ──→ (内存已释放) ←─┘

第三步:*pphead = next;

*pphead = NULL

pphead *pphead

│ │

└──→ [ 头指针 ] ──→ NULL

6.查找

全部代码如下:

cpp 复制代码
//查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x);
cpp 复制代码
//查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
	SLTNode* pcur = phead;
	while (pcur) //等价于 pcur!=NULL
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

输出结果如下:

解析:
遍历查找

cpp 复制代码
while (pcur) //等价于 pcur != NULL
{
    if (pcur->data == x)
    {
        return pcur;
    }
    pcur = pcur->next;
}

循环条件while (pcur)

  • 等价于 while (pcur != NULL)

  • pcur 为 NULL 时,说明已经遍历完整个链表,没找到

循环体

  1. if (pcur->data == x):判断当前节点的数据是否等于要查找的值

  2. return pcur;:如果相等,立即返回当前节点的指针

  3. pcur = pcur->next;:如果不相等,移动到下一个节点继续查找

查找过程图解

假设链表:5 -> 12 -> 8 -> 3 -> NULL,查找值为 8

步骤 pcur指向 pcur->data 是否等于8 操作
初始 节点5 5 继续
第1步后 节点12 12 继续
第2步后 节点8 8 返回节点8的指针

如果查找值为 10,会一直遍历到 NULL,然后返回 NULL。

后面还有5个内容,我们留到下一章来讲!!!!

相关推荐
计算机安禾1 小时前
【C语言程序设计】第33篇:二级指针与指针数组
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
DANGAOGAO2 小时前
数据结构复习(持续更新)
数据结构
cui_ruicheng2 小时前
C++ 数据结构进阶:哈希表原理
数据结构·c++·算法·哈希算法
xiaoye-duck2 小时前
C++ 二叉搜索树(BST)深度解析:从概念原理、核心操作到底层实现
数据结构·c++
丶小鱼丶2 小时前
数据结构和算法之【队列】
java·数据结构
robch3 小时前
golang container/heap 是一个为任意类型实现堆(优先队列)接口的包
数据结构·算法·golang
leonkay11 小时前
Golang语言闭包完全指南
开发语言·数据结构·后端·算法·架构·golang
casual~12 小时前
第?个质数(埃氏筛算法)
数据结构·c++·算法
仰泳的熊猫12 小时前
题目2308:蓝桥杯2019年第十届省赛真题-旋转
数据结构·c++·算法·蓝桥杯