双指针

21:删除倒数第k个节点

:::info 题目:如果给定一个链表,请问如何删除链表中的倒数第k个节点?假设链表中节点的总数为n,那么1≤k≤n。要求只能遍历链表一次。例如,输入图4.1(a)中的链表,删除倒数第2个节点之后的链表如图4.1(b)所示。 :::

  1. public class Node {
  2. public Node next;
  3. public int val;
  4. public Node(int val) {
  5. this.val = val;
  6. }
  7. }
  8. public class DeleteKNodeFromEnd() {
  9. public void deleteKNodeFromEnd(Node head, int k) {
  10. // 哨兵节点
  11. Node dummy = new Node(0);
  12. dummy.next = head;
  13. // 定义前后指针
  14. Node front = dummy;
  15. Node back = dummy;
  16. // 先走前指针k步
  17. for(int i = 0; i < k; i++) {
  18. front = frone.next;
  19. }
  20. // 再同时走前后指针,直到后指针走到最后一个节点
  21. while(front.next != null) {
  22. front = front.next;
  23. back = back.next;
  24. }
  25. // 删除back的下一个节点
  26. back.next = back.next.next;
  27. }
  28. }

22:链表中环的入口节点

:::info 题目:如果一个链表中包含环,那么应该如何找出环的入口节点?从链表的头节点开始顺着next指针方向进入环的第1个节点为环的入口节点。例如,在如图4.3所示的链表中,环的入口节点是节点3。 :::

  1. /*
  2. * Ant Group
  3. * Copyright (c) 2004-2024 All Rights Reserved.
  4. */
  5. package algorithm.list;
  6. /**
  7. * @author jiaming
  8. * @version DetectCycle22.java, v 0.1 2024年02月07日 11:44 jiaming
  9. */
  10. public class DetectCycle22 {
  11. // 1. 先写一个辅助函数去找到链表中的环
  12. // 如果有环,则返回环中的一个node,否则返回null
  13. private static Node findLoopNode(Node head) {
  14. if(head == null) {
  15. return null;
  16. }
  17. Node fast = head;
  18. Node slow = head;
  19. int countSlowStep = 0;
  20. while(fast != null && fast.next != null) {
  21. // 移动快慢指针
  22. fast = fast.next.next;
  23. slow = slow.next;
  24. countSlowStep++;
  25. // 如果有环,快慢指针终究会相遇(本质上是套圈了)
  26. if (fast == slow) {
  27. return fast;
  28. }
  29. }
  30. // 如果最终退出循环了,且快慢指针没有相遇,那就是没有环,返回null
  31. return null;
  32. }
  33. // 2. 找到环之后,可以确定环的长度k
  34. // 3. 再利用快慢指针,快指针先走环的长度k;然后快慢指针同时开始走,直到两者相遇,就是入口节点
  35. public static Node detectCycle(Node head) {
  36. // 找到环中的一个节点
  37. Node loopNode = findLoopNode(head);
  38. // 如果没有环直接退出
  39. if (loopNode == null) {
  40. return null;
  41. }
  42. // 计算环的长度
  43. Node p = loopNode.next;
  44. int loopLength = 1;
  45. while(p != loopNode) {
  46. loopLength++;
  47. p = p.next;
  48. }
  49. // 快慢指针找环的入口
  50. Node fast = head;
  51. Node slow = head;
  52. // 快指针先走loopLength
  53. // 这里解释一下,我们的目标是希望快慢指针在环的入口处相遇,那就是希望慢指针走到环的入口时,快指针刚好已经走完一圈了,那么两者就会相遇
  54. // 这样的情况下,快指针要比慢指针先多走一圈的长度
  55. // 其实这里的trick在于,环以外的长度两个指针都是要走的,所以步数差值的0,所以可以不考虑,甚至我们可以假设head就是环的起点
  56. for (int i = 0; i < loopLength; i++) {
  57. fast = fast.next;
  58. }
  59. // 快慢指针同时走,直到相遇,
  60. while(fast != slow) {
  61. fast = fast.next;
  62. slow = slow.next;
  63. }
  64. return fast;
  65. }
  66. public static Node detectCycleWithOutLength(Node head) {
  67. // loopNode为两个快慢指针相遇的节点
  68. // 此时,慢指针走了k步,快指针走了2k步,两者相差了k步
  69. // 这 k 步一定是环的长度的整数倍(本质是套圈的逻辑)
  70. // PS:其实不用考虑环之外的那一截,因为快慢指针都走过,所以不在差值之内
  71. // 所以2k也是环的长度的整数倍
  72. Node loopNode = findLoopNode(head);
  73. if (loopNode == null) {
  74. return null;
  75. }
  76. // 那接下来的思路就是再搞俩快慢指针
  77. // 快指针就在前面找到的环的一个节点上,慢指针从head开始
  78. // 此时的关键是,fast比slow多走的k步,k =慢指针走的步数(是环的周长的倍数)
  79. // 所以当slow走到入口的时候,fast一定也在入口节点
  80. Node fast = loopNode;
  81. Node slow = head;
  82. while (fast != slow) {
  83. fast = fast.next;
  84. slow = slow.next;
  85. }
  86. return fast;
  87. }
  88. public static void main(String[] args) {
  89. Node one = new Node(1);
  90. Node two = new Node(2);
  91. Node three = new Node(3);
  92. Node four = new Node(4);
  93. Node five = new Node(5);
  94. Node six = new Node(6);
  95. one.next = two;
  96. two.next = three;
  97. three.next = four;
  98. four.next = five;
  99. five.next = six;
  100. six.next = three;
  101. System.out.println("Node=" + detectCycle(one));
  102. System.out.println("Node=" + detectCycleWithOutLength(one));
  103. // six.next = one;
  104. // // System.out.println(detectCycle(one));
  105. // System.out.println("Node=" + detectCycleWithOutLength(one));
  106. // six.next = four;
  107. // // System.out.println(detectCycle(one));
  108. // System.out.println("Node=" + detectCycleWithOutLength(one));
  109. }
  110. }

23:两个链表的第1个重合节点‘

:::info 题目:输入两个单向链表,请问如何找出它们的第1个重合节点。例如,图4.5中的两个链表的第1个重合节点的值是4。 :::

image.png

  1. public class IntersectNode{
  2. public static Node getIntersectNode(Node headA, Node headB) {
  3. if(headA == null || headB == null) {
  4. return null;
  5. }
  6. int lengthA = getListLength(headA);
  7. int lengthB = getListLength(headB);
  8. Head long = lengthA > lengthB ? headA : headB;
  9. Head short = long == headA ? headB : headA;
  10. int diff = Math.abd(lengthA - lengthB);
  11. for(int i = 0; i < diff; i++) {
  12. long = long.next;
  13. }
  14. while(long != short) {
  15. long = long.next;
  16. short = short.next;
  17. }
  18. return short;
  19. }
  20. private static int getListLength(Node head) {
  21. Node p = head;
  22. int length = 0;
  23. while(p != null) {
  24. length++;
  25. p = p.next;
  26. }
  27. return length;
  28. }
  29. }

反转链表

24:反转链表

:::info 题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。例如,把图4.8(a)中的链表反转之后得到的链表如图4.8(b)所示。 :::

  1. public class ReverseList {
  2. public static Node reverseList(Node head) {
  3. Node pre = null;
  4. Node cur = head;
  5. while(cur != null) {
  6. // 保存下一个节点
  7. Node next = cur.next;
  8. // 反转当前节点
  9. cur.next = pre;
  10. // 更新cur 和 pre
  11. pre = cur;
  12. cur = next;
  13. }
  14. // cur 是 null, pre是最后一个节点
  15. return pre;
  16. }
  17. }

25:链表中的数字相加

:::info 题目:给定两个表示非负整数的单向链表,请问如何实现这两个整数的相加并且把它们的和仍然用单向链表表示?链表中的每个节点表示整数十进制的一位,并且头节点对应整数的最高位数而尾节点对应整数的个位数。例如,在图4.10(a)和图4.10(b)中,两个链表分别表示整数123和531,它们的和为654,对应的链表如图4.10(c)所示。 :::

  1. public class AddTwoNumbers {
  2. public static Node addTwoNumbers(Node headA, Node headB) {
  3. // 反转两个链表
  4. Node x = reverseList(headA);
  5. Node y = reverseList(headB);
  6. // 记录新链表的
  7. Node dummy = new Node(0);
  8. Node cur = dummy;
  9. // 从个位数相加
  10. int carry = 0;
  11. while(x!=null || y!=null) {
  12. Node node = new Node;
  13. int sum = 0;
  14. if(x != null) {
  15. sum += x.val;
  16. }
  17. if(y != null) {
  18. sum += y.val;
  19. }
  20. sum += carry;
  21. // 计算新的carry
  22. carry = sum / 10;
  23. // 计算当前位的值
  24. node.val = sum%10;
  25. cur.next = node;
  26. cur = node;
  27. }
  28. if(carry != 0) {
  29. cur.next = new Node(carry);
  30. }
  31. Node newHead = dummy.next;
  32. return reverseList(newHead);
  33. }
  34. }

26:重排链表

:::info 问题:给定一个链表,链表中节点的顺序是L0→L1→L2→…→Ln-1→Ln,请问如何重排链表使节点的顺序变成L0→Ln→L1→Ln-1→L2→Ln-2→…?例如,输入图4.12(a)中的链表,重排之后的链表如图4.12(b)所示。 :::

  1. public class reorderList {
  2. private static Node reorderList(Node head) {
  3. if(head == null || head.next == null) {
  4. return head;
  5. }
  6. // 找到链表中间节点;
  7. Node middle = findMiddleNode(head);
  8. // 将链表分成前后两半,并反转后一半的链表;
  9. Node secondHalf = middle.next;
  10. middle.next = null;
  11. secondHalf = reverseList(secondHalf);
  12. // 拼接链表
  13. Node x = head;
  14. Node y = secondHalf;
  15. while(x != null && y != null) {
  16. Node xNext = x.next;
  17. Node yNext = y.next;
  18. x.next = y;
  19. y.next = xNext;
  20. x = xNext;
  21. y = yNext;
  22. }
  23. return head;
  24. }
  25. private static Node reverseList(Node head) {
  26. if (head == null) {
  27. return null;
  28. }
  29. Node pre = null;
  30. Node cur = head;
  31. while(cur != null) {
  32. // 保存下一个节点
  33. Node next = cur.next;
  34. // 反转链表
  35. cur.next = pre;
  36. // 更新指针
  37. pre = cur;
  38. cur = next;
  39. }
  40. return pre;
  41. }
  42. private static Node findMiddleNode(Node head) {
  43. if(head == null) {
  44. return null;
  45. }
  46. Node dummy = new Node(0);
  47. dummy.next = head;
  48. Node fast = dummy;
  49. Node slow = dummy;
  50. while(fast != null && fast.next !=null) {
  51. fast = fast.next.next;
  52. slow = slow.next;
  53. }
  54. System.out.println(slow);
  55. return slow;
  56. }
  57. public static void main(String[] args) {
  58. int[] nums = {1, 2, 3, 4, 5, 6};
  59. Node head = AlgorithmUtils.constructNode(nums);
  60. AlgorithmUtils.printListNode(reorderList(head));
  61. }
  62. }

27:回文链表

:::info 问题:如何判断一个链表是不是回文?要求解法的时间复杂度是O(n),并且不得使用超过O(1)的辅助空间。如果一个链表是回文,那么链表的节点序列从前往后看和从后往前看是相同的。例如,图4.13中的链表的节点序列从前往后看和从后往前看都是1、2、3、3、2、1,因此这是一个回文链表。 :::

  1. public class palindromeList(Node head) {
  2. public static boolean checkPalindromeList(Node head) {
  3. if (head == null || head.next == null) {
  4. return true;
  5. }
  6. // 将链表分成前后两半
  7. Node slow = head;
  8. Node fast = head.next;
  9. // 这个循环条件,能让前一半比较短
  10. while (fast.next != null && fast.next.next != null) {
  11. fast = fast.next.next;
  12. slow = slow.next;
  13. }
  14. // 如果链表总程度是奇数 fast == null
  15. Node secondHalf;
  16. // 如果后一半比较长,说明链表长度是奇数,需要丢弃后一半的第一节点,也就是整个链表的最中间的节点
  17. if (fast.next != null) {
  18. // 把最中间的值丢弃
  19. secondHalf = slow.next.next;
  20. } else {
  21. secondHalf = slow.next;
  22. }
  23. slow.next = null;
  24. // 翻转前一半的链表
  25. head = reverseList(head);
  26. // 对比两条链表的节点是否一致
  27. Node x = head;
  28. Node y = secondHalf;
  29. while (x != null && y != null) {
  30. if (x.val != y.val) {
  31. return false;
  32. }
  33. x = x.next;
  34. y = y.next;
  35. }
  36. return x == null && y == null;
  37. }
  38. private Node findMiddleNode(Node head) {
  39. if (head == null || head.next == null) {
  40. return head;
  41. }
  42. Node dummy = new Node(0);
  43. dummy.next = head;
  44. Node fast = dummy;
  45. Node slow = dummy;
  46. while (fast != null && fast.next != null) {
  47. fast = fast.next.next;
  48. slow = slow.next;
  49. }
  50. return slow;
  51. }
  52. public static void main(String[] args) {
  53. int[] nums = {1, 2, 3, 3, 2, 1};
  54. Node head = AlgorithmUtils.constructNode(nums);
  55. System.out.println(checkPalindromeList(head));
  56. int[] nums2 = {1, 2, 3, 2, 1};
  57. Node head2 = AlgorithmUtils.constructNode(nums2);
  58. System.out.println(checkPalindromeList(head2));
  59. int[] nums3 = {1, 2, 3, 4, 2, 1};
  60. Node head3 = AlgorithmUtils.constructNode(nums3);
  61. System.out.println(checkPalindromeList(head3));
  62. }
  63. }

双向链表和循环链表

28:展平多级双向链表

:::info 问题:在一个多级双向链表中,节点除了有两个指针分别指向前后两个节点,还有一个指针指向它的子链表,并且子链表也是一个双向链表,它的节点也有指向子链表的指针。请将这样的多级双向链表展平成普通的双向链表,即所有节点都没有子链表。例如,图4.14(a)所示是一个多级双向链表,它展平之后如图4.14(b)所示。 :::

  1. /**
  2. * 定义带child的双联表节点
  3. */
  4. public class ChildNode {
  5. public ChildNode next;
  6. public ChildNode pre;
  7. public ChildNode child;
  8. public int val;
  9. public ChildNode(int val) {
  10. this.val = val;
  11. }
  12. public void link(ChildNode pre, ChildNode next, ChildNode child) {
  13. this.pre = pre;
  14. this.next = next;
  15. this.child = child;
  16. }
  17. @Override
  18. public String toString() {
  19. return String.valueOf(val);
  20. }
  21. }
  1. public Flatten {
  2. public static ChildNode flatten(ChildNode head) {
  3. flattenGetTail(head);
  4. return head;
  5. }
  6. /**
  7. * flatten子链表,并返回子链表的尾结点,用于接上当前节点的next
  8. */
  9. private static ChildNode flattenGetTail(ChildNode head) {
  10. if (head == null) {
  11. return null;
  12. }
  13. ChildNode node = head;
  14. ChildNode tail = null;
  15. while (node != null) {
  16. ChildNode next = node.next;
  17. // 找子节点的tail
  18. if (node.child != null) {
  19. ChildNode child = node.child;
  20. // 递归获取子链表的尾结点
  21. ChildNode childTail = flattenGetTail(child);
  22. // 处理当前节点
  23. node.next = node.child;
  24. node.child = null;
  25. // 处理child节点
  26. child.pre = node;
  27. // 处理子链表的tail节点
  28. childTail.next = next;
  29. // 处理next节点
  30. if (next != null) {
  31. next.pre = childTail;
  32. }
  33. // 返回子链表的tail
  34. tail = childTail;
  35. } else {
  36. // 如果没有子链表,tail就是自己
  37. tail = node;
  38. }
  39. // 继续处理下一个节点
  40. node = next;
  41. }
  42. return tail;
  43. }
  44. public static void main(String[] args) {
  45. ChildNode one = new ChildNode(1);
  46. ChildNode two = new ChildNode(2);
  47. ChildNode three = new ChildNode(3);
  48. ChildNode four = new ChildNode(4);
  49. ChildNode five = new ChildNode(5);
  50. ChildNode six = new ChildNode(6);
  51. ChildNode seven = new ChildNode(7);
  52. ChildNode eight = new ChildNode(8);
  53. ChildNode nine = new ChildNode(9);
  54. one.link(null, two, null);
  55. two.link(one, three, five);
  56. three.link(two, four, null);
  57. four.link(three, null, null);
  58. five.link(null, six, null);
  59. six.link(five, seven, eight);
  60. seven.link(six, null, null);
  61. eight.link(null, nine, null);
  62. nine.link(eight, null, null);
  63. AlgorithmUtils.printListChildNode(flatten(one));
  64. }
  65. }

29:排序的循环链表

:::info 问题:在一个循环链表中节点的值递增排序,请设计一个算法在该循环链表中插入节点,并保证插入节点之后的循环链表仍然是排序的。例如,图4.15(a)所示是一个排序的循环链表,插入一个值为4的节点之后的链表如图4.15(b)所示。 :::

  1. public class insert {
  2. public static Node insert(Node head, Node node) {
  3. if (head == null) {
  4. head = node;
  5. head.next = head;
  6. } else if (head.next == null) {
  7. head.next = node;
  8. node.next = head;
  9. } else {
  10. insertCore(head, node);
  11. }
  12. return head;
  13. }
  14. private static void insertCore(Node head, Node node) {
  15. Node cur = head;
  16. Node biggest = head;
  17. // 循环的条件是前后节点的大小不符合,且还未走一圈回到head
  18. while (!(cur.val <= node.val && cur.next.val >= node.val) && cur.next != head) {
  19. cur = cur.next;
  20. if (cur.val > biggest.val) {
  21. biggest = cur;
  22. }
  23. }
  24. if (cur.val <= node.val && cur.next.val >= node.val) {
  25. Node next = cur.next;
  26. cur.next = node;
  27. node.next = next;
  28. } else {
  29. node.next = biggest.next;
  30. biggest.next = node;
  31. }
  32. }
  33. public static void main(String[] args) {
  34. // int[] nums = {1, 2, 3, 4, 5, 6};
  35. // Node head = AlgorithmUtils.constructNode(nums);
  36. //
  37. // Node tail = head;
  38. // while(tail.next != null) {
  39. // tail = tail.next;
  40. // }
  41. // tail.next = head;
  42. //
  43. // insert(head, new Node(3));
  44. // AlgorithmUtils.printListCycleNode(head);
  45. Node head = null;
  46. AlgorithmUtils.printListCycleNode(insert(head, new Node(3)));
  47. // Node head = new Node(4);
  48. // AlgorithmUtils.printListCycleNode(insert(head, new Node(3)));
  49. // int[] nums = {1, 2, 3, 4, 5, 6};
  50. // Node head = AlgorithmUtils.constructNode(nums);
  51. //
  52. // Node tail = head;
  53. // while(tail.next != null) {
  54. // tail = tail.next;
  55. // }
  56. // tail.next = head;
  57. //
  58. // insert(head, new Node(8));
  59. // AlgorithmUtils.printListCycleNode(head);
  60. }
  61. }