树常用来表达一对多的关系,比如企业里从总经理到部门到员工的关系,军队中从军、师、团到营、连、排、班的关系,XML文档中各种文档元素之间的组成关系等等都可以用树表示。下图给出了树的一个示例:
树的一个例子
树由结点构成,有一个唯一的结点称为根,包括根在内每个结点可以有0个、1个或者多个子结点,子结点又可以有自己的子结点,依此类推,构成了一个树根在上的倒过来的树形结构。除了根之外,每个结点都有且仅有一个父结点。没有子结点的结点称为叶子。除了根和叶子之外,其他结点称为中间结点。
树以及树所构成的集合树林上的操作主要有:
- 在一个指定结点下构建一个新的子结点
- 删除一个结点
- 获取所有结点
- 获取或者更改一个结点所关联的值
- 获取一个结点的度(即子结点的数目)
- 判断一个结点是否在树上,是不是根结点
- 获取或者更改一个结点的父结点
- 获取一个结点所在树的根结点、所有子结点、所有子孙结点或者祖先结点
- 获取所有的根结点
根据面向对象方法,下面代码创建了一个Forest类。该类的核心数据结构是一个字典型的成员变量.nodes,用来保存每个结点的键和结点的映射关系。结点是_Node类型的对象,每个结点中封装了结点的键(.key)、值(.value)、指向父结点的指针(.parent)和所有子结点的列表(.children)。当新增一个结点时,不但要在.nodes字典中进行登记,还要在父结点的.children列表中添加当前结点。
Forest使用键来引用每个结点,而不是让用户直接操作结点(_Node)对象,这是因为否则的话,用户对结点中数据的改动有可能导致不一致的结果。比如一个结点的父结点是某个结点,但前者在后者的.children列表中没有出现。或者反过来,一个结点在某结点的.children列表中出现,但是其.parent却指向另一个结点。用户在使用Forest时要注意键的唯一性。
值(.value)是结点中保存的用户数据,比如XML文档中的一个元素,企业中的一个部门对象等等。这样就使得结点可以与树之外的数据发生关联。另外,Forest中允许存在多棵树。一个结点如果没有父结点那么它就是一个根。Forest允许存在任意多个根。代码如下:
Python对树林的实现
class Forest:
def __init__(self):
self._nodes = {}
def __contains__(self, key): # 实现key in self操作,判断key是否在树林中
return key in self._nodes
def get_all(self): # 获取所有结点的key
keys = self._nodes.keys()
keys.remove(None)
def add_child(self, key, child_key, child_value=None):
"""
在key所对应的结点下增加一个新的子结点
:param key: key对应父结点, 如果找不到会扔出KeyError, key可以是None
:param child_key: 子结点的键,如果已被占用则抛出AssertionError
:param child_value: 子结点的值
"""
assert child_key not in self._nodes, 'Key %s 已经被占用' % child_key
if key is None:
child = _Node(child_key, child_value)
else:
parent = self._get_node(key)
child = _Node(child_key, child_value, parent)
parent.children.append(child)
self._nodes[child_key] = child
def _get_node(self, key):
# 获取key所对应的结点,如果找不到会扔出KeyError
return self._nodes[key]
def remove(self, key):
"""
删除结点key,并返回该结点的值。如果key不存在会扔出KeyError
:param key: 指定的key
:return: 返回指定结点的值
"""
node = self._get_node(key)
self._remove(node)
return node.value
def _remove(self, node): # 私有成员方法,不对外公开
for child in node.children:
self._remove(child) # 递归地删除子结点
node.parent.children.remove(node) # 从父结点中删除自己
del self._nodes[node.key]
def get_value(self, key):
"""
获取key所对应的值,如果key不存在会扔出KeyError
:param key: 结点的key
:return: key所对应的值
"""
return self._get_node(key).value
def set_value(self, key, new_value):
# 设置结点key的值,如果key不存在会扔出KeyError
self._get_node(key).value = new_value
def get_parent(self, key): # 获取父结点的键,根的父结点是None
p = self._get_node(key).parent
return None if p is None else p.key
def set_parent(self, key, new_parent_key): # 更改结点key的父结点
me = self._get_node(key)
np = self._get_node(new_parent_key)
p = np
while p is not None:
if p == me:
raise AssertionError('被移动的结点%s是目标结点%s的祖先' \
% (key, new_parent_key))
p = p.parent
me.parent.children.remove(me)
me.parent = np
np.children.append(me)
def is_parent(self, parent, key): # 判断结点parent是不是key的父结点
c = self._get_node(key)
if parent is None:
return c.parent is None
p = self._get_node(parent)
return c.parent == p
def is_root(self, key): # 判断结点key是不是一个根结点
return self._get_node(key).parent is None
def get_roots(self): # 获取所有的根结点
return [key for key in self._nodes if self._nodes[key].parent is None]
def get_root(self, key):
# 获取结点key所在树的根。如果key不存在会扔出KeyError
node = self._get_node(key)
while node.parent is not None:
node = node.parent
return node.key
def get_children_num(self, key):
# 获取子结点的数量
return len(self._get_node(key).children)
def get_children(self, key):
# 获取所有子结点的键
return [child.key for child in self._get_node(key).children]
def get_descendants(self, key, root_first:bool=True):
"""
从结点key开始获取子孙结点的键(含key)。如果key不存在会扔出KeyError
:param key: 指定的key
:param root_first: True表示先根序,对每一棵子树来说,根总是出现
在子树上所有其他结点之前。False表示后根序,根出现在所有其他结点之后。
"""
return self._get_descendants(self._get_node(key), root_first)
def _get_descendants(self, node, root_first): # 私有成员方法,不对外公开
result = []
if root_first:
result.append(node.key)
for child in node.children:
children = self._get_descendants(child, root_first) # 递归调用
result.extend(children)
if not root_first:
result.append(node.key)
return result
def get_ancestors(self, key):
# 从结点key开始由近及远获取所有祖先(含key)。如果key不存在会扔出KeyError
node = self._get_node(key)
result = []
while node is not None:
result.append(node.key)
node = node.parent
return result
def get_ancestors_to(self, key, ancestor):
# 从结点key开始由近及远获取到ancestor为止的所有祖先。
# 如果key不存在会扔出KeyError。如果ancestor并不是key的祖先则扔出KeyError
# 这个函数常被用来确定两个结点之间是否存在亲缘关系
node = self._get_node(key)
result = []
while node is not None:
result.append(node.key)
if node.key == ancestor:
break
node = node.parent
if node is None:
raise KeyError('%s不是%s的祖先' % (ancestor, key))
return result
def __repr__(self):
result = []
for root in self.get_roots():
result.append(_to_str(self._get_node(root), '', '\t'))
return '\n'.join(result)
def _to_str(node, prefix, unit):
"""
把以node为根的树转为字符串输出
:param node: 被输出的结点
:param prefix: 当前输出的前缀
:param unit: 输出子结点是需要增加的单位前缀字符
:return: 以node为根的树转成的字符串
"""
result = ['%s%s:%s' % (prefix, node.key, node.value)]
prefix += unit
for child in node.children:
result.append(_to_str(child, prefix, unit))
return '\n'.join(result)
class _Node: # 结点
def __init__(self, key, value, parent=None): # value: 用户数据
"""
:param key: 结点的key
:param value: 结点对应的用户数据
:param parent: 父结点的key
"""
self.key = key
self.value = value
self.parent = parent
self.children = [] # 所有子结点的列表
if __name__ == '__main__':
f = Forest()
f.add_child(None, 'A', 10)
f.add_child('A', 'B', 11)
f.add_child('A', 'C', 12)
f.add_child('A', 'D', 13)
f.add_child('B', 'B1', 111)
f.add_child('B', 'B2', 112)
f.add_child('C', 'C1', 121)
f.add_child('C', 'C2', 122)
f.add_child('D', 'D1', 131)
f.add_child('D', 'D2', 132)
print(f)
代码中用键来引用结点,成功地隔离了树所要表达的一对多关系与事物本身的联系,降低了它们之间的耦合度。举个例子,一个人可以在一个军队体系中成为一个班的士兵,同时他也可以在一个公司体系中成为一个老总。
代码还采用了面向对象技术,把所有复杂性都封装在Forest类中,用户只需简单地调用Forest提供的成员函数即可。私有类_Node则封装了结点的所有数据,包括值value,父结点parent,子结点列表children以及该结点的键key。这种封装很好地屏蔽了结点内部的复杂性。
比如,代码中用列表children保存所有子结点。学过数据结构中二叉树概念的读者可以把这个列表换成两个指针:first_son和next_brother,前者指向第一个子结点,后者指向下一个兄弟结点。由于每个结点都有这两个指针,所以只需访问当前结点的第一个子结点A,再访问A的下一个兄弟,再访问以及兄弟的兄弟,......,依此类推就可以获得当前结点的所有子结点。
代码中为了实现对树林的打印输出,特别定义了一个全局私有的递归函数_to_str(node, prefix, unit)以便输出以node为根的子树。请读者以递归三步曲分析这个函数为什么能起作用。代码输出:
A:10
B:11
B1:111
B2:112
C:12
C1:121
C2:122
D:13
D1:131
D2:132