数据结构:单链表(2)

目录

前言

一、实现单链表

介绍:

1.链表节点查找

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

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

举实例:

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

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

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

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

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

1.h

1.cpp

main.cpp

四、链表的分类

按节点连接方向分类

按是否有头节点分类

总结


前言

上篇文章讲解了单链表的知识, 包括:单链表的概念,单链表的结构、实现单链表(单链表的尾插、单链表的头插、单链表的尾删、单链表的头删)知识的相关内容,实现单链表其余的函数、链表的分类、单链表算法题知识的相关内容,为本章节知识的内容。

一、实现单链表

介绍:

1.链表节点查找

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

函数形式:

SListNode* SLTFind(SListNode* h, type x)

代码实现为:

cpp 复制代码
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

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

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

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

函数形式:

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

代码实现为:

cpp 复制代码
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;
	}
}

讲解:

cpp 复制代码
// 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.新用到的函数:

cpp 复制代码
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.示例代码:

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
    SListDestroy(&h);

}
int main()
{
    test2();
}

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

函数形式:

void SLTInsertAfter(SListNode* pos, type x)

代码实现为:

cpp 复制代码
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. 边界条件检查

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

2. 保存 pos 的原后继节点

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

3. 创建新节点

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

4. 连接新节点与原后继

cpp 复制代码
newnode->next = p;  // 新节点的next指向pos原本的后继p
  • 图示

    复制代码
    原链表:pos ——→ p ——→ ... 执行后:newnode ——→ p ——→ ...

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

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

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

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

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

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

具体代码:

cpp 复制代码
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);
	}	}
}

讲解:

cpp 复制代码
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),假设该函数实现如下(标准头删逻辑):

    cpp 复制代码
    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 节点。

  • 遍历逻辑

    cpp 复制代码
    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 == NULLp->next = NULLp 变为新尾节点),pos 被释放。
pos 不在链表中 遍历后 p == NULL → 打印"没有该节点",不修改链表。
pos 为中间节点 p->next = pos->next → 链表跳过 pospos 内存被释放。

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

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

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

函数形式:

void SLTEraseAfter(SListNode* pos)

代码实现为:

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

讲解:

cpp 复制代码
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 == NULL pos 是空指针(未指向任何节点) 直接访问 pos->next 会导致 空指针异常(崩溃)
pos->next == NULL pos 是链表的 尾节点 尾节点没有后续节点,删除操作无意义
  • 结论:两种情况均需提前拦截,避免程序崩溃或无效操作。

SListNode* p = pos->next; // 步骤1:用 p 暂存待删除节点地址

pos->next = p->next; // 步骤2:pos 直接指向 p 的下一个节点(链表"跳过" p)

free(p); // 步骤3:释放 p 指向的内存(避免内存泄漏)

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

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

1.h

cpp 复制代码
#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

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

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种类型:

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

总结

以上就是今天要讲的内容,本篇文章涉及的知识点为:实现单链表其余的函数、举实例,测试代码(包括所有代码展现)、链表的分类等知识 的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,下篇文章,我将会讲解**双向带头不循环链表,简称双链表,**接下来的内容我会很快更新。

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa9 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio