文章目录
455. 分发饼干
- 最初代码
var findContentChildren = function(g, s) {
g.sort((a, b) => a - b);
s.sort((a, b) => a - b);
let sum = 0, i = 0, j = 0;
while (i < g.length && j < s.length) {
if (s[j] >= g[i]) {
sum++;
i++;
j++;
}
else j++;
}
return sum;
};
- 接下来是正解,小饼干先喂小胃口,不符合就增
var findContentChildren = function(g, s) {
g.sort((a, b) => a - b);
s.sort((a, b) => a - b);
let res = 0, index = s.length - 1;
for (let i = g.length - 1; i >= 0; i--) {
if (index >= 0 && s[index] >= g[i]) {
res++;
index--;
}
}
return res;
};
376. 摆动序列
- 实际上不需要删除元素,只需要将坡上的节点忽略即可
var wiggleMaxLength = function(nums) {
if (nums.length <= 1) return nums.length;
let curDiff = 0, preDiff = 0;
let res = 1;
for (let i = 0; i < nums.length - 1; i++) {
curDiff = nums[i + 1] - nums[i];
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
res++;
preDiff = curDiff;
}
}
return res;
};
53. 最大子数组和
- 用dp比较容易,但是对于贪心来说有点难
- 当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
- 每次循环都要比较记录最大的总和
var maxSubArray = function(nums) {
let result = -Infinity
let count = 0
for(let i = 0; i < nums.length; i++) {
count += nums[i]
if(count > result) {
result = count
}
if(count < 0) {
count = 0
}
}
return result
};
122. 买卖股票的最佳时机 II
假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。
相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!
我们就只要收集每天的正收入,累加即可得最大收益
var maxProfit = function(prices) {
let res = 0;
for (let i = 1; i < prices.length; i++) {
res += Math.max(0, prices[i] - prices[i - 1]);
}
return res;
};
55. 跳跃游戏
- 每次的跳跃步数即为跳跃的覆盖范围,那么问题就转换为跳跃的覆盖范围能不能覆盖到终点
- 每次移动取最大的跳跃步数,同时更新覆盖范围
var canJump = function(nums) {
let cover = 0;
for (let i = 0; i <= cover; i++) {
cover = Math.max(cover, nums[i] + i);
if (cover >= nums.length - 1) return true;
}
return false;
};
45. 跳跃游戏 II
- 就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数
var jump = function(nums) {
let res = 0, curDistance = 0, nextDistance = 0;
if (nums.length === 1) return 0;
for (let i = 0; i < nums.length; i++) {
nextDistance = Math.max(nextDistance, nums[i] + i);
if (i === curDistance) {
if (curDistance !== nums.length - 1) {
res++;
curDistance = nextDistance;
if (curDistance >= nums.length - 1) break;
} else break;
}
}
return res;
};
- 第二种方法简化了,移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。移动下标最大只能移动到nums.size - 2
var jump = function(nums) {
let curIndex = 0
let nextIndex = 0
let steps = 0
for(let i = 0; i < nums.length - 1; i++) {
nextIndex = Math.max(nums[i] + i, nextIndex)
if(i === curIndex) {
curIndex = nextIndex
steps++
}
}
return steps
};
1005. K 次取反后最大化的数组和
- 先把数组排序,从头开始让负数取正,同时求和,接下来会有几种情况
- k用完,返回求和结果
- k为偶数,对结果没有影响
- k为奇数,我们需要找到最小的数(当前数组已全部为正),将求和结果 - 2 x 最小值
var largestSumAfterKNegations = function (nums, k) {
let res = 0;
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
if (nums[i] < 0 && k > 0) {
nums[i] *= -1;
k--;
}
res += nums[i];
}
if (k === 0) {
return res;
} else {
if (k % 2 === 0) {
return res;
} else {
return res - 2 * Math.min(...nums);
}
}
};
- 题解还有优化的空间
- 改sort,让数组以绝对值从大到小排序,这样后面减的时候只需要拿数组的最后一个元素(最小元素)即可
// 版本二 (优化: 一次遍历)
var largestSumAfterKNegations = function(nums, k) {
nums.sort((a, b) => Math.abs(b) - Math.abs(a)); // 排序
let sum = 0;
for(let i = 0; i < nums.length; i++) {
if(nums[i] < 0 && k-- > 0) { // 负数取反(k 数量足够时)
nums[i] = -nums[i];
}
sum += nums[i]; // 求和
}
if(k % 2 > 0) { // k 有多余的(k若消耗完则应为 -1)
sum -= 2 * nums[nums.length - 1]; // 减去两倍的最小值(因为之前加过一次)
}
return sum;
};
134. 加油站
- 不太简单的暴力
var canCompleteCircuit = function(gas, cost) {
for(let i = 0; i < cost.length; i++) {
let rest = gas[i] - cost[i] //记录剩余油量
// 以i为起点行驶一圈,index为下一个目的地
let index = (i + 1) % cost.length
while(rest > 0 && index !== i) {
rest += gas[index] - cost[index]
index = (index + 1) % cost.length
}
if(rest >= 0 && index === i) return i
}
return -1
};
- 还可以从全局着眼
情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。
例如:例1的rest(gas - cost)数组为[-2, -2, -2, 3, 3],min = -6,我们需要从后往前找出一段能够填补min的一段路,即一段路亏油,我们要找一段路存储到足够他亏的油
var canCompleteCircuit = function(gas, cost) {
let curSum = 0, min = Infinity;
for (let i = 0; i < cost.length; i++) {
curSum += gas[i] - cost[i];
if (curSum < min) min = curSum;
}
if (curSum < 0) return -1;
if (min > 0) return 0;
for (let i = cost.length - 1; i >= 0; i--) {
min += gas[i] - cost[i];
if (min >= 0) {
return i;
}
}
return -1;
};
135. 分发糖果
- 两次贪心的策略:
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
var candy = function(ratings) {
let candys = new Array(ratings.length).fill(1);
for (let i = 1; i < candys.length; i++) {
if (ratings[i] > ratings[i - 1]) {
candys[i] = candys[i - 1] + 1;
}
}
for (let i = candys.length - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candys[i] = Math.max(candys[i], candys[i + 1] + 1);
}
}
return candys.reduce((a, b) => {
return a + b;
})
};
860. 柠檬水找零
- 简单模拟即可,对于20的情况首选10+5为贪心思想
var lemonadeChange = function(bills) {
let five = 0, ten = 0;
for (let i = 0; i < bills.length; i++) {
if (bills[i] === 5) five++;
if (bills[i] === 10) {
if (five) {
five--;
ten++;
} else {
return false;
}
}
if (bills[i] === 20) {
if (ten && five) {
ten--;
five--;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
};
406. 根据身高重建队列
- 与发糖果一样,有两个维度先单一考虑一个维度,这里我们若按k排序没有意义,按身高排序,前面的节点一定比本节点高,这样后续就可以不顾忌的按k插入
var reconstructQueue = function(people) {
let que = [];
people.sort((a, b) => {
if (a[0] !== b[0]) {
return b[0] - a[0];
} else {
return a[1] - b[1];
}
})
for (let i = 0; i < people.length; i++) {
que.splice(people[i][1], 0, people[i]);
}
return que;
};
452. 用最少数量的箭引爆气球
- 将数组照头位置排序后,若后一个气球左边界大于了前一个气球的最小右边界,则需要添加箭
var findMinArrowShots = function(points) {
let res = 1;
points.sort((a, b) => {
return a[0] - b[0];
})
for (let i = 1; i < points.length; i++) {
if (points[i][0] > points[i - 1][1]) {
res++;
} else {
points[i][1] = Math.min(points[i - 1][1], points[i][1]);
}
}
return res;
};
435. 无重叠区间
- 按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。
- 从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。
- 右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。
var eraseOverlapIntervals = function(intervals) {
intervals.sort((a, b) => a[1] - b[1]);
let end = intervals[0][1], count = 1;
for (let i = 1; i < intervals.length; i++) {
if (end <= intervals[i][0]) {
end = intervals[i][1];
count++;
}
}
return intervals.length - count;
};
763. 划分字母区间
var partitionLabels = function(s) {
let hash = {};
let res = [];
for (let i = 0; i < s.length; i++) {
hash[s[i]] = i; // // 统计每一个字符最后出现的位置
}
let right = 0, left = 0;
for (let i = 0; i < s.length; i++) {
right = Math.max(right, hash[s[i]]); // 找到字符出现的最远边界
if (i === right) {
res.push(right - left + 1);
left = i + 1;
}
}
return res;
};
56. 合并区间
- 按左边界从小到大排序,然后针对重叠的区间取最大的右边界
var merge = function(intervals) {
let res = [];
intervals.sort((a, b) => a[0] - b[0]);
let pre = intervals[0];
for (let i = 1; i < intervals.length; i++) {
let cur = intervals[i];
if (cur[0] > pre[1]) {
res.push(pre);
pre = cur;
} else {
pre[1] = Math.max(cur[1], pre[1]);
}
}
res.push(pre);
return res;
};
738. 单调递增的数字
- 最重要的是遍历的顺序,若从左到右遍历,那么332的情况我们就只能得到329,因为无法判断改变后是否还符合递增顺序;而从右到左遍历,我们就可以使用到上一次改变的数字
var monotoneIncreasingDigits = function(n) {
let flag;
n = n.toString();
n = n.split('').map((item) => +item);
for (let i = n.length - 1; i > 0; i--) {
if (n[i] < n[i - 1]) {
flag = i;
n[i - 1]--;
n[i] = 9;
}
}
for (let i = flag; i < n.length; i++) {
n[i] = 9;
}
return +n.join('');
};