link
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
input: 1->2->3->3->4->4->5
output: 1->2->5
使用 cpp 实现无需手动管理内存。
解题思路
我们给出了 leetcode 中两道类似的题目,“删除排序链表中的重复元素 II” 正是本节题目描述中所述的问题,而前一个是这个问题的简化版,即重复时仅保留其中一个结点。既然保留重复结点的做法是更简单的,那么我们不妨先解决简单的问题,最后利用一个标记来删除最后这个被保留下来的结点即可。
我们定义的链表结点的结构如下:
struct Node {
int val;
Node *next = nullptr;
};
首先,我们给出"删除排序链表中的重复元素"的实现代码,这个问题比较简单,不另外建立页面:
void delete_repeat_node(Node **head) {
Node *p = *head;
while (p) {
while (p->next && p->next->val == p->val) {
p->next = p->next->next;
}
p = p->next;
}
}
非常简单对不对?在内层的 while 循环,我们比较了当前选中的结点和下一个结点的值,如果相等,则我们修改p->next以表明我们从链表中删除了下一个结点,直到遇到一个新的不同的值。外层的 while 循环则保证将指针移动到这个不同的值所代表的结点。
相信你已经察觉出如何扩展上述代码来解决一个更复杂的问题了。没错,在第 6 行到第 7 行之间,p 很有可能指向了那个还没有被删除的,现在已经成了“孤家寡人”的重复元素。因此,我们在这其中插入检查并删除的代码,就可以解决本问题。伪代码如下:
void delete_repeat_node(Node **head) {
Node *p = *head;
while (p) {
while (p->next && p->next->val == p->val) {
p->next = p->next->next;
}
// 如果p是重复元素, 我们应当从链表中删除p
if (p_is_repeat){
delele p;
}
p = p->next;
}
}
沿着这个思路来解决问题需要有两个要点。
第一,我们需要判断 p 是否指向重复元素,可以简单设置一个标志位,并在进入内层 while 循环后置为 true ,说明 p 确实指向重复元素。第二,要想删除 p,我们就必须找到它的前向结点,好在,我们外层 while 循环算得上是对链表的线性遍历,因此,我们在遍历时可以多保存一个变量,表示当前结点的前向结点prev。
代码实现
第一种实现如下:
void delete_repeat_node(Node **head) {
Node *prev = nullptr;
Node *p = *head;
while (p) {
bool delete_self = false;
while (p->next && p->next->val == p->val) {
delete_self = true;
p->next = p->next->next;
}
if (delete_self) {
if (prev) {
prev->next = p->next;
} else {
// header
*head = p->next;
}
} else {
prev = p;
}
p = p->next;
}
}
注意到,我们在第 2 行设置了prev以保存前向结点,在第 5 行设置了delete_self标志位以记录p是否需要删除。第 10 行到第 19 行对prev的处理可能会有些难以理解,我们解析一下。
首先,如果p需要被删除,而且prev不为空,说明p不是头结点。原本prev->next指向p,在第 12 行,我们更新prev->next为p->next,这样p就从链表中被删除了。此时,prev需要被更新吗?不需要,因为此时prev依然将是下一轮迭代的指针的前向节点!
若prev为空,那么p是头结点,在第 14 行我们必须修改头指针才能完成删除操作。
若p并不需要被删除,那么,结合第 20 行,p将继续向下移动,因此,我们必须修改prev指针,否则prev性质就被破坏了。
我们可以进一步优化上述代码。事实上,标志位只需要设置和判断一次,因此,我们可以用一次看似冗余的前置 if 判断来消除对标志位的使用,如下所示:
void delete_repeat_node_re(Node **head) {
Node *prev = nullptr;
Node *p = *head;
while (p) {
if (p->next && p->next->val == p->val) {
while (p->next && p->next->val == p->val) {
p->next = p->next->next;
}
if (prev) {
prev->next = p->next;
} else {
// header
*head = p->next;
}
} else {
prev = p;
}
p = p->next;
}
}
我们抢先判断p是否有可能被删除,如果不可能,则我们直接移动prev和p的指针即可;否则我们进入一个内层 while 循环,直到找到第一个拥有不同的值的结点。
这个问题还有另一种解法,采用了递归形式来实现,可能没有这么直观。
Node *delete_repeat_node_recursion(Node *head) {
if (!head || !head->next) {
return head;
}
Node *p = head->next;
while (p && head->val == p->val) {
p = p->next;
}
if (head->next != p) {
return delete_repeat_node_recursion(p);
}
head->next = delete_repeat_node_recursion(p);
return head;
}
首先,在第 2 行至第 4 行,我们定下递归结束的基础条件,在传入的结点为空或无后继结点的情况下,我们直接返回头指针即可。
接下来,在第 5 行到第 8 行,我们进行一次遍历,结束条件是指针p的值和头指针的值不一致。这里存在两种可能,第一种可能是 while 循环的第一次条件检查就失败了,即head->next的值和head的值不一致,这种情况下不会命中第 9 行的 if 判断;第二种可能是 while 循环的第一次条件检查成功,因此指针p继续向后移动直到为 null 或第一个与head的值不相等的结点,此时,命中第 9 行的 if 语句。
第 9 行到第 11 行的 if 语句本质上是这个意思:头节点和后续若干结点的值重复,我们直接抛弃这些结点,然后以p为头节点的链表去重的结果为整个问题的结果(以一个新的子问题的解答作为整个问题的解答)。
如果未命中 if 语句,则头节点是整个问题解答的一部分,我们只要再获得以head->next(提醒一下,此时head->next == p是成立的哦)为头节点的链表去重的结果。由于这个子链表的头节点依然可能发生变更,因此我们需要重新为head->next赋值。
递归解法的思路在于,找到第一个不一致的结点然后直接抛弃前面的所有结点。如果我们沿着这种思路来组织代码,迭代实现会有什么不同吗?以下是实现:
void delete_repeat_node_refactor(Node **head) {
Node *prev = nullptr;
Node *p = *head;
while (p) {
Node *next = p->next;
while (next && next->val == p->val) {
next = next->next;
}
if (next != p->next) {
if (prev) {
prev->next = next;
} else {
// header
*head = next;
}
p = next;
} else {
prev = p;
p = p->next;
}
}
}
在第 5 行,我们使用next结点来迭代并保存我们遇到的第一个持有不同的值的结点。在第 11 行,我们用赋值prev->next = next来表示直接抛弃next前面的所有结点。但,两个 if 分支的指针p的移动策略将会发生变化,在直接抛弃结点的分支中,由于p->next已经无效了,我们必须让p移动到next的位置。
by the way,如果你把next看作是p->next的别名,那么上述实现代码其实和第一种思路几乎一致。而且少一个next指针,p的移动策略还能统一为p = p->next,而不是只能分类讨论。