C++03阶段练习(一)

C++ 数据结构:二叉树右视图 + 最大宽度 + 修剪二叉树,从原理到可运行代码

目录

  1. 二叉树知识回顾
  2. [例题 1:二叉树的右视图](#例题 1:二叉树的右视图)
  3. [例题 2:二叉树的最大宽度](#例题 2:二叉树的最大宽度)
  4. [例题 3:修剪二叉树](#例题 3:修剪二叉树)
  5. 三道题目对比总结
  6. [时间 & 空间复杂度通俗分析](#时间 & 空间复杂度通俗分析)
  7. 高频踩坑易错点汇总
  8. 课后练习题
  9. 全文总结

1. 二叉树知识回顾

基础概念

表格

名词 解释
由结点和边组成;根结点没有父结点,其余结点有且仅有 1 个父结点。
二叉树 每个结点最多 2 个子结点,分为左子结点、右子结点。
结点的度 一个结点拥有的子结点数量。
树的度 整棵树里面所有结点的度的最大值。
树的深度 根到最远叶子经过的边的条数;部分教材定义为结点个数,做题要看清题干。

四类特殊二叉树

  1. 满二叉树:除最后一层叶子,每一层所有结点都拥有 2 个孩子。
  2. 完全二叉树:深度 k,叶子集中在 k、k‑1 层;最底层结点全部靠左摆放。

✨小知识点:满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树

二叉搜索树

  • 左子树全部结点值 ≤ 根;右子树全部结点值 ≥ 根。

  • 左右子树同样满足该规则。

  • 中序遍历可以拿到有序序列;增删查效率和树高 h 相关,(O(\log h))。

    1. 哈夫曼树(最优二叉树)
    • 叶子带权,WPL(带权路径长度)最小的二叉树
    • WPL:全部叶子结点的「权 × 路径长度」相加;用于数据压缩哈夫曼编码。

二叉树的三种存储方式

  1. 数组表示法:只适合完全二叉树,根编号为 1,从上往下从左往右存入数组下标。
  1. 结构体数组存储 :结构体保存valuels左孩子编号、rs右孩子编号,存的是数组下标,不是指针。本文章三道例题全部使用这种存储。
  1. 链式存储 :结构体保存*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 

思路解析

  1. 使用队列做层序遍历,必须记录每一层结点的数量levelCount
  2. 循环处理当前层全部levelCount个结点。
  3. levelCount == 0,代表当前是本层最后一个结点,输出,这就是右视图结点。
  4. 把当前结点的左右孩子依次入队(不为‑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;
}

代码解析

  1. levelCount = q.size():进入一层循环瞬间,队列里面全部都是本层结点。
  2. levelCount--,每弹出一个结点就减一;减到 0 = 当前为本层末尾。
  3. 孩子必须判断不等于‑1,否则会把无效编号压入队列,程序出错。

3. 例题 2:二叉树的最大宽度

【配图说明】蓝色虚线框标记第三层,中间空心圆圈代表不存在的空结点,计算宽度时空结点也要计入。

题目描述:

层宽度定义:该层最左结点到最右结点,中间包含空结点在内的总数量。最大宽度:所有层宽度的最大值。

输入样例

复制代码
7
2 6
3 4
-1 -1
5 -1
-1 -1
-1 7
-1 -1

输出样例

复制代码
4

思路解析

  1. 把原二叉树想象补全成一棵完全二叉树,空位置也算作占位。
  2. 给每个结点标记虚拟位置position:根 position=1;x 结点左孩子2*x,右孩子2*x+1
  3. 层序遍历,每一层:
    • 本层最左结点:q.front();本层最右结点:q.back()
    • 本层宽度 = 最右.position − 最左.position + 1
  4. 不断更新记录全局最大宽度。

⚠重点:宽度统计要算中间空的位置,不能只统计真实存在的结点。

完整 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;
}

代码解析

  1. 每次进入 while 循环,队列内存放完整一层结点,队首队尾直接拿本层左右边界。
  2. 虚拟 position 只用来计算宽度,和结点数组下标不是同一个东西。
  3. 必须包含<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 

思路解析

  • 修剪逻辑和后序遍历完全一致:先修左,再修右,最后处理自己
  1. 如果传入 root 等于‑1,直接返回‑1(空)。
  2. 递归修剪左子树,用返回值更新当前结点的左孩子编号。
  3. 递归修剪右子树,用返回值更新当前结点的右孩子编号。
  4. 修剪完左右之后判断:叶子结点 && value == 0 → 返回‑1,代表剪掉该结点。
  5. 不满足删除条件,返回当前结点编号保留结点。

关键点:一定要先递归处理孩子,再判断自己,不能反过来!

完整 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;
}

代码解析

  1. trimTree返回值非常关键:返回‑1 代表这个子树被删除;返回编号代表保留。
  2. 递归返回值赋值给tree[root].lstree[root].rs,相当于修改树的结构。
  3. 修剪完成拿到新根newRoot,再去做中序、后序遍历输出结果。

5. 三道题目对比总结

表格

题目 核心技术 关键点
二叉树右视图 队列层序遍历,分层处理 输出每层最后一个结点
二叉树最大宽度 队列层序遍历,虚拟 position 编号 宽度要计入中间空结点
修剪二叉树 递归(后序思想) 先处理子结点,再处理自身

右视图、最大宽度都属于层序 BFS 类题目;修剪二叉树属于深度优先 DFS 递归类题目,二叉树两大主流解题套路。

6. 时间 & 空间复杂度通俗分析

n 代表二叉树结点总数量。

  1. 二叉树右视图
    • 时间复杂度 (O(n)):每个结点入队出队恰好一次。
    • 空间复杂度 (O(n)):队列最多存储一层结点,最坏完全二叉树底层约 n/2 个结点。
  2. 二叉树最大宽度
    • 时间复杂度 (O(n)):每个结点访问一次,给 position 赋值。
    • 空间复杂度 (O(n)):队列存储层结点。
  3. 修剪二叉树
    • 时间复杂度 (O(n)):每个结点递归访问一次。
    • 空间复杂度 (O(n)):递归调用栈,最坏单链树递归深度 n。

7. 高频踩坑易错点汇总

  1. 层序遍历忘记设置levelCount,不分层直接遍历,拿不到每一层末尾结点。
  2. 右视图错输出每层第一个结点,混淆右视图、左视图逻辑。
  3. 做最大宽度时,只统计真实结点,忽略中间空结点,宽度算小
  4. 虚拟 position 计算错误,左孩子pos*2、右孩子pos*2+1不要写反。
  5. 修剪二叉树顺序写反,先判断自己,再递归子树,修剪逻辑失效
  6. 混淆赋值=与等于判断==;把 C++ 不等于!=写成!==,编译报错。
  7. 孩子结点判断条件忘记!= -1,把‑1 压入队列造成死循环。
  8. 数组开太小,本题 n<=10,数组至少开辟 11,数组越界乱码。

8. 课后练习题

  1. 手动给定权值集合{4,7,2,9},手动构造哈夫曼树并且算出 WPL。
  2. 修改【二叉树右视图】代码,实现输出二叉树左视图(每层第一个结点)。
  3. 思考题:求二叉树最大宽度这道题,为什么我们需要把中间空结点算进宽度?如果不算空结点会发生什么?

9. 全文总结

  1. 二叉树题目两大核心工具:队列实现层序 BFS,递归实现深度优先 DFS
  2. 右视图本质层序分层遍历,输出每层最末尾结点。
  3. 求最大宽度要引入虚拟 position,宽度计算必须包含层中间空位置。
  4. 修剪二叉树遵循后序思想,优先递归修改左右子树,再判断是否删除自己。
  5. 结构体数组存储二叉树时,‑1 统一用来标记不存在子结点。
  6. 做题时注意边界:空树、叶子结点,数组大小预留足够。
  7. BFS 类题目一定要留意队列入队条件,不要把无效编号压入队列。
  8. 递归类二叉树题:多数时候先处理孩子,再处理当前结点。
相关推荐
刃神太酷啦1 小时前
Redis 核心进阶:哨兵、集群、缓存问题与分布式锁详解----《Hello Redis!》(6)
linux·c语言·数据库·c++·redis·分布式·缓存
郝学胜-神的一滴1 小时前
C++20模板元编程 01:从零吃透模板核心底层逻辑
开发语言·c++·vscode·程序人生·开源
6Hzlia2 小时前
【Classic 150 刷题计划】 LeetCode 123. 买卖股票的最佳时机 III | C++ 状态机动态规划与通用交易拓展
c++·算法·leetcode
luj_17682 小时前
生物体如何应对功能击穿?
c语言·开发语言·c++·经验分享·算法
luj_17682 小时前
击穿分析五大关键步骤
开发语言·网络·c++·经验分享·算法
小王C语言2 小时前
cmake教程
c++·cmake
安大小万2 小时前
蓝桥杯Dev-C++ 5.11(编译器)使用细节排坑
c++
shirsl2 小时前
算法 Day3-队列 / deque + 链表
数据结构·python·算法·链表
raindayinrain2 小时前
c++并发--原子操作,内存顺序,屏障
c++·原子操作·操作顺序