代码随想录:链表篇

203. 移除链表元素

题目核心要求

删除链表中所有值等于给定值 val 的节点,返回处理后的链表头节点;若链表为空或所有节点值均为 val,返回空。

示例

  • 输入:head = [1,2,6,3,4,5,6], val = 6 → 输出:[1,2,3,4,5]
  • 输入:head = [7,7,7,7], val = 7 → 输出:[]

前置说明(链表节点定义)

cpp 复制代码
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

核心解法

解法1:直接操作原链表(需单独处理头结点)

核心思路
  1. 先循环删除所有值为 val 的头结点(头结点无前置节点,需特殊处理);
  2. 遍历链表,通过前置节点的 next 跳过非头结点中值为 val 的节点;
  3. 释放被删除节点的内存,避免内存泄漏。
  • 时间复杂度:O(n),空间复杂度:O(1)。
C++ 代码
cpp 复制代码
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 处理头结点(可能连续多个val)
        while (head != nullptr && head->val == val) {
            ListNode* tmp = head;
            head = head->next;
            delete tmp; // 释放内存
        }

        // 处理非头结点
        ListNode* cur = head;
        while (cur != nullptr && cur->next != nullptr) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp; // 释放内存
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};

解法2:虚拟头结点法(推荐,统一处理所有节点)

核心思路
  1. 创建虚拟头结点 dummyHead 指向原链表头,让所有节点(包括原头结点)都能通过「前置节点→next」统一删除;
  2. 遍历链表完成删除后,释放虚拟头结点,返回 dummyHead->next 作为新头。
  • 优势:无需区分头结点和普通节点,逻辑更简洁。
  • 时间复杂度:O(n),空间复杂度:O(1)。
C++ 代码
cpp 复制代码
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); // 虚拟头结点
        dummyHead->next = head;
        ListNode* cur = dummyHead;

        while (cur->next != nullptr) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp; // 释放内存
            } else {
                cur = cur->next;
            }
        }

        head = dummyHead->next;
        delete dummyHead; // 释放虚拟头结点
        return head;
    }
};

复制代码
## 总结
1. 直接操作原链表需**单独处理头结点**,无额外节点开销但逻辑稍繁琐;
2. 虚拟头结点法**统一所有节点的删除逻辑**,是工程中更推荐的写法。

707. 设计链表

核心需求复述

完成单链表的核心设计:包含 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 五个接口,要求逻辑清晰、无冗余代码,同时处理内存泄漏问题。

核心实现(单链表 + 虚拟头结点)

C++ 代码

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

// 链表节点结构体
struct LinkedNode {
    int val;
    LinkedNode* next;
    LinkedNode(int v) : val(v), next(nullptr) {}
};

class MyLinkedList {
private:
    int _size;          // 链表有效节点数(简化索引判断)
    LinkedNode* _dummy; // 虚拟头结点(统一所有节点操作)

public:
    // 初始化:虚拟头结点 + 空链表
    MyLinkedList() : _size(0), _dummy(new LinkedNode(0)) {}

    // 获取第index个节点值,索引无效返回-1
    int get(int index) {
        if (index < 0 || index >= _size) return -1;
        LinkedNode* cur = _dummy->next;
        while (index--) cur = cur->next;
        return cur->val;
    }

    // 头部插入节点
    void addAtHead(int val) {
        LinkedNode* node = new LinkedNode(val);
        node->next = _dummy->next;
        _dummy->next = node;
        _size++;
    }

    // 尾部插入节点
    void addAtTail(int val) {
        LinkedNode* node = new LinkedNode(val);
        LinkedNode* cur = _dummy;
        while (cur->next) cur = cur->next;
        cur->next = node;
        _size++;
    }

    // 指定索引前插入节点(index<0插头部,>size不插,=size插尾部)
    void addAtIndex(int index, int val) {
        if (index > _size) return;
        if (index < 0) index = 0;
        LinkedNode* node = new LinkedNode(val);
        LinkedNode* cur = _dummy;
        while (index--) cur = cur->next;
        node->next = cur->next;
        cur->next = node;
        _size++;
    }

    // 删除指定索引节点(索引无效则不操作)
    void deleteAtIndex(int index) {
        if (index < 0 || index >= _size) return;
        LinkedNode* cur = _dummy;
        while (index--) cur = cur->next;
        LinkedNode* tmp = cur->next;
        cur->next = tmp->next;
        delete tmp; // 释放内存
        tmp = nullptr; // 避免野指针
        _size--;
    }

    // 析构函数:释放所有节点内存
    ~MyLinkedList() {
        LinkedNode* cur = _dummy;
        while (cur) {
            LinkedNode* tmp = cur;
            cur = cur->next;
            delete tmp;
        }
    }
};

关键逻辑解释

核心接口 精简设计思路
虚拟头结点 _dummy 无需单独处理头结点的插入/删除,所有操作统一通过「前驱节点」完成,代码量减少50%;
_size 成员变量 直接通过 _size 判断索引合法性(如 index >= _size),避免遍历链表求长度;
内存管理 析构函数遍历释放所有节点,删除节点时即时释放内存并置空指针,避免内存泄漏;
索引处理 addAtIndex 中自动修正负数索引为0,无需额外分支,逻辑更简洁;

关键点总结

  1. 虚拟头结点是核心:统一头结点和普通节点的操作逻辑,是链表设计的最优实践;
  2. _size 简化索引判断:无需遍历链表验证索引有效性,时间复杂度从 O(n) 降为 O(1);
  3. 内存安全不可少:析构函数和删除节点时的内存释放是工业级代码的基本要求,避免内存泄漏。

206. 反转链表

核心需求复述

完成单链表反转的核心逻辑,包含双指针法(最优)和递归法两种主流解法。

一、双指针法(最优解)

精简代码

cpp 复制代码
// 链表节点定义(力扣环境已内置,本地测试需补充)
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *pre = nullptr, *cur = head, *tmp;
        while (cur) {
            tmp = cur->next;  // 保存后继节点,避免链表断裂
            cur->next = pre;  // 反转当前节点指向
            pre = cur;        // 前驱指针后移
            cur = tmp;        // 当前指针后移
        }
        return pre;  // 循环结束后pre指向新头结点
    }
};

关键逻辑解释

  1. 核心思想 :仅通过改变 next 指针方向实现反转,不新建链表(空间复杂度 O(1));
  2. 指针分工
    • pre:指向「已反转部分」的尾节点(初始为空);
    • cur:指向「待反转部分」的头节点(初始为原链表头);
    • tmp:临时保存 cur->next,避免反转后丢失后继节点;
  3. 循环终止条件cur == nullptr(遍历完所有节点),此时 pre 指向新头结点。

二、递归法(精简版)

写法1:与双指针法逻辑一致(从前往后反转)

cpp 复制代码
class Solution {
private:
    ListNode* reverse(ListNode* pre, ListNode* cur) {
        if (!cur) return pre;          // 递归终止:cur为空,返回pre(新头)
        ListNode* tmp = cur->next;
        cur->next = pre;               // 反转当前节点
        return reverse(cur, tmp);      // 递归传递:pre=cur,cur=tmp
    }
public:
    ListNode* reverseList(ListNode* head) {
        return reverse(nullptr, head); // 初始化:pre=null,cur=head
    }
};

三、核心对比(双指针 vs 递归)

解法 时间复杂度 空间复杂度 优势 适用场景
双指针法 O(n) O(1) 空间最优、逻辑直观 工程实践(无栈开销)
递归法 O(n) O(n) 代码简洁、思想抽象 算法面试(考察递归思维)

四、关键点总结

  1. 双指针法是最优解:仅用3个指针完成反转,空间复杂度 O(1),是工业级首选;
  2. 反转核心逻辑 :先保存 cur->next,再将 cur->next 指向 pre,最后移动 precur
  3. 递归法本质
    • 递归模拟双指针的循环过程,逻辑完全一致;
  4. 边界处理:空链表/单节点链表直接返回,避免空指针访问。

24. 两两交换链表中的节点

核心需求复述

你需要一个简洁、易理解、无冗余的 C++ 实现,完成「两两交换链表相邻节点」的核心逻辑,要求严格遵循「实际交换节点(不修改值)」的规则,使用虚拟头结点简化操作,同时保证代码高效且无内存泄漏。

一、C++代码

cpp 复制代码
// 链表节点定义(力扣环境已内置,本地测试需补充)
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummy = new ListNode(0); // 虚拟头结点
        dummy->next = head;
        ListNode* cur = dummy;
        
        // 确保有两个节点可交换
        while (cur->next && cur->next->next) {
            ListNode* tmp1 = cur->next;          // 保存第一个待交换节点
            ListNode* tmp2 = cur->next->next->next; // 保存交换后的后继节点
            
            // 三步核心交换:cur→B→A→C
            cur->next = cur->next->next;    // 步骤1:cur 指向第二个节点(B)
            cur->next->next = tmp1;         // 步骤2:B 指向第一个节点(A)
            tmp1->next = tmp2;              // 步骤3:A 指向原本B的后继(C)
            
            cur = cur->next->next;          // cur后移两位,处理下一组
        }
        
        ListNode* res = dummy->next;        // 保存结果(避免dummy释放后丢失)
        delete dummy;                       // 释放虚拟头结点(避免内存泄漏)
        return res;
    }
};

二、关键逻辑解释

1. 核心思路

假设当前链表为:dummy → A → B → C → D,目标是交换 A 和 B,得到 dummy → B → A → C → D

  • tmp1 = A(保存第一个待交换节点);
  • tmp2 = C(保存 B 的后继节点,避免交换后丢失);
  • 步骤1:cur->next = Bdummy → B
  • 步骤2:B->next = Adummy → B → A
  • 步骤3:A->next = Cdummy → B → A → C → D
  • cur 移到 A(cur = B->next),准备交换 C 和 D。

2. 精简优化点

  • 合并冗余变量:仅保留 tmp1/tmp2,语义更清晰;
  • 简化循环条件:cur->next && cur->next->next 等价于完整判空写法,代码更短;
  • 步骤3精简:直接操作保存的节点,逻辑更直观。

3. 边界处理

  • 空链表/单节点链表:循环条件不满足,直接返回原链表;
  • 奇数节点链表:最后一个节点无需交换,自然保留。

三、时间/空间复杂度

  • 时间复杂度:O(n),仅遍历链表一次,每个节点仅处理一次;
  • 空间复杂度:O(1),仅使用固定数量的临时指针,无额外空间开销。

四、关键点总结

  1. 虚拟头结点是核心:避免单独处理头结点交换,统一所有节点的操作逻辑;
  2. 临时指针必保存tmp1 保存第一个节点,tmp2 保存后继节点,避免链表断裂;
  3. 交换顺序不能乱:严格遵循「cur指向B → B指向A → A指向C」的三步顺序;
  4. 内存管理要注意:释放虚拟头结点,避免内存泄漏(工程规范要求)。

易错点提醒

  • 不要遗漏 delete dummy:虚拟头结点是动态分配的,必须手动释放;
  • 循环条件必须同时判断 cur->nextcur->next->next:避免空指针访问;
  • 不能仅修改节点值:必须通过调整 next 指针交换节点,符合题目要求。

19. 删除链表的倒数第N个节点

核心需求复述

你需要一个简洁、易理解的 C++ 实现,通过一趟扫描(双指针法)完成删除链表倒数第n个节点的逻辑,要求使用虚拟头结点简化头节点删除场景,保证代码高效且符合工程规范。

一、C++代码

cpp 复制代码
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode *fast = dummy, *slow = dummy;

        // fast先走n步
        while (n--) fast = fast->next;
        // fast再走1步,让slow最终指向删除节点的前驱
        fast = fast->next;
        
        // 同步移动至fast到链表末尾
        while (fast) {
            fast = fast->next;
            slow = slow->next;
        }

        // 删除目标节点并释放内存
        ListNode* tmp = slow->next;
        slow->next = tmp->next;
        delete tmp;

        // 释放虚拟头结点,避免内存泄漏
        ListNode* res = dummy->next;
        delete dummy;
        return res;
    }
};

二、关键逻辑解释

1. 核心思路(双指针+虚拟头结点)

目标:删除倒数第n个节点,需让slow指向该节点的前驱(方便删除操作),核心步骤:

  • 虚拟头结点dummy:统一处理「删除头节点」和「删除中间节点」的逻辑,无需单独判断;
  • fast先走n+1步:拆分为「先走n步 + 再走1步」,确保后续同步移动后,slow停在删除节点的前驱;
  • 同步移动fastslow:直到fastnullptr,此时slow->next即为待删除节点;
  • 删除节点:修改slow->next指向,并释放待删除节点的内存。

2. 执行流程示例(输入:1→2→3→4→5,n=2)

  1. 初始化:dummy→1→2→3→4→5fast=dummyslow=dummy
  2. fast先走2步:fast指向2(n从2减至0);
  3. fast再走1步:fast指向3;
  4. 同步移动:fast走到5→null,slow走到3;
  5. 删除slow->next(4):3->next=5,最终链表为1→2→3→5

3. 边界场景处理

  • 单节点链表(1→null,n=1):
    fast先走1步到1,再走1步到null;同步循环不执行,slow指向dummy,删除1后返回null;
  • 删除头节点(1→2→null,n=2):
    fast先走2步到2,再走1步到null;同步循环不执行,slow指向dummy,删除1后返回2。

三、时间/空间复杂度

  • 时间复杂度:O(n),仅遍历链表一次(一趟扫描);
  • 空间复杂度:O(1),仅使用固定数量的临时指针,无额外空间开销。

四、关键点总结

  1. 虚拟头结点是核心:避免单独处理头节点删除的特殊逻辑;
  2. fast先走n+1步 :拆分两步实现(先走n步+再走1步),确保slow指向删除节点的前驱;
  3. 内存管理不可少:释放待删除节点和虚拟头结点,避免内存泄漏;
  4. 一趟扫描实现:双指针仅遍历链表一次,满足进阶要求。

易错点提醒

  • 不要遗漏fast额外走的1步:否则slow会指向待删除节点,而非前驱;
  • 必须释放动态分配的节点:new创建的dummy和待删除节点需手动delete
  • 题目保证n有效(1≤n≤链表长度),无需额外判断n的合法性。

142. 环形链表 II

你需要解决的问题是:找到链表中环的入口节点,无环则返回 null,且不能修改原链表。核心思路是用快慢指针先判断是否有环,再通过数学推导找到环的入口。

核心解法(代码+关键思路)

1. 核心逻辑
  • 判环:快慢指针(fast 走2步,slow 走1步),若相遇则链表有环;
  • 找入口:相遇后,从头节点和相遇点各放一个指针(均走1步),相遇处即为环的入口。
2. 数学推导(精简)

设:头节点到入口距离为 x,入口到相遇点距离为 y,相遇点到入口距离为 z

相遇时:fast 走了 x + y + n(y+z)slow 走了 x + y

fast = 2*slow 得:x = (n-1)(y+z) + z

n=1 时,x=z → 头节点和相遇点指针同步走,相遇即入口。

3. C++代码
cpp 复制代码
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *fast = head, *slow = head;
        // 1. 快慢指针判环
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            // 2. 找到相遇点后,双指针找入口
            if (slow == fast) {
                ListNode *p = head;
                while (p != slow) {
                    p = p->next;
                    slow = slow->next;
                }
                return p;
            }
        }
        return NULL; // 无环
    }
};

总结

  1. 判环:快慢指针(2步/1步)相遇则有环,时间复杂度 O(n);
  2. 找入口:相遇点与头节点同步走(均1步),相遇处即环入口;
  3. 空间复杂度:O(1),仅用常量指针,无额外空间。
相关推荐
专注前端30年2 小时前
智能物流路径规划系统:核心算法实战详解
算法
json{shen:"jing"}3 小时前
字符串中的第一个唯一字符
算法·leetcode·职场和发展
追随者永远是胜利者3 小时前
(LeetCode-Hot100)15. 三数之和
java·算法·leetcode·职场和发展·go
程序员酥皮蛋4 小时前
hot 100 第二十七题 27.合并两个有序链表
数据结构·leetcode·链表
BlockWay4 小时前
西甲赛程搬进平台:WEEX以竞猜开启区域合作落地
大数据·人工智能·算法·安全
404未精通的狗5 小时前
(高阶数据结构)并查集
数据结构
im_AMBER6 小时前
Leetcode 121 翻转二叉树 | 二叉树中的最大路径和
数据结构·学习·算法·leetcode
数智工坊6 小时前
【数据结构-排序】8.3 简单选择排序-堆排序
数据结构
mit6.8246 小时前
二分+贪心
算法