C/C++ 数据结构(十一)树

本篇 核心知识点 :树基础通用概念、二叉树全套知识(定义 / 名词 / 数学公式 / 存储方式 / 堆)、二叉树四种遍历、二叉树递归方法(高度 / 节点数 / 插入删除)、层级遍历

一、树通用基础概念(所有树共用)

1. 概念

树属于非线性分层结构,节点一对多,任意两个节点有且仅有一条唯一通路;多条通路则为图结构。

2. 核心基础特性

  1. 一棵树有且仅有 1 个根节点,存储时仅需保存根指针;

  2. 根无父节点,其余节点有唯一直接父节点;

  3. 一个父节点可分出多个子树分支;

  4. 子树之间互相独立。

3. 专业名词(必背)

  1. 父 / 双亲节点:直接包含当前子节点的上层节点;

  2. 子节点:父节点分出的下层节点;

  3. 兄弟节点:同一个父节点的所有子节点;

  4. 节点的度:该节点拥有子节点的数量;

  5. 叶子节点(叶节点):度 = 0,无任何子节点;

  6. 树的高度 / 深度:从根到最远叶子节点的最大层数(默认层数从 1 开始计算)。

4. 拓展:树的工业用途

树结构核心优势是查找效率远高于链表(对数级复杂度),游戏、引擎大量使用:

四叉树:2D 地图、地形遮挡渲染优化;

八叉树:3D 空间场景裁剪、AI 寻路分区;

二叉树:数据检索、排序堆。

二、二叉树专项(重点)

2.1 二叉树定义

概念

所有节点的度 ≤ 2,每个节点最多分出左、右两个子树,左右子树严格区分,不可互换。

数学公式
  1. 二叉树第 k 层,最多节点数:(2^{k-1})

  2. k 层二叉树,整棵树最大总节点:(2^k -1)

  3. 通用恒等式:

    叶子节点个数 = 度为 2 节点个数 + 1

    已知总节点数 M、度 1 节点 N,可反推叶子、二度节点数量。

2.2 满二叉树 & 完全二叉树

1)满二叉树
概念

每一层都达到最大节点容量,无空缺,不存在只有单侧子树的节点。

特性:总节点一定满足 (2^k-1)。
2)完全二叉树
概念

将满二叉树从最下层最右侧依次删除节点得到的树;空缺仅允许出现在最后一层右侧。

硬性规则

若节点无左子树,则一定无右子树;不可能出现有右无左的节点。

核心价值

支持数组顺序存储(普通二叉树不适合顺序存储)

2.3 二叉树两种存储方式

方式 1:链式存储(手写二叉树默认方案)
概念

节点包含数据、左孩子指针、右孩子、可选父指针;树类仅保存根节点。

特性

任意形态二叉树均可存储,无空间浪费,增删灵活。

代码示例(带父指针节点模板)
复制代码
template<typename T>
struct BinNode
{
    T data;
    BinNode<T>* left;   // 左子节点
    BinNode<T>* right;  //  BinNode<T>* parent; // 父节点指针
    BinNode(const T& val) : data(val), left(nullptr), right(nullptr), parent(nullptr) {}
};
​
template<typename T>
class BinTree
{
private:
    BinNode<T>* root; // 根节点
public:
    BinTree() : root(nullptr) {}
    ~BinTree() { /* 递归释放所有节点 */ }
    void preOrder(BinNode<T>* p); // 前序遍历
    int getHeight(BinNode<T>* p); // 求树高
    void insert(BinNode<T>* p, const T& val);
};
方式 2:顺序存储(仅完全二叉树适用)
概念

使用数组存放节点,依靠下标计算父子位置,无需指针:

下标 i 左孩子:2*i

下标 i 右孩子:2*i+1

下标 i 父节点:i/2

特性

普通二叉树使用顺序存储必须填充大量空占位节点,造成严重空间浪费;完全二叉树节点连续无空缺,无浪费。

拓展

堆(大顶堆 / 小顶堆)底层为完全二叉树,依赖数组实现,优先队列底层就是大顶堆。

2.4 堆(完全二叉树衍生结构)

概念

堆是满足特殊大小规则的完全二叉树,分为大顶堆、小顶堆。

  1. 大顶堆:任意节点值 ≥ 左右子节点,根为全局最大值;

  2. 小顶堆:任意节点值 ≤ 左右子节点,根为全局最小值。

应用

优先队列、堆排序、TOP-K 极值查找。

三、二叉树四大遍历方式

核心规则(递归统一逻辑)

前 / 中 / 后序遍历均以当前节点为区分基准:

  1. 前序:根 → 左 → 右

  2. 中序:左 → 根 → 右

  3. 后序:左 → 右 → 根

  4. 层序(广度遍历):从上到下、从左到右,非递归,依赖队列

3.1 前序遍历

递归实现

复制代码
template<typename T>
void BinTree<T>::preRec(BinNode<T>* p){
    if (p == nullptr) return;
    cout << p->data << " "; // 访问根
    preRec(p->left);        // 递归左
    preRec(p->right);       // 递归右
}

非递归(stack)

思路:先压右、再压左,保证出栈顺序 根→左→右

复制代码
template<typename T>
void BinTree<T>::preOrderNoRec(){
    if (root == nullptr) return;
    stack<BinNode<T>*> st;
    st.push(root);
    while (!st.empty()){
        BinNode<T>* cur = st.top();
        st.pop();
        cout << cur->data << " ";
        // 先压右,后压左
        if (cur->right != nullptr)
            st.push(cur->right);
        if (cur->left != nullptr)
            st.push(cur->left);
    }
    cout << "\n";
}

3.2 中序遍历

递归实现

复制代码
template<typename T>
void BinTree<T>::inRec(BinNode<T>* p){
    if (p == nullptr) return;
    inRec(p->left);         // 左
    cout << p->data << " "; // 根
    inRec(p->right);        // 右
}

非递归(stack)

思路:一路向左压栈,无左则弹出访问,再走右子树

复制代码
template<typename T>
void BinTree<T>::inOrderNoRec(){
    stack<BinNode<T>*> st;
    BinNode<T>* cur = root;
    while (cur != nullptr || !st.empty()){
        // 持续往左走,全部入栈
        while (cur != nullptr){
            st.push(cur);
            cur = cur->left;
        }
        cur = st.top();
        st.pop();
        cout << cur->data << " ";
        cur = cur->right; // 处理右子树
    }
    cout << "\n";
}

3.3 后序遍历

递归实现

复制代码
template<typename T>
void BinTree<T>::postRec(BinNode<T>* p){
    if (p == nullptr) return;
    postRec(p->left);       // 左
    postRec(p->right);      // 右
    cout << p->data << " "; // 根
}

非递归(双stack)

思路:栈 1 存节点,栈 2 逆序保存输出,最终栈 2 弹出即为后序

复制代码
template<typename T>
void BinTree<T>::postOrderNoRec(){
    if (root == nullptr) return;
    stack<BinNode<T>*> st1, st2;
    st1.push(root);
    while (!st1.empty()){
        BinNode<T>* cur = st1.top();
        st1.pop();
        st2.push(cur);
        // 先左后右入栈1,出栈后右先进st2
        if (cur->left != nullptr)
            st1.push(cur->left);
        if (cur->right != nullptr)
            st1.push(cur->right);
    }
    // st2弹出 左 右 根
    while (!st2.empty()){
        cout << st2.top()->data << " ";
        st2.pop();
    }
    cout << "\n";
}

3.4 层序遍历

层序无标准递归写法,只能借助队列queue容器逐层遍历,逐层输出节点,BFS 广度思想 。

复制代码
template<typename T>
void BinTree<T>::levelOrder(){
    if (root == nullptr) return;
    queue<BinNode<T>*> q;
    q.push(root);
    while (!q.empty()){
        BinNode<T>* cur = q.front();
        q.pop();
        cout << cur->data << " ";
        if (cur->left != nullptr)
            q.push(cur->left);
        if (cur->right != nullptr)
            q.push(cur->right);
    }
    cout << "\n";
}

拓展考点

已知两种遍历序列可唯一还原二叉树;层序不能递归实现。

四、二叉树常用递归算法(求高 / 节点数 / 插入 / 删除)

4.1 求子树高度

概念

递归获取左右子树高度,取最大值 + 1 为当前节点高度;空节点高度 = 0。

代码
复制代码
template<typename T>
int BinTree<T>::getHeight(BinNode<T>* p)
{
    if(nullptr == p) return 0;
    int leftH = getHeight(p->left);
    int rightH = getHeight(p->right);
    return leftH > rightH ? leftH + 1 : rightH + 1;
}

4.2 统计整棵树节点总数

概念

空节点返回 0,非空 = 1 + 左子节点数 + 右子节点数

复制代码
template<typename T>
int BinTree::countNode(BinNode<T>* p)
{
    if(!p) return 0;
    return 1 + countNode(p->left) + countNode(p->right);
}

4.3 二叉搜索树插入(默认插入规则:小左大右)

概念

小于等于当前值放左子树,大于放右子树;递归找到空位置新建节点。

复制代码
template<typename T>
void BinTree<T>::insert(BinNode<T>* p, const T& val)
{
    if(val <= p->data)
    {
        if(p->left == nullptr)
        {
            p->left = new BinNode<T>(val);
            p->left->parent = p;
        }
        else insert(p->left, val);
    }
    else
    {
        if(p->right == nullptr)
        {
            p->right = new BinNode<T>(val);
            p->right->parent = p;
        }
        else insert(p->right, val);
    }
}

4.4 节点删除(三大情况)

特性
  1. 叶节点(度 0):直接 delete,父对应指针置空;

  2. 度 1 节点:将唯一子树对接给父节点,释放自身;

  3. 度 2 节点:取前驱 / 后继节点替换数据后删除替代节点。

拓展

删除依赖父指针快速定位,因此手写节点建议保存 parent 指针。

五、拓展:四叉树 & 八叉树(游戏开发优化)

1. 四叉树(2D 场景)

概念

正方形地图不断均等四分,边长推荐 2 的幂次方(位运算加速);

用途

摄像机视锥裁剪,只渲染镜头内地块,远处低精度纹理,减少渲染面,提升帧率。

2. 八叉树(3D 场景)

概念

正方体空间沿 XYZ 三轴八等分;

用途

3D 场景遮挡剔除、AI 分区寻路、碰撞检测优化。

核心原理

树结构分层分区,跳过视野外物体渲染,大幅降低图形计算量,是游戏性能优化核心手段。