1月準備考數據結構;成天窩在家裏鼓搗這玩意.現在勉強弄出了個可以實用的樹和圖的庫.接下來是實現樹和圖的各種算法.說到算法必然就要有用來測試的數據.這幾天在弄樹.剛開始是從文件系統來構造.但是每次從硬盤文件系統構造不但要花大量的時間而且過於複雜不方便測試.書上有不少例子,但是問題是數據結構書上的方法都是在代碼內手動編碼構造.說實在寫着很憋屈.現在不都是自動化的時代了麼?回想起某書上所說的”純文本的威力”,再加上上次做圖的時候爲圖弄了個腳本引擎來記錄圖的架構;於是覺定爲樹結構也弄一個.
首先想到的自然是XML.XML有現成的引擎,XML有標準的支持;但是XML書寫之麻煩…弄出來後感覺比手動硬編碼輕鬆不了多少.在考慮了不到兩分鐘後決定放棄轉而弄自己的腳本.
接下來就是腳本的設計…回想做圖結構的腳本我是這樣設計的:
//Graph定義文件
//語法:
//以//開頭的行爲註釋
//連接符:
//a->b
//表示a到b間有一條以a起始止於b的路徑
//a<->b表示a與之間是互通的
//每行定義爲路徑的名稱,後面跟路徑的定義,以冒號分隔;
//每個定義以分號表示結尾;
//節點&路徑命名規則:
//字數無限制,字符必須是a-z A-Z 0-9中的字符
//defnode 用於定義節點(可選,用於定義沒有出度入度的孤立點)
defnode a,b,c,d,e,f,g;
12:a -> c;
15:b->e;
//c
30:c->b;
3:c->e;
//d
8:d->b;
//e
40:e->d;
5:e->f;
//f
40:f->a;
10:f->d;
但樹和圖不太一樣,樹主要保存節點之間的父子兄弟關係.沒必要記錄邊.先是翻了半天數據結構的書,找到了通過前序/後序遍歷序列 + 中序遍歷序列構造二叉樹的方法.而樹是可以轉成二叉樹的.嗯…於是第一種方案出來了,使用兩行節點序列構造二叉樹後通過”左兒子右兄弟”的法則再構造成樹:
節點1,節點2,節點3,……,節點N
節點1,節點2,節點3,……,節點N
正當我準備動手做解析器的時候某人給我提出異議:
這玩意但是表達二叉樹就已經夠麻煩了;做比較複雜的樹比如有十來個節點的難不成要先在腦內補完成二叉樹後再寫出來?
……
…
囧……我把這茬兒給忘了,光想着怎麼以文本表達節點間關係了.這樣弄不是比硬編碼更麻煩嘛?還好還沒開始編碼.謝天謝地.親手刪掉寫了半天的代碼會是很痛苦的事…
接下來腳本的設計目標就很明顯了:
1. 要便於使用文本編輯器直接書寫.
2. 要直觀.不用解析器也能很好的表達節點間的關係.
在換了n個方案後,(其實前後也就幾分鐘)在我無聊的點擊VS代碼編輯器裏的+號玩的時候;纔想到C++/C#的類語法不就是很好的表達了節點關係的東東嘛:將類看做是父節點,內部可以包含子結點(嵌套類/類方法).而且也很直觀.於是就有了這麼個設計方案:
//樹的構成腳本.
//腳本僅描述了樹節點間的關係,並不攜帶節點數據信息
//腳本語法:
//雙斜槓"//"開頭的行爲註釋.
//節點命名規則:只能包含a-z A-Z 或 0-9中的字符
//節點名稱可以重複出現,有節點名的地方僅表示"這裏有一個節點"而已
//如果一個節點沒有子結點.則以分號";"結尾.如:n;
//如果一個節點有子結點則緊跟節點名的是一對大括號,大括號必須配對,其子結點寫在大括號內.
//大括號可以嵌套 例: a{b; c{d;}}
//一段腳本只可包含一個根結點.即只能包含一棵樹;擁有多個根的腳本解析雖然不會出錯,但僅會解析第一棵樹
a{//根結點
//子樹
b111{
c;
//子樹中的子樹
d{
e;
}
}
//單個的節點
f;
gn;
h;
//還是子樹
i{
j;
k;
}
l;
//仍然是子樹
m{
n;
}
q;
}
接下來就是腳本的解析器了.先是設計了一個簡單的類FSM:
/// <summary>
/// 一個簡單的有限狀態機
/// </summary>
/// <typeparam name="S"></typeparam>
class FSM
{
public int State
{
get;
set;
}
int m_stopState;
Action<FSM,int> m_proc;//狀態的轉換處理函數
public FSM(int initState, int stopState, Action<FSM,int> proc)
{
State = initState;
m_stopState = stopState;
m_proc = proc;
}
public void Run()
{
while (State.CompareTo(m_stopState)!=0)
{
m_proc(this,State);
}
}
}
用它來記錄腳本代碼掃描的狀態.
然後爲Tree添加一個靜態的Parse方法:
Tree <T> Parse(string script,Func <string,T> valfunc);
Valfunc用於在構造樹節點的時候通過節點名獲取節點數據.結合C#的Lambda表達式,將節點數據的保存/獲取交給調用方實現.可以讓樹的存儲/讀取變得更加的靈活.
然後在Parse中先對腳本做預處理:
script = Regex.Replace(script, "//.*//n", "");//去除註釋
script = Regex.Replace(script, "//s*", "") ;//去除無用的空白
PS:這是頭一次寫正則表達式一次成功.淚奔ing…
經過這個處理後形如
A{
B{
C;
}
D;
}
的代碼就會變成這樣:
A{B{C;}D;}
的代碼.然後內部新建了一個解析器類scriptParser,送進去進行解析.
至於scriptParser的實現….先添加一個FSM成員,然後爲它定義3種狀態:
const int Symbol = 0;//當前狀態爲取符號
const int Match = 1;//正在配對花括號或分號
const int Complete = 3;//解析完成,結束
然後定義成員函數nextword用於每調用一次從腳本中讀取出一個節點名(這裏稱之爲Symbol)或是一個特殊符號(花括號或分號).並自動增加字符串中的遊標curPos.然後開始考慮如何提取每個節點名的問題:
FSM的初始態爲Symbol;此時解析器將期盼獲取一個節點名.
在獲取一個Symbol後,它將嘗試匹配一對花括號或者一個分號.
編寫FSM的回調函數void parseProc(FSM fsm, int state)
第一行先讀取一個字符串:
string word = nextword();
然後開始處理這個word:
switch (state)
{
case State.Symbol:
fsm.State = State.Match;
break;
case State.Match
switch (word)
{
case "{":
//接下來期望取得的將是一個節點
fsm.State = State.Symbol;
//定義一個int match用於配對花括號
match++;
break;
case "}":
{
//先保存curPos
int tmp = curPos;
//下一個可能會跟花括號
if (nextword() != "}")
{
fsm.State = State.Symbol;
}
//恢復curPos
curPos = tmp;
match--;
}
break;
case ";":
{
int tmp = curPos;
//分號後可能會跟花括號
if (nextword() != "}")
{
fsm.State = State.Symbol;
}
curPos = tmp;
}
break;
default:
throw new Exception(string.Format("非法符號/"{0}/"", word));
}
break;
}
if (curPos >= m_script.Length - 1)
{
//讀取完畢,置爲終止狀態
fsm.State = State.Complete;
}
這樣.在case Symbol:中取到的word將都是節點名.調用FSM.Run()反覆執行這條函數的話.將會得到一個節點名的序列;接下來考慮如何將這個序列變成一棵樹.
說到構造樹,最常用的數據結構就是棧.定義一個類成員Stack <TreeNode> nodeStack;用於存儲節點.
然後在處理節點符號的地方加上這些語句:
TreeNode node;
//獲取節點值
T value = valfunc(word);
//m_result爲最終返回的Tree<T>
if (m_result == null)
{
//樹尚未被構建出來,先構造一個樹
m_result = new Tree<T>(value);
node = m_result.Root;
}
else
{
node = new Tree<T>.TreeNode(value);
//棧頂的節點將是這個節點的父節點
nodeStack.Peek().Children.Add(node);
}
//無論如何,將新的節點壓入棧
nodeStack.Push(node);
//改變fsm的狀態
fsm.State = State.Match;
這樣,決定下一個節點是否爲當前節點的子節點的關鍵就在nodeStack的Pop操作上了.首先是對分號的處理.因爲一個以分號結尾的節點沒有子結點.所以在處理”;”的時候加上一條nodeStack.Pop ();對於”{“,它代表接下來直到配對的”}”前的所有節點都是當前節點的子結點.所以在處理”{“的時候什麼都不做,而在處理”}”的時候加上一條nodeStack.Pop ();
完整的parseScript:(加上了簡單的語法檢錯)
void parseProc(FSM fsm, int state)
{
//讀取下一個詞/符號
string word = nextword();
switch (state)
{
case State.Symbol:
//驗證節點命名的合法性
if (validateSymbol(word))
{
TreeNode node;
T value = valfunc(word);
if (m_result == null)
{
m_result = new Tree<T>(value);
node = m_result.Root;
}
else
{
node = new Tree<T>.TreeNode(value);
nodeStack.Peek().Children.Add(node);
}
nodeStack.Push(node);
fsm.State = State.Match;
}
else
{
throw new Exception(string.Format("/"{0}/"不是一個合法的標識符", word));
}
break;
case State.Match:
switch (word)
{
//接下來期望取得的將是一個節點
case "{":
fsm.State = State.Symbol;
match++;
break;
case "}":
{
int tmp = curPos;
//下一個可能會跟花括號
if (nextword() != "}")
{
fsm.State = State.Symbol;
}
curPos = tmp;
nodeStack.Pop();
match--;
}
break;
case ";":
{
int tmp = curPos;
//分號後可能會跟花括號
if (nextword() != "}")
{
fsm.State = State.Symbol;
}
curPos = tmp;
nodeStack.Pop();
}
break;
default:
throw new Exception(string.Format("非法符號/"{0}/"", word));
}
break;
}
if (curPos >= m_script.Length - 1)
{
fsm.State = State.Complete;
}
}
這樣就能由簡單易寫的腳本構造出各種樹的結構了.比起照着書上的例子一行一行錄入代碼構成樹要省下大量的時間J
接下來編譯運行測試這段代碼:
a{//根結點
//子樹
b111{
c;
//子樹中的子樹
d{
e;
}
}
//單個的節點
f;
gn;
h;
//還是子樹
i{
j;
k;
}
l;
//仍然是子樹
m{
n;
}
q;
}
在按鈕事件中編寫代碼:
//讀取腳本
var s = LoadScript();
if (s == null)
return;
//直接用節點名當作節點數據
Tree<string> t = Tree<string>.Parse(s, (n) => { return n; });
treeView.Nodes.Clear();
treeView.ShowTree(t);
運行後的結果: