【LeetCode Solutions】LeetCode 96 ~ 100 题解

LeetCode 96. 不同的二叉搜索树(中等)

【题目描述】

给你一个整数 n n n,求恰由 n n n 个节点组成且节点值从 1 1 1 n n n 互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。

【示例 1】

在这里插入图片描述

输入:n = 3
输出:5

【示例 2】

输入:n = 1
输出:1

【提示】

1 < = n < = 19 1 <= n <= 19 1<=n<=19


【分析】

LeetCode 95. 一样,由 1 ∼ n 1\sim n 1n 构成的二叉搜索树的数量是卡特兰数,可以直接用卡特兰数的公式求解,此处主要分析一下如何递归求解。

假设当前枚举的根节点为 k k k,那么当前节点的方案数就是左子树区间 [L, k - 1] 的所有方案数与右子树区间 [k + 1, r] 的所有方案数的乘积。那么如何快速求出 [L, k - 1][k + 1, r] 中有多少子树?

其次,区间值的大小是相对的,只要区间长度相同,那么能构成的二叉搜索树种类一样多,例如 [1, 10][2, 11]。那么设 f [ i ] f[i] f[i] 表示区间长度为 i i i 的二叉搜索树方案数,则 f[i] = f[k - L] * f[R - k],然后枚举根节点 k k k,将所有方案数加起来就是最终的 f [ i ] f[i] f[i]


【代码】

【递归方法】

class Solution {
public:
    int numTrees(int n) {
        return dfs(1, n);
    }

    int dfs(int l, int r) {  // 区间 [l, r] 的方案数
        if (l >= r) return 1;
        int res = 0;
        for (int k = l; k <= r; k++)  // 枚举根节点
            res += dfs(l, k - 1) * dfs(k + 1, r);
        return res;
    }
};

【递推方法】

class Solution {
public:
    int numTrees(int n) {
        vector<int> f(n + 1);
        f[0] = 1;  // 区间长度为 0 算一种方案
        for (int i = 1; i <= n; i++)  // 枚举区间长度
            for (int k = 1; k <= i; k++)  // 枚举根节点
                f[i] += f[k - 1] * f[i - k];
        return f[n];
    }
};

LeetCode 97. 交错字符串(中等)

【题目描述】

给定三个字符串 s1s2s3,请你帮忙验证 s3 是否是由 s1s2 交错组成的。

两个字符串 s s s t t t 交错的定义与过程如下,其中每个字符串都会被分割成若干非空子字符串:

  • s = s 1 + s 2 + . . . + s n s = s_1 + s_2 + ... + s_n s=s1+s2+...+sn
  • t = t 1 + t 2 + . . . + t m t = t_1 + t_2 + ... + t_m t=t1+t2+...+tm
  • ∣ n − m ∣ < = 1 |n - m| <= 1 nm<=1
  • 交错是 s 1 + t 1 + s 2 + t 2 + s 3 + t 3 + . . . s_1 + t_1 + s_2 + t_2 + s_3 + t_3 + ... s1+t1+s2+t2+s3+t3+... 或者 t 1 + s 1 + t 2 + s 2 + t 3 + s 3 + . . . t_1 + s_1 + t_2 + s_2 + t_3 + s_3 + ... t1+s1+t2+s2+t3+s3+...

注意:a + b 意味着字符串 ab 连接。

【示例 1】

在这里插入图片描述

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

【示例 2】

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

【示例 3】

输入:s1 = "", s2 = "", s3 = ""
输出:true

【提示】

0 < = s 1. l e n g t h , s 2. l e n g t h < = 100 0 <= s1.length, s2.length <= 100 0<=s1.length,s2.length<=100
0 < = s 3. l e n g t h < = 200 0 <= s3.length <= 200 0<=s3.length<=200
s1s2、和 s3 都由小写英文字母组成


【分析】

  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j] 表示所有由 s1[1, i]s2[1, j] 交错形成 s3[1, i + j] 的方案集合是否非空。
  • 状态计算:判断 s3[i + j] 这个字符是属于 s1 还是属于 s2
    • 属于 s1:首先要保证 s1[i] == s3[i + j],然后需要取决于 s1 的前 i − 1 i - 1 i1 个字符与 s2 的前 j j j 个字符能否交错形成 s3 的前 i + j − 1 i + j - 1 i+j1 个字符,也就是 f[i][j] |= f[i - 1][j]
    • 属于 s2:同理要满足 s2[j] == s3[i + j],状态转移为:f[i][j] |= f[i][j - 1]

【代码】

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n = s1.size(), m = s2.size();
        if (s3.size() != n + m) return false;
        s1 = ' ' + s1, s2 = ' ' + s2, s3 = ' ' + s3;  // 下标从 1 开始
        vector<vector<bool>> f(n + 1, vector<bool>(m + 1));
        f[0][0] = true;  // 空串与空串可以交错凑成空串
        for (int i = 0; i <= n; i++)
            for (int j = 0; j <= m; j++) {
                if (i && s3[i + j] == s1[i]) f[i][j] = f[i - 1][j];
                if (j && s3[i + j] == s2[j]) f[i][j] = f[i][j] || f[i][j - 1];
            }
        return f[n][m];
    }
};

LeetCode 98. 验证二叉搜索树(中等)

【题目描述】

给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。

有效二叉搜索树定义如下:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

【示例 1】

在这里插入图片描述

输入:root = [2,1,3]
输出:true

【示例 2】

在这里插入图片描述

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

【提示】

树中节点数目范围在 [ 1 , 1 0 4 ] [1, 10^4] [1,104]
− 2 31 < = N o d e . v a l < = 2 31 − 1 -2^{31} <= Node.val <= 2^{31} - 1 231<=Node.val<=2311


【分析】

比较简单的方式就是判断一下中序遍历结果是否有序,中序遍历的方式就是 LeetCode 94.

如果按定义来判断,那么就是要判断当前节点的左子树中所有节点是否都小于当前节点的值,以及右子树是否都大于当前节点的值。只要记录每个子树的最小值和最大值,然后判断当前节点左子树的最大值是否小于自己,以及右子树的最小值是否大于自己即可。


【代码】

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return dfs(root)[0];
    }

    vector<int> dfs(TreeNode* root) {  // 返回结果分别表示是否有效、子树最小值、子树最大值
        vector<int> res = {1, root->val, root->val};
        if (root->left) {
            vector<int> left = dfs(root->left);
            if (!left[0]) res[0] = 0;  // 左子树不合法
            if (left[2] >= root->val) res[0] = 0;
            res[1] = left[1];  // 根节点的最小值就是左子树的最小值
        }
        if (root->right) {
            vector<int> right = dfs(root->right);
            if (!right[0]) res[0] = 0;  // 右子树不合法
            if (right[1] <= root->val) res[0] = 0;
            res[2] = right[2];  // 根节点的最大值就是右子树的最大值
        }
        return res;
    }
};

LeetCode 99. 恢复二叉搜索树(中等)

【题目描述】

给你二叉搜索树的根节点 root,该树中的恰好两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树。

【示例 1】

在这里插入图片描述

输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。

【示例 2】

在这里插入图片描述

输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。

【提示】

树上节点的数目在范围 [ 2 , 1000 ] [2, 1000] [2,1000]
− 2 31 < = N o d e . v a l < = 2 31 − 1 -2^{31} <= Node.val <= 2^{31} - 1 231<=Node.val<=2311

进阶:使用 O ( n ) O(n) O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O ( 1 ) O(1) O(1) 空间的解决方案吗?


【分析】

如果交换的是相邻两个节点,例如 [1, 2, 3, 4, 5] 中交换了 2 和 3,那么只要找到这个序列中唯一的逆序对 [3, 2] 也就找到了被交换的两个节点。

如果交换的节点不相邻,例如 [1, 2, 3, 4, 5, 6, 7, 8] 中交换了 2 和 6,那么我们就找出两个相邻数构成的逆序对:[6, 3][5, 2],那么被交换的两个数其实就是前一个逆序对的第一个数后一个逆序对的第二个数

因此如果能够进行中序遍历就可以做出来了,但是题目要求使用常数级的空间复杂度求解。无论用栈还是递归遍历二叉树所使用的空间都是和树的高度成正比的。

这里要介绍无需使用额外空间的 Morris 遍历,这是对 LeetCode 94. 迭代遍历方式的进一步改进。对于某个节点,我们分以下几种情况讨论:

  • 没有左子树:直接遍历该点,然后走到右儿子;
  • 有左子树:当遍历完左子树的最后一个节点后应当跳回当前点,之前我们是先把当前点存到栈里。其实我们可以利用左子树最后一个节点的右儿子指针指向所要回溯的节点,这就是 Morris 遍历的核心。

因此对于某个节点,我们需要判断是从父节点下来的还是从子节点回溯上来的,如果是从下面上来的就说明左子树已经遍历完了,就不能再遍历左子树了。

可以查看当前节点有没有左子树,如果有左子树就判断左子树的最后一个点是否已经指向自身,如果指向自身了说明是从子树回溯上来的,这时候就可以将子树指向自己的指针清空。

因此对于有左子树的情况,详细的算法流程就是先找到当前点 root 的前驱节点 p(左子树最大的节点):

  • 如果 p->right == nullptr,则 p->right = root,然后遍历左子树:root = root->left
  • 否则 p->right = nullptr,接着遍历 root,然后遍历右子树:root = root->right

综上,我们一共有两种情况会遍历当前点 root,第一种是 root 没有左子树,第二种是左子树中的前驱节点指向自身也就是回溯回来后要遍历 root

最后再回到题目,我们在遍历的时候用两个节点指针 firstsecond 记录被交换过的点,用 last 表示上一个遍历过的点,在第一次找到相邻逆序对的时候,first = last, second = root,如果第二次找到了相邻逆序对,那么 second = root


【代码】

【递归遍历】

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<TreeNode*> res;

    void recoverTree(TreeNode* root) {
        inorderTraversal(root);
        int first = 0, second = 0;
        for (int i = 1; i < res.size(); i++)
            if (res[i - 1]->val > res[i]->val) {  // 找到相邻逆序对
                if (!second) first = i - 1, second = i;  // 第一个逆序对
                else second = i;  // 第二个逆序对
            }
        swap(res[first]->val, res[second]->val);
    }

    void inorderTraversal(TreeNode* root) {  // 递归中序遍历
        if (root->left) inorderTraversal(root->left);
        res.push_back(root);
        if (root->right) inorderTraversal(root->right);
    }
};

【Morris 遍历】

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void recoverTree(TreeNode* root) {
        TreeNode *first = nullptr, *second = nullptr, *last = nullptr;
        while (root) {
            if (!root->left) {  // 第一种遍历情况:没有左子树
                if (last && last->val > root->val) {  // 找到相邻逆序对
                    if (!first) first = last, second = root;  // 第一个逆序对
                    else second = root;  // 第二个逆序对
                }
                last = root;  // 记录遍历过的上一个节点
                root = root->right;  // 遍历右子树
            } else {  // 有左子树
                TreeNode* p = root->left;  // 前驱节点
                while (p->right && p->right != root) p = p->right;  // 注意要判断是否指向自己,否则可能死循环
                if (p->right == root) {  // 第二种遍历情况:前驱节点指向自己
                    p->right = nullptr;  // 清空指针
                    if (last && last->val > root->val) {
                        if (!first) first = last, second = root;
                        else second = root;
                    }
                    last = root;
                    root = root->right;  // 遍历右子树
                } else {  // 前驱节点没指向自己
                    p->right = root;  // 使前驱节点指向自己
                    root = root->left;  // 先遍历左子树
                }
            }
        }
        swap(first->val, second->val);  // 交换两个错误节点的值
    }
};

LeetCode 100. 相同的树(简单)

【题目描述】

给你两棵二叉树的根节点 pq,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

【示例 1】

在这里插入图片描述

输入:p = [1,2,3], q = [1,2,3]
输出:true

【示例 2】

在这里插入图片描述

输入:p = [1,2], q = [1,null,2]
输出:false

【示例 3】

在这里插入图片描述

输入:p = [1,2,1], q = [1,1,2]
输出:false

【提示】

两棵树上的节点数目都在范围 [ 0 , 100 ] [0, 100] [0,100]
− 1 0 4 < = N o d e . v a l < = 1 0 4 -10^4 <= Node.val <= 10^4 104<=Node.val<=104


【分析】

本题就很简单了,两棵树按同步的方向遍历,判断所遍历的每个节点是否都一样即可。


【代码】

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isSameTree(TreeNode* p, TreeNode* q) {
        if (!p && !q) return true;  // 两个节点都为空表示相同
        if (!p || !q || p->val != q->val) return false;  // 只有一个为空或两个节点值不一样则不相同
        return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);  // 递归判断左右子树是否都相同
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柃歌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值