华为OD的Python面试,八股文到底背到什么程度才算过关?最近好几个准备OD机考的朋友都问过我这个。说实话,这个问题的答案有点反直觉:单纯背结论很难过关,但完全不背、指望临场发挥,同样容易翻车,因为OD技术面非常喜欢在基础概念上连续追问,一个问题至少会往下追问两三层。我把自己整理的Python面试八股文系列推进到了第六期,这一期筛掉了大量“背了不加分”的偏题,只保留高频且能体现真实功底的题目,核心覆盖内置对象、装饰器闭包、内存回收、GIL并行、机考算法模板几个模块,每题都附参考答案和追问方向。适合正在准备华为OD机试与技术面的Python候选人,也适合Java转Python、想快速补基础的朋友直接当复习提纲。
顺便回应一下总有人问的“华为OD好进吗”:从实际操作来看,机试和技术面是硬门槛,这两关能稳定通过,说明基本盘已经没问题了,后面的环节更多是匹配和沟通。所以与其焦虑“好不好进”,不如先把技术面高频题的真实理解拿下来。
1. 华为OD面试的考察逻辑:为什么Python基础题值得认真背
从公开面经和周围上岸朋友的反馈来看,华为OD的流程一般是:机试、性格测试、技术面、综合面。机试是算法关卡,主要筛编程能力;技术面才是八股文的战场,面试官会结合简历项目、语言基础、系统设计能力来提问。很多人把机试当成了唯一难点,结果挂在技术基础上,非常可惜。
八股文在技术面里的作用不是“决定项”,而是“淘汰项”。如果基础题答得磕磕绊绊,面试官对你项目中讲出来的东西也会打折扣;反过来,基础题答得干净利落,哪怕项目稍微普通一点,通过概率也会明显提高。我见过不少候选人机试高分,却在问“字典底层怎么实现的”这种问题时愣住,最后被刷。这其实是可以靠系统复习避免的。
那什么样的八股文回答才算好?我总结的标准是:结论、代码、边界三件套。换句话说,既要能一句话说清概念,也要能顺手写出演示代码,还得知道这个知识点在什么情况下不成立。比如问“Python里列表和元组有什么区别”,初级回答是“列表可变,元组不可变”,这个回答只能拿三分。往上走一层:列表底层是可变长度的指针数组,能append、insert、pop,支持扩容;元组底层是定长的结构体,创建速度更快、内存更紧凑。再走一层:元组里的元素如果是可变对象,这个可变对象内部还是能改,元组的不可变是指容器大小和元素引用不可变,不是嵌套对象绝对不变。这个深度才是OD技术面期望的水平。
我在准备系列资料时发现,很多题的面试追问方向其实是固定的,大概就三类:底层机制、边界条件、实际场景。后面每一道题我都按这个套路来展开,你直接拿这个标准练,比自己瞎背有效得多。
2. 高频基础题拆解:可变性、is与==、深拷贝与浅拷贝
这三组题是OD面试里真正的“送命题”,因为它们最能暴露候选人对Python对象模型的理解深度。从Java转Python的人特别容易在这里翻车,因为Java里==比较引用、equals比较值,Python的is和==划分逻辑类似,但实现细节完全不同,小整数缓存和字符串驻留又让结果变得很“玄学”。
2.1 可变对象与不可变对象:三个必答层次
面试官问法:Python里哪些对象可变,哪些不可变?可变对象当函数默认参数会怎样?
参考答案分三层。
第一层,结论。不可变对象包括int、float、str、tuple、frozenset、bytes;可变对象包括list、dict、set、bytearray。
第二层,代码演示。
# 可变对象:内容改变,引用不变 a = [1, 2] b = a b.append(3) print(a) # [1, 2, 3] # 不可变对象:任何修改都会产生新对象 s = "hello" t = s.upper() print(s) # hello,原对象没变 print(t) # HELLO第三层,边界条件和经典陷阱。最常考的就是默认参数陷阱:
def append_item(item, target=[]): target.append(item) return target print(append_item(1)) # [1] print(append_item(2)) # [1, 2] # 第二次调用共用同一个默认列表原因是默认参数在函数定义时就被创建并缓存了,后面每次调用用的是同一个对象。正确写法是:
def append_item(item, target=None): if target is None: target = [] target.append(item) return target面试官继续追问:不可变对象真的绝对不可变吗?注意tuple里嵌套list的情况——t = (1, [2, 3]),t[1].append(4)是可以成功的,因为tuple只保证元素的引用不可变,不保证引用指向的对象内部不可变。这个问题能答出来,基本就能区分“背概念”和“真理解”了。
2.2 is与==:值比较和身份比较的红线
面试官问法:is和==有什么区别?为什么a = 256; b = 256; a is b是True,换成257就是False?
参考答案:==比较的是两个对象的值是否相等,会调用对象的__eq__方法;is比较的是两个对象是否指向同一个内存地址,等价于id(a) == id(b)。
代码演示:
a = [1, 2] b = [1, 2] print(a == b) # True,内容相同 print(a is b) # False,两个不同的对象 x = 256 y = 256 print(x is y) # True,小整数缓存 m = 257 n = 257 print(m is n) # 通常为False,超出缓存范围这里要把原因说透:CPython启动时会预先创建-5到256范围内的小整数对象,所有引用这个范围内的代码都指向同一组对象,所以is为True。而257不在预创建范围里,每次计算都会新建对象,所以is通常为False。严格来说,同一段代码块中常量和字节码的常量池合并也会影响结果,存在边界情况,但面试只要讲到缓存机制这一层就足够了。
进一步追问:字符串驻留是怎么回事?Python会对部分字符串(短字符串、看起来像标识符的字符串、编译期常量)做intern操作,让它们复用同一对象。但规则并不保证所有字符串都驻留,所以不要依赖字符串的is比较,写业务代码一律用==,判断None才用is。最后这个习惯是面试官很看重的点,能主动说出来非常加分。
2.3 深拷贝、浅拷贝和直接赋值:一张表讲完
面试官问法:copy模块用过吗?浅拷贝和深拷贝区别是什么?
参考答案配合代码演示。直接赋值只是把引用复制一份,两个变量指向同一个对象;浅拷贝copy.copy()会创建一个新容器对象,但容器里的元素还是原对象的引用;深拷贝copy.deepcopy()会递归复制所有层级的可变对象。
import copy lst = [1, [2, 3], [4, 5]] lst2 = lst lst3 = copy.copy(lst) lst4 = copy.deepcopy(lst) lst[1].append(999) print(lst2) # [1, [2, 3, 999], [4, 5]] 引用,跟随变化 print(lst3) # [1, [2, 3, 999], [4, 5]] 新外壳,内层还是同一个 print(lst4) # [1, [2, 3], [4, 5]] 完全独立表格整理更直观:
| 操作 | 外层容器 | 内层可变元素 | 修改原对象后 |
|---|---|---|---|
| 直接赋值 | 不新建 | 完全共享 | 所有变量都变 |
| 浅拷贝 | 新建 | 共享 | 内层变化会同步 |
| 深拷贝 | 新建 | 新建 | 完全隔离 |
面试官常追问:浅拷贝在什么场景下够用?如果你只是需要一个独立的顶层容器,比如list(original)这种操作,浅拷贝就够;但如果列表里嵌套了字典、列表,并且要修改内层,浅拷贝就会互相污染,必须用深拷贝。还有一个小坑:copy.deepcopy对递归结构(对象自己引用自己)也能处理,它会维护一个记忆字典来避免无限递归,这个可以提一句,体现对库实现的理解。
3. 装饰器、闭包与生成器:必须讲到原理层
这三样是Python特色,其他语言背景的候选人基本都答不好。OD面试里出现的频率很高,尤其喜欢让手写装饰器和讲解闭包陷阱。
3.1 手写装饰器:为什么要加functools.wraps
面试官问法:手写一个计时装饰器,说说装饰器的本质。
装饰器的本质是闭包加语法糖:它接收一个函数,返回一个新函数,新函数在调用原函数的前后插入额外逻辑。基础写法:
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} cost {time.perf_counter() - start:.6f}s") return result return wrapper @timer def process(): time.sleep(0.1) process() # process cost 0.100xxx s这里必须讲清楚@wraps的作用:如果不加,wrapper会覆盖原函数的__name__和__doc__,导致调试信息丢失,单元测试和日志里看到的函数名全部变成wrapper。@wraps本质上是把原函数的元信息复制到wrapper上。
追问方向有两个。第一个是带参数的装饰器怎么写:用三层嵌套,外层接收装饰器参数,中间层接收函数,内层接收调用参数。
def repeat(times): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(): print("hello")第二个追问是装饰器实际用在哪。我在项目里常用的场景包括:接口鉴权、操作日志、输入参数校验、重试机制、结果缓存。能主动说出这些场景,面试官会觉得你不是只会背语法,而是真的有工程意识。
3.2 闭包与late binding:循环里创建lambda的经典坑
面试官问法:什么是闭包?下面这段代码输出什么?
funcs = [lambda: i for i in range(3)] for f in funcs: print(f())答案是输出三个2,不是0、1、2。原因是闭包捕获的是变量i的引用,而不是创建时的值。循环结束后i已经变成了2,所有lambda函数调用时取到的都是2。
这个坑叫late binding,延迟绑定。在JS里用let可以规避,在Python里常见的解决方案是使用默认参数提前绑定:
funcs = [lambda i=i: i for i in range(3)] for f in funcs: print(f()) # 0 1 2因为默认参数在函数定义时就被求值了,i的当前值被固定下来。
进一步讲闭包的原理:当一个内嵌函数引用了外层函数的局部变量,这个变量会保存在函数的__closure__属性里,即使外层函数已经返回,闭包环境依然存活。可以现场演示看一下f.__closure__里的cell对象。这个操作能直接证明自己理解的是机制,不是背出来的结论。
3.3 生成器与迭代器:yield的挂起与恢复模型
面试官问法:迭代器和生成器什么关系?yield是怎么工作的?
参考答案:迭代器是实现了__iter__和__next__协议的对象,for循环本质上就是不断调用next()直到抛出StopIteration。生成器是写起来最方便的迭代器,任何包含yield的函数都会变成生成器函数,调用它不会执行函数体,而是返回一个生成器对象。
def countdown(n): while n > 0: yield n n -= 1 gen = countdown(3) print(next(gen)) # 3 print(next(gen)) # 2 print(next(gen)) # 1yield的执行模型是:每次next()让函数体运行到yield处,返回右边的值并挂起,整个局部变量状态被保留;下一次next()从挂起点恢复继续执行。这就是“惰性求值”的核心。
面试官很喜欢考生成器的内存优势:
# 生成器方式:常数级内存 total = sum(range(100_000_000)) # 列表方式:先创建1000万元素的列表,再求和,内存暴涨 total = sum(list(range(100_000_000)))这背后的逻辑是生成器不会一次性把所有元素加载到内存,而是逐个产出逐个丢弃。遇到海量日志处理、超大文件读取这类场景,这个特性非常关键。最后可以提一句yield from用于子生成器委托,让生成器可以复用另一个生成器的产出,属于进阶加分项。
4. 内存管理与垃圾回收:从引用计数到分代回收的完整链路
OD面试对Python内存管理的考察深度一直在线,这个模块答得好,直接证明你对Python运行时是真的了解,而不是只会写业务代码。
4.1 引用计数:Python垃圾回收的地基
CPython的垃圾回收以引用计数为主。每个对象内部维护一个引用计数,被引用一次计数加一,引用解除计数减一,计数归零时立即回收内存。可以用sys.getrefcount观察:
import sys a = [] print(sys.getrefcount(a)) # 2,注意getrefcount本身会临时引用一次 b = a print(sys.getrefcount(a)) # 3引用计数的优点是实时性,对象销毁时机确定,没有漫长的停顿;缺点是维护计数本身有开销,而且无法解决循环引用问题。两个对象互相引用,外部又没有变量指向它们,引用计数永远到不了零,就泄漏了。这就是为什么还需要gc模块。
4.2 循环引用与分代回收:gc模块在做的事
经典例子:
class Node: def __init__(self): self.next = None a = Node() b = Node() a.next = b b.next = a del a del b此时两个Node对象引用计数都不是0,但已经无法从外部访问了,纯靠引用计数回收不掉。gc模块用标记清除算法处理:从根对象(全局变量、调用栈等)出发,遍历所有可达对象并标记,没有被标记的对象就是垃圾,直接清理。
分代回收是gc的性能优化手段。对象分为三代:0代、1代、2代。新建对象进入0代,每发生一次0代回收,存活的对象晋升到1代,1代回收后再晋升到2代。代越老,被回收的频次越低,因为存活越久的对象越不可能是垃圾。默认阈值可以用gc.get_threshold()查看,通常返回(700, 10, 10),意思是0代每累计700次分配与释放差值就触发一次回收,1代在0代回收10次后触发,2代在1代回收10次后触发。
这个“回收频率随代数递减”的设计很像缓存的分层策略:新对象大多是短命的,频繁扫描新生代收益最高;老对象稳定存活,没必要反复扫。理解了这一点,面试官怎么追问底层逻辑你都能接住。
4.3 内存泄漏场景与排查思路
面试官会问:Python不是有垃圾回收吗,为什么还会内存泄漏?能说出下面几个场景就有深度:
第一,全局容器无限增长。比如全局缓存dict不断往里面塞数据,没有清理机制,这属于业务层面的泄漏。第二,循环引用加上类里定义了__del__方法。Python 3.4之前这种情况gc无法回收,3.4之后虽然能回收,但回收顺序不确定,可能导致__del__里访问的对象已经被回收。第三,闭包意外持有大对象。外层函数返回内层函数后,整个闭包环境一直被函数对象引用,如果环境里包含一个大列表,只要函数对象还在,内存就释放不掉。第四,长连接池或requests.Session对象过期后没有清理。
排查方式可以提两个趁手的工具:tracemalloc能定位到具体文件行号的分配点,gc.get_objects()能查看当前存活对象并按类型统计。实测下来,线上问题先跑tracemalloc拿分配快照,再对比前后几次快照的差异,基本几轮就能定位到泄漏源头。面试时候能说出这套排查链路,比单纯背概念强得多。
5. GIL、多线程与协程:并发编程题的答题主线与两个坑
并发编程几乎是OD技术面必考板块,GIL又是所有答案绕不开的锚点。很多人倒在这里不是因为不知道GIL是什么,而是不知道怎么组织答案,把基本原理、适用场景、绕过方式混在一起讲,逻辑混乱。
5.1 GIL的本质:为什么同一时刻只能跑一个线程
面试官问法:Python多线程为什么不能利用多核?GIL到底是什么?
参考答案:GIL是全局解释器锁,CPython解释器级别的一个互斥锁。它保证同一时刻只有一个线程在执行Python字节码,所以CPython的多线程无法真正并行执行CPU密集任务。
要解释为什么存在GIL:CPython的对象管理(特别是引用计数)不是线程安全的。如果没有GIL,多线程同时修改对象的引用计数,内存就会崩溃。加一把全局锁,简单粗暴地保证解释器内部数据安全。另一个历史原因是早期多核CPU不普及,设计者优先保证了单线程性能。
细节上要掌握:GIL不是永远锁着的。线程运行一段时间会被强制切换,默认阈值可以用sys.getswitchinterval()查看,通常是0.005秒,也就是5毫秒;另外,遇到IO操作(socket读写、文件读写、sleep)时,线程会主动释放GIL,等待期间其他线程可以执行。这两个知识点直接决定你对“IO密集任务多线程有效”的理解是否正确。
补充一点最新进展:Python 3.13开始提供试验性的free-threading构建,也就是不依赖GIL的多线程模式,但距离主流应用还有一段路,面试里提一句证明你关注版本演进就够了,不用深入。
5.2 多线程适合什么:IO密集和CPU密集的分界线
面试官问法:Python多线程还有用吗?什么场景下有用?
标准回答应该是分情况讨论:
| 任务类型 | 多线程效果 | 原因 |
|---|---|---|
| IO密集(爬虫、文件读写、数据库请求) | 明显提升 | IO期间释放GIL,多个线程可以重叠等待 |
| CPU密集(计算、循环、加解密) | 没有提升甚至变慢 | 无法并行执行,还要付出线程切换开销 |
所以写爬虫用多线程是对的,因为网络等待占大头;做大量数值计算用多线程是错的,应该用多进程。这个结论能讲清楚,面试官就会觉得你有实际经验。
再往上加一个工程细节:多线程环境下线程安全控制。Python的queue.Queue是线程安全的,适合生产者消费者模型;共享变量需要加锁,threading.Lock配合with语句使用,不然会出现脏读。这些配合项目案例讲,给面试官的印象完全不同。
5.3 多进程和协程:绕过GIL的两种思路
面试官问法:CPU密集任务怎么做?协程和线程什么区别?
CPU密集任务的正解是多进程,每个进程有自己的GIL和数据空间,可以真正并行。工程上建议直接使用ProcessPoolExecutor,不用手动管进程生命周期:
from concurrent.futures import ProcessPoolExecutor def heavy_calc(x): return x * x with ProcessPoolExecutor(max_workers=4) as pool: futures = [pool.submit(heavy_calc, i) for i in range(100)] results = [f.result() for f in futures]协程则是单线程内的并发调度。async def函数是协程,await表示让出控制权,事件循环在单线程内切换不同协程。它适合IO密集高并发场景,比如同时发起上千个网络请求,线程数开上千个不现实,协程却可以轻松承受。
关键是讲出不建议在协程里使用同步阻塞调用的原因:比如在async函数里直接time.sleep(1)或调用同步requests.get(),整个线程会被卡住,所有协程都跟着停,并发能力瞬间归零。正确做法是用asyncio.sleep和异步HTTP客户端(aiohttp、httpx的async模式)。在OD的项目面里,这个细节最能体现真实的异步编程功底。
6. OD机考算法的Python速成写法:送分题别丢分
机试是OD面试的第一道硬门槛,Python方向的机试主要考算法与数据结构基本功。从面经反馈来看,通常是三道编程题,分值分布多为100、100、200,前两题偏向数组、字符串、模拟类,第三题偏向搜索、动态规划或复杂模拟。目标不是拿满分,而是稳稳拿到前两题,第三题尽量拿部分分。
6.1 高效应试策略:先保送分题,再攻坚难题
我对备考朋友的建议是:不要一开始就刷难题,先把高频基础题型练到条件反射。真正考试时时间紧张,数组遍历、字符串处理这类题必须做到看到就能写。推荐的练习范围包括:列表与字符串操作、排序与自定义排序、哈希表计数、双指针、滑动窗口、二分查找、DFS/BFS、简单动态规划。
时间分配上,前两道简单题控制在40分钟内,给第三题留足时间。如果第三题卡了15分钟没思路,果断回去检查前两题有没有边界问题,把所有能拿的分拿稳。我见过不少人把时间耗在第三题上,结果前两题因为赶时间出现低级错误,非常不划算。
6.2 读懂输入输出的ACM模式:IO模板先背熟
OD机试是ACM模式,需要自己处理输入输出。强烈建议直接用sys.stdin.buffer读取,性能远高于input(),在面对大数据量输入时不容易超时。
import sys def solve(): data = sys.stdin.buffer.read().split() if not data: return # 示例:第一行是数组长度n,后面n个元素 n = int(data[0]) arr = list(map(int, data[1:1 + n])) # 核心逻辑 result = ... # 数组类输出统一join,避免反复print sys.stdout.write(" ".join(map(str, result))) if __name__ == "__main__": solve()养成一次读完全部输入的习惯,可以省去大量判断结尾的麻烦。如果题目必须逐行读,用sys.stdin.buffer.readline()代替input()。输出别带多余空格和换行,严格按题目要求来,有些人对这里的格式扣分没有概念,其实很冤。
6.3 高频题型的Python模板:直接抄
整理几个机试高频模板,按我自己的实战经验优化过。
排序场景:list.sort(key=...)处理多关键字排序时,元组表示优先级。
# 按第二个字段降序,再按第一个字段升序 items.sort(key=lambda x: (-x[1], x[0]))二分查找用bisect模块,别手写。
from bisect import bisect_left idx = bisect_left(arr, target) if idx < len(arr) and arr[idx] == target: print(idx) # 找到了 else: print(-1) # 没找到BFS遍历网格的模板,坐标类题目大量出现:
from collections import deque def bfs(grid, start): m, n = len(grid), len(grid[0]) q = deque([start]) visited = [[False] * n for _ in range(m)] visited[start[0]][start[1]] = True steps = 0 while q: for _ in range(len(q)): x, y = q.popleft() # 检查是否到达目标,处理当前格子 for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)): nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny]: if 可通行: visited[nx][ny] = True q.append((nx, ny)) steps += 1 return steps滑动窗口模板,处理连续子数组问题非常好用:
left = 0 cur = 0 ans = 0 for right in range(len(nums)): cur += nums[right] while cur > target: cur -= nums[left] left += 1 ans = max(ans, right - left + 1)背包类DP的滚动数组模板,内层循环记得倒序:
dp = [0] * (capacity + 1) for weight, value in items: for c in range(capacity, weight - 1, -1): dp[c] = max(dp[c], dp[c - weight] + value) print(dp[capacity])这些模板能覆盖机试60%以上的题型,剩下的临时推也能有个思路框架。
6.4 提交环境容易踩的三个坑
第一个坑是超时。Python在OJ上跑得慢,如果暴力解法时间复杂度到了O(n^2),数据规模到10万基本就危险了。经验值是一秒大约能执行一亿次简单操作,超过这个量级就要优化,常见手段是换哈希表、双指针、减少循环层数。
第二个坑是递归深度。Python默认递归深度大约1000,DFS类题目如果递归深度可能超过这个值,必须用sys.setrecursionlimit()调高,或者干脆写成栈模拟。我建议搜索题默认优先考虑栈模拟和BFS,把递归留到思路最清晰的时候再用。
第三个坑是输出格式。多组测试用例时,很多人用print反复输出,结果是每行多了一个空格或者少了换行。统一用sys.stdout.write拼接字符串,在本地自测时多复制几组样例验证,能避免大量格式分损失。
7. 从被问到卡壳到顺利通过:复盘出的答题与避坑经验
最后这部分是我一路踩坑总结出来的软技能,也是我认为比背题更重要的东西。很多人在技术面上挂了,不是不会,而是不知道怎么组织回答。
7.1 背题与理解的差距:一个追问就能现原形
有一次模拟面试,候选人很流利地背出了“is比较地址,==比较值”,我追问了一句:“那为什么a = 256; b = 256; a is b是True,换成257就是False?”对方愣了半天。这就是典型的背题——知道结论,不知道结论为什么成立。
真正的理解不只是知道答案,而是知道答案的边界。每个八股文知识点都可以问自己三个为什么:为什么是这样?为什么不那样?什么情况下这个结论不成立?用这个标准过一遍,面试官再怎么追问你都能找到方向。
7.2 技术面结构化作答三步走
我推荐的答题结构是:结论先行、代码验证、边界补充。比如问装饰器,先说“装饰器是接收函数并返回新函数的可调用对象”,然后手写一个简单装饰器验证,最后说“使用@wraps保留元信息”“带参数的装饰器需要三层嵌套”等边界情况。这样回答逻辑清楚,面试官也不用反复拉扯。
遇到真不会的题,不要硬编。直接说“这个点我没有深入研究”,然后补一句自己的分析思路。面试官一般都能接受,因为编造的答案经不起追问,而诚实的回答至少展示了你解决问题的思考方式。我在实际面试里发现,面试官更看重分析路径,而不是一个完美答案。
7.3 机考和综合面的实战提醒
机考环节,前两题按模板快速写完后,务必手动跑一遍边界样例,比如空数组、只有一个元素、最大数值输入。很多隐藏的bug都是在边界条件下暴露的。第三题如果时间不够,把暴力的部分分写法写上,不要留空。
综合面环节,聊项目时不要只放履历,重点讲“我具体负责了哪块”“遇到什么技术难点”“怎么排查和决策”。如果你是从其他语言转Python,提前想好这个问题:为什么转?你原来的语言能力和Python积累怎么结合?这类问题不是压力测试,是在考察自我认知和稳定度。我的建议是,把自己的语言切换经历包装成优势——意味着你有多语言思维,也能更快适应不同技术栈的团队。
最后再分享一个我复盘面试时的习惯:每道八股文我都会做一次“反问练习”。拿出一张纸,先写下答案,然后假设自己是面试官,对答案里的每个结论追问一个Why,一直问到问不出来为止。这个方法帮我撑过了不少深度追问,也是我推荐给每个准备OD面试的朋友最高性价比的复习方式。如果你能把这篇文章里每道题都按这个标准过一遍,机试和技术面的通过率会有非常明显的提升。