单链表专题(完整代码版)
文章目录
- 单链表专题(完整代码版)
-
- 一、链表的概念及结构
-
- [1.1 什么是链表?](#1.1 什么是链表?)
- [1.2 链表的节点结构](#1.2 链表的节点结构)
- 二、单链表的完整实现
-
- [2.1 节点定义与函数声明](#2.1 节点定义与函数声明)
- [2.2 打印链表](#2.2 打印链表)
- [2.3 尾插(在链表末尾插入节点)](#2.3 尾插(在链表末尾插入节点))
- [2.4 头插(在链表头部插入节点)](#2.4 头插(在链表头部插入节点))
- [2.5 尾删(删除最后一个节点)](#2.5 尾删(删除最后一个节点))
- [2.6 头删(删除第一个节点)](#2.6 头删(删除第一个节点))
- [2.7 查找节点(返回第一个值为x的节点指针)](#2.7 查找节点(返回第一个值为x的节点指针))
- [2.8 在指定位置之前插入](#2.8 在指定位置之前插入)
- [2.9 在指定位置之后插入](#2.9 在指定位置之后插入)
- [2.10 删除指定节点](#2.10 删除指定节点)
- [2.11 删除指定节点之后的节点](#2.11 删除指定节点之后的节点)
- [2.12 销毁整个链表](#2.12 销毁整个链表)
- 三、测试代码示例
- 四、链表的分类
-
- [4.1 单向 or 双向](#4.1 单向 or 双向)
- [4.2 带头 or 不带头](#4.2 带头 or 不带头)
- [4.3 循环 or 不循环](#4.3 循环 or 不循环)
- [4.4 实际中最常用的两种结构](#4.4 实际中最常用的两种结构)
- 五、总结
一、链表的概念及结构
1.1 什么是链表?
链表 是一种物理存储结构上非连续、非顺序 的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
。
1.2 链表的节点结构
每个节点由两部分组成:
- 数据域:存放实际数据
- 指针域:存放下一个节点的地址
c
struct SListNode {
int data; // 数据域
struct SListNode* next; // 指针域,指向下一个节点
};
注意:链表中的节点是动态申请的(通常从堆上申请),因此物理地址可能不连续,但逻辑上是连续的。
二、单链表的完整实现
2.1 节点定义与函数声明
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); // 打印链表
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); // 查找
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); // 删除pos之后的节点
void SLTDestroy(SLTNode** pphead); // 销毁链表
为什么很多函数需要二级指针?
因为我们要修改头指针本身(例如头插、头删、销毁等操作会改变链表的头节点地址),必须传二级指针才能改变实参。
2.2 打印链表
c
void SLTPrint(SLTNode* phead) {
SLTNode* cur = phead;
while (cur != NULL) {
printf("%d -> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
2.3 尾插(在链表末尾插入节点)
c
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
// 空链表特殊处理
if (*pphead == NULL) {
*pphead = newnode;
return;
}
// 找到尾节点
SLTNode* tail = *pphead;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode;
}
2.4 头插(在链表头部插入节点)
c
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = *pphead;
*pphead = newnode;
}
2.5 尾删(删除最后一个节点)
c
void SLTPopBack(SLTNode** pphead) {
// 空链表
if (*pphead == NULL) return;
// 只有一个节点
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
return;
}
// 多个节点
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL) {
prev = tail;
tail = tail->next;
}
prev->next = NULL;
free(tail);
}
2.6 头删(删除第一个节点)
c
void SLTPopFront(SLTNode** pphead) {
if (*pphead == NULL) return;
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
2.7 查找节点(返回第一个值为x的节点指针)
c
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
SLTNode* cur = phead;
while (cur) {
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
2.8 在指定位置之前插入
c
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
// 如果pos是头节点,相当于头插
if (*pphead == pos) {
SLTPushFront(pphead, x);
return;
}
// 找到pos的前一个节点
SLTNode* prev = *pphead;
while (prev && prev->next != pos) {
prev = prev->next;
}
if (prev == NULL) return; // pos不在链表中
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = pos;
prev->next = newnode;
}
2.9 在指定位置之后插入
c
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
if (pos == NULL) return;
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = pos->next;
pos->next = newnode;
}
2.10 删除指定节点
c
void SLTErase(SLTNode** pphead, SLTNode* pos) {
if (*pphead == NULL || pos == NULL) return;
// 删除头节点
if (*pphead == pos) {
*pphead = pos->next;
free(pos);
return;
}
// 找pos的前一个节点
SLTNode* prev = *pphead;
while (prev && prev->next != pos) {
prev = prev->next;
}
if (prev == NULL) return; // pos不在链表中
prev->next = pos->next;
free(pos);
}
2.11 删除指定节点之后的节点
c
void SLTEraseAfter(SLTNode* pos) {
if (pos == NULL || pos->next == NULL) return;
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
}
2.12 销毁整个链表
c
void SLTDestroy(SLTNode** pphead) {
SLTNode* cur = *pphead;
while (cur) {
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
三、测试代码示例
c
int main() {
SLTNode* plist = NULL;
// 尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPrint(plist); // 1 -> 2 -> 3 -> NULL
// 头插
SLTPushFront(&plist, 0);
SLTPrint(plist); // 0 -> 1 -> 2 -> 3 -> NULL
// 查找
SLTNode* pos = SLTFind(plist, 2);
if (pos) {
printf("找到了节点:%d\n", pos->data);
// 在2之前插入99
SLTInsert(&plist, pos, 99);
SLTPrint(plist); // 0 -> 1 -> 99 -> 2 -> 3 -> NULL
// 在2之后插入100
SLTInsertAfter(pos, 100);
SLTPrint(plist); // 0 -> 1 -> 99 -> 2 -> 100 -> 3 -> NULL
// 删除2节点
SLTErase(&plist, pos);
SLTPrint(plist); // 0 -> 1 -> 99 -> 100 -> 3 -> NULL
}
// 头删
SLTPopFront(&plist);
SLTPrint(plist); // 1 -> 99 -> 100 -> 3 -> NULL
// 尾删
SLTPopBack(&plist);
SLTPrint(plist); // 1 -> 99 -> 100 -> NULL
// 销毁链表
SLTDestroy(&plist);
return 0;
}
四、链表的分类
链表的结构非常多样,以下情况组合起来共有 8种:
| 分类维度 | 类型 |
|---|---|
| 单向 / 双向 | 单向链表、双向链表 |
| 带头 / 不带头 | 带头结点、不带头结点 |
| 循环 / 不循环 | 循环链表、非循环链表 |
4.1 单向 or 双向
- 单向链表 :每个节点只有一个指向下一个节点的指针。
,简化插入删除操作。

- 笔试面试中出现频率很高
-
带头双向循环链表
- 结构最复杂,但实现起来反而简单(因为边界条件少)
- 常用于实际存储数据(如C++ STL中的list)
五、总结
- 链表是动态数据结构,插入删除不需要移动元素,但需要额外的指针存储空间。
- 单链表实现简单,但只能单向遍历,删除节点时需要找到前驱。
- 带头双向循环链表虽然结构复杂,但操作统一,实际开发中更常用。
- 掌握单链表是理解更复杂链表的基础,也是面试中的高频考点。