C++ 数据结构与算法(八)(二叉树、遍历)

1. 树

 代码随想录知识星球成员:青

1.1 基本概念

树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。树分为二叉树和多叉树。

- 结点
根节点root,左右节点,子树中又有父节点,子节点,兄弟节点,没有子节点的称为叶(子)节点(终端节点)。

- 子树和空树
单个结点也是一棵树。
空树中没有结点。

- 结点的度和层次
对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree)

一棵树的度是树内各结点的度的最大值。

结点的层次:从一棵树的树根开始,树根所在层为第一层,根的孩子结点所在的层为第二层,依次类推。

一棵树的深度(高度)是树中结点所在的最大的层次。

- 有序树和无序树
如果树中结点的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树;反之称为无序树。
在有序树中,一个结点最左边的子树称为"第一个孩子",最右边的称为"最后一个孩子"。

- 森林
由 m(m >= 0)个互不相交的树组成的集合被称为森林

1.2. 树的存储结构

顺序存储 与 链式存储

1.2.1 双亲表示法

1.2.2 孩子表示法

1.2.3 孩子兄弟表示法

2. 二叉树

  1. 本身是有序树
  2. 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2。
    请添加图片描述

2.1 二叉树的性质

  1. 二叉树中,第 i i i最多有 2i-1 个结点。
  2. 如果二叉树的深度为 K K K,那么此二叉树最多 2 K − 1 2^K-1 2K1 个结点。
  3. 二叉树中,终端结点数(叶子结点数)为 n 0 n_0 n0,度为 2 的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

性质 3 的计算方法为:
对于一个二叉树来说,除了度为 0 的叶子结点和度为 2 的结点,剩下的就是度为 1 的结点(设为 n1),那么总结点 n=n0+n1+n2。
同时,对于每一个结点来说都是由其父结点分支表示的,假设树中分枝数(连线数)为 B,那么总结点数 n=B+1。而分枝数是可以通过 n1 和 n2 表示的,即 B=n1+2n2。所以,n 用另外一种方式表示为 n=n1+2n2+1。
两种方式得到的 n 值组成一个方程组,就可以得出 n0=n2+1。

2.2 二叉树种类

2.2.1 斜树

都只有左子树或右子树的二叉树,分为左斜树和右斜树。

2.2.2 满二叉树

在这里插入图片描述

如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树

满二叉树除了满足普通二叉树的性质,还具有以下性质:

  1. 满二叉树中第 i 层的节点数为 2n-1 个。
  2. 深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1
  3. 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层
  4. 具有 n 个节点的满二叉树的深度 k 为 l o g 2 ( n + 1 log_2(n+1 log2(n+1)。(n = 2k - 1)

2.2.3 完全二叉树

请添加图片描述
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树

特点:

  1. n 个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 ⌊log_2n⌋+1 log2n+1。( ⌊ l o g 2 n ⌋ ⌊log_2n⌋ log2n表示取小于 l o g 2 n log_2n log2n 的最大整数。例如, ⌊ l o g 2 4 ⌋ ⌊log_24⌋ log24 = 2,而 ⌊ l o g 2 5 ⌋ ⌊log_25⌋ log25 结果也是 2。)
  2. 叶子结点只能出现在最下两层。
  3. 同样结点树的二叉树,完全二叉树深度最小。

对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(如图 3a)),对于任意一个结点 i ,完全二叉树还有以下几个结论成立:
4. 当 i > 1 时,父亲结点为结点 i / 2 。(i=1 时,表示的是根结点,无父亲结点)
5. 如果 2 * i > n(总结点的个数),则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2 * i 。
6. 如果 2 * i + 1 > n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2 * i + 1 。

2.2.4 二叉搜索树

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

(注意二分搜索树不一定是完全二叉树)
搜索树

二分搜索树的优势:不仅可以查找数据,还可以高效的插入、删除数据。请添加图片描述

2.2.5 平衡二叉搜索树

又被称为 AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
请添加图片描述
C++中 map、set、multimap、multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,而unordered_map、unordered_set、unordered_map、unordered_map底层实现是哈希表。

2.3 二叉树的存储结构

2.3.1 顺序存储结构

使用顺序表(数组)存储二叉树。
需要注意的是,顺序存储只适用于完全二叉树。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树:
在这里插入图片描述

完全二叉树的顺序存储,仅需从根节点开始,按照层次依次将树中节点存储到数组即可:

请添加图片描述

  • 用数组来存储二叉树如何遍历的呢(从顺序表中还原完全二叉树)?

如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1右孩子就是 i * 2 + 2

2.3.2 链式存储结构

普通二叉树使用顺序表存储或多或多会存在空间浪费的现象。链式存储则是通过指针把分布在散落在各个地址的节点串联一起。
请添加图片描述
二叉链表的节点结构由 3 部分构成:

  1. 节点存储的数据(data);
  2. 指向左孩子节点的指针(Lchild);
  3. 指向右孩子节点的指针(Rchild);

二叉树的链式存储结构应根据需求灵活设计数据域和指针域的结构(如增加双亲结点指针(三叉链表))。

3. 二叉树的前中后序遍历

3.1 递归方法(隐式栈)

递归三要素:

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
  • 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

144. 二叉树的前序遍历 ●

二叉树先序遍历的实现思想是:(中左右

  1. 访问根节点
  2. 访问当前节点的左子树
  3. 若当前节点无左子树,则访问当前节点的右子树.
    在这里插入图片描述
class Solution {
public:
    void traversal(TreeNode* node, vector<int> & ans){	// 引用调用
        if(node == nullptr) return;			// 终止条件
        ans.emplace_back(node->val);        // 访问根节点
        traversal(node->left, ans);         // 前序遍历左子树
        traversal(node->right, ans);        // 前序遍历右子树
    }

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        traversal(root, ans);
        return ans;
    }
};

94. 二叉树的中序遍历 ●

二叉树中序遍历的实现思想是:(左中右

  1. 访问当前节点的左子树;
  2. 访问根节点;
  3. 访问当前节点的右子树.
    在这里插入图片描述
class Solution {
public:
    void traversal(TreeNode* node, vector<int> & ans){	// 引用调用
        if(node == nullptr) return;			// 终止条件
        traversal(node->left, ans);         // 中序遍历左子树
        ans.emplace_back(node->val);        // 访问根节点
        traversal(node->right, ans);        // 中序遍历右子树
    }

    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        traversal(root, ans);
        return ans;
    }
};

145. 二叉树的后序遍历 ●

二叉树后序遍历的实现思想是:(左右中
从根节点出发,依次遍历各节点的左右子树,直到当前节点左右子树遍历完成后,才访问该节点元素。(根节点在最后一个
在这里插入图片描述

class Solution {
public:
    void traversal(TreeNode* curr, vector<int> & ans){
        if(curr == nullptr) return;
        traversal(curr->left, ans);     // 后序遍历左子树
        traversal(curr->right, ans);    // 后序遍历右子树
        ans.emplace_back(curr->val);    // 访问根节点

    }
    
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        traversal(root, ans);
        return ans;
    }
};

已知前序遍历和中序遍历序列,可以唯一确定一棵二叉树;
已知后序遍历和中序遍历序列,可以唯一确定一棵二叉树;
已知前序遍历和后序遍历序列,不能确定一棵二叉树。

如何通过遍历序列来建立二叉树结构?(首先对树进行扩展,使序列中每个节点都有左右子节点,再递归建立,《大话数据结构》P159)

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数。

3.2 迭代遍历(显式栈)

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

144. 二叉树的前序遍历 ●

  1. 处理:将元素放进 ans 数组中
  2. 访问:遍历节点

将根节点压入栈,从栈顶弹出的时候,取出该元素值,并将右孩子和左孩子按顺序压入栈中,再循环访问、处理栈顶元素。
前序遍历中,要访问的元素和要处理的元素顺序是一致的,都是中间节点。

在这里插入图片描述

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;        // 栈st,存放节点指针    
        if(!root) return ans;       // 空树判断
        st.push(root);              // 将根节点压入栈
        while(!st.empty()){
            TreeNode* curr = st.top();  // curr 存放栈顶元素
            st.pop();                   // 将栈顶弹出,将元素值取出,同时将右孩子和左孩子压入栈中
            ans.emplace_back(curr->val);
            if(curr->right) st.push(curr->right);   // 先压右孩子
            if(curr->left) st.push(curr->left);     // 后压左孩子
        }
        return ans;
    }
};

94. 二叉树的中序遍历 ●

中序遍历中,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的

在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点栈则用来处理节点上的元素
在这里插入图片描述

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;
        TreeNode* curr = root;
        while(curr != nullptr || !st.empty()){        
            if(curr){               // 当前节点(某节点的左孩子)为非空指针
                st.push(curr);      // 将节点压入栈
                curr = curr->left;  // 当前节点指向左孩子(指针的遍历访问)
            }else{
                curr = st.top();    // 当左孩子为空指针时,当前节点指向栈顶
                st.pop();           // 弹出栈顶
                ans.emplace_back(curr->val);    // 取出栈顶元素值
                curr = curr->right; // 当前节点指向右孩子(指针的遍历访问)
            }
        }
        return ans;
    }
};

145. 二叉树的后序遍历 ●

先按照/“中右左”/的顺序进行遍历,然后将得到的数组进行反转即为后序遍历序列。

前序遍历为“中左右”,因此在前序遍历的基础上,要将节点的左孩子先压入栈
在这里插入图片描述

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        if(!root) return ans;
        stack<TreeNode*> st;
        st.push(root);
        while(!st.empty()){             // 中右左
            TreeNode* curr = st.top();
            st.pop();
            ans.emplace_back(curr->val);
            if(curr->left) st.push(curr->left);     // 左孩子先压栈
            if(curr->right) st.push(curr->right);   
        }
        reverse(ans.begin(), ans.end());
        return ans;
    }
};

3.3 统一迭代写法

上述迭代方法无法同时解决中序遍历时访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。

那我们就将访问的节点放入栈中,同时把要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法

144. 二叉树的前序遍历 ●

  • 入栈顺序为:右-左-中-null; 出栈顺序为:null-中-左-右
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;        // 栈st,存放节点指针    
        if(root) st.push(root);		// 根节点先加入栈顶
        while(!st.empty()){
            TreeNode* curr = st.top();
            if(curr != nullptr){	// 对非空栈顶的孩子进行访问
                st.pop();			// 入栈顺序为:右-左-中-null; 出栈顺序为:null-中-左-右
                if(curr->right) st.push(curr->right);
                if(curr->left) st.push(curr->left);
                st.push(curr);
                st.push(nullptr);
            }else{
                st.pop();			// null后面的元素表示已经访问过,此时需要弹出并取出元素值
                curr = st.top();
                st.pop();
                ans.emplace_back(curr->val);
            }
        }
        return ans;
    }
};

94. 二叉树的中序遍历 ●

  • 入栈顺序为:右-中-null-左; 出栈顺序为:左-null-中-右
    在这里插入图片描述
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;
        if(root) st.push(root);     // 根节点先加入栈顶
        while(!st.empty()){
            TreeNode* curr = st.top();
            if(curr != nullptr){    // 对非空栈顶的孩子进行访问
                st.pop();           // 入栈顺序为:右-中-null-左; 出栈顺序为:左-null-中-右
                if(curr->right) st.push(curr->right);
                st.push(curr);
                st.push(nullptr);
                if(curr->left) st.push(curr->left);
            }else{
                st.pop();
                curr = st.top();    // null后面的元素表示已经访问过,此时需要弹出并取出元素值
                st.pop();
                ans.emplace_back(curr->val);
            }
        }
        return ans;
    }
};

145. 二叉树的后序遍历 ●

  • 入栈顺序为:中-null-右-左; 出栈顺序为:左-右-null-中
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;        // 栈st,存放节点指针    
        if(root) st.push(root);		// 根节点先加入栈顶
        while(!st.empty()){
            TreeNode* curr = st.top();
            if(curr != nullptr){	// 对非空栈顶的孩子进行访问
                st.pop();			// 入栈顺序为:中-null-右-左; 出栈顺序为:左-右-null-中
                st.push(curr);
                st.push(nullptr);
                if(curr->right) st.push(curr->right);
                if(curr->left) st.push(curr->left);  
            }else{
                st.pop();			// null后面的元素表示已经访问过,此时需要弹出并取出元素值
                curr = st.top();
                st.pop();
                ans.emplace_back(curr->val);
            }
        }
        return ans;
    }
};

4. 二叉树的层序遍历

102. 二叉树的层序遍历 ●●

在这里插入图片描述

1. 队列先进先出,符合层序遍历的逻辑(BFS)

用数组 num[] 记录层的节点数和 curr_depth[] 当前遍历深度

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        queue<TreeNode*> que;
        if (root == NULL) return ans;
        que.push(root);         // 根节点加入队列que
        vector<int> num;        // 当前层的孩子数,即下一层的数量
        num.emplace_back(1);    // 第一层的数量 num[0] = 1;
        num.emplace_back(0);    // 扩容,存放第二层的节点数量
        ans.emplace_back();     // vector扩容  
        int curr_depth = 0;     // 当前访问的深度
        while(!que.empty()){
            TreeNode* curr = que.front();           // 处理队列头结点
            ans[curr_depth].emplace_back(curr->val);// 取出元素值
            que.pop();
            --num[curr_depth];                      // 当前层的剩余数量-1
            if(curr->left){                         
                que.push(curr->left);               // 左孩子加入队列
                ++num[curr_depth+1];                // 下一层数量+1
            }
            if(curr->right){
                que.push(curr->right);              // 右孩子加入队列
                ++num[curr_depth+1];                // 下一层数量+1
            }
            if(num[curr_depth] == 0 && !que.empty()){   // 当前节点遍历完成,且还有下一层节点
                ++curr_depth;                       // 遍历深度+1
                num.emplace_back(0);                // 扩容
                ans.emplace_back();                 // 扩容
            } 
        }
        return ans;
    }
};

2.(BFS 优化)队列先进先出,队列元素个数记录层节点数,创建层的元素数组vec

  • 时间复杂度:O(n),每个点进队出队各一次。
  • 空间复杂度:O(n),队列中元素的个数不超过 n 个。
    在这里插入图片描述
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if (root == NULL) return ans;
        queue<TreeNode*> que;
        que.push(root);             // 根节点加入队列que
        while(!que.empty()){
            int size = que.size();  // 当前层的节点数(即处理前的队列元素个数)
            vector<int> vec;
            for(int i = 0; i < size; ++i){
                TreeNode* curr = que.front();   // 处理队列头结点
                vec.emplace_back(curr->val);    // 取出元素值
                que.pop();
                if(curr->left) que.push(curr->left);    // 左孩子加入队列
                if(curr->right) que.push(curr->right);  // 右孩子加入队列
            }
            ans.emplace_back(vec);
        }
        return ans;
    }
};

3. DFS递归 (运行过程中不按照严格层序进行遍历)

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        dfs(root, 0, ans);  // 根节点为第0层
        return ans;
    }

    void dfs(TreeNode* node, int depth, vector<vector<int>> &ans){
        if(!node) return;
        if(depth == ans.size()) ans.emplace_back(); // 当前节点层数大于数组长度时,扩容
        ans[depth].emplace_back(node->val);         // 当前元素值加入到索引为depth的数组中
        dfs(node->left, depth+1, ans);              // 遍历左孩子,depth+1
        dfs(node->right, depth+1, ans);             // 遍历右孩子,depth+1
    }
};

107. 二叉树的层序遍历 II ●●

返回自底层向上的层序遍历

1. 在正向层序遍历的基础上,反转ans[[]]

reverse(ans.begin(), ans.end());

2. DFS递归

class Solution {
public:
    void dfs(TreeNode* node, int depth, vector<vector<int>> &ans){
        if(!node) return;                   // dfs(node, depth, ans)
        if(depth > ans.size()){             // node为当前节点,depth为当前深度,ans为答案容器
            ans.emplace(ans.begin());       // 答案容器在开头插入一个新的vector
        }
        ans[ans.size()-depth].emplace_back(node->val);  // 在ans.size()-depth位置加入当前元素值
        dfs(node->left, depth+1, ans);      // 遍历左孩子,深度+1
        dfs(node->right, depth+1, ans);     // 遍历右孩子,深度+1
        
    }
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        vector<vector<int>> ans;
        dfs(root, 1, ans);
        return ans;
    }
};

199. 二叉树的右视图 ●●

在这里插入图片描述

1. BFS

层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进ans数组中,最后返回ans就可以了。

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> ans;
        if(!root) return ans;
        queue<TreeNode*> que;       // 队列 层序遍历
        que.push(root);
        while(!que.empty()){
            int size = que.size();  // 当前层的元素个数
            for(int i = 0; i < size; ++i){
                TreeNode* curr = que.front();
                que.pop();
                if(i == size-1) ans.emplace_back(curr->val);    // 如果是当前层的最后一个,则取出元素值
                if(curr->left) que.push(curr->left);
                if(curr->right) que.push(curr->right);
            }
        }
        return ans;
    }
};

2. DFS

按照 「根结点 -> 右子树 -> 左子树」 的顺序访问,就可以保证每层都是最先访问最右边的节点的。
先迭代遍历右孩子,保证新层遍历的第一个节点为最右节点;
当层数大于数组长度时,即当前节点为该层第一个节点,也是最右边的节点。

  • 时间复杂度: O(N),每个节点都访问了 1 次。
  • 空间复杂度: O(N),因为这不是一棵平衡二叉树,二叉树的深度最少是 logN, 最坏的情况下会退化成一条链表,深度就是 N,因此递归时使用的栈空间是 O(N) 的。
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> ans;
        dfs(root, 1, ans);  // 根节点为第1层
        return ans;
    }

    void dfs(TreeNode* node, int depth, vector<int> &ans){
        if(!node) return;
        if(depth > ans.size()) ans.emplace_back(node->val);     // 当层数大于数组长度时,即当前节点为该层第一个节点,也是最右边的节点
        dfs(node->right, depth+1, ans);     // 先迭代遍历右孩子,保证第一个节点为最右节点
        dfs(node->left, depth+1, ans);
    }
};

637.二叉树的层平均值 ●

层序遍历,求和再求平均。

1. BFS

class Solution {
public:
    vector<double> averageOfLevels(TreeNode* root) {
        vector<double> ans;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()){
            double sum = 0;      // int型会溢出,且要类型转换成double
            int size = que.size();
            for(int i = 0; i < size; ++i){
                TreeNode* curr = que.front();
                que.pop();
                sum += curr->val;   // 层求和
                if(curr->left) que.push(curr->left);
                if(curr->right) que.push(curr->right);
            }
            ans.emplace_back(sum/size); // 取平均值
        }
        return ans;
    }
};

2. DFS

class Solution {
public:
    vector<double> averageOfLevels(TreeNode* root) {
        vector<double> ans;
        vector<double> sum = vector<double>{};
        vector<int> size = vector<int> {};
        dfs(root, 0, sum, size);
        for(int i = 0; i < sum.size(); ++i){        // 递归结束后计算平均值
            ans.emplace_back(sum[i]/size[i]);
        }
        return ans;
    }

    void dfs(TreeNode* node, int depth, vector<double> &sum, vector<int> &size){
        if(!node) return;
        if(depth >= sum.size()){     
            sum.emplace_back();     // 和数组扩容
            size.emplace_back();    // 层个数扩容
        }
        ++size[depth];
        sum[depth] += node->val;
        dfs(node->left, depth+1, sum, size);
        dfs(node->right, depth+1, sum, size);
    }
};

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

1. BFS(队列queue)

class Solution {
public:
    vector<int> largestValues(TreeNode* root) {
        vector<int> ans;
        if(!root) return ans;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()){
            int size = que.size();
            TreeNode* curr = que.front();       // 该层第一个数赋最大值
            que.pop();
            int max = curr->val;                // max 当前层最大值
            if(curr->left) que.push(curr->left);
            if(curr->right) que.push(curr->right);
            for(int i = 1; i < size; ++i){      // 遍历后面的数
                curr = que.front();
                que.pop();
                if(curr->val > max) max = curr->val;
                if(curr->left) que.push(curr->left);
                if(curr->right) que.push(curr->right);
            }
            ans.emplace_back(max);
        }
        return ans;
    }
};

2. DFS 递归

class Solution {
public:
    vector<int> largestValues(TreeNode* root) {
        vector<int> ans;
        if(!root) return ans;
        dfs(root, 0, ans);
        return ans;
    }

    void dfs(TreeNode* node, int depth, vector<int> &ans){
        if(!node) return;
        if(depth >= ans.size()){
            ans.emplace_back(node->val);        // 该层第一个数,赋最大值
        }else{
            if(node->val > ans[depth]) ans[depth] = node->val;  // 最大值比较
        }
        dfs(node->left, depth+1, ans);          // 递归遍历
        dfs(node->right, depth+1, ans);
    }
};

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

117. 填充每个节点的下一个右侧节点指针 II ●●

226. 翻转二叉树

在这里插入图片描述
对每个节点遍历一次,并交换左右孩子节点即可。

1. 层序遍历 BFS 迭代

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        queue<TreeNode*> que;
        if(!root) return root;
        que.push(root);
        while(!que.empty()){		// 遍历队列
            TreeNode* curr = que.front();	
            que.pop();
            swap(curr->left, curr->right);	// 交换左右孩子节点
            if(curr->left) que.push(curr->left);	// 孩子节点加入队列
            if(curr->right) que.push(curr->right);
        }
        return root;
    }
};

2. 前序遍历 DFS 迭代

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        stack<TreeNode*> st;
        if(!root) return root;
        st.push(root);
        while(!st.empty()){
            TreeNode* curr = st.top();
            st.pop();
            swap(curr->left, curr->right);
            if(curr->right) st.push(curr->right);
            if(curr->left) st.push(curr->left);     // 先进后出,前序遍历,中左右
        }
        return root;
    }
};

3. 前(后)序遍历 递归

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(!root) return root;
        swap(root->left, root->right);  // 中
        invertTree(root->left);         // 左
        invertTree(root->right);        // 右
        // swap(root->left, root->right);  // 中,取消注释即为后序遍历
        return root;
    }
};

4. (伪)中序遍历 递归

注意在递归到右孩子时,要写成遍历左孩子,因为中间节点已经翻转了,否则原左孩子将被翻转两次。

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == NULL) return root;
        invertTree(root->left);         // 左
        swap(root->left, root->right);  // 中
        invertTree(root->left);         // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了
        return root;					// 否则原左孩子将被翻转两次
    }
};

589. N 叉树的前序遍历 ●

在这里插入图片描述

1、递归

class Solution {
public:
    void traversal(Node* curr, vector<int> &ans){
        if(!curr)  return;
        ans.emplace_back(curr->val);
        for(auto ch : curr->children){		// 前序遍历孩子节点
            traversal(ch, ans);
        }
    }
    
    vector<int> preorder(Node* root) {
        vector<int> ans;
        traversal(root, ans);
        return ans;
    }
};

2、迭代

利用vector容器的反向迭代器进行遍历入栈,从后往前

class Solution {
public:
    vector<int> preorder(Node* root) {
        vector<int> ans;
        if(!root) return ans;
        stack<Node*> st;
        st.push(root);              // 根节点入栈
        while(!st.empty()){
            Node* curr = st.top();  // 处理栈顶
            st.pop();
            ans.emplace_back(curr->val);    
            // 利用vector容器的反向迭代器进行遍历入栈,从后往前
            for(auto iter = curr->children.rbegin(); iter != curr->children.rend(); ++iter){
                st.push(*iter);
            }
        }
        return ans;
    }
};

590. N 叉树的后序遍历 ●

1. 统一的迭代方法

NULL 指针作为标记符,对遍历过的节点进行处理。

class Solution {
public:
    vector<int> postorder(Node* root) {
        vector<int> ans;
        if(!root) return ans;
        stack<Node*> st;
        st.push(root);
        while(!st.empty()){
            Node* curr = st.top();
            // st.pop();
            if(curr != NULL){		// 非空指针
                // st.push(curr);
                st.push(NULL);		// 在根节点上压入NULL指针作为标记
                for(auto iter = curr->children.rbegin(); iter != curr->children.rend(); ++iter){
                    st.push(*iter);	// 利用迭代器,从右往左对孩子节点压栈
                }
            }else{
                st.pop();			// 空指针时,对下一个指针进行处理
                curr = st.top();
                st.pop();
                ans.emplace_back(curr->val);
            }
        }
        return ans;
    }
};

2. 递归

class Solution {
public:
    void traversal(Node* node, vector<int> &ans){
        if(!node) return;
        for(auto ch : node->children){
            traversal(ch, ans);		// 从左往右遍历递归孩子节点
        }
        ans.emplace_back(node->val);	// 最后取出根节点元素值
    }
    
    vector<int> postorder(Node* root) {
        vector<int> ans;
        if(!root) return ans;
        traversal(root, ans);
        return ans;
    }
};

429. N 叉树的层序遍历 ●

1. BFS 队列 迭代

class Solution {
public:
    vector<vector<int>> levelOrder(Node* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        queue<Node*> que;
        que.push(root);
        while(!que.empty()){
            int size = que.size();
            ans.emplace_back();                     // 新一层数组
            for(int i = 0; i < size; ++i){          // 当前层节点个数
                Node* curr = que.front();
                que.pop();
                ans.back().emplace_back(curr->val); // 取出元素值
                for(auto ch : curr->children){
                    que.push(ch);                   // 孩子节点加入队列
                }
            }
        }
        return ans;
    }
};

2. DFS 递归

class Solution {
public:
    void dfs(Node* node, int depth, vector<vector<int>> &ans){
        if(!node) return;
        if(depth == ans.size()){
            ans.emplace_back();     // 新一层数组扩容
        }
        ans[depth].emplace_back(node->val); // 取出元素值
        for(auto ch : node->children){  
            dfs(ch, depth+1, ans);          // 递归遍历孩子节点
        }
    }

    vector<vector<int>> levelOrder(Node* root) {
        vector<vector<int>> ans;
        if(!root) return ans;
        dfs(root, 0, ans);
        return ans;
    }
};

101. 对称二叉树 ●

在这里插入图片描述
在遍历时分左右两棵子树,只要左边元素的左孩子节点与右边元素的右孩子节点一一对应比较即可(入栈/队列顺序对应)。

1. BFS 队列 迭代

这里用到了左右子树两个队列,但把元素按顺序加入到一个队列,然后再成对取出来比较也可以实现。

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        queue<TreeNode*> lque;
        queue<TreeNode*> rque;
        if(!root) return true;
        lque.push(root->left);
        rque.push(root->right);
        while(!lque.empty()){
            int size = lque.size();
            for(int i = 0; i < size; ++i){		// 不按层节点数循环亦可,因为不需要按层进行输出
                TreeNode* currl = lque.front();     // 左半边队列
                TreeNode* currr = rque.front();     // 右半边队列
                lque.pop();
                rque.pop();
                if(currl != nullptr && currr != nullptr){   // 都不为空节点
                    if(currl->val != currr->val){
                        return false;
                    }   
                    lque.push(currl->left);     // 左半边队列 从左往右遍历
                    lque.push(currl->right);
                    rque.push(currr->right);    // 右半边队列 从右往左遍历
                    rque.push(currr->left);
                }else if(currl == nullptr && currr == nullptr){ // 都为空节点
                    continue;
                }else{                                      // 只有一个是空节点
                    return false;
                }       
            }
        }
        return true;
    }
};

2. DFS 栈 迭代

这里用到了左右子树两个栈,但把元素按顺序加入到一个栈,然后再成对取出来比较也可以实现。

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        stack<TreeNode*> stl;
        stack<TreeNode*> str;
        if(!root) return true;
        stl.push(root->left);
        str.push(root->right);
        while(!stl.empty()){
            TreeNode* currl = stl.top();
            TreeNode* currr = str.top();
            stl.pop();
            str.pop();
            if(currl != nullptr && currr != nullptr){   // 都不为空节点
                if(currl->val != currr->val){
                    return false;
                }   						// 
                stl.push(currl->right);     // 左半边 中左右出栈
                stl.push(currl->left);
                str.push(currr->left);      // 右半边 中右左出栈
                str.push(currr->right);
            }else if(currl == nullptr && currr == nullptr){ // 都为空节点
                continue;
            }else{                                      // 只有一个是空节点
                return false;
            }
        }
        return true;
    }
};

3. 递归

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑
class Solution {
public:
    bool traversal(TreeNode* currl, TreeNode* currr){
        bool curr;
        if(currl != nullptr && currr != nullptr){       // 都不为空节点
            if(currl->val != currr->val){
                return false;           // 元素值不相等,结束递归
            }                           // 元素值相等才进行递归遍历
            bool outside = traversal(currl->left, currr->right);
            bool inside = traversal(currl->right, currr->left);
            return outside && inside; 
        }else if(currl == nullptr && currr == nullptr){ // 都为空节点
            return true;
        }else{                                          // 只有一个是空节点
            return false;
        }
    }
    
    bool isSymmetric(TreeNode* root) {
        if(!root) return true;
        return traversal(root->left, root->right);
    }
};

104. 二叉树的最大深度 ●

1. BFS 队列 层序遍历 迭代

class Solution {
public:
    int maxDepth(TreeNode* root) {
        queue<TreeNode*> que;
        if(!root) return 0;
        int depth = 0;
        que.push(root);
        while(!que.empty()){
            ++depth;                    // 层数加一
            int size = que.size();      // 遍历一层
            for(int i = 0; i < size; ++i){
                TreeNode* curr = que.front();
                que.pop();
                if(curr->left) que.push(curr->left);
                if(curr->right) que.push(curr->right);
            }
        }
        return depth;
    }
};

2. DFS 递归

class Solution {
public:
    void dfs(TreeNode* node, int depth, int &ans){
        if(!node) return;
        if(depth > ans) ans = depth;		// 中
        dfs(node->left, depth+1, ans);		// 左
        dfs(node->right, depth+1, ans);		// 右
    }

    int maxDepth(TreeNode* root) {
        if(!root) return 0;
        int depth = 1;
        dfs(root, 1, depth);
        return depth;
    }
};
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(!root) return 0;
        int depth_left = maxDepth(root->left);      //左
        int depth_right = maxDepth(root->right);    //右
        return 1 + max(depth_left, depth_right);    //中
    }
};

111.二叉树的最小深度

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。
在这里插入图片描述

1. BFS 层序遍历 队列 迭代

class Solution {
public:
    int minDepth(TreeNode* root) {
        if(!root) return 0;
        queue<TreeNode*> que;
        que.push(root);
        int ans = 1;
        while(!que.empty()){
            int size = que.size();
            for(int i = 0; i < size; ++i){
                TreeNode* curr = que.front();
                que.pop();
                if(!curr->left && !curr->right) return ans; // 没有孩子节点时,为最小深度
                if(curr->left) que.push(curr->left);
                if(curr->right) que.push(curr->right);
            }
            ++ans;          // 都存在孩子节点,最小深度加一
        }
        return ans;
    }
};

2. DFS 递归

104. 二叉树的最大深度 ● 的递归写法有所区别,需判断是否为叶子节点

class Solution {
public:
    int minDepth(TreeNode* root) {
        if(!root) return 0;
        int depth_left = minDepth(root->left);          // 左
        int depth_right = minDepth(root->right);        // 右
        
        // 与最大深度递归写法的区别,不能单纯取两边深度的最小值
        // 对于只有一个子节点的情况要特殊处理,因为叶子节点是指没有子节点的节点
        // 当一个左子树为空,右不为空,这时并不是最低点     	// 中
        if(root->left == nullptr && root->right != nullptr){
            return 1 + depth_right;
        }
        // 当一个右子树为空,左不为空,这时并不是最低点
        if(root->left != nullptr && root->right == nullptr){
            return 1 + depth_left;
        }
        // 取最小深度
        return 1 + min(depth_left, depth_right);
    }
};

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

1. BFS 层序遍历 队列 迭代

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

class Solution {
public:
    int countNodes(TreeNode* root) {
        queue<TreeNode*> que;
        int ans = 0;
        if(!root) return ans;
        que.push(root);
        while(!que.empty()){
            TreeNode* curr = que.front();
            que.pop();
            ++ans;		// 节点个数+1
            if(curr->left) que.push(curr->left);
            if(curr->right) que.push(curr->right);
        }
        return ans;
    }
};

2. 利用完全二叉树性质

完全二叉树的两种情况:

  1. 满二叉树,直接用2 ^ depth - 1计算节点数,depth从 1 开始算;
  2. 非满二叉树,分别递归其左右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树(包括只有一个节点),然后按照情况1来计算。
    在这里插入图片描述
  • 时间复杂度: O ( l o g n × l o g n ) O(log n × log n) O(logn×logn)
  • 空间复杂度: O ( l o g n ) O(log n) O(logn)
class Solution {
public:
    int countNodes(TreeNode* root) {
        if(!root) return 0;
        int leftDepth = 1;		// 左节点深度
        int rightDepth = 1;		// 右节点深度
        TreeNode* left = root->left;
        TreeNode* right = root->right;
        while(left){		
            left = left->left;	// 遍历最左节点
            ++leftDepth;
        }
        while(right){
            right = right->right;	// 遍历最右节点
            ++rightDepth;
        }
        if(leftDepth == rightDepth){	// 满二叉树
            return (1 << leftDepth) - 1;// 移位指数运算 (1 << leftDepth)  ==  pow(2,leftDepth)
        }
        return countNodes(root->left) + countNodes(root->right) + 1;// 非满二叉树时,递归左右子节点
    }
};

110. 平衡二叉树 ●

给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差绝对值不超过 1

树的深度:前序遍历;
树的高度:后序遍历。 (但深度和高度可以进行转换,104. 二叉树的最大深度
在这里插入图片描述

1. 后序遍历 递归

  • 参数:根节点,
  • 返回值:子树高度,当子树出现非平衡二叉树时,返回-1作为标记
  • 终止条件:空节点;
  • 单层逻辑:递归左右子树,求最大高度,同时判断子树是否为非平衡二叉树
  • 时间复杂度:O(n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n)。
  • 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
class Solution {
public:
    int TreeHeight(TreeNode* root){
        int leftHeight = 0;
        int rightHeight = 0;
        if(!root) return 0;
        leftHeight = TreeHeight(root->left);        // 左
        rightHeight = TreeHeight(root->right);      // 右
        if(leftHeight == -1 || rightHeight == -1) return -1;// 子树已出现false,返回-1
        if(abs(leftHeight - rightHeight) > 1) return -1;	// 当前子树高度差大于-1,返回-1,表示fasle
        return max(leftHeight, rightHeight) + 1;    // 中
    }
    
    bool isBalanced(TreeNode* root) {
        if(!root) return true;
        int leftHeight = TreeHeight(root->left);	// 将整棵树分为左右两棵树,也可以只处理一整棵树
        int rightHeight = TreeHeight(root->right);
        if(leftHeight == -1 || rightHeight == -1) return false;
        return true;

		// return TreeHeight(root) == -1? false : true;	// 只处理一整棵树,在递归中已进行高度差判断
    }
};

257. 二叉树的所有路径 ●

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

1. 前序遍历 递归+回溯

在这里插入图片描述
回溯和递归是一一对应的,有一个递归,就要有一个回溯,因此回溯要出现在递归后面,即加进判断大括号内,否则出错。

class Solution {
public:
    vector<string> ans;
    vector<int> path_vec;   

    void backtrack(TreeNode* root){
        path_vec.emplace_back(root->val);       // 中
        
        if(!root->left && !root->right){        // 叶子节点处理
            string path = to_string(path_vec[0]);
            for(int i = 1; i < path_vec.size(); ++i){
                path += "->" + to_string(path_vec[i]);
            }
            ans.emplace_back(path); 
            return;
        }     
        if(root->left){
            backtrack(root->left);      // 左 递归
            path_vec.pop_back();        // 回溯
        }
        if(root->right){
            backtrack(root->right);     // 右 递归
            path_vec.pop_back();        // 回溯
        } 
        
    }
    
    vector<string> binaryTreePaths(TreeNode* root) {
        if(!root) return ans;
        backtrack(root);
        return ans;
    }
};
  • 回溯代码精简
class Solution {
private:

    void traversal(TreeNode* cur, string path, vector<string>& result) {	// path为非引用调用,否则不能回溯
        path += to_string(cur->val); // 中
        if (cur->left == NULL && cur->right == NULL) {
            result.push_back(path);
            return;
        }
        if (cur->left) traversal(cur->left, path + "->", result); 	// 左,path + "->" 隐藏回溯
        if (cur->right) traversal(cur->right, path + "->", result); // 右
    }

public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        string path;
        if (root == NULL) return result;
        traversal(root, path, result);
        return result;

    }
};

2. 前序遍历 栈 迭代

一个用来模拟递归,存放节点
另一个用来存放对应的遍历路径

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> ans;
        stack<TreeNode*> nodeST;            // 存放节点
        stack<string> pathST;               // 存放遍历路径
        if(!root) return ans;
        nodeST.push(root);
        pathST.push(to_string(root->val));  // 处理根节点
        while(!nodeST.empty()){
            TreeNode* curr = nodeST.top();  // 中
            nodeST.pop();
            string path = pathST.top();
            pathST.pop();
            if(!curr->left && !curr->right){// 叶子节点
                ans.emplace_back(path);
                continue;
            }
            if(curr->right){                // 右(栈先进后出,入栈顺序:右->左)
                nodeST.push(curr->right);
                pathST.push(path + "->" +  to_string(curr->right->val));
            }
            if(curr->left){                 // 左
                nodeST.push(curr->left);
                pathST.push(path + "->" +  to_string(curr->left->val));
            }
        }
        return ans;
    }
};

404. 左叶子之和 ●

给定二叉树的根节点 root ,返回所有左叶子之和

1. 前序遍历 栈 迭代(空指针标记左孩子)

利用空指针标记左孩子,再判断是不是叶子节点

class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        stack<TreeNode*> st;
        int ans = 0;
        if(!root) return ans;
        st.push(root);
        TreeNode* curr;
        while(!st.empty()){
            curr = st.top();            // 中
            st.pop();
            if(curr == nullptr){        // 下一个为左孩子
                curr = st.top();
                st.pop();
                if(!curr->left && !curr->right) {
                    ans += curr->val;
                }
            }
            if(curr->right) st.push(curr->right);   // 右孩子入栈
            if(curr->left){
                st.push(curr->left);    // 左孩子入栈
                st.push(nullptr);       // 空指针标记入栈
            }   
        }
        return ans;
    }
};

2. 递归

不用标记,直接判断左孩子是否为叶子节点。

class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if (root == NULL) return 0;	// 空节点,退出
        int midValue = 0;
        // 存在左孩子且为叶子节点,那么对于这个节点得到的值就是midValue
        if (root->left != NULL && root->left->left == NULL && root->left->right == NULL) {
            midValue = root->left->val;
        }
        return midValue + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right);
    }
};

513. 找树左下角的值 ●●

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。
假设二叉树中至少有一个节点。

在这里插入图片描述

1. 层序遍历 队列 迭代

将每一层的第一个元素赋值给ans即可。

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        int ans = 0;
        que.push(root);
        while(!que.empty()){
            int size = que.size();
            for(int i = 0; i < size; ++i){
                TreeNode* curr = que.front();	//中
                que.pop();
                if(i == 0) ans = curr->val;		// 新一层的最左边元素值
                if(curr->left) que.push(curr->left);	// 左
                if(curr->right) que.push(curr->right);	// 右
            }
        }
        return ans;
    }
};

2. 前序遍历 DFS 递归

创建一个全局变量curr_depth来记录当前的最大深度,当更大深度首次出现时,则赋值给ans

class Solution {
public:
    int ans;
    int curr_depth = 0;

    void dfs(TreeNode* node, int depth){
        if(!node) return;
        if(depth > curr_depth){    // 新层最左边元素
            ans = node->val;
            ++curr_depth;			// 最大深度+1
        }
        dfs(node->left, depth+1);	// 左孩子,隐藏回溯
        dfs(node->right, depth+1);	// 右孩子,隐藏回溯
    }
 
    int findBottomLeftValue(TreeNode* root) {
        dfs(root, 1);
        return ans;
    }
};

112. 路径总和 ●

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

路径总和sum没有说一定是正的;可以为负数,所以必须遍历到叶子节点才能结束,不能提前结束;

1. 递归(回溯)

  • 时间复杂度: O ( N ) O(N) O(N),其中 N 是树的节点数。对每个节点访问一次。
  • 空间复杂度: O ( H ) O(H) O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O ( log ⁡ N ) O(\log N) O(logN)
class Solution {
public:
    bool backtrack(TreeNode* node, int targetSum, int &sum){
        sum += node->val;
        if(!node->left && !node->right && sum == targetSum) return true;	// 叶子节点
        if(node->left){
            if(backtrack(node->left, targetSum, sum)) return true;
            sum -= node->left->val;		// 回溯
        }
        if(node->right){
            if(backtrack(node->right, targetSum, sum)) return true;
            sum -= node->right->val;	// 回溯
        }
        return false;
    }
    
    bool hasPathSum(TreeNode* root, int targetSum) {
        if(!root) return false;
        int sum = 0;
        return backtrack(root, targetSum, sum);
    }
};
  • 递归精简(隐藏回溯)
class Solution {
public:
    bool hasPathSum(TreeNode* root, int targetSum) {
        if(!root) return false;
        int value = root->val;
        if(!root->left && !root->right) return value == targetSum;
        return hasPathSum(root->left, targetSum - value) || hasPathSum(root->right, targetSum - value);
    }
};

3. 迭代

用栈模拟递归,此时栈里一个元素不仅要记录该节点指针,还要记录该节点之前的路径数值总和

c++就我们用pair结构来存放这个栈里的元素。(#include <utility>

定义为:pair<Treenode*, int>

class Solution {
public:
    typedef pair<TreeNode*, int> newPair;           // typedef简化声明 pair类型对象
    bool hasPathSum(TreeNode* root, int targetSum) {
        if(!root) return false;
        stack<newPair> st;                          // pair类型的栈  #include <utility>
        st.push(newPair(root, 0));                  // first -> 节点; second -> 该节点之前路径的和
        while(!st.empty()){
            newPair pair_curr = st.top();           // 中
            st.pop(); 
            TreeNode* curr = pair_curr.first;
            int curr_sum = pair_curr.second + curr->val;    // 算上当前节点的和
            if(!curr->left && !curr->right && curr_sum == targetSum) return true;
            if(curr->right){                        // 右入栈
                st.push(newPair(curr->right, curr_sum));
            }
            if(curr->left){                         // 左入栈
                st.push(newPair(curr->left, curr_sum));
            }
        }
        return false;
    }
};

113. 路径总和 II ●●

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径

1. 回溯

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;

    void backtrack(TreeNode* node, int diff){
        diff -= node->val;              // 当前差值
        path.emplace_back(node->val);   // 加入路径
        if(!node->left && !node->right){// 叶子节点
            if(diff == 0) ans.emplace_back(path);   // 差值为零,返回
            return;
        }
        
        if(node->left){                 // 递归左孩子
            backtrack(node->left, diff);
            path.pop_back();            // 一个递归、一个回溯
        }

        if(node->right){                // 递归右孩子
            backtrack(node->right, diff);
            path.pop_back();            // 一个递归、一个回溯
        }
    }
    
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        if(!root) return ans;
        backtrack(root, targetSum);
        return ans;
    }
};

437. 路径总和 III ●●

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

在这里插入图片描述

1. 分治

对每一个节点都求一次以该节点为路径起点的路径数量。

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2),其中 N 为该二叉树节点的个数。对于每一个节点,求以该节点为起点的路径数目时,则需要遍历以该节点为根节点的子树的所有节点,因此求该路径所花费的最大时间为 O(N),我们会对每个节点都求一次以该节点为起点的路径数目,因此时间复杂度为 O ( N 2 ) O(N^{2}) O(N2)
  • 空间复杂度:O(N),考虑到递归需要在栈上开辟空间。
class Solution {
public:
    int rootSum(TreeNode* node, long long target){ 
        if(!node) return 0;
        int count = 0;
        if(target == node->val) ++count;    // 当前节点满足条件,计数+1,并继续往下查找
        count += rootSum(node->left, target-node->val) + rootSum(node->right, target-node->val);
        return count;
        // return (target == node->val) 
        //        + rootSum(node->left, target-node->val) 
        //        + rootSum(node->right, target-node->val);
    }  
    
    int pathSum(TreeNode* root, int targetSum) {
        if(!root) return 0;
        int ans = 0;                            // 分治
        ans += rootSum(root, targetSum);        // 以root为起点,查找以该节点为路径开头,且和为targetSum的路径数量
        ans += pathSum(root->left, targetSum);  // 以root->left为根的子树中,和为targetSum的路径数量
        ans += pathSum(root->right, targetSum); // 以root->right为根的子树中,查找和为targetSum的路径数量
        return ans;
    }
};

2. 前缀和

解法一中应该存在许多重复计算。

我们定义节点的前缀和为:由根结点到当前结点的路径上所有节点的和。

我们利用先序遍历二叉树,记录下根节点 root 到当前节点 node 的路径上除当前节点以外所有节点的前缀和,在已保存的路径前缀和中查找是否存在前缀和刚好等于当前节点到根节点的前缀和 curr 减去 targetSum,如果存在,表示存在中间节点到 node 节点(结尾)的路径和为 targetSum,且这些路径的数量由 preCnt 前缀和哈希表给出。

注意:

  1. 对于空路径我们也需要保存预先处理一下,此时因为空路径不经过任何节点,因此它的前缀和为 0,即preCnt[0] = 1;
  2. 退出当前节点的路径时,需要注意更新哈希表中对应前缀和的数量。
  3. 利用深度优先搜索,计算以每个节点为结尾时的符合条件的路径之和。
  • 时间复杂度:O(N),其中 N 为二叉树中节点的个数。利用前缀和只需遍历一次二叉树即可。
  • 空间复杂度:O(N)。
class Solution {
public:
    unordered_map<long long, int> preCnt;       // <路径和, 个数>
    
    int dfs(TreeNode* node, long long curr, int target){
        if(node == nullptr) return 0;
        int cnt = 0;
        curr += node->val;                      // curr 为根节点到 node 节点的路径和,存在curr-target表示存在中间节点到node节点的路径和为target
        if(preCnt.count(curr-target)){          // 以node节点结尾的路径和为target的数量
            cnt += preCnt[curr-target];
        }
        
        ++preCnt[curr];                         // 哈希记录路径和
        cnt += dfs(node->left, curr, target);   // 以node->left结尾的路径和为target的数量
        cnt += dfs(node->right, curr, target);  // 以node->eight结尾的路径和为target的数量
        --preCnt[curr];                         // 退出当前节点,回溯
        return cnt;
    }
    
    int pathSum(TreeNode* root, int targetSum) {
        preCnt[0] = 1;                      // 有一条空路径,和为0
        return dfs(root, 0, targetSum);
    }
};

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

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

在这里插入图片描述

  • 在后序遍历序列中,最后一个元素为树的根节点
  • 在中序遍历序列中,根节点的左边为左子树,根节点的右边为右子树
  • 中序遍历和后序遍历序列的长度一样

1. 递归

二叉树的还原过程:
(1)首先在后序遍历序列中找到根节点(最后一个元素);
(2)根据根节点在中序遍历序列中找到根节点的位置
(3)根据根节点的位置将中序遍历序列分为左子树和右子树;
(4)根据根节点的位置确定左子树和右子树在中序数组和后序数组中的左右边界位置
(5)递归构造左子树和右子树;
(6)返回根节点结束。

边界位置关系:
在这里插入图片描述

class Solution {
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
    	// 空树
        if(inorder.size() == 0) return nullptr;

        // 后序末尾确定中间节点元素值
        int rootVal = postorder.back();
        TreeNode* root = new TreeNode(rootVal);

        // 叶子节点
        if(inorder.size() == 1) return root;
		
		// 找分割点(根节点在中序数组中的位置)
        int rootIndex;
        for(rootIndex = 0; rootIndex < inorder.size(); ++rootIndex){
            if(inorder[rootIndex] == rootVal) break;
        }

        // 左子树前序
        vector<int> leftInorder(inorder.begin(), inorder.begin() + rootIndex);
        // 右子树前序
        vector<int> rightInorder(inorder.begin() + rootIndex + 1, inorder.end());

        // 左子树后序
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        // 右子树后序
        vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.begin() + leftInorder.size() + rightInorder.size());
		
		// 递归左右子树
        root->left = buildTree(leftInorder, leftPostorder);
        root->right = buildTree(rightInorder, rightPostorder);
		// 返回中间节点(根节点)
        return root;
    }
};

以上代码中在每次递归创建新的数组来记录子树序列,时间消耗较大,因此可以通过传递子树序列在原序列的边界位置索引来进行复杂度优化;

此外,在查找根节点在中序数组中的位置时每次新数组的使用限制了只能for循环进行遍历查找,此处可以用哈希表unordered_map<int, int>进行优化。

  • 时间复杂度:O(n),其中 n 是树中的节点个数。
  • 空间复杂度:O(n)。我们需要使用 O(n) 的空间存储哈希表,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h<n,所以总空间复杂度为 O(n)。
class Solution {
public:
 	unordered_map<int, int> hashMap;    // 索引哈希表

    TreeNode* IndexTraversal(vector<int>& inorder, vector<int>& postorder, int inStart, int inEnd, int postStart, int postEnd){
        // 中序区间:[inStart, inEnd),后序区间[postStart, postEnd)
        // 空树
        if(inEnd - inStart == 0) return nullptr;

        // 后序末尾确定根节点元素值(注意左闭右开)
        int rootVal = postorder[postEnd - 1];
        TreeNode* root = new TreeNode(rootVal);

        // 叶子节点
        if(inEnd - inStart == 1) return root;

        // 查找根节点在中序数组中的位置
        int rootIndex = hashMap[rootVal];   

        // 子树序列边界位置分割
        int leftInStart = inStart;
        int leftInEnd = rootIndex;
        int leftPostStart = postStart;
        int leftPostEnd = postStart + leftInEnd - leftInStart;

        int rightInStart = rootIndex + 1;
        int rightInEnd = inEnd;
        int rightPostStart = postStart + leftInEnd - leftInStart;
        int rightPostEnd = postEnd - 1;

        // 递归左右子树
        root->left = IndexTraversal(inorder, postorder, leftInStart, leftInEnd, leftPostStart,leftPostEnd);
        root->right = IndexTraversal(inorder, postorder, rightInStart, rightInEnd, rightPostStart, rightPostEnd);

        return root;
    }

    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        for(int i = 0; i < inorder.size(); ++i){
            hashMap[inorder[i]] = i;    // 建立根节点元素位置的哈希表
        }
        return IndexTraversal(inorder, postorder, 0, inorder.size(), 0, postorder.size());
    }
};

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

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

106. 从中序与后序遍历序列构造二叉树 ●●类似。

class Solution {
public:
    TreeNode* traversal(vector<int>& preorder, vector<int>& inorder){
        if(inorder.size() == 0) return nullptr;
        // 前序数组首位
        int rootVal = preorder[0];
        TreeNode* root = new TreeNode(rootVal);
        // 叶子节点
        if(inorder.size() == 1) return root;
        // 查找根节点在中序的位置
        int rootIndex;
        for(rootIndex = 0; rootIndex < inorder.size(); ++rootIndex){
            if(inorder[rootIndex] == rootVal) break;
        }
        // 左右子树序列生成
        vector<int> leftIn(inorder.begin(), inorder.begin() + rootIndex);
        vector<int> leftPre(preorder.begin() + 1, preorder.begin() + 1 + leftIn.size());
        vector<int> rightIn(inorder.begin() + rootIndex + 1, inorder.end());
        vector<int> rightPre(preorder.begin() + 1 + leftIn.size(), preorder.end());

        root->left = traversal(leftPre, leftIn);
        root->right = traversal(rightPre, rightIn);

        return root;
    }
    
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder){
        return traversal(preorder, inorder);
    }
};
class Solution {
public:
    unordered_map<int, int> hashMap;
    
    TreeNode* traversal(vector<int>& preorder, vector<int>& inorder, int preStart, int preEnd, int InStart, int InEnd){
        if(InEnd - InStart == 0) return nullptr;

        int rootVal = preorder[preStart];
        TreeNode* root = new TreeNode(rootVal);

        if(InEnd - InStart == 1) return root;

        int rootIndex = hashMap[rootVal];
        int leftInStart = InStart;
        int leftInEnd = rootIndex;
        int leftPreStart = preStart + 1;
        int leftPreEnd = preStart + 1+ leftInEnd - leftInStart;

        int rightInStart = rootIndex + 1;
        int rightInEnd = InEnd;
        int rightPreStart = preStart + 1 + leftInEnd - leftInStart;
        int rightPreEnd = preEnd;

        root->left = traversal(preorder, inorder, leftPreStart, leftPreEnd, leftInStart, leftInEnd);
        root->right = traversal(preorder, inorder, rightPreStart, rightPreEnd, rightInStart, rightInEnd);

        return root;
    }
    
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder){
        for(int i = 0; i < inorder.size(); ++i){
            hashMap[inorder[i]] = i;    // 根节点在中序数组的位置
        }
        return traversal(preorder, inorder, 0, preorder.size(), 0, inorder.size());
    }
};

654. 最大二叉树 ●●

给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
1.创建一个根节点,其值为 nums 中的最大值。
2.递归地在最大值 左边 的 子数组前缀上 构建左子树。
3.递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。

找到数组中的最大值索引,并分割左右子树数组,递归进行构造。

1. 递归,创建左右新数组进行构建

class Solution {
public:
    TreeNode* buildTree(vector<int> &nums){
        if(nums.size() == 0) return nullptr;
        int maxIndex = 0;
        int maxVal = nums[0];
        for(int i = 0; i < nums.size(); ++i){
            if(nums[i] > maxVal){
                maxVal = nums[i];
                maxIndex = i;
            }
        }
        TreeNode* node = new TreeNode(maxVal);
        if(nums.size() == 1) return node;
        vector<int> left(nums.begin(), nums.begin() + maxIndex);
        vector<int> right(nums.begin() + maxIndex + 1, nums.end());
        node->left = buildTree(left);
        node->right = buildTree(right);
        return node;
    }
    
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return buildTree(nums);
    }
};

2. 传递区间索引范围进行遍历,减少消耗

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)。方法 buildTree 一共被调用 n 次。每次递归寻找根节点时,需要遍历当前索引范围内所有元素找出最大值。一般情况下,每次遍历的复杂度为 O ( log ⁡ n ) O(\log n) O(logn),总复杂度为 O ( n log ⁡ n ) O\big(n\log n\big) O(nlogn)。最坏的情况下,数组 nums 有序,总的复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 空间复杂度: O ( n ) O(n) O(n)。递归调用深度为 n。平均情况下,长度为 n 的数组递归调用深度为 O ( log ⁡ n ) O(\log n) O(logn)

class Solution {
public:
    TreeNode* buildTree(vector<int> &nums, int start, int end){
        if(end - start < 0) return nullptr;
        int maxIndex = start;
        int maxVal = nums[start];
        for(int i = start; i <= end; ++i){      // 左闭右闭区间,找最大值
            if(nums[i] > maxVal){
                maxVal = nums[i];
                maxIndex = i;
            }
        }
        TreeNode* node = new TreeNode(maxVal);  // 叶子节点
        if(end - start == 0) return node;

        int lstart = start;                 // 更新左右子树索引范围
        int lend = maxIndex-1;
        int rstart = maxIndex+1;
        int rend = end;

        node->left = buildTree(nums, lstart, lend);     // 递归左右子树
        node->right = buildTree(nums, rstart, rend);
        return node;
    }
    
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return buildTree(nums, 0, nums.size()-1);
    }
};

617. 合并二叉树 ●

在这里插入图片描述

1. 前序遍历 DFS 递归

  • 时间复杂度: O ( min ⁡ ( m , n ) ) O(\min(m,n)) O(min(m,n)),其中 m 和 n 分别是两个二叉树的节点个数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会对该节点进行显性合并操作,因此被访问到的节点数不会超过较小的二叉树的节点数。
  • 空间复杂度: O ( min ⁡ ( m , n ) ) O(\min(m,n)) O(min(m,n)),其中 m 和 n 分别是两个二叉树的节点个数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。
class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
         // 有一个为空节点就返回另外一个,即使另外一个也可能为空节点
        if(!root2) return root1;    
        if(!root1) return root2;
        // 两个都不为空节点,则元素值相加到root1上并递归、返回
        root1->val = root1->val + root2->val;                   // 中
        root1->left = mergeTrees(root1->left, root2->left);     // 左
        root1->right = mergeTrees(root1->right, root2->right);  // 右
        return root1;
    }
};

2. 层序遍历 BFS 队列 迭代

注意处理子节点时的操作,分不同情况进行判断;复杂度与以上类似。

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if(!root1) return root2;
        if(!root2) return root1;
        queue<TreeNode*> que;
        que.push(root1);
        que.push(root2);
        while(!que.empty()){
            TreeNode* curr1 = que.front();
            que.pop();
            TreeNode* curr2 = que.front();
            que.pop();

            // 当前两个节点一定都不为空
            curr1->val += curr2->val;

            // 两个节点的左子节点都不为空,则加入队列
            if(curr1->left && curr2->left){
                que.push(curr1->left);
                que.push(curr2->left);
            }
            // 两个节点的右子节点都不为空,则加入队列
            if(curr1->right && curr2->right){
                que.push(curr1->right);
                que.push(curr2->right);
            }
            // 只有节点1的子节点为空,将节点2的子节点赋值过去
            if(!curr1->left && curr2->left){
                curr1->left = curr2->left;
            }
            if(!curr1->right && curr2->right){
                curr1->right = curr2->right;
            }
            // 以上情况之外,就是只有节点2的子节点为空,不处理
        }
        return root1;
    }
};

700. 二叉搜索树中的搜索 ●

给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。

对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向

1. 迭代法

  • 时间复杂度:O(N),其中 N 是二叉搜索树的节点数。最坏情况下二叉搜索树是一条链,且要找的元素比链末尾的元素值还要小(大),这种情况下我们需要迭代 N 次。
  • 空间复杂度:O(1)。没有使用额外的空间。
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        TreeNode* curr = root;          // 当前节点
        while(curr){
            if(curr->val > val){        // 值过大,遍历左子树
                curr = curr->left;
            }else if(curr->val < val){  // 值过小,遍历右子树
                curr = curr->right;
            }else{
                return curr;            // 值相同,返回节点
            }
        }
        return nullptr;                 // 未找到
    }
};

2. 递归法

  • 时间复杂度:O(N),其中 N 是二叉搜索树的节点数。最坏情况下二叉搜索树是一条链,且要找的元素比链末尾的元素值还要小(大),这种情况下我们需要递归 N 次。
  • 空间复杂度:O(N)。最坏情况下递归需要 O(N) 的栈空间。
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if(root == nullptr || root->val == val) return root;	// 返回 空节点 或 目标节点
        if(root->val > val) return searchBST(root->left, val);	// 值过大,遍历左子树
        if(root->val < val) return searchBST(root->right, val);	// 值过小,遍历右子树
        return nullptr;                 // 未找到
    }
};

98. 验证二叉搜索树 ●●

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

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

因此可以中序遍历用数组记录所有元素,再遍历数组进行判断。

但也可以全局定义一个待比较值,直接在中序遍历过程中进行比较判断并不断更新待比较值,需要注意的是:

不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了,我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点,因此还要与前面的节点进行比较。
在这里插入图片描述

1. 中序遍历 递归

  • 初始化待比较值为long long型最小值,应对题目中存在整型最小值的情况;
class Solution {
public:
    long long preValue = LONG_MIN;              // 初始化前一个数为长型最小值
    bool isValidBST(TreeNode* root) {
        if(!root) return true;
        bool left = isValidBST(root->left);     // 左
        if(root->val <= preValue){              // 中
            return false;                       // 中序遍历时与前一个数比较判断有序性
        }else{
            preValue = root->val;       
        }
        bool right = isValidBST(root->right);   // 右
        return left && right;                   // 左右子树的有效性
    }
};
  • 或者定义待比较值为节点类型
class Solution {
public:
    TreeNode* preNode = nullptr;              // 待比较节点
    bool isValidBST(TreeNode* root) {
        if(!root) return true;
        bool left = isValidBST(root->left);     // 左
        if(preNode != nullptr && root->val <= preNode->val){      // 中
            return false;                       // 无效,退出
        }else{
            preNode = root;                     // 中序遍历时与前一个数比较判断有序性
        }
        bool right = isValidBST(root->right);   // 右
        return left && right;                   // 左右子树的有效性
    }
};

2. 中序遍历 迭代

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* preNode = nullptr;              // 待比较节点
        TreeNode* curr = root;
        while(curr != nullptr || !st.empty()){
            if(curr){
                st.push(curr);              // 节点入栈
                curr = curr->left;          // 节点指针指向左孩子
            }else{                          // 遍历完所有左孩子
                curr = st.top();            
                st.pop();                   // 中
                if(preNode && curr->val <= preNode->val) return false;
                preNode = curr;
                curr = curr->right;         // 节点指针指向右孩子
            }
        }
        return true;
    }
};

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

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。

中序遍历下,输出的二叉搜索树节点的数值是有序序列。
因此在中序遍历时,就类似于遍历有序数组并求相邻数字之间的最小差值。
在这里插入图片描述

1. 迭代

class Solution {
public:
    int getMinimumDifference(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* curr = root;
        TreeNode* pre = nullptr;
        int ans = INT_MAX;
        while(curr != nullptr || !st.empty()){
            if(curr){
                st.push(curr);          // 中入栈
                curr = curr->left;      // 指针来访问节点,访问到最底层
            }
            else{
                curr = st.top();        // 左出栈
                st.pop();               // 中
                if(pre != nullptr){     // 与前一个节点求差值
                    ans = min(ans, curr->val - pre->val);
                }
                pre = curr; 
                curr = curr->right;     // 右
            }
        }
        return ans;
    }
};

2. 递归

class Solution {
private:
int result = INT_MAX;
TreeNode* pre;
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. 二叉搜索树中的众数 ●

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。

1. 哈希表 + 中序递归遍历

unordered_map<int, vector<int>> hashMap; 存储相应频率下的元素集合,再取最大频率下的集合。

class Solution {
public:
    unordered_map<int, vector<int>> hashMap;    // <频率,元素集合>
    TreeNode* pre = nullptr;
    int maxTimes = 1;
    int currTimes = 1;

    void traversal(TreeNode* curr){
        if(curr == nullptr) return;
        traversal(curr->left);                  // 左
        if(pre != nullptr){                 
            if(pre->val == curr->val){          // 中
                ++currTimes; 
                maxTimes = max(maxTimes, currTimes);    // 最大频率    
            }
            else{
                currTimes = 1;
            }    
        }
        hashMap[currTimes].emplace_back(curr->val); // 当前频率下的哈希表
        pre = curr;
        traversal(curr->right);                 // 右
    }

    vector<int> findMode(TreeNode* root) {
        traversal(root);
        return hashMap[maxTimes];
    }
};

2. 最大频率集合 + 中序递归遍历

用一个集合记录最大频率下的元素,若出现更大频率,则先清空集合再进行记录。

class Solution {
public:
    vector<int> ans;
    TreeNode* pre = nullptr;
    int maxTimes = 1;
    int currTimes = 1;

    void traversal(TreeNode* curr){
        if(curr == nullptr) return;
        traversal(curr->left);                  // 左
        if(pre == nullptr){                     // 中
            currTimes = 1;                      
        }else if(pre->val == curr->val){         
            ++currTimes;     
        }
        else{
            currTimes = 1;                      // 不相等元素
        }    
        if(currTimes > maxTimes){               // 新的最大频率
            maxTimes = currTimes;
            ans.clear();                        // 清空集合
            ans.emplace_back(curr->val);        // 记录当前元素
        }else if(currTimes == maxTimes){
            ans.emplace_back(curr->val);        // 记录最大频率下的元素
        }
        pre = curr;         // 更新pre节点
        traversal(curr->right);                 // 右
    }

    vector<int> findMode(TreeNode* root) {
        traversal(root);
        return ans;
    }
};

拓展:普通二叉树求众数集合

把这个树的元素都遍历一次,用map<元素, 频率>统计元素的频率,把频率排个序,最后取前面高频的元素的集合。

class Solution {
private:

void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍历
    if (cur == NULL) return ;
    map[cur->val]++; // 统计元素频率
    searchBST(cur->left, map);
    searchBST(cur->right, map);
    return ;
}
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {	// 频率排序规则
    return a.second > b.second;
}
public:
    vector<int> findMode(TreeNode* root) {
        unordered_map<int, int> map; // key:元素,value:出现频率
        vector<int> result;
        if (root == NULL) return result;
        searchBST(root, map);
        vector<pair<int, int>> vec(map.begin(), map.end());
        sort(vec.begin(), vec.end(), cmp); // 给频率排个序
        result.push_back(vec[0].first);
        for (int i = 1; i < vec.size(); i++) {
            // 取最高的放到result数组中
            if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
            else break;
        }
        return result;
    }
};

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

给定一个二叉树, 找到该树中两个指定节点最近公共祖先(一个节点也可以是它自己的祖先)。
在这里插入图片描述
p = 5, q = 1. > out = 3.
p = 5, q = 4. > out = 5.

后序遍历(回溯)

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

但是还存在第二个情况,就是节点本身 p(q),它拥有一个子孙节点 q§。

使用后序遍历,回溯的过程,就是从低向上遍历节点,一旦发现满足第一种情况的节点,就是最近公共节点了。

但是如果p或者q本身就是最近公共祖先呢?

其实只需要找到一个节点是 p 或者 q 的时候,直接返回当前节点,无需继续递归子树。如果接下来的(向上的)遍历中找到了后继节点(父辈节点)满足第一种情况则修改返回值为后继节点,否则,继续返回已找到的节点即可。

在递归函数有返回值的情况下:
如果要搜索一条边,递归函数返回值不为空的时候,立刻返回;

if (递归函数(root->left)) return ;
if (递归函数(root->right)) return ;

如果要搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)。

left = 递归函数(root->left);
right = 递归函数(root->right);
left与right的逻辑处理;
  • 时间复杂度 O(N) : 其中 N 为二叉树节点数;最差情况下,需要递归遍历树的所有节点。
  • 空间复杂度 O(N) : 最差情况下,递归深度达到 N ,系统使用 O(N) 大小的额外空间。

在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == q || root == p || root == NULL) return root;     // 判断当前节点
        TreeNode* left = lowestCommonAncestor(root->left, p, q);    // 遍历左子树
        TreeNode* right = lowestCommonAncestor(root->right, p, q);  // 遍历右子树
        if(left != NULL && right != NULL){
            return root;    // 左右子树都有返回值,则当前为最近公共祖先
        }else if(left != NULL){
            return left;    // 只有左子树有返回值
        }else if(right != NULL){
            return right;   // 只有右子树有返回值
        }
        return NULL;
    }
};

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

给定一个二叉搜索树, 找到该树中两个指定节点最近公共祖先。(一个节点也可以是它自己的祖先)
在这里插入图片描述
p = 4, q = 7;输出: 6
p = 2, q = 4;输出: 2

与普通二叉树不同,二叉搜索树是有序的,只要从上到下遍历的时候,cur 节点是数值在 [p, q] 区间中则说明该节点 cur 就是最近公共祖先了,而且不需要遍历整棵树,找到结果直接返回。

1. 层序遍历 队列 迭代

未根据节点的大小进行搜索方向的判断调整,效率较低。

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        int Min = min(p->val, q->val);
        int Max = max(p->val, q->val);
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()){
            TreeNode* curr = que.front();
            que.pop();
            if(curr->val <= Max && curr->val >= Min) return curr;
            if(curr->left) que.push(curr->left);
            if(curr->right) que.push(curr->right);
        }
        return NULL;
    }
};

2. 前序遍历

根据节点的大小进行搜索方向的判断调整。

当节点值同时大于p、q值时,往左子树搜索;
当节点值同时小于p、q值时,往右子树搜索;
否则节点值在区间内,满足条件,直接返回。

递归

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == NULL) return NULL;

        if(root->val > p->val && root->val > q->val){                   // 中
            TreeNode* left = lowestCommonAncestor(root->left, p, q);    // 左
            if(left != NULL) return left;
        }

        if(root->val < p->val && root->val < q->val){
            TreeNode* right = lowestCommonAncestor(root->right, p, q);  // 右
            if(right != NULL) return right;
        }

        return root;	// 节点值在区间内
    }
};

迭代

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        while(root != NULL){
            if(root->val > p->val && root->val > q->val){
                root = root->left;	// 往左
            }else if(root->val < p->val && root->val < q->val){
                root = root->right;	// 往右
            }else return root;		// 节点值在区间内
        }
        return NULL;
    }
};

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

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
在这里插入图片描述
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]

1. 中序递归遍历,查找第一个大于目标值的节点

第一个大于目标值的节点的左孩子,
或最后一个小于目标值的节点的右孩子。

class Solution {
public:
    TreeNode* pre = nullptr;

    bool traversal(TreeNode* curr, int val){            
        if(curr == nullptr) return false;               // 中序遍历,递增序列
        if(traversal(curr->left, val)) return true;     // 左
        if(curr->val > val){                            // 中
            if(curr->left == nullptr){                  // 当前节点大于目标值,且不存在左孩子
                curr->left = new TreeNode(val);         // 则直接插入左孩子
            }else{
                pre->right = new TreeNode(val);         // 否则插入上一个更小节点的右孩子
            }
            return true;                                // 返回true,表示插入完成
        }
        pre = curr;
        if(traversal(curr->right, val)) return true;    // 右
        return false;
    }

    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root == nullptr){    
            return new TreeNode(val);       // 空节点
        } 
        if(traversal(root, val) == false){
            pre->right = new TreeNode(val); // 遍历完成后未插入目标值,则在最大节点的右孩子插入
        }
        return root;
    }
};

2. 模拟,寻找空节点插入

在这里插入图片描述

  • 递归

终止条件:找到遍历的节点为 null 的时候,就是要插入节点的位置了,并把插入的节点返回;

单层递归逻辑:根据节点数值决定递归方向。

class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root == nullptr){    
            return new TreeNode(val);       // 空节点
        }
        if(root->val > val){
            root->left = insertIntoBST(root->left, val);
        }else{
            root->right = insertIntoBST(root->right, val);
        }
        return root;
    }
};
  • 迭代
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root == nullptr){    
            return new TreeNode(val);       // 空节点
        } 
        TreeNode* curr = root;
        while(curr != nullptr){
            if(curr->val > val){
                if(curr->left == nullptr){
                    curr->left = new TreeNode(val);
                    return root;
                }
                curr = curr->left;
            }else{
                if(curr->right == nullptr){
                    curr->right = new TreeNode(val);
                    return root;
                }
                curr = curr->right;
            }
        }
        return root;
    }
};

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

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
在这里插入图片描述
输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]

在这里插入图片描述
删除节点涉及树结构调整。

有以下五种情况:

  1. (1)没找到删除的节点,遍历到空节点直接返回了
  2. 找到删除的节点
    (2)左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    (3)删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    (4)删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    (5)左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
  • 时间复杂度: O ( l o g N ) O(logN) O(logN),H 值得是树的高度,若树是一个平衡树则 H = log ⁡ N H = \log N H=logN。在算法的执行过程中,我们一直在树上向左或向右移动。首先先用 O ( H 1 ) O(H_1) O(H1)的时间找到要删除的节点, H 1 H_1 H1 值得是从根节点到要删除节点的高度。然后删除节点需要 O ( H 2 ) O(H_2) O(H2) 的时间, H 2 H_2 H2指的是从要删除节点到替换节点的高度。由于 O ( H 1 + H 2 ) = O ( H ) O(H_1 + H_2) = O(H) O(H1+H2)=O(H)
  • 空间复杂度: O ( H ) O(H) O(H),递归时堆栈使用的空间,H 是树的高度。

递归

class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if(root == nullptr) return root;
        if(root->val > key){
            root->left = deleteNode(root->left, key);   // 根据节点值大小决定查找方向
        }else if(root->val < key){
            root->right = deleteNode(root->right, key); // 根据节点值大小决定查找方向
        }else{                                          
            TreeNode* retNode;
            if(root->right != nullptr){                 // 找到目标节点
                retNode = root->right;        			// 目标节点存在右孩子,则右孩子上移
                TreeNode* leftNode = root->right;       // 并将左孩子移到右孩子的最左边孩子节点
                while(leftNode->left != nullptr){
                    leftNode = leftNode->left;
                }
                leftNode->left = root->left;
            }else{
                retNode = root->left;   // 目标节点不存在右孩子,则直接左孩子上移(包含目标节点为叶子节点的情况)
            }   
            delete root;                                // 释放内存
            return retNode;                           	// 返回调整后的节点
        }
        return root;
    }
};

迭代

需要判断目标节点在父节点 pre 上的位置。

class Solution {
public:
    TreeNode* deleteTheNode(TreeNode* target){  // 删除目标节点
        TreeNode* retNode;
        if(target->right != nullptr){               
            retNode = target->right;        	// 目标节点存在右孩子,则右孩子上移
            TreeNode* leftNode = target->right; // 并将左孩子移到右孩子的最左边孩子节点
            while(leftNode->left != nullptr){
                leftNode = leftNode->left;
            }
            leftNode->left = target->left;
        }else{
            retNode = target->left;   // 目标节点不存在右孩子,则直接左孩子上移(包含目标节点为叶子节点的情况)
        }   
        delete target;
        return retNode;
    }
    
    TreeNode* deleteNode(TreeNode* root, int key) {
        if(root == nullptr) return root;
        TreeNode* pre = nullptr;
        TreeNode* curr = root;
        while(curr != nullptr){     
            if(curr->val == key){
                if(pre == nullptr){     // 删除根节点
                    return deleteTheNode(curr);
                }
                if(pre->val < key){     // 删除pre右孩子
                    pre->right = deleteTheNode(curr);
                }else{                  // 删除pre左孩子
                    pre->left = deleteTheNode(curr);
                }
                break;
            }
            pre = curr;
            if(curr->val > key){        // 判断迭代查找方向
                curr = curr->left;
            }else{
                curr = curr->right;
            }
        }
        return root;
    }
};

普通二叉树的删除节点操作

不利用搜索树的有序性,遍历整棵树,通过交换值的操作来删除目标节点,包括两个操作:

  1. 与目标节点的右子树最左边节点的值进行交换;
  2. 当交换到目标节点处不存在右子树时,用空指针覆盖(删除)。
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root;
        if (root->val == key) {
            if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用
                return root->left;
            }
            TreeNode *cur = root->right;
            while (cur->left) {
                cur = cur->left;
            }
            swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。
        }
        root->left = deleteNode(root->left, key);
        root->right = deleteNode(root->right, key);
        return root;
    }
};

669. 修剪二叉搜索树 ●●

给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案
在这里插入图片描述
输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
输出:[3,2,null,1]

递归

  • 从上往下遍历,在对某一节点进行修剪操作后,还需要重新再判断一次该节点(返回修剪后的子树);
class Solution {
public:
    TreeNode* pre = nullptr;
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if(root == nullptr) return nullptr;
        if(root->val < low){
            return trimBST(root->right, low, high);	// 返回修剪后的右子树
        }else 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;
    }
};
  • 从下往上遍历,修剪子树不影响上面节点的结构,同时不需要重新判断某一节点也能遍历完成整棵树。
class Solution {
public:
    TreeNode* pre = nullptr;
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if(root == nullptr) return nullptr;
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        if(root->val < low){		
            // root->left = nullptr;	// 删除左子树
            return root->right;		// 并用右子树替换该节点
        }else if(root->val > high){
            // root->right = nullptr;	// 删除右子树
            return root->left;		// 并用左子树替换该节点
        }
        return root;
    }
};

迭代

由于搜索树的有序性,因此不需要用栈来模拟递归过程,处理过程为:

  1. 先将根节点 root 移动到区间范围内,确定新的根节点;
  2. 处理当前根节点左子树中不符合条件的节点;
  3. 处理当前根节点右子树中不符合条件的节点。
class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if(root == nullptr) return nullptr;
        // 确定在区间内的根节点
        while(root != nullptr){         
            if(root->val < low){
                root = root->right;
            }else if(root->val > high){
                root = root->left;
            }else{
                break;
            }
        }
        // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
        TreeNode* curr = root;
        while(curr != nullptr){
            while(curr->left != nullptr && curr->left->val < low){
                curr->left = curr->left->right;
            }
            curr = curr->left;
        }
        // 此时root已经在[L, R] 范围内,处理右孩子大于R的情况
        curr = root;
        while(curr != nullptr){
            while(curr->right != nullptr && curr->right->val > high){
                curr->right = curr->right->left; 
            }
            curr = curr->right;
        }

        return root;
    }
};

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

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取,所以想构成不平衡的二叉树是自找麻烦。

分割点就是数组中间位置的节点,
如果数组长度为偶数,中间节点有两个,不同的规则对应不同的结构,是取左边元素 就是树1,取右边元素就是树2。
在这里插入图片描述

class Solution {
public:
    TreeNode* transform(vector<int>& nums, int start, int end){
        if(start > end) return nullptr;
        int mid = start + (end - start) / 2;        // 根节点,偶数时取左边元素,树1
        // int mid = start + (end - start + 1) / 2;  // 取右边元素,树2
        TreeNode* root = new TreeNode(nums[mid]);
        root->left = transform(nums, start, mid-1); // 左子树
        root->right = transform(nums, mid+1, end);  // 右子树
        return root;
    }
    
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        return transform(nums, 0, nums.size()-1);
    }
};

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

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

在这里插入图片描述

反中序遍历二叉树,即从大到小累加并更新节点值即可。

递归

class Solution {
public:
    int sum = 0;					// 累加值
    TreeNode* convertBST(TreeNode* root) {
        if(root == nullptr) return root;
        convertBST(root->right);	// 右
        sum += root->val;			// 中
        root->val = sum;
        convertBST(root->left);		// 左
        return root;
    }
};

迭代

利用指针来遍历二叉树。

class Solution {
public:
    TreeNode* convertBST(TreeNode* root) {
        if(root == nullptr) return root;
        int sum = 0;                
        TreeNode* curr = root;
        stack<TreeNode*> st;
        while(curr != nullptr || !st.empty()){
            if(curr){
                st.push(curr);
                curr = curr->right; // 右
            }else{
                curr = st.top();    // 中
                st.pop();
                sum += curr->val;   // 更新累加值
                curr->val = sum;
                curr = curr->left;  // 左
            }
        }
        return root;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值