详解数据结构之树、二叉树、二叉搜索树详解 C++实现

树是n ( n>=0 )个结点的有限集。n=0时称为空树。在任意一棵非空树中:

  1. 有且仅有一个特定的称为根(Root)的结点;
  2. 当n>1时,其余结点可分为m (m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树;
  3. n>0时根结点是唯一的,不可能存在多个根结点;
  4. m>0时,子树的个数没有限制,但它们一定是互不相交的;
    如下图两个就不是树因为它们都有相交的子树;

下面讲解树中的几个名词:

  • :结点拥有的子树的个数称为结点的度,树的度是树内各结点的度的最大值。
  • 叶结点:度为0的结点称为叶结点(Leaf)或终端结点
  • 分支结点:度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。
  • 深度:这是之树的最大层次根为第一层,根的孩子为第二层以此类推
  • 孩子:结点的子树的根称为该结点的孩子(Child)
  • 双亲:孩子节点的上一级结点叫做双亲
  • 兄弟 :同一个双亲的孩子之间互称兄弟

图中的G H I J为叶子节点,B C D E F为分支结点也叫做内部结点,整颗树的度为3因为节点D的度为3

二叉树

二叉树是一种特殊的树,它的每个节点最大只能由两个度,如下图所示

  • 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
    下面再介绍二叉树的几种特殊形态:
  1. 斜树:顾名思义,斜树一定要是斜的,但是往哪斜还是有讲究。所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树
  2. 满二叉树 :在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在 同一层上,这样的二叉树称为满二叉树。
    1. 叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
    2. 非叶子结点的度一定是2。否则就是"缺胳膊少腿" 了。
    3. 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
  3. 完全二叉树 :对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树,对于完全二叉树最重要的一点是,每一层的编号必须是连续的不能由断
    1. 叶子结点只能出现在最下两层。
    2. 最下层的叶子一定集中在左部连续位置。
    3. 倒数二层,若有叶子结点,一定都在右部连续位置。
    4. 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情.况。
    5. 同样结点数的二叉树,完全二叉树的深度最小。

二叉树的性质

  1. 在二叉树的第i层上至多有2的i-1次方个结点(i>=1);

  2. 深度为k的二叉树至多有2的k次方-1个结点(k>=1);

  3. 对任何一棵二叉树T,如果其终端结点数为N,度为2的结点数为U,则 N=U+1

  4. 具有n个结点的完全二叉树的深度为|log2n| + 1

  5. 如果对一棵有n个结点的完全二叉树(其深度为|log2n| + 1)的结点按层序编号(从第1层到第|log2n| + 1层,每层从左到右),对任一结点i (1<=i<=n) 有:

  6. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点 |i/2|

  7. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i

  8. 如果2i + 1>n,则结点i无右孩子;否则其右孩子是结点2i + 1。

二叉树的存储形式

  • 顺序存储
    顺序存储是一种适合完全二叉树的存储方式,完全二叉树按层序编号后,每个结点的编号对应了数组中的索引
    ![[完全二叉树顺序存储.jpg]]
    考虑一种极端的情况,一棵深度为k的右斜树,它只有k个结点,却需要分配2k-1个存储单元空间,这显然是对存储空间的浪费。所以,顺序存储结构一般只用于完全二叉树。
  • 二叉链表
    我们将一个二叉树结点的数据结构定义为如下:
cpp 复制代码
struct BinaryTreeNode{
    T value;
    BinaryTreeNode* left;
    BinaryTreeNode* right;
    BinaryTreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};

一个节点包含值,与指向左右孩子的指针,当没有某个孩子时我们只需要将对应的节点置为空就可以,这就解决了顺序存储的空间两位问题

下面演示一个简单二叉树的实现:

cpp 复制代码
class BinaryTree {
public:
    BinaryTree(BinaryTreeNode* node) : root(node) {}
    ~BinaryTree() { clear(root); }

    // 创建新节点(用户需自行管理节点连接)
    BinaryTreeNode* createNode(int data) {
        return new BinaryTreeNode(data);
    }
    BinaryTreeNode* getRoot() const {
        return root;
    }

    void setLeftChild(BinaryTreeNode* parent, BinaryTreeNode* child) {
        if (parent) parent->left = child;
    }
    void setRightChild(BinaryTreeNode* parent, BinaryTreeNode* child) {
        if (parent) parent->right = child;
    }
private:
    BinaryTreeNode* root;
    // 递归释放节点内存
    void clear(BinaryTreeNode* node) {
        if (node) {
            clear(node->left);
            clear(node->right);
            delete node;
        }
    }
};

root这是一个指向 BinaryTreeNode 类型的指针,代表二叉树的根节点。由于它被声明为私有成员,外部代码无法直接访问,只能通过类提供的公共方法来操作。createNode该方法接收一个整数 data 作为参数,使用 new 运算符动态分配一个新的 BinaryTreeNode 对象,并将 data 作为节点的值进行初始化。最后返回指向该节点的指针。需要注意的是,用户需要自行管理这些节点之间的连接关系。
setLeftChildsetRightChild负责将指定结点,放到parent结点的左孩子或者右孩子中

cpp 复制代码
BinaryTree tree(new BinaryTreeNode(1));
BinaryTreeNode* root = tree.getRoot();

BinaryTreeNode* node2 = tree.createNode(2);
BinaryTreeNode* node3 = tree.createNode(3);
BinaryTreeNode* node4 = tree.createNode(4);
BinaryTreeNode* node5 = tree.createNode(5);
// 连接节点
tree.setLeftChild(root, node2);
tree.setRightChild(root, node3);
tree.setLeftChild(node2, node4);
tree.setRightChild(node2, node5);

// 构建树结构:
//        1
//      /   \
//     2     3
//    / \
//   4   5

遍历二叉树

二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

  • 前序遍历
    先访问根结点,然后前序遍历左子树,再前序遍历右子树。
    访问顺序 :根节点 → 左子树 → 右子树
    特点 :优先访问当前节点,再递归遍历左、右子树。
    应用 :常用于复制二叉树结构(先创建父节点,再复制子节点),或生成表达式的前缀表示 (如 + a * b c)。
cpp 复制代码
void _preorderTraversal(BinaryTreeNode* root){
    if (root == nullptr) return;
    std::cout << root->data << " ";
    _preorderTraversal(root->left);
    _preorderTraversal(root->right);
}

前序遍历是一种深度优先遍历方式,其访问顺序为根节点、左子树、右子树。函数首先检查 root 是否为空指针,如果为空则直接返回,因为空树没有节点可遍历。若 root 不为空,函数会先输出当前根节点的数据,也就是打印 root->data 的值,这体现了前序遍历中先访问根节点的特点。接着,函数递归调用自身,对左子树进行前序遍历,即 _preorderTraversal(root->left),以同样的规则去遍历左子树中的所有节点。

最后,函数再次递归调用自身对右子树进行前序遍历,即 _preorderTraversal(root->right),完成整个子树的前序遍历操作。通过不断递归调用,该函数可以遍历整棵二叉树并按前序顺序输出所有节点的数据。

  • 中序遍历
    从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
    访问顺序 :左子树 → 根节点 → 右子树
    特点 :先遍历左子树到底,再回溯访问根节点,最后处理右子树。
    应用 :在二叉搜索树(BST)中,中序遍历会按升序输出节点 (如排序结果)。也可用于生成表达式的中缀表示 (如 a + b * c)。
cpp 复制代码
void _inorderTraversal(BinaryTreeNode* root){
    if (root == nullptr) return;
    _inorderTraversal(root->left);
    std::cout << root->data << " ";
    _inorderTraversal(root->right);
}

中序遍历函数先检查 root 是否为空指针,若为空则直接返回,因为空树没有节点可遍历。若 root 不为空,函数会先递归调用自身对左子树进行中序遍历,即执行 _inorderTraversal(root->left),以相同规则遍历左子树中的所有节点;接着输出当前根节点的数据,也就是打印 root->data 的值,这符合中序遍历先访问左子树,再访问根节点的特点;最后,函数再次递归调用自身对右子树进行中序遍历,即执行 _inorderTraversal(root->right),完成整个子树的中序遍历操作。通过不断递归调用,该函数能够遍历整棵二叉树并按中序顺序输出所有节点的数据。

  • 后序遍历
    从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
    访问顺序 :左子树 → 右子树 → 根节点
    特点 :最后访问根节点,确保子节点处理完成后才处理父节点。
    应用 :适合需要先删除子节点再删除父节点 的场景(如释放二叉树内存),或计算表达式的后缀表示 (如 a b c * +)。
cpp 复制代码
void _postorderTraversal(BinaryTreeNode* root){
    if (root == nullptr) return;
    _postorderTraversal(root->left);
    _postorderTraversal(root->right);
    std::cout << root->data << " ";
}

函数先检查 root 是否为空,若为空指针则直接返回,因为空树没有节点可处理。若 root 不为空,函数先递归调用自身对左子树进行后序遍历,即 _postorderTraversal(root->left),按同样规则遍历左子树所有节点;接着又递归调用自身对右子树进行后序遍历,也就是 _postorderTraversal(root->right);最后输出当前根节点的数据,即打印 root->data 的值,这符合后序遍历先访问左子树、再访问右子树、最后访问根节点的顺序。通过不断递归,该函数可遍历整棵二叉树并按后序顺序输出所有节点的数据。

  • 层序遍历
    从树的第一层,也就是根结点开始访问, 从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
    访问顺序 :按层从上到下、每层从左到右访问
    特点 :使用队列 逐层记录节点,实现广度优先搜索(BFS)。
    应用 :适合计算树的层级属性 (如最大深度、最小深度),或按层输出节点(如打印树的结构)。
cpp 复制代码
void levelorderTraversal(){
    if (root == nullptr) return;
    std::queue<BinaryTreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        BinaryTreeNode* node = q.front();
        q.pop();
        std::cout << node->data << " ";
        if (node->left) q.push(node->left);
        if (node->right) q.push(node->right);
    }
}

层序遍历是按照二叉树的每一层,从左到右依次访问节点。函数首先检查二叉树的根节点 root 是否为空,若为空则直接返回,因为空树没有节点可遍历。接着,函数创建了一个 std::queue 类型的队列 q,并将根节点 root 入队。之后进入一个循环,只要队列不为空,就从队列头部取出一个节点 node,并将其从队列中移除,然后输出该节点的数据 node->data。随后,检查该节点是否有左子节点,如果有则将左子节点入队;再检查该节点是否有右子节点,如果有则将右子节点入队。这样,队列中始终存储着当前层和下一层待访问的节点,且能保证按层从左到右的顺序依次访问节点,直至队列为空,完成整个二叉树的层序遍历。

二叉搜索树(BST树)

二叉查找树简写BST,是满足某些条件的特殊二叉树。任何一个节点的左子树上的点,都必须小于当前节点。任何一个节点的右子树上的点,都必须大于当前节点。任何一棵子树,也都满足上面两个条件。另外二叉查找树中,是不存在重复节点 的。

二叉搜索树必须满足以下条件:

  1. 有序性 :对于树中的任意节点:
    • 左子树 上的所有节点值 该节点的值。
    • 右子树 上的所有节点值 该节点的值。
  2. 递归性 :左子树和右子树本身也必须是二叉搜索树。

对于二叉搜索树若插入数据本身有序(如 1, 2, 3, 4),BST 会退化为链表 ,时间复杂度退化为 O(N) 。这时候可以使用平衡二叉搜索树 (如AVL树、红黑树),强制保持树高接近 log N

  • 查找
    从根节点开始,逐层比较目标值与当前节点
  • 若目标值 < 当前节点值 → 进入左子树。
  • 若目标值 > 当前节点值 → 进入右子树。
  • 若相等 → 找到目标节点。
cpp 复制代码
BinaryTreeNode* _search(BinaryTreeNode* root, int data){
    if (root == nullptr || root->data == data){
        return root;
    }
    if (data < root->data){
        return _search(root->left, data);
    }else{
        return _search(root->right, data);
    }
}
  • 插入
    类似查找,找到合适的位置插入新节点,保持有序性。
cpp 复制代码
void _insert(BinaryTreeNode* root, BinaryTreeNode* node){
    if (node->data < root->data){//左
        if (root->left == nullptr) {
            root->left = node;
            return;
        }
        _insert(root->left, node);
    }else if (node->data > root->data){//右
        if (root->right == nullptr) {
            root->right = node;
            return;
        }
        _insert(root->right, node);
    }else{
        std::cout << "Data already exists" << std::endl;
    }
}
  • 删除
    删除分为三种情况:
    1. 叶子节点:直接删除。
    2. 有一个子节点:用子节点替换被删除节点。
    3. 有两个子节点:找到右子树的最小节点(或左子树的最大节点)替换被删除节点,再递归删除该最小/大节点。
cpp 复制代码
void _remove(BinaryTreeNode* root, int data, BinaryTreeNode* parent = nullptr){
    if (root == nullptr){
        return;
    }
    if (data < root->data){
        return _remove(root->left, data, root);
    }else if (data > root->data){
        return _remove(root->right, data, root);
    }else{
        if (root->left == nullptr && root->right == nullptr) { // 叶子节点
            if (parent->left == root) {
                parent->left = nullptr;
            } else {
                parent->right = nullptr;
            }
            delete root;
            return;
        } else if (root->left != nullptr && root->right != nullptr) {
            BinaryTreeNode* minNode = _findMin(root->right);
            root->data = minNode->data;
            _remove(root->right, minNode->data, root);
        } else if (root->left != nullptr || root->right != nullptr) {
            BinaryTreeNode* child = 
		            root->left != nullptr ? root->left : root->right;
            *root = *child;
            delete child;
        }
    }
}

函数接收三个参数,root 是当前子树的根节点,data 是要删除的节点的数据值,parentroot 的父节点,默认为 nullptr。函数首先检查 root 是否为空,若为空则直接返回,因为空树中不存在要删除的节点。若 data 小于 root 的数据值,说明要删除的节点在左子树中,递归调用 _remove 函数继续在左子树中查找并删除。若 data 大于 root 的数据值,表明要删除的节点在右子树中,递归调用 _remove 函数在右子树中查找并删除。

data 等于 root 的数据值时,说明找到了要删除的节点,此时分三种情况处理:若该节点是叶子节点(即没有左右子节点),根据它是父节点的左子节点还是右子节点,将父节点对应的子节点指针置为 nullptr,然后删除该节点;若该节点有左右两个子节点,先调用 _findMin 函数找到其右子树中的最小节点,将该最小节点的数据值赋给当前节点,再递归调用 _remove 函数在右子树中删除该最小节点;若该节点只有一个子节点,将该子节点的数据和指针信息复制到当前节点,然后删除该子节点。通过这种方式,函数能正确地从二叉搜索树中删除指定数据的节点。

完整代码如下:

cpp 复制代码
#ifndef _TEST_H
#define _TEST_H

#include <iostream>
#include <vector>
#include <functional>
#include <queue>
#include <vector>
#include <iomanip>

// 二叉树节点结构
struct BinaryTreeNode {
    int data;
    BinaryTreeNode* left;
    BinaryTreeNode* right;
    BinaryTreeNode(int val) : data(val), left(nullptr), right(nullptr) {}
};

class BinarySearchTree {
public:
    BinarySearchTree(BinaryTreeNode* node) : _rootNode(node) {}
    ~BinarySearchTree() { clear(_rootNode); }

    void insert(int data) {
        BinaryTreeNode* node = new BinaryTreeNode(data);
        _insert(_rootNode, node);
    }
    BinaryTreeNode* search(int data) {
        return _search(_rootNode, data);
    }
    void remove(int data) {
        _remove(_rootNode, data);
    }
    void inorderTraversal(){
        _inorderTraversal(_rootNode);
        std::cout << std::endl;
    }
    void levelorderTraversal(){
        if (_rootNode == nullptr) return;
        std::queue<BinaryTreeNode*> q;
        q.push(_rootNode);
        while (!q.empty()) {
            BinaryTreeNode* node = q.front();
            q.pop();
            std::cout << node->data << " ";
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
        std::cout << std::endl;
    }

private:
    void clear(BinaryTreeNode* node) {
        if (node) {// 递归释放节点内存
            clear(node->left);
            clear(node->right);
            delete node;
        }
    }
    void _insert(BinaryTreeNode* root, BinaryTreeNode* node){
        if (node->data < root->data){//左
            if (root->left == nullptr) {
                root->left = node;
                return;
            }
            _insert(root->left, node);
        }else if (node->data > root->data){//右
            if (root->right == nullptr) {
                root->right = node;
                return;
            }
            _insert(root->right, node);
        }else{
            std::cout << "Data already exists" << std::endl;
        }
    }
    void _remove(BinaryTreeNode* root, int data, BinaryTreeNode* parent = nullptr){
        if (root == nullptr){
            return;
        }

        if (data < root->data){
            return _remove(root->left, data, root);
        }else if (data > root->data){
            return _remove(root->right, data, root);
        }else{
            if (root->left == nullptr && root->right == nullptr) { // 叶子节点
                if (parent->left == root) {
                    parent->left = nullptr;
                } else {
                    parent->right = nullptr;
                }
                delete root;
                return;
            } else if (root->left != nullptr && root->right != nullptr) {
                BinaryTreeNode* minNode = _findMin(root->right);
                root->data = minNode->data;
                _remove(root->right, minNode->data, root);
            } else if (root->left != nullptr || root->right != nullptr) { // 只有一个节点
                BinaryTreeNode* child = root->left != nullptr ? root->left : root->right;
                *root = *child;
                delete child;
            }
        }
    }
    BinaryTreeNode* _findMin(BinaryTreeNode* root){
        if (root == nullptr) return nullptr;
        while (root->left != nullptr) {
            root = root->left;
        }
        return root;
    }
    BinaryTreeNode* _search(BinaryTreeNode* root, int data){
        if (root == nullptr || root->data == data){
            return root;
        }

        if (data < root->data){
            return _search(root->left, data);
        }else{
            return _search(root->right, data);
        }
    }
    void _inorderTraversal(BinaryTreeNode* root){
        if (root == nullptr) return;
        _inorderTraversal(root->left);
        std::cout << root->data << " ";
        _inorderTraversal(root->right);
    }

private:
    BinaryTreeNode* _rootNode;

};

#endif
相关推荐
鹿屿二向箔11 分钟前
阀门流量控制系统MATLAB仿真PID
开发语言·matlab
jiet_h13 分钟前
深入解析Kapt —— Kotlin Annotation Processing Tool 技术博客
android·开发语言·kotlin
序属秋秋秋14 分钟前
算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】
c语言·c++·学习·算法
想睡hhh21 分钟前
c语言数据结构——八大排序算法实现
c语言·数据结构·排序算法
anda010922 分钟前
11-leveldb compact原理和性能优化
java·开发语言·性能优化
tRNA做科研24 分钟前
通过Bioconductor/BiocManager安装生物r包详解(问题汇总)
开发语言·r语言·生物信息学·bioconductor·biocmanager
Tiger Z25 分钟前
R 语言科研绘图 --- 韦恩图-汇总
开发语言·程序人生·r语言·贴图
爱吃馒头爱吃鱼28 分钟前
QML编程中的性能优化二
开发语言·qt·学习·性能优化
幻想趾于现实1 小时前
C# Winform 入门(1)之跨线程调用,程序说话
开发语言·c#·winform
KeithTsui1 小时前
GCC RISCV 后端 -- 控制流(Control Flow)的一些理解
linux·c语言·开发语言·c++·算法