C++ 数据结构:二叉树右视图 + 最大宽度 + 修剪二叉树,从原理到可运行代码
目录
- 二叉树知识回顾
- [例题 1:二叉树的右视图](#例题 1:二叉树的右视图)
- [例题 2:二叉树的最大宽度](#例题 2:二叉树的最大宽度)
- [例题 3:修剪二叉树](#例题 3:修剪二叉树)
- 三道题目对比总结
- [时间 & 空间复杂度通俗分析](#时间 & 空间复杂度通俗分析)
- 高频踩坑易错点汇总
- 课后练习题
- 全文总结
1. 二叉树知识回顾
基础概念
表格
| 名词 | 解释 |
|---|---|
| 树 | 由结点和边组成;根结点没有父结点,其余结点有且仅有 1 个父结点。 |
| 二叉树 | 每个结点最多 2 个子结点,分为左子结点、右子结点。 |
| 结点的度 | 一个结点拥有的子结点数量。 |
| 树的度 | 整棵树里面所有结点的度的最大值。 |
| 树的深度 | 根到最远叶子经过的边的条数;部分教材定义为结点个数,做题要看清题干。 |
四类特殊二叉树



- 满二叉树:除最后一层叶子,每一层所有结点都拥有 2 个孩子。
- 完全二叉树:深度 k,叶子集中在 k、k‑1 层;最底层结点全部靠左摆放。
✨小知识点:满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树 。
二叉搜索树
-
左子树全部结点值 ≤ 根;右子树全部结点值 ≥ 根。
-
左右子树同样满足该规则。
-
中序遍历可以拿到有序序列;增删查效率和树高 h 相关,(O(\log h))。
-
- 哈夫曼树(最优二叉树)
- 叶子带权,WPL(带权路径长度)最小的二叉树。
- WPL:全部叶子结点的「权 × 路径长度」相加;用于数据压缩哈夫曼编码。
二叉树的三种存储方式
- 数组表示法:只适合完全二叉树,根编号为 1,从上往下从左往右存入数组下标。

- 结构体数组存储 :结构体保存
value、ls左孩子编号、rs右孩子编号,存的是数组下标,不是指针。本文章三道例题全部使用这种存储。


- 链式存储 :结构体保存
*left、*right指针,用new新建结点。 

4 种遍历方式

- 层序遍历 :从上到下、从左到右逐层访问,依靠队列 queue 实现。
- 先序遍历:根 → 左子树 → 右子树(递归)
- 中序遍历:左子树 → 根 → 右子树(递归)
- 后序遍历:左子树 → 右子树 → 根(递归)
小考点:
先序+中序、后序+中序可以唯一还原一棵二叉树;先序 + 后序无法唯一确定二叉树。
2. 例题 1:二叉树的右视图

【配图说明】从树的右侧看向二叉树,虚线表示视线,只有每层最右侧结点可以被看见。
题目描述:站在二叉树的右侧看向树,能看到的结点集合就是二叉树右视图 。本质:输出每一层最右侧的结点编号。
输入
第一行 n 代表结点总数,根固定编号 1;后续 n 行,每行两个整数,代表该结点的左孩子、右孩子编号;-1代表没有孩子。(n \le 10)。
输入样例
cpp
7
2 7
3 6
4 5
-1 -1
-1 -1
-1 -1
-1 -1



输出样例
1 7 6 5
思路解析
- 使用队列做层序遍历,必须记录每一层结点的数量
levelCount。 - 循环处理当前层全部
levelCount个结点。 - 当
levelCount == 0,代表当前是本层最后一个结点,输出,这就是右视图结点。 - 把当前结点的左右孩子依次入队(不为‑1 才入队)。
核心关键点:普通层序遍历是一个一个处理结点;右视图需要按层批量处理。
完整 C++ 代码
#include<iostream>
#include<queue>
using namespace std;
//结构体数组存储,ls rs存孩子结点编号,-1代表无孩子
struct tree_node{
int ls, rs;
};
tree_node tree[11];
int n;
queue<int> q;
//打印二叉树右视图
void printRightView(){
q.push(1); //根结点编号1入队列
while(!q.empty())
{
int levelCount = q.size(); //获取当前这一层一共有多少结点
while(levelCount > 0)
{
levelCount--;
int id = q.front();
q.pop();
//levelCount递减到0,代表本层最后一个结点
if(levelCount == 0)
{
cout << id << " ";
}
//左孩子不为空入队
if(tree[id].ls != -1)
q.push(tree[id].ls);
//右孩子不为空入队
if(tree[id].rs != -1)
q.push(tree[id].rs);
}
}
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> tree[i].ls >> tree[i].rs;
}
printRightView();
return 0;
}
代码解析
levelCount = q.size():进入一层循环瞬间,队列里面全部都是本层结点。levelCount--,每弹出一个结点就减一;减到 0 = 当前为本层末尾。- 孩子必须判断不等于‑1,否则会把无效编号压入队列,程序出错。
3. 例题 2:二叉树的最大宽度


【配图说明】蓝色虚线框标记第三层,中间空心圆圈代表不存在的空结点,计算宽度时空结点也要计入。
题目描述:
层宽度定义:该层最左结点到最右结点,中间包含空结点在内的总数量。最大宽度:所有层宽度的最大值。
输入样例
7
2 6
3 4
-1 -1
5 -1
-1 -1
-1 7
-1 -1
输出样例
4

思路解析
- 把原二叉树想象补全成一棵完全二叉树,空位置也算作占位。
- 给每个结点标记虚拟位置
position:根 position=1;x 结点左孩子2*x,右孩子2*x+1。 - 层序遍历,每一层:
- 本层最左结点:
q.front();本层最右结点:q.back() - 本层宽度 =
最右.position − 最左.position + 1
- 本层最左结点:
- 不断更新记录全局最大宽度。
⚠重点:宽度统计要算中间空的位置,不能只统计真实存在的结点。
完整 C++ 代码
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
struct tree_node{
int ls, rs;
int position; //虚拟补全完全二叉树的位置编号
};
tree_node tree[11];
int n;
queue<int> q;
int getMaxWidth(){
int maxWidth = 0;
q.push(1);
tree[1].position = 1; //根结点虚拟位置为1
while(!q.empty())
{
int leftId = q.front();
int rightId = q.back();
//计算本层宽度
int width = tree[rightId].position - tree[leftId].position + 1;
maxWidth = max(maxWidth, width);
int levelCount = q.size();
while(levelCount > 0)
{
levelCount--;
int id = q.front();
q.pop();
int pos = tree[id].position;
//左孩子
if(tree[id].ls != -1)
{
int lchild = tree[id].ls;
tree[lchild].position = pos * 2;
q.push(lchild);
}
//右孩子
if(tree[id].rs != -1)
{
int rchild = tree[id].rs;
tree[rchild].position = pos * 2 + 1;
q.push(rchild);
}
}
}
return maxWidth;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> tree[i].ls >> tree[i].rs;
}
cout << getMaxWidth();
return 0;
}
代码解析
- 每次进入 while 循环,队列内存放完整一层结点,队首队尾直接拿本层左右边界。
- 虚拟 position 只用来计算宽度,和结点数组下标不是同一个东西。
- 必须包含
<algorithm>头文件,否则max()函数编译报错。
4. 例题 3:修剪二叉树


【配图说明】递归顺序按照后序遍历,先处理左右子结点,再处理自己;灰色表示被剪掉的 0 结点。
题目描述:
修剪全部子结点都为 0 的子树:如果一个结点修剪完左右子树之后变成叶子,并且值等于 0,就把这个结点删掉(返回‑1 代表空) 。输出修剪后的中序、后序遍历;修剪后整棵树为空输出两行NULL。

输入样例
7
12 2 6
3 3 4
5 -1 -1
0 5 -1
0 -1 -1
6 -1 7
7 -1 -1
输出样例
5 3 12 6 7
5 3 7 6 12
思路解析
- 修剪逻辑和后序遍历完全一致:先修左,再修右,最后处理自己。
- 如果传入 root 等于‑1,直接返回‑1(空)。
- 递归修剪左子树,用返回值更新当前结点的左孩子编号。
- 递归修剪右子树,用返回值更新当前结点的右孩子编号。
- 修剪完左右之后判断:叶子结点 && value == 0 → 返回‑1,代表剪掉该结点。
- 不满足删除条件,返回当前结点编号保留结点。
关键点:一定要先递归处理孩子,再判断自己,不能反过来!
完整 C++ 代码
#include<iostream>
using namespace std;
struct tree_node{
int value;
int ls, rs;
}tree[11];
int n;
//修剪子树,返回修剪之后子树根编号,-1为空
int trimTree(int root){
if(root == -1)
return -1;
//先递归修剪左右子树,更新孩子编号
tree[root].ls = trimTree(tree[root].ls);
tree[root].rs = trimTree(tree[root].rs);
//修剪后变成叶子,并且值等于0 → 删除
if(tree[root].ls == -1 && tree[root].rs == -1 && tree[root].value == 0)
{
return -1;
}
else
{
return root;
}
}
//中序遍历:左→根→右
void inOrder(int root){
if(root != -1)
{
inOrder(tree[root].ls);
cout << tree[root].value << " ";
inOrder(tree[root].rs);
}
}
//后序遍历:左→右→根
void postOrder(int root){
if(root != -1)
{
postOrder(tree[root].ls);
postOrder(tree[root].rs);
cout << tree[root].value << " ";
}
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> tree[i].value;
cin >> tree[i].ls >> tree[i].rs;
}
int newRoot = trimTree(1);
if(newRoot == -1)
{
cout << "NULL" << endl << "NULL";
}
else
{
inOrder(newRoot);
cout << endl;
postOrder(newRoot);
}
return 0;
}
代码解析
trimTree返回值非常关键:返回‑1 代表这个子树被删除;返回编号代表保留。- 递归返回值赋值给
tree[root].ls、tree[root].rs,相当于修改树的结构。 - 修剪完成拿到新根
newRoot,再去做中序、后序遍历输出结果。
5. 三道题目对比总结
表格
| 题目 | 核心技术 | 关键点 |
|---|---|---|
| 二叉树右视图 | 队列层序遍历,分层处理 | 输出每层最后一个结点 |
| 二叉树最大宽度 | 队列层序遍历,虚拟 position 编号 | 宽度要计入中间空结点 |
| 修剪二叉树 | 递归(后序思想) | 先处理子结点,再处理自身 |
右视图、最大宽度都属于层序 BFS 类题目;修剪二叉树属于深度优先 DFS 递归类题目,二叉树两大主流解题套路。
6. 时间 & 空间复杂度通俗分析
n 代表二叉树结点总数量。
- 二叉树右视图
- 时间复杂度 (O(n)):每个结点入队出队恰好一次。
- 空间复杂度 (O(n)):队列最多存储一层结点,最坏完全二叉树底层约 n/2 个结点。
- 二叉树最大宽度
- 时间复杂度 (O(n)):每个结点访问一次,给 position 赋值。
- 空间复杂度 (O(n)):队列存储层结点。
- 修剪二叉树
- 时间复杂度 (O(n)):每个结点递归访问一次。
- 空间复杂度 (O(n)):递归调用栈,最坏单链树递归深度 n。
7. 高频踩坑易错点汇总
- 层序遍历忘记设置
levelCount,不分层直接遍历,拿不到每一层末尾结点。 - 右视图错输出每层第一个结点,混淆右视图、左视图逻辑。
- 做最大宽度时,只统计真实结点,忽略中间空结点,宽度算小。
- 虚拟 position 计算错误,左孩子
pos*2、右孩子pos*2+1不要写反。 - 修剪二叉树顺序写反,先判断自己,再递归子树,修剪逻辑失效。
- 混淆赋值
=与等于判断==;把 C++ 不等于!=写成!==,编译报错。 - 孩子结点判断条件忘记
!= -1,把‑1 压入队列造成死循环。 - 数组开太小,本题 n<=10,数组至少开辟 11,数组越界乱码。
8. 课后练习题
- 手动给定权值集合
{4,7,2,9},手动构造哈夫曼树并且算出 WPL。 - 修改【二叉树右视图】代码,实现输出二叉树左视图(每层第一个结点)。
- 思考题:求二叉树最大宽度这道题,为什么我们需要把中间空结点算进宽度?如果不算空结点会发生什么?
9. 全文总结
- 二叉树题目两大核心工具:队列实现层序 BFS,递归实现深度优先 DFS。
- 右视图本质层序分层遍历,输出每层最末尾结点。
- 求最大宽度要引入虚拟 position,宽度计算必须包含层中间空位置。
- 修剪二叉树遵循后序思想,优先递归修改左右子树,再判断是否删除自己。
- 结构体数组存储二叉树时,‑1 统一用来标记不存在子结点。
- 做题时注意边界:空树、叶子结点,数组大小预留足够。
- BFS 类题目一定要留意队列入队条件,不要把无效编号压入队列。
- 递归类二叉树题:多数时候先处理孩子,再处理当前结点。
