加载中...
返回

难题本 | LeetCode82. 删除排序链表中的重复元素 II

题目链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-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
  • 题目数据保证链表已经按升序排列

分析

有序 的链表中去重,表示我们理论上只需要进行一次遍历即可。

而两次遍历的办法则更为简单,也是最初浮现在我脑海中的念头。考虑到链表节点的数据范围在 [-100, 100] 区间中,我们在第一次遍历的过程中记录每个数值出现的次数,而在第二次遍历过程中考虑下一个节点(的数值)是否应该存在于最终的结果中即可。


当然,两次遍历的办法随着节点数据范围的变大很快就失效了,投机不可取,一次遍历的办法如何实现呢?

在有序的数组中取得一段相同元素的子数组,直接考虑滑动窗口。

我们使用一个指针,指向窗口 左边界的左邻居 ,使用两个指针维护窗口的左右边界。当窗口右边界数值等于左边界数值时,窗口向右扩张,否则进行一定的更新操作。

容易想象,在不发生重复的情况下,窗口的大小(right - left)始终为 1 ,而发生重复的时候整段窗口需要全部从链表上删除。

需要注意,由于我们的规则是:right指针指向的元素与 left 指向的元素不同时,才停止窗口的扩张,进入更新操作,因此,right 指针指向的元素并不属于窗口本身。

那么如何更新呢?

我们注意到,当 right == left->next 时,即 winSize == 1 时,不需要对窗口中的元素进行操作,则将三个指针往后移动,直接进入下一步的窗口更新环节即可。

而窗口大小大于1时,就比较有趣了。我们直接将窗口 左边界的左邻居 ,即此处的 Out-left 指向的元素链向 right 元素,这样就直接跳过了整个重复的部分,示意如下:

到现在为止,我们已经完成了在正常情况下的遍历更新策略,但是循环遍历的边界条件很重要,且题目要求返回结果链表的头结点,这个头结点该如何确定也很重要。

首先是循环条件,我们使得 right 指针指向最后一个节点时结束循环,因此 while (right->next)

在我的首次提交中,使用了比较复杂的逻辑来判断头结点是否已经出现。当符合条件的头结点还没有出现时,对于当前的 right 指针,当它的右邻居值与它不同,则说明当前这个 right 指向的节点是不用删除的,可以作为结果链表的头结点。

同理进行 Out-left 指针的判断。

AC代码1

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head || !head->next)
            return head;

        ListNode* lefleft = NULL;
        ListNode* left = head;
        ListNode* right = head;
        ListNode* ret = NULL;
        int winSize = 1;

        while (right->next)
        {
            // cout << right->val << endl;
            right = right->next;
            if (right->val == left->val)
            {
                winSize++;
            }
            else
            {
                // cout << right->val << endl;
                if (winSize != 1)  // delete the window
                {
                    // cout << "WinSize: " << winSize << endl;
                    if (!lefleft)  // do not have previous node
                    {
                        if (!right->next || right->val != right->next->val)
                        {
                            ret = right;
                            lefleft = right;
                        }
                    }
                    else
                    {
                        lefleft->next = right;
                    }
                    left = right;
                    winSize = 1;
                }
                else
                {
                    ret = (ret == NULL ? left : ret);
                    lefleft = left;
                    left = right;
                }
            }
        }

        // right points to the last node
        if (winSize != 1)  // delete the window
        {
            // cout << "WinSize: " << winSize << endl;
            if (lefleft)
            {
                lefleft->next = NULL;
            }
        }
        else
        {
            ret = (ret == NULL ? left : ret);
            lefleft = left;
            left = right;
        }
        return ret;
    }
};

分析2

上面的代码显然由于引入了头结点的逻辑而变得非常臃肿,能否对其进行优化呢?

我们引入一个哑结点 dummy ,它作为头结点之前的一个节点,初始化为 dummy->next = head

此外,滑动窗口的思想实际上在此过于复杂,因为我们只需要判断窗口的大小是否为1,则只需要考虑 right 是否等于 left->next 即可。

于是一种较为简洁的思想诞生了:使用 prenownxt 三个指针来标识前一个节点、现在的节点和下一个节点。还是按照之前的办法对 now (即之前的 left )和 nxt (即之前的 right )进行更新。由于 pre 被初始化为 dummy 节点,故一轮遍历之后,dummy->next 所指向的节点一定是第一次符合条件的节点,为返回的链表头部。

AC代码2

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head || !head->next)
            return head;

        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* pre = dummy;
        ListNode* now = head;
        ListNode* nxt = head->next;

        while (nxt)
        {
            if (nxt->val == now->val)
            {
                nxt = nxt->next;
            }
            else
            {
                if (nxt == now->next)
                {
                    pre = now;
                    now = nxt;
                    nxt = nxt->next;
                }
                else
                {
                    pre->next = nxt;
                    now = nxt;
                    nxt = nxt->next;
                }
            }
        }
		// 注意此处细节,循环结束之后进行最后一次判断
        if (nxt != now->next)
        {
            pre->next = NULL;
        }
        
        return dummy->next;
    }
};

参考资料

[1] 力扣官方题解.删除排序链表中的重复元素 II[EB/OL].2021-03-24

https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/solution/shan-chu-pai-xu-lian-biao-zhong-de-zhong-oayn/

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy