【LeetCode】二叉树刷题总结(二)96、101、114、543、654

内代码已上传GitHub:点击我 去GitHub查看代码
写在前面: 虽然说是刷题总结,但是其实只是再回顾一下自己的解题过程,题目之前没有什么关联。

自然界中的二叉树
【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第1张图片
堇紫珊瑚菌 --- 图片来源网络

96. 不同的二叉搜索树

【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第2张图片

这道题主要考察组合的知识,来看看思路:

  • 首先再次明确二叉树的结构 , 由 根节点、左右子树构成

  • 设n个节点组成的二叉搜索数有 nums(n) 种。

  • 假如 根节点是1 ,那么我们不需要考虑左子树了,左子树种数为nums(0) = 1(此时为空),右子树的种数为 nums(n - 1)

  • 如果 根节点是2 , 那么左子树种数为 nums(1) = 1(此时非空),右子树的种数为 nums(n - 2)

  • 写到这相信大家已经总结出规律了:
    nums(n) = nums(0) * nums(n - 1) + nums(1) * nums(n - 2) + ...+ nums(n - 1) * nums(0)

接下来就是很经典的动态规划解题过程了,用一个数组保存中间过程解,代码如下:时间复杂度O(n^2),空间复杂度O(n)

// 动态规划 时间复杂度O(n*n),空间复杂度O(n)
int numTrees(int n){
    int* nums = (int*)malloc(sizeof(int) * (n + 1));
    memset(nums, 0, sizeof(int) * (n + 1));
    nums[0] = nums[1] = 1;
    
    for(int i = 2; i <= n; ++i){
        for(int j = 1; j <= i; ++j){
            nums[i] += nums[j - 1] * nums[i - j];
        }
    }
    return nums[n];
}

注意空树也是树,所以别忘了 nums[0] 哦,以及nums的大小应该为 n + 1

到这以为就完了嘛?往下看!!


原来,刚刚我们发现的规律是卡塔兰数。 重点看以下的另类递推式!
【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第3张图片
均来源于百度百科

h(n) = h(n - 1) * (4 * n - 2) / (n + 1) , 就是这个式子! 一下子把我们刚刚的 O(n^2) 时间复杂度降到了线性时间复杂度O(n),所以一定要好好学数学呀!!代码如下:

// 数学方法 时间复杂度O(n) 空间复杂度O(1)
// 卡塔兰数 nums[n] = nums[n - 1] * 2*(2n + 1) / (n + 2)
int numTrees(int n){
    // 乘法过程会溢出,所以先使用long存储
    long num = 1;
    // 一层循环
    for(int i = 0; i < n; ++i){
        num = num * 2 * (2 * i + 1) / (i + 2);
    }
    return (int)num;
}

大家可能会疑惑为什么要用long 而不是和动态规划一样用 int 存储。这里即使题目能确保num始终在合法范围内,但是num * 2 * (2 * i + 1)在计算时可能超出int范围造成溢出。要知道计算机是不会直接计算出最后的答案的,在表达式计算的过程中,需要用 与表达式各组成成分最大类型 相同的中间变量来存储。所以这里先用上 long,到计算完毕返回时强制转换为int返回,符合题目需求。

101. 对称二叉树

【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第4张图片

这里的镜像其实就是根节点的左右子树镜像对称,所以很容易就能想到:

  • 对左右子树同时进行遍历,比较他们镜面对称节点的值,不相等的话返回false。

  • 需要比较的是 左子树的左节点和右子树的右节点、左子树的右节点和右子树的左节点

  • 所以递归的过程其实像 扒皮...... , 就是把树皮一层一层扒掉, 如果发现扒掉的树皮不是对称的,那么这棵树就不是镜像对称的...(假装这里有分析图...反正就是扒树皮...)

来看看代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

bool judge(struct TreeNode* lnode, struct TreeNode* rnode){
    // 不同时为空, 说明不对称,返回false
    if(lnode == NULL ^ rnode == NULL){
        return false;
    // 同时为空, 对称, 返回true
    }else if(lnode == NULL){
        return true;
    }
    // 同时不为空, 判定值是否相等且judge2次完成所有镜像的判断
    if(lnode -> val != rnode -> val || !judge(lnode -> left, rnode -> right) || !judge(lnode -> right, rnode -> left))return false;
    // 当前搜索2棵子树镜像对称,返回true
    return true;
}

// 递归方法
bool isSymmetric(struct TreeNode* root){
    // root 为空 , 直接返回true
    return root ? judge(root -> left, root -> right) : true;
}

注意点都写注释里了。
小编突然发现还没用迭代做过...假装看不到题目的暗示...溜了溜了

114. 二叉树展开为链表

【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第5张图片

题目中强调了 原地 两个字,意思就是在原有树结构上进行修改,不使用新的空间存储链表。展开顺序按照先序遍历顺序来,看上去不会太难,通过树的右节点把他们一个一个按顺序接起来就行了,看思路:(类似后序遍历)

【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第6张图片
  • 首先我们既然要尝试展开,就来个比较小的。 把上图树结构转为 链表 [1,2,5]的步骤可以是:

  • 先找到右子树,右子树的right节点置空,用一个 指针变量p 保存右节点。

  • 再找到左子树,左子树的right节点 = p, 再用p保存左节点

  • 最后找到根节点, 根节点的right节点 = p,展开完毕

上图以及所示思想是基于这个较小的基本结构的,这时候就可以使用递归来实现我们以上的思想:
先右子树,再左子树,后根节点 类似于后序遍历

// 最小化递归问题
void MinTri(struct TreeNode* root, struct TreeNode** now_r){
    // 遍历到树叶返回
    if(root == NULL) return;
    // 后序遍历??
    // 不对, 是先遍历右子树的后序遍历
    MinTri(root -> right, now_r);
    MinTri(root -> left, now_r);
    // 更新右节点
    root -> right = *now_r;
    // 左节点置空
    root -> left = NULL;
    // 返回上一层前保存当前节点
    *now_r = root;
}
// 二叉树展开为链表
void flatten(struct TreeNode* root){
    // 记录节点,同时也是当前父节点的右子节点
    struct TreeNode* now_r = NULL;
    // flatten
    MinTri(root, &now_r);
}

然而做到这就结束了? NO,递归解法总是无法超越100%
虽然 迭代是人,递归是神 , 但是迭代往往有时候比递归更难实现,且少了递归栈调用后更有价值。来看看迭代解法:

  • 前面说的都是废话,这题的迭代很容易实现....

  • 从根节点起,逐个遍历右节点,如果当前节点有左节点,那么找到该左子树的最右节点,然后把左子树接到右边去...

  • 过程中需要保存左子树,便于衔接,具体看代码:

// 尝试迭代解法
void flatten(struct TreeNode* root){
    // last为左子树最右节点
    struct TreeNode* p = root, * last;
    while(p != NULL){
        // 如果p节点有左子树, 取下来接右边去...
        if(p -> left){
            last = p -> left;
            while(last -> right) last = last -> right;
            last -> right = p -> right;
            p -> right = p -> left;
            p -> left = NULL;
        }
        // 右移
        p = p -> right;
    }
}

注意要将遍历过的左子树置空

543. 二叉树的直径

【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第7张图片

做着题时马上就想到了110题 平衡二叉树 ,当时这道题计算子树的高度时通过分别计算左子树和右子树的高度,取其最大值。而这题呢?

  • 这题的要求是求最大路径,且这个路径可能不经过根节点。

  • 这时我们只需要把求得每个节点左右子树的高度加起来,遍历过一遍后即可得到左右子树高度和的最大值,也就是最大路径。

110题
【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第8张图片

本题题解:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */

// dfs 记录 l r深度
int search(struct TreeNode* root, int* max){
    // 访问到叶子,返回0
    if(!root) return 0;
    // 左子树深度 , 右子树深度
    int l, r;
    // 分别搜索左右子树
    l = search(root -> left, max);
    r = search(root -> right, max);
    // 更新最大值
    *max = (l + r > *max) ? l + r : *max;
    // 返回子树高度
    return 1 + (l > r ? l : r);
}

// 计算直径
int diameterOfBinaryTree(struct TreeNode* root){
    int max = 0;
    search(root, &max);
    return max;
}

基本和110题思路和过程一样,所求的东西其实换个角度想就一样了。

终于到这次总结的最后一题了,,,

654. 最大二叉树

【LeetCode】二叉树刷题总结(二)96、101、114、543、654_第9张图片

看到第二点和第三点,就可以反应过来,这题 分治 + 递归 即可解决,具体怎么分呢?

  • 先找到最大的数6, 6的下标是3。

  • 这时候我们需要把数组分为两个部分,分别生成左子树和右子树。范围为:[0,2] 和 [4, 5]

  • 划分后我们还需要继续细分为 [], [1,2] 和 [0], [] , 直至数组长度为0

  • 看代码吧...

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */


// 递归 & 分治
struct TreeNode* constructMaximumBinaryTree(int* nums, int numsSize){
    // numsSize == 0, 返回空节点
    if(!numsSize) return NULL;
    struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    int maxnum = 0;
    // 找到区间内最大值位置
    for(int i = 0; i < numsSize; ++i)
        if(nums[i] > nums[maxnum]) maxnum = i;
    // 存储当前最大值
    root -> val = nums[maxnum];
    // 分治 && 递归
    root -> left = constructMaximumBinaryTree(nums, maxnum);
    root -> right = constructMaximumBinaryTree(nums + maxnum + 1, numsSize - maxnum - 1);
    return root;
}

总结:

  • 要注意运算中间过程的溢出,结果范围并不是中间计算值范围。

  • 递归相比于迭代有时候更容易想到,但想不出来的时候就是毫无思路。做题的时候还是两种都练练好。

  • 树的遍历真的太重要了!!!基本这些题都绕不开各种遍历方式,要熟练掌握树的遍历。

  • 递归问题总是要找到最小化的模型或者理解最小化的操作。找到了,题目也就解了。

如果有看不懂的,私信我!!!
~^^
每天进步一点,加油!

End

END

你可能感兴趣的:(【LeetCode】二叉树刷题总结(二)96、101、114、543、654)