一文通数据结构与算法之——贪心算法+常见题型与解题策略+Leetcode经典题
Posted 尚墨1111
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文通数据结构与算法之——贪心算法+常见题型与解题策略+Leetcode经典题相关的知识,希望对你有一定的参考价值。
文章目录
- 贪心算法
- 1 概念
- 2 Leetcode经典题
- 2.1 分发物品
- 2.2 数组序列相关
- [1005. K 次取反后最大化的数组和](https://leetcode-cn.com/problems/maximize-sum-of-array-after-k-negations/)
- [376. 摆动序列](https://leetcode-cn.com/problems/wiggle-subsequence/)
- [738. 单调递增的数字](https://leetcode-cn.com/problems/monotone-increasing-digits/)
- [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)
- [55. 跳跃游戏](https://leetcode-cn.com/problems/jump-game/)
- [45. 跳跃游戏 II](https://leetcode-cn.com/problems/jump-game-ii/)
- 2.3 区间类题目:
- [406. 根据身高重建队列](https://leetcode-cn.com/problems/queue-reconstruction-by-height/)
- [452. 用最少数量的箭引爆气球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/)
- [435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/)——笔试题中区间问题很多
- [763. 划分字母区间](https://leetcode-cn.com/problems/partition-labels/)
- [56. 合并区间](https://leetcode-cn.com/problems/merge-intervals/)
- 2.4 其他
贪心算法
1 概念
1.1 心得
-
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
-
往往最平淡无奇、看似毫无技巧的方法,可能就是贪心。返璞归真。
- 比如:加油站问题只是求个总和,找和最小的地方
-
我们要找到组成题目元素中最有价值的点是什么,从而求局部最优解,再拓展到全局最优
- 比如分饼干,大的饼干喂胃口最大的孩子,实现饼干价值最大化
- 比如柠檬水找零,5块既可以找10块的零,也可以找20的零是最有价值的,所以我们尽可能保留最多的
1.2 题目列表
- 分发物品类
- 数组序列相关
- 区间类题目(笔试最常考)
- 其他问题
2 Leetcode经典题
2.1 分发物品
135.分发糖果(非常经典)
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。问最少的糖果
- 每个孩子至少分配到 1 个糖果。
- 评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
局部最优:左右分别遍历一遍,每个元素取left和right的最大值就是局部最优,n个元素都是最优就是全局最优了
public int candy(int[] ratings) {
int n = ratings.length;
int[] left = new int[n];
int[] right = new int[n];
for (int i = 1; i < n; i++) {
if(ratings[i]>ratings[i-1]){
left[i] = left[i-1]+1;
}
}
int count = left[n-1]+n;
for (int i = n-2; i >=0 ; i--) {
if(ratings[i]>ratings[i+1]){
right[i] = right[i+1]+1;
}
count+=Math.max(right[i],left[i]);
}
return count;
}
455.分发饼干
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
输入: g = [1,2,3], s = [1,1] 输出: 1
**局部最优:**大饼干喂给胃口大的,让大饼干的价值最大化,**全局最优:**喂饱尽可能多的小孩。
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(s);
Arrays.sort(g);
int count=0;
int i = s.length-1,j = g.length-1;
while(i>=0 && j>=0){
if(s[i]>=g[j]){
count++;
i--;
j--;
}else{
j--;
}
}
return count;
}
860. 柠檬水找零
每一杯柠檬水的售价为 5 美元。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。一开始你手头没有任何零钱。给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false
输入:bills = [5,5,5,10,20] 输出:true
思路:局部最优:5元可以找零10、20,尽可能的保留5元,就能尽可能的找零更多的顾客。全局最优:全部顾客都成功找零
public boolean lemonadeChange(int[] bills) {
int count5 = 0;
int count10 = 0;
// 1.遍历数组,记录5元10元个数
for (int i = 0; i < bills.length; i++) {
if(bills[i]==5){
count5++;
}else if(bills[i]==10){
if(count5==0){
return false;
}else{
count5--;
count10++;
}
}else{
//2.局部最优,先用10块的,保留5块的
if(count10>0 && count5>0){
count10--;
count5--;
}else if(count5>3){
count5-=3;
}else{
return false;
}
}
}
return true;
}
2.2 数组序列相关
1005. K 次取反后最大化的数组和
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。我们可以多次选择同一个索引 i。以这种方式修改数组后,返回数组可能的最大和。
输入:A = [3,-1,0,2], K = 3 输出:6
思路:**局部最优:**每次选取最小的元素变,影响最小,全局最优:总和自然也是最大的。
public int largestSumAfterKNegations(int[] nums, int k) {
if(nums==null || nums.length==0){
return 0;
}
Arrays.sort(nums);
int index = 0;
while(k>0){
// 1.每次把最小的反转,总和也是最大的
nums[index] = -nums[index];
k--;
// 2.重新确定最小元素
// 每次取反之后只会影响自身,只需要比较前后两个元素的大小关系,就是最小的元素
// 我原来的思路是重新排一次序,但是效率太低了。
if(index+1< nums.length && nums[index]>nums[index+1]){
index++;
}
}
int sum = 0;
for (int num : nums) {
sum +=num;
}
return sum;
}
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**给你一个整数数组
nums
,返回nums
中作为 摆动序列 的 最长子序列的长度 。输入:nums = [1,7,4,9,2,5] 输出:6
思路:局部最优:局部的峰值个数之和,全局最优:就是全局最长的摆动子序列
记录两边的差值
public int wiggleMaxLength(int[] nums) {
// 1.记录两边的差值,因为两端的默认是摆动部分,所以pre=0
int pre = 0;
int after = 0;
int count = 1;
// 2.遍历每一个元素,找波峰、波谷
for (int i = 1; i < nums.length; i++) {
after = nums[i]-nums[i-1];
if(pre<=0 && after>0 ||pre>=0 && after<0){
count++;
pre = after;
}
}
return count;
}
738. 单调递增的数字
给定一个非负整数
N
,找出小于或等于N
的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。输入: N = 332 输出: 299
局部最优:从左往右遍历各位数字,找到第一个开始下降的数字[i],将[i]减1,然后将[i+1 …]各位数字全部置为9即可
注意:有相等的情况,33332,要找到第一个3——29999
事实证明:直接修改
char数组
的效率要比StringBuilder
的效率要高
public int monotoneIncreasingDigits(int n) {
// 1.特殊情况排除
if(n<10){
return n;
}
char[] ch = String.valueOf(n).toCharArray();
int start = 0;
// 2.找到第一个下降的数字, 232—— 3
while(start<ch.length-1 && ch[start]<=ch[start+1]){
start++;
}
// 3.判断这个是否为末尾
if(start==ch.length-1){
return n;
}
// 4.判断前面是否有相同的元素
while(start>0 && ch[start]==ch[start-1]){
start--;
}
// 5.变元素,直接修改char数组的效率要比 StringBuilder的效率要高
ch[start++] = (char)(ch[start]-1);
while(start<ch.length){
ch[start++] = '9';
}
return Integer.parseInt(String.valueOf(ch));
}
122. 买卖股票的最佳时机 II
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。你可以多次买卖一支股票,你必须在再次购买前出售掉之前的股票,求最大利润
输入: prices = [7,1,5,3,6,4] 输出: 7 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
思路:如果可以把所有的上坡全部收集到,一定是利益最大化的,相比于我刻意计算波峰波谷,这不就直接前后比较大小就可
public int maxProfit(int[] prices) {
int res = 0;
for (int i = 1; i < prices.length; i++) {
if(prices[i]-prices[i-1]>0){
res+=(prices[i]-prices[i-1]);
}
}
return res;
}
55. 跳跃游戏
给定一个非负整数数组
nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。输入:nums = [2,3,1,1,4]——[2,4,4,5,8] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
局部最优:如果一个位置能够到达,那么这个位置左侧所有位置都能到达。
public boolean canJump(int[] nums) {
// 1.特殊情况的排除
if(nums==null){
return false;
}
// 2.遍历所有元素,维护最长可达到距离
int max = 0;
for (int i = 0; i < nums.length; i++) {
max = Math.max(max,nums[i]+i);
// 3.如果当前位置到达的位置小于索引,且不是终点代表不能到达,比如[2,0]既可以到达
if(max<=i && max<nums.length-1){
return false;
}
}
return true;
}
45. 跳跃游戏 II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。
输入: nums = [2,3,1,1,4] 输出: 2
局部最优:维护每一步能到的最大距离,超过最大距离,记为下一步,一旦这个距离大于数组长度,就里面返回
public int jump(int[] nums) {
//1.排除特殊情况
if(nums==null || nums.length<=1){
return 0;
}
int max = 0,end = 0,count=0;
// 2.注意边界,因为最后一步肯定会跳到端点,所以num.length-1,不然在边界刚刚好是num.length时会多算一次
for (int i = 0; i < nums.length-1; i++) {
max = Math.max(nums[i]+i,max);
// 3.如果从这个跳点 起跳叫做第 1 次 跳跃,最长可以跳3步,那么在后面 3 个格子起跳 都叫做第 2 次 跳跃。
// 所以只有当i达到第二次能跳到的最大距离,才计步数
if(i==end){
end = max;
count++;
}
}
return count;
}
2.3 区间类题目:
406. 根据身高重建队列
套路:一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。
假设有打乱顺序的一群人站成一个队列,数组 people 表示,每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。请你重新构造并返回输入数组 people 所表示的队列。
输入: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]]
根据题目要求应该为身高降序,也就是说先考虑把身高较高的人放入新集合,这样在高个子前面或后面插入矮个子都不会影响当前高个子的
k
值;其次,k
值应该升序排列,k
值较大的较后插入。
public int[][] reconstructQueue(int[][] people) {
// 首先先将数组按第一个元素身高降序,按第二个元素人数升序
Arrays.sort(people,(p1,p2)->p1[0]==p2[0]?p1[1]-p2[1]:p2[0]-p1[0]);
List<int[]> list = new ArrayList<>();
for (int i = 0; i < people.length; i++) {
// 1.如果k值大于现在的长度,说明他的位置还在后面,直接插入
if(people[i][1]>=list.size()){
list.add(people[i]);
}else{
//2.否则把它插入对应的位以上是关于一文通数据结构与算法之——贪心算法+常见题型与解题策略+Leetcode经典题的主要内容,如果未能解决你的问题,请参考以下文章
一文通数据结构与算法之——数组+常见题型与解题策略+Leetcode经典题
一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题
一文通数据结构与算法之——二叉树+常见题型与解题策略+Leetcode经典题