

文章目录
摘要
LeetCode 457 这道题,表面看是"环形数组 + 循环判断",但真正的坑点在于:
不是所有循环都算数,必须方向一致 ,而且长度要大于 1 。
如果你一上来就把它当成普通的"链表找环",十有八九会被各种边界条件搞崩。
这篇文章会从直觉出发,一步一步把思路拆清楚,最后用一个 O(n) 时间、O(1) 额外空间 的 Swift 解法,把这道题稳稳拿下。

描述
题目给了一个 不包含 0 的环形数组,每个位置的数字,代表你要从当前位置往前或者往后跳多少步。
- 正数:往前跳
- 负数:往后跳
- 数组是环形的,越界就绕回来
然后它问你一件事:
数组里有没有一个"合法的循环"。
但注意,这里的"循环"有非常严格的定义:
- 跳跃路径会形成一个闭环
- 环里的所有元素,方向必须一致(要么全正,要么全负)
- 环的长度必须大于 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 的假循环
- 如何避免重复遍历
如果你在实际项目中做过流程引擎、状态流转校验、或者检测"用户操作是否陷入死循环",你会发现这道题的思路其实非常实用。。