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

目录

一:核心操作及代码要点

1.在指定位置之前插入数据

2.在指定位置之后插入数据

情况1:pos在中间

情况2:pos在末尾

3.删除pos节点

情况1:删除非头节点

情况2:删除头节点

情况3:删除尾节点

4.删除pos之后的节点

情况1:删除中间节点

情况2:删除最后一个节点

情况3:错误使用(会导致断言失败)

5.销毁链表


一:核心操作及代码要点

1.在指定位置之前插入数据

全部代码如下:

cpp 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
cpp 复制代码
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(pphead && *pphead);
	assert(pos);

	//先调用申请节点空间的函数
	SLTNode* newnode = SLTBuyNode(x);

	//如果pos==*pphead 则说明是头部插入
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//此时prev->newnode->pos
		newnode->next = pos;
		prev->next = newnode;
	}
	
}
cpp 复制代码
SLTNode* find = SLTFind(plist, 1);
SLTInsert(&plist, find, 11); 
SLTPrint(plist);

输出结果如下:

解析:
函数声明

cpp 复制代码
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
  • 参数1SLTNode** pphead ------ 二级指针,指向链表的头指针

  • 参数2SLTNode* pos ------ 指定位置的节点指针(在这个节点之前插入)

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

  • 返回值void

  • 作用 :在链表中指定的 pos 节点之前插入一个新节点

为什么用二级指针? 因为如果 pos 是头节点,插入后头指针需要更新。

函数头

cpp 复制代码
void SLTInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
  • 注意参数名是 phead(二级指针)

断言检查

cpp 复制代码
assert(phead && *phead);
assert(pos);
  • 第一行:链表不能为空(phead 有效且 *phead 不为 NULL)

  • 第二行:pos 不能为空,必须是一个有效的节点指针

  • 注意 :这里假设 pos 一定是链表中的节点,函数不会验证这一点

判断是否为头插

cpp 复制代码
//如果pos==*phead则说明是头部插入
if (pos == *phead)
    SLTPushFront(phead, x);
  • 如果 pos 指向头节点,那么"在pos之前插入"就是头插

  • 直接复用之前写好的 SLTPushFront 函数

  • 注意传的是 phead(二级指针)和 x

非头插情况

cpp 复制代码
else
{
    SLTNode* prev = *phead;
    while (prev->next != pos)
    {
        prev = prev->next;
    }
    //此时prev->newnode->pos
    newnode->next = pos;
    prev->next = newnode;
}

找pos的前一个节点

cpp 复制代码
SLTNode* prev = *phead;
while (prev->next != pos)
{
    prev = prev->next;
}
  • 从头开始遍历,找到 next 指向 pos 的那个节点

  • 循环条件:prev->next != pos,当 prev 的下一个节点不是 pos 时继续

  • 循环结束时,prev 就是 pos 的前一个节点

插入新节点

cpp 复制代码
newnode->next = pos;    // 新节点指向pos
prev->next = newnode;   // 前一个节点指向新节点
  • 这两步的顺序不能颠倒

  • 先让新节点指向 pos

  • 再让前一个节点指向新节点

插入过程图解

情况1:pos不是头节点

假设链表 1->2->3->4->NULL,要在 3 之前插入 11

初始状态:

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

pos

第一步:找prev

prev从1开始:

  • prev=1: 1->next=2 ≠ 3 → prev=2

  • prev=2: 2->next=3 == pos → 停止

此时 prev 指向节点2

第二步:newnode->next = pos

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

↑ ↑

│ pos

└──[11|●]──┘

newnode

第三步:prev->next = newnode

1\|●\]──→\[2\|●\]──→\[11\|●\]──→\[3\|●\]──→\[4\|NULL

最终结果:1->2->11->3->4->NULL

情况2:pos是头节点

假设链表 1->2->3->NULL,要在 1 之前插入 11

if (pos == *phead) 成立

直接调用 SLTPushFront(phead, 11)

结果:11->1->2->3->NULL

2.在指定位置之后插入数据

全部代码如下:

cpp 复制代码
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLDataType x);
cpp 复制代码
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLDataType x)
{
	assert(pos);

	//先调用申请节点空间的函数
	SLTNode* newnode = SLTBuyNode(x);
	//此时我们需 pos->newnode-> pos->next
	newnode->next = pos->next;
	pos->next = newnode;
}
cpp 复制代码
//在指定位置之后插入数据
SLTNode* find = SLTFind(plist, 1);
SLTInsertAfter(find, 100); 
SLTPrint(plist);

输出结果如下:

解析:
函数声明

cpp 复制代码
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLDataType x);
  • 参数1SLTNode* pos ------ 一级指针,指定位置的节点

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

  • 返回值void

  • 作用 :在链表中指定的 pos 节点之后插入一个新节点

为什么用一级指针?

因为在节点之后插入,不需要修改头指针。即使 pos 是最后一个节点,插入后也只是修改 pos->next,头指针不变

核心操作(两步)

cpp 复制代码
newnode->next = pos->next;  // 第一步:新节点指向pos的下一个节点
pos->next = newnode;        // 第二步:pos指向新节点

这两步的顺序至关重要:

  • 必须先执行第一步,让新节点先链接到后面的节点

  • 再执行第二步 ,让 pos 指向新节点

插入过程图解

情况1:pos在中间

假设链表 1->2->3->4->NULL,要在节点 2 之后插入 100

初始状态:

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

pos

第一步:newnode->next = pos->next

newnode pos->next

100\|●\]────→\[3\|●\]──→\[4\|NULL

新节点指向节点3

第二步:pos->next = newnode

1\|●\]──→\[2\|●\]──→\[100\|●\]──→\[3\|●\]──→\[4\|NULL

pos指向新节点

最终结果:1->2->100->3->4->NULL

情况2:pos在末尾

假设链表 1->2->3->4->NULL,要在节点 4 之后插入 100

初始状态:

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

pos

第一步:newnode->next = pos->next

newnode pos->next = NULL

100\|●\]────→NULL 第二步:pos-\>next = newnode \[1\|●\]──→\[2\|●\]──→\[3\|●\]──→\[4\|●\]──→\[100\|NULL

pos指向新节点

最终结果:1->2->3->4->100->NULL(尾插效果)

3.删除pos节点

全部代码如下:

cpp 复制代码
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
cpp 复制代码
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);

	//pos是头节点 以及 pos不是节点的情况
	if (pos == *pphead)
	{
		//头删
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//此时找到了 prev pos  pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
cpp 复制代码
////删除pos节点
SLTNode* find = SLTFind(plist, 2);
SLTErase(&plist,find);
SLTPrint(plist); 

输出结果如下:

解析:
处理头节点删除

cpp 复制代码
//pos是头节点 以及 pos不是节点的情况
if (pos == *pphead)
{
    //头删
    SLTPopFront(pphead);
}
  • 如果 pos 指向头节点,直接复用之前写好的 SLTPopFront 函数

  • SLTPopFront 已经处理了释放内存和更新头指针

找pos的前一个节点

cpp 复制代码
SLTNode* prev = *pphead;
while (prev->next != pos)
{
    prev = prev->next;
}
  • 从头开始遍历,找到 next 指向 pos 的那个节点

  • 循环条件:prev->next != pos,当 prev 的下一个节点不是 pos 时继续

  • 循环结束时,prev 就是 pos 的前一个节点

删除节点

cpp 复制代码
prev->next = pos->next;  // 前一个节点指向pos的下一个节点(绕过pos)
free(pos);               // 释放pos节点的内存
pos = NULL;              // 将局部指针置NULL(好习惯,但非必须)

删除过程图解

情况1:删除非头节点

假设链表 1->2->3->4->NULL,要删除节点 3

初始状态:

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

pos

第一步:找prev

prev从1开始:

  • prev=1: 1->next=2 ≠ 3 → prev=2

  • prev=2: 2->next=3 == pos → 停止

此时 prev 指向节点2

第二步:prev->next = pos->next

1\|●\]──→\[2\|●\]──→\[4\|NULL

│ ↑

└─────┘

prev->next 跳过节点3指向节点4

pos(3) 还在中间,但已被跳过

第三步:free(pos)

释放节点3的内存

最终结果:1->2->4->NULL

情况2:删除头节点

假设链表 1->2->3->4->NULL,要删除节点 1

if (pos == *pphead) 成立

直接调用 SLTPopFront(pphead)

SLTPopFront 执行:

  1. next = (*pphead)->next; // next指向节点2

  2. free(*pphead); // 释放节点1

  3. *pphead = next; // 头指针指向节点2

最终结果:2->3->4->NULL

情况3:删除尾节点

假设链表 1->2->3->4->NULL,要删除节点 4

初始状态:

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

pos

第一步:找prev

prev从1开始,一直走到节点3(3->next=4 == pos)

第二步:prev->next = pos->next // pos->next = NULL

prev(3) 的 next 指向 NULL

第三步:free(pos) // 释放节点4

最终结果:1->2->3->NULL

4.删除pos之后的节点

全部代码如下:

cpp 复制代码
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
cpp 复制代码
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);

	SLTNode* del = pos->next;
	//此时找到了 pos del del->next
	pos->next = del->next;
	free(del);
	del = NULL;
}
cpp 复制代码
	////删除pos之后的节点
	SLTNode* find = SLTFind(plist, 2);
	SLTEraseAfter(find);
	SLTPrint(plist); 

输出结果如下:

解析:
函数实现

cpp 复制代码
void SLTEraseAfter(SLTNode* pos)
{
    assert(pos && pos->next);  // 确保pos存在且后面有节点

    SLTNode* del = pos->next;   // del 指向要删除的节点(pos的下一个)
    
    // 此时找到了 pos del del->next
    pos->next = del->next;      // pos 跳过 del,指向 del 的下一个
    free(del);                  // 释放 del 节点的内存
    del = NULL;                 // 将局部指针置NULL(好习惯)
}

断言检查

cpp 复制代码
assert(pos && pos->next);
  • 同时检查两件事:

    • pos != NULL:位置节点不能为空

    • pos->next != NULLpos 后面必须有节点才能删除

  • 如果 pos 是最后一个节点(pos->next == NULL),断言失败,程序终止

定位要删除的节点

cpp 复制代码
SLTNode* del = pos->next;   // del 指向要删除的节点(pos的下一个)
  • del 指针保存要删除的节点地址

  • 为什么要保存?因为马上要修改 pos->next,如果不保存就找不到这个节点了

重新链接

cpp 复制代码
pos->next = del->next;      // pos 跳过 del,指向 del 的下一个
  • posnext 指针直接指向 del 的下一个节点

  • 这样 del 节点就从链表中被"跳过"了

释放内存

cpp 复制代码
free(del);                  // 释放 del 节点的内存
del = NULL;                 // 将局部指针置NULL(好习惯)
  • free(del):释放被删除节点的内存

  • del = NULL:局部变量置 NULL(防止误用,但函数结束就销毁,不是必须)

删除过程图解

情况1:删除中间节点

假设链表 1->2->3->4->NULL,要删除节点 2 之后的节点(即删除节点3):

初始状态:

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

pos

第一步:SLTNode* del = pos->next;

del 指向节点3

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

↑ ↑

pos del

第二步:pos->next = del->next;

del->next 指向节点4

pos->next 从指向 del 改为指向节点4

1\|●\]──→\[2\|●\]──→\[4\|NULL

└─────┘

↑ ↑

pos del(仍指向3,但已被跳过)

第三步:free(del);

释放节点3的内存

最终结果:1->2->4->NULL

情况2:删除最后一个节点

假设链表 1->2->3->4->NULL,要删除节点 3 之后的节点(即删除节点4):

初始状态:

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

pos

第一步:del = pos->next; // del指向节点4

第二步:pos->next = del->next; // del->next = NULL

pos->next 指向 NULL

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

第三步:free(del); // 释放节点4

最终结果:1->2->3->NULL

情况3:错误使用(会导致断言失败)

// 错误1:pos 是最后一个节点

SLTNode* find = SLTFind(plist, 4);

SLTEraseAfter(find); // assert(pos->next) 失败,程序终止

// 错误2:pos 是 NULL

SLTEraseAfter(NULL); // assert(pos) 失败,程序终止

5.销毁链表

全部代码如下:

cpp 复制代码
//销毁链表
void SListDesTroy(SLTNode** pphead);
cpp 复制代码
//销毁链表
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);

	SLTNode* pcur = *pphead;
	while(pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//此时pcur为空
	*pphead = NULL;
}
cpp 复制代码
//销毁链表
SListDesTroy(&plist);
SLTPrint(plist); 

输出结果如下:

解析:

初始化遍历指针

cpp 复制代码
SLTNode* pcur = *pphead;    // pcur 指向当前要处理的节点
  • pcur 指向链表的第一个节点

循环释放所有节点

cpp 复制代码
while (pcur)                 // 只要 pcur 不为 NULL,继续循环
{
    SLTNode* next = pcur->next;  // 先保存下一个节点的地址
    free(pcur);                  // 释放当前节点
    pcur = next;                  // 移动到下一个节点
}

为什么需要先保存 next

因为 free(pcur) 之后,pcur 指向的内存被释放,不能再访问 pcur->next。所以必须在释放之前把下一个节点的地址保存下来。

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

循环次数 pcur指向 next保存 操作 下一轮pcur
第1次 节点1 节点2 free(节点1) 节点2
第2次 节点2 节点3 free(节点2) 节点3
第3次 节点3 节点4 free(节点3) 节点4
第4次 节点4 NULL free(节点4) NULL
第5次 NULL - 循环结束 -

头指针置 NULL

cpp 复制代码
*pphead = NULL;  // 头指针置 NULL,防止野指针
  • 所有节点释放完后,将外部的头指针设为 NULL

  • 这样调用者再使用 plist 时,知道它是空链表,不会误访问已释放的内存

销毁过程图解

假设链表:1->2->3->NULL

初始状态:

plist → [1|●]──→[2|●]──→[3|NULL]

pcur

第1次循环:

next = pcur->next; // next指向节点2

free(pcur); // 释放节点1

pcur = next; // pcur指向节点2

状态:

plist → (已释放) [2|●]──→[3|NULL]

pcur

第2次循环:

next = pcur->next; // next指向节点3

free(pcur); // 释放节点2

pcur = next; // pcur指向节点3

状态:

plist → (已释放) (已释放) [3|NULL]

pcur

第3次循环:

next = pcur->next; // next = NULL

free(pcur); // 释放节点3

pcur = next; // pcur = NULL

状态:

plist → (已释放) (已释放) (已释放)

循环结束,pcur = NULL

最后:*pphead = NULL;

plist → NULL

以上就是关于单链表的全部内容了!!!!

相关推荐
夏日听雨眠2 小时前
数据结构1
数据结构·算法
jing-ya2 小时前
day 55 图论part7
java·数据结构·算法·图论
zyq99101_12 小时前
蓝桥杯刷题算法实战解析
数据结构·python·算法·蓝桥杯
Wave8452 小时前
数据结构—线性表
数据结构
Book思议-2 小时前
【数据结构实战】双向链表尾插法
c语言·数据结构·链表
做一个码农都是奢望2 小时前
计算机控制系统:最小拍控制系统设计入门
数据结构·算法
重生之后端学习3 小时前
31. 下一个排列
数据结构·算法·leetcode·职场和发展·排序算法·深度优先
没头脑的男大3 小时前
环形链表很曼妙的一个做题思路
数据结构·链表
shehuiyuelaiyuehao3 小时前
算法9,滑动窗口,长度最小的子数组
数据结构·算法·leetcode