【JavaOj】链表十连问

JavaOj & 链表十连问

1. 删除链表中等于给定值 val 的所有节点

203. 移除链表元素 - 力扣(Leetcode)

在这里插入图片描述

1.1 代码实现~

在这里插入图片描述

  • 这个模式在数据结构的题目中,尤为常见,一定要重点熟悉~
  • 这个类就是现成的节点类~
class Solution {
    public ListNode removeElements(ListNode head, int key) {
        if(head == null) {
            return head;
        }
        ListNode prev = head;
        ListNode cur = head.next;
        while(cur != null) {
            if(cur.val == key) {
                prev.next = cur.next;
            }else {
                prev = cur;
            }cur = cur.next;
        }
        if(head.val == key) {
            head = head.next;
        }
        return head;
    }
}

1.2 深度讲解 + 动图分析

  • 这道题就是我们的removeAllKey方法呀~

  • 再讲一次~

  • 删除所有同键值节点~

    • 这个稍微复杂,思想仍然是(跳跃式忽视节点的方法)【即prev的后驱指向cur的后驱】
    • 用到了一个前后指针的算法
      • prev一开始处于head的位置,cur在head.next的位置
      • 遍历链表,结束条件是cur为null
      • 正常情况下,即删除节点之前,prev和cur都是同步走的
      • 但cur遇到要删除的节点的时候,要让cur去找下一个非此键值的节点如果还是该键值,这个节点将不会被后续操作删除或者null,cur每次遇到键值匹配的节点,size–
        1. cur每次找到key,prev的后驱指向cur的后驱 ===》实现删除连续节点
        2. cur一旦找不到key,prev就会继承cur的位置
      • 最不应该忘记的是这个:
        • 头节点留到最后再判断,如果一开始将头结点删了,那么后续仍然有头结点无法被判断,反反复复无法解决问题~,这样还不如先判断后面的节点,最后再判断头节点
        • 如果头节点该删,head = head.next~
        • 在这里插入图片描述
  • 动图演示:
    在这里插入图片描述

2.反转单链表

206. 反转链表 - 力扣(Leetcode)

在这里插入图片描述

2.1 代码实现

在这里插入图片描述

  • see it again~
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null) {
            return null;
        }
        ListNode cur = head.next;
        head.next = null;
        while(cur != null) {
            ListNode curNext = cur.next;
            cur.next = head;
            head = cur;
            cur = curNext;
        }
        return head;
    }
}

2.2 深度解析 + 动图分析

  • 不知道你有没有联想到可以用头插法~
  • *你可能在链表学习中,在测试的时候会发现,连续头插的结果是逆序的,那么我们就可以借助这一现象,逆序链表,借助探路指针还有“**传送指针”*进行操作,且听我细细道来~
  1. head 为 null,不用逆序,直接返回null
  2. cur探路指针去遍历,curNext去记录cur的后驱,防止回收
    • 将cur直接头插链表~
    • 通过 “传送指针curNext” 前往原来的位置的后驱~
    • 直到cur被传送到null,退出循环~
    • return head;
  • 动图解析:

在这里插入图片描述

3. 链表的中间结点

876. 链表的中间结点 - 力扣(Leetcode)

在这里插入图片描述

3.1 代码实现

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast .next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
}

3.2 深度解析 + 动图分析

  • 这里有一个很重要的思想
    • “快慢指针” --> 在Java中是引用
  • 两个探路指针去探路,但是有一个指针速度是另一个的一倍,这就是快指针
    • 那么快指针到达null的时候,慢指针就处于中间位置了~
  • 但是要区分这个链表是偶数节点数还是奇数节点数~
    • 我们可以通过节点数确定该节点的下标,但是我要求在只遍历一次的情况下去解决问题~
    • 奇数:正中间节点~
    • 偶数:正中间节点~
      • 很巧的是,题目要求的就是这个~
  • 动图解析
3.2.1 奇数:

在这里插入图片描述

3.2.2 偶数:

在这里插入图片描述

4. 链表中倒数第k个结点

链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

在这里插入图片描述

4.1 代码实现

public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        if(head == null) {
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        for(int i = 0; i < k; i++) {
            fast = fast.next;
            if(fast == null && i < k - 1) {
                return null;
            }
        }
        while(fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

4.2 深度解析 + 动图分析

  • 通过第三题,我们这道题不难联想到,也可以用“快慢指针”

  • 但是 “快慢指针” 一定要速度不相同吗?

    • 显然不是必要的~
    • 这里的快慢指针,有一个指针快人一步~
    • 也就是说,除了速度快,还可以 “笨鸟先飞”~
      • 我们学习也是要这样,虽然可能学得不快,但是可以先学~
    • 在这里插入图片描述
  • 我们当然也可以遍历两次表去解决这个问题,但是我要求只遍历一次~

  • 那么我们就可以让fast的快指针,先走k步,然后再与slow一起走,那么fast走完的时候,slow的位置,就是倒数第k个~

    • 因为fastslow差距k,最终就是slownull差距k~

在这里插入图片描述

  • 结合牛客给的测试用例,有可能k很大,或者headnull~
    • 我们要处理这些细节~

在这里插入图片描述

  • 动图分析:

在这里插入图片描述

5.合并有序链表

21. 合并两个有序链表 - 力扣(Leetcode)

在这里插入图片描述

5.1 代码实现

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode head = new ListNode();
        ListNode cur = head;
        while(list1 != null && list2 != null) {
            if(list1.val <= list2.val) {
                cur.next = list1;
                list1 = list1.next;
            }else {
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }
        if(list1 != null) {
            cur.next = list1;
        }else {
            cur.next = list2;
        }
        head = head.next;
        return head;
    }
}

5.2 深度解析 + 动图分析

  • 这时候我们就需要这个特殊的链表:带头链表
    • 这头指针又称之为:“哨兵”
    • 而根据它的用途,我更喜欢称之为:“外来异物”
img

外界异物侵入珍珠蚌内后,在保护机制下,珍珠蚌会分泌一种珍珠质,将异物层层包裹,最终形成了珍珠。

  • 那么这个头,不是有效节点,但是可以帮助我们累积构造形成链表~
    • list1.val <= list2.val 那么就连接list1,list1往后走
    • 反之,连接list2,list2往后走
    • 无论连接谁,cur保持处于待构造表的尾部

在这里插入图片描述

  • 动图解析:

在这里插入图片描述

6. 链表分割

链表分割_牛客题霸_牛客网 (nowcoder.com)

在这里插入图片描述

6.1 代码实现

public class Partition {
    public ListNode partition(ListNode pHead, int x) {
        if (pHead == null) {
            return null;
        }
        ListNode fast = pHead.next;
        ListNode prev = pHead;
        ListNode slow = null;
        ListNode cur = null;
        while (fast != null) {
            if (fast.val < x) {
                if (slow == null) {
                    slow = fast;
                    cur = slow;
                } else {
                    cur.next = fast;
                    cur = cur.next;
                }
            } else {
                prev.next = fast;
                prev = fast;
            }
            fast = fast.next;
        }
        prev.next = null;
        if (pHead.val < x) {
            ListNode tmp = pHead;
            pHead = pHead.next;
            tmp.next = slow;
            slow = tmp;
        }
        if (slow == null) {
            slow = pHead;
        } else {
            cur.next = pHead;
        }
        return slow;
    }
}

6.2 深度解析 + 动图分析

  • 这个问题会比较难,因为涉及多个中介节点

  • 要求以给定的一个值为标准,小于该值的放在左边,大于该值放在右边,等于的话在哪边都无所谓~

    • 并且要求,左右两条子链表是按照原来的顺序排的~
  • 在这里插入图片描述

  • 写一次不用 “外来异物” 的方法~

  • 重要思想:

    • 类似于单链表删除操作~
    • 头节点不应该在最开始的时候进行判断,而是留到最后判断
  1. fast做为最初的探路指针,它完整遍历原链表~
  2. slow的存在是“较小数节点”的链表~
  3. 如果fast此时键值小于x

在这里插入图片描述

  1. 如果fast此时键值不小于x

在这里插入图片描述

  1. 处理原链表的头节点
  2. 连接两条链表~

在这里插入图片描述

6.2.1 头节点为较小节点图示:

在这里插入图片描述

6.2.2 头结点不为较小节点图示:

在这里插入图片描述

7. 回文链表

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

在这里插入图片描述

7.1 代码实现

public class PalindromeList {
    public boolean chkPalindrome(ListNode A) {
        ListNode fast = A;
        ListNode slow = A;
 
        if (fast == null) {
            return false;
        }
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
 
        //逆序后面的表
        ListNode cur = slow.next;
        slow.next = null;
        while (cur != null) {
            ListNode curNext = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curNext;
        }
 
        while (slow != null) {
             if (A.val != slow.val) {
                return false;
            }
            A = A.next;
            slow = slow.next;
        }
        return true;
    }
}

7.2 深度解析 + 动图分析

在这里插入图片描述

  • 回文,我们可以分别判断首与尾,首与尾,首与尾,就像顺序表那样~
    • 但是这种算法的时间复杂度为O(N2)
    1. 我们可以逆序一整个链表,判断是否与原链表一样
    2. 我们可以只逆序一半,判断前半段与后半段是否相同
  • 法2, 显然是最合适的~
    • 这道题是中间节点,与逆序链表的结合题~
  1. 找到中间节点,用头插逆序的方法将后半段进行逆序

在这里插入图片描述

  1. 判断是否回文~

在这里插入图片描述

  • 对于奇数偶数节点的链表,用刚才的方法有如上两种不同形式~

    • 相交链表~
  • 只要slow指针能达到null,则说明true~

    • 只要不相等,直接返回false~

在这里插入图片描述

7.2.1 奇数节点链表后半段逆序 + 判断回文动图解释:
  • 逆序后半段:

在这里插入图片描述

  • 判断回文:

在这里插入图片描述

7.2.2 偶数节点链表后半段逆序 + 判断回文动图解释:
  • 逆序后半段:

在这里插入图片描述

  • 判断回文:

在这里插入图片描述

8. 相交链表

160. 相交链表 - 力扣(Leetcode)

在这里插入图片描述

8.1 代码实现

  • 节点类型跟之前的一样~
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA = headA; 
        ListNode curB = headB;
        while(curA != null && curB != null) {
            curA = curA.next;
            curB = curB.next;
        }
        while(curA != null) {
            headA = headA.next;
            curA = curA.next;
        }
        while(curB != null) {
            headB = headB.next;
            curB = curB.next;
        }
        while(true) {
            if(headA == headB) {
                return headA;
            }
            headA = headA.next;
            headB = headB.next;
        }     
    }
}

8.2 深度解析 + 动图分析

  • 一开始想到的方法就是,遍历N2次, 即一个链表,每一个节点都在另一条链表上(整趟)找
    • 很笨~
  • 相交单链表的特点就是,交点到链尾,两个表是完全重合的,也就是说,两条相交的单链表,个数上的差异取决于节点之前的个数差异~
    • 当然也可以用两次遍历确认两条的长度,然后让他们前半段的个数差抵消~
    • 但是我不喜欢多遍历一次去测长度~
  • 且看看我的方法~

在这里插入图片描述

  1. 是相交链表:

在这里插入图片描述

  1. 不是相交链表:
  • 这样都指向null的时候,就会返回null
    • 不相交返回空~

在这里插入图片描述

9. 循环链表之判断是否带环

141. 环形链表 - 力扣(Leetcode)

在这里插入图片描述

9.1 代码实现

  • 节点类型跟之前的一样~
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null) {       
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow) {
                return true;
            }
        }
        return false;
    }
}

9.2 深度解析 + 动图分析

  • 这里结合了 “快慢指针” 的性质
    • 让两个指针速度不一样,当进入循环链表的时候
    • 由于速度不一样,两个指针会相遇~
  • 如果不是循环链表,快指针会很快指向null~
  • 为什么“快慢指针”的速度为2:1?
    • 因为其他比例,例如3:1 ,可能会导致循环链表,两个引用一直没相遇,反复错过,绕多了很多圈~

      • 如果结尾出现,两个节点的循环,并且slow在前,fast在后~
      • 就会出现死循环~
      • 在这里插入图片描述
    • 对于第十问,2:1 有很大的帮助!

在这里插入图片描述

10. 循环链表之入口点

142. 环形链表 II - 力扣(Leetcode)

在这里插入图片描述

10.1 代码实现

  • 节点类型跟前面一模一样~
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        ListNode cur = head;
        while(fast != null && fast.next != null) {       
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow) {
                while(cur != slow) {
                    cur = cur.next;
                    slow = slow.next;
                }
                return slow;
            }
        }        
          return null;
    }
}

10.2 深度解析 + 动图分析

  • 只需要在第九题的基础上,做一些更改~

在这里插入图片描述

  1. 作为返回值,如果不是循环链表返回null
  2. 如果是循环链表,返回入口点,那么入口点的计算算法是什么呢?
      1. 在第九题的基础上,我们找到了相遇点,而相遇点不一定是入口点~
        • 所以两个指针必须同时走,一快一慢~
      2. 对于相遇点有这么一个性质:相遇点与链表头节点,两个引用这两个点出发,速度为1的情况下,会在入口点相遇~
      3. 下面看我证明:
    • 在这里插入图片描述

    • 得到这个结论,因为我是按单位长度来算的,所以,只要是循环链表,就满足这个公式~

      • 无非就是有些量为0的特殊情况~
      • 这也是为什么用两倍的关系,这样才有这么好的性质~
    • 这就是这一段代码的由来~

在这里插入图片描述

  • 差不多就这样:

在这里插入图片描述

11. 补充知识点~

11.1 LinkedList链表的反向遍历~

  • 对于单链表,逆序打印开销实在太大了~
  • 所以可以借助迭代器(了解)
  • ListIterator是List受重写后的一个方法,listIterator(int index)
    • index为迭代器内部的引用,指向的是index下标~
    • 这个迭代器既可以顺序打印,也可以逆序打印~
    • 逆序的话,用listIterator.hasPrevious()为条件,内置引用前有元素,即可打印
      • listIterator.previous()为内置引用的元素
        • 并且调用一次,引用往前走一步
    public static void main(String[] args) {
        List<Integer> list = new LinkedList<>();
        list.add(55);
        list.add(56);
        list.add(57);
        list.add(58);
        list.add(59);
        ListIterator<Integer> listIterator = list.listIterator(list.size());
        while(listIterator.hasPrevious()) {
            System.out.print(listIterator.previous() + " ");
        }
    }    

在这里插入图片描述

  • 结果正常~

11.2 ArrayList 与 LinkedList的区别

不同点ArrayListLinkedList
存储空间空间分布紧密连续逻辑上连续,但是空间分布分散
随机访问O(1)O(N)
头插需要挪动元素,O( N )O(1)
插入空间不够需扩容无容量概念
应用场景元素高效存储 + 访问次数多频繁插入,频繁删除

11.3 链表归并排序(知识扩展)【涉及排序原理】

  • 时间复杂度O(N * log2N), 空间复杂度O( 1 )

  • 思路跟刚才归并怎么排一致,重点在于用快慢指针找到中点,节省速度

    • 是可以先求长度然后一直用的(就是算一次长度,然后每次都用这个长度为基准,去找mid
  • 之后不断合并有序链表。

  • 看几次动图:在这里插入图片描述

  • 递归的重点就分清“整体感”,比如说下面的,我们就看做左右边已经弄好了,只要满足小问题(递归出口

  • ),通过数学归纳法就能知道成立

  •     ListNode left = sortList(head);
        ListNode right = sortList(tmp);
    
    public ListNode sortList(ListNode head) {
        if(head == null || head.next == null) {
            return head;
        }
        ListNode slow = head;
        ListNode fast = head.next;
        //避免当剩余两个节点时,中间节点变成右边那个,这样会死递归!
        //(打断链表更没有意义,右边链表是null,没用)
        while(fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode tmp = slow.next;
        slow.next = null; //打断链表

技巧1平分链表(快慢指针在Java中应该是引用,或者下标)

  • 快慢指针,慢指针应该走到中间偏前的一个位置【不然会死递归】
    • 以两个节点为例子,快指针走两步,慢指针走一步,然后slow.next = null不就等于没有意义吗, 进入递归后,(左侧)依旧是两个节点,以此造成栈溢出!
      • 解决方法:fast先走一步,最终slow停在中节点偏前
      • fast不先走一步,最终slow停在中节点偏后
        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        
        ListNode ret = new ListNode(0);
        //临时头节点,最终不要即可,因为我们不能确定头结点是否是谁的
        ListNode cur = ret;
        while(left != null && right != null) {
            if(left.val <= right.val) {
                cur.next = left;
                left = left.next;
            }else {
                cur.next = right;
                right = right.next;
            }
            cur = cur.next;
        }
        cur.next = left != null ? left : right;
        return ret.next;
    }
  • 技巧2: 给需要构造的链表提供一个起始节点,(就像珍珠需要有一个小石子,最终才能积累成珠)

    • ListNode ret = new ListNode(0);这一句代码,提供一个带头链表
      • 目的是因为,我们不知道首节点是谁,毕竟尾入法在首节点需要判断
      • 那我们不如直接给一个,最终不考虑进去就行了
  • 技巧3:合并有序链表,不用多说


文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s:103

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值