【优选算法】(实战剖析链表核心操作技巧)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法与数据结构的世界里,链表是一座连接理论与工程的关键桥梁,更是算法面试中考察频率极高的"黄金考点".它以非连续的内存存储特性,与数组形成鲜明分野------数组依赖连续空间实现随机访问,而链表通过指针/引用串联节点,在动态增删场景中展现出无可替代的灵活性.然而,许多学习者在链表学习与实战中常陷入两大困境:一是指针操作混乱,节点断裂、内存泄漏等问题频发;二是边界条件特判繁琐,头节点处理与空指针异常成为解题"拦路虎".这些问题的本质,在于对链表核心操作的底层逻辑理解不深,缺乏系统化的技巧拆解与实战演练.本文聚焦链表核心操作技巧,以实战剖析为核心,从原理深度解析与代码落地实现双维度展开.我们将逐一拆解哨兵节点(Dummy Node)的统一化思维、快慢指针的高效遍历策略、三指针反转的经典迭代逻辑,以及头插/尾插、指定位置增删、环检测等高频操作的底层实现.结合C++语言特性与LeetCode经典题型,我们不仅会给出可直接复用的代码模板,更会深入剖析每一步操作的设计思路与易错点,帮助你彻底打通指针逻辑,构建从原理到实战的完整知识闭环.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.链表算法思想背景介绍

链表的实现原理

链表是线性表的链式存储结构 ,其核心实现原理是:不依赖连续的内存空间存储数据,而是将数据封装为独立的节点,通过指针/引用将离散的节点串联成线性序列 ,所有增删改查操作均通过调整节点间的指针指向完成.它彻底解决了数组连续内存、扩容困难、插入删除效率低的痛点,是动态数据存储的核心实现方式.
1️⃣核心结构:链表的最小单元------节点

链表的所有实现都基于节点(Node),这是不可拆分的基本存储单元,每个节点固定包含两部分:

  1. 数据域(Data):存储实际需要保存的数据(如数字、字符串、对象等);
  2. 指针域(Next) :存储下一个节点的内存地址,用于建立节点之间的关联.

以最基础的单链表为例,节点的逻辑结构如下:

复制代码
+----------------+
| 数据域 | 指针域 |
|  Data  |  Next  |  ---> 指向【下一个节点】
+----------------+

2️⃣整体存储原理

  1. 内存分配:非连续、离散化
    数组需要预先申请一整块连续内存,而链表的节点可以散落在内存的任意位置,无需连续空间,系统会按需为每个节点分配内存.
  2. 关联方式:指针寻址
    节点之间没有物理上的相邻关系,完全依靠指针域记录的地址互相连接.就像一串钥匙扣,每个钥匙(节点)不挨着,但通过挂环(指针)串在一起.
  3. 链表入口:头指针(Head)
    链表必须有一个头指针,它不存储数据,仅指向链表的第一个节点;
  • 空链表:头指针直接指向 NULL(空地址);
  • 非空链表:从头指针出发,依次跳转指针,就能遍历所有节点.
  1. 链表结尾:尾节点
    最后一个节点的指针域不指向任何节点,固定赋值为 NULL,作为遍历结束的标志.

单链表完整逻辑结构:

复制代码
头指针 -> 节点1 -> 节点2 -> 节点3 -> ... -> 尾节点 -> NULL

3️⃣核心操作的实现原理(单链表)

链表的所有操作不移动数据本身 ,仅修改指针指向,这是它与数组最本质的区别:

1.遍历查找

  • 原理:从头指针开始,用临时指针依次跳转,直到遇到 NULL 停止;
  • 特点:只能顺序查找 ,无法像数组一样随机访问(不能直接找第n个节点),时间复杂度 O ( n ) O(n) O(n).

2.节点插入(以中间插入为例)

  • 创建新节点;
  • 让新节点的指针指向插入位置的后继节点;
  • 让插入位置的前驱节点指针指向新节点;
  • 特点:仅需修改2个指针,无需移动其他节点,时间复杂度 O ( 1 ) O(1) O(1)(忽略查找位置的耗时).

3.节点删除

  • 找到待删除节点的前驱节点;
  • 修改前驱节点的指针,直接指向待删除节点的后继节点(跳过待删除节点);
  • 释放待删除节点的内存;
  • 特点:断开指针关联即可,无数据迁移,效率极高.

4️⃣拓展链表的实现原理差异

在基础单链表之上,衍生出常用链表结构,原理仅在指针域上做扩展:

  1. 双向链表
    节点拥有两个指针:prev(指向前驱节点)+ next(指向后继节点),支持向前+向后双向遍历,解决了单链表无法回溯的问题.
  2. 循环链表
    尾节点的指针不指向 NULL,而是重新指向头节点,形成闭环,适合环形数据场景(如轮询调度).

2.两数相加(OJ题)


算法思路:解法(模拟两数相加的过程即可):

两个链表都是逆序存储数字的,即两个链表的个位数、⼗位数等都已经对应,可以直接相加.在相加过程中,我们要注意是否产⽣进位,产⽣进位时需要将进位和链表数字⼀同相加.如果产⽣进位的位置在链表尾部,即答案位数⽐原链表位数⻓⼀位,还需要再new⼀个结点储存最⾼位.


核心代码

cpp 复制代码
class Solution 
{
public:
    //函数功能:接收两个链表l1、l2,返回相加后的新链表
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
    {
        //1.定义遍历指针:不修改原链表头节点,用临时指针遍历
        ListNode *cur1 = l1, *cur2 = l2;
        
        //2.虚拟头节点(哨兵节点):链表操作核心技巧!
        // 作用:统一处理所有节点的插入逻辑,不用单独判断结果链表为空的情况
        ListNode* newhead = new ListNode(0); 
        
        //3.尾指针:始终指向结果链表的最后一个节点,方便尾插新节点
        ListNode* prev = newhead;            
        
        //4.进位变量:存储「当前位的和 + 上一位的进位」,初始进位为0
        int t = 0;                           
        
        //5.核心循环:三大终止条件(任意一个满足就继续循环)
        //cur1没遍历完 || cur2没遍历完 || 还有进位未处理
        while (cur1 || cur2 || t) 
        {
            //累加链表1当前位的值(如果节点存在)
            if (cur1) 
            {
                t += cur1->val;  //把当前数字加到总和里
                cur1 = cur1->next; //指针后移
            }
            //累加链表2当前位的值(如果节点存在)
            if (cur2) 
            {
                t += cur2->val;
                cur2 = cur2->next;
            }

            //6.构造结果节点:当前位的结果 = 总和 % 10(取个位数)
            prev->next = new ListNode(t % 10);
            prev = prev->next; //尾指针后移,指向新的末尾
            
            //7.更新进位:总和 / 10(整数除法,取十位数,0或1)
            t /= 10;
        }

        //8.内存管理:虚拟头节点是临时创建的,释放内存避免泄漏
        prev = newhead->next; // 真正的结果链表头节点
        delete newhead;       
        return prev; // 返回结果链表
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
using namespace std;

//1.定义链表节点结构体
struct ListNode
{
    int val;        //节点存储的值
    ListNode *next; //指向下一个节点的指针
    //构造函数:初始化节点
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution
{
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
    {
        ListNode *cur1 = l1, *cur2 = l2;
        ListNode* newhead = new ListNode(0);
        ListNode* prev = newhead;
        int t = 0;

        while (cur1 || cur2 || t)
        {
            if (cur1)
            {
                t += cur1->val;
                cur1 = cur1->next;
            }
            if (cur2)
            {
                t += cur2->val;
                cur2 = cur2->next;
            }
            prev->next = new ListNode(t % 10);
            prev = prev->next;
            t /= 10;
        }

        prev = newhead->next;
        delete newhead;
        return prev;
    }
};

//3.根据数组创建链表(尾插法)
ListNode* createList(int arr[], int n)
{
    if (n == 0) return NULL;
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < n; i++)
    {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

//4.打印链表
void printList(ListNode* head)
{
    ListNode* cur = head;
    while (cur)
    {
        cout << cur->val;
        if (cur->next) cout << " -> ";
        cur = cur->next;
    }
    cout << endl;
}

int main()
{
    Solution sol;

    // 测试用例 1:常规场景
    // l1 = 2 -> 4 -> 3  (数字 342)
    // l2 = 5 -> 6 -> 4  (数字 465)
    // 结果:7 -> 0 -> 8  (数字 807)
    int a1[] = {2, 4, 3};
    int a2[] = {5, 6, 4};
    ListNode* l1 = createList(a1, 3);
    ListNode* l2 = createList(a2, 3);
    cout << "测试用例1:" << endl;
    cout << "链表1:"; printList(l1);
    cout << "链表2:"; printList(l2);
    ListNode* res1 = sol.addTwoNumbers(l1, l2);
    cout << "相加结果:"; printList(res1);
    cout << "-------------------------" << endl;

    //  测试用例 2:最高位进位 
    // l1 = 9 -> 9 -> 9  (数字 999)
    // l2 = 9 -> 9 -> 9  (数字 999)
    // 结果:8 -> 9 -> 9 -> 1  (数字 1998)
    int a3[] = {9,9,9};
    int a4[] = {9,9,9};
    ListNode* l3 = createList(a3, 3);
    ListNode* l4 = createList(a4, 3);
    cout << "测试用例2:" << endl;
    cout << "链表1:"; printList(l3);
    cout << "链表2:"; printList(l4);
    ListNode* res2 = sol.addTwoNumbers(l3, l4);
    cout << "相加结果:"; printList(res2);
    cout << "-------------------------" << endl;

    // 测试用例 3:零值场景
    // l1 = 0  (数字 0)
    // l2 = 0  (数字 0)
    // 结果:0
    int a5[] = {0};
    int a6[] = {0};
    ListNode* l5 = createList(a5, 1);
    ListNode* l6 = createList(a6, 1);
    cout << "测试用例3:" << endl;
    cout << "链表1:"; printList(l5);
    cout << "链表2:"; printList(l6);
    ListNode* res3 = sol.addTwoNumbers(l5, l6);
    cout << "相加结果:"; printList(res3);

    return 0;
}

3.两两交换链表中的节点(OJ题)


算法思路:解法(模拟):

  1. 先处理特殊边界:链表为空/只有一个节点时,无需交换,直接返回原链表;
  2. 创建虚拟头节点:因为链表的头节点会参与交换,用虚拟头节点统一所有节点的操作逻辑,避免单独处理头节点;
  3. 多指针定位:定义4个指针,分别记录前驱节点、当前交换节点1、当前交换节点2、下一组交换节点,防止交换时链表断裂;
  4. 循环交换:只要当前组有两个节点,就执行指针交换操作,完成后将所有指针后移,处理下一组节点;
  5. 清理并返回:删除虚拟头节点,返回交换后的链表头.
  6. 4个指针分别锁定四个关键位置,交换时不会丢失链表节点的指向.
cpp 复制代码
ListNode *prev = newHead,  //前驱节点:指向当前交换组的前一个节点
         *cur = prev->next,//当前节点:待交换的第一个节点
         *next = cur->next,//后继节点:待交换的第二个节点
         *nnext = next->next;//下下节点:下一组待交换的第一个节点

核心代码

cpp 复制代码
class Solution 
{
public:
    //函数功能:输入链表头节点head,返回两两交换后的新链表头节点
    ListNode* swapPairs(ListNode* head) 
    {
        //1. 边界条件处理
        //如果链表为空,或者只有1个节点:无法两两交换,直接返回原链表
        if (head == nullptr || head->next == nullptr)
            return head;

        //2. 创建虚拟头节点(哨兵节点)
        //作用:解决【头节点会被交换】的问题,统一所有节点的交换逻辑,不用单独处理头节点
        ListNode* newHead = new ListNode(0);
        newHead->next = head; //让虚拟头节点指向原链表的第一个节点

        //3. 初始化4个关键指针
        // prev:指向待交换节点的**前一个节点**(初始是虚拟头节点)
        // cur:待交换的第一个节点
        // next:待交换的第二个节点
        // nnext:下一组待交换的第一个节点(防止交换后链表断裂)
        ListNode *prev = newHead, *cur = prev->next, *next = cur->next, *nnext = next->next;

        //4. 核心循环:只要有两个节点就交换 
        //循环条件:cur和next都存在,说明有两个节点可以交换
        while (cur && next) 
        {
            //核心:交换两个节点 
            //步骤1:前驱节点指向第二个节点(交换后,第二个节点变成组内第一个)
            prev->next = next;
            //步骤2:第二个节点指向第一个节点(完成交换)
            next->next = cur;
            //步骤3:第一个节点指向下一组的第一个节点(衔接后续链表,防止断裂)
            cur->next = nnext;

            //指针后移:准备交换下一组节点
            prev = cur;   //前驱指针移动到【交换后的尾节点】,为下一次交换做准备
            cur = nnext;  //当前指针移动到下一组的第一个节点
            //如果cur存在,更新next为下一个节点
            if (cur)
                next = cur->next;
            //如果next存在,更新nnext为下下个节点
            if (next)
                nnext = next->next;
        }

        //5. 清理内存 + 返回结果 
        cur = newHead->next;  //虚拟头节点的next就是交换后链表的**真实头节点**
        delete newHead;       //释放虚拟头节点,避免内存泄漏
        return cur;           //返回新链表
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
using namespace std;
//1. 链表节点结构体定义
struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution
{
public:
    //函数功能:输入链表头节点head,返回两两交换后的新链表头节点
    ListNode* swapPairs(ListNode* head)
    {
        //1.边界条件处理
        //如果链表为空,或者只有1个节点:无法两两交换,直接返回原链表
        if (head == nullptr || head->next == nullptr)
            return head;

        //2.创建虚拟头节点(哨兵节点)
        //作用:解决【头节点会被交换】的问题,统一所有节点的交换逻辑,不用单独处理头节点
        ListNode* newHead = new ListNode(0);
        newHead->next = head; // 让虚拟头节点指向原链表的第一个节点

        //3.初始化4个关键指针 
        //prev:指向待交换节点的**前一个节点**(初始是虚拟头节点)
        //cur:待交换的第一个节点
        //next:待交换的第二个节点
        //nnext:下一组待交换的第一个节点(防止交换后链表断裂)
        ListNode *prev = newHead, *cur = prev->next, *next = cur->next, *nnext = next->next;

        //4 核心循环:只要有两个节点就交换 
        //循环条件:cur和next都存在,说明有两个节点可以交换
        while (cur && next)
        {
            //步骤1:前驱节点指向第二个节点(交换后,第二个节点变成组内第一个)
            prev->next = next;
            //步骤2:第二个节点指向第一个节点(完成交换)
            next->next = cur;
            //步骤3:第一个节点指向下一组的第一个节点(衔接后续链表,防止断裂)
            cur->next = nnext;
            
            prev = cur;   //前驱指针移动到【交换后的尾节点】,为下一次交换做准备
            cur = nnext;  //当前指针移动到下一组的第一个节点
            //如果cur存在,更新next为下一个节点
            if (cur)
                next = cur->next;
            //如果next存在,更新nnext为下下个节点
            if (next)
                nnext = next->next;
        }

        //5. 清理内存 + 返回结果
        cur = newHead->next;  //虚拟头节点的next就是交换后链表的**真实头节点**
        delete newHead;       //释放虚拟头节点,避免内存泄漏
        return cur;           //返回新链表
    }
};

//根据数组创建链表(尾插法)
ListNode* createList(int arr[], int n)
{
    if (n == 0) return NULL;
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < n; i++)
    {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

//打印链表
void printList(ListNode* head)
{
    ListNode* cur = head;
    while (cur)
    {
        cout << cur->val;
        if (cur->next) cout << " -> ";
        cur = cur->next;
    }
    cout << endl;
}

int main()
{
    Solution sol;

    //测试用例1:偶数个节点(标准场景) 
    //输入:1 -> 2 -> 3 -> 4
    //输出:2 -> 1 -> 4 -> 3
    int a1[] = {1,2,3,4};
    ListNode* l1 = createList(a1, 4);
    cout << "测试用例1(偶数节点):" << endl;
    cout << "原链表:"; printList(l1);
    ListNode* res1 = sol.swapPairs(l1);
    cout << "交换后:"; printList(res1);
    cout << "-------------------------" << endl;

    //测试用例2:奇数个节点
    //输入:1 -> 2 -> 3
    //输出:2 -> 1 -> 3
    int a2[] = {1,2,3};
    ListNode* l2 = createList(a2, 3);
    cout << "测试用例2(奇数节点):" << endl;
    cout << "原链表:"; printList(l2);
    ListNode* res2 = sol.swapPairs(l2);
    cout << "交换后:"; printList(res2);
    cout << "-------------------------" << endl;

    //测试用例3:空链表 
    cout << "测试用例3(空链表):" << endl;
    ListNode* l3 = nullptr;
    cout << "原链表:空链表" << endl;
    ListNode* res3 = sol.swapPairs(l3);
    cout << "交换后:"; printList(res3);
    cout << "-------------------------" << endl;

    //测试用例4:单个节点 
    int a4[] = {5};
    ListNode* l4 = createList(a4, 1);
    cout << "测试用例4(单个节点):" << endl;
    cout << "原链表:"; printList(l4);
    ListNode* res4 = sol.swapPairs(l4);
    cout << "交换后:"; printList(res4);

    return 0;
}

4.重排链表(OJ题)


算法思路:解法(模拟)

步骤1:边界条件预处理

如果链表为空、只有1个节点、只有2个节点;无法/无需按照规则重排,直接退出函数,避免无效操作.

步骤2:快慢双指针寻找链表中间节点

  • 定义两个指针:slow(慢指针,每次走1步)、fast(快指针,每次走2步);
  • 遍历结束时,fast 到达链表末尾,slow 恰好指向链表中点;
  • 关键操作 :找到中点后,执行 slow->next = nullptr,将链表从中间断开 ,拆分为前半段后半段 两个独立链表,防止后续操作混乱.
    • 示例:1→2→3→4→5 断开后 → 前半段 1→2→3,后半段 4→5

步骤3:头插法反转后半段链表

  • 创建虚拟头节点,用头插法将后半段链表反转;
  • 作用:原后半段是正序,反转后变为倒序,刚好匹配重排需求(前正序+后倒序 = 目标顺序);
    • 示例:后半段 4→5 反转后 → 5→4.

步骤4:双指针交替合并两个链表

  • 用两个指针分别遍历前半段反转后的后半段;
  • 合并规则:先取前半段1个节点,再取后半段1个节点,循环交替拼接;
  • 处理奇偶长度:后半段节点数可能少于前半段,合并时判断后半段节点是否存在,避免空指针;
    • 示例:前半段1→2→3 + 反转后半段5→4 → 合并为 1→5→2→4→3.

步骤5:内存清理

释放代码中创建的虚拟头节点,避免内存泄漏.

核心代码

cpp 复制代码
class Solution 
{
public:
    void reorderList(ListNode* head) 
    {
        //处理边界情况
        if (head == nullptr || head->next == nullptr || head->next->next == nullptr)
            return;
        //1.找到链表的中间节点 - 快慢双指针(⼀定要考虑slow的落点在哪⾥)
        ListNode *slow = head, *fast = head;
        while (fast && fast->next) 
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        //2.把slow后⾯的部分给逆序 - 头插法
        ListNode* head2 = new ListNode(0);
        ListNode* cur = slow->next;
        slow->next = nullptr; //注意把两个链表给断开
        while (cur) {
            ListNode* next = cur->next;
            cur->next = head2->next;
            head2->next = cur;
            cur = next;
        }
        //3.合并两个链表 - 双指针
        ListNode* ret = new ListNode(0);
        ListNode* prev = ret;
        ListNode *cur1 = head, *cur2 = head2->next;
        while (cur1) 
        {
            //先放第⼀个链表
            prev->next = cur1;
            cur1 = cur1->next;
            prev = prev->next;
            //再放第⼆个链表
            if (cur2) 
            {
                prev->next = cur2;
                prev = prev->next;
                cur2 = cur2->next;
            }
        }
        delete head2;
        delete ret;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
using namespace std;

//链表节点结构体定义
struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution
{
public:
    void reorderList(ListNode* head)
    {
        //处理边界情况
        if (head == nullptr || head->next == nullptr || head->next->next == nullptr)
            return;
        //1.找到链表的中间节点 - 快慢双指针
        ListNode *slow = head, *fast = head;
        while (fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        //2.把slow后面的部分给逆序 - 头插法
        ListNode* head2 = new ListNode(0);
        ListNode* cur = slow->next;
        slow->next = nullptr; //断开两个链表
        while (cur) {
            ListNode* next = cur->next;
            cur->next = head2->next;
            head2->next = cur;
            cur = next;
        }
        //3.合并两个链表 - 双指针
        ListNode* ret = new ListNode(0);
        ListNode* prev = ret;
        ListNode *cur1 = head, *cur2 = head2->next;
        while (cur1)
        {
            //先放第一个链表
            prev->next = cur1;
            cur1 = cur1->next;
            prev = prev->next;
            //再放第二个链表
            if (cur2)
            {
                prev->next = cur2;
                prev = prev->next;
                cur2 = cur2->next;
            }
        }
        delete head2;
        delete ret;
    }
};

//数组转链表
ListNode* createList(int arr[], int n)
{
    if (n == 0) return NULL;
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < n; i++)
    {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

//打印链表
void printList(ListNode* head)
{
    if (!head) {
        cout << "空链表" << endl;
        return;
    }
    ListNode* cur = head;
    while (cur)
    {
        cout << cur->val;
        if (cur->next) cout << " -> ";
        cur = cur->next;
    }
    cout << endl;
}

int main()
{
    Solution sol;

    //测试用例1:偶数长度链表
    //原链表:1->2->3->4
    //重排后:1->4->2->3
    int a1[] = {1,2,3,4};
    ListNode* l1 = createList(a1, 4);
    cout << "测试用例1(偶数节点):" << endl;
    cout << "原链表:"; printList(l1);
    sol.reorderList(l1);
    cout << "重排后:"; printList(l1);
    cout << "-------------------------" << endl;

    //测试用例2:奇数长度链表
    //原链表:1->2->3->4->5
    //重排后:1->5->2->4->3
    int a2[] = {1,2,3,4,5};
    ListNode* l2 = createList(a2, 5);
    cout << "测试用例2(奇数节点):" << endl;
    cout << "原链表:"; printList(l2);
    sol.reorderList(l2);
    cout << "重排后:"; printList(l2);
    cout << "-------------------------" << endl;

    //测试用例3:空链表 
    cout << "测试用例3(空链表):" << endl;
    ListNode* l3 = nullptr;
    cout << "原链表:"; printList(l3);
    sol.reorderList(l3);
    cout << "重排后:"; printList(l3);
    cout << "-------------------------" << endl;

    //测试用例4:单个节点
    int a4[] = {6};
    ListNode* l4 = createList(a4, 1);
    cout << "测试用例4(单个节点):" << endl;
    cout << "原链表:"; printList(l4);
    sol.reorderList(l4);
    cout << "重排后:"; printList(l4);
    cout << "-------------------------" << endl;

    //测试用例5:两个节点 
    int a5[] = {7,8};
    ListNode* l5 = createList(a5, 2);
    cout << "测试用例5(两个节点):" << endl;
    cout << "原链表:"; printList(l5);
    sol.reorderList(l5);
    cout << "重排后:"; printList(l5);

    return 0;
}

5.合并k个升序链表(OJ题)


算法思路:解法⼀(利⽤堆):

合并两个有序链表是⽐较简单且做过的,就是⽤双指针依次⽐较链表1 、链表2未排序的最⼩元素,选择更⼩的那⼀个加⼊有序的答案链表中.合并K个升序链表时,我们依旧可以选择K个链表中,头结点值最⼩的那⼀个.那么如何快速的得到头结点最⼩的是哪⼀个呢?⽤堆这个数据结构就好啦,我们可以把所有的头结点放进⼀个⼩根堆中,这样就能快速的找到每次K个链表中,最⼩的元素是哪个.

步骤1:自定义小根堆比较规则

  1. C++ 默认的优先队列是大根堆(堆顶为最大值),不符合需求;
  2. 自定义结构体 cmp 重载运算符,将优先队列改为小根堆 ,保证堆顶始终是值最小的链表节点.

步骤2:初始化小根堆

  1. 遍历 K 个链表,将每个链表的非空头节点依次加入小根堆;
  2. 此时堆中包含了所有链表的第一个节点,堆顶就是全局最小的节点.

步骤3:创建虚拟头节点

创建虚拟头节点 ret,用于统一拼接结果链表,避免处理头节点为空的边界情况.

步骤4:循环合并链表(核心逻辑)

  1. 取出堆顶的最小节点,将其拼接到结果链表的末尾;
  2. 移动结果链表的指针,指向新的末尾;
  3. 关键操作:如果取出的节点还有后继节点,就将其后继节点加入堆中)保持堆里始终有各链表当前的候选最小节点);
  4. 重复上述操作,直到堆中没有任何节点.

步骤5:清理并返回结果

  1. 虚拟头节点的下一个节点,就是合并后链表的真实头节点;
  2. 释放虚拟头节点的内存,避免内存泄漏;
  3. 返回最终的合并链表.

核心代码

cpp 复制代码
class Solution 
{
    struct cmp 
    {
        bool operator()(const ListNode* l1, const ListNode* l2) 
        {
            return l1->val > l2->val;
        }
    };
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        //创建⼀个⼩根堆
        priority_queue<ListNode*, vector<ListNode*>, cmp> heap;
        //让所有的头结点进⼊⼩根堆
        for (auto l : lists)
            if (l)
                heap.push(l);
        //合并k个有序链表
        ListNode* ret = new ListNode(0);
        ListNode* prev = ret;
        while (!heap.empty()) 
        {
            ListNode* t = heap.top();
            heap.pop();
            prev->next = t;
            prev = t;
            if (t->next)
                heap.push(t->next);
        }
        prev = ret->next;
        delete ret;
        return prev;
    }
};

算法思路:解法⼆(递归/分治思想):

逐⼀⽐较时,答案链表越来越⻓,每个跟它合并的⼩链表的元素都需要⽐较很多次才可以成功排序.

⽐如,我们有8个链表,每个链表⻓为100.逐⼀合并时,我们合并链表的⻓度分别为(0, 100), (100, 100), (200, 100), (300, 100), (400, 100), (500,100), (600, 100), (700, 100).所有链表的总⻓度共计3600.

如果尽可能让⻓度相同的链表进⾏两两合并呢?这时合并链表的⻓度分别是(100, 100) x 4, (200, 200)

x 2, (400, 400),共计2400.⽐上⼀种的计算量整整少了1/3.迭代的做法代码细节会稍多⼀些,这⾥给出递归的实现,代码相对简洁,不易写错.

算法流程:

  1. 特判,如果题⽬给出空链表,⽆需合并,直接返回;
  2. 返回递归结果.

递归函数设计:

  1. 递归出⼝:如果当前要合并的链表编号范围左右值相等,⽆需合并,直接返回当前链表;
  2. 应⽤⼆分思想,等额划分左右两段需要合并的链表,使这两段合并后的⻓度尽可能相等;
  3. 对左右两段分别递归,合并[l, r]范围内的链表;
  4. 再调⽤ mergeTwoLists 函数进⾏合并(就是合并两个有序链表)

步骤1:主函数入口

主函数 mergeKLists 仅做一件事:调用递归分治函数 ,传入链表数组和完整区间 [0, lists.size()-1],启动分治流程.

步骤2:递归分治拆分(核心函数 merge)

该函数负责拆分问题+递归求解:

  1. 终止条件
    • 区间无效(left > right):返回空指针;
    • 区间只剩1个链表 (left == right):无需合并,直接返回该链表(递归到底层).
  2. 二分拆分
    计算区间中点 mid,将K个链表平分为左右两个子区间,把大问题拆分为两个规模减半的子问题.
  3. 递归求解
    分别递归处理左区间 [left, mid]、右区间 [mid+1, right],得到两个已合并完成的有序链表 l1l2.
  4. 两两合并
    调用 mergeTwoList 函数,合并 l1l2,返回合并后的有序链表.

步骤3:双指针合并两个有序链表(基础函数 mergeTwoList)

这是分治算法的基础操作,专门解决合并两个有序链表:

  1. 边界处理:若其中一个链表为空,直接返回另一个链表;
  2. 虚拟头节点:创建临时虚拟头节点,统一拼接逻辑,无需处理头节点为空的情况;
  3. 双指针遍历 :同时遍历两个链表,取节点值较小的节点拼接到结果链表;
  4. 拼接剩余节点:一个链表遍历完后,直接拼接另一个链表的剩余部分;
  5. 返回合并后的有序链表.

步骤4:递归回溯归并

所有子区间的合并结果会逐层向上合并 ,最终将K个链表合并为一个全局有序的链表.

核心代码

cpp 复制代码
class Solution 
{
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        return merge(lists, 0, lists.size() - 1);
    }
    ListNode* merge(vector<ListNode*>& lists, int left, int right) 
    {
        if (left > right)
            return nullptr;
        if (left == right)
            return lists[left];
        //1.平分数组
        int mid = left + right >> 1;
        //[left, mid] [mid + 1, right]
        //2.递归处理左右区间
        ListNode* l1 = merge(lists, left, mid);
        ListNode* l2 = merge(lists, mid + 1, right);
        //3.合并两个有序链表
        return mergeTowList(l1, l2);
    }
    ListNode* mergeTowList(ListNode* l1, ListNode* l2) 
    {
        if (l1 == nullptr)
            return l2;
        if (l2 == nullptr)
            return l1;
        //合并两个有序链表
        ListNode head;
        ListNode *cur1 = l1, *cur2 = l2, *prev = &head;
        head.next = nullptr;
        while (cur1 && cur2) 
        {
            if (cur1->val <= cur2->val) 
            {
                prev = prev->next = cur1;
                cur1 = cur1->next;
            } else 
            {
                prev = prev->next = cur2;
                cur2 = cur2->next;
            }
        }
        if (cur1)
            prev->next = cur1;
        if (cur2)
            prev->next = cur2;
        return head.next;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

//链表节点结构体定义
struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x = 0) : val(x), next(NULL) {}
};

class Solution
{
public:
    ListNode* mergeKLists(vector<ListNode*>& lists)
    {
        return merge(lists, 0, lists.size() - 1);
    }
    ListNode* merge(vector<ListNode*>& lists, int left, int right)
    {
        if (left > right)
            return nullptr;
        if (left == right)
            return lists[left];
        //平分数组
        int mid = left + right >> 1;
        //递归处理左右区间
        ListNode* l1 = merge(lists, left, mid);
        ListNode* l2 = merge(lists, mid + 1, right);
        //合并两个有序链表
        return mergeTowList(l1, l2);
    }
    ListNode* mergeTowList(ListNode* l1, ListNode* l2)
    {
        if (l1 == nullptr)
            return l2;
        if (l2 == nullptr)
            return l1;
        //合并两个有序链表
        ListNode head;
        ListNode *cur1 = l1, *cur2 = l2, *prev = &head;
        head.next = nullptr;
        while (cur1 && cur2)
        {
            if (cur1->val <= cur2->val)
            {
                prev = prev->next = cur1;
                cur1 = cur1->next;
            }
            else
            {
                prev = prev->next = cur2;
                cur2 = cur2->next;
            }
        }
        if (cur1)
            prev->next = cur1;
        if (cur2)
            prev->next = cur2;
        return head.next;
    }
};

//数组转链表
ListNode* createList(int arr[], int n)
{
    if (n == 0) return NULL;
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < n; i++)
    {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

//打印链表
void printList(ListNode* head)
{
    if (!head)
    {
        cout << "空链表" << endl;
        return;
    }
    ListNode* cur = head;
    while (cur)
    {
        cout << cur->val;
        if (cur->next) cout << " -> ";
        cur = cur->next;
    }
    cout << endl;
}

int main()
{
    Solution sol;

    //测试用例1:标准场景(3个有序链表)
    //链表1:1->4->5
    //链表2:1->3->4
    //链表3:2->6
    //合并结果:1->1->2->3->4->4->5->6
    cout << "===== 测试用例1:3个有序链表 =====" << endl;
    int a1[] = {1,4,5};
    int a2[] = {1,3,4};
    int a3[] = {2,6};
    ListNode* l1 = createList(a1, 3);
    ListNode* l2 = createList(a2, 3);
    ListNode* l3 = createList(a3, 2);
    vector<ListNode*> lists1 = {l1, l2, l3};
    ListNode* res1 = sol.mergeKLists(lists1);
    cout << "合并结果:"; printList(res1);

    //测试用例2:空链表数组 
    cout << "\n===== 测试用例2:空链表数组 =====" << endl;
    vector<ListNode*> lists2;
    ListNode* res2 = sol.mergeKLists(lists2);
    cout << "合并结果:"; printList(res2);

    //测试用例3:包含空链表的场景 
    cout << "\n===== 测试用例3:包含空链表 =====" << endl;
    int a4[] = {7,9};
    ListNode* l4 = createList(a4, 2);
    vector<ListNode*> lists3 = {nullptr, l4, nullptr};
    ListNode* res3 = sol.mergeKLists(lists3);
    cout << "合并结果:"; printList(res3);

    //测试用例4:单个链表 
    cout << "\n===== 测试用例4:单个链表 =====" << endl;
    int a5[] = {2,3,5,8};
    ListNode* l5 = createList(a5, 4);
    vector<ListNode*> lists4 = {l5};
    ListNode* res4 = sol.mergeKLists(lists4);
    cout << "合并结果:"; printList(res4);

    return 0;
}

6.k个一组翻转链表(OJ题)


算法思路:解法(模拟):

我们可以把链表按K个为⼀组进⾏分组,组内进⾏反转,并且记录反转后的头尾结点,使其可以和前、后连接起来.思路⽐较简单,但是实现起来是⽐较复杂的.我们可以先求出⼀共需要逆序多少组(假设逆序n组),然后重复n次⻓度为 k 的链表的逆序即可.

步骤1:统计总节点数,计算需要翻转的组数

  1. 遍历整个链表,统计总节点数量;
  2. 计算翻转组数:组数 = 总节点数 / k(整数除法,自动舍去余数,余数即为最后不足k个、不翻转的节点);
    ✅ 作用:提前确定循环翻转次数,无需在翻转时判断节点数量.

步骤2:初始化辅助节点

  1. 创建虚拟头节点 newHead:固定链表入口,统一所有组的翻转逻辑,无需单独处理头节点翻转;
  2. prev 指针:始终指向当前待翻转组的前驱节点,用于头插法拼接翻转后的节点;
  3. cur 指针:遍历原链表,指向当前待处理的节点.

步骤3:循环翻转每一组(核心逻辑)

循环执行 组数 次,每次翻转长度为 k 的节点:

  1. 保存当前组的第一个节点 tmp:该节点翻转后会变成当前组的最后一个节点,用于定位下一组的前驱;
  2. 对本组 k 个节点执行头插法翻转 :
    逐个将节点插入到 prev->next 位置,实现本组节点的逆序;
  3. 移动指针:将 prev 移动到 tmp(当前组尾节点),准备下一组翻转.

步骤4:拼接剩余未翻转节点

循环结束后,cur 指向最后不足k个、无需翻转的节点,直接将其拼接到已翻转链表的末尾即可.

步骤5:清理内存并返回结果

  1. 虚拟头节点的下一个节点,就是翻转后的完整链表头;
  2. 释放虚拟头节点,避免内存泄漏;
  3. 返回最终结果链表.

举例演示

链表:1->2->3->4->5k=2

  1. 总节点数=5,组数=5/2=2组;
  2. 第一组翻转:1->22->1
  3. 第二组翻转:3->44->3
  4. 剩余节点 5 直接拼接;
  5. 最终结果:2->1->4->3->5

核心代码

cpp 复制代码
class Solution 
{
public:
    ListNode* reverseKGroup(ListNode* head, int k) 
    {
        //1.先求出需要逆序多少组
        int n = 0;
        ListNode* cur = head;
        while (cur) 
        {
            cur = cur->next;
            n++;
        }
        n /= k;
        //2.重复 n 次:⻓度为 k 的链表的逆序即可
        ListNode* newHead = new ListNode(0);
        ListNode* prev = newHead;
        cur = head;
        for (int i = 0; i < n; i++) 
        {
            ListNode* tmp = cur;
            for (int j = 0; j < k; j++) 
            {
                ListNode* next = cur->next;
                cur->next = prev->next;
                prev->next = cur;
                cur = next;
            }
            prev = tmp;
        }
        //把不需要翻转的接上
        prev->next = cur;
        cur = newHead->next;
        delete newHead;
        return cur;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
using namespace std;

//链表节点结构体定义
struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution
{
public:
    ListNode* reverseKGroup(ListNode* head, int k)
    {
        //1.先求出需要逆序多少组
        int n = 0;
        ListNode* cur = head;
        while (cur)
        {
            cur = cur->next;
            n++;
        }
        n /= k;
        //2.重复 n 次:长度为 k 的链表的逆序即可
        ListNode* newHead = new ListNode(0);
        ListNode* prev = newHead;
        cur = head;
        for (int i = 0; i < n; i++)
        {
            ListNode* tmp = cur;
            for (int j = 0; j < k; j++)
            {
                ListNode* next = cur->next;
                cur->next = prev->next;
                prev->next = cur;
                cur = next;
            }
            prev = tmp;
        }
        //把不需要翻转的接上
        prev->next = cur;
        cur = newHead->next;
        delete newHead;
        return cur;
    }
};

//数组转链表(尾插法)
ListNode* createList(int arr[], int n)
{
    if (n == 0) return NULL;
    ListNode* head = new ListNode(arr[0]);
    ListNode* cur = head;
    for (int i = 1; i < n; i++)
    {
        cur->next = new ListNode(arr[i]);
        cur = cur->next;
    }
    return head;
}

//打印链表
void printList(ListNode* head)
{
    if (!head)
    {
        cout << "空链表" << endl;
        return;
    }
    ListNode* cur = head;
    while (cur)
    {
        cout << cur->val;
        if (cur->next) cout << " -> ";
        cur = cur->next;
    }
    cout << endl;
}

int main()
{
    Solution sol;

    //测试用例1:标准场景 k=2 
    //原链表:1->2->3->4->5
    //翻转后:2->1->4->3->5
    int a1[] = {1,2,3,4,5};
    ListNode* l1 = createList(a1, 5);
    cout << "测试用例1 (k=2):" << endl;
    cout << "原链表:"; printList(l1);
    ListNode* res1 = sol.reverseKGroup(l1, 2);
    cout << "翻转后:"; printList(res1);
    cout << "-------------------------" << endl;

    //测试用例2:k=3 
    //原链表:1->2->3->4->5
    //翻转后:3->2->1->4->5
    int a2[] = {1,2,3,4,5};
    ListNode* l2 = createList(a2, 5);
    cout << "测试用例2 (k=3):" << endl;
    cout << "原链表:"; printList(l2);
    ListNode* res2 = sol.reverseKGroup(l2, 3);
    cout << "翻转后:"; printList(res2);
    cout << "-------------------------" << endl;

    //测试用例3:k=1(不翻转)
    int a3[] = {1,2,3,4};
    ListNode* l3 = createList(a3, 4);
    cout << "测试用例3 (k=1):" << endl;
    cout << "原链表:"; printList(l3);
    ListNode* res3 = sol.reverseKGroup(l3, 1);
    cout << "翻转后:"; printList(res3);
    cout << "-------------------------" << endl;

    //测试用例4:空链表 
    cout << "测试用例4(空链表):" << endl;
    ListNode* l4 = nullptr;
    cout << "原链表:"; printList(l4);
    ListNode* res4 = sol.reverseKGroup(l4, 2);
    cout << "翻转后:"; printList(res4);
    cout << "-------------------------" << endl;

    //测试用例5:k等于链表长度 
    //原链表:6->7->8  k=3
    //翻转后:8->7->6
    int a5[] = {6,7,8};
    ListNode* l5 = createList(a5, 3);
    cout << "测试用例5 (k=链表长度):" << endl;
    cout << "原链表:"; printList(l5);
    ListNode* res5 = sol.reverseKGroup(l5, 3);
    cout << "翻转后:"; printList(res5);

    return 0;
}

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【优选算法】(实战玩转哈希表:底层逻辑与刷题技巧)


每日心灵鸡汤:做自己,而不是解释自己!
你没有必要不停地向别人说其实我是怎样的人,因为这是无效的,人们只愿意看到他们愿意看到的你,
不用在意这样的话语,那样的目光,没有必要让所有人都知道真实的你.干掉标准答案,你就是标准答案,因为相信你的人自然相信你,而不相信你的人百口莫辩,你的时间就那么长,外界的声音只是参考,不喜欢就不参考.你就是你,他人就是他人,请好好爱自己,尊重理解自己.大风刮倒梧桐树,自有旁人论长短.

相关推荐
Boop_wu2 小时前
[Java算法] 递归(1)
java·算法·深度优先
wjs20242 小时前
Shell 变量
开发语言
代码改善世界2 小时前
【C++初阶】string类(二):常用接口全解析
开发语言·c++
前端郭德纲2 小时前
JavaScript原生开发与鸿蒙原生开发对比
开发语言·javascript·harmonyos
li星野2 小时前
DeepSeek-V3介绍
学习
stolentime2 小时前
树套树+标记永久化:[POI 2006] TET-Tetris 3D&&SPOJ1741 TETRIS3D - Tetris 3D题解
c++·算法·线段树·树套树·标记永久化
csbysj20202 小时前
JSP 指令
开发语言
XiYang-DING2 小时前
【LeetCode】链表 + 快慢指针找倒数结点 | 链表中倒数第k个结点
算法·leetcode·链表
LSL666_2 小时前
JVM面试题——垃圾回收GC
java·开发语言·jvm