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:直接操作原链表(需单独处理头结点)
核心思路
- 先循环删除所有值为
val的头结点(头结点无前置节点,需特殊处理); - 遍历链表,通过前置节点的
next跳过非头结点中值为val的节点; - 释放被删除节点的内存,避免内存泄漏。
- 时间复杂度: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:虚拟头结点法(推荐,统一处理所有节点)
核心思路
- 创建虚拟头结点
dummyHead指向原链表头,让所有节点(包括原头结点)都能通过「前置节点→next」统一删除; - 遍历链表完成删除后,释放虚拟头结点,返回
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. 设计链表
核心需求复述
完成单链表的核心设计:包含 get、addAtHead、addAtTail、addAtIndex、deleteAtIndex 五个接口,要求逻辑清晰、无冗余代码,同时处理内存泄漏问题。
核心实现(单链表 + 虚拟头结点)
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,无需额外分支,逻辑更简洁; |
关键点总结
- 虚拟头结点是核心:统一头结点和普通节点的操作逻辑,是链表设计的最优实践;
_size简化索引判断:无需遍历链表验证索引有效性,时间复杂度从 O(n) 降为 O(1);- 内存安全不可少:析构函数和删除节点时的内存释放是工业级代码的基本要求,避免内存泄漏。
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指向新头结点
}
};
关键逻辑解释
- 核心思想 :仅通过改变
next指针方向实现反转,不新建链表(空间复杂度 O(1)); - 指针分工 :
pre:指向「已反转部分」的尾节点(初始为空);cur:指向「待反转部分」的头节点(初始为原链表头);tmp:临时保存cur->next,避免反转后丢失后继节点;
- 循环终止条件 :
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) | 代码简洁、思想抽象 | 算法面试(考察递归思维) |
四、关键点总结
- 双指针法是最优解:仅用3个指针完成反转,空间复杂度 O(1),是工业级首选;
- 反转核心逻辑 :先保存
cur->next,再将cur->next指向pre,最后移动pre和cur; - 递归法本质 :
- 递归模拟双指针的循环过程,逻辑完全一致;
- 边界处理:空链表/单节点链表直接返回,避免空指针访问。
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 = B→dummy → B; - 步骤2:
B->next = A→dummy → B → A; - 步骤3:
A->next = C→dummy → 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),仅使用固定数量的临时指针,无额外空间开销。
四、关键点总结
- 虚拟头结点是核心:避免单独处理头结点交换,统一所有节点的操作逻辑;
- 临时指针必保存 :
tmp1保存第一个节点,tmp2保存后继节点,避免链表断裂; - 交换顺序不能乱:严格遵循「cur指向B → B指向A → A指向C」的三步顺序;
- 内存管理要注意:释放虚拟头结点,避免内存泄漏(工程规范要求)。
易错点提醒
- 不要遗漏
delete dummy:虚拟头结点是动态分配的,必须手动释放; - 循环条件必须同时判断
cur->next和cur->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停在删除节点的前驱;- 同步移动
fast和slow:直到fast为nullptr,此时slow->next即为待删除节点; - 删除节点:修改
slow->next指向,并释放待删除节点的内存。
2. 执行流程示例(输入:1→2→3→4→5,n=2)
- 初始化:
dummy→1→2→3→4→5,fast=dummy,slow=dummy; fast先走2步:fast指向2(n从2减至0);fast再走1步:fast指向3;- 同步移动:
fast走到5→null,slow走到3; - 删除
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),仅使用固定数量的临时指针,无额外空间开销。
四、关键点总结
- 虚拟头结点是核心:避免单独处理头节点删除的特殊逻辑;
- fast先走n+1步 :拆分两步实现(先走n步+再走1步),确保
slow指向删除节点的前驱; - 内存管理不可少:释放待删除节点和虚拟头结点,避免内存泄漏;
- 一趟扫描实现:双指针仅遍历链表一次,满足进阶要求。
易错点提醒
- 不要遗漏
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; // 无环
}
};
总结
- 判环:快慢指针(2步/1步)相遇则有环,时间复杂度 O(n);
- 找入口:相遇点与头节点同步走(均1步),相遇处即环入口;
- 空间复杂度:O(1),仅用常量指针,无额外空间。