📖 第69课:根据身高重建队列

3 阅读14分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第69课:根据身高重建队列

模块:贪心算法 | 难度:Medium ⭐ LeetCode 链接:leetcode.cn/problems/qu… 前置知识:第7课(移动零-快慢指针)、第9课(三数之和-排序) 预计学习时间:30分钟


🎯 题目描述

给定一群人的身高和排在他们前面且身高大于等于他们的人数,要求重建这个队列。

每个人用一个整数对 [h, k] 表示,其中 h 是这个人的身高,k 是排在这个人前面且身高大于等于 h 的人数。

示例:

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5,前面没有身高大于等于 5 的人
编号为 1 的人身高为 7,前面没有身高大于等于 7 的人
编号为 2 的人身高为 5,前面有 2 个身高大于等于 5 的人
编号为 3 的人身高为 6,前面有 1 个身高大于等于 6 的人
编号为 4 的人身高为 4,前面有 4 个身高大于等于 4 的人
编号为 5 的人身高为 7,前面有 1 个身高大于等于 7 的人

约束条件:

  • 1 <= people.length <= 2000
  • 0 <= hi <= 10^6
  • 0 <= ki < people.length
  • 题目数据确保队列可以被重建

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
最小输入[[7,0]][[7,0]]单人队列
身高相同[[5,0],[5,1],[5,2]][[5,0],[5,1],[5,2]]k值递增顺序
全部k=0[[7,0],[6,0],[5,0]][[7,0],[6,0],[5,0]]按身高降序
大规模n=2000性能边界O(n²)

💡 思路引导

生活化比喻

想象你是一个剧场经理,观众陆续进场,每个观众告诉你:"我身高 h,在我前面应该有 k 个人比我高或一样高。"你需要安排他们的座位。

🐌 笨办法:让所有人随便站,然后不断调整位置,直到每个人前面的高个子人数都符合要求。这样需要反复检查、移动,非常低效。

🚀 聪明办法:先让最高的人入座,因为后面来的矮个子不会影响他们的"前面有几个高个子"这个条件!然后按身高从高到低依次安排,矮个子可以"见缝插针"地插入到指定位置,不会破坏已经就座的高个子的约束。

关键洞察

高个子先入座,矮个子后插入,互不干扰! 因为矮个子插入不会影响已就座高个子的 k 值统计。


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:people = [[h, k], ...],每个元素是 [身高, 前面比他高的人数]
  • 输出:重新排列后的队列,满足每个人的 k 值约束
  • 限制:需要原地重建,考虑性能

Step 2:先想笨办法(暴力法)

最直接的想法:尝试所有可能的排列,检查每个排列是否满足所有人的 k 值约束。

  • 时间复杂度:O(n! × n) — n! 种排列,每种检查需要 O(n)
  • 瓶颈在哪:排列数爆炸,完全不可行

Step 3:瓶颈分析 → 优化方向

核心观察:

  1. 如果我们按某种顺序插入人员,能否避免后续插入破坏已插入的约束?
  2. 关键发现:矮个子插入不影响高个子的 k 值!因为 k 统计的是"身高 ≥ 自己"的人数。

优化思路:

  • 按身高从高到低排序
  • 身高相同时,按 k 值从小到大排序(为什么?因为 k 小的应该排在前面)
  • 然后依次插入到结果数组的第 k 个位置

Step 4:选择武器

  • 选用:排序 + 贪心插入
  • 理由:排序保证高个子优先处理,贪心插入利用 k 值直接定位,时间复杂度降为 O(n²)

🔑 模式识别提示:当题目出现"相对位置约束 + 需要重建顺序",优先考虑"排序 + 贪心"


🔑 解法一:暴力模拟(仅用于理解,不推荐)

思路

尝试逐个验证每个位置是否符合约束,通过交换调整。这种方法仅用于说明问题复杂性,实际不可行。

优缺点

  • ✅ 直观易懂
  • ❌ 时间复杂度过高,无法通过 LeetCode

🏆 解法二:排序 + 贪心插入(最优解)

优化思路

核心策略:

  1. 按身高降序排序:高个子优先处理
  2. 身高相同时按 k 升序:k 小的排前面(因为他们需要前面的高个子更少)
  3. 贪心插入:每个人插入到结果数组的第 k 个位置

为什么这样有效?

  • 高个子先插入,后续矮个子插入不会改变他们的 k 值(因为 k 只统计≥自己身高的人)
  • 插入位置就是 k 值,因为前面已经有 k 个身高≥当前人的人

💡 关键想法:从高到低处理,保证后来者不影响前者的约束!

图解过程

输入:[[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]

Step 1:排序
按 h 降序,h 相同时按 k 升序:
[[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]]
  ↑       ↑      ↑      ↑      ↑      ↑
 h=7,k=0 h=7,k=1 h=6    h=5,k=0 h=5,k=2 h=4

Step 2:依次插入到位置 k

初始结果:result = []

插入 [7,0]:插入到位置 0
result = [[7,0]]
           ↑ 位置0

插入 [7,1]:插入到位置 1
result = [[7,0], [7,1]]
                  ↑ 位置1

插入 [6,1]:插入到位置 1(会把 [7,1] 挤到后面)
result = [[7,0], [6,1], [7,1]]
                  ↑ 插入位置1

插入 [5,0]:插入到位置 0(所有元素后移)
result = [[5,0], [7,0], [6,1], [7,1]]
           ↑ 插入位置0

插入 [5,2]:插入到位置 2
result = [[5,0], [7,0], [5,2], [6,1], [7,1]]
                         ↑ 插入位置2

插入 [4,4]:插入到位置 4
result = [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
                                       ↑ 插入位置4

最终结果:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

验证:

[5,0]:前面 0 个 ≥5 的 ✅
[7,0]:前面 0 个 ≥7 的([5]不算) ✅
[5,2]:前面 2 个 ≥5 的([5],[7]) ✅
[6,1]:前面 1 个 ≥6 的([7]) ✅
[4,4]:前面 4 个 ≥4 的([5],[7],[5],[6]) ✅
[7,1]:前面 1 个 ≥7 的([7]) ✅

Python代码

from typing import List


def reconstructQueue(people: List[List[int]]) -> List[List[int]]:
    """
    解法二:排序 + 贪心插入
    思路:高个子优先,按 k 值插入对应位置
    """
    # Step 1:排序
    # 按身高降序,身高相同时按 k 升序
    people.sort(key=lambda x: (-x[0], x[1]))

    # Step 2:贪心插入
    result = []
    for person in people:
        h, k = person
        # 插入到位置 k(Python 的 insert 会自动后移)
        result.insert(k, person)

    return result


# ✅ 测试
print(reconstructQueue([[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]))
# 期望输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

print(reconstructQueue([[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]))
# 期望输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

print(reconstructQueue([[7,0]]))
# 期望输出:[[7,0]]

复杂度分析

  • 时间复杂度:O(n²) — 排序 O(n log n),插入操作最坏每次 O(n),共 n 次插入
    • 具体地说:如果输入规模 n=100,排序约 664 次比较,插入最坏情况下需要移动约 100×50=5000 次元素
  • 空间复杂度:O(n) — 结果数组占用 O(n)

为什么是最优?

  • 排序是必需的(O(n log n)),插入操作虽然是 O(n²),但贪心策略保证一次遍历即可完成
  • 无法优化到 O(n log n),因为插入操作本质上需要移动元素

优缺点

  • ✅ 贪心策略简洁,易于理解和实现
  • ✅ 一次遍历完成重建,代码清晰
  • ❌ 插入操作导致 O(n²),但已是此问题的理论最优

🐍 Pythonic 写法

利用 Python 的列表推导和 insert 方法的简洁性:

def reconstructQueue(people: List[List[int]]) -> List[List[int]]:
    # 排序 + 插入一气呵成
    result = []
    for p in sorted(people, key=lambda x: (-x[0], x[1])):
        result.insert(p[1], p)
    return result

解释:

  • sorted() 直接返回排序后的新列表
  • lambda x: (-x[0], x[1]) 实现双关键字排序(身高降序,k升序)
  • 循环中直接插入,代码仅 4 行

⚠️ 面试建议:先写清晰版本展示思路,再提 Pythonic 写法展示语言功底。 面试官更看重你的思考过程,而非代码行数。


📊 解法对比

维度🏆 排序 + 贪心插入(最优)
时间复杂度O(n²) ← 理论最优
空间复杂度O(n)
代码难度简单
面试推荐⭐⭐⭐ ← 首选
适用场景通用,面试首选

为什么是最优解:

  • 贪心策略保证正确性:高个子先入座,矮个子不影响已就座者
  • 时间复杂度 O(n²) 已是此问题的理论下限(插入操作无法避免)
  • 代码简洁,易于理解和实现

面试建议:

  1. 先口述核心思路:"按身高降序排序,然后按 k 值贪心插入"
  2. 重点强调为什么排序策略是这样(h 降序保证高个子优先,k 升序保证同高度中 k 小的在前)
  3. 解释为什么插入位置就是 k(前面已有 k 个≥自己身高的人)
  4. 手动演示一个小例子,展示插入过程

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你解决一下这道题。

:(审题30秒)好的,这道题要求根据每个人的身高和前面高个子的人数重建队列。让我先想一下...

我的第一个想法是暴力枚举所有排列,但时间复杂度是 O(n!),完全不可行。

关键观察是:如果按身高从高到低处理,矮个子的插入不会影响高个子的约束!因为 k 统计的是"身高≥自己"的人数,矮个子对高个子来说是"不可见"的。

所以策略是:

  1. 按身高降序排序(身高相同时按 k 升序)
  2. 依次插入到结果数组的第 k 个位置

时间复杂度 O(n²),空间 O(n)。

面试官:很好,请写一下代码。

:(边写边说)首先排序,用 lambda 函数实现双关键字排序:(-x[0], x[1])。然后遍历排序后的数组,每个人插入到位置 k,Python 的 insert 方法会自动后移元素。

面试官:为什么身高相同时要按 k 升序?

:因为 k 小的人需要前面的高个子更少,应该排在前面。如果 k 大的排前面,后续插入会打乱顺序。举例:[[7,0],[7,1]],k=0 的应该在位置 0,k=1 的在位置 1,符合直觉。

面试官:测试一下?

:用示例 [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 走一遍... (手动模拟插入过程) 排序后:[[7,0],[7,1],[6,1],[5,0],[5,2],[4,4]] 依次插入:位置 0 → 位置 1 → 位置 1 → 位置 0 → 位置 2 → 位置 4 最终:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

再测一个边界情况:[[7,0]] → 直接返回 [[7,0]]

高频追问

追问应答策略
"还有更优解吗?""时间 O(n²) 已是理论最优,因为插入操作无法避免元素移动。可以用链表优化插入到 O(1),但整体仍是 O(n²)"
"为什么不能按 k 值排序?""按 k 排序无法保证正确性,因为 k 值的有效性依赖于其他人的身高。必须先确定高个子的位置"
"如果有重复的 [h,k] 怎么办?""题目保证数据可重建,意味着不会有冲突的重复。实际处理时,重复元素会按排序规则自然处理"
"能否用线段树优化插入?""理论上可以,线段树可以优化插入到 O(log n),总时间降为 O(n log n),但实现复杂,面试中通常不要求"

🎓 知识点总结

Python技巧卡片 🐍

# 1. 双关键字排序 — 多条件排序
people.sort(key=lambda x: (-x[0], x[1]))
# 解释:-x[0] 实现降序,x[1] 实现升序

# 2. list.insert(index, item) — 插入元素到指定位置
result.insert(k, person)
# 时间复杂度:O(n),因为需要移动后续元素

# 3. sorted() vs list.sort() — 返回新列表 vs 原地排序
new_list = sorted(people, key=...)  # 返回新列表
people.sort(key=...)  # 原地排序,返回 None

💡 底层原理(选读)

为什么 Python 的 insert 是 O(n)?

Python 的列表底层是动态数组(类似 C++ 的 vector)。插入元素到中间位置时,需要将插入点之后的所有元素向后移动一位,这个操作是 O(n)。

有没有更快的插入方式?

  • 链表:插入是 O(1),但需要 O(n) 时间找到插入位置
  • 线段树/树状数组:可以优化到 O(log n),但实现复杂

对于本题,Python 列表的 insert 已经足够高效且代码简洁。

算法模式卡片 📐

  • 模式名称:排序 + 贪心插入
  • 适用条件:相对位置约束问题,需要按某种顺序重建序列
  • 识别关键词:"重建队列"、"相对位置"、"约束条件"
  • 模板代码:
def reconstruct(items):
    # 1. 设计排序规则,确保先处理的不被后处理的破坏
    items.sort(key=lambda x: custom_key(x))

    # 2. 贪心插入
    result = []
    for item in items:
        position = get_insert_position(item)
        result.insert(position, item)

    return result

易错点 ⚠️

  1. 排序规则错误:身高相同时忘记按 k 升序

    • 为什么错:k 大的排前面会导致后续插入位置混乱
    • 正确做法:key=lambda x: (-x[0], x[1]) — k 升序
  2. 插入位置理解错误:以为要找"第 k 个空位"

    • 为什么错:插入位置直接就是 k,因为前面已有 k 个≥自己身高的人
    • 正确做法:result.insert(k, person) — 直接插入位置 k
  3. 忘记验证边界用例:单人队列、全部 k=0、身高全部相同

    • 正确做法:手动测试这些边界,确保算法通用性

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:任务调度系统中,按优先级和依赖关系重建任务队列
  • 场景2:游戏排行榜中,按多个维度(等级、积分、时间)重建排名
  • 场景3:数据库查询优化中,按选择性(selectivity)和代价重建执行计划

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 435. 无重叠区间Medium贪心 + 排序按结束时间排序,贪心选择
LeetCode 452. 用最少数量的箭引爆气球Medium贪心 + 排序类似区间调度
LeetCode 56. 合并区间Medium排序 + 贪心合并按起始位置排序

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:如果题目改为"每个人告诉你他后面有多少个比他矮的人",如何重建队列?

💡 提示(实在想不出来再点开)

反过来思考:按身高从矮到高排序,然后从后往前插入!

✅ 参考答案
def reconstructQueue(people: List[List[int]]) -> List[List[int]]:
    """
    变体:后面比自己矮的人数
    策略:按身高升序,从后往前插入
    """
    # 按身高升序,k 降序
    people.sort(key=lambda x: (x[0], -x[1]))

    result = []
    for person in people[::-1]:  # 从后往前
        h, k = person
        # 插入到从后数第 k 个位置
        result.insert(len(result) - k, person)

    return result

核心思路:矮个子先确定位置,高个子从后面插入不影响矮个子的"后面比我矮"的统计。


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。