1. 题目解析与需求拆解
这道PAT(Programming Ability Test)真题要求我们处理一个特殊的链表去重问题。题目给定一个带整型键值的单链表L,要求对链表进行去重操作:绝对值相同的节点视为重复节点,首次出现的节点保留,后续出现的所有重复节点需要被移除,并按原始顺序单独组成一个新链表。
举个例子,假设原始链表为21→-15→-15→-7→15,处理后的结果应该是:
- 保留链表:21→-15→-7
- 移除链表:-15→15
1.1 输入输出规范分析
根据PAT考试的一贯风格,这类题目通常有明确的输入输出要求:
- 输入格式:第一行给出首节点地址、节点总数N(≤10^5)。随后N行,每行格式为"地址 键值 下一个节点地址"
- 输出格式:需要先输出处理后的保留链表,然后输出被移除的链表
1.2 核心算法选择
面对这种链表处理问题,我们需要考虑几个关键点:
- 如何高效检测重复节点(绝对值相同)
- 如何在不破坏原始顺序的情况下分离节点
- 如何处理大规模数据(N可达10^5)
基于这些需求,哈希表(Hash Table)成为最合适的选择。我们可以用哈希表记录已经出现过的绝对值,实现O(1)时间复杂度的重复检测。
2. 数据结构设计与实现细节
2.1 节点存储方案
由于节点地址是5位非负整数,我们可以直接用静态数组模拟链表:
cpp复制struct Node {
int key;
int next;
} nodes[100000];
这种静态分配方式比动态分配更高效,也避免了内存管理的麻烦。数组下标直接对应节点地址,实现O(1)访问。
2.2 哈希表实现
C++中可以直接使用STL的unordered_set:
cpp复制unordered_set<int> seen;
对于其他语言:
- Java: HashSet
- Python: set()
2.3 链表遍历逻辑
核心遍历流程如下:
- 初始化两个虚拟头节点(dummy head)用于保留链表和移除链表
- 维护两个尾指针分别跟踪两个链表的末尾
- 遍历原始链表,对每个节点:
- 计算键值绝对值
- 如果未见过,加入保留链表,更新哈希表
- 如果已见过,加入移除链表
3. 完整代码实现与注释
以下是C++的完整实现,包含详细注释:
cpp复制#include <iostream>
#include <unordered_set>
#include <cmath>
using namespace std;
struct Node {
int key;
int next;
} nodes[100000];
int main() {
int head, n, addr;
cin >> head >> n;
// 读取所有节点
for (int i = 0; i < n; i++) {
cin >> addr;
cin >> nodes[addr].key >> nodes[addr].next;
}
unordered_set<int> seen;
int kept_head = -1, kept_tail = -1; // 保留链表头尾
int removed_head = -1, removed_tail = -1; // 移除链表头尾
for (int p = head; p != -1; p = nodes[p].next) {
int abs_key = abs(nodes[p].key);
if (seen.count(abs_key)) { // 重复节点
if (removed_head == -1) {
removed_head = removed_tail = p;
} else {
nodes[removed_tail].next = p;
removed_tail = p;
}
} else { // 首次出现
seen.insert(abs_key);
if (kept_head == -1) {
kept_head = kept_tail = p;
} else {
nodes[kept_tail].next = p;
kept_tail = p;
}
}
}
// 处理两个链表的尾节点
if (kept_tail != -1) nodes[kept_tail].next = -1;
if (removed_tail != -1) nodes[removed_tail].next = -1;
// 输出保留链表
for (int p = kept_head; p != -1; p = nodes[p].next) {
printf("%05d %d ", p, nodes[p].key);
if (nodes[p].next == -1) printf("-1\n");
else printf("%05d\n", nodes[p].next);
}
// 输出移除链表
for (int p = removed_head; p != -1; p = nodes[p].next) {
printf("%05d %d ", p, nodes[p].key);
if (nodes[p].next == -1) printf("-1\n");
else printf("%05d\n", nodes[p].next);
}
return 0;
}
4. 关键问题与优化策略
4.1 边界条件处理
实际编码时需要特别注意以下边界情况:
- 空链表输入(虽然题目保证N≥1)
- 所有节点键值绝对值相同
- 没有重复节点的情况
- 链表只有一个节点
4.2 输出格式陷阱
PAT考试对输出格式要求极为严格:
- 节点地址必须用5位数字表示,不足前导补零
- 最后一个节点的next地址必须输出-1
- 两个链表之间不能有多余空行
4.3 性能优化
虽然O(N)时间复杂度已经最优,但仍有一些优化空间:
- 提前终止:如果哈希表大小达到10^4(不同绝对值数量),后续节点可以全部归入移除链表
- 内存局部性:可以预先将节点按地址排序,但需要额外空间
5. 扩展思考与变种问题
5.1 双向链表版本
如果题目改为双向链表,处理逻辑类似,但需要额外维护prev指针。在节点转移时需要注意同时更新前后节点的指针。
5.2 稳定去重算法
当前算法保证了节点的原始顺序。如果允许改变顺序,可以使用更高效的两指针法,但会破坏稳定性。
5.3 分布式环境处理
对于超大规模链表(如分布在多台机器上),可以考虑:
- 使用布隆过滤器(Bloom Filter)代替哈希表
- 分片处理,最后合并结果
- MapReduce框架实现
提示:在实际编程竞赛中,建议先用小样本测试所有边界情况,再提交完整代码。我曾在类似题目中因忽略-1的输出格式而失分多次。