
🔥小龙报:个人主页
🎬作者简介: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)),为后续学习复杂链表结构奠定基础。
