Day17 二叉树:654.最大二叉树 617.合并二叉树 700.二叉搜索树中的搜索 98.验证二叉搜索树

160 阅读11分钟

654.最大二叉树

题目链接:

难度指数:😀😐😕

给我们一个数组,用这个数组来构造一棵二叉树。

规则:在数组中选取一个最大的元素作为根节点,然后最大元素的左区间再去继续构造左子树,仍然是选取一个最大的元素作为左子树的根节点;右区间同理。

在这个规则下构造出来的最大二叉树,

思考用什么样的遍历方式来构造这棵二叉树?

A:凡是构造二叉树类的题目,我们都要使用前序遍历

前序遍历是中左右,即一定要先构造。(要把根节点先构造出来,再去构造左子树,右子树;在左子树中依然先构造中,再构造左、右……)

动画:

17.01.gif

代码思路:

确定递归函数的参数和返回值:

传入递归函数的是一个数组nums,最终要返回以这个数组构造的最大二叉树的根节点,因此返回类型是TreeNode*。

 TreeNode* contruct(nums) {
     //终止条件
     if (nums.size() == 1) {  //说明到叶子节点
         return new TreeNode(nums[0]);  //return数组里唯一一个元素
     }
     //确定单层递归的逻辑:找到数组中的最大值,还要找到最大值所在的下标
     //中
     int maxValue = 0;  //最大值
     int index = 0;  //最大值所在的下标
     //遍历数组
     for (int i = 0; i < nums.size(); i++) {
         if (nums[i] > maxValue) {  //找到比maxValue还大的值
             maxValue = nums[i];
             index = i;  //记录下标
         }
     }
     node = new TreeNode(maxValue);  //求完最大值之后,定义了一个新节点
     
     //构造节点的左子树和右子树:
     //左  (构造左子树,需要分割数组,需要保证左区间的元素至少有1个)
     if (index > 0) {  //说明左区间至少有一个元素  (index是这个数组最大值的下标)
         //分割数组
         newVec(0, index);    //[ ) 左闭右开        (不包含index)
         node->left = contruct(newVec);
     }
     //右
     if (index < (nums.size() - 1)) {
         newVec(index + 1, nums.size());  //[ ) 左闭右开    (不包含nums.size()这个下标)
         node->right = contruct(newVec);
     }
     return node;
 ​
 }

TreeNode* contruct(nums) { if (nums.size() == 1) { //说明到叶子节点 return new TreeNode(nums[0]); } }

如果传进来的数组大小是1的话,说明我们要构造的根节点,它就是叶子节点。

那么,就return数组里唯一一个元素

同学疑惑:这里面为什么没有对数组为空的情况进行判断?

因为力扣上本题的题目要求就是传入递归函数的数组nums,其大小是 >= 1 (因此我们不用对数组为空的情况进行考虑)

到叶子节点,及时返回,不用考虑数组为空的情况。


单层递归的逻辑:

int maxValue = 0; //最大值

为什么要求这个最大值? 因为构造中节点的时候要用最大值来构造。

为什么一开始要赋成0 ? 因为数组里面的所有元素都是正整数。 (题目要求)

为什么要有下标index? 因为接下来分割数组的时候要根据下标来分割。

求完最大值之后,定义了一个新节点

node = new TreeNode(maxValue);

在构造节点的左子树和右子树时,

构造左子树,需要分割数组;同时,需要保证左区间的元素至少有1个,为什么?

if (nums.size() == 1) { //说明到叶子节点 return new TreeNode(nums[0]); }

A:因为这里要求至少只有一个元素的时候,才进行返回。

分割数组:

在分割数组的时候,一定要注意对区间的定义,要么就坚持左闭右开,要么就坚持左闭右闭。

在nums里面定义一个新数组newVec,它取nums里面的下标范围是:0~index,取的是左闭右开区间。

有了新数组,就可以继续向左递归:node->left = contruct(newVec); ,传入新构造的数组newVec

也就是说左区间的数组再传进来,自己调用自己,传进递归函数之后,用这个新数组构造的二叉树就是这个node的左子树 (node->left)

右区间,需要保证数组的大小是大于等于1,

if (index < nums.size() - 1) 说明分割点的右区间,这个数组的大小一定是大于等于1


梳理一下:构造中节点,然后去递归构造左子树(在构造过程中进行分割数组的操作,左闭右开);递归构造右子树(同理)。

这个节点构造完了,左子树、右子树构造完了,最后将这个节点返回,就是根据这棵二叉树所构造的最大二叉树的根节点的下标。

return node; 就是根据这个数组所构造的最大二叉树的根节点的指针。

代码比较冗余,效率低。

AC代码: (核心代码模式)

 class Solution {
 public:
     TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
         TreeNode* node = new TreeNode(0);
         if (nums.size() == 1) {
             return new TreeNode(nums[0]);  //return数组里唯一一个元素
         }
         //中
         int maxValue = 0;  //最大值
         int index = 0;  //最大值所在的下标
         //遍历数组
         for (int i = 0; i < nums.size(); i++) {
             if (nums[i] > maxValue) {
                 maxValue = nums[i];
                 index = i;  //记录下标
             }
         }
         node = new TreeNode(maxValue);  //求完最大值之后,定义一个新的节点
         //构造节点的左子树和右子树:
         //左
         if (index > 0) {
             //分割数组
             vector<int> newVec(nums.begin(), nums.begin() + index);
             node->left = constructMaximumBinaryTree(newVec);
         }
         //右
         if (index < (nums.size() - 1)) {
             vector<int> newVec(nums.begin() + index + 1, nums.end());
             node->right = constructMaximumBinaryTree(newVec);
         }
         return node;
     }
 }

617.合并二叉树

题目链接:

难度指数:😀😐

本题是考查一起操作2棵二叉树的能力。我们前面操作的基本都是一棵二叉树,遇到这个可能有点懵。

使用递归法就会涉及到前中后序遍历。本题,前序是最容易理解的。(中左右符合将两棵树合并成一棵树的正常逻辑)

本题也可以使用迭代法:

递归解法:(前序)

代码思路:

1️⃣ 要合并两个数,就把2个数的根节点传进来

2️⃣ 在遍历t1和t2的时候,是同步进行遍历的,不是异步。

if (t1 == NULL) { return t2; } if (t2 == NULL) { return t1; }

这里面已经包含了t1和t2同时为空的情况,比如:t1为空,就返回t2,若t2也为空,其实返回的就是null

3️⃣ tree1所遍历节点和tree2所遍历节点不为空 t1->val += t2->val; //值相加

合并之后的这个二叉树的左子树,需要把旧的tree1的左子树和旧的tree2的右子树作为参数传进来。

 TreeNode* mergeTree(TreeNode* t1, TreeNode* t2) {
     //终止条件
     if (t1 == NULL) {
         return t2;
     }
     if (t2 == NULL) {
         return t1;
     }
     //确定单层递归的逻辑
     //tree1所遍历节点和tree2所遍历节点不为空
     t1->val += t2->val;  //值相加     中
     t1->left = mergeTree(t1->left, t2->right);  //左
     t1->right = mergeTree(t1->right, t2->right);  //右
     
     return t1;
 }

中序、后序也是可以的,只不过我们正常思路还是倾向于前序的。

上面代码其实是重复用了tree1的结构,也就是说把题目中传进来的数给改了,而没有重新定义一个新的二叉树。

重新定义一个新的二叉树,也不复杂:

 TreeNode* mergeTree(TreeNode* t1, TreeNode* t2) {
     //终止条件
     if (t1 == NULL) {
         return t2;
     }
     if (t2 == NULL) {
         return t1;
     }
 ​
     TreeNode* root = new TreeNode(0);
     root->val = t1->val + t2->val;  //值相加     中
     root->left = mergeTree(t1->left, t2->right);  //左
     root->right = mergeTree(t1->right, t2->right);  //右
     
     return root;
 }

空间复杂度:O(n) tree1和tree2重新开辟了一个空间。

AC代码: (核心代码模式)

 class Solution {
 public:
     TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
         if (root1 == NULL) {
             return root2;
         }
         if (root2 == NULL) {
             return root1;
         }
         //确定单层递归的逻辑
         //tree1所遍历的节点和tree2所遍历的节点不为空
         root1->val += root2->val;  //值相加    中
         root1->left = mergeTrees(root1->left, root2->left);  //左
         root1->right = mergeTrees(root1->right, root2->right);  //右
 ​
         return root1;
     }
 };

最后的总结建议重新看下视频

迭代解法:

AC代码: (核心代码模式)

700.二叉搜索树中的搜索

题目链接:

难度指数:😀🙂

递归解法:

代码思路:

2️⃣ 如果二叉树中没有我们要搜的目标值的节点,就return null;

或者说root == val (即root这个数值等于我们想要搜索的数值),那么就要把这个节点返回。

 TreeNode* search(TreeNode* root, int val) {
     if (root == NULL || root->val == val) {
         return root;
     }
     //确定单层递归的逻辑
     TreeNode* result = NULL;
     if (val < root->val) {  //如果要搜索的这个值 < root->val
         //我们应该去左子树遍历
         result = search(root->left, val);  //用result接收递归返回的变量
     }
     if (val > root->val) {
         result = search(root->right, val);
     }
     return result;  //没有搜到结果的话,result的默认值就是NULL
 }

用递归的方式实现在二叉搜索树中,搜索我们想要查询的这个数值的节点。

而且,也没有涉及到前中后序遍历,因为二叉搜索树的特性,已经帮我们确定了遍历顺序,

AC代码: (核心代码模式)

 class Solution {
 public:
     TreeNode* searchBST(TreeNode* root, int val) {
         if (root == NULL || root->val == val) {
             return root;
         }
         TreeNode* result = NULL;
         if (val < root->val) {
             result = searchBST(root->left, val);
         }
         if (val > root->val) {
             result = searchBST(root->right, val);
         }
         return result;  //没有搜索到结果的话,result的默认值就是NULL
     }
 };

迭代解法:

本题的迭代法出乎意外的简单

AC代码: (核心代码模式)

 class Solution {
 public:
     TreeNode* searchBST(TreeNode* root, int val) {
         //遍历root,如果root不为空,就一直往下遍历
         while (root != NULL) {
             if (val < root->val) {
                 root = root->left;
             }
             else if (val > root->val) {
                 root = root->right;
             }
             else {  //相等
                 return root;
             }
         }
         return NULL;
     }
 };

98.验证二叉搜索树

题目链接:

难度指数:😀😐😕

给我们一棵树,判断这棵树是不是二叉搜索树。

正是因为二叉搜索树的特性,在递归遍历的时候,如果是中序遍历,那么这些元素都是有序的

验证二叉搜索树:就是验证这棵树在中序遍历的时候是不是单调递增,如果是,那么它就是一棵二叉搜索树。

本题的一个直观想法:中序遍历这棵二叉树,把所有的元素都保存到一个数组中,判断这个数组是不是单调递增,即可得知这棵树是不是二叉搜索树。

递归解法:(中序)

代码思路:

返回值是这棵二叉树是不是二叉搜索树,bool类型;传入的参数就是这个二叉树的根节点。

PS:一棵空的二叉树,它也是一棵二叉搜索树,也是完全二叉树,也是满二叉树……

bool isvalid(TreeNode* root) {
    if (root == NULL) {  //若这棵二叉树是空
        return true;
    }
    isvalid(root->left);  //左
    vec.push_back(root->val);  //中
    isvalid(root->right);  //右
}

AC代码: (核心代码模式)

/**
 * 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 {
private:
    vector<int> vec;
    void traversal(TreeNode* root) {
        if (root == NULL) {
            return;
        }
        traversal(root->left);  //左
        vec.push_back(root->val);  //中  将二叉搜索树转换为有序数组
        traversal(root->right);  //右
    }
public:
    bool isValidBST(TreeNode* root) {
        vec.clear();
        traversal(root);
        for (int i = 1; i < vec.size(); i++) {
            //这里注意要 <=,搜索树中不能有相同的元素
            if (vec[i] <= vec[i - 1]) {
                return false;
            }
        }
        return true;
    }
};

这段代码的for循环还是值得一看的,要弄懂为什么这么写。

这并不是最优的方法 ↑↑,

我们没有必要把二叉树转变成一个数组,直接在遍历过程中就可以判断是否有序:

不将二叉树转变成数组:

递归法是左中右,关键是如何进行比较,

long long maxVal = LONG_MIN;
bool isvalid(TreeNode* root) {
    if (root == NULL) {
        return true;
    }
    bool left = isvalid(root->left);  //左
    //中
    if (root->val > maxVal) {
        maxVal = root->val;
    }
    else {  //就不是二叉搜索树
        return false;
    }
    //右
    bool right = isvalid(root->right);
    
    return left && right;  //左子树和右子树都符合条件,就return true
}

maxVal记录的是当前节点的前一个节点的数值。

每次root->val始终比前一个结点大,始终比前一个节点大,以至于每次都可以去更新max_value。

同学疑惑:return left && right; 这里判断左子树、右子树符合条件,貌似是否符合条件还没有判断呀

因为力扣的后台测试数据有int的最小值,

使用long long maxVal = LONG_MIN;

如果你输入的数据中有int的最小值,就把这个maxVal定义成一个更小的,

这样才能保证在遍历第一个节点的时候,root->val 一定 > maxVal

不过,这种方法其实也不太好

AC代码: (核心代码模式)

class Solution {
public:
    long long maxVal = LONG_MIN;  //因为后台测试数据中有int最小值
    bool isValidBST(TreeNode* root) {
        if (root == NULL) {
            return true;
        }
        bool left = isValidBST(root->left);  //左
        //中
        if (root->val > maxVal) {
            maxVal = root->val;
        }
        else {  //就不是二叉搜索树
            return false;
        }
        //右
        bool right = isValidBST(root->right);

        return left && right;  //若左子树和右子树都符合,就return true
    }
};

这种写法不是很好

可以选择不额外定义一个变量,而是在遍历二叉树的时候,前一个结点和后一个结点进行比较就可以了。

↓↓↓

双指针优化:

定义一个pre节点,初始化为NULL:TreeNode pre = NULL;*

pre记录当前遍历的前一个节点,root就可以和pre进行比较。

AC代码: (核心代码模式)

/**
 * 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:
    TreeNode* pre = NULL;  //pre记录当前遍历的前一个节点
    bool isValidBST(TreeNode* root) {
        if (root == NULL) {
            return true;
        }
        bool left = isValidBST(root->left);  //左

        if (pre != NULL && pre->val >= root->val) return false;
        pre = root;  //记录前一个结点

        bool right = isValidBST(root->right);  //右

        return left && right;
    }
};

pre = root; //记录前一个结点

这一行不是很懂,得琢磨一下

迭代解法:

是一个模板类的题目,即中序遍历的模板,稍稍改一下就可以了。

AC代码: (核心代码模式)