• 二叉树图形化显示


    在刷 OJ 二叉树题目的时候,文字描述的输入都是 [1, null, 2] 这种形式,但输入参数却是 TreeNode *root,很不直观,一旦节点数目很多,很难想象输入的二叉树是什么样子的。leetcode 上提供了一个很好的二叉树图形显示,现在自己动手实现一遍,也方便在其他地方使用。

    第零步:前言

    用 C++ 实现。假定输入格式是 [6,2,8,0,4,7,9,null,null,3,5] 这种形式的字符串(因为 C++ 没有像 Python 那样的 list)。

    上面的二叉树图形如下:

          6
       /     
      2       8
     /      / 
    0   4   7   9
       / 
      3   5
    

    二叉树节点定义如下:

    struct TreeNode
    {
        int val;
        int x, y;
        TreeNode *left, *right;
        TreeNode(int v) : val(v), left(nullptr), right(nullptr), x(-1), y(-1) {}
    };
    

    其中,(x, y) 表示该节点在屏幕上的坐标。

    第一步:建树

    给定字符串 string s = "[6,2,8,0,4,7,9,null,null,3,5]",首先我们将其转换为一个 vector<string> v ,其中 v 的元素为 "6", "2", ..., "null", ..., "5"

    预处理字符串的操作如下:

    // discard the '[]'
    s = s.substr(1, s.size() - 2);
    // change s into ["1", ...,"2"]
    auto v = split(s, ",");
    

    split 函数如下:

    static vector<string> split(string &s, const string &token)
    {
        replaceAll(s, token, " ");
        vector<string> result;
        stringstream ss(s);
        string buf;
        while (ss >> buf)
            result.push_back(buf);
        return result;
    }
    

    replaceAll 函数的作用是把所有的 oldChars 替换为 newChars

    static void replaceAll(string &s, const string &oldChars, const string &newChars)
    {
        int pos = s.find(oldChars);
        while (pos != string::npos)
        {
            s.replace(pos, oldChars.length(), newChars);
            pos = s.find(oldChars);
        }
    }
    

    那么,这个 v 就是二叉树的数组形式(根节点是 v[0] ),其具有以下性质:

    v[i].leftv[2 * i + 1]v[i].rightv[2 * i + 2]

    建树是常见的递归操作:

    // create binary tree
    TreeNode *root = nullptr;
    innerCreate(v, 0, root);
    

    innerCreate的具体实现如下:

    static void innerCreate(vector<string> &v, size_t idx, TreeNode *&p)
    {
        if (idx >= v.size() || v[idx] == "null")
            return;
        p = new TreeNode(stoi(v[idx]));
        innerCreate(v, 2 * idx + 1, p->left);
        innerCreate(v, 2 * idx + 2, p->right);
    }
    

    第二步:定义坐标

    定义坐标系:Console 中左上角为原点,向右为 X 轴正方向,向下为 Y 轴正方向。

    很自然地,我们就把节点所在的层数作为节点的 Y 轴坐标。

    二叉树还有一个有趣的性质:中序遍历是二叉树的「从左往右」的遍历。所以我们把节点中序遍历所在的位置作为节点的 X 轴坐标。

    层次遍历初始化所有节点的 Y 坐标:

    static void initY(TreeNode *root)
    {
        if (root == nullptr)
            return;
    
        typedef pair<TreeNode *, int> Node;
    
        root->y = 1;
    
        queue<Node> q;
        q.push(Node(root, root->y));
        while (!q.empty())
        {
            auto p = q.front();
            q.pop();
            if (p.first->left != nullptr)
            {
                p.first->left->y = p.second + 1;
                q.push(Node(p.first->left, p.second + 1));
            }
            if (p.first->right != nullptr)
            {
                p.first->right->y = p.second + 1;
                q.push(Node(p.first->right, p.second + 1));
            }
        }
    }
    

    中序遍历初始化所有节点的 X 坐标:

    static void initX(TreeNode *p, int &x)
    {
        if (p == nullptr)
            return;
        initX(p->left, x);
        p->x = x++;
        initX(p->right, x);
    }
    

    完整的建树操作(包括初始化坐标操作):

    static TreeNode *create(string &s)
    {
        // discard the '[]'
        s = s.substr(1, s.size() - 2);
        // change s into ["1", ...,"2"]
        auto v = split(s, ',');
        // create binary tree
        TreeNode *root = nullptr;
        innerCreate(v, 0, root);
        // init x and y of tree nodes
        initCoordinate(root);
        return root;
    }
    static void initCoordinate(TreeNode *root)
    {
        int x = 0;
        initX(root, x);
        initY(root);
    }
    

    第三步:定义画布

    所谓的画布 Canvas 其实就是一个二维数组 char buffer[HEIGHT][WIDTH] ,我们把要输出的字符都放到 buffer 相应的位置,最后输出 buffer

    Canvas 类代码如下:

    class Canvas
    {
    public:
        static const int HEIGHT = 10;
        static const int WIDTH = 80;
        static char buffer[HEIGHT][WIDTH + 1];
    
        // print buffer
        static void draw()
        {
            cout << endl;
            for (int i = 0; i < HEIGHT; i++)
            {
                buffer[i][WIDTH] = '';
                cout << buffer[i] << endl;
            }
            cout << endl;
        }
    
        // put 's' at buffer[r][c]
        static void put(int r, int c, const string &s)
        {
            int len = s.length();
            int idx = 0;
            for (int i = c; (i < WIDTH) && (idx < len); i++)
                buffer[r][i] = s[idx++];
        }
        // put n 'ch' at buffer[r][c]
        static void put(int r, int c, char ch, int num)
        {
            while (num > 0 && c < WIDTH)
                buffer[r][c++] = ch, num--;
        }
    
        // clear the buffer
        static void resetBuffer()
        {
            for (int i = 0; i < HEIGHT; i++)
                memset(buffer[i], ' ', WIDTH);
        }
    };
    // Do not remove this line
    char Canvas::buffer[Canvas::HEIGHT][Canvas::WIDTH + 1];
    

    调用方法如下:

    Canvas::resetBuffer();
    // call Cancas::put() to put something into buffer
    Canvas::put(3, 3, "hello world");
    Cancas::draw();
    

    第五步:绘制二叉树

    绘制样式我想到 2 种。

    经典型。看着好看,一旦考虑到每个节点 val 的长度不一致,节点多的时候,画出来的效果很不好。

    Tree-1
        1
       / 
      2   4
       
        3
    
    Tree-2
                   1
                  / 
      111111111111   22222222222222222 
     /            
    4              5
    

    对于前面定义的 X 坐标,中序遍历的序列当中,X 坐标都是连续的,即从 0 到 n 变化。这样画出来显然不行,因为 node.val 占据了一定的长度,符号 / 也要占据一个宽度,所以采取的办法是 将横坐标统一乘以定值 widthZoom 。根据输入的实际情况,自己调整 widthZoom 的大小(数值都是个位数,widthZoom 取 1 即可)。

    对于 Y 坐标也是连续的,但显然符号 / 要占据一行,所以节点 node 在画布中的 Y 轴位置应该为 2 * node.y

    static void show2(TreeNode *root)
    {
        const int widthZoom = 1;
        Canvas::resetBuffer();
        queue<TreeNode *> q;
        q.push(root);
        int x, y, val;
        while (!q.empty())
        {
            auto p = q.front();
            q.pop();
            x = p->x, y = p->y, val = p->val;
            Canvas::put(2 * y, widthZoom * x, to_string(val));
            if (p->left != nullptr)
            {
                q.push(p->left);
                Canvas::put(2 * y + 1, widthZoom * ((p->left->x + x) / 2), '/', 1);
            }
            if (p->right != nullptr)
            {
                q.push(p->right);
                Canvas::put(2 * y + 1, widthZoom * ((x + p->right->x) / 2) + 1, '\', 1);
            }
        }
        Canvas::draw();
    }
    

    不知道叫什么型。节点数少,效果固然不如第一种。但是节点数一多,效果比第一种稍好(但是还是不太满意),应付一般的场景够用。

         1
      ___|____________
      2     4444444444
      |______
        33333
    

    X 和 Y 坐标的处理同上。此处 widthZoom 的值最好取大于等于 3 。代码如下:

    static void show(TreeNode *root)
    {
        const int widthZoom = 3;
        Canvas::resetBuffer();
        queue<TreeNode *> q;
        q.push(root);
        int x, y, val;
        string sval;
        while (!q.empty())
        {
            auto p = q.front();
            q.pop();
            bool l = (p->left != nullptr);
            bool r = (p->right != nullptr);
            x = p->x, y = p->y, val = p->val, sval = to_string(p->val);
            Canvas::put(2 * y, widthZoom * x, sval);
            if (l)
            {
                q.push(p->left);
                Canvas::put(2 * y + 1, widthZoom * p->left->x, '_', widthZoom * (x - p->left->x) + sval.length() / 2);
            }
            if (r)
            {
                q.push(p->right);
                Canvas::put(2 * y + 1, widthZoom * x, '_',
                            widthZoom * (p->right->x - x) + to_string(p->right->val).length());
            }
            if (l || r)
                Canvas::put(2 * y + 1, widthZoom * x + sval.length() / 2, "|");
        }
        Canvas::draw();
    }
    

    最终步:效果

    • s = [6,2,8,0,4,7,9,null,null,3,5]

      width zoom: 3
                     6
         ____________|______
         2                 8
      ___|______        ___|___
      0        4        7     9
            ___|___
            3     5
      width zoom: 1
           6
         /   
       2     8
      /    / 
      0  4  7 9
        / 
        3 5
      
    • s = [512,46, 7453,35, 6,26,null,-1,null,9,null]

      width zoom: 3
                     512
            __________|________
            46             7453
         ____|_____     _____|
         35       6     26
      ____|    ___|
      -1       9
      width zoom: 2
              512
            /      
          46        7453
        /        /
        35    6   26
      /     /
      -1    9
      

    完整代码

    #include <queue>
    #include <vector>
    #include <string>
    #include <cstring>
    #include <sstream>
    #include <iostream>
    using namespace std;
    struct TreeNode
    {
        int val;
        int x, y;
        TreeNode *left, *right;
        TreeNode(int v) : val(v), left(nullptr), right(nullptr), x(-1), y(-1) {}
    };
    
    class Canvas
    {
    public:
        static const int HEIGHT = 10;
        static const int WIDTH = 80;
        static char buffer[HEIGHT][WIDTH + 1];
    
        static void draw()
        {
            cout << endl;
            for (int i = 0; i < HEIGHT; i++)
            {
                buffer[i][WIDTH] = '';
                cout << buffer[i] << endl;
            }
            cout << endl;
        }
    
        static void put(int r, int c, const string &s)
        {
            int len = s.length();
            int idx = 0;
            for (int i = c; (i < WIDTH) && (idx < len); i++)
                buffer[r][i] = s[idx++];
        }
        static void put(int r, int c, char ch, int num)
        {
            while (num > 0 && c < WIDTH)
                buffer[r][c++] = ch, num--;
        }
    
        static void resetBuffer()
        {
            for (int i = 0; i < HEIGHT; i++)
                memset(buffer[i], ' ', WIDTH);
        }
    };
    
    char Canvas::buffer[Canvas::HEIGHT][Canvas::WIDTH + 1];
    
    class BinaryTreeGui
    {
    public:
        static TreeNode *create(string &s)
        {
            // discard the '[]'
            s = s.substr(1, s.size() - 2);
    
            // change s into ["1", ...,"2"]
            auto v = split(s, ",");
    
            // create binary tree
            TreeNode *root = nullptr;
            innerCreate(v, 0, root);
    
            // init x and y of tree nodes
            initCoordinate(root);
    
            return root;
        }
    
        static void show(TreeNode *root)
        {
            const int widthZoom = 3;
            printf("width zoom: %d
    ", widthZoom);
            Canvas::resetBuffer();
            queue<TreeNode *> q;
            q.push(root);
            int x, y, val;
            string sval;
            while (!q.empty())
            {
                auto p = q.front();
                q.pop();
                bool l = (p->left != nullptr);
                bool r = (p->right != nullptr);
                x = p->x, y = p->y, val = p->val, sval = to_string(p->val);
                Canvas::put(2 * y, widthZoom * x, sval);
                if (l)
                {
                    q.push(p->left);
                    Canvas::put(2 * y + 1, widthZoom * p->left->x, '_', 
                                widthZoom * (x - p->left->x) + sval.length() / 2);
                }
                if (r)
                {
                    q.push(p->right);
                    Canvas::put(2 * y + 1, widthZoom * x, '_',
                                widthZoom * (p->right->x - x) + to_string(p->right->val).length());
                }
                if (l || r)
                    Canvas::put(2 * y + 1, widthZoom * x + sval.length() / 2, "|");
            }
            Canvas::draw();
        }
        static void show2(TreeNode *root)
        {
            const int widthZoom = 2;
            printf("width zoom: %d
    ", widthZoom);
            Canvas::resetBuffer();
            queue<TreeNode *> q;
            q.push(root);
            int x, y, val;
            while (!q.empty())
            {
                auto p = q.front();
                q.pop();
                x = p->x, y = p->y, val = p->val;
                Canvas::put(2 * y, widthZoom * x, to_string(val));
                if (p->left != nullptr)
                {
                    q.push(p->left);
                    Canvas::put(2 * y + 1, widthZoom * ((p->left->x + x) / 2), '/', 1);
                }
                if (p->right != nullptr)
                {
                    q.push(p->right);
                    Canvas::put(2 * y + 1, widthZoom * ((x + p->right->x) / 2) + 1, '\', 1);
                }
            }
            Canvas::draw();
        }
    
        static void destroy(TreeNode *root)
        {
            if (root == nullptr)
                return;
            destroy(root->left);
            destroy(root->right);
            delete root;
            root = nullptr;
        }
    
    private:
        static void innerCreate(vector<string> &v, size_t idx, TreeNode *&p)
        {
            if (idx >= v.size() || v[idx] == "null")
                return;
            p = new TreeNode(stoi(v[idx]));
            innerCreate(v, 2 * idx + 1, p->left);
            innerCreate(v, 2 * idx + 2, p->right);
        }
    
        static void replaceAll(string &s, const string &oldChars, const string &newChars)
        {
            int pos = s.find(oldChars);
            while (pos != string::npos)
            {
                s.replace(pos, oldChars.length(), newChars);
                pos = s.find(oldChars);
            }
        }
    
        static vector<string> split(string &s, const string &token)
        {
            replaceAll(s, token, " ");
            vector<string> result;
            stringstream ss(s);
            string buf;
            while (ss >> buf)
                result.push_back(buf);
            return result;
        }
    
        static void initX(TreeNode *p, int &x)
        {
            if (p == nullptr)
                return;
            initX(p->left, x);
            p->x = x++;
            initX(p->right, x);
        }
        static void initY(TreeNode *root)
        {
            if (root == nullptr)
                return;
    
            typedef pair<TreeNode *, int> Node;
    
            root->y = 1;
    
            queue<Node> q;
            q.push(Node(root, root->y));
            while (!q.empty())
            {
                auto p = q.front();
                q.pop();
                if (p.first->left != nullptr)
                {
                    p.first->left->y = p.second + 1;
                    q.push(Node(p.first->left, p.second + 1));
                }
                if (p.first->right != nullptr)
                {
                    p.first->right->y = p.second + 1;
                    q.push(Node(p.first->right, p.second + 1));
                }
            }
        }
    
        static void initCoordinate(TreeNode *root)
        {
            int x = 0;
            initX(root, x);
            initY(root);
        }
    
        // print info of tree nodes
        static void inorder(TreeNode *p)
        {
            if (p == nullptr)
                return;
            inorder(p->left);
            printf("val=%d, x=%d, y=%d
    ", p->val, p->x, p->y);
            inorder(p->right);
        }
    };
    
    int main(int argc, char *argv[])
    {
        string s = "[512,46, 7453,35, 6,26,null,-1,null,9,null]";
        // string s = "[6,2,8,0,4,7,9,null,null,3,5]";
        // string s(argv[1]);
        auto root = BinaryTreeGui::create(s);
        BinaryTreeGui::show(root);
        BinaryTreeGui::show2(root);
        BinaryTreeGui::destroy(root);
    }
    
  • 相关阅读:
    String类的substring方法
    postman绕过登录,进行接口测试的方法
    Dubbo
    那些吊炸天的互联网名词
    版本控制工具git
    Ubunto20.04 sudo apt-get update 出现目标被重置多次!
    ubuntu环境下搭建Hadoop集群中必须需要注意的问题
    Python作业---内置数据类型
    python作业完成简单的文件操作
    python3实现计算器
  • 原文地址:https://www.cnblogs.com/sinkinben/p/12615183.html
Copyright © 2020-2023  润新知