[Python] 看binarytree源码深入学习二叉树

1. binarytree 库

binarytree

1.1 运行环境
Python 2.7, 3.4, 3.5 或 3.6
1.2 安装方法
pip install binarytree
1.3 自动构建随机二叉树
>>> from binarytree import tree, bst, heap
>>> 
>>> # 构建节点随机的二叉树
>>> my_tree = tree(height=3, is_perfect=True)
>>> print(my_tree)

        ________7_______
       /                \
    __9__             ___8___
   /     \           /       \
  3       10        14       _1
 / \     /  \      /  \     /  \
4   5   2    12   6    0   11   13

>>> # 构建随机的二叉查找树
>>> my_bst = bst(height=3, is_perfect=False)
>>> print(my_bst)

  1______
 /       \
0       __13
       /    \
      3      14
     / \
    2   8
    
>>> # 构建随机最大/小堆二叉树
>>> my_heap = heap(height=3, is_max=True, is_perfect=False)
>>> print(my_heat)

      ___14__
     /       \
    11        13
   /  \      /  \
  6    8    5    7
 /
0
1.4 手动构建二叉树
>>> from binarytree import Node
>>> # 创建根节点
>>> root = Node(1)
>>> # 创建左节点
>>> root.left = Node(2)
>>> # 创建右节点
>>> root.right = Node(3)
>>> # 创建右节点的左节点
>>> root.right.left = Node(4)

  1__
 /   \
2     3
     /
    4
>>> # 取节点
>>> root[1]
Node(2)
>>> root[2]
Node(3)
1.5 查看二叉树性质
>>> from binarytree import Node
>>>
>>> root = Node(1)
>>> root.left = Node(2)
>>> root.right = Node(3)
>>> root.left.left = Node(4)
>>> root.left.right = Node(5)
>>>
>>> print(root)

    __1
   /   \
  2     3
 / \
4   5

>>> root.height
2
>>> root.is_balanced
True
>>> root.is_bst
False
>>> root.is_complete
True
>>> root.is_max_heap
False
>>> root.is_min_heap
True
>>> root.is_perfect
False
>>> root.is_strict
True
>>> root.leaf_count
3
>>> root.max_leaf_depth
2
>>> root.max_node_value
5
>>> root.min_leaf_depth
1
>>> root.min_node_value
1
>>> root.size
5

>>> properties = root.properties  # Get all properties at once
>>> properties['height']
2
>>> properties['is_balanced']
True
>>> properties['max_leaf_depth']
2

>>> root.leaves
[Node(3), Node(4), Node(5)]

>>> root.levels
[[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]]

2. 二叉树创建源码解析

接下来就跟着binarytree的源码更深入地学习使用python构建二叉树吧。

2.1 Node

看一下节点Node的源码,看到这里我惭愧了,要知道我自己创建的节点代码只有5行...而binarytree库中的Node代码不算注释与空格就有286行。

首先看一下它的初始化函数__init__

def __init__(self, value, left=None, right=None):
    if not isinstance(value, numbers.Number):
        raise NodeValueError('node value must be a number')
    if left is not None and not isinstance(left, Node):
        raise NodeTypeError('left child must be a Node instance')
    if right is not None and not isinstance(right, Node):
        raise NodeTypeError('right child must be a Node instance')

    self.value = value
    self.left = left
    self.right = right

这里限制了定义节点时,它的值则必须是数字,且它的左右子节点为空或是Node类的实例(默认为空)。

再看一下设置属性值时会调用的函数__setattr__:

def __setattr__(self, attr, obj):

    if attr == 'left':
        if obj is not None and not isinstance(obj, Node):
            raise NodeTypeError(
                'left child must be a Node instance')
    elif attr == 'right':
        if obj is not None and not isinstance(obj, Node):
            raise NodeTypeError(
                'right child must be a Node instance')
    elif attr == 'value' and not isinstance(obj, numbers.Number):
        raise NodeValueError('node value must be a number')

    object.__setattr__(self, attr, obj)

可以看到这里对设定的属性值做了一下参数校验,随后继承了object的__setattr__

1.4中的栗子说到,通过Node添加好二叉树后,可以通过index去访问相应节点,如下:

>>>root = Node(1)
>>>root.left = Node(2)
>>>root.right = Node(3)
>>>root.left.left = Node(4)
>>>root.left.right = Node(5)
>>>root[2]
Node(3)

这个用法主要靠__getitem__实现,贴一下__getitem__的代码:

def __getitem__(self, index):
    if not isinstance(index, int) or index < 0:
        raise NodeIndexError(
            'node index must be a non-negative int')
    # 进行遍历的序列
    current_nodes = [self]
    # 设置初始index
    current_index = 0
    # 设置未遍历完的flag
    has_more_nodes = True

    while has_more_nodes:
        has_more_nodes = False
        next_nodes = []
        
        # 遍历队列中的节点
        for node in current_nodes:
            # 若index符合,返回或抛异常
            if current_index == index:
                if node is None:
                    break
                else:
                    return node
            current_index += 1
 
            if node is None:
                next_nodes.extend((None, None))
                continue
            next_nodes.extend((node.left, node.right))
            # 判断是否有后续节点
            if node.left is not None or node.right is not None:
                has_more_nodes = True
        
        # 将节点的子节点添加到队列
        current_nodes = next_nodes

    raise NodeNotFoundError('node missing at index {}'.format(index))

这段代码主要用了队列(先进先出)的思想,广度优先遍历队列中每个节点验证其index,再将该节点的左右子节点加入队列,不断循环直到找到index所对应的节点或是遍历完整个二叉树都没有找到相应的节点(抛出异常)。

再看一下修改节点值的功能,如下:

>>>root = Node(1)
>>>root.left = Node(2)
>>>root.right = Node(3)
>>>root.left.left = Node(4)
>>>root[1] = Node(100)
>>>print(root)
   _1
  /  \
100   3

这个功能依靠__setitem__来实现:

def __setitem__(self, index, node):
    # 修改根节点时,报错
    if index == 0:
        raise NodeModifyError('cannot modify the root node')
    # 判断是否有父节点
    parent_index = (index - 1) // 2
    try:
        parent = self.__getitem__(parent_index)
    except NodeNotFoundError:
        raise NodeNotFoundError(
            'parent node missing at index {}'.format(parent_index))
    # 通过修改父节点的left, right属性来修改索引值
    setattr(parent, 'left' if index % 2 else 'right', node)

可以看到有两种情况会导致报错,一种是更改根节点(root[0]),另一种是更改不存在的节点(无父节点的节点)。

2.2 tree

通过binarytree中的tree可以随机创建指定高度的二叉树。

>>>my_tree = tree(height=3, is_perfect=False)
>>>print(my_tree)

          _____9_____
         /           \
    ____7___       ___11__
   /        \     /       \
  10        _2   8         4
 /  \      /      \       /
5    12   13       14    1

下面看一下源码:

def tree(height=3, is_perfect=False):
    _validate_tree_height(height)
    # 生成随机的节点值
    values = _generate_random_node_values(height)
    if is_perfect:
        return build(values)

    # 根据高度随机生成叶节点的数量
    leaf_count = _generate_random_leaf_count(height)
    root = Node(values.pop(0))
    leaves = set()

    for value in values:
        node = root
        depth = 0
        inserted = False

        while depth < height and not inserted:
            # 随机选择‘左右’子节点
            attr = random.choice(('left', 'right'))
            # 验证是否该节点的’左右‘子节点是否已添加
            if getattr(node, attr) is None:
                setattr(node, attr, Node(value))
                inserted = True
            node = getattr(node, attr)
            depth += 1
            
        # 到了二叉树的最下层,记录叶节点
        if inserted and depth == height:
            leaves.add(node)
        # 叶节点到了指定数量,退出循环
        if len(leaves) == leaf_count:
            break

    return root

从上述代码我们可以看到,当创建一个不完美(is_perfect =False)的指定高度的随机二叉树时,首先会随机生成叶节点的个数leaf_count,当叶节点个数到达此个数时,就跳出循环。

节点的添加也是自上而下,随机选择左右子节点,每当某个值的节点被插入后,就会将inserted标记为True,该值不会继续作为节点值被插入。

我们再来看一下创建一个'完美'随机二叉树(is_perfect =True)调用的代码:

def build(values):
    # 创建‘节点’列表
    nodes = [None if v is None else Node(v) for v in values]

    for index in range(1, len(nodes)):
        node = nodes[index]
        if node is not None:
            # 获取父节点索引
            parent_index = (index - 1) // 2
            parent = nodes[parent_index]
            if parent is None:
                raise NodeNotFoundError(
                    'parent node missing at index {}'.format(parent_index))
            setattr(parent, 'left' if index % 2 else 'right', node)

    return nodes[0] if nodes else None

按广度优先的顺序一一添加节点,非叶节点不存在为空的情况。

2.3 BST

通过binarytree中的bst以及is_bst可以轻松的创建和判断二叉搜索树。先简单看一下二叉搜索树的概念哦:每个节点的值都比左子节点的大,比右子节点的小。

看一下bst的使用哦。

>>>from binarytree import bst
>>>my_tree = bst(height=3, is_perfect=False)
>>>print(my_tree)

        ____8_____
       /          \
    __3__       ___11___
   /     \     /        \
  1       6   9         _13
 / \     /     \       /   \
0   2   5       10    12    14

源码如下:

def bst(height=3, is_perfect=False):
    _validate_tree_height(height)
    if is_perfect:
        return _generate_perfect_bst(height)

    values = _generate_random_node_values(height)
    leaf_count = _generate_random_leaf_count(height)

    root = Node(values.pop(0))
    leaves = set()

    for value in values:
        node = root
        depth = 0
        inserted = False

        while depth < height and not inserted:
            attr = 'left' if node.value > value else 'right'
            if getattr(node, attr) is None:
                setattr(node, attr, Node(value))
                inserted = True
            node = getattr(node, attr)
            depth += 1

        if inserted and depth == height:
            leaves.add(node)
        if len(leaves) == leaf_count:
            break

    return root

与2.2tree的创建的不同点就是,这里的attr不再随机生成,而是根据节点值的大小设定,然后根据attr来决定是插入成左子节点还是右子节点。

看一下perfect bst tree的创建哦:

def _generate_perfect_bst(height):
    max_node_count = 2 ** (height + 1) - 1
    node_values = list(range(max_node_count))
    return _build_bst_from_sorted_values(node_values)


def _build_bst_from_sorted_values(sorted_values):
    if len(sorted_values) == 0:
        return None
    mid_index = len(sorted_values) // 2
    root = Node(sorted_values[mid_index])
    root.left = _build_bst_from_sorted_values(sorted_values[:mid_index])
    root.right = _build_bst_from_sorted_values(sorted_values[mid_index + 1:])
    return root

感觉这段代码十分巧妙,首先在_generate_perfect_bst中生成有序数组,然后在_build_bst_from_sorted_value不断将中值以下的值和中值以上的值放入左右节点进行递归。

3. 二叉树属性源码解析

3.1 中序遍历

中序遍历的顺序是先左-再父-后右,看一下实现:

>>> from binarytree import Node
>>>
>>> root = Node(1)
>>> root.left = Node(2)
>>> root.right = Node(3)
>>> root.left.left = Node(4)
>>> root.left.right = Node(5)
>>> root.inorder
[Node(4), Node(2), Node(5), Node(1), Node(3)]

实现源码如下:

def inorder(self):
    node_stack = []
    result = []
    node = self

    while True:
        if node is not None:
            node_stack.append(node)
            # 取左
            node = node.left
        elif len(node_stack) > 0:
            # 弹出
            node = node_stack.pop()
            result.append(node)
            # 取右
            node = node.right
        else:
            break

    return result

这里用了压栈的方式实现了二叉树的中序遍历。

3.2 先序遍历

先序遍历的顺序是先父-再左-后右,看一下实现:

>>> from binarytree import Node
>>>
>>> root = Node(1)
>>> root.left = Node(2)
>>> root.right = Node(3)
>>> root.left.left = Node(4)
>>> root.left.right = Node(5)
>>> root.inorder
[Node(1), Node(2), Node(4), Node(5), Node(3)]

实现源码:

def preorder(self):
    node_stack = [self]
    result = []

    while len(node_stack) > 0:
        node = node_stack.pop()
        result.append(node)

        if node.right is not None:
            node_stack.append(node.right)
        if node.left is not None:
            node_stack.append(node.left)

    return result

同样适用栈实现,这里不做赘述。

3.3 后序遍历

后序遍历的顺序:先右-再左-后父,看一下实现:

>>> from binarytree import Node
>>>
>>> root = Node(1)
>>> root.left = Node(2)
>>> root.right = Node(3)
>>> root.left.left = Node(4)
>>> root.left.right = Node(5)
>>> root.inorder
[Node(4), Node(5), Node(2), Node(3), Node(1)]

实现源码:

def postorder(self):
    node_stack = []
    result = []
    node = self

    while True:
        while node is not None:
            if node.right is not None:
                node_stack.append(node.right)
            node_stack.append(node)
            node = node.left

        node = node_stack.pop()
        if (node.right is not None and
                    len(node_stack) > 0 and
                    node_stack[-1] is node.right):
            node_stack.pop()
            node_stack.append(node)
            node = node.right
        else:
            result.append(node)
            node = None

        if len(node_stack) == 0:
            break

    return result
3.4 平衡二叉树判断

平衡二叉树指的是任何节点左右子节点高度差不大于1的二叉树,这里可以通过is_balanced来判断:

>>> from binarytree import Node
>>>
>>> root = Node(1)
>>> root.left = Node(2)
>>> root.right = Node(3)
>>> root.left.left = Node(4)
>>> root.left.right = Node(5)
>>> root.is_balanced
True

看一下源码:

def _is_balanced(root):
    if root is None:
        return 0
    left = _is_balanced(root.left)
    if left < 0:
        return -1
    right = _is_balanced(root.right)
    if right < 0:
        return -1
    return -1 if abs(left - right) > 1 else max(left, right) + 1

def is_balanced(self):
    return _is_balanced(self) >= 0

这里使用了递归的方式来判断左右子节点的高度差并计算节点的高度,若发现某阶段左右子节点的高度差大于1则返回-1,表示不是平衡二叉树;否则会继续通过后序遍历的方式计算节点高度,直到计算出根节点的高度,此时若根节点的高度大于0,则该二叉树是平衡的。

binarytree库中的功能远远不止本文所述,跟着源码学习二叉树,还算是一种不错的体验~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容