【数据结构与算法】单链表核心精讲:从概念到实战,吃透指针与动态内存操作

🔥小龙报:个人主页

🎬作者简介:C++研发,嵌入式,机器人等方向学习者

❄️个人专栏:《C语言》《【初阶】数据结构与算法》
永远相信美好的事情即将发生

文章目录

  • 前言
  • 一、 链表
    • 1.1 概念及结构
  • 二、单链表
  • 三、单链表的核心操作
    • 3.0 传值调用和传址调用在链表使用中的辨析
    • 3.1 创建
    • 3.2申请结点
    • 3.3 尾插
    • 3.4 头插
    • 3.5 尾删
    • 3.6 头删
    • 3.7 打印
    • 3.8 销毁
    • 四、代码展现
    • 4.1 SList.h
    • 4.2 SList.c
    • 4.3 test.c
  • 总结与每日励志

前言

在数据结构的学习中,链表是继顺序表后的核心线性结构,也是理解指针操作与动态内存管理的关键载体。相较于顺序表的连续存储,单链表以非连续的物理结构、灵活的节点增删特性,适配频繁插入删除的场景。本文将从单链表概念入手,详解其结构特性,再逐步实现创建、增删等核心操作,吃透传值与传址调用的精髓,为后续复杂链表及数据结构学习筑牢基础。

一、 链表

1.1 概念及结构

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

二、单链表

单链表是链表结构中最基础,使用最广泛,所以让我们来重点学习。

注意:

(1) 链式结构在逻辑上是连续的,但是在物理上不一定连续

(2)现实中的结点一般都是从堆上申请出来的

(3)从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

三、单链表的核心操作

3.0 传值调用和传址调用在链表使用中的辨析

3.1 创建

核心点 : 节点 = 指针域 + 数值域

csharp 复制代码
typedef int SLTDataType;
typedef struct SLTNode
{
	SLTDataType x; //数值域 
	struct SLTNode* next; //指针域外
}SLTNode;

3.2申请结点

因为基于链表在存储结构上不一定是连续的特点且链表节点是一个一个申请的故我们再用动态开辟内存时使用malloc;

csharp 复制代码
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("开辟失败!\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

3.3 尾插

思路:

(1)先判断当头结点为空时,是无法直接尾插的,我们直接令新节点为头结点就行了

(2)再就是我们需要找到尾结点,利用ptail从头开始往后找,如图所示,直到ptail->next==NULL时结束找到尾结点后,将新节点链接上去就行了


代码:

csharp 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x); //申请节点
    if (*pphead == NULL) //链表为空,新节点为首节点
      	*pphead = newnode;
	else
	{
		SLTNode* pcur = *pphead;
		while (pcur->next != NULL) //寻找尾节点
			pcur = pcur->next;

		pcur->next = newnode;
	}
     }

测试

csharp 复制代码
//测试尾插
void test1()
{
	SLTNode* head = NULL;
	SLTPushBack(&head, 1);
	SLTPushBack(&head, 2);
	SLTPushBack(&head, 3);
	SLTPrint(head); //打印
}

运行结果:

时间复杂度:O(n)

3.4 头插

思路: 先断言一下pphead不能为空,再就是先用申请的新节点链接上原来的头指针,再令新节点成为头指针就可以了

代码:

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

测试

csharp 复制代码
void test1()
{
	SLTNode* head = NULL;
	SLTPushFront(&head, 1);
	SLTPushFront(&head, 2);
	SLTPushFront(&head, 3);
	SLTPrint(head); //打印
}

运行结果:

时间复杂度: O(1)

3.5 尾删

思路: 不仅要找到尾结点删掉,还要找到前一个结点把他的存储下一个结点地址的指针给为NULL,先让prev的指针指向NULL,然后删掉尾结点 ,prev一定是ptail的前一个结点
注意: 只有一个结点和多个结点的操作是不同的 一个结点只需要直接释放掉然后赋NULL

代码:

csharp 复制代码
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL; //尾节点的前一个节点
		SLTNode* pcur = *pphead;
		while (pcur->next != NULL)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		free(pcur);
		pcur = NULL;
		prev->next = NULL;
	}
}

测试

csharp 复制代码
//测试尾删
void test1()
{
	SLTNode* head = NULL;
	SLTPushBack(&head, 1);
	SLTPushBack(&head, 2);
	SLTPushBack(&head, 3);	

	SLTPopBack(&head);
	SLTPopBack(&head);
	SLTPrint(head); //打印
}

运行结果:

时间复杂度: O(N)

3.6 头删

思路: 断言和尾删一样,这里主要就是先定义一个next记录phead的下一个节点,再直接free掉*pphead。最后让next成为新的头节点就可以了

代码:

csharp 复制代码
//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next; //指向头节点下一个节点
	free(*pphead);
	*pphead = next; //下一个节点成为新的头结点

}

测试

csharp 复制代码
//测试头删
void test1()
{
	SLTNode* head = NULL;
	SLTPushFront(&head, 1);
	SLTPushFront(&head, 2);
	SLTPushFront(&head, 3);

	SLTPopFront(&head);
	SLTPopFront(&head);
	SLTPrint(head); //打印
}

运行结果:

时间复杂度: O(1)

3.7 打印

打印思路: 这里先用一个pcur指针存下头指针,移动它去打印,打印完一个数据后就利用pcur->next往前走,直pcur==NULL。

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

3.8 销毁

思路
链表每个结点都是独立申请的,所以每个结点都需要一个一个的释放(free)掉,当我们从头结点先释放掉,我们先需要将下一个结点存起来,然后将头结点走到存着的这个结点,循环此操作直到free掉所有结点(走到为空)

csharp 复制代码
void SLTDestory(SLTNode** pphead)
{
	SLTNode* pcur = *pphead; //pcur从头结点开始走
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL; //手动置空,避免成为野指针
}

四、代码展现

4.1 SList.h

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

typedef int SLTDataType;
typedef struct SLTNode
{
	SLTDataType data; //数值域 
	struct SLTNode* next; //指针域外
}SLTNode;

void SLTPrint(SLTNode* phead); //打印
void SLTDestory(SLTNode** pphead); //销毁

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

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

4.2 SList.c

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

//打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

//节点申请
SLTNode* SLTBuyNode(SLTDataType x)
 {
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL)
	{
		printf("开辟失败!\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

//销毁
void SLTDestory(SLTNode** pphead)
{
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL; //手动置空,避免成为野指针
}

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x); //申请节点
    if (*pphead == NULL) //链表为空,新节点为首节点
      	*pphead = newnode;
	else
	{
		SLTNode* pcur = *pphead;
		while (pcur->next != NULL) //寻找尾节点
			pcur = pcur->next;

		pcur->next = newnode;
	}
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

//尾删
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL; //尾节点的前一个节点
		SLTNode* pcur = *pphead;
		while (pcur->next != NULL)
		{
			prev = pcur;
			pcur = pcur->next;
		}
		free(pcur);
		pcur = NULL;
		prev->next = NULL;
	}
}

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next; //指向头节点下一个节点
	free(*pphead);
	*pphead = next; //下一个节点成为新的头结点

}

4.3 test.c

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

//测试尾插
//void test1()
//{
//	SLTNode* head = NULL;
//	SLTPushBack(&head, 1);
//	SLTPushBack(&head, 2);
//	SLTPushBack(&head, 3);
//	SLTPrint(head); //打印
//}


//测试头插
//void test1()
//{
//	SLTNode* head = NULL;
//	SLTPushFront(&head, 1);
//	SLTPushFront(&head, 2);
//	SLTPushFront(&head, 3);
//	SLTPrint(head); //打印
//}

//测试尾删
//void test1()
//{
//	SLTNode* head = NULL;
//	SLTPushBack(&head, 1);
//	SLTPushBack(&head, 2);
//	SLTPushBack(&head, 3);	
//
//	SLTPopBack(&head);
//	SLTPopBack(&head);
//	SLTPrint(head); //打印
//}

//测试头删
//void test1()
//{
//	SLTNode* head = NULL;
//	SLTPushFront(&head, 1);
//	SLTPushFront(&head, 2);
//	SLTPushFront(&head, 3);
//
//	SLTPopFront(&head);
//	SLTPopFront(&head);
//	SLTPrint(head); //打印
//}

int main()
{
	test1();
	return 0;
}

总结与每日励志

✨本文系统介绍了单链表的基本概念、核心操作及实现方法。单链表作为非连续存储的线性结构,通过指针链接实现逻辑顺序。文章详细讲解了单链表的创建、节点申请、尾插/头插、尾删/头删等核心操作,并配以图示说明各操作的具体执行过程。重点分析了传值调用与传址调用的区别在链表操作中的应用,强调指针操作和动态内存管理的重要性。通过代码示例展示了各操作的具体实现,并给出时间复杂度分析(尾插/尾删O(n),头插/头删O(1)),为后续学习复杂链表结构奠定基础。

相关推荐
鱼跃鹰飞2 小时前
Leetcode279:完全平方数
数据结构·算法·leetcode·面试
测试工程师成长之路2 小时前
AI视觉模型如何重塑UI自动化测试:告别DOM依赖的新时代
人工智能·ui
m5655bj2 小时前
通过 C# 设置 Word 文档背景颜色、背景图
开发语言·c#·word
野犬寒鸦2 小时前
从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例
java·开发语言·数据库·后端·学习
long3162 小时前
合并排序 merge sort
java·数据结构·spring boot·算法·排序算法
Code Slacker2 小时前
第八届传智杯AI虚实共振实拍创作大赛练习题库
人工智能
格林威2 小时前
Baumer相机碳纤维布纹方向识别:用于复合材料铺层校验的 5 个核心技巧,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·计算机视觉·视觉检测
人工智能培训2 小时前
如何将模拟器中的技能有效迁移到物理世界?
人工智能·大模型·知识图谱·具身智能·人工智能 培训·企业人工智能培训
范纹杉想快点毕业2 小时前
STM32单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)
linux·数据结构·数据库·算法·mongodb