- 示例工程
【免费下载链接】leetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
本篇题解围绕 doocs/leetcode 仓库中 0157.Read N Characters Given Read4 一题展开,剖析如何只借助read4这个「每次固定读 4 个字符」的底层 API 来实现read方法、按需读取 n 个字符。读完本文,你将掌握交互式(Interactive)题型的核心套路——临时缓冲 + 循环模拟的边界处理技巧,并能直接复用仓库中 Python3、Java、C++、Go 四套可直接运行的实现。
题目背景与核心概念
本题是 LeetCode 上一道加锁题(🔒),属于「数组 / 交互 / 模拟」标签下的经典题目。它模拟了一个非常贴近真实工程的场景:一个文件只能通过给定的read4方法读取,你被要求在此基础上实现read方法,且不能直接操作文件。
read4 API:以 4 字符为单位的读取原语
read4的定义如下:
参数类型: char[] buf4 返回类型: int 注意: buf4[] 是目标缓存区不是源缓存区,read4 的返回结果将会复制到 buf4[] 当中。它每次从文件中读取4 个连续的字符,写入调用方提供的缓存数组buf4,返回值是实际读取到的字符个数。需要特别强调的是,read4()自身拥有独立的文件指针(fp),很像 C 语言中的FILE *fp——也就是说,多次调用read4时,文件指针会自动前进,不需要(也不允许)你手动维护位置。
下面这张示意图直观展示了read4的工作过程(图片来源:英文版题解):

结合示例代码可以更清晰地理解它的行为:
File file("abcde"); // 文件名为 "abcde",初始文件指针 (fp) 指向 'a' char[] buf4 = new char[4]; // 创建一个缓存区使其能容纳足够的字符 read4(buf4); // read4 返回 4。现在 buf4 = "abcd",fp 指向 'e' read4(buf4); // read4 返回 1。现在 buf4 = "e",fp 指向文件末尾 read4(buf4); // read4 返回 0。现在 buf4 = "",fp 指向文件末尾可见read4的返回值有三种情况:4(读满一组)、1~3(文件剩余不足 4 个,最后一次读取)、0(已经到达文件末尾 EOF)。这个「返回值即实际字符数」的设计,是后续模拟算法的关键依据。
read 方法:基于 read4 封装目标读取
你要实现的方法定义如下:
参数类型: char[] buf, int n 返回类型: int 注意: buf[] 是目标缓存区不是源缓存区,你需要将结果写入 buf[] 中。与read4类似,buf同样只是目标缓存区,你需要把从文件中读到的字符写入buf;返回值是实际读取到的字符数。
关键约束
本题有三个重要约束,直接决定了算法形态:
- 不能直接操作文件:文件只能通过
read4访问,不能用read之外的手段读取; read函数只在每个测试用例调用一次:这意味着不需要考虑跨调用保留缓冲区内残留字符的问题,实现可以大幅简化;- 目标缓存数组
buf保证有足够空间:可以放心写入 n 个字符而不必担心越界。
此外,题目数据范围(见 英文版文档)为:1 <= file.length <= 500,文件由英文字母和数字组成,1 <= n <= 1000。
示例分析
题目给出了四个典型示例,覆盖了各种边界情况:
| 输入 file | n | 输出 | 说明 |
|---|---|---|---|
"abc" | 4 | 3 | 文件只有 3 个字符,读不满 n,返回实际读取数 3 |
"abcde" | 5 | 5 | 文件共 5 个字符,恰好读完,返回 5 |
"abcdABCD1234" | 12 | 12 | 文件共 12 个字符,是 4 的整数倍,完整读回 |
"leetcode" | 5 | 5 | 文件字符多于 n,只读取前 5 个字符"leetc",返回 5 |
前两个示例分别覆盖了「文件比 n 短」和「文件长度不是 4 的倍数」两种情况;后两个则分别覆盖了「恰好是 4 的倍数」与「文件比 n 长、需要提前截断」的情况。任何实现都必须同时正确处理这四种场景。
解题思路:循环模拟读取过程
题解文档给出的方法一是模拟(Simulation),思路非常直接:
- 准备一个长度固定为 4 的临时缓冲
buf4; - 循环调用
read4(buf4),把读到的v个字符逐个拷贝到目标缓冲buf中; - 每拷贝一个字符计数
i加一,一旦i达到n就提前返回 n; - 当
read4返回值小于 4(即文件已读完)时结束循环,返回实际拷贝的字符数i。
题解文档中的思考要点原话为:
只能通过
read4读文件,且本题read只调用一次。需要的字符数可能不是 4 的倍数,最后一次还可能不足 4 个。循环调用read4写入临时缓冲,再拷到目标,凑满n或读到文件末尾即停。
这句话点明了算法的三个终止条件:凑满 n、读到文件末尾、以及二者的任意组合。时间复杂度为O(n),其中 n 为要读取的字符数目。
多语言实现
仓库目录 0157.Read N Characters Given Read4 下提供了 Python3、Java、C++、Go 四种语言的完整解法源码,与题解文档中的代码一一对应。下面逐一展开。
Python3
Python 版本中,read4已由平台预定义,你只需实现Solution.read:
""" The read4 API is already defined for you. @param buf4, a list of characters @return an integer def read4(buf4): # Below is an example of how the read4 API can be called. file = File("abcdefghijk") # File is "abcdefghijk", initially file pointer (fp) points to 'a' buf4 = [' '] * 4 # Create buffer with enough space to store characters read4(buf4) # read4 returns 4. Now buf = ['a','b','c','d'], fp points to 'e' read4(buf4) # read4 returns 4. Now buf = ['e','f','g','h'], fp points to 'i' read4(buf4) # read4 returns 3. Now buf = ['i','j','k',...], fp points to end of file """ class Solution: def read(self, buf, n): """ :type buf: Destination buffer (List[str]) :type n: Number of characters to read (int) :rtype: The number of actual characters read (int) """ i = 0 buf4 = [0] * 4 v = 5 while v >= 4: v = read4(buf4) for j in range(v): buf[i] = buf4[j] i += 1 if i >= n: return n return i对应源码见 Solution.py。这里用[0] * 4预分配 4 格缓冲,v = 5作为初始哨兵值保证循环至少执行一次(原因见下文「实现细节」一节)。
Java
Java 版本中,read4定义在父类Reader4中,Solution通过继承获得该 API:
/** * The read4 API is defined in the parent class Reader4. * int read4(char[] buf4); */ public class Solution extends Reader4 { /** * @param buf Destination buffer * @param n Number of characters to read * @return The number of actual characters read */ public int read(char[] buf, int n) { char[] buf4 = new char[4]; int i = 0, v = 5; while (v >= 4) { v = read4(buf4); for (int j = 0; j < v; ++j) { buf[i++] = buf4[j]; if (i >= n) { return n; } } } return i; } }对应源码见 Solution.java。Java 中char数组默认初始化为\0,但每次read4后只读取前v个有效字符,因此残留值不会影响结果。
C++
C++ 版本与 Java 结构一致,read4同样是父类Reader4提供的接口:
/** * The read4 API is defined in the parent class Reader4. * int read4(char *buf4); */ class Solution { public: /** * @param buf Destination buffer * @param n Number of characters to read * @return The number of actual characters read */ int read(char* buf, int n) { char buf4[4]; int i = 0, v = 5; while (v >= 4) { v = read4(buf4); for (int j = 0; j < v; ++j) { buf[i++] = buf4[j]; if (i >= n) { return n; } } } return i; } };对应源码见 Solution.cpp。
Go
Go 版本使用了闭包式的函数签名:read4作为参数传入,solution返回一个func([]byte, int) int类型的读取函数:
/** * The read4 API is already defined for you. * * read4 := func(buf4 []byte) int * * // Below is an example of how the read4 API can be called. * file := File("abcdefghijk") // File is "abcdefghijk", initially file pointer (fp) points to 'a' * buf4 := make([]byte, 4) // Create buffer with enough space to store characters * read4(buf4) // read4 returns 4. Now buf = ['a','b','c','d'], fp points to 'e' * read4(buf4) // read4 returns 4. Now buf = ['e','f','g','h'], fp points to 'i' * read4(buf4) // read4 returns 3. Now buf = ['i','j','k',...], fp points to end of file */ var solution = func(read4 func([]byte) int) func([]byte, int) int { // implement read below. return func(buf []byte, n int) int { buf4 := make([]byte, 4) i, v := 0, 5 for v >= 4 { v = read4(buf4) for j := 0; j < v; j++ { buf[i] = buf4[j] i++ if i >= n { return n } } } return i } }对应源码见 Solution.go。可以看出,四种语言的算法骨架完全一致,只是语法与函数签名不同。
实现细节与边界处理
v >= 4 循环条件与哨兵初始化
四份实现都不约而同地使用了v = 5初始化 +while (v >= 4)循环结构的组合。这并非随意为之,而是精心设计的技巧:
read4的合法返回值只有 0、1、2、3、4 五种;- 把
v初始化为 5,可以保证第一次进入循环时v >= 4恒成立,从而让循环必然执行至少一次,完成首次read4调用; - 循环体末尾对
v重新赋值后,v >= 4意味着「上一次确实读满了 4 个字符,文件还没读完,继续读」;一旦v < 4(包括读到 3 个及以下的最后一次,或读到 0 到达 EOF),循环立即终止。
这样就不需要额外的while True+break结构,代码更紧凑且不易出错。
提前返回 n 与 EOF 兜底返回 i
在将buf4拷贝到buf的内层循环中,每拷贝一个字符都会检查if (i >= n) return n;,这是为了处理「文件字符数多于 n」的情况(如示例 4 中file = "leetcode"只取前 5 个字符):一旦目标数量已凑满,立即返回n,避免多读或越界写入。
而循环结束后return i则是 EOF 兜底:当read4返回不足 4 个字符时,说明文件已耗尽,此时i就是实际读到的字符总数(如示例 1 中返回 3)。由于i不可能超过n(超了会在内层提前返回),两种返回路径的结果都是「实际读取的字符数」,语义一致。
边界场景推演
把四种示例套入上述逻辑逐一验证:
- file = "abc", n = 4:首次
read4返回 3,v = 3 < 4循环结束,i = 3,返回 3; - file = "abcde", n = 5:第一次
read4返回 4(拷贝 4 个,i = 4未达 5),继续循环;第二次read4返回 1,拷贝 1 个后i = 5 >= n,返回 5; - file = "abcdABCD1234", n = 12:三次
read4各返回 4,最后一次拷贝中i = 12恰好触发return n; - file = "leetcode", n = 5:第一次
read4返回 4(拷贝 4 个),第二次read4返回 4,但只拷贝第 1 个字符后i = 5即提前返回 5,剩余字符被丢弃——这正是本题「每次测试只调用一次 read」约束所允许的行为。
从源码结构看,正是因为题目约束了read只调用一次,才可以用这种「读到就返回」的简单策略;若换成多次调用的场景(需要把buf4中未消费的字符缓存到下一次调用),就必须额外维护一个残留缓冲,逻辑会复杂得多。这也是本题与其系列后续题目(支持多次调用版本)的核心区别点。
复杂度分析
- 时间复杂度:O(n)。循环每轮调用一次
read4至多处理 4 个字符,总处理字符数不超过 n(或文件总长度),且每个字符至多被拷贝一次; - 空间复杂度:O(1)。仅使用固定大小的临时缓冲
buf4(长度为 4),不随输入规模增长。
总结
LeetCode 157 是一道非常典型的「交互式 + 模拟」题目:给定一个只能按 4 字符粒度读取的底层 API,要求封装出按需读取 n 个字符的上层方法。其核心要点可以浓缩为三点:
- 临时缓冲 + 拷贝:用固定 4 格的
buf4承接read4结果,再逐字符写入目标buf; - 双重终止条件:
i >= n提前返回、read4返回值小于 4 时自然退出循环; - 哨兵技巧:
v = 5初始化配合v >= 4循环条件,保证循环必然执行且逻辑自洽。
仓库中 题解文档 与 英文版 提供了完整的题目描述与解法说明,Solution.py、Solution.java、Solution.cpp、Solution.go 四份源码可直接对照阅读或运行。掌握这道题,你也就掌握了处理「受限 API 封装」类交互题目的通用方法论。
- 示例工程
【免费下载链接】leetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
相关推荐
刷穿 LeetCode 1374:生成每种字符都是奇数个的字符串——基于奇偶性的模拟构造题解(LogicStack-LeetCode 仓库实战解读)
刷穿 LeetCode 1374:生成每种字符都是奇数个的字符串——基于奇偶性的模拟构造题解(LogicStack LeetCode 仓库实战解读) 本篇技术指
教程文档交替合并字符串(LeetCode 1768):双指针模拟实现 O(n+m) 交替拼接——LogicStack-LeetCode 刷穿系列题解
交替合并字符串(LeetCode 1768):双指针模拟实现 O n+m 交替拼接——LogicStack LeetCode 刷穿系列题解 本篇技术指南围绕 L
教程文档LeetCode 6. Z 字形变换(Zigzag Conversion)——doocs/leetcode 模拟法多语言题解全解析
LeetCode 6. Z 字形变换(Zigzag Conversion)——doocs/leetcode 模拟法多语言题解全解析 本文围绕 LeetCode
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考