贪心算法及相关leetcode习题详解 java代码实现

前言

这篇博客,南国根据自己之前一段时间的学习和刷题,对贪心算法这一知识点做个小的归纳。这篇博客的内容首先是基础知识点,随后是具体的实战习题。话不多说,干货速来~

基础知识点

在计算机专业常见的极大算法里面,贪心算法算是比较好理解的了。一句话用来概括就是,贪心算法:在对问题求解时,总是做出当前看来最好的选择
在这里插入图片描述
这个问题的本质借助的就是贪心算法。考虑当前的决定就是最好的情况。

贪心有许多非常经典的应用,例如说霍夫曼编码,最小生成树算法,Dijkstra单源最短路径等。

其实对于贪心算法,总结的概括性话并不多。它并不像动态规划那样有我们常说的“一个模型三个特征”,也不像回溯算法那样需要递归。关于这两个 在后续博客中,南国有时间会在继续更新。当然不是所有用贪心算法解决的问题他都是给出了最优解。

接下来,我们就通过一些实战的题目来进行算法具体讲解。

实战习题

1.分配饼干

leetcode 455 Assign Cookies
这是非常经典的一道贪心算法的题,难度不大。

解题思路:给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。

证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说在这个问题中不存在比贪心策略更优的策略,即贪心策略就是最优策略。

所以我们首先对两个数组进行升序排序好,然后尝试着把最小的饼干分配给需求最小的小孩,这样逐次对应,求出最多可以满足多少个小孩。

Java实现代码:

package GreedyAlgorithm;

import java.util.Arrays;

/**
 * leetcode 455 Assign Cookies
 * @author xjh 2019.02.27
 * 贪心算法的典型案例 经常看 经常做!!
 */
public class t455_AssignCookies {
    public int findContentChildren(int[] g, int[] s) {
        //1.首先对两个数组及逆行排序
        Arrays.sort(g);Arrays.sort(s);
        int gi=0,si=0;
        while (gi<g.length&&si<s.length){
            if (g[gi]<=s[si]) gi++; //比较遍历到的最小值
            si++;
        }
        return gi;
    }
}

2.不重叠区间个数

leetcode 435 Non-overlapping Intervals
题目的要求是给定一组区间,找出需要删除的最小区间个数,以使其余的区间不重叠。
解题思路:
想要找到删除空间的最小个数count,对应的就是找到最多能组成不重叠的个数m 这m个区间去填满从最小下标到最大下标的值。count=n-m

在每次选择时,区间的结尾很重要,结尾越小,留给后面的空间越大,则m也会越大,count就越小。

Java实现代码:

package GreedyAlgorithm;

import java.util.Arrays;
import java.util.Comparator;

/**
 * leetcode 435 Non-overlappingIntervals
 * 贪心算法的典型案例:计算让一组区间不重叠所需要移除的区间个数。[经常练习 回顾]
 * 首先需要计算出最多的可不重叠的区间个数m 然后总个数n减去m
 * 计算m 采用贪心算法:我们每次选择的时候,左端点跟前面的已经覆盖的区间不重合的,
    * 右端点又尽量小的,这样可以让剩下的未覆盖区间尽可能的大,就可以放置更多的区间。
 * @author xjh 2019.02.27
 */
public class t435_NonoverlappingIntervals {
     public class Interval {
      int start;
      int end;
      Interval() { start = 0; end = 0; }
      Interval(int s, int e) { start = s; end = e; }
  }
    public int eraseOverlapIntervals(Interval[] intervals) {
         if (intervals.length==0||intervals.length==1) return 0;
         //1.贪心的第一步 首先需要对区间end进行排序 这里是升序
        Arrays.sort(intervals, new Comparator<Interval>() {
            @Override
            public int compare(Interval o1, Interval o2) {
                return o1.end-o2.end;
            }
        });
        //上面这个排序语句 可以用lambda表达式简写为:
        //Arrays.sort(intervals,Comparator.comparingInt(o->o.end));
        //2.计算出m
        int m=1;
        int end=intervals[0].end;
        for (int i=1;i<intervals.length;i++){
            if (intervals[i].start<end) continue;
            end=intervals[i].end;
            m++;
        }
        return intervals.length-m;
    }
}

3.根据身高和序号重组排列

leetcode 406 Queue Reconstruction by Height
题目要求:假设你有一列随机的人在排队。每个人由一对整数(h, k)描述,其中h是这个人的高度,k是这个人前面的高度大于或等于h的人数。编写一个算法来重构队列。

Input:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]

Output:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

解题思路:这个题目有点难度 需要考虑h k两个属性。h大的 k小的应该排在前面的位置,利用贪心的思想,我们先对原来的数据进行排序(h降序 k升序),然后进行顺序插入

Java代码实现:

package GreedyAlgorithm;

import java.util.Arrays;
import java.util.Comparator;

/**
 * leetcode 406 Queue Reconstruction by Height
 * @author xjh 2019.02.28
 * 这道典型的算法题 有一定的难度 需要考虑同时考虑两个属性 h k
 */
public class t406QueueReconstruction {
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0]==o2[0]?o1[1]-o2[1]:o2[0]-o1[0];    //按照h降序 k升序进行排序
            }
        });
        //同理 上面这条语句可缩写为: Arrays.sort(people,(o1,o2)->o1[0]==o2[0]?o1[1]-o2[1]:o2[0]-o1[0]);
//        for (int i=0;i<people.length;i++){
//            for (int j=0;j<2;j++)
//                System.out.print(people[i][j]+",");
//            System.out.print("  ");
//        }//验证得到输出结果为 7,0,  7,1,  6,1,  5,0,  5,2,  4,4,

        //排好序之后进行插入操作 首先将排序后h最大并且k最小的 放在最前面,,然后选出次最大的 由k值进行插入操作 ,如此循环
        //        int[][] res=new int[people.length][2];  //结果数组
//        for (int i=0;i<people.length;i++){
//            int pos=people[i][1];   //k值
//            for (int j=i;j>pos;j--) //当前位置与k值 进行比较
//                res[j]=res[j-1];    //元素后移
//            res[pos]=people[i]; //插入指定位置
//        }
//        return res;

        //一种跟简单的使用方法
        List<int[]> queue = new ArrayList<>();
        for (int[] p : people) {
            queue.add(p[1], p); //原型:add(int index, E element) 在index位置插入元素element
        }
        return queue.toArray(new int[queue.size()][]);
    }

    public static void main(String[] args) {
        t406QueueReconstruction xjh=new t406QueueReconstruction();
        int[][] people={{7,0},{4,4},{7,1},{5,0},{6,1},{5,2}};
        int[][] res=xjh.reconstructQueue(people);
        for (int i=0;i<res.length;i++){
            for (int j=0;j<2;j++)
                System.out.print(res[i][j]+",");
            System.out.print("  ");
        }
    }
}

4.分割字符串使同种字符出现在一起

763. Partition Labels
题干要求:给出了一个由小写字母组成的字符串S。我们希望将这个字符串划分为尽可能多的部分,以便每个字母最多出现在一个部分中,并返回一个表示这些部分大小的整数列表。

测试样例:

Input: S = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation:
The partition is "ababcbaca", "defegde", "hijhklij".
This is a partition so that each letter appears in at most one part.
A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts.

Java实现代码:

package GreedyAlgorithm;

import java.util.ArrayList;
import java.util.List;

/**
 * leetcode 763 Partition Labels
 * @author xjh 2019.02.28
 * 典型的贪心算法  有难度!!
 */
public class t763_763PartitionLabels {
    public List<Integer> partitionLabels(String S) {
    //首先计算出字符串中每个字符的最末尾下标位置
        int[] lastIndex=new int[26];    //表示26个字母最后下标
        for (int i=0;i< S.length();i++)
            lastIndex[charIndex(S.charAt(i))]=i;
            //i是实时字母下标
        List<Integer> loc=new ArrayList<>();    //因为这里事先无法知道要被分割成为多少个子字符串,所以一定是用容器来存储 最终结果
        int first=0;
        //从字符串S首字母开始分割 当该字母当前位置等于对应lastIndex位置时分割
        while (first< S.length()){
            int last=first;
            for (int i=first;i< S.length()&&i<=last;i++){
                int index=lastIndex[charIndex(S.charAt(i))];    //得到他在lastIndex中的位置
                if (index>last) last=index;
            }
            loc.add(last-first+1);  //将当前分割的子字符串加入List中
            first=last+1;
        }
        return loc;
    }
    public int charIndex(char i){
        return i-'a';
    }

    public static void main(String[] args) {
        t763_763PartitionLabels xjh=new t763_763PartitionLabels();
        String s="ababcbacadefegdehijhklij";
        List<Integer>list=xjh.partitionLabels(s);
        for (Integer i:list)
            System.out.print(i+" ");
        System.out.println();
    }
}

5.种植花朵

605. Can Place Flowers
题干要求:种植的花朵至少有一个单位的间隔,求解能否种下n朵花。

测试样例:

Input: flowerbed = [1,0,0,0,1], n = 1
Output: True

Input: flowerbed = [1,0,0,0,1], n = 2
Output: False

解题思路:贪心求解最多能种下多少朵花,然后和n做比较。

Java实现代码:

package GreedyAlgorithm;

/**
 * leetcode 605 Can Place Flowers
 * @author xjh 2019.03.04
 * 判断是否能够种植n朵花:
    * 贪心算法: 首尾相邻两个元素为0时 可种植,中间元素必须相邻3个为0 才能在3者中间元素终止花
 * 快于84.92%
 */
public class t605_CanPlaceFlowers {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int count=0,pre,last;
        for (int i=0;i<flowerbed.length;i++){
            if (flowerbed[i]==1) continue;
            pre=i==0?0:flowerbed[i-1];  //左相邻元素
            last=i==flowerbed.length-1?0:flowerbed[i+1];    //右相邻元素
            if (pre==0&&last==0){
                flowerbed[i]=1;count++;
            }
        }
        return count>=n;
    }
}

6.判断是否为子序列

392. Is Subsequence
792. Number of Matching Subsequences
这两道题是一样的思路,所以放到一起。

392的题干条件:一个字符串的子序列是一个新的字符串,它由原来的字符串组成,删除一些字符(可以是none),而不影响其余字符的相对位置。(例如,“ace”是“abcde”的子序列,而“aec”不是)。

测试样例:

Example 1:
s = "abc", t = "ahbgdc"

Return true.

Example 2:
s = "axc", t = "ahbgdc"

792的题干条件:给出一个字符串S和一个单词字典,找出S的子序列中单词的个数[i]。

Example :
Input: 
S = "abcde"
words = ["a", "bb", "acd", "ace"]
Output: 3
Explanation: There are three words in words that are a subsequence of S: "a", "acd", "ace".

这道题的解题思路很明显,主要考查你对Java indexOf的熟悉程度。

package GreedyAlgorithm;
/**
 * leetcode 392 Is Subquence
 * leetcode 792 Number of Matching Subsequences
 * @author xjh 2019.03.08
 */
public class t392_IsSubquence {
    public boolean isSubsequence(String s, String t) {
        int index=-1;
        for (char c:s.toCharArray()){
            index=t.indexOf(c,index+1);
            //表示从下标index+1开始 查找第一次出现c的位置
            if (index==-1) return false;
        }
        return true;
    }

    public int numMatchingSubseq(String S, String[] words) {
        int count=0;
        for (String str:words){
            int index=-1;
            for (char c:str.toCharArray()){
                index= S.indexOf(c,index+1);
                if (index==-1) break;
            }
            if (index!=-1) count++;
        }
        return count;
    }

    public static void main(String[] args) {
        t392_IsSubquence xjh=new t392_IsSubquence();
//        System.out.println(xjh.isSubsequence("abc","afgsbsjdivc"));
        String s="abcde";
        String[] words={"a","bb","acd","ace"};
        System.out.println(xjh.numMatchingSubseq(s,words));
    }
}

7.修改一个数成为非递减数组

665. Non-decreasing Array
题干条件:判断一个数组是否修改一次后就能成为非递减数组

测试样例:

Example 1:
Input: [4,2,3]
Output: True
Explanation: You could modify the first 4 to 1 to get a non-decreasing array.
Example 2:
Input: [4,2,1]
Output: False
Explanation: You can't get a non-decreasing array by modify at most one element.

观察测试样例 这道题时修改一次 并不局限于交换,而且看example 1发现,他的意思时把4直接改为1.所以,这里考虑的时出现 nums[i-1]>nums[i]时的复制情况。

在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 不影响后续的操作 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。

Java实现代码:

package GreedyAlgorithm;

/**
 * leetcode 665 Non-decreasing Array
 * @author xjh 2019.03.08
 * 贪心算法:当nums[i-1]>nums[i]时考虑两种情况
 * 1.优先考虑变小num[i-1] 因为盲目的nums[i]变大 可能使后面元素出现递减的可能性更高
 * 2.如果nums[i-2]>nums[i] 这是选择变大num[i] 不然的话 就需要变换两次
 * 这道题的解题思想很难想到~~
 */
public class t665_NonDescArray {
    //这道题和605的思路很类似 某个数比较前后两个数的大小关系
    public boolean checkPossibility(int[] nums) {
        int count=0;
        for (int i=1;i<nums.length;i++){
            if (nums[i-1]>nums[i]){ //出现递减关系
                count++;
                if (i-2<0||nums[i-2]<=nums[i]) nums[i-1]=nums[i];   //优先考虑变小nums[i-1]
                else nums[i]=nums[i-1]; //变大nums[i]
            }
        }
        return count<=1;
    }
}

8.子数组的最大和

53. Maximum Subarray
题干条件:给定整数数组号,找到具有最大和的相邻子数组(至少包含一个数字)并返回其和。

测试样例:

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Java实现代码:

package GreedyAlgorithm;

/**
 * leetcode 53 Mximum Subarray
 * @author xjh 2019.03.10
 * 利用两个临时变量存储当前遍历的大于0的数 和已知最大值
 */
public class t53_MaxSubarray {
    public int maxSubArray(int[] nums) {
        if (nums.length==1) return nums[0]; //处理边界条件
        int tSum,maxSum;
        tSum=maxSum=nums[0];
        for (int i=1;i<nums.length;i++){
            tSum=tSum>0?tSum+nums[i]:nums[i];
            maxSum=Math.max(maxSum,tSum);
        }
        return maxSum;
    }

    public static void main(String[] args) {
        t53_MaxSubarray xjh=new t53_MaxSubarray();
        int[] nums={-2,1,-3,4,-1,2,1,-5,4};
        System.out.println(xjh.maxSubArray(nums));
    }
}

9.股票问题

121. Best Time to Buy and Sell Stock
122. Best Time to Buy and Sell Stock II

121题干条件:假设你有一个数组,其中第i个元素是某只股票在第i天的价格。如你最多只获准完成一项交易(即,买一股,卖一股),设计一个算法来寻找最大的利润。注意,你不能在买股票之前卖掉它。

测试样例:

Example 1:

Input: [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
             Not 7-1 = 6, as selling price needs to be larger than buying price.
Example 2:

Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

Java代码实现:

public static int maxProfit(int[] prices) {
        //1.很巧妙的思路 一层循环解决 更新数组里面的最小值 和最大插值
        Integer maxProfit = Integer.MIN_VALUE;
        Integer minPrice = Integer.MAX_VALUE;
        for(Integer price:prices){
            minPrice=Math.min(minPrice,price);
            maxProfit = Math.max(maxProfit,price-minPrice);
        }
        return maxProfit<=0?0:maxProfit;
    }

122题干条件:假设你有一个数组,其中第i个元素是某只股票在第i天的价格。设计一个算法来寻找最大的利润。您可以完成任意数量的事务(即,买进一股,再卖出一股)。

测试样例:

Example 1:

Input: [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
             Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.
Example 2:

Input: [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
             Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are
             engaging multiple transactions at the same time. You must sell before buying again.
Example 3:

Input: [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.

Java代码实现:

public int maxProfit(int[] prices) {
        int profit=0;
       for(int i=1;i<prices.length;i++){
           if(prices[i]>prices[i-1]){
               profit+=prices[i]-prices[i-1];
           }
       }
       return  profit;
       }

小结:
这里南国总结在leetcode里贪心算法应用的10来道题型,有的属于很经典的贪心问题,也有的是可用其他方法来进行解决的题 例如说最后股票问题的两个题 他们也可以用DP来进行解决,关于DP方法的求解 会放到后续的博客里展开描述。因为本篇总结归纳的是贪心算法的知识点。

算法,不同于数据结构有固定的存储结构,他更多的是一种解决问题的思路。 不局限于某种数据结构 也不局限于某种编程语言。 南国也会一直努力,如果更深一步的认知和见解,有时间就会写在博客中 ,相互学习 共同进步。

猜你喜欢

转载自blog.csdn.net/weixin_38073885/article/details/88381566