第一章:贪心算法的核心思想与适用场景
贪心算法是一种在每一步选择中都采取当前状态下最优决策的算法设计策略,期望通过局部最优解达到全局最优解。该算法不回溯,一旦做出选择便不可更改,因此其效率较高,但并不适用于所有问题。
核心思想
贪心算法的核心在于“局部最优选择”。它在每个阶段都选择能使目标函数值最大的选项,而不考虑未来可能产生的影响。这种策略的关键是问题必须具备贪心选择性质和最优子结构。
- 贪心选择性质:全局最优解可以通过一系列局部最优的选择来构造
- 最优子结构:一个问题的最优解包含其子问题的最优解
典型适用场景
贪心算法适用于一些特定优化问题,例如:
- 活动选择问题
- 最小生成树(如Prim和Kruskal算法)
- 霍夫曼编码
- 单源最短路径(Dijkstra算法)
| 问题类型 | 是否适用贪心 | 说明 |
|---|
| 背包问题(分数型) | 是 | 可按单位价值排序贪心选取 |
| 0-1背包问题 | 否 | 需动态规划求解 |
| 找零钱问题 | 视币种而定 | 标准币系下贪心有效 |
代码示例:分数背包问题
// 分数背包贪心实现
package main
import (
"fmt"
"sort"
)
type Item struct {
value, weight float64
}
func fractionalKnapsack(items []Item, capacity float64) float64 {
// 按单位重量价值降序排列
sort.Slice(items, func(i, j int) bool {
return items[i].value/items[i].weight > items[j].value/items[j].weight
})
var totalValue float64 = 0.0
for _, item := range items {
if capacity >= item.weight {
totalValue += item.value
capacity -= item.weight
} else {
// 可分割物品,取部分
totalValue += item.value * (capacity / item.weight)
break
}
}
return totalValue
}
func main() {
items := []Item{{60, 10}, {100, 20}, {120, 30}}
capacity := 50.0
fmt.Printf("最大价值: %.2f\n", fractionalKnapsack(items, capacity))
}
graph TD
A[开始] --> B{按单位价值排序}
B --> C[选取最高单位价值物品]
C --> D{容量足够?}
D -- 是 --> E[装入整件]
D -- 否 --> F[装入部分]
E --> G{还有物品?}
F --> H[结束]
G -- 是 --> C
G -- 否 --> H
第二章:贪心算法基础题型精讲
2.1 区间调度问题:如何选择最多的不重叠区间
在众多贪心算法的经典应用中,区间调度问题是一个典型代表:给定一组区间的起止时间,目标是选出尽可能多的互不重叠的区间。
问题建模与策略选择
关键在于选择策略。若按起始时间排序,可能被迫接受长区间;而按结束时间升序排列,能尽早释放资源,为后续区间腾出空间。
算法实现
def interval_scheduling(intervals):
intervals.sort(key=lambda x: x[1]) # 按结束时间排序
selected = []
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
selected.append((start, end))
last_end = end
return selected
该函数接收区间列表
intervals,每项为
(start, end)。排序后遍历,仅当当前区间起始时间不早于已选区间的最晚结束时间时才纳入。
复杂度分析
排序耗时
O(n log n),扫描过程为
O(n),整体时间复杂度由排序主导。空间复杂度为
O(1)(不计输出)。
2.2 分发饼干问题:从贪心策略理解资源最优匹配
在资源分配场景中,分发饼干问题是贪心算法的经典应用。目标是让尽可能多的孩子满足,每个孩子有最小的胃口值,每块饼干有特定尺寸。
问题建模与策略选择
将孩子胃口和饼干尺寸分别排序,采用贪心策略:优先用最小的饼干满足最小胃口且能被满足的孩子。
算法实现
func findContentChildren(g []int, s []int) int {
sort.Ints(g)
sort.Ints(s)
i, j := 0, 0
for i < len(g) && j < len(s) {
if g[i] <= s[j] { // 饼干能满足孩子
i++
}
j++ // 无论是否满足,都尝试下一块饼干
}
return i
}
该函数通过双指针遍历排序后的数组,时间复杂度为 O(n log n),主要开销在排序。参数 g 表示孩子胃口,s 表示饼干尺寸。
2.3 跳跃游戏:基于局部最优判断全局可行性
在跳跃游戏问题中,核心目标是判断能否从数组起点到达最后一个位置。通过贪心策略,每一步都更新能到达的最远边界,即可高效求解。
算法思路
维护一个变量
maxReach 表示当前可达的最远下标。遍历数组时,若当前下标
i 超过
maxReach,说明无法继续前进,直接返回
false;否则更新
maxReach = max(maxReach, i + nums[i])。
代码实现
func canJump(nums []int) bool {
maxReach := 0
for i := 0; i < len(nums); i++ {
if i > maxReach {
return false
}
maxReach = max(maxReach, i + nums[i])
}
return true
}
该实现时间复杂度为 O(n),空间复杂度 O(1)。每次迭代检查是否可到达位置
i,并动态扩展最远可达范围,体现了局部最优决策推动全局可行性的思想。
2.4 钱币找零问题:经典贪心模型的成立条件分析
在特定货币系统中,钱币找零问题常被视为贪心算法的经典应用。其核心思想是每次选择面值不超过剩余金额的最大硬币,以期用最少数量完成找零。
贪心策略的适用前提
该策略成立的关键在于货币面值的构造方式。若面值序列满足“规范性”条件——即任意面值都能被其后续更大面值整除或构成最优组合,则贪心解即为全局最优解。例如标准人民币体系(1, 5, 10, 20, 50, 100)就具备此性质。
算法实现示例
def coin_change_greedy(coins, amount):
coins.sort(reverse=True) # 降序排列
count = 0
for coin in coins:
while amount >= coin:
amount -= coin
count += 1
return count if amount == 0 else -1
上述代码按贪心策略计算最小硬币数。参数
coins 为可用面值列表,
amount 为目标金额。循环中逐次扣除最大可选面值,直至金额归零或无解。
反例与局限性
当面值为 [1, 3, 4],目标金额为 6 时,贪心法选择 4+1+1(共3枚),而最优解为 3+3(仅2枚)。这表明贪心模型依赖于面值系统的数学结构,并非普适。
2.5 划分字母区间:贪心划分在字符串处理中的应用
在字符串处理中,合理划分字符区间有助于优化存储与检索。一个典型问题是:如何将字符串划分为尽可能少的子串,使得每个字母最多只出现在一个子串中?
问题建模与贪心策略
核心思想是记录每个字符最后一次出现的位置。遍历字符串时,动态扩展当前区间的右边界,直到当前位置等于当前区间内所有字符的最远出现位置。
func partitionLabels(s string) []int {
lastPos := make(map[byte]int)
for i := range s {
lastPos[s[i]] = i
}
var result []int
start, end := 0, 0
for i := range s {
if lastPos[s[i]] > end {
end = lastPos[s[i]]
}
if i == end {
result = append(result, end-start+1)
start = i + 1
}
}
return result
}
上述代码通过一次预处理确定每个字符的最远位置,再进行单次扫描完成区间划分。时间复杂度为 O(n),空间复杂度 O(1)(字符集固定)。该贪心策略确保每步决策不可逆且局部最优,最终达成全局最优划分。
第三章:贪心与数据结构结合应用
3.1 用优先队列优化贪心决策:IPO问题解析
在IPO问题中,给定初始资本和多个项目(具有收益和启动资金要求),目标是通过最多k次投资最大化最终资本。该问题的核心在于每一步选择当前可承担项目中收益最大的一个——典型的贪心策略。
贪心策略的瓶颈
若每次遍历所有项目寻找可执行且收益最高的项目,时间复杂度为O(kn),效率低下。此时引入**优先队列**优化候选项目的选取过程。
双堆优化思路
使用两个数据结构:
- 按启动资金排序的项目列表(升序),用于筛选可投资项目;
- 最大堆(基于收益)存储当前可投资的项目。
priority_queue maxProfit;
sort(projects.begin(), projects.end());
int i = 0;
for (int k = 0; k < K; ++k) {
while (i < n && projects[i][0] <= W)
maxProfit.push(projects[i++][1]);
if (maxProfit.empty()) break;
W += maxProfit.top(); maxProfit.pop();
}
上述代码中,先按成本排序所有项目,再将符合条件的项目收益加入最大堆。每次从堆顶取出最大收益项目进行投资,显著提升决策效率至O(n log n + k log n)。
3.2 结合排序技巧:任务调度问题中的贪心排序策略
在任务调度问题中,贪心算法常通过合理的排序策略实现近似最优解。关键在于选择合适的排序准则,如最早截止时间优先(EDF)或最短执行时间优先(SJF)。
贪心排序的核心思想
将任务按某一维度排序后依次处理,可显著降低复杂度。例如,在单处理器上调度独立任务以最小化总延迟,按截止时间升序排列通常是最优策略。
代码实现示例
// Task 表示一个任务
type Task struct {
id int
duration int // 执行时长
deadline int // 截止时间
}
// 按截止时间升序排序并计算总延迟
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].deadline < tasks[j].deadline
})
上述代码对任务按截止时间排序,确保紧迫任务优先执行。排序后遍历任务,累计完成时间并与截止时间比较,即可得出延迟总量。该策略在多项式时间内逼近最优解,广泛应用于实时系统调度场景。
3.3 区间合并问题:贪心思维下的高效合并算法
在处理重叠区间时,区间合并问题要求将所有存在交集的区间进行合并,最终输出互不重叠的区间集合。该问题广泛应用于日程安排、资源分配等场景。
核心思路:贪心策略
通过按左端点排序,依次遍历区间,若当前区间与结果集中最后一个区间重叠,则合并;否则将其加入结果集。这种局部最优选择保证全局最优解。
代码实现
// merge 合并区间
func merge(intervals [][]int) [][]int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0] // 按左端点升序
})
var result [][]int
for _, interval := range intervals {
if len(result) == 0 || result[len(result)-1][1] < interval[0] {
result = append(result, interval) // 无重叠,直接添加
} else {
result[len(result)-1][1] = max(result[len(result)-1][1], interval[1]) // 更新右端点
}
}
return result
}
上述代码时间复杂度为 O(n log n),主要消耗在排序阶段。合并过程仅需一次线性扫描,体现了贪心算法的高效性。
第四章:复杂场景下的贪心策略设计
4.1 加油站问题:环形路径下的贪心可行性证明
在环形路径上,加油站问题要求判断从某一起点出发能否环绕一周。核心条件是总油量不小于总消耗量。
贪心策略的正确性
若从站点
i 出发无法到达
j,则区间
[i, j) 中任意站点作为起点均会失败。因此可跳过这些点,直接尝试从
j 开始。
算法实现
func canCompleteCircuit(gas []int, cost []int) int {
total, curr, start := 0, 0, 0
for i := 0; i < len(gas); i++ {
diff := gas[i] - cost[i]
total += diff
curr += diff
if curr < 0 {
start = i + 1
curr = 0
}
}
return total >= 0 ? start : -1
}
total 跟踪净油量,决定全局可行性;
curr 记录当前累积油量,为负时更新起点。
4.2 救生艇问题:双指针与贪心的协同优化
在救生艇问题中,给定一个数组表示乘客体重,每艘船最多载两人且有最大承重限制,目标是求出运送所有人的最少船只数量。该问题可通过贪心策略结合双指针技术高效求解。
贪心策略的核心思想
优先将最重的人与最轻的人配对,若二者体重之和未超限,则共乘一船;否则,重者单独乘船。这样可最大化每艘船的利用率。
双指针实现方案
先对数组排序,使用左右指针分别指向最轻和最重者:
func numRescueBoats(people []int, limit int) int {
sort.Ints(people)
left, right := 0, len(people)-1
boats := 0
for left <= right {
if people[left]+people[right] <= limit {
left++ // 轻者可同行
}
right-- // 重者必上船
boats++
}
return boats
}
代码中,
left 和
right 分别代表当前最轻和最重的未安排乘客。每次循环判断是否可配对,若可以则左指针右移,否则仅右指针左移,每轮均消耗一艘船。
4.3 最大数拼接:自定义比较器实现贪心排序
在处理将一组非负整数拼接成最大数的问题时,关键在于定义正确的排序规则。简单的数值大小排序无法满足需求,需通过贪心策略比较两种拼接顺序的字典序大小。
核心思路
对于任意两个数 a 和 b,若字符串拼接结果 a+b > b+a,则 a 应排在 b 前面。该比较规则可保证全局最优解。
代码实现
func largestNumber(nums []int) string {
strNums := make([]string, len(nums))
for i, num := range nums {
strNums[i] = strconv.Itoa(num)
}
sort.Slice(strNums, func(i, j int) bool {
return strNums[i]+strNums[j] > strNums[j]+strNums[i]
})
if strNums[0] == "0" {
return "0"
}
return strings.Join(strNums, "")
}
上述代码将整数转为字符串后,使用自定义比较器进行排序。比较时拼接两个字符串并判断字典序,确保高位尽可能放置“更大贡献”的数字。最后合并结果,特殊处理全零情况。
4.4 买卖股票的最佳时机Ⅱ:贪心视角下的多交易建模
在允许多次交易的前提下,目标是最大化累计利润。关键洞察在于:只要明天的价格高于今天,就应今天买入、明天卖出。
贪心策略的直观理解
将价格序列视为趋势片段,每一段上升区间都可拆解为连续的单日价差。因此,总利润等于所有正向价差之和。
算法实现
func maxProfit(prices []int) int {
profit := 0
for i := 1; i < len(prices); i++ {
if prices[i] > prices[i-1] {
profit += prices[i] - prices[i-1] // 累加所有正向差值
}
}
return profit
}
该函数遍历价格数组,每当发现价格上涨即刻“交易”。时间复杂度为 O(n),空间复杂度 O(1)。核心思想是将全局最优分解为局部最优决策的叠加。
第五章:总结与进阶学习建议
构建持续学习的技术路径
技术演进迅速,掌握基础后应主动拓展知识边界。例如,在深入理解 Go 语言并发模型后,可进一步研究 runtime 调度机制。以下代码展示了如何通过
sync.Pool 优化高频对象分配:
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func getBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func putBuffer(b *bytes.Buffer) {
b.Reset()
bufferPool.Put(b)
}
参与开源项目提升实战能力
实际工程项目中,协作与代码审查至关重要。建议从贡献文档、修复简单 bug 入手,逐步参与核心模块开发。以下是推荐的参与步骤:
- 在 GitHub 上筛选标记为 "good first issue" 的项目
- 阅读 CONTRIBUTING.md 并配置本地开发环境
- 提交 PR 前确保通过所有单元测试和 CI 流程
- 积极回应维护者的评审意见,学习工程规范
系统化知识体系构建
为避免陷入“碎片化学习”,建议建立个人知识图谱。可参考以下分类结构进行归纳:
| 领域 | 核心技术 | 推荐资源 |
|---|
| 分布式系统 | 共识算法、服务发现 | 《Designing Data-Intensive Applications》 |
| 云原生 | Kubernetes、Service Mesh | 官方文档 + CNCF 项目实践 |