1. 链表去重问题解析
这道PAT甲级真题考察的是对链表结构的操作能力,题目要求我们对一个包含整数数据的链表进行去重处理。具体来说,需要将链表中绝对值重复的节点移除,并分别输出保留的链表和被移除的链表。这类问题在实际开发中非常常见,比如内存管理中的重复块合并、数据库索引去重等场景都会用到类似的算法。
链表去重看似简单,但有几个关键点需要注意:
- 需要同时维护两个结果链表(保留链表和移除链表)
- 必须保持节点间的原始相对顺序
- 需要高效检测重复的绝对值(时间复杂度考量)
- 要考虑边界条件(空链表、全重复链表等)
2. 算法设计与实现思路
2.1 数据结构选择
我们选择使用静态链表表示法来处理这个问题,这是处理链表类题目的常用技巧。具体来说:
c复制#define MAXSIZE 100000
struct Node {
int data;
int next;
} nodes[MAXSIZE];
这种表示法的优势在于:
- 可以直接通过地址访问节点,避免动态内存分配
- 适合处理PAT题目中常见的"地址作为索引"的输入格式
- 时间复杂度稳定在O(n),空间复杂度O(max_address)
2.2 核心算法流程
-
初始化阶段:
- 创建两个虚拟头节点head和removed_head
- 初始化一个哈希集合exist记录已存在的绝对值
- 定义两个指针tail和removed_tail分别指向两个链表的末尾
-
遍历处理阶段:
c复制while (current != -1) { int abs_data = abs(nodes[current].data); if (exist.find(abs_data) == exist.end()) { // 添加到保留链表 exist.insert(abs_data); tail->next = current; tail = current; } else { // 添加到移除链表 removed_tail->next = current; removed_tail = current; } current = nodes[current].next; } -
收尾处理:
- 将两个链表的末尾节点的next置为-1
- 处理可能的空链表情况
3. 关键实现细节
3.1 重复检测优化
使用unordered_set来存储已经出现的绝对值,可以将查找时间复杂度降到O(1)。这里需要注意:
c复制unordered_set<int> exist;
// 插入和查找操作都是O(1)
exist.insert(abs_data);
if (exist.count(abs_data)) {...}
3.2 链表连接处理
在操作链表时,要特别注意指针的更新顺序。一个常见的错误是:
c复制// 错误的顺序会导致链表断裂
tail->next = current;
current = nodes[current].next;
tail = current; // 这里current已经被更新了
正确的做法应该是:
c复制int next_node = nodes[current].next; // 先保存下一个节点
tail->next = current;
tail = current;
current = next_node;
3.3 输出格式处理
PAT题目对输出格式要求严格,需要注意:
- 每个节点的地址输出为5位数字,不足补前导零
- -1不能输出为-00001
- 最后一个节点的next地址输出为-1
建议封装一个输出函数:
c复制void printList(int head) {
while (head != -1) {
printf("%05d %d ", head, nodes[head].data);
if (nodes[head].next != -1) {
printf("%05d\n", nodes[head].next);
} else {
printf("-1\n");
}
head = nodes[head].next;
}
}
4. 完整代码实现
c复制#include <cstdio>
#include <unordered_set>
#include <cmath>
using namespace std;
#define MAXSIZE 100000
struct Node {
int data;
int next;
} nodes[MAXSIZE];
int main() {
int head, n;
scanf("%d%d", &head, &n);
for (int i = 0; i < n; i++) {
int addr, data, next;
scanf("%d%d%d", &addr, &data, &next);
nodes[addr].data = data;
nodes[addr].next = next;
}
unordered_set<int> exist;
int new_head = -1, new_tail = -1;
int removed_head = -1, removed_tail = -1;
int current = head;
while (current != -1) {
int abs_data = abs(nodes[current].data);
if (exist.find(abs_data) == exist.end()) {
exist.insert(abs_data);
if (new_head == -1) {
new_head = current;
new_tail = current;
} else {
nodes[new_tail].next = current;
new_tail = current;
}
} else {
if (removed_head == -1) {
removed_head = current;
removed_tail = current;
} else {
nodes[removed_tail].next = current;
removed_tail = current;
}
}
current = nodes[current].next;
}
// 处理链表末尾
if (new_tail != -1) nodes[new_tail].next = -1;
if (removed_tail != -1) nodes[removed_tail].next = -1;
// 输出结果链表
current = new_head;
while (current != -1) {
printf("%05d %d ", current, nodes[current].data);
if (nodes[current].next != -1) {
printf("%05d\n", nodes[current].next);
} else {
printf("-1\n");
}
current = nodes[current].next;
}
// 输出被移除的链表
current = removed_head;
while (current != -1) {
printf("%05d %d ", current, nodes[current].data);
if (nodes[current].next != -1) {
printf("%05d\n", nodes[current].next);
} else {
printf("-1\n");
}
current = nodes[current].next;
}
return 0;
}
5. 常见错误与调试技巧
5.1 内存越界问题
由于题目给出的地址可能很大(5位数),在定义数组大小时要确保足够:
c复制#define MAXSIZE 100000 // 不是10000!
5.2 特殊输入处理
需要考虑以下边界情况:
- 空链表输入(head为-1)
- 所有节点都重复的情况
- 只有一个节点的链表
- 所有节点都不重复的情况
5.3 指针更新顺序
在处理链表连接时,建议按照以下顺序:
- 先保存下一个节点的地址
- 更新当前链表尾部的next指针
- 移动尾指针到当前节点
- 将当前节点移动到之前保存的下一个节点
5.4 输出格式验证
建议使用以下测试用例验证输出格式:
code复制00001 1 00002
00002 2 -1
预期输出应该保持地址的5位格式,即使是0也要补齐。
6. 算法优化思路
6.1 空间优化
如果题目对内存有严格要求,可以用位图代替哈希集合:
c复制bool exist[10001] = {false}; // 假设数据绝对值不超过10000
这样可以减少内存使用,但会限制数据的取值范围。
6.2 并行处理优化
对于特别大的链表,可以考虑:
- 预扫描一次链表,统计所有绝对值
- 标记需要保留和移除的节点
- 第二次遍历直接构建两个链表
这种方法虽然增加了一次遍历,但可以减少哈希表的操作次数。
6.3 多级哈希优化
当数据范围很大时,可以考虑使用多级哈希或者布隆过滤器来减少内存使用,虽然可能会有一定的误判率。
7. 实际应用场景
这种链表去重算法在实际工程中有广泛应用:
- 数据库系统:合并重复的索引项
- 编译器设计:常量池去重
- 内存管理:合并空闲内存块
- 网络协议:处理重复的数据包
- 大数据处理:MapReduce中的combiner阶段
理解这个算法有助于处理各种需要维护顺序的去重场景。在面试中,类似的链表操作问题也经常出现,比如:
- 删除排序链表中的重复元素
- 合并两个有序链表
- 链表奇偶重排
- 反转链表的一部分
掌握链表去重的核心思想,可以举一反三解决这类问题。