問題描述
樹狀結構為一種程式設計者常用的資料結構,但較不容易在畫面上展示。常見的樹狀結構展示方式為「樹根在左、樹枝朝右」的橫向目錄型式,但其實若能以「樹根在上、樹枝朝下」的直向表示法,更能展現樹狀結構的精神。而要在電腦的二維螢幕上畫出美觀的樹狀結構,必須計算其中各個節點的座標位置。以下圖的樹狀結構為例,在一般文字模式的螢幕上(一個位置一個字元),其畫出來的樣子如下所示。其中名稱為N-1的節點,其起始座標為(5, 3),名稱為N-2的節點,其起始座標為(22, 3),名稱為N-1-1的節點,其起始座標為(1, 6),其餘依此類推。
根據上例的表示方法,請寫一程式讀入如下格式的樹狀結構後,轉換為直向的樹狀結構,輸出每個節點的二維座標位置。
在將輸入轉換成直向的樹狀圖時,為求美觀,轉換時必須符合下列規則:
1. 節點以其名稱輸出。所有的距離、位置、寬度,都以「字元數」計算。
2. 同一階層的節點有相同垂直位置。
3. 同一階層的節點間必須求出最小間隔,但至少距離一個空白字元。
4. 若某一節點有多個子節點,則其水平位置必需在所有子節點的總寬度的正中央。其計算公式為:(LS + LE + L) / 2。
其中:
S:本身節點的起始水平位置;當S 無法整除時,取其四捨五入值。
LS:第一個子節點的起始水平位置。
LE:最後一個子節點的最後水平位置。
L:本身子節點的長度。
輸入檔格式(C:\tree\input.txt)
1. 輸入為多行字串,每一行代表一個由根節點到本身節點的路徑,節點名稱由英文、數字與可列印符號表示,而上、下節點之間以冒號隔開。
2. 所有的輸入依照深度優先演算法(Depth-First)列出,不會有如下情形:
N-1
N-1:N-1-1
N-2
N-1:N-1-2
3. 輸入格式一定正確,不需考慮輸入格式錯誤情形。
輸出檔格式 (C:\tree\output.txt)
1. 輸出時,先輸出每個節點的路徑,其順序輸入相同。
2. 每個路徑後面,空一格後加上括號,括號內第一個值為該節點的起始水平座標位置,第二個值為其垂直座標位置。
3. 水平與垂直座標位置均以字元數計算,由1 開始。如上例中的節點N-1,其起始水平座標位置為5,則輸出5,垂直座標為3,則輸出3。
輸入檔範例1
N-1
N-1:N-1-1
N-1:N-1-2
N-1:N-1-2:N-1-2-1
N-2
N-2:N-2-1
N-2:N-2-1:N-2-1-1
N-2:N-2-2
N-2:N-2-3
輸出檔範例1
N-1 (5,3)
N-1:N-1-1 (1,6)
N-1:N-1-2 (7,6)
N-1:N-1-2:N-1-2-1 (6,9)
N-2 (22,3)
N-2:N-2-1 (15,6)
N-2:N-2-1:N-2-1-1 (14,9)
N-2:N-2-2 (21,6)
N-2:N-2-3 (27,6)
輸入檔範例2
A
A:AB
A:AC
a
a:ab
a:ac
輸出檔範例2
A (3,3)
A:AB (1,6)
A:AC (4,6)
a (9,3)
a:ab (7,6)
a:ac (10,6)
解題思考
這題老實說比較麻煩,我的作法是把問題分成兩個部份:儲存輸入資料,以及處理解析後的資料(也就是得出題目要求的節點座標)。
要如何根據輸入,將必要的資料儲存在變數呢?在一次讀取檔案的一整行之後,根據題意,我們把這筆資料根據":"作字串切割。這些切割出來的內容,就是一個節點的名稱以及它的節點路徑。
假若這一行的字串共切割成n個部份,則第1個到第n-1個部份即為節點路徑,而最後一個部份就是節點名稱了。至於資料的儲存,就只要根據切割出來的節點路徑,找到其父節點並新增節點資料就可以了。
現在,我們已經能將資料正確的存入結構中了。接著要怎麼得知節點座標呢?
首先,因為每一階層的節點都需要有最小間隔,且至少要距離一個空白,所以我們需要一些變數來存取每個階層的"邊界"。接著,我們就照著深度優先、遞迴的方式,來放置每個節點。
我們舉題目的範例1來看,請看下圖:

第一層目前的邊界為0,因此節點"N-1"暫時放置在第一格的位置,並將邊界加上L+1(節點長度+間隔的空格)。

第二層目前的邊界為0,因此節點"N-1-1"暫時放置在第一格的位置,並將邊界加上L+1。所以"N-1-2"放置於第七格,並同樣再將邊界加上L+1。

第三層目前的邊界為0,因此節點"N-1-2-1"暫時放置在第一格的位置,並將邊界加上L+1。

由"N-1-2-1"節點的位置回推"N-1-2"的節點位置(利用題目給予的公式):結果小於最小間隔,故將"N-1-2-1"節點調整到配合"N-1-2"節點的位置。

由"N-1-1"與"N-1-2"節點的位置回推"N-1"的節點位置:結果大於其目前的位置,故將"N-1"節點調整到配合"N-1-1"與"N-1-2"節點的位置。
只要能讓程式重複以上步驟,就能夠達成題目的要求了。最後再利用深度優先的方式輸出結果,這題就完成了!
參考解答(C++)
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#define _GET_MAX_ 256
using namespace std;
struct node
{
int s; // 座標起始點
string name; // 節點名稱
vector<node> childen; // 子節點
};
int move(node &, int);
void show(node &, int, string, ofstream &);
vector<int> border;
int main(void)
{
ifstream in("C:/tree/input.txt");
ofstream out("C:/tree/output.txt", ios_base::trunc);
// 建立一個"虛擬根"作存取起始點
node root;
root.s = 0;
cout << "讀入資料 . . ." << endl;
char get[_GET_MAX_];
while (in.getline(get, _GET_MAX_))
{
// 建立一個節點指標, 指向根節點
node *tree = &root;
string str = get;
string sub;
int start = 0, end;
while (1)
{
// 根據冒號擷取資料
end = str.find(":", start);
sub = str.substr(start, end - start);
// 路徑結束, 跳出
if (end == string::npos) { break; }
// 尋找節點路徑
for (vector<node>::iterator it = (*tree).childen.begin();
it < (*tree).childen.end(); it++)
{
if ((*it).name == sub)
{
// 更新節點指標為此節點
tree = &(*it);
break;
}
}
start = end + 1;
}
// 新增一個節點, 使之為當前節點的子節點
node newnode;
newnode.name = sub;
newnode.s = 1;
(*tree).childen.push_back(newnode);
}
in.close();
// 呼叫遞迴函式處理資料並印出結果
move(root, 0);
show(root, 0, "", out);
out.close();
cout << "輸出資料在 ouput.txt" << endl;
system("pause");
}
int move(node &tree, int level)
{
for (vector<node>::iterator it = tree.childen.begin();
it < tree.childen.end(); it++)
{
// 此層級邊界不存在就新增一個
if (border.size() <= level) { border.push_back(0); }
// 保持節點間隔
(*it).s += *(border.begin() + level);
*(border.begin() + level) += (*it).name.size() + 1;
// 假如有子節點
if ((*it).childen.size())
{
// 呼叫遞迴函式得出偏移量, 以更新起始座標及邊界
int p = move(*it, level + 1);
(*it).s += p;
*(border.begin() + level) += p;
}
}
int m = ((*tree.childen.begin()).s + (*(tree.childen.end() - 1)).s
+ (*(tree.childen.end() - 1)).name.size() - tree.name.size()) / 2;
if (m < tree.s)
{
// 偏移量為負, 修正子節點座標及邊界
for (vector<node>::iterator it = tree.childen.begin();
it < tree.childen.end(); it++)
{
(*it).s += tree.s - m;
}
*(border.begin() + level) += tree.s - m;
return 0;
}
// 返回父節點偏移量
return m - tree.s;
}
void show(node &tree, int level, string str, ofstream &out)
{
if (level)
{
// 印出當前節點座標
cout << str << tree.name << " (" << tree.s << ", " << 3 * level << ")" << endl;
out << str << tree.name << " (" << tree.s << ", " << 3 * level << ")" << endl;
str += tree.name + ":";
}
// 遞迴呼叫函式顯示結果
for (vector<node>::iterator it = tree.childen.begin();
it < tree.childen.end(); it++)
{
show(*it, level + 1, str, out);
}
}
4 回覆:
這行
tree = &(*t)
貌似多此一舉
另外呼叫指標中的成員可用->
我知道你的意思,不過這行若以 "tree = it;" 來寫會 compile error。錯誤訊息如下:
Tree.cpp||In function `int main()':|
Tree.cpp|60|error: cannot convert `it' from type `__gnu_cxx::__normal_iterator<node*, std::vector<node, std::allocator<node> > >' to type `node*'|
||=== Build finished: 1錯誤,0警告 ===|
另外,STL中的 iterator 所指向的內容必須以 "*iterator" 提取。
若是將提取物件(結構體)的"*iterator."改成 "iterator->"仍然會發生類似的錯誤訊息。
這個就牽涉到 STL 的實做原理了,我猜大概跟 operator overloading 有關。
原來如此
沒仔細測試
但我在VC++測試iterator->可以正常運作@@
經過與智涵於MSN討論之後,我實際做了個測試:
http://src.wtgstudio.com/?17TUN1
代表"iterator->"提取指標中的成員是可以的。
至於上面提到的錯誤,是我自己的誤解。感謝智涵的糾正。
張貼留言