第32篇 数据结构入门 单链表的增删查改实现

文章目录

🔗 什么是链表?

为了解决顺序表的痛点,链表应运而生。

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

形象比喻:火车

顺序表像是一整块固定的车厢,想加一节很难;而链表就像一列火车,每节车厢(结点)都是独立的。旺季时挂上几节,淡季时摘掉几节,互不影响。

1. 结点的构造

链表的每个结点包含两部分:

  • 数据域:保存当前的数据(如整型、字符等)。
  • 指针域:保存下一个结点的地址。
c 复制代码
// 单链表结点结构
struct SListNode {
    int data;               // 数据
    struct SListNode* next; // 指向下一个结点的指针
};
2. 链表的"八大金刚"

链表的结构非常多样,通过以下三个维度的组合,可以形成8种不同的链表结构:

  • 单向 / 双向
  • 带头 / 不带头(带头结点通常称为"哨兵位",不存有效数据,只为了操作方便)
  • 循环 / 不循环

在实际应用和面试中,最常考察的是两种:

  • 无头单向非循环链表:结构简单,常作为哈希桶、图的邻接表的子结构。
  • 带头双向循环链表:结构最复杂,但实现起来逻辑反而更统一,常用于单独存储数据(如Linux内核链表)。

因为内容太多,这篇博客只讲解单链表,接下来让我们一起实现单链表的增删查改接口。


🛠️ 单链表接口实现

现在先来实现单链表。与顺序表一样,先来定义并且将列表与其中的数据重命名:

c 复制代码
#include<stdlib.h>
typedef int SListDate;
typedef struct SLN
{
	SListDate date;
	struct SLN* next;
}SListNode;

与顺序表不同,链表是一个节点接着一个节点,因此并不需要扩容和初始化函数,与之相对的是申请新节点的函数:

申请新节点
  • 参数类型int x(需要存入节点的数据)。
  • 返回值SListNode*(新申请节点的指针)。
  • 函数内实现逻辑 :通过 malloc 在堆区申请一块与节点大小相同的内存;若申请失败则报错并返回 NULL;若成功,则将传入的数据 x 赋值给数据域 date,并将指针域 next 置为 NULL,最后返回该新节点的指针。
c 复制代码
SListNode* NewNode(int x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{	
		perror("malloc fail:");
		return NULL;
	}
	newnode->date = x;
	newnode->next = NULL;
	return newnode;
}

💡 核心难点:传址调用与二级指针的深刻剖析

在进行后续的增删改查之前,我们必须深刻剖析一个核心问题:为什么有的函数需要初始节点的二级指针作为参数,而有的不需要?

这涉及到 C 语言中传值调用传址调用 的本质:

当我们把一级指针(例如 SListNode* phead)作为参数传递给函数时,本质上是传递了一个"内容为地址的数值"。在函数内部,我们确实可以通过这个地址去修改它所指向的内存空间(比如修改节点里的 datenext)。但是,我们无法通过这个一级指针去修改"一级指针本身"的指向。 也就是说,如果在函数内部执行 phead = newnode;,这仅仅修改了函数内部的局部变量 phead,外部的真实头指针依然指向原来的位置。

因此,只要一个操作可能会改变"头结点指针本身的指向",我们就必须传递该一级指针的地址,即二级指针(SListNode** pphead)。 通过解引用 *pphead = newnode;,我们才能真正修改外部的头指针。反之,如果操作只是在链表中间修改某个节点的 next 指针,不涉及头指针的改变,那么传递一级指针就足够了。


尾插
  • 参数类型SListNode** pphead(链表起始节点的二级指针),SListDate x(插入的数据)。
  • 返回值void
  • 函数内实现逻辑 :首先断言 pphead 不为空。因为链表可能为空,若为空,则需要通过 *pphead = NewNode(x); 直接修改外部头指针(这就是为什么尾插必须传二级指针的原因 )。若不为空,为了避免遍历后丢失链表起点,我们设置一个临时指针 phead 等于 *pphead,通过 while 循环遍历直到 phead->next == NULL,最后将新节点链接到尾部。
c 复制代码
void SLpushback(SListNode** pphead,SListDate x)
{
	assert(pphead );
	SListNode* phead = *pphead;
	if (phead==NULL)
	{
		phead= NewNode(x);
        *pphead = phead; // 补充:将新节点赋给外部头指针
	}
    else 
    {
        while (phead->next != NULL)
        {
            phead = phead->next;
        }
        phead->next = NewNode(x);
    }
}
打印链表
  • 参数类型SListNode** pphead(链表初始位置的二级指针)。
  • 返回值void
  • 函数内实现逻辑 :断言 *pphead 不为空。定义一个临时指针 phead 接收 *pphead,通过 while 循环遍历链表,依次打印每个节点的数据,直到指针为 NULL,最后打印 NULL\n
c 复制代码
void SLprint(SListNode** pphead)
{
	assert(*pphead);

	SListNode* phead = *pphead;
	while (phead)
	{
		printf("%d->", phead->date);
		phead = phead->next;
	}
	printf("NULL\n");
}
头插
  • 参数类型SListNode** pphead(链表起始节点的二级指针),SListDate x(插入的数据)。
  • 返回值void
  • 函数内实现逻辑 :断言 pphead 不为空。创建一个新的节点 p1,将其 next 指向原来的第一个节点(即 *pphead),然后通过 *pphead = p1; 修改外部头指针的指向因为头指针发生了改变,所以必须传二级指针)。
c 复制代码
void SLpushfront(SListNode** pphead, SListDate x)
{
	assert(pphead);
	SListNode *p1 = NewNode(x);
	p1->next = *pphead;
	*pphead = p1;
}
尾删
  • 参数类型SListNode** pphead(链表起始节点的二级指针)。
  • 返回值void
  • 函数内实现逻辑 :断言 pphead*pphead 不为空。如果链表只有一个节点,释放该节点后必须将外部头指针置为 NULL涉及头指针修改,需二级指针 )。如果有多个节点,使用双指针遍历,找到倒数第二个节点,将其 next 指针置为 NULL,并 free 掉最后一个节点。
c 复制代码
void SLpopback(SListNode**pphead)
{
	assert(pphead && *pphead);
	SListNode* p1 = NULL;
	SListNode* p2 = *pphead;
	if (p2->next == NULL)
	{
		free(p2);
		*pphead = NULL; // 补充:修改外部头指针
	}
    else 
    {
        while (p2->next)
        {
            p1 = p2;
            p2 = p2->next;
        }
        p1->next = NULL;
        free(p2);
        p2 = NULL;
    }
}
头删
  • 参数类型SListNode** pphead(链表起始节点的二级指针)。
  • 返回值void
  • 函数内实现逻辑 :断言不为空。将外部头指针 *pphead 指向第二个节点,然后 free 掉原来的第一个节点(因为头指针发生了改变,必须传二级指针)。
c 复制代码
void SLpopfront(SListNode** pphead)
{
	assert(pphead && *pphead);

	SListNode* phead = *pphead;
	*pphead = phead->next;
	free(phead);
	phead = NULL;
}
查找指定数据
  • 参数类型SListNode** pphead(链表起始节点的二级指针),SListDate x(要查找的数据)。
  • 返回值SListNode*(返回找到节点的地址,未找到返回 NULL)。
  • 函数内实现逻辑 :断言不为空。通过 while 循环遍历链表,若当前节点的 date 等于 x,直接返回该节点指针;若遍历结束未找到,返回 NULL
c 复制代码
SListNode* SLfind(SListNode** pphead, SListDate x)
{
	assert(pphead && *pphead);

	SListNode* p1 = *pphead;
	while (p1)
	{
		if (p1->date == x)
			return p1;
		p1 = p1->next;
	}
	return NULL;
}
删除指定节点
  • 参数类型SListNode** pphead(二级指针),SListNode* passign(要删除的节点地址)。
  • 返回值void
  • 函数内实现逻辑 :断言参数不为空。首先判断指定节点是否是第一个节点,如果是,直接调用头删(因为需要改变头指针,所以传二级指针 )。如果不是,则遍历找到该节点前面的一个节点,将其 next 指针指向指定节点后的节点,最后 free 掉指定节点。
c 复制代码
void SLassign(SListNode** pphead, SListNode* passign)
{
	assert(pphead && *pphead&&passign);

	SListNode* p1 = *pphead;
	if (p1==passign)
	{
		SLpopfront(pphead);
		return;
	}
	while (p1->next != passign)
	{
		p1 = p1->next;
	}
	p1->next = passign->next;
	free(passign);
	passign = NULL;
}
删除指定节点后面的数据
  • 参数类型SListNode* passign(指定节点的一级指针)。
  • 返回值void
  • 函数内实现逻辑 :断言 passign 及其 next 不为空。因为只需修改指定节点的 next 指针,不涉及头指针的改变,因此只需要一级指针 。记录指定节点后面的节点,断开链接后将其 free 掉。此操作时间复杂度为 O(1)。
c 复制代码
void SLassignback(SListNode*passign)
{
	assert(passign&&passign->next);
	SListNode* p1 = passign->next;
	passign->next=NULL;
	free(p1);
	p1 = NULL;
}
删除指定位置前面的数据
  • 参数类型SListNode** pphead(二级指针),SListNode* passign(指定节点的一级指针)。
  • 返回值void
  • 函数内实现逻辑 :断言参数合法且 passign 不是头节点。如果 passign 的前一个节点就是头节点,直接调用头删(需二级指针 )。否则,遍历找到 passign 前面第二个节点,修改其 next 指针跳过前一个节点,最后 free 掉被删除的节点。
c 复制代码
void SLassignfront(SListNode** pphead, SListNode* passign)
{
	assert(pphead && *pphead && passign&&(passign!=*pphead));

	SListNode* p1 = *pphead;
	SListNode* del=p1->next;
	if (p1->next == passign)
	{
		SLpopfront(pphead);
		return;
	}
	while (del->next != passign)
	{
		p1 = p1->next;
		del= p1->next;
	}
	p1->next = passign->next;
	free(del);
	del = NULL;
}
在指定数据前面添加数据
  • 参数类型SListNode** pphead(二级指针),SListNode* passign(指定节点地址),SListDate x(新数据)。
  • 返回值void
  • 函数内实现逻辑 :断言参数不为空。因为单链表无法链接到前面的节点,若指定节点是第一个节点,直接调用头插(需二级指针)。如果不是,则遍历找到指定节点前面的节点,将新节点两端连接到链表中。
c 复制代码
void SLassignfrontadd(SListNode** pphead, SListNode* passign,SListDate x)
{
	assert(pphead && *pphead && passign);

	SListNode* p1 = *pphead;
	if (p1 == passign)
	{
		SLpushfront(pphead,x);
		return;
	}
	while (p1->next != passign)
	{
		p1 = p1->next;
	}
	p1->next = NewNode(x);
	p1->next->next=passign;
}
在指定数据后面添加数据
  • 参数类型SListNode* passign(指定节点的一级指针 ),SListDate x(新数据)。
  • 返回值void
  • 函数内实现逻辑 :断言 passign 不为空。因为不再需要更改指定位置前面节点的指针,不涉及头指针的改变,只需一级指针 。若指定节点是最后一个节点,直接将新节点连接到其后;若不是,则将新节点插入到 passign 与其原后继节点之间。
c 复制代码
void SLassignbackadd(SListNode* passign, SListDate x)
{
	assert(passign);
	SListNode* p1= passign->next;
	if (passign->next==NULL)
	{
		passign->next = NewNode(x);
		return;
	}
	passign->next = NewNode(x);
	passign->next->next = p1;
}

📝 完整代码汇总

SListNode.h:

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
typedef int SListDate;
typedef struct SLN
{
	SListDate date;
	struct SLN* next;
}SListNode;

SListNode* NewNode(int x);

void SLpushback(SListNode** pphead,SListDate x);

void SLprint(SListNode** pphead);

void SLpushfront(SListNode** pphead, SListDate x);

void SLpopback(SListNode** pphead);

void SLpopfront(SListNode** pphead);

SListNode* SLfind(SListNode** pphead, SListDate x);

void SLassign(SListNode** pphead, SListNode* passign);

void SLassignback(SListNode* passign);

void SLassignfront(SListNode** pphead, SListNode* passign);

void SLassignfrontadd(SListNode** pphead, SListNode* passign, SListDate x);

void SLassignbackadd(SListNode* passign, SListDate x);

SListNode.c:

c 复制代码
#include "SListNode.h"

SListNode* NewNode(int x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{	
		perror("malloc fail:");
		return NULL;
	}
	newnode->date = x;
	newnode->next = NULL;
	return newnode;
}

void SLpushback(SListNode** pphead,SListDate x)
{
	assert(pphead );
	SListNode* phead = *pphead;
	if (phead==NULL)
	{
		phead= NewNode(x);
        *pphead = phead;
	}
    else 
    {
        while (phead->next != NULL)
        {
            phead = phead->next;
        }
        phead->next = NewNode(x);
    }
}

void SLprint(SListNode** pphead)
{
	assert(*pphead);

	SListNode* phead = *pphead;
	while (phead)
	{
		printf("%d->", phead->date);
		phead = phead->next;
	}
	printf("NULL\n");
}

void SLpushfront(SListNode** pphead, SListDate x)
{
	assert(pphead);
	SListNode *p1 = NewNode(x);
	p1->next = *pphead;
	*pphead = p1;
}

void SLpopback(SListNode**pphead)
{
	assert(pphead && *pphead);
	SListNode* p1 = NULL;
	SListNode* p2 = *pphead;
	if (p2->next == NULL)
	{
		free(p2);
		*pphead = NULL;
	}
    else 
    {
        while (p2->next)
        {
            p1 = p2;
            p2 = p2->next;
        }
        p1->next = NULL;
        free(p2);
        p2 = NULL;
    }
}

void SLpopfront(SListNode** pphead)
{
	assert(pphead && *pphead);

	SListNode* phead = *pphead;
	*pphead = phead->next;
	free(phead);
	phead = NULL;
}

SListNode* SLfind(SListNode** pphead, SListDate x)
{
	assert(pphead && *pphead);

	SListNode* p1 = *pphead;
	while (p1)
	{
		if (p1->date == x)
			return p1;
		p1 = p1->next;
	}
	return NULL;
}

void SLassign(SListNode** pphead, SListNode* passign)
{
	assert(pphead && *pphead&&passign);

	SListNode* p1 = *pphead;
	if (p1==passign)
	{
		SLpopfront(pphead);
		return;
	}
	while (p1->next != passign)
	{
		p1 = p1->next;
	}
	p1->next = passign->next;
	free(passign);
	passign = NULL;
}

void SLassignback(SListNode*passign)
{
	assert(passign&&passign->next);
	SListNode* p1 = passign->next;
	passign->next=NULL;
	free(p1);
	p1 = NULL;
}

void SLassignfront(SListNode** pphead, SListNode* passign)
{
	assert(pphead && *pphead && passign&&(passign!=*pphead));

	SListNode* p1 = *pphead;
	SListNode* del=p1->next;
	if (p1->next == passign)
	{
		SLpopfront(pphead);
		return;
	}
	while (del->next != passign)
	{
		p1 = p1->next;
		del= p1->next;
	}
	p1->next = passign->next;
	free(del);
	del = NULL;
}

void SLassignfrontadd(SListNode** pphead, SListNode* passign,SListDate x)
{
	assert(pphead && *pphead && passign);

	SListNode* p1 = *pphead;
	if (p1 == passign)
	{
		SLpushfront(pphead,x);
		return;
	}
	while (p1->next != passign)
	{
		p1 = p1->next;
	}
	p1->next = NewNode(x);
	p1->next->next=passign;
}

void SLassignbackadd(SListNode* passign, SListDate x)
{
	assert(passign);
	SListNode* p1= passign->next;
	if (passign->next==NULL)
	{
		passign->next = NewNode(x);
		return;
	}
	passign->next = NewNode(x);
	passign->next->next = p1;
}

test.c:

c 复制代码
#include "SListNode.h"

int main()
{
	SListNode* phead = NewNode(1);
	SListNode* p2 = NewNode(2);
	SListNode* p3 = NewNode(3);
	SListNode* p4 = NewNode(4);
	SListNode* p5 = NewNode(5);

	phead->next = p2;
	p2->next = p3;
	p3->next = p4;
	p4->next = p5;
	p5->next = NULL;

	//SLprint(&phead);

	//SLpushback(&phead, 6);
	//SLprint(&phead);

	SLpushfront(&phead,6);
	SLprint(&phead);

	SLpopback(&phead);
	SLprint(&phead);

	SLpopfront(&phead);
	SLprint(&phead);

	SListDate x;  printf("请输入指定的数据"); scanf("%d",&x);

	//SLassign(&phead, SLfind(&phead, x));

	//SLassignback(SLfind(&phead, x));

	//SLassignfront(&phead, SLfind(&phead, x));

	SListDate y; printf("请输入新节点的数据:"); scanf("%d", &y);

	//SLassignfrontadd(&phead, SLfind(&phead, x), y);

	SLassignbackadd(SLfind(&phead, x), y);

	SLprint(&phead);

}

🌟 写在最后

代码写完了,但我想对正在阅读这篇博客的你说几句心里话。

最近身边有很多同学在迷茫、害怕,大体原因是只能靠自己一个人在这个世界打拼,无依无靠;而世界又在不断变化进步,不知道自己的出路在哪里。

我把送给他们的话,也送给正在屏幕前努力的你:

保持乐观,悲观者正确,乐观者前行。

相信时间的力量,就不必问何时才有回报。

拥有坚持的毅力,就不必在意暂时的苟且。

保持每天的进步,就不必对未来感到迷茫。

愿你在代码的世界里披荆斩棘,也在人生的道路上步履不停。

相关推荐
文祐2 小时前
C语言用双向链表实现单调递减(递增)队列
c语言·开发语言·链表
OOJO4 小时前
哈希表实现
数据结构·哈希算法·散列表
剑挑星河月6 小时前
234. 回文链表
java·数据结构·算法·leetcode·链表
六bring个六7 小时前
链表学习(常规链表)
数据结构·学习·链表
tachibana21 天前
hot100 回文链表(234)
java·网络·数据结构·leetcode·链表
aaaameliaaa1 天前
进制练习题【找出只出现一次的数字、交换两个变量(不创建临时变量)、统计二进制中1的个数、打印整数二进制的奇数位和偶数位、求两个数二进制中不同位的个数】
c语言·数据结构·笔记·算法
-dzk-1 天前
【系统架构设计师】案例分析篇
开发语言·数据结构·python·算法·架构·系统架构·架构设计
东华万里1 天前
第30篇 代码习惯 初学C与数据结构有感
c语言·数据结构·大学生专区
星空露珠1 天前
迷你世界UGc3.0脚本Wiki[剧情动画模块管理接口 Timeline]
开发语言·数据结构·算法·游戏·lua