双指针法

在数组的时候使用过双指针法,在对链表进行操作的时候,双指针法依旧可以使用在对链表遍历的操作。

反转链表

题目描述:

力扣链接🔗

双指针法 - 图1

题目分析:

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

双指针法 - 图2

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

步骤:

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

代码:

解法一:

  1. /**
  2. * 双指针法
  3. * @param head
  4. * @return
  5. */
  6. public ListNode reverseList(ListNode head) {
  7. ListNode pre = null;
  8. ListNode cur = head;
  9. ListNode tmp = null; // 临时节点保存cur下一个节点,防止指针指向反转后找不到下一个节点
  10. while (cur != null) {
  11. tmp = cur.next;
  12. cur.next = pre;
  13. // 向后移动
  14. pre = cur;
  15. cur = tmp;
  16. }
  17. return pre;
  18. }

解法二:递归法(详细见递归)

两两交换链表中的节点

题目描述:

力扣链接🔗

双指针法 - 图3

题目分析:

建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。

双指针法 - 图4

代码:

解法一:虚拟头结点 + 双指针法

  1. /**
  2. * 虚拟头节点法
  3. * @param head
  4. * @return
  5. */
  6. public ListNode swapPairs(ListNode head) {
  7. // 虚拟头节点
  8. ListNode dummyNode = new ListNode(0);
  9. dummyNode.next = head;
  10. ListNode pre = dummyNode;
  11. ListNode tmp = null; // 临时节点
  12. // 注意判断只有一个节点的情况,所以head.next也需要判断
  13. // 也可以使用pre.next != null && pre.next.next != null判断
  14. while (head != null && head.next != null) {
  15. tmp = head.next.next;
  16. pre.next = head.next;
  17. head.next.next = head;
  18. head.next = tmp;
  19. // 向后移动
  20. pre = head;
  21. head = tmp;
  22. }
  23. return dummyNode.next;
  24. }

解法二:递归法(详细见递归)

链表相交

题目描述:

力扣链接🔗
image.png

题目分析:

简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等。
为了方便举例,假设节点元素数值相等,则节点指针相等。
看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:
image.png
我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:
image.png
此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。

代码:

解法一:

  1. public class Solution {
  2. /**
  3. * 优化方法
  4. *
  5. * @param headA
  6. * @param headB
  7. * @return
  8. */
  9. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  10. ListNode curA = headA;
  11. ListNode curB = headB;
  12. int lenA = 0, lenB = 0;
  13. // 分別计算两个链表的长度
  14. while (curA != null) {
  15. curA = curA.next;
  16. lenA++;
  17. }
  18. while (curB != null) {
  19. curB = curB.next;
  20. lenB++;
  21. }
  22. curA = headA;
  23. curB = headB;
  24. // 让curA为最长链表的头
  25. if (lenA < lenB) {
  26. int tmpLen = lenA;
  27. lenA = lenB;
  28. lenB = tmpLen;
  29. // 交换指针
  30. ListNode tmpNode = curA;
  31. curA = curB;
  32. curB = tmpNode;
  33. }
  34. // 将长的链表移动到和短的链表尾部平齐
  35. int count = lenA - lenB;
  36. while (count-- > 0) {
  37. curA = curA.next;
  38. }
  39. // 进行比较,相等则返回,此时长度相同
  40. while (curA != null) {
  41. if (curA == curB) {
  42. return curA;
  43. }
  44. curA = curA.next;
  45. curB = curB.next;
  46. }
  47. return null;
  48. }
  49. }

解法二:

  1. public class Solution {
  2. /**
  3. * 循环遍历(时间复杂度为O(n的平方))
  4. * @param headA
  5. * @param headB
  6. * @return
  7. */
  8. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  9. if (headA == null || headB == null) {
  10. return null;
  11. }
  12. ListNode curA = headA;
  13. ListNode curB = headB;
  14. // 循环判断
  15. while (curA != null) {
  16. while (curB != null) {
  17. if (curA == curB)
  18. return curA;
  19. curB = curB.next;
  20. }
  21. curA = curA.next;
  22. curB = headB;
  23. }
  24. return null;
  25. }
  26. }

解法三:

  1. // 双指针法
  2. public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
  3. if (headA == null || headB ==null) return null;
  4. ListNode nodeA = headA, nodeB = headB;
  5. while (nodeA != nodeB) {
  6. // 移动到尾部之后,在移动到另一个节点的头节点点
  7. nodeA = nodeA == null ? headB : nodeA.next;
  8. nodeB = nodeB == null ? headA : nodeB.next;
  9. }
  10. return nodeA;
  11. }

环形链表Ⅱ

题目描述:

力扣链接🔗

题目解析:

详细解析🔗

代码:

解法一:在查重的情况下可以考虑hash表。

  1. public class Solution {
  2. /**
  3. * 哈希表
  4. *
  5. * @param head
  6. * @return
  7. */
  8. public ListNode detectCycle(ListNode head) {
  9. HashSet<ListNode> set = new HashSet<ListNode>();
  10. ListNode cur = head;
  11. while (cur != null) {
  12. if (set.contains(cur)) {
  13. return cur;
  14. }
  15. set.add(cur);
  16. cur = cur.next;
  17. }
  18. return null;
  19. }
  20. }

解法二:

  1. public class Solution {
  2. /**
  3. * 快慢指针法
  4. *
  5. * @param head
  6. * @return
  7. */
  8. public ListNode detectCycle(ListNode head) {
  9. ListNode fast = head;
  10. ListNode slow = head;
  11. // 判断为null或者一个节点时
  12. while (fast != null && fast.next != null) {
  13. // fast移动两步,slow移动一步
  14. slow = slow.next;
  15. fast = fast.next.next;
  16. // 在环中相交时
  17. if (slow == fast) {
  18. ListNode index1 = slow;
  19. ListNode index2 = head;
  20. while (index1 != index2) {
  21. index1 = index1.next;
  22. index2 = index2.next;
  23. }
  24. return index1;
  25. }
  26. }
  27. return null;
  28. }
  29. }

删除链表的倒数第N个节点

题目描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
image.png
输入:head = [1,2,3,4,5], n = 2输出:[1,2,3,5]示例 2:
输入:head = [1], n = 1输出:[]示例 3:
输入:head = [1,2], n = 1输出:[1]

思路

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
思路是这样的,但要注意一些细节。
分为如下几步:

  • 首先这里我推荐大家使用虚拟头结点,这样方面处理删除实际头结点的逻辑。
  • 定义fast指针和slow指针,初始值为虚拟头结点,如图:

image.png

  • fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:image.png
  • fast和slow同时移动,直到fast指向末尾,如题:image.png
  • 删除slow指向的下一个节点,如图:image.png

    代码:

    1. class Solution {
    2. public ListNode removeNthFromEnd(ListNode head, int n) {
    3. ListNode dummy = new ListNode(-1);
    4. dummy.next = head;
    5. ListNode slow = dummy;
    6. ListNode fast = dummy;
    7. while (n-- > 0) {
    8. fast = fast.next;
    9. }
    10. // 记住 待删除节点slow 的上一节点
    11. ListNode prev = null;
    12. while (fast != null) {
    13. prev = slow;
    14. slow = slow.next;
    15. fast = fast.next;
    16. }
    17. // 上一节点的next指针绕过 待删除节点slow 直接指向slow的下一节点
    18. prev.next = slow.next;
    19. // 释放 待删除节点slow 的next指针, 这句删掉也能AC
    20. slow.next = null;
    21. return dummy.next;
    22. }
    23. }