算法通关村第一关——链表白银挑战笔记(双指针、回文、合并、旋转、删除重复元素)

该篇文章主要梳理相关问题思路,强化巩固新方法,提出自己的理解感悟,形成自己的模板套路,解决非递归线性链表的绝大多数题目。

与此同时本篇文章也涉及部分集合相关内容,需要掌握集合所在的包名以及集合使用方法。

1.寻找链表公共节点四种处理方式

四种方式中前两种方式的使用主要是强化集合的使用方法;第三种方式开阔视野,属于新思路,需要强化理解记忆;至于第四种快慢指针,太简单了无需复习。

1.1使用哈希

第一种方法的思想非常简单,就是利用一个数据结构存储节点信息,一边遍历一边检索节点信息是否在给定的数据结构中存在

为什么必须是哈希?(检索速度块!当然你可以写一个新的链表存储节点信息,再自己写个新的函数进行检索。重要的是速度,没有什么比HashSet和HashMap检索速度更快的了)

一个小tips:使用HashMap存储哈,key存ListNode,val随便存个数据结构就行,占位符罢了,上代码!

(What?HashSet实现代码也想要?我的建议:勤能补拙!)

public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
        //创建map存储链表1中元素,第二个位置为占位符
        HashMap<ListNode, Integer> map = new HashMap<>();
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while(p1 != null){
            map.put(p1, null);
            p1 = p1.next;
        }
        while(p2 != null){
            if(map.containsKey(p2)){
                return p2;
            }
            p2 = p2.next;
        }
        return null;
    }

1.2使用栈

第二种方法的思想我称其“逆过程”,它和物理中的一个场景非常相似,一个匀加速直线运动可以看成是一个匀减速直线运动的逆运动。那么存在公共节点的链表,逆向来看,它的尾部必然相同!什么数据结构是从尾部来观察数据的?答:栈!

需要注意的是,有着公共节点的链表其尾部相同,找到第一个不相同的元素cur,它的下一位就是公共节点!上代码!!

public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
        Stack<ListNode> stack1 = new Stack<>();
        Stack<ListNode> stack2 = new Stack<>();
        ListNode p1 = headA;
        ListNode p2 = headB;
        //链表AB压栈
        while(p1 != null){
            stack1.push(p1);
            p1 = p1.next;
        }
        while(p2 != null){
            stack2.push(p2);
            p2 = p2.next;
        }
        //出栈比较
        ListNode pNode = null;
        while(!stack1.empty() && !stack2.empty()){
            if(stack1.peek() == stack2.peek()){
                pNode = stack1.pop();
                stack2.pop();
            } else {
                break;
            }
        }
        return pNode;
    }

1.3拼接两个字符串

第三种方法的思想非常神奇!重点重点重点!

有公共节点的两个链表,再拼接之后,其尾部元素必定相同,第一个相同的尾部元素就是公共节点!(无论AB排列,还是BA排列)

很显然,我们可以构造两个新链表,第一个链表AB,第二个链表BA,然后设置两个指针,逐位比较节点是否相同即可!

可是这意味着空间浪费啊!我们不妨就用链表A和链表B,指针pANode用于遍历AB排列,当链表A遍历完了,接着就去遍历列表B(指针pBNode同理)大大缩减空间!直接上代码!(咳咳,这一段嘛,来自鱼骨头)

当心Leetcode提供的示例中,有两个链表没有公共节点存在的特殊情况(会进入死循环)!特殊情况特殊记忆!加上if(p1 != p2),比如链表1 2 3 和链表4 5 6,那么123456和456123在最后的时候p1 = 6,p2 = 3,紧接着p1 = 4,p2 = 1,陷入死循环!!

public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
        if(pHead1 == null || pHead2 == null){
            return null;
        }
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        //逐位比较节点是否相等
        while(p1 != p2){
            p1 = p1.next;
            p2 = p2.next;
            //防止出现p1 = null, p2 = null重置查询链表出现死循环
            if(p1 != p2){
                //某个链表访问完,访问另一个链表
                if(p1 == null){
                    p1 = pHead2;
                }
                if(p2 == null){
                    p2 = pHead1;
                }
            }
        }
        return p1;
    }

1.4差和双指针

第四种方法的思想就是分别遍历一遍链表A,链表B,统计它们的长度lenA,lenB

使用两个指针,长链表的指针先移动|lenA-lenB|,然后两个指针同时移动,指向元素相同就是公共节点。太简单了,代码不上了...

2.回文序列

回文,什么意思,说白了逆向看这个序列和顺序看这个序列一样,刚刚上面说了,什么数据结构用到逆向?栈!(幻听),ok直接上代码!

public static boolean isPalindromeByAllStack(ListNode head) {
        Stack<ListNode> stack = new Stack<>();
        ListNode p = head;
        //压栈
        while(p != null){
            stack.push(p);
            p = p.next;
        }
        p = head;
        //出栈,逆向顺向比较
        while(!stack.empty()){
            if(stack.peek().val != p.val){
                return false;
            }
            stack.pop();
            p = p.next;
        }
        return true;
    }

3.合并有序链表

这类题目变式很多,归根结底,在于如何合并两个有序链表。

搞两个指针,比较元素大小,插入新的链表,其中一个链表遍历完了,把另一个链表的剩余元素贴过去。上代码!

    public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        while(l1 != null && l2 != null){
            if(l1.val <= l2.val){
                p.next = l1;
                l1 = l1.next;
            }else{
                p.next = l2;
                l2 = l2.next;
            }
            p = p.next;
        }
        ListNode res = l1 == null ? l2 : l1;
        p.next = res;
        return dummy.next;
    }

变式训练1

如何合并K个有序链表?

简单点,搞一个空链表,先把空链表和第一个链表合并,合并之后的链表和第二个链表合并,依次...K个有序链表合并完成!(还是那句话,韩信带净化,勤能补拙!)

变式训练2

两个链表list1和list2,将list1中下标a到b的节点删除,把list2接在被删除的地方

关键:找到下标a节点的前驱,下标b节点的后继

如何变换出更多题目??

出题点1,开闭区间[a,b] (a,b) (a,b] [a,b)

出题点2,若[a,b]升序,list2升序,将[a,b]和list2合并按照升序排列

4.双指针

方法的本质在于设置双指针,两个指针移动步幅不同,找到中间/倒数第k位置的节点

令人振奋的是,找到第K位置节点有模板!我们只需要关注边缘条件,套用模板,就可以解决问题!

4.1寻找中间节点

思想很简单,那么为什么拎出来记一记,讲一讲?

对于一个链表 1 2 3 4 5 6 中间节点,有的题目说返回3,有的题目说返回4,那么你会精确控制嘛?这就是我要说的点!上代码!

当慢指针先移动,输出结果是4,当慢指针后移动,输出结果是3。这个现象如何记忆?

笨鸟先飞,跑得远!

    public static ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        //注意这里先慢指针移动还是先快指针移动
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

4.2寻找倒数第K个元素

注意哈,这个是模板直接套用模板,修改边界条件,大部分问题迎刃而解,不信接着看4.3旋转链表!

直接上模板代码!反复记忆,特别是第一个while循环条件

    public static ListNode getKthFromEnd(ListNode head, int k) {
        ListNode slow = head, fast = head;
        while(fast != null && k > 0){
            fast = fast.next;
            k--;
        }
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

4.3旋转链表

旋转链表,很简单,我们先找到倒数第k节点前驱,然后修改链表头,链表尾,拼接链表就行!

问题来了,如何寻找倒数第k节点的前驱?答:模板

public static ListNode rotateRight(ListNode head, int k) {
        //规范化到底旋转多少个元素
        int count = 0;
        ListNode p = head;
        while(p != null){
            p = p.next;
            count++;
        }
        k = k % count;
        //先找到倒数第k个节点
        ListNode slow = head, fast = head;
        ListNode oldTail = null, newTail = null;
        while(fast != null && k >0){
            fast = fast.next;
            k--;
        }
        while(fast != null){
            if(fast.next == null){
                oldTail = fast; // 原来链表的表尾
                newTail = slow; // 新链表的表尾,第k-1位置
            }
            fast = fast.next;
            slow = slow.next;
        }
        //再完成拼接
        oldTail.next = head;
        newTail.next = null;
        return slow;
    }

5.删除元素

5.1删除特定节点

删除特定节点和删除第n个节点的关键在于找到前驱节点。前者可以使用cur.next == val来判断,cur是否是前驱节点,后者使用模板找到前驱节点cur(修改边界条件)这里不再花多余篇幅赘述。

5.2删除重复元素

题型1:重复元素仅保留一个

我们使用cur记录当前遍历的节点,用cur.val==cur.next.val来判断,后面节点是否和前面节点重复,重复则删除,直接上代码!

需要注意的,while条件,我的理解因为使用cur.val==cur.next.val这一判断条件,因此while循环的判断条件是cur.next != null

    public static ListNode deleteDuplicate(ListNode head) {
        if(head == null){
            return null;
        }
        ListNode cur = head;
        while(cur.next != null){
            if(cur.val == cur.next.val){
                cur.next = cur.next.next;
            }else{
                cur = cur.next;
            }
        }
        return head;
    }

题型2:重复元素全删除

因为重复元素全部删除,存在链表1 1 2 3删除重复元素,变成2 3,修改链表表头,因此使用一个dummy虚拟节点记录表头节点

我们设定需要比较的节点是cur节点之后的两个节点,如果这两个节点相同(cur.next.val == cur.next.next.val),则删除;如果两个节点不相同,继续往后遍历查找。

存在cur后面连着好几个元素都相同的情况,怎么办?

使用变量x记录第一次重复时,元素值,后面节点依次比较x,如果和x相同则删除,删除后cur.next指向节点如图中的红色线所示,当后继再次相同继续删除,直到全部删除干净为止。这个过程可以使用while迭代删除,当元素值不等表示删除干净,退出while。

如何判断while边界条件?

同上一题型所述,我的理解:因为使用cur.next.val == cur.next.next.val这一判断条件

厘清思路之后,直接上代码!

public static ListNode deleteDuplicates(ListNode head) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode cur = dummy;
        int x;
        while(cur.next != null && cur.next.next !=null){
            //cur后面的两个元素相同
            if(cur.next.val == cur.next.next.val){
                //记录元素值
                x = cur.next.val;
                //删除后面可能的元素
                while(cur.next != null){
                    if(cur.next.val == x){
                        cur.next = cur.next.next;
                    }else{
                        break;
                    }
                }
            }else{
                cur = cur.next;
            }
        }
        return dummy.next;
    }

Ok,《算法通关村第一关——链表白银挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)

再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y...emmm还没想好,ok,拜拜,第一关第三幕见!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值