您的位置:首页 > 编程语言

《编程之美: 求二叉树中节点的最大距离》的另一个解法

2011-10-11 19:28 411 查看
12
0

(请您对文章做出评价)

Spiga

Posts-23,Articles-0,Comments-1089
Cnblogs
Dashboard
Login

Home
Contact
Gallery
RSS

Milo的游戏开发

《编程之美:求二叉树中节点的最大距离》的另一个解法

2010-02-2503:32byMiloYip,7408visits,收藏,
编辑

昨天花了一个晚上为《编程之美》,在豆瓣写了一篇书评《迟来的书评和感想──给喜爱编程的朋友》。书评就不转载到这里了,取而代之,在这里介绍书里其中一条问题的另一个解法。这个解法比较简短易读及降低了空间复杂度,或者可以说觉得比较「美」吧。

问题定义

如果我们把二叉树看成一个图,父子节点之间的连线看成是双向的,我们姑且定义"距离"为两节点之间边的个数。写一个程序求一棵二叉树中相距最远的两个节点之间的距离。



书上的解法

书中对这个问题的分析是很清楚的,我尝试用自己的方式简短覆述。

计算一个二叉树的最大距离有两个情况:

情况A:路径经过左子树的最深节点,通过根节点,再到右子树的最深节点。
情况B:路径不穿过根节点,而是左子树或右子树的最大距离路径,取其大者。

只需要计算这两个情况的路径距离,并取其大者,就是该二叉树的最大距离。



我也想不到更好的分析方法。

但接着,原文的实现就不如上面的清楚(源码可从这里下载):

viewsourceprint?

01
//数据结构定义
02
struct
NODE
03
{
04
NODE*pLeft;
//左子树
05
NODE*pRight;
//右子树
06
int

nMaxLeft;
//左子树中的最长距离
07
int

nMaxRight;
//右子树中的最长距离
08
char

chValue;
//该节点的值
09
};
10
11
int

nMaxLen=0;
12
13
//寻找树中最长的两段距离
14
void

FindMaxLen(NODE*pRoot)
15
{
16
//遍历到叶子节点,返回
17
if
(pRoot==NULL)
18
{

19
return
;
20
}

21
22
//如果左子树为空,那么该节点的左边最长距离为0
23
if
(pRoot->pLeft==NULL)
24
{

25
pRoot->nMaxLeft=0;
26
}

27
28
//如果右子树为空,那么该节点的右边最长距离为0
29
if
(pRoot->pRight==NULL)
30
{

31
pRoot->nMaxRight=0;
32
}

33
34
//如果左子树不为空,递归寻找左子树最长距离
35
if
(pRoot->pLeft!=NULL)
36
{

37
FindMaxLen(pRoot->pLeft);
38
}

39
40
//如果右子树不为空,递归寻找右子树最长距离
41
if
(pRoot->pRight!=NULL)
42
{

43
FindMaxLen(pRoot->pRight);
44
}

45
46
//计算左子树最长节点距离
47
if
(pRoot->pLeft!=NULL)
48
{

49
int

nTempMax=0;
50
if
(pRoot->pLeft->nMaxLeft>pRoot->pLeft->nMaxRight)
51
{
52
nTempMax=pRoot->pLeft->nMaxLeft;
53
}
54
else
55
{
56
nTempMax=pRoot->pLeft->nMaxRight;
57
}
58
pRoot->nMaxLeft=nTempMax+1;
59
}

60
61
//计算右子树最长节点距离
62
if
(pRoot->pRight!=NULL)
63
{

64
int

nTempMax=0;
65
if
(pRoot->pRight->nMaxLeft>pRoot->pRight->nMaxRight)
66
{
67
nTempMax=pRoot->pRight->nMaxLeft;
68
}
69
else
70
{
71
nTempMax=pRoot->pRight->nMaxRight;
72
}
73
pRoot->nMaxRight=nTempMax+1;
74
}

75
76
//更新最长距离
77
if
(pRoot->nMaxLeft+pRoot->nMaxRight>nMaxLen)
78
{

79
nMaxLen=pRoot->nMaxLeft+pRoot->nMaxRight;
80
}

81
}
这段代码有几个缺点:

算法加入了侵入式(intrusive)的资料nMaxLeft,nMaxRight
使用了全局变量nMaxLen。每次使用要额外初始化。而且就算是不同的独立资料,也不能在多个线程使用这个函数
逻辑比较复杂,也有许多NULL相关的条件测试。

我的尝试

我认为这个问题的核心是,情况A及B需要不同的信息:A需要子树的最大深度,B需要子树的最大距离。只要函数能在一个节点同时计算及传回这两个信息,代码就可以很简单:

viewsourceprint?

01
#include<iostream>
02
03
using
namespace
std;
04
05
struct
NODE
06
{
07
NODE*pLeft;
08
NODE*pRight;
09
};
10
11
struct

RESULT
12
{
13
int

nMaxDistance;
14
int

nMaxDepth;
15
};
16
17
RESULTGetMaximumDistance(NODE*root)
18
{
19
if

(!root)
20
{

21
RESULTempty={0,-1};
//trick:nMaxDepthis-1andthencallerwillplus1tobalanceitaszero.
22
return

empty;
23
}

24
25
RESULTlhs=GetMaximumDistance(root->pLeft);
26
RESULTrhs=GetMaximumDistance(root->pRight);
27
28
RESULTresult;
29
result.nMaxDepth=max(lhs.nMaxDepth+1,rhs.nMaxDepth+1);
30
result.nMaxDistance=max(max(lhs.nMaxDistance,rhs.nMaxDistance),lhs.nMaxDepth+rhs.nMaxDepth+2);
31
return

result;
32
}
计算result的代码很清楚;nMaxDepth就是左子树和右子树的深度加1;nMaxDistance则取A和B情况的最大值。

为了减少NULL的条件测试,进入函数时,如果节点为NULL,会传回一个empty变量。比较奇怪的是empty.nMaxDepth=-1,目的是让调用方+1后,把当前的不存在的(NULL)子树当成最大深度为0。

除了提高了可读性,这个解法的另一个优点是减少了O(节点数目)大小的侵入式资料,而改为使用O(树的最大深度)大小的栈空间。这个设计使函数完全没有副作用(sideeffect)。

测试代码

以下也提供测试代码给读者参考(页数是根据第7次印刷,节点是由上至下、左至右编号):

viewsourceprint?

01
void

Link(NODE*nodes,
int

parent,
int
left,
int
right)

02
{
03
if

(left!=-1)
04
nodes[parent].pLeft=&nodes[left];
05
06
if

(right!=-1)
07
nodes[parent].pRight=&nodes[right];
08
}
09
10
void

main()
11
{
12
//P.241Graph3-12
13
NODEtest1[9]={0};
14
Link(test1,0,1,2);
15
Link(test1,1,3,4);
16
Link(test1,2,5,6);
17
Link(test1,3,7,-1);
18
Link(test1,5,-1,8);
19
cout<<
"test1:"
<<GetMaximumDistance(&test1[0]).nMaxDistance<<endl;
20
21
//P.242Graph3-13left
22
NODEtest2[4]={0};
23
Link(test2,0,1,2);
24
Link(test2,1,3,-1);
25
cout<<
"test2:"
<<GetMaximumDistance(&test2[0]).nMaxDistance<<endl;
26
27
//P.242Graph3-13right
28
NODEtest3[9]={0};
29
Link(test3,0,-1,1);
30
Link(test3,1,2,3);
31
Link(test3,2,4,-1);
32
Link(test3,3,5,6);
33
Link(test3,4,7,-1);
34
Link(test3,5,-1,8);
35
cout<<
"test3:"
<<GetMaximumDistance(&test3[0]).nMaxDistance<<endl;
36
37
//P.242Graph3-14
38
//SameasGraph3-2,nottest
39
40
//P.243Graph3-15
41
NODEtest4[9]={0};
42
Link(test4,0,1,2);
43
Link(test4,1,3,4);
44
Link(test4,3,5,6);
45
Link(test4,5,7,-1);
46
Link(test4,6,-1,8);
47
cout<<
"test4:"
<<GetMaximumDistance(&test4[0]).nMaxDistance<<endl;
48
}
你想到更好的解法吗?

分类:
数据结构和算法

绿色通道:好文要顶关注我收藏该文与我联系






MiloYip

关注-31

粉丝-486

荣誉:推荐博客
+加关注

«
博主前一篇:混合语言的游戏开发系统架构

»
博主后一篇:解构Unity的腳本物件模型



Categories:数据结构和算法

Addyourcomment

21条回复

1929379

#1楼

yeka2010-02-2506:30

Milo又熬夜啦.......
 回复 引用 查看 

#2楼

陈硕2010-02-2508:56

第19~21行有线程安全问题:

staticconstRESULTempty={0,-1};//trick:nMaxDepthis-1andthencallerwillplus1tobalanceitaszero.

if(!root)

returnempty;
建议改为:

if(!root){RESULTempty={0,-1};//trick:nMaxDepthis-1andthencallerwillplus1tobalanceitaszero.

returnempty;
//trustcompiler,PODdatawillbeoptimizedwell.

}因为按标准,functionstaticvariable只在函数第一次调用时初始化,这个的初始化只有在最新的编译器里才是线程安全的。

在旧的编译器(GCC3及以前)上,原来的写法可能会读到partialinitialized'empty'变量,如果两个线程同时(首次)调用GetMaximumDistance的话。
 回复 引用 查看 

#3楼

秋醒半梦时[未注册用户]2010-02-2509:12

这不就是求树的直径的问题吗?

树的直径最简单的解法(无论是几叉):

从任意一点i一次BFS找到树中与他距离最远的点j,从j再一次BFS找到树中里j最远的点k,那么D[j][k](j与k的距离)即为答案。

稍微好理解的方法:树形动态规划
 回复 引用 

#4楼

Dbger2010-02-2510:07

@陈硕

如果非要考虑多线程安全,我倾向于用“全局变量”来表示这些常用的常量,就和向量,矩阵类中一些单元向量,单元矩阵等。
 回复 引用 查看 

#5楼

JeffreyZhao2010-02-2510:46

我觉得直接把递归语意翻译过来最直接和清晰吧:

viewsourceprint?

01
type

BinaryTree=
02
|Node
of

BinaryTree*BinaryTree
03
|Empty
04
05
let
rec
height(tree:BinaryTree)=
06
match

tree
with
07
|Empty->0
08
|Node(l,r)->1+
max
(heightl)(heightr)
09
10
let
rec
calculate(tree:BinaryTree)=
11
match

tree
with
12
|Empty->0
13
|Node(l,r)->
14
(heightl)+(heightr)
15
|>
max
(calculatel)
16
|>
max
(calculater)
这里我用了F#,不过C#,C++其实也是一回事情吧。
 回复 引用 查看 

#6楼

ToddWei2010-02-2512:11

@秋醒半梦时

进行两次BFS:先从树根A出发进行广度优先搜索(BFS),找到最远的结点B,然后再从结点B出BFS,找到离B最远的结点C,BC就是最大距离。

下面是正确性证明

假设存在结点X和Y,它们的距离是所有结点中最大的;分两种情况讨论:

1.若路径XY与路径AB有交点O,

...A

...|

X-O--Y

...|

...B

由于|OB|>=|OX|且|OB|>=|OY|,所以,|BX|>=|XY|,|BY|>=|XY|。即从B出发可以构造出最长路径。

2.若路径XY与路径AB无交点,

A...BX...Y

A是树根,XY与B分属不同的子树,假设XY的最近祖先为O,由于

|AB|>=|AO|+|AX|,所以|BY|=|AB|+|AO|+|OY|>|XY|。即从B出发构造出长于XY的路径,与假设XY是最长路径矛盾。
 回复 引用 查看 

#7楼[楼主]

MiloYip2010-02-2512:19

@Dbger

@陈硕

我覺得兩個方法都可以解決潛在的多線程問題。我現在先相信compiler,改用了陳碩的寫法。

從另一個角度看這個問題,localstaticvariable是會做成sideeffect,所以thread-safe會不成立。
 回复 引用 查看 

#8楼[楼主]

MiloYip2010-02-2512:40

@JeffreyZhao

我未學過任何一個functionalprogramming語言。希望趙大能指正不對的地方。

用FP的確可以增加可讀性,同時能減少錯誤的機會。

FP能對purefunction用自動的cacheoptimization,這是優點也是缺點。如果沒有這優化,在你提供的代碼中,height的調用次數估計是O(n^2);而有了這優化,就需要O(n)的空間去儲存n個height()的運算結果。而這優化我估計應該需要做tablelookup,帶來額外overhead。

我的嘗試中,並不需要O(n)的額外空間,而且仍維持每節點只遍歷一次。

又反過來說,在效能上,FP的好處是可以自動做並行,用procedural語言手動做這個就會顯得複雜。
 回复 引用 查看 

#9楼[楼主]

MiloYip2010-02-2513:09

@ToddWei

@秋醒半梦时

多謝你們的回應,我方知道這個「距離」應該是叫「直徑」(TreeDiameter)。

這該我找到一點參考文章:

http://www.cs.duke.edu/courses/spring00/cps100/assign/trees/diameter.html

http://www.cs.cmu.edu/afs/cs.cmu.edu/project/phrensy/pub/papers/LeisersonM88/node17.html

發現前一篇文章基本上和Jeffrey的嘗試一樣,但用proceduralprogramming會有O(N^2)的height()調用。我覺得我寫的邊界條件(那個trick)可能不需要,今晚回家試試。

第二篇談到的幾個詞彙我都不太認識,可能要再多看一些參考。也想請教,用BFS的方法會比現時的方法簡單或高效麼?還是現時的方法實際上有錯誤?
 回复 引用 查看 

#10楼

ToddWei2010-02-2513:20

@MiloYip

BFS是O(N)的,所以复杂度更低。特别是基于BFS的方法不局限于2叉树,而前面递归方法在多叉树情况下复杂度会更高。
 回复 引用 查看 

#11楼

JeffreyZhao2010-02-2513:23

@MiloYip

其实你的算法还是用了O(h)的空间啦,h是高度,(非尾)递归算法嘛,栈空间是省不了的。

的确这里height会反复调用,所以如果必要的话,还是要做memorization的。

作了momorization以后,时间和空间“复杂度”和你的过程式算法是一致的了。
 回复 引用 查看 

#12楼[楼主]

MiloYip2010-02-2513:31

@JeffreyZhao

引用
JeffreyZhao:

@MiloYip

其实你的算法还是用了O(h)的空间啦,h是高度,(非尾)递归算法嘛,栈空间是省不了的。

的确这里height会反复调用,所以如果必要的话,还是要做memorization的。

作了momorization以后,时间和空间“复杂度”和你的过程式算法是一致的了。

本文也提及,我的嘗試用了O(h)的棧空間代替原文的O(n)intrusivedata,而你寫的height函數的memorization空間是O(n)。因為h<=n,O(h)應該是比O(n)好吧。
 回复 引用 查看 

#13楼[楼主]

MiloYip2010-02-2513:36


引用ToddWei:

@MiloYip

BFS是O(N)的,所以复杂度更低。特别是基于BFS的方法不局限于2叉树,而前面递归方法在多叉树情况下复杂度会更高。

我的嘗試也是O(N),而且只需遍歷一次。跟據你的描述,BFS要做兩次,而且要加入parent?不過對於一般的多叉樹,可能BFS的方法是最好的方法。
 回复 引用 查看 

#14楼

ToddWei2010-02-2515:09

@MiloYip

哦,是的,你的递归也是O(N),开始分析错了。

树哪个结点作为parent没关系,任选即可。图论里面对树的一种定义方式是:具有n个结点和n+1条边的连通图。
 回复 引用 查看 

#15楼

郑晖2010-02-2516:06

@MiloYip

>>我方知道這個「距離」應該是叫「直徑」(TreeDiameter)。

的确是“直径”——在数学中直径的定义是:一个距离空间中任意两点间距离的上确界(supremum)。
 回复 引用 查看 

#16楼

秋醒半梦时[未注册用户]2010-02-2517:03


引用ToddWei:

@MiloYip

哦,是的,你的递归也是O(N),开始分析错了。

树哪个结点作为parent没关系,任选即可。图论里面对树的一种定义方式是:具有n个结点和n+1条边的连通图。

我所了解的树的定义是:一个无环的无向图
 回复 引用 

#17楼[楼主]

MiloYip2010-02-2517:04

@郑晖

引用
郑晖:

@MiloYip

>>我方知道這個「距離」應該是叫「直徑」(TreeDiameter)。

的确是“直径”——在数学中直径的定义是:一个距离空间中任意两点间距离的上确界(supremum)。

謝謝鄭老師的數學指導。在網上找到了關於這個的定義:

http://mathworld.wolfram.com/GeneralizedDiameter.html

http://mathworld.wolfram.com/Supremum.html

 回复 引用 查看 

#18楼

郑晖2010-02-2517:17


引用MiloYip:

引用郑晖:

@MiloYip

>>我方知道這個「距離」應該是叫「直徑」(TreeDiameter)。

的确是“直径”——在数学中直径的定义是:一个距离空间中任意两点间距离的上确界(supremum)。

謝謝鄭老師的數學指導。在網上找到了關於這個的定義:

http://mathworld.wolfram.com/GeneralizedDiameter.html

http://mathworld.wolfram.com/Supremum.html

http://mathworld.wolfram.com/GeneralizedDiameter.html

上面对的直径定义尚不足够general,它只提到了欧氏空间(EuclideanspaceR^n),

实际可扩展到更广泛的距离空间(metricspace)。事实上,你这里提到的树就不是欧氏空间(因为这里的距离并非欧氏距离)。
 回复 引用 查看 

#19楼[楼主]

MiloYip2010-02-2517:27

@郑晖

是的,我理解只要是metric就可以定義diameter。
 回复 引用 查看 

#20楼

flyinghearts2010-05-1914:05

这是我的解法:

http://blog.csdn.net/flyinghearts/archive/2010/05/19/5605995.aspx

欢迎大家指正。
 回复 引用 查看 

#21楼

gzroy2010-10-0618:24

这是我的递归解法,欢迎交流:

http://blog.csdn.net/yui/archive/2010/10/06/5924020.aspx
 回复 引用 查看 

注册用户登录后才能发表评论,请
登录或
注册,返回博客园首页。

简洁阅读版式
不3k就业不给1分学费(java,.net,php,android)

首页博问闪存新闻园子招聘知识库

最新IT新闻:

·10个超棒的jQuery工具提示插件推荐

·亲久等了!小米手机零售版销售策略公布

·哥终于知道了苹果为什么发布的是iPhone4S而非iPhone5

·分析师:谷歌三星推迟发NexusPrime是明智之举

·《星际争霸2:虫群之心》新兵种曝光

»更多新闻...
最新知识库文章:

·
Scrum实施经验

·Doclist压缩方法简介

·专家视角看IT与架构

·跨平台的移动开发框架介绍

·为您的Web项目构建一个简单的JSON控制器

»更多知识库文章...





China-pub2011秋季教材巡展

China-Pub计算机绝版图书按需印刷服务

About

MiloYip是香港同胞,现任职于上海麻辣马,开发多平台游戏项目。

在高一高二时兼职开发游戏《王子传奇》后,便潜心向学(伪),得到认知科学学士和系统工程及工程管理学哲学硕士后,于大学里做游戏科技的相关项目,直至2008年才来到上海,再次投身游戏业界,之前的作品是《美食从天而降》Xbox360/PS3/Wii/PC。

因为写了一个书评而认识到很多朋友,决定在内地设一个blog,和大家分享交流。

Twitter

新浪微博

昵称:MiloYip

园龄:1年8个月

荣誉:推荐博客

粉丝:486

关注:31
+加关注

最新闪存

在上海最後一個晚上。三年裡,在兩間公司工作過,在兩處居所暫住過,開發過兩個遊戲,製造過兩個兒女,撰寫過兩編雜誌稿,結識到無數好友。不枉此行。
12-1920:05
更多闪存

最新评论

Re:12年前的作品──《美绿中国象棋》制作过程及算法简介

@DiryBoy

电脑也看不下去了--霸王降臨
Re:用JavaScript玩转计算机图形学(一)光线追踪入门

写得非常好,可以很朋理论没有看懂,呵--网易小前

Re:C++强大背后

c++学习中--仙道男

Re:《编程之美:分层遍历二叉树》的另外两个实现

考虑一次广度遍历,把节点和它的度都存下来,记录在一个vector中~typedefintDataType;typedefstructBstNode{DataTypedata...--siegeweb

Re:用JavaScript玩转游戏物理(一)运动学模拟与粒子系统

很酷哦关注学习下:)--lmh2072005

随笔档案

2011年6月(1)
2010年9月(2)
2010年8月(1)
2010年7月(1)
2010年6月(3)
2010年5月(2)
2010年4月(5)
2010年3月(4)
2010年2月(4)

我参加的小组

女程序员之家
边走边拍

日历

<2010年2月>
31123456
78910111213
14151617181920
21222324252627
28123456
78910111213

随笔分类

Rss
计算机图形学(5)
Rss
人工智能(1)
Rss
数据结构和算法(6)
Rss
物理模拟(1)
Rss
游戏编程(2)
Rss
游戏引擎(4)
Rss
杂谈(4)

阅读排行榜

爱丽丝的发丝──《爱丽丝惊魂记:疯狂再临》制作点滴(45046)

C++强大背后(28128)

史上最强女游戏程序员(25950)

C++/C#/F#/Java/JS/Lua/Python/Ruby渲染比试(23025)

用JavaScript玩转计算机图形学(一)光线追踪入门(21145)

www.spiga.com.mx

Copyright©2011MiloYip

博客园
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: