删除排序链表中重复的结点

174 阅读6分钟

link

Leetcode 删除排序链表中的重复元素

Leetcode 删除排序链表中的重复元素 II

题目描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。

 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->nextp->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是否有可能被删除,如果不可能,则我们直接移动prevp的指针即可;否则我们进入一个内层 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,而不是只能分类讨论。