Skip to content

Lec 6 二叉树

一种几乎优于所有目前我们所了解的数据结构——二叉树

  • 二叉树
  • 应用: 集合
  • 应用: 序列

二叉树

二叉树的每个节点有三个指针,也是一种基于指针的数据结构。节点的表示: Node.{item, parent, left, right}

例子:

image-20250410021105158

性质

在一棵二叉树中,树的根节点没有父节点(例如:<A>),树的叶节点没有子节点(例如:<C>、<E> 和 <F>)

  • <R>定义深度<R>(树中某个节点的范畴): 树中以 <R> 为根的节点 <X> 的深度(depth(<X>))为从 <X> 到 <R> 路径的长度 = #祖先的个数
    • 比如<A>的深度是0, <B>,<C>的深度是1
  • <R>定义高度<R>(树(包括子树)的范畴):定义节点 <X> 的高度(height(<X>))为以 <X> 为根的子树中任何节点的最大深度
    • 比如, <C>,<E>,<F>的高度为0, 而<A>的高度为3

<R>关键思想<R>: 我们希望设计的操作在树的高度为 h 时能在 O(h) 时间内完成,并保持 h = O(log n),以保证效率。二叉树具有一种天然的顺序,称为<R>遍历顺序(traversal order)/ 中序遍历<R>节点 <X> 的左子树中的每个节点都在 <X> 之前,节点 <X> 的右子树中的每个节点都在 <X> 之后。这种遍历顺序通过递归实现:先列出左子树,再列出自身,最后列出右子树。每个节点只访问一次,整体运行时间为 O(n),例如,图中这棵树的遍历顺序为(<F>,<D>,<B>,<E>,<A>,<C>)

虽然目前遍历顺序还没有与节点中的数据绑定具体语义,但之后我们会利用它来实现诸如序列或集合等抽象数据接口。

遍历操作

二叉树的节点有一个自然顺序,这基于我们区分一个孩子为左孩子,另一个孩子为右孩子的事实。我们根据以下隐含特征来定义二叉树的遍历顺序:

  • 节点 <X> 的左子树中的所有节点都在遍历时出现在 <X> 之前;
  • 节点 <X> 的右子树中的所有节点都在遍历时出现在 <X> 之后。

<R>找第一个(Find first)<R>:即找到节点 <X> 子树遍历顺序中的第一个节点(与找最后一个节点是对称的)

  • 如果 <X> 有左孩子,递归返回左子树中的第一个节点。
  • 否则,<X> 是第一个节点,因此返回它。
  • 运行时间为 O(h),其中 h 是树的高度。
  • 例如:<A> 子树中的第一个节点是 <F>。

<R>找后继(FInd successor)<R>:找到节点 <X> 在遍历顺序中的后继节点(<R>与找前驱节点是对称的<R>)

  • 如果 <X> 有右孩子,返回右子树中的第一个节点。
  • 否则,向上遍历树(node = node.parent),直到找到一个祖先节点,<X>是在祖先节点左孩子上(node == node.parent.left)。这个祖先就是 <X> 的后继节点,将其返回。(假如要对一个没有右孩子的节点求后继,那我们说它肯定需要爬上树去找)
  • 运行时间为 O(h),其中 h 是树的高度。
  • 例如:<B> 的后继是 <E>,<R><E> 的后继是 <A><R>,<C> 的后继是 None。
  • 根据对称性, 找前驱节点
    • 如果<X>有左孩子,返回左子树的最后一个节点
    • 否则,向上遍历树, 向上遍历树(node = node.parent),直到找到一个祖先节点,<X>是在祖先节点右孩子上(node == node.parent.right)。这个祖先就是 <X> 的前驱节点,将其返回。

动态操作

动态操作允许我们通过插入或删除叶子结点来修改二叉树结构,从而保持树的遍历顺序。比如,若要在遍历顺序中将新节点 <Y>插入到节点 <X> 之后(在 <X> 之前插入是对称操作),有两种情况如下。运行时间:O(h),其中 h 是树的高度。

  • 如果 <X> 没有右孩子:将 <Y>作为 <X> 的右孩子。
  • 如果 <X> 有右孩子:将 <Y>作为 <X> 的后继节点的左孩子(后继节点不能有左孩子)。【就是在右孩子树找第一个,同理,如果是在其之前插入, 就找左子树的最后一个】

image-20250410023426800

从树中删除节点 <X> 的操作会根据 <X> 是否为叶节点而有所不同。如果 <X> 是叶节点,直接将其从父节点中断开即可,操作结束。若 <X> 不是叶节点,说明它至少有一个子节点,此时需借助遍历顺序中的前驱或后继节点来完成删除,有两种情况如下:

  • 如果 <X> 有左孩子:与 <X> 的前驱节点交换数据项(node.item),然后递归地从前驱节点的位置继续删除。
  • 如果 <X> 没有左子节点但有右子节点,则找到它的后继节点,与其交换数据,然后递归地从后继节点的位置继续删除。

这种方式保证了我们最终删除的是一个叶节点,从而简化结构调整,且整个过程的运行时间为 O(h),其中 h 是树的高度。

image-20250410023651042

应用:集合

一个关键的想法是使用集合二叉树(也称为二叉搜索树,Binary Search Tree,简称 BST),其核心在于将<R>中序遍历的顺序<R>对应为<R>按键值递增排序的顺序<R>。等价于BST属性:对于每个节点,左子树中的每个键 ≤ 节点的键 ≤ 右子树中的每个键。

借助这个性质,我们可以像二分查找一样然后可以在O(h)时间内像二分查找一样在节点<X>的子树中找到键为k的节subtree_find(k)

  • 如果k小于<X>的键,在左子树中递归(或返回None)
  • 如果k大于<X>的键,在右子树中递归(或返回None)
  • 否则,返回存储在<X>的项

应用:序列

一个核心想法是构建<R>序列二叉树<R>:其遍历顺序对应于序列中的元素顺序。若要在某个子树中查找第 i 个节点(即subtree_at(i) 操作),最直接的方法是顺序遍历整个子树,但这会耗费 O(n) 时间,效率很低。

更高效的做法是:在每个节点维护其子树的大小,使得我们可以通过“大小信息”在 O(h) 时间内定位第 i 个节点。具体步骤如下:

  • 假设当前节点的左子树大小为nL
  • 如果i < nL,说明第 i 个节点在左子树中,递归进入左子树;
  • 如果i > nL,说明第 i 个节点在右子树中,递归进入右子树,并更新索引为i=inL1
  • 否则,i = nL​,你已经到达了所需的节点!

为了支持这种查找方式,每个节点通过“增强(augmentation)”方式添加一个 size 字段,记录其子树中节点的总数。在插入叶节点时,沿祖先路径将 size 值加 1;删除时则减 1,两个操作的时间复杂度都是 O(h)。

一旦我们能高效实现 subtree_at(i) 操作,其他序列操作(如插入、删除、切片等)也就能自然地建立在其上。虽然朴素地构建一棵序列树可能需 O(nh) 时间,但实际上可以优化为 O(n) 完成构建。

代码实现

binary_node.py

python
# binary_node.py
class Binary_Node:
    def __init__(self, x):              # O(1)
        self.item = x
        self.left = None
        self.right = None
        self.parent = None
        #self.subtree_update()      # lec 7
        
    def subtree_iter(self):             # O(n)
        if self.left: 
            yield from self.left.subtree_iter()
        yield self
        if self.right: 
            yield from self.right.subtree_iter()
    
    def subtree_first(self):            # O(h)
        if self.left: 
            return self.left.subtree_first()
        else:
            return self
    
    def subtree_last(self):             # O(h)
        if self.right:
            return self.right.subtree_last()
        else:
            return self
    
    def successor(self):                # O(h)
        if self.right:
            return self.right.subtree_first()
        while self.parent and (self is self.parent.right):
            self = self.parent
        return self.parent
    
    def predecessor(self):              # O(h)
        if self.left:
            return self.left.subtree_first()
        while self.parent and (self is self.parent.left):
            self = self.parent
        return self.parent
    
    def subtree_insert_after(self, B):  # O(h)
        if not self.right:
            self.right = B
            B.parent = self
        else:
            C = self.right.subtree_first()
            C.left = B
            B.parent = C
        self.maintain()
        
    def subtree_insert_before(self, B): # O(h)
        if not self.left:
            self.left = B
            B.parent = self
        else:
            C = self.left.subtree_last()
            C.right = B
            B.parent = C
        self.maintain()

    def subtree_delete(self):          # O(h)
        if self.left or self.right:
            B = None
            if self.left:
                B = self.predecessor()
            else:
                B = self.successor()
            self.item, B.item = B.item, self.item
            return B.subtree_delete()
        if self.parent:
            if self.parent.left is self:
                self.parent.left = None
            else:
                self.parent.right = None
            self.maintain()
        return self

    def maintain(self):
        pass

binary_tree.py

python
from binary_node import Binary_Node
class Binary_Tree:
    def __init__(self, Node_Type = Binary_Node):
        self.root = None
        self.size = 0
        self.Node_Type = Node_Type
    
    def __len__(self): 
        return self.size
    def __iter__(self):
        if self.root:
            for A in self.root.subtree_iter():
                yield A.item

bst_node.py

python
"""
用二叉树实现Set的接口,用二叉树的遍历顺序来存储递增的key,
这种性质的树称为二叉搜索树(BST)
下面为二叉搜索树节点数据结构
"""
from binary_node import Binary_Node
class BST_Node(Binary_Node):
    def subtree_find(self, k):              # O(h)
        if k < self.item.key:
            if self.left: 
                return self.left.subtree_find(k)
        elif k > self.item.key:
            if self.right:
                return self.right.subtree_find(k)
        else:
            return self
        return None
    
    def subtree_find_prev(self, k):         # O(h)
        if k < self.item.key:
            if self.left:
                return self.left.subtree_find_prev(self, k)
            else:
                return None
        elif self.right:
            B = self.right.subtree_find_prev(k)
            if B:
                return B
        return self

    def subtree_find_next(self, k):         # O(h)   
        if k > self.item.key:
            if self.right:
                return self.right.subtree_find_prev(self, k)
            else:
                return None
        elif self.left:
            B = self.left.subtree_find_prev(k)
            if B:
                return B
        return self
    
    # 与二叉树的方法略有不同,因此需要重写
    def subtree_insert(self, B):               # O(h)
        if B.item.key < self.item.key:
            if self.left: 
                self.left.subtree_insert(B)
            else:
                self.subtree_insert_before(B)
        elif B.item.key > self.item.key:
            if self.right:
                self.right.subtree_insert(B)
            else:
                self.subtree_insert_after(B)
        else:
            self.item = B.item

set_bst.py

python
"""
用二叉树实现Set的接口,用二叉树的遍历顺序来存储递增的key,
这种性质的树称为二叉搜索树(BST)
下面为二叉搜索树结构以及接口实现
"""
from binary_tree import Binary_Tree
from bst_node import BST_Node

class Set_Binary_Tree(Binary_Tree): # 二叉搜索树
    def __init__(self):
        super().__init__(BST_Node)
    
    def iter_order(self):
        yield from self
    
    def build(self, X):
        for x in X:
            self.insert(x)
    
    def find_min(self):
        if self.root:
            return self.root.subtree_first().item
        
    def find_max(self):
        if self.root:
            return self.root.subtree_last().item
        
    def find_next(self, k):
        if self.root:
            node =  self.root.subtree_find_next(k)
            if node:
                return node.item

    def find_prev(self, k):
        if self.root:
            node = self.root.subtree_find_prev(k)
            if node:
                return node.item
    
    def find(self, k):
        if self.root:
            node = self.root.subtree_find(k)
            if node:
                return node.item

    def insert(self, x):
        new_node = self.Node_Type(x)
        if self.root:
            self.root.subtree_insert(new_node)
            # 检查是否存在相同key值,如果是更新value,并返回false
            if new_node.parent is None: 
                return False 
        else:
            self.root = new_node
        self.size += 1
        return True
    
    def delete(self, k):
        assert self.root
        node = self.root.subtree_find(k)
        assert node
        ext = node.subtree_delete()
        if ext.parent is None:
            self.root = None
        self.size -= 1
        return ext.item