一棵树是一些节点的集合。这个集合能够是空集,若非空,则一棵树由称做 根(root) 的节点 r 和 0 个或以上的非空子树组成。这些子树中每一棵的根都被来自根 r 的**一条有向边(edge)**链接。node
父子节点,叶子节点定义略。。。算法
从节点 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)。树在正常状况下的查找上显然符合这个特征。code
能够将树当作是序列的变形,它在序列之上容许了多子节点。排序
二叉树即每一个节点的子节点数均不超过 2 的状况。索引
为了便于操做,实际的树一般会在定义之上再增长一些特性,如rem
下面是一个使用字典实现的二叉查找树,功能方面只实现了 __contains__
方法:it
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 并无什么肯定的意思,尤为不要理解为 Binary,由于通常它并不实现为二叉树,而是多路树。
阶为 M 的 B 树是这样一种树:
这个定义看起来不是很直观,能够这样理解:
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/LogM
可得 M 的最佳值为 (2, 3, 4),当再高时,插入效率会变低。而搜索复杂度与 M 无关。
B树也是数据库经常使用的一种索引,所以 M 的选择更多会参考实际的应用场景。如存储在机械硬盘上的 SQL 数据库,硬盘寻址操做的开销比连续读要高得多,所以使内部节点所占空间尽可能接近单扇区可用容量是最好的作法。如 512 字节的扇区容量,每一个节点元素占 4 字节的话,M 就能够设置为 128。对于 SSD,或者更大扇区的磁盘,这个数字均可作相应调整。