算法刷题笔记 Day_1 7道链表题

╰+攻爆jí腚メ 2024-03-26 14:10 177阅读 0赞

链表算法

目录

链表算法

1.合并两个有序链表

2.单链表的分解

3.合并 k 个有序链表

4.单链表的倒数第 k 个节点

5.单链表的中点

6.判断链表是否包含环

7.两个链表是否相交


1.合并两个有序链表

最基本的链表技巧,力扣第 21 题「 合并两个有序链表」

给你输入两个有序链表,请你把他俩合并成一个新的有序链表,函数签名如下:

  1. ListNode mergeTwoLists(ListNode l1, ListNode l2);

这题比较简单,我们直接看解法:

  1. ListNode mergeTwoLists(ListNode l1, ListNode l2) {
  2. // 虚拟头结点
  3. ListNode dummy = new ListNode(-1), p = dummy;
  4. ListNode p1 = l1, p2 = l2;
  5. while (p1 != null && p2 != null) {
  6. // 比较 p1 和 p2 两个指针
  7. // 将值较小的的节点接到 p 指针
  8. if (p1.val > p2.val) {
  9. p.next = p2;
  10. p2 = p2.next;
  11. } else {
  12. p.next = p1;
  13. p1 = p1.next;
  14. }
  15. // p 指针不断前进
  16. p = p.next;
  17. }
  18. if (p1 != null) {
  19. p.next = p1;
  20. }
  21. if (p2 != null) {
  22. p.next = p2;
  23. }
  24. return dummy.next;
  25. }

代码中还用到一个链表的算法题中是很常见的「虚拟头结点」技巧,也就是 dummy 节点,它相当于是个占位符,可以避免处理空指针的情况,降低代码的复杂性。

2.单链表的分解

力扣第 86 题「 分隔链表」

  1. class Solution {
  2. public ListNode partition(ListNode head, int x) {
  3. ListNode font = new ListNode(-1),fontRest = font;
  4. ListNode end = new ListNode(-1),endRest = end;
  5. while (head!=null){
  6. if (head.val < x){
  7. font.next = head;
  8. font = font.next;
  9. }else {
  10. end.next = head;
  11. end = end.next;
  12. }
  13. // head = head.next;
  14. //断层操作 断开head链表中该结点和下个结点的联系
  15. ListNode temp = head.next;
  16. head.next = null;
  17. head = temp;
  18. }
  19. font.next = endRest.next;
  20. return fontRest.next;
  21. }
  22. }

在合并两个有序链表时让你合二为一,而这里需要分解让你把原链表一分为二。具体来说,我们可以把原链表分成两个小链表,一个链表中的元素大小都小于 x,另一个链表中的元素都大于等于 x,最后再把这两条链表接到一起,就得到了题目想要的结果.

关键点:在遍历head结点的时候,外面要注意将head结点掐断一下,防止head.next的数据被全部赋值到font或者是end两个链表中,我们在每一次(分割链表)遍历的过程中,都需要断层处理,保证我们每次只处理一个数据!

3.合并 k 个有序链表

力扣第 23 题「 合并K个升序链表」

  1. ListNode mergeKLists(ListNode[] lists) {
  2. if (lists.length == 0) return null;
  3. // 虚拟头结点
  4. ListNode dummy = new ListNode(-1);
  5. ListNode p = dummy;
  6. // 优先级队列,最小堆
  7. PriorityQueue<ListNode> pq = new PriorityQueue<>(
  8. lists.length, (a, b)->(a.val - b.val));
  9. // 将 k 个链表的头结点加入最小堆
  10. for (ListNode head : lists) {
  11. if (head != null)
  12. pq.add(head);
  13. }
  14. while (!pq.isEmpty()) {
  15. // 获取最小节点,接到结果链表中
  16. ListNode node = pq.poll();
  17. p.next = node;
  18. if (node.next != null) {
  19. pq.add(node.next);
  20. }
  21. // p 指针不断前进
  22. p = p.next;
  23. }
  24. return dummy.next;
  25. }

这里有个重要知识点:二叉堆的优先队列

参考资料链接:

二叉堆详解实现优先级队列 :: labuladong的算法小抄

优先级队列PriorityQueue_优先队列 poll_秋鸣之时的博客-CSDN博客

  1. //优先队列 PriorityQueue类实现方法:
  2. public class PriorityQueue<E>
  3. extends java.util.AbstractQueue<E>
  4. implements java.io.Serializable

基于优先级堆的无限优先级队列。优先级队列的元素根据它们的自然顺序进行排序,或者由队列构建时提供的比较器进行排序,具体取决于使用的构造函数。优先级队列不允许空元素。依赖于自然排序的优先级队列也不允许插入不可比较的对象(这样做可能会导致ClassCastException)。 此队列的头部是与指定排序相关的最少元素。如果多个元素以最小值绑定,则头部是这些元素中的一个——绑定被任意断开。队列检索操作轮询、删除、偷看和元素访问队列头部的元素。 优先级队列是无限的,但它有一个内部容量来控制用于存储队列中元素的数组的大小。它总是至少与队列大小一样大。当元素添加到优先级队列中时,其容量会自动增长。未指定增长政策的详细信息。 此类及其迭代器实现Collection和iterator接口的所有可选方法。方法迭代器()中提供的迭代器不能保证以任何特定顺序遍历优先级队列的元素。如果需要有序遍历,请考虑使用Arrays.sort(pq.toArray())。 请注意,此实现不同步。如果任何线程修改队列,则多个线程不应同时访问PriorityQueue实例。而是使用线程安全的java.util.concurrent.PriorityBlockingQueue类。

实现说明:此实现为入队和出队方法(offer、poll、remove()和add)提供了O(log(n))时间;移除(Object)和包含(Object)方法的线性时间;以及检索方法(peek、元素和大小)的恒定时间。 此类是Java集合框架的成员。

lambda表达式写法

如下lambda表达式:

  1. (int a,int b)-> {return a + b;}

如上,本质是一个函数。
一般的函数如下:

  1. int add(int a,int b){
  2. return a + b;
  3. }

有返回值,方法名,参数列表,方法体

lambda表达式只有参数列表方法体

  1. (参数列表) -> {
  2. 方法体;
  3. }

4.单链表的倒数第 k 个节点

只遍历了一次链表,就获得了链表倒数第 k 个结点。(更加高级!)

(普通方法:遍历两次 然后得到值)

  1. // 返回链表的倒数第 k 个节点
  2. ListNode findFromEnd(ListNode head, int k) {
  3. ListNode p1 = head;
  4. // p1 先走 k 步
  5. for (int i = 0; i < k; i++) {
  6. p1 = p1.next;
  7. }
  8. ListNode p2 = head;
  9. // p1 和 p2 同时走 n - k 步
  10. while (p1 != null) {
  11. p2 = p2.next;
  12. p1 = p1.next;
  13. }
  14. // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
  15. return p2;
  16. }

相关例题:力扣第 19 题「 删除链表的倒数第 N 个结点」

  1. class Solution {
  2. public ListNode removeNthFromEnd(ListNode head, int n) {
  3. ListNode dummy = new ListNode(-1);
  4. dummy.next = head;
  5. ListNode ListDeleteNode = findFromEnd(dummy, n + 1);
  6. ListDeleteNode.next = ListDeleteNode.next.next; //删除操作!
  7. return dummy.next;
  8. }
  9. // 返回链表的倒数第 k 个节点
  10. ListNode findFromEnd(ListNode head, int k) {
  11. ListNode point = head;
  12. //先让point走k步
  13. for (int i = 0; i < k; i++) {
  14. point = point.next;
  15. }
  16. while (point != null) {//两指针同时往后面走
  17. head = head.next;
  18. point = point.next;
  19. }
  20. return head;
  21. }
  22. }

关键:注意我们又使用了虚拟头结点的技巧,也是为了防止出现空指针的情况,比如说链表总共有 5 个节点,题目就让你删除倒数第 5 个节点,也就是第一个节点,那按照算法逻辑,应该首先找到倒数第 6 个节点。但第一个节点前面已经没有节点了,这就会出错。

但有了我们虚拟节点 dummy 的存在,就避免了这个问题,能够对这种情况进行正确的删除

5.单链表的中点

力扣第 876 题「 链表的中间结点」,问题的关键是我们无法直接得到单链表的长度 n常规方法也是先遍历链表计算 n,再遍历一次得到第 n / 2 个节点,也就是中间节点。这样做比较呆,下面看巧妙解法

使用「快慢指针」的技巧:

我们让两个指针 slowfast 分别指向链表头结点 head

每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点

上述思路的代码实现如下:

  1. class Solution {
  2. public ListNode middleNode(ListNode head) {
  3. //初始化 快指针 和 慢指针
  4. ListNode slow = head,fast = head;
  5. while (fast!=null&&fast.next!=null){// 快指针走到末尾时停止
  6. // 慢指针走一步,快指针走两步
  7. fast =fast.next.next;
  8. slow = slow.next;
  9. }
  10. //此时慢指针走到中点了
  11. return slow;
  12. }
  13. }

如果链表长度为偶数,也就是说中点有两个的时候,我们这个解法返回的节点是靠后的那个节点。

另外,这段代码稍加修改就可以直接用到判断链表成环的算法题上。

6.判断链表是否包含环

解决方案也是用快慢指针

每当慢指针 slow 前进一步,快指针 fast 就前进两步。

如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。

只需要把寻找链表中点的代码稍加修改就行了:

  1. boolean hasCycle(ListNode head) {
  2. // 快慢指针初始化指向 head
  3. ListNode slow = head, fast = head;
  4. // 快指针走到末尾时停止
  5. while (fast != null && fast.next != null) {
  6. // 慢指针走一步,快指针走两步
  7. slow = slow.next;
  8. fast = fast.next.next;
  9. // 快慢指针相遇,说明含有环
  10. if (slow == fast) {
  11. return true;
  12. }
  13. }
  14. // 不包含环
  15. return false;
  16. }

这个问题还有进阶版:如果链表中含有环,如何计算这个环的起点

解法代码:具体查看 双指针技巧秒杀七道链表题目 :: labuladong的算法小抄

7.两个链表是否相交

力扣第 160 题「 相交链表」

  1. public class Solution {
  2. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  3. ListNode p1 = headA,p2 = headB;
  4. while (p1!=p2){
  5. // p1 走一步,如果走到 A 链表末尾,转到 B 链表
  6. if (p1 == null) p1 = headB;
  7. else p1 = p1.next;
  8. // p2 走一步,如果走到 B 链表末尾,转到 A 链表
  9. if (p2 == null) p2 = headA;
  10. else p2 = p2.next;
  11. }
  12. return p1;
  13. }
  14. }

参考学习资料:双指针技巧秒杀七道链表题目 :: labuladong的算法小抄

发表评论

表情:
评论列表 (有 0 条评论,177人围观)

还没有评论,来说两句吧...

相关阅读

    相关 [算法笔记]哈希(1)

    学习算法,还有一些知识的时候,有时候看书后以为自己懂了,结果做题就发现自己没什么思路,为此,博主决定坚持刷题,这里给大家推荐一个适合大家做题复习,准备面试的网站点此进入,...

    相关 -DAY7

    题目一 双向链表节点结构和二叉树节点结构是一样的,如果你把last认为是left,next认为是right的话。给定一个搜索二叉树的头节点head,请转化成一条有序的双向