2778 字
14 分钟
二叉树的概念和存储

树的定义#

树是一种抽象数据类型,用于表示具有层次关系的数据结构。它由节点(Node)和边(Edge)组成,具有以下特性:

  1. 层次结构:树的节点具有父子关系,形成层次结构。每个节点可以有零个或多个子节点,但只能有一个父节点(根节点除外)。或者说每个节点只能有一个直接前驱,但可以有0个或多个直接后继。

    eg: 操作系统中的文件系统就是一种树形结构,目录是节点,文件是叶子节点。cd ..表示返回上一级目录。cd dir表示进入子目录。

  2. 路径:从一个节点到另一个节点的边的序列称为路径(Path),路径长度是指路径中边的数量。

  3. 层数:根节点的层数定义为1;若某节点的父节点层数为k,则该节点的层数为k+1。

  4. 高度/深度:树的高度/深度是指所有节点的最大层数,不是边数。

  5. 子树:树的任意节点及其所有后代节点组成的树称为子树。

  6. 节点的度:节点的度是指该节点所有的子树的个数。

    NOTE

    和离散数学中节点度的定义不同,离散数学中节点的度是指与该节点直接相连的边的数量。

  7. 树的度:树的度是指树中节点的最大度。

  8. 根节点:树的最上层节点称为根节点(Root),没有父节点。

  9. 叶子节点:没有子节点的节点称为叶子节点(Leaf),也就是度为零的节点。

  10. 有序树:如果树中每个节点的子节点有一个固定的顺序(一般是从左到右),则称为有序树(Ordered Tree)。否则称为无序树(Unordered Tree)。

树的一个重要特例是二叉树,它的每个节点最多只能有两个子节点,分别称为左子节点和右子节点。

树的应用例子#

表达式树:用于表示数学表达式的结构,节点表示操作符,叶子节点表示操作数。

eg: (a + b) * (c - d)

*
/ \
+ -
/ \ / \
a b c d

决策树:用于机器学习中的分类和回归任务,节点表示决策条件,叶子节点表示结果。

文件系统:操作系统中的文件和目录结构通常以树的形式组织,根目录是树的根节点,子目录和文件是子节点。

HTML DOM:网页的结构以树的形式表示,HTML元素是节点,嵌套关系表示父子关系。

二叉树#

binary tree的定义#

二叉树是每个节点最多有两个子节点的树结构,分别称为左子节点和右子节点。二叉树的节点度数最大为2,并且是有序树,用左右区分序。

斜树:只有左子树的二叉树称为左斜树,只有右子树的二叉树称为右斜树。

满二叉树:高度为 KK 且有 2K12^K-1 个节点的二叉树称为满二叉树。每个分支节点都有两棵子树,且所有叶子节点都在最后一层。

完全二叉树:高度为 KK 的二叉树,除第 KK 层外,其余各层节点数均达到最大值,第 KK 层所有节点都连续集中在最左边。

TIP

深度为k的完全二叉树前k-1层满二叉树,第k层的节点从左到右依次编号为 1,2,...,m(m<=2(k1))1,2,...,m(m<=2^{(k-1)})

二叉树的性质#

  1. 深度节点数:深度为 KK 的二叉树最多有 2K12^K-1 个节点,最少有 KK 个节点;

    TIP

    每一层节点数是前一层的两倍,所以第i层最多有 2i12^{i-1} 个节点,等比数列求和公式

    i=0n12i=2n1\sum_{i=0}^{n-1}2^i=2^n-1

    就得到了满二叉树节点数的公式

  2. 每一层节点数:第 ii 层最多有 2i12^{i-1} 个节点,最少有1个节点。

  3. 叶子节点数:对于非空二叉树,叶子节点数 n0n_0 与度为2的节点数 n2n_2 满足关系 n0=n2+1n_0=n_2+1

    对于 NN 节点的满二叉树,叶子节点数为 (N+1)/2(N+1)/2

    证明

    设二叉树中度为0的节点数为 n0n_0,度为1的节点数为 n1n_1,度为2的节点数为 n2n_2,总节点数为 NN,则有:

    N=n0+n1+n2(1)N=n_0+n_1+n_2 \tag{1}

    每个节点有一条边指向它(根节点除外),所以边数 EE 满足:

    E=N1(2)E=N-1 \tag{2}

    另一方面,边数也可以通过节点的度数来计算:

    E=n1+2n2(3)E=n_1+2n_2 \tag{3}

    将 (2) 和 (3) 结合,得到:

    N1=n1+2n2(4)N-1=n_1+2n_2 \tag{4}

    将 (1) 代入 (4):

    (n0+n1+n2)1=n1+2n2(n_0+n_1+n_2)-1=n_1+2n_2

    化简得到:

    n0=n2+1n_0=n_2+1
  4. 节点数和高度:具有n个节点的完全二叉树的高度为 log2(n+1)\lceil log_2(n+1)\rceillog2n+1\lfloor log_2n \rfloor +1

    证明

    设完全二叉树的高度为 hh,则前 h1h-1 层是满二叉树,有 2h112^{h-1}-1 个节点,第 hh 层有 mm 个节点,1m2h11\leq m\leq 2^{h-1},所以总节点数 nn 满足

    2h1n2h12^{h-1}\leq n\leq 2^h-1

    取对数得到

    h1log2n<hh-1\leq log_2n<h

    h=log2(n+1)=log2n+1h=\lceil log_2(n+1)\rceil=\lfloor log_2n \rfloor +1

完全二叉树的顺序存储#

把一个n个节点的二叉树编号,从顶向下,同一层从左至右,从1开始编号

NOTE

这里编号从1开始

完全二叉树的节点编号性质:

  • 若i = 1, 则 i 是根结点,无父结点

  • 若i > 1, 则 i 的父结点为 i2\lfloor \dfrac{i}{2} \rfloor

  • 2in2i\leq n : 则 ii 有左儿子且为 2i2i ; 否则 ii 无左儿子

  • 2i+1n2i+1\leq n : 则 ii 有右儿子且为 2i+12i+1 ;否则 ii 无右儿子

  • ii 为偶数, 且 i<ni < n : 则有右兄弟且为 i+1i + 1

  • ii 为奇数, 且 i<n  ,i1i < n \;, i \neq 1 :则有左兄弟且为 i1i-1

WARNING

这个规律仅对完全二叉树成立

将完二叉树的节点依次存储在数组中。把编号 ii 变成数组的下标 ii ,下标从1开始,空出0,构成顺序表。

一般二叉树的顺序存储#

一般二叉树的下标之间没有任何规律,仅靠从顶向下,同一层从左至右编号不能体现树形结构。

用空节点补充成为完全二叉树再按完全二叉树的存储方式存。这时候数组里不存在的节点就对应占位元素(实际代码中和数据类型有关),出现空间浪费

一般二叉树的空间浪费#

  • 规律:随节点数量指数增长

  • 最坏:右单枝树,所有节点全是right child

nn 节点的二叉树需要 2n12^n-1 个存储单元(nn 节点的右单支树有n层)

3131 个节点的二叉树就要 2121亿 个存储单元

二叉树的链式存储#

定义:每个节点由一个数据域和两个指针域组成,分别指向左子节点和右子节点。

对于任意二叉树,nn 个结点,就有 2n2n 个指针,但只有 n1n-1 个指针指向了实际的节点(存在 n1n-1 条边)

空指针的个数=节点数+1\text{空指针的个数}=\text{节点数}+1
struct BTreeNode {
int data;
BTreeNode* left;
BTreeNode* right;
TreeNode(int val) : data(val), left(nullptr), right(nullptr)
}

从父亲找孩子很容易,孩子找父亲麻烦。可以再加一个指针域指向父亲,这就是三叉链表

TIP

空指针并非无用,在线索二叉树里可以作为线索指针。

二叉链表的建立#

  1. 先序遍历建立二叉树
    按照先序遍历的顺序输入节点值,遇到空节点输入特殊标记(如#),递归建立二叉树。

    #include <iostream>
    #include <sstream>
    using namespace std;
    TreeNode* createTree() {
    string val;
    if (!(cin >> val)) return nullptr;
    if (val == "#") return nullptr;
    TreeNode* node = new TreeNode(stoi(val));
    node->left = createTree();
    node->right = createTree();
    return node;
    }
  2. 索引式构建二叉链表

    先创建所有节点对象,然后根据输入的左右孩子编号建立指针连接。

    #include <iostream>
    #include <vector>
    #include <string>
    using namespace std;
    struct TreeNode {
    char data;
    TreeNode* left;
    TreeNode* right;
    TreeNode(char val) : data(val), left(nullptr), right(nullptr) {}
    };
    // 每个节点的输入信息
    struct NodeInfo {
    char data;
    int leftIndex;
    int rightIndex;
    };
    TreeNode* buildTree(const vector<NodeInfo>& input) {
    int n = input.size();
    vector<TreeNode*> nodes(n);
    // 第一步:创建所有节点对象
    for (int i = 0; i < n; ++i) {
    nodes[i] = new TreeNode(input[i].data);
    }
    // 第二步:建立左右孩子指针连接
    for (int i = 0; i < n; ++i) {
    int l = input[i].leftIndex;
    int r = input[i].rightIndex;
    if (l != 0) nodes[i]->left = nodes[l];
    if (r != 0) nodes[i]->right = nodes[r];
    }
    return nodes[0]; // 返回根节点
    }
    // 前序遍历用于验证构建结果
    void preorder(TreeNode* root) {
    if (!root) return;
    cout << root->data << " ";
    preorder(root->left);
    preorder(root->right);
    }
    int main() {
    // 示例输入:构建如下结构
    // A
    // / \
    // B C
    // / \ / \
    // D E F G
    vector<NodeInfo> input = {
    {'A', 1, 2},
    {'B', 3, 4},
    {'C', 5, 6},
    {'D', 0, 0},
    {'E', 0, 0},
    {'F', 0, 0},
    {'G', 0, 0}
    };
    TreeNode* root = buildTree(input);
    cout << "前序遍历结果: ";
    preorder(root);
    cout << endl;
    return 0;
    }

二叉树的遍历#

遍历是指按照某种顺序访问二叉树的所有节点,要求不重不漏。常见的遍历方式有三种:先序遍历、中序遍历和后序遍历。(先中后描述的是根节点访问的位置)

先序遍历(Preorder Traversal):得到先序根序列

  • 访问根节点,
  • 遍历根节点的左子树,
  • 遍历右子树。
根左右\boxed{\text{根左右}}

特点:能确定根节点,第一个就是

void preorder(TreeNode* root) {
if (!root) return;
cout << root->data << " ";
preorder(root->left);
preorder(root->right);
}

中序遍历(Inorder Traversal):得到中序根序列

  • 先遍历左子树,
  • 访问根节点,
  • 遍历右子树。
左根右\boxed{\text{左根右}}

特点:不能确定根,但能确定左右子树

void inorder(TreeNode* root) {
if (!root) return;
inorder(root->left);
cout << root->data << " ";
inorder(root->right);
}

后序遍历(Postorder Traversal):得到后序根序列

  • 遍历左子树
  • 遍历右子树
  • 访问根节点。其递归实现如下:
左右根\boxed{\text{左右根}}

特点:能确定根节点,最后一个就是

void postorder(TreeNode* root) {
if (!root) return;
postorder(root->left);
postorder(root->right);
cout << root->data << " ";
}

例如,给定如下二叉树:

A
/ \
B C
/ \ / \
D E F G
  • 先序遍历结果:A B D E C F G
  • 中序遍历结果:D B E A F C G
  • 后序遍历结果:D E B F C G A

三种遍历的性质:

  1. 三种遍历结果叶子节点的相对顺序是一致的(图中加粗部分)

  2. 每一个树对应唯一的遍历序列,但是反推则不一定(卡特蓝数)。

层序遍历(Level-order Traversal):按层次从上到下、从左到右访问节点。通常使用队列实现。

特点:能确定根节点,第一个就是

从遍历序列到二叉树#

只给出四种遍历中的一个序列,不能唯一确定二叉树。

只给出先序中序后序的任意组合,也不能构造唯一二叉树。

思路:先序,后序,层序遍历可以确定根节点,中序遍历知道根就可以划分左右子树(结点)。先通过前三者确定根,在通过中序划分子树,对子树递归。

先序和前序确定根,最靠前的。

后序确定根:最靠后的。

中序确定左右:左边为左右边为右

二叉树的概念和存储
https://biscuit0613.github.io/posts/algorithm/alg_bitree/
作者
Biscuit
发布于
2025-09-26
许可协议
CC BY-NC-SA 4.0