树是一种非线性的数据结构,用它能很好地描述有分支和层次特性的数据集合

树型结构在现实世界中广泛存在,比如组织关系图。
树型结构在计算机领域中也有广泛应用,如在编译系统中,用树表示源程序的语法结构。
在数据库系统中,树型结构是数据库层次模型的基础,也是各种索引和目录的主要组织形式。
在许多算法中,经常用树型结构描述问题的求解过程、所有解的状态和求解对策。

在树型结构中,二叉树是最常用的结构,分治个数确定,又可以为空,有良好的递归特性,特别适宜于程序设计。一般树会转换成二叉树进行处理。

======简单树======

树的定义及其相关概念

image.png
image.png
image.pngimage.png

树的父亲表示法,数组

image.png
image.png
image.png

  1. // 优缺点:利用了树中除根结点外每个结点都有唯一的父结点这个性质
  2. // 很容易找到树根,但找孩子时需要遍历整个线性表
  3. struct node{
  4. int data, parent;
  5. }tree[110];

二叉树的定义及其基本性质

二叉树(Binary Tree, BT)是一种特殊的树型结构,每个结点最多有两个子节点。

5种基本形态:

  • 空二叉树
  • 仅有根结点的二叉树
  • 右子树为空的二叉树
  • 左右子树均非空的二叉树
  • 左子树为空的二叉树

性质:

  • 在二叉树的第 i 层上最多有trees树(一) - 图8个结点
  • 深度为 depth 的二叉树,至多有trees树(一) - 图9个结点
  • 对任意一棵二叉树,如果叶子结点数为 trees树(一) - 图10,度为 2 的结点数为trees树(一) - 图11,一定满足trees树(一) - 图12

证明:trees树(一) - 图13,算两次

  • 具有 n 个结点的完全二叉树,深度为trees树(一) - 图14。注意,根的深度,有的定义是0,有的定义是1
  • 对于一棵具有 n 个结点的完全二叉树,对于任意一个结点,编号为 i

if (i > 1) 父结点编号 i / 2
if (2 i > n) 没有左儿子 else 左儿子编号为 2 i
if (2 i + 1 > n) 没有右儿子 else 右儿子编号为 2 i + 1

Catalan数:

  • 具有 n 个结点的不同形态的二叉树,有多少棵 ```cpp 一棵具有 n(n > 1) 个结点的二叉树

可以看成是, 由 一个根结点, 一棵具有 i 个结点的左子树, 一棵具有 n - i - 1个结点的右子树组成

i 的取值范围[0, n - 1]

  1. ![](https://cdn.nlark.com/yuque/__latex/05b433e6ac82dcfcdda48f7408b40c12.svg#card=math&code=%E5%85%AC%E5%BC%8F1%EF%BC%9AC_%7Bn%7D%20%3D%20%5Csum_%7Bi%3D0%7D%5E%7Bn-1%7DC_i%2AC_%7Bn-i-1%7D%EF%BC%8C%E5%85%B6%E4%B8%ADC_0%20%3D%201%2C%20C_1%20%3D%201&id=yx28a)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1559239/1630495797611-08d6f1fb-2f82-4f6e-8dcd-5fde9ec32467.png#clientId=u6ff515de-abf4-4&from=paste&height=718&id=uf19a29ee&margin=%5Bobject%20Object%5D&name=image.png&originHeight=718&originWidth=1686&originalType=binary&ratio=1&size=190881&status=done&style=none&taskId=ud53ceace-6324-4203-9d72-73dd4cbb5be&width=1686)
  2. ![](https://cdn.nlark.com/yuque/__latex/f1162e08dfa0edfa096647a8ab0ad5be.svg#card=math&code=%E5%85%AC%E5%BC%8F2%EF%BC%9AC_n%20%3D%20%5Cfrac%7B1%7D%7Bn%2B1%7DC_%7B2n%7D%5E%7Bn%7D&id=OcFEm)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1559239/1630495706224-cb83f12c-ede9-4c12-8197-f2a0aac653c5.png#clientId=u6ff515de-abf4-4&from=paste&height=372&id=ue53f6d5b&name=image.png&originHeight=372&originWidth=1698&originalType=binary&ratio=1&size=119311&status=done&style=none&taskId=u5cf972ac-6037-4a36-b62d-2201297653a&width=1698)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1559239/1630495717849-9f920eea-e010-499a-af8d-5a20547b77c1.png#clientId=u6ff515de-abf4-4&from=paste&height=606&id=u6161b9a8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=606&originWidth=1692&originalType=binary&ratio=1&size=160096&status=done&style=none&taskId=uea5142f1-9ce7-4aa8-b47f-c01cacd4f34&width=1692)
  3. <a name="TFOMj"></a>
  4. ## 二叉树的孩子表示法,链式存储结构+顺序存储结构
  5. ```cpp
  6. // 链式存储结构
  7. struct node{
  8. int data;
  9. node *lchild, *rchild; //左儿子,右儿子
  10. };
  11. node *root;
  12. // 顺序存储结构
  13. const int N = 110;
  14. int data[N];
  15. int lchild[N], rchild[N]; // int child[N][2]; 也可以这样写
  16. int root;

https://www.cnblogs.com/tarbitrary/p/4030652.html
image.png
image.png
image.png
image.png

二叉树的遍历:前序、中序、后序遍历,递归

image.png

性质:

  • 已知先序和中序,可以确定出二叉树
  • 已知中序和后序,可以确定出二叉树
  • 已知先序和后序,不可以确定出二叉树
  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. struct node{
  4. char data;
  5. node *lchild, *rchild;
  6. };
  7. node *root;
  8. // 按先序输入二叉树的结点,输入$表示空树
  9. void build(node* &p){
  10. char c;
  11. scanf("%c", &c);
  12. if (c == '$') p = NULL;
  13. else{
  14. p = new node();
  15. p->data = c;
  16. build(p->lchild); build(p->rchild);
  17. }
  18. }
  19. void pre_order(node *p){
  20. if (p == NULL) return;
  21. if (p->data != '$') cout << p->data;
  22. pre_order(p->lchild);
  23. pre_order(p->rchild);
  24. }
  25. void in_order(node *p){
  26. if (p == NULL) return;
  27. in_order(p->lchild);
  28. if (p->data != '$') cout << p->data;
  29. in_order(p->rchild);
  30. }
  31. void post_order(node *p){
  32. if (p == NULL) return;
  33. post_order(p->lchild);
  34. post_order(p->rchild);
  35. if (p->data != '$') cout << p->data;
  36. }
  37. int main(){
  38. build(root);
  39. pre_order(root);
  40. puts("");
  41. in_order(root);
  42. puts("");
  43. post_order(root);
  44. puts("");
  45. return 0;
  46. }
  47. /*
  48. 输入:AB$$C$$
  49. 输出:
  50. ABC
  51. BAC
  52. BCA
  53. */

======特殊树======

完全二叉树的定义与基本性质

image.png

完全二叉树的数组表示法

  • 对于一棵具有 n 个结点的完全二叉树,对于任意一个结点,编号为 i

if (i > 1) 父结点编号 i / 2
if (2 i > n) 没有左儿子 else 左儿子编号为 2 i
if (2 i + 1 > n) 没有右儿子 else 右儿子编号为 2 i + 1

image.png

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int N = 1010;
  4. char data[N];
  5. int node[N];
  6. int n;
  7. void pre_order(int i){
  8. if (data[i] == '$') return ;
  9. if (data[i] != '$') cout << data[i];
  10. if (i * 2 <= n) pre_order(i * 2);
  11. if (i * 2 + 1 <= n) pre_order(i * 2 + 1);
  12. }
  13. void in_order(int i){
  14. if (data[i] == '$') return ;
  15. if (i * 2 <= n) in_order(i * 2);
  16. if (data[i] != 'S') cout << data[i];
  17. if (i * 2 + 1 <= n) in_order(i * 2 + 1);
  18. }
  19. void post_order(int i){
  20. if (data[i] == '$') return ;
  21. if (i * 2 <= n) post_order(i * 2);
  22. if (i * 2 + 1 <= n) post_order(i * 2 + 1);
  23. if (data[i] != 'S') cout << data[i];
  24. }
  25. int main(){
  26. memset(data, '$', sizeof data);
  27. cin >> n; // 结点总数
  28. for (int i = 1; i <= n; i++) cin >> data[i]; // 根结点下标是1,从上到下,从左到右输入
  29. pre_order(1);
  30. puts("");
  31. in_order(1);
  32. puts("");
  33. post_order(1);
  34. puts("");
  35. return 0;
  36. }
  37. /*
  38. 输入:
  39. 3
  40. A B C
  41. 输出:
  42. ABC
  43. BAC
  44. BCA
  45. */

哈夫曼树的定义、构造及其遍历

定义

Huffman Tree, 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的WPL(带权路径)长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。

构造

在我们已有的权值(例子里的频次)中,每次都找到2个最小的,将它们构造成一颗二叉树,再插入我们已经构造好的二叉树中,将每个值都插完后,我们的哈夫曼树就构造完成了。
image.png
image.png
image.png
image.png
image.png
那我们的代码如何实现呢?如果我们要按如上的方法实现哈夫曼树,首先我们要找到的就是最小值,如何找到最小值一定是我们需要面对的最关键的问题。这里有2种方法:
1、先将所有的权值排序,如何再建立哈夫曼树。
2、先建立最小堆,每次从堆顶选元素来建立哈夫曼树。

遍历

二叉排序树的定义、构造及其遍历

定义

二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的节点。
image.png

构造

假设我们要为数组 a[] = {10 , 5 , 15 , 6 , 4 , 16 }构建一个二叉排序树,我们按顺序逐个插入元素
image.png
image.png

遍历

查找指定元素的节点(While循环):

  1. 判断当前结点是否为空指针,不是空指针进入
  2. key直接和当前结点value相等,return当前结点
  3. key大于当前结点value,将右子节点赋予当前结点
  4. key小于当前结点value,将左子节点赋予当前结点
  5. 直到key等于当前结点或者当前结点为空停止while循环
  1. // NOIP2013普及组初赛
  2. #include <iostream>
  3. using namespace std;
  4. const int SIZE = 100;
  5. const int INFINITE = 1000000;
  6. struct node{
  7. int left_child, right_child, value;
  8. }a[SIZE];
  9. int is_bst( int root, int lower_bound, int upper_bound ){
  10. int cur;
  11. if (root == 0) return 1;
  12. cur = a[root].value;
  13. if ((cur > lower_bound) && (cur < upper_bound) &&
  14. (is_bst(a[root].left_child, lower_bound, cur) == 1) &&
  15. (is_bst(a[root].right_child, cur, upper_bound) == 1) )
  16. return 1;
  17. return 0;
  18. }
  19. int main()
  20. {
  21. int i, n;
  22. cin >> n;
  23. for (i = 1; i <= n; i++)
  24. cin >> a[i].value >> a[i].left_child >> a[i].right_child;
  25. cout << is_bst(1, -INFINITE, INFINITE) << endl;
  26. return 0;
  27. }

====Handbook部分=====

概念

注:先学习图,再学习树。

A tree is a connected, acyclic graph that consists of n nodes and n − 1 edges. Removing any edge from a tree divides it into two components, and adding any edge to a tree creates a cycle. Moreover, there is always a unique path between any two nodes of a tree.(树是图的一部分,连通的,无环,有n个顶点,n-1条边)

The leaves of a tree are the nodes with degree 1, i.e., with only one neighbor.

In a rooted tree, one of the nodes is appointed the root of the tree, and all other nodes are placed underneath the root. For example, in the following tree, node 1 is the root node.
image.png
In a rooted tree, the children of a node are its lower neighbors, and the parent of a node is its upper neighbor. Each node has exactly one parent, except for the root that does not have a parent. For example, in the above tree, the children of node 2 are nodes 5 and 6, and its parent is node 1.

The structure of a rooted tree is recursive: each node of the tree acts as the root of a subtree that contains the node itself and all nodes that are in the subtrees of its children. For example, in the above tree, the subtree of node 2 consists of nodes 2, 5, 6 and 8:
image.png

Tree traversal树的遍历

General graph traversal algorithms can be used to traverse the nodes of a tree. However, the traversal of a tree is easier to implement than that of a general graph, because there are no cycles in the tree and it is not possible to reach a node from multiple directions.

The typical way to traverse a tree is to start a depth-first search at an arbitrary node. The following recursive function can be used:

  1. void dfs(int s, int e) { //e代表父亲节点
  2. // process node s
  3. for (auto u : adj[s]) {
  4. if (u != e) dfs(u, s);
  5. }
  6. }

The function is given two parameters: the current node s and the previous node e. The purpose of the parameter e is to make sure that the search only moves to nodes that have not been visited yet.

  1. //In the first call e = 0, because there is no previous node,
  2. //and it is allowed to proceed to any direction in the tree.
  3. dfs(x, 0);

Dynamic programming使用dp求子树大小

Dynamic programming can be used to calculate some information during a tree traversal. Using dynamic programming, we can, for example, calculate in O(n) time for each node of a rooted tree the number of nodes in its subtree or the length of the longest path from the node to a leaf.

As an example, let us calculate for each node s a value count[s]: the number of nodes in its subtree. The subtree contains the node itself and all nodes in the subtrees of its children, so we can calculate the number of nodes recursively using the following code:

  1. void dfs(int s, int e) {
  2. count[s] = 1;
  3. for (auto u : adj[s]) {
  4. if (u == e) continue;
  5. dfs(u, s);
  6. count[s] += count[u];
  7. }
  8. }

Diameter树的直径

The diameter of a tree is the maximum length of a path between two nodes. Forexample, consider the following tree:
image.png
Note that there may be several maximum-length paths. In the above path, we could replace node 6 with node 5 to obtain another path with length 4.

Next we will discuss two O(n) time algorithms for calculating the diameter of a tree. The first algorithm is based on dynamic programming, and the second algorithm uses two depth-first searches.

Algorithm 1

A general way to approach many tree problems is to first root the tree arbitrarily. After this, we can try to solve the problem separately for each subtree. Our first algorithm for calculating the diameter is based on this idea.

An important observation is that every path in a rooted tree has a highest point: the highest node that belongs to the path. Thus, we can calculate for each node the length of the longest path whose highest point is the node. One of those paths corresponds to the diameter of the tree.

For example, in the following tree, node 1 is the highest point on the path that corresponds to the diameter:
image.png
We calculate for each node x two values:

  • toLeaf(x): the maximum length of a path from x to any leaf
  • maxLength(x): the maximum length of a path whose highest point is x

For example, in the above tree, toLeaf(1) = 2, because there is a path 1 → 2 → 6, and maxLength(1)=4, because there is a path 6→2→1→4→7. In this case, maxLength(1) equals the diameter.

Dynamic programming can be used to calculate the above values for all nodes in O(n) time. First, to calculate toLeaf(x), we go through the children of x, choose a child c with maximum toLeaf(c) and add one to this value. Then, to calculate maxLength(x), we choose two distinct children a and b such that the sum toLeaf(a) + toLeaf(b) is maximum and add two to this sum.

  1. //类似floyd呢
  2. //还没实现过这种写法,要写一下

Algorithm 2 两次dfs

Another efficient way to calculate the diameter of a tree is based on two depth- first searches. First, we choose an arbitrary node a in the tree and find the farthest node b from a. Then, we find the farthest node c from b. The diameter of the tree is the distance between b and c.

In the following graph, a, b and c could be:
image.png
This is an elegant method, but why does it work?

It helps to draw the tree differently so that the path that corresponds to the diameter is horizontal, and all other nodes hang from it:
image.png
Node x indicates the place where the path from node a joins the path that corresponds to the diameter. The farthest node from a is node b, node c or some other node that is at least as far from node x. Thus, this node is always a valid choice for an endpoint of a path that corresponds to the diameter.

  1. //这个更好实现,就是跑两遍dfs
  2. #include <bits/stdc++.h>
  3. using namespace std;
  4. const int N = 100010;
  5. int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
  6. int dep[N];
  7. int T, n;
  8. int num, maxn;
  9. void add(int a, int b, int c)
  10. {
  11. e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
  12. }
  13. void dfs(int u, int fa)
  14. {
  15. if (dep[u] > maxn) maxn = dep[u], num = u;
  16. for (int i = h[u]; i != -1; i = ne[i])
  17. {
  18. int j = e[i];
  19. if (j == fa) continue;
  20. dep[j] = dep[u] + w[i];
  21. dfs(j, u);
  22. }
  23. }
  24. int main()
  25. {
  26. scanf("%d", &T);
  27. for (int id = 1; id <= T; id++)
  28. {
  29. memset(h, -1, sizeof h);
  30. idx = 0;
  31. scanf("%d", &n);
  32. for (int i = 0; i < n - 1; i++)
  33. {
  34. int a, b, w;
  35. scanf("%d%d%d", &a, &b, &w);
  36. a++, b++;
  37. add(a, b, w);
  38. add(b, a, w);
  39. }
  40. maxn = 0;
  41. memset(dep, 0, sizeof dep);
  42. dfs(1, 0);
  43. maxn = 0;
  44. memset(dep, 0, sizeof dep);
  45. dfs(num, 0);
  46. printf("Case %d: %d\n", id, maxn);
  47. }
  48. return 0;
  49. }

Binary trees二叉树

A binary tree is a rooted tree where each node has a left and right subtree. It is possible that a subtree of a node is empty. Thus, every node in a binary tree has zero, one or two children.

For example, the following tree is a binary tree:
image.png
The nodes of a binary tree have three natural orderings that correspond to different ways to recursively traverse the tree:

  • pre-order: first process the root, then traverse the left subtree, then traverse the right subtree
  • in-order: first traverse the left subtree, then process the root, then traverse the right subtree
  • post-order: first traverse the left subtree, then traverse the right subtree, then process the root

For the above tree, the nodes in pre-order are [1,2,4,5,6,3,7], in in-order [4,2,6,5,1,3,7] and in post-order [4,6,5,2,7,3,1].

If we know the pre-order and in-order of a tree, we can reconstruct the exact structure of the tree. For example, the above tree is the only possible tree with pre-order [1, 2, 4, 5, 6, 3, 7] and in-order [4, 2, 6, 5, 1, 3, 7]. In a similar way, the post-order and in-order also determine the structure of a tree.

However, the situation is different if we only know the pre-order and post- order of a tree. In this case, there may be more than one tree that match the orderings. For example, in both of the trees the pre-order is [1,2] and the post-order is [2,1], but the structures of the trees are different.
image.png

一本通中有很多针对二叉树的题目,都是使用the technique of recursion,NOIP真题中有多道类似的题目,需要多加练习。

《一本通》题目

【例3-1】找树根和孩子
  1. //fa[N]维护每个结点的父亲,如果一个节点没有fa,他就是根
  2. //son[N]维护孩子个数

【例3-2】单词查找树
  1. //trie树,可以高效的存储和查找字符串

【例3-3】医院设置
  1. //floyd维护出任意两点的距离
  2. //枚举医院的位置,更新res

【例3-4】求后序遍历
  1. //给出先序和中序,求后续
  2. void dfs(int l1, int r1, int l2, int r2)
  3. //根据先序l1位置的根,去找中序中根的位置

【例3-5】扩展二叉树
  1. //给出扩展二叉树的先序序列,输出中序和后序
  2. //题解给出的是指针写法【指针】

小球(drop)
  1. //起初所有节点都是false,访问这个点就会设置成相反状态
  2. //求第i个小球停在哪个叶子结点上
  3. //方法1,模拟1...i个小球的下落过程
  4. //方法2,根据左儿子pos*2,右儿子pos*2+1
  5. //开一个一维数组,维护结点的true和false

二叉树遍历(flist)
  1. //给出中序序列和按层序列,求先序序列
  2. //在按层遍历序列中先遇到根,在中序序列中,找到根的位置,break出来
  3. //进行递归处理
  4. void dfs(int l1, int r1, int l2, int r2)

FBI树(fbi)
  1. void dfs(int l, int r)
  2. {
  3. //边界
  4. //子问题
  5. int len = (r - l) / 2;
  6. dfs(l, l + len);
  7. dfs(l + len + 1, r);
  8. //按题意模拟操作
  9. }

二叉树输出(btout)
  1. //给出先序和中序,求凹入表示
  2. int dfs(int l1, int r1, int l2, int r2)
  3. //照样递归下去,dfs返回的是子树大小,用son[N]维护每个结点的子树大小

查找二叉树(tree_a)
  1. //题意是,中序遍历的顺序,值为target的结点是第几个被遍历到的

对称二叉树(tree_c)
  1. //思路1,从s[1]开始,两两为一单位,要么都是#,要么都不是#
  2. //思路2,根据左儿子和右儿子的位置,去判断左右儿子要么同时为#,要么同时不是#
  3. //两个思路,都需要在读入的字符串后面加上一个#,避免最后一个叶子结点没有兄弟的情况

合并果子(fruit)
  1. //小根堆

最小函数值(minval)
  1. //用一个大根堆,维护前m个较小值
  2. //如果堆的大小>=m,新读入的数字,比堆顶小,就更新维护进来

看病
  1. //priority_queue<node> q;自定义结构体使用优先队列

小明的账单
  1. //每天付最大账单和最小账单
  2. //使用multiset维护

鱼塘钓鱼(fishing)
  1. //这道题目的处理思路,在另外一个题目也遇到过
  2. //类似的操作是,把路程消耗先消耗掉,然后就可以看做随意跳来跳去的
  3. //枚举最远走到的池塘位置,先把路上消耗的时间先减去
  4. //然后就可以看做已经走过的池塘,可以任意流窜
  5. //把已经走过的池塘,每一分钟可以钓的鱼,维护到一个大根堆中
  6. //然后就变成,有限的时间,取有限次,每次都是取最大值(greedy)