通常来说树具有以下性质:
- 树通常有唯一的根节点;
- 除了根节点以外,所有的节点都有唯一的父节点;
- 没有子节点的节点称为叶子节点;
- 至少有一个子节点的非根节点称为内部节点;
- 由
n
个节点组成的树,具有n-1
条边; - 将子节点独立于其父节点观察,子节点也具备树的性质(子树)
在计算机科学中通常从树的根节点开始,从上至下描绘树的层级结构。
本文主要介绍二叉树,因为它既简单又重要
通常一个二叉树节点的定义如下,left
表示左子树根节点,right
表示右子树根节点,val
即是对应的值。
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
复制代码
对于二叉树,其遍历一般有三种方式:先序、中序和后序,它们主要区别在于访问节点的时机不同,具体伪代码如下:
def visit(node):
<< preorder actions >>
left_val = visit(node.left)
<< inorder actions >>
right_val = visit(node.right)
<< postorder actions >>
复制代码
二叉搜索树是一种特殊的二叉树,对于其每个节点 node
有以下性质:
node
的所有左子节点的值都小于node
node
的所有右子节点的值都大于node
LeetCode链接
Subtree of Another Tree
Given two non-empty binary trees s and t, check whether tree t has exactly the same structure and node values with a subtree of s. A subtree of s is a tree consists of a node in s and all of this node's descendants. The tree s could also be considered as a subtree of itself.
样例
Given tree s:
3
/ \
4 5
/ \
1 2
复制代码
Given tree t:
4
/ \
1 2
复制代码
Return true, because t has the same structure and node values with a subtree of s.
Given tree s:
3
/ \
4 5
/ \
1 2
/
0
复制代码
Given tree t:
4
/ \
1 2
复制代码
Return false.
题解
此题大意就是给你两棵树,判断第二棵树是不是第一棵树的子树,方法其实很直接:首先找出第一棵树值等于第二棵树根节点值的所有节点,然后再从这些子树节点(可能包含根节点)集合找出与第二棵树完全等价的树节点。因此本题就转换为,给定两个节点,判断两棵树是否相等。 因为二叉树的递归结构,我们可以很轻松地写出递归函数以判断两棵树是否全等:在给定两个节点非空情况下,左子树全等,右子树全等,根的值也相等;若两节点至少有一个为空,则全等的条件为两节点必须同时为空。
bool isSameTree(TreeNode *p, TreeNode *q) {
if (p && q) {
bool leftSame = isSameTree(p->left, q->left);
bool rightSame = isSameTree(p->right, q->right);
return leftSame && rightSame && p->val == q->val;
}
return p == NULL && q == NULL;
}
复制代码
主函数主要就是利用上述函数做层级搜索啦。
bool isSubtree(TreeNode* s, TreeNode* t) {
TreeNode *p;
queue<TreeNode*> q;
q.push(s);
vector<TreeNode*> targets;
while (!q.empty()) {
p = q.front();
q.pop();
if (p->val == t->val)
targets.push_back(p);
if (p->left)
q.push(p->left);
if (p->right)
q.push(p->right);
}
for (int i = 0; i < targets.size(); ++i) {
if (isSameTree(t, targets[i])) {
targets.clear();
return true;
}
}
return false;
}
复制代码
Diameter of Binary Tree
Given a binary tree, you need to compute the length of the diameter of the tree. The diameter of a binary tree is the length of the longest path between any two nodes in a tree. This path may or may not pass through the root.
样例
Given a binary tree
1
/ \
2 3
/ \
4 5
复制代码
Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].
The length of path between two nodes is represented by the number of edges between them.
题解
简单地说,这道题就是找出树中距离最远的两个顶点,并输出这个距离的长度。
刚开始我以为这道题,不就是简单地对左右子树的深度分别进行计算,求和后就找到结果了? 于是第一次提交我是这样写的:
int diameterOfBinaryTree(TreeNode *root) {
if (root == NULL)
return 0;
int ld = depth(root->left), rd = depth(root->right);
return ld + rd;
}
int depth(TreeNode *r) {
if (r == NULL)
return 0;
return max(depth(r->left), depth(r->right)) + 1;
}
int max(int a, int b) {
return a > b ? a : b;
}
复制代码
上述函数中,depth
用于求某棵根节点为r
的深度(令叶子节点深度为1
),那么最长路径就是左右子树的深度之和了,但是这样是错误的!因为这样就隐含了一个
:一棵树的最长路径一定经过它的根节点。 看一个例子:
1
/
2
/ \
4 5
/ \
6 7
复制代码
如上图所示,经过根节点1
的路径最长为3,然而这棵树的最长路径应该是[6, 4, 2, 5, 7],长度应为4。 于是我将上述代码改成如下,其实具体算法是一样的,只是这一次我遍历了所有的树节点:如果说上面的代码是查找经过根节点的最长路径长度,那么这里的代码就是将所有树节点的最长路径长度通通算出来,再选择最大的。
int diameterOfBinaryTree(TreeNode *root) {
if (root == NULL)
return 0;
int ld = diameterOfBinaryTree(root->left), rd = diameterOfBinaryTree(root->right);
return max(max(ld, rd), maxPath(root));
}
int maxPath(TreeNode *r) {
if (r == NULL)
return 0;
int ld = depth(r->left), rd = depth(r->right);
return ld + rd;
}
int depth(TreeNode *r) {
if (r == NULL)
return 0;
return max(depth(r->left), depth(r->right)) + 1;
}
int max(int a, int b) {
return a > b ? a : b;
}
复制代码
Unique Binary Search Trees
给出 n
,问由 1...n
为节点组成的不同的二叉查找树有多少种?
样例
给出 n = 3
,有 5
种不同形态的二叉查找树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
复制代码
题解
这道题我主要是通过找规律。令 n
为节点个数,f(n)
为由 1...n
为节点组成的不同的二叉查找树的形态个数。
n = 1
n = 1
的答案是肯定的,仅有一种形态,f(1) = 1
。
1
复制代码
n = 2
n = 2
的情况也很好理解,但这时候似乎还看不出规律,f(2) = 2
。
1 2
\ /
2 1
复制代码
n = 3
样例中的图给得不是很清楚,我稍微排列一下,按照根节点值大小从小到大排序:
1 1 2 3 3
\ \ / \ / /
3 2 1 3 2 1
/ \ / \
2 3 1 2
复制代码
1
时树的形态个数,是由
n = 2
的形态个数决定的;
2
时树的形态个数,是由
n = 1
的形态个数决定的;
3
时树的形态个数,是由
n = 2
的形态个数决定的。
为什么这样说呢,因为根节点为 1
时,左子树必为空,而右子树由 2
和 3
组成,也就是 n = 2
的情况;而根节点为 2
时,左右子树的规模(即节点数)均为 1
,也就是 n = 1
的情况。 同时也注意当根节点变化时,其左右子树的规模和形态变化。由二叉查找树的性质可知,左子树的所有节点的值小于根节点,右子树的所有节点的值大于根节点,又因为节点值范围在集合 {1, 2, ..., n}
内,所以当根节点确定时,左右子树的规模也确定了。 假定根节点值为 i
,则其左子树规模为i - 1
,右子树规模为n-i
。如上,n = 3, i = 1
时,左右子树规模分别为0和2,以此类推。
能不能就得出结论呢:
f(1) = 1
f(2) = 2
f(3) = f(2) + f(1) + f(2)
复制代码
最好看看 n = 4
的情况。
n = 4
由于此情况较为占篇幅,所以我会以比较抽象的方式表示,令 g(n)
表示 n
个节点组成的不同二叉树的形态集合,则有:
1 2 3 4
\ / \ / \ /
g(3) g(1) g(2) g(2) g(1) g(3)
复制代码
可以看到这里有一个很微妙的地方(根节点为2或3),就是二叉树的左右子树的形态组合,左子树与右子树的形态是相互独立的,因此固定根节点值的情况下,总的形态个数应该是两者形态数目的乘积。
f(1) = 1
f(2) = f(1) * f(0) + f(0) * f(1)
f(3) = f(2) * f(0) + f(1) * f(1) + f(0) * f(2)
f(4) = f(3) * f(0) + f(2) * f(1) + f(1) * f(2) + f(0) * f(3)
复制代码
从上式我们能看出,为了计算f(4)
,需要重复计算2
次f(1)、f(2)和f(3)
,而f(3)
又要重复调用f(1)和f(2)
,当n
相对比较大的时候,这样是很消耗计算时间的,因此在计算时需缓存计算结果。
为编程方便,定义 f(0) = 1
,则有:
class Solution {
public:
/**
* @paramn n: An integer
* @return: An integer
*/
int cache[1000];
Solution() {
for (int i = 0; i < 1000; ++i)
cache[i] = -1;
}
int numTrees(int n) {
// write your code here
if (cache[n] != -1)
return cache[n];
if (n == 0)
return cache[n] = 1;
if (n == 1 || n == 2)
return cache[n] = n;
int r = 0;
for (int i = 1; i <= n; ++i)
r += numTrees(n - i) * numTrees(i - 1);
return cache[n] = r;
}
};复制代码