xuzhengfu / pilot

进入编程世界的第一课
1 stars 0 forks source link

p2-a-tree 第十章 树 #41

Open xuzhengfu opened 4 years ago

xuzhengfu commented 4 years ago

1. 定义问题

当我们说 “树形结构” 的时候,那是从自然界的大树上得到的启发,从根开始不断展开的枝丫,代表了从单一到复杂的一层层展开,在现实世界充满了这样的事物:国家的行政区划(以及任何组织的架构)、电商网站的商品分类、论坛里的板块等等。

虽然自然界里树是根在下面而向上分支,但是我们在处理树形结构时,从上向下分支更容易画也更容易看。

解决问题的第一步是先要清晰明确地定义问题,我们来尝试把我们对树的直觉印象书写成尽可能清晰明确的定义和表述。在一个树形结构里:

  • 树(tree)由节点(node)和连接节点的边(edge)组成;
  • 总有一个节点是分支的起点,它分出了所有其他的节点,这个节点叫根节点(root node);
  • 一个节点分支出来的节点叫它的 “子节点(child nodes)”,它是其子节点的 “父节点(parent node)”;上图中节点 A 的父节点是根节点,子节点是 B 和 C,而节点 C 的父节点是 A,子节点是 D 和 E;节点 D 的父节点是 C,而它没有子节点;
  • 拥有共同父节点的两个节点互为 “兄弟姐妹(sibling nodes)”;比如上图中 B 和 C,D 和 E;
  • 没有子节点的节点也叫 “叶子节点(leaf node)”;比如上图中的 D 和 E;
  • 一个节点的父节点,以及父节点的父节点…直至根节点,都是这个节点的 “祖先节点(ancestor nodes)”;比如上图中 E 的祖先节点包括 C、A 和 根节点;
  • 一个节点的所有子节点,以及所有子节点的子节点…直至叶子节点,都是这个节点的 “后代节点(descendant nodes)”;
  • 一个节点 X 和它所有后代节点、以及这些节点之间的边,也组成一棵树,叫做原数的一棵(以 X 为根的)“子树(sub-tree)”;
  • 根节点没有父节点也没有祖先节点;
  • 除了根节点以外的任何节点,有且只有 1 个父节点,有至少 1 个祖先节点;
  • 任何节点都可以有 0、1 或者多个子节点,可以有 0、1 或者多个后代节点;
  • 一个节点和它的任一祖先或者后代节点之间,一定存在一条由边首尾连接组成的路径(path),比如上图中根节点是 E 的祖先,它们之间的路径是 root -> A -> C -> E;这个路径就像是两个节点之间的亲属关系链;
  • 两个节点之间 path 的长度,就是它由几条边组成,决定了这两个节点之间隔了多少 “代”,也叫节点之间的 “距离(distance)”;
  • 一个节点和根节点之间的 distance 经常有特别的含义,相当于该节点在树的 “第几层级(tier)”;比如在行政区划里哪些是一级节点(省、直辖市、自治区)哪些是二级节点等;
  • 有些树里节点的子节点是有顺序的,叫做 “有序树(ordered tree)”;如果我们不特别指出,那就是不考虑这种顺序概念。

上面这种 “把直觉规则化” 的过程,对后面设计解决方案是至关重要的,大家可以自己尝试,多多体会。

事实上在数学图论里有对树的更严谨更数学化的定义,不过上面的表述对目前的我们来说基本够用了。

2. 分析操作场景

可以看到树和我们前面讲的所有数据结构都不一样,没办法用一种 “线性(linear)” 结构来表达,如何在计算机中实现一个树形数据结构?面对这样的问题,我们应该如何思考呢?

一个优秀的起点是问题解决的一半。设计数据结构的起点是:思考我们会怎么操作和使用这个数据结构

假定我们已经有一个树形数据结构,我们可能会有这些操作场景(可以尝试用行政区划或者论坛版块等熟悉的实例来帮助思考):

  • 查找和遍历:

    • 给定一个 node 找出它的 parent ✪
    • 给定一个 node 找出它所有的 children ✪
    • 给定一个 node 找出它某个特定 child
    • 给定一个 node 找出它所有的 siblings
    • 给定一个 node 遍历其 sub-tree 即找出它的所有 descendants ✪
    • 给定 node A 和 B,找到 A 和 B 之间的 path 和 distance
    • 给定一个 node 确定它的 tier
  • 编辑

    • 给一个 node 增加一个 child ✪
    • 删除一个 node ✪ -> 新一步思考:这意味着什么?删除整个子树还是?
    • 修改一个 node 的 parent ✪

差不多就这些,其中标记了 ✪ 号的是感觉特别重要和基础的操作。

通过形象化的图示、对基本概念的定义和表述、对可能操作场景的罗列,已经增加了很多我们对问题理解的深度和全面度,下来我们可以尝试做出一些初步设计判断了。

3. 设计初步方案

  • 树中节点之间的父子关系是核心和本质的内容;
  • 表达这种父子关系的关键是节点,节点应保存父节点和子节点相关信息;
  • 树的结构上带有显著的 “递归” 特点,可以有助于我们的设计。

如果你不记得 “递归(recursion,recursive)” 是怎么回事了,可以温习递归函数一章。

树也是递归的一个典型例子,因为一棵树可以看做由根节点、根节点的子节点和以这些子节点为根的子树合起来组成的,如果我们想实现对树进行操作的函数 f(),那么我们可以让这个函数接受一个树的根节点作为输入参数,这个函数大致上会是这个样子:

def f(root):
    # 对 root 做一些操作
    # 然后取出 root 的所有子节点,对其中每个子节点调用 f() 本身
    for node in root.children:
        f(node)

也就是说,我们只要对某个节点做操作,然后获取这个节点的所有子节点,递归调用自己,就能对整个树做操作了。

在树形数据结构中,“获取一个节点的所有子节点” 是至关重要的操作。

class TreeNode:
    def __init__(self, name='root', data=None, parent=None, children=None):
        self.name = name
        self.data = data

        if parent:
            # 确认 parent 参数是 TreeNode 类型
            assert isinstance(parent, TreeNode)
            parent.add_child(self)
        self.parent = parent

        self.children = []
        if children:
            for child in children:
                self.add_child(child)

    def add_child(self, node):
        # 1. 确认 node 参数是 TreeNode 类型
        # 2. 将要加入的子节点的 parent 属性设为自己
        # 3. 然后将其加入 children 列表
        assert isinstance(node, TreeNode)
        node.parent = self
        self.children.append(node)

我们设计的核心数据结构是表示树节点的自定义类型 TreeNode,这个类型的对象有四个实例变量(属性):

  • name:节点的名字,最好能唯一标识出一个节点
  • data:节点相关的任何数据,可以是任何数据类型
  • parent:节点的父节点,如果没有父节点就是 None
  • children:节点所有子节点组成的一个列表

这里面 parent 和 children 里的元素都必须是 TreeNode 类型的对象,我们在处理这两个属性时要先确认这一点,在上面的代码中我们用 Python 的 assert 语句和 isinstance() 函数来实现:

  • assert 关键字后面的表达式必须返回 True,否则程序将抛出 AssertionError 异常后终止;
  • isinstance(obj, type) 之前我们就介绍过,它接受两个参数,第一个参数是一个对象,而第二个参数是一个类型,函数判断第一个参数是不是第二个参数指明的类型,如果是返回 True,否则返回 False

如上定义的 TreeNode 类,它实例化出来的对象 node 具备如下的能力:

  • 很容易取得其父节点 parent;
  • 很容易取得其所有子节点 children;
  • 已经实现了增加子节点的操作。

最有意思的是,TreeNode 类型实际上也代表了树本身,因为一个节点加上它所有子节点,本来就是一棵树嘛!

在实际项目中,我们并不需要定义树的数据结构,因为有优秀的第三方实现可用,比如 Python 非常棒的第三方库 anytree,你可以试试,看和你自己实现的有什么区别。

4. 小结

这一章介绍非常常见的树形逻辑怎么实现。没有很多代码,主要讲的是思维方法,请你读完一遍之后尝试自己从头思考和建立一个基本的树的数据结构,如果遇到问题就再读一遍。

Logging

2020-04-07 17:38:11 initialize