☰
LeetCode 157 用 Read4 读取 N 个字符:基于交互式模拟的多语言题解
2026/10/17 6:33:38 网站建设 项目流程
  • 示例工程

【免费下载链接】leetcode

🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解

项目地址:https://gitcode.com/doocs/leetcode
点击查看免费下载

本篇题解围绕 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的工作过程(图片来源:英文版题解):

![LeetCode 157 read4 从文件中读取连续字符到 buf4 缓冲区的工作原理示例](https://raw.gitcode.com/doocs/leetcode/raw/0b2b77121bbd592d32a4020611e34e92412448d8/solution/0100-0199/0157.Read N Characters Given Read4/images/157_example.png?utm_source=gitcode_repo_files)

结合示例代码可以更清晰地理解它的行为:

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。

示例分析

题目给出了四个典型示例,覆盖了各种边界情况:

输入 filen输出说明
"abc"43文件只有 3 个字符,读不满 n,返回实际读取数 3
"abcde"55文件共 5 个字符,恰好读完,返回 5
"abcdABCD1234"1212文件共 12 个字符,是 4 的整数倍,完整读回
"leetcode"55文件字符多于 n,只读取前 5 个字符"leetc",返回 5

前两个示例分别覆盖了「文件比 n 短」和「文件长度不是 4 的倍数」两种情况;后两个则分别覆盖了「恰好是 4 的倍数」与「文件比 n 长、需要提前截断」的情况。任何实现都必须同时正确处理这四种场景。

解题思路:循环模拟读取过程

题解文档给出的方法一是模拟(Simulation),思路非常直接:

  1. 准备一个长度固定为 4 的临时缓冲buf4;
  2. 循环调用read4(buf4),把读到的v个字符逐个拷贝到目标缓冲buf中;
  3. 每拷贝一个字符计数i加一,一旦i达到n就提前返回 n;
  4. 当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(超了会在内层提前返回),两种返回路径的结果都是「实际读取的字符数」,语义一致。

边界场景推演

把四种示例套入上述逻辑逐一验证:

  1. file = "abc", n = 4:首次read4返回 3,v = 3 < 4循环结束,i = 3,返回 3;
  2. file = "abcde", n = 5:第一次read4返回 4(拷贝 4 个,i = 4未达 5),继续循环;第二次read4返回 1,拷贝 1 个后i = 5 >= n,返回 5;
  3. file = "abcdABCD1234", n = 12:三次read4各返回 4,最后一次拷贝中i = 12恰好触发return n;
  4. 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 个字符的上层方法。其核心要点可以浓缩为三点:

  1. 临时缓冲 + 拷贝:用固定 4 格的buf4承接read4结果,再逐字符写入目标buf;
  2. 双重终止条件:i >= n提前返回、read4返回值小于 4 时自然退出循环;
  3. 哨兵技巧:v = 5初始化配合v >= 4循环条件,保证循环必然执行且逻辑自洽。

仓库中 题解文档 与 英文版 提供了完整的题目描述与解法说明,Solution.py、Solution.java、Solution.cpp、Solution.go 四份源码可直接对照阅读或运行。掌握这道题,你也就掌握了处理「受限 API 封装」类交互题目的通用方法论。

  • 示例工程

【免费下载链接】leetcode

🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解

项目地址:https://gitcode.com/doocs/leetcode
点击查看免费下载

相关推荐

上一篇:明日方舟美术素材怎么找?一套开源资源库集齐立绘、头像与技能图标的完整方法
下一篇:Cocos Creator 网页调试工具快速上手:ccc-devtools 一键安装与使用完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询