前言:2-3樹的資料比較少,國內的某本參考書關於2-3樹的介紹和國外的還不一樣,網上搜了一下,發現這一篇翻譯的介紹比較詳細,不錯,於是轉貼一下。
資料來源於:http://blog.donews.com/sowen/
前言
備註:文中可能偶爾多用了英文,倒不是賣弄,很多時候只是習慣性的,因爲如果你平時接觸的東西都是英文的,你寫下來的時候自然想到的是英文字眼,而不是多一層先翻譯成中文。還有一些是我只記得英文資料上的定義,比如我寫之前想到 balanced tree,卻不知道中文應該是什麼,查google才知道應該翻譯成平衡樹。還有的原因是可能英文解釋能更清晰表達就直接使用英文了。反正這篇東西的對象都是同行,我相信大部分人都有閱讀英文資料的習慣,因此一定能夠了解。如果閣下不喜歡中文夾着英文的東西,請不必閱讀了,謝謝。
tree 在計算機數據結構裏是一種非常重要的東西,介紹性的東西就不多講了,樹多數用於檢索量比較大的地方,比如數據庫和硬盤文件排列之類。樹分很多種,最簡單的當然是 BST (binary search tree), 中文應該是 二叉檢索樹,這個東西雖然簡單,可是毛病很多,比如很容易出現 worse case,就是檢索中最不願意看到的線性檢索(一棵只有右子樹或者左子樹的樹);而且BST不是balanced的。所以,BST一般很少應用,前人發明了很多其他的樹,比如AVL,B-TREE,2-3 tree, 2-3-4 tree 等等。這篇文章要介紹的就是2-3 tree,主要是因爲它的中文資料……似乎沒有(反正我沒有在google上找到)。而 2-3-4 tree 只是一個變種,最後會簡單介紹一下。
本文的實現代碼我用了JAVA,本來這種需要訪問指針的東西,我更想用c++,但考慮易讀性還是JAVA比較好點,比較適合介紹性質的代碼。
特性
2-3 tree 嚴格來說只有兩個要求:
1.所有的結點有2個或者3個子樹
2.所有的leaves(葉)都在同一個級別上。
不知道這樣的中文解釋是否夠清楚,解釋一下就是:葉是樹中最後一個結點,它們的高度(path from root)都必須一樣;每個結點最多只能有3個子樹。
比如下面這棵樹就是一個合法的2-3 tree
<10 20>
/
|
/
/
|
/
<5> <15> <30 40>
其實2-3 tree也是從BST演變過來的,小值就在左邊,大值在右邊。每個結點最多隻有兩個值,一個小,一個大。如果一個結點有兩個值,這個結點稱爲full,那麼如果它有子樹,那麼它一定有3個子樹。如上圖所示,小值(<10) 在左子樹,大值 (>20) 在右子樹,而中間值( 10<x<20 ),就在中間的子樹。
爲什麼要這樣排列?
因爲它是balanced的!一棵高度爲k的2-3 tree有2k – 1 到 3k – 1 之間的葉;一棵有n elements 的2-3 tree 高度最小爲 1+log3n, 最大爲 1+log2n。它的檢索時間爲O(logn)。
檢索的pseudo code
LOOP until curr = nil
IF ( data = curr.small OR data = curr.large )
Return curr
ELSE IF ( data < curr.small )
curr = curr.left
ELSE IF ( data > curr.small AND data < curr.large )
curr = curr.middle
ELSE
curr = curr.large
也可以通過遞歸來做,跟BST差不多,檢索並不是本文介紹的目的,因爲實在沒什麼好說的。
2-3 tree的結點結構
最簡單的結構如下
struct tree23Node {
int small,
int large,
tree23Node left,
tree23Node middle,
tree23Node large
}
插入
對於2-3 tree,所有的插入(新值)都必須發生在leaf。所以,首先找到新值應該所在的leaf,然後根據這個leaf的情況做出判斷:
1. 該點只有一個元素。直接加入就可以了,判斷是small還是large。
2. 該點full(small和large都有值),其父結點不是full。那就split該結點,並把中間值推上去。
3. 該點和其父結點都full。如果父結點full,表示父結點已經有3個子樹。那就需要一直split 並一直往上推,直到發生以下情況:
1) 父結點最多隻有3個子樹
2) 父結點是根,創建一個新root,保存中間值。樹的高度增加1
最好的解釋其實還是例子,請先看一棵合法的2-3 tree如下:
<53>
/ /
/ /
<27> <65, 78>
/ / / | /
<12> <39,43> <60> <69,74> <93>
樹一
1) 加入 15
新加入一個值的時候,要記住,所有的新增一定發生在leaf。所以第一步我們需要找到15的位置。不錯,就是在<12>那裏。這個 leaf 只有一個元素,就是說不是full,這是最簡單的一種情況,直接把15加在那個leaf就可以了。因爲15>12,所以樹會變成
<53>
/ /
/ /
<27> <65, 78>
/ / / | /
<12,15> <39,43> <60> <69,74> <93>
樹二
我們可以再檢查一下,是否滿足2-3 tree的兩個要求。(1) 所有結點只有2個或者3個子樹,這個滿足了。(2)所有的leaves都在同一個級別(高度一樣),這個也滿足了。
2) 加入22
這次好像有點麻煩了,22似乎應該加在<12,15>這個leaf上,可是它已經full了。如果加入後,應該變成
<53>
/ /
/ /
<27> <65, 78>
/ / / | /
<12,15,22> <39,43> <60> <69,74> <93>
樹三 (not valid yet)
按照前面提到的插入要求,需要split這個結點,split之後,該結點應該變成<12> <15> <22>三個結點,然後把中間那個推上去。(爲什麼呢?因爲只有中值上去變成父樹裏的值,原來的large變成的那個結點才能變成中子樹的結點,還記得它的特性嗎?所以,最後的樹應該變成
<53>
/ /
/ /
<15,27> <65, 78>
/ | / / | /
<12> <22> <39,43> <60> <69,74> <93>
樹四
請自行檢驗是否合法2-3 tree
3) 現在讓我們試試新加入 32
首先,大家都知道32應該在那個leaf開始動作了吧。不錯,就是<39,43>,加入後,這個leaf應該變成<32,39,43>;full了,要split,變成<32><39><43>,再把中值推上去,變成以下的一棵樹
<53>
/ /
/ /
<15,27,39> <65, 78>
/ | | / / | /
<12><22><32><43> <60> <69,74> <93>
樹五(not valid yet)
很顯然,這不是一個合法的樹。因爲有結點有超過3個子樹,而且有結點超過兩個元素(full)。那麼就需要繼續split,然後又是中值往上推,直到見到只有兩個子樹的結點<53>。所以,再split一次後的樹應該變成:
<27,53>
/ | /
/ | /
<15> <39> <65, 78>
/ / / / / | /
<12> <22> <32> <43> <60> <69,74> <93>
樹六
4) 在【樹六】的基礎上加入 34,35,37,後,2-3 tree應該變成如下:
<27,53>
/ | /
/ | /
<15> <34,39> <65, 78>
/ / / | / / | /
<12> <22> <32><35,37><43> <60> <69,74> <93>
樹七
你對了嗎?
1) 最後,讓我們看看最複雜的一種情況,如果在【樹七】中,加入36,應該變成怎樣?
我們可以一步一步來,首先,找到36應該在的leaf,<35,37>,加入後變成<35,36,37>,該leaf變成full了。Split,然後推中值上去。變成如下:
<34,36,39>
/ | | /
<32> <35> <37> <43>
好,父樹也變成full了,這種情況我們見過,再split和再推中值上去。
<27,36,53>
/ | | /
<15> <34> <39> <65,78>
// / / / / / | /
… <32><35><37><43> …
嗯,現在我們的root也變成full了。那就在split一次,然後新建一個root,保存中值。 變成
<36>
/ /
<27> <53>
/ / / /
<15> <34> <39> <65,78>
………………………………… (以下省略,大家都知道下面怎麼連接了吧)
這個時候,我們的2-3 tree高度增加了1
到這裏,已經列舉了所有增加新值的例子。如果還不是太清晰,那我就在此總結一下吧:
l 新值的增加總是發生在leaf。(它最後是否在leaf上就很難說,但增加這個動作永遠是發生在leaf上,所以第一步永遠都是找到新值應該位於的leaf)
l 如果一個結點有三個值,我們說它是full了,需要split,把中值推到父結點上去。
l 如果一個結點需要split而它又剛好是root,那麼就新創建一個root,保存那個中值。樹的高度增加1。
接下來我們要做什麼呢?大家是想看刪除的特性嗎?嗯,我只能說,刪除相當複雜,即使只是BST的刪除也很麻煩,大家都還記得BST的刪除吧。我現在可以說的2-3 tree的刪除大概是和新增反過來的,新增是往上推一個值,刪除是往下找一個子樹合併。
我不想立刻就開始講刪除,因爲我想趁大家還記得新增的特性時讓大家看看代碼,加深大家對新增的印象。
我的代碼- 新增部分
前面我給大家看過一個最簡單的structureof 2-3 treenode,那個結構其實也可以做,但是比較麻煩。因爲我們需要往上讀父結點,如果沒有parent指針,一般都是兩種方法,一是用臨時變量保存,這種方法在2-3tree裏不是太現實,因爲你不知道要往上查多少次;另外一種方法是用回溯,把所有順序訪問過的結點放到stack裏,而我前面也說過,2-3tree的高度最大爲log2n +1,所以stack的實現用數組都可以,比較方便。如果stack爲空,表示我們已經到root了。但是用stack的話,我們就只能用循環。(大家都明白爲什麼吧)
如果在c++裏,我們還可以用遞歸回到父結點上;但是java裏參數傳遞沒有引用傳遞(也有人說JAVA只有引用傳遞,都是說法一個,這裏不追究)這個概念,所以我們很難用遞歸一步一步往上走。
不過既然寫程序的是我們,我們可以對那個結構稍做修改,以適應我們的需要。
請參照上面那些樹的圖,有沒有發現很多時候新增動作發生之後,一個結點會出現4個子樹,3個值。這是最大的可能發生的情況,那麼我們就給結構3個值,4個子樹。同時爲了方便,也給它一個父結點的引用,以及一個itemCount的值標誌該結點有多少個值。
因此,新的tree23Node結構如下:
public class tree23Node { int small; // the small item in this node int large; // the large item in this node int temp; // stores a temporary item
tree23Node left; // specifies left child tree23Node middle; // specifies middle child tree23Node right; // specifies right child int itemCount; // how many items in this node
// 以下一個結點用來儲存臨時子樹,爲什麼設置成爲private,是因爲我打算將讀取它們的動作都放在這個class裏,就自然不需要讓別人看到它們。 private tree23Node rtNode; private tree23Node parent; }
|
下面是tree23Node的constructor,一共有兩個,很容易看到,這裏就不註釋了。
需要注意的是,我設置了default value是 -1,其實也是最小值。所以我是假定後面的2-3 tree 加入的值都是正數。當然你也可做相應修改以至能夠接受負數。
tree23Node() { itemCount=0; small = large = temp = -1; left = middle = right = parent = null; } // end tree23Node constructor
tree23Node(int newVal) { itemCount = 1; small = newVal; large = temp = -1; left = middle = right = parent = null; } // end tree23Node constructor |
同時tree23Node還提供兩個方法,一個是isLeaf,返回真如果該node是一個leaf;代碼如下(對不起,註釋我一般習慣用英文):
/**************************************************************** * method isLeft * purpose: tell whether this node is a leaf or not. * (a leaf is a node that has no child) * return true if it is a leaf ***************************************************************/ public boolean isLeaf() { boolean retval = false; if (left==null && middle ==null && right==null) { retval = true; } return retval; } // end method isLeaf |
另外一個重要的方法是 split,這個代碼會放到最後。
下一篇,我們會看到tree23 class 裏面的insert方法
對於 class tree23,我提供了一個public的insert方法,如下:
public void insert(int data) { // if the root is empty, which means the tree is emtpy. // create the tree. otherwise, call a private method
if (root==null) { root = new tree23Node(data); } else { insert(root, data); } } // end insert |
同時,我也提供了一個private的insert方法,其實也可以放到同一個地方,主要就是免得一個方法太長而已。該方法代碼如下:
private void insert(tree23Node curr, int data) { // stores the node in a temporary variable
tree23Node ptr = curr;
// for 2-3 tree, all new item must be inserted in a leaf. // find the correct leaf firstly // 還記得我前面說過的吧,第一步永遠都是先找到一個leaf
while (!ptr.isLeaf()) { if (data <= ptr.small && ptr.left!=null) { ptr = ptr.left; } else if (data>=ptr.large && ptr.right!=null) { ptr = ptr.right; } else if (data<ptr.large && ptr.middle!=null) { ptr = ptr.middle; } }
// 下面首先判斷兩種情況,如果當前的leaf只有一個元素,直接放進去就是了
if (ptr.itemCount==1) { if (ptr.small <= data) { ptr.large = data; ptr.itemCount ++; } else { ptr.large = ptr.small; ptr.small = data; ptr.itemCount ++; } } // end outer if
//否則的話,這個leaf已經full了,因爲它有兩個值。那我就split它好了。但是在split之前,我先做一件事,就是先還是照加入新值,並按順序排列好。就是說,如果新值最小,就放到small,把原來的small放到large,把原來的large放到temp。如果新值最大,就放到temp;如果它是中間值,就放到large,而原來的large就放到temp。
else { if (ptr.large <= data) { ptr.temp = data; } else if (ptr.small <= data) { ptr.temp = ptr.large; ptr.large = data; } else { ptr.temp = ptr.large; ptr.large = ptr.small; ptr.small = data; }
// increase the item count of this node, which // should be 3 now
ptr.itemCount++;
// call split method and overwrite root
root=ptr.split(); // split 方法返回的是一棵完整的樹
} // end else } // end method insert |
終於到了最激動人心的時候,就是 class tree23Node 的split方法。
爲什麼要把這個方法放到tree23Node裏面呢?一開始我只是覺得會方便一些,因爲比較split的時候,對每個結點的各種屬性都需要訪問和改變,如果在this裏面訪問,可以不需要寫 “[對象].” 這樣。呵呵,很無聊的原因吧。
其實,放到tree23Node還是有原因的,從OO的概念上看,tree23.split()沒有意義,無論它是否private。Split這個操作是屬於一個node的。另外,從可讀性和操作性上,放在tree23裏都相當麻煩,我也看過不少這樣做的代碼,大家也不妨去google查查看。
在顯示代碼之前,先做一些說明。
Split方法只有當一個node是full的時候才被調用,而且返回值是一棵完整的2-3tree。
該結點其中small是最小值,large是中間值,temp包含最大值。我們要做的是:
1. 將最小值和最大值分開,變成獨立的兩個node,讓我們叫一個是小結點,一個是大結點
2. 把中間值推到父結點。
1)如果父結點是空的,那麼表示我們已經到了root,創建一個新root包含那個中間值,並連接小結點到left,大結點到right,返回新創建的root
2) 看看this是屬於父結點的哪一個子樹
a) 屬於左子樹,推上去的值一定是一個父結點的最小值,需要保存在small裏
如果父結點是full的,先把原來的right連接到rtNode裏做個備份,原來的large放到temp裏,small放到large,推上去的值放在small裏。現在輪到父結點需要split了,請在這裏記住rtNode不爲null。
如果父結點不是full,那大家都知道怎麼做了。
b)屬於右子樹,推上去的一定是父結點的最大值,需要保存在large(也可能在temp裏,要看父結點是否full)
然後如上,分析父結點是否爲full
c) 屬於中子樹,如果父子樹有中子樹,那麼表示它一定已經full了。
最後,看看父結點的 rtNode是否爲空,如果不是,表示需要繼續往上split。遞歸調用。否則就一直循環推上去直到root,返回。
可能我上面解釋的不太清楚,還是讓我們看代碼吧
tree23Node split() { tree23Node retval, node1, node2; int s, m, l;
s = small; m = large; l = temp;
// firstly, prepare two new nodes that are splited // in this node. That means, I have two new nodes that // one has the smallest item, another has the largest item. // Then, I will pass the middle item up.
node1 = new tree23Node(s) ; node2 = new tree23Node(l);
// there are two cases // 1. this is the root, I just need to create a new root // that contains the middle item and connect two new // nodes to it // 2. this is not the root, I need to do the following things // 1) consider this node is parent’s left, middle, or // right, to do different connections // 2) if parent is still full, recursively split
if (parent==null) { // create a new node contains the middle value
retval = new tree23Node(m); retval.left = node1; retval.right = node2; } else { // get the parent node
retval = parent;
// if this node is parent’s left child
if (retval.left==this) { // if parent is full already, now parent should // have four children(1 is in rtNode), three items
if (retval.itemCount>1) { retval.rtNode = retval.right; retval.right = retval.middle; retval.temp = retval.large; }
retval.left = node1; retval.middle = node2; retval.large = retval.small; retval.small = m; } else if (retval.right==this) { // if parent of this is not full, now parent // should have 3 children, otherwise 4, 1 is in // rtNode
if (retval.itemCount==1) { retval.middle = node1; retval.right = node2; retval.large = m; } else { retval.right = node1; retval.rtNode = node2; retval.temp = m; } } else if (retval.middle == this) { // if this is the parent’s middle child // it means parent must have 2 items, it // has been full, I need to store one in rtNode
retval.middle = node1; retval.rtNode = retval.right; retval.right = node2; retval.temp = retval.large; retval.large = m; } else { //should never happen
System.out.println(”pointer error!”); System.exit(0); }
// 我已經把新元素加到父結點去了,它是否合法我暫時不管,總之我先增父結點的總數,減掉當前結點的總數
retval.itemCount++; itemCount–;
} // end else parent != null
// fix the new nodes
node1.parent = node2.parent = retval;
// if this node is not a leaf, I also need to fix // new nodes’ children and those parents
if (!isLeaf()) { node1.left = left; node1.right = middle; node2.left = right; node2.right = rtNode;
if (node1.left!=null) { node1.left.parent = node1; }
if (node1.right!=null) { node1.right.parent = node1; }
if (node2.left!=null) { node2.left.parent = node2; }
if (node2.right!=null) { node2.right.parent = node2; } } // end if
// now I can clean the resources
left = middle = right = rtNode = parent = null;
// if parent still has an rtNode, it means parent is full // now, it needs to be splited too.
if (retval.rtNode!=null) { return retval.split(); }
// if I am here, that means I have splited everything // now I move to the top, return the root
while (retval.parent!=null) { retval = retval.parent; }
return retval;
} // end method split |
到這裏,新增就全部講完了。真是累得可以,明天繼續。
刪除
這次,讓我們直接看例子,請回顧之前看到的【樹一】
1. 最簡單的情況就是,刪除一個leaf結點的值後,改結點仍然不會空。比如刪除【樹一】的39,43,69,73(是說一次刪除一個,不是指依次刪除)。提到的值都是位於leaf上,而且這些結點都是已經有兩個值;不過要記得移除了large值的話,不需要多做什麼;但移除small值,需要將large值設置爲-1,然後把原來的large移動到small上。
2. 在【樹一】上移除12
如果移走了12,樹會變成如下:
<53>
/ /
/ /
<27> <65, 78>
/ / / | /
<> <39,43> <60> <69,74> <93>
樹八 (not valid yet)
<27> 的左子樹空了,這不合法。這時候,我們需要向<12>的“親戚”(sibling)借一個值,也就是<27>的右子樹,它剛好有兩個值。將小值39推上去,再將27拉下來,最後如下
<53>
/ /
/ /
<39> <65, 78>
/ / / | /
<27> <43> <60> <69,74> <93>
樹九
這裏使用的策略是,當刪除一個結點的值會識得該結點變空,就嘗試向它的親戚結點借一個值,如果可能的話。
3. 刪除非leaf結點的值
這次讓我們刪除【樹九】的65,這是一個在非leaf結點上的值。
因爲當一個結點非leaf,它一定有子樹,因此也必定存在有“inorder successor”(這個東西真的不知道怎麼翻譯,這個跟BST的刪除一樣,找到一個最小的比65大的元素)
所以,我們可以找到69,然後跟65替換,最後的樹變成
<53>
/ /
/ /
<39> <69, 78>
/ / / | /
<27> <43> <60> <74> <93>
樹十
4. 刪除一個元素,其親戚結點無值可借
比如【樹十】我們要刪除74,它右邊的親戚結點只有一個值,無法借過來。還記得我之前曾經提過嗎,對,我們把父結點的值拉下來,就剛好如同我們新增時候所做的反過來。我們可以將69拖下來,並與60合併。最後樹變成
<53>
/ /
/ /
<39> <78>
/ / / /
<27> <43> <60,69> <93>
樹十一
請自己嘗試在【樹十一】上增加74,看看是否和【樹十】一樣
5. 刪除一個元素,其親戚結點無值可借,而且父結點只有一個元素,無法下推
比如,我們要從【樹十】中刪除43,如果我們像剛纔所說的那樣做,把父結點拖下來合併,樹就會變成這樣:
<53>
/ /
/ /
<> <69, 78>
/ / / | /
<27,39> <> <60> <74> <93>
樹十二 (not valid yet)
這明顯不合法。我們需要做的時候重複一次,再把父結點(39的父結點是53)拖下來合併,原來39的位置變成53了,53的位置又變空了。可是這次原來39的位置有親戚子樹<69,78>可以借。請參照情況2,把小值69推上去,因爲53去了左子樹那邊,需要找它的inordersuccessor(找到60),把它移到53的右子樹去。最終,完整的樹變成
<69>
/ /
/ /
<53> <78>
/ / / /
<27,39> <60> <74> <93>
樹十三
現在是一棵合法的2-3樹了,但是這次你不能用將43重新插入來檢驗是否正確,因爲如果重新插入43會變成
<69>
/ /
/ /
<39,53> <78>
/ | / / /
<27> <43> <60> <74> <93>
樹十四
6. 刪除一個元素,其親戚結點無法借值,其父結點只有一個元素,其父結點的親戚結點也無法借值……
這是最複雜的情況,其實也就是需要不斷的重複步驟,直到出現以下情況
1) 當一個結點有兩個元素
2) 當一個結點的親戚結點可以借值
3) 當已經到達root
當最後一種情況發生的時候,root的兩個子樹就合併,原來的root就被刪除,新的root出現,樹的高度減1
總結
刪除元素i的方法
1) 找到包含元素i的結點n
2) 如果n是一個leaf,先刪除i
如果n不是一個leaf,找到i的inorder successor,與i交換,然後刪除i
3) 如果這個時候n不是空,那麼刪除完成
如果n變空了,惡夢開始(^^),我們需要修復樹
a) 檢查n的親戚結點,如果有兩個元素,借一個過來(要注意借過來的元素和父結點元素的安排)比如你從左子樹借一個過來,那當然是借一個左子樹的large,然後原來的父結點元素拖下來;如果是從右子樹借一個,那當然是借small,還要同時把small變成large,再把small變成新父結點,原來的父結點放到空結點去。(如果覺得比較混亂請自己畫個圖就清楚了)
b) 如果沒有親戚結點有兩個元素,那就直接把父結點拖下來合併,讓父結點變成空。
c) 遞歸的往上重複,直到遇到在情況6中所述的三種情形。
代碼
首先我要給大家看的是 tree23裏面的delete方法
public void delete(int data)
{
tree23Node ptr, found;
int tmp;
found = locate(data); // 首先我找到該元素所在的結點
if (found==null)
{
return;
}
else
{
if (!found.isLeaf()) // if it is not a leaf
{
if (found.small==data && found.itemCount>1)
{
ptr = found.middle;
}
else
{
ptr = found.right;
}
// 這裏我是找 inorder successor
while(ptr.left!=null) ptr = ptr.left;
// 然後交換需要刪除的那個值
if (found.small==data)
{
tmp = found.large;
found.small = ptr.small;
ptr.large = tmp;
}
else
{
tmp = found.large;
found.large = ptr.smll;
ptr.large = tmp;
}
}
else // found is a leaf
{
ptr = found;
}
// 交換完畢了就刪除元素
if (ptr.large == data)
{
ptr.large = -1;
ptr.itemCount–;
}
else if (ptr.small==data)
{
ptr.small = ptr.large;
ptr.large = -1;
ptr.itemCount–;
}
// if it is empty, fix the root
// 這裏我用了跟新增一樣的技巧,調用fix方法修復,然後返回整棵樹
if (ptr.itemCount == 0)
root = ptr.fix();
} // end if found==null
} // end delete |
剩下的工作,就是在 tree23Node裏增加一個方法fix,不需要任何參數,該方法調用的時候this一定是一個空結點,然後就往上找就是。具體實現的步驟已經說得很清楚了,而方法也類似split那樣。
如果感興趣的朋友不妨自己試試,倒的確是一個鍛鍊的好機會。
後記
這篇東西純粹是介紹性質的文章,因爲2-3樹在國外是很受重視的,可惜似乎國內倒不太關注。我在google上找到有人問,卻沒有什麼比較詳細的中文資料。於是才決定寫這篇東西。寫得比較潦草,也沒有非常專業的口吻,不妥的地方還請指教。非常歡迎轉載,但是請不要自己修改後發表到盈利的報刊雜誌上,只限網上轉載,轉載的時候也請聲明出處,謝謝。