HOWTO · C++

在 C++ 中实现二叉树数据结构

本文演示了如何用 C++ 实现二叉树数据结构。

本页内容

在 C++ 中,可以用包含一个值和两个子节点链接的 struct 表示二叉树。下面的示例会构建一棵小型二叉树,统计节点数,按边数计算树高,并通过中序遍历打印节点值。

使用节点 struct 表示二叉树

二叉树节点最多有两个子节点,通常称为 left 和 right。struct 是一种类类型,其成员默认公开,因此适合用于这种小型数据结构。每个节点存储一个整数,并通过 std::unique_ptr 拥有其子节点;空指针表示缺少对应的子节点链接。空指针本身并不意味着该节点是叶节点:左右两个子节点链接都为空的节点才是叶节点。

满二叉树的每个节点要么没有子节点,要么有两个子节点。完美二叉树既是满二叉树,也要求所有叶节点位于同一深度。下面构建的示例是满二叉树,但不是完全二叉树:最后一层前面有一个空位,后面才有节点。这些术语描述的是不同的树形,不能混为一谈。

构建一棵小型二叉树

程序会创建如下确定的树形,所选数值便于跟踪遍历过程:8 (left: 3, right: 10 (left: 9, right: 14))。

std::make_unique 会创建节点并返回拥有该节点的指针。将指针移动到子节点成员中会转移所有权,因此根节点拥有整棵树;根节点离开作用域时,所有节点都会自动释放。这样就不必像使用 new 进行裸分配时那样手动清理。

运行以下示例。由于代码块保持原样,其中的输出标签仍为英文:

#include <iostream>
#include <memory>

struct BinTreeNode {
    int data;
    std::unique_ptr<BinTreeNode> left;
    std::unique_ptr<BinTreeNode> right;

    explicit BinTreeNode(int value) : data(value) {}
};

int treeSize(const BinTreeNode* node) {
    if (node == nullptr) {
        return 0;
    }
    return 1 + treeSize(node->left.get()) + treeSize(node->right.get());
}

int treeHeight(const BinTreeNode* node) {
    if (node == nullptr) {
        return -1;
    }
    const int leftHeight = treeHeight(node->left.get());
    const int rightHeight = treeHeight(node->right.get());
    return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}

void printData(const BinTreeNode* node) {
    if (node == nullptr) {
        return;
    }
    printData(node->left.get());
    std::cout << node->data << ' ';
    printData(node->right.get());
}

int main() {
    auto root = std::make_unique<BinTreeNode>(8);
    root->left = std::make_unique<BinTreeNode>(3);
    root->right = std::make_unique<BinTreeNode>(10);
    root->right->left = std::make_unique<BinTreeNode>(9);
    root->right->right = std::make_unique<BinTreeNode>(14);

    std::cout << "size = " << treeSize(root.get()) << '\n';
    std::cout << "height = " << treeHeight(root.get()) << '\n';
    std::cout << "inorder = ";
    printData(root.get());
    std::cout << '\n';
}

这些函数接收节点指针,但不会取得所有权。用 root.get() 调用它们,可以让函数访问这棵树,同时仍由拥有对象的 unique_ptr 负责其生命周期。同样的递归定义适用于任意树形。

统计节点数并计算树高

treeSize 遇到缺失的子树时返回零;否则,它会统计当前节点,并加上左右子树的节点数。treeHeight 遇到缺失的子树时返回 -1,然后在较高的子树高度上加一。按照这种约定,树高是根到叶最长路径所包含的边数:叶节点的树高为 0,上面的示例树高为 2。有些定义统计的是节点数,因此叶节点的树高为 1;选择一种约定并始终保持一致。

使用中序遍历打印节点

printData 会先访问左子树,再打印当前值,最后访问右子树。这就是中序遍历;对于二叉搜索树,它会按排序顺序打印值,而对于一般二叉树,它只按这种遍历顺序访问节点。更多详情请参阅中序遍历指南。

要跟踪递归过程,可以观察函数在每个节点上的执行顺序:它先向左深入,直到没有子节点;返回父节点时打印值,然后处理右子树。因此,示例中的值会按 3 8 9 10 14 的顺序出现。这棵树有五个节点,按边数计算的树高为二。程序是一个完整的独立示例;你可以调整节点值的类型或添加操作,同时明确所有权管理方式和所采用的树高约定。