根据身高重建队列 图解题解
这道题到底在问什么
- 输入
- 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]]
最优解:为什么这么做
一句话答案:LeetCode 406 根据身高重建队列的经典解是贪心排序加插入:按身高从高到矮排序(同高时 k 小的在前),再把每个人插到结果数组的下标 k 处。先放的人都不比后来者矮,矮个子插队不影响已放好的 k,一遍插完即得答案。数组中间插入 O(n),整体时间 O(n²)、空间 O(n)。
根据身高重建队列到底在问什么
每个人用一对数 [h, k] 描述:h 是身高,k 是「排在他前面、身高大于等于 h 的人数」。输入是被打乱的一组人,要求重新排出一支队伍,使队伍里每个人的 k 都与实际站位吻合。注意 k 数的不是「前面所有人」,而只数「前面身高不低于自己的人」——比自己矮的人站在前面多少个都不影响 k。这个不对称正是解题的突破口。
为什么贪心要按身高从高到矮排序
直觉的做法是枚举排列逐一验证,n 个人有 n! 种排法,完全不可行。换个角度问:谁的位置最容易先定下来?答案是最高的人——他的 k 只受同样高或更高的人影响,而比他矮的人对他完全「透明」。
于是贪心策略成形:按身高从高到矮逐个安排。轮到某个人时,已经放进队列的全是身高不小于他的人,还没放的全比他矮。后面来的矮个子无论插到哪,都不会被已放好的人「数进」自己的 k 里,所以先定下来的位置永远不会被推翻——这就是这个贪心不需要回头修改的原因。
同样身高时为什么 k 小的要排在前面
身高相同的两个人会互相计入对方的 k:站在后面的那位,前面多了一个「身高等于自己」的人。既然 k 小意味着前面这样的人更少,k 小的就必须站在 k 大的前面。排序时把同身高按 k 升序排,先插 k 小的,后插的同高者落在其后,两人的相对顺序自然正确。如果反过来按 k 降序排,先插进去的 k 大者会被后插的同高者挤到错误的位置上。
为什么每个人恰好插到下标 k 就是对的
轮到 [h, k] 入队时,队列里现有的人身高全都大于等于 h。把他插到下标 k,意味着他前面恰好有 k 个人,而这 k 个人个个身高不低于他——他的 k 当场成立,一位不多一位不少。插到末尾、或凭感觉加个偏移量都不行:队列里此刻没有比他矮的人,下标就是精确的计数。
拿题目示例验证:people = [[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]],与标准答案一致。
复杂度是多少,还能不能更快
排序 O(n log n);瓶颈在插入——往数组中间插一个元素要把后面的整体右移,最坏 O(n),n 次插入合计 O(n²),这也是整体时间复杂度。空间 O(n),用于存放结果队列。
想更快可以把「插到第 k 个空位」交给树状数组或线段树维护空位的前缀和,每次二分定位第 k 个空位并占掉,降到 O(n log n)。面试中先讲清 O(n²) 的贪心与正确性论证,被追问再谈这层优化即可。最常见的翻车点有两个:身高排成升序(矮的先放,后来的高个子会改变其 k 的计数,全盘皆错),以及同身高时把 k 排成降序。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3抓两点:先处理高的人(高的人只看更高的人,干扰最少);同样高的先放 k 小的(保证 k 小的排在前面)。排好序后,每个人「插到第 k 位」就是答案——这就是这道题最漂亮的地方。
- 4这是题目给的乱序输入(每格是一个人 "身高,k")。贪心的第一步永远是排序——按身高从高到矮,身高相同就让 k 小的排前面。下一帧看排好序长什么样。
- 5排好序了(数组重排成这一行):身高从高到矮 7,7,6,5,5,4;两个 7 里 k 小的 [7,0] 在前。排序是整个贪心的地基。下面开始逐个「插队」,每插一个就看队伍怎么长出来。
- 6蓝色光标停在 [7,0]。关键一步:此刻队列里已经放好的人,身高全都 ≥ 7(因为我们是从高到矮处理的)。所以他的 k=0 直接告诉我们落点——插到下标 0,让前面恰好 0 个比他高(或一样高)的人。
- 7数落点:当前队列长度 0,要插到下标 0。意味着原来在下标 0 及之后的人,都向右挪一格,给 [7,0] 腾位。灰色是已放置、即将被挤动的人。
- 8把 [7,0] 插入到下标 0,队列长出一节:[[7,0]]。绿色高亮的是已经放进队列的人。后面再来的都比他矮,矮个子谁也不会被他「数进」k 里,所以他这个坑位是稳的。
- 9蓝色光标停在 [7,1]。关键一步:此刻队列里已经放好的人,身高全都 ≥ 7(因为我们是从高到矮处理的)。所以他的 k=1 直接告诉我们落点——插到下标 1,让前面恰好 1 个比他高(或一样高)的人。
- 10数落点:当前队列长度 1,要插到下标 1。意味着原来在下标 1 及之后的人,都向右挪一格,给 [7,1] 腾位。灰色是已放置、即将被挤动的人。
- 11把 [7,1] 插入到下标 1,队列长出一节:[[7,0], [7,1]]。绿色高亮的是已经放进队列的人。后面再来的都比他矮,矮个子谁也不会被他「数进」k 里,所以他这个坑位是稳的。
- 12蓝色光标停在 [6,1]。关键一步:此刻队列里已经放好的人,身高全都 ≥ 6(因为我们是从高到矮处理的)。所以他的 k=1 直接告诉我们落点——插到下标 1,让前面恰好 1 个比他高(或一样高)的人。
- 13数落点:当前队列长度 2,要插到下标 1。意味着原来在下标 1 及之后的人,都向右挪一格,给 [6,1] 腾位。灰色是已放置、即将被挤动的人。
- 14把 [6,1] 插入到下标 1,队列长出一节:[[7,0], [6,1], [7,1]]。绿色高亮的是已经放进队列的人。后面再来的都比他矮,矮个子谁也不会被他「数进」k 里,所以他这个坑位是稳的。
- 15蓝色光标停在 [5,0]。关键一步:此刻队列里已经放好的人,身高全都 ≥ 5(因为我们是从高到矮处理的)。所以他的 k=0 直接告诉我们落点——插到下标 0,让前面恰好 0 个比他高(或一样高)的人。
- 16数落点:当前队列长度 3,要插到下标 0。意味着原来在下标 0 及之后的人,都向右挪一格,给 [5,0] 腾位。灰色是已放置、即将被挤动的人。
- 17把 [5,0] 插入到下标 0,队列长出一节:[[5,0], [7,0], [6,1], [7,1]]。绿色高亮的是已经放进队列的人。后面再来的都比他矮,矮个子谁也不会被他「数进」k 里,所以他这个坑位是稳的。
- 18蓝色光标停在 [5,2]。关键一步:此刻队列里已经放好的人,身高全都 ≥ 5(因为我们是从高到矮处理的)。所以他的 k=2 直接告诉我们落点——插到下标 2,让前面恰好 2 个比他高(或一样高)的人。
- 19数落点:当前队列长度 4,要插到下标 2。意味着原来在下标 2 及之后的人,都向右挪一格,给 [5,2] 腾位。灰色是已放置、即将被挤动的人。
- 20把 [5,2] 插入到下标 2,队列长出一节:[[5,0], [7,0], [5,2], [6,1], [7,1]]。绿色高亮的是已经放进队列的人。后面再来的都比他矮,矮个子谁也不会被他「数进」k 里,所以他这个坑位是稳的。
- 21蓝色光标停在 [4,4]。关键一步:此刻队列里已经放好的人,身高全都 ≥ 4(因为我们是从高到矮处理的)。所以他的 k=4 直接告诉我们落点——插到下标 4,让前面恰好 4 个比他高(或一样高)的人。
- 22数落点:当前队列长度 5,要插到下标 4。意味着原来在下标 4 及之后的人,都向右挪一格,给 [4,4] 腾位。灰色是已放置、即将被挤动的人。
- 23最后一个人 [4,4] 插到下标 4,队伍补全为 [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]。整支队伍每个人的 k 都成立——重建完成。
- 24全部插完,答案 [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]。可以挑一个验证:比如 [6,1],它前面是 [5,0]、[7,0],其中身高 ≥6 的只有 [7,0] 一个 → 正好 k=1,成立。先排高、再按 k 插入,一次成型。
⚠️ 容易写错的地方
✗ 错:身高升序排(从矮到高)
✓ 对:必须身高降序(从高到矮)
先放矮的,后面来的高个子会改变矮个子前面「≥」的人数,位置全乱
✗ 错:同身高时 k 也降序
✓ 对:同身高必须 k 升序
k 小的要排在前面,先插 k 小的才能保证两人相对顺序正确
✗ 错:插到末尾或下标 k+某偏移
✓ 对:精确插到下标 = k
插入时队列里全是 ≥ 他的人,下标 k 处前面恰好 k 个,差一位都错
完整代码(Python / C++ / Java)
Python
def reconstructQueue(people):
# 身高降序;同高时 k 升序
people.sort(key=lambda p: (-p[0], p[1]))
queue = []
for p in people:
queue.insert(p[1], p) # 插到下标 k
return queueC++
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), [](auto& a, auto& b){
return a[0] != b[0] ? a[0] > b[0] : a[1] < b[1]; // 身高降序, 同高 k 升序
});
vector<vector<int>> q;
for (auto& p : people)
q.insert(q.begin() + p[1], p); // 插到下标 k
return q;
}Java
public int[][] reconstructQueue(int[][] people) {
// 身高降序; 同高 k 升序
Arrays.sort(people, (a, b) ->
a[0] != b[0] ? b[0] - a[0] : a[1] - b[1]);
List<int[]> q = new ArrayList<>();
for (int[] p : people)
q.add(p[1], p); // 插到下标 k
return q.toArray(new int[q.size()][]);
}复杂度
时间
O(n²)
排序 O(n log n),但逐个 insert 到数组中间最坏 O(n),共 n 次 → O(n²) 主导
空间
O(n)
结果队列存 n 个人(排序若原地则不算额外)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 根据身高重建队列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么后插入的人不会破坏前面已确定的人的 k?+
因为我们从高到矮处理,后插入的人身高严格更矮(或同高但 k 更大、已在其后)。前面那个人的 k 只统计「身高 ≥ 自己」的人,矮个子根本不在统计范围内,所以无论矮个子插在他前面还是后面,都不会改变他前面「≥ 他身高」的计数。
插入是 O(n),能优化吗?+
能。把「插到第 k 个空位」用线段树/树状数组维护剩余空位的前缀和,每次二分找第 k 个空位并标记,整体降到 O(n log n)。面试讲清 O(n²) 思路即可,追问优化再上线段树。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 根据身高重建队列 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。