【笔记】二叉树

本系列总计六篇文章,是 基于STL实现的笔试题常考七大基本数据结构 该文章在《代码随想录》和《labuladong的算法笔记》题目中的具体实践,每篇的布局是这样的:开头是该数据结构的总结,然后是在不同场景的应用,以及不同的算法技巧。本文是系列最后一篇,第六篇,介绍了树的相关题目,重点是要掌握二叉树、多叉树的构造、遍历(递归、非递归、层次),以及二叉树、二叉搜索树的属性,体会递归算法的本质是二叉树。

下面文章是在《代码随想录》和《labuladong的算法笔记》题目中的具体实践:
【笔记】数组
【笔记】链表
【笔记】哈希表
【笔记】字符串
【笔记】栈与队列
【笔记】二叉树

0、总结

  • 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序遍历,都是先构造中节点。
  • 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
  • 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。
  • 注意体会递归函数带返回值和不带返回值的区别

【笔记】二叉树_第1张图片

1、二叉树的构建

【ACM模式】根据输入的数组构造二叉树

力扣上如何自己构造二叉树输入用例? | 代码随想录 (programmercarl.com)

  • 思路,借助顺序存储方式

1、把输入的 int 数组,先转化为二叉树节点数组,切记root绑定到新数组首元素

2、遍历,根据规则给左、右孩子赋值。节点 i 的左孩子下标 2 * i + 1,右孩子下标2 * i + 2。for中条件 i * 2 + 1 < vec.size(),不然会漏掉 i * 2 + 2节点值

3、层序遍历,打印

#include 
using namespace std;

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

TreeNode* ConstructBinaryTree (const vector<int>& vec) {
    if (vec.size() == 0) return nullptr;
    vector<TreeNode*> vecTree (vec.size(), nullptr);
    TreeNode* root = nullptr;
    // 把输入的 int 数组,先转化为二叉树节点数组
    for (int i = 0; i < vec.size(); i++) {
        TreeNode* node = nullptr;
        if (vec[i] != -1) {
            node = new TreeNode(vec[i]);
        }
        vecTree[i] = node;
        if (i == 0) {
            root = node;
        }
        // cout << vec[i] << " ";
    }
    // cout << endl;
    // 遍历,根据规则给左、右孩子赋值
    for (int i = 0; i * 2 + 1 < vec.size(); i++) {
        if (vecTree[i] != nullptr) {
            vecTree[i]->left = vecTree[i * 2 + 1];
            if (i * 2 + 2 < vec.size())
            vecTree[i]->right = vecTree[i * 2 + 2];
        }
    }
    return root;
}

// 层序遍历,按每层打印输出
void PrintBinaryTree (TreeNode* root) {
    // 这一句多余了,遇见 -1 就直接返回了
    // if (root = nullptr) return;
    // 只有非空节点才入队列
    queue<TreeNode*> que;
    if (root != nullptr) que.push(root);
    // 总结果集
    vector<vector<int>> result;
    while (!que.empty()) {
        int size = que.size();
        // 每层的结果集
        vector<int> level;
        for (int i = 0; i < size; i++) {
            TreeNode* node = que.front();
            que.pop();
            if (node != nullptr) {
                level.push_back(node->val);
                que.push(node->left);
                que.push(node->right);
            } else {
                level.push_back(-1);
            }
        }
        result.push_back(level);
    }
    for (int i = 0; i < result.size(); i++) {
        for (int j = 0; j < result[i].size(); j++) {
            cout << result[i][j] << " ";
        }
        cout << endl;
    }
}

int main() {
    // 用 -1 来表示nullptr
    vector<int> vec = {4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8};
    TreeNode* root = ConstructBinaryTree(vec);
    PrintBinaryTree(root);
}

// 输入结果如下
// 4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8

// 打印结果如下
// 4 
// 1 6 
// 0 2 5 7 
// -1 -1 -1 3 -1 -1 -1 8 
// -1 -1 -1 -1 这四个多余的 -1 是叶子节点3和8的左右孩子

2、二叉树的遍历

层序遍历

借助队列,见上段代码的 PrintBinaryTree() 函数

学会二叉树的层序遍历,可以一口气打完以下十题:

  • 102.二叉树的层序遍历
  • 107.二叉树的层次遍历II
  • 199.二叉树的右视图
  • 637.二叉树的层平均值
  • 429.N叉树的层序遍历
  • 515.在每个树行中找最大值
  • 116.填充每个节点的下一个右侧节点指针
  • 117.填充每个节点的下一个右侧节点指针II
  • 104.二叉树的最大深度
  • 111.二叉树的最小深度

【递归】前、中、后序遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)

94. 二叉树的中序遍历 - 力扣(LeetCode)

145. 二叉树的后序遍历 - 力扣(LeetCode)

589. N 叉树的前序遍历 - 力扣(LeetCode)

590. N 叉树的后序遍历 - 力扣(LeetCode)

  • 最基础的递归,记住三部曲(后面会介绍回溯三步曲)
  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
  • 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

二叉树前序遍历

根->左->右

/**
 * 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<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        preorder(root, result);
        return result;
    }
    // 要修改原本的vec需要传入引用,否则只是对复制的vec进行修改,原本的vec依旧为空
    void preorder(TreeNode* root, vector<int>& vec) {
        if (root == nullptr) return;
        // 前序位置
        vec.push_back(root->val);
        preorder(root->left, vec);
        // 中序位置
        preorder(root->right, vec);
        // 后序位置
    }
};
N叉树前序/后序遍历
class Solution {
private:
    vector<int> result;
public:
    vector<int> preorder(Node* root) {
        if (root == NULL) return {};
        result.clear();
        traverse(root);
        return result;
    }
    void traverse(Node* root) {
        if (root == NULL) return;
        result.push_back(root->val);
        for (Node* node : root->children) {
            traverse(node);
        }
    }
};

中序遍历

左->根->右

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        inorder(root, result);
        return result;
    }
    void inorder(TreeNode* cur, vector<int>& vec) {
        if (cur == nullptr) return;
        inorder(cur->left, vec);
        // 中序位置
        vec.push_back(cur->val);
        inorder(cur->right, vec);
    }
};

后序遍历

左->右->根

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> result;
        postorder(root, result);
        return result;
    }
    void postorder(TreeNode* cur, vector<int>& vec) {
        if (cur == nullptr) return;
        postorder(cur->left, vec);
        postorder(cur->right, vec);
        // 后序位置
        vec.push_back(cur->val);
    }
};

【迭代】前、中、后序遍历

  • 计算机处理递归用函数调用栈,因此理论上任何一个递归程序都可以用栈来迭代处理

  • 会了前序就会后序(reverse),中序引入了指针,必须掌握。

前序遍历

先根入栈,然后注意入栈顺序是先右后左

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        if (root == nullptr) return{};
        vector<int> result;
        stack<TreeNode*> st;
        st.push(root);
        while (!st.empty()) {
            // 根
            TreeNode* node = st.top();
            result.push_back(node->val);
            st.pop();
            // 入栈是右左,才能保证输出是左右
            if (node->right) st.push(node->right);
            if (node->left) st.push(node->left);
            
        }
        return result;
    }
};

中序遍历

与前后续遍历的逻辑不同,注意指针先一路向左,然后处理中,然后向右。

这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        if (root == nullptr) return {};
        stack<TreeNode*> st;
        vector<int> result;
        // 引入指针处理当前要输出的元素,栈则保存已遍历过的元素
        TreeNode* cur = root;
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {
                st.push(cur);
                cur = cur->left; // 左
            } else {
                cur = st.top();
                st.pop();
                result.push_back(cur->val); // 中
                cur = cur->right; // 右
            }
        }
        return result;
    }
};

后序遍历

小trick,并不是真正左右根,而是借助先序遍历非递归根左右入栈,出栈结果是根右左,然后reverse成为左右根

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        if (root == nullptr) return{};
        vector<int> result;
        stack<TreeNode*> st;
        st.push(root);
        while (!st.empty()) {
            // 根
            TreeNode* node = st.top();
            result.push_back(node->val);
            st.pop();
            // 入栈是根左右,才能保证输出是根右左
            if (node->left) st.push(node->left);
            if (node->right) st.push(node->right);
        }
        reverse(result.begin(), result.end());
        return result;
    }
};

利用线索二叉树进行Morris遍历

为什么需要线索二叉树?

左右孩子节点不再空,分别指向其前驱和后继。知道了“前驱”和“后继”信息,就可以把二叉树看作一个链表结构,从而可以像遍历链表那样来遍历二叉树,进而提高效率

如何线索化二叉树?

这里例举了中序线索二叉树的方法

一个二叉树通过如下的方法“串起来”

所有原本为空的右(孩子)指针改为指向该节点在中序序列中的后继,所有原本为空的左(孩子)指针改为指向该节点的中序序列的前驱

是否有指向父节点的指针?

同理,通过指向父节点的指针,就可以把二叉树看作一个链表结构,从而可以像遍历链表那样来遍历二叉树,进而提高效率

3、二叉树的属性

101. 对称二叉树

101. 对称二叉树 - 力扣(LeetCode)

这道题目的本质是要比较两个树(这两个树是根节点的左右子树),遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。

同类型题目:

100.相同的树

572.另一个树的子树

剑指offer 28 对称的二叉树

  • 递归,后序遍历,构建辅助函数compare()同时操作两棵树进行比较,先给出所有分支判断的详细版本
  • 递归,比较先序遍历(根左右)和对称先序遍历(根右左)的结果是否相同
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if (root == nullptr) return true;
        return compare(root->left, root->right);
    }
    bool compare(TreeNode* left, TreeNode* right) {
        // 排除一空,一非空的情况
        if (left == nullptr && right != nullptr) return false;
        else if (left != nullptr && right == nullptr) return false;
        // 全空返回true
        else if (left == nullptr && right == nullptr) return true;
        // 都不空,但数值不同
        else if (left->val != right->val) return false;
        // 此时就是:左右节点都不为空,且数值相同的情况
        return compare(left->left, right->right) && compare(left->right, right->left);
    }
};
  • 迭代,层序遍历,两两入队,两两比较
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if (root == nullptr) return true;
        queue<TreeNode*> que;
        que.push(root->left);
        que.push(root->right);
        while (!que.empty()) {
            TreeNode* leftNode = que.front();
            que.pop();
            TreeNode* rightNode = que.front();
            que.pop();
            // 左节点为空、右节点为空,此时说明是对称的
            if (!leftNode && !rightNode) continue;
            // 一个空另一个不空,或者都不空但节点值不相同,说明不对称
            if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) return false;
            que.push(leftNode->left);   // 加入左节点左孩子
            que.push(rightNode->right); // 加入右节点右孩子
            que.push(leftNode->right);  // 加入左节点右孩子
            que.push(rightNode->left);  // 加入右节点左孩子
        }
        return true;
    }
};

104.二叉树最大深度

104. 二叉树的最大深度 - 力扣(LeetCode)

同类型题目:

559. n叉树的最大深度

剑指offer 55-1 二叉树的深度

  • 后序遍历求根节点的高度,即得深度
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        return max(maxDepth(root->left), maxDepth(root->right)) + 1;
    }
};
  • 也可层序遍历,逐层累加

  • 也可通过前序遍历真正求深度

class Solution {
public:
    int result = 0;
    void getdepth(TreeNode* node, int depth) {
        result = depth > result ? depth : result; // 中

        if (node->left == NULL && node->right == NULL) return ;

        if (node->left) { // 左
            depth++;    // 深度+1
            getdepth(node->left, depth);
            depth--;    // 回溯,深度-1
        }
        if (node->right) { // 右
            depth++;    // 深度+1
            getdepth(node->right, depth);
            depth--;    // 回溯,深度-1
        }
        return ;
    }
    int maxDepth(TreeNode* root) {
        if (root == NULL) return result;
        getdepth(root, 1);
        return result;
    }
};

111.二叉树的最小深度

111. 二叉树的最小深度 - 力扣(LeetCode)

  • 递归,后序遍历,和最大深度唯一不同是多了左右孩子不为空的处理情况。三种情况:左不空右空,左空右不空,都为空(到达叶子节点),每种都有不同的返回结果。
class Solution {
public:
    int minDepth(TreeNode* root) {
        if (root == NULL) return 0;
        int leftDepth = minDepth(root->left);           // 左
        int rightDepth = minDepth(root->right);         // 右
                                                        // 中
        // 当一个左子树为空,右不为空,这时并不是最低点
        if (root->left == NULL && root->right != NULL) { 
            return 1 + rightDepth;
        }   
        // 当一个右子树为空,左不为空,这时并不是最低点
        if (root->left != NULL && root->right == NULL) { 
            return 1 + leftDepth;
        }
        int result = 1 + min(leftDepth, rightDepth);
        return result;
    }
};
  • 层序遍历,BFS 算法和 DFS(回溯)算法的一大区别就是,BFS 第一次搜索到的结果是最优的,此题若是遍历到第一个叶子节点可以直接返回,这个得益于 BFS 算法的搜索逻辑。
class Solution {
public:
    int minDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        queue<TreeNode*> que;
        que.push(root);
        int result = 0;
        while (!que.empty()) {
            int size = que.size();
            result++;
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (!node->left && !node->right) return result;
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }
};

222.完全二叉树的节点个数

222. 完全二叉树的节点个数 - 力扣(LeetCode)

同类题目:

  • 后序遍历统计,这样子其实也可以应用到非完全二叉树上来
class Solution {
public:
    int countNodes(TreeNode* root) {
        // 后序遍历
        if (root == nullptr) return 0;
        return 1 + countNodes(root->left) + countNodes(root->right);
    }
};
  • 层次遍历直接秒杀,非完全二叉树也能做
  • 最好根据完全二叉树性质来进行数学计算,在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。

110.平衡二叉树

110. 平衡二叉树 - 力扣(LeetCode)

相似题目:

剑指offer 55-2 平衡二叉树

  • 后序遍历,判断当前节点是否满足要求,且判断该节点的左右节点是否满足要求
class Solution {
public:
    bool isBalanced(TreeNode* root) {
        if (root == nullptr) return true;
        return (abs(depth(root->left) - depth(root->right)) <= 1) && isBalanced(root->left) && isBalanced(root->right);
    }
    int depth(TreeNode* root) {
        if (root == nullptr) return 0;
        return 1 + max(depth(root->left), depth(root->right));
    }
};

257. 二叉树的所有路径(回溯)

257. 二叉树的所有路径 - 力扣(LeetCode)

  • 递归+回溯

回溯法,先序遍历。vectorpath记录所有路径,用int的理由是放方便回溯,不管每 路径收集的数字是几位数,总之一定是int,所以就一次 pop_back就可以回溯。如果到叶子节点了,将int转化为string之后,结果存入result。

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        if (root == nullptr) return {};
        vector<int> path;// 存放所有路径
        vector<string> result;// 存放符合要求的路径
        backtracking(root, path, result);
        return result; 
    }
    void backtracking(TreeNode* cur, vector<int>& path, vector<string>& result) {
        // 根
        path.push_back(cur->val);
        // 到达叶子节点时,将path转换后存入result
        if (!cur->left && !cur->right) {
            string sPath;
            for (int i = 0 ; i < path.size() - 1; ++i) {
                sPath += to_string(path[i]);
                sPath += "->";
            }
            sPath += to_string(path[path.size() - 1]);
            result.push_back(sPath);
            return;
        }
        // 左
        if (cur->left) {
            backtracking(cur->left, path, result);
            path.pop_back();
        }
        // 右
        if (cur->right) {
            backtracking(cur->right, path, result);
            path.pop_back();
        }
    }
};

404.左叶子之和

404. 左叶子之和 - 力扣(LeetCode)

  • 递归,后序遍历,是因为要通过递归函数的返回值来累加求取左叶子数值之和。另外,通过节点的父节点判断其左孩子是否是左叶子,涉及三层关系。
class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if (root == nullptr) return 0;
        if (root->left == nullptr && root->right == nullptr) return 0;
        return traverse(root);
    }
    int traverse(TreeNode* cur) {
        int sum = 0;
        if (cur->left != nullptr) sum += traverse(cur->left);
        if (cur->right != nullptr) sum += traverse(cur->right);
        if (cur->left != nullptr && cur->left->left == nullptr && cur->left->right == nullptr) sum += cur->left->val; 
        return sum;
    }
};

513.找树左下角的值

513. 找树左下角的值 - 力扣(LeetCode)

  • 层序遍历,不断覆盖记录每行第一个元素,最后留下的是最后一行第一个元素
class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        if (root == nullptr) return 0;
        int result = 0;
        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 == 0) result = node->val; // 不断覆盖每行第一个元素,最后留下的是最后一行第一个元素
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }
};

112.路径总和(回溯)

112. 路径总和 - 力扣(LeetCode)

  • 递归,前中后序都可以,只要是叶子节点且和值满足要求,就直接返回,因此辅助函数返回值是bool
class Solution {
private:
    bool traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
        if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回

        if (cur->left) { // 左
            count -= cur->left->val; // 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val; // 回溯,撤销处理结果
        }
        if (cur->right) { // 右
            count -= cur->right->val; // 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val; // 回溯,撤销处理结果
        }
        return false;
    }

public:
    bool hasPathSum(TreeNode* root, int sum) {
        if (root == NULL) return false;
        // 已经加入了根节点
        return traversal(root, sum - root->val);
    }
};
  • 精简版,有理解的难度
class Solution {
public:
    bool hasPathSum(TreeNode* root, int targetSum) {
        if (root == nullptr) return false;
        // 到达叶子节点并且路径和满足要求
        if (!root->left && !root->right && targetSum == root->val) {
            return true;
        }
        // 回溯隐藏在传入的参数中,因为把count - cur->left->val 直接作为参数传进去,函数结束,count的数值没有改变
        return hasPathSum(root->left, targetSum - root->val) || hasPathSum(root->right, targetSum - root->val);
    }
};

113.路径总和 ii(回溯)

113. 路径总和 II - 力扣(LeetCode)

  • 需要记录所有符合要求的路径,因此辅助函数返回值是void
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
public:
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        result.clear();
        path.clear();
        if (root == nullptr) return {};
        path.push_back(root->val);
        backtracking(root, targetSum - root->val);
        return result;
    }
    void backtracking(TreeNode* cur, int sum) {
        if (!cur->left && !cur->right && sum == 0) {
            result.push_back(path);
            return; // 注意返回
        }
        if (!cur->left && !cur->right) return; // 遇到叶子节点而没有找到合适的边,直接返回
        if (cur->left) {
            sum -= cur->left->val;
            path.push_back(cur->left->val);
            backtracking(cur->left, sum);
            path.pop_back();
            sum += cur->left->val;
        }
        if (cur->right) {
            sum -= cur->right->val;
            path.push_back(cur->right->val);
            backtracking(cur->right, sum);
            path.pop_back();
            sum += cur->right->val;
        }
    }
};

4、二叉树的修改与构造

226.翻转二叉树

226. 翻转二叉树 - 力扣(LeetCode)

  • 前序遍历,迭代法

  • 层序遍历,迭代法

  • 递归,前序遍历

翻转当前节点的左右子树,递归地翻转以当前节点的左右孩子为根节点的子树

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == nullptr) return nullptr;
        TreeNode* tmp;
        tmp = root->left;
        root->left = root->right;
        root->right = tmp; // 可用swap函数
        root->left = invertTree(root->left);
        root->right = invertTree(root->right);
        return root;
    }
};

106.由中序后序序列构造二叉树

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

  • 注意采用左闭右闭区间,辅助函数中先判断后序遍历数组起止点的合法性,构造完root后若只含一个元素直接返回,找index时for中的判断是<=
class Solution {
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
        return build(inorder, 0, inorder.size() - 1, postorder, 0, postorder.size() - 1);
    }
    TreeNode* build(vector<int>& inorder, int inorderStart, int inorderEnd, vector<int>& postorder, int postorderStart, int postorderEnd) {
        if (postorderStart > postorderEnd) return nullptr;
        // 先在后序遍历最末尾找根节点
        int pivot = postorder[postorderEnd];
        TreeNode* root = new TreeNode(pivot);
        // 采用左闭右闭区间,若左子数只有一个元素直接返回
        if (postorderStart == postorderEnd) return root;
        // 然后在中序遍历位置找分割点
        int index = 0;
        for (int i = inorderStart; i <= inorderEnd; i++) { //由于采用左闭右闭区间,这里一定要加=
            if (pivot == inorder[i])
                index = i;
        }
        // 记录分割点左边的元素个数
        int leftSize = index - inorderStart;
        root->left = build(inorder, inorderStart, index - 1, postorder, postorderStart, postorderStart + leftSize - 1);
        root->right = build(inorder, index + 1, inorderEnd, postorder, postorderStart + leftSize, postorderEnd - 1);
        return root;
    }
};

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

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

class Solution {
public:
    TreeNode* build(vector<int>& preorder, int preorderBegin, int preorderEnd, vector<int>& inorder, int inorderBegin, int inorderEnd) {
        if (preorderBegin > preorderEnd) return nullptr;
        int rootValue = preorder[preorderBegin];
        TreeNode* root = new TreeNode(rootValue);
        if (preorderBegin == preorderEnd) return root;
        // 找中序分割点,一定要注意添加=
        int index;
        for(index = 0; index <= inorderEnd; index++) {
            if (inorder[index] == rootValue) break;
        }
        int leftSize = index - inorderBegin;
        // 递归
        root->left = build(preorder, preorderBegin + 1, preorderBegin + leftSize, inorder, inorderBegin, index - 1);
        root->right = build(preorder, preorderBegin + leftSize + 1, preorderEnd, inorder, index + 1, inorderEnd);

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

654.最大二叉树

654. 最大二叉树 - 力扣(LeetCode)

  • 递归,先序遍历,注意遵守前闭后闭的原则
  • 注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。
  • 一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。
class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        if (nums.size() == 0) return nullptr;
        return build(nums, 0, nums.size() - 1);
    }
    TreeNode* build(vector<int>& nums, int start, int end) {
        if (start > end) return nullptr;
        int index = findMax(nums, start, end);
        TreeNode* root = new TreeNode(nums[index]);
        if (start == end) return root;
        root->left = build(nums, start, index - 1);
        root->right = build(nums, index + 1, end);
        return root;
    }
    int findMax(vector<int>& nums, int start, int end) {
        int max = INT_MIN;
        int index = start;
        for (int i = index; i <= end; i++) {
            if (max < nums[i]) {
                index = i;
                max = nums[i];
            }   
        }
        return index;
    }
};

617.合并二叉树

617. 合并二叉树 - 力扣(LeetCode)

  • 递归,先序遍历,每次new一个新节点构建树
class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if (root1 == nullptr && root2 == nullptr) return nullptr;
        if (root1 == nullptr) return root2;
        if (root2 == nullptr) return root1;
        TreeNode* root = new TreeNode(root1->val + root2->val);
        root->left = mergeTrees(root1->left, root2->left);
        root->right = mergeTrees(root1->right, root2->right);
        return root;
    }
};

剑指offer37.序列化与反序列化二叉树(hard)

  • 序列化采用队列辅助的层次遍历,将结果存储到string
  • 反序列化即是构建树,先把string转化为vector,然后构建二叉树
class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        if (root == NULL) return "";
        string result;
        queue<TreeNode*> que;
        que.push(root);
        while (!que.empty()) {
            TreeNode* node = que.front();
            que.pop();
            // 空节点返回 # ,非空节点返回数值
            if (node == NULL) {
                result.push_back('#');
                result += ',';
            } else {
                result += to_string(node->val);
                result += ',';
                que.push(node->left);
                que.push(node->right);
            }
        }
        return result;
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        if (data.size() == 0) return NULL;
        vector<TreeNode*> vec;
        int j = 0;
        while (j < data.size()) {
            string stmp = "";
            // 防止漏掉多位的int,比如123,12,3431
            while (data[j] != ',') {
                stmp += data[j];
                j++;
            }
            // 把 string data 转化为 vector
            if (stmp == "#") {
                vec.push_back(nullptr);
            } else {
                int tmp = atoi(stmp.c_str());
                TreeNode* node = new TreeNode(tmp);
                vec.push_back(node);
            }
            j++;
        }
        // 根据vec构建二叉树,注意与 “根据输入的数组构造二叉树” 的区别
        int pos = 1;
        for (int i = 0; i < vec.size(); ++i) {
            // 空节点不可能有孩子,直接跳过,进入下一循环
            if (vec[i] == nullptr) continue;
            vec[i]->left = vec[pos++];
            vec[i]->right = vec[pos++];
        }
        return vec[0];
    }
};

5、求二叉搜索树的属性

700.二叉搜索树中的搜索

700. 二叉搜索树中的搜索 - 力扣(LeetCode)

  • 递归

  • 1、root为空或是找到,就要返回
    2、注意return searchBST,递归带返回值,因为是找到了就返回

  • 迭代,写法也很简单

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if (root == nullptr) return nullptr;  
        if (root->val < val) return searchBST(root->right, val);
        if (root->val > val) return searchBST(root->left, val);
        if (root->val == val) return root;
        return nullptr;
    }
};

98.验证二叉搜索树(有陷阱)

98. 验证二叉搜索树 - 力扣(LeetCode)

  • 法一,利用中序遍历为升序的性质
  • 法二,递归,左中右,若是有不符合要求的情况出现直接返回false
    • 注意,不能单纯的比较当前节点大于左子树,右子树大于当前节点。这个代码逻辑是错的。应该设立变量,记录是否递增。
    • 注意,大数问题,因为后台数据有int最小值测试用例,所以都把maxVal改成了longlong最小值。但是,若根节点是longlong最小值,还有左子树需要比较,那么该变量的初始值也不能用最小的longlong来设置了。利用pre指针,记录每次遍历root的前一个值。
class Solution {
public:
    // 用来记录中序遍历的前一个节点
    TreeNode* pre = nullptr;
    bool isValidBST(TreeNode* root) {
        if (root == nullptr) 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;
    }
};

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

530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)

  • 法一,利用中序遍历为升序的性质,最小绝对差一定出现在中序遍历的相邻节点之间
  • 法二,参考98题,利用pre指针来记录cur的上一个值
class Solution {
private:
    int result = INT_MAX;
    TreeNode* pre = NULL;
    void traversal(TreeNode* cur) {
        if (cur == NULL) return;
        traversal(cur->left);   // 左
        if (pre != NULL) {       // 中
            result = min(result, cur->val - pre->val);
        }
        pre = cur; // 记录前一个,注意不要漏这一行代码
        traversal(cur->right);  // 右
    }
public:
    int getMinimumDifference(TreeNode* root) {
        traversal(root);
        return result;
    }
};

501.二叉搜索树中的众数(好题,不使用额外空间)

501. 二叉搜索树中的众数 - 力扣(LeetCode)

  • 若不限制空间要求,任何二叉树均可使用此方法,任意方式遍历树,第一遍遍历构建map,记录元素值和出现总次数,然后按照value进行排序,第二遍遍历输出众数
  • 利用pre指针。若当前出现的元素次数大于最大次数,及时清空结果集,并将新元素入集。这样只需要遍历一次。
class Solution {
private:
    TreeNode* pre = nullptr;
    vector<int> result;
    int maxCount = 0;
    int count = 0;
public:
    vector<int> findMode(TreeNode* root) {
        result.clear();
        traverse(root);
        return result;
    }
    void traverse(TreeNode* cur) {
        if (cur == nullptr) return;
        traverse(cur->left);
        if (pre == NULL) { // 第一个节点
            count = 1; // 频率为1
        } else if (pre->val == cur->val) { // 与前一个节点数值相同
            count++;
        } else { // 与前一个节点数值不同
            count = 1;
        }
        pre = cur; // 更新上一个节点
        // 如果和最大值相同,则加入
        if (count == maxCount) result.push_back(cur->val);
        // 如果计数大于最大值频率
        if (count > maxCount) { 
            maxCount = count;   // 更新最大频率
            result.clear();     // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
            result.push_back(cur->val);
        }
        traverse(cur->right);
        // return;
    }
};

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

538. 把二叉搜索树转换为累加树 - 力扣(LeetCode)

  • 借助pre指针,右中左遍历累加
class Solution {
public:
    TreeNode* pre = nullptr;
    TreeNode* convertBST(TreeNode* root) {
        traverse(root);
        return root;
    }
    void traverse(TreeNode* cur) {
        if (cur == nullptr) return;
        traverse(cur->right);
        if (pre != nullptr) {
            cur->val += pre->val;
        }
        pre = cur;
        traverse(cur->left);
    }
};

6、二叉树公共祖先问题(好题)

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

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

  • 只能后序遍历,将结果自下而上传递上来
  • 用到递归函数有返回值时如何搜索整棵树的写法:先用left和right接住,然后再进行逻辑处理。即使左子树已有结果返回,仍需搜索右子树
  • 当左空右不空时,了解结果是如何从右子树传递上来的
  • 后序遍历时,如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者左子树出现结点q,右子树出现节点p,那么该节点一定就是节点p和q的最近公共祖先
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == NULL) return NULL;
        if (root == p || root == q) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if (left != NULL && right != NULL) return root;
        if (left == NULL && right != NULL) return right;
        else if (left != NULL && right == NULL) return left;
        else return NULL; // 左、右都为空
    }
};

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

235. 二叉搜索树的最近公共祖先 - 力扣(LeetCode)

  • 无所谓遍历顺序,不涉及对中节点的操作
  • 用到递归函数有返回值时,只搜索部分树的写法:搜索到直接返回,不需要left和right接住后再做处理
  • 类似 700.二叉搜索树中的搜索 处理方式,迭代法形式比较简单
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == NULL || root == p || root == q) return root;
        if (root->val > max(p->val, q->val)) {
            return lowestCommonAncestor(root->left, p, q);
        }     
        if (root->val < min(p->val, q->val)) {
            return lowestCommonAncestor(root->right, p, q);
        }
        return root;      
    }
};

7、二叉搜索树的修改与构造

701.插入节点

701. 二叉搜索树中的插入操作 - 力扣(LeetCode)

  • 不考虑重构的情况,直接插入
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        TreeNode* node = new TreeNode(val);
        if (root == nullptr) return node;
        if (root->val > val)
            root->left = insertIntoBST(root->left, val);
        if (root->val < val)
            root->right = insertIntoBST(root->right, val);
        return root;
    }
};

450.删除节点(有难度)

450. 删除二叉搜索树中的节点 - 力扣(LeetCode)

  • 删除有以下五种情况:
    • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
    • 找到删除的节点
      • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
      • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
      • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
      • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
  • 第五种情况需要认真体会,可以画图辅助理解
  • delete的过程最好写出,不写也可以通过
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root;
        // 寻找过程
        if (root->val > key) root->left = deleteNode(root->left, key);
        if (root->val < key) root->right = deleteNode(root->right, key);
        // 找到
        if (root->val == key) {
            // 被删除的是叶子节点,直接删
            if (root->left == nullptr && root->right == nullptr) {
                delete root;
                return nullptr;
            }
            // 被删除的节点只有左或右子树,直接代替
            else if (root->left == nullptr) {
                TreeNode* node = root->right;
                delete root;
                return node;
            }
            else if (root->right == nullptr) {
                TreeNode* node = root->left;
                delete root;
                return node;
            }
            // 左右都不空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点
            else {
                // 找右子树最左面节点
                TreeNode* cur = root->right;
                while (cur->left != nullptr) cur = cur->left;
                // 被删除节点的左孩子接到cur的左子树
                cur->left = root->left;
                // 删除root,返回其右孩子
                TreeNode* tmp = root->right;
                delete root;
                return tmp;
            }
        }
        // 未找到
        return root;
    }
};

669.修剪二叉搜索树(不太好理解)

669. 修剪二叉搜索树 - 力扣(LeetCode)

  • 理解用递归函数返回子树、父节点接住返回值可以达到修剪树的目的

针对下图中二叉树的情况:

669.修剪二叉搜索树1

如下代码相当于把节点0的右孩子(节点2)返回给上一层,

if (root->val < low)
    return trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点

然后如下代码相当于用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住。

root->left = trimBST(root->left, low, high);

此时节点3的左孩子就变成了节点2,将节点0从二叉树中移除了。

  • 整体代码如下:
class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if (root == nullptr) return root;
        if (root->val < low)
            return trimBST(root->right, low, high);
        if (root->val > high)
            return trimBST(root->left, low, high);
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        return root;
    }
};

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

108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

  • 由数组构造二叉树,先取到根节点,再左右递归地构建子树,坚持左闭右闭的原则
  • 本题的数组是有序的,可以取中间值,再左右递归构建树,能保证是二叉搜索树
class Solution {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        if (nums.size() == 0) return nullptr;
        return build(nums, 0, nums.size() - 1);
    }
    TreeNode* build(vector<int>& nums, int start, int end) {
        if (start > end) return nullptr;
        int mid = (end - start) / 2 + start;
        TreeNode* node = new TreeNode(nums[mid]);
        if (start == end) 
            return node;
        node->left = build(nums, start, mid - 1);
        node->right = build(nums, mid + 1, end);
        return node;
    }
};

你可能感兴趣的:(刷题总结,算法,java,数据结构,c++,leetcode)