学习算法的时候,总会有一些让人生畏的名词,比方动态规划
,贪心算法
等,听着就很难;而这一 part 就是为了攻破之前一直没有系统学习的 贪心算法
;
有一说一,做了这些贪心题,其实并没觉得发现了什么套路新大陆等,因为贪心有的时候很巧妙,而且想到就是想到了,没想到可能就不用贪心去做了,所以这属于做完只是刷了存在感的 part;
唯一的收获就是减轻了对贪心的恐惧,明白它也就是一种 局部贪心导致全局贪心得到最优解
的一种思路方法,所以以后遇到了,也就能心平气和的去学习使用它了;
下一 part 去做一下比较难的并查集
分析 – 贪心
var findContentChildren = function (g, s) {g.sort((a,b) => a-b)s.sort((a,b) => a-b)let ret = 0let sl = s.length-1; let gl = g.length-1while(gl>=0){// 人没了,饼干可以还存在if(s[sl]>=g[gl] && sl>=0){// 最大的饼干能否满足最大胃口的孩子ret++sl--}gl--}return ret
}
分析 – 贪心
var wiggleMaxLength = function(nums) {if(nums.length<2) return nums.lengthlet ret = 1 // 从 1 开始是因为要求的是整个摆动序列的长度,所以先初始化1,然后遇到极值递增即可let preDiff = 0 // 初始化第一个差值;设置为0,则无论真正第一个差值是多少,得到的都是 0let curDiff = 0for(let i = 1;icurDiff = nums[i]- nums[i-1]// 差值必须是正负数,如果是 0 则跳过if(curDiff === 0) continueif(preDiff * curDiff <= 0){ret++preDiff = curDiff}}return ret
};
分析 – 贪心
连续子数组
var maxSubArray = function (nums) {let max = -Infinity;let sum = 0for(let i = 0 ;isum+=nums[i]max = Math.max(sum,max)if(sum<=0){sum=0}}return max
};
分析 – 回溯 – 超时了
var canJump = function (nums) {let ret = false;const dfs = (start) => {// 只要有一个成功,就直接不做其他处理了if (start >= nums.length || ret) return;if (start+nums[start] >= nums.length-1) {ret = true;return;}for (let i = 1; i <= nums[start]; i++) {dfs(start + i); // 在当前这一个节点,可以跳的步数}};dfs(0)return ret;};
分析
参考视频:传送门
var canJump = function (nums) {for(let i=0;iif(nums[i] === 0){// 开始寻找可以跳过当前 i 值的节点let valIndex = i-1while(nums[valIndex]<= i -valIndex && valIndex>=0){valIndex--}if(valIndex<0) return false}}return true}
/** * @分析 -- 已知能到达位置,求最少跳跃次数 * 1. 看到最少,想到用 dp 做;其中 dp[i] 就是到达 i 这个位置最少需要跳跃的次数, 但是控制当前状态的变量在上一个值,感觉 dp 不太合适 * 2. 感觉用贪心+回溯会更好一点,每一次尽量远的跳,如果不行再跳回来 * 3. 然后正常超时了 */
var jump = function(nums) {if(nums.length < 2) return 0let ret = Infinityconst dfs = (index,sum) => {if(index>=nums.length-1) {// 贪心走出来的,肯定是ret = Math.min(sum,ret)return }if(sum>=ret || nums[index] === 0) return // 只要出了第一个,后面的全部不玩了for(let i = nums[index];i>0;i--){dfs(index+i,sum+1)}}dfs(0,0)return ret
};/** * @分析 * 1. 考虑到跳跃范围必须覆盖一定范围,求最小的目的,还是从后倒推前面会更舒服一点,所以考虑 dp; * 2. dp[i] 表示跳跃到 i 这个位置最小的次数 * 3. 状态转移方程: dp[i] = Math.min(dp[i-valid]+1) 这里的 valid 是值符合 nums[j]+j >= i 的 dp[j], 这样在 j 这个位置才能一次跳到 i * 4. base case: dp[0] = 0 原地蹦跶 * 5. 时间复杂度 ${O(n^2)}$ */
var jump = function(nums) {const dp = new Array(nums.length)dp[0] = 0 // 原地蹦跶for(let i=1;idp[i] = Infinityfor(let j = i-1;j>=0;j--){if(nums[j]+j>=i){// 这样才能从 j 跳到 idp[i] = Math.min(dp[i],dp[j]+1)}}}return dp[nums.length-1]
}/** * @分析 -- 贪心 * 1. 每一次跳动都可以缓存最大跳跃范围,这是一个范围而不是一个值,所以下一跳的时候,需要从这个范围内找到最最大跳跃的范围 * 2. 所以只要迭代每一个值,就可以找到跑到这个值的时候,最大跳跃的覆盖范围 nextIndex 的位置, 同样的,我们将上一轮的最大距离设置为 curIndex * 3. 每当迭代到 curIndex, 表明上一次跳跃的覆盖范围都已经遍历完,并且记录好了这个范围内的最大值 nextIndex 了,这个时候更改 curIndex = nextIndex * 4. 其实整个过程就是在 [curIndex,nextIndex] 中找最大范围,然后不断迭代; * 5. 只需要遍历一次就能找到结果了,所以时间复杂度 ${O(n)}$ */var jump = function(nums) {let curIndex = nextIndex = 0let ret = 0for(let i =0;iif(curIndex >=nums.length-1) return ret // 如果最大覆盖范围已经找到了地方,那么就直接跳出遍历了nextIndex = Math.max(nextIndex,nums[i]+i) // 最远覆盖范围if(curIndex === i) {// 如果 i 到达上一次最远覆盖位置,那么 nextIndex 就是上一轮 [cur,next] 的最大距离,现在需要更新一下curIndex = nextIndex// 所谓覆盖,就是 jump 一次ret++}}
}
注意,这里并没有用到贪心,但是这是一个主题的题目,所以也放在一起来学习了;比较分块学习也是按组类学习,而我们真正遇到问题的时候,是不会给你打 tag 说是用啥方法做的,所以相类似的题放一起做,即便由于题目改变了,没有用到相应的技术,也值得放在一起学习一哈;
分析 – BFS
var canReach = function (arr, start) {const queue = [];queue.push(start);const useSet = new Set();while (queue.length) {let len = queue.length;while (len--) {const node = queue.shift();const l = node - arr[node];const r = node + arr[node];if (l >= 0 && !useSet.has(l)) {if (arr[l] === 0) return true;queue.push(l);useSet.add(l);}if (r < arr.length && !useSet.has(r)) {if (arr[r] === 0) return true;queue.push(r);useSet.add(r);}}}return false;
};
分析 – dfs
var canReach = function (arr, start) {let ret = false;const useSet = new Set(); // 剪枝用的const dfs = (node) => {if (useSet.has(node) || ret === true) return;if (arr[node] === 0) {ret = true;return;}useSet.add(node);if (node - arr[node] >= 0) {dfs(node - arr[node]);}if (node - arr[node] < arr.length) {dfs(node + arr[node]);}};dfs(start);return ret;
};
分析
var largestSumAfterKNegations = function(nums, k) {nums.sort((a,b)=>a-b)let index = 0while(k && nums[index] < 0){// 如果 k 还存在且当前值还是负数的时候,就转换nums[index] = - nums[index]k--index++}// 转换后 index 所在的位置就是最开始最小值非负数了,但是它有可能比转换后的最小正数小,所以要对比一下// 但是如果 index 是第一个值,也就是一开始全都是非负数的时候,这个时候就没有 index-1 了;// 同理,如果全是负数,那么 index 就不存在了let min = index=== 0 ? nums[index] : index=== nums.length?nums[index-1] :Math.min( nums[index], nums[index-1])// 先将所有负数都转成正数 -- 如果 k 还存在,那么就处理 nums[index] 就好了let sum = nums.reduce((pre,cur)=>pre+cur,0)if(k % 2) sum -= min*2return sum
};
分析 – 贪心
var maxProfit = function(prices) {let ret = 0for(let i = 1;iconst temp = prices[i]-prices[i-1]if(temp>0){ret+=temp}}return ret
}
分析
var canCompleteCircuit = function (gas, cost) {const leaves = gas.map((g, i) => g - cost[i]); // 每一个站台加油后跑路之后,剩余值的数组,正数就是有剩余,负数就是不足,需要在某些地方补充;let ret = -1;let sum = 0; // 缓存当前油量for (let i = 0; i < leaves.length; i++) {if (leaves[i] >= 0) {if (ret === -1) {ret = i;}sum += leaves[i];continue;}if (sum + leaves[i] < 0) {// 之前那个起点已经失败了ret = -1; //恢复到 -1sum = 0;} else {sum += leaves[i]; // 继续走着}}if (ret === -1) return -1; // 如果走完这一段,sum 还存在,证明在 [ret,leaves.length-1] 是合格的,那么继续走一下 [0,ret]for (let i = 0; i < ret; i++) {if (leaves[i] >= 0) {sum += leaves[i];continue;}if (sum + leaves[i] < 0) {// 在这个循环中一旦出现不合适的,就不再走下去了,因为已经走过一次了return -1;} else {sum += leaves[i]; // 继续走着}}return ret
};
分析
var canCompleteCircuit = function (gas, cost) {const leaves = gas.map((g, i) => g - cost[i]); // 每一个站台加油后跑路之后,剩余值的数组,正数就是有剩余,负数就是不足,需要在某些地方补充;let ret = -1;let sum = 0; // 缓存当前油量let gasSum = 0let costSum = 0for (let i = 0; i < leaves.length; i++) {costSum+=cost[i]gasSum+=gas[i]if (leaves[i] >= 0) {if (ret === -1) {ret = i;}sum += leaves[i];continue;}if (sum + leaves[i] < 0) {// 之前那个起点已经失败了ret = -1; //恢复到 -1sum = 0;} else {sum += leaves[i]; // 继续走着}}if (gasSum
分析 – 题目描述有问题
var candy = function (ratings) {const len = ratings.length;const candies = new Array(len).fill(1); // 发糖果的数组for (let i = 1; i < len; i++) {if (ratings[i] > ratings[i - 1]) {candies[i] = candies[i - 1] + 1;}}for (let i = len - 2; i >= 0; i--) {if (ratings[i] > ratings[i + 1]) {candies[i] = Math.max(candies[i + 1] + 1,candies[i]); // 从右边数的时候,就要判断哪边更大了}}return candies.reduce((pre, cur) => pre + cur, 0);
};
分析
var lemonadeChange = function(bills) {let fives = 0let tens = 0for(let i =0;iconst b = bills[i] if(b === 5){fives++}if(b === 10 ) {if(fives>0){fives--tens++}else {return false}}if(b === 20){// 现在用贪心,先尽可能的用 10 块去找零,因为 5 块是粒度更小的零钱,它通用性更强,所以尽可能贪心的保存 5 块if(tens>0 && fives>0){tens--fives--}else if (tens === 0 && fives>=3){fives -=3}else{return false}}}return true};
分析
最高且前面人数最少
的 item, 这个时候队列的两个条件已经一起限制好,只需要按照 item[i] 插入到 ret 上就足够了 – 后续的插入是不会影响到当前插入的,因为后续的值肯定会贴合现有排好的 ret;var reconstructQueue = function(people) {const map = new Map(); // 先将身高一眼给的缓存起来for(let i = 0;iconst key = people[i][0]map.set(key,map.get(key)?[...map.get(key),people[i]]:[people[i]])}const arr = [...map.keys()].sort((a,b)=>b-a) // 从大到小const ret = []for(let i = 0;iconst tempArr = map.get(arr[i]) // 取出数组tempArr.sort((a,b)=>a[1]-b[1]) // 身高相同的数组,要根据在他们前面的人的数量进行排序,这样才能保证前面人少的在前面// 这个时候需要只需要按找数组的第二个值,插入到最终数组即可for(let temp of tempArr){ret.splice(temp[1],0,temp) // 在 temp[1] 的位置插入 temp}}return ret
};const ret = reconstructQueue([[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]);
console.log(ret)
分析 – 失败
重叠最多
的位置进行射击,当气球射完需要多少箭;-- 也就是找到交集的数量分析2
var findMinArrowShots = function(points) {const len = points.length let ret = [] // 缓存没有交集的数组for(let i =0;iconst pp = points[i]let isMerge = falsefor(let i = 0;iconst rr = ret[i]// 如果起始位置都超过了终止位置,那么就没有交集了if(pp[0]>rr[1] || pp[1]< rr [0]) continue// 否则就是有交集了,那么只要保存交集就好,因为射中交集的时候,一次性就完成所有的气球爆炸ret[i] = pp[0]<=rr[0]?[rr[0],Math.min(pp[1],rr[1])]:[pp[0],Math.min(pp[1],rr[1])]isMerge = true // 如果合并了break}if(!isMerge){ret.push(pp)}}return ret.length
};
分析2
var findMinArrowShots = function(points) {const len = points.length points.sort((a,b)=>a[0]-b[0])let cur = -Infinity;let ret = 0for(let i = 0 ;iconst pp = points[i]if(pp[0]>cur) {// 超出范围了ret++cur = pp[1] // 修改}else{cur = Math.min(cur,pp[1])}}return ret
}findMinArrowShots([[10,16],[2,8],[1,6],[7,12]])
findMinArrowShots([[1,2]])
findMinArrowShots([[3,9],[7,12],[3,8],[6,8],[9,10],[2,9],[0,9],[3,9],[0,6],[2,8]])
分析3 – 右侧节点排序
var findMinArrowShots = function(points) {const len = points.length points.sort((a,b)=>a[1]-b[1]) // 右侧排序let right = -Infinity;let ret = 0for(let i = 0 ;iconst pp = points[i]if(pp[0]>right) {// 超出范围了ret++right = pp[1] // 修改}}return ret
}
分析
var eraseOverlapIntervals = function(intervals) {const length = intervals.lengthintervals.sort((a,b) => a[1]-b[1]) // 按右侧大小排列好let right = -Infinitylet ret = 0 // 集合数量for(let i = 0;iconst ii = intervals[i]if(ii[0]>=right) {ret++ right = ii[1]}}return length-ret
}
分析
var partitionLabels = function(s) {const map = new Map() // 记录字符和最后一个字符对应的下标for(let i = 0;iconst ss = s[i]map.set(ss,i)}console.log(map)let ret = []let start = 0// 现在尽可能短的获取片段while(startlet temp = start // 起始值let end = map.get(s[start]) //第一个字母的最后一个下标while(start<=end){if(map.get(s[start])>end){end = map.get(s[start]) // 将 end 变长}start++}// 抛出一轮了ret.push(start-temp)}return ret
};console.log(partitionLabels('ababcbacadefegdehijhklij'))
分析
var merge = function (intervals) {intervals.sort((a, b) => a[0] - b[0]);let ret = [];let cur = intervals[0];for (let i = 1; i < intervals.length; i++) {const temp = intervals[i];if (temp[0] > cur[1]) {// 当取出的空间的起始值已经比当前值要大的时候,那么剩下的其他值,也会完全和当前的 cur 隔离开,所以将当前 cur 推入 ret 中ret.push(cur);cur = temp; // 替换 cur}if (cur[1] < temp[1]) {cur[1] = temp[1];}}return [...ret, cur];
};console.log(merge([[1,4],[2,3]])
);
分析
var monotoneIncreasingDigits = function (n) {if(n<10) return n //如果是个位数,直接返回 nconst str = String(n)const len = str.lengthconst arr = str.split('')let flag = Infinity // 标记最后一个设置为 9 的下标,从这个下标之后的值,都得换成 9for(let i =len-1;i>=0;i--){if(arr[i-1]>arr[i]){// 如果前一位大于后一位,那么为了当增,需要将当前位减一,后一位换成 9flag = iarr[i-1] = arr[i-1] -1 }}for (let i = flag; i < len; i++) {arr[i] = 9}return Number(arr.join(''))
};
分析
父节点
上去安装,这样就可以一次性覆盖到叶子节点,同时由于是自底向上的遍历,那么不需要考虑更底层的覆盖,只需要考虑当前节点和它的叶子节点即可var minCameraCover = function (root) {if (!root) return 0;let ret = 0; // 装了多少摄像头const dfs = (root) => {if (!root.left && !root.right) return; // 到达叶子节点,直接返回,不加摄像头if (root.left) dfs(root.left);if (root.right) dfs(root.right);// 后序遍历,遇到父子节点存在摄像头,那就不需要加了if ((root.left && root.left.val !== 0 || !root.left) && (root.right && root.right.val !== 0 || !root.right)){if((root.left && root.left.val === 1) || (root.right && root.right.val === 1)){// 存在摄像头才能波及root.val = 2 // 波及到的}return }// 必须要保证存在的子节点都已经是 1 的时候,才可以放心继续往上走root.val = 1; //如果大家伙都没有装,那就我来装吧ret++;};dfs(root);return root.val === 0 ? ret+1 : ret
};
上一篇:二维码数据压缩实践 | 使用python对二维码数据进行压缩 |不乱码,支持中文
下一篇:selenium driver.find_element 报错 invalid argument: invalid locator