算法学习——链表之双指针技巧

349 阅读3分钟

这是我参与更文挑战的第17天,活动详情查看:更文挑战

  链表与数组一样,是线性数据结构,使用双指针技巧,可以巧妙解决很多问题。

反转链表

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

反转链表.png

  反转链表,可以通过定义一个新链表实现反转,但是这样造成极大浪费。我们知道,在链表中,每个节点有一个指针域指向后继节点。那么只需要改变指针的指向,把每个节点的指针指向其前驱节点(表头指针指向null),就可以完成反转链表。

  使用双指针技巧,定义两个指针,curpre。指针cur指向当前处理的节点,指针pre指向cur的前驱节点。初始化时,cur指向链表头结点,pre指向null。开始反转时,先使用临时指针保存cur的后继节点,再将cur的指针指向pre,改变cur的后继节点。最后移动curpre,当cur`为null时,完成反转。如下:

反转链表.gif

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode current = head;
        while (current != null) {
            ListNode next = current.next;
            current.next = pre;
            pre = current;
            current = next;
        }
        return pre;
    }
}

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

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

  • 输入:head = [1,2,3,4,5], n = 2
  • 输出:[1,2,3,5]

  双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;

        ListNode slow = dummy;
        ListNode fast = dummy;
        while (n-- > 0) {
            fast = fast.next;
        }
        // 记住 待删除节点slow 的上一节点
        ListNode prev = null;
        while (fast != null) {
            prev = slow;
            slow = slow.next;
            fast = fast.next;
        }
        // 上一节点的next指针绕过 待删除节点slow 直接指向slow的下一节点
        prev.next = slow.next;
        return dummy.next;
    }
}

环形链表

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

  可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

  • 为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢?
    • 这个其实可以类比两个速度不同的人跑步,如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。
  • 下一个问题:这两个指针的适当速度应该是多少?
    • 一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head == null || head.next == null){
            return false;
        }
        ListNode fast = head.next;
        ListNode slow = head;
        while (fast != slow && fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }

        return fast == slow;
    }
}