树

exdoubled Lv3

无根树: 没有指定根节点的树,意味着它没有深度、高度、祖先、父子关系等概念

有根树: 有指定根节点的树

当有根树的有向边转化为无向边时,即为无根树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct TreeNode {
int val; // 节点值
TreeNode* left; // 左子节点
TreeNode* right; // 右子节点

TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

struct NaryTreeNode {
int val; // 节点值
std::vector<NaryTreeNode*> children; // 子节点列表

NaryTreeNode(int x) : val(x) {}
};

树的遍历

树的遍历是指按照某种顺序访问树中的所有节点。常见的树遍历方法有以下几种:

  • 前序遍历(先根遍历)(Pre-order Traversal):先访问根节点,然后依次访问左子树和右子树
  • 中序遍历(In-order Traversal):先访问左子树,然后访问根节点,最后访问右子树
  • 后序遍历(后根遍历)(Post-order Traversal):先访问左子树,然后访问右子树,最后访问根节点

递归的实现

前序遍历

1
2
3
4
5
6
void preOrder(TreeNode* root) {
if (root == nullptr) return;
visit(root); // 访问根节点
preOrder(root->left); // 递归遍历左子树
preOrder(root->right); // 递归遍历右子树
}

中序遍历

1
2
3
4
5
6
void inOrder(TreeNode* root) {
if (root == nullptr) return;
inOrder(root->left); // 递归遍历左子树
visit(root); // 访问根节点
inOrder(root->right); // 递归遍历右子树
}

后序遍历

1
2
3
4
5
6
void postOrder(TreeNode* root) {
if (root == nullptr) return;
postOrder(root->left); // 递归遍历左子树
postOrder(root->right); // 递归遍历右子树
visit(root); // 访问根节点
}

非递归的实现

前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
void preOrder(TreeNode* root) {
if (root == nullptr) return;
std::stack<TreeNode*> stack;
stack.push(root);
while (!stack.empty()) {
TreeNode* node = stack.top();
stack.pop();
visit(node); // 访问根节点
if (node->right) stack.push(node->right); // 先右后左
if (node->left) stack.push(node->left);
}
}

中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void inOrder(TreeNode* root) {
std::stack<TreeNode*> stack;
TreeNode* current = root;
while (current != nullptr || !stack.empty()) {
while (current != nullptr) {
stack.push(current);
current = current->left; // 一直向左走
}
current = stack.top();
stack.pop();
visit(current); // 访问根节点
current = current->right; // 转向右子树
}
}

后序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void postOrder(TreeNode* root) {
if (root == nullptr) return;
std::stack<TreeNode*> stack1, stack2;
stack1.push(root);
while (!stack1.empty()) {
TreeNode* node = stack1.top();
stack1.pop();
stack2.push(node);
if (node->left) stack1.push(node->left); // 先左后右
if (node->right) stack1.push(node->right);
}
while (!stack2.empty()) {
visit(stack2.top()); // 访问根节点
stack2.pop();
}
}

前缀表达式就是前序遍历,中缀表达式就是中序遍历,后缀表达式就是后序遍历

树的存储

等长链表表示法

根据树的度为每个节点分配固定数量的指针域

优点:简单,管理容易

缺点:浪费空间,灵活性差

注意,由于分配的内存空间是固定的,所以占用空间只与节点总数有关,而与树的结构无关

1
2
3
4
5
6
7
8
9
10
struct TreeNode {
int val; // 节点值
TreeNode* children[MaxDegree]; // 固定数量的子节点指针

TreeNode(int x) : val(x) {
for (int i = 0; i < MaxDegree; ++i) {
children[i] = nullptr;
}
}
};

变长链表表示法

采用边长链表存储每个节点的子节点

和上面类似,但是有几个指针域就有几个分叉,不会存在空指针域

优点:避免了定长链表的空间浪费

缺点:需要额外管理子女节点链表

1
2
3
4
5
6
struct TreeNode {
int val; // 节点值
std::list<TreeNode*> children; // 子节点列表

TreeNode(int x) : val(x) {}
};

左儿子-右兄弟表示法

每个节点包含两个指针:一个指向其第一个子节点(左儿子),另一个指向其下一个兄弟节点(右兄弟)

这是一种将多叉树转化为二叉树的表示方法

由于二叉树的度数为则可以节省空间

优点:节省空间,适用于多叉树

缺点:访问子节点和兄弟节点需要额外的指针跳转

1
2
3
4
5
6
7
struct TreeNode {
int val; // 节点值
TreeNode* leftChild; // 指向第一个子节点
TreeNode* rightSibling; // 指向下一个兄弟节点

TreeNode(int x) : val(x), leftChild(nullptr), rightSibling(nullptr) {}
};

进行多叉树到二叉树的转化后

多叉树的先根遍历为二叉树的前序遍历

多叉树的后根遍历为二叉树的中序遍历

双亲表示法

每个节点包含一个指向其父节点的指针

并查集是一种典型的双亲表示法应用

优点:便于访问父节点

缺点:不便于访问子节点,空间浪费

1
2
3
4
5
6
struct TreeNode {
int val; // 节点值
TreeNode* parent; // 指向父节点

TreeNode(int x) : val(x), parent(nullptr) {}
};

数组的实现

1
2
int data[MAX_N]; // 存储节点值
int parent[MAX_N]; // 存储父节点索引,指向自己表示根节点

层号表示法

按照前序写出树中全部结点,在节点前带上节点的层号

也就是层序遍历+前序遍历唯一确定一颗树

1
2
3
4
5
6
struct TreeNode {
int val; // 节点值
int level; // 节点层号

TreeNode(int x, int l) : val(x), level(l) {}
};

广义表表示法

根节点表示广义表的表头

广义表表示需要特别强调左右子树的区别

通过树构建广义表

  • 深度优先遍历整棵树
  • 进入节点时,添加,即将节点压入序列
  • 如果有子节点
    • 左括号压入序列
    • 递归遍历节点的每个子节点
    • 右括号压入序列
  • 退出节点

广义表构建树

  • 依次从保存广义表的字符串中输入字符
  • 若是字母则表示结点值,建立新结点,作为左子 女或右子女链接到其父结点上
  • 左括号则代表子树开始,,右括号代表子树结束
  • 遇到逗号则代表左子女为根的子树处理完毕,
  • 使用一个栈,在进入子树前将根节点指针进栈,处理完子树后出栈,继续处理根节点的其他子女

仅保留广义表表示中的括号

左括号用替代,右括号用替代

得到一个序列,称为欧拉序列

任意一对之间的元素构成以为根节点的子树的欧拉序列

霍夫曼树和霍夫曼编码

霍夫曼树(Huffman Tree)是一种带权路径长度最短的二叉树,常用于数据压缩中的霍夫曼编码

带权路径长度(WPL, Weighted Path Length)是指树中所有叶子节点的权值与其到根节点路径长度的乘积之和

霍夫曼编码(Huffman Coding)是一种无损数据压缩算法,通过构建霍夫曼树为每个符号分配可变长度的二进制编码,频率高的符号分配较短的编码,频率低的符号分配较长的编码,从而实现数据压缩

构建霍夫曼树的步骤:

  1. 初始化:将每个符号及其对应的权值作为一个独立的节点,放入一个优先队列(最小堆)中
  2. 构建树:重复以下步骤直到队列中只剩下一个节点为止:
  • 从队列中取出两个权值最小的节点,作为新节点的左子节点和右子节点
  • 创建一个新节点,其权值为两个子节点权值之和
  • 将新节点插入队列中
  1. 生成编码:从根节点开始,为每个左子节点分配一个 ‘0’,为每个右子节点分配一个 ‘1’,直到到达叶子节点,生成对应的霍夫曼编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void HuffmanCoding(const std::vector<int>& weights) {
struct Node {
int weight;
Node* left;
Node* right;
Node(int w) : weight(w), left(nullptr), right(nullptr) {}
};

auto cmp = [](Node* a, Node* b) { return a->weight > b->weight; };
std::priority_queue<Node*, std::vector<Node*>, decltype(cmp)> minHeap(cmp);

for (int w : weights) {
minHeap.push(new Node(w));
}

while (minHeap.size() > 1) {
Node* left = minHeap.top(); minHeap.pop();
Node* right = minHeap.top(); minHeap.pop();
Node* parent = new Node(left->weight + right->weight);
parent->left = left;
parent->right = right;
minHeap.push(parent);
}

Node* root = minHeap.top();
generateCodes(root, "", codes);
}

void generateCodes(Node* node, const std::string& code, std::unordered_map<char, std::string>& codes) {
if (!node) return;
if (!node->left && !node->right) {
codes[node->symbol] = code; // 叶子节点,存储编码
}
generateCodes(node->left, code + "0", codes);
generateCodes(node->right, code + "1", codes);
}

为什么霍夫曼编码的贪心选择过程是正确的?

可以通过交换论证法证明霍夫曼编码的贪心选择过程是正确的

假设我们有一个最优树,但它没有选择当前权值最小的两个节点进行合并

可以通过交换这两个节点与当前选择的节点来构造一个新的树,证明新树的总权值路径长度不会增加

从而说明选择当前权值最小的两个节点是正确的

霍夫曼编码是一种最优的前缀编码,但不是最优的熵编码,一般作为压缩算法的一个环节出现

算术编码是一种最优的熵编码,但也更复杂

多叉霍夫曼树

多叉霍夫曼树是霍夫曼树的一种扩展,允许每个节点有多个子节点,而不仅限于二叉树的两个子节点

构建多叉霍夫曼树的步骤类似于二叉霍夫曼树,但在每次合并节点时,可以选择多个权值最小的节点进行合并,具体步骤与二叉的有些许不同,需要补充节点使得节点数满足的倍数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void buildDaryHuffmanTree(const std::vector<int>& weights, int d) {
struct Node {
int weight;
std::vector<Node*> children;
Node(int w) : weight(w) {}
};

auto cmp = [](Node* a, Node* b) { return a->weight > b->weight; };
std::priority_queue<Node*, std::vector<Node*>, decltype(cmp)> minHeap(cmp);

for (int w : weights) {
minHeap.push(new Node(w));
}

// 补充0节点
int n = weights.size();
int remainder = (d - 1 - (n - 1) % (d - 1)) % (d - 1);
for (int i = 0; i < remainder; ++i) {
minHeap.push(new Node(0));
}

while (minHeap.size() > 1) {
Node* parent = new Node(0);
for (int i = 0; i < d; ++i) {
if (minHeap.empty()) break;
Node* child = minHeap.top(); minHeap.pop();
parent->children.push_back(child);
parent->weight += child->weight;
}
minHeap.push(parent);
}

Node* root = minHeap.top();
// 生成编码的过程类似于二叉霍夫曼树
}

树的直径

树的同构

无根树的同构等价于图同构

树的重心

LCA 最近公共祖先

朴素算法

倍增算法

欧拉序列

稀疏表

  • Title:
  • Author: exdoubled
  • Created at : 2025-11-05 15:00:00
  • Updated at : 2025-11-12 15:26:50
  • Link: https://github.com/exdoubled/exdoubled.github.io.git/sjjg/sjjg06/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments