ADT - 树

定义

一棵树是一些节点的集合。这个集合可以是空集,若非空,则一棵树由称作 根(root) 的节点 r 和 0 个或以上的非空子树组成。这些子树中每一棵的根都被来自根 r 的**一条有向边(edge)**连接。

父子节点,叶子节点定义略。。。

从节点 n<sub>1</sub> 到 n<sub>k</sub> 的 路径(path) 定义为节点 n<sub>1</sub>, n<sub>2</sub>, ..., n<sub>k</sub> 的一个序列,使得对于 1 ≤ i ≤ k,节点 n<sub>i+1</sub> 是 节点 n<sub>i</sub> 的父亲。这条路径的 长度(length) 定义为该路径上边的条数,即k - 1。

可见任意两节点 n<sub>i</sub> 和 n<sub>k</sub> 之间是否存在一条路径取决于 n<sub>i</sub> 是否是 n<sub>k</sub> 的祖先节点。且如果有,也仅有一条。(因为上面树的定义里,一个非根节点有且只有一条边)

而从根节点到任意节点均存在一条唯一的路径,这条路径的长度被称为该节点的 深度(depth)。一棵树的深度被定义为其最深的一个节点的深度。

按照之前算法分析的某条定理:如果一个算法可以使用常数时间将问题缩小为原问题的一部分,那么这个算法的复杂度为 O(logN)。树在正常情况下的查找上显然符合这个特征。

可以将树看成是序列的变形,它在序列之上允许了多子节点。

二叉树

二叉树即每个节点的子节点数均不超过 2 的情况。


实现

为了便于操作,实际的树通常会在定义之上再增加一些特性,如

  1. 在每个节点中记录其父节点,即反向边
  2. 删除时并不真正删除节点,而是将其计数 -1

下面是一个使用字典实现的二叉查找树,功能方面只实现了 __contains__ 方法:

class Node(dict):
    def __init__(self, value, parent=None, left=None, right=None):
        self['value'] = value
        self['count'] = 1
        if parent:
            self['parent'] = parent
        if left:
            self['left'] = left
        if right:
            self['right'] = right


class Tree(object):
    def __init__(self, init_value):
        self.root = Node(init_value)

    def _find_near(self, value):
        node = self.root
        try:
            while True:
                if value == node['value']:
                    return node
                elif value < node['value']:
                    node = node['left']
                else:
                    node = node['right']
        except KeyError:
            return node

    def add(self, value):
        node = self._find_near(value)
        if value == node['value']:
            node['count'] += 1
        elif value < node['value']:
            node['left'] = Node(value)
        else:
            node['right'] = Node(value)

    def rem(self, value):
        node = self._find_near(value)
        if node['value'] == value and node['count'] > 0:
            node['count'] -= 1

    def __contains__(self, value):
        node = self._find_near(value)
        return node['value'] == value and node['count'] > 0

平衡问题

容易看到,二叉树可能出现一个极端状态,即所有节点的子节点数均不超过 1 ,此时的树实际上就是链表。而其深度为 N-1.

因此使一棵树拥有尽可能低的深度,对于查找性能来说显得尤为重要,这个优化的过程便称为平衡,即取,使每个节点的左右子树的深度尽可能平衡之意。

平衡是个很麻烦的事情,它意味着每次更新操作后都有可能要变更树的结构。此过程称为旋转(ratation)。如 AVL 树,要求每个节点的左子树和右子树的高度最多差 1.

B 树

B-树的 B 并没有什么确定的意思,尤其不要理解为 Binary,因为一般它并不实现为二叉树,而是多路树。

阶为 M 的 B 树是这样一种树:

  • 其根节点要么没有子节点,要么子节点数在 2 ~ M 之间
  • 除根外,所有内部节点(非叶子节点)的子节点数在 M/2 ~ M 之间
  • 所有的叶子节点都拥有相同深度
  • 所有的数据都存储在叶子上

这个定义看起来不是很直观,可以这样理解:

M 阶 B树的数据部分是一个序列。除了这个序列外,还额外存在一个树型结构,用于索引这个序列。具体方法为:序列从前向后每 M 个元素便向上生出一个父节点,父节点的值等于这 M 个元素的最小值(或最大值,取决于序列的排序),然后这批父节点再每 M 个向上生出次级父节点,如此反复直到生出根来。

当然,实际 B树的生成过程是正好相反的。也相应的有更多需要考虑的问题。

B树每个节点的子节点数均小于等于 M。另外为了尽量降低树的深度,我们还规定内部节点的子节点数需大于等于 M/2 (ceil),当不满足此条件时,就要将子节点合并。

由此我们可以算出,M 阶 B树的深度最小可以为 Log<sub>M</sub>N,最大也不过是 Log<sub>M/2</sub>N。另外因为每个节点最多有 M 个子节点,所以一次节点内分支选择的复杂度为 LogM。故搜索的复杂度为 (M/2)Log<sub>M/2</sub>N,可化简为 O(LogN)。增删操作可能需要 O(M) 的时间来调整节点数据,因此增删操作的复杂度为 O(MLog<sub>M</sub>N) ,可化简为 O((M/LogM)LogN)

M 的选择

由前面的增删操作复杂度公式前的常数部分 M/LogM 可得 M 的最佳值为 (2, 3, 4),当再高时,插入效率会变低。而搜索复杂度与 M 无关。

B树也是数据库常用的一种索引,因此 M 的选择更多会参考实际的应用场景。如存储在机械硬盘上的 SQL 数据库,硬盘寻址操作的开销比连续读要高得多,因此使内部节点所占空间尽量接近单扇区可用容量是最好的做法。如 512 字节的扇区容量,每个节点元素占 4 字节的话,M 就可以设置为 128。对于 SSD,或者更大扇区的磁盘,这个数字都可做相应调整。

转载于:https://my.oschina.net/lionets/blog/727982

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值