当前位置: 首页 > news >正文

从零掌握贪心算法Java版:LeetCode 10题实战解析(上)


目录

1.前言

插播一条消息~

2正文

2.1概念讲解

2.2.题目奉上

2.2.1柠檬水找零

2.2.2将数组和减半的最少操作次数

2.2.3最大数

2.2.4摆动序列

2.2.5最长递增子序列

2.2.6递增的三元子序列

2.2.7最长连续递增序列

2.2.8买卖股票的最佳时机

2.2.9买卖股票的最佳时机 II

2.2.10K次取反后最大化的数组和

3.小结


1.前言

在算法世界里,有一种思想如同生活中的"见好就收"——每次做出当前看来最优的选择,寄希望于通过局部最优达成全局最优。这种思想就是贪心算法,它以其简洁高效的特点,成为解决最优问题的利器。今天我们就来系统学习贪心算法的核心思想,并通过10道LeetCode经典题目实战演练,带你掌握这种"步步为营"的解题思维。


插播一条消息~

🔍十年经验淬炼 · 系统化AI学习平台推荐

系统化学习AI平台https://www.captainbed.cn/scy/

  • 📚 完整知识体系:从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
  • 💻 实战为王:每小节配套可运行代码案例(提供完整源码)
  • 🎯 零基础友好:用生活案例讲解算法,无需担心数学/编程基础

🚀 特别适合

  • 想系统补强AI知识的开发者
  • 转型人工智能领域的从业者
  • 需要项目经验的学生

2正文

2.1概念讲解

什么是贪心算法?

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。它的核心思想可以概括为:局部最优策略,逐步逼近全局最优

举个生活中的例子:如果你要从家里出发去公司,贪心策略就是每次选择眼前最近的路口前进,而不考虑长远路线;而动态规划则会规划所有可能路线后选择最优解。

贪心算法的基本要素

实现贪心算法需要满足两个关键条件:

  1. 贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择来达到
  2. 最优子结构性质:问题的最优解包含子问题的最优解

贪心与动态规划的对比

特性贪心算法动态规划
决策方式自顶向下,每次做局部最优选择自底向上,存储子问题解
子问题关系独立无关联重叠且依赖
适用场景有明显贪心选择性质的问题多阶段决策问题
时间复杂度通常为O(n)或O(nlogn)通常为O(n²)或O(n³)
空间复杂度通常为O(1)或O(n)通常为O(n)或O(n²)

贪心算法的一般流程


2.2.题目奉上

2.2.1柠檬水找零

问题描述:在柠檬水摊上,每一杯柠檬水的售价为5美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付5美元。一开始你手头没有任何零钱。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

贪心策略:优先使用大额钞票找零,保留更多小额钞票以应对未来可能的找零需求。

代码解析

public boolean lemonadeChange(int[] bills) {int five = 0, ten = 0, twenty = 0; // 记录三种钞票的数量int n = bills.length;for(int i = 0; i < n; i++){if(bills[i] == 5){// 收5美元,不用找零,直接增加5美元钞票数量five++;}else if(bills[i] == 10){// 收10美元,需要找5美元ten++;if(five < 1){ // 如果没有5美元钞票,无法找零return false;}five--; // 用一张5美元找零}else{// 收20美元,有两种找零方式:1张10+1张5,或3张5twenty++;if(ten > 0 && five > 0){ // 优先使用10+5的组合,保留更多5美元five--;ten--;}else if(five > 2){ // 其次使用3张5美元five -= 3;} else{ // 两种方式都无法找零return false;}}}return true; // 所有顾客都能正确找零
}

关键思路:20美元找零时,优先消耗10美元钞票,因为5美元钞票在后续找零中更灵活。这就像生活中我们钱包里如果有小面额钞票,会尽量先用大面额付款,保留小面额以备不时之需。

复杂度分析:时间复杂度O(n),空间复杂度O(1),其中n为顾客数量。


2.2.2将数组和减半的最少操作次数

问题描述:给你一个正整数数组 nums 。每一次操作中,你可以选择 nums 中 任意 一个元素并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的元素继续执行操作)。请你返回将数组和至少减少一半的 最少 操作数。

贪心策略:每次选择当前数组中最大的元素进行减半,这样可以最快地减少数组总和。

代码解析

public int halveArray(int[] nums) {// 使用大根堆(优先级队列)存储数组元素,默认是小根堆,通过比较器实现大根堆PriorityQueue<Double> pq = new PriorityQueue<>((a, b) -> b.compareTo(a));double sum = 0.0; // 数组总和// 初始化大根堆并计算总和for (int num : nums) {pq.offer((double) num);sum += num;}sum /= 2.0; // 需要减少到的目标总和(原总和的一半)int ans = 0; // 操作次数// 每次取出最大元素减半,直到总和减少至少一半while (sum > 0.0) {double max = pq.poll() / 2.0; // 取出最大元素并减半sum -= max; // 总和减少max(因为我们将元素减少了max)pq.offer(max); // 将减半后的元素放回堆中ans++; // 操作次数加1}return ans;
}

生活类比:这就像我们有一堆不同大小的石头,要最快地将石头总量减少一半,每次肯定会先搬最大的那块石头。

复杂度分析:时间复杂度O(nlogn),其中n为数组长度,每次堆操作需要O(logn)时间;空间复杂度O(n),用于存储堆。


2.2.3最大数

问题描述:给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

贪心策略:自定义排序规则,对于两个数字a和b,比较ab和ba哪个组合更大,从而决定它们的排列顺序。

代码解析

public String largestNumber(int[] nums) {// 将整数数组转换为字符串数组String[] strs = new String[nums.length];for(int i = 0; i < nums.length; i++){strs[i] = nums[i] + "";}// 自定义排序规则:比较b+a和a+b的大小Arrays.sort(strs, (a, b) -> (b + a).compareTo(a + b));// 拼接排序后的字符串StringBuilder sb = new StringBuilder();for(String str : strs){sb.append(str);}// 特殊情况处理:如果结果以0开头,说明所有数字都是0if(sb.charAt(0) == '0'){return "0";}return sb.toString();
}

关键思路:排序规则是本题的核心。例如比较3和30:330 vs 303,显然330更大,所以3应该排在30前面。这种局部最优的比较最终会得到全局最优的结果。

复杂度分析:时间复杂度O(nlogn),主要是排序操作;空间复杂度O(n),用于存储字符串数组。


2.2.4摆动序列

问题描述:如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。给定一个整数序列,返回作为摆动序列的最长子序列的长度。

贪心策略:记录序列中的上升和下降趋势变化,每当趋势发生变化时,增加摆动序列长度。

代码解析

public int wiggleMaxLength(int[] nums) {int n = nums.length;if(n < 2) return n; // 少于两个元素的序列是摆动序列int ans = 0; // 摆动次数int left = 0; // 前一个差值for(int i = 0; i < n - 1; i++){int right = nums[i + 1] - nums[i]; // 当前差值if(right == 0) continue; // 差值为0,不影响摆动序列// 如果当前差值与前一个差值异号(或前一个差值为0),说明发生了摆动if(left * right <= 0){ans++;left = right; // 更新前一个差值}}return ans + 1; // 摆动次数+1就是序列长度
}

示例分析:以序列[1,7,4,9,2,5]为例,差值序列为[6,-3,5,-7,3],摆动发生在6→-3、-3→5、5→-7、-7→3,共4次摆动,所以序列长度为5。

复杂度分析:时间复杂度O(n),空间复杂度O(1)。


2.2.5最长递增子序列

问题描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

贪心策略:维护一个递增序列,对于每个新元素,若大于序列最后一个元素则加入,否则替换序列中第一个大于等于它的元素,这样可以为后续元素留出更多空间。

代码解析

public int lengthOfLIS(int[] nums) {ArrayList<Integer> list = new ArrayList<>();int n = nums.length;list.add(nums[0]); // 初始化序列for (int i = 1; i < n; i++) {if(nums[i] > list.get(list.size() - 1)){// 当前元素大于序列最后一个元素,直接加入list.add(nums[i]);}else {// 二分查找找到第一个大于等于nums[i]的元素位置int left = 0, right = list.size() - 1;while (left < right) {int mid = (left + right) / 2;if(list.get(mid) < nums[i]){left = mid + 1;}else {right = mid;}}// 替换该位置的元素list.set(left, nums[i]);}}return list.size();
}

生活类比:这就像我们在玩俄罗斯方块,当新方块无法放在当前列时,我们会找到最合适的列放置,以留出更多空间给后续方块。

复杂度分析:时间复杂度O(nlogn),其中n为数组长度,二分查找需要O(logn)时间;空间复杂度O(n),用于存储序列。


2.2.6递增的三元子序列

问题描述:给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。

贪心策略:维护两个变量a和b,分别表示递增子序列的第一个和第二个元素,不断更新这两个值,使其尽可能小,为第三个元素的出现创造条件。

代码解析

public boolean increasingTriplet(int[] nums) {if(nums.length < 3) return false; // 数组长度小于3,直接返回falseint a = nums[0], b = Integer.MAX_VALUE; // a为第一个元素,b为第二个元素for(int i = 1; i < nums.length; i++){if(nums[i] > b){// 找到第三个元素,返回truereturn true;}else if(nums[i] > a){// 更新b为更小的值,为后续元素留出空间b = nums[i];}else{// 更新a为更小的值a = nums[i];}}return false;
}

关键思路:这个算法的巧妙之处在于,a和b并不一定是最终三元组的前两个元素,但它们的存在表明已经有一个长度为2的递增序列,且我们在不断优化这个序列,使其更容易找到第三个元素。

复杂度分析:时间复杂度O(n),空间复杂度O(1)。


2.2.7最长连续递增序列

问题描述:给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。

贪心策略:遍历数组,记录当前连续递增序列的起始位置,当序列不再递增时,更新起始位置并计算当前序列长度。

代码解析

public int findLengthOfLCIS(int[] nums) {int n = nums.length;if (n == 1) return 1; // 数组长度为1,返回1int ans = 1; // 最长序列长度int left = 0; // 当前序列起始位置int right = 1; // 当前位置while(right < n) {if(nums[right] > nums[right - 1]) {// 连续递增,更新最长序列长度ans = Math.max(ans, right - left + 1);right++;} else {// 序列中断,更新起始位置left = right;right++;}}return ans;
}

示例分析:以数组[1,3,5,4,7]为例,连续递增序列有[1,3,5](长度3)和[4,7](长度2),所以最长为3。

复杂度分析:时间复杂度O(n),空间复杂度O(1)。


2.2.8买卖股票的最佳时机

问题描述:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

贪心策略:遍历数组,记录当前最低价格,计算每天卖出的利润,取最大值。

代码解析

public int maxProfit(int[] prices) {int ans = 0; // 最大利润int minM = Integer.MAX_VALUE; // 最低价格for(int i = 0; i < prices.length; i++) {// 计算今天卖出的利润,并更新最大利润ans = Math.max(ans, prices[i] - minM);// 更新最低价格minM = Math.min(minM, prices[i]);}return ans;
}

生活类比:这就像我们炒股,每天都看看今天的价格和历史最低价相比能赚多少,同时记录新的最低价。

复杂度分析:时间复杂度O(n),空间复杂度O(1)。


2.2.9买卖股票的最佳时机 II

问题描述:给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

贪心策略:只要后一天价格高于前一天,就进行一次交易,赚取差价。

代码解析

public int maxProfit(int[] prices) {int ans = 0; // 总利润// 遍历数组,只要后一天价格高于前一天,就进行交易for (int i = 0; i < prices.length - 1; i++) {if (prices[i] < prices[i + 1]) {ans += prices[i + 1] - prices[i];}}return ans;
}

示例分析:以 prices = [7,1,5,3,6,4] 为例,可进行的交易为(1→5)赚4,(3→6)赚3,总利润7。

复杂度分析:时间复杂度O(n),空间复杂度O(1)。


2.2.10K次取反后最大化的数组和

问题描述:给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好 k 次。可以多次选择同一个下标。以这种方式修改数组后,返回数组可能的最大和。

贪心策略:优先反转绝对值最大的负数,若没有负数则反转最小的正数(如果k为奇数)。

代码解析

public int largestSumAfterKNegations(int[] nums, int k) {int ans = 0;int n = nums.length;// 计算初始总和for(int i = 0; i < n; i++) {ans += nums[i];}// 排序数组,便于找到绝对值最大的负数Arrays.sort(nums);// 优先反转绝对值最大的负数for(int i = 0; i < n; i++) {if(nums[i] < 0 && k > 0) {ans -= 2 * nums[i]; // 总和增加-2*nums[i](因为nums[i]变为正数)nums[i] = -nums[i]; // 反转该数k--; // 剩余反转次数减1} else {break; // 遇到非负数,跳出循环}}// 如果还有剩余反转次数,且为奇数,反转最小的数if(k % 2 == 1) {// 重新排序找到最小的数Arrays.sort(nums);ans -= 2 * nums[0]; // 总和减少2*nums[0](因为nums[0]变为负数)}return ans;
}

生活类比:这就像我们有一堆债务(负数)和资产(正数),要在有限的反转次数内最大化总资产,肯定会先把最大的债务转为资产,最后如果还有次数,就把最小的资产转为债务。

复杂度分析:时间复杂度O(nlogn),主要是排序操作;空间复杂度O(1)(不考虑排序所需空间)。


3.小结

贪心算法是一种简单而强大的算法思想,它通过每一步的局部最优选择,逐步构建全局最优解。本文我们学习了贪心算法的基本概念、适用场景,并通过10道LeetCode题目实战演练了不同类型的贪心策略:

  1. 资源分配类:如柠檬水找零,通过合理分配资源(钞票)实现目标
  2. 排序比较类:如最大数,通过自定义排序规则实现最优排列
  3. 序列处理类:如摆动序列、最长递增子序列,通过维护特定序列特性求解
  4. 优化选择类:如买卖股票系列,通过寻找最佳时机实现最大收益

掌握贪心算法的关键在于:

  • 能够识别问题是否具有贪心选择性质
  • 找到合适的贪心策略
  • 证明该策略能够得到全局最优解

贪心算法虽然简单,但在很多问题上能提供高效的解决方案。不过需要注意的是,并非所有问题都适合用贪心算法,有些问题看似可以用贪心解决,实则需要动态规划等其他方法。在实际应用中,我们需要根据问题特性选择合适的算法。

希望通过本文的学习,你能对贪心算法有更深入的理解,并能在实际编程中灵活运用。在下篇中,我们将探讨更多复杂的贪心算法问题,敬请期待!

http://www.dtcms.com/a/496173.html

相关文章:

  • matlab_学习_均分数据
  • 深圳免费建站山东省建设节能协会网站
  • 青岛万维网站设计珠宝类企业网站(手机端)
  • 【展厅多媒体】触摸查询一体机实现数据可视化
  • linux学习笔记(37)mysql视图详解
  • 自定义配置小程序tabbar逻辑思路
  • 临沂网站制作网站微信商城搭建
  • 月牙河做网站公司搜索引擎排名规则
  • 多字节串口收发IP设计(七)串口接收模块合并及排故(含源码)
  • 建立网站的意义哪个建站平台较好
  • 如何进行坡度分析
  • 做网站源代码网页制作模板
  • VexIR2Vec : An Architecture-Neutral Embedding Framework for Binary Similarity
  • 判断链表是否为回文
  • 知名设计网站公司想学网络运营怎么开始
  • AI产品经理学习笔记4 - Agent的技术框架
  • 中国住房和建设部网站首都之窗官网
  • 淘宝买cdk自己做网站湖北手机网站制作
  • JavaSE面向对象(下)
  • 网站怎么做前台跟后台的接口小说网站推广方式
  • Node.js v25 重磅发布!革新与飞跃:深入探索 JavaScript 运行时的未来
  • 2一、u-boot下载编译
  • C++ MFC控件实现小型通讯录
  • 东莞网站优化一般多少钱深圳网站seo优化公司
  • 免费制作app生成器网站馆陶网站建设电话
  • 从发币到行为经济:BSC 发币工具演化的下一站
  • 优秀企业网站wordpress导航怎么设置
  • 自己的主机做服务器网站如何备案企业融资流程
  • 通过强化学习让多模态大模型自主决策图像token压缩的新范式-VisionThink实现思路及奖励函数设计
  • 【C++】深入理解vector(1):vector的使用和OJ题