单链表可类比为链式连接的车厢,每个节点包含数据域与指针域,指针域存放下一节点的地址。其核心特征为逻辑上连续、物理内存分布离散,这一特性使其成为哈希桶、图的邻接表等复杂结构的基础组件。
本文将从单链表的定义与核心操作切入,结合移除链表元素、反转链表、合并有序链表等经典问题,通过C语言代码实现,系统解析单链表的设计思想与应用场景。
一、链表的定义
1. 车厢类比
◦ 每节"车厢"就是一个独立的节点,车厢里放着"下一节车厢的钥匙",对应节点里存着下一个节点的地址(指针)。
◦ 这样,你就可以从车头(头节点)走到车尾(尾节点),每次只需要拿着当前节点里存的"钥匙"去找下一个节点。
2. 节点的结构
◦ 每个节点包含两部分:
◦ data:用来存放当前节点的数据(如整数、字符等)
◦ next:是一个指针,用来存放下一个节点的地址
◦ 用C语言结构体定义就是:
cpp
struct SListNode
{
int data; // 节点数据
struct SListNode* next; // 指向下一个节点的指针
};
◦ 为了让代码更通用,通常会用 typedef 重命名数据类型:
cpp
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
3. 链表的物理与逻辑特性
◦ 逻辑上连续:从我们的视角看,节点是一个接一个连起来的。
◦ 物理上不一定连续:每个节点都是单独向操作系统申请的内存,这些内存块在物理上可能分散在不同位置。
◦ 节点来源:节点的内存一般是从堆上动态申请的。
二、单链表的核心操作(接口)
1. 遍历与打印
cpp
void SLTPrint(SLTNode* phead);
• 从 phead 指向的头节点开始,依次访问每个节点,直到遇到 NULL(尾节点的 next 就是 NULL)。
• 核心思路是用一个 pcur 指针"接力":
cpp
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d ", pcur->data);
pcur = pcur->next; // 拿到下一个节点的地址
}
printf("\n");
}
2. 增删操作
• 尾部插入:void SLTPushBack(SLTNode** pphead, SLDataType x);
• 头部插入:void SLTPushFront(SLTNode** pphead, SLDataType x);
• 尾部删除:void SLTPopBack(SLTNode** pphead);
• 头部删除:void SLTPopFront(SLTNode** pphead);
• 指定位置前插入:void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
• 指定位置后插入:void SLTInsertAfter(SLTNode* pos, SLDataType x);
• 删除指定节点:void SLTErase(SLTNode** pphead, SLTNode* pos);
• 删除指定节点的后一个节点:void SLTEraseAfter(SLTNode* pos);
3. 查找与销毁
• 查找:SLTNode* SLTFind(SLTNode* phead, SLDataType x);
• 销毁链表:void SListDestroy(SLTNode** pphead);
4. 代码实现
头文件 SList.h
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 用typedef定义通用数据类型,方便后续扩展
typedef int SLDataType;
// 单链表节点结构
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
// 接口函数声明
// 打印链表
void SLTPrint(SLTNode* phead);
// 尾部插入
void SLTPushBack(SLTNode** pphead, SLDataType x);
// 头部插入
void SLTPushFront(SLTNode** pphead, SLDataType x);
// 尾部删除
void SLTPopBack(SLTNode** pphead);
// 头部删除
void SLTPopFront(SLTNode** pphead);
// 查找节点
SLTNode* SLTFind(SLTNode* phead, SLDataType x);
// 在pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
// 在pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLDataType x);
// 删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置之后的节点
void SLTEraseAfter(SLTNode* pos);
// 销毁链表
void SListDestroy(SLTNode** pphead);
实现文件 SList.c
cpp
#include "SList.h"
#include <stdlib.h>
#include <assert.h>
// ------------------------------ 工具函数 ------------------------------
// 静态函数:仅在当前文件可见,用于创建一个新节点
// 参数:x - 新节点要存储的数据
// 返回值:成功返回新节点指针,失败则终止程序
static SLTNode* BuySLTNode(SLDataType x)
{
// 向堆申请一个节点大小的内存
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail"); // 打印内存申请失败原因
exit(1); // 异常退出程序
}
newnode->data = x; // 初始化节点数据
newnode->next = NULL; // 初始化指针,避免野指针
return newnode;
}
// ------------------------------ 接口1:打印链表 ------------------------------
// 功能:遍历并打印整个链表
// 参数:phead - 链表头节点指针(仅读取,无需修改头节点,所以传一级指针)
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead; // 用pcur遍历,避免修改原头指针
while (pcur != NULL)
{
printf("%d->", pcur->data); // 打印当前节点数据
pcur = pcur->next; // 指针后移
}
printf("NULL\n"); // 链表末尾标识
}
// ------------------------------ 接口2:尾部插入 ------------------------------
// 功能:在链表的尾部插入新节点
// 参数:pphead - 头节点指针的地址(需要修改头节点,所以传二级指针)
// x - 要插入的数据
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead); // 确保pphead不是空指针
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
// 链表为空时,新节点直接作为头节点
*pphead = newnode;
}
else
{
// 找到尾节点
SLTNode* ptail = *pphead;
while (ptail->next != NULL)
{
ptail = ptail->next;
}
ptail->next = newnode; // 尾节点的next指向新节点
}
}
// ------------------------------ 接口3:头部插入 ------------------------------
// 功能:在链表的头部插入新节点
// 参数:pphead - 头节点指针的地址(需要修改头节点,所以传二级指针)
// x - 要插入的数据
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
// 新节点的next指向原头节点
newnode->next = *pphead;
// 更新头节点为新节点
*pphead = newnode;
}
// ------------------------------ 接口4:尾部删除 ------------------------------
// 功能:删除链表的尾节点
// 参数:pphead - 头节点指针的地址(可能需要修改头节点,所以传二级指针)
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead != NULL); // 链表不能为空
if ((*pphead)->next == NULL)
{
// 链表只有一个节点时,直接释放头节点并置空
free(*pphead);
*pphead = NULL;
}
else
{
// 找到倒数第二个节点
SLTNode* ptail = *pphead;
while (ptail->next->next != NULL)
{
ptail = ptail->next;
}
free(ptail->next); // 释放尾节点
ptail->next = NULL; // 倒数第二个节点成为新的尾节点
}
}
// ------------------------------ 接口5:头部删除 ------------------------------
// 功能:删除链表的头节点
// 参数:pphead - 头节点指针的地址(需要修改头节点,所以传二级指针)
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead != NULL); // 链表不能为空
SLTNode* next = (*pphead)->next; // 保存原头节点的下一个节点
free(*pphead); // 释放原头节点
*pphead = next; // 更新头节点
}
// ------------------------------ 接口6:查找节点 ------------------------------
// 功能:根据数据查找节点
// 参数:phead - 链表头节点指针
// x - 要查找的数据
// 返回值:找到返回节点指针,未找到返回NULL
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
// ------------------------------ 接口7:在pos前插入 ------------------------------
// 功能:在指定节点pos的前面插入新节点
// 参数:pphead - 头节点指针的地址
// pos - 目标节点指针
// x - 要插入的数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead);
assert(pos);
assert(*pphead != NULL);
if (*pphead == pos)
{
// 如果pos是头节点,等价于头插
SLTPushFront(pphead, x);
}
else
{
// 找到pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
// ------------------------------ 接口8:在pos后插入 ------------------------------
// 功能:在指定节点pos的后面插入新节点
// 参数:pos - 目标节点指针
// x - 要插入的数据
void SLTInsertAfter(SLTNode* pos, SLDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
// 先让新节点指向pos的下一个节点,防止链表断裂
newnode->next = pos->next;
pos->next = newnode;
}
// ------------------------------ 接口9:删除pos节点 ------------------------------
// 功能:删除指定节点pos
// 参数:pphead - 头节点指针的地址
// pos - 目标节点指针
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
assert(*pphead != NULL);
if (*pphead == pos)
{
// 如果pos是头节点,等价于头删
SLTPopFront(pphead);
}
else
{
// 找到pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
// ------------------------------ 接口10:删除pos之后的节点 ------------------------------
// 功能:删除指定节点pos的下一个节点
// 参数:pos - 目标节点指针
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next != NULL); // pos不能是尾节点
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
// ------------------------------ 接口11:销毁链表 ------------------------------
// 功能:释放链表所有节点的内存,并将头节点置空
// 参数:pphead - 头节点指针的地址
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur != NULL)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
测试文件 test.c
cpp
#include "SList.h"
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 != NULL)
{
// 在pos前插入
SLTInsert(&plist, pos, 10);
SLTPrint(plist); // 0->1->10->2->3->NULL
// 在pos后插入
SLTInsertAfter(pos, 20);
SLTPrint(plist); // 0->1->10->2->20->3->NULL
}
// 测试尾删
SLTPopBack(&plist);
SLTPrint(plist); // 0->1->10->2->20->NULL
// 测试头删
SLTPopFront(&plist);
SLTPrint(plist); // 1->10->2->20->NULL
// 测试删除pos节点
pos = SLTFind(plist, 10);
SLTErase(&plist, pos);
SLTPrint(plist); // 1->2->20->NULL
// 测试删除pos之后的节点
pos = SLTFind(plist, 2);
SLTEraseAfter(pos);
SLTPrint(plist); // 1->2->NULL
// 销毁链表
SListDestroy(&plist);
SLTPrint(plist); // NULL
return 0;
}
三、链表的分类
链表可以按三个维度组合出8种结构:
-
是否带头节点:带头 / 不带头
-
方向:单向 / 双向
-
是否循环:循环 / 不循环
*单链表------无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
*双向链表------带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
四、思考题
思考:"当我们想保存的数据类型为字符型、浮点型或者其他自定义的类型时,该如何修改?"
很简单,我们只需要修改 typedef 那一行就可以了:
• 存字符:typedef char SLDataType;
• 存浮点数:typedef float SLDataType;
• 存自定义结构体(比如通讯录里的联系人信息):
cpp
typedef struct PersonInfo
{
char name[20];
int age;
char phone[12];
}SLDataType;
这样整个链表的代码都不需要大改,只换一个类型定义,就能支持不同的数据,这就是泛型思想的一种体现。
五、练习
所有代码基于C语言实现,延续之前的单链表结构体定义:
cpp
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
1. 移除链表元素(LeetCode 203)
题目描述:给你一个链表的头节点 head 和一个整数 val,删除链表中所有满足 Node.val == val 的节点,并返回新的头节点。
解题思路
方法一
◦ 用 newHead 和 newTail 维护一个新链表,尾插法把原链表中值不等于 val 的节点接进去。
◦ 复用原链表节点,不额外创建新节点,只改变指针指向。
关键处理:
◦ 最后必须将 newTail->next = NULL,避免新链表的尾部指向原链表中被移除的节点,导致链表结构错误。
复杂度分析
• 时间复杂度:O(n)
只遍历一次原链表,n 为链表节点数。
• 空间复杂度:O(1)
只使用了几个指针变量,没有额外创建节点,是原地算法。
边界情况测试
-
链表为空:head = NULL → 直接返回 NULL。
-
头节点需要移除:head->val == val → 新链表会从第一个有效节点开始。
-
所有节点都需要移除:所有节点值都是 val → 返回 NULL。
-
链表只有一个节点:如果该节点值等于 val 则返回 NULL,否则返回原节点。
代码实现
cpp
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
// 1. 初始化新链表的头、尾指针
ListNode* newHead, *newTail;
newHead = newTail = NULL;
// 2. 初始化遍历原链表的指针
ListNode* pcur = head;
// 3. 遍历原链表
while (pcur) {
// 筛选出值不等于val的节点
if (pcur->val != val) {
if (newHead == NULL) {
// 新链表为空时,头和尾都指向当前节点
newHead = newTail = pcur;
} else {
// 新链表不为空时,尾插操作
newTail->next = pcur;
newTail = newTail->next;
}
}
// 继续遍历下一个节点
pcur = pcur->next;
}
// 4. 处理新链表的尾节点,防止指向原链表残留节点
if (newTail) {
newTail->next = NULL;
}
// 5. 返回新链表的头节点
return newHead;
}
方法二:双指针定义
◦ src(源指针):负责遍历整个数组,寻找不等于目标值 val 的元素。
◦ dst(目标指针):负责记录新数组的位置,用来存放从 src 找到的有效元素。
核心步骤
◦ 当 nums[src] == val 时:说明当前元素是需要移除的,src 直接后移跳过它。
◦ 当 nums[src] != val 时:说明当前元素需要保留,把它赋值给 nums[dst],然后 src 和 dst 同时后移。
返回值
◦ 循环结束后,dst 的值就是新数组的有效长度,因为它刚好记录了所有保留元素的个数。
复杂度分析
• 时间复杂度:O(n)
每个元素最多被访问一次,src 指针最多遍历整个数组。
• 空间复杂度:O(1)
只使用了常量级的额外空间,完全满足题目"原地修改"的要求。
cpp
int removeElement(int* nums, int numsSize, int val) {
// 初始化两个指针
int src, dst;
src = dst = 0;
while (src < numsSize) {
if (nums[src] == val) {
// 当前元素需要移除,仅源指针后移
src++;
} else {
// 当前元素需要保留,赋值给目标指针位置
nums[dst] = nums[src];
dst++;
src++;
}
}
// 目标指针的位置就是新数组的有效长度
return dst;
}
2. 反转链表(LeetCode 206)
题目描述:给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
解题思路
这个方法的本质是原地反转指针指向,用三个指针 n1(前驱)、n2(当前)、n3(后继)来逐个处理节点:
-
保存后继:n3 = n2->next,避免修改 n2->next 后丢失后续节点。
-
反转指向:n2->next = n1,让当前节点指向前一个节点。
-
指针后移:n1 = n2 和 n2 = n3,继续处理下一个节点。
-
终止条件:当 n2 为 NULL 时,n1 就是反转后的新头节点。
代码实现
cpp
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
// 空链表或只有一个节点时直接返回
if (head == NULL || head->next == NULL) {
return head;
}
ListNode* n1 = NULL;
ListNode* n2 = head;
ListNode* n3 = NULL;
while (n2) {
n3 = n2->next;
n2->next = n1;
n1 = n2;
n2 = n3;
}
return n1;
}
3. 合并两个有序链表(LeetCode 21)
题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解题思路
方法一:哨兵节点+双指针;
• 用两个指针分别遍历两个链表,每次取较小值的节点接入新链表;
• 遍历结束后,将剩余未遍历的链表直接接入新链表尾部。
代码实现
cpp
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
// 处理其中一个链表为空的边界情况
if (list1 == NULL) return list2;
if (list2 == NULL) return list1;
// 创建虚拟头节点(哨兵节点),统一尾插逻辑
ListNode* newHead = (ListNode*)malloc(sizeof(ListNode));
ListNode* newTail = newHead;
ListNode* l1 = list1;
ListNode* l2 = list2;
// 合并两个链表的有效节点
while (l1 && l2) {
if (l1->val < l2->val) {
newTail->next = l1;
l1 = l1->next;
} else {
newTail->next = l2;
l2 = l2->next;
}
newTail = newTail->next;
}
// 处理剩余节点
if (l1) newTail->next = l1;
if (l2) newTail->next = l2;
// 保存真正的头节点,释放虚拟节点
ListNode* ret = newHead->next;
free(newHead);
newHead = NULL;
return ret;
}
方法二:逆序双指针法
步骤1:初始化三个指针
• l1:指向nums1有效元素的末尾 → l1 = m - 1
• l2:指向nums2的末尾 → l2 = n - 1
• l3:指向nums1数组的末尾(合并后元素的存放位置) → l3 = m + n - 1
步骤2:从后往前合并两个数组的有效元素
循环条件:l1 >= 0 && l2 >= 0(两个数组都还有未处理元素)
• 比较nums1[l1]和nums2[l2],将更大的元素赋值给nums1[l3];
• 赋值后,对应的指针(大元素的指针+l3)向前移动一位(自减)。
步骤3:处理nums2的剩余元素
循环结束后,若l2 >= 0(说明nums1的有效元素已合并完,nums2还有剩余),将nums2剩余元素依次赋值到nums1剩余的位置;
若l1 >= 0,无需处理(nums1剩余元素本就在正确位置)。
方法适用场景
-
两个升序/降序的有序数组,需要原地合并(无额外数组空间);
-
其中一个数组预留了足够的合并空间(如本题nums1长度为m+n);
-
要求时间复杂度最优(O(m+n))、空间复杂度O(1)。
cpp
#include <stdio.h>
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
// 定义三个指针,分别指向nums1有效末尾、nums2末尾、nums1整体末尾
int l1 = m - 1;
int l2 = n - 1;
int l3 = m + n - 1;
// 从后往前合并,直到其中一个数组遍历完
while (l1 >= 0 && l2 >= 0) {
if (nums1[l1] < nums2[l2]) {
// nums2当前元素更大,放到nums1末尾
nums1[l3--] = nums2[l2--];
} else {
// nums1当前元素更大/相等,放到nums1末尾
nums1[l3--] = nums1[l1--];
}
}
// 处理nums2剩余的元素(nums1已遍历完,nums2还有元素)
while (l2 >= 0) {
nums1[l3--] = nums2[l2--];
}
}
// 测试代码
int main() {
int nums1[] = {1,2,3,0,0,0};
int nums2[] = {2,5,6};
int m = 3, n = 3;
merge(nums1, 6, m, nums2, 3, n);
for (int i = 0; i < m + n; i++) {
printf("%d ", nums1[i]);
}
return 0;
}
4. 链表的中间结点(LeetCode 876)
题目描述
给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
解题思路
• 方法:快慢指针法(快指针fast+慢指针slow);
• 快指针每次走2步,慢指针每次走1步;
• 当快指针走到链表尾部时,慢指针恰好指向中间节点。
• 短路求值:fast && fast->next中,如果fast是NULL,fast->next不会被执行,避免空指针错误;
• 偶数节点情况:比如链表1->2->3->4,循环结束后slow指向3(靠后的中间节点),这是LeetCode等平台的标准答案要求;
• 边界情况:链表为空返回NULL,链表只有一个节点返回自身,代码无需额外判空,循环条件会自动处理。
代码实现
cpp
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
// 初始化快慢指针,都指向头节点
ListNode* slow = head;
ListNode* fast = head;
// 循环条件:fast不为空 且 fast的下一个节点不为空
while (fast && fast->next) {
slow = slow->next; // 慢指针走1步
fast = fast->next->next; // 快指针走2步
}
// 循环结束,slow指向中间节点
return slow;
}
5. 环形链表的约瑟夫问题
题目描述
41个人围成一圈,从第1个人开始报数,每报到第3人该人必须自杀,直到只剩最后两人。Josephus和朋友分别在16、31位,最终存活。
解题思路
-
构建循环链表:将41个节点连成环形;
-
模拟报数:从第一个节点开始,数到3时删除该节点;
-
终止条件:链表中只剩2个节点时停止。
buyNode(int x):创建一个值为 x 的链表节点,并初始化 next 为 NULL。
createCircle(int n):创建一个包含 n 个节点的环形链表,最后返回尾节点(为后续约瑟夫环的遍历做准备)。
ysf(int n, int m):约瑟夫环的核心逻辑,从环形链表中每次数到第 m 个节点并删除,直到只剩一个节点,返回其值。
代码实现
cpp
ListNode* buyNode(int x) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL) {
exit(1); // 内存分配失败直接退出
}
node->val = x;
node->next = NULL;
return node;
}
ListNode* createCircle(int n) {
ListNode* phead = buyNode(1);
ListNode* ptail = phead;
for (int i = 2; i <= n; i++) {
ptail->next = buyNode(i);
ptail = ptail->next;
}
ptail->next = phead; // 首尾相连,形成环
return ptail; // 返回尾节点,便于后续约瑟夫环的遍历
}
int ysf(int n, int m) {
ListNode* prev = createCircle(n); // prev初始为环形链表的尾节点
ListNode* pcur = prev->next; // pcur初始为头节点
int count = 1;
// 当链表中不止一个节点时继续循环
while (pcur->next != pcur) {
if (count == m) {
// 找到第m个节点,删除
prev->next = pcur->next;
free(pcur);
pcur = prev->next;
count = 1; // 重置计数
} else {
// 未到第m个节点,继续移动指针
prev = pcur;
pcur = pcur->next;
count++;
}
}
// 保存最后一个节点的值,释放内存
int val = pcur->val;
free(pcur);
return val;
}
• 初始化:prev 指向环形链表的尾节点,pcur 指向头节点,count 用于计数。
• 循环淘汰:每次移动指针并计数,当 count == m 时删除当前节点,并重置计数;否则继续移动。
• 终止条件:当 pcur->next == pcur 时,说明链表只剩一个节点。
• 内存释放:返回最后一个节点的值前,先释放该节点的内存,避免内存泄漏。
6. 分割链表(LeetCode 86)
题目描述:给你一个链表的头节点 head 和一个特定值 x,请你对链表进行分割,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。你应当保留两个分区中每个节点的初始相对位置。
核心思路解析
- 虚拟头节点的作用
◦ 用 lessHead 和 greaterHead 两个虚拟节点,分别管理小于 x 和大于等于 x 的节点。
◦ 虚拟节点让尾插逻辑完全统一,无需判断链表是否为空。
- 节点分流
◦ 遍历原链表,将节点按值的大小尾插到对应的虚拟链表中。
◦ 这个过程是原地操作,仅改变指针指向,不创建新节点。
- 关键收尾操作
◦ greaterTail->next = NULL:必须将大链表的尾节点置空,否则原链表的后续节点可能导致循环。
◦ lessTail->next = greaterHead->next:将小链表的尾节点与大链表的第一个有效节点相连,合并成一个完整链表。
- 内存管理
◦ 虚拟节点是动态分配的,使用后必须释放,避免内存泄漏。
边界情况测试
• 链表为空 → 直接返回 NULL。
• 所有节点都小于 x → 大链表为空,直接返回原链表。
• 所有节点都大于等于 x → 小链表为空,返回大链表的有效节点。
代码实现
cpp
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x) {
if (head == NULL) {
return head;
}
// 创建两个虚拟头节点
ListNode* lessHead = (ListNode*)malloc(sizeof(ListNode));
ListNode* lessTail = lessHead;
ListNode* greaterHead = (ListNode*)malloc(sizeof(ListNode));
ListNode* greaterTail = greaterHead;
// 遍历原链表,分流节点
ListNode* pcur = head;
while (pcur) {
if (pcur->val < x) {
lessTail->next = pcur;
lessTail = lessTail->next;
} else {
greaterTail->next = pcur;
greaterTail = greaterTail->next;
}
pcur = pcur->next;
}
// 避免循环链表
greaterTail->next = NULL;
// 合并两个链表
lessTail->next = greaterHead->next;
// 保存结果头节点并释放虚拟节点
ListNode* newHead = lessHead->next;
free(lessHead);
free(greaterHead);
return newHead;
}
单链表作为数据结构的基础模块,其设计思想与操作逻辑贯穿于各类复杂场景。通过对核心操作的梳理与经典问题的实战解析,不难发现其"指针联动"的核心本质------无论是节点的增删改查,还是问题求解中的指针技巧(如快慢指针、虚拟头节点),均是对链表特性的灵活运用。
掌握单链表不仅能夯实数据结构基础,更能培养逻辑拆解与边界处理能力,为后续哈希表、图等复杂结构的学习奠定根基。希望本文的解析能为各位开发者提供参考,在实际应用中灵活运用链表思维,高效解决各类编程问题。