某医院急诊科改了一套新分诊系统。老系统是先到先看:谁先挂号谁先见医生。结果一个只是擦破皮的患者排在前面,而后面一位胸口剧痛的大爷一直等着——这显然不合理。
新系统的规则只有一句话:每次都从候诊区里挑出「紧急度最高」的那位患者。
护士长给出的排班表是这样运作的:
挂号(紧急度) → 进入候诊区 → 每次叫号取紧急度最高者 → 就诊完成即离开
问题在于:候诊区随时可能有两三百人,每来一位患者就要重新找一遍「谁最紧急」吗?那是 O(n) 的查找——如果每分钟来一位、每次都全表扫描,一天下来就是几百万次比较。
信息科的小张想了个办法:他不把患者按顺序摆成一排,而是摆成一棵「父亲永远比儿子更紧急」的特殊形状的树。于是「最紧急的人」永远站在树根上,取出来只要 O(1);来了新患者或者送走了根上的那位,也只要沿着一条从根到叶的路径调整,最多 log n 步。
运维主管看了一眼说:「你这不是发明了新东西——这就是二叉堆。而且它根本不需要真的建立一棵树,一个一维数组就够了:下标为 i 的节点,父亲是 i/2,左儿子是 2i,右儿子是 2i+1。」
堆的魔法在于:树形结构 + 数组存储 = 极致的缓存友好 + 对数级操作。
💡 为什么不能直接用「有序数组」或「排序 + 取末尾」
方案一 · 每次插入后重新排序:插入 O(log n)(二分找位置)但移动元素 O(n),总 O(n)。n 次操作 O(n2)——n = 105 时是 1010,超时。
方案二 · 有序数组 + 二分插入:插入时找位置 O(log n),但为了保持有序必须搬移后面的所有元素,还是 O(n)。
方案三 · 每次「取最大值」时全表扫一遍:O(n) 一次,n 次操作 O(n2)。
方案四 · 二叉堆:找最大值 O(1)(就在根上),插入 O(log n),删除堆顶 O(log n)。n 次操作总 O(n log n)——这就是我们想要的答案。
核心权衡:堆放弃了「完全有序」,只维持「父优于子」这个弱得多的性质。正因为要求变弱了,维护成本才能从 O(n) 降到 O(log n)。这是一条非常重要的工程哲学:只维持你需要的那部分有序性。