剑指Offer-58-删除链表中重复的节点

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dawn_after_dark/article/details/83060401

项目地址:https://github.com/SpecialYy/Sword-Means-Offer

题目

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

对应为书本的p122

解析

预备知识

链表删除问题的本质其实就是修改指针指向问题,最传统的删除链表中某一个节点时,一般都是采用以下做法:

FireShot Capture 2 - 未命名文件 - Proces_ - https___www.processon.com_diagraming_5b73b6e7e4b025cf4946388a

只需将待删除节点的前一个节点的next指向待删除节点的后一个节点即可,相当于把3节点从当前链表剔除掉。所以我们这道删除链表中重复的节点的操作同样如此,只需把重复节点群的前驱节点指向重复群的后继节点即可。

思路一

我们这里采用滑动窗口的思想来确定当前重复节点的范围,固定窗口的左边界,然后依次扩展窗口的右边界,直到出现不同的节点为止。这样我们只需把当前窗口左边界的前驱节点指向窗口的右边界为止即可(note:此时的右边界为不重复的节点)。然后我们重新更新滑动窗口的左边界即可。

这里有一个特殊的情况就是链表首部节点可能也是重复节点,所以删除此部分重复节点时需要额外处理。为了统一代码分格,我们引入一个头结点作为链表的首部head,最后只需返回head.next,大大简化了代码而且方便理解。

图解:

屏幕快照 2018-10-15 15.57.14

	/**
     * 添加头结点的方式降低了当首部有重复的节点时的复杂性
     * @param pHead
     * @return
     */
    public ListNode deleteDuplication1(ListNode pHead) {
        if (pHead == null || pHead.next == null) {
            return pHead;
        }
        //添加头结点
        ListNode head = new ListNode(0);
        head.next = pHead;
        //p为目前构建的链表的最后一个节点
        ListNode p = head;
        //s为窗口的左边界,d为窗口的右边界
        ListNode s = pHead;
        ListNode d = pHead.next;
        while (d != null) {
            //窗口中出现不重复的节点
            if (s.val != d.val) {
                //若当前窗口大小为1,说明s不是重复节点
                if (s.next == d) {
                    //把s加到当前的链表
                    p.next = s;
                    //更新p
                    p = p.next;
                }
                /**
                 * 下面主要是更新窗口
                 * 1. 若当前窗口为1,s已加入到链表中,所以更新s
                 * 2. 若当前窗口不为1,说明s到d-1的窗口中都是重复的,所以更新s
                 */
                s = d;
                d = d.next;
            } else {
                //窗口中出现重复元素,更新窗口的右边界
                d = d.next;
            }
        }
        /**
         * 上面的循环结束,可能是窗口为1或者大于1时退出的
         * 可根据窗口的左边界判断出窗口的大小
         */
        p.next = s.next == null ? s : null;
        return head.next;
    }

思路二

能不能不使用头结点呢?当然可以,我们这里采用递归的方式。有时候也不要一味的排斥递归的缺点,其实写写递归也能很好的锻炼思路,不妨写着玩,嘿嘿。

思路为每一次递归的输入参数为构建的链表的末尾的下一个即将插入的节点,因为这样才能适用当链表首部也是重复节点的情况。此处我们还是分2种情况处理:

  1. 若当前输入的节点为空或者是走到了链表的末尾,这时肯定不存在重复节点,直接返回输入的节点即可
  2. 初始化滑动窗口并且不断扩展大小,直到有不重复节点加入。
    1. 判断窗口大小,若为1,则说明窗口大小的左边界应该加入到构建链表的下一个位置。然后继续递归处理以滑动窗口右边界开始的链表,新添加的节点的next域应指向下一个递归返回节点
    2. 若窗口大小大于2,说明窗口中的除了右边界的节点都应该删除,然后继续递归处理以滑动窗口右边界开始的链表,然后继续递归处理以滑动窗口右边界开始的链表。当前递归的形参等于下一个递归返回节点,因为本次并没有添加节点。
/**
     * 递归实现,这样就不必创建头结点了
     * 没事写写递归,锻炼一下思维也挺好哈
     * @param pHead 表示不包含重复节点的链表末尾的下一个即将插入节点的槽位
     * @return
     */
    public ListNode deleteDuplication2(ListNode pHead) {
        //为空或者走到末尾,直接返回当前节点即可
        if (pHead == null || pHead.next == null) {
            return pHead;
        }
        ListNode p = pHead;
        ListNode q = pHead.next;
        //不断扩大包含重复节点的窗口的大小
        while(q != null && p.val == q.val) {
            q = q.next;
        }
        //如果包含重复节点的窗口的大小为1,则添加到构建的链表中
        if (p.next == q) {
            pHead = p;
            //为当前已添加到链表中节点指示后续节点,后续节点从q开始考察
            pHead.next = deleteDuplication2(q);
        } else {
            //当前包含重复节点的窗口的大小大于1,则跳过整个窗口,并从q开始
            pHead = deleteDuplication2(q);
        }
        return pHead;
    }

总结

链表删除问题注意节点的指针的指向即可,同时要保证不出现空指针异常。碰到连续问题可以考虑滑动窗口模型。

猜你喜欢

转载自blog.csdn.net/dawn_after_dark/article/details/83060401