一、题目描述
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。如果存在解,则保证它是唯一的。
示例 1:
输入: gas = 1,2,3,4,5, cost = 3,4,5,1,2
输出: 3 解释:从 3 号加油站出发,可以走完一圈。
示例 2:
输入: gas = 2,3,4, cost = 3,4,3
输出: -1 解释:无论从哪个加油站出发,都无法走完一圈。
二、思路分析
核心前置结论:所有加油站总油量 ≥ 总消耗,才一定存在解;否则直接返回 - 1。
- 暴力思路:依次选取每个加油站作为起点,模拟绕环行驶,中途油量小于 0 说明起点不行,换下一个起点。
- 贪心跳跃优化:如果从起点 start 走到 i 位置时油量不足,说明
[start, i]区间内所有加油站都不能作为合法起点,起点直接跳到i+1,跳过无效候选。 - 前缀和最小值优化:把环形展开,计算差分前缀和,前缀和最小位置的下一站就是合法起点。
三、基础实现
cpp
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
// 依次尝试每一个加油站作为起点
for (int start = 0; start < n; ++start) {
int curOil = 0; // 当前油箱油量
bool ok = true;
// 从start出发,走n步绕一圈
for (int step = 0; step < n; ++step) {
int pos = (start + step) % n;
curOil += gas[pos] - cost[pos];
// 油量不够,无法到达下一站
if (curOil < 0) {
ok = false;
break;
}
}
if (ok) {
return start;
}
}
// 所有起点都失败
return -1;
}
};
时间复杂度:O(n^2),n 是加油站数量,每个起点最多遍历 n 个站点,数据量大的时候超时,仅用于理解题目逻辑。
空间复杂度:O(1),只用常数临时变量。
四、优化一:跳跃贪心
优化点 :暴力里很多起点是无效的。走到 i 没油了,start~i 之间任意点出发,走到 i 依然缺油,直接把起点设置为i+1,省去大量无效遍历。

cpp
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
int total = 0; // 全局总净油量,判断是否有解
int currOil = 0; // 当前起点累计净油量
int start = 0; // 候选起点
for (int i = 0; i < n; ++i) {
int diff = gas[i] - cost[i];
total += diff;
currOil += diff;
// 当前油量不足,[start, i]全部不能作为起点
if (currOil < 0) {
start = i + 1;
currOil = 0; // 重置油量,从新起点重新统计
}
}
// 总油量>=0才有解,返回记录的起点;否则无解
return total >= 0 ? start : -1;
}
};
时间复杂度:O(n),仅遍历数组一次。
空间复杂度:O(1),常数空间。
五、优化二:前缀和找最小值
核心思路:把环拆成线性,计算每个位置差分的前缀和。前缀和最小的位置,说明在这里最容易缺油,它下一个位置就是能保证全程油量不亏的起点。


cpp
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
int total = 0;
int preSum = 0;
int minPreSum = INT_MAX;
int minIdx = 0;
for (int i = 0; i < n; ++i) {
int diff = gas[i] - cost[i];
total += diff;
preSum += diff;
// 更新最小前缀和以及对应下标
if (preSum < minPreSum) {
minPreSum = preSum;
minIdx = i;
}
}
if (total < 0) return -1;
// 最小前缀位置的下一个点就是起点,取模防止越界
return (minIdx + 1) % n;
}
};
时间复杂度:O(n),一次遍历求前缀和。
空间复杂度:O(1)。
六、总结
加油站是贪心经典题,两种 O (n) 思路侧重点不一样:
- 跳跃贪心:逻辑直观,代码简短,面试优先写这个;核心是失败区间全部舍弃。
- 前缀和最小值:偏数学推导,适合被追问还有没有别的 O (n) 解法时讲。
同类推荐题目: LeetCode 53 最大子数组和、LeetCode 918 环形子数组的最大和