单链表 -->增、删、查、改等详细操作

个人主页流年如梦

专栏《C语言》 《数据结构》

文章目录

Ladies and gentlemen,本篇文章先了解一下链表的概念和结构,其中主要学习 单链表的实现(重点) ;全程高能,不容错过!!!

前言

链表是线性表的链式存储结构,采用非连续物理空间,通过指针链接节点,解决了顺序表插入删除效率低、空间浪费的问题。单链表作为最基础的链表结构,仅支持单向遍历,本章采用模块化编程实现其核心接口,为后续复杂链表打下基础

一.链表的概念及结构

1.1概念

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

1.2打个比方(火车类比)

  1. 链表像一列火车,每节车厢独立存在。
  2. 增加或删除车厢不会影响其他车厢。
  3. 每节车厢 = 一个节点

节点的组成

每个节点包含两部分:
数据域 --> 存储当前节点的数据
指针域 --> 存储下一个节点的地址

1.3结构体定义

c 复制代码
struct SListNode
{
    int data;
    struct SListNode* next;
};

🧐分析 :其中data是存放要保存的数据SListNode* next存放下一个节点的地址

1.4特点

  1. 逻辑上连续,物理空间不一定连续
  2. 节点都是从堆区malloc申请的
  3. 每次申请的节点空间可能连续,也可能不连续

二.单链表的实现

2.1头文件声明 --> SList.h

参考代码如下:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

//单链表节点结构
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
} SLTNode;

// 打印
void SLTPrint(SLTNode* phead);

//创建新节点
SLTNode* BuyNode(SLTDataType x);

//尾插头插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//尾删头删
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

//在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后的节点
void SLTEraseAfter(SLTNode* pos);

//销毁
void SListDestroy(SLTNode** pphead);

2.2源文件实现 --> SList.c

2.2.1销毁

c 复制代码
void SListDestroy(SLTNode** pphead)
{
    SLTNode* pcur = *pphead;
    while (pcur != NULL)
    {
        SLTNode* next = pcur->next;
        free(pcur);
        pcur = next;
    }
    *pphead = NULL;
}

🧐分析逐个节点释放 ,不能直接释放头,会内存泄漏;每次保存next,再释放当前节点;最后把头指针置NULL,避免野指针

2.2.2打印

c 复制代码
void SLTPrint(SLTNode* phead)
{
    SLTNode* pcur = phead;
    while (pcur != NULL)
    {
        printf("%d->", pcur->data);
        pcur = pcur->next;
    }
    printf("NULL\n");
}

🧐分析 :用临时指针 cur = phead遍历,不改变原头指针while(cur != NULL)走到空停止;printf依次打印每个节点的值;再用cur = cur->next走到下一个节点;最后打印NULL,结束

2.2.3创建新节点

c 复制代码
SLTNode* BuyNode(SLTDataType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

🧐分析 :用malloc堆区 申请一个节点大小;接着newnode->data = x把数据存入节点;然后newnode->next = NULL让新节点暂时不指向任何节点;最后返回新节点地址,供插入函数使用

2.2.4尾插尾删

尾插(在最后面加节点):

c 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    SLTNode* newnode = BuyNode(x);

    if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    else
    {
        SLTNode* tail = *pphead;
        while (tail->next != NULL)
        {
            tail = tail->next;
        }
        tail->next = newnode;
    }
}

🧐分析 :之所以pphead是二级指针是因为要修改头指针本身,必须传二级指针 ;再调用BuyNode创建新节点;如果为空链表,则让*ppheadNULL,直接让头指向新节点;如果为非空链表,则用tail找尾节点即tail->next == NULL,再把尾节点的next指向新节点,完成链接

尾删(删除最后一个节点):

c 复制代码
void SLTPopBack(SLTNode** pphead)
{
    if (*pphead == NULL)
        return;

    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLTNode* prev = NULL;
        SLTNode* tail = *pphead;
        while (tail->next != NULL)
        {
            prev = tail;
            tail = tail->next;
        }
        free(tail);
        prev->next = NULL;
    }
}

🧐分析 :如果是空链表直接返回 ,因为没有节点可以删;当只有一个节点 ,则直接释放并置空NULL;若有多个节点 ,先找尾节点tail,同时记录前驱pre;再释放尾节点;最后把前驱的next`置空,使其成为新尾

2.2.5头插头删

头插(在最前面加节点):

c 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    SLTNode* newnode = BuyNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}

🧐分析 :首先创建新节点 ,而且新节点指向原来的头节点 ,保证链表不断;最后更新头指针,让新节点变成新头

头删(删除第一个节点):

c 复制代码
void SLTPopFront(SLTNode** pphead)
{
    if (*pphead == NULL)
        return;

    SLTNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;
}

🧐分析 :如果是空链表直接返回 ;若为非空链表,则先保存第二个节点地址 ,防止释放头后找不到后续节点,然后释放头节点最后更新头指针,指向第二个节点

2.2.6在pos之前与之后插入

在pos之前插入:

c 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
    if (pos == *pphead)
    {
        SLTPushFront(pphead, x);
        return;
    }

    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
        prev = prev->next;
    }

    SLTNode* newnode = BuyNode(x);
    prev->next = newnode;
    newnode->next = pos;
}

🧐分析 :因为pos是头节点 ,所以直接调用头插;接着pos的前驱节点prev ,必须找到前一个才能插入;再创建新节点;使前驱指向新节点最后新节点指向pos,完成插入

在pos之后插入():

c 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
    SLTNode* newnode = BuyNode(x);
    newnode->next = pos->next;
    pos->next = newnode;
}

🧐分析 :新节点先指向pos的下一个节点;再让pos指向新节点,完成插入;因为不用找前驱perv,所以效率更高

2.2.7删除pos节点

c 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    if (*pphead == pos)
    {
        SLTPopFront(pphead);
        return;
    }

    SLTNode* prev = *pphead;
    while (prev->next != pos)
    {
        prev = prev->next;
    }

    prev->next = pos->next;
    free(pos);
}

🧐分析 :因为pos是头节点 ,所以直接调用头删再找前驱prev ,通过前驱跨过posprev->next = pos->next;最后释放pos,以避免内存泄漏

2.2.8查找

常用于定位插入或删除位置:

c 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
    SLTNode* pcur = phead;
    while (pcur != NULL)
    {
        if (pcur->data == x)
            return pcur;
        pcur = pcur->next;
    }
    return NULL;
}

🧐分析先遍历链表 ,找到data == x后返回该节点地址;如果遍历结束还没有没找到 ,则返回NULL

2.3主函数 --> test.c

参考代码如下:

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

void TestSList()
{
	SLTNode* plist = NULL;

	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	printf("尾插后:");
	SLTPrint(plist);

	//头插
	SLTPushFront(&plist, 0);
	printf("头插后:");
	SLTPrint(plist);

	//尾删
	SLTPopBack(&plist);
	printf("尾删后:");
	SLTPrint(plist);

	//头删
	SLTPopFront(&plist);
	printf("头删后:");
	SLTPrint(plist);

	//查找
	SLTNode* pos = SLTFind(plist, 2);
	if (pos)
	{
		//在pos之前插入
		SLTInsert(&plist, pos, 99);
		printf("在2之前插入99:");
		SLTPrint(plist);

		//删除pos
		SLTErase(&plist, pos);
		printf("删除节点2:");
		SLTPrint(plist);
	}

	//后插、后删
	pos = SLTFind(plist, 1);
	if (pos)
	{
		SLTInsertAfter(pos, 66);
		printf("在1后插入66:");
		SLTPrint(plist);

		SLTEraseAfter(pos);
		printf("删除1后的节点:");
		SLTPrint(plist);
	}

	//销毁
	SListDestroy(&plist);
	printf("销毁后:");
	SLTPrint(plist);
}

int main()
{
	TestSList();

	return 0;
}

运行结果

🎯总结

  1. 单链表是物理非连续、逻辑连续 的线性表,通过指针链接节点,每个节点包含数据域指针域
  2. 单链表节点从堆区malloc申请,按需创建,无扩容开销,无空间浪费
  3. 头插、头删时间复杂度为O(1),尾插、尾删、指定位置操作需遍历,时间复杂度O(N)
  4. 实现采用分文件编程,.h声明、.c实现、test.c测试,结构清晰规范
  5. 解决了顺序表头部或中间插入删除低效、扩容消耗大、空间浪费的缺陷
  6. 不支持随机访问,访问任意节点只能从头遍历

⚠️易错点

  1. 传参不使用二级指针,导致头指针修改无效
  2. 遍历修改时不保存next指针,造成链表断裂
  3. 删除或插入不判断空链表、边界条件
  4. 释放节点后不置空NULL,产生野指针
  5. 找前驱节点时循环条件写错,导致越界
  6. 销毁链表只释放头节点,造成内存泄漏

👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步

相关推荐
handler013 小时前
【算法模板】最小生成树:稠密图选 Prim,稀疏图选 Kruskal
c语言·数据结构·c++·算法
此生决int4 小时前
快速复习之数据结构篇——栈和队列
数据结构·c++
昵称小白4 小时前
子串专题部分
数据结构·算法·哈希算法
怀庆同学5 小时前
C语言基础-单链表
c语言·开发语言
ShoreKiten5 小时前
cpp考前急救
数据结构·c++·算法
Byron Loong5 小时前
【基础】c,c++编译过程
c语言·c++
诙_6 小时前
C++数据结构--AVL树
数据结构
消失的旧时光-19436 小时前
为什么 Linux / Android 系统里全是 struct + 函数指针?—— 一篇讲透 C 语言如何实现面向对象(OOP)
android·linux·c语言
MZ_ZXD0016 小时前
springboot音乐播放器系统-计算机毕业设计源码76317
java·c语言·c++·spring boot·python·flask·php