华为OD机试里这道“堆内存申请 / 堆内存最佳分配”,光看名字很容易让人误以为是考 JVM 堆、Go runtime 内存池,结果真正上手才发现,它的内核其实是操作系统里最常见的内存分配策略——最佳适配(Best Fit)。我第一次刷到的时候也愣了一下,直到把输入输出跑通才意识到,这道题考的是最基础的“空闲块管理 + 按规则查找 + 原地切分”,只是套了个“堆内存”的壳。
下面我用 Java 和 Go 两种语言,把这道题从题面到代码一层层剥开。不管是准备 OD 机试的,还是想补一补内存分配算法基础的,都能直接照着落地方案,愿意深挖的话还能顺带把最佳适配、首次适配、最差适配的差异都搞明白。
1. 题目理解与核心考点拆解
1.1 先还原一个接近真题的题面
不同题库里这题细节可能略有出入,但我综合考友反馈,梳理出一个非常接近原题的形态,题面大概是这样的:
系统中有若干块连续的空闲内存,每块内存用起始地址 start 和长度 size 描述。现在按顺序到达 K 个内存申请,每个申请需要一块长度不小于 applySize 的连续内存块。
分配规则采用“最佳适配”:
- 在所有长度不小于 applySize 的空闲块中,优先选择长度最小的那块。
- 如果存在多个长度相同且都是最小的空闲块,则选择起始地址最小的那块。
- 分配时从选中块的前端划出 applySize 个字节,返回起始地址;如果该块还有剩余,则剩余部分继续作为空闲块留在空闲列表中。
- 如果没有长度不小于 applySize 的空闲块,本次申请失败,输出 fail。
输入格式:
code复制N
start1 size1
start2 size2
...
K
applySize1
applySize2
...
输出格式:按申请顺序,每行输出本次申请分配到的起始地址,分配失败就输出 fail。
举一个能跑出来的完整例子:
输入:
code复制2
1 10
50 20
4
5
10
15
10
输出:
code复制1
50
fail
60
这个过程手工验证一遍:初始有两块空闲内存,地址 1 到 10 一块,地址 50 到 69 一块。
- 申请 5 字节时,两块都够,但最佳适配选的是“最小的足够块”,也就是 size=10 的那块,所以返回起始地址 1。这块剩余 5 字节,起始地址变为 6。
- 申请 10 字节时,第一块只剩 5 字节不够,第二块 size=20 可以,返回 50,剩余 10 字节,起始地址变 60。
- 申请 15 字节时,两块都不够,输出 fail。
- 申请 10 字节时,第二块剩余部分正好 10 字节,返回 60,整块耗尽。
这个样例非常典型,它同时覆盖了“能分配”“剩余继续用”“完全耗尽”“分配失败”四种情况。
1.2 什么叫“最佳分配”,为什么这样设计
操作系统里常见的空闲内存分配策略有几种:首次适配(First Fit)是从低地址开始找第一块满足大小的空闲块;最差适配(Worst Fit)是找满足条件下最大的空闲块;最佳适配(Best Fit)是找满足条件下最小的空闲块。
“最佳”体现在哪里?它希望尽可能保留大块内存,避免下一次的大内存申请被小碎片卡死。比如现在有一块 5KB 和一块 100KB 的空闲空间,来了一个 4KB 的申请,最佳适配会选择 5KB 那块,把 100KB 的大块留着应对后面的“大家伙”。
但最佳适配也并非没有缺点,它非常容易产生大量小而难用的外部碎片。实际工程里的分配器通常不会只用一种策略,而是组合拳。不过机试不就讲这个,我们只需要严格按题面规则实现。
这道题里“若多个空闲块大小相同,选择起始地址较小的”这个条件很关键,它决定了查找时不能只按 size 取最小,必须同时关心 start 字段。
1.3 考官到底想考什么
这道题表面是内存分配,实际考察了三个基本功:
- 自定义数据结构:Java 里是内部类,Go 里是 struct,用来封装“起始地址 + 长度”这个二元组。
- 线性容器上的遍历和比较:在一个列表里反复寻找符合条件的元素,本质上是一个“带条件的最优值查找”。
- 更新逻辑的严谨性:分配成功后,原空闲块的长度、起始地址怎么变化,完全耗尽时如何标记无效,这些细节一处不留神就会出错。
理解了这三点,后面写代码就不会跑偏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心数据结构与分配策略设计
2.1 空闲块怎么存,为什么不用数组
一个空闲块的基本属性是 start 和 size,用单独的两个 int 数组也能存,但可读性太差。更推荐的做法是定义成 Block 对象或结构体,把两个字段绑在一起。
存储容器方面,直接用 ArrayList(Java)或 slice(Go)就够了。有一种常见做法是用 LinkedList,配合频繁删除节点。我不推荐在机试里这样做,因为 Java 的 LinkedList 在索引遍历上反而不如 ArrayList 直接,而且无脑删除节点会引入不少边界处理。
还有一个隐藏问题:这个题目中如果只做“申请”,空闲块总数只会减少或不变,不会增加。因为每次分配是从已有块中切出一段,而不是新增一块。这就意味着我可以不用物理删除,直接把耗尽块的 size 标记成 0,后续遍历时跳过即可。这样可以省去“删除元素导致数组搬移”的开销,也不容易出现迭代过程中删元素越界的问题。
2.2 每次申请如何选出“最佳”空闲块
先明确比较规则:假设有两个空闲块 A 和 B
