逐层排序二叉树所需的最少操作数目 图解题解
这道题到底在问什么
- 输入
- root=[45,80,30,50,70,20,90]
- 输出
- 3
- 输入
- root=[1,2,3,4,5,6]
- 输出
- 0
最优解:为什么这么做
一句话答案:LeetCode 2471 逐层排序二叉树的最少操作:按层广度优先取出每层的值,把值换成升序名次后用环排序,一层的最少交换等于节点数减环数,各层加总,时间 O(n log n)。
每层各自排成严格递增,最少换几次
给你一棵二叉树,值互不相同。一次操作能挑同一层里的任意两个节点对调它们的值;层数按到根走过的边数算。目标是让每层从左到右严格递增,问最少几次。以题面的 root=[45,80,30,50,70,20,90] 为例:根 45 在第 0 层,80、30 在第 1 层,50、70、20、90 在第 2 层,答案是 3。跨层不能换,这一条决定了整道题的拆法。
为什么不能把全树的值一起排
题目只准交换同一层的两个节点,跨层挪不动。把七个值当一整串去数交换,会算出把 20 搬去别层这种非法步骤,得数偏小。还有个易混点:最少交换不是逆序对,逆序对数的是相邻两两交换,而这里任意两个位置都能直接对调,一次就修好隔很远的两个值,最少次数常比逆序对少不少。所以每层都得单独当成一道排成升序的最少交换小题。
把一层的值换成名次,交换看环
先用广度优先,拿队列一层层取出节点、凑齐本层的值。对一层,先算出它升序后每个值排第几名,从 0 数起,再把每个位置上的值替成名次。这一换,本层就成了 0 到 k−1 的排列,k 是本层节点数。接着按『谁该去谁的位置』连环:从某位置跳到它的值该去处,再顺着跳,绕回起点成环。长为 L 的环归位刚好要 L−1 次交换——每交换一次至少送一个值回家,最后一个自动落位,故省一次;已在位的值是长度 1 的自环,不用动。整层最少交换就是节点数减环数。
分层取值、逐层结算、层层相加
两层循环。外层用队列做广度优先:每轮先记下队列当前长度,正好是这层的节点数,按它把本层节点依次弹出、收集成值数组,同时把孩子压回队列。内层处理数组:建一张值到名次的映射替换每个元素,再从左到右扫,只要某位置的值名次不等于位置号,就把它跟名次对应位置的值对调,每对调一次答案加一,直到住进正确的值。一层处理完把交换数累加进总答案,队列空了就是结果。
三层挨个算,0 次加 1 次加 2 次
第 0 层只有 45,一个数天然有序,0 次。第 1 层是 80、30,升序里 30 排第 0、80 排第 1,位置上的名次是 1、0;位置 0 住着名次 1、不该在这,和名次 1 的位置对调后换来名次 0 归位,位置 1 正好也是名次 1,一共 1 次。第 2 层是 50、70、20、90,名次依次 1、2、0、3;从位置 0 出发,它住名次 1、和位置 1 对调后住进名次 2,再和位置 2 对调后住进名次 0 归位,位置 1、2、3 都已就位,用了 2 次。三层相加得 3,正是题面给的答案。
单节点层免算,最宽层定复杂度
时间 O(n log n):每层开销主要在给本层的值排序求名次,一层 k 个节点是 O(k log k),各层的 k 相加等于总节点数 n,排序项加起来不超过 O(n log n);建映射和环排序都是每层线性的。空间 O(n):队列在最宽那层要装下整层节点,再加值数组和名次映射,峰值与节点总数同阶。边界也直接:只有一个节点的层,或本就升序的层,像 [1,2,3,4,5,6] 那样每层都递增,交换都是 0;某层只有两个节点且大的在前,一次对调就修好。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:一层的最少交换次数,等于本层节点数减去环的个数。落到代码里就是把每层的值换算成名次,再用环排序数交换。下面从第 0 层开始,一层一层走。
- 4答案累计 = 0先把整棵树看清楚。层数是按到根走了几条边算的:根 45 在第 0 层;它的两个孩子 80、30 在第 1 层;最底下 50、70、20、90 在第 2 层。同一层内部才能互相交换,跨层不行。所以这题拆成三道互不相干的小题:每一层各自排好,把次数加起来。
- 5本层: 45第 0 层紫色高亮,只有一个根节点 45。一个数不用比,天生就是升序,一次都不用换。它这层贡献 0。别小看这一步,它提醒我们:节点数是 1 的层永远 0 次。
- 6本层用 0 次 · 累计 0这一层就一个 45,已经有序,变绿表示排好了,本层交换 0 次,累计答案还是 0。收工,进下一层。
- 7本层: 80, 30把第 1 层紫色高亮起来,从左到右是 80、30。目标是把它排成升序 30、80。同一层的值可以任意两两交换,现在的问题就变成:最少换几次能从当前顺序变成升序。
- 8名次序列: 1, 0关键一步:把每个值换成它在升序里排第几,从 0 数起。80 是第 1 名,30 是第 0 名。于是这一层的位置序列就变成名次 1、0。排好序的样子,就是名次从左到右正好是 0、1,顺次排开。
- 9位置 0 值 80 名次 1 不在家轮到位置 0,紫色点亮它。它现在住着 80,这个值的名次是 1,不等于位置号 0,说明它坐错了地方。那就动手,把它送回名次 1 对应的位置去。
- 10换位置 0 和位置 1 · 累计 1看位置 0,它现在住着 80,这个值的名次是 1,该去位置 1。那就直接把位置 0 和位置 1 的值对换,红色标出这两个正在交换的节点。换完位置 1 住进了 80,名次刚好是 1,它安家了。位置 0 又换来 30,还得继续看它归不归位。这一层已经换了 1 次,累计答案到 1。
- 11位置 0 住进 30(名次 0)换到现在,位置 0 住的值是 30,名次 0,终于等于位置号了,这一整条环理顺了。位置 0 变绿归位。这就是环排序的节奏:盯住一个位置,把不属于它的值一步步顶回各自的家,顺手也把沿途的位置都安顿好。
- 12位置 1 值 80 名次 1 已在家轮到位置 1。它现在住的值是 80,名次是 1,正好等于位置号 1,说明它本来就在自己家里,一次都不用动,变绿跳过。名次等于位置号的,都是这种省心的自环。
- 13本层 1 次 · 累计 1第 1 层全部变绿,从左到右是 30、80,严格递增排好了。这一层一共换了 1 次。回头验证那条结论:本层 2 个节点,拆成了 1 个环,2 减 1 正好是 1,对得上。累计答案现在是 1。
- 14本层: 50, 70, 20, 90把第 2 层紫色高亮起来,从左到右是 50、70、20、90。目标是把它排成升序 20、50、70、90。同一层的值可以任意两两交换,现在的问题就变成:最少换几次能从当前顺序变成升序。
- 15名次序列: 1, 2, 0, 3关键一步:把每个值换成它在升序里排第几,从 0 数起。50 是第 1 名,70 是第 2 名,20 是第 0 名,90 是第 3 名。于是这一层的位置序列就变成名次 1、2、0、3。排好序的样子,就是名次从左到右正好是 0、1、2、3,顺次排开。
- 16位置 0 值 50 名次 1 不在家轮到位置 0,紫色点亮它。它现在住着 50,这个值的名次是 1,不等于位置号 0,说明它坐错了地方。那就动手,把它送回名次 1 对应的位置去。
- 17换位置 0 和位置 1 · 累计 2看位置 0,它现在住着 50,这个值的名次是 1,该去位置 1。那就直接把位置 0 和位置 1 的值对换,红色标出这两个正在交换的节点。换完位置 1 住进了 50,名次刚好是 1,它安家了。位置 0 又换来 70,还得继续看它归不归位。这一层已经换了 1 次,累计答案到 2。
- 18换位置 0 和位置 2 · 累计 3看位置 0,它现在住着 70,这个值的名次是 2,该去位置 2。那就直接把位置 0 和位置 2 的值对换,红色标出这两个正在交换的节点。换完位置 2 住进了 70,名次刚好是 2,它安家了。位置 0 又换来 20,还得继续看它归不归位。这一层已经换了 2 次,累计答案到 3。
- 19位置 0 住进 20(名次 0)换到现在,位置 0 住的值是 20,名次 0,终于等于位置号了,这一整条环理顺了。位置 0 变绿归位。这就是环排序的节奏:盯住一个位置,把不属于它的值一步步顶回各自的家,顺手也把沿途的位置都安顿好。
- 20位置 1 值 50 名次 1 已在家轮到位置 1。它现在住的值是 50,名次是 1,正好等于位置号 1,说明它本来就在自己家里,一次都不用动,变绿跳过。名次等于位置号的,都是这种省心的自环。
- 21位置 2 值 70 名次 2 已在家轮到位置 2。它现在住的值是 70,名次是 2,正好等于位置号 2,说明它本来就在自己家里,一次都不用动,变绿跳过。名次等于位置号的,都是这种省心的自环。
- 22位置 3 值 90 名次 3 已在家轮到位置 3。它现在住的值是 90,名次是 3,正好等于位置号 3,说明它本来就在自己家里,一次都不用动,变绿跳过。名次等于位置号的,都是这种省心的自环。
- 23本层 2 次 · 累计 3第 2 层全部变绿,从左到右是 20、50、70、90,严格递增排好了。这一层一共换了 2 次。回头验证那条结论:本层 4 个节点,拆成了 2 个环,4 减 2 正好是 2,对得上。累计答案现在是 3。
- 24第 2 层: 一个 3 环加一个自环把第 2 层单独拎出来品一品。它的名次序列本来是 1、2、0、3。跟着箭头走:位置 0 想去 1,位置 1 想去 2,位置 2 又指回 0,这三个位置转成一个长度 3 的大环;位置 3 的名次就是 3,自己指自己,是个长度 1 的自环。一个长度 k 的环,理顺它要 k 减 1 次交换;所以 3 环花 2 次、自环花 0 次,合起来 2 次。节点数减环数,就是这么来的。
- 25答案 = 3三层全部排好,回放一遍总账:第 0 层单节点 0 次,第 1 层 80 和 30 对换 1 次,第 2 层理顺那个 3 环用了 2 次。零加一加二,一共 3 次,跟开头说的 3 完全对上。整道题就是广度优先分层,每层名次映射再环排序,把各层次数加起来。
⚠️ 容易写错的地方
✗ 错:用整棵树一起排序来数交换
✓ 对:必须按层各自独立处理
题目只允许交换同一层的两个节点,跨层不能换,分层后每层是一道独立的最少交换子问题
✗ 错:直接对原始值跑环排序
✓ 对:先映射成 0 到 k 减 1 的名次再排
环排序要求元素恰好是 0 到 k 减 1 的排列,才能用值当目标下标;原始值有大有小,必须先换算成名次
✗ 错:把最少交换当成逆序对个数
✓ 对:最少交换等于节点数减环的个数
逆序对是相邻交换的次数,本题允许任意两个位置直接对换,答案由排列的环结构决定,通常远小于逆序对
✗ 错:发现某位置已归位仍去交换
✓ 对:名次等于位置号就跳过
自环不需要任何操作,强行交换会白白多算次数,得不到最少值
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def minimumOperations(self, root: Optional[TreeNode]) -> int:
def swap(arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def f(t):
n = len(t)
m = {v: i for i, v in enumerate(sorted(t))}
for i in range(n):
t[i] = m[t[i]]
ans = 0
for i in range(n):
while t[i] != i:
swap(t, i, t[i])
ans += 1
return ans
q = deque([root])
ans = 0
while q:
t = []
for _ in range(len(q)):
node = q.popleft()
t.append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
ans += f(t)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int minimumOperations(TreeNode* root) {
queue<TreeNode*> q{{root}};
int ans = 0;
auto f = [](vector<int>& t) {
int n = t.size();
vector<int> alls(t.begin(), t.end());
sort(alls.begin(), alls.end());
unordered_map<int, int> m;
int ans = 0;
for (int i = 0; i < n; ++i) m[alls[i]] = i;
for (int i = 0; i < n; ++i) t[i] = m[t[i]];
for (int i = 0; i < n; ++i) {
while (t[i] != i) {
swap(t[i], t[t[i]]);
++ans;
}
}
return ans;
};
while (!q.empty()) {
vector<int> t;
for (int n = q.size(); n; --n) {
auto node = q.front();
q.pop();
t.emplace_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
ans += f(t);
}
return ans;
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int minimumOperations(TreeNode root) {
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int ans = 0;
while (!q.isEmpty()) {
List<Integer> t = new ArrayList<>();
for (int n = q.size(); n > 0; --n) {
TreeNode node = q.poll();
t.add(node.val);
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
ans += f(t);
}
return ans;
}
private int f(List<Integer> t) {
int n = t.size();
List<Integer> alls = new ArrayList<>(t);
alls.sort((a, b) -> a - b);
Map<Integer, Integer> m = new HashMap<>();
for (int i = 0; i < n; ++i) {
m.put(alls.get(i), i);
}
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = m.get(t.get(i));
}
int ans = 0;
for (int i = 0; i < n; ++i) {
while (arr[i] != i) {
swap(arr, i, arr[i]);
++ans;
}
}
return ans;
}
private void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}复杂度
时间
O(n log n)
n 是节点总数。每层大小为 k 时,排序取名次是 O(k log k),建映射 O(k),环排序每次交换至少归位一个值、整层是 O(k)。各层 k 相加等于 n,排序项相加不超过 O(n log n),是整体上界
空间
O(n)
按峰值算。队列在最宽那一层最多同时装下该层所有节点;每层还要一份值数组和名次映射。最坏一层宽到接近 n,所以峰值是 O(n),与节点总数同阶
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 逐层排序二叉树所需的最少操作数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题一句话核心思路是什么?+
广度优先按层拆树,每层单独解。对一层,把值换成它们在升序里的名次,这层就成了 0 到 k−1 的排列,再用环排序把每个值送回名次对应的位置,用掉的交换数就是这层的最少操作。所有层的交换数加起来是答案。
为什么最少交换等于节点数减环数?+
把这层的名次排列看成一张有向图,每个位置指向它的值该去的位置,会自然分成若干不相交的环。一个长为 m 的环,要把环里的值全部归位恰好 m−1 次交换。所有环的长度加起来等于这层的节点数 k,一共 c 个环,总交换就是 k−c。已经在位的值是长度 1 的自环,一次都不用动。
时间和空间复杂度是多少?+
时间 O(n log n),主要花在每层排序求名次上,各层节点数相加等于 n,排序项相加不超过 n log n,环排序部分每层是线性的。空间 O(n),队列和每层的值数组在最宽的一层达到峰值,与节点总数同阶。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 逐层排序二叉树所需的最少操作数目 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。