秋招八股准备
不得不向八股文低头了喵。本文应该会持续更新一段时间。
Python
Python 如果函数没有使用 return 语句,则返回的是
如果一个 Python 函数没有显式地使用 return 语句,或者执行到了函数的末尾也没有遇到 return 语句,那么函数将默认返回 None。
Python 的 pass
pass 不做任何事情,一般用做占位语句。pass 的唯一作用就是作为占位符。当你定义一个函数、类、循环或条件语句的结构,但暂时还没想好具体实现时,可以使用 pass 来保证程序语法正确,可以正常运行。
Python 的 yield
yield 关键字的主要作用是将一个普通的函数转变为一个“生成器”(Generator)。一个包含 yield 的函数在被调用时,不会立即执行,而是返回一个生成器对象。这个生成器是一个可以迭代的对象,它“懒惰地”计算值,只有在你请求时才生成下一个值。
当函数执行到 yield 时,它也会返回一个值,但函数会在此处暂停,并保存当前所有的状态(包括局部变量、执行位置等)。下次从这个生成器请求值时,函数会从它上次暂停的地方(上次 yield 语句的下一行代码处)无缝地继续执行。
示例:
1 | def countdown_generator(n): |
上面代码的输出结果:
1 | 创建了生成器对象: <generator object countdown_generator at 0x...> |
为什么使用 yield (它的优点)?
- 极高的内存效率:这是 yield 最重要的优点。生成器是“懒加载”的,它不会一次性在内存中创建所有元素。它只在你需要的时候才生成下一个元素。对于处理大数据集、读取大文件或者生成无限序列等场景,这可以极大地节省内存。
1
2
3
4
5# 如果用列表,会立即在内存中创建10亿个整数,可能导致内存崩溃
# my_list = [i for i in range(1_000_000_000)]
# 如果用生成器,它只占用极小的内存,因为它是一个对象,记录着如何生成下一个值
my_generator = (i for i in range(1_000_000_000)) - 能够处理无限序列:因为值是动态生成的,所以你可以创建一个永不停止的生成器。例如,一个生成所有自然数的生成器。
- 代码更简洁、可读性更高:相比于自己编写一个包含
__iter__()和__next__()方法的迭代器类,使用yield的生成器函数要简洁得多,逻辑也更清晰。状态管理由 Python 自动完成,无需手动维护。
Python 的 GIL锁
Python 中的 GIL 指什么?谈一下对 GIL 的理解。
简介
GIL 的全称是 Global Interpreter Lock,即全局解释器锁。
简单来说,GIL 是 CPython 解释器(我们最常使用的官方 Python 解释器)中的一把“大锁”。它的核心作用是:在同一时刻,只允许一个线程执行 Python 的字节码。
这意味着,即使你的计算机有多个 CPU 核心,一个 CPython 进程中的多个线程也无法真正地并行执行 Python 代码。它们可以在不同线程间快速切换,实现并发(Concurrency),但无法在同一瞬间利用多核优势实现并行(Parallelism)。
对 GIL 的理解可以分为三个方面:它为什么存在,它带来了什么问题,以及应该如何应对它。
为什么会有 GIL?为了简化和安全
GIL 的存在主要是历史原因,其核心目的是为了简化 CPython 的内存管理,使其线程安全。
CPython 的垃圾回收机制主要基于“引用计数”(Reference Counting)。每个 Python 对象都有一个计数器,记录着有多少个变量指向它。当计数器变为0时,对象的内存就会被回收。
想象一下,如果没有 GIL,两个线程同时尝试修改同一个对象的引用计数(一个增加,一个减少),就可能会发生“竞态条件”(Race Condition),导致计数错误。这可能会让不该被回收的对象被回收(导致程序崩溃),或者该被回收的对象永远不被回收(导致内存泄漏)。
解决这个问题,要么使用大量精细的、复杂的锁来保护每一个对象,要么就用一把简单粗暴的“全局锁”——GIL。当年的设计者选择了后者,因为它极大地简化了 CPython 本身以及 C 语言扩展库的编写,保证了内存管理的线程安全。
GIL 带来了什么问题?多线程无法利用多核
GIL 最大的负面影响就是它成为了 CPU密集型(CPU-bound) 任务的性能瓶颈。
对于 CPU 密集型任务:比如大量的数学计算、图像处理、数据分析等,程序需要持续占用 CPU。在这种情况下,即使你创建了多个线程,GIL 也会确保只有一个线程在执行 Python 代码。其他线程只能等待。这就像一条有8车道的高速公路,但只有一个收费站窗口在工作,所有车都得排队通过这一个窗口,多车道的优势完全无法发挥。
对于 I/O 密集型任务:比如网络请求、文件读写、数据库操作等,情况则完全不同。当一个 Python 线程在执行 I/O 操作时,它会因为等待数据返回而被“阻塞”。CPython 在遇到 I/O 阻塞时,会主动释放 GIL,让其他线程有机会执行。对于 I/O 密集型应用,使用多线程依然能通过并发显著提升效率。
如何应对 GIL?选择正确的工具
理解了 GIL 的影响后,我们就可以在不同的场景下选择合适的并发策略:
多线程(
threading模块):- 适用场景:I/O 密集型任务。这是利用多线程提升效率的最佳场景。
- 不适用场景:CPU 密集型任务。用了也不会比单线程快,甚至可能因为线程切换的开销而更慢。
多进程(
multiprocessing模块):- 适用场景:CPU 密集型任务。这是绕过 GIL 实现真并行计算的标准方法。
- 工作原理:
multiprocessing会创建多个独立的 Python 解释器进程。每个进程都有自己独立的内存空间和独立的 GIL。这样,操作系统就可以将这些进程调度到不同的 CPU 核心上,实现真正的并行计算。 - 缺点:进程间通信(IPC)比线程间共享内存更复杂,且创建和管理进程的开销也比线程大。
异步编程(
asyncio库):- 适用场景:高并发的 I/O 密集型任务,尤其适合需要同时处理成千上万个网络连接的场景(如 Web 服务器、爬虫)。
- 工作原理:它在单线程内通过事件循环(Event Loop)来切换任务,避免了多线程的上下文切换开销,是一种更高效的并发模型。
使用其他 Python 解释器:
- 像 Jython(运行在 JVM 上)和 IronPython(运行在 .NET CLR 上)就没有 GIL,可以实现真正的多线程并行。但它们的生态和 CPython 有差异。
| 任务类型 | 瓶颈 | GIL 影响 | 推荐方案 |
|---|---|---|---|
| CPU 密集型 | CPU 运算速度 | 大 (无法利用多核) | multiprocessing (多进程) |
| I/O 密集型 | I/O 等待时间 | 小 (I/O 等待时会释放 GIL) | threading (多线程) 或 asyncio (异步) |
杂
对 git workflow 的理解,并举例协作流程
Git Workflow 是一套团队协作的规范和策略。其核心目的是为了让多人高效、安全地并行开发,同时保持代码库历史的清晰和可追溯性。一个好的工作流可以极大地减少代码冲突,简化 Code Review,并且能保证主分支的稳定性。
它主要围绕几个关键概念:
- 分支管理:如何以及何时创建分支,分支的命名规范,以及分支的生命周期。这是工作流的核心。
- 代码集成:如何将不同分支的代码合并到主干,是通过
merge还是rebase? - 代码审查:在代码正式合入之前,通过 Pull Request (或者叫 Merge Request) 的方式让同事评审代码,确保质量。
- 版本发布:如何管理线上版本,以及如何在出现紧急 Bug 时进行修复。
我认为 “功能分支工作流 (Feature Branch Workflow)” 结合 “代码审查 (Pull Request)” 是目前最流行也最高效的协作流程。如果项目再复杂一点,可以借鉴 GitFlow 的思想,引入一个 develop 分支。
下面我来描述一下这个流程的具体样貌:
核心分支:
main(或master):这个分支的代码永远是稳定、可随时部署到生产环境的。任何人都不能直接向main分支推送代码。我们会设置分支保护规则来强制执行。develop:这是一个集成分支。所有完成开发的功能分支都会合并到这里。我们可以把它看作是“下一个版本”的预备状态。日常的开发都是基于这个分支进行的。
协作步骤:
任务开始 - 创建功能分支:
- 当我接到一个新的开发任务时(比如 “开发用户登录功能”),我首先会确保我的本地
develop分支是最新版本 (git pull origin develop)。 - 然后,我会从
develop分支创建一个新的功能分支。分支命名会清晰地描述它的用途,比如feature/user-login或者feature/TICKET-123-user-login(如果用了Jira等工具)。 - 命令:
git checkout -b feature/user-login develop
- 当我接到一个新的开发任务时(比如 “开发用户登录功能”),我首先会确保我的本地
本地开发 - 提交代码:
- 在这个
feature/user-login分支上,我进行所有的编码、修改和测试工作。 - 我会遵循“小步快跑”的原则,频繁地进行
git commit。每次提交都有一个清晰的、原子性的目的。Commit message 我会写得很规范,比如feat: add email and password validation,遵循团队的约定(比如 Conventional Commits)。 - 这保证了我的开发过程是可以被追溯的。
- 在这个
保持同步 - 更新分支:
- 为了避免我的分支和主开发分支 (
develop) 偏离太远,导致最后合并时出现大量冲突,我会定期地将develop的最新代码同步到我的功能分支。 - 我个人更倾向于使用
rebase来保持一个线性的、整洁的提交历史。 - 命令:
git pull --rebase origin develop
- 为了避免我的分支和主开发分支 (
发起审查 - 创建 Pull Request (PR):
- 当功能开发完成并且在本地测试通过后,我将功能分支推送到远程仓库:
git push origin feature/user-login。 - 然后,我会在代码托管平台(如 GitHub, GitLab)上创建一个 Pull Request,请求将我的
feature/user-login分支合并到develop分支。 - 在 PR 的描述里,我会清晰地说明:这个 PR 做了什么,为什么这么做,以及如何测试。如果有关闭某个 issue,也会在这里关联。
- 当功能开发完成并且在本地测试通过后,我将功能分支推送到远程仓库:
代码审查与持续集成 (CI):
- PR 创建后,会自动触发 CI 工具(比如 Jenkins, GitHub Actions)运行自动化检查,包括代码风格检查 (Linter)、单元测试、构建等。如果任何一项检查失败,PR 会被标记为不合格,不能合并。
- 同时,我会邀请一到两位同事来审查我的代码。他们会提出修改建议。
- 我根据反馈进行修改,提交新的 commit,然后再次推送到我的功能分支。PR 会自动更新这些改动。这个过程会一直持续到代码被批准为止。
完成合并 - 清理分支:
- 当 PR 通过了所有自动化检查和同事的审查后,项目维护者或我自己会将它合并到
develop分支。 - 这里我特别喜欢用 “Squash and Merge” 的方式。它能将我这个功能分支上所有的零散提交(比如 “fix typo”, “add comments”)合并成一个有意义的 commit,再合入
develop分支。这让develop的历史记录非常干净,每个 commit 都对应一个完整的功能或修复。 - 合并后,这个功能分支的历史使命就完成了,我会删除远程和本地的功能分支,保持仓库的整洁。
- 当 PR 通过了所有自动化检查和同事的审查后,项目维护者或我自己会将它合并到
发布和热修复 (Hotfix):
- 当
develop分支上积累了足够多的功能,准备发布新版本时,我们会将develop分支合并到main分支,并打上版本标签(如v1.2.0)。 - 如果线上版本 (
main) 出现紧急 Bug,我们会从main分支直接创建一个hotfix/fix-login-bug分支,修复后,必须同时合并回main(立即发布修复)和develop(保证后续开发也包含此修复)。
- 当
我认为这个流程最大的优点是结构清晰、责任明确、自动化程度高。它通过分支隔离了开发,通过 PR 保证了代码质量,通过 CI 保证了基本的功能正确性,最终确保了 main 分支的绝对稳定。对于一个实习生来说,这套流程能让我快速融入团队,并且我的每一行代码都会经过审查,是一个非常好的学习和成长的过程。
如果由你来主动推进项⽬怎么做?如何与相关⼈⼠讨论什么内容
如果由我来主动推进项目,我会首先确保我对项目的核心目标和价值有清晰的理解,然后将其拆解为可执行的小任务和明确的时间节点。接着,我会主动发起沟通:和产品经理讨论确认需求的优先级和验收标准;和资深同事或导师讨论技术方案的可行性和潜在风险;和前端或后端等协作方明确接口定义和联调排期。在整个过程中,我会通过简短的站会或文档保持信息透明,确保大家对进度和遇到的问题有共同的认知,从而保证项目能高效、顺畅地向前推进。
智力题
烧绳子
你有两条绳⼦,每条都需要烧⼀个⼩时才能烧完。但是每条绳⼦在每个点都有不同的密度,所以不能保证它在不同的部分燃烧时间,所以你怎么使⽤这两条绳⼦衡量45分钟?
解决方法关键在于利用“同时从两端点燃”这个操作。
这是一个衡量45分钟的具体步骤:
开始计时 (0分钟):
- 拿起第一根绳子 (绳子A),从两端同时点燃。
- 同时,拿起第二根绳子 (绳子B),只从一端点燃。
30分钟时刻:
- 绳子A因为是从两端燃烧,所以会在30分钟时正好完全烧完。
- 在绳子A烧完的那一瞬间,立即将绳子B的另一端也点燃。
45分钟时刻:
- 在30分钟时,绳子B已经燃烧了30分钟,所以它剩下的燃烧时间也是30分钟。
- 当你把绳子B的另一端也点燃后,这根“剩下30分钟燃烧时间的绳子”就会在15分钟后烧完(因为30分钟的路程由两个火头分担)。
因此,从开始到绳子B最终烧完,总时间就是 $30分钟 + 15分钟 = 45分钟$。
缺陷球
你有12个单位球,其中⼀个球是⽐其它轻或者⽐其它重的,给你⼀个天平,你如何⽤三次测量就判断哪个球是有缺陷的?并确定它是偏重还是偏轻。
将球编号为 1 到 12,然后按以下步骤操作:
第一次称量将球分成三组:
- 左盘: 球 1, 2, 3, 4
- 右盘: 球 5, 6, 7, 8
- 桌上: 球 9, 10, 11, 12
这次称量会有三种结果:
情况A:天平平衡
- 推断: 缺陷球一定在桌上的四颗球(9, 10, 11, 12)中,而且1到8号球都是标准球。
- 进入第二次称量(情况A)
情况B:左盘下沉 (左重右轻)
- 推断: 缺陷球在参与称量的八颗球中。可能性是:(1, 2, 3, 4) 中有一个是重球,或者 (5, 6, 7, 8) 中有一个是轻球。
- 进入第二次称量(情况B)
情况C:右盘下沉 (左轻右重)
- 推断: 这和情况B完全对称。可能性是:(1, 2, 3, 4) 中有一个是轻球,或者 (5, 6, 7, 8) 中有一个是重球。
- 进入第二次称量(情况C)
第二次称量,现在根据第一次的结果进行操作:
第二次称量(情况A下)
- 我们知道缺陷球在 (9, 10, 11, 12) 中,且(1, 2, 3)是标准球。
- 左盘: 球 9, 10, 11
- 右盘: 球 1, 2, 3 (已知标准球)
- 结果1:天平平衡。说明 (9, 10, 11) 都是好球,那么缺陷球就是12号。为了判断轻重,进行第三次称量:将12号球与任意一个标准球(如1号)称量,即可知道12号是重是轻。
- 结果2:左盘下沉。说明缺陷球在 (9, 10, 11) 中,并且是重球。进行第三次称量:左盘放9号,右盘放10号。如果左盘下沉,则9号是重球;右盘下沉,则10号是重球;如果平衡,则11号是重球。
- 结果3:右盘下沉。说明缺陷球在 (9, 10, 11) 中,并且是轻球。进行第三次称量:左盘放9号,右盘放10号。如果左盘下沉,则10号是轻球;右盘下沉,则9号是轻球;如果平衡,则11号是轻球。
第二次称量(情况B下 - 左重右轻)
- 我们知道可能性是:(1,2,3,4)中一个重 或 (5,6,7,8)中一个轻。同时,9,10,11是标准球。
- 我们将这些可能性混合重组:
- 左盘: 球 1, 2, 5
- 右盘: 球 3, 6, 9
- 结果1:天平平衡。说明缺陷球在剩下的 (4, 7, 8) 之中。根据第一次称量,我们知道4只可能是重球,7和8只可能是轻球。进行第三次称量:左盘放7号,右盘放8号。如果平衡,则4号是重球;如果左盘下沉,8号更轻,则8号是轻球;如果右盘下沉,7号更轻,则7号是轻球。
- 结果2:左盘下沉。说明问题出在 (1, 2, 5, 3, 6, 9) 中。结合两次称量结果分析:不可能是3号(因为它在右盘),不可能是5号(它如果是轻球,右盘会下沉)。所以只可能是1号或2号是重球,或者6号是轻球。进行第三次称量:左盘放1号,右盘放2号。如果左盘下沉,1号是重球;右盘下沉,2号是重球;如果平衡,则6号是轻球。
- 结果3:右盘下沉。同样分析:不可能是1,2,6号。所以只可能是5号是轻球,或者3号是重球。进行第三次称量:左盘放3号,右盘放一个标准球(如9号)。如果左盘下沉,3号是重球;如果天平平衡,则5号是轻球。
第二次称量(情况C下 - 左轻右重)
- 这个情况和B完全相反,所有的推理逻辑镜像对称即可。
- 左盘: 球 1, 2, 5
- 右盘: 球 3, 6, 9
- 结果1:天平平衡。缺陷球在 (4, 7, 8) 中。4只可能是轻球,7和8只可能是重球。第三次称量7号和8号,重的胜出,平衡则是4号轻。
- 结果2:左盘下沉。5号是重球或3号是轻球。第三次称量3号和标准球,平衡则5号重,不平衡则3号轻。
- 结果3:右盘下沉。1号或2号是轻球或6号是重球。第三次称量1号和2号,轻的胜出,平衡则是6号重。
通过这个流程,无论出现哪种情况,我都能在第三次称量时准确地找到那个缺陷球,并确定它的重量是偏重还是偏轻。
100!里有多少个0
100! (100的阶乘) 的末尾有 24 个零。
一个数的末尾有零,是因为这个数是10的倍数。而10可以分解为质因数 $2 \times 5$。
在 100! 这个庞大的乘积($1 \times 2 \times 3 \times \dots \times 100$)中,我们需要找出有多少对 (2, 5)。
分析因子:在1到100的数字中,因子
2的数量远远多于因子5的数量(例如,所有偶数都包含因子2)。因此,(2, 5)对的数量实际上取决于有多少个因子5,因为5是这里的“短板”。计算因子5的数量:问题就简化为计算从1到100的所有数字总共能分解出多少个因子
5。- 首先,能被5整除的数会提供一个因子
5。这些数是 5, 10, 15, …, 100。
数量为:$100 \div 5 = 20$ 个。 - 其次,需要特别注意的是,能被25(即 $5^2$)整除的数会提供两个因子
5。在上面的计算中,我们只算了一个,所以需要把额外的这一个补上。这些数是 25, 50, 75, 100。
数量为:$100 \div 25 = 4$ 个。 - 再往上,能被125(即 $5^3$)整除的数会提供三个因子
5。但在1到100的范围内没有能被125整除的数。
- 首先,能被5整除的数会提供一个因子
得出结论:
将上面计算出的数量相加,就得到了因子5的总数:$20 + 4 = 24$ 个。
因为有24个因子5,我们就可以组成24对(2, 5),所以100!的末尾有 24 个零。
计算平均工资
员工们对工资好奇,但员工都有和公司签保密协议,每个人都不能告诉对方自己的工资,那你怎么知道你们的平均工资呢?
这是一个非常经典的关于“安全多方计算”(Secure Multi-Party Computation)问题的趣味版本。这个问题的核心在于,如何在不泄露个体数据的前提下,完成对群体数据的计算。
假设有 N 个员工,按顺序坐成一圈。
第一位员工(发起者):
- 想一个非常大的随机数(比如一个10位数,我们称之为“随机噪声R”)。
- 将 [自己的工资 + 随机噪声R] 的结果,告诉他/她旁边的第二位员工。
第二位员工:
- 拿到第一个员工传来的数字。
- 将 [这个数字 + 自己的工资] 的结果,传给第三位员工。
后续员工:
- 每一位员工都重复第二步的操作:将上一个人传来的数字加上自己的工资,再传给下一个人。
流程回到发起者:
- 当链条传了一圈后,最后一位员工会将最终的数字传回到第一位员工(发起者)手中。
- 此时,第一位员工拿到的数字是 [(所有人的工资总和)+ 他自己最初想的那个随机噪声R]。
计算结果:
- 第一位员工用他/她收到的最终数字,减去他/她自己最初加上的那个随机噪声R,就得到了所有人真实的总工资。
- 再用这个总工资除以员工人数 N,就得到了所有人的平均工资。
- 最后,由第一位员工向所有人公布这个计算出的平均工资。
这个方案如何保证保密性?
- 对于中间的员工:每个人拿到的都只是一个巨大的、无意义的中间数字,因为其中包含了那个神秘的“随机噪声R”,所以他们无法反推出任何人的工资信息。
- 对于发起者:他/她只知道最终的总工资,但无法知道除自己以外任何一个人的具体工资是多少。
这个方法巧妙地通过增加一个只有发起者知道的“噪声数据”,保护了所有人的隐私,同时又得出了准确的聚合结果。这是密码学和数据安全领域一个非常基础和重要的思想。
蒙提霍尔问题
参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊。当参赛者选定了一扇门,主持人会开启另一扇是山羊的门。问:要不要换一扇门?
关于这个问题的仿真实验: https://education.ti.com/-/media/ti/files/china/downloads/pdf/montyhall_problem_simulation.pdf
答案是:一定要换。
换门之后,赢得汽车的概率会从 1/3 变为 2/3。
情况1:你第一次就选对了车门(概率为 1/3)
- 在这种情况下,主持人会在另外两个“山羊门”中随便打开一扇。
- 如果你换门,你必然会选中另一扇“山羊门”,你会输。
情况2:你第一次选的是一扇山羊门(概率为 2/3)
- 在这种情况下,剩下的一扇门是车,一扇门是羊。
- 主持人必须打开另一扇有山羊的门(他不能打开车门,也不能打开你选的门)。
- 所以,剩下的那扇未被打开的门必然是车门。
- 如果你换门,你就必然会选中车门,你会赢。
结论:
- 只有在你一开始就蒙对(1/3的概率)的情况下,不换才能赢。
- 只要你一开始选错了(2/3的概率),换门就一定会赢。
所以,选择换门,你获胜的概率是 2/3,远高于不换门的 1/3。
如果还是觉得有点绕,可以想象一个更极端的版本:
假设有100扇门,1扇门后是汽车,剩下99扇门后都是山羊。
- 你选了一扇门(比如7号门)。你选对的概率是
1/100,选错的概率是99/100。 - 这时,知道车在哪里的主持人,在剩下的99扇门中,打开了98扇有山羊的门。现在场上只剩下你最初选的7号门,和另一扇未被打开的门(比如54号门)。
- 主持人问你:“你要不要从7号门换到54号门?”
现在感觉是不是非常清晰了?
- 你最初选的7号门,它有车的概率依然是渺茫的
1/100。 - 而主持人帮你排除了其他98个错误答案,相当于把那
99/100的巨大获胜概率,全都集中到了剩下的54号门上!
在这种情况下,肯定会毫不犹豫地选择换门。
三扇门的逻辑和一百扇门是完全一样的,只是因为数字小,所以迷惑性更强。主持人的行为不是随机的,他利用自己掌握的“内部信息”(车在哪扇门后)帮你排除了一个错误选项,这个行为本身极大地改变了剩下那扇门的获胜概率。
最少赛马问题
有25只马,每个马的速度都和其它马不⼀样。因为赛场只有5个赛道,所以一次比赛最多五只马,你需要找出最快的三只马,需⽤最少的比赛场次是多少?
将25匹马分成5个小组(A, B, C, D, E),每组5匹马。对每个小组进行一次比赛。
这需要5场比赛。比赛结束后,我们得到每个小组内马匹的排名。例如,A组的排名是 A1 > A2 > A3 > A4 > A5 (A1最快)。
现在我们有了5个小组的冠军(A1, B1, C1, D1, E1)。但这并不意味着他们就是总排名的前五,我们只知道他们是各自小组中最快的。
让这5匹小组冠军进行一场比赛。这是第6场比赛。假设这场比赛的结果是:A1 > B1 > C1 > D1 > E1。
根据这个结果,我们可以得出几个关键结论:
- A1 是毫无疑问的总冠军(第1名)。
- D1和E1被淘汰了。
- D组和E组的所有其他马(D2-D5, E2-E5)也都被淘汰了。
我们已经找到了第1名(A1),现在需要从剩下的马中找出第2名和第3名。
谁有可能是第2名或第3名呢?
- C组的马:C1在冠军赛中是第3名,在它前面已经有A1和B1。所以C1有可能是第3名。但C组更慢的马(C2, C3等)不可能是前三,所以它们被淘汰。
- B组的马:B1在冠军赛中是第2名,它有可能是总排名的第2或第3。B2在小组赛中仅次于B1,所以B2也有可能是总排名的第3名。B3及更慢的马则不可能,因为至少A1, B1, B2都比它快。
- A组的马:A2和A3在小组赛中仅次于总冠军A1,所以它们都有可能是总排名的第2或第3名。
综上,有资格争夺第2名和第3名的候选马匹只剩下 5 匹:
- B1 和 C1 (冠军赛的亚军和季军)
- A2 和 A3 (总冠军所在小组的亚军和季军)
- B2 (冠军赛亚军所在小组的亚军)
让这5匹马(B1, C1, A2, A3, B2)进行最后一场比赛。这是第7场比赛。这场比赛的前两名,就是我们最终要找的总排名第2和第3的马。











