LeetCode 157. 用 Read4 读取 N 个字符
题目描述
题意分析
系统只给了一个底层接口
read4(buf4):它从一个文件里读取字符,一次最多读 4 个,写进调用者提供的 4 字节缓冲区,返回实际读到的个数。要用它实现read(buf, n),把最多n个字符读进buf,并返回实际读到的个数。「最多
n个」是关键措辞:文件可能比n短,此时能读多少算多少,返回值就是真实读到的数量,而不是n。所以返回值必须是累计计数,不能无脑返回n。
read4的返回值本身携带了两条信息:一是这次实际拿到几个字符,二是文件有没有到底。返回值小于 4 就说明文件读完了;返回 0 更是明确的终止信号。这两点决定了循环的退出条件有两个而不是一个。4 与
n之间没有整除关系,所以最后一批必然可能读多。比如n = 5时第二次read4会拿回 4 个字符,但只能用其中 1 个,多出来的 3 个不能写进buf,否则就越界或污染了调用方的缓冲区。这是本题唯一真正的技术点。题目还有一个隐含前提:
read只会被调用一次,所以不需要在两次调用之间保存上次没用完的字符(那是 158 题的事)。多读出来的字符直接丢弃即可。边界上
n = 0时应当一次read4都不调用、直接返回 0;文件为空时第一次read4就返回 0,同样返回 0。
解法:分批 read4 拷贝
核心思路
read4每次最多返回 4 个字符,但目标缓冲区本轮可能只剩不足 4 个位置。因此每轮真正复制的数量是min(count, n-copied),其中count是本次底层读取量。维护
copied表示已经写入目标数组的字符数。循环只在copied < n时调用read4;复制本批可用字符后,若count < 4,说明已经到达文件末尾,立即停止。不变量:每轮开始时,
buf[0..copied)恰好是文件开头的copied个字符,且0 <= copied <= n。本轮只追加临时缓冲区的前take个字符,因此顺序和边界继续成立。正确性:每次
read4返回的字符都按原顺序复制,且不会超过调用方要求的n个。循环因读满n或文件结束而停止,所以返回值恰好是实际复制数量。题目保证read只调用一次,本批多读但未使用的字符无需跨调用保存。
解题步骤
- 创建长度为 4 的临时缓冲区,初始化
copied=0。- 当
copied<n时调用read4,计算take=min(count,n-copied)。- 将临时缓冲区前
take个字符复制到buf,同步增加copied。- 若
count<4,文件已结束;否则继续读取下一批。- 返回
copied。文件为
"abcdefg"、n=5时,两次read4分别返回 4 和 3;第二批只复制e,最终返回 5。多出的f、g在本题单次调用语义下可以丢弃。边界上,
n=0时不得调用read4;文件为"ab"、n=5时首批返回 2,复制后立即结束并返回 2。
代码实现
public class Solution extends Reader4 {
public int read(char[] buf, int n) {
char[] tmp = new char[4];
int copied = 0;
while (copied < n) {
int cnt = read4(tmp);
int take = Math.min(cnt, n - copied);
System.arraycopy(tmp, 0, buf, copied, take);
copied += take;
if (cnt < 4) {
break;
}
}
return copied;
}
}
func read(buf []byte, n int) int {
tmp := make([]byte, 4)
copied := 0
for copied < n {
cnt := read4(tmp)
take := cnt
if n-copied < take {
take = n - copied
}
copy(buf[copied:], tmp[:take])
copied += take
if cnt < 4 {
break
}
}
return copied
}
复杂度分析
- 时间复杂度:$O(n)$,最多复制
n个字符,并调用 $O(\lceil n/4 \rceil)$ 次read4。- 空间复杂度:$O(1)$,临时缓冲区固定为 4 个字符。
关键点总结
- 底层读取量与本轮需求量不同,复制量必须取两者较小值。
count<4表示文件结束,但这一批已有字符仍要先复制。n=0时循环不进入,避免无意义地推进底层文件指针。- 返回实际复制量,不假设文件一定有
n个字符。- 本题只调用一次
read,无需保存本轮多读字符;多次调用版本才需要持久缓冲区。
易错点总结
- 每批全部复制:文件
"abcdefg"、n=5时第二批会越过目标缓冲区边界。- 在
count<4时先退出再复制:文件"abc"会错误返回 0。take=min(count,n):第二批没有扣除已复制数量,仍可能越界。- 不处理
count=0:它也满足count<4;若不退出,文件较短时会死循环。- 固定返回
n:文件不足时会把未写入位置冒充有效字符。- 为单次调用保存剩余字符:不影响正确性,但增加了本题不需要的跨调用状态。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 158. 用 Read4 读取 N 个字符 II - 多次调用 | 困难 | 必须跨调用缓存上次多读的字符,需要持久化缓冲区与消费指针 |
| 341. 扁平化嵌套列表迭代器 | 中等 | 同样要把底层不规则的数据源包装成按需取用的接口,靠栈做惰性展开 |
| 281. 锯齿迭代器 | 中等 | 多个数据源轮流取值,重点在 hasNext 与 next 的状态一致性 |
| 251. 展开二维向量 | 中等 | 需要跳过空的子数组,指针推进逻辑必须在取值前完成对齐 |
| 173. 二叉搜索树迭代器 | 中等 | 把递归遍历改写成可暂停的显式栈,考的是控制流的手工拆解 |
| 622. 设计循环队列 | 中等 | 定长缓冲区上的读写指针管理,与本题的临时缓冲区管理同源 |
| 1188. 设计有限阻塞队列 | 中等 | 生产消费两端都要处理缓冲区满与空的边界,多了并发同步的维度 |