解决数组循环问题

719 阅读4分钟

上回我提到凡是链表有环问题或者数组有环问题都可以用快慢指针算法来解决。还不了解快慢指针的同学可以先看这里:玩转快慢指针。快慢指针在链表中常用到什么地步呢,到了求链表中点都能用快慢指针得解的地步,让快慢指针同时从起点出发,快指针到达终点得时候,慢指针就在链表得中间位置(因为快指针速度是慢指针得两倍)。但是我在上文中没有提到的是数组有环是个什么意思,今天正好面试面到了,靠着我整理的这些解题规律我也顺利地拿到了offer,我们就一起来看一下这个问题。

题目来自LeetCode第457题: 给定一个含有正整数和负整数的环形数组 nums。 如果某个索引中的数 k 为正数,则向前移动 k 个索引。相反,如果是负数 (-k),则向后移动 k 个索引。因为数组是环形的,所以可以假设最后一个元素的下一个元素是第一个元素,而第一个元素的前一个元素是最后一个元素。 确定 nums 中是否存在循环(或周期)。循环必须在相同的索引处开始和结束并且循环长度 > 1。此外,一个循环中的所有运动都必须沿着同一方向进行。换句话说,一个循环中不能同时包括向前的运动和向后的运动。

题目描述得很详细,只要从数组得某个元素开始,一直在某几个元素之间重复移动,那就称这个数组有环。我们来看两个例子,对于[1, 2, -1, 2, 2]这个数组来说它就是有环的,因为它一直在索引0 -> 1 -> 3 -> 0中循环,而对于数组[2, 1, -1, -2]来说,它却是没环的。

那这道题我们该怎么解?既然也是找环问题,我们也用快慢指针来尝试一下。我们从数组的每个索引开始,去判断是否有环。如果某个元素无环,那我们就该对它的下一个元素进行检测。这个过程中我们有两个注意点:

  1. 题目中提到,环的循环长度必须>1,也就是说当我们把一个指针向前移动的时候,如果它还指向同一个元素,那我们就是有了一个循环长度为1的环。我们就可以结束对当前元素的寻找环的操作。
  2. 另外一个需要注意的是这个环不能包含两个方向的运动,这个很好办,我们可以记住之前的方向。如果我们遇到的是正数,那我们就是要向正方向移动,遇到的是负数,就往反方向移动,这样我们碰到方向变化的时候,我们就可以结束对当前元素寻找环的操作了。

那我们的解题思路大致就出来了,除了判断这上面两种情况,就是用快慢指针来寻找环了。

我们先把代码整理出来:

public static boolean loopExists(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            boolean isForward = arr[i] >= 0; // 判断方向
            int slow = i, fast = i;

            // 如果快慢指针变成了-1,那我们就无法在当前这个数上找到环
            do {
                slow = findNextIndex(arr, isForward, slow); // 慢指针移动一步
                fast = findNextIndex(arr, isForward, fast); // 快指针移动一步
                if (fast != -1)
                    fast = findNextIndex(arr, isForward, fast); // 快指针再走一步
            } while (slow != -1 && fast != -1 && slow != fast);

            if (slow != -1 && slow == fast)
                return true;
        }

        return false;
    }

    private static int findNextIndex(int[] arr, boolean isForward, int currentIndex) {
        boolean direction = arr[currentIndex] >= 0;
        if (isForward != direction)
            return -1; // 方向改变,返回-1

        int nextIndex = (currentIndex + arr[currentIndex]) % arr.length;
        if (nextIndex < 0)
            nextIndex += arr.length; //确保索引是正数

        // 循环长度是1,返回-1
        if (nextIndex == currentIndex)
            nextIndex = -1;

        return nextIndex;
    }

这样做时间复杂度是O(N^2),空间复杂度为O(1),还不错。

我们也可以发现,我们为了给某一元素i寻找环的时候,移动到下一个元素i+1,那我们就会开始判断i+1上的元素能不能找到环,这跟直接从i+1开始,结果是一样的,所以我们可以用一个数组来标记我们已经判断过有没有环的数字,这会使得我们的时间复杂度降到O(N),但是相应地,空间复杂度上升到O(N),这也是算法中一个常见的概念,所谓的空间换时间。

通过这道题我们可以发现,确实所有判断有环的问题,最后都可以应用快慢指针。这种类型的问题应该再也不能难倒大家了。(管它链表还是数组,看到有环无脑套快慢指针!)

关注我,跟我一起讨论吧