【华为OD机试真题】堆内存申请 · 堆内存最佳分配(Java/Go)
·
一、真题
题目描述:
有一个总空间为100字节的堆,现要从中申请一块内存,内存分配原则为:优先紧接着前一块已使用内存,分配空间足够且最接近申请大小的空闲内存。
输入描述:
第1行是1个整数,表示期望申请的内存字节数。
第2到第N行是用空格分割的两个整数,表示当前已分配的内存的情况,每一行表示一块已分配的连续内存空间,每行的第1和第2个整数分别表示偏移地址和内存块大小,如:
0 1 3 2表示0偏移地址开始的1个字节和3偏移地址开始的2个字节已被分配,其余内存空闲。
输出描述:
若申请成功,输出申请到内存的偏移
若申请失败,输出-1。备注:
- 若输入信息不合法或无效,则申请失败
- 若没有足够的空间供分配,则申请失败
- 堆内存信息有区域重叠或有非法值等都是无效输入
示例1:
输入:
1 0 1 3 2输出:
说明
堆中已使用的两块内存是偏移从0开始的1字节和偏移从3开始的2字节,空闲的两块内存是偏移从1开始2个字节和偏移从5开始95字节。根据分配原则,新申请的内存应从1开始分配1个字节,所以输出偏移为1。
二、解题思路图解💡
步骤 1:数据清洗与校验
- 读取所有已分配块,存储为对象/结构体列表。
- 校验逻辑:
offset < 0或size <= 0→→ 非法。offset + size > 100→→ 越界,非法。- 重叠检测:将已分配块按
offset排序,遍历检查current.offset < prev.offset + prev.size。若成立,则重叠,非法。 - 若任何校验失败,直接输出
-1。
步骤 2:构建空闲区间列表
假设已分配块已按 offset 升序排序:[B1, B2, ..., Bn]。
空闲块来源于三部分:
- 头部空闲:若
B1.offset > 0,则存在空闲块[0, B1.offset),长度B1.offset。 - 中间空闲:遍历
i从0到n-2,若B[i].offset + B[i].size < B[i+1].offset,则存在空闲块[B[i].end, B[i+1].offset),长度B[i+1].offset - B[i].end。 - 尾部空闲:若
Bn.end < 100,则存在空闲块[Bn.end, 100),长度100 - Bn.end。
步骤 3:贪心匹配 (Best Fit)
遍历所有生成的空闲块:
- 若
free.size < reqSize:跳过。 - 若
free.size >= reqSize:- 记录当前差值
diff = free.size - reqSize。 - 维护一个全局最小差值
minDiff和对应的最佳偏移bestOffset。 - 若
diff < minDiff,更新minDiff和bestOffset。 - (进阶):若
diff == minDiff,通常选择偏移地址更小的(题目示例未涉及冲突,但代码中可加上此逻辑保证确定性)。
- 记录当前差值
步骤 4:输出结果
- 若找到
bestOffset,输出该值。 - 若未找到(即没有足够大的空闲块),输出
-1。
三、Java 语言实现
import java.util.*;
import java.io.*;
public class MemoryAllocation {
static class Block implements Comparable<Block> {
int offset;
int size;
int end;
public Block(int offset, int size) {
this.offset = offset;
this.size = size;
this.end = offset + size;
}
@Override
public int compareTo(Block o) {
return Integer.compare(this.offset, o.offset);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取申请大小
if (!scanner.hasNextInt()) {
System.out.println("-1");
return;
}
int reqSize = scanner.nextInt();
if (reqSize <= 0) {
System.out.println("-1");
return;
}
List<Block> allocated = new ArrayList<>();
// 读取已分配内存
while (scanner.hasNextInt()) {
int offset = scanner.nextInt();
if (!scanner.hasNextInt()) {
// 格式错误:有offset没size
System.out.println("-1");
return;
}
int size = scanner.nextInt();
allocated.add(new Block(offset, size));
}
// 1. 基础校验与排序
Collections.sort(allocated);
for (Block b : allocated) {
// 校验负数、零大小
if (b.offset < 0 || b.size <= 0) {
System.out.println("-1");
return;
}
// 校验越界
if (b.end > 100) {
System.out.println("-1");
return;
}
}
// 2. 重叠检测
for (int i = 1; i < allocated.size(); i++) {
Block prev = allocated.get(i - 1);
Block curr = allocated.get(i);
// 如果当前块的起点小于前一块的终点,说明重叠
// 注意:题目说"紧接着",通常意味着 [0,1) 和 [1,2) 是不重叠的,所以用 < 而不是 <=
if (curr.offset < prev.end) {
System.out.println("-1");
return;
}
}
// 3. 生成空闲块并寻找最佳适配
int bestOffset = -1;
int minDiff = Integer.MAX_VALUE;
int currentPos = 0;
for (Block b : allocated) {
if (b.offset > currentPos) {
// 发现空闲块 [currentPos, b.offset)
int freeSize = b.offset - currentPos;
if (freeSize >= reqSize) {
int diff = freeSize - reqSize;
if (diff < minDiff) {
minDiff = diff;
bestOffset = currentPos;
} else if (diff == minDiff && bestOffset == -1) {
// 理论上不会发生,因为是从左到右遍历,先遇到的offset更小
bestOffset = currentPos;
}
}
}
currentPos = b.end;
}
// 检查尾部空闲
if (currentPos < 100) {
int freeSize = 100 - currentPos;
if (freeSize >= reqSize) {
int diff = freeSize - reqSize;
if (diff < minDiff) {
minDiff = diff;
bestOffset = currentPos;
}
}
}
System.out.println(bestOffset);
}
}
Java 代码亮点
- 面向对象设计:使用
Block类封装偏移和大小,逻辑清晰。 - 流式处理:利用
Scanner动态读取不定行数输入。 - 严谨的边界检查:涵盖了负数、越界、重叠、格式错误等所有异常场景。
- 一次遍历:在生成空闲块的同时进行贪心比较,无需额外存储空闲块列表,空间复杂度 O(1)O(1) (不计输入存储)。
四、Go 语言实现
package main
import (
"bufio"
"fmt"
"os"
"sort"
"strconv"
"strings"
)
type Block struct {
Offset int
Size int
End int
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 读取第一行:申请大小
if !scanner.Scan() {
fmt.Println("-1")
return
}
reqSizeStr := strings.TrimSpace(scanner.Text())
reqSize, err := strconv.Atoi(reqSizeStr)
if err != nil || reqSize <= 0 {
fmt.Println("-1")
return
}
var allocated []Block
// 读取后续行:已分配内存
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) != 2 {
// 格式错误
fmt.Println("-1")
return
}
offset, err1 := strconv.Atoi(parts[0])
size, err2 := strconv.Atoi(parts[1])
if err1 != nil || err2 != nil {
fmt.Println("-1")
return
}
allocated = append(allocated, Block{
Offset: offset,
Size: size,
End: offset + size,
})
}
// 1. 排序
sort.Slice(allocated, func(i, j int) bool {
return allocated[i].Offset < allocated[j].Offset
})
// 2. 校验
for _, b := range allocated {
if b.Offset < 0 || b.Size <= 0 || b.End > 100 {
fmt.Println("-1")
return
}
}
// 3. 重叠检测
for i := 1; i < len(allocated); i++ {
if allocated[i].Offset < allocated[i-1].End {
fmt.Println("-1")
return
}
}
// 4. 贪心查找最佳适配
bestOffset := -1
minDiff := 101 // 最大可能差值不会超过100
currentPos := 0
for _, b := range allocated {
if b.Offset > currentPos {
freeSize := b.Offset - currentPos
if freeSize >= reqSize {
diff := freeSize - reqSize
if diff < minDiff {
minDiff = diff
bestOffset = currentPos
}
}
}
currentPos = b.End
}
// 检查尾部
if currentPos < 100 {
freeSize := 100 - currentPos
if freeSize >= reqSize {
diff := freeSize - reqSize
if diff < minDiff {
minDiff = diff
bestOffset = currentPos
}
}
}
fmt.Println(bestOffset)
}
Go 代码亮点
- 高效 IO:使用
bufio.Scanner处理标准输入,适合处理多行数据。 - 切片操作:利用
sort.Slice匿名函数轻松实现结构体排序。 - 错误处理:对
strconv.Atoi的返回值进行严格判断,确保输入数字合法。 - 逻辑简洁:Go 的语法特性使得区间遍历和条件判断非常直观,执行效率高。
五、易错点总结⚠️
- 重叠的定义:
[0, 1)和[1, 2)是不重叠的。代码中必须使用<来判断重叠(curr.start < prev.end),如果用<=会误判相邻块为重叠。 - 空输入处理:如果没有已分配内存行,程序应能正确处理,此时整个
0-100都是空闲块。上述代码逻辑天然支持(循环不执行,直接进入尾部检查)。 - 相等长度的选择:题目要求“最接近”,若有两个空闲块长度一样且都最小(例如都需要 2,有两个长度为 2 的空闲块),通常选择偏移地址较小的。上述代码从左向右遍历,天然保证了这一点(只有
diff < minDiff才更新,相等时不更新,保留了前面的小偏移)。 - 非法值检测:题目备注提到“非法值”,包括负数、非整数等,代码中必须包含
try-catch(Java) 或error检查 (Go)。
六、复杂度分析📊
- 时间复杂度:
- 排序: O(NlogN) ,其中 N 是已分配块的数量。
- 遍历扫描: O(N) 。
- 总体: O(NlogN)。对于 N 较小的情况(通常机考题 N 不会极大),效率极高。
- 空间复杂度:
- O(N) 用于存储已分配块列表。
- 若使用流式处理且不存储(需预排序则不可行,但本题需排序检测重叠,故必须存储),空间主要为输入存储。
八、结语
这道题是贪心算法与区间管理的经典结合。它不仅考察了基本的逻辑思维能力,还考验了对边界条件和异常输入的鲁棒性处理。
- Java 选手:注意
Scanner的混合读取技巧和ArrayList的使用。 - Go 选手:享受切片和标准库带来的简洁,注意
strconv的转换陷阱。
掌握这种“排序 -> 扫描 -> 贪心”的三段式解法,你将能轻松应对各类资源分配、区间覆盖类的机考题目。祝大家代码一遍过,Offer 拿到手软!
更多推荐




所有评论(0)