开始前,会这些就够了
- 会遍历列表、比较两个值是否相等,并统计出现次数。
- 先读 复杂度入门,区分变量数量、扫描次数和输入长度。
七条记录是 A、A、A、B、B、C、C。一个只保留候选和计数的小算法最后留下了 C,但 C 只出现两次;出现最多的 A 也只有三次,根本没有谁超过半数。
下面把这个看似矛盾的结果拆开:Boyer–Moore 的第一遍负责保住可能的严格多数,第二遍负责确认。我们还会观察为什么抵消后的余额不是这个名字在原数据中的次数。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。可重复读取的字符串list/tuple;9841条有限序列与Counter核对并反向复核9841条;严格超过半数、空/单项/恰好半数及3类拒绝输入;无一次性流或性能保证。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
第一遍留下一个名字,第二遍才给它结论
正在准备实验控件…
最多 20 个大写英文字母,每个字母代表一条记录;严格多数要求次数×2大于总数。浏览器保留输入以便重读,展示轨迹也占空间;常数辅助空间的算法分析针对不保存轨迹的求解部分。
先约定目标:严格超过半数,和出现最多不同
对长度 n 的列表,本文寻找出现次数满足 2 × 次数 > n 的标签。七项中至少要出现四次,四项中至少要出现三次。恰好一半不算,单纯排第一也不算。
最多只能有一个标签严格超过半数,否则两者出现次数相加会超过总长度。没有这样的标签时,函数返回 None;输入只允许字符串标签,因此这个 None 不会和合法数据混淆。
默认数据 AAABBCC 的最多项是 A,但它只占 3/7。本例没有要求找众数,因此算法返回“没有严格多数”才正确。
把不同名字配对抵消,为什么不会丢掉真正的多数
想象每次拿走两个不同标签。若 A 本来严格超过半数,每次最多拿走一个 A,也会拿走至少一个非 A;原来多出来的 A 不会被全部抵消。这里只是在解释计数的含义,程序并不反复删除列表元素。
程序维护 candidate 和 balance。余额为零时,当前元素成为新候选并把余额设为 1;遇到相同标签加 1,遇到不同标签减 1。余额归零就暂时没有活跃候选,下一项再开始。
这个保证是单向的:真正存在的严格多数会留下来,但留下来的标签不一定是严格多数。没有多数的输入也可能抵消出一个剩余标签。
看实际轨迹:剩下一个 C,不代表 C 只出现一次
读完前三个 A,状态是 A、余额 3。接着两个 B 把余额降到 1;第一个 C 再把它降到 0。最后一个 C 成为新候选,余额为 1。
第二遍重新扫描原数据,发现 C 的真实出现次数是 2。因为 2 × 2 不大于 7,它被否决,最终结果是 None。第一遍余额 1、真实次数 2、列表总长 7 是三个不同量。
另一个输入 AAAABBC 会留下 A、余额 1,但 A 实际出现四次,能够通过 4 × 2 > 7 的检查。两种输入的余额都是 1,却得到不同的最终结论。
第二遍读的是同一份数据,不能拿已经耗尽的迭代器再数
如果业务没有提前保证严格多数存在,就要核对候选在原输入中的次数。最后余额为零时,全部记录已被不同标签成对抵消,可以直接判定没有严格多数;有候选时才进行二遍计数。
因此 exact_half 和 empty 输出里的 frequency=0 表示没有活跃候选,没有执行候选频次统计;它不是在声称 A 或 B 在原列表中出现零次。
本例只接收可重复遍历的 list 或 tuple,并明确拒绝一次性迭代器。若从文件读取,可以按业务条件重新打开同一份稳定数据;若输入只能消费一次,就必须另外设计存储或问题约定,不能把耗尽后的零次统计当成核验。
运行默认反例,再对照完整计数结果
把下面完整代码保存为 demo.py,执行 python demo.py。2026-10-23 在 Windows、CPython 3.14.0 实际运行;只使用标准库。打印的 candidate、balance、frequency 和 majority 分别对应挑选、抵消、核验和最终结论。
程序穷举长度 0–8、标签取 A/B/C 的 9,841 条序列,以 Counter 的完整频次统计作独立核对;每条再反向读取一次,确认最终严格多数结论不变。反向读取可能改变假候选,但不该改变验证后的答案。
另外检查恰好半数、空列表、单元素,拒绝一次性迭代器、None 标签和非字符串标签。显式 AssertionError 检查不依赖 Python assert 开关。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Strict majority for replayable string sequences; candidate then verification."""
from collections import Counter
from itertools import product
def require(condition, message):
if not condition:
raise AssertionError(message)
def majority(values, trace=False):
if not isinstance(values, (list, tuple)):
raise TypeError("a replayable list or tuple is required")
if any(type(value) is not str for value in values):
raise TypeError("this example accepts string labels only")
candidate, balance, events = None, 0, []
for index, value in enumerate(values):
if balance == 0:
candidate, balance = value, 1
elif value == candidate:
balance += 1
else:
balance -= 1
if balance == 0:
candidate = None
if trace:
events.append((index, value, candidate, balance))
frequency = sum(value == candidate for value in values) if balance else 0
verified = candidate if frequency * 2 > len(values) else None
return candidate, balance, frequency, verified, events
def oracle(values):
winners = [key for key, count in Counter(values).items() if count * 2 > len(values)]
require(len(winners) <= 1, "strict majority is unique")
return winners[0] if winners else None
def main():
values = list("AAABBCC")
candidate, balance, frequency, verified, events = majority(values, trace=True)
print(f"values={values}")
for index, value, current, remaining in events:
print(f"i={index}; value={value}; candidate={current}; balance={remaining}")
print(f"candidate={candidate}; balance={balance}; frequency={frequency}; majority={verified}")
require((candidate, balance, frequency, verified) == ("C", 1, 2, None), "false candidate")
for label, data in [("has_majority", list("AAAABBC")), ("exact_half", list("AABB")),
("empty", []), ("single", ["Z"])]:
c, b, f, result, _ = majority(data)
require(result == oracle(data), label)
print(f"{label}: candidate={c}; balance={b}; frequency={f}; majority={result}")
rejected = 0
for unsupported in [iter(["A", "A"]), [None], [1]]:
try:
majority(unsupported)
except TypeError:
rejected += 1
else:
raise AssertionError("unsupported input accepted")
cases = 0
for length in range(9):
for data in product("ABC", repeat=length):
expected = oracle(data)
require(majority(data)[3] == expected, data)
require(majority(tuple(reversed(data)))[3] == expected, "reversal changed answer")
cases += 1
print(f"oracle_sequences={cases}; reversed_sequences={cases}; rejected_inputs={rejected}")
print("PASS")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。可重复读取的字符串list/tuple;9841条有限序列与Counter核对并反向复核9841条;严格超过半数、空/单项/恰好半数及3类拒绝输入;无一次性流或性能保证。
运行后对照结果
values=['A', 'A', 'A', 'B', 'B', 'C', 'C']
i=0; value=A; candidate=A; balance=1
i=1; value=A; candidate=A; balance=2
i=2; value=A; candidate=A; balance=3
i=3; value=B; candidate=A; balance=2
i=4; value=B; candidate=A; balance=1
i=5; value=C; candidate=None; balance=0
i=6; value=C; candidate=C; balance=1
candidate=C; balance=1; frequency=2; majority=None
has_majority: candidate=A; balance=1; frequency=4; majority=A
exact_half: candidate=None; balance=0; frequency=0; majority=None
empty: candidate=None; balance=0; frequency=0; majority=None
single: candidate=Z; balance=1; frequency=1; majority=Z
oracle_sequences=9841; reversed_sequences=9841; rejected_inputs=3
PASS
两遍仍是线性工作,常数状态也要说清楚范围
两遍扫描的工作量相加仍为 O(n)。关闭 trace 时,求解部分只保存候选、余额和频次等常数个状态变量,不为每一种标签建立计数表;已有输入列表的存储不算新增的辅助空间。
这里的常数空间按常数个机器字的常见分析口径表述。若逐位计量,能计到 n 的整数本身需要约 log n 位。打开 trace 会额外保存 n 条状态,Counter 也只用于测试,不能把整个教学程序都说成只用常数空间。
字符串比较还取决于标签长度。本文的有限字母实验不测长字符串比较成本、吞吐或外部读取速度,也没有把算法计数直接换算成毫秒承诺。
把候选当作待验证结果,别顺手扩大它能回答的问题
这个算法适合严格多数的存在性与身份问题,不会同时列出所有标签频率,也不能仅凭最后一个候选求出出现最多的标签。默认反例中,留下的 C 就连众数都不是。
若目标换成“超过三分之一”或“前几个最常见项”,候选数量、抵消规则和核验方式都需要重新设计。先把阈值写清楚,再判断现有保证是否适用。
代码评审时可以直接问三件事:是否保证多数存在、是否验证严格阈值、第二遍读取的是否仍是相同数据。它们决定候选什么时候能成为最终答案。
记住这些要点
- 第一遍留下的是候选,不是存在严格多数的证明。
- balance 是抵消余额,不是原数据中的出现次数。
- 没有多数保证时,需要按原输入再次核验候选。
- 恰好半数和出现最多都不等于严格超过半数。
别把例子用过头
- 直接返回候选,漏掉没有严格多数的输入。
- 用 balance 代替真实频次检查阈值。
- 把 > 写成 >=,让恰好一半通过。
- 第二遍遍历已经耗尽的迭代器,误把零次当成真实计数。
自己试一小步
只保留第一遍的候选和余额,能否区分所有“有多数”和“无多数”的输入?请用实验检查这个主张。
- 自选两组相同长度的列表,记录第一遍结束状态和完整频次表。
- 搜索是否能找到相同第一遍状态但最终多数结论不同的输入。
- 对找到的输入调换顺序,区分候选变化和验证后答案变化。
- 说明你的数据源怎样支持第二遍读取,并写明失败时的处理方式。
做到这些,就算完成
- 用严格大于半数的条件判断,不把众数替代为答案。
- 输出原数据频次和第一遍余额两个独立字段。
- 结论包含空输入和一次性数据源的处理约定。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- A Linear-Time Majority Vote Algorithm:作者逐步示例 (新标签页)↗
候选与计数的更新规则、仅在存在多数时的候选保证,以及必须再次扫描才能确认的反例。
- CS 3530 Session 15:Solving the Elections Puzzle (新标签页)↗
删除不同元素对的保持性质、正反扫描可留下不同假候选、二次计数确认和常数个状态变量。