通关算法题之 ⌈二叉树⌋ 下

填充每个节点的下一个右侧节点指针

116、填充每个节点的下一个右侧节点指针

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

示例:

img
输入:root = [1,2,3,4,5,6,7]
输出:[1,##,2,3,##,4,5,6,7,##]

这道题若使用常规的遍历二叉树的话,只能将一个节点的左子树指向右子树,不同节点的子树之间无法建立联系,所以要改变遍历的方式。传统的 traverse 函数是遍历二叉树的所有节点,但现在我们想遍历的其实是两个相邻节点之间的「空隙」,所以可以在二叉树的基础上进行抽象,把图中的每一个方框看做一个节点:

img

解法一:递归遍历二叉树

这样,一棵二叉树被抽象成了一棵三叉树,三叉树上的每个节点就是原先二叉树的两个相邻节点

现在,我们只要实现一个 traverse 函数来遍历这棵三叉树,每个「三叉树节点」需要做的事就是把自己内部的两个二叉树节点穿起来。这样,traverse 函数遍历整棵「三叉树」,将所有相邻节的二叉树节点都连接起来,也就避免了我们之前出现的问题,把这道题完美解决。

class Solution {
public:
    Node* connect(Node* root) {
        if(!root) return root;
        traverse(root->left, root->right);
        return root;
    }
    // 三叉树遍历框架
    void traverse(Node* node1, Node* node2){
        if(!node1 || !node2){
            return;
        }
        node1->next = node2;
        traverse(node1->left, node1->right);
        traverse(node2->left, node2->right);
        traverse(node1->right, node2->left);
    }
};

解法二:层序遍历

在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点。

填充每个节点的下一个右侧节点指针.drawio.png

核心代码逻辑为:

  • 遍历到每一层的第一个节点时,用pre记录该节点,然后节点出队,node记录pre;

  • 向后每遍历一个节点,用node记录该节点,然后节点出队,pre指向node,pre指针向后移动一位;

  • 该层最后一个节点指向nullptr;

class Solution {
public:
    Node* connect(Node* root) {
        queue<Node*> que;
        if (root != NULL) que.push(root);
        while (!que.empty()) {
            int size = que.size();
            Node* pre;
            Node* node;
            for (int i = 0; i < size; i++) {
                if (i == 0) {
                    pre = que.front(); // 取出一层的头结点
                    que.pop();
                    node = pre;
                } else {
                    node = que.front();
                    que.pop();
                    pre->next = node; // 本层前一个节点next指向本节点
                    pre = pre->next;
                }
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            pre->next = NULL; // 本层最后一个节点指向NULL
        }
        return root;
    }
};

二刷:

class Solution {
public:
    Node* connect(Node* root) {
        queue<Node*> que;
        if(root) que.push(root);
        while(!que.empty()){
            int size = que.size();
            Node* pre;
            for(int i = 0; i < size; i++){
                Node* node = que.front();
                que.pop();
                if(i == 0){
                    pre = node;
                }else{
                    pre->next = node;
                    pre = node;
                }
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
            pre->next = NULL;
        }
        return root;
    }
};

二叉树序列化

297. 二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

示例:

img
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]

使用拼接字符串的方式把二叉树序列化

class Codec {
public:

    // 序列化
    string serialize(TreeNode* root) {
        if(!root) return "##";
        return to_string(root->val) + " " + serialize(root->left) + " " + serialize(root->right);
    }

     TreeNode* build(istringstream& iss){
        string tmp;
        iss>>tmp;
        if(tmp == "##") return NULL;
        TreeNode* root = new TreeNode(stoi(tmp));
        root->left = build(iss);
        root->right = build(iss);
        return root;
    }
    // 反序列化
    TreeNode* deserialize(string data) {
        istringstream iss(data);
        return build(iss);
    }
};

652、寻找重复的子树

给定一棵二叉树 root,返回所有重复的子树。

对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。

如果两棵树具有相同的结构和相同的结点值,则它们是重复的。

示例

img
输入:root = [1,2,3,4,null,2,4,null,null,4]
输出:[[2,4],[4]]

如何知道以某个节点为根的子树是不是重复的,是否应该加入结果列表中,需要知道什么信息呢?

你需要知道以下两点

1、以我为根的这棵二叉树(子树)长啥样?——后序遍历

2、以其他节点为根的子树都长啥样?——利用哈希表存起来做比较

现在,明确了要用后序遍历,那应该怎么描述一棵二叉树的模样呢?二叉树的前序/中序/后序遍历结果可以描述二叉树的结构。所以,可以通过拼接字符串的方式把二叉树序列化

class Solution {
public:
    unordered_map<string, int> map;
    vector<TreeNode*> res;
    
    vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
		traverse(root);
		return res;
    }
    //定义:输入一个节点,将以该节点为根节点的二叉树序列化,返回序列化后的字符串
    string traverse(TreeNode* root){
		if(!root) return "##";
       	string left = traverse(root->left);
        string right = traverse(root->right);
        // 后序位置获取二叉树序列化后的字符串
        string str = left + "," + right + "," + to_string(root->val);
        if(map[str] == 1){
            res.push_back(root);
        }
        map[str]++;
        return str;
    }
};

构造二叉树

😶‍🌫️二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树

617、合并二叉树

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。返回合并后的二叉树。

注意: 合并过程必须从两个树的根节点开始。

示例:

img
输入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出:[3,4,5,5,4,null,7]
class Solution {
public:
    //定义:输入两二叉树根节点,返回二叉树处理后的根节点
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if(!root1) return root2;
        if(!root2) return root1;
        root1->val += root2->val; 
        root1->left = mergeTrees(root1->left, root2->left);
        root1->right = mergeTrees(root1->right, root2->right);
        return root1;
    }
};

654、构造最大二叉树

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边子数组前缀上 构建左子树。
  3. 递归地在最大值 右边子数组后缀上 构建右子树。

返回 nums 构建的最大二叉树

示例

输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
               6
             /  \
            3    5
             \   /      
              2 0   
               \
                1

首先遍历数组把找到最大值 maxVal,从而把根节点 root 构造出来,然后对 maxVal 左边的数组和右边的数组进行递归构建,作为 root 的左、右子树。

//定义二叉树
struct TreeNode{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x):val(x), left(nullptr), right(nullptr){}
}

class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
		return build(nums, 0, nums.size() - 1);
    }
    
    //定义:输入数组和区间左右端点,返回构造的最大二叉树的根节点
    TreeNode* build(vector<int>& nums, int lo, int hi){
        if(lo > hi){
            return nullptr;
        }
        // 找到数组中的最大值和对应的索引
        // 注意不能写成 int rootVal = 0;
        int rootVal = INT32_MIN;
        int index = 0;
        for(int i = lo; i <= hi; i++){
			if(rootVal < nums[i]){
                rootVal = nums[i];
                index = i;
            }
        }
        TreeNode* root = new TreeNode(rootVal);
        root->left = build(nums, lo, index - 1);
        root->right = build(nums, index + 1, hi);
        return root;
    }
};

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

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的先序遍历inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
img

前序遍历preorder 和 中序遍历inorder 数组中的元素分布有如下特点:

image-20220417151011413

找到根节点是很简单的,前序遍历的第一个值 preorder[0] 就是根节点的值。关键在于如何通过根节点的值,将 preorderpostorder 数组划分成两半,构造根节点的左右子树?

暴力方法是利用for循环遍历整个数组找出index,但是通过 for 循环遍历的方式去确定 index 效率不算高,可以进一步优化:采用哈利表的查到 rootVal 对应的 index,数组元素无序且不重复,故采用 unordered_map来实现,底层由哈希表实现。

本道题的一大难点在于如何确定左右数组对应的起始索引和终止索引,这个可以通过左子树的节点数推导出来,假设左子树的节点数为 leftSize,那么 preorder 数组上的索引情况是这样的:

image-20220417151152530

做这种题目一定要画示意图,选择闭区间,若索引填错了,就会引起栈溢出的错误。

class Solution {
public:
    unordered_map<int, int> map;
    
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
		int size = inorder.size();
        //遍历数组构造哈希表
        for(int i = 0; i < size; i++){
            map[inorder[i]] = i;
        }
        return build(preorder, 0, size - 1, inorder, 0, size - 1);
    }
    //定义:输入两个数组和区间左右端点,输入构造的二叉树的根节点
    TreeNode* build(vector<int>& preorder, int preStart, int preEnd, 
                    vector<int>& inorder, int inStart, int inEnd){
    	if(preStart > preEnd){
            return nullptr;
        }
        //根节点的值,注意不能写成 int rootVal = preorder[0];
        int rootVal = preorder[preStart];
        //利用哈希表查询到 rootVal 在 inorder 中的索引 index
        int index = map[rootVal];
        
        int leftSize = index - inStart;
        
        TreeNode* root = new TreeNode(rootVal);
        root->left = build(preorder, preStart + 1, preStart + leftSize, 
                          inorder, inStart, index - 1);
    	root->right = build(preorder, preStart + leftSize + 1, preEnd,
                           inorder, index + 1, inEnd);
    	return root;
    }
};

106、从中序与后序遍历序列构造二叉树

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]
img

postoderinorder 对应的状态如下:

image-20220417161545340
class Solution {
public:
    unordered_map<int, int> map;
    
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
		int size = inorder.size();
        //遍历数组构造哈希表
        for(int i = 0; i < size; i++){
            map[inorder[i]] = i;
        }
        return build(inorder, 0, size - 1, postorder, 0, size - 1);
    }
    //定义:输入两个数组和区间左右端点,输入构造的二叉树的根节点
    TreeNode* build(vector<int>& inorder, int inStart, int inEnd, 
                    vector<int>& postorder, int postStart, int postEnd){
    	if(postStart > postEnd){
            return nullptr;
        }
        //根节点的值
        int rootVal = postorder[postEnd];
        //利用哈希表查询到 rootVal 在 inorder 中的索引 index
        int index = map[rootVal];
        
        int leftSize = index - inStart;
        
        TreeNode* root = new TreeNode(rootVal);
        root->left = build(inorder, inStart, index - 1, 
                          postorder, postStart, postStart + leftSize - 1);
    	root->right = build(inorder, index + 1, inEnd,
                           postorder, postStart + leftSize, postEnd - 1);
    	return root;
    }
};

889、根据前序和后序遍历构造二叉树

给定两个整数数组,preorderpostorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。

如果存在多个答案,您可以返回其中 任何 一个。

输入:preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1]
输出:[1,2,3,4,5,6,7]
img

这道题和前两道题有一个本质的区别:

通过前序中序,或者后序中序遍历结果可以确定一棵原始二叉树,但是通过前序后序遍历结果无法确定原始二叉树。这道题,你可以确定根节点,但是无法确切的知道左右子树有哪些节点,以下是一种构造方法:

1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。

2、然后把前序遍历结果的第二个元素作为左子树的根节点的值。

3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可。

假设前序遍历的第二个元素是左子树的根节点,但实际上左子树有可能是空指针,那么这个元素就应该是右子树的根节点。由于这里无法确切进行判断,所以导致了最终答案的不唯一。

image-20220417163947215
class Solution {
public:
    unordered_map<int, int> map;
    
    TreeNode* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {
		int size = postorder.size();
        //遍历数组构造哈希表
        for(int i = 0; i < size; i++){
            map[postorder[i]] = i;
        }
        return build(preorder, 0, size - 1, postorder, 0, size - 1);
    }
    
    //定义:输入两个数组和区间左右端点,输入构造的二叉树的根节点
    TreeNode* build(vector<int>& preorder, int preStart, int preEnd, 
                    vector<int>& postorder, int postStart, int postEnd){
    	if(postStart > postEnd){
            return nullptr;
        }
        if(postStart == postEnd){
            return new TreeNode(postorder[postStart]);
        }
        //根节点的值
        int rootVal = preorder[preStart];
        int leftRootVal = preorder[preStart + 1];
        //利用哈希表查询到左子树根节点 leftRoot 在 postorder 中的索引 index
        int index = map[leftRootVal];
        
        int leftSize = index - postStart + 1;
        
        TreeNode* root = new TreeNode(rootVal);
        root->left = build(preorder, preStart + 1, preStart + leftSize, 
                          postorder, postStart, index);
    	root->right = build(preorder, preStart + leftSize + 1, preEnd,
                           postorder, index + 1, postEnd - 1);
    	return root;
    }
};

层序遍历

102、二叉树的层序遍历

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

102.二叉树的层序遍历

需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。

while循环用来从上到下遍历二叉树的每一层,for循环用来从左到右遍历每一层当中的节点。

image-20220418183400018
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<vector<int>> result;
        while (!que.empty()) {
            int size = que.size();
            vector<int> vec;
            // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(vec);
        }
        return result;
    }
};

107、二叉树的层次遍历II

给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

107.二叉树的层次遍历II

相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<vector<int>> result;
        while (!que.empty()) {
            int size = que.size();
            vector<int> vec;
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(vec);
        }
        reverse(result.begin(), result.end()); // 在这里反转一下数组即可
        return result;
    }
};

103、 二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

示例:

img
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]

这题和 ⌈102. 二叉树的层序遍历⌋ 类似,只是要控制遍历方向,另外使用双向队列deque来记录遍历路径path。

解法一:使用数组的深度奇偶变化来控制遍历顺序

class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> res;
        queue<TreeNode*> que;
        if(root) que.push(root);
        int depth = 0;
        while(!que.empty()){
            int size = que.size();
            // 双向队列记录遍历路径
            deque<int> path;
            depth++;
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                // 深度为奇数则从左到右
                if(depth % 2 != 0) path.push_back(node->val);
                // 深度为偶数则从右到左
                else path.push_front(node->val);
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
            res.push_back(vector<int>{path.begin(), path.end()});
        }
        return res;
    }
};

解法二:用一个布尔变量 flag 控制遍历方向

class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> res;
        queue<TreeNode*> que;
        bool flag = true;
        if(root) que.push(root);
        while(!que.empty()){
            int size = que.size();
            // 双向队列记录遍历路径
            deque<int> path;
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                // flag为true则从左到右
                if(flag == true) path.push_back(node->val);
                // flag为
                else path.push_front(node->val);
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
            flag = !flag;
            res.push_back(vector<int>{path.begin(), path.end()});
        }
        return res;
    }
};

515、在每个树行中找最大值

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

515.在每个树行中找最大值

解法一:递归遍历二叉树

我们先来思考这样一个问题:如果把根节点看做第 1 层,如何打印出每一个节点所在的层数?

traverse遍历函数的参数当中只包含TreeNode* root 可以吗?**不行!**还要加上节点所在的层数level,因为在递归遍历的过程中,遍历左子树接着遍历右子树,层数leve并不是一直增加,同一层的节点level是一样的,所以不能定义全局变量,而是要放在traverse函数参数当中维护。

如果要在遍历二叉树的过程中获取每个节点所在的层数,则要在traverse遍历函数的参数当中添加层数level

// 定义:二叉树遍历函数,输入一个节点和该节点所在的层数
void traverse(TreeNode root, int level) {
    if (root == null) {
        return;
    }
    // 前序位置
    printf("节点 %s 在第 %d 层", root, level);
    //注意如下代码中的 level + 1
    traverse(root.left, level + 1);
    traverse(root.right, level + 1);
}

// 人为设定根节点位于第1层
traverse(root, 1);

回到本题当中,本质上是一个求节点最大值的问题,但问题是如何保证参与比大小求最值的节点来自于同一层呢

🥳在遍历二叉树的过程中获取每一个节点所在的层数,比较层数就可以了呀,也就是说这是一个涉及二叉树层数的问题,要在traverse遍历函数的参数当中添加层数level

假设二叉树的层数从第0层开始算,res数组所以默认从0开始:

  • 假设数组遍历到二叉树第2层某节点,而res数组当中只有2个元素(已获取前两层的最大值),即level == res.size(),表示res数组当中无该层任何节点,将该节点添加到res数组中
image-20220419102205366
  • 假设数组遍历到二叉树第2层某节点,而res数组当中只有3个元素(已获取前三层的最大值),表示该层已经有节点被访问了,则比大小取最值
image-20220419102248364

所以也就是说,核心代码逻辑要使用if语句分以上两种情况讨论。

版本一:

class Solution {
public:
	vector<int> res;
    void traverse(TreeNode* root,int level){
        if(root == NULL){
            return;
        }
        if(level == res.size()){//每层第一个节点
            res.push_back(root->val);
        } else{					//每层第一个节点之外的节点
            res[level] = max(res[level], root->val);
        } 
        traverse(root->left, level + 1);
        traverse(root->right, level + 1);
    }
    vector<int> largestValues(TreeNode* root) {
        //二叉树层数从第0层开始算
        traverse(root,0);
        return res;
    }
};

版本二:

class Solution {
public:
    vector<int> res;
    int depth = 0;
    vector<int> largestValues(TreeNode* root) {
        traverse(root);
        return res;
    }

    void traverse(TreeNode* root){
        if(!root) return;
        if(depth == res.size()) res.push_back(root->val);
        else{
            res[depth] = max(res[depth], root->val);
        }
        depth++;
        traverse(root->left);
        traverse(root->right);
        depth--;
    }
};

注意:这个版本的代码会把根节点当成第一层,但是本题是要将根节点当作第0层,所以主要代码逻辑要写在depth++;前面,或者初始化int depth = -1

解法二:层序遍历

层序遍历,取每一层的最大值。

  • while循环开始遍历每一层时,创建最大值maxValue
  • for循环遍历该层每一个节点,顺便更新maxValue
  • 退出for循环,该层所有节点遍历完成,maxValue放入结果数据result中;
class Solution {
public:
    vector<int> largestValues(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<int> result;
        while (!que.empty()) {
            int size = que.size();
            int maxValue = INT_MIN; // 取每一层的最大值
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                maxValue = node->val > maxValue ? node->val : maxValue;
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(maxValue); // 把最大值放进数组
        }
        return result;
    }
};

199、二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

199.二叉树的右视图

解法一:递归遍历

要找到每一层最右边的节点,涉及二叉树节点所在层数的问题,还是使用 ⌈515、在每个树行中找最大值⌋ 中的遍历框架。

注意:由于本题是要找每层最右侧节点,所以先遍历右子树再遍历左子树,这样每层遍历时首先访问到的就是要求的节点。

class Solution {
public:
    vector<int> res;

    vector<int> rightSideView(TreeNode* root) {
        traverse(root, 0);
        return res;
    }

    void traverse(TreeNode* root, int level){
        if(!root) return;
		//这一层还没有记录值,说明 root 就是右侧视图的第一个节点
        if(res.size() == level){
            res.push_back(root->val);
        }
		// 注意,这里反过来,先遍历右子树再遍历左子树
        // 这样,首先遍历的一定是右侧节点
        traverse(root->right, level + 1);
        traverse(root->left, level + 1);
    }
};

那如果是要求二叉树的左视图呢,那就先遍历左子树再遍历右子树呗,又AC了一题😜。

解法二:层序遍历

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
		queue<TreeNode*> que;
        vector<int> res;
        if(root) 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)){
                    res.push_back(node->val);
                }
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
        }
        return res;
    }
};

637、二叉树的层平均值

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。

示例 :

img
输入:root = [3,9,20,null,null,15,7]
输出:[3.00000,14.50000,11.00000]

层序遍历轻松完成,注意:先求一层所有节点的和,最后再作除法。

class Solution {
public:
    vector<double> averageOfLevels(TreeNode* root) {
        vector<double> res;
        queue<TreeNode*> que;
        if(root) que.push(root);
        while(!que.empty()){
            int size = que.size();
            // 记录当前层所有节点之和
            double sum = 0;
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                sum += node->val;
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
            // 记录当前行的平均值
            res.push_back(sum / size);
        }
        return res;
    }
};

二叉树的所有路径

257、二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

示例:

img
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

遍历二叉树,这道题和 ⌈515、在每个树行中找最大值⌋ 在思路上很相似——回溯,遍历函数 traverse 的参数中还需要加入路径 path。再说的通俗一点儿就是,左右子树都需要对路径path进行操作,所以左子树操作完需要回溯,右子树才能再操作,不然右子树操作的就是左子树操作完的结果。

class Solution {
public:
    // 记录所有从根节点到叶子节点的路径
    vector<string> res;
	string path;
    vector<string> binaryTreePaths(TreeNode* root) {
        // 记录 traverse 函数递归时的路径
        traverse(root, path);
        return res;
    }
    
    void traverse(TreeNode* root, string path){
        if(!root) return;
        path += to_string(root->val);
        //遍历到叶子节点,路径path加入res数组
        if(!root->left && !root->right){
            res.push_back(path);
        }
        traverse(root->left, path + "->");
        traverse(root->right, path + "->");
    }
};

最近公共祖先

何为最近公共祖先呢?

如果一个节点能够在它的左右子树中分别找到pq,则该节点为LCA节点

先来实现一个简单的算法:输入一棵没有重复元素的二叉树根节点root和目标值val1val2,写一个函数寻找树中值为val1val2的节点。

//定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode* find(TreeNode* root, int val1, int val2){
    if(!root) return nullptr;
    // 前序位置,在每一个节点处判断
    if(root->val == val1 || root->val == val2) return root;
    //左右子树寻找
    TreeNode* left = find(root->left, val1, val2);
    TreeNode* right = find(root->right, val1, val2);
    // 后序位置,已经知道左右子树是否存在目标值
	return left != nullptr ? left : right;
}

最近公共祖先系列问题的解法都是把这个函数作为框架的

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

给你输入一棵不含重复值的二叉树,以及存在于树中的两个节点pq,请你计算pq的最近公共祖先节点。

最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。

示例:

640 640 (1)

find函数的后序位置,如果发现leftright都非空,就说明当前节点是LCA节点,即解决了第一种情况;

find函数的前序位置,如果找到一个值为val1val2的节点则直接返回,恰好解决了第二种情况。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        return find(root, p->val, q->val);
    }
    // 在二叉树中寻找 val1 和 val2 的最近公共祖先节点
    TreeNode* find(TreeNode* root, int val1, int val2){
        if(!root) return nullptr;
        // 前序位置,在每一个节点处判断,遇到目标值立即返回
        if(root->val == val1 || root->val == val2) return root;
        //左右子树寻找
        TreeNode* left = find(root->left, val1, val2);
        TreeNode* right = find(root->right, val1, val2);
        // 后序位置,已经知道左右子树是否存在目标值
        if(left && right) return root;
        return left != nullptr ? left : right;
    }
};

因为题目说了pq一定存在于二叉树中(这点很重要),所以即便我们遇到q就直接返回,根本没遍历到p,也依然可以断定pq底下,q就是LCA节点。

1676、二叉树的最近公共祖先 IV

给定一棵二叉树的根节点 root 和 TreeNode 类对象的数组(列表) nodes,返回 nodes 中所有节点的最近公共祖先(LCA)。数组(列表)中所有节点都存在于该二叉树中,且二叉树中所有节点的值都是互不相同的。

示例 3:

在这里插入图片描述
输入: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [7,6,2,4]
输出: 5
解释: 节点 7624 的最近公共祖先节点是 5

依然给你输入一棵不含重复值的二叉树,但这次不是给你输入pq两个节点了,而是给你输入一个包含若干节点的列表nodes(这些节点都存在于二叉树中),其实换汤不换药,还是使用这个框架。

为了高效地查找数组nodes中的元素,使用哈希表来装载数组元素,unordered_set 来实现。

class Solution {
public:
	unordered_set<int> set; 
    TreeNode* lowestCommonAncestor(TreeNode* root, vector<TreeNode*> &nodes) {
    	for(TreeNode* node: nodes){
            set.insert(node->val);
        }
        return find(root);
    }
    
    // 在二叉树中寻找 val 的最近公共祖先节点
    TreeNode* find(TreeNode* root){
        if(!root) return nullptr;
        // 前序位置,在每一个节点处判断,遇到目标值立即返回
        if(set.count(root->val)) return root;
        //左右子树寻找
        TreeNode* left = find(root->left);
        TreeNode* right = find(root->right);
        // 后序位置,已经知道左右子树是否存在目标值
        if(left && right) return root;
        return left != nullptr ? left : right;
    }
};

不过需要注意的是,这两道题的题目都明确告诉我们这些节点必定存在于二叉树中,如果没有这个前提条件,就需要修改代码了。

1644、二叉树的最近公共祖先 II

给定一棵二叉树的根节点 root,返回给定节点 p 和 q 的最近公共祖先(LCA)节点。如果 p 或 q 之一不存在于该二叉树中,返回 null。树中的每个节点值都是互不相同的。

在解决标准的最近公共祖先问题时,我们在find函数的前序位置有这样一段代码:

// 前序位置
if (root.val == val1 || root.val == val2) {
    // 如果遇到目标值,直接返回
    return root;
}

因为pq都存在于树中,所以这段代码恰好可以解决最近公共祖先的第二种情况:

640 (1)

但对于这道题来说,pq不一定存在于树中,所以不能遇到一个目标值就直接返回,而应该对二叉树进行完全搜索(遍历每一个节点),如果发现pq不存在于树中,那么是不存在LCA的。

哪种写法能够对二叉树进行完全搜索呢?只需要把前序位置的判断逻辑放到后序位置即可:

TreeNode find(TreeNode root, int val) {
    if (root == null) {
        return null;
    }
    // 先去左右子树寻找
    TreeNode left = find(root.left, val);
    TreeNode right = find(root.right, val);
    // 后序位置,判断 root 是不是目标节点
    if (root.val == val) {
        return root;
    }
    // root 不是目标节点,再去看看哪边的子树找到了
    return left != null ? left : right;
}

同时还要使用bool变量记录一下pq 是否存在于二叉树中,若有一个不存在,则返回NULL;若两者都存在,则找到了LCA

class Solution{
    // 用于记录 p 和 q 是否存在于二叉树中
    bool findP = false;
    bool findQ = false;
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		if(!findP || !findQ) return nullptr;
        // p 和 q 都存在二叉树中,才有公共祖先
        return find(root, p->val, q->val);
	}
	
	// 在二叉树中寻找 val1 和 val2 的最近公共祖先节点
	TreeNode* find(TreeNode* root, int val1, int val2){
        if(!root) return nullptr;
        TreeNode* left = find(root->left, val1, val2);
        TreeNode* right = find(root->right, val1, val2);
		// 后序位置,判断当前节点是不是 LCA 节点
        if(left && right) return root;
        // 后序位置,判断当前节点是不是目标值
        if(root->val == val1 || root->val == val2){
            if(root->val == val1) findP = true;
            if(root->val == val2) findQ = true;
            return root;
        }
        return left != nullptr ? left : right;
    }
};

235、二叉搜索树的最近公共祖先

输入一棵不含重复值的二叉搜索树,以及存在于树中的两个节点pq,请计算pq的最近公共祖先节点。

把之前的解法代码复制过来肯定也可以解决这道题,但没有用到 BST「左小右大」的性质,显然效率不是最高的。

在标准的最近公共祖先问题中,我们要在后序位置通过左右子树的搜索结果来判断当前节点是不是LCA

TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);

// 后序位置,判断当前节点是不是 LCA 节点
if (left != null && right != null) {
    return root;
}

但对于 BST 来说,根本不需要老老实实去遍历子树,由于 BST 左小右大的性质,将当前节点的值与val1val2作对比,即可判断当前节点是不是LCA

  • 假设val1 < val2,那么val1 <= root->val <= val2,则说明当前节点就是LCA

  • root->valval1还小,则需要去值更大的右子树寻找LCA

  • root->valval2还大,则需要去值更小的左子树寻找LCA

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 保证 val1 较小,val2 较大
        int val1 = min(p->val, q->val);
        int val2 = max(p->val, q->val);
        return find(root, val1, val2);
    }
    // 在 BST 中寻找 val1 和 val2 的最近公共祖先节点
	TreeNode* find(TreeNode* root, int val1, int val2){
        if(!root) return nullptr;
        // 当前节点太大,去左子树找
        if(root->val > val2) return find(root->left, val1, val2);
        // 当前节点太小,去右子树找
        if(root->val < val1) return find(root->right, val1,val2);
        // val1 <= root.val <= val2
    	// 则当前节点就是最近公共祖先
        return root;
    }
};

1650、二叉树的最近公共祖先 III

给定一棵二叉树中的两个节点 pq,返回它们的最近公共祖先节点(LCA)。每个节点都包含其父节点的引用(指针),Node 的定义如下:

class Node {
    int val;
    Node* left;
    Node* right;
    Node* parent;
};

这道题其实不是公共祖先的问题,而是单链表相交的问题,你把parent指针想象成单链表的next指针,题目就变成了:给你输入两个单链表的头结点pq,这两个单链表必然会相交,请你返回相交点。

image-20220424150259400

解法一:双指针法

class Solution {
public:
    Node* lowestCommonAncestor(Node* p, Node * q) {
		// 链表双指针技巧
    	node* a = p;
        node* b = q;
        while(a != b){
            // a 走一步,如果走到根节点,转到 q 节点
            if(!a) a = q;
            else a = a->parent;
            // b 走一步,如果走到根节点,转到 p 节点
            if(!b) b = p;
            else b = b->parent;
        }
        return a;
    };

解法二:哈希表

class Solution {
public:
    Node* lowestCommonAncestor(Node* p, Node * q) {
        unordered_set<int> sign;
        // 将节点p的元素存入哈希表
        while(p!=NULL){
            sign.insert(p->val);
            p=p->parent;
        }
        // 节点q的元素在哈希表中查询,第一次出现的即LCA
        while(q!=NULL){
            if(sign.count(q->val)){
                return q;
            }
            q=q->parent;
        }
        return NULL;
    }
};

二叉搜索树

二叉搜索树(Binary Search Tree,后文简写 BST)有如下特性:

1、对于 BST 的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。

2、对于 BST 的每一个节点 node,它的左侧子树和右侧子树都是 BST。

二叉搜索树并不算复杂,它可以算是数据结构领域的半壁江山,直接基于 BST 的数据结构有AVL 树红黑树等等,拥有了自平衡性质,可以提供 logN级别的增删查改效率;还有 B+ 树,线段树等结构都是基于 BST 的思想来设计的。

从做算法题的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)

也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来:

void traverse(TreeNode* root) {
    if (!root) return;
    traverse(root->left);
    // 这里添加中序遍历代码
    traverse(root->right);
}

那么根据这个性质,我们来做两道算法题。

BST 相关的问题,要么利用 BST 左小右大的特性提升算法效率,要么利用中序遍历的特性满足题目的要求。

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

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

示例:

img
输入:root = [3,1,4,null,2], k = 1
输出:1

最直接的思路就是升序排序,然后找第 k 个元素,BST 的中序遍历其实就是升序排序的结果。

class Solution {
public:
    // 记录当前元素的排名
    int count = 0;
    int res;
    int kthSmallest(TreeNode* root, int k) {
        // 利用 BST 的中序遍历特性
        traverse(root, k);
        return res;
    }

    void traverse(TreeNode* root, int k){
        if(!root) return;
        traverse(root->left, k);
        /* 中序遍历代码位置 */
        count++;
        // 找到第 k 小的元素
        if(count == k){
            res = root->val;
            return;
        }
        traverse(root->right, k);
    }
};

538. 把二叉搜索树转换为累加树

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

示例:

img
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]

BST 的中序遍历代码可以升序打印节点的值,那降序打印节点的值怎么办呢?

只要把递归顺序改一下就行了:

void traverse(TreeNode* root) {
    if (!root) return;
    traverse(root->right);
    // 这里添加中序遍历代码
    traverse(root->left);
}

这段代码可以降序打印 BST 节点的值,如果维护一个外部累加变量 sum,然后把 sum 赋值给 BST 中的每一个节点,不就将 BST 转化成累加树了吗?

class Solution {
public:
    //记录累加和
    int sum = 0;
    TreeNode* convertBST(TreeNode* root) {
        traverse(root);
        return root;
    }

    void traverse(TreeNode* root){
        if(!root) return;
        traverse(root->right);
        // 维护累加和
        sum += root->val;
        // 将 BST 转化成累加树
        root->val = sum;
        traverse(root->left);
    }
};

530. 二叉搜索树的最小绝对差

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

差值是一个正数,其数值等于两值之差的绝对值。

示例:

img
输入:root = [4,2,6,1,3]
输出:1

中序遍历会有序遍历 BST 的节点,遍历过程中计算最小差值即可。因为要做差,所以需要维护一个pre指针来记录上一个节点。

class Solution {
public:
    int res = INT_MAX;
    TreeNode* pre = nullptr;
    int getMinimumDifference(TreeNode* root) {
        traverse(root);
        return res;
    }

    void traverse(TreeNode* root){
        if(!root) return;
        traverse(root->left);
        //这里的if(pre)判断条件不能掉
        if(pre) res = min(root->val - pre->val, res);
        pre = root;
        traverse(root->right);
    }
};

501. 二叉搜索树中的众数

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有众数(即出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

示例:

img
输入:root = [1,null,2,2]
输出:[2]

首先,需要统计节点出现的频率,维护一个pre指针:

  • pre为空,则为第一个节点;
  • pre->val == root->val,则频率加1;
  • pre->val != root->val,则出现新的节点,频率为1;

然后,与最大频率作比较:

  • 若频率等于最大频率,节点加入数组;
  • 若频率大于最大频率,则更新最大数值,清空数组后加入该节点;
class Solution {
public:
    int count = 0;
    int maxCount = 0;
    TreeNode* pre = nullptr;
    vector<int> res;

    vector<int> findMode(TreeNode* root) {
        traverse(root);
        return res;
    }

    void traverse(TreeNode* root){
        if(!root) return;
        traverse(root->left);
        // 统计root节点出现的频率
        if(!pre) count = 1;						//第一个节点
        else if(pre->val == root->val) count++;	//与前一个节点值相等
        else count = 1;							//与前一个节点值不等
        pre = root;
        
        // 和最大频率作比较,判断是否加入数组
        if(count == maxCount) res.push_back(root->val);
        else if(count > maxCount){
            maxCount = count;
            res.clear();
            res.push_back(root->val);
        }
        traverse(root->right);
    }
};

剑指 Offer 36. 二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

以下面的二叉搜索树为例:

img

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

img

特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

  1. 排序链表: 节点应从小到大排序,因此应使用 中序遍历 “从小到大”访问树的节点。
  2. 双向链表: 在构建相邻节点的引用关系时,设前驱节点 pre 和当前节点 cur ,不仅应构建 pre->right = cur ,也应构建 cur->left = pre
  3. 循环链表: 设链表头节点 head 和尾节点 tail ,则应构建 head->left = tailtail->right = head
Picture1.png
class Solution {
public:
    Node* head = NULL;
    Node* pre = NULL;
    Node* treeToDoublyList(Node* root) {
        if(!root) return head;
        traverse(root);
        head->left = pre;
        pre->right = head;
        return head;
    }

    void traverse(Node* root){
        if(!root) return;
        traverse(root->left);
        if(pre){
            pre->right = root;
            root->left = pre;
        }else{
            head = root;
        }
        pre = root;
        traverse(root->right);
    }
};

BST 的完整定义如下:

1、BST 中任意一个节点的左子树所有节点的值都小于该节点的值,右子树所有节点的值都大于该节点的值。

2、BST 中任意一个节点的左右子树都是 BST。

有了 BST 的这种特性,就可以在二叉树中做类似二分搜索的操作,搜索一个元素的效率很高。

比如下面这就是一棵合法的二叉树:

img

对于 BST 相关的问题,经常会看到类似下面这样的代码逻辑:

void BST(TreeNode* root, int target) {
    if (root->val == target)
        // 找到目标,做点什么
    if (root->val < target) 
        BST(root->right, target);
    if (root->val > target)
        BST(root->left, target);
}

这个代码框架其实和二叉树的遍历框架差不多,无非就是利用了 BST 左小右大的特性而已。

98. 验证二叉搜索树

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

示例:

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

解法一:中序遍历,需要比较节点和上一个节点的大小关系,所以引入指针pre

class Solution {
public:
    TreeNode* pre = nullptr; //记录前一个节点
    bool isValidBST(TreeNode* root) {
        // 二叉搜索树也可以为空
        if(!root) return true;
        bool left = isValidBST(root->left);
        if(pre && pre->val >= root->val) return false;
        pre = root;
        bool right = isValidBST(root->right);
        return left && right;
    }
};

解法二:二叉树定义,左小右大

这里有一个陷阱:BST 不是左小右大么,那我只要检查 root->val > root->left->valroot->val < root->right->val 不就行了?

这样是错误的,因为 BST左小右大的特性是指 root->val 要比左子树的所有节点都更大,要比右子树的所有节点都小,只检查左右两个子节点当然是不够的。

正确解法是通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉搜索树算法的一个小技巧吧。

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return valid(root, nullptr, nullptr);
    }

    bool valid(TreeNode* root, TreeNode* min, TreeNode* max){
        if(!root) return true;
        if(min && root->val <= min->val) return false;
        if(max && root->val >= max->val) return false;
        return valid(root->left, min, root) && valid(root->right, root, max);
    }
};

669. 修剪二叉搜索树

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例:

img
输入:root = [1,0,2], low = 1, high = 2
输出:[1,null,2]

明确了递归函数的定义之后进行思考,如果一个节点的值没有落在 [lo, hi] 中,有两种情况:

1、root.val < lo,这种情况下 root 节点本身和 root 的左子树全都是小于 lo 的,都需要被剪掉,返回root 的右子树;

2、root.val > hi,这种情况下 root 节点本身和 root 的右子树全都是大于 hi 的,都需要被剪掉, 返回root 的左子树;

class Solution {
public:
    // 定义:删除 BST 中小于 low 和大于 high 的所有节点,返回结果 BST
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if(!root) return root;
        // 直接返回 root.right,等于删除 root 以及 root 的左子树
        if(root->val < low) return trimBST(root->right, low,high);
        // 直接返回 root.left,等于删除 root 以及 root 的右子树
        if(root->val > high) return trimBST(root->left, low, high);
        // 闭区间 [lo, hi] 内的节点什么都不做
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        return root;
    }
};

700. 二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

示例:

img
输入:root = [4,2,7,1,3], val = 2
输出:[2,1,3]

解法一:二叉树定义,左小右大

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if(!root) return root;
        //比目标值大,到左子树寻找
        if(root->val > val) return searchBST(root->left, val);
        //比目标值小,到右子树找
        if(root->val < val) return searchBST(root->right, val);
        //等于目标值,返回节点
        return root;
    }
};

解法二:中序遍历

class Solution {
public:
    TreeNode* res;
    TreeNode* searchBST(TreeNode* root, int val) {
        traverse(root, val);
        return res;
    }

    void traverse(TreeNode* root, int val){
        if(!root) return;
        traverse(root->left, val);
        if(root->val == val){
            res = root;
            return;
        }
        traverse(root->right, val);
    }
};

701. 二叉搜索树中的插入操作

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树,返回插入后二叉搜索树的根节点。 输入数据 保证新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果

示例:

img
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]

如果要递归地插入或者删除二叉树节点,递归函数一定要有返回值,而且返回值要被正确的接收。

插入的过程可以分两部分:

1、寻找正确的插入位置,类似 700. 二叉搜索树中的搜索

2、把元素插进去,这就要把新节点以返回值的方式接到父节点上。

class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        // 找到空位置插入新节点
        if(!root) return new TreeNode(val);
        if(root->val > val) root->left = insertIntoBST(root->left, val);
        if(root->val < val) root->right = insertIntoBST(root->right, val);
        return root;
    }
};

450. 删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变,返回二叉搜索树(有可能被更新)的根节点的引用。

跟插入操作类似,先「找」再「改」,先把框架写出来:

TreeNode* deleteNode(TreeNode* root, int key) {
    if (root->val == key) {
        // 找到啦,进行删除
    } else if (root->val > key) {
        // 去左子树找
        root->left = deleteNode(root->left, key);
    } else if (root.val < key) {
        // 去右子树找
        root->right = deleteNode(root->right, key);
    }
    return root;
}

删除比插入和搜索都要复杂一些,分三种情况:

情况 1A 恰好是末端节点,两个子节点都为空,那么直接删除:

img
if (root->left == null && root->right == null)
    return null;

情况 2A 只有一个非空子节点,那么它要让这个孩子接替自己的位置。

img
// 排除了情况 1 之后
if (root->left == null) return root->right;
if (root->right == null) return root->left;

情况 3A 有两个子节点,麻烦了,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己,下列为第二种方式。

img

if (root->left != null && root->right != null) {
    // 找到右子树的最小节点
    TreeNode* minNode = getMin(root->right);
    // 把 root 改成 minNode
    root->val = minNode->val;
    // 转而去删除 minNode
    root->right = deleteNode(root->right, minNode->val);
}

三种情况分析完毕,填入框架,简化一下代码:

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if(!root) return root;
        if(root->val == key){
        	// 这两个 if 把情况 1 和 2 都正确处理了
            if(!root->left) return root->right;
            if(!root->right) return root->left;
        	// 处理情况 3
        	// 获得右子树最小的节点
            TreeNode* minNode = getMin(root->right);
            // 删除右子树最小的节点
            root->right = deleteNode(root->right, minNode->val);
            // 用右子树最小的节点替换 root 节点
            minNode->left = root->left;
            minNode->right = root->right;
            root = minNode;
        }else if(root->val > key){
            root->left = deleteNode(root->left, key);
        }else if(root->val < key){
            root->right = deleteNode(root->right, key);
        }
        return root;
    }

    TreeNode* getMin(TreeNode* root){
        // BST 最左边的就是最小的
        while(root->left) root = root->left;
        return root;
    }
};

96. 不同的二叉搜索树

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

示例:

img
输入:n = 3
输出:5

dp[i]:1到i为节点组成的二叉搜索树的个数为dp[i],也可以理解是i的不同元素节点组成的二叉搜索树的个数为dp[i],都是一样的。

dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]j相当于是头结点的元素,从1遍历到i为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]j - 1j为头结点的左子树节点数量,i - j 为以j为头结点的右子树节点数量。

初始化:从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,所以初始化dp[0] = 1

class Solution {
public:
   int numTrees(int n) {
       vector<int> dp(n + 1);
       dp[0] = 1;
       for (int i = 1; i <= n; i++) {
           for (int j = 1; j <= i; j++) {
               dp[i] += dp[j - 1] * dp[i - j];
           }
       }
       return dp[n];
   }
};

95. 不同的二叉搜索树 II

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1n 互不相同的不同 二叉搜索树 ,可以按 任意顺序 返回答案。

示例:

img
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]

想要构造出所有合法 BST,分以下三步:

1、穷举 root 节点的所有可能;

2、递归构造出左右子树的所有合法 BST;

3、给 root 节点穷举所有左右子树的组合。

class Solution {
public:
    vector<TreeNode*> generateTrees(int n) {
        if(n == 0) return {};
        return build(1, n);
    }
    //定义:构造闭区间 [lo, hi] 组成的 BST
    vector<TreeNode*> build(int lo, int hi){
        vector<TreeNode*> res;
        if(lo > hi){
            res.push_back(nullptr);
            return res;
        }
        //1、穷举root节点所有可能
        for(int i = lo; i <= hi; i++){
            // 2、递归构造出左右子树的所有合法 BST
            vector<TreeNode*> leftTree = build(lo, i - 1);
            vector<TreeNode*> rightTree = build(i + 1, hi);
            // 3、给 root 节点穷举所有左右子树的组合
            for(TreeNode* left: leftTree){
                for(TreeNode* right: rightTree){
                    // i 作为根节点 root 的值
                    TreeNode* root = new TreeNode(i);
                    root->left = left;
                    root->right = right;
                    res.push_back(root);
                }
            }
        }
        return res;
    }
};

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

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

示例:

img
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]

二叉树的构建问题很简单,说白了就是:构造根节点,然后构建左右子树。

一个有序数组对于 BST 来说就是中序遍历结果,根节点在数组中心,数组左侧是左子树元素,右侧是右子树元素。

class Solution {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        return build(nums, 0, nums.size() - 1);
    }

    TreeNode* build(vector<int>& nums, int left, int right){
        if(left > right) return nullptr;
        int mid = (left + right + 1) / 2;
        TreeNode* root = new TreeNode(nums[mid]);
        root->left = build(nums, left, mid - 1);
        root->right = build(nums, mid + 1, right);
        return root;
    }
};

109、有序链表转换二叉搜索树

给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。

示例:

img
输入: head = [-10,-3,0,5,9]
输出: [0,-3,9,-10,null,5]

链表和数组相比的一个关键差异是无法通过索引快速访问元素,所以这题有几个思路:

1、把链表转化成数组,然后直接复用 108. 将有序数组转换为二叉搜索树 的解法。

2、稍微改写 108. 将有序数组转换为二叉搜索树 的解法,用 单链表的六大解题套路 说到的双指针方法获取链表的中点,时间复杂度略高一些。

3、如果深刻理解二叉树算法,可以利用中序遍历的特点写出最优化的解法。

解法一:中序遍历,分冶的思想

class Solution {
public:
    TreeNode* buildTree(ListNode* &p, int left, int right) {
        if (left > right) {
            return nullptr;
        }
        int mid = (left + right + 1) / 2;
        TreeNode* leftTree = buildTree(p, left, mid - 1);
        TreeNode* root = new TreeNode(p->val);
        p = p->next;
        TreeNode* rightTree = buildTree(p, mid + 1, right);
        root->left = leftTree;
        root->right = rightTree;
        return root;
    }

    TreeNode* sortedListToBST(ListNode* head) {
        int len = 0;
        for(ListNode* ptr = head; ptr != nullptr; ptr = ptr->next){
            len++;
        }
        return buildTree(head, 0, len - 1);
    }
};

解法二双指针获取链表的中点

class Solution {
public:
    TreeNode* sortedListToBST(ListNode* head) {
        return build(head, nullptr);
    }

    // 把链表左闭右开区间 [begin, end) 的节点构造成 BST
    TreeNode* build(ListNode* begin, ListNode* end){
        if(begin == end) return nullptr;
        ListNode* mid = getMid(begin, end);
        TreeNode* root = new TreeNode(mid->val);
        root->left = build(begin, mid);
        root->right = build(mid->next, end);
        return root;
    }

    // 获取链表左闭右开区间 [begin, end) 的中心节点
    ListNode* getMid(ListNode* begin, ListNode* end){
        ListNode* slow = begin;
        ListNode* fast = begin;
        while(fast != end && fast->next != end){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};

剑指 Offer 33. 二叉搜索树的后序遍历序列

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

参考以下这颗二叉搜索树:

     5
    / \
   2   6
  / \
 1   3

示例:

输入: [1,6,3,2,5]
输出: false
  • 利用二叉搜索树左大右小的性质划分左右子树:

    遍历后序遍历的[start, end]区间的元素,寻找第一个大于根节点的节点,索引记为 midIndex 。此时,可划分出左子树区间 [start, midIndex - 1],右子树区间[midIndex, end - 1]、根节点索引 end

  • 判断是否为二叉搜索树:
    左子树区间 [start, midIndex - 1]内的所有节点都应 < postorder[end] 。而第 1.划分左右子树 步骤已经保证左子树区间的正确性,因此只需要判断右子树区间即可。
    右子树区间 [midIndex, end - 1] 内的所有节点都应 > postorder[j] 。实现方式为遍历,当遇到 ≤postorder[j]的节点则跳出;则可通过index == end判断是否为二叉搜索树。

  • 返回值:

    所有子树都需正确才可判定正确,因此使用 与逻辑符 && 连接。

class Solution {
public:
    bool traversal(vector<int>& postorder, int start, int end) {
        /* 递归终止条件 */
        if(start > end) return true;
        int index = start;
        /* 中间处理逻辑 */
        while(postorder[index] < postorder[end]) index++;
        /* 记录分割点 */
       int midIndex = index;
        while(postorder[index] > postorder[end]) index++;
        /* 递归左右子树 */
        bool left = traversal(postorder, start, midIndex - 1);
        bool right = traversal(postorder, midIndex, end - 1);
        return index == end && left && right;
    }

    bool verifyPostorder(vector<int>& postorder) {
        return traversal(postorder, 0, postorder.size() - 1);
    }
};
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

海岸星的清风

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

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

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

打赏作者

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

抵扣说明:

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

余额充值