当前位置: 首页 > news >正文

二叉树实战笔记:结构、遍历、接口与 OJ 实战

文章目录

  • 1. 节点与树的基本定义
  • 2. 手搓一个二叉树
  • 3. 二叉树的前中后序遍历
    • 3.1. 递归前序遍历(根 左 右)
    • 3.2. 中序遍历(左 根 右)
    • 3.3. 后序遍历(左 右 根)
    • 3.4. 图示总结
  • 4. 二叉树的层序遍历
  • 5. 二叉树相关OJ题
    • 5.1. 单值二叉树
    • 5.2. 相同的树
    • 5.3. 对称二叉树
    • 5.4. 另一棵树的子树
  • 6. 二叉树其他接口
    • 6.1. 求二叉树的最大深度
    • 6.2. 求二叉树的节点总数
    • 6.3. 求第k层节点个数
    • 6.4. 查找值为x的节点
  • 7. 总结

1. 节点与树的基本定义

关于二叉树和树的相关概念和性质,请看这篇文章的前置知识:

从堆到TopK:一文吃透核心原理与实战应用

我们现在直接用代码实现一个二叉树的基本结构:

// 二叉树节点结构体
template <class T>
struct tree_node {T _val;tree_node* _left; // 指向左子树的指针tree_node* _right;// 指向右子树的指针// 默认构造tree_node(const T& val = T()):_val(val), _left(nullptr), _right(nullptr){ }
};

2. 手搓一个二叉树

二叉树是一种复杂的非线性结构,增删查改效率太低,我们学习二叉树是为了学习递归和分治的思想

我们用一颗固定的树来演示:

     1/ \2   4/   /  \3   5    6

构造代码:

template <class T>
class binary_tree {
public:typedef Node tree_node<T>;Node* build_sample_tree() {Node* n1 = new Node(1);Node* n2 = new Node(2);Node* n3 = new Node(3);Node* n4 = new Node(4);Node* n5 = new Node(5);Node* n6 = new Node(6);n1->_left = n2;n1->_right = n4;n2->_left = n3;n4->_left = n5;n4->_right = n6;root = n1;return root;}
private:Node* root;};

3. 二叉树的前中后序遍历

3.1. 递归前序遍历(根 左 右)

原理: 先处理根节点,再递归到左子树 右子树

将根节点处理完后,处理所有的左子树,再处理所有的右子树

递归三部曲: _node -> _left -> _right

在这里插入图片描述

如图:

开始根1完成,往下走,

走到以2为根的树,根2,完成,往下走

走到以3为根的树,根3,完成,往下走

遇到N,3的左子树为空,回溯,回到3,开始找3的右子树,往下走

遇到N,3的右子树为空,回溯,回到3,以3为根的树遍历完成,而3是2的左孩子,回溯,回到2,开始找2的右孩子,往下走

遇到N,2的右子树为空,回溯,回到2,以w为根的树遍历完成,而2是1的左孩子,回溯,回到1,开始找1的右孩子,往下走

…………………………

这体现了 “先走到尽头,再回溯继续” 的思想,前中后序都是这种思想

将复杂的一颗树不断细分,分成根-左子树-右子树,直到这个树不能再分,这便是递归的思想

  • “递”表示开启新方法,程序在此过程中访问下一个节点。
  • “归”表示函数返回,代表当前节点已经访问完毕。
	// 前序遍历void preorder(Node* root) const {// 根 左 右if (root == nullptr) {std::cout << "N";return;}// 根cout << root->_val << " ";// 左preorder(root->_left);// 右preorder(root->_right);}

3.2. 中序遍历(左 根 右)

原理: 先左,再根,再右,二叉搜索树常用(会得到有序序列)

中序遍历按照 左子树 根 右子树的顺序

开始遇到根1,找根1的左子树,往下走

遇到根2,找根2的左子树,往下走

遇到根3,找根3的左子树,往下走

遇到N,根3的左子树找到,回溯,回到根3,开始找根3的右子树,往下走

遇到N,根3的右子树找到,回溯,回到根3,以3为根的树结束,回溯,回到根2,开始找根2的右子树

………………………………

// 中序遍历 左 右 根
void inorder(Node* root) const {if (root == nullptr) {std::cout << "N";return;}// 左inorder(root->_left);// 根std::cout << root->_val << " ";// 右inorder(root->_right);
}

3.3. 后序遍历(左 右 根)

原理: 常用来释放内存(先删子节点再删父节点)

// 后序遍历 左 右 根
void postorder(Node* root) const {if (root == nullptr) {std::cout << "N";return;}// 左postorder(root->_left);// 右postorder(root->_right);// 根std::cout << root->_val << " ";
}

3.4. 图示总结

在这里插入图片描述

前中后序遍历都属于深度优先遍历(depth‑first traversal),也称深度优先搜索(depth‑first
search, DFS)
,它体现了一种 “先走到尽头,再回溯继续” 的遍历方式。

上图展示了对二叉树进行深度优先遍历的工作原理。深度优先遍历就像是绕着整棵二叉树的外围走一圈,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。

4. 二叉树的层序遍历

层序遍历(Level Order Traversal)是二叉树遍历中非常特殊的一种方式:要求按照节点所在的「层次」依次访问,从根节点(第 1 层)开始,逐层向下,同一层的节点从左到右依次访问。

对于这颗树

    1/ \2   4/   /  \
3   5    6

层序遍历严格按照:1->2->4->3->5->6这样的顺序

这里的核心矛盾是:如何保证「先处理完当前层所有节点,再处理下一层」?这就是队列要解决的问题。

要实现「按层访问」,关键是要维护一个「待处理节点的顺序」,而队列的先进先出(FIFO) 特性恰好完美匹配这个需求。

只需要一层带一层即可
在这里插入图片描述

// 层序遍历
void level_order(Node* start) {// 处理空节点if (start == nullptr) return;// 构造队列处理std::queue<Node*> q;q.push(start);// 一层出队列 一层入队列 直到队列为空 遍历完成while (!q.empty()) {Node* cur = start;q.pop();std::cout << cur->_val << " ";if(cur->_left) q.push(cur->_left);if(cur->_right) q.push(cur->_right);}
}

5. 二叉树相关OJ题

5.1. 单值二叉树

965. 单值二叉树 - 力扣(LeetCode)

思路:遍历二叉树,判断每个节点的值是否相同即可,值得注意的是,空树是单值二叉树

  • 层序遍历,借助队列实现
class Solution {
public:bool isUnivalTree(TreeNode* root) {if(!root) return true;// 层序遍历std::queue<TreeNode*> q;q.push(root);int val = root->val;while(!q.empty()){TreeNode* cur = q.front();q.pop();if(!cur) continue;if(cur->val != val)return false;q.push(cur->left);q.push(cur->right);}return true;}
};
  • 深度优先遍历,递归实现
class Solution {
public:bool isUnivalTree(TreeNode* root) {if(!root) return true;// 层序遍历std::queue<TreeNode*> q;q.push(root);int val = root->val;while(!q.empty()){TreeNode* cur = q.front();q.pop();if(cur->val != val) return false;// 左子节点入队(若存在)if (cur->left != nullptr) q.push(cur->left);// 右子节点入队(若存在)if (cur->right != nullptr) q.push(cur->right);}return true;}
};

5.2. 相同的树

100. 相同的树 - 力扣(LeetCode)

思路: 两棵树走相同的路径比较路径是否相同,比较路径上的值是否相同

class Solution {
public:bool isSameTree(TreeNode* p, TreeNode* q) {// 走相同的路径 比较值是否相同if(p == nullptr && q != nullptr) return false;if(p != nullptr && q == nullptr) return false;if(p == nullptr && q == nullptr) return true;if(p->val != q->val) return false;return isSameTree(p->left, q->left) && isSameTree(p->right, q->right); }
};

5.3. 对称二叉树

**思路:**左子树和右子树比较,调用isSameTree就好

class Solution {
public:bool isSymmetric(TreeNode* root) {return isSameTree(root->left,root->right);}
private:bool isSameTree(TreeNode* p, TreeNode* q) {// 走相同的路径 比较值是否相同if(p == nullptr && q != nullptr) return false;if(p != nullptr && q == nullptr) return false;if(p == nullptr && q == nullptr) return true;if(p->val != q->val) return false;return isSameTree(p->left, q->right) && isSameTree(p->right, q->left); }
};

5.4. 另一棵树的子树

572. 另一棵树的子树 - 力扣(LeetCode)

思路:

  • 如果root为空,一定没有子树,子树题目给了是不为空的
  • root不为空,复用isSameTree,从当前节点开始比较,当前节点比较结束之后,遍历这棵树即可,需要注意的是,对于左子树和右子树遍历,逻辑条件是,因为有一棵子树和目标子树相同就相当于这个树含有SubRoot
class Solution {
public:bool isSubtree(TreeNode* root, TreeNode* subRoot) {// 根会走到子树 子树为空节点 一定没有subRootif(!root) return false;// 比较当前节点if(root->val == subRoot->val && isSameTree(root,subRoot))return true;// 当前节点比较完成 遍历子树比较return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);}
private:bool isSameTree(TreeNode* p, TreeNode* q) {// 走相同的路径 比较值是否相同if(p == nullptr && q != nullptr) return false;if(p != nullptr && q == nullptr) return false;if(p == nullptr && q == nullptr) return true;if(p->val != q->val) return false;return isSameTree(p->left, q->left) && isSameTree(p->right, q->right); }
};

6. 二叉树其他接口

6.1. 求二叉树的最大深度

思路:

  • 最大深度是从根节点到最深叶节点的路径长度
  • 对于每个节点来说:
    1. 空节点的深度为0
    2. 非空节点深度 = max(左子树深度,右子树深度) + 1

所以可以得到代码:

// 求一棵二叉树最大深度
size_t get_depth(Node* root) {if (!root) return 0;// 分别求左子树和右子树的深度size_t left_depth = get_depth(root->_left);size_t right_depth = get_depth(root->_right);// 更深的子树加根节点1 就是一棵树的深度return left_depth > right_depth ? left_depth + 1 : right_depth + 1;
}

6.2. 求二叉树的节点总数

思路: 一个节点的总数 = 左子树节点数 + 右子树节点数 + 当前节点本身(1)

利用分治的思想,举个例子

一个学院一个院长,两个专业,一个专业四个班,统计这个学院总共有多少人

院长将任务分发给两个专业负责人,两个专业负责人继续分发任务,给自己管理的班级再次下发任务,任务分配给四个班长,班长统计好之后,回溯给两个专业负责人,两个负责人再回溯给院长,院长根据回溯回来的数据即可得到统计

将一个大问题不断细化,分成若干个子问题,直到子问题不可再分,解决最小子问题之后,不断回溯

// 二叉树节点数目
// 左子树节点数目 + 右子树节点数目 + 当前节点本身(1)
size_t tree_size(Node* root) {if (!root) rteurn 0;return tree_size(root->_left) + tree_size(root->_right) + 1;
}

6.3. 求第k层节点个数

思路:

在这里插入图片描述

  • 第1层只有根节点
  • 第k层节点数 = 左子树第(k-1)层节点数 + 右子树第(k-1层)节点数

所以可以递归向下寻找,当k==1时返回1(说明当前节点就是那一层的节点)

// 第k层节点数目
size_t get_level_node_count(Node* root, size_t k) {if (!root) return 0; // 空树if (root != nullptr && k == 1) return 1; // 只有根节点return get_level_node_count(root->_left, k - 1) + get_level_node_count(root->_right, k - 1);
}

6.4. 查找值为x的节点

思路: 对于任意节点root

  • 当前节点为空---->返回空
  • 当前节点的值==x---->找到了
  • 否则,递归到左子树找,没找到,递归到右子树去找

递归的含义是:我不关心子树内部怎么找,我只关心找到了没有

// 找值为x的节点
Node* find_x(Node* root, const T& x) {if (!root) return nullptr;if (root->_val == x) return root;// 递归去左子树找Node* left = find_x(root->_left, x);if (left) return left;// 左子树没找到 右子树开始return find_x(root->_right, x);
}

7. 总结

通过这一章对二叉树的实现与讲解,我们从「节点结构体」出发,逐步构建了整个二叉树类的逻辑框架,真正理解了二叉树从内存组织 → 递归思想 → 遍历操作 → 内部实现机制的全过程:

  1. 树的本质是递归结构
    每个节点既是独立的对象,又是更大结构的一部分。递归构建、递归遍历、递归销毁,都是利用了树的天然递归性。掌握这一点,就能从容理解所有树形算法。
  2. 节点指针与内存管理是关键
    二叉树不像vector那样连续存储,而是通过指针连接节点。要时刻关注内存分配与释放,防止“野指针”和“内存泄漏”。
  3. 遍历是核心操作
    前序、中序、后序遍历本质是访问顺序的不同:
    • 前序:先根 → 左子树 → 右子树
    • 中序:左子树 → 根 → 右子树
    • 后序:左子树 → 右子树 → 根
      熟练掌握递归与非递归两种实现,是后续理解搜索树、平衡树的基础。
  4. 从“结构”到“抽象”
    二叉树类(binary_tree)将节点操作封装起来,隐藏底层实现,让用户只需通过接口使用。这体现了封装思想面向对象设计的精髓。
  5. 从“代码”到“思维”
    实现二叉树不是记语法,而是训练一种“分而治之”的思维方式:每个问题都可以被拆解为子问题,解决子问题的过程往往与整体相同。
http://www.dtcms.com/a/445700.html

相关文章:

  • 哈尔滨php网站开发公司设置网站默认首页
  • 华为OD机试C卷 - 分披萨 - 贪心 DFS - (Java C++ JavaScript Python)
  • 仿照STM32 HAL库设计思想使用FreeRTOS实现异步非阻塞式设备驱动
  • 铜川做网站电话网页设计规范
  • ssc-FinLLM 金融大模型 相关链接
  • 二叉排序树(建树、查找、删除)
  • Linux学习笔记--i2cget 命令
  • 网站建设的标签指的是响应式网页设计图片
  • 用 CodeBuddy CLI + Prompt,从零到可运行:前后端混合管理系统的高效实战
  • 电源——电荷泵详解
  • 榆林国贸网站建设网站的关键词挖掘方式
  • 从思路到落地:用 Redis 搭建超低延迟在线特征存储库
  • sosdp
  • 快速了解BERT
  • 在线Deflate压缩工具(支持添加zlib头及无zlib头模式)
  • 第14节-增强表结构-ALTER-TABLE
  • Ubuntu之apt更新源
  • T527 IR-RX 调试
  • 低成本能谱仪设计:基于分立器件的模拟前端与数字后端实现方案
  • 计算机视觉的数据收集与标注
  • LeetCode:92.最小路径和
  • 百度竞价推广属于什么广告广东网站se0优化公司
  • Anaconda路径配置
  • GitHub 热榜项目 - 日榜(2025-10-05)
  • java中Math.random()和random()方法区别
  • Django SimpleUI 详解:现代化的Django Admin界面美化方案
  • 网站开发客户流程 6个阶段简述常用的网站开发软件
  • 区块链分层学:新的开始
  • Qt与CMakeLists.txt
  • Ubuntu20.04安装Anaconda3-2025.06