数据结构与算法之深入解析“删除排序链表中的重复元素II”的求解思路与算法示例

Posted Serendipity·y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“删除排序链表中的重复元素II”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。

一、题目要求

  • 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字,返回已排序的链表。
  • 示例 1:

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
  • 示例 2:

输入:head = [1,1,1,2,3]
输出:[2,3]
  • 提示:
    • 链表中节点数目在范围 [0, 300] 内;
    • -100 <= Node.val <= 100;
    • 题目数据保证链表已经按升序排列。

二、求解算法

① 递归

  • 递归函数直接使用题目给出的函数 deleteDuplicates(head) ,它的含义是删除以 head 作为开头的有序链表中,值出现重复的节点。
  • 递归的终止条件就是能想到的基本的、不用继续递归处理的 case:
    • 如果 head 为空,那么肯定没有值出现重复的节点,直接返回 head;
    • 如果 head.next 为空,那么说明链表中只有一个节点,也没有值出现重复的节点,也直接返回 head。
  • 递归调用:
    • 如果 head.val != head.next.val ,说明头节点的值不等于下一个节点的值,所以当前的 head 节点必须保留;但是 head.next 节点要不要保留呢?我们还不知道,需要对 head.next 进行递归,即对 head.next 作为头节点的链表,去除值重复的节点。所以 head.next = self.deleteDuplicates(head.next)。
    • 如果 head.val == head.next.val ,说明头节点的值等于下一个节点的值,所以当前的 head 节点必须删除,并且 head 之后所有与 head.val 相等的节点也都需要删除;删除到哪个节点为止呢?需要用 move 指针一直向后遍历寻找到与 head.val 不等的节点。此时 move 之前的节点都不保留了,因此返回 deleteDuplicates(move)。
  • 题目返回删除了值重复的节点后剩余的链表,结合上面两种递归调用的情况:
    • 如果 head.val != head.next.val ,头结点需要保留,因此返回的是 head;
    • 如果 head.val == head.next.val ,头结点需要删除,需要返回的是 deleteDuplicates(move)。
  • 对链表 1 -> 2 -> 2 -> 3 递归的过程如下:

  • C++ 示例:
class Solution 
public:
    ListNode* deleteDuplicates(ListNode* head) 
        if (!head || !head->next) 
            return head;
        
        if (head->val != head->next->val) 
            head->next = deleteDuplicates(head->next);
         else 
            ListNode* move = head->next;
            while (move && head->val == move->val) 
                move = move->next;
            
            return deleteDuplicates(move);
        
        return head;
    
;
  • Python 示例:
class Solution(object):
    def deleteDuplicates(self, head):
        if not head or not head.next:
            return head
        if head.val != head.next.val:
            head.next = self.deleteDuplicates(head.next)
        else:
            move = head.next
            while move and head.val == move.val:
                move = move.next
            return self.deleteDuplicates(move)
        return head

② 一次遍历

  • 跟递归方法中的 while 语句跳过所有值相等的节点的思路是一样的:如果 cur.val == cur.next.val 说明两个相邻的节点值相等,所以继续后移,一直找到 cur.val != cur.next.val ,此时的 cur.next 就是值不等的节点。
  • 比如: 1 -> 2 -> 2 -> 2 -> 3,用一个 pre 指向 1;当 cur 指向第一个 2 的时候,发现 cur.val == cur.next.val,所以出现了值重复的节点啊,所以 cur 一直后移到最后一个 2 的时候,发现 cur.val != cur.next.val ,此时 cur.next = 3 ,所以 pre.next = cur.next ,即让1 的 next 节点是 3,就把中间的所有 2 都删除。
  • 代码中用到了一个常用的技巧:dummy 节点,也叫做“哑节点”,它在链表的迭代写法中非常常见,因为对于本题而言,可能会删除头结点 head,为了维护一个不变的头节点,所以添加了 dummy,让dummy.next = head,这样即使 head 被删了,那么会操作 dummy.next 指向新的链表头部,所以最终返回的也是 dummy.next。
  • C++ 示例:
class Solution 
public:
    ListNode* deleteDuplicates(ListNode* head) 
        if (!head || !head->next) return head;
        ListNode* preHead = new ListNode(0);
        preHead->next = head;
        ListNode* pre = preHead;
        ListNode* cur = head;
        while (cur) 
            //跳过当前的重复节点,使得cur指向当前重复元素的最后一个位置
            while (cur->next && cur->val == cur->next->val) 
                cur = cur->next;
            
            if (pre->next == cur) 
                //pre和cur之间没有重复节点,pre后移
                pre = pre->next; 
             else 
                //pre->next指向cur的下一个位置(相当于跳过了当前的重复元素)
                //但是pre不移动,仍然指向已经遍历的链表结尾
                pre->next = cur->next;
            
            cur = cur->next;
        
        return preHead->next;
    
;
  • Python 示例:
class Solution(object):
    def deleteDuplicates(self, head):
        if not head or not head.next:
            return head
        dummy = ListNode(0)
        dummy.next = head
        pre = dummy
        cur = head
        while cur:
            # 跳过当前的重复节点,使得cur指向当前重复元素的最后一个位置
            while cur.next and cur.val == cur.next.val:
                cur = cur.next
            if pre.next == cur:
                 # pre和cur之间没有重复节点,pre后移
                pre = pre.next
            else:
                # pre->next指向cur的下一个位置(相当于跳过了当前的重复元素)
                # 但是pre不移动,仍然指向已经遍历的链表结尾
                pre.next = cur.next
            cur = cur.next
        return dummy.next

③ 利用计数,两次遍历

  • 这个做法忽略了链表有序这个性质,使用了两次遍历,第一次遍历统计每个节点的值出现的次数,第二次遍历的时候,如果发现 head.next 的 val 出现次数不是 1 次,则需要删除 head.next。
  • C++ 示例:
class Solution 
public:
    ListNode* deleteDuplicates(ListNode* head) 
        unordered_map<int, int> m;
        ListNode dummy(0);
        ListNode* dummy_move = &dummy;
        ListNode* move = head;
        while (move) 
            m[move->val]++;
            move = move->next;
        
        move = head;
        while (move) 
            if (m[move->val] == 1) 
                dummy_move->next = move;
                dummy_move = dummy_move->next;
            
            move = move->next;
        
        dummy_move->next = nullptr;
        return dummy.next;
    
;
  • Python 示例:
class Solution:
    def deleteDuplicates(self, head):
        dummy = ListNode(0)
        dummy.next = head
        val_list = []
        while head:
            val_list.append(head.val)
            head = head.next
        counter = collections.Counter(val_list)
        head = dummy
        while head and head.next:
            if counter[head.next.val] != 1:
                head.next = head.next.next
            else:
                head = head.next
        return dummy.next

④ 一次遍历(LeetCode 官方解法)

  • 由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此只需要对链表进行一次遍历,就可以删除重复的元素。由于链表的头节点可能会被删除,因此需要额外使用一个哑节点(dummy node)指向链表的头节点。
  • 具体地,从指针 cur 指向链表的哑节点,随后开始对链表进行遍历。如果当前 cur.next 与 cur.next.next 对应的元素相同,那么就需要将 cur.next 以及所有后面拥有相同元素值的链表节点全部删除。我们记下这个元素值 x,随后不断将 cur.next 从链表中移除,直到 cur.next 为空节点或者其元素值不等于 x 为止。此时,我们将链表中所有元素值为 x 的节点全部删除。
  • 如果当前 cur.next 与 cur.next.next 对应的元素不相同,那么说明链表中只有一个元素值为 cur.next 的节点,那么就可以将 cur 指向 cur.next。
  • 当遍历完整个链表之后,返回链表的的哑节点的下一个节点 dummy.next 即可。
  • 需要注意 cur.next 以及 cur.next.next 可能为空节点,如果不加以判断,可能会产生运行错误。
  • C++ 示例:
class Solution 
public:
    ListNode* deleteDuplicates(ListNode* head) 
        if (!head) 
            return head;
        
        
        ListNode* dummy = new ListNode(0, head);

        ListNode* cur = dummy;
        while (cur->next && cur->next->next) 
            if (cur->next->val == cur->next->next->val) 
                int x = cur->next->val;
                while (cur->next && cur->next->val == x) 
                    cur->next = cur->next->next;
                
            
            else 
                cur = cur->next;
            
        

        return dummy->next;
    
;
  • Python 示例:
class Solution:
    def deleteDuplicates(self, head: ListNode) -> ListNode:
        if not head:
            return head
        
        dummy = ListNode(0, head)

        cur = dummy
        while cur.next and cur.next.next:
            if cur.next.val == cur.next.next.val:
                x = cur.next.val
                while cur.next and cur.next.val == x:
                    cur.next = cur.next.next
            else:
                cur = cur.next

        return dummy.next

以上是关于数据结构与算法之深入解析“删除排序链表中的重复元素II”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章

算法之小细节(细节~链表的特殊结点~提升优化度)~反转链表删除排序链表中的重复元素

《LeetCode之每日一题》:288.删除排序链表中的重复元素

《LeetCode之每日一题》:288.删除排序链表中的重复元素

《LeetCode之每日一题》:98.删除排序链表中的重复元素

Python描述 LeetCode 83. 删除排序链表中的重复元素

Python描述 LeetCode 83. 删除排序链表中的重复元素