树的术语和定义
节点
节点是树的基础部分。节点可以有自己的名字,被称作“键”,节点也可以有附带信息,被称为“有效载荷”。
边
两个节点通过一条边相连,表示他们之间存在关系。除了根节点之外,其他每个节点仅有一条入边,出边则可能有多条。
根节点
根节点是树中唯一没有入边的节点。
路径
路径是由边连接的有序节点列表。比如,哺乳纲->食肉目->猫科->猫属->家猫。
子节点
一个节点通过出边与子节点相连。
父节点
一个节点是所有其子节点的父节点。
兄弟节点
具有同一父节点的节点互称为兄弟节点。
子树
一个父节点及其所有后代的节点和边构成一颗子树
叶子节点
叶子节点没有子节点。
层数
节点 n 的层数是从根节点到 n 的唯一长度路径。
高度
树的高度是其中节点层数的最大值。
树的定义一
树有以下属性:
- 有一个根节点
- 除根节点外,其他每个节点都与其唯一的父节点相连
- 从根节点到其他每个节点都有且仅有一条路径
- 如果每个节点最多有两个子节点,这样的树就被称为二叉树
树的定义二
一棵树要么为空,要么由一个根节点和零颗或多颗子树构成,子树本身也是一棵树。
每颗子树的根节点通过一条边连到父树的根节点。
树的 Python 实现
根据上面的定义,可以用以下的函数创建并操作二叉树。
- BinaryTree() 创建一个二叉树实例
- getLeftChild() 返回当前节点的左子节点所对应的二叉树
- getRightChild() 返回当前节点的右子节点所对应的二叉树
- setRootVal(val) 在当前节点中存储参数 val 中的对象
- getRootVal() 返回当前节点存储的对象
- insertLeft(val) 新建一棵二叉树,并将其作为当前节点的左子节点
- insertRight(val) 新建一棵二叉树,并将其作为当前节点的右子节点
树的实现形式之一:列表之列表
在列表之列表的树中,根节点的值作为列表的第一个元素,第二个元素是代表左子树的列表,第三个元素代表右子树。
graph TB a((a)) b((b)) c((c)) d((d)) e((e)) f((f)) a-->b a-->c b-->d b-->e c-->f
1 | my_tree = [ |
可以通过标准的列表切片访问子树。子树的列表结构也符合树的定义,所以列表之列表的结构是递归地。这种表示法可以推广到有很多子树的情况。
1 | >>> my_tree = ['a', ['b', ['d', [], []], ['e', [], []]], ['c', ['f', [], []], []]] |
通过函数定义树的数据结构:
1 | def binary_tree(r): |
要给树添加左子树时要特别注意,如果列表的左子树位置已经有内容了,要保留已有内容,并将它作为新列表的左子树。右子树同理。
1 | 3) r = binary_tree( |
树的实现形式之二:节点与引用
节点与引用表示法将树定义为一个类, 其中有根节点与左右子树的属性。树的左子树属性和右子树属性会指向树类的其他实例。
1 | ## BinaryTree 类 |
正如二叉树的子树都是二叉树,根节点的左右子节点本身都是 BinaryTree 类的实例:
1 | 'a') r = BinaryTree( |
二叉树树的应用
解析树
可以用上面已经实现的树来解析一些实际问题,比如数学表达式这样的构造。比如,可以将 ((7 + 3) * (5 - 2)) 这样的数学表达式表示成解析树。
1 | from pythonds.basic import Stack |
定义如下 4 条规则:
- 如果当前标记是 ( ,就为当前节点添加一个左子节点,并下沉至该子节点
- 如果当前标记在列表 [‘+’, ‘-‘, ‘/‘, ‘*’] 中,就将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节点,并下沉至该子节点
- 如果当前标记是数字,就将当前节点的值设为这个数并返回至父节点
- 如果当前标记是 ) ,就跳到当前节点的父节点
1 | import operator |
1 | '( ( 7 + 3 ) * ( 5 - 2 ) )' fpexp = |
树的遍历
树的遍历方法有三种: 前序遍历、中序遍历和后序遍历
- 前序遍历:在前序遍历中,先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。
- 中序遍历:在中序遍历中,先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
- 后序遍历:在后序遍历中,先递归地后序遍历右子树,然后递归地后序遍历左子树,最后访问根节点。
1 | ## 将前序遍历算法实现为 BinaryTree 类的方法 |
用后序遍历重写计算函数:
1 | def post_order_eval(tree): |
用中序遍历打印括号表达式:
1 | def printexp(tree): |
1 | from pythonds.trees import BinaryTree |
二叉堆
二叉堆是实现优先级队列的经典方法。而插队的入队和出队操作都可以达到 O(logn)。二叉堆画出来很像一棵树,但实现时只用一个列表作为内部表示。
二叉堆的两个常见的变体为:最小堆(最小的元素一直在队首)与最大堆(最大的元素一直在队首)。
二叉堆的实现
如果一个二叉树除了最底层,其他每一层的节点都是满的,这个二叉树就被称为完全二叉树。引出完全二叉树的概念有助于高效地实现二叉堆。
堆的有序性:对于堆中任意元素 x 及其父元素 p,p 都不大于 x。
二叉堆类 BinaryHeap 的 heal_list 的第一个元素是 0 ,它的唯一用途是为了使后续的方法可以使用整数除法。
BinaryHeap 的 insert 方法先将元素追加到列表的末尾,然后通过比较新元素与父元素的大小。如果新元素小于其父元素就将二者交换,以此来保证堆的结构性质。通过 perc_up 方法,元素将一直沿着树向上移动,指导重获堆的结构性质。
BinaryHeap 的 del_min 方法的难点是,如何在移除根节点后重构堆的性质和有序性。可以分两步重建堆:第一步,取出列表中最后一个元素,将其移到根节点的位置;第二步,将新的根节点沿着树推到正确的位置。通过辅助函数 perc_down 和 min_child 方法来实现上述 del_min 方法。
1 | class BinaryHeap: |
1 | >>>a = BinaryHeap() |
二叉搜索树
二叉搜索树是映射的另一种实现。
二叉搜索树依赖于这样一个性质:小于父节点的键都在左子树中,大于父节点的键则都在右子树中。
使用 BinarySearchTree 类和 TreeNode 类来实现二叉搜索树:
1 | class BinarySearchTree: |