在 LeetCode 中等难度题目中,134. 加油站是一道经典的数组应用题,核心考察对“循环路径”和“油量平衡”的逻辑分析能力。题目看似复杂,但通过逐步拆解,能从暴力验证思路优化到贪心最优解,两种解法各有侧重,适合不同层次的理解需求。本文将详细讲解题目背景、两种解法的逻辑的细节,以及优化思路的推导过程。
一、题目分析
题目描述
一条环路上有 n 个加油站,第 i 个加油站有汽油 gas[i] 升。汽车油箱容量无限,从第 i 个加油站开往第 i+1 个加油站需消耗汽油 cost[i] 升,初始油箱为空。若能按顺序绕环路行驶一周,返回出发加油站编号;否则返回 -1。题目保证若存在解,则解唯一。
核心条件
-
环路特性:最后一个加油站的下一站是第一个加油站,需处理索引循环问题。
-
油量平衡:行驶过程中油箱油量不能为负,否则无法到达下一站。
-
解的唯一性:若存在有效起点,仅需找到这一个即可。
关键前提
若所有加油站的总油量 sum(gas) < 总消耗 sum(cost),则必然无法绕环一周,直接返回 -1;若 sum(gas) ≥ sum(cost),则必然存在唯一有效起点(题目保证解唯一)。这一前提是两种解法的共同基础,可快速排除无解场景。
二、解法一:候选起点暴力验证(易懂优先)
思路推导
既然存在解时唯一,且只有 sum(gas) ≥ sum(cost) 才有解,我们可以先筛选出“潜在有效起点”,再逐个验证是否能绕环一周。潜在起点的筛选逻辑的:从该加油站出发时,油量 gas[i] ≥ 消耗 cost[i],否则第一步就会油量不足,直接排除。
步骤拆解:
-
计算总油量和总消耗,若总消耗更大,直接返回 -1。
-
遍历所有加油站,筛选出 gas[i] ≥ cost[i] 的候选起点,存入列表。
-
对每个候选起点,模拟绕环过程:从起点出发,累计油箱油量,依次经过每个加油站,若中途油量为负则该起点无效,换下一个候选验证;若能绕环一周,则返回该起点。
代码实现
function canCompleteCircuit_1(gas: number[], cost: number[]): number {
const nodeL = gas.length;
let gasSum = 0;
let costSum = 0;
const mayIndex = [];
for (let i = 0; i < nodeL; i++) {
gasSum += gas[i];
costSum += cost[i];
// 筛选潜在有效起点:当前加油站油量≥消耗
if (gas[i] >= cost[i]) {
mayIndex.push(i);
}
}
// 总油量不足,直接无解
if (costSum > gasSum) return -1;
// 逐个验证候选起点
for (const start of mayIndex) {
let currentGas = 0; // 当前油箱油量
let currentIndex = start; // 当前所在加油站索引
let canComplete = true; // 是否能绕环
// 模拟绕环一周,共经过 nodeL 个加油站
for (let j = 0; j < nodeL; j++) {
currentGas += gas[currentIndex]; // 加当前加油站的油
currentGas -= cost[currentIndex]; // 减去前往下一站的消耗
// 油量不足,该起点无效
if (currentGas < 0) {
canComplete = false;
break;
}
// 移动到下一站,处理环路索引
currentIndex = (currentIndex + 1) % nodeL;
}
// 验证通过,返回起点
if (canComplete) {
return start;
}
}
// 理论上sum(gas)≥sum(cost)时必有解,此处为兜底
return -1;
};
复杂度分析
-
时间复杂度:O(n²)。最坏情况下,候选起点数量为 n,每个起点需遍历 n 个加油站验证,总操作数为 n²。
-
空间复杂度:O(n)。需存储候选起点列表,最坏情况下存储所有 n 个加油站索引。
优缺点
优点:逻辑直观,容易理解,适合新手入门,无需复杂算法思维,仅通过模拟就能得到结果。
缺点:效率较低,在 n 较大(如 10⁴ 级别)时会超时,仅适用于小规模数据。
三、解法二:贪心算法(最优解)
优化思路推导
暴力解法的核心问题是“重复验证无效起点”,我们可以通过贪心策略减少无效验证,将时间复杂度降至 O(n)。关键观察如下:
假设从起点 s 出发,行驶到第 i 个加油站时油量不足(currentGas < 0),则 s 到 i 之间的所有加油站都不能作为有效起点。原因:从 s 到 i-1 时油量均为非负,若从 s+1 出发,少了 s 站的油量补充,只会更早出现油量不足,同理 s 到 i 之间的所有站点都无需验证。
基于此,我们可以在一次遍历中完成“累计油量计算”和“起点更新”,无需候选列表。
算法逻辑
-
维护三个变量:totalGas(总油量差值 sum(gas[i]-cost[i]))、currentGas(当前油箱油量)、start(候选起点)。
-
遍历每个加油站,计算当前站点的油量差值 diff = gas[i] - cost[i],累计到 totalGas 和 currentGas。
-
若 currentGas < 0,说明从当前 start 到 i 之间的站点均无效,将 start 更新为 i+1,同时重置 currentGas 为 0(新起点从空油箱开始)。
-
遍历结束后,若 totalGas ≥ 0,返回 start(唯一有效起点);否则返回 -1。
代码实现
function canCompleteCircuit_2(gas: number[], cost: number[]): number {
const n = gas.length;
let totalGas = 0; // 总油量差值(替代sum(gas)-sum(cost))
let currentGas = 0; // 当前油箱油量
let start = 0; // 候选起点
for (let i = 0; i < n; i++) {
const diff = gas[i] - cost[i];
totalGas += diff;
currentGas += diff;
// 关键贪心逻辑:当前油量不足,更新起点为下一站
if (currentGas < 0) {
start = i + 1;
currentGas = 0;
}
}
// 总油量足够则返回起点,否则无解
return totalGas >= 0 ? start : -1;
};
复杂度分析
-
时间复杂度:O(n)。仅需遍历一次数组,每个元素操作一次,效率最优。
-
空间复杂度:O(1)。仅用三个变量存储状态,无额外空间消耗。
核心疑问解答
Q:为什么遍历结束后 start 就是唯一有效起点?
A:因为 totalGas ≥ 0 时必然存在解,且我们通过贪心策略跳过了所有无效起点(s 到 i 之间的站点),最终剩下的 start 是唯一可能的有效起点,无需额外验证。
Q:若 start 超过数组长度怎么办?
A:由于 totalGas ≥ 0 时必有解,遍历结束后 start 一定在 0~n-1 范围内(若 start = n,说明前 n 个站点均无效,但 totalGas ≥ 0 矛盾,故不可能出现)。
四、两种解法对比与实战建议
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 | 核心优势 |
|---|---|---|---|---|
| 暴力验证 | O(n²) | O(n) | 小规模数据、面试快速上手 | 逻辑直观,易于调试 |
| 贪心算法 | O(n) | O(1) | 大规模数据、算法优化场景 | 效率最优,空间消耗低 |
实战建议:面试时,若一时无法想到贪心策略,可先写出暴力解法,再基于“跳过无效起点”的思路推导贪心优化,体现逻辑递进能力;刷题时直接使用贪心算法,应对大规模测试用例更高效。
五、测试用例验证
通过以下测试用例验证两种解法的正确性,覆盖单节点、常规环路、无解场景:
// 测试用例1:单节点(油量等于消耗)
console.log(canCompleteCircuit_1([2], [2])); // 0
console.log(canCompleteCircuit_2([2], [2])); // 0
// 测试用例2:常规环路(有唯一解)
console.log(canCompleteCircuit_1([1,2,3,4,5], [3,4,5,1,2])); // 3
console.log(canCompleteCircuit_2([1,2,3,4,5], [3,4,5,1,2])); // 3
// 测试用例3:无解场景(总油量不足)
console.log(canCompleteCircuit_1([2], [3])); // -1
console.log(canCompleteCircuit_2([2], [3])); // -1
// 测试用例4:多个候选起点
console.log(canCompleteCircuit_1([3,1,1], [1,2,2])); // 0
console.log(canCompleteCircuit_2([3,1,1], [1,2,2])); // 0
六、总结
LeetCode 134. 加油站的核心是“油量平衡”与“无效起点排除”。暴力解法通过筛选候选起点+模拟绕环,降低了思维难度;贪心算法则通过关键观察跳过无效起点,实现了时间和空间的最优解。
解题关键在于理解“sum(gas) ≥ sum(cost) 是有解的必要条件”,以及贪心策略中“无效起点区间跳过”的合理性。掌握这两种解法,既能应对不同场景的需求,也能加深对数组循环问题和贪心思想的理解,为后续复杂算法题打下基础。