Leetcode刷题笔记--二叉树


前言

一、二叉树基本用法

1.1 创建二叉树

二叉树节点通常包含一个值以及指向左子节点和右子节点的指针。定义一个节点结构来表示二叉树的节点。我们可以通过动态分配内存来创建二叉树节点,并将它们连接起来形成一棵树。

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);

1.2 释放二叉树

使用完二叉树后,需要释放所有节点的内存以防止内存泄漏。可以通过递归方式释放每个节点的内存。

void deleteTree(TreeNode* root) {
    if (root == nullptr) return;
    deleteTree(root->left);
    deleteTree(root->right);
    delete root;
}

1.3 二叉树的简单遍历

二叉树的遍历有三种常见方式:前序遍历、中序遍历和后序遍历。这些遍历方法分别是:
前序遍历(Preorder Traversal): 首先访问根节点,然后递归遍历左子树,最后递归遍历右子树。
中序遍历(Inorder Traversal): 先递归遍历左子树,然后访问根节点,最后递归遍历右子树。
后序遍历(Postorder Traversal): 先递归遍历左子树,然后递归遍历右子树,最后访问根节点。
代码如下。

void preorderTraversal(TreeNode* root) {
    if (root == nullptr) return;
    cout << root->val << " ";
    preorderTraversal(root->left);
    preorderTraversal(root->right);
}

void inorderTraversal(TreeNode* root) {
    if (root == nullptr) return;
    inorderTraversal(root->left);
    cout << root->val << " ";
    inorderTraversal(root->right);
}

void postorderTraversal(TreeNode* root) {
    if (root == nullptr) return;
    postorderTraversal(root->left);
    postorderTraversal(root->right);
    cout << root->val << " ";
}

二. 二叉树的特性

二叉树的很多基本性质都是通过递归来实现的,二叉树本身的结构天然适合递归求解,因为在二叉树中,每个节点都有左子树和右子树,而每个子树本身也是一个二叉树。咱们先看几个简单题,让我们面对二叉树不再害怕 !

1.计算二叉树的节点个数

二叉树的节点个数=左子树的节点个数+右子树的节点个数+1。 其中,左子树和右子树又能分为更多的二叉树,所以可以用递归来解决。代码很简单。

int countNodes(TreeNode* root) {
    if(!root)
    {
        return 0;
    }
    return countNodes(root->left) + countNodes(root->right) + 1;
}

2.计算二叉树的最大深度

二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。如下图所示,二叉树的最远叶子节点为6,路径为从1到6,其深度为4。那么如何找到这条路线呢?还是要进行递归。二叉树的最大深度=1+max(左子树最大深度,右子树最大深度)。 那么左右子树的最大深度依旧可以按照这个公式递归求解。
这次我们思考一下判断的条件:如果maxDepth(root)中root为空,那么直接返回0,说明没有深度,在其他情况下,返回上述公式。
在这里插入图片描述

int maxDepth(TreeNode* root) {

    if (!root)
    {
        return 0;
    }
    return max(maxDepth(root->right), maxDepth(root->left))+1;
}

3.计算二叉树的最小深度
刚做完上面的最大深度,一看这个最小深度,简直是so easy!把max改成min就行了吧。但是代码报错了。分析一下原因。发现我们漏掉了一个关键的情况。如下图,如果二叉树只有左子树或者只有右子树,那么他的最小深度还是等于整个二叉树的深度!
为什么会这样呢?因为当一个节点只有左子树或者只有右子树时,它并不是叶子节点,因为叶子节点是指没有子节点的节点。同时,根节点也不是叶子节点。 所以本题我们要判断多种情况:

1.当前节点为空,返回0。
2.当前节点没有左子树,递归右子树的最小深度并且加一。
3.当前节点没有右子树,递归左子树的最小深度并且加一。
4.当前节点既有左子树,又有右子树,这时候才和最大深度的判断方法类似。

在这里插入图片描述

    int minDepth(TreeNode* root) {
    if(!root) return 0;
    else if(!root->left) return minDepth(root->right)+1;
    else if(!root->right) return minDepth(root->left)+1;
    return min(minDepth(root->left),minDepth(root->right))+1;
    }

4.翻转二叉树

做了上面几道题,我们大概明白了二叉树的简单题目分析思路。先分析一个二叉树的情况,然后看是否可以递归。先分析下图。
在这里插入图片描述
如果想要翻转二叉树,根节点不变,只需要翻转左右子树即可。代码也很简单,先备份一下,然后交换:

 TreeNode* temp = root->left;
 root->left = root->right;
 root->right = temp;

然后我们分析每个子树是否也可以这样呢?答案是可以的。如果2,3下面还有节点,那么我们只翻转了2,3,不翻转他们下面的节点肯定是不行的。所以这道题可以用递归来实现。我们基于上面的代码,加上空节点和非空节点返回条件,就可以轻松拿下了!

TreeNode* invertTree(TreeNode* root) {
    if (!root) return nullptr;
    // 备份左子树
    TreeNode* temp = root->left;   
    // 翻转左子树为右子树
    root->left = invertTree(root->right);
    // 翻转右子树为备份的左子树
    root->right = invertTree(temp);
    return root;

5.判断二叉树是否对称
这道题又和上一题比较类似,考察二叉树是否对称,刚开始我们可能会判断左子树的值和右子树是否相等。单个二叉树是这样的,但是如果把二叉树扩展,就不太一样了。如下图所示。第二层的两个子树并没有满足左右节点相等的条件,而是对于整个树来说是对称的。所以这跟上一题又不同了。但其实仔细分析一下,对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的。 我们可以把下面那个图当作递归循环的典范。
如果根节点的左右子节点相同,同时左子树的左子节点与右子树的右节点相同,同时左子树的右节点与右子树的左节点相同。同时满足这三个条件,才能判断二叉树对称。
同时我们需要讨论情况:若左右节点都为空,则对称,若左右节点有一个为空,则必定不对称。代码如下:
在这里插入图片描述

bool check(TreeNode* l, TreeNode* r) {
    if (!l && !r) return true;
    if (!l || !r)  return false;
    return l->val == r->val && check(l->left, r->right)&& check(l->right,r->left);
}
bool isSymmetric(TreeNode* root) 
{
   return  check(root->left, root->right);
}

6.完全二叉树的节点个数
首先我们需要了解,什么是完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。

这种特殊的结构使得完全二叉树可以利用数组进行有效的存储。对于一个完全二叉树,如果将它的节点从上到下、从左到右依次编号,那么节点的编号与数组中的下标可以建立一种对应关系,这样就可以在数组中按照一定规律存储二叉树的节点,从而减少了存储空间的浪费。
在这里插入图片描述
对于这道题目,我们当然可以直接使用二叉树节点个数的代码写出来,但是完全二叉树由于有了更多的特性,所以我们要思考有没有其他的方法完成这个代码,从而减少时间复杂度。

本题的关键在于判断子树是否为满二叉树,因为满二叉树是可以使用公式计算节点数量的。可以通过左深度和右深度是否相同来判断该子树是不是满二叉树。如果是满二叉树,那么直接返回满二叉树的节点数量。创建两个节点指针,就可以得到左右的深度,然后进行判断。代码如下:

int countNodes(TreeNode* root) {
        if (!root) return 0;
        TreeNode* l = root->left;
        TreeNode* r = root->right;
        int leftdepth = 0, rightdepth = 0;
        while (l)
        {
            l = l->left;
            leftdepth++;
        }
        while (r)
        {
            r = r->right;
            rightdepth++;
        }
        if (leftdepth == rightdepth)  return (2 << leftdepth) - 1;
        return  countNodes(root->left) + countNodes(root->right) + 1;
    }

7.平衡二叉树
平衡二叉树是一种二叉树结构,其具有以下特点:

1.对于任意一个节点,其左子树和右子树的高度差不超过 1,也就是说,任意节点的左右子树高度差不超过 1。
2.左子树和右子树都是平衡二叉树。

平衡二叉树的设计旨在确保树的高度较小,使得在最坏情况下,树的各种操作(例如查找、插入、删除等)的时间复杂度能够保持较低的水平,通常为 O(log n),其中 n 是节点的个数。因此,平衡二叉树的设计可以提高二叉搜索树的性能,避免出现退化为链表的情况,从而保证了各种操作的高效性。

如果要判断左子树和右子树的高度差是否超过 1,那么就需要计算左右子树的高度。之前我们都是算的深度,那这高度和深度又有什么区别呢?这里我贴上代码随想录的一张图,非常的清晰明了:
在这里插入图片描述
在力扣上就是按照上图来看深度和高度的。根节点的深度为1,而最深层的节点高度为1。深度是从下往上进行计算,而高度恰恰相反,如果仍然按照从上往下的前序遍历,那是得不到想要的结果的,我们只能通过后序遍历左右中的方法从下到上遍历。
先求出节点高度。

int getHeight(TreeNode* root) {
    if (root == nullptr) return 0;
    return 1 + max(getHeight(root->left), getHeight(root->right));
}

然后根据平衡二叉树定义,列举出判断条件。

bool isBalanced(TreeNode* root) {
    if (root == nullptr) return true;
    
    int leftHeight = getHeight(root->left);
    int rightHeight = getHeight(root->right);
    
    if (std::abs(leftHeight - rightHeight) > 1) return false;
    
    return isBalanced(root->left) && isBalanced(root->right);
}

三、二叉树的dfs与bfs

3.1 dfs的路径问题

简单来说,深度优先搜索(dfs)的基本思想是从起始节点开始,递归地访问每个节点的所有子节点,直到达到某个终止条件为止。 具体来说,DFS会沿着一条路径一直向下深入,直到不能再继续向下为止,然后回退到上一个节点,继续探索其他分支。
所以上面介绍的通过递归的三种遍历方式都属于dfs。

1.二叉树的所有路径
做dfs的题目时候,需要要先思考使用哪种遍历方式。我们所求的路径是所有从根节点到叶子节点的路径,也就是说,需要使用前序遍历解决。
通常在解决这类问题时候,我们要单独写出一个dfs函数,为什么呢?因为大多数情况下,为了解决问题,我们需要记录递归过程中的其他参数,而大部分原函数的参数只有一个root。同时,将递归遍历二叉树的过程封装成一个函数,使得代码结构更清晰,易于理解和修改。
接下来就开始写dfs函数。我们先看下图,黄色的虚线就是遍历的过程,我们从1节点开始遍历,向左遍历2节点,当我们遍历到4的时候,表明已经找到了一条路径,之后需要返回到2,看2节点还有没有其他的路径,我们找到了5,记录之后继续返回到2节点,2已经没有其他路径了,继续返回到1,1节点继继续向右到3节点。这刚好是一个前序遍历的过程。
分析过后,我们可以得出下面的结论

1.遍历到叶子节点是一个路径完成的标志
2.一条路径要用到多个递归中才能得出结果。

所以我们就可以确定dfs的参数了,一个是根节点,一个是要得到的所有路径结果,还有一个是当前的路径。

void dfs(TreeNode* root, vector<string>& paths, string cur_path)

之后我们每当一个路径完成,也就是该节点没有左右子树的时候,我们就把当前路径储存到路经总和中,如果当前路径没有走完,那就按照前序遍历的顺序进行,代码如下:

在这里插入图片描述

void dfs(TreeNode* root, vector<string>& paths, string cur_path) {

    if (root == nullptr) return;

    cur_path += to_string(root->val);

    if (root->left == nullptr && root->right == nullptr) {
        paths.push_back(cur_path);
        return;
    }
    cur_path += "->";
    dfs(root->left, paths, cur_path);
    dfs(root->right, paths, cur_path);
}


vector<string> binaryTreePaths(TreeNode* root) {
    vector<std::string> paths;
    string cur_path = "";
    dfs(root, paths, cur_path);
    return paths;
}

2.路经总和1

这道题目也和路径相关。但是求解的是路径节点值的总和是否等于目标值。因为是从根节点除出发,这样的话继续用前序遍历。当一个路径完成的时候,才可以比较大小,所以只需要在每次遍历到叶子节点的时候,判断此路径的总和是否等于目标值。那么问题的关键来了,如何得到一条路径的长度呢?由于前序遍历使用递归的方式进行,我们不能统计每条路径的长度和,因为在前序遍历回溯的时候,不会从头开始重新遍历。 我们看图,假如我们遍历了1,2,4,然后返回到2,去遍历5,那么1,2,5这条路径的总和不能从头开始计算,而是应该把1,2,4这条路径的总和减去4,再加上5,才可以。

从递归的思路来看,每次我们只需要在路径结束时,比较目标值减去根节点的数值是否为0即可,看下图,就可以更清晰。
在这里插入图片描述

    bool hasPathSum(TreeNode* root, int targetSum) {

    if (!root ) return false;

    if (!root->left && !root->right && targetSum == root->val) return true;

    return hasPathSum(root->left, targetSum - root->val) || hasPathSum(root->right, targetSum - root->val);
    }

3.路经总和2
这道题目相当于第一题和第二题的结合,但也有不同。由于本题要遍历整个树,找到所有路径,所以递归函数,没有返回值。这点非常重要,因为上一题之所以返回bool类型的结果,是因为他只检测是否存在,如果存在,那么遍历就结束了,并不是遍历整个二叉树。

void dfs(TreeNode* root, vector<vector<int>>&paths, vector<int>&cur_path, int targetSum) {
    if (!root) return;

    cur_path.push_back(root->val);
    targetSum -= root->val;
    if (!root->left && !root->right&&targetSum==0) paths.push_back(cur_path);
    

    dfs(root->left, paths, cur_path, targetSum);
    dfs(root->right, paths, cur_path, targetSum);
    cur_path.pop_back();
}


vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
    vector<vector<int>>ans;
    vector<int>cur_path;
    dfs(root, ans, cur_path, targetSum);
    return ans;
}

4.路经总和3

这道题已经算是很难了,他与上一道题目不同的点在于:路径不需要从根节点开始,也不需要在叶子节点结束。 相同的点在于路径方向必须是向下的(只能从父节点到子节点)。
虽然只改变了这一点,但是这会导致我们要额外思考很多种情况。最初的想法就是,可以从每个节点都要出发,进行统计,不过之前是只从根结点出发,所以在此基础上又要嵌套一个dfs递归,时间复杂度为O(n2)。

更好的做法是, 可以把一个节点到根节点的路径之和看作是一个前缀和,那么节点前缀和的差就是我们想要求得的路径和,然后判断是否为目标值和即可。所以,如果能把前缀和的思想结合到这个题目,那么问题就可以迎刃而解了。那接下来看看如何解题。
我们要使用一个哈希表prefixSumCount,使得key是前缀和, value是该前缀和的节点数量,记录数量是因为有出现复数路径的可能。这样的话,使用 prefixSumCount[currentSum - targetSum] 就可以快速检查是否存在一条从树的任意节点到当前节点的路径,其路径和正好等于 targetSum。 这是通过检查当前的累积和 currentSum 减去 targetSum 是否已经在之前的某个节点的累积和中出现过来实现的。

举个例子,假设 targetSum 是 8,当前路径的累积和 currentSum 是 10,我们在哈希表中查找 currentSum - targetSum,也就是 10 - 8 = 2。如果哈希表中 2 的计数是非零的,这意味着之前存在一条路径和为 2,从那个节点到当前节点的路径和正好是 8,这样我们就找到了一条有效路径。

如果currentSum 刚好等于targetSum ,说明这条路径等于目标路径,不需要查找,直接加上就可以了,为了代码可读性更强,可以初始化哈希表prefixSumCount[0] = 1。代码如下。

int totalPaths = 0; // 全局变量,用于存储路径总数

void dfs(TreeNode* node, int currentSum, int targetSum, unordered_map<int, int>& prefixSumCount) {
    if (!node) return;

    // 更新当前路径和
    currentSum += node->val;

    // 如果找到一条路径和等于targetSum,则更新路径总数
    if (prefixSumCount.find(currentSum - targetSum) != prefixSumCount.end()) {
        totalPaths += prefixSumCount[currentSum - targetSum];
    }

    // 更新哈希表
    prefixSumCount[currentSum]++;

    // 继续探索左右子树
    dfs(node->left, currentSum, targetSum, prefixSumCount);
    dfs(node->right, currentSum, targetSum, prefixSumCount);

    // 回溯,撤销当前节点的路径和
    prefixSumCount[currentSum]--;
}

int pathSum3(TreeNode* root, int targetSum) {
    unordered_map<int, int> prefixSumCount;
    prefixSumCount[0] = 1;
    dfs(root, 0, targetSum, prefixSumCount);
    return totalPaths;
}

下面我做了一个相关过程的动画,可希望以帮助更好理解到本题。
在这里插入图片描述

5.二叉树中的最大路径和

这道题目参考官方题解。题解把本题的路径之和转换为每个节点的贡献值,简直太巧妙了。

1.空节点的最大贡献值等于0。
2.非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和
3.叶节点而言,最大贡献值等于节点值。

这样的话,我们只需要求得每个节点的左右节点和自身的贡献值即可。对于子节点,只统计贡献值为正的情况。然后找到最大贡献值的节点,也就是最大路径的值!

int maxSum = INT_MIN;
int dfs(TreeNode* root) {
    if (!root) return 0;

    int left_gain = max(dfs(root->left),0);
    int right_gain = max(dfs(root->right), 0);
    int path = root->val + left_gain + right_gain;
    maxSum = max(path, maxSum);
    return root->val+max(left_gain,right_gain) ;
}
/// <summary>
/// 二叉树中的最大路径和
/// </summary>
int maxPathSum(TreeNode* root) {
    dfs(root);
    return maxSum;
}

3.2 bfs的层次问题

二叉树中的广度优先遍历其实就是层序遍历一个二叉树。也就是从左到右一层一层的去遍历二叉树。这种遍历的方式和之前讲过的都不太一样。需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
在这里插入图片描述
如上图所示,如果我们要进行层序遍历,那么遍历的顺序就是1,2,3,4,5,6,7。这很好理解,那接下来就看代码怎么写。

1.二叉树的层序遍历

通常使用队列来实现层序遍历。关键在于我们不仅要输出所有的遍历顺序,还需要把每层分别有哪些给表达出来。所以这道题目的关键在于如何分割每一层的节点,让我们的结果精确表达每层的节点顺序。

其实这个比较容易想,就按照上图来说。刚开始我们把节点1放进队列,然后我们要弹出1,把他的左右子节点放入栈中,然后我们又要把2和3的左右节点放入队列,然后我想要2,3一块弹出,4,5,6,7一起弹出。这时候我们就发现规律了:每次弹出元素,我们都要吧他们的左右子树放添加到队列。但如果按照层次的话,应该是第一次弹出一个,第二次弹出2个,第三次弹出4个。而弹出元素的次数刚好是每层的节点数,所以这个可以用for循环来实现。每次循环之后,重新统计循环的大小。

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>>ans;
    queue<TreeNode*>q;
    if(root) q.push(root);
    while (!q.empty())
    {
        int size = q.size();
        vector<int>vec;
        for (int i = 0; i < size; i++)
        {
            TreeNode* cur = q.front();
            q.pop();
            vec.push_back(cur->val);
            if (cur->left) q.push(cur->left);
            if (cur->right) q.push(cur->right);

        }
        ans.push_back(vec);
    }
    return ans;
    }

2.二叉树的锯齿形层序遍历
只要会了层次遍历,那么这道题就很简单了。每一行添加的顺序不同,也就是队列的出入顺序不同。我们需要使用双端队列,就可以很容易的变换方向。

vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
    vector<vector<int>>ans;
    deque<TreeNode*>dq;
    if (root)
       dq.push_front(root);
    bool leftToRight = true; // 控制遍历方向
    while (!dq.empty())
    {
        vector<int>vals;
        int levelSize = dq.size();
        for (int i = 0; i < levelSize; i++)  //这里不能直接 i<q.size(),因为for循环中的操作会改变size的大小
        {
            if (leftToRight)
            {
            TreeNode* cur = dq.front();
            dq.pop_front();

            vals.push_back(cur->val);
            if (cur->left)
                dq.push_back(cur->left);
            if (cur->right)
                dq.push_back(cur->right);
            }
            else
            {  
                TreeNode* cur = dq.back();
                dq.pop_back();
                vals.push_back(cur->val);
                if (cur->right)
                    dq.push_front(cur->right);
                if (cur->left)
                    dq.push_front(cur->left);

            }

        }
        leftToRight = !leftToRight;
        ans.push_back(vals);
    }
    return ans;
}

3.二叉树的右视图

解读一下题意,这道题就是把二叉树每层的最后一个数写出来,用层次遍历再合适不过了!

vector<int> rightSideView(TreeNode* root) {
    vector<int>ans;
    if (root == nullptr)  return ans;
    queue<TreeNode*>q;
    q.push(root);
    while (!q.empty())
    {
        int size = q.size();
        for (int i = 0; i < size; i++)
        {
            TreeNode* cur = q.front();
            q.pop();
            if (cur->left)q.push(cur->left);
            if (cur->right)q.push(cur->right);
            if (i == size - 1) ans.push_back(cur->val);
        }

    }
    return ans;
}

做完这道题,我们是不是发现,只要会了层次遍历的模板,相关的题目只需要进行一些改进,就可以秒杀了!毕竟二叉树的层次遍历不像dfs那样使用了太多递归,直观上我们更容易理解。

但实际上很多题目使用dfs和bfs都可以解决,比如上面的题目。使用dfs同样可以求解。
前序遍历的遍历顺序是中左右,也就是说,如果求二叉树的左视图的话,前序遍历直接拿下了。但是求右视图的话,我们把遍历顺序改成 中右左,是不是就可以实现了呢?本题的关键在于 深度相同的情况下,右子树是最先被访问的。 按照这个规律就可以写出dfs的代码了。

void dfs(TreeNode* root, int depth,vector<int>&ans) {
    if (root == nullptr)  return;
    if (depth == ans.size()) ans.push_back(root->val);
    depth++;
    dfs(root->right, depth, ans);
    dfs(root->left, depth, ans);
}

vector<int> rightSideView(TreeNode* root) {
    vector<int>ans;
    dfs(root, 0, ans);
    return ans;

}

四、更多习题

1.二叉树中倒数第二小的节点
这道题目可以直接遍历整个二叉树求解,记录二叉树去重后的值,排个序得到第二小。但是我们要用到题目中的关键信息,这并不是一个普通的二叉树。他有一个限制条件。root.val = min(root.left.val, root.right.val) 总成立。 如下图所示,我们可以得出一个信息:根节点是最小的节点! 那么第二小的节点会出现在哪里呢?有两种情况。

1.可能出现在和根节点相同的子树的子树中。如果在子树中找到与根节点不同的节点,储存下来,遍历找到不同节点中的最小值。
2可能是第二层的较大节点。不可能出现在较大节点的子树中,因为对于第二层的较大节点,它相当于子树的根节点,这个子树最小的节点就是根节点。

在这里插入图片描述

int answer = -1;
void dfs(TreeNode* root, int cur) {
    if (!root) return;
    if (root->val != cur) {
        if (answer == -1) answer = root->val;
        else answer = min(answer,root->val);
        return;
    }
    else
    {
        dfs(root->left, cur);
        dfs(root->right, cur);
    }
}

int findSecondMinimumValue(TreeNode* root) {
 
    dfs(root, root->val);
    return answer;

}

2.二叉树的直径

这道题目的重点是,可以不经过根节点。如果经过的话,那就是左右子树的深度值和,但是不经过的话,有可能是子节点的左右子树深度之和,所以需要做一个判断。

int maxDepth(TreeNode* root,int& ans){
        if(!root)   return 0;
        int l = maxDepth(root->left,ans);
        int r = maxDepth(root->right,ans);
        ans = max(ans,r+l);
        return max(r,l)+1;
    }

    int diameterOfBinaryTree(TreeNode* root) {
        int ans = 0;
        maxDepth(root,ans);
        return ans;

3.从中序与后序遍历序列构造二叉树

这道题目可以很好的考察对二叉树遍历算法的掌握程度。首先我们需要知道的是,后序遍历最后一个一定是根节点!
中序遍历:左中右
后序遍历:左右中
我们可以发现了,只要知道中序遍历中根节点的位置,就可以知道左右子树的数量来,因为这个根节点把中序遍历的数组分为左子树集合和右子树集合。那么知道中序遍历中左子树的数量之后,后序遍历的开头就对应了左子树的元素,这样我们就得到了数量一样,顺序也许不同的左子树的中序后序遍历数组,同时也得到了右子树对应的遍历数组。到这里就可以进行递归得出整个树的形状啦!代码很好理解,加上终止条件即可。

TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {

    if (inorder.size() == 0) return nullptr;
    TreeNode* root = new TreeNode(postorder[postorder.size() - 1]);
    int num = 0;

    for (auto in : inorder) if (in == root->val) break; else num++;

    postorder.pop_back();
    //cout << num<<endl;
    vector<int> inorder1(inorder.begin(), inorder.begin() + num);

    vector<int> inorder2(inorder.begin() + num+1, inorder.end());
    for (auto in : inorder1) { cout << in << " "; } cout << endl;
    for (auto in : inorder2) { cout << in << " "; }
    vector<int> postorder1(postorder.begin(), postorder.begin() + num);
    vector<int> postorder2(postorder.begin()+num, postorder.end());
    /*for (auto in : postorder1) { cout << in << " "; }cout << endl;
    for (auto in : postorder2) { cout << in << " "; }*/
    root->left = buildTree(inorder1, postorder1);
    root->right = buildTree(inorder2, postorder2);

    return root;
}

做完这道题目,类似的题目都可以求解啦。但是我们发现一个问题,上面的的代码性能并不好,因为每层递归定义了新的vector(就是数组),既耗时又耗空间,这时候我们需要对代码进行优化。其实可以每次递归不定义新的数组,只记录左右边界的索引,这样的话也可以解决问题,还能大大节省时间。代码太长了,就不贴出来了。

五、二叉搜索树

二叉搜索树也是一个重要的知识点,二叉搜索树是一个有序树。它满足下面几个条件:

1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.它的左、右子树也分别为二叉排序树。

先来一个简单题,了解一下BST的性质。

1.二叉搜索树中的搜索
这道题只需要明白,查找的值比当前节点小,那就查右子树,如果比当前节点大,那就查左子树,直到找到目标值。如果二叉树不存在目标值,那么返回空节点。代码如下:

TreeNode* searchBST(TreeNode* root, int val) {
        if (!root || root->val == val) return root;
        TreeNode* result = nullptr;
        if (root->val > val) result = searchBST(root->left, val);
        else if (root->val < val) result = searchBST(root->right, val);
        return result;
    }

2.验证二叉搜索树
判断是否为BST,只需要满足上面的三个条件。我们需要注意的是,左子树的所有节点都要小于根节点,右子树所有节点都大于根节点。下面的二叉树就不满足这个条件。虽然它对于每个节点都有根节点大于左孩子,小于右孩子,但是不满足根节点的左右子树所有节点都满足标准。所以写代码时候要警惕这种情况。
在这里插入图片描述
二叉搜索树其实还有一个重要的特性:他的中序遍历是一个递增的序列。 利用这个特性,就很容易求解。

void dfs(TreeNode* root,vector<int>& ans)
{
    if (!root) return;
    dfs(root->left, ans);
    ans.push_back(root->val);
    dfs(root->right, ans);
}


bool isValidBST(TreeNode* root) {
    vector<int>ans;
    dfs(root, ans);
    for (int i = 0; i < ans.size() - 1; i++)
    {
        if (ans[i] >= ans[i + 1]) return 0;
    }
    return 1;
}

3.二叉搜索树中第K小的元素

  • 26
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值