LeetCode系列之二叉树

是计算机科学中较为常见的数据结构,它将数据节点以层级排列的方式组织在一起,是一种特殊的图。

通常来说树具有以下性质:

  1. 树通常有唯一的根节点;
  2. 除了根节点以外,所有的节点都有唯一的父节点;
  3. 没有子节点的节点称为叶子节点;
  4. 至少有一个子节点的非根节点称为内部节点;
  5. n 个节点组成的树,具有 n-1 条边;
  6. 将子节点独立于其父节点观察,子节点也具备树的性质(子树)

在计算机科学中通常从树的根节点开始,从上至下描绘树的层级结构。


本文主要介绍二叉树,因为它既简单又重要

树的每个节点最多只能有两个子节点

通常一个二叉树节点的定义如下,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 时,左子树必为空,而右子树由 23 组成,也就是 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),需要重复计算2f(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;
    }
};复制代码


转载于:https://juejin.im/post/5d07cf61518825684b589289

猜你喜欢

转载自blog.csdn.net/weixin_34245749/article/details/93167936
今日推荐