Leetcode环形链表系列题目

晚上刷了一道leetcode叫寻找重复数的题目,用链表成环的思路实现挺有意思的,梳理下思路顺便回顾下环形链表

1. 环形链表

题目链接

这道题就是给个链表让你判断这个链表有没有绕成环

1.1 思路一

最简单的方法是直接用Set,遍历链表并添加元素,如果链表存在环,那么入环的节点再次添加到Set容器中时会失败

public boolean hasCycle(ListNode head) {
    Set<ListNode> set = new HashSet<>();
    while (head != null) {
        if (!set.add(head)) return true;
        head = head.next;
    }
    return false;
}

但是这种方法效率堪忧

1.2 思路二

用快慢指针,如果链表有环的话,快慢指针遍历链表会在环中相遇,否则会直接结束遍历

public boolean hasCycle(ListNode head) {
    if (head == null) return false;
    ListNode fast = head, slow = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

这种方法效率高多了

这里有个问题要思考一下,为什么说链表有环的话一定会在环中相遇呢?
其实可以用数学归纳法证明下,假设某一时刻慢指针也进入到环中,这时快指针肯定也早在环中转圈圈了,快慢指针在环中相遇的过程其实可以理解为快指针追慢指针的过程,对是快指针追慢指针而不是慢指针追快指针,因为最终快慢指针相遇时,快指针一定是比慢指针多绕环走了几圈的,假设快指针落后慢指针N步,那么下一次时,快指针走两步,慢指针走一步,快指针落后慢指针N-1步,再下一次落后N-2步,直到落后1步,落后0步即快指针追上慢指针。

2. 环形链表II

题目链接

这题就是在第一题的基础上找到那个入环的节点

2.1 思路一

和第一题一样用Set,代码就不写了

2.2 思路二

public ListNode detectCycle(ListNode head) {
    if (head == null) return null;
    ListNode fast = head, slow = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            fast = head;
            while (slow != fast) {
                slow = slow.next;
                fast = fast.next;
            }
            return slow;
        }
    }
    return null;
}

同样还是快慢指针的思路,按照第一题的方法先找到快慢指针在环中相遇的节点,然后快指针移到头节点,接下来快指针和慢指针一样也是一步一步的走,快慢指针再次相遇的点就是入环的节点。

这又是什么道理呢?其实也很好解释
这里我们假设起点到入环节点的距离为x,入环节点到相遇节点的距离为z,整个环的距离为y
那么慢指针从起点到相遇节点一共走的步数是:x + z + m * y,m为慢指针绕环的圈数
由于快指针一次走两步,慢指针一次走一步,所以快指针从起点到相遇节点一共走的步数是:2 * (x + z + m * y)
而快指针追上慢指针的过程多饶了n圈,所以有
2 * (x + z + m * y) - (x + z + m * y) = n * y,即
x = (n - m) * y - z = (y - z) + (n - m - 1) * y, 即
x等于相遇节点到入环节点的距离加上几圈环的距离,所以快指针移到头节点后一步一走一定会和慢指针在入环节点再次相遇的。

3. 寻找重复数

题目链接

这题什么意思呢,就是说有个n + 1长度的数组,数组里面的元素都是1到n之间(包括1和n),所以一定会至少存在一个重复的元素,假设只有一个重复(可能不止重复出现一次)的整数,找出这个重复的数
题目要求:

  1. 不能更改数组
  2. 只能用O(1)的额外空间

这题我的第一感觉就是用bitmap,但是题目限制了O(1)的空间,所以不行。
这里可以借鉴第一题和第二题的快慢指针思路,关键就是要根据数组构建出一个链表出来,其实数组存储的信息无非就是某个位置的元素是几,那我们能不能根据下标与元素值的映射关系建立链表呢?
仔细想想,数组长度为n + 1,所以下标的范围为[0, n],数组元素的范围根据题意是[1, n],[1, n]在[0, n]的范围内,所以可以这样处理映射关系

假设数组没有重复的元素,以数组[1, 3, 4, 2]为例,我们建立数组下标n和数nums[n]的映射关系f(n),其映射关系n -> f(n)为:
0 -> 1
1 -> 3
2 -> 4
3 -> 2
我们从下标0出发,根据f(n)计算出一个值,以这个值作为新的下标,再用这个函数计算,以此类推,可以产生一个类似链表的结构
0 -> 1 -> 3 -> 2 -> 4 -> null

如果数组中有重复的数,以数组[1, 3, 4, 2, 2]为例,按照同样的方式建立的映射关系为:
0 -> 1
1 -> 3
2 -> 4
3 -> 2
4 -> 2
建立链表的过程是这样的(箭头中间的数字表示的是第几步)

对应的链表结构为:
0 -> 1 -> 3 -> 2 -> 4 -> 2 -> 4 -> 2 ......

可以看到2 -> 4绕成了环
从理论上讲,数组中如果有重复的数,那么就会产生多对一的映射(下标3和4都映射到数组的元素2),这样形成的链表就一定有环了,并且环的起点就是那个重复的元素

public int findDuplicate(int[] nums) {
    int fast = nums[nums[0]];
    int slow = nums[0];

    while (fast != slow) {
        slow = nums[slow];
        fast = nums[nums[fast]];
    }

    fast = 0;
    while (fast != slow) {
        slow = nums[slow];
        fast = nums[fast];
    }
    return slow;
}

最后上代码,需要注意的是上面讲的链表并不真实存在的,快指针从数组下标到数组元素再到数组下标,慢指针从数组下标到数组元素,只是快慢指针在数组下标和数组元素之间的移动过程其实和环形链表的思路是一样的
这题第一次接触还是不容易想出来的,有一定的tricks,或者说叫奇技淫巧。不过也是考验我们举一反三知识迁移的能力,如果提示我可以通过数组下标和数组元素建立类似环形链表的结构来处理这道题,不知道我能不能想出来呢?

posted @ 2020-06-05 00:17  sakura1027  阅读(126)  评论(0编辑  收藏  举报