C语言——单链表

单链表可类比为链式连接的车厢,每个节点包含数据域与指针域,指针域存放下一节点的地址。其核心特征为逻辑上连续、物理内存分布离散,这一特性使其成为哈希桶、图的邻接表等复杂结构的基础组件。

本文将从单链表的定义与核心操作切入,结合移除链表元素、反转链表、合并有序链表等经典问题,通过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种结构:

  1. 是否带头节点:带头 / 不带头

  2. 方向:单向 / 双向

  3. 是否循环:循环 / 不循环

*单链表------无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

*双向链表------带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

四、思考题

思考:"当我们想保存的数据类型为字符型、浮点型或者其他自定义的类型时,该如何修改?"

很简单,我们只需要修改 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)

只使用了几个指针变量,没有额外创建节点,是原地算法。

边界情况测试

  1. 链表为空:head = NULL → 直接返回 NULL。

  2. 头节点需要移除:head->val == val → 新链表会从第一个有效节点开始。

  3. 所有节点都需要移除:所有节点值都是 val → 返回 NULL。

  4. 链表只有一个节点:如果该节点值等于 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(后继)来逐个处理节点:

  1. 保存后继:n3 = n2->next,避免修改 n2->next 后丢失后续节点。

  2. 反转指向:n2->next = n1,让当前节点指向前一个节点。

  3. 指针后移:n1 = n2 和 n2 = n3,继续处理下一个节点。

  4. 终止条件:当 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剩余元素本就在正确位置)。

方法适用场景

  1. 两个升序/降序的有序数组,需要原地合并(无额外数组空间);

  2. 其中一个数组预留了足够的合并空间(如本题nums1长度为m+n);

  3. 要求时间复杂度最优(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位,最终存活。

解题思路

  1. 构建循环链表:将41个节点连成环形;

  2. 模拟报数:从第一个节点开始,数到3时删除该节点;

  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 的节点之前。你应当保留两个分区中每个节点的初始相对位置。

核心思路解析

  1. 虚拟头节点的作用

◦ 用 lessHead 和 greaterHead 两个虚拟节点,分别管理小于 x 和大于等于 x 的节点。

◦ 虚拟节点让尾插逻辑完全统一,无需判断链表是否为空。

  1. 节点分流

◦ 遍历原链表,将节点按值的大小尾插到对应的虚拟链表中。

◦ 这个过程是原地操作,仅改变指针指向,不创建新节点。

  1. 关键收尾操作

◦ greaterTail->next = NULL:必须将大链表的尾节点置空,否则原链表的后续节点可能导致循环。

◦ lessTail->next = greaterHead->next:将小链表的尾节点与大链表的第一个有效节点相连,合并成一个完整链表。

  1. 内存管理

◦ 虚拟节点是动态分配的,使用后必须释放,避免内存泄漏。

边界情况测试

• 链表为空 → 直接返回 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;
}

单链表作为数据结构的基础模块,其设计思想与操作逻辑贯穿于各类复杂场景。通过对核心操作的梳理与经典问题的实战解析,不难发现其"指针联动"的核心本质------无论是节点的增删改查,还是问题求解中的指针技巧(如快慢指针、虚拟头节点),均是对链表特性的灵活运用。

掌握单链表不仅能夯实数据结构基础,更能培养逻辑拆解与边界处理能力,为后续哈希表、图等复杂结构的学习奠定根基。希望本文的解析能为各位开发者提供参考,在实际应用中灵活运用链表思维,高效解决各类编程问题。

相关推荐
夏乌_Wx2 小时前
练题100天——DAY40:合并两个有序链表
数据结构
2501_944526422 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 关于页面实现
android·java·开发语言·javascript·python·flutter·游戏
Dem12 小时前
怎么安装jdk
java·开发语言
hans汉斯2 小时前
建模与仿真|基于GWO-BP的晶圆机器人大臂疲劳寿命研究
大数据·数据结构·算法·yolo·机器人·云计算·汉斯出版社
wazmlp0018873692 小时前
python第一次作业
开发语言·python·算法
Miqiuha2 小时前
二次散列学习
学习·算法·哈希算法
墨雪不会编程2 小时前
C++【string篇4】string结尾篇——字符编码表、乱码的来源及深浅拷贝
android·开发语言·c++
Engineer-Jsp2 小时前
A problem occurred starting process ‘command ‘bash‘‘
开发语言·bash
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 127 题:单词接龙
算法·leetcode·职场和发展