二叉树的相关知识

二叉树的相关知识

问题一:知道二叉树的后序遍历和中序遍历,如何得到前序遍历

我的想法:

  1. 遍历后序序列,找到根结点

  2. 在前序序列中找到你刚刚找到的根结点

  3. 根据找到的根结点,把前序列表中的序列分为两部分,一部分为根结点的左子树,另一部分为根结点的右子树

  4. 分别遍历左子树和右子树

  5. 再重复之前的前3步

  6. 直到一个序列没有左子树与右子树后,把这个子结点加到根结点的左或右子树

困扰我的点:

  1. 用什么数据结构来存储二叉树

    • 数组:适合完全二叉树、树大小已知的场景,实现简单、访问快;
    • 指针:适合任意二叉树、树大小不确定的场景,空间灵活、逻辑直观。

    最常用的是链式结构(通过指针 / 引用关联节点),每个节点包含:

    • 自身值(val)
    • 左子树指针(left)
    • 右子树指针(right)
    struct node{
        int val;
        node* left;
        node* right;
    }
    
    struct TreeNode {
        int val;         // 节点值
        TreeNode* left;  // 左子树指针
        TreeNode* right; // 右子树指针
        TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} //这是构造函数
    };
    
  2. 遍历后序序列,找到根结点后,如何拆分左、右子树

    • 两个序列的左子树对应的下标范围是一致的(这是错的,只有在两个序列开始下标为0开始时是正确的)

      根据后序和中序遍历序列构建二叉树

      TreeNode* buildTree(vector<int>& post, int postStart, int postEnd, vector<int>& in, int inStart, int inEnd, unordered_map<int, int>& inMap)

      参数列表:(后序序列,后序序列的起点,后续序列的终点,中序序列,中序序列的终点,中序序列值与下标的对应map(可有可无,可用循环实现));

      一共6个参数

      拆分左、右子树就是如何构建这6个参数!

      左子树:

      // 可不可以直接使用root

      // index 为root在中序序列中的下标,所以可以在中序序列的范围中使用

      root -> left = bulidTree(post, poststart, poststart + leftSize - 1, in, inStart, index - 1, inMap)

      右子树:

      root -> right = bulidTree(post, postStart + leftSize, postEnd - 1, in, index + 1, inEnd, inMap)

    优化

    优化前:

    TreeNode* buildTree(vector<int>& post, int postStart, int postEnd, vector<int>& in, int inStart, int inEnd, unordered_map<int, int>& inMap)

    优化后:

    TreeNode* buildTree(int subroot, int inStart, int inEnd)

    在构造二叉树时,后续序列的起点与终点的作用主要是用来得到根节点,我们可以直接将参数列表加一个root的下标即可

  3. 之后左右子树的根结点怎么确定

    子树的根节点是其对应后序序列的最后一个元素

  4. 如何判断这个递归该结束直接返回

    子树的节点数量为 0时,递归终止,返回nullptr
    具体表现为:后序序列的起始索引 > 结束索引(或中序序列的起始索引 > 结束索引)。
    例如:

    • 左子树的左子树:中序序列为空(inStart > inEnd),此时返回nullptr
    • 右子树的右子树:如果节点已处理完(postStart > postEnd),返回nullptr
    if (postStart > postEnd || inStart > inEnd) {
        return nullptr; // 子树无节点,递归终止
    }
    
  5. 什么时候把这个子结点加到根结点的左或右子树

    递归构建完子树后,将子树的根节点赋值给当前根的左 / 右指针。
    步骤:

    1. 先创建当前根节点
    2. 递归构建左子树,赋值给root->left
    3. 递归构建右子树,赋值给root->right

总结:核心逻辑链

  1. 从后序末尾取根节点 → 2. 在中序中找根位置,拆分左 / 右子树的中序序列 → 3. 根据左子树节点数,拆分左 / 右子树的后序序列 → 4. 递归构建左 / 右子树,挂到当前根的左右指针 → 5. 当子树无节点时(索引越界),递归终止。

扩展点

  1. TreeNode* root = new TreeNode(rootVal);动态内存分配结合带参数构造函数

  2. 链式结构的实现

    指针是 “地址工具”,动态数组是 “连续内存块”;指针可以指向动态数组的首地址,帮助我们操作动态数组,但指针本身不是动态数组。

    比如:你用 “钥匙(指针)” 打开 “房间(动态数组)”,钥匙和房间是两个东西 —— 钥匙是工具,房间是存储物品的空间。

    所以可以通过指针来实现链式结构

  3. 二叉树的 索引规律(按层次序列)

    • 若父节点索引为 i,则左子节点索引为 2i + 1,右子节点索引为 2i + 2
    • 若子节点索引为 j,则父节点索引为 (j - 1) // 2(整数除法)。
  4. 为什么不用数组来实现二叉树的存储

    • 空间浪费严重:若不是完全二叉树,会浪费大量空间
    • 插入 / 删除不灵活:若要在非叶子节点插入子节点,可能需要移动后续大量元素(破坏原有下标规律)
    • 逻辑不直观:无法直接通过节点本身找到子节点(需依赖下标计算),尤其不适合递归构建(如你 PAT 题中 “后序 + 中序转树” 的场景)。

    递归插入结点的算法适合使用链式结构

  5. 顺序结构与链式结构的比较

    • 数组(顺序结构):适合完全二叉树、树大小已知的场景,实现简单、访问快;结点 <= 30;

      也可以用map来实现;

    • 指针(链式结构):适合任意二叉树、树大小不确定的场景,空间灵活、逻辑直观。

    最常用的是链式结构(通过指针 / 引用关联节点)

    但在算法竞赛中,为了方便实现,一般采用静态存储,即顺序结构

    代码实现:(链式结构)

    #include <iostream>
    #include <vector>
    #include <unordered_map>
    #include <queue>
    using namespace std;
    
    vector<int> post(35), in(35);
    unordered_map<int, int> inMap;
    // 二叉树节点定义
    struct TreeNode {
    	int val;
    	TreeNode* left;
    	TreeNode* right;
    	TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    };
    
    // 根据后序和中序遍历序列构建二叉树
    TreeNode* buildTree(int subroot, int inStart, int inEnd) {
    	// 递归终止条件:区间无效
    	if (inStart > inEnd) {
    		return nullptr;
    	}	
    	// 后序遍历的最后一个元素是当前根节点
    	int rootVal = post[subroot];
    	TreeNode* root = new TreeNode(rootVal);
    
    	// 找到根节点在中序遍历中的位置
    	int inRoot = inMap[rootVal];
    
    	root->left = buildTree(subroot - (inEnd - inRoot + 1), inStart, inRoot - 1);
    
    	root->right = buildTree(subroot - 1, inRoot + 1, inEnd);
    
    	return root;
    }
    
    // 层序遍历二叉树
    vector<int> levelOrder(TreeNode* root) {
    	vector<int> res;
    	if (!root) return res;
    
    	queue<TreeNode*> q;
    	q.push(root);
    
    	while (!q.empty()) {
    		TreeNode* node = q.front();
    		q.pop();
    		res.push_back(node->val);
    
    
    		if (node->left) q.push(node->left);
    
    		if (node->right) q.push(node->right);
    	}
    
    	return res;
    }
    
    int main() {
    	int N;
    	cin >> N;
    
    	// 读取后序遍历序列
    	for (int i = 0; i < N; ++i) {
    		cin >> post[i];
    	}
    
    	// 读取中序遍历序列并建立值到下标的映射
    
    	for (int i = 0; i < N; ++i) {
    		cin >> in[i];
    		inMap[in[i]] = i;
    	}
    
    	// 构建二叉树
    	TreeNode* root = buildTree(N-1, 0, N-1);
    
    	vector<int> result = levelOrder(root);
    
    	for (int i = 0; i < result.size(); ++i) {
    		if (i > 0) cout << " ";
    		cout << result[i];
    	}
    	cout << endl;
    
    	return 0;
    }
    
    

    二叉树的遍历

    • 前,中,后遍历都可以靠dfs实现
    • 层次遍历可以靠bfs实现

    二叉搜索树

    定义与特性

    二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树。
    二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:

    1. 非空左子树的所有键值小于其根结点的键值。
    2. 非空右子树的所有键值大于其根结点的键值。
    3. 左、右子树都是二叉搜索树。

    根据其特性,可直接通过前序序列或后序序列来重构二叉树,无序中序序列

    因为根据其特性可直接在其中一个序列中划分左,右子树。

    特点:

    • BST 的中序遍历结果是 严格递增序列非递减序列

    可用验证一颗二叉树是否 BST

    • 反过来,若一棵二叉树的中序遍历是有序的,则它是 BST(可用于验证 BST);

    高频竞赛题型与解题策略

    1. 验证是否为 BST
      • 方法 1:中序遍历,检查序列是否严格递增(规避 “左子树最大值 < 根 < 右子树最小值” 的复杂判断);
      • 方法 2:递归验证,为每个节点限定合法值范围(如左子树上限为根值,右子树下限为根值)。
    2. 根据遍历序列重构 BST
      • 前序遍历重构:首元素为根,左子树为所有小于根的元素,右子树为所有大于根的元素(直接划分左右区间,无需中序辅助);
      • 后序遍历重构:尾元素为根,同理划分左右子树(左子树元素均小于根,右子树均大于根)。
    3. 动态维护有序序列
      场景:插入元素、删除元素、查询第 k 小 / 大、查询元素排名等。
      • 解法 1:用 set 配合迭代器(如 advance(it, k) 找第 k 元素);
      • 解法 2:自定义 BST,每个节点记录左子树节点数(用于快速计算排名:左子树 size + 1 即为当前节点排名)。
    4. BST 的变形问题
      • 镜像 BST:左子树元素 > 根,右子树元素 < 根(中序遍历为严格递减序列),解题思路与普通 BST 对称;
      • 带权 BST:节点附加权重,需结合权重计算路径和、子树权重和等(如 “从根到叶的最小权重和”)。

    完全二叉树

    • 完全二叉树只要结点个数n确定,只需要知道一种排列序列,就可以重构二叉树

    原理完全二叉树的固定结构特性 + 二叉树(BST)的遍历特性

    完全二叉树的n确定,除了最后一层都是满的,最后一层也是从左向右依次填充

    所以当知道一种序列时,因为二叉树的结构是固定的,剩下的就是在二叉树上增加结点,而不同序列就是按不同的固定顺序排列,所以就是在这个树上填充结点。

    遍历序列的作用是提供节点值的顺序逻辑(如前序 “根→左→右”、中序 “左→根→右”),而完全二叉树的 “结构约束”(由n确定的LR)则提供了节点的位置逻辑。二者结合,可通过递归唯一确定每个节点的位置,进而锁定整棵树的结构。

    简单的说就是完全二叉树确定了树长什么样,而一种完全二叉树的遍历序列中则是确定了树的哪个位置放置是哪一个结点

    比如说中序序列( 1 2 3 4 5 6 7)(左 - 根 - 右)

    1 是代表它是这个树的最左边的结点 ,而这颗树最左边的结点是在哪个位置呢,则可以通过递归得到:左子树的位置编号是 2i + 1(从0开始),到2i + 1满足是小 n 的最大值时,i确定就是这颗树的最左边,以此类推,

    AVL树

    AVL 树是一种自平衡二叉搜索树

    特点:对于树中的每个节点,其左右两个子树的高度差(称为 “平衡因子”)的绝对值不超过 1。当插入或删除节点导致平衡被破坏时,AVL 树会通过旋转操作(左旋、右旋及组合旋转)恢复平衡,从而保证树的高度始终维持在 O (log n) 级别。

    AVL 树的核心概念

    1. 平衡因子:对于任意节点,平衡因子 = 左子树高度 - 右子树高度。AVL 树要求所有节点的平衡因子只能是 - 1、0 或 1。
    2. 旋转操作:当插入 / 删除节点导致平衡因子超出范围时,通过以下旋转修复平衡:
      • LL 旋转:左子树的左子树过深(平衡因子 > 1,且新节点插入左子树的左侧),需右旋。
      • RR 旋转:右子树的右子树过深(平衡因子 <-1,且新节点插入右子树的右侧),需左旋。
      • LR 旋转:左子树的右子树过深(平衡因子 > 1,且新节点插入左子树的右侧),先左旋左子树,再右旋当前节点。
      • RL 旋转:右子树的左子树过深(平衡因子 <-1,且新节点插入右子树的左侧),先右旋右子树,再左旋当前节点。
    3. 操作复杂度:插入、删除、查询的时间复杂度均为O(log n)(因树高严格为 O (log n)),优于普通二叉搜索树(最坏 O (n))。

    AVL 树之所以能通过四种旋转操作维持平衡原因

    关键逻辑:失衡场景的 “穷尽性” 与旋转的 “针对性”

    AVL 树的 “失衡” 定义为:某节点的平衡因子(左子树高度 - 右子树高度)的绝对值为 2(即 + 2 或 - 2)。这种失衡只会由新插入的节点引发,且插入位置只会导致四种 “典型失衡模式”,而四种旋转操作正是为这四种模式量身设计的。

    红黑树

    红黑树(Red-Black Tree)是一种自平衡的二叉搜索树(BST),其核心目标是通过一套严格的颜色规则和调整机制,避免普通二叉搜索树在极端情况下(如插入有序数据)退化为链表,从而保证查询、插入、删除等操作的时间复杂度稳定在 O(log n)(n 为树中节点总数)。

    一、红黑树的核心特性(颜色规则)

    红黑树的每个节点除了存储数据、左/右子节点指针外,还会额外存储一个“颜色”属性(仅为红色黑色)。所有节点必须满足以下 5 条规则,这是维持树“平衡”的关键:

    1. 根节点必为黑色:树的最顶层节点颜色固定为黑色,确保树的“基准”平衡。
    2. 叶子节点(NIL 节点)必为黑色:红黑树的叶子并非实际存储数据的节点,而是指向一个虚构的“空节点”(称为 NIL 节点),所有 NIL 节点均为黑色(可理解为“边界标记”)。
    3. 红色节点的父节点必为黑色:即不存在两个连续的红色节点(父-子不能同时为红),避免红色节点“聚集”导致树倾斜。
    4. 从任意节点到其所有后代 NIL 节点的路径中,黑色节点的数量相同:这条规则是“平衡”的核心——它保证了树的“最长路径”(包含最多红色节点的路径)不会超过“最短路径”(全黑节点路径)的 2 倍(因为红色节点不能连续,最长路径最多是“黑-红-黑-红…”,最短路径是“黑-黑-黑…”)。
    5. 每个节点要么是红色,要么是黑色:颜色属性仅两种选择,无其他可能。

    二、红黑树的“自平衡”机制

    当执行插入删除操作时,新节点的加入或节点的移除可能会破坏上述 5 条规则(称为“树失衡”)。此时红黑树会通过以下两种核心操作修复平衡:

    1. 旋转(Rotation):调整节点的父子关系,改变树的结构但不破坏二叉搜索树的性质(即左子树所有值 < 父节点值 < 右子树所有值)。分为两种:

      • 左旋:以某个节点为“轴”,将其右子节点提升为新父节点,原父节点变为新父节点的左子节点,同时处理原右子节点的左子树(挂到原父节点的右子树)。
      • 右旋:与左旋对称,以某个节点为“轴”,将其左子节点提升为新父节点,原父节点变为新父节点的右子节点,同时处理原左子节点的右子树(挂到原父节点的左子树)。
    2. 颜色翻转(Color Flip):在特定场景下(如插入时“红红冲突”且叔叔节点也为红色),直接修改节点的颜色(如将父节点和叔叔节点改为黑色,祖父节点改为红色),以快速修复规则 3 和规则 4。

    三、红黑树的优缺点

    优点

    • 高效稳定:所有核心操作(查询、插入、删除)的时间复杂度均为 O(log n),避免了普通 BST 的 O(n) 最坏情况。
    • 调整开销低:相比另一种自平衡树(AVL 树,要求左右子树高度差 ≤ 1),红黑树的平衡规则更宽松,插入/删除时的旋转和颜色调整次数更少,适合频繁修改的场景。

    缺点

    • 实现复杂:颜色规则和平衡调整逻辑(尤其是删除后的修复)较为繁琐,代码实现难度高于普通 BST 和 AVL 树。
    • 空间开销略高:每个节点需额外存储 1 位颜色信息(通常用一个布尔变量表示)。

    四、红黑树的典型应用场景

    红黑树因“高效修改 + 稳定性能”的特点,被广泛应用于需要动态维护有序数据的场景:

    • 编程语言的标准库:如 C++ 的 std::map/std::set、Java 的 TreeMap/TreeSet,底层均基于红黑树实现。
    • 操作系统内核:Linux 内核的“完全公平调度器(CFS)”用红黑树管理进程的调度周期;内核中的内存管理模块也会用红黑树维护内存块信息。
    • 数据库索引:部分数据库(如 MySQL 的 InnoDB 引擎)的索引结构(B+ 树)在分裂/合并时,可能会用红黑树临时维护中间节点。

    五、红黑树与 AVL 树的对比

    红黑树和 AVL 树都是自平衡 BST,但适用场景有明显差异,对比如下:

    对比维度红黑树(Red-Black Tree)AVL 树(AVL Tree)
    平衡标准黑色节点路径长度一致(宽松)左右子树高度差 ≤ 1(严格)
    查询效率较高(最长路径是最短的 2 倍)最高(树更矮,路径更短)
    插入/删除效率更高(调整次数少)较低(严格平衡导致调整频繁)
    实现复杂度较复杂(颜色规则 + 旋转)复杂(高度维护 + 旋转)
    适用场景频繁插入/删除(如动态集合、调度器)频繁查询、修改少(如静态索引、字典)

    综上,红黑树是一种“权衡查询效率与修改效率”的自平衡树,凭借其稳定的性能和较低的调整开销,成为工程领域中处理有序动态数据的核心数据结构之一。

    为什么旋转不会改变一棵合法 BST 中序遍历

    核心在于旋转操作仅调整节点的父子关系,不改变节点间的 “大小关系”,而 BST 的中序遍历顺序完全由节点的大小关系决定。
    无论左旋还是右旋,操作前后的局部节点(参与旋转的几个节点)的 “左子树 < 根 < 右子树” 关系始终保持

    • 对于右旋中的 Y 和 X:旋转前 Y < X,旋转后仍有 Y < X;
    • 对于 Z 节点:旋转前 Z 在 Y 的右子树且在 X 的左子树(故 Y < Z < X),旋转后仍满足 Y < Z < X;
    • 其他节点(A、C)的位置虽受影响,但它们与 Y、X、Z 的大小关系不变。

    由于中序遍历的顺序由 “左 < 根 < 右” 的全局大小关系决定,而旋转不改变这种关系,因此中序遍历结果必然不变。

    红黑树插入修复情况整理(父节点为祖父左孩子时)

    情况分类关键前提(父节点为祖父左孩子)核心处理步骤修复目标
    情况 11. 父节点为红色2. 叔节点(父的兄弟)为红色1. 父节点、叔节点→黑色2. 祖父节点→红色3. 以祖父为新 “待检查节点”,向上递归分散红色节点,消除连续红;保持黑高平衡
    情况 21. 父节点为红色2. 叔节点为黑色(或 NIL)3. 新节点是父节点的右孩子1. 对父节点执行左旋2. 交换 “父节点” 与 “新节点” 的角色3. 转入「情况 3」继续处理将 “左 - 右” 弯曲结构转为 “左 - 左” 直线结构,统一后续逻辑
    情况 31. 父节点为红色2. 叔节点为黑色(或 NIL)3. 新节点是父节点的左孩子1. 父节点→黑色2. 祖父节点→红色3. 对祖父节点执行右旋彻底消除连续红节点;修复后无需递归,局部性质满足

    对称场景说明

    若父节点是祖父节点的右孩子,上述 3 种情况的处理逻辑完全对称(仅需交换 “左 / 右” 操作):

    • 情况 2(叔黑 + 新节点为左孩子):对父节点执行右旋,再转入对称的 “情况 3”;
    • 情况 3(叔黑 + 新节点为右孩子):对祖父节点执行左旋,其余着色逻辑不变。

    各种二叉树的总结与比较

    PTA二叉树中的题型分析

    1. 从其他几种序列中得到其中一种序列

      不需要构建二叉树

      • 后序+中序 -> 层序
      • 先序中序转后序
      • 先序后序-> 中序
      • 找两个结点的LCA

      需要构建二叉树

      • Z字形输出树序列(根据中序和后序建树 保存在tree二维数组中
    2. 二叉搜索树

      不需要构建二叉树(得到其序列,只需要知道左右子树的划分即可)

      • 判断一个前序序列是否是其二叉搜索树或镜像树的前序序列,得到其后序序列
      • 得到完全二叉搜索树的层序遍历

      需要构建二叉树

      • 给出一个二叉树的结构与二叉搜索树的结点,得到其层序遍历
      • 最后二层的结点个数
      • 给二叉搜索树的前序序列,判断是否为红黑树
    3. AVL树

      需要建立二叉树(实时插入,动态变化)

      • 建立给出序列的AVL树,输出AVL的根结点
    4. 完全二叉树

      需要建立二叉树

      • 给一个二叉树的结构,判断是否是完全二叉树

      建树的几种方式

    5. 结构体链表

    6. 二维数组

    7. tree[i][0] = val表示post[i]的左孩子是post[val]tree[ i ][ 1 ] = val表示post[i]的右孩子是post[val]

    只需要左右子树就可以得出答案的并且你知道他的左右子树的划分就可以不用建树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值