树的定义
树是一种抽象数据类型,用于表示具有层次关系的数据结构。它由节点(Node)和边(Edge)组成,具有以下特性:
-
层次结构:树的节点具有父子关系,形成层次结构。每个节点可以有零个或多个子节点,但只能有一个父节点(根节点除外)。或者说每个节点只能有一个直接前驱,但可以有0个或多个直接后继。
eg: 操作系统中的文件系统就是一种树形结构,目录是节点,文件是叶子节点。
cd ..表示返回上一级目录。cd dir表示进入子目录。 -
路径:从一个节点到另一个节点的边的序列称为路径(Path),路径长度是指路径中边的数量。
-
层数:根节点的层数定义为1;若某节点的父节点层数为k,则该节点的层数为k+1。
-
高度/深度:树的高度/深度是指所有节点的最大层数,不是边数。
-
子树:树的任意节点及其所有后代节点组成的树称为子树。
-
节点的度:节点的度是指该节点所有的子树的个数。
NOTE
和离散数学中节点度的定义不同,离散数学中节点的度是指与该节点直接相连的边的数量。
-
树的度:树的度是指树中节点的最大度。
-
根节点:树的最上层节点称为根节点(Root),没有父节点。
-
叶子节点:没有子节点的节点称为叶子节点(Leaf),也就是度为零的节点。
-
有序树:如果树中每个节点的子节点有一个固定的顺序(一般是从左到右),则称为有序树(Ordered Tree)。否则称为无序树(Unordered Tree)。
树的一个重要特例是二叉树,它的每个节点最多只能有两个子节点,分别称为左子节点和右子节点。
树的应用例子
表达式树:用于表示数学表达式的结构,节点表示操作符,叶子节点表示操作数。
eg: (a + b) * (c - d)
* / \ + - / \ / \ a b c d决策树:用于机器学习中的分类和回归任务,节点表示决策条件,叶子节点表示结果。
文件系统:操作系统中的文件和目录结构通常以树的形式组织,根目录是树的根节点,子目录和文件是子节点。
HTML DOM:网页的结构以树的形式表示,HTML元素是节点,嵌套关系表示父子关系。
二叉树
binary tree的定义
二叉树是每个节点最多有两个子节点的树结构,分别称为左子节点和右子节点。二叉树的节点度数最大为2,并且是有序树,用左右区分序。
斜树:只有左子树的二叉树称为左斜树,只有右子树的二叉树称为右斜树。
满二叉树:高度为 且有 个节点的二叉树称为满二叉树。每个分支节点都有两棵子树,且所有叶子节点都在最后一层。
完全二叉树:高度为 的二叉树,除第 层外,其余各层节点数均达到最大值,第 层所有节点都连续集中在最左边。
TIP深度为k的完全二叉树的前k-1层是满二叉树,第k层的节点从左到右依次编号为 。
二叉树的性质
-
深度节点数:深度为 的二叉树最多有 个节点,最少有 个节点;
TIP
每一层节点数是前一层的两倍,所以第i层最多有 个节点,等比数列求和公式
就得到了满二叉树节点数的公式
-
每一层节点数:第 层最多有 个节点,最少有1个节点。
-
叶子节点数:对于非空二叉树,叶子节点数 与度为2的节点数 满足关系 。
对于 节点的满二叉树,叶子节点数为 。
证明设二叉树中度为0的节点数为 ,度为1的节点数为 ,度为2的节点数为 ,总节点数为 ,则有:
每个节点有一条边指向它(根节点除外),所以边数 满足:
另一方面,边数也可以通过节点的度数来计算:
将 (2) 和 (3) 结合,得到:
将 (1) 代入 (4):
化简得到:
-
节点数和高度:具有n个节点的完全二叉树的高度为 或 。
证明设完全二叉树的高度为 ,则前 层是满二叉树,有 个节点,第 层有 个节点,,所以总节点数 满足
取对数得到
即
完全二叉树的顺序存储
把一个n个节点的二叉树编号,从顶向下,同一层从左至右,从1开始编号。
NOTE这里编号从1开始
完全二叉树的节点编号性质:
-
若i = 1, 则 i 是根结点,无父结点
-
若i > 1, 则 i 的父结点为
-
若 : 则 有左儿子且为 ; 否则 无左儿子
-
若 : 则 有右儿子且为 ;否则 无右儿子
-
若 为偶数, 且 : 则有右兄弟且为
-
若 为奇数, 且 :则有左兄弟且为
WARNING这个规律仅对完全二叉树成立
将完二叉树的节点依次存储在数组中。把编号 变成数组的下标 ,下标从1开始,空出0,构成顺序表。
一般二叉树的顺序存储
一般二叉树的下标之间没有任何规律,仅靠从顶向下,同一层从左至右编号不能体现树形结构。
用空节点补充成为完全二叉树再按完全二叉树的存储方式存。这时候数组里不存在的节点就对应占位元素(实际代码中和数据类型有关),出现空间浪费。
一般二叉树的空间浪费
-
规律:随节点数量指数增长
-
最坏:右单枝树,所有节点全是right child
节点的二叉树需要 个存储单元( 节点的右单支树有n层)
个节点的二叉树就要 亿 个存储单元
二叉树的链式存储
定义:每个节点由一个数据域和两个指针域组成,分别指向左子节点和右子节点。
对于任意二叉树, 个结点,就有 个指针,但只有 个指针指向了实际的节点(存在 条边)
struct BTreeNode { int data; BTreeNode* left; BTreeNode* right; TreeNode(int val) : data(val), left(nullptr), right(nullptr)}从父亲找孩子很容易,孩子找父亲麻烦。可以再加一个指针域指向父亲,这就是三叉链表
TIP空指针并非无用,在线索二叉树里可以作为线索指针。
二叉链表的建立
-
先序遍历建立二叉树
按照先序遍历的顺序输入节点值,遇到空节点输入特殊标记(如#),递归建立二叉树。#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;} -
索引式构建二叉链表
先创建所有节点对象,然后根据输入的左右孩子编号建立指针连接。
#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 Gvector<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):得到先序根序列
- 访问根节点,
- 遍历根节点的左子树,
- 遍历右子树。
特点:能确定根节点,第一个就是
void preorder(TreeNode* root) { if (!root) return; cout << root->data << " "; preorder(root->left); preorder(root->right);}中序遍历(Inorder Traversal):得到中序根序列
- 先遍历左子树,
- 访问根节点,
- 遍历右子树。
特点:不能确定根,但能确定左右子树
void inorder(TreeNode* root) { if (!root) return; inorder(root->left); cout << root->data << " "; inorder(root->right);}后序遍历(Postorder Traversal):得到后序根序列
- 遍历左子树
- 遍历右子树
- 访问根节点。其递归实现如下:
特点:能确定根节点,最后一个就是
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
三种遍历的性质:
-
三种遍历结果叶子节点的相对顺序是一致的(图中加粗部分)
-
每一个树对应唯一的遍历序列,但是反推则不一定(卡特蓝数)。
层序遍历(Level-order Traversal):按层次从上到下、从左到右访问节点。通常使用队列实现。
特点:能确定根节点,第一个就是
从遍历序列到二叉树
只给出四种遍历中的一个序列,不能唯一确定二叉树。
只给出先序中序后序的任意组合,也不能构造唯一二叉树。
思路:先序,后序,层序遍历可以确定根节点,中序遍历知道根就可以划分左右子树(结点)。先通过前三者确定根,在通过中序划分子树,对子树递归。
先序和前序确定根,最靠前的。
后序确定根:最靠后的。
中序确定左右:左边为左右边为右