二叉樹圖形化顯示


在刷 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] = '\0';
            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] = '\0';
            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\n", 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\n", 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\n", 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);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM