Leetcode Hot 100 —— 二叉树 part02

108.将有序数组转换为二叉搜索树

根据数组构造一棵二叉树。本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。

因为是有序数组构造二叉搜索树,寻找分割点就比较容易了。分割点就是数组中间位置的节点。

如果数组长度为偶数,中间节点有两个,取哪一个?

取哪一个都可以,只不过构成了不同的平衡二叉搜索树。

例如:输入:[-10,-3,0,5,9]

如下两棵树,都是这个数组的平衡二叉搜索树,取左边元素就是树1,取右边元素就是树2:

递归三部曲:

1、确定递归函数返回值及其参数

cpp 复制代码
// 左闭右闭区间[left, right]
TreeNode* traversal(vector<int>& nums, int left, int right)

nums 是传入的有序整数数组,left 和 right 是数组索引,用于表示当前递归处理的子数组范围。

2、确定递归终止条件

这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。

3、确定单层递归的逻辑

① 首先取中间位置,然后开始以中间位置的元素构造节点:
TreeNode* root = new TreeNode(nums[mid]);

② 接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。

③ 最后返回root节点。

一定要明确区间边界,本题取左闭右闭!!!
初始调用: traversal(nums, 0, nums.size() - 1) 表示整个数组范围,从第一个元素(索引 0)到最后一个元素(索引 nums.size()-1)都参与构建。

递归分割:

取中间索引mid = left + (right - left) / 2,用 nums[mid] 构建当前根节点。

左子树的构建范围是 [left, mid-1](包含 left 到 mid-1 的所有元素)。

右子树的构建范围是 [mid+1, right](包含 mid+1 到 right 的所有元素)。

终止条件: 当 left > right 时,表示当前区间没有任何元素(例如左子区间 [left, mid-1] 中如果 left 比 mid-1 大,说明没有元素),返回 nullptr。

核心代码:

cpp 复制代码
class Solution {
private:
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if (left > right) return nullptr;
        int mid = left + ((right - left) / 2);
        TreeNode* root = new TreeNode(nums[mid]);
        root->left = traversal(nums, left, mid - 1);
        root->right = traversal(nums, mid + 1, right);
        return root;
    }
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        TreeNode* root = traversal(nums, 0, nums.size() - 1);
        return root;
    }
};

注意:
在调用traversal的时候传入的left和right为什么是0和nums.size() - 1,因为定义的区间为左闭右闭。

1、注意是nums[mid],是数组元素!!不是mid。

98. 验证二叉搜索树


思路与解法

中序遍历下,输出的二叉搜索树节点的数值是有序序列。

有了这个特性,验证二叉搜索树,就相当于变成了判断中序序列是不是递增的即可。

解法1:

可以递归中序遍历将二叉搜索树转变成一个数组,代码如下:

cpp 复制代码
vector<int> vec;
void traversal(TreeNode* root) {
    if (root == NULL) return;
    traversal(root->left);
    vec.push_back(root->val); // 将二叉搜索树转换为有序数组
    traversal(root->right);
}

然后只要比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素。

cpp 复制代码
traversal(root);
for (int i = 1; i < vec.size(); i++) {
    // 注意要小于等于,搜索树里不能有相同元素
    if (vec[i] <= vec[i - 1]) return false;
}
return true;

整体代码:

cpp 复制代码
class Solution {
public:
    void traversal(TreeNode* root, vector<int>& vec){
        if(root==nullptr) return;
        traversal(root->left,vec);
        vec.push_back(root->val);
        traversal(root->right,vec);        
    }

    bool isValidBST(TreeNode* root) {
        vector<int> result;
        traversal(root,result);
        for(int i=1;i<result.size();i++){
            if(result[i]<=result[i-1]) return false;
        }
        return true;       
    }
};

【注】

1、注意i从1开始,因为需要比较当前元素vec[i]与前一个元素vec[i-1],检查该序列是否严格递增

2、注意要小于等于,搜索树里不能有相同元素!!

解法2:

以上代码中,我们把二叉树转变为数组来判断,是最直观的,但其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。这里不讲了。

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

核心代码:

cpp 复制代码
class Solution {
public:
    void traversal(TreeNode* root, vector<int>& vec){
        if(root==nullptr) return;
        traversal(root->left,vec);
        vec.push_back(root->val);
        traversal(root->right,vec);        
    }

   int kthSmallest(TreeNode* root, int k) {
        vector<int> result;
        traversal(root,result);
        return result[k-1];
    }
};

【注】

1、与上一题一样,递归中序遍历将二叉搜索树转变成一个数组后,return result[k-1];即可。

199. 二叉树的右视图


思路与解法

从右侧看二叉树,我们看到的实际上是每一层最右边的节点。因此,问题转化为按层遍历二叉树,并记录每一层最后一个节点。

因此可以使用广度优先搜索方法(BFS),使用队列进行层序遍历。

对于每一层,记录该层最后一个节点的值。

核心代码:

cpp 复制代码
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> results;
        if(root==nullptr) return results;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()){
            int size = que.size();
            for(int i=0;i<size;i++){
                TreeNode* node = que.front();
                que.pop();
                if(i==size-1) results.push_back(node->val);
                if(node->left)  que.push(node->left);
                if(node->right) que.push(node->right);               
            }
        }
        return results;       
    }
};

【注】

1、 for(int i=0;i<size;i++){

是size,一定不能写成que.size( )了!!!

114. 二叉树展开为链表


思路与解法

使用迭代法,每次操作中,我们将左子树整体插入到当前节点和原右子树之间。因为前序遍历的顺序是:根 → 左子树 → 右子树。所以将左子树移到右边,并将原右子树接在左子树后面,正好保持了前序顺序。

步骤(假设当前节点为 cur,且其左子树非空):

1、找到左子树最右节点:这个节点是左子树中"最右边"的节点,也就是在左子树的前序遍历中最后一个被访问的节点。

2、将原右子树接到左子树最右节点的右边:这样原右子树的所有节点就会在左子树之后被访问。

3、将左子树整体移到当前节点的右边:即 cur->right = cur->left,然后 cur->left = nullptr。

4、移动 cur 指针:令 cur = cur->right(left已经移动过了,为空了),继续处理下一个节点。

上图是示例演示,中间是cur=1时的处理(找到4,然后把右子树5-6移动到4后面),右边是cur=2时的处理,当cur运动到3开始,都没有左子树,直接移动到最后结束。

核心代码:

cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        TreeNode* cur=root;
        while(cur){
            if(cur->left){
                 TreeNode *pre = cur->left;
                 while(pre->right){
                    pre=pre->right;
                 }
                 pre->right = cur->right;
                 cur->right = cur->left;
                 cur->left = nullptr;
            }
            cur = cur->right;
        } 
    }
};

【注】

1、注意if和while
if(cur->left){

存在左子树才需要进行指针调整,再定义pre=cur->left
while(pre->right){:定位左子树的最后一个节点

105. 从前序与中序遍历序列构造二叉树

先看从中序与后序遍历序列构造二叉树。

依次确定,首先要确定根节点

后序左右中,因此最后一个元素一定是中节点。

以后序数组的最后一个元素为切割点,先切中序数组(根据中分离出左---右---中),根据中序数组,反过来再切后序数组(此时后续的左右中的右也是左右中的顺序)。一层一层切下去,每次后序数组最后一个元素就是节点元素。

流程如图:

如上图,先看后序,最后一个数字是3,所以根节点是3,再用3去分割中序(通过索引从0开始找),9是左,15 20 7是右,然后再看后序,按照中序分割后得到的左右的元素个数(左1个,右3个)分离出后序的左和右,以右子树为例,15 7 20也是按后序排列,因此20是右子树的根节点。依此类推。

那么代码应该怎么写呢?

说到一层一层切割,就应该想到了递归。

来看一下一共分几步:

第一步:如果数组大小为零的话,说明是空节点了。

第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。

第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点

第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)

第五步:切割后序数组(根据上一步的中序左数组和中序右数组各自的元素个数 ),切成后序左数组和后序右数组

第六步:递归处理左区间和右区间

不难写出如下代码:(先把框架写出来)

cpp 复制代码
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {

    // 第一步
    if (postorder.size() == 0) return NULL;

    // 第二步:后序遍历数组最后一个元素,就是当前的中间节点
    int rootValue = postorder[postorder.size() - 1];
    TreeNode* root = new TreeNode(rootValue);

    // 叶子节点,提前终止递归,直接返回当前叶子节点
    if (postorder.size() == 1) return root;

    // 第三步:找切割点
    int Index;
    for (Index = 0; Index < inorder.size(); Index++) {
        if (inorder[Index] == rootValue) break;
    }

    // 第四步:切割中序数组,得到 中序左数组和中序右数组
    // 第五步:切割后序数组,得到 后序左数组和后序右数组

    // 第六步
    root->left = traversal(中序左数组, 后序左数组);
    root->right = traversal(中序右数组, 后序右数组);

    return root;
}

难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。

此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。

在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套!

在数组中强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。

首先要切割中序数组,为什么先切割中序数组呢?

切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必须先切割中序数组。

中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割,如下代码中我坚持左闭右开的原则

cpp 复制代码
// 找到中序遍历的切割点
int Index;
for (Index = 0; Index < inorder.size(); Index++) {
    if (inorder[Index] == rootValue) break;
}

// 左闭右开区间:[0, Index)
vector<int> leftInorder(inorder.begin(), inorder.begin() + Index);
// [Index + 1, end)
vector<int> rightInorder(inorder.begin() + Index + 1, inorder.end() );

接下来就要切割后序数组了。

首先后序数组的最后一个元素指定不能要了,这是切割点也是当前二叉树中间节点的元素,已经用了。

后序数组的切割点怎么找?

后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。

此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

代码如下:

cpp 复制代码
// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了
postorder.resize(postorder.size() - 1);

// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size)
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。

接下来可以递归了,代码如下:

cpp 复制代码
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);

整体思路:
终止条件(看后序,因为后序先确定根节点)------通过后序找到根节点------找切割点------切割中序------要删除末尾再切割后序------递归

做的时候得画图,看纸质笔记!!

整体代码:

cpp 复制代码
class Solution {
private:
    TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
        if (postorder.size() == 0) return NULL;

        // 后序遍历数组最后一个元素,就是当前的中间节点
        int rootValue = postorder[postorder.size() - 1];
        TreeNode* root = new TreeNode(rootValue);

        // 叶子节点,提前终止递归,直接返回当前叶子节点
        if (postorder.size() == 1) return root;

        // 找到中序遍历的切割点
        int Index;
        for (Index = 0; Index < inorder.size(); Index++) {
            if (inorder[Index] == rootValue) break;
        }

        // 切割中序数组
        // 左闭右开区间:[0, Index)
        vector<int> leftInorder(inorder.begin(), inorder.begin() + Index);
        // [Index + 1, end)
        vector<int> rightInorder(inorder.begin() + Index + 1, inorder.end() );

        // postorder 舍弃末尾元素
        postorder.resize(postorder.size() - 1);

        // 切割后序数组
        // 依然左闭右开,注意这里使用了左中序数组大小作为切割点
        // [0, leftInorder.size)
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        // [leftInorder.size(), end)
        vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

        root->left = traversal(leftInorder, leftPostorder);
        root->right = traversal(rightInorder, rightPostorder);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return NULL;
        return traversal(inorder, postorder);
    }
};

【注】

1、Index一定要定义在for循环外面,因为后面要用!!

2、参数一定要是迭代器!

下面看从前序与中序遍历序列构造二叉树。

整体思路:
终止条件(看前序,因为前序先确定根节点)------通过前序找到根节点------找切割点------切割中序------切割前序(不用删末尾了)------递归

cpp 复制代码
class Solution {
public:
    TreeNode* traversal(vector<int>& preorder, vector<int>& inorder){
        if(preorder.size()==0) return nullptr;
        
        int rootValue = preorder[0];
        TreeNode* root = new TreeNode(rootValue);

        if(preorder.size()==1) return root;

        int index;
        for(index=0;index<inorder.size();index++){
            if(inorder[index]==rootValue) break;
        }

        vector<int> leftInorder(inorder.begin(),inorder.begin()+index);
        vector<int> rightInorder(inorder.begin()+index+1,inorder.end());   

        vector<int> leftPreorder(preorder.begin()+1,preorder.begin()+1+leftInorder.size());
        vector<int> rightPreorder(preorder.begin()+1+leftInorder.size(),preorder.end());

        root->left = traversal(leftPreorder, leftInorder);
        root->right= traversal(rightPreorder, rightInorder);

        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(inorder.size()==0||preorder.size()==0) return nullptr;
        return traversal(preorder, inorder);
    }
};

437. 路径总和 III

规定路径方向必须向下

思路与解法

这道题目要求我们计算二叉树中所有路径和等于给定目标值 targetSum 的路径数量。可以利用前缀和 + 哈希表的思路来优化算法。

关键点分析

1、路径的定义:路径不一定从根节点开始,也不一定在叶子节点结束,路径必须是从父节点到子节点进行传递。

2、前缀和的思路:前缀和是一种在数组或树中计算区间和的技巧,可以通过记录路径的前缀和来减少冗余计算,避免重复遍历子树。

前缀和的思想

我们可以通过在遍历树的过程中维护当前路径的前缀和,并使用哈希表记录每个前缀和出现的次数。具体的思想是:

1、在递归过程中,每当访问一个节点时,计算从当前节点到根节点的路径和(前缀和)。

2、如果当前前缀和减去目标值 targetSum 结果在哈希表中出现过,说明有路径和等于 targetSum,因为从某个先前的节点到当前节点的路径和就是 targetSum。

3、通过维护前缀和的哈希表,可以高效地统计满足条件的路径数。

解题步骤

1、前缀和的哈希表:使用哈希表 prefixSum 来记录每个前缀和出现的次数,初始时 prefixSum[0] = 1,表示从根节点开始就有一个路径和为 0。

2、深度优先搜索(DFS):通过递归方式遍历树的每个节点,在递归过程中维护当前路径的前缀和。每访问一个节点,就更新前缀和,并检查是否存在某个前缀和为 current_sum - targetSum。

3、路径计数:每当发现某个路径的和等于目标值时,就更新结果计数。

算法步骤

1、从根节点开始,递归地遍历每个节点。

2、对每个节点,计算从该节点到根节点的路径的前缀和。

3、使用哈希表记录出现过的前缀和,每次计算当前路径的前缀和时,检查 prefixSum[current_sum - targetSum] 是否存在,若存在,则路径和等于目标值,计数加一。

4、递归地访问左右子树,并更新前缀和。

cpp 复制代码
class Solution {
private:
    unordered_map<long,int> prefixCount; //前缀和(key) -> 出现次数(value)
    int count=0;                         //统计路径数目
    void traversal(TreeNode* node, long curSum,int targetSum){
        if(node==nullptr) return;
        curSum=curSum+node->val; //更新从该节点到根节点的路径的前缀和
        if(prefixCount.count(curSum-targetSum)){
            count+=prefixCount[curSum-targetSum];
        }
        prefixCount[curSum]++;
        traversal(node->left,curSum,targetSum);
        traversal(node->right,curSum,targetSum);
        prefixCount[curSum]--;              
    }
public:
    int pathSum(TreeNode* root, int targetSum) {
        prefixCount[0]=1;
        traversal(root,0,targetSum);
        return count;
    }
};

【注】

1、void traversal(TreeNode* node, long curSum,int targetSum){ curSum要定义成long,否则会溢出。

2、prefixCount[curSum]--; 回溯。当从当前节点返回父节点时,需要撤销当前节点对哈希表的影响,因为当前节点的前缀和只应该在该节点的子树范围内有效。

236. 二叉树的最近公共祖先

注意是最近!

遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。
那么二叉树如何可以自底向上查找呢?

回溯啊,二叉树回溯的过程就是从底到上。

后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。

接下来就看如何判断一个节点是节点q和节点p的公共祖先呢?

情况一: 最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。

判断逻辑 是如果递归遍历遇到q,就将q返回,遇到p就将p返回,那么如果左右子树的返回值都不为空,说明此时的中节点,一定是q和p的最近祖先。

情况二: 很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)

其实情况一和情况二代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。因为遇到q或者p就返回,这样也包含了q或者p本身就是公共祖先的情况。

核心代码:

递归三部曲:

1、确定递归函数返回值以及参数

需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。

但我们还要返回最近公共节点,可以利用题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。

cpp 复制代码
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)

2、确定终止条件

遇到空的话,因为树都是空了,所以返回空。

如果 root == q,或者 root == p,说明找到 q、p ,则将其返回。这个返回值后面在中节点的处理过程中会用到。

cpp 复制代码
if (root == q || root == p || root == NULL) return root;

3、确定单层递归逻辑

值得注意的是本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断。

如果递归函数有返回值,如何区分要搜索一条边 ,还是搜索整个树呢?

搜索一条边的写法:

cpp 复制代码
if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;

搜索整个树写法:

cpp 复制代码
left = 递归函数(root->left);   // 左
right = 递归函数(root->right); // 右
left与right的逻辑处理;          // 中 

看出区别了没?

在递归函数有返回值的情况下:
如果要搜索一条边 ,递归函数返回值不为空的时候,立刻返回。
如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)。

那么本题为什么要遍历整棵树呢 ?直观上来看,找到最近公共祖先,直接一路返回就可以了。

因为本题代码的后序遍历中,需要利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。

那么先用left和right接住左子树和右子树的返回值,代码如下:

cpp 复制代码
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);

如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解。

如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。

如果left和right都为空,则返回left或者right都是可以的,也就是返回空。

代码如下:

cpp 复制代码
// 如果左右都非空,说明 p 和 q 分别位于当前节点的两侧,当前节点就是 LCA(最近公共祖先)
        if (left != nullptr && right != nullptr) {
            return root;
        }
        // 否则返回非空的那一侧(可能为空,也可能包含 LCA)
        return left != nullptr ? left : right;

完整流程:

整体代码:

cpp 复制代码
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root==p||root==q||root==nullptr) return root;
        TreeNode* left=lowestCommonAncestor(root->left,p,q);
        TreeNode* right=lowestCommonAncestor(root->right,p,q);
        if(left!=nullptr&&right!=nullptr) return root;
        else return left!=nullptr?left:right;
    }
};

归纳如下三点:

1、求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历 (即:回溯)实现从底向上的遍历方式。

2、在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断

3、要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。

相关推荐
N1_WEB2 小时前
HDU:杭电 2017 复试真题汇总
算法
努力学算法的蒟蒻2 小时前
day111(3.13)——leetcode面试经典150
算法·leetcode·面试
参.商.2 小时前
【Day37】94.二叉树的中序遍历 递归+迭代遍历
leetcode·golang
爱学习的小囧2 小时前
VCF 9.0 操作对象与指标报告自动化教程
运维·服务器·算法·自动化·vmware·虚拟化
嫂子开门我是_我哥2 小时前
心电域泛化研究从0入门系列 | 第四篇:域泛化核心理论与主流方法——破解心电AI跨域失效难题
人工智能·算法·机器学习
Olivia_su2 小时前
数据分析及可视化Tableau自学入门
算法·数据分析·tableau
Sakinol#2 小时前
Leetcode Hot 100 —— 矩阵
leetcode·矩阵
天疆说2 小时前
【拓扑学+航天轨道动力学】同伦(Homotopy)概念解析
人工智能·算法·拓扑学
爱装代码的小瓶子2 小时前
【c++与Linux进阶】线程篇 -互斥锁
linux·c++·算法