在刷 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].left
為v[2 * i + 1]
,v[i].right
為v[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);
}