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

在算法学习中,贪心算法 是一类非常重要、也非常容易"看起来简单,写起来容易出错"的思想.它的核心思路是在每一步选择中,都做出当前看来最优的决定,并希望通过一系列局部最优选择,最终得到全局最优解.不过,贪心算法并不是"见到最优就选"这么简单.真正的难点在于:如何判断当前选择是否会影响后续结果?如何证明局部最优能够推出全局最优? 这也是很多人在刷题时容易困惑的地方.本文将围绕几个经典实战题目展开,包括 柠檬水找零、将数组和减半的最少操作次数、最大数、摆动序列.这些题目分别对应了贪心算法中不同的应用场景:有的考察局部资源分配,有的结合优先队列寻找最大收益,有的通过自定义排序确定最优顺序,还有的通过趋势变化统计最优子序列长度.通过这几道题,我们不仅能理解贪心算法在实际问题中的使用方式,还能进一步体会到:贪心并不是固定模板,而是一种根据题目性质灵活选择当前最优策略的思维方式.希望通过本文的解析,能够帮助大家建立起对贪心算法更清晰的认识,并在后续遇到类似问题时,能够快速找到突破口.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录
1.贪心算法背景介绍
1.贪心算法概念
**贪心算法(Greedy Algorithm)**是一种在每一步选择中都采取当前最优解(局部最优)的算法策略,希望通过局部最优选择来达到整体最优(全局最优)或接近全局最优的结果.
特点:
- 局部最优:每次选择时,只考虑当前状态下最优的选择.
- 不可回溯:一旦做出选择,就不再回退.
- 快速:通常时间复杂度低于动态规划.
2.背景与发展
-
算法思想起源
贪心算法的思想早在数学优化问题中就已出现,例如:
- 最短路径问题(如 Dijkstra 算法)
- 最小生成树问题(如 Prim 和 Kruskal 算法)
这些问题中,局部最优的选择往往可以得到全局最优解.
-
发展历程
- 20 世纪 50 年代:图论和运筹学中开始广泛使用贪心策略.
- 20 世纪 70~80 年代:计算机科学中算法设计理论逐步成熟,贪心算法被系统化.
- 现代应用:数据压缩、调度问题、网络路由、图算法等都使用贪心算法.
3.贪心算法的基本思路
初始化一个解的集合.
在当前可选择的候选中选择一个局部最优的元素.
将该元素加入解集合,并更新候选集合.
重复步骤 2~3,直到得到最终解.
关键问题:
- 贪心选择性质(Greedy Choice Property):局部最优选择可以构成全局最优解.
- 最优子结构(Optimal Substructure):问题的整体最优解包含其子问题的最优解.
4.常见应用场景
| 问题类型 | 示例 |
|---|---|
| 图论 | 最小生成树(Prim、Kruskal)、最短路径(Dijkstra) |
| 排序与调度 | 活动选择问题、任务调度问题 |
| 数学与组合优化 | 背包问题(分数背包)、霍夫曼编码 |
| 网络与通信 | 最优路由选择、数据压缩 |
总结:贪心算法是一种简单高效的算法思想,它通过每步选择局部最优解来构建全局解.适用于那些具有"贪心选择性质"和"最优子结构"的问题.

2.柠檬水找零(OJ题)

解法(贪心):
贪心策略:
分情况讨论:
a. 遇到 5 元钱,直接收下;
b. 遇到 10 元钱,找零 5 元钱之后,收下;
c. 遇到 20 元钱:
i. 先尝试凑 10 + 5 的组合;
ii. 如果凑不出来,拼凑 5 + 5 + 5 的组合;





核心代码
cpp
class Solution
{
public:
//函数功能:判断是否能为所有顾客正确找零
//参数:bills 数组,存储每个顾客支付的钞票(仅 5/10/20 元)
//返回值:true=能正确找零,false=无法找零
bool lemonadeChange(vector<int>& bills)
{
//仅需统计 5元、10元 的数量(20元无法用于找零,无需统计)
int five = 0; //手中 5元 钞票的数量
int ten = 0; //手中 10元 钞票的数量
//遍历每一位顾客支付的钞票
for(auto x : bills)
{
//情况1:顾客支付 5元
//柠檬水5元,无需找零,直接收下
if(x == 5)
five++;
//情况2:顾客支付 10元
//需要找零 5元,必须有至少一张5元
else if(x == 10)
{
//没有5元,无法找零,直接返回false
if(five == 0)
return false;
//找零1张5元,5元数量-1;收下10元,10元数量+1
five--;
ten++;
}
//情况3:顾客支付 20元
//需要找零 15元,核心贪心策略:优先用1张10+1张5,其次用3张5
else
{
//优先方案:有10元也有5元 → 用1张10+1张5找零(贪心关键)
//原因:10元只能给20元找零,5元通用性更强,要尽量保留5元
if(ten && five)
{
ten--;
five--;
}
//备用方案:没有10元,用3张5元找零
else if(five >= 3)
{
five -= 3;
}
//两种方案都不满足,无法找零,返回false
else
return false;
}
}
//遍历完所有顾客,都成功找零,返回true
return true;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
// 函数功能:判断是否能为所有顾客正确找零
// 参数:bills 数组,存储每个顾客支付的钞票(仅 5/10/20 元)
// 返回值:true=能正确找零,false=无法找零
bool lemonadeChange(vector<int>& bills)
{
// 仅需统计 5元、10元 的数量(20元无法用于找零,无需统计)
int five = 0; // 手中 5元 钞票的数量
int ten = 0; // 手中 10元 钞票的数量
// 遍历每一位顾客支付的钞票
for(auto x : bills)
{
// 情况1:顾客支付 5元
// 柠檬水5元,无需找零,直接收下
if(x == 5)
five++;
// 情况2:顾客支付 10元
// 需要找零 5元,必须有至少一张5元
else if(x == 10)
{
// 没有5元,无法找零,直接返回false
if(five == 0)
return false;
// 找零1张5元,5元数量-1;收下10元,10元数量+1
five--;
ten++;
}
// 情况3:顾客支付 20元
// 需要找零 15元,核心贪心策略:优先用1张10+1张5,其次用3张5
else
{
// 优先方案:有10元也有5元 → 用1张10+1张5找零
if(ten && five)
{
ten--;
five--;
}
// 备用方案:没有10元,用3张5元找零
else if(five >= 3)
{
five -= 3;
}
// 两种方案都不满足,无法找零,返回false
else
return false;
}
}
// 遍历完所有顾客,都成功找零,返回true
return true;
}
};
void printBills(const vector<int>& bills)
{
cout << "[";
for(size_t i = 0; i < bills.size(); i++)
{
cout << bills[i];
if(i != bills.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
// 测试用例
vector<vector<int>> testCases = {
{5, 5, 5, 10, 20}, // true
{5, 5, 10, 10, 20}, // false
{5, 5, 10}, // true
{10, 5, 5}, // false
{5, 5, 5, 5, 20, 20}, // true
{5, 10, 20}, // false
{5, 5, 5, 10, 10, 20}, // true
{5, 5, 5, 20}, // true
{5, 20}, // false
{} // true,空数组,没有顾客
};
// 期望结果
vector<bool> expected = {
true,
false,
true,
false,
true,
false,
true,
true,
false,
true
};
// 执行测试
for(size_t i = 0; i < testCases.size(); i++)
{
vector<int> bills = testCases[i];
bool result = sol.lemonadeChange(bills);
cout << "测试用例 " << i + 1 << ": ";
printBills(bills);
cout << endl;
cout << "输出结果: " << (result ? "true" : "false") << endl;
cout << "期望结果: " << (expected[i] ? "true" : "false") << endl;
if(result == expected[i])
cout << "测试通过";
else
cout << "测试失败";
cout << endl << "------------------------" << endl;
}
return 0;
}

3.将数组和减半的最少操作次数(OJ题)

解法(贪心):
贪心策略:
a. 每次挑选出当前数组中最大的数,然后减半;
b. 直到数组和减少到至少一半为止.
为了快速挑选出数组中最大的数,我们可以利用堆这个数据结构.





核心代码
cpp
class Solution
{
public:
//函数功能:计算将数组总和减少到原来的一半所需的最少操作次数
//规则:每次操作可以选择任意一个数字,将其减少一半
//参数:nums 输入的整数数组
//返回值:最少操作次数
int halveArray(vector<int>& nums)
{
//定义大根堆(优先队列):每次能自动取出堆中最大的元素
//贪心核心:每次减半最大的数,能最快让总和减半
priority_queue<double> heap;
//sum:存储数组的原始总和(用double避免精度问题)
double sum = 0.0;
//第一步:遍历数组,将所有元素加入大根堆,并计算数组总和
for(int x : nums)
{
heap.push(x); //把当前数字放入堆中
sum += x; //累加计算数组总大小
}
//第二步:计算目标值:我们需要把总和减少到原来的 1/2
sum /= 2.0;
//count:记录操作次数
int count = 0;
//第三步:循环操作,直到减少的总和 >= 原始总和的一半
while(sum > 0)
{
//取出堆顶的**最大数字**,将其减半
double t = heap.top() / 2.0;
heap.pop(); //弹出堆顶的原数字
sum -= t; //总和减去减半的数值(相当于完成了一次有效缩减)
count++; //操作次数 +1
heap.push(t); //把减半后的数字重新放回堆中,参与下一次比较
}
//返回最少操作次数
return count;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
class Solution
{
public:
// 函数功能:计算将数组总和减少到原来的一半所需的最少操作次数
// 规则:每次操作可以选择任意一个数字,将其减少一半
// 参数:nums 输入的整数数组
// 返回值:最少操作次数
int halveArray(vector<int>& nums)
{
// 定义大根堆(优先队列):每次能自动取出堆中最大的元素
// 贪心核心:每次减半最大的数,能最快让总和减半
priority_queue<double> heap;
// sum:存储数组的原始总和(用 double 避免精度问题)
double sum = 0.0;
// 第一步:遍历数组,将所有元素加入大根堆,并计算数组总和
for(int x : nums)
{
heap.push(x);
sum += x;
}
// 第二步:计算目标值:需要把总和减少原始总和的一半
sum /= 2.0;
// count:记录操作次数
int count = 0;
// 第三步:循环操作,直到减少的总和 >= 原始总和的一半
while(sum > 0)
{
// 取出堆顶的最大数字,并将其减半
double t = heap.top() / 2.0;
heap.pop();
// 本次操作减少了 t
sum -= t;
count++;
// 把减半后的数字重新放入堆中
heap.push(t);
}
return count;
}
};
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 = {
{5, 19, 8, 1}, // 经典示例,答案 3
{3, 8, 20}, // 经典示例,答案 3
{1}, // 只有一个数,1 -> 0.5,答案 1
{10}, // 10 -> 5,答案 1
{1, 1}, // 总和 2,需要减少 1,操作两次,答案 2
{4, 4, 4, 4}, // 总和 16,需要减少 8,操作 4 次
{100, 1, 1}, // 优先不断减半 100,答案 2
{6, 6, 6}, // 总和 18,需要减少 9,答案 3
{10, 20, 30}, // 总和 60,需要减少 30,答案 3
{1, 2, 3, 4, 5} // 普通测试,答案 5
};
vector<int> expected = {
3,
3,
1,
1,
2,
4,
2,
3,
3,
5
};
for(size_t i = 0; i < testCases.size(); i++)
{
vector<int> nums = testCases[i];
int result = sol.halveArray(nums);
cout << "测试用例 " << i + 1 << ": ";
printVector(nums);
cout << endl;
cout << "输出结果: " << result << endl;
cout << "期望结果: " << expected[i] << endl;
if(result == expected[i])
cout << "测试通过";
else
cout << "测试失败";
cout << endl << "------------------------" << endl;
}
return 0;
}

4.最大数(OJ题)

解法(贪心):
可以先优化:
将所有的数字当成字符串处理,那么两个数字之间的拼接操作以及比较操作就会很方便.
贪心策略:
按照题目的要求,重新定义一个新的排序规则,然后排序即可.
排序规则:
a. A 拼接B大于B拼接 A,那么A 在前,B在后;
b. A 拼接B等于B拼接 A,那么AB的顺序无所谓;
c. A 拼接B小于B拼接 A,那么B在前,A 在后;





核心代码
cpp
class Solution
{
public:
//函数功能:返回拼接后的最大数字(字符串形式)
//参数:nums 非负整数数组
//返回值:拼接成的最大数字字符串
string largestNumber(vector<int>& nums)
{
//第一步:将所有整数转换为字符串
//原因:直接比较数字大小无法得到正确拼接顺序,字符串拼接后比较更直观
vector<string> strs;
for(int x : nums)
strs.push_back(to_string(x));
//第二步:自定义排序规则(核心贪心逻辑)
//排序规则:对于两个字符串 s1、s2
//比较 s1+s2 和 s2+s1 的大小,选择更大的组合排在前面
sort(strs.begin(), strs.end(), [](const string& s1, const string& s2)
{
//降序排列:s1+s2 更大,s1 就排在前面
return s1 + s2 > s2 + s1;
});
//第三步:将排序后的字符串依次拼接,得到最终结果
string ret;
for(auto& s : strs)
ret += s;
//第四步:处理特殊边界情况
//如果结果以 0 开头,说明数组全是 0,直接返回 "0"(避免返回 "0000")
if(ret[0] == '0')
return "0";
//返回最终拼接的最大数
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 函数功能:返回拼接后的最大数字(字符串形式)
// 参数:nums 非负整数数组
// 返回值:拼接成的最大数字字符串
string largestNumber(vector<int>& nums)
{
// 第一步:将所有整数转换为字符串
vector<string> strs;
for (int x : nums)
strs.push_back(to_string(x));
// 第二步:自定义排序规则(核心贪心逻辑)
sort(strs.begin(), strs.end(), [](const string& s1, const string& s2)
{
return s1 + s2 > s2 + s1;
});
// 第三步:拼接结果
string ret;
for (auto& s : strs)
ret += s;
// 第四步:处理全 0 情况
if (ret[0] == '0')
return "0";
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 = {
{10, 2},
{3, 30, 34, 5, 9},
{0, 0},
{0, 0, 0, 0},
{1},
{12, 121},
{8308, 8308, 830},
{432, 43243},
{999999991, 9},
{20, 1}
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> nums = testCases[i];
cout << "测试用例 " << i + 1 << ":";
printVector(nums);
cout << endl;
cout << "最大拼接结果:" << sol.largestNumber(nums) << endl;
cout << "------------------------" << endl;
}
return 0;
}

5.摆动序列(OJ题)

解法(贪心):
贪心策略:
对于某一个位置来说:
- 如果接下来呈现上升趋势的话,我们让其上升到波峰的位置;
- 如果接下来呈现下降趋势的话,我们让其下降到波谷的位置.
因此,如果把整个数组放在折线图中,我们统计出所有的波峰以及波谷的个数即可.





核心代码
cpp
class Solution
{
public:
//函数功能:计算数组的最长摆动子序列长度
//参数:nums 整数数组
//返回值:最长摆动序列的长度
int wiggleMaxLength(vector<int>& nums)
{
//获取数组长度
int n = nums.size();
//边界条件:数组长度小于2时,本身就是摆动序列,直接返回长度
if(n < 2)
return n;
//ret:统计摆动的**次数**(波峰/波谷的数量)
//left:记录**上一次**相邻元素的差值(标记趋势:正=上升,负=下降)
int ret = 0, left = 0;
//遍历数组,计算每一对相邻元素的差值
for(int i = 0; i < n - 1; i++)
{
//right:计算**当前**相邻元素的差值(当前趋势:上升/下降/水平)
int right = nums[i + 1] - nums[i];
//情况1:差值为0(水平趋势),不影响摆动,直接跳过
if(right == 0)
continue;
//核心贪心逻辑:
//当前趋势 × 上一次趋势 ≤ 0 → 说明**趋势发生反转**(上升变下降/下降变上升)
//即找到了一个波峰或波谷,摆动次数+1
if(right * left <= 0)
ret++;
//更新上一次趋势为当前趋势,为下一次判断做准备
left = right;
}
//最终长度 = 摆动次数 + 1(次数比元素个数少1)
return ret + 1;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
// 函数功能:计算数组的最长摆动子序列长度
// 参数:nums 整数数组
// 返回值:最长摆动序列的长度
int wiggleMaxLength(vector<int>& nums)
{
int n = nums.size();
if (n < 2)
return n;
int ret = 0, left = 0;
for (int i = 0; i < n - 1; i++)
{
int right = nums[i + 1] - nums[i];
if (right == 0)
continue;
if (right * left <= 0)
ret++;
left = right;
}
return ret + 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 = {
{1, 7, 4, 9, 2, 5}, // 普通摆动序列
{1, 17, 5, 10, 13, 15, 10, 5, 16, 8}, // 需要删除部分元素
{1, 2, 3, 4, 5, 6, 7, 8, 9}, // 单调递增
{9, 8, 7, 6, 5}, // 单调递减
{1, 1, 1, 1}, // 全部相等
{0, 0}, // 两个相等元素
{1}, // 单个元素
{}, // 空数组
{3, 3, 3, 2, 5}, // 前面有重复元素
{1, 7, 7, 7, 4, 9, 2, 5}, // 中间有重复元素
{1, 2, 2, 3, 4, 3, 2}, // 包含平台区间
{10, 5, 10, 5, 10} // 完全摆动
};
for (int i = 0; i < testCases.size(); i++)
{
vector<int> nums = testCases[i];
cout << "测试用例 " << i + 1 << ":";
printVector(nums);
cout << endl;
cout << "最长摆动子序列长度:" << sol.wiggleMaxLength(nums) << endl;
cout << "------------------------" << endl;
}
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容的更新【贪心算法】(经典实战应用解析(二):最⻓递增⼦序列、递增的三元⼦序列、最⻓连续递增序列、买卖股票的最佳时机、买卖股票的最佳时机II)
每日心灵鸡汤: 安静去做,直到成功!
努力成为你最喜欢的那种人,就算不成功,至少你喜欢现在努力的自己.停止精神深处的内耗,停止无意义的内耗.你要留点精力,去读书、去运动、去爱人、去奔赴你想要的生活.不要觉得独来独往会很奇怪,沉淀的日子总是安静无声的,也不要怕努力了没有一个好结果,提前焦虑只会加重你的负担,还没到最后,你怎就知道自己不行.把消极的态度扔掉,把拖延的毛病改掉.认准了一条路,就不要打听它有多远,只管大胆的向前走,你踏出的每一个脚印,都会成为你日后的经验和底气.
