别说链表不重要(二):双指针巧解链表经典问题,环形链表、相交链表

双指针技巧与链表的经典问题

虚心接受批评和指正,互相成就,共勉!

从这篇文章你会收获什么?

  • 双指针技巧;
  • 双指针的常见用法;
  • 经典问题;
  • 总结

目录

  • 双指针技巧;
    • 环形链表
    • 环形链表II
    • 相交链表
    • 删除链表的倒数第N个节点
  • 注意

指针:c语言中指针变量是用来存放内存地址的变量

两种常用双指针技巧的情景

  1. 两个指针从不同位置出发:一个从起点开始,一个从终点开始;
  2. 两个指针以不同速度出发:一个指针快一些,一个指针慢一些;

在单链表中,不同位置出发毫无意义,但是不同速度出发,却可以让我们找到一些规律,这种技巧也叫快慢指针

话不多说,我们上LeetCode上链表的经典题去理解快慢指针的作用

环形链表

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

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。

思路

  • 一个有环的链表,比作一个环形跑道,跑的快的运动员迟早会再次和跑得慢的运动员相遇
  • 慢指针一次一定1位,快指针一次移动2位,快指针如果跑道了终点,证明无环,否则一定会相遇

代码

var hasCycle = function(head) {
    if(!head || !head.next) return false;
    var slow = head
    var fast = head.next
    while( slow != fast ){
        if(fast == null || fast.next == null){
            return false
        }
        slow = slow.next
        fast = fast.next.next
    }
    return true
};

环形链表II

思路

  • 第一阶段,我们先确定当前链表是否存在环;
  • 第二阶段,如果存在环,我们要确定入口的位置:
    • 入口前节点个数为a,环节点个数为b;
    • 快节点走过的节点数 = 2 * 慢节点走过的节点数,即 f = 2s;
    • 快节点走过的节点数 = s + nb(多揍了n倍的环)(相遇时);
    • 由此得出:s = nb(相遇时);
    • 如果说慢指针走到入口的距离 k = a + nb,两点相遇时s = nb,即再走a步即可;
    • 我们重新确定双指针(速度要相同),新指针走 a 步应该到达入口,慢指针走a步也会到达入口;
    • 新指针推断为 头节点,两者相遇的节点为入口;

代码

var hasCycle = function(head) {
    if(!head || !head.next) return false;
    var slow = head
    var fast = head.next
    while( slow != fast ){
        if(fast == null || fast.next == null){
            return false
        }
        slow = slow.next
        fast = fast.next.next
    }
    return true
};

相交链表

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。如果有环,则确定它的入口位置。

思路

  • 定义A、B节点对应headA、headB链表
  • 如果两个链表相交,那么他们的共同点就是相交节点及以后的节点相等;
  • 在未知长度的链表中,为了保证两个节点走相同的路程。
    • A节点走完后,走B节点,假设A链表交点前的节点数尾a
    • B节点走完后,走A节点,假设A链表交点前的节点数尾b
    • 如果存在交点:A和B在相遇前都走了 a+共同节点+b,此时A === B,并指向入口位置

代码

var getIntersectionNode = function(headA, headB) {
    var A = headA
    var B = headB
    while(A || B){
        if(A === B) return A
        A = A ? A.next : headB
        B = B ? B.next : headA
    }
    return null
};

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

思路

这里利用的思想是,不同位置,相同速度的双指针思想

  • 倒数N个节点,可以理解成正数第链表长度 - N节点。
  • 所以我们要找到头节点之后的第 链表长度 - N - 1个节点
  • 让它跳过下一节点即可
    • 让快节点先走N+1步
    • 之后,让慢节点从头节点与快节点一起移动,
    • 快节点走到尾端时,慢节点走了 链表长度 - N - 1
    • 做删除操作即可

代码

var removeNthFromEnd = function(head, n) {
    var current = { val: '', next: head }
    var fast = current
    var slow = current

    for (let i = 1; i <= n + 1; i++) {
        fast = fast.next;
    }
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return current.next;
}

注意

我们要注意一些事情:

  1. 在调用 next 字段之前,始终检查节点是否为空。

获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fast 和 fast.next 不为空。

  1. 仔细定义循环的结束条件。

运行几个示例,以确保你的结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。

复杂度分析

  • 空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析运行循环的次数。

  • 在前面的查找循环示例中

    • 假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。
    • 如果没有循环,快指针需要 N/2 次才能到达链表的末尾,其中 N 是链表的长度。
    • 如果存在循环,则快指针需要 M 次才能赶上慢指针,其中 M 是列表中循环的长度。
    • 显然,M <= N 。所以我们将循环运行 N 次。对于每次循环,我们只需要常量级的时间。
    • 因此,该算法的时间复杂度总共为 O(N)。

下一篇,我们来介绍一下双链表的原理,及Js的基本实现,共勉!

猜你喜欢

转载自blog.csdn.net/jbj6568839z/article/details/105945871