零基础小白在学习链表(LinkedList)的思考 带你拆解底层逻辑
在数据结构中,我们知道顺序表虽然尾插的时间复杂度O(1),但其无论是
中间/头部的插⼊删除(时间复杂度O(N)),还是增容时所面临空间的消耗都不可忽视,思考我们当如何解决以上问题呢?
一、什么是链表?其与数组的区别是什么?
1、链表:一种物理上不连续,但数据元素的逻辑顺序通过链表中指针次序所链接的存储结构
2、其与数组的区别:
| 对比维度 | 数组(顺序表) | 单链表(无尾指针) |
|---|---|---|
| 内存存储 | 物理内存连续 | 内存碎片化、不连续 |
| 访问方式 | 支持,arr[i] 直接取,O(1) | 不支持,必须从头遍历,O(n) |
| 头部插入/删除 | O(n),所有元素后移 | O(1),仅修改指针指向 |
| 尾部插入 | 均摊O(1),偶尔扩容拷贝 | O(n),每次要遍历到末尾 |
| 中间插入/删除 | O(n),后续元素整体移位 | O(n),需要先找到目标位置 |
| 空间开销 | 仅存储数据,额外开销小 | 每个节点多存1个next指针,额外内存开销大 |
| 容量大小 | 固定/动态扩容,扩容有性能损耗 | 无固定上限,按需申请节点,无需扩容 |
3、链表结构(以单链表SLT [Single Linked Table]为例)
#include <stdio.h>
#include <stdlib.h>
// 1. 定义单链表节点结构体
struct SLTNode
{
int data; // 数据域:存储节点数据,可按需更换数据类型
struct SLTNode* next; // 指针域:指向下一个节点的指针
};
// 2. 给结构体起别名,简化代码书写
typedef struct SLTNode Node; // Node 等价于 struct SLTNode
typedef struct SLTNode* PNode; // PNode 等价于 struct SLTNode*(节点指针)
采用struct关键字定义链表节点结构体,节点分为两部分:
- 数据域 int data:用于存储链表需要保存的数据,可按需更换数据类型;
- 指针域 struct SListNode* next:一级结构体指针,存储下一个相邻节点的内存地址,通过地址串联所有节点形成链式结构。
使用typedef对结构体及结构体指针重命名:
- Node:等价于 struct SLTNode,代表单个节点实体;
- PNode:等价于 struct SLTNode*,代表节点指针,简化链表操作代码书写。
单链表逻辑特征:节点内存不连续,仅依靠next指针维系前后节点关系,尾部节点next指针置NULL代表链表结束。
二、链表的基本功能代码实现
1、工具函数:新建节点
/**
* @brief 创建新节点并分配内存
* @param x 节点存储的数据值
* @return PNode 成功返回新节点指针,失败则程序退出
*/
PNode BuyNode(int x)
{
// 动态分配节点内存
PNode newNode = (PNode)malloc(sizeof(Node));
// 检查内存分配是否成功
if (newNode == NULL)
{
perror("malloc failed");
exit(-1); // 内存分配失败,程序退出
}
// 初始化节点数据
newNode->data = x; // 设置数据域
newNode->next = NULL; // 新节点next指针置空
return newNode; // 返回新节点指针
}
malloc动态开辟内存,初始化数据,next 置空,所有插入操作都会复用这个函数 注意事项 这里的newNode的类型是一级指针PNode,即newNode存有对应节点的地址,其指向一个节点
/**
* 打印链表所有节点的数据(调试用)
* phead 链表头指针(哨兵头节点)
*/
void SLTPrint(PNode phead)
{
// 跳过哨兵头节点,从第一个有效节点开始
PNode cur = phead->next;
// 遍历链表直到末尾
while (cur != NULL)
{
printf("%d -> ", cur->data); // 打印当前节点数据
cur = cur->next; // 移动到下一个节点
}
printf("NULL\n"); // 链表结束标记
}
这里phead是一个不存有数据的指针(哨兵头节点),通过phead->next找到链表中第一个存有数据的有效节点,通过cur指针不断将节点的数据打印而出,并且将cur不断赋值成其所指向的下一节点的指针,直至cur变为空指针 打印截止
3、头插:在链表最前面插入数据
/**
* 在链表头部插入新节点
* pphead 指向头指针的二级指针(用于修改外部头指针)
* x 要插入的数据
*/
void SLTPushFront(Node** pphead, LinkData x)
{
// 创建新节点
Node* newnode = BuyNode(x);
// 新节点指向原头节点
newnode->next = *pphead; // 关键:二级指针解引用获取原头指针
// 更新头指针指向新节点
*pphead = newnode; // 关键:通过二级指针修改外部头指针
}
// 错误写法:无法修改外部头指针
void SLTPushFront(Node* phead, LinkData x)
{
Node* newnode = BuyNode(x);
newnode->next = phead;
phead = newnode; // 错误:只修改函数内局部副本,外部原指针完全不变!
}
C 语言函数传参属于值拷贝传递,如果头插函数仅使用一级指针Node* phead作形参:当head为空指针时调用函数,head(NULL)的值被拷贝到形参phead中,此时phead也是NULL,此时虽然他们的值都是NULL,但地址却是不一样的,比如下方中的phead(值为NULL 存有地址0x0003) 自己地址为0x0002,传入后会出现新的phead(值NULL 存有地址0x0003),但其地址可能时0x00A3

4、尾插:在链表末尾插入数据(原理与3差不多) ps:这里的ptail是为了找到原链表的尾节点,不直接用pphead为了便于寻找的同时不改动头节点(当phead为空时)
void SLTPushBack(Node** pphead,LinkData x)
{
Node* newnode=BuyNode(x);
Node* ptail = *pphead;
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
5、其余本质上实现原理与之前一致,这里简要概述
// 头删
void SLTPopFront(Node** pphead);
// 尾删
void SLTPopBack(Node** pphead);
// 按值查找节点
Node* SLTFind(Node* phead, LinkData x);
// 在pos节点前插入数据
void SLTInsert(Node** pphead, LinkData x, Node* pos);
// 在pos节点后插入数据
void SLTInsertAfter(Node* pos, LinkData x);
// 释放整条链表并置空头指针
void SLTDestroy(Node** pphead);
各功能实现与简短说明
5.1. 头删 SLTPopFront
void SLTPopFront(Node** pphead)
{
Node* del = *pphead;
*pphead = del->next;
free(del);
}
说明:可能修改头指针,使用二级指针;直接更新表头并释放原首节点,局部指针del在函数结束后 会销毁,所以不用置空
5.2. 尾删 SLTPopBack
void SLTPopBack(Node** pphead)
{
// 链表仅有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
// 找到倒数第二个节点
Node* cur = *pphead;
while (cur->next->next)
cur = cur->next;
free(cur->next);
cur->next = NULL;
}
说明:链表只剩单个节点时需要置空头指针,必须二级指针。
5.3. 查找 SLTFind
Node* SLTFind(Node* phead, LinkData x)
{
Node* cur = phead;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
说明:仅遍历读取数据,不修改表头与其他结点链接,一级指针即可。
(虽然不用cur也可以 因为函数的形参是值的拷贝,但工程规范尽量用cur表示头节点,进而不修改头节点)
5.4. 指定节点前插入 SLTInsert
void SLTInsert(Node** pphead, LinkData x, Node* pos)
{
Node* newnode = BuyNode(x);
// 插入位置为头部,更新表头
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
return;
}
// 寻找 pos 的前驱节点
Node* cur = *pphead;
while (cur->next != pos)
cur = cur->next;
cur->next = newnode;
newnode->next = pos;
}
说明:若 pos 是头节点,会改变链表头部,使用二级指针。
5.5. 指定结点后插入 SLTInsertAfter
void SLTInsertAfter(Node* pos, LinkData x)
{
Node* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
说明:1、永远不会改动链表头部,仅修改节点内部 next,一级指针够用。2、需要先将指定结点的next先复制给newnode的next,不然会丢失pos的下一节点的地址
5.6. 销毁链表 SLTDestroy
void SLTDestroy(Node** pphead)
{
assert(pphead);//断定不为空
Node* cur = *pphead;
while (cur)
{
Node* next = cur->next;
free(cur);
cur = next;
}
// 外部头指针置空,防止野指针
*pphead = NULL;
}
说明:销毁后需要清空外部头指针变量,必须二级指针。
总结
需要修改外部头指针:头删、尾删、pos 前插、销毁链表 → 参数 Node**
仅遍历 / 后置插入,不改动表头:查找、pos 后插 → 参数 Node*
二级指针核心逻辑:C 语言值传递,只有传入指针变量的地址,才能修改外部原始指针。
更多推荐




所有评论(0)