134. 加油站
在一条环路上有 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 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
方法一:
// 解法1
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int sum = 0;
int min = 0;
for (int i = 0; i < gas.length; i++) {
sum += (gas[i] - cost[i]);
min = Math.min(sum, min);
}
if (sum < 0) return -1;
if (min >= 0) return 0;
for (int i = gas.length - 1; i > 0; i--) {
min += (gas[i] - cost[i]);
if (min >= 0) return i;
}
return -1;
}
}
这段Java代码是用于解决“环形赛道加油问题”的一种解法。给定两个长度相等的整数数组 gas
和 cost
,分别表示在环形赛道上每个地点的汽油量和从该地点出发到达下一个地点所需的汽油量。该函数 canCompleteCircuit
的目的是找到一个起始加油站,使得一辆恰好能完成环形赛道一圈的行程,即车能够在途中不耗尽汽油。如果可行,返回起始加油站的编号,否则返回 -1
。
代码解析:
-
计算总差值
sum
和最小差值min
:
首先,遍历数组一次,累加gas[i] - cost[i]
的值到sum
,同时维护这个差值序列中的最小值min
。这一步是为了判断整个环形赛道上是否有足够的汽油完成一圈。如果sum < 0
,说明总的汽油不足以覆盖总成本,直接返回-1
。 -
判断是否有解:
- 如果
min >= 0
,说明从任一位置出发都能完成环形赛道,因为最“不利”的点也能保持油量非负,故返回0
作为起始点(实际上任一起点均可)。 - 否则,需要找到第一个能够让累计汽油量由负转正的位置,即为起始加油站。
- 如果
-
寻找起始加油站:
从数组末尾向前遍历到第二个元素(因为第一个元素是否符合条件已在前面判断),累加gas[i] - cost[i]
到min
,一旦min
变为非负,即找到了能够作为起始加油站的位置,返回当前索引i
。 -
无解情况:
如果遍历结束都没有找到满足条件的起始位置,则返回-1
表示无法完成一圈行程。
注意点:
- 该解法在第二次遍历时其实没有必要,因为第一次遍历已经能够确定是否有解以及起始位置。正确的逻辑是在第一次遍历时记录下
min
对应的索引,如果sum >= 0
则直接返回该索引作为起始位置。如果我的描述有误,请允许我更正:根据原始代码逻辑,第二次遍历实则是冗余的,应该在第一次遍历时记录下min
首次出现时的索引(若sum >= 0
),并在循环结束后检查是否找到了这样的起始位置。正确的逻辑应该简化并修正这部分。
方法二:
// 解法2
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0;
int totalSum = 0;
int index = 0;
for (int i = 0; i < gas.length; i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if (curSum < 0) {
index = (i + 1) % gas.length ;
curSum = 0;
}
}
if (totalSum < 0) return -1;
return index;
}
}
这段Java代码提供了另一种更为简洁且正确的解法来解决“环形赛道加油问题”。与之前的解法相比,这个版本在单次遍历中就完成了所有必要的计算,避免了冗余的循环。下面是这个解法的详细解析:
代码解析:
-
变量初始化:
curSum
初始化为0,用于记录从当前起点开始累积的汽油剩余量。totalSum
初始化为0,用于记录整个环形赛道上汽油总量与成本总量之差。index
初始化为0,用于记录可能的起始加油站位置。
-
单次遍历:
遍历数组一次,对于每个位置i
:- 更新
curSum
为当前位置的汽油减去成本,即curSum += gas[i] - cost[i]
。 - 累加到
totalSum
中,用于最后判断是否有解。 - 如果
curSum
小于0,说明从当前起点无法继续行驶到下一个位置,此时更新index
为下一个位置(i + 1) % gas.length
,并重置curSum
为0,表示从这个新位置重新尝试开始计算剩余油量。
- 更新
-
判断是否有解:
- 在遍历结束后,检查
totalSum
是否小于0,如果是,则说明整个环形赛道上的汽油总量不足以覆盖行驶的总成本,返回-1
。 - 如果
totalSum >= 0
,则返回之前记录的index
作为可以成功完成一圈的起始加油站位置。注意,这里的index
实际上记录的是“下一个”加油站的索引,因为在遍历中一旦累加的油量不足就立即更新了index
,因此最终的index
是理论上最后一个“失败点”的下一个位置,也就是实际的起始位置。
- 在遍历结束后,检查
这个解法高效地解决了问题,仅通过一次遍历就找到了可能的起始加油站或者确定了无解的情况。
135. 分发糖果
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
class Solution {
/**
分两个阶段
1、起点下标1 从左往右,只要 右边 比 左边 大,右边的糖果=左边 + 1
2、起点下标 ratings.length - 2 从右往左, 只要左边 比 右边 大,此时 左边的糖果应该 取本身的糖果数(符合比它左边大) 和 右边糖果数 + 1 二者的最大值,这样才符合 它比它左边的大,也比它右边大
*/
public int candy(int[] ratings) {
int len = ratings.length;
int[] candyVec = new int[len];
candyVec[0] = 1;
for (int i = 1; i < len; i++) {
candyVec[i] = (ratings[i] > ratings[i - 1]) ? candyVec[i - 1] + 1 : 1;
}
for (int i = len - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
}
}
int ans = 0;
for (int num : candyVec) {
ans += num;
}
return ans;
}
}
这段Java代码是用于解决“糖果分配问题”的一个经典实现。给定一个表示孩子们评分的整数数组 ratings
,你需要按照以下要求给孩子们分发糖果:
- 每个孩子至少得到一个糖果。
- 如果一个孩子的评分比他旁边的孩子高,那么这个孩子应该得到更多的糖果。
目标是求出最少需要多少糖果。
代码解析:
-
初始化:首先,根据题目要求初始化一个长度与
ratings
相同的数组candyVec
用于存储每个孩子应得的糖果数,并初始化candyVec[0]
为1,因为至少有一个糖果。 -
第一阶段遍历:从左到右遍历
ratings
,如果当前孩子的评分比前一个孩子高,那么他在candyVec
中对应的糖果数就设置为前一个孩子糖果数加1,否则至少分配1个糖果。这一步确保了每个孩子比左边评分低的孩子多至少一个糖果。 -
第二阶段遍历:然后从右到左遍历
ratings
(从倒数第二个孩子开始,因为最后一个孩子已经在第一阶段考虑过了),对于每个孩子,如果他的评分比右边的孩子高,那么他的糖果数应该更新为他自己当前的糖果数与右边孩子糖果数加1之间的较大值。这样确保了每个孩子也比右边评分低的孩子多至少一个糖果,同时考虑到两边都比较的情况,保证分配合理。 -
计算总数:最后,遍历
candyVec
数组,将所有孩子的糖果数相加,得到最小需要的糖果总数。 -
返回结果:返回糖果总数
ans
。
总结:
该算法通过两次遍历确保了每个孩子的糖果数满足题目的两个条件,同时尽可能地减少了糖果的总消耗,是一种既直观又高效的解决方案。
860. 柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
示例 2:
输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0;
int ten = 0;
for (int i = 0; i < bills.length; i++) {
if (bills[i] == 5) {
five++;
} else if (bills[i] == 10) {
five--;
ten++;
} else if (bills[i] == 20) {
if (ten > 0) {
ten--;
five--;
} else {
five -= 3;
}
}
if (five < 0 || ten < 0) return false;
}
return true;
}
}
这段代码是用来解决“柠檬水找零”问题的,具体问题是这样的:你是一个卖柠檬水的小摊贩,手上最初只有足够的零钱可以找开5美元、10美元和20美元的钞票。每当顾客付给你一张钞票时,你必须提供正确数额的找零。如果能找回零钱,该函数返回true
,否则返回false
。
代码解析:
- 初始化两个整型变量
five
和ten
,分别表示手头上5美元和10美元的钞票数量,初始都为0。 - 使用
for
循环遍历输入数组bills
,数组中的每个元素代表顾客给的钞票面额。- 如果顾客给的是5美元,那么5美元的钞票数量
five
加1。 - 如果顾客给的是10美元,那么需要找回5美元,因此5美元的数量减1,10美元的数量加1。
- 如果顾客给的是20美元,优先考虑如果有10美元和5美元的情况,即10美元减1张,5美元减1张;如果没有10美元,则需要3张5美元来找零,因此5美元的数量减3。
- 在每次处理完一笔交易后,检查是否有足够的5美元和10美元(即
five
和ten
是否小于0),如果不足则直接返回false
,表示无法找零。
- 如果顾客给的是5美元,那么5美元的钞票数量
- 如果循环结束,说明所有顾客都能得到正确的找零,返回
true
。
注意点:
- 代码中没有处理输入为空或非法输入的情况,实际应用中可能需要添加相应的错误处理。
- 这个解法是基于贪心策略的,总是优先使用更小面额的钞票进行找零,以期望尽量减少找零所需的钞票数量。
406. 根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
1 <= people.length <= 2000
0 <= hi <= 106
0 <= ki < people.length
题目数据确保队列可以被重建
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 身高从大到小排(身高相同k小的站前面)
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
return b[0] - a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
});
LinkedList<int[]> que = new LinkedList<>();
for (int[] p : people) {
que.add(p[1],p); //Linkedlist.add(index, value),會將value插入到指定index裡。
}
return que.toArray(new int[people.length][]);
}
}
这段Java代码是用于解决“队列重建”问题的一个实现。给定一个二维数组 people
,其中 people[i] = [hi, ki]
,表示第 i
个人的身高为 hi
,前面 正好 有 ki
个身高大于或等于 hi
的人。该函数的目标是根据这些信息重构队列,返回一个按正确顺序排列的数组。
代码解析:
-
排序:首先使用
Arrays.sort()
方法对people
数组进行排序。排序的规则是首先按照身高h
降序排列(即身高高的在前),当身高相同时,按照k
值(前面比自己身高高或相等的人数)升序排列。这样保证了在身高相同的情况下,k
值较小的人会排在前面。 -
构造队列:创建一个
LinkedList
(链表)que
用于存储重建后的队列。选择链表是因为它支持在列表的任何位置高效地插入元素,这正是本问题所需要的。 -
遍历排序后的数组:遍历排序后的
people
数组,对于每个人p
,使用que.add(p[1], p)
将其插入到链表中正确的位置。这里的p[1]
是k
值,即此人前面应该有多少个人比他高或等高,p
本身代表这个人(整个数组[hi, ki]
)。 -
返回结果:最后,使用
toArray
方法将链表转换回二维数组,并返回结果。
总结:
该算法通过先排序后构建的方法,有效地解决了队列重建的问题。排序步骤确保了在插入过程中可以直接根据人的身高和前面人数的要求找到合适的位置,而链表的特性支持了在特定位置快速插入元素,从而保证了整体的时间效率。