【623. 在二叉树中增加一行】

该博客介绍了如何在给定深度上向二叉树中添加新的一行节点,提供了深度优先搜索和广度优先搜索两种解决方案,详细分析了两种方法的时间和空间复杂度。

来源:力扣(LeetCode)

描述:

给定一个二叉树的根 root 和两个整数 valdepth ,在给定的深度 depth 处添加一个值为 val 的节点行。

注意,根节点 root 位于深度 1

加法规则如下:

  • 给定整数 depth,对于深度为 depth - 1 的每个非空树节点 cur ,创建两个值为 val 的树节点作为 cur 的左子树根和右子树根。
  • cur 原来的左子树应该是新的左子树根的左子树。
  • cur 原来的右子树应该是新的右子树根的右子树。
  • 如果 depth == 1 意味着 depth - 1 根本没有深度,那么创建一个树节点,值 val 作为整个原始树的新根,而原始树就是新根的左子树。

示例 1:
1

输入: root = [4,2,6,3,1,5], val = 1, depth = 2
输出: [4,1,1,2,null,null,6,3,1,5]

示例 2:

2

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

提示:

  • 节点数在 [1, 104] 范围内
  • 树的深度在 [1, 104]范围内
  • -100 <= Node.val <= 100
  • -105 <= val <= 105
  • 1 <= depth <= the depth of tree + 1

方法一:深度优先搜索
思路

当输入 depth 为 1 时,需要创建一个新的 root,并将原 root 作为新 root 的左子节点。当 depth 为 2 时,需要在 root 下新增两个节点 left 和 right 作为 root 的新子节点,并把原左子节点作为 left 的左子节点,把原右子节点作为right 的右子节点。当 depth 大于 2 时,需要继续递归往下层搜索,并将 depth 减去 1,直到搜索到 depth 为 2。

代码:

class Solution {
public:
    TreeNode* addOneRow(TreeNode* root, int val, int depth) {
        if (root == nullptr) {
            return nullptr;
        }
        if (depth == 1) {
            return new TreeNode(val, root, nullptr);
        }
        if (depth == 2) {
            root->left = new TreeNode(val, root->left, nullptr);
            root->right = new TreeNode(val, nullptr, root->right);
        } else {
            root->left = addOneRow(root->left, val, depth - 1);
            root->right = addOneRow(root->right, val, depth - 1);
        }
        return root;
    }
};

执行用时:12 ms, 在所有 C++ 提交中击败了93.33%的用户
内存消耗:24.2 MB, 在所有 C++ 提交中击败了75.63%的用户
复杂度分析
时间复杂度:O(n),其中 n 为输入的树的节点数。最坏情况下,需要遍历整棵树。
空间复杂度:O(n),递归的深度最多为 O(n)。

方法二:广度优先搜索

思路

与深度优先搜索类似,我们用广度优先搜索找到要加的一行的上一行,然后对这一行的每个节点 node,都新增两个节点 left 和 right 作为 node 的新子节点,并把原左子节点作为 left 的左子节点,把原右子节点作为 right 的右子节点。

代码:

class Solution {
public:
    TreeNode* addOneRow(TreeNode* root, int val, int depth) {
        if (depth == 1) {
            return new TreeNode(val, root, nullptr);
        }
        vector<TreeNode *> curLevel(1, root);
        for (int i = 1; i < depth - 1; i++) {
            vector<TreeNode *> tmpt;
            for (auto &node : curLevel) {
                if (node->left != nullptr) {
                    tmpt.emplace_back(node->left);
                }
                if (node->right != nullptr) {
                    tmpt.emplace_back(node->right);
                }
            }
            curLevel = move(tmpt);
        }
        for (auto &node : curLevel) {
            node->left = new TreeNode(val, node->left, nullptr);
            node->right = new TreeNode(val, nullptr, node->right);
        }
        return root;
    }
};

执行用时:20 ms, 在所有 C++ 提交中击败了47.59%的用户
内存消耗:24.6 MB, 在所有 C++ 提交中击败了5.06%的用户
复杂度分析
时间复杂度:O(n),其中 n 为输入的树的节点数。最坏情况下,需要遍历整棵树。
空间复杂度:O(n),数组空间开销最多为 O(n)。
author:LeetCode-Solution

<think>我们被要求输出二叉树的“全貌”,这通常意味着以树形结构可视化二叉树,包括其层次和分支关系。 常见的二叉树输出方法有:垂直输出(类似树状)或水平输出(根在左侧,向右展开子树)。这里我们通常使用深度优先搜索(DFS)或广度优先搜索(BFS)来遍历树,并在输出时添加适当的缩进和连接符。 一种常见的做法是使用“逆中序遍历”(即右子树->根->左子树)并记录每个节点的深度,然后为每一行添加缩进和连接线。但更直观的是按层级输出(BFS),但是这样需要处理分支的连线。 这里我们采用一种递归方法,从根节点开始,先递归右子树,然后输出当前节点,再递归左子树(这样在控制台上,根在顶部,左子树在下方右侧)。同时,我们为每个节点标记其在树中的位置(通过传递一个缩进字符串和连接字符串)。 以下是一个示例代码,用于垂直打印二叉树(根在上方,向右延伸的是左子树,向左延伸的是右子树?但通常我们让根在顶部,然后向下展开): 但实际上,我们更常见的是水平输出,即根节点在左侧,然后向右下方是右子树,向右下方是左子树(或者反过来)。这里我们实现一种根在左侧,子树向右展开的方式。 另一种更直观的方法是使用层序遍历(BFS)并记录每个节点的位置,然后按行输出。但是这种方法需要计算每个节点的位置偏移量,较为复杂。 这里我们采用一种递归打印的方法,类似于树状图,每递归一层增加缩进,并画出分支线。 示例代码(注意:为了简单,我们使用逆中序,先右后根再左,这样在输出时右子树在上方,根在中间,左子树在下方,形成垂直旋转90°的树): 定义二叉树节点: struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; 打印函数: void printTree(TreeNode* root, std::string prefix = "", bool isLeft = false) { if (root == nullptr) { return; } // 先打印右子树(因为我们要从上到下打印,所以先处理右子树,这样当前节点在中间,左子树在下面) // 这里我们调整:使用水平输出,根在左侧,右子树在上面,左子树在下面。或者也可以调整顺序。 // 实际上,我们可以选择先打印右子树,然后打印当前节点,再打印左子树,这样在屏幕上右子树在上方,左子树在下方(垂直旋转90°的树)。 // 另一种常见的水平打印(根在左侧)是先打印右子树(带缩进),然后打印当前节点,再打印左子树(带缩进)。这样在屏幕上,树是从右侧开始(右子树)然后根,然后左子树(在根的下方)。 // 这里我们使用如下方式: // 打印右子树(右子树在上方) printTree(root->right, prefix + (isLeft ? "│ " : " "), false); // 打印当前节点 std::cout << prefix; std::cout << (isLeft ? "└── " : "┌── "); std::cout << root->val << std::endl; // 打印左子树(左子树在下方) printTree(root->left, prefix + (isLeft ? " " : "│ "), true); } 但是注意:上面的函数中,我们传入的isLeft表示当前节点是否是父节点的左子节点,以便绘制分支线。 然而,这个函数在根节点调用时,isLeft默认为false(因为根没有父节点),所以根节点的前缀为空,且使用"┌── "?这样不太好看。我们可以修改根节点的显示。 我们可以这样调用: void printTree(TreeNode* root, std::string prefix = "", bool isLeft = true) { if (root == nullptr) { return; } // 首先,打印右子树,传入的isLeft为false,因为右子树不是左子节点(而是右子节点) printTree(root->right, prefix + (isLeft ? "│ " : " "), false); // 打印当前节点 std::cout << prefix; std::cout << (isLeft ? "└── " : "┌── "); std::cout << root->val << std::endl; // 打印左子树,传入的isLeft为true printTree(root->left, prefix + (isLeft ? " " : "│ "), true); } 然后在调用时,对根节点使用: printTree(root, "", true); 这样根节点会使用"└── ",然后它的左子树分支用空格,右子树分支用"│ "?但是这样根节点显示为左分支(因为它被当作左子节点传入,但实际上根节点既不是左也不是右)。所以我们需要调整。 另一种做法是去掉根节点的分支符号,直接输出根节点。我们可以调整函数,在根节点调用时不使用分支符号,但这样需要区分根节点和非根节点。 我们可以将根节点单独处理: void printTree(TreeNode* root) { if (root == nullptr) return; // 先调用内部的打印函数,但前缀为空,并且将根节点当作左节点(这样根节点会显示为└──,但我们不希望这样,所以调整) // 或者我们可以这样:根节点不显示分支线 printTree(root, "", true); // 这里传入true,让根节点显示为左分支(└──),然后左子树在下面,右子树在上面 } 这样输出时,根节点会显示为:空字符串加上"└── ",然后数值。这样根节点前面有一个└──,不太美观。我们可以修改函数,对根节点特殊处理。 改进:我们可以修改函数,当prefix为空时(即根节点),不显示分支线,只显示数值。 另一种常见的做法是使用垂直输出(根在上方),我们这里采用另一种方法:先右子树,然后当前节点(带缩进和分支),再左子树。同时我们用一个变量记录深度,然后用空格缩进来表示深度?但是我们上面的方法已经通过递归和前缀实现了。 这里我们采用一个改进版本,在打印根节点时,不显示分支符号,只显示数值。然后其子树才显示分支。 但是,我们的函数是递归的,无法知道当前节点是否是根节点。所以我们在第一次调用时,可以传入一个标志,但这样修改函数参数不太优雅。 下面是一个更通用的版本(来自Leetcode等地方的常见打印方法): 我们稍作修改,让根节点显示时没有分支线,但子树有分支线。我们可以这样: 在内部递归函数中,我们使用一个前缀字符串和一个分支前缀(用于子树)。然后,根节点调用时,传入空的前缀和空的分支前缀。 具体实现如下: void printBT(const std::string& prefix, const TreeNode* node, bool isLeft) { if (node != nullptr) { std::cout << prefix; std::cout << (isLeft ? "├──" : "└──"); // 打印当前节点值 std::cout << node->val << std::endl; // 递归子树 printBT(prefix + (isLeft ? "│ " : " "), node->right, true); // 右子树,标记为左分支(为了显示?)这里我们调整:左子树在下面,右子树在上面,所以先递归右子树 printBT(prefix + (isLeft ? "│ " : " "), node->left, false); // 左子树,标记为右分支(显示为└──) // 注意:上面的递归顺序是先右后左,这样在输出时,右子树在上面,左子树在下面。 } } 然后我们这样调用: void printTree(const TreeNode* root) { if (root == nullptr) return; // 对于根节点,我们传入空的前缀,并且没有分支线,但是我们还是需要标记它是左还是右?这里我们传入isLeft=false,这样根节点显示为└──,但这样不好。 // 我们可以改为:根节点单独打印 std::cout << root->val << std::endl; // 然后分别打印左右子树,并传入不同的分支标记 printBT("", root->right, true); // 右子树,传入true表示是左分支(将显示为├──) printBT("", root->left, false); // 左子树,传入false表示是右分支(将显示为└──) } 但是这样根节点单独打印,然后子树分别调用。然而,这样无法处理多级子树的分支线。 更常用的方法是:在递归函数中,我们传入前缀和分支标记,并且先打印右子树(这样右子树在上方),再打印左子树(在下方)。 下面是一个经过调整的版本,在调用根节点时,我们传入空的前缀,并且根节点不显示分支标记(即根节点没有前缀),但是子树有。 我们可以这样设计递归函数: void printBST(const TreeNode* node, const std::string& prefix = "", bool isLeft = false) { if (node == nullptr) { return; } // 首先,打印当前节点:包括前缀和分支标记 std::cout << prefix; // 根据isLeft判断当前节点是父节点的左子节点还是右子节点,从而绘制不同的标记 std::cout << (isLeft ? "├──" : "└──" ); // 打印节点值 std::cout << node->val << std::endl; // 递归打印子树:注意我们先打印右子树,然后是左子树,这样右子树显示在上方,左子树在下方 // 对于右子树,我们传入新的前缀:当前前缀加上(如果是左分支则"│ ",否则" ")?这里需要根据当前节点的分支情况 // 同时,右子树传入的isLeft=true,因为右子树在父节点的右侧,但是我们在打印右子树时,它会是下一个节点的父节点,我们希望在右子树下打印其子树时,它的左子树(如果有)会有一条竖线连接到父节点?所以这里传入true表示需要连接线。 printBST(node->right, prefix + (isLeft ? "│ " : " "), true); printBST(node->left, prefix + (isLeft ? "│ " : " "), false); } 调用时: printBST(root); // 根节点传入,默认prefix为空,isLeft=false,因此根节点只显示"└── 值",然后换行 但是这样根节点显示为└──,而不是没有符号。我们可以调整:在根节点调用时,我们传入一个空的前缀,并且不使用分支符号。修改方法:在打印根节点时,因为prefix为空,所以只打印"└── ",我们希望根节点没有这个符号。 因此,我们修改:增加一个判断,如果prefix为空,则只打印节点值,不打印分支符号。否则打印前缀和分支符号。 修改后: void printBST(const TreeNode* node, const std::string& prefix = "", bool isLeft = false) { if (node == nullptr) { return; } // 如果前缀不为空,说明不是根节点(或者至少不是第一次调用的根节点),则打印前缀和分支标记 if (prefix != "") { std::cout << prefix; std::cout << (isLeft ? "├──" : "└──" ); std::cout << node->val << std::endl; } else { // 根节点,只打印节点值 std::cout << node->val << std::endl; } // 构建新的前缀:对于子树,我们需要在prefix基础上添加空格或竖线 std::string newPrefix = prefix + (prefix != "" ? (isLeft ? "│ " : " ") : ""); // 注意:对于根节点,newPrefix就是空字符串(因为prefix为空,所以添加空)。这样子树的第一级就没有竖线连接。但我们希望子树有连接线。 // 递归子树:先右后左 printBST(node->right, newPrefix, true); printBST(node->left, newPrefix, false); } 但是这样,根节点的子树在打印时,newPrefix为空(因为根节点的prefix为空),所以子节点的前缀为空,然后又会进入根节点的分支(即只打印节点值),这显然不对。 为了修正,我们改变策略:在递归调用子树时,不管prefix是否为空,都要添加相应的空格或竖线。但是,对于根节点,我们不想在子树前加上任何前缀(即子树的前缀就是空格),所以这样: 我们去掉根节点的特殊处理,直接使用分支符号,但将根节点的分支符号改为空。我们可以这样:在调用根节点时,我们传入一个标记,表示这是根节点。 由于函数原型已经固定,我们可以在外部包装: void printTree(const TreeNode* root) { if (root == nullptr) return; // 打印根节点 std::cout << root->val << std::endl; // 打印子树,传入前缀,其中右子树传入true(表示后续需要绘制竖线),左子树传入false printBST(root->right, "", true); printBST(root->left, "", false); } 同时,递归函数printBST是: void printBST(const TreeNode* node, const std::string& prefix, bool isLeft) { if (node == nullptr) return; // 打印当前节点 std::cout << prefix; // 根据isLeft选择分支标记 std::cout << (isLeft ? "├──" : "└──"); std::cout << node->val << std::endl; // 递归子树 std::string newPrefix = prefix + (isLeft ? "│ " : " "); printBST(node->right, newPrefix, true); printBST(node->left, newPrefix, false); } 这样,根节点单独打印,然后子树调用printBST,传入的前缀为空。这样输出: 根节点值 ├──右子树根节点值 └──左子树根节点值 然后递归下去。 但是这样,对于右子树,它下面还有子树时,我们希望它继续添加前缀。例如: 1 / \ 2 3 / \ 4 5 我们希望的输出: 1 ├──3 └──2 ├──5 └──4 但实际上,按照上面的递归函数,输出可能是: 1 ├──3 └──2 ├──5 └──4 这符合预期。 但是,如果节点3有右子树,那么输出会是: 1 ├──3 │ ├── (3的右子树) │ └── (3的左子树) └──2 ├──5 └──4 所以这个方法可行。 然而,我们注意:在递归printBST时,是先递归右子树,再递归左子树。但是我们希望右子树在上面,所以先打印右子树(在函数中,我们先打印当前节点,然后递归是先右后左,但是打印顺序是先打印右子树的整个子树,再打印左子树的整个子树,所以右子树及其所有子节点会先于左子树打印)。 所以,我们调整递归顺序:在printBST函数中,递归部分,我们希望先打印右子树,再打印左子树,这样右子树在上,左子树在下。但是我们上面的递归调用是先右后左,并且打印也是按照这个顺序,所以没问题。 但是,有一个问题:在递归调用printBST(node->right)时,它会在下一行打印右子树,然后再打印左子树。所以整体输出顺序是: 当前节点 右子树的所有节点(按递归顺序) 左子树的所有节点 这样,右子树在左子树上方,符合要求。 因此,我们采用以下代码: 定义二叉树节点: #include <iostream> #include <string> struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; void printBST(const TreeNode* node, const std::string& prefix, bool isLeft) { if (node == nullptr) return; // 打印当前节点 std::cout << prefix; std::cout << (isLeft ? "├──" : "└──"); std::cout << node->val << std::endl; // 递归子树,先右后左 if (node->right || node->left) { // 其实可以不用判断,直接递归,内部会判断空 printBST(node->right, prefix + (isLeft ? "│ " : " "), true); printBST(node->left, prefix + (isLeft ? "│ " : " "), false); } } void printTree(const TreeNode* root) { if (root == nullptr) return; std::cout << root->val << std::endl; printBST(root->right, "", true); printBST(root->left, "", false); } 测试: 构建树:根为1,左为2,右为3,2的左为4,2的右为5。 输出: 1 ├──3 └──2 ├──5 └──4 但是,如果我们希望3显示在2的上面,并且3和2是兄弟节点,那么输出顺序正确。 注意:我们也可以调整分支符号,使其更适合观察。 另一种常见的打印方式是将树旋转90度(逆时针旋转90度),这样根在左侧,右子树在上方,左子树在下方。我们上面的打印就是这种方式。 但是,有时候我们希望根在顶部,子树在下方展开。这通常需要计算每层的节点位置,然后按行打印。这种方法适合层序遍历,但实现起来较复杂。 总结:我们提供了上述方法,可以在控制台垂直打印二叉树(旋转90度后的树)。 如果用户希望按层打印(根在顶部),那么我们可以使用层序遍历,并记录每个节点的位置(需要知道树的深度和每层的宽度),然后格式化输出。但这种方法更复杂,这里不展开。 我们选择实现上述旋转90度的打印方式。 注意:如果二叉树很大,控制台可能无法显示完整,但作为调试输出,这种方法比较直观。 最后,我们给出完整示例代码,包括测试。 </think>在C++中输出二叉树的完整结构(全貌),可以采用递归遍历并添加缩进和分支符号的方法。以下是实现代码和说明: ### 实现代码 ```cpp #include <iostream> #include <string> struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; // 辅助递归函数 void printBT(const std::string& prefix, const TreeNode* node, bool isRight) { if (node != nullptr) { // 打印当前节点 std::cout << prefix; std::cout << (isRight ? "├── " : "└── "); std::cout << node->val << std::endl; // 递归子树:先右后左(确保右子树在上方) std::string newPrefix = prefix + (isRight ? "│ " : " "); printBT(newPrefix, node->right, true); // 右子树标记为右分支 printBT(newPrefix, node->left, false); // 左子树标记为左分支 } } // 主打印函数 void printTree(const TreeNode* root) { if (root == nullptr) return; std::cout << "Root: " << root->val << std::endl; printBT("", root->right, true); // 先打印右子树 printBT("", root->left, false); // 再打印左子树 } // 测试示例 int main() { /* 构建树结构: 1 / \ 2 3 / \ 4 5 */ TreeNode* root = new TreeNode(1); root->left = new TreeNode(2); root->right = new TreeNode(3); root->left->left = new TreeNode(4); root->left->right = new TreeNode(5); printTree(root); // 输出树形结构 // 释放内存(实际使用需根据场景调整) delete root->left->left; delete root->left->right; delete root->left; delete root->right; delete root; return 0; } ``` ### 输出效果 ``` Root: 1 ├── 3 └── 2 ├── 5 └── 4 ``` ### 关键说明 1. **符号含义**: - `├──` :右分支(子树继续向右延伸) - `└──` :左分支(子树末端) - `│ ` :垂直连接线(保持层级关系) 2. **递归逻辑**: - 采用**先右后左**的递归顺序,确保右子树显示在上方 - 通过`prefix`字符串动态生成缩进,表示节点层级 - `isRight`参数控制分支符号的显示方式 3. **特点**: - **垂直布局**:树结构逆时针旋转90°(根在顶部,左子树向右下延伸) - **直观展示**:清晰显示父子关系、分支方向和层级深度 - **支持任意深度**:递归自动处理多级嵌套 ### 扩展说明 - **空树处理**:若根节点为`nullptr`,函数直接返回无输出 - **自定义节点**:可修改`TreeNode`结构体添加更多数据字段 - **复杂树优化**:对于超深/超宽树,可结合广度优先搜索(BFS)分层打印[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

千北@

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

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

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

打赏作者

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

抵扣说明:

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

余额充值