想系统提升编程能力、查看更完整的学习路线,欢迎访问 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 <= 20000 <= hi <= 10^60 <= 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:瓶颈分析 → 优化方向
核心观察:
- 如果我们按某种顺序插入人员,能否避免后续插入破坏已插入的约束?
- 关键发现:矮个子插入不影响高个子的 k 值!因为 k 统计的是"身高 ≥ 自己"的人数。
优化思路:
- 按身高从高到低排序
- 身高相同时,按 k 值从小到大排序(为什么?因为 k 小的应该排在前面)
- 然后依次插入到结果数组的第 k 个位置
Step 4:选择武器
- 选用:排序 + 贪心插入
- 理由:排序保证高个子优先处理,贪心插入利用 k 值直接定位,时间复杂度降为 O(n²)
🔑 模式识别提示:当题目出现"相对位置约束 + 需要重建顺序",优先考虑"排序 + 贪心"
🔑 解法一:暴力模拟(仅用于理解,不推荐)
思路
尝试逐个验证每个位置是否符合约束,通过交换调整。这种方法仅用于说明问题复杂性,实际不可行。
优缺点
- ✅ 直观易懂
- ❌ 时间复杂度过高,无法通过 LeetCode
🏆 解法二:排序 + 贪心插入(最优解)
优化思路
核心策略:
- 按身高降序排序:高个子优先处理
- 身高相同时按 k 升序:k 小的排前面(因为他们需要前面的高个子更少)
- 贪心插入:每个人插入到结果数组的第 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²) 已是此问题的理论下限(插入操作无法避免)
- 代码简洁,易于理解和实现
面试建议:
- 先口述核心思路:"按身高降序排序,然后按 k 值贪心插入"
- 重点强调为什么排序策略是这样(h 降序保证高个子优先,k 升序保证同高度中 k 小的在前)
- 解释为什么插入位置就是 k(前面已有 k 个≥自己身高的人)
- 手动演示一个小例子,展示插入过程
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道题。
你:(审题30秒)好的,这道题要求根据每个人的身高和前面高个子的人数重建队列。让我先想一下...
我的第一个想法是暴力枚举所有排列,但时间复杂度是 O(n!),完全不可行。
关键观察是:如果按身高从高到低处理,矮个子的插入不会影响高个子的约束!因为 k 统计的是"身高≥自己"的人数,矮个子对高个子来说是"不可见"的。
所以策略是:
- 按身高降序排序(身高相同时按 k 升序)
- 依次插入到结果数组的第 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
易错点 ⚠️
-
排序规则错误:身高相同时忘记按 k 升序
- 为什么错:k 大的排前面会导致后续插入位置混乱
- 正确做法:
key=lambda x: (-x[0], x[1])— k 升序
-
插入位置理解错误:以为要找"第 k 个空位"
- 为什么错:插入位置直接就是 k,因为前面已有 k 个≥自己身高的人
- 正确做法:
result.insert(k, person)— 直接插入位置 k
-
忘记验证边界用例:单人队列、全部 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 学习资料都在这里,后续复习和拓展会更省时间。