一、问题现象:AddressSanitizer 报 SEGV 错误
1. 错误日志
Line 17: Char 22:
AddressSanitizer:DEADLYSIGNAL
==22==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x560396d93c39 bp 0x7ffef0182410 sp 0x7ffef01823d0 T0)
==22==The signal is caused by a READ memory access.
==22==Hint: address points to the zero page.
#0 0x560396d93c39 in Solution::partition(ListNode*, int) solution.cpp:17:22
SUMMARY: AddressSanitizer: SEGV solution.cpp:17:22 in Solution::partition(ListNode*, int)
2. 错误解读
SEGV on unknown address 0x000000000000:核心问题是空指针解引用(尝试读取地址为 0 的内存,这是系统保护的无效内存);- 错误定位:
solution.cpp第 17 行的partition函数,是链表遍历过程中触发的内存访问错误。
二、初始错误代码分析
我的初始思路是用 vector 收集 "小于 x" 和 "大于等于 x" 的节点,再拼接成新链表,代码如下:
cpp
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
if(!head||!head->next) return head;
vector<ListNode*> vec;
// 第一遍遍历:收集小于x的节点(逻辑正常)
for(ListNode *l=head;l;l=l->next){
if(l->val<x)
vec.push_back(l);
}
// 第二遍遍历:收集大于等于x的节点(致命错误)
for(ListNode* l;l;l=l->next){ // 未初始化l!
if(l->val>=x)
vec.push_back(l);
}
// 拼接链表(次要问题:尾节点未置空)
for(int i=0;i<vec.size()-1;++i){
vec[i]->next=vec[i+1];
}
return vec[0];
}
};
3. 核心问题拆解
(1)致命错误:循环变量未初始化(直接触发 SEGV)
第二个 for 循环中 ListNode* l; 仅声明了指针,但未初始化,此时 l 的值是随机的垃圾值(大概率是 0x0,即空指针)。
- 循环条件
l;会直接判断这个垃圾值,若为 0 则循环不执行,若为非 0 则访问l->next,触发空指针解引用; - 这是导致 AddressSanitizer 报 SEGV 的直接原因(第 17 行的核心问题)。
(2)次要但关键的问题:链表尾节点未置空
拼接链表后,最后一个节点的 next 仍指向原链表的节点,会导致:
- 链表出现环,遍历时报错;
- 访问原链表的无效内存,触发潜在的内存错误。
三、修复思路与正确代码
1. 修复核心原则
- 所有指针使用前必须初始化;
- 访问
node->val/node->next前,先检查node是否为空; - 链表拼接完成后,尾节点的
next必须置空。
2. 修复后的完整代码
cpp
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
// 边界条件:空链表或单节点链表直接返回
if(!head || !head->next) return head;
vector<ListNode*> vec;
// 第一遍遍历:收集小于x的节点(初始化l=head)
for(ListNode *l = head; l; l = l->next) {
if(l->val < x) {
vec.push_back(l);
}
}
// 第二遍遍历:收集大于等于x的节点(关键修复:初始化l=head)
for(ListNode *l = head; l; l = l->next) {
if(l->val >= x) {
vec.push_back(l);
}
}
// 拼接链表
for(int i = 0; i < vec.size() - 1; ++i) {
vec[i]->next = vec[i+1];
}
// 关键修复:尾节点next置空,避免环或无效访问
vec.back()->next = nullptr;
return vec[0];
}
};
3. 关键修改点
表格
| 问题点 | 修复方案 |
|---|---|
循环变量 l 未初始化 |
第二个循环中 l 初始化为 head,确保从链表头开始遍历 |
尾节点 next 未置空 |
添加 vec.back()->next = nullptr;,终止链表 |
四、进阶优化:O (1) 空间复杂度方案
上述方案用 vector 存储节点,空间复杂度为 O (n),链表题的最优解通常要求 O (1) 空间,可通过虚拟头节点实现:
cpp
ListNode* partition(ListNode* head, int x) {
// 定义两个虚拟头节点,分别存储小于x和大于等于x的节点
ListNode* dummy1 = new ListNode(0);
ListNode* dummy2 = new ListNode(0);
ListNode* p1 = dummy1; // 遍历dummy1的指针
ListNode* p2 = dummy2; // 遍历dummy2的指针
// 一次遍历完成分区,避免重复遍历
while(head) {
if(head->val < x) {
p1->next = head;
p1 = p1->next;
} else {
p2->next = head;
p2 = p2->next;
}
head = head->next;
}
// 尾节点置空 + 拼接两个链表
p2->next = nullptr;
p1->next = dummy2->next;
// 释放虚拟节点,避免内存泄漏
ListNode* res = dummy1->next;
delete dummy1;
delete dummy2;
return res;
}
优化点:
- 仅遍历链表一次,时间复杂度保持 O (n);
- 无需额外容器存储节点,空间复杂度降为 O (1);
- 虚拟头节点避免了空指针判断的冗余代码,逻辑更简洁。