跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

贪心算法核心解析:原理、策略与 C++ 实战

综述由AI生成贪心算法通过局部最优选择追求全局最优解,核心在于利用贪心选择性质和最优子结构性质。文章详细阐述了贪心算法的适用条件、解题步骤,并通过活动选择、哈夫曼编码、Dijkstra 最短路径及 Kruskal 最小生成树四个经典案例,结合 C++ 代码实现深入剖析了具体应用。同时对比了贪心与动态规划的区别,总结了优缺点及实际场景,帮助开发者快速掌握该算法的核心逻辑与工程实践要点。

追风少年发布于 2025/11/20更新于 2026/6/923 浏览
贪心算法核心解析:原理、策略与 C++ 实战

贪心算法核心解析:原理、策略与 C++ 实战

一、贪心算法的核心定义与本质

贪心算法是一种在每一步选择中都采取当前状态下最优(即局部最优)的选择,以期最终获得全局最优解的启发式算法。其核心思想可以概括为:'走一步看一步,每步都选最好的,不回头'。

与动态规划(DP)需要存储子问题的最优解并回溯不同,贪心算法不依赖历史决策——它通过每一步的局部最优积累,直接推导全局最优。这种'短视'的特性使其实现简单、效率极高,但也决定了它并非适用于所有问题,必须满足严格的前提条件。

二、贪心算法的适用条件

判断一个问题能否用贪心算法解决,必须同时满足以下两个核心性质,缺一不可:

1. 贪心选择性质

每一步的局部最优选择,能够导向全局最优解。即:在选择当前最优解时,不需要考虑后续的决策,其选择结果不会影响后续子问题的最优性。

  • 示例:活动选择问题中,'选择最早结束的活动'这一局部最优选择,能为后续留下更多时间选择其他活动,最终导向全局最优(最多活动数)。
  • 反例:0-1 背包问题中,'选择价值密度最高的物品'无法保证全局最优(可能因剩余空间无法容纳其他高价值物品,导致总价值更低)。

2. 最优子结构性质

全局最优解中必然包含其子问题的最优解。即:问题的最优解可以分解为若干个子问题的最优解的组合。

  • 示例:Dijkstra 算法中,'从源点到节点 v 的最短路径'必然包含'从源点到路径上某中间节点 u 的最短路径'——若存在更短的 源点→u 路径,替换后可得到更短的 源点→v 路径,与全局最优矛盾。

3. 经典反例:错误的贪心策略

以'找零钱问题'为例:若硬币面额为 [1,3,4],需找 6 元。直觉贪心策略(选最大面额优先)会得到 4+1+1=6(3 枚硬币),但最优解是 3+3=6(2 枚硬币)。此时'最大面额优先'的贪心策略不满足'贪心选择性质',导致全局最优失效。

三、贪心算法的解题步骤

使用贪心算法解决问题需遵循固定流程,核心是策略设计与正确性证明:

  1. 问题建模:将实际问题抽象为'选择问题',明确目标函数(如'最多活动数''最短路径和')和约束条件(如'活动不冲突''边权非负')。
  2. 设计贪心策略:确定每一步如何选择'局部最优'。常见策略包括:按结束时间排序、按价值密度排序、按边权排序等。
  3. 证明策略正确性:通过数学归纳法或反证法,验证策略满足'贪心选择性质'和'最优子结构'。
  4. 编码实现:根据策略选择合适的数据结构(如排序、优先队列、并查集),处理边界情况(如空输入、极端值)。
  5. 测试优化:验证结果正确性,优化时间复杂度(如用快排替代冒泡排序)。

四、经典问题与 C++ 实现

贪心算法的应用场景高度集中,以下为 4 类核心问题的详细实现:

1. 活动选择问题(最多不冲突活动)

问题描述

给定 n 个活动,每个活动有开始时间 start[i] 和结束时间 end[i],选择最多不重叠的活动集合。

贪心策略

按活动的结束时间升序排序,优先选择最早结束的活动——该选择能为后续活动预留最多时间,最大化总活动数。

C++ 实现




  std;


  {
     start; 
     end;   
};


{
     a.end < b.end;
}


{
    vector<Activity> result;
     (activities.())  result;

    
    (activities.(), activities.(), compare);

    
    result.(activities[]);
     lastEnd = activities[].end;

    
     ( i = ; i < activities.(); i++) {
         (activities[i].start >= lastEnd) {
            result.(activities[i]);
            lastEnd = activities[i].end; 
        }
    }
     result;
}

{
    vector<Activity> activities = {{,},{,},{,},{,},{,},{,},{,},{,},{,},{,},{,}};
    vector<Activity> selected = (activities);

    
    cout <<  << endl;
     (& act : selected) {
        cout <<  << act.start <<  << act.end <<  << endl;
    }
    cout <<  << selected.() <<  << endl;
     ;
}
#include <iostream>
#include <vector>
#include <algorithm>
using
namespace
// 定义活动结构体
struct
Activity
int
// 开始时间
int
// 结束时间
// 排序规则:按结束时间升序
bool compare(const Activity& a, const Activity& b)
return
// 选择最多不冲突活动
vector<Activity> selectMaxActivities(vector<Activity>& activities)
if
empty
return
// 1. 按结束时间排序
sort
begin
end
// 2. 选择第一个活动(最早结束)
push_back
0
int
0
// 3. 遍历后续活动,选择不冲突的(开始时间>=上一个结束时间)
for
int
1
size
if
push_back
// 更新最后一个活动的结束时间
return
int main()
1
4
3
5
0
6
5
7
3
9
5
9
6
10
8
11
8
12
2
14
12
16
selectMaxActivities
// 输出结果
"选择的活动(开始时间,结束时间):"
for
auto
"("
", "
")"
"最多可选择 "
size
" 个活动"
return
0
输出结果
选择的活动(开始时间,结束时间): (1, 4) (5, 7) (8, 11) (12, 16) 最多可选择 4 个活动 

2. 哈夫曼编码(最优前缀编码)

问题描述

给定字符的频率分布(如 a:5, b:9, c:12, d:13),构造前缀编码(无编码是另一编码的前缀),使总编码长度(频率×编码长度之和)最小。

贪心策略
  1. 构建最小堆(优先队列),存储所有字符的频率;
  2. 每次取出两个频率最小的节点,合并为一个新节点(频率为两节点之和);
  3. 将新节点入堆,重复步骤 2,直到堆中只剩一个节点(哈夫曼树的根);
  4. 树的左分支记为 0,右分支记为 1,叶子节点的路径即为对应字符的编码。
C++ 实现
#include <iostream>
#include <queue>
#include <vector>

using namespace std;

// 计算哈夫曼编码的总长度
int huffmanCodeTotalLength(const vector<int>& frequencies) {
    // 最小堆:priority_queue<Type, Container, Compare>
    priority_queue<int, vector<int>, greater<int>> minHeap;

    // 1. 将所有频率入堆
    for (int freq : frequencies) {
        minHeap.push(freq);
    }

    int totalLength = 0; // 总编码长度

    // 2. 合并节点,直到堆中只剩 1 个节点
    while (minHeap.size() > 1) {
        // 取出两个最小频率
        int first = minHeap.top(); minHeap.pop();
        int second = minHeap.top(); minHeap.pop();

        // 合并后的新节点频率
        int merged = first + second;
        totalLength += merged; // 合并节点的频率即编码长度贡献

        // 新节点入堆
        minHeap.push(merged);
    }
    return totalLength;
}

int main() {
    // 字符频率:a:5, b:9, c:12, d:13, e:16, f:45
    vector<int> frequencies = {5, 9, 12, 13, 16, 45};
    int total = huffmanCodeTotalLength(frequencies);
    cout << "哈夫曼编码的总长度:" << total << endl;
    // 输出:224
    return 0;
}
原理说明

总长度 224 的计算逻辑: 合并过程为 5+9=14(贡献 14)→12+13=25(贡献 25)→14+16=30(贡献 30)→25+30=55(贡献 55)→45+55=100(贡献 100),总和 14+25+30+55+100=224。

3. Dijkstra 算法(单源最短路径)

问题描述

在带非负权的无向/有向图中,找到从源点 S 到所有其他节点的最短路径长度。

贪心策略
  1. 用 dist[] 数组记录源点到各节点的当前最短距离(初始为 INF,dist[S]=0);
  2. 构建最小堆,存储(当前最短距离,节点),初始将(0, S)入堆;
  3. 每次取出堆顶节点 u(当前距离源点最近的未确定节点),标记为'已确定';
  4. 遍历 u 的所有邻接节点 v,执行松弛操作:若 dist[v] > dist[u] + 边权 w,则更新 dist[v],并将(dist[v], v)入堆;
  5. 重复步骤 3-4,直到堆为空。
C++ 实现
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <stdexcept> // 用于边界校验异常

using namespace std;

const int INF = INT_MAX;

vector<int> dijkstra(int n, int source, const vector<vector<pair<int, int>>>& adj) {
    // 边界校验:源点合法性
    if (source < 0 || source >= n) {
        throw invalid_argument("源点超出节点范围!");
    }

    vector<int> dist(n, INF);
    dist[source] = 0;

    // 最小堆:(当前距离,节点)
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> minHeap;
    minHeap.push({0, source});

    while (!minHeap.empty()) {
        auto [currentDist, u] = minHeap.top(); // C++17 结构化绑定
        minHeap.pop();

        if (currentDist > dist[u]) continue;

        // 遍历 u 的所有邻接节点
        for (auto [v, w] : adj[u]) {
            // 松弛操作:更新 dist[v]
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                minHeap.push({dist[v], v});
            }
        }
    }
    return dist;
}

int main() {
    int n = 6; // 节点数(0~5)
    vector<vector<pair<int, int>>> adj(n); // 局部邻接表,替代全局变量

    // 构建图(边:u->v,权 w)
    adj[0].emplace_back(1, 2); // emplace_back 比 push_back 更高效(直接构造对象)
    adj[0].emplace_back(2, 4);
    adj[1].emplace_back(2, 1);
    adj[1].emplace_back(3, 7);
    adj[2].emplace_back(4, 3);
    adj[3].emplace_back(5, 1);
    adj[4].emplace_back(3, 2);
    adj[4].emplace_back(5, 5);

    int source = 0;
    try {
        vector<int> dist = dijkstra(n, source, adj);

        // 输出正确结果
        cout << "源点 " << source << " 到各节点的最短距离:" << endl;
        for (int i = 0; i < n; ++i) {
            if (dist[i] == INF) {
                cout << "到节点 " << i << ":不可达" << endl;
            } else {
                cout << "到节点 " << i << ":" << dist[i] << endl;
            }
        }
    } catch (const invalid_argument& e) {
        // 捕获边界校验异常
        cerr << "错误:" << e.what() << endl;
        return 1;
    }
    return 0;
}
输出结果
源点 0 到各节点的最短距离: 到节点 0:0 到节点 1:2 到节点 2:3(0→1→2) 到节点 3:8(0→1→2→4→3) 到节点 4:6(0→1→2→4) 到节点 5:9(0→1→2→4→3→5) 

4. Kruskal 算法(最小生成树)

问题描述

在无向带权图中,找到一棵连接所有节点、总边权和最小的生成树(Minimum Spanning Tree, MST)。

贪心策略
  1. 将所有边按权值升序排序;
  2. 用并查集(Union-Find) 维护已选节点的连通性;
  3. 遍历排序后的边,若边的两个端点属于不同连通分量(不构成环),则将该边加入 MST,并合并两个连通分量;
  4. 重复步骤 3,直到 MST 包含 n-1 条边(n 为节点数)。
C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 定义边结构体
struct Edge {
    int u;     // 起点
    int v;     // 终点
    int weight;// 边权
};

// 并查集(Union-Find):维护连通分量
class UnionFind {
private:
    vector<int> parent; // 父节点
    vector<int> rank;   // 秩(用于路径压缩优化)
public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 0);
        for (int i = 0; i < n; i++) {
            parent[i] = i; // 初始父节点为自身
        }
    }

    // 查找根节点(路径压缩)
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // 递归压缩路径
        }
        return parent[x];
    }

    // 合并两个连通分量(按秩合并)
    bool unite(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX == rootY) return false; // 已在同一分量

        // 秩小的树合并到秩大的树
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else {
            parent[rootY] = rootX;
            if (rank[rootX] == rank[rootY]) {
                rank[rootX]++;
            }
        }
        return true;
    }
};

// Kruskal 算法:返回 MST 的总边权
int kruskal(int n, vector<Edge>& edges) {
    // 1. 按边权升序排序
    sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
        return a.weight < b.weight;
    });

    UnionFind uf(n);
    int mstTotal = 0; // MST 总边权
    int edgeCount = 0; // 已选边数

    // 2. 遍历边,选择不构成环的边
    for (auto& edge : edges) {
        if (uf.unite(edge.u, edge.v)) {
            mstTotal += edge.weight;
            edgeCount++; // MST 需 n-1 条边,提前退出
            if (edgeCount == n - 1) break;
        }
    }

    // 若边数不足 n-1,说明图不连通
    return (edgeCount == n - 1) ? mstTotal : -1;
}

int main() {
    int n = 5; // 节点数(0~4)
    vector<Edge> edges = {{0,1,2},{0,3,6},{1,2,3},{1,3,8},{1,4,5},{2,4,7},{3,4,9}};
    int mstTotal = kruskal(n, edges);

    if (mstTotal == -1) {
        cout << "图不连通,无法构建 MST" << endl;
    } else {
        cout << "最小生成树的总边权:" << mstTotal << endl; // 输出:16
    }
    return 0;
}
原理说明

MST 的边为 (0,1,2)、(1,2,3)、(1,4,5)、(0,3,6),总权 2+3+5+6=16,覆盖所有 5 个节点且无环。

五、贪心算法与动态规划的对比

贪心与 DP 均依赖'最优子结构',但核心差异在于子问题的处理方式:

对比维度贪心算法(Greedy)动态规划(DP)
核心思想局部最优→全局最优,不回溯存储子问题最优解,回溯推导全局最优
子问题处理不存储子问题解,每步直接选最优存储子问题解(如 dp 数组),避免重复计算
适用场景满足'贪心选择性质'的问题不满足贪心选择性质,但有最优子结构
时间复杂度低(通常 O(nlogn),排序主导)较高(通常 O(n²) 或 O(nm))
典型问题活动选择、哈夫曼编码、Dijkstra0-1 背包、最长公共子序列、斐波那契
最优解保证需证明策略正确性,否则不保证只要状态转移正确,必为全局最优

六、贪心算法的优缺点与应用场景

1. 优点

  • 实现简单:无需复杂的状态转移或子问题存储,代码逻辑清晰;
  • 效率极高:时间复杂度多为 O(nlogn)(排序)或 O(MlogN)(优先队列),远低于 DP;
  • 空间紧凑:无需存储子问题解,空间复杂度通常为 O(n)。

2. 缺点

  • 适用范围窄:仅能解决满足'贪心选择性质'的问题,多数问题不适用;
  • 正确性难证:需严格证明策略的有效性,直觉性策略易出错(如找零钱反例);
  • 无回溯机制:一旦选择错误,无法修正,只能重新设计策略。

3. 实际应用

  • 资源调度:CPU 短作业优先(SJF)调度、任务优先级调度;
  • 编码压缩:哈夫曼编码(用于 ZIP、JPEG 等格式);
  • 路径规划:Dijkstra 算法(导航软件核心算法之一);
  • 网络优化:Kruskal/Prim 算法(构建通信网络最小成本拓扑)。

贪心算法是一种'高效但挑剔'的算法:它通过局部最优的积累快速推导全局最优,但仅适用于满足'贪心选择性质'和'最优子结构'的问题。掌握贪心算法的核心在于:

  1. 学会判断问题是否符合贪心适用条件;
  2. 设计正确的贪心策略并证明其有效性;
  3. 熟练运用排序、优先队列、并查集等数据结构实现策略。

目录

  1. 贪心算法核心解析:原理、策略与 C++ 实战
  2. 一、贪心算法的核心定义与本质
  3. 二、贪心算法的适用条件
  4. 1. 贪心选择性质
  5. 2. 最优子结构性质
  6. 3. 经典反例:错误的贪心策略
  7. 三、贪心算法的解题步骤
  8. 四、经典问题与 C++ 实现
  9. 1. 活动选择问题(最多不冲突活动)
  10. 问题描述
  11. 贪心策略
  12. C++ 实现
  13. 输出结果
  14. 2. 哈夫曼编码(最优前缀编码)
  15. 问题描述
  16. 贪心策略
  17. C++ 实现
  18. 原理说明
  19. 3. Dijkstra 算法(单源最短路径)
  20. 问题描述
  21. 贪心策略
  22. C++ 实现
  23. 输出结果
  24. 4. Kruskal 算法(最小生成树)
  25. 问题描述
  26. 贪心策略
  27. C++ 实现
  28. 原理说明
  29. 五、贪心算法与动态规划的对比
  30. 六、贪心算法的优缺点与应用场景
  31. 1. 优点
  32. 2. 缺点
  33. 3. 实际应用
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 龙虾 AI 全能助手安装与配置指南
  • Python 异步编程与协程实战指南
  • Python 实现三位数水仙花数判断与求解方法
  • Python 通达信数据获取技术解析与应用
  • Windows 下创建并激活 Python 虚拟环境 venv
  • 大语言模型提示词调试的 6 个关键步骤
  • SpringBoot 实现微信支付接口调用及回调处理
  • 五分钟构建动态知识图谱:利用大模型提取实体关系与数据对话
  • 2024 年中国 AI 算力行业发展现状与趋势分析
  • Llama3 医疗大模型 OpenBioLLM 安装与应用指南
  • 大语言模型微调技术详解:从原理到实践
  • Flutter for OpenHarmony 集成 dart_openai 实现 AIGC 功能
  • OpenClaw 本地极简部署与 QQ 机器人接入教程
  • Self-Attention 与 Multi-head Attention 核心原理及代码实现
  • 基于 OpenClaw 与微信实现 AI 自动回复接入指南
  • Windows Docker 命令行使用手册
  • 2025 年免费数字人工具评测:五大 AI 平台对比
  • Python 打造 AI 三剑客:文档总结、代码生成与智能检索
  • WebGIS 技术栈与项目实战经验总结
  • Web 基础技术详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online