【贪心算法】(经典实战应用解析(一):柠檬水找零、将数组和减半的最少操作次数、最大数、摆动序列)


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

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

目录

1.贪心算法背景介绍

1.贪心算法概念

**贪心算法(Greedy Algorithm)**是一种在每一步选择中都采取当前最优解(局部最优)的算法策略,希望通过局部最优选择来达到整体最优(全局最优)或接近全局最优的结果.

特点:

  • 局部最优:每次选择时,只考虑当前状态下最优的选择.
  • 不可回溯:一旦做出选择,就不再回退.
  • 快速:通常时间复杂度低于动态规划.

2.背景与发展

  1. 算法思想起源

    贪心算法的思想早在数学优化问题中就已出现,例如:

    • 最短路径问题(如 Dijkstra 算法)
    • 最小生成树问题(如 Prim 和 Kruskal 算法)

    这些问题中,局部最优的选择往往可以得到全局最优解.

  2. 发展历程

    • 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)


每日心灵鸡汤: 安静去做,直到成功!

努力成为你最喜欢的那种人,就算不成功,至少你喜欢现在努力的自己.停止精神深处的内耗,停止无意义的内耗.你要留点精力,去读书、去运动、去爱人、去奔赴你想要的生活.不要觉得独来独往会很奇怪,沉淀的日子总是安静无声的,也不要怕努力了没有一个好结果,提前焦虑只会加重你的负担,还没到最后,你怎就知道自己不行.把消极的态度扔掉,把拖延的毛病改掉.认准了一条路,就不要打听它有多远,只管大胆的向前走,你踏出的每一个脚印,都会成为你日后的经验和底气.

相关推荐
05候补工程师1 小时前
【408考研】数据结构核心笔记:单链表与栈操作精髓总结
数据结构·笔记·考研·链表·c#
kdxiaojie1 小时前
U-Boot分析【学习笔记】(7)
linux·笔记·学习
初心未改HD1 小时前
机器学习之支持向量机SVM详解
算法·机器学习·支持向量机
少司府1 小时前
C++基础入门:vector深度解析(七千字深度剖析)
c语言·开发语言·数据结构·c++·容器·vector·顺序表
he___H1 小时前
子串----
java·数据结构·算法·leetcode
计算机安禾1 小时前
【c++面向对象编程】第8篇:const成员与mutable:常对象与常函数
开发语言·javascript·c++
05候补工程师2 小时前
【ROS 2 避坑指南】从 SLAM 实时建图到 Nav2 导航算法深度调优全过程
算法·ubuntu·机器人
Dlrb12112 小时前
C语言-函数传参
c语言·数据结构·算法
草莓熊Lotso3 小时前
【Linux网络】UDP Socket 编程全解析:从回显服务到通用字典服务,从零实现工业级代码
linux·运维·服务器·数据库·c++·单片机·udp