树的遍历
之前的工作都没有接触到树,也就很少研究它。幸运地的是,在目前的工作中多次遇到树型结构的数据,那么访问树节点中的数据就是必然的了,而且还需要按照指定规则对节点中的数据进行额外处理。经过学习之后,对与树相关的基本算法有了一些认知,就计划写几篇小文。其实这样的文章早已是汗牛充栋,而我只是把它当作我的学习总结罢了,以加深记忆与理解,如能对其他朋友有所助益,则更感愉悦了 :-) (2009.04.03最后更新)
这次先从最基础的开始--树的遍历。本文使用了两种极常用的方法来遍历树中的所有节点--递归;迭代,但它们实现的都是深度优先(Depth-First)算法。
1. 树节点与数据
先定义树节点及数据(用户对象),并创建测试用的数据。
TreeNode是树节点的定义。
/**
* 树节点的定义。
*/
public
interface
TreeNode {
/**
* 获取指定下标处的子节点。
*
*
@param
index
* 下标。
*
@return
子节点。
*/
public
TreeNode getChildAt(
int
index);
/**
* 返回指定子节点的下标。
*
*
@param
index
* 下标。
*
@return
子节点。
*/
public
int
getChildIndex(TreeNode index);
/**
* 获取子节点的数量。
*
*
@return
子节点的数量。
*/
public
int
getChildCount();
/**
* 返回父节点。
*
*
@return
父节点。
*/
public
TreeNode getParent();
/**
* 设置父节点。注:此处不需要改变父节点中的子节点元素。
*
*
@param
parent
* 父节点。
*/
public
void
setParent(TreeNode parent);
/**
* 获取所有的子节点。
*
*
@return
子节点的集合。
*/
public
List
<?>
getChildren();
/**
* 是否为叶节点。
*
*
@return
是叶节点,返回true;否则,返回false。
*/
public
boolean
isLeaf();
}
GenericTreeNode是一个通用的树节点实现。
public
class
GenericTreeNode
<
T
>
implements
TreeNode {
private
T userObject
=
null
;
private
TreeNode parent
=
null
;
private
List
<
GenericTreeNode
<
T
>>
children
=
new
ArrayList
<
GenericTreeNode
<
T
>>
();
public
GenericTreeNode(T userObject) {
this
.userObject
=
userObject;
}
public
GenericTreeNode() {
this
(
null
);
}
/**
* 添加子节点。
*
*
@param
child
*/
public
void
addChild(GenericTreeNode
<
T
>
child) {
children.add(child);
child.setParent(
this
);
}
/**
* 删除指定的子节点。
*
*
@param
child
* 子节点。
*/
public
void
removeChild(TreeNode child) {
removeChildAt(getChildIndex(child));
}
/**
* 删除指定下标处的子节点。
*
*
@param
index
* 下标。
*/
public
void
removeChildAt(
int
index) {
TreeNode child
=
getChildAt(index);
children.remove(index);
child.setParent(
null
);
}
public
TreeNode getChildAt(
int
index) {
return
children.get(index);
}
public
int
getChildCount() {
return
children.size();
}
public
int
getChildIndex(TreeNode child) {
return
children.indexOf(child);
}
public
List
<
GenericTreeNode
<
T
>>
getChildren() {
return
Collections.unmodifiableList(children);
}
public
void
setParent(TreeNode parent) {
this
.parent
=
parent;
}
public
TreeNode getParent() {
return
parent;
}
/**
* 是否为根节点。
*
*
@return
是根节点,返回true;否则,返回false。
*/
public
boolean
isRoot() {
return
getParent()
==
null
;
}
public
boolean
isLeaf() {
return
getChildCount()
==
0
;
}
/**
* 判断指定的节点是否为当前节点的子节点。
*
*
@param
node
* 节点。
*
@return
是当前节点的子节点,返回true;否则,返回false。
*/
public
boolean
isChild(TreeNode node) {
boolean
result;
if
(node
==
null
) {
result
=
false
;
}
else
{
if
(getChildCount()
==
0
) {
result
=
false
;
}
else
{
result
=
(node.getParent()
==
this
);
}
}
return
result;
}
public
T getUserObject() {
return
userObject;
}
public
void
setUserObject(T userObject) {
this
.userObject
=
userObject;
}
@Override
public
String toString() {
return
userObject
==
null
?
""
: userObject.toString();
}
}
UserObject是节点上的用户对象,相当于是数据。
public
class
UserObject {
private
String name
=
null
;
private
Integer value
=
Integer.valueOf(
0
);
public
UserObject() {
}
public
UserObject(String code, Integer value) {
this
.name
=
code;
this
.value
=
value;
}
public
String getName() {
return
name;
}
public
void
setName(String code) {
this
.name
=
code;
}
public
Integer getValue() {
return
value;
}
public
void
setValue(Integer value) {
this
.value
=
value;
}
@Override
public
String toString() {
StringBuilder result
=
new
StringBuilder();
result.append(
"
[name=
"
).append(name).append(
"
, value=
"
).append(value).append(
"
]
"
);
return
result.toString();
}
}
TreeUtils是用于创建树的工具类。
public
class
TreeUtils {
public
static
GenericTreeNode
<
UserObject
>
buildTree() {
GenericTreeNode
<
UserObject
>
root
=
new
GenericTreeNode
<
UserObject
>
();
root.setUserObject(
new
UserObject(
"
ROOT
"
, Integer.valueOf(
0
)));
GenericTreeNode
<
UserObject
>
node1
=
new
GenericTreeNode
<
UserObject
>
();
node1.setUserObject(
new
UserObject(
"
1
"
, Integer.valueOf(
0
)));
GenericTreeNode
<
UserObject
>
node2
=
new
GenericTreeNode
<
UserObject
>
();
node2.setUserObject(
new
UserObject(
"
2
"
, Integer.valueOf(
0
)));
GenericTreeNode
<
UserObject
>
node3
=
new
GenericTreeNode
<
UserObject
>
();
node3.setUserObject(
new
UserObject(
"
3
"
, Integer.valueOf(
5
)));
root.addChild(node1);
root.addChild(node2);
root.addChild(node3);
GenericTreeNode
<
UserObject
>
node11
=
new
GenericTreeNode
<
UserObject
>
();
node11.setUserObject(
new
UserObject(
"
11
"
, Integer.valueOf(
0
)));
GenericTreeNode
<
UserObject
>
node21
=
new
GenericTreeNode
<
UserObject
>
();
node21.setUserObject(
new
UserObject(
"
21
"
, Integer.valueOf(
0
)));
node1.addChild(node11);
node2.addChild(node21);
GenericTreeNode
<
UserObject
>
node111
=
new
GenericTreeNode
<
UserObject
>
();
node111.setUserObject(
new
UserObject(
"
111
"
, Integer.valueOf(
3
)));
GenericTreeNode
<
UserObject
>
node112
=
new
GenericTreeNode
<
UserObject
>
();
node112.setUserObject(
new
UserObject(
"
112
"
, Integer.valueOf(
9
)));
GenericTreeNode
<
UserObject
>
node211
=
new
GenericTreeNode
<
UserObject
>
();
node211.setUserObject(
new
UserObject(
"
211
"
, Integer.valueOf(
6
)));
GenericTreeNode
<
UserObject
>
node212
=
new
GenericTreeNode
<
UserObject
>
();
node212.setUserObject(
new
UserObject(
"
212
"
, Integer.valueOf(
3
)));
node11.addChild(node111);
node11.addChild(node112);
node21.addChild(node211);
node21.addChild(node212);
return
root;
}
}
2. 递归法
使用递归法的最大好处就是--简单,但一般地,我们都认为递归的效率不高。
private
static
void
recursiveTravel(GenericTreeNode
<
UserObject
>
node) {
travelNode(node);
//
访问节点,仅仅只是打印该节点罢了。
List
<
GenericTreeNode
<
UserObject
>>
children
=
node.getChildren();
for
(
int
i
=
0
; i
<
children.size(); i
++
) {
recursiveTravel(children.get(i));
//
递归地访问当前节点的所有子节点。
}
}
大家肯定知道,系统在执行递归方法(对于其它方法也是如此)时是使用运行时栈。对方法的每一次调用,在栈中都会创建一份此次调用的活动记录--包括方法的参数,局部变量,返回地址,动态链接库,返回值等。
既然系统能够隐式地使用栈去执行递归方法,那么我们就可以显式地使用栈来执行上述递归程序,这也是将递归程序转化为迭代程序的常用思想。下面的iterativeTravel方法就运用了这一思想。
3. 迭代法
private
static
void
iterativeTravel(GenericTreeNode
<
UserObject
>
node) {
Stack
<
GenericTreeNode
<
UserObject
>>
nodes
=
new
Stack
<
GenericTreeNode
<
UserObject
>>
();
nodes.push(node);
//
将当前节点压入栈中。
while
(
!
nodes.isEmpty()) {
GenericTreeNode
<
UserObject
>
bufNode
=
nodes.pop();
//
从栈中取出一个节点。
travelNode(bufNode);
//
访问节点。
if
(
!
bufNode.isLeaf()) {
//
如果该节点为分枝节点,则将它的子节点全部加入栈中。
nodes.addAll(bufNode.getChildren());
}
}
}
与递归法相比,迭代法的代码略多了几行,但仍然很简单。
4. 小结
由于上述两种方法均(隐式或显式地)使用了运行栈,所以此处的迭代法并不能提高整个程序的效率。相反地,由于在应用程序中显式地使用栈(java.util.Stack),iterativeTravel方法的效率可能反而更低。但iterativeTravel的最大好处是,能够有效地避免运行时栈溢出(java.lang.StackOverflowError)。
如果树的层次不太深,每层的子节点数不太多,那么使用递归法应该是没有问题的。毕竟,简洁地程序会提供更多的好处。