c++可视化 横向打印二叉树(连线、规整)

这次其实是之前一片博客的改进版。这次的二叉树我们使用了经典的 string + vector + shared_ptr 组合,从而使二叉树代码更直观,同时也把内存管理交由 STL 负责。同样地,本文也遵守 Google 风格,以供大家参考。


效果图

先来看看效果:
在这里插入图片描述

二叉树讲解

树类和树节点类声明如下:

class TreeNode {
   public:
    TreeNode() : children_(2, nullptr) {}
    TreeNode(const T &x) : data_(x), children_(2, nullptr) {}

    int Show(vector<vector<string>> &pic, int dep) const;
    void Insert(const shared_ptr<TreeNode> &data_ptr);
    bool Erase(const T &x, shared_ptr<TreeNode> &pre_ptr);

   private: // utility
    inline shared_ptr<TreeNode> &left() { return children_.at(0); }
    inline shared_ptr<TreeNode> &right() { return children_.at(1); }
    inline const shared_ptr<TreeNode> &left() const { return children_.at(0); }
    inline const shared_ptr<TreeNode> &right() const { return children_.at(1); }

   private:
    T data_;
    vector<shared_ptr<TreeNode>> children_;
};

class Tree {
   public:
    void Show() const {
        vector<vector<string>> ret;
        root_->Show(ret, 0);
        for (auto p = ret.rbegin(); p != ret.rend(); ++p) {
            for (auto &q : *p) cout << q;
            puts("");
        }
    }
    void Insert(const T &x) {
        if (nullptr == root_)
            root_ = make_shared<TreeNode>(x);
        else
            root_->Insert(make_shared<TreeNode>(x));
    }
    bool Erase(const T &x) { return root_->Erase(x, root_); }

   private:
    shared_ptr<TreeNode> root_;
};

Tree 类负责管理树根指针、封装 TreeNode 类接口。主要功能在 TreeNode 类中递归实现。
我们这里只实现了插入和删除,其余查询功能交由大家自行实现。代码如下:

void TreeNode::Insert(const shared_ptr<TreeNode> &data_ptr) {
    auto &next_ptr = data_ptr->data_ < data_ ? left() : right();
    if (nullptr == next_ptr)
        next_ptr = data_ptr;
    else
        next_ptr->Insert(data_ptr);
}

bool TreeNode::Erase(const T &x, shared_ptr<TreeNode> &pre_ptr) {
    if (x != data_) {
        auto &next_ptr = x < data_ ? left() : right();
        if (nullptr == next_ptr) return false;
        return next_ptr->Erase(x, next_ptr);
    }
    // x == data_
    if (nullptr == left() && nullptr == right()) {
        pre_ptr = nullptr;
        return true;
    }
    if (nullptr == left() || nullptr == right()) {
        pre_ptr = children_.at(nullptr != right());
        return true;
    }
    // both not null
    if (nullptr == right()->left()) {
        right()->left() = this->left();
        pre_ptr = right();
        return true;
    }
    if (nullptr == left()->right()) {
        left()->right() = this->right();
        pre_ptr = left();
        return true;
    }
    // find the min node in right
    auto &min_node_pre = right();
    while (nullptr != min_node_pre->left()->left()) min_node_pre = min_node_pre->left();
    min_node_pre->left()->left() = this->left();
    pre_ptr = this->right();
    return true;
}

由于我们并没有加入平衡树高的机制,删除节点我们只是使用最简单的:将左子树插入到右子树的最左子节点处,然后右子树代替原节点。

打印二叉树讲解

重点来了。我们的思路是这样的:

  • 节点类中序遍历节点,后续处理图像。
  • 处理好相关数据,放入二维 vec 中。二维 vec 第一维度对应打印的一行,第二维对应打印的前缀+信息内容。
  • 树类倒序打印 vec

我们先上代码(但是你可以先不看)再讲解:

int TreeNode::Show(vector<vector<string>> &pic, int dep) const {
    int start = pic.size(), sl, sr, ret;
    int this_string_length = to_string(data_).size();
    string line;
    for (int i = 0; i < this_string_length; ++i) line += "─";
    // gain
    if (nullptr != left()) {
        sl = left()->Show(pic, dep + 1);
    } else {
        pic.push_back(vector<string>{"", "null"});
        sl = pic.size() - 1;
    }
    pic.push_back(vector<string>{"", to_string(data_)});
    ret = pic.size() - 1;
    if (nullptr != right()) {
        sr = right()->Show(pic, dep + 1);
    } else {
        pic.push_back(vector<string>{"", "null"});
        sr = pic.size() - 1;
    }
    // al
    for (int i = start; i <= sl - 1; ++i) pic[i][0] = string(this_string_length + 1, ' ') + pic[i][0];
    pic[sl][0] = "└" + line + pic[sl][0];
    for (int i = sl + 1; i <= sr - 1; ++i) pic[i][0] = "│" + string(this_string_length, ' ') + pic[i][0];
    pic[ret][0] = "";
    pic[sr][0] = "┌" + line + pic[sr][0];
    for (int i = sr + 1; i < pic.size(); ++i) pic[i][0] = string(this_string_length + 1, ' ') + pic[i][0];

    return ret;
}

我们再看这张图:
在这里插入图片描述
我们先递归处理左子树 98 的信息,然后放入节点 100 的信息,接着递归处理右子树 101 的信息,最后将左右子树和节点连线,并做一些其它更新(一会儿你将看到)。

为了连线正确,我们需要知道左右子树所在行,我们通过递归返回这个信息。那么当前节点所在行则是当前调用的返回值。这就是 sl, sr, ret 三个变量的意义。

然后我们中序遍历,将信息放入 pic。简单地说,递归左子树(不存在则放入 null);放入当前节点,顺便记录返回值 ret;递归右子树(不存在则放入 null)。

最后便是画线部分了,首先是:

    pic[sl][0] = "└" + line + pic[sl][0];
    for (int i = sl + 1; i <= sr - 1; ++i) pic[i][0] = "│" + string(this_string_length, ' ') + pic[i][0];
    pic[ret][0] = "";
    pic[sr][0] = "┌" + line + pic[sr][0];

这是说:当前节点的列画竖线,左右子树所在行画横线,横线长应该是根据当前节点信息长度自适应的。

画完线之后,我们应该考虑一个问题,这个子树[最左侧,左子树根]和[右子树根,子树最右侧]之间的行应该补上相同数量的空格。这便是如下代码的由来:

    for (int i = start; i <= sl - 1; ++i) pic[i][0] = string(this_string_length + 1, ' ') + pic[i][0];
    pic[sl][0] = "└" + line + pic[sl][0];
    for (int i = sl + 1; i <= sr - 1; ++i) pic[i][0] = "│" + string(this_string_length, ' ') + pic[i][0];
    pic[ret][0] = "";
    pic[sr][0] = "┌" + line + pic[sr][0];
    for (int i = sr + 1; i < pic.size(); ++i) pic[i][0] = string(this_string_length + 1, ' ') + pic[i][0];

注意,start 是在函数开始时进行记录的

完整代码

#include <iostream>
#include <memory>
#include <string>
#include <vector>
using namespace std;

using T = int;

class TreeNode {
   public:
    TreeNode() : children_(2, nullptr) {}
    TreeNode(const T &x) : data_(x), children_(2, nullptr) {}

    int Show(vector<vector<string>> &pic, int dep) const;
    void Insert(const shared_ptr<TreeNode> &data_ptr);
    bool Erase(const T &x, shared_ptr<TreeNode> &pre_ptr);

   private:
    inline shared_ptr<TreeNode> &left() { return children_.at(0); }
    inline shared_ptr<TreeNode> &right() { return children_.at(1); }
    inline const shared_ptr<TreeNode> &left() const { return children_.at(0); }
    inline const shared_ptr<TreeNode> &right() const { return children_.at(1); }

   public:
    T data_;
    vector<shared_ptr<TreeNode>> children_;
};

class Tree {
   public:
    void Show() const {
        vector<vector<string>> ret;
        root_->Show(ret, 0);
        for (auto p = ret.rbegin(); p != ret.rend(); ++p) {
            for (auto &q : *p) cout << q;
            puts("");
        }
    }
    void Insert(const T &x) {
        if (nullptr == root_)
            root_ = make_shared<TreeNode>(x);
        else
            root_->Insert(make_shared<TreeNode>(x));
    }
    bool Erase(const T &x) { return root_->Erase(x, root_); }

   private:
    shared_ptr<TreeNode> root_;
};

int main() {
    Tree root;
    root.Insert(52);
    root.Insert(2);
    root.Insert(102);
    root.Insert(1);
    root.Insert(100);
    root.Insert(98);
    root.Insert(90);
    root.Insert(101);
    root.Insert(1024);
    root.Show();
}

void TreeNode::Insert(const shared_ptr<TreeNode> &data_ptr) {
    auto &next_ptr = data_ptr->data_ < data_ ? left() : right();
    if (nullptr == next_ptr)
        next_ptr = data_ptr;
    else
        next_ptr->Insert(data_ptr);
}

int TreeNode::Show(vector<vector<string>> &pic, int dep) const {
    int start = pic.size(), sl, sr, ret;
    int this_string_length = to_string(data_).size();
    string line;
    for (int i = 0; i < this_string_length; ++i) line += "─";
    // gain
    if (nullptr != left()) {
        sl = left()->Show(pic, dep + 1);
    } else {
        pic.push_back(vector<string>{"", "null"});
        sl = pic.size() - 1;
    }
    pic.push_back(vector<string>{"", to_string(data_)});
    ret = pic.size() - 1;
    if (nullptr != right()) {
        sr = right()->Show(pic, dep + 1);
    } else {
        pic.push_back(vector<string>{"", "null"});
        sr = pic.size() - 1;
    }
    // al
    for (int i = start; i <= sl - 1; ++i) pic[i][0] = string(this_string_length + 1, ' ') + pic[i][0];
    pic[sl][0] = "└" + line + pic[sl][0];
    for (int i = sl + 1; i <= sr - 1; ++i) pic[i][0] = "│" + string(this_string_length, ' ') + pic[i][0];
    pic[ret][0] = "";
    pic[sr][0] = "┌" + line + pic[sr][0];
    for (int i = sr + 1; i < pic.size(); ++i) pic[i][0] = string(this_string_length + 1, ' ') + pic[i][0];

    return ret;
}

bool TreeNode::Erase(const T &x, shared_ptr<TreeNode> &pre_ptr) {
    if (x != data_) {
        auto &next_ptr = x < data_ ? left() : right();
        if (nullptr == next_ptr) return false;
        return next_ptr->Erase(x, next_ptr);
    }
    // x == data_
    if (nullptr == left() && nullptr == right()) {
        pre_ptr = nullptr;
        return true;
    }
    if (nullptr == left() || nullptr == right()) {
        pre_ptr = children_.at(nullptr != right());
        return true;
    }
    // both not null
    if (nullptr == right()->left()) {
        right()->left() = this->left();
        pre_ptr = right();
        return true;
    }
    if (nullptr == left()->right()) {
        left()->right() = this->right();
        pre_ptr = left();
        return true;
    }
    // find the min node in right
    auto &min_node_pre = right();
    while (nullptr != min_node_pre->left()->left()) min_node_pre = min_node_pre->left();
    min_node_pre->left()->left() = this->left();
    pre_ptr = this->right();
    return true;
}
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

岚花落_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值