
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习中,贪心算法是一类非常经典且高频出现的解题思想.它的核心在于:每一步都选择当前看起来最优的方案,并期望通过局部最优最终得到全局最优.虽然这种思想听起来简单,但真正应用到具体题目中时,往往需要我们准确判断"贪心策略"是否成立,以及如何设计合理的选择规则.本文将继续围绕贪心算法展开,通过几个经典实战题目进行深入解析,包括:分发饼干、最优除法、跳跃游戏、跳跃游戏Ⅱ、加油站.这些题目覆盖了排序贪心、区间推进、最远可达范围、步数优化以及环形路径判断等常见场景,能够帮助我们进一步理解贪心算法在不同问题中的应用方式.通过本文的学习,希望你不仅能够掌握这些经典题目的解法,更能逐步建立起分析贪心问题的思维框架:如何寻找局部最优,如何证明贪心选择的合理性,以及如何将复杂问题转化为清晰高效的算法实现.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录
1.分发饼干(OJ题)

解法(贪心):
贪心策略 :先将两个数组排序.针对胃口较小的孩子,从小到大挑选饼干:
i. 如果当前饼干能满足,直接喂(最小的饼干都能满足,不要浪费大饼干);
ii. 如果当前饼干不能满足,放弃这个饼干,去检测下一个饼干(这个饼干连最小胃口的孩子都无法满足,更别提那些胃口大的孩子了).






核心代码
cpp
class Solution
{
public:
//g: 孩子的胃口数组 s: 饼干的尺寸数组
int findContentChildren(vector<int>& g, vector<int>& s)
{
//核心贪心策略:用最小的饼干满足最小的胃口,最大化满足的孩子数量
//1.对孩子胃口数组 升序排序
sort(g.begin(), g.end());
//2.对饼干尺寸数组 升序排序
sort(s.begin(), s.end());
//ret:记录最终能满足的孩子数量
//n:饼干数组的长度,简化后续代码书写
int ret = 0, n = s.size();
//双指针遍历:
//i:指向当前待满足的孩子(从胃口最小的开始)
//j:指向当前尝试分配的饼干(从尺寸最小的开始)
//循环条件:孩子没遍历完 且 饼干没遍历完
for(int i = 0, j = 0; i < g.size() && j < n; i++, j++)
{
//内层循环:找到第一块 尺寸 >= 当前孩子胃口 的饼干
//如果当前饼干太小,就换下一块,直到找到合适的饼干或遍历完所有饼干
while(j < n && s[j] < g[i])
j++;
//如果找到了合适的饼干,满足的孩子数量+1
if(j < n)
ret++;
}
//返回最多能满足的孩子数量
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// g: 孩子的胃口数组 s: 饼干的尺寸数组
int findContentChildren(vector<int>& g, vector<int>& s)
{
// 核心贪心策略:用最小的饼干满足最小的胃口,最大化满足的孩子数量
// 1. 对孩子胃口数组升序排序
sort(g.begin(), g.end());
// 2. 对饼干尺寸数组升序排序
sort(s.begin(), s.end());
// ret:记录最终能满足的孩子数量
// n:饼干数组的长度
int ret = 0, n = s.size();
// 双指针遍历
// i:指向当前待满足的孩子
// j:指向当前尝试分配的饼干
for (int i = 0, j = 0; i < g.size() && j < n; i++, j++)
{
// 找到第一块尺寸 >= 当前孩子胃口的饼干
while (j < n && s[j] < g[i])
j++;
// 如果找到了合适的饼干,满足的孩子数量 +1
if (j < n)
ret++;
}
return ret;
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (size_t i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<pair<vector<int>, vector<int>>> testCases = {
{{1, 2, 3}, {1, 1}}, // 只能满足 1 个孩子
{{1, 2}, {1, 2, 3}}, // 可以满足 2 个孩子
{{1, 2, 3}, {3}}, // 只能满足胃口为 1 的一个孩子
{{1, 1, 1}, {1, 1, 1}}, // 全部满足
{{2, 3, 4}, {1, 1, 1}}, // 一个都不能满足
{{10}, {10}}, // 单个孩子,刚好满足
{{10}, {5}}, // 单个孩子,不能满足
{{}, {1, 2, 3}}, // 没有孩子,结果 0
{{1, 2, 3}, {}}, // 没有饼干,结果 0
{{5, 1, 3}, {1, 2, 3, 4}}, // 无序输入,排序后满足 3 个
{{1, 2, 2, 3}, {1, 1, 2, 2}}, // 包含重复元素,满足 3 个
{{4, 5, 6}, {3, 4, 5, 6}}, // 满足 3 个
{{2, 2, 3}, {1, 2, 2, 2, 3}} // 饼干较多,满足 3 个
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> g = testCases[i].first;
vector<int> s = testCases[i].second;
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "孩子胃口数组 g:";
printVector(g);
cout << endl;
cout << "饼干尺寸数组 s:";
printVector(s);
cout << endl;
cout << "最多能满足的孩子数量:";
cout << sol.findContentChildren(g, s) << endl;
cout << "------------------------" << endl;
}
return 0;
}

2.最优除法(OJ题)

解法(贪心):
贪心策略:在最终的结果中,前两个数的位置是无法改变的.因为每一个数的都是大于等于 2 的,为了让结果更大,我们应该尽可能的把剩下的数全都放在分子上.





核心代码
cpp
class Solution
{
public:
//输入:正整数数组 nums
//输出:能得到最大结果的除法表达式字符串
string optimalDivision(vector<int>& nums)
{
//获取数组的长度
int n = nums.size();
//边界情况1:数组只有1个数字,直接返回该数字的字符串形式
if(n == 1)
{
return to_string(nums[0]);
}
//边界情况2:数组有2个数字,直接拼接为 a/b 即可(无括号必要)
if(n == 2)
{
return to_string(nums[0]) + "/" + to_string(nums[1]);
}
//核心逻辑:数组长度 ≥3 时,构造格式:nums[0]/(nums[1]/nums[2]/.../nums[n-1])
//初始化结果字符串:开头为 第一个数/(第二个数
string ret = to_string(nums[0]) + "/(" + to_string(nums[1]);
//遍历剩余数字(从第三个开始),依次拼接 /数字
for(int i = 2; i < n; i++)
{
ret += "/" + to_string(nums[i]);
}
//最后补上右括号,完成表达式构造
ret += ")";
//返回最终构造的字符串
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution
{
public:
// 输入:正整数数组 nums
// 输出:能得到最大结果的除法表达式字符串
string optimalDivision(vector<int>& nums)
{
// 获取数组的长度
int n = nums.size();
// 边界情况1:数组只有1个数字,直接返回该数字的字符串形式
if (n == 1)
{
return to_string(nums[0]);
}
// 边界情况2:数组有2个数字,直接拼接为 a/b 即可
if (n == 2)
{
return to_string(nums[0]) + "/" + to_string(nums[1]);
}
// 核心逻辑:数组长度 >= 3 时
// 构造格式:nums[0]/(nums[1]/nums[2]/.../nums[n-1])
string ret = to_string(nums[0]) + "/(" + to_string(nums[1]);
// 遍历剩余数字,从第三个开始,依次拼接 /数字
for (int i = 2; i < n; i++)
{
ret += "/" + to_string(nums[i]);
}
// 最后补上右括号
ret += ")";
return ret;
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (size_t i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<vector<int>> testCases = {
{1000, 100, 10, 2}, // 经典示例:1000/(100/10/2)
{2, 3, 4}, // 三个数字:2/(3/4)
{2}, // 只有一个数字:2
{2, 3}, // 两个数字:2/3
{6, 2, 3, 4}, // 四个数字:6/(2/3/4)
{10, 5, 2}, // 三个数字:10/(5/2)
{1, 2, 3, 4, 5}, // 多个数字:1/(2/3/4/5)
{9, 8, 7, 6, 5}, // 多个数字
{100, 10, 5}, // 100/(10/5)
{50, 25}, // 50/25
{7} // 7
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> nums = testCases[i];
cout << "测试用例 " << i + 1 << ":";
printVector(nums);
cout << endl;
cout << "最优除法表达式:";
cout << sol.optimalDivision(nums) << endl;
cout << "------------------------" << endl;
}
return 0;
}

3.跳跃游戏(OJ题)

解法(贪心):
贪心策略 :遍历数组,维护当前能跳跃到的最远位置.
i. 如果当前位置超出了最远可达位置,说明无法到达,直接失败;
ii. 否则更新最远跳跃位置;
iii. 最远位置覆盖最后一个下标,直接成功.





核心代码
cpp
class Solution
{
public:
bool canJump(vector<int>& nums)
{
//变量定义:
//left:当前能到达区间的左边界
//right:当前能到达区间的右边界
//maxPos:遍历后能跳到的最远位置
//n:数组长度,最后一个下标为 n-1
int left = 0, right = 0, maxPos = 0, n = nums.size();
//循环条件:当前区间有效(左边界 ≤ 右边界),说明还有位置可以遍历
while(left <= right)
{
//提前终止:如果最远能到达的位置已经覆盖最后一个下标,直接返回成功
if(maxPos >= n - 1)
{
return true;
}
//遍历当前区间 [left, right] 内的所有位置
//计算每个位置能跳到的最远位置 i + nums[i],更新全局最远位置 maxPos
for(int i = left; i <= right; i++)
{
maxPos = max(maxPos, nums[i] + i);
}
//更新下一轮遍历的区间:
//左边界 = 原右边界 + 1(新的未遍历区域)
//右边界 = 最新的最远可达位置
left = right + 1;
right = maxPos;
}
//循环结束仍未到达最后一个下标,返回失败
return false;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
bool canJump(vector<int>& nums)
{
// 变量定义:
// left:当前能到达区间的左边界
// right:当前能到达区间的右边界
// maxPos:遍历后能跳到的最远位置
// n:数组长度,最后一个下标为 n - 1
int left = 0, right = 0, maxPos = 0, n = nums.size();
// 循环条件:当前区间有效,说明还有位置可以遍历
while (left <= right)
{
// 提前终止:如果最远能到达的位置已经覆盖最后一个下标,直接返回成功
if (maxPos >= n - 1)
{
return true;
}
// 遍历当前区间 [left, right] 内的所有位置
for (int i = left; i <= right; i++)
{
maxPos = max(maxPos, nums[i] + i);
}
// 更新下一轮遍历的区间
left = right + 1;
right = maxPos;
}
// 循环结束仍未到达最后一个下标,返回失败
return false;
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (size_t i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
void printBool(bool result)
{
cout << (result ? "true" : "false");
}
int main()
{
Solution sol;
vector<vector<int>> testCases = {
{2, 3, 1, 1, 4}, // 可以到达最后一个下标
{3, 2, 1, 0, 4}, // 无法越过 0
{0}, // 只有一个元素,已经在终点
{1, 0}, // 可以从 0 跳到 1
{0, 1}, // 无法从 0 跳出
{2, 0, 0}, // 可以直接跳到最后
{1, 1, 1, 1}, // 每次跳一步,可以到达
{4, 0, 0, 0, 0}, // 第一步可以覆盖终点
{1, 2, 0, 1}, // 可以到达
{2, 5, 0, 0}, // 可以到达
{1, 0, 1, 0}, // 卡在下标 1,无法到达
{2, 0, 2, 0, 1}, // 可以到达
{5, 4, 3, 2, 1, 0, 0}, // 无法到达最后一个 0
{} // 空数组,当前代码会返回 true
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> nums = testCases[i];
cout << "测试用例 " << i + 1 << ":";
printVector(nums);
cout << endl;
cout << "是否可以到达最后一个下标:";
printBool(sol.canJump(nums));
cout << endl;
cout << "------------------------" << endl;
}
return 0;
}

4.跳跃游戏Ⅱ(OJ题)

解法(动态规划 + 类似层序遍历):
动态规划 :
a. 状态表示:
dp[i] 表示从 0 位置开始,到达 i 位置时候的最小跳跃次数
b. 状态转移方程:
对于 dp[i],我们遍历 0 ~ i - 1 区间(用指针 j 表示),只要能够从 j 位置跳到 i 位置(nums[j] + j >= i),我们就用 dp[j] + 1 更新 dp[i] 里面的值,找到所有情况下的最小值即可.
类似层序遍历的过程 :
用类似层序遍历的过程,将第 i 次跳跃的起始位置和结束位置找出来,用这次跳跃的情况,更新出下一次跳跃的起始位置和终止位置.
这样循环往复,就能更新出到达 n - 1 位置的最小跳跃步数.





核心代码
cpp
class Solution
{
public:
int jump(vector<int>& nums)
{
//变量定义:
//left:当前遍历区间的左边界
//right:当前遍历区间的右边界
//maxPos:当前能跳跃到的最远位置
//ret:记录最少跳跃次数
//n:数组长度,最后一个下标为 n-1
int left = 0, right = 0, maxPos = 0, ret = 0, n = nums.size();
//循环遍历:当前区间有效(左边界 <= 右边界),说明还有位置可以处理
//兜底判断:防止极端情况无法到达终点
while(left <= right)
{
//提前终止:如果当前最远位置已经覆盖最后一个下标
//直接返回当前的跳跃次数,无需继续遍历
if(maxPos >= n - 1)
{
return ret;
}
//核心逻辑:遍历当前区间 [left, right] 内的所有位置
//计算每个位置能跳到的最远位置,更新全局最远可达位置 maxPos
for(int i = left; i <= right; i++)
{
maxPos = max(maxPos, nums[i] + i);
}
//更新下一轮遍历的区间:
//左边界 = 原右边界 + 1(进入新的一层/区间)
//右边界 = 最新的最远可达位置
left = right + 1;
right = maxPos;
//每完成一层区间的遍历,代表完成一次跳跃,次数+1
ret++;
}
//兜底返回:题目保证一定能到达终点,此情况理论上不会触发
return -1;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int jump(vector<int>& nums)
{
// 变量定义:
// left:当前遍历区间的左边界
// right:当前遍历区间的右边界
// maxPos:当前能跳跃到的最远位置
// ret:记录最少跳跃次数
// n:数组长度,最后一个下标为 n - 1
int left = 0, right = 0, maxPos = 0, ret = 0, n = nums.size();
// 循环遍历:当前区间有效,说明还有位置可以处理
while (left <= right)
{
// 提前终止:如果当前最远位置已经覆盖最后一个下标
if (maxPos >= n - 1)
{
return ret;
}
// 遍历当前区间 [left, right] 内的所有位置
for (int i = left; i <= right; i++)
{
maxPos = max(maxPos, nums[i] + i);
}
// 更新下一轮遍历的区间
left = right + 1;
right = maxPos;
// 每完成一层区间遍历,代表完成一次跳跃
ret++;
}
// 兜底返回:如果无法到达终点,返回 -1
return -1;
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (size_t i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<vector<int>> testCases = {
{2, 3, 1, 1, 4}, // 最少跳 2 次:0 -> 1 -> 4
{2, 3, 0, 1, 4}, // 最少跳 2 次:0 -> 1 -> 4
{1, 1, 1, 1}, // 每次只能跳一步,最少跳 3 次
{0}, // 只有一个元素,不需要跳,结果 0
{1, 0}, // 跳 1 次到终点
{2, 0, 0}, // 跳 1 次直接到终点
{3, 2, 1}, // 跳 1 次可以覆盖终点
{1, 2, 1, 1, 1}, // 最少跳 3 次
{4, 1, 1, 3, 1, 1, 1}, // 最少跳 2 次:0 -> 3 -> 6
{5, 4, 3, 2, 1, 0}, // 跳 1 次可覆盖终点
{2, 1, 2, 1, 1}, // 最少跳 2 次:0 -> 2 -> 4
{1, 2, 3}, // 最少跳 2 次:0 -> 1 -> 2
{3, 0, 0, 0}, // 最少跳 1 次
{1, 0, 1}, // 无法到达,返回 -1
{} // 空数组,当前逻辑返回 0
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> nums = testCases[i];
cout << "测试用例 " << i + 1 << ":";
printVector(nums);
cout << endl;
cout << "最少跳跃次数:";
cout << sol.jump(nums) << endl;
cout << "------------------------" << endl;
}
return 0;
}

5.加油站(OJ题)

解法(暴力解法 -> 贪心):
暴力解法 :
a. 依次枚举所有的起点;
b. 从起点开始,模拟一遍加油的流程
贪心优化 :
我们发现,当从 i 位置出发,走了 step 步之后,如果失败了.那么 [i, i + step] 这个区间内任意一个位置作为起点,都不可能环绕一圈.
因此我们枚举的下一个起点,应该是 i + step + 1.





核心代码
cpp
class Solution
{
public:
//gas:每个加油站的汽油量 cost:到达下一站的耗油量
int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
{
int n = gas.size(); //加油站的总数量
//外层循环:依次枚举每一个加油站作为起始点
for(int i = 0; i < n; i++)
{
int rest = 0; //记录当前剩余油量(净收益:加油量-耗油量)
int step = 0; //记录从起点出发已经行走的步数
//内层循环:尝试从起点i出发,绕行一圈
for( ; step < n; step++)
{
//环形路线计算当前到达的加油站下标
int index = (i + step) % n;
//更新剩余油量:加上当前加油站油量,减去到下一站的耗油量
rest = rest + gas[index] - cost[index];
//剩余油量 < 0,无法继续前行,跳出当前起点的尝试
if(rest < 0) break;
}
//成功走完n步(绕行一圈),返回当前起点i
if(rest >= 0) return i;
//贪心优化核心:
//从i到i+step之间的所有点都无法作为起点,直接跳过,减少循环次数
i = i + step;
}
//遍历完所有起点都无法绕行一周,返回-1
return -1;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
// gas:每个加油站的汽油量 cost:到达下一站的耗油量
int canCompleteCircuit(vector<int>& gas, vector<int>& cost)
{
int n = gas.size(); // 加油站的总数量
// 外层循环:依次枚举每一个加油站作为起始点
for (int i = 0; i < n; i++)
{
int rest = 0; // 记录当前剩余油量
int step = 0; // 记录从起点出发已经行走的步数
// 内层循环:尝试从起点 i 出发,绕行一圈
for (; step < n; step++)
{
// 环形路线计算当前到达的加油站下标
int index = (i + step) % n;
// 更新剩余油量
rest = rest + gas[index] - cost[index];
// 剩余油量 < 0,无法继续前行
if (rest < 0)
break;
}
// 成功走完 n 步,返回当前起点 i
if (rest >= 0)
return i;
// 从 i 到 i + step 之间的所有点都无法作为起点,直接跳过
i = i + step;
}
return -1;
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (size_t i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<pair<vector<int>, vector<int>>> testCases = {
{{1, 2, 3, 4, 5}, {3, 4, 5, 1, 2}}, // 可以从下标 3 出发
{{2, 3, 4}, {3, 4, 3}}, // 无法绕行一周
{{5, 1, 2, 3, 4}, {4, 4, 1, 5, 1}}, // 可以从下标 4 出发
{{3, 3, 4}, {3, 4, 4}}, // 总油量小于总消耗,返回 -1
{{1}, {1}}, // 单个加油站,刚好可以
{{2}, {3}}, // 单个加油站,不可以
{{4}, {3}}, // 单个加油站,可以
{{6, 1, 4, 3, 5}, {3, 8, 2, 4, 2}}, // 可以从下标 2 出发
{{2, 2, 2}, {2, 2, 2}}, // 每站刚好抵消,可以从下标 0 出发
{{1, 0, 1, 0, 1}, {0, 1, 0, 1, 0}}, // 可以从下标 0 出发
{{0, 0, 0}, {0, 0, 0}}, // 全为 0,可以从下标 0 出发
{{10, 1, 1, 1}, {1, 2, 2, 2}}, // 可以从下标 0 出发
{{1, 1, 10, 1}, {2, 2, 1, 2}}, // 可以从下标 2 出发
{{}, {}} // 空数组,返回 -1
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> gas = testCases[i].first;
vector<int> cost = testCases[i].second;
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "gas 数组:";
printVector(gas);
cout << endl;
cout << "cost 数组:";
printVector(cost);
cout << endl;
cout << "可完成环路的起点下标:";
cout << sol.canCompleteCircuit(gas, cost) << endl;
cout << "------------------------" << endl;
}
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容的更新【贪心算法】(经典实战应用解析(五):单调递增的数字、坏了的计算器、合并区间、⽆重叠区间、⽤最少数量的箭引爆⽓球)
每日心灵鸡汤: 回头看,轻舟已过万重山;向前看,长路漫漫亦灿灿!
很喜欢一句话:"山海自有归期,风雨自有相逢,意难平终将和解,万事终将如意."这个世界上不存在无法治愈的伤痛,没有不能结束的沉沦,所有失去的会以另一种归来.那些我们现在非常在意的事情,久久放不下的人,可能多年以后再回头,会觉得根本不算什么.就像现在的你不会再纠结以前的事情一样,曾经放不下的人和事,到最后,岁月终究会教你学会轻描淡写.一切都是瞬息,一切都将会过去,调整好心态,别纠结于得失上,请你一定相信,如果事与愿违,那一定是另有安排!
