PTA(顺序表)2-5 链表去重

2-5 链表去重

分数 5

作者 陈越

单位 浙江大学

给定一个带整数键值的链表 L,你需要把其中绝对值重复的键值结点删掉。即对每个键值 K,只有第一个绝对值等于 K 的结点被保留。同时,所有被删除的结点须被保存在另一个链表上。例如给定 L 为 21→-15→-15→-7→15,你需要输出去重后的链表 21→-15→-7,还有被删除的链表 -15→15。

输入格式:

输入在第一行给出 L 的第一个结点的地址和一个正整数 N(≤105,为结点总数)。一个结点的地址是非负的 5 位整数,空地址 NULL 用 -1 来表示。

随后 N 行,每行按以下格式描述一个结点:

1
地址 键值 下一个结点

其中地址是该结点的地址,键值是绝对值不超过104的整数,下一个结点是下个结点的地址。

输出格式:

首先输出去重后的链表,然后输出被删除的链表。每个结点占一行,按输入的格式输出。

输入样例:

1
2
3
4
5
6
00100 5
99999 -7 87654
23854 -15 00000
87654 15 -1
00000 -15 99999
00100 21 23854

输出样例:

1
2
3
4
5
00100 21 23854
23854 -15 99999
99999 -7 -1
00000 -15 87654
87654 15 -1

此题中的键值,指的是链表节点中存储的整数数据(即 data 字段的值)

c++答案(豆包)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <iostream>
#include <cstdio>
#include <cmath>
#include <unordered_set>

using namespace std;

// 定义链表节点的结构体,每个节点包含三个信息:自身地址、存储的键值、下一个节点的地址
struct Node {
int addr; // 节点的地址(5位整数)
int data; // 节点存储的键值(整数)
int next; // 下一个节点的地址(-1表示空)
} nodes[100000]; // 用数组存储所有节点,数组下标直接对应节点地址(因为地址是5位整数,最大99999)

int main() {
int head, n;
// 读取链表的头节点地址和节点总数n
cin >> head >> n;

// 读取n个节点的信息,并存入nodes数组中
for (int i = 0; i < n; ++i) {
int addr, data, next;
cin >> addr >> data >> next; // 读取当前节点的地址、键值、下一个节点地址
nodes[addr] = {addr, data, next}; // 将节点信息存入数组对应位置(下标=地址)
}

// 创建一个哈希集合,用于记录已经出现过的键值的绝对值(快速判断是否重复)
unordered_set<int> exist;
// 定义去重后链表的头指针和尾指针(初始为-1表示空)
int res_head = -1, res_tail = -1;
// 定义被删除节点组成的链表的头指针和尾指针(初始为-1表示空)
int del_head = -1, del_tail = -1;

// 从链表头开始遍历整个原链表
int current = head; // current记录当前遍历到的节点地址
while (current != -1) { // 当current为-1时,链表遍历结束
int val = nodes[current].data; // 获取当前节点的键值
int abs_val = abs(val); // 计算键值的绝对值(用于判断重复)

// 检查当前键值的绝对值是否在哈希集合中(即是否已出现过)
if (exist.find(abs_val) == exist.end()) {
// 未出现过:将当前节点加入去重后的链表

exist.insert(abs_val); // 把该绝对值存入集合,标记为已出现

// 如果去重链表还是空的(头指针为-1),则当前节点成为头节点
if (res_head == -1) {
res_head = current;
res_tail = current; // 尾指针也指向当前节点(此时链表只有一个节点)
} else {
// 去重链表非空:将当前节点接在尾节点后面
nodes[res_tail].next = current; // 原尾节点的next指向当前节点
res_tail = current; // 更新尾指针为当前节点
}
} else {
// 已出现过:将当前节点加入被删除的链表

// 如果删除链表还是空的(头指针为-1),则当前节点成为删除链表的头节点
if (del_head == -1) {
del_head = current;
del_tail = current; // 尾指针也指向当前节点
} else {
// 删除链表非空:将当前节点接在删除链表的尾节点后面
nodes[del_tail].next = current; // 原删除链表尾节点的next指向当前节点
del_tail = current; // 更新删除链表的尾指针为当前节点
}
}

// 移动到下一个节点:当前节点的next地址就是下一个节点的位置
current = nodes[current].next;
}

// 处理两个链表的尾节点:将尾节点的next设为-1(表示链表结束)
if (res_tail != -1) { // 如果去重链表非空
nodes[res_tail].next = -1;
}
if (del_tail != -1) { // 如果删除链表非空
nodes[del_tail].next = -1;
}

// 输出去重后的链表
current = res_head; // 从去重链表的头节点开始遍历
while (current != -1) {
// 如果当前节点是链表的最后一个(next为-1),按格式输出地址、键值、-1
if (nodes[current].next == -1) {
printf("%05d %d -1\n", nodes[current].addr, nodes[current].data);
} else {
// 非最后一个节点,输出地址、键值、下一个节点的地址(格式化为5位整数)
printf("%05d %d %05d\n", nodes[current].addr, nodes[current].data, nodes[current].next);
}
current = nodes[current].next; // 移动到下一个节点
}

// 输出被删除的链表(格式与去重链表相同)
current = del_head; // 从删除链表的头节点开始遍历
while (current != -1) {
if (nodes[current].next == -1) {
printf("%05d %d -1\n", nodes[current].addr, nodes[current].data);
} else {
printf("%05d %d %05d\n", nodes[current].addr, nodes[current].data, nodes[current].next);
}
current = nodes[current].next; // 移动到下一个节点
}

return 0;
}

这题给我第一个反应:

  • 咋给的链表各个元素不是按链表的连接顺序00100→23854→00000→99999→87654)排列的
  • 而是模拟真实的内存存储场景:链表的结点在内存中可能分散存储,仅通过指针(下一个地址)关联,而非连续排列。
  • 因此,解题时需要先根据 “地址” 建立索引,再从起始地址开始,通过 “下一个结点地址” 依次遍历,才能还原出链表的实际顺序。

逻辑

  1. 数据存储:使用数组nodes存储所有节点,下标为节点地址(利用题目中地址是 5 位整数的特性,直接映射)。
  2. 核心去重逻辑
    • unordered_set记录已出现过的键值绝对值
    • 遍历原链表,对每个节点判断其键值绝对值是否已存在
    • 未存在的节点放入结果链表,已存在的放入删除链表
  3. 链表构建
    • 分别维护两个链表(结果链表和删除链表)的头指针和尾指针
    • 通过尾指针快速插入新节点,避免每次遍历查找尾节点
  4. 输出格式
    • 地址需按 5 位整数格式输出(不足 5 位前补 0)
    • 最后一个节点的下一个地址为-1
  5. 时间复杂度:O (N),其中 N 为节点总数,只需遍历一次原链表即可完成去重。

unordered_set<int> exist; 是什么

是 C++ 标准库中的哈希集合对象,核心作用是:

快速记录和判断 “整数是否已经出现过”,本质是一个不存储重复元素、查询效率极高的容器。

关键特性(对应本题用法):

  1. 去重性:集合中不会存储重复元素(本题用于记录已出现的键值绝对值,避免重复保留节点)。
  2. 高效查询:通过哈希表实现,判断元素是否存在的时间复杂度接近 O (1)(比数组遍历快得多,适合本题 1e5 规模的数据)。
  3. 核心操作
    • exist.insert(abs_val):将整数 abs_val 存入集合(标记为 “已出现”)。
    • exist.find(abs_val) == exist.end():判断 abs_val 是否不在集合中(若不在,说明是第一次出现,需保留对应节点)。

结构体数组

使用了结构体数组来存储链表节点。

具体来说:

  • 定义了结构体 struct Node,包含节点的地址(addr)、键值(data)和下一个节点的地址(next)。
  • 声明了结构体数组 nodes[100000],数组的下标直接对应节点的地址(因为题目中节点地址是 5 位整数,范围为 0~99999,刚好可以用数组下标覆盖)。

这种设计的优势是:

  • 可以通过节点地址(如 addr)直接访问对应的节点数据,时间复杂度为 O (1)(例如 nodes[addr] 即可获取地址为 addr 的节点信息)。
  • 避免了使用指针动态分配内存的复杂性,更适合处理题目中 “地址已知且范围固定” 的场景。

本质上,这是用数组模拟了链表的存储结构,通过 “地址 - 下标” 的映射实现快速访问,是处理这类链表问题的高效方法。

c答案(豆包)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

// 节点结构体
typedef struct {
int addr; // 地址
int data; // 键值
int next; // 下一个节点地址
} Node;

// 哈希表用于记录已出现的绝对值(简单实现)
#define HASH_SIZE 20001 // 键值绝对值最大为10000,哈希表大小设为2倍+1
int hash_table[HASH_SIZE] = {0}; // 0表示未出现,1表示已出现

int main() {
Node nodes[100000]; // 地址是5位整数,最大为99999
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].addr = addr;
nodes[addr].data = data;
nodes[addr].next = next;
}

// 去重后链表的头和尾
int res_head = -1, res_tail = -1;
// 被删除链表的头和尾
int del_head = -1, del_tail = -1;

int current = head;
while (current != -1) {
int val = nodes[current].data;
int abs_val = abs(val);
int hash_idx = abs_val % HASH_SIZE; // 哈希索引

if (hash_table[hash_idx] == 0) {
// 未出现过,加入结果链表
hash_table[hash_idx] = 1;
if (res_head == -1) {
res_head = current;
res_tail = current;
} else {
nodes[res_tail].next = current;
res_tail = current;
}
} else {
// 已出现过,加入删除链表
if (del_head == -1) {
del_head = current;
del_tail = current;
} else {
nodes[del_tail].next = current;
del_tail = current;
}
}

current = nodes[current].next; // 移动到下一个节点
}

// 处理链表尾的next(设为-1)
if (res_tail != -1) {
nodes[res_tail].next = -1;
}
if (del_tail != -1) {
nodes[del_tail].next = -1;
}

// 输出去重后的链表
current = res_head;
while (current != -1) {
if (nodes[current].next == -1) {
printf("%05d %d -1\n", nodes[current].addr, nodes[current].data);
} else {
printf("%05d %d %05d\n", nodes[current].addr, nodes[current].data, nodes[current].next);
}
current = nodes[current].next;
}

// 输出被删除的链表
current = del_head;
while (current != -1) {
if (nodes[current].next == -1) {
printf("%05d %d -1\n", nodes[current].addr, nodes[current].data);
} else {
printf("%05d %d %05d\n", nodes[current].addr, nodes[current].data, nodes[current].next);
}
current = nodes[current].next;
}

return 0;
}
  • 相当于把c++中的unorderedset中的函数实现了一遍

用一个数组模拟了 C++ 中 unordered_set 的核心功能,相当于手动实现了 “记录元素是否存在” 的逻辑。

具体来说:

  • C++ 中 unordered_set<int> exist 的核心作用是快速判断一个整数(绝对值)是否已经出现过(通过 find 方法),以及记录新出现的整数(通过 insert 方法)。
  • C 语言中没有内置的哈希集合,因此我们用hash_table数组模拟了这个过程:
    • 用数组下标对应 “键值的绝对值”(通过哈希映射),数组元素值为 0 表示 “未出现”,1 表示 “已出现”。
    • 判断是否存在:直接检查 hash_table[hash_idx] 是否为 1(对应 C++ 的 exist.find(...) != exist.end())。
    • 记录新元素:将 hash_table[hash_idx] 设为 1(对应 C++ 的 exist.insert(...))。

这种实现本质上是用数组模拟了一个简单的哈希集合,实现了 unordered_set 在本题中所需的核心功能(存在性判断和插入),只是实现方式更底层(依赖数组下标和哈希映射)。

由于本题中键值的绝对值范围有限(≤10⁴),用数组模拟哈希表不仅简单,而且效率很高(无哈希冲突),完全能满足需求