LeetCode 457 - 环形数组是否存在循环


文章目录

摘要

LeetCode 457 这道题,表面看是"环形数组 + 循环判断",但真正的坑点在于:

不是所有循环都算数,必须方向一致 ,而且长度要大于 1

如果你一上来就把它当成普通的"链表找环",十有八九会被各种边界条件搞崩。

这篇文章会从直觉出发,一步一步把思路拆清楚,最后用一个 O(n) 时间、O(1) 额外空间 的 Swift 解法,把这道题稳稳拿下。

描述

题目给了一个 不包含 0 的环形数组,每个位置的数字,代表你要从当前位置往前或者往后跳多少步。

  • 正数:往前跳
  • 负数:往后跳
  • 数组是环形的,越界就绕回来

然后它问你一件事:
数组里有没有一个"合法的循环"

但注意,这里的"循环"有非常严格的定义:

  1. 跳跃路径会形成一个闭环
  2. 环里的所有元素,方向必须一致(要么全正,要么全负)
  3. 环的长度必须大于 1(自己跳回自己不算)

一旦存在这样的循环,直接返回 true

题解答案

这道题的核心解法,其实是 快慢指针 + 方向约束 + 原地标记

整体思路可以概括成一句话:

把数组当成一张"每个点只有一条出边"的图,对每个起点用快慢指针找环,只要发现一个方向一致、长度大于 1 的环,就直接返回 true。

关键点有三个:

  • 环形下标计算要处理负数
  • 快慢指针走的过程中,方向一旦不一致,立刻终止
  • 已经确认"不可能成环"的路径,可以原地标记,避免重复遍历

题解代码分析

核心代码(Swift 可运行)

swift 复制代码
class Solution {
    func circularArrayLoop(_ nums: [Int]) -> Bool {
        let n = nums.count
        var nums = nums  // 拷贝一份,用于原地标记

        func nextIndex(_ index: Int) -> Int {
            let next = (index + nums[index]) % n
            return next >= 0 ? next : next + n
        }

        for i in 0..<n {
            if nums[i] == 0 { continue }

            var slow = i
            var fast = i
            let direction = nums[i] > 0

            while true {
                let nextSlow = nextIndex(slow)
                let nextFast = nextIndex(fast)
                let nextFast2 = nextIndex(nextFast)

                // 方向不一致,直接退出
                if (nums[nextSlow] > 0) != direction ||
                   (nums[nextFast] > 0) != direction ||
                   (nums[nextFast2] > 0) != direction {
                    break
                }

                slow = nextSlow
                fast = nextFast2

                if slow == fast {
                    // 长度为 1 的自循环不算
                    if slow == nextIndex(slow) {
                        break
                    }
                    return true
                }
            }

            // 当前路径无法成环,全部标记为 0
            var index = i
            while nums[index] != 0 && (nums[index] > 0) == direction {
                let next = nextIndex(index)
                nums[index] = 0
                index = next
            }
        }

        return false
    }
}

关键逻辑拆解

1. 为什么用快慢指针

这道题本质上就是在一个"函数图"里找环:

每个下标只会跳到一个固定的下标。

这种结构,用快慢指针找环是最省空间、也最稳定的做法。

慢指针一次走一步

快指针一次走两步

如果存在合法环,一定会相遇。

2. 为什么要强制方向一致

题目明确规定:

  • 一个循环里,所有跳跃方向必须相同

所以在每一步移动前,都要检查:

swift 复制代码
(nums[nextIndex] > 0) == direction

一旦中途方向反了,说明这条路径 不可能形成合法循环,直接放弃。

现实里可以理解成:

你在一条"单行道"上找闭环,一旦发现有人逆行,这条路线就废了。

3. 为什么长度为 1 的环不算

如果一个元素跳完刚好回到自己,比如:

text 复制代码
index = 3
nums[3] = n

这在数学上是环,但题目明确说了:
k > 1

所以我们要额外判断:

swift 复制代码
if slow == nextIndex(slow) {
    break
}
4. 原地标记为什么重要

数组最大长度是 5000,如果每个位置都暴力跑一次快慢指针,最坏情况会超时。

解决办法就是:
一旦确认从某个起点出发不可能成环,就把整条路径标记成 0

这样后续再遇到这些点,直接跳过。

这是这道题能稳定跑到 O(n) 的关键。

示例测试及结果

示例 1

swift 复制代码
let nums = [2, -1, 1, 2, 2]
print(Solution().circularArrayLoop(nums))

输出:

text 复制代码
true

解释:

0 → 2 → 3 → 0,方向全是正,长度大于 1,合法。

示例 2

swift 复制代码
let nums = [-1, -2, -3, -4, -5, 6]
print(Solution().circularArrayLoop(nums))

输出:

text 复制代码
false

解释:

唯一的循环长度是 1,不符合要求。

示例 3

swift 复制代码
let nums = [1, -1, 5, 1, 4]
print(Solution().circularArrayLoop(nums))

输出:

text 复制代码
true

解释:

3 → 4 → 3,方向一致,长度为 2,是合法循环。

时间复杂度

O(n)

  • 每个元素最多被访问和标记一次
  • 快慢指针整体也是线性复杂度

空间复杂度

O(1)

  • 只使用了常量级指针
  • 原地修改数组,没有额外数据结构

总结

LeetCode 457 是一道非常典型的"看起来像找环,实际全是细节"的题。

真正的难点不在算法本身,而在于:

  • 环形下标处理
  • 方向一致性约束
  • 排除长度为 1 的假循环
  • 如何避免重复遍历

如果你在实际项目中做过流程引擎、状态流转校验、或者检测"用户操作是否陷入死循环",你会发现这道题的思路其实非常实用。。

相关推荐
白帽黑客-晨哥2 小时前
Web安全方向的面试通常会重点考察哪些漏洞和防御方案?
安全·web安全·面试·职场和发展·渗透测试
2401_877274242 小时前
2025数据结构实验八:排序
数据结构·算法·排序算法
J2虾虾3 小时前
空间矢量数据结构及其表达
算法
Neil今天也要学习3 小时前
永磁同步电机无速度算法--永磁同步电机转子位置精确估计的误差抑制方法
算法
Irene19913 小时前
JavaScript 常见算法复杂度总结(大O表示法)
javascript·算法
开心比对错重要3 小时前
进程、线程、虚拟线程详解及线程个数设置
java·jvm·算法·面试
爱学大树锯3 小时前
【594 · 字符串查找 II】
java·开发语言·算法
m0_692457103 小时前
图像噪点消除
人工智能·算法
2401_841495643 小时前
【Python高级编程】图着色动态可视化 APP
python·算法·matplotlib·tkinter·回溯法·图着色算法·动态可视化工具