数据结构:单链表

前言 

  本篇文章将讲解单链表的知识,包括:单链表的概念,单链表的结构为、实现单链表、链表的分类、单链表算法题知识的相关内容,为5大模块,为本章节知识的内容。(本篇文章数据结构:单链表(1)与数据结构:单链表(2)内容的总结)

一.单链表的概念

介绍

  在之前我们学习了逻辑结构和物理结构都是线性的顺序表,但是我们会发现顺序表有以下3个比较明显的缺陷:

  • 中间/头部的插入删除,时间复杂度为O(N)。
  • 增容需要申请新空间,拷贝数据,释放旧空间,有不小的消耗。
  • 增容一般呈两倍的增长,会有一定的空间浪费。

而链表可以很好的解决该问题:

首先,先介绍一下链表的基础知识:

  链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,即:逻辑顺序通过指针链接 的线性数据结构它由多个节点组成,每个节点包含 数据域(存储具体值)和 指针域(存储下一个节点的地址),通过指针域建立节点间的逻辑关系,形成线性序列(如 A→B→C→NULL)。

例:

  每个节点包含 数据域(存储具体值)和 指针域(存储下一个节点的地址)形象化来看:数据域方面用数字来表示,指针域方面用next,表示:

  图中指针变量list保存的是第⼀个结点的地址,我们称list此时“指向”第⼀个结点,如果我们希望 list“指向”第⼆个结点时,只需要修改plist保存的内容为next即可,链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。

  结构上非连续,非顺序,但它的逻辑结构还是线性的。我们可以把链表想象成火车的一节节车厢链接在一起,只不过不是通过下标来访问节点了,是通过每个节点的地址来访问下一个节点。

  所以,回到上文,链表刚好可以使头部插入与删除的时间复杂度为O(1),不需要增容也不存在空间的浪费。提高使用的效率。

二.单链表的结构

介绍

  简单来说:

单链表,链表是由一个一个节点组成的,它的节点由两个组成部分

  1. 数据域:保存的数据。
  2. 指针域:指针,存放的是下一个结点的地址。

根据前面的知识,我们可以得出链表的结构:

#include<stdio.h>
typedef int type;
typedef struct SListNode
{
    type data;
    struct SListNode* next;
} SListNode;
  • 当然,数据域的内容并不唯一,你也可以写其他或很多的成员的。
  • 当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。 当我们想要从第⼀个结点⾛到最后⼀个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了。

具体使用例子:

#include<stdio.h>
#include<stdlib.h>
typedef int type;
typedef struct SListNode
{
    type data;
    struct SListNode* next;
} SListNode;
int main()
{
    SListNode* node1=(SListNode*)malloc(sizeof(SListNode));
    SListNode* node2=(SListNode*)malloc(sizeof(SListNode));
    SListNode* node3=(SListNode*)malloc(sizeof(SListNode));
    SListNode* node4=(SListNode*)malloc(sizeof(SListNode));
    node1->data=1;
    node1->next=node2;
    node2->data=2;
    node2->next=node3;
    node3->data=3;
    node3->next=node4;
    node4->data=4;
    node4->next=NULL;
}

  对于该链表,如果想打印的话,则需要先讲解一下链表的打印,而每一个链表的节点均为malloc所得的结果,需要将申请的空间释放掉:所以接下来也会讲解一下链表的销毁函数。

链表的打印

单链表的打印是遍历链表并输出节点数据的基础操作,函数原形为:

void SLTPrint(SListNode* h);

代码实现为:

void SLTPrint(SListNode* h)
{
	if (h == NULL)
	{
		printf("链表为空,无值\n");
		return;
       }
	SListNode* p = h;
	while (p)
	{
		printf("%d  ", p->data);
		p = p->next;
	}
}

讲解:

核心逻辑解析

1. 空链表判断与处理

if (h == NULL) 
{ printf("链表为空,无值\n");
 return; 
}
  • 作用:检查链表是否为空(头指针 h 为 NULL 时,链表无节点)。
  • 处理逻辑
    • 若为空链表,打印提示信息 链表为空,无值,并通过 return 终止函数(避免后续无效操作)。
    • 为什么要 return
      若不终止,会继续执行后续的 p = h 和 while (p) 循环,但此时 p 为 NULL,循环不会执行,虽无语法错误,但逻辑冗余,提前返回更高效

2. 非空链表:遍历打印数据

SListNode* p = h;  // 定义临时指针 p,初始化为头指针 h(指向第一个节点)
while (p)  // 等价于 while (p != NULL):当 p 指向有效节点时循环
{
    printf("%d  ", p->data);  // 打印当前节点的数据域(假设 data 为 int 类型)
    p = p->next;  // p 移动到下一个节点(通过 next 指针)
}
  • 遍历逻辑
    • 临时指针 p:用于遍历链表,避免直接修改头指针 h(保护原始链表结构)。
    • 循环条件 while (p):当 p 不为 NULL 时,持续访问节点(p 指向 NULL 表示已到链表尾部)。
    • 打印数据:通过 p->data 获取当前节点的值,用空格分隔(%d )。

3.执行流程图解

开始
  ↓
判断 h 是否为 NULL?
  ├─ 是 → 打印"链表为空,无值" → 结束函数
  └─ 否 → 定义 p = h
       ↓
     while (p != NULL):
       ├─ 打印 p->data
       ├─ p = p->next(移动到下一个节点)
       └─ 重复循环,直到 p 为 NULL
  ↓
结束

链表的销毁

  单链表销毁的核心目标是 释放所有节点占用的堆内存,避免内存泄漏。

 函数原形为:

void SListDestroy(SListNode** h);

参数为二级指针的原因:

  我们知道,函数调用过程中,简单来说,实参的值会被拷贝给形参,形参与实参本质上是两个独立的变量,函数内对形参的修改不会影响实参。

若要通过函数修改外部变量的值,必须传递该变量的 地址。对于指针变量 head,其地址就是 二级指针

代码实现为:

void SListDestroy(SListNode** h)
{
	if (*h == NULL)
	{
		printf("链表为空\n");
		return;
    }
	SListNode* p = *h;
  while(p)
	{
	  SListNode* q = p;
	  p = p->next;
	  free(q);
	}
}

讲解  核心知识:

  • 变量 p 和 q 的分工
    • p:遍历指针,从 *h(头节点)开始,逐步移动到 NULL(链表尾)。
    • q:临时指针,用于暂存当前要释放的节点(q = p),避免 p 移动后丢失当前节点地址。
  • 循环条件 while (p):当 p 不为 NULL 时,继续遍历(即还有节点未释放)。
  • 释放顺序:必须先通过 p = p->next 保存下一个节点地址,再 free(q),否则释放 q 后,p->next 会变为无效地址(断链)。

根据上方的代码:

我们可以打印与销毁了

 SLTPrint(node1);
 SListDestroy(&node1);

结果:

三、实现单链表

接下来,我将对实现单链表的代码进行实现:

1.单链表的尾插

  我们学习过顺序表的知识了,也清楚,尾插入的逻辑,只不过单链表的尾插,并没有扩容两倍的需要,基本都是随插随申请值。

  我们链表使用是要通过头结点对后面节点进行访问的,你看图,可以明确得知有几个节点,节点的地址、值,但我们写代码时是看不到的,这是链表的“抽象性”与“内存不可见性” 问题——代码中无法直接“看到”链表的物理结构(如节点地址、实际节点数),只能通过头指针间接操作。

函数形式:

void SLTPushBack(SListNode** h, type x);

  由参数可知,在实现单链表的尾插之前,我们要先申请新节点,而后续头插等接口的实现也会用上,所以将其写为函数。

结点的创建

函数形式:

SListNode* SLTBuyNode(type x)

实现代码为:

SListNode* SLTBuyNode(type x)
{ 
	SListNode* p = (SListNode*)malloc(sizeof(SListNode));
	if (p)
	{
		p->data = x;
		p->next = NULL;
		return p;
	}
	else
	{
		perror("malloc failed");  
		return NULL;  
	}
}

  该函数用于动态创建单链表节点,分配内存并初始化数据域和指针域。

接下来,我们来实现下链表的尾插入:

void SLTPushBack(SListNode** h, type x)
{
	SListNode* p = SLTBuyNode(x);
	if (*h == NULL)
	{
		*h = p;
	}
	else
	{
		SListNode* pr = *h;
		while (pr->next)
		{
			pr = pr->next;
		}
		pr->next = p;
	}
}

注:

  • 参数1SListNode** h(头节点指针的地址)
    • 必须传递二级指针:因为若链表为空(*h == NULL),需要修改头指针本身(而非头指针指向的内容),此时一级指针无法实现(值传递特性)。
  • 参数2type x

函数中为什么用 *h
因为h 是二级指针,*h 才是头指针本身。直接修改 *h 会改变原链表的头指针地址。

else
{
    SListNode* pr = *h;  // 定义遍历指针,从头部开始
    while (pr->next)     // 循环条件:pr的next不为NULL(即未到尾节点)
    {
        pr = pr->next;   // 移动到下一个节点
    }
    pr->next = p;  // 尾节点的next指向新节点
}

else的逻辑:

pr 从头部出发,通过 pr = pr->next 移动,直到 pr->next == NULL(此时 pr 即为尾节点)。

以下图为例,链表的尾部插入,需要先遍历一遍已有的值,在pr->next == NULL为尾时停止遍历,

尾部之后插入新的值,时间复杂度O(N)。

2.单链表的头插

即为头部插入,与顺序表的整体向后移动一位不同,链表的实现较为简单,效率也高。

函数形式:

void SLTPushFront(SListNode** h, type x)

  在单链表的头部插入新节点,新节点成为新的头节点,原链表(若存在)则链接到新节点之后。

实现

void SLTPushFront(SListNode** h, type x)
{
	SListNode* newnode = SLTBuyNode(x);
	if (*h == NULL)
	{
		*h = newnode;
	}
	else
	{
		newnode->next = *h;
		*h = newnode;
	}
}

例:(*h不为空)情况:

  1. 创建新节点 newnodedata=0next=NULL)。
  2. newnode->next = *h:新节点的 next 指向原头节点,此时 newnode -> 1 -> 2 -> 3-> 4 -> NULL
  3. *h = newnode:头指针更新为 newnode,链表变为 0 -> 1 -> 2 -> 3 ->4 -> NULL

属于直接操作,时间复杂度为O(1);

3.单链表的尾删

  删除单链表的最后一个节点,并释放其内存,需处理链表为空、一个节点、多个节点的不同场景。

函数:

void SLTPopBack(SListNode** h)

void SLTPopBack(SListNode** h)
{
	if (*h == NULL)
	{
		return;
    }
	if ((*h)->next == NULL)
	{
		free(*h);
		*h = NULL;
	}
	else
	{
		SListNode* p = *h;
		SListNode* pr = *h;
		while (p->next)
		{
			 pr = p;
			p = p->next;
		}
		free(p->next);
		pr->next = NULL;
	}
}
场景处理流程
链表为空直接返回,无操作。
单节点链表释放头节点,*h = NULL(头指针置空)。
多节点链表遍历找到尾节点 p 和前节点 pr,释放 ppr->next = NULL
  • 首先是断言,链表不可以为空
  • 特别判断只有一个节点的情况,如果只有一个节点的话直接释放掉就好了
  • 找尾节点的同时找到尾节点的前一个节点,每次尾节点向前走之前,先让pr指向其原来的位置
  • 最后直接让pr->next=NULL,释放掉p就好了。

4.单链表的头删

  单链表的头删是指删除链表的第一个节点,核心目标是 释放头节点内存 + 更新头指针,需处理空链表、单节点链表等边界情况。

void SLTPopFront(SListNode** h)

void SLTPopFront(SListNode** h)
{
	if (*h == NULL)
	{
		return;
	}
		SListNode* p = (*h)->next;
		free(*h);
		*h = p;

}

讲解:

void SLTPopFront(SListNode** h) {
    // 1. 空链表检查:若头指针指向 NULL(链表为空),直接返回
    if (*h == NULL) {
        return;
    }
    
    // 2. 保存原头节点的下一个节点地址(避免释放后丢失后续链表)
    SListNode* p = (*h)->next;  // p 指向原头节点的 next(新头节点)
    
    // 3. 释放原头节点内存,并更新头指针
    free(*h);       // 释放原头节点(*h 是指向头节点的指针)
    *h = p;         // 头指针指向 p(新头节点,可能为 NULL)
}

这里主要就是先定义一个中间变量记录头的下一个节点,再直接free掉头节点。最后让中间变量成为新的头节点就可以了。

接下来我将列出练习的代码,和示例:

代码

  代码我分三个文件写的分别是 1.h 1.cpp main.cpp

1.h

#include<stdio.h>
#include<stdlib.h>
typedef int type;
typedef struct SListNode
{
    type data;
    struct SListNode* next;
}SListNode;

void SLTPrint(SListNode* h);
void SListDestroy(SListNode** h);
void SLTPushBack(SListNode** h, type x);
void SLTPushFront(SListNode** h, type x);
void SLTPopBack(SListNode** h);
void SLTPopFront(SListNode** h);
SListNode* SLTBuyNode(type x);

1.cpp

#include"1.h"
void SLTPrint(SListNode* h)
{
	if (h == NULL)
	{
		printf("链表为空,无值\n");
		return;
       }
	SListNode* p = h;
	while (p)
	{
		printf("%d  ", p->data);
		p = p->next;
	}
	printf("\n");
}
void SListDestroy(SListNode** h)
{
	if (*h == NULL)
	{
		printf("链表为空\n");
		return;
    }
	SListNode* p = *h;
  while(p)
	{
	  SListNode* q = p;
	  p = p->next;
	  free(q);
	}
}
SListNode* SLTBuyNode(type x)
{ 
	SListNode* p = (SListNode*)malloc(sizeof(SListNode));
	if (p)
	{
		p->data = x;
		p->next = NULL;
		return p;
	}
	else
	{
		perror("malloc failed");  
		return NULL;  
	}
}
void SLTPushBack(SListNode** h, type x)
{
	SListNode* p = SLTBuyNode(x);
	if (*h == NULL)
	{
		*h = p;
	}
	else
	{
		SListNode* pr = *h;
		while (pr->next)
		{
			pr = pr->next;
		}
		pr->next = p;
	}
}
void SLTPushFront(SListNode** h, type x)
{
	SListNode* newnode = SLTBuyNode(x);
	if (*h == NULL)
	{
		*h = newnode;
	}
	else
	{
		newnode->next = *h;
		*h = newnode;
	}
}
void SLTPopBack(SListNode** h)
{
	if (*h == NULL)
	{
		return;
    }
	if ((*h)->next == NULL)
	{
		free(*h);
		*h = NULL;
	}
	else
	{
		SListNode* p = *h;
		SListNode* pr = *h;
		while (p->next)
		{
			 pr = p;
			p = p->next;
		}
		free(p);
		pr->next = NULL;
	}
}
void SLTPopFront(SListNode** h)
{
	if (*h == NULL)
	{
		return;
	}
		SListNode* p = (*h)->next;
		free(*h);
		*h = p;

}

main.cpp

#include"1.h"
void test()
{
    SListNode* node1 = (SListNode*)malloc(sizeof(SListNode));
    SListNode* node2 = (SListNode*)malloc(sizeof(SListNode));
    SListNode* node3 = (SListNode*)malloc(sizeof(SListNode));
    SListNode* node4 = (SListNode*)malloc(sizeof(SListNode));
    node1->data = 1;
    node1->next = node2;
    node2->data = 2;
    node2->next = node3;
    node3->data = 3;
    node3->next = node4;
    node4->data = 4;
    node4->next = NULL;
    SLTPrint(node1);
    SListDestroy(&node1);
}
void test2()
{
    SListNode* h=NULL;
    SLTPushBack(&h, 10);
    SLTPushBack(&h, 20);
    SLTPrint(h);
    SLTPushFront(&h, 30);
    SLTPushFront(&h, 40);
    SLTPrint(h);
    SLTPopBack(&h);
    SLTPrint(h);
    SLTPopFront(&h);
    SLTPrint(h);
    SListDestroy(&h);

}
int main()
{
    test2();
}

结果为:

 SListNode* h=NULL;
 SLTPushBack(&h, 10);    //10
 SLTPushBack(&h, 20);   //10 20
 SLTPrint(h);
 SLTPushFront(&h, 30);   // 30 10 20
 SLTPushFront(&h, 40);   // 40 30 10 20
 SLTPrint(h);
 SLTPopBack(&h);        //40 30 10
 SLTPrint(h);
 SLTPopFront(&h);       //30 10
 SLTPrint(h);
 SListDestroy(&h);

  

 5.链表节点查找

在链表中查找节点是基础操作,核心目标是根据指定条件(如值匹配)定位目标节点,本函数以值匹配作为条件:

函数形式:

SListNode* SLTFind(SListNode* h, type x)

代码实现为:

SListNode* SLTFind(SListNode* h, type x)
{
	SListNode* p = h;
	while (p)
	{
		if (p->data == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}

讲解:

函数参数:

  • SListNode* h:指向单链表头节点的指针。
  • type x:要查找的值,type 是一个泛型类型,可以是任意数据类型。

函数返回值:

  • 如果找到了值为 x 的节点,则返回指向该节点的指针。
  • 如果没有找到,则返回 NULL

函数实现步骤:

  1. 初始化一个指针 p,并将其指向链表的头节点 h
  2. 使用 while 循环遍历链表,只要 p 不为 NULL,就继续循环。
  3. 在循环内部,检查当前节点 p 的数据 p->data 是否等于要查找的值 x
  4. 如果相等,则返回当前节点的指针 p
  5. 如果不相等,则将 p 指向下一个节点,继续循环。
  6. 如果遍历完整个链表都没有找到值为 x 的节点,则返回 NULL

简单来说:如果找到对应的元素,返回指针,找不到就返回空。

6.链表在指定位置之前或之后插入元素

(1)链表在指定位置之前插入元素

函数形式:

void SLTInsert(SListNode** h, SListNode* pos, type x)

代码实现为:

void SLTInsert(SListNode** h, SListNode* pos, type x)
{
	if (h == NULL || pos == NULL||*h == NULL)
	{
		printf("前插入失败\n");
		return;
	}
	if (*h == pos)
	{
		SLTPushFront(h, x);
		return;
	}
	SListNode* p = *h;
	while (p!=NULL&&p->next != pos)
	{
		p = p->next;
	}
	if (p)
	{
SListNode* newnode = SLTBuyNode(x);
	 newnode->next=pos;
	p->next=newnode ;
	}
	else
	{
		printf("没有找到该节点\n");
		return;
	}
}

讲解:


// 1. 边界条件检查(避免无效操作)
if (h == NULL || pos == NULL || *h == NULL)

    // 2. 特殊情况:pos是头节点 → 直接调用头插函数
    if (*h == pos)
    {
        SLTPushFront(h, x);
        return;
    }

// 3. 遍历链表,找pos的前驱节点p
SListNode* p = *h;
while (p != NULL && p->next != pos)
{
    p = p->next;
}

// 4. 根据p是否存在,决定插入或报错
if (p)    // p存在(pos在链表中)→ 执行插入
{
    SListNode* newnode = SLTBuyNode(x);  // 创建新节点
    newnode->next = pos;                 // 新节点连接pos
    p->next = newnode;                   // 前驱p连接新节点
}
else      // p不存在(pos不在链表中)→ 报错
{
    printf("没有找到该节点\n");
    return;
}
}

为什么这两步不可颠倒?
若先执行 p->next = newnode,会丢失 pos 的地址(p->next 原指向 pos),导致无法将 newnode->next 连接到 pos

插入之后的图示:

举实例:

首先操作的代码:

1.新用到的函数:

SListNode* SLTFind(SListNode* h, type x)
{
	SListNode* p = h;
	while (p)
	{
		if (p->data == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}
void SLTInsert(SListNode** h, SListNode* pos, type x)
{
	if (h == NULL || pos == NULL||*h == NULL)
	{
		printf("前插入失败\n");
		return;
	}
	if (*h == pos)
	{
		SLTPushFront(h, x);
		return;
	}
	SListNode* p = *h;
	while (p!=NULL&&p->next != pos)
	{
		p = p->next;
	}
	if (p)
	{
SListNode* newnode = SLTBuyNode(x);
	 newnode->next=pos;
	p->next=newnode ;
	}
	else
	{
		printf("没有找到该节点\n");
		return;
	}
}

2.示例代码:

#include"1.h"
void test2()
{
    SListNode* h=NULL;
    SLTPushBack(&h, 10);    //10
    SLTPushBack(&h, 20);   //10 20
    SLTPrint(h);
    SLTPushFront(&h, 30);   // 30 10 20
    SLTPushFront(&h, 40);   // 40 30 10 20
    SLTPrint(h);
    SLTPopBack(&h);        //40 30 10
    SLTPrint(h);
    SLTPopFront(&h);       //30 10
    SLTPrint(h);
    SListNode *p=SLTFind(h, 10);
    if (p)
    {
        printf("%d\n", p->data);   //10
    }
    SLTInsert(&h, p, 1000);
    SLTPrint(h);            //30  1000  10
    SListDestroy(&h);

}
int main()
{
    test2();
}

(2)链表在指定位置之后插入元素

函数形式:

void SLTInsertAfter(SListNode* pos, type x)

代码实现为:

void SLTInsertAfter(SListNode* pos, type x)
{
	if (pos == NULL)
	{
		printf("后插入失败\n");
		return;
	}
	SListNode* p = pos->next;
	SListNode* newnode = SLTBuyNode(x);
	newnode->next = p;
	pos->next = newnode;
}

讲解:

函数核心逻辑(一句话总结)

在 pos 节点后插入新节点,本质是“切断 pos 与原后继节点的连接,插入新节点作为 pos 的新后继”
流程:

  1. 检查 pos 是否有效 → 2. 保存 pos 原后继节点 → 3. 创建新节点 → 4. 新节点连接原后继 → 5. pos 连接新节点。

代码逐行深度解析

1. 边界条件检查

if (pos == NULL) { printf("后插入失败\n"); return; }
  • 作用pos 是插入位置的基准节点,若 pos 为空(无效),直接报错并返回,避免后续 pos->next 导致空指针崩溃。
  • 对比前插函数:后插无需检查头节点(h 或 *h),因为插入位置由 pos 直接确定,与头节点无关。

2. 保存 pos 的原后继节点

SListNode* p = pos->next;  // p暂存pos原本的下一个节点
  • 必要性:若不保存 pos->next,后续 pos->next = newnode 会覆盖原后继地址,导致链表断裂(原后继节点永久丢失)。
  • 类比:相当于搬家时先把“当前位置的下一个箱子”挪开,腾出空间放新箱子。

3. 创建新节点

SListNode* newnode = SLTBuyNode(x);  // 假设SLTBuyNode已实现:分配内存+初始化数据
  • 核心功能:动态申请新节点内存,并将数据 x 存入新节点。
  • 注意:实际工程中需检查 newnode 是否为 NULL(内存分配失败),此处简化未展开。

4. 连接新节点与原后继

newnode->next = p;  // 新节点的next指向pos原本的后继p
  • 图示
    原链表:pos ——→ p ——→ ... 执行后:newnode ——→ p ——→ ...

5. pos 连接新节点(完成插入)

pos->next = newnode;  // pos的next指向新节点,新节点成为pos的直接后继
  • 最终链表结构

7.链表在指定位置删除或指定位置之后删除

(1)链表在指定位置删除

单链表删除指定位置节点包括 “删除指定节点 pos 和 “删除第 n 个节点” 两种场景,删除第n个节点可通过循环来实现,而删除指定节点 pos较为复杂,所以,本文选取讲解删除指定节点 pos。

void SLTErase(SListNode** h, SListNode* pos)

具体代码:

void SLTErase(SListNode** h, SListNode* pos)
{
	if (h == NULL || *h == NULL || pos == NULL) 
	{
		return;  
	}
	if (pos == *h)
	{
		SLTPopFront(h);
	}
	else
	{SListNode* p = *h;
	while (p != NULL && p->next != pos)
	{
		p = p->next;
	}
	if (p == NULL)
	{
		printf("没有该节点\n");
		return;
	}
	else
	{
		p->next = pos->next;
		free(pos);
	}	}
}

讲解:

void SLTErase(SListNode** h, SListNode* pos) {
    // 前置过滤:空指针或空链表
    if (h == NULL || *h == NULL || pos == NULL) {
        return;  
    }

    // 分支1:删除头节点 → 调用头删函数
    if (pos == *h) {
        SLTPopFront(h);  // 头删后自动退出(无需显式return,因无后续代码)
    } 
    // 分支2:删除中间/尾节点 → 找前驱+删除
    else {  
        SListNode* p = *h;
        while (p != NULL && p->next != pos) {  // 遍历找pos的前驱
            p = p->next;
        }
        if (p == NULL) {  // 未找到pos节点
            printf("没有该节点\n");
            return;
        } else {  // 找到前驱,执行删除
            p->next = pos->next;
            free(pos);
        }
    }
}

核心逻辑:分场景处理的底层原理

1. 分支1:删除头节点(pos == *h

  • 触发条件:待删除节点 pos 就是头节点(*h 是头节点指针)。
  • 处理方式:调用 SLTPopFront(h),假设该函数实现如下(标准头删逻辑):
    void SLTPopFront(SListNode** h) {
        SListNode* tmp = *h;  // 保存旧头节点
        *h = (*h)->next;      // 更新头指针为下一个节点
        free(tmp);            // 释放旧头节点内存
    }
    
  • 为什么无需 return
    因 if 分支执行后,函数直接进入 else 分支或结束(无 else 时),当前代码通过 else 确保头删后不会执行后续遍历逻辑。

2. 分支2:删除中间/尾节点(pos != *h

  • 核心目标:找到 pos 的 前驱节点 p(即 p->next == pos),通过修改 p->next 跳过 pos 节点。
  • 遍历逻辑
    SListNode* p = *h;  // 从新头节点开始遍历(若头节点未删除)
    while (p != NULL && p->next != pos) {
        p = p->next;
    }
    
    • 正常终止p->next == pos → 找到前驱 p,执行 p->next = pos->next(切断 pos 节点)。
    • 异常终止p == NULL → 遍历到链表尾仍未找到 pospos 不在链表中),打印提示并返回。

边界场景:覆盖所有可能情况

场景代码行为
空链表(*h == NULL前置条件过滤,直接 return,无操作。
删除头节点(唯一节点)pos == *h → 调用 SLTPopFront → *h 变为 NULL,链表为空。
删除头节点(多节点)*h 更新为原 *h->next,旧头节点被释放,链表长度-1。
删除尾节点pos->next == NULL → p->next = NULLp 变为新尾节点),pos 被释放。
pos 不在链表中遍历后 p == NULL → 打印“没有该节点”,不修改链表。
pos 为中间节点p->next = pos->next → 链表跳过 pospos 内存被释放。

(2)链表在指定位置之后删除

操作位删除 pos 节点的下一个节点(若 pos 是尾节点或链表为空,则不操作)。

  • 删除单链表中 pos 节点之后 的第一个节点(即删除 pos->next)。
  • 适用场景:需删除指定位置后续节点时使用(注意:不能删除头节点前的节点,也不能直接删除 pos 自身)。

函数形式:

void SLTEraseAfter(SListNode* pos)

代码实现为:

void SLTEraseAfter(SListNode* pos)
{
	if (pos==NULL || pos->next == NULL)
	{
		printf("不可删后节点\n");
		return;
    }
	SListNode* p = pos->next;
	pos->next = p->next;
	free(p);
}

讲解:

void SLTEraseAfter(SListNode* pos)  // 参数:指向目标节点的指针 pos
{
    // 1. 边界条件判断:过滤无效删除场景
    if (pos == NULL || pos->next == NULL)  // 两种不可删除的情况
    {
        printf("不可删后节点\n");  // 错误提示
        return;                    // 直接返回,不执行删除
    }

    // 2. 保存待删除节点的地址
    SListNode* p = pos->next;  // p 指向 pos 的下一个节点(即要删除的节点)

    // 3. 修改指针:跳过待删除节点,维持链表连续性
    pos->next = p->next;       // pos 的 next 指向 p 的下一个节点(切断与 p 的连接)

    // 4. 释放待删除节点的内存(避免内存泄漏)
    free(p);                   // 释放 p 指向的节点内存
}

边界条件 if (pos == NULL || pos->next == NULL) 的必要性

条件含义风险
pos == NULLpos 是空指针(未指向任何节点)直接访问 pos->next 会导致 空指针异常(崩溃)
pos->next == NULLpos 是链表的 尾节点尾节点没有后续节点,删除操作无意义
  • 结论:两种情况均需提前拦截,避免程序崩溃或无效操作。

SListNode* p = pos->next;   // 步骤1:用 p 暂存待删除节点地址  
pos->next = p->next;        // 步骤2:pos 直接指向 p 的下一个节点(链表"跳过" p)  
free(p);                    // 步骤3:释放 p 指向的内存(避免内存泄漏)  

四、举实例,测试代码(包括所有代码展现)

首先操作的代码,我分为三个文件所写,接下来,我将展示代码内容:

1.h

#include<stdio.h>
#include<stdlib.h>
typedef int type;
typedef struct SListNode
{
    type data;
    struct SListNode* next;
}SListNode;

void SLTPrint(SListNode* h);
void SListDestroy(SListNode** h);
void SLTPushBack(SListNode** h, type x);
void SLTPushFront(SListNode** h, type x);
void SLTPopBack(SListNode** h);
void SLTPopFront(SListNode** h);
SListNode* SLTBuyNode(type x);
SListNode* SLTFind(SListNode* h, type x);
void SLTInsert(SListNode** h, SListNode* pos, type x);
void SLTInsertAfter(SListNode* pos, type x);
void SLTErase(SListNode** h, SListNode* pos);
void SLTEraseAfter(SListNode* pos);

1.cpp

#include"1.h"
void SLTPrint(SListNode* h)
{
	if (h == NULL)
	{
		printf("链表为空,无值\n");
		return;
       }
	SListNode* p = h;
	while (p)
	{
		printf("%d  ", p->data);
		p = p->next;
	}
	printf("\n");
}
void SListDestroy(SListNode** h)
{
	if (*h == NULL)
	{
		printf("链表为空\n");
		return;
    }
	SListNode* p = *h;
  while(p)
	{
	  SListNode* q = p;
	  p = p->next;
	  free(q);
	}
}
SListNode* SLTBuyNode(type x)
{ 
	SListNode* p = (SListNode*)malloc(sizeof(SListNode));
	if (p)
	{
		p->data = x;
		p->next = NULL;
		return p;
	}
	else
	{
		perror("malloc failed");  
		return NULL;  
	}
}
void SLTPushBack(SListNode** h, type x)
{
	SListNode* p = SLTBuyNode(x);
	if (*h == NULL)
	{
		*h = p;
	}
	else
	{
		SListNode* pr = *h;
		while (pr->next)
		{
			pr = pr->next;
		}
		pr->next = p;
	}
}
void SLTPushFront(SListNode** h, type x)
{
	SListNode* newnode = SLTBuyNode(x);
	if (*h == NULL)
	{
		*h = newnode;
	}
	else
	{
		newnode->next = *h;
		*h = newnode;
	}
}
void SLTPopBack(SListNode** h)
{
	if (*h == NULL)
	{
		return;
    }
	if ((*h)->next == NULL)
	{
		free(*h);
		*h = NULL;
	}
	else
	{
		SListNode* p = *h;
		SListNode* pr = *h;
		while (p->next)
		{
			 pr = p;
			p = p->next;
		}
		free(p);
		pr->next = NULL;
	}
}
void SLTPopFront(SListNode** h)
{
	if (*h == NULL)
	{
		return;
	}
		SListNode* p = (*h)->next;
		free(*h);
		*h = p;

}
SListNode* SLTFind(SListNode* h, type x)
{
	SListNode* p = h;
	while (p)
	{
		if (p->data == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}
void SLTInsert(SListNode** h, SListNode* pos, type x)
{
	if (h == NULL || pos == NULL||*h == NULL)
	{
		printf("前插入失败\n");
		return;
	}
	if (*h == pos)
	{
		SLTPushFront(h, x);
		return;
	}
	SListNode* p = *h;
	while (p!=NULL&&p->next != pos)
	{
		p = p->next;
	}
	if (p)
	{
SListNode* newnode = SLTBuyNode(x);
	 newnode->next=pos;
	p->next=newnode ;
	}
	else
	{
		printf("没有找到该节点\n");
		return;
	}
}
void SLTInsertAfter(SListNode* pos, type x)
{
	if (pos == NULL)
	{
		printf("后插入失败\n");
		return;
	}
	SListNode* p = pos->next;
	SListNode* newnode = SLTBuyNode(x);
	newnode->next = p;
	pos->next = newnode;
}
void SLTErase(SListNode** h, SListNode* pos)
{
	if (h == NULL || *h == NULL || pos == NULL) 
	{
		return;  
	}
	if (pos == *h)
	{
		SLTPopFront(h);
	}
	else
	{SListNode* p = *h;
	while (p != NULL && p->next != pos)
	{
		p = p->next;
	}
	if (p == NULL)
	{
		printf("没有该节点\n");
		return;
	}
	else
	{
		p->next = pos->next;
		free(pos);
	}	}
}
void SLTEraseAfter(SListNode* pos)
{
	if (pos==NULL || pos->next == NULL)
	{
		printf("不可删后节点\n");
		return;
    }
	SListNode* p = pos->next;
	pos->next = p->next;
	free(p);
}

main.cpp

#include"1.h"
void test2()
{
    SListNode* h=NULL;
    SLTPushBack(&h, 10);    //10
    SLTPushBack(&h, 20);   //10 20
    SLTPrint(h);
    SLTPushFront(&h, 30);   // 30 10 20
    SLTPushFront(&h, 40);   // 40 30 10 20
    SLTPrint(h);
    SLTPopBack(&h);        //40 30 10
    SLTPrint(h);
    SLTPopFront(&h);       //30 10
    SLTPrint(h);
    SListNode *p=SLTFind(h, 10);
    if (p)
    {
        printf("%d\n", p->data);   //10
    }
    SLTInsert(&h, p, 1000);
    SLTPrint(h);            //30  1000  10
    SListNode* q = SLTFind(h, 30);
    if (q)
    {
        printf("%d\n", q->data);   //30
    }
    SLTErase(&h, q);
    SLTPrint(h);            // 1000  10
    SListDestroy(&h);

}
int main()
{
    test2();
}

结果:

五、链表的分类

链表是一种动态数据结构,通过指针/引用连接节点,根据节点结构和连接方式可分为以下几类:

按节点连接方向分类
  • 单链表(Singly Linked List)

    • 结构:每个节点含 数据域 和 一个指针域(指向下一节点)。
    • 特点:只能从表头向表尾遍历,插入/删除需修改前驱节点指针。
    • 图示

  • 双链表(Doubly Linked List)

    • 结构:每个节点含 数据域 和 两个指针域(前驱指针prev和后继指针next)。
    • 特点:可双向遍历,插入/删除无需回溯前驱节点,但内存开销略大。
    • 图示

  • 循环链表(Circular Linked List)

    • 结构:尾节点指针指向头节点(单循环)或头节点前驱指向尾节点(双循环)。
    • 特点:可从任意节点开始遍历,适合实现环形队列、约瑟夫问题等。
    • 图示

按是否有头节点分类
  • 带头节点链表

    • 结构:首节点为 头节点(不存储数据或存链表长度),后续为数据节点。
    • 优势:统一空链表和非空链表的操作逻辑(无需单独处理头节点插入/删除)。
    • 图示

  • 不带头节点链表

    • 结构:首节点直接存储数据。
    • 劣势:插入/删除首节点时需特殊处理(修改头指针)。
    • 图示

从上面讲解的类型中,我们可以总结一下:

链表的结构多样,总共能组合出来8种类型:

虽然链表的类型有很多种,但他们的结构体类型基本一样,都具有数据域和指针域两部分,根据上面的知识,我们可以得出本单链表为单向不带头不循环链表,而下篇文章,我将会讲解双向带头不循环链表,简称双链表,敬请期待。

  总结

  以上就是今天要讲的内容,本篇文章涉及的知识点为:单链表的概念,单链表的结构为、实现单链表、链表的分类知识的相关内容的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,下篇文章,我将会讲解双向带头不循环链表,简称双链表,接下来的内容我会很快更新。

评论 79
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值