1. 程式人生 > >樹布局算法(翻譯)

樹布局算法(翻譯)

object cati 及其 邏輯 minimal 很多 分發 中介 輸出

繪制可展現的樹

比爾.米爾

當我需要為某個項目繪制一些樹時,我認為繪制整齊樹木會有一個經典而簡單的算法。我發現的更有趣得多:樹布局不僅是一個NP完全問題1,但樹繪圖算法背後有一個漫長而有趣的歷史。我將使用樹繪圖算法的歷史來逐一介紹核心概念,使用它們來構建一個完整的O(n)算法,以繪制一顆迷人的樹。

這裏有什麽問題?


圖1

給定一棵樹T,我們要做的就是繪制它,這樣觀眾就會發現它很有吸引力。本文中介紹的每種算法的目標都是為樹的每個節點分配一個(x,y)坐標,以便在算法運行後將其繪制到屏幕上或打印出來。

為了存儲樹繪圖算法的結果,我們將創建一個DrawTree數據結構來鏡像我們正在繪制的樹; 我們唯一假定的是每個樹節點都可以叠代其子節點。清單1中可以找到DrawTree的基本實現。

class DrawTree(object):

def __init __(self,tree,depth = 0):

self.x = -1

self.y = depth

self.tree = tree

self.children = [DrawTree(t,depth + 1)for t in tree]

隨著我們方法的復雜性增加,DrawTree的復雜性也會增加。現在,它只將-1指定給每個節點的x坐標,將節點的深度指定給其y坐標,並存儲對當前樹的根的引用。然後通過遞歸地為每個節點創建一個DrawTree來建立該節點的子節點列表。通過這種方式,我們構建了一個DrawTree,它會封裝要繪制的樹並將繪圖特定的信息添加到每個節點。

隨著我們在本文中貫徹執行更好的算法,我們將利用我們每個人的經驗幫助我們產生有助於我們構建下一個的原則。雖然生成一個“有吸引力”的樹圖是品味的問題,但這些原則將有助於指導我們改進程序的輸出。

一開始,有Knuth

我們將要制作的特定類型的繪圖是根部位於頂部,其子位於其下的位置等等。這種類型的圖表,以及由此產生的這類問題,主要歸功於Donald Knuth 2,我們將從中提出我們的前兩個原則:

原則1樹的邊不應該相互交叉。

原則2相同深度的所有節點應繪制在同一水平線上。這有助於明確樹的結構。


圖2

Knuth算法具有簡單和快速的優點,但它只適用於二叉樹,它可以產生一些相當畸形的圖形。它是一個簡單的

樹的遍歷,與被用作x變量,則在每個節點增加一個全局計數器。清單2中的代碼演示了這種技術。

i = 0

def knuth_layout(tree, depth):

if tree.left_child:

knuth_layout(tree.left_child, depth+1)

tree.x = i

tree.y = depth

i += 1

if tree.right_child:

knuth_layout(tree.right_child, depth+1)

從圖2可以看出,該算法生成的樹滿足原則1,但不是特別有吸引力。你也可以看到Knuth圖將會非常快速地擴展,因為即使樹可能顯著變窄,它們也不會重用x坐標。為了避免這種浪費空間,我們將介紹第三個原則:

原則3樹木應盡可能狹窄。

簡要的復習

在我們繼續研究一些更高級的算法之前,停止並同意我們將在本文中使用的術語可能是一個好主意。首先,我們將在描述數據節點之間的關系時使用家族樹的隱喻。一個節點可以有下面的孩子,左邊或右邊的兄弟姐妹和上面的父親

我們已經討論過樹遍歷了,我們也將討論前序遍歷和後序遍歷。很久以前,您可能在“數據結構”測試中看到了這三個術語,但除非您一直在玩樹最近,他們可能變得有點朦朧。

遍歷類型簡單地決定了我們在給定節點上執行處理時需要做什麽。中序遍歷,如上面的Knuth算法,只適用於二叉樹,並意味著我們處理左孩子,然後再處理當前節點,最後右子。後序遍歷意味著我們處理當前節點,那麽它的所有孩子,後序遍歷簡直是相反的。

最後,您可能已經在之前看到過大O符號的概念,以表示算法運行時間的大小順序。在這篇文章中,我們將快速放松地使用它作為一個簡單的工具來區分可接受的運行時間和不可接受的運行時間。如果已經在它的主回路的算法頻繁遍歷其中一個節點的所有孩子,我們要調用它O(n^2),或二次。除此之外,我們將稱之為O(n)或線性。如果您想了解更多詳細信息,本文結尾部分引用的論文更多地介紹了這些算法的運行時特性。

從底部起


圖3

Charles Wetherell和Alfred Shannon 3於1979年,Knuth在提出樹布局問題8年後,引入了一整套創新技術。首先,他們展示了如何生成滿足前三項原則的最小寬度樹。簡單地維護每一行上的下一個可用插槽,以後序遍歷樹,為該插槽分配一個節點,並增加插槽計數器,如清單3所示。

 

nexts = [0] * maximum_depth_of_tree

def minimum_ws(tree, depth=0):

tree.x = nexts[depth]

tree.y = depth

nexts[depth] += 1

for c in tree.children:

minimum_ws(tree, c)

盡管它符合我們所有的原則,但也許你會同意產出是醜陋的。即使是像圖3那樣的簡單例子,也很難快速確定節點之間的關系,整個結構似乎都是一起松散的。現在是我們介紹另一個有助於改善Knuth樹和最小寬度樹的原則的時候了:

原則4父母應該集中在孩子身上。


圖4

到目前為止,我們已經能夠用非常簡單的算法來繪制樹,因為我們並不需要考慮本地情境; 我們依靠全局計數器來避免節點彼此重疊。為了滿足父母應該以孩子為中心的原則,我們需要考慮每個節點的本地情境,因此需要一些新的策略。

Wetherell和Shannon介紹的第一個策略是從底部開始構建樹,然後對樹進行後序遍歷,而不是像清單2那樣從頂部開始,或者像清單3那樣從中間開始。一旦你看到這樣樹,居中父母是一個簡單的操作:簡單地把它的孩子的x坐標分成兩半。

但是,我們必須記住,在構建時要留意樹的左側。圖4顯示了樹的右側已經被推出到右側以容納左側的情況。為了改善這種分離,Wetherell和Shannon維護了列表2中引入的下一個可用點的陣列,但是如果將父項居中會導致樹的右側與左側重疊,則僅使用下一個可用點。

Mods和Rockers

在我們開始查看更多代碼之前,讓我們仔細看看我們自下而上構建樹的後果。如果它是一片葉子,我們會給每個節點下一個可用的x坐標,如果它是一個分支,則將它放在子節點上方。但是,如果將分支居中會導致分支與樹的另一部分發生沖突,我們需要將分支移到正確的位置以避免沖突。

當我們將分支移到右邊時,我們必須移動它的所有子項,否則我們將失去我們一直努力維護的中心父節點。很容易想出一個簡單的函數來將分支及其子樹右移一些空間:

def move_right(branch, n):

branch.x += n

for c in branch.children:

move_right(c, n)

它有效,但提出了一個問題。如果我們使用這個函數向右移動一個子樹,我們將在遞歸內部(放置節點)進行遞歸(移動樹),這意味著我們將有一個低效率的算法,它可能會在時間O (N ^ 2)。

為了解決這個問題,我們會給每個節點一個額外的成員mod。當我們到達需要用n空格向右移動的分支時,我們將添加n到其x坐標和其mod值,並且愉快地繼續放置算法。因為我們正在從下往上移動,所以我們不必擔心我們的樹木的底部會發生沖突(我們已經表明它們不是),我們將等到稍後將它們移動到右邊。

一旦第一次樹遍歷發生,我們運行第二次樹遍歷將分支移動到右側,需要將其移到右側。由於我們將訪問每個節點一次並僅對其執行算術運算,因此我們可以肯定,這個遍歷將是O(n),就像第一個一樣,並且它們一起也將是O(n)。

清單5中的代碼演示了父節點的居中和使用mod值來提高代碼的效率。

from collections import defaultdict

class DrawTree(object):

def __init__(self, tree, depth=0):

self.x = -1

self.y = depth

self.tree = tree

self.children = [DrawTree(t, depth+1) for t in tree]

self.mod = 0

def layout(tree):

setup(tree)

addmods(tree)

return tree

def setup(tree, depth=0, nexts=None, offset=None):

if nexts is None: nexts = defaultdict(lambda: 0)

if offset is None: offset = defaultdict(lambda: 0)

for c in tree.children:

setup(c, depth+1, nexts, offset)

tree.y = depth

if not len(tree.children):

place = nexts[depth]

tree.x = place

elif len(tree.children) == 1:

place = tree.children[0].x - 1

else:

s = (tree.children[0].x + tree.children[1].x)

place = s / 2

offset[depth] = max(offset[depth], nexts[depth]-place)

if len(tree.children):

tree.x = place + offset[depth]

nexts[depth] += 2

tree.mod = offset[depth]

def addmods(tree, modsum=0):

tree.x = tree.x + modsum

modsum += tree.offset

for t in tree.children:

addmods(t, modsum)

樹作為塊

盡管在很多情況下它確實產生了很好的結果,但清單5可以生成一些破損的樹,比如圖5中的樹(可悲的是,已經在時間的流逝中消失了)。解釋Wetherell-Shannon算法產生的樹的另一個困難在於,當放置在樹中的不同點處時,相同的樹結構可以被不同地繪制。為了避免這種情況,我們會從Edward Reingold和John Tilford的論文中偷取原理4

原則5無論樹怎樣都應該繪制成同一棵子樹。

盡管這可能會擴大我們的圖紙,但這一原則將有助於使它們傳達更多信息。這也有助於簡化樹的自底向上遍歷,因為它的一個後果是,一旦我們找出了子樹的x坐標,我們只需要將它作為一個單元向左或向右移動即可。

這是清單6中實現的算法概述:

?對樹進行後序遍歷
?如果節點是葉子,則給它一個0的x坐標
?否則,將其右邊的子樹盡可能靠近左邊而不發生沖突
    ?使用與先前的算法在O(n)時間內移動樹
?將節點放在其子節點的中間位置
?執行樹的第二步,將
  累加的mod值添加到x坐標

這個算法很簡單,但要執行它,我們需要引入一些復雜性。

輪廓


圖6

樹的輪廓是樹的一側的最大或最小坐標的列表。在圖6中,有一棵左樹和一棵右樹,每個節點的x坐標重疊。如果我們沿著左邊的樹的左邊追蹤每個層的最小x坐標,我們就得到[1,1,0],我們稱之為樹的左邊輪廓。如果我們沿著右邊走,從每一層取最右邊的x坐標,我們得到[1,1,2],這是樹的右邊輪廓

為了找到右邊樹的左邊輪廓,我們再次取每層的最左邊節點的x坐標,給我們[1,0,1]。這一次,輪廓有一個有趣的特性,並非所有節點都以父子關系連接; 第二層的0不是第三層的1的父層。

如果我們按照清單6加入這兩棵樹,我們可以找到左樹的右輪廓和右樹的左輪廓。然後我們可以很容易地找到我們需要的最小量,將右邊的樹推向右邊,這樣它就不會與左邊的樹重疊。清單7給出了一個簡單的方法。

from operator import lt, gt

def push_right(left, right):

wl = contour(left, lt)

wr = contour(right, gt)

return max(x-y for x,y in zip(wl, wr)) + 1

def contour(tree, comp, level=0, cont=None):

if not cont:

cont = [tree.x]

elif len(cont) < level+1:

cont.append(tree.x)

elif comp(cont[level], tree.x):

cont[level] = tree.x

for child in tree.children:

contour(child, comp, level+1, cont)

return cont

如果我們在圖6的樹上運行清單7中的程序push_right(),我們將得到[1,1,2]作為左樹的右輪廓,[1,0,1]作為右樹的左輪廓。然後我們比較這些列表以找到它們之間的最大空間,並為填充添加一個空格。在圖6的情況下,將右側樹向右推2個空格將防止它與左側樹重疊。

新建線程

使用清單7中的代碼,我們找到了正確的值以表明我們構建正確的樹,但為此我們必須掃描兩個子樹中的每個節點,以獲得所需的輪廓。由於它很可能是O(n ^ 2)操作,因此Reingold和Tilford引入了一個混淆稱為線程的概念,這根本不像用於並行執行的線程。


圖7

線程是一種通過在輪廓上的節點之間創建鏈接(如果其中一個不是另一個的子節點)來減少掃描其輪廓的子樹所花費的時間的方法。在圖7中,虛線表示線程,而實線表示父子關系。

我們還可以利用這樣一個事實,即如果一棵樹比另一棵樹深,我們只需要下降到更短的樹。任何比這更深的東西都不會影響兩棵樹之間必要的分離,因為它們之間不會有任何沖突。

使用線程並且只需要遍歷我們需要的深度,我們就可以得到樹的輪廓,並使用清單8中的過程以線性時間設置線程。

def nextright(tree):

if tree.thread: return tree.thread

if tree.children: return tree.children[-1]

else: return None

def nextleft(tree):

if tree.thread: return tree.thread

if tree.children: return tree.children[0]

else: return None

def contour(left, right, max_offset=0, left_outer=None, right_outer=None):

if not left_outer:

left_outer = left

if not right_outer:

right_outer = right

if left.x - right.x > max_offset:

max_offset = left.x - right.x

lo = nextleft(left)

li = nextright(left)

ri = nextleft(right)

ro = nextright(right)

if li and ri:

return contour(li, ri, max_offset, lo, ro)

return max_offset

很容易看到,該過程僅訪問正在掃描的子樹的每個級別上的兩個節點。這篇論文有一個很好的證據表明這是在線性時間內發生的; 如果你有興趣,我建議你閱讀它。

把它放在一起

清單8給出的輪廓過程整潔快速,但它不適用於我們之前討論的mod技術,因為節點的實際x值是節點的x值加上從本身到根的路徑上所有修改的總和。為了處理這種情況,我們需要給輪廓算法增加一些復雜度。

我們需要做的第一件事是保留兩個額外的變量,即左子樹上的修飾符的總和和右子樹上的修飾符的總和。這些和是計算輪廓上每個節點的實際位置所必需的,這樣我們可以檢查它是否與相反一側的節點發生沖突。參見清單9。

def contour(left, right, max_offset=None, loffset=0, roffset=0, left_outer=None, right_outer=None):

delta = left.x + loffset - (right.x + roffset)

if not max_offset or delta > max_offset:

max_offset = delta

if not left_outer:

left_outer = left

if not right_outer:

right_outer = right

lo = nextleft(left_outer)

li = nextright(left)

ri = nextleft(right)

ro = nextright(right_outer)

if li and ri:

loffset += left.mod

roffset += right.mod

return contour(li, ri, max_offset,

loffset, roffset, lo, ro)

return (li, ri, max_offset, loffset, roffset, left_outer, right_outer)

我們需要做的另一件事是在退出時返回函數的當前狀態,以便我們可以在線程節點上設置適當的偏移量。掌握這些信息後,我們準備查看使用清單8中的代碼的函數,將兩棵樹盡可能緊密地放在一起:

def fix_subtrees(left, right):

li, ri, diff, loffset, roffset, lo, ro \

= contour(left, right)

diff += 1

diff += (right.x + diff + left.x) % 2

right.mod = diff

right.x += diff

if right.children:

roffset += diff

if ri and not li:

lo.thread = ri

lo.mod = roffset - loffset

elif li and not ri:

ro.thread = li

ro.mod = loffset - roffset

return (left.x + right.x) / 2

在我們運行輪廓過程之後,我們將左右樹之間的最大差異加1,以使它們不會相互沖突,如果它們之間的中點是奇數,則再添加1。這讓我們保留了一個便利的測試屬性 - 所有節點都具有整數x坐標,而且不會降低精度。

然後我們將右邊的樹移動到右邊。請記住,我們都將diff添加到x坐標並將其保存到mod值的原因是mod值僅適用於當前節點下面的節點。如果右子樹有多個節點,我們將diff添加到roffset中,因為右節點的所有子節點都將移動到右邊。

如果樹的左側比右側更深,反之亦然,我們需要設置一個線程。我們只需檢查一側的節點指針是否比另一側的節點指針前進得更遠,如果已經存在,則將線程從較淺的樹的外部設置到較深的樹的外部。

為了正確處理我們之前談到的mod值,我們需要在線程節點上設置一個特殊的mod值。由於我們已經更新了右側偏移值以反映右側樹的向右移動,因此我們需要在此處執行的操作是將線程節點的mod值設置為更深樹的偏移量與其自身之間的差值。

現在我們已經有了代碼來查找樹的輪廓並盡可能地將兩棵樹放在一起,我們可以輕松實現上述算法。我提供其余的代碼而沒有評論:

def layout(tree):

return addmods(setup(dt))

def addmods(tree, mod=0):

tree.x += mod

for c in tree.children:

addmods(c, mod+tree.mod)

return tree

def setup(tree, depth=0):

if len(tree.children) == 0:

tree.x = 0

tree.y = depth

return tree

if len(tree.children) == 1:

tree.x = setup(tree.children[0], depth+1).x

return tree

left = setup(tree.children[0], depth+1)

right = setup(tree.children[1], depth+1)

tree.x = fix_subtrees(left, right)

return tree

對N叉樹的擴展

現在我們終於得到了一個繪制二叉樹的算法,它滿足了我們的原則,在一般情況下看起來很好,並且在線性時間內運行,所以考慮如何將它擴展到具有任意數量子級的樹上是很自然的。如果你跟著我走了這麽遠,你可能認為我們應該采用我們剛剛定義的美妙算法,並將其應用於節點的所有子節點。

先前算法在n元樹上工作的擴展可能如下所示:

  • 進行樹的後序遍歷
  • 如果節點是葉子,則給它一個0的x坐標
  • 否則,對於其每個孩子,盡可能將孩子盡可能靠近其左兄弟姐妹
  • 將父節點放在其最左邊和最右邊的孩子之間

該算法工作,速度快,但存在一個簡單的問題。它將節點的所有子樹放置在盡可能遠的地方。如果右邊的一個節點與左邊的一個節點發生沖突,那麽它們之間的樹將全部填充到右邊,如圖7所示。讓我們采用樹圖的最後一個原則來解決這個問題:

原則6父節點的子節點應均勻分布。


圖8

為了對稱地繪制一個n元樹,並且很快,我們將需要迄今為止開發的所有技巧加上一些新的技巧。感謝Christoph Buchheim等人5最近發表的一篇論文,我們已經掌握了所有的工具,並且仍然能夠以線性時間繪制我們的樹。

要修改上面的算法以符合原則6,我們需要一種方法來隔離兩棵相互沖突的大樹之間的樹。最簡單的方法是,每當兩棵樹發生沖突時,將可用空間除以樹的數量,然後移動每棵樹使其與其兄弟姐妹分開。例如,在圖7中,右邊和左邊的大樹之間有一段距離n,它們之間有三棵樹。如果我們簡單地將中間的第一棵樹n/3與左邊的樹分開,下一個n/3遠離那棵樹,等等,我們就會有一棵滿足原則6的樹。

到目前為止,我們已經看到了這篇文章中的一個簡單的算法,但我們發現它並不合適,而這一次也不例外。如果我們必須改變每兩棵相互沖突的樹之間的所有樹,那麽我們冒著在我們的算法中引入O(n ^ 2)操作的風險。

對於這個問題的解決方法類似於我們前面介紹的移位問題的修復方法mod。每次發生沖突時,我們都不需要將中間的每個子樹都移動到中間,我們將保存中間需要移動樹的值,然後在放置節點的所有子節點後應用這些移位。

為了找出我們想要移動中間節點的正確值,我們需要能夠找到沖突的兩個節點之間的樹數。當我們只有兩棵樹時,顯然發生的任何沖突都是在左邊和右邊的樹之間。當可能有多少樹時,找出哪棵樹導致沖突成為一個挑戰。

為了迎接這個挑戰,我們將引入一個default_ancestor變量,並將另一個成員添加到我們稱之為的樹形數據結構中ancestor。祖先節點或者指向它自己或者指向它所屬的樹的根。當我們需要找到一個節點屬於哪棵樹時,我們將使用祖先成員(如果已設置),但是會回落到指向的樹上default_ancestor。

當我們放置節點的第一個子樹時,我們只需將default_ancestor設置為指向該子樹,並假定由下一個樹造成的任何沖突都與第一個樹相沖突。在我們放置第二個子樹之後,我們區分兩種情況。如果第二個子樹的深度小於第一個子樹的深度,我們遍歷它的右邊界,將祖先成員設置為等於第二棵樹的根。否則,第二棵樹比第一棵樹大,這意味著與下一棵樹的任何沖突都與第二棵樹放置在一起,因此我們只需將default_ancestor設置為指向它即可。

所以,不用多說,如Buchheim提出的用於布置富有吸引力的樹的O(n)算法的python實現在清單12中。

class DrawTree(object):

def __init__(self, tree, parent=None, depth=0, number=1):

self.x = -1.

self.y = depth

self.tree = tree

self.children = [DrawTree(c, self, depth+1, i+1)

for i, c

in enumerate(tree.children)]

self.parent = parent

self.thread = None

self.offset = 0

self.ancestor = self

self.change = self.shift = 0

self._lmost_sibling = None

#this is the number of the node in its group of siblings 1..n

self.number = number

def left_brother(self):

n = None

if self.parent:

for node in self.parent.children:

if node == self: return n

else: n = node

return n

def get_lmost_sibling(self):

if not self._lmost_sibling and self.parent and self != \

self.parent.children[0]:

self._lmost_sibling = self.parent.children[0]

return self._lmost_sibling

leftmost_sibling = property(get_lmost_sibling)

def buchheim(tree):

dt = firstwalk(tree)

second_walk(dt)

return dt

def firstwalk(v, distance=1.):

if len(v.children) == 0:

if v.leftmost_sibling:

v.x = v.left_brother().x + distance

else:

v.x = 0.

else:

default_ancestor = v.children[0]

for w in v.children:

firstwalk(w)

default_ancestor = apportion(w, default_ancestor,

distance)

execute_shifts(v)

midpoint = (v.children[0].x + v.children[-1].x) / 2

ell = v.children[0]

arr = v.children[-1]

w = v.left_brother()

if w:

v.x = w.x + distance

v.mod = v.x - midpoint

else:

v.x = midpoint

return v

def apportion(v, default_ancestor, distance):

w = v.left_brother()

if w is not None:

#in buchheim notation:

#i == inner; o == outer; r == right; l == left;

vir = vor = v

vil = w

vol = v.leftmost_sibling

sir = sor = v.mod

sil = vil.mod

sol = vol.mod

while vil.right() and vir.left():

vil = vil.right()

vir = vir.left()

vol = vol.left()

vor = vor.right()

vor.ancestor = v

shift = (vil.x + sil) - (vir.x + sir) + distance

if shift > 0:

a = ancestor(vil, v, default_ancestor)

move_subtree(a, v, shift)

sir = sir + shift

sor = sor + shift

sil += vil.mod

sir += vir.mod

sol += vol.mod

sor += vor.mod

if vil.right() and not vor.right():

vor.thread = vil.right()

vor.mod += sil - sor

else:

if vir.left() and not vol.left():

vol.thread = vir.left()

vol.mod += sir - sol

default_ancestor = v

return default_ancestor

def move_subtree(wl, wr, shift):

subtrees = wr.number - wl.number

wr.change -= shift / subtrees

wr.shift += shift

wl.change += shift / subtrees

wr.x += shift

wr.mod += shift

def execute_shifts(v):

shift = change = 0

for w in v.children[::-1]:

w.x += shift

w.mod += shift

change += w.change

shift += w.shift + change

def ancestor(vil, v, default_ancestor):

if vil.ancestor in v.parent.children:

return vil.ancestor

else:

return default_ancestor

def second_walk(v, m=0, depth=0):

v.x += m

v.y = depth

for w in v.children:

second_walk(w, m + v.mod, depth+1, min)

結論

我在本文中略過了一些內容,僅僅是因為我認為嘗試並向呈現的最終算法呈現合乎邏輯的進展比使用純代碼重載文章更重要。如果您想了解更多詳細信息,或者查看我在各種代碼清單中使用的樹形數據結構,可以訪問http://github.com/llimllib/pymag-trees/下載每種算法的源代碼,一些基本測試以及用於生成本文圖形的代碼。

參考

1 K. Marriott, NP-Completeness of Minimal Width Unordered Tree Layout, Journal of Graph Algorithms and Applications, vol. 8, no. 3, pp. 295-312 (2004). http://www.emis.de/journals/JGAA/accepted/2004/MarriottStuckey2004.8.3.pdf

2 D. E. Knuth, Optimum binary search trees, Acta Informatica 1 (1971)

3 C. Wetherell, A. Shannon, Tidy Drawings of Trees, IEEE Transactions on Software Engineering. Volume 5, Issue 5

4 E. M. Reingold, J. S Tilford, Tidier Drawings of Trees, IEEE Transactions on Software Engineering. Volume 7, Issue 2

5 C. Buchheim, M. J Unger, and S. Leipert. Improving Walker‘s algorithm to run in linear time. In Proc. Graph Drawing (GD), 2002. http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.16.8757

樹布局算法(翻譯)