数据结构 | 树

141 阅读14分钟

树与二叉树

定义

树是n个节点的有限集。当n=0时,称为空树。在任意一颗非空树中都满足:

  1. 有且仅有一个称为根的节点
  2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,...TmT_1,T_2,...T_m,其中每个集合本身又是一棵树,并且称为根的子树

显然,树的定义本身递归,在树的定义中用到了自身,所以树本身就是一种帝国的数据结构。

树作为一种逻辑结构,同时也是一种分层结构,具有一下特点:

  1. 除了根结点,任何一个结点都有且仅有一个前驱
  2. 树中所有节点可以有0个或多个后继节点

因此nn个节点的树中有n1n-1条边 77681220-334c-4a90-bb62-5f314cebcc1c.png

常用术语

  1. 根节点

    位于树顶层的节点,是树中唯一没有双亲的节点

  2. 叶节点

    没有子节点的节点,其两个指针指向None

  3. 连接两个节点的线段,在代码中即为节点引用(指针)

  4. 节点所在层

    从顶至底递增,根节点所在层位1

  5. 节点的度

    节点的子节点的数量,也就是节点有几个分支,m叉树,节点度的范围为0m0 - m

  6. 树的高度

    从根节点到最远节点所经过的节点的数量

  7. 节点的深度

    从根节点到该节点所经过的节点的数量

  8. 节点的高度

    从距离最远该节点最远的叶节点到该节点经过的边的长度

  9. 有序树

    逻辑上看,树中结点的各子树从左至右是有次序的,不能互换

  10. 无序树

    逻辑上看,树中结点的各子树从左至右是无次序的,可以互换

性质

假设根节点为第1层

  1. 结点数=总度数+1

  2. 树的度——各结点的度的最大值

    mm叉树——每个节点最多只能有mm个分支

度为mm的树mm叉树
任意结点的度<=mm(最多mm个分支)任意结点的度<=mm(最多mm个分支)
至少有一个结点度=mmmm个分支)允许所有结点的度都<mm
一定是非空树,至少有mm+1个结点可以是空树
  1. 度为 mm 的树第 ii至多mi1m^{i-1} 个结点

    mm叉树第 ii 层至多有mi1m^{i-1}个结点

  2. 高度为 hhmm叉树至多mh1m1\frac{m^h-1}{m-1} 个总节点

  3. 高度为 hhmm叉树至少hh 个结点

    高度为 hh ,度为 mm 的树至少h+m1h+m-1 个结点

  4. 具有 nn 个总节点的 mm 叉树的最小高度logm(n(m1)+1)\log _m\left( n\left( m-1 \right) +1 \right) 向上取整

二叉树

二叉树,是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。

基本单元是节点,每个节点包含值,左子节点引用,右字节引用

class TreeNode:
    """二叉树节点类"""
    def __init__(self,value):
        self.val = value
        # 左子树
        self.left = None
        # 右子树
        self.right = None
The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.

二叉树基本操作

初始化二叉树
n1 = TreeNode(1)
n2 = TreeNode(2)
n3 = TreeNode(3)
n1.left = n2
n1.right = n3
n1.left.val
2
插入与删除节点

通过修改指针实现二叉树逻辑上的变化

# 插入与删除节点
p = TreeNode(0)
# 在 n1 -> n2 中间插入节点 P
n1.left = p
p.left = n2
# 删除节点 P
n1.left = n2

二叉树种类

1. 完美二叉树

又叫满二叉树,以0为根节点层,高度为hh时,该层有2h2^{h}个节点,总节点数有2h+112^{h+1}-1

  1. 只有最后一层有叶子结点
  2. 不存在度为1的结点
  3. 按层序从1开始编号,结点ii的左分支为2i2i,右分支为 2i+12i+1;结点ii的父节点为[i/2][i/2] (如果有的话)
2. 完全二叉树

该树可以看作是在满二叉树的基础上去掉若干个编号更大的节点形成的树

  1. 只在最后两层可能有叶子节点
  2. 最多只有一个度为1的结点
  3. 按层序从1开始编号,结点ii的左分支为 2i2i,右分支为 2i+12i+1;结点i的父节点为[i/2][i/2] (如果有的话)
  4. i<[n/2]i<[n/2]为分支结点,i>[n/2]i>[n/2]为叶子结点
3. 完满二叉树

除了叶节点之外,其余所有节点都有两个子节点

4. 平衡二叉树

该种树提供更高的搜索效率

  • 任意节点的左子树和右子树的高度之差的绝对值不超过 1
5. 二叉排序(搜索)树

该树可用于元素的排序、搜索

  1. 左子树上所有结点的关键字均小于根结点的值;

  2. 右子树上所有结点的关键字均大于根结点的值;

    左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值

  3. 左子树和右子树又各是一棵二叉排序树

二叉树存储结构

顺序存储结构

将所有节点按照层序遍历顺序存储在数组中,则每个节点都对应唯一的数组索引

  • 映射公式: 若某节点的索引为ii,则该节点的左节点的索引为2i+12i+1, 右节点的索引为2i+22i+2

可以通过索引找到任意节点的左右子节点

可以使用数组来存储完美二叉树、完全二叉树等,在使用数组表示时,若对应位置没有元素值,需要设置为编程语言的空(None/null),在完全二叉树中,空值只出现在底层且靠右位置,因此空一定出现在层序遍历的末尾,可以省略所有的空值。

class ArrayBinaryTree:
    """数组表示下的二叉树类"""
    def __init__(self, arr):
        self._tree = list(arr)

    def size(self):
        return len(self._tree)

    def value(self, i):
        """索引i的值"""
        if 0 < i < self.size():
            return None
        return self._tree[i]

    def left(self, i):
        """左子树节点"""
        return 2 * i + 1

    def right(self, i):
        """右子数节点"""
        return 2 * i + 2

    def parent(self, i):
        """父节点"""
        return (i - 1) // 2

    def level_order(self):
        """层次遍历"""
        self.res = []
        for i in range(self.size()):
            if self.value(i) is not None:
                self.res.append(self.value(i))
        return self.res

    def dfs(self, i, order):
        """深度优先遍历"""
        if not self.value(i):
            return
        # 前序遍历
        if order == 'pre':
            self.res.append(self.value(i))
        self.dfs(self, self.left(), order)
        # 中序遍历
        if order == 'in':
            self.res.append(self.value(i))
        self.dfs(self, self.right, order)
        # 后序遍历
        if order == 'post':
            self.res.append(self.value(i))

    def pre_order(self):
        self.res = []
        self.dfs(0, order='pre')
        return self.res

    def in_order(self):
        self.res = []
        self.dfs(0, order='in')
        return self.res

    def post_order(self):
        self.res = []
        self.dfs(0, order='post')
        return self.res
优点
  • 数组存储在来连续空间,对缓存友好,访问和遍历速度快
  • 不需要存储指针,节省了空间
  • 允许随机访问节点
缺点
  • 数组存储需要连续内存空间,不适合大量数据操作
  • 增删节点通过数组添加和删除操作,时间复杂度高,效率低
  • 二叉树中存在大量None时,数组中包含真正数据的节点比重较低,造成了空间利用率低
链式存储结构

用链表表示一棵树,即用链来指示元素的逻辑关系。

  • 通常的方法是将链表中的每个节点由三个域组成,分为数据域、左右指针域。

左右指针域用来指出该节点的左右孩子节点所在链节点的存储地址。

  • 链式结构又分为二叉链和三叉链。

ec2ec210-951c-455a-93d8-1fc5a6bda3a4.png

二叉树的遍历

在基于链表的存储结构上,可以通过指针逐个访问节点实现遍历,但是由于树的非线性结构,使得遍历更加复杂,借助搜索算法实现

常见遍历方式有层序遍历、前序遍历、中序遍历、后序遍历

前序遍历:根->左->右

中序遍历:左->根->右

后序遍历:左->右->根

层次遍历

从顶到底逐层遍历节点,本质上属于广度优先遍历(BFS)

复杂度分析:

  • 时间复杂度:O(n)O(n)nn为节点个数
  • 空间复杂度:O(n)O(n),最差满二叉,到底层队列中还有(n+1)/2(n+1)/2个节点
from collections import deque

def level_order(root):
    """层次遍历二叉树"""
    # 双端队列
    queue = deque()
    # 从根节点开始
    queue.append(root)
    res = []
    while queue:
        # 队首出队一个元素
        node = queue.popleft()
        # 出队节点加入存储数组
        res.append(node.val)
        if node.left is not None:
            # 左子树入队
            queue.append(node.left) 
        if node.right is not None:
            # 右子树入队
            queue.append(node.right)
    return res
level_order(n1)
[1, 2, 3]
前、中、后序遍历

属于深度优先遍历

复杂度分析:

  • 时间复杂度:O(n)O(n)nn为节点个数
  • 空间复杂度:O(n)O(n),二叉树退化为链表,总深度为nn,占用O(n)O(n)栈帧空间
def pre_order(root, res):
    """前序遍历"""
    if root is None:
        return
    # 前序遍历:根左右
    res.append(root.val)
    pre_order(root.left, res)
    pre_order(root.right, res)
    return res

def in_order(root, res):
    """中序遍历"""
    if root is None:
        return
    # 中序遍历:左根右
    in_order(root.left,res)
    res.append(root.val)
    in_order(root.right,res)
    return res

def post_order(root, res):
    """后序遍历"""
    if root is None:
        return 
    # 后序遍历:左右根
    post_order(root.left, res)
    post_order(root.right, res)
    res.append(root.val)
    return res
# 前序遍历
res = []
pre_order(n1,res)
res
[1, 2, 3]
# 中序遍历
res = []
in_order(n1,res)
res
[2, 1, 3]
# 后序遍历
res = []
post_order(n1, res)
res
[2, 3, 1]

二叉搜索树

性质

  1. 对于根节点,左子树所有节点的值 < 根节点的值 < 右子树所有节点的值
  2. 任意节点的左右子树也是二叉搜索树

51a6865b-647d-42ce-a7a1-9872ce4a6b1a.png 6dd9b987-c3f1-4329-a36e-71d8d2b6bf9e.png

操作

查找节点

给定目标节点值num,根据二叉搜索树的性质进行查找方便许多,设节点指针cur

  • 若cur.val < num,说明目标节点在cur的右子树中,下一步cur = cur.right
  • 若cur.val > num,说明目标节点在cur的左子树中,下一步cur = cur.left
  • 若cur.val = num,说明找到了目标节点,退出查找

插入节点

给定一个待插入元素num,插入元素时需要考虑保持二叉搜索树的性质,操作如下:

  1. 查找插入位置:从根节点出发,根据当前节点值和num的大小关系向下循环搜索,直到越过叶节点时结束循环
  2. 在该点插入节点:初始化节点num,将该节点置于None的位置

时间复杂度O(logn)O(logn)

删除节点

先在二叉树中查找到目标节点,再将其删除。删除节点过后,需要保证该二叉树满足二叉搜索树的性质

根据目标节点的子节点数量,分为0、1、2三种情况,执行对应的删除节点操作

  • 当待删除节点度为0时,表示该节点时叶节点,直接删除
  • 当待删除节点度为1时,将待删除节点替换为子节点即可
  • 当待删除节点度为2时,无法直接删除,需要使用一个节点替换该节点,
    • 由于二叉搜索树的性质,这个节点可以是右子树最小节点或左子树最大节点
      • 选择右子树最小节点:
      • 找到待删除节点在“中序遍历序列”中的下一个节点,记为temp
      • 用temp值覆盖待删除节点的值,并在树种递归删除节点tem

二叉树中序遍历是升序的

时间复杂度O(logn)O(logn)

class BinarySearchTree:
    """左子树中所有节点的值 < 根节点的值 < 右子树中所有节点的值"""
    
    def __init__(self, root):
        self._root = root
        
    def search(self, num):
        """二叉搜索树查找节点"""
        cur = self._root
        while cur is not None:
            if cur.val < num: # 当前值小于寻找值:右子树
                cur = cur.right
            elif cur.val > num: # 当前值大于寻找值:左子树
                cur = cur.left
            else: # 值相等
                break
        return cur

    def insert(self, num):
        """插入节点"""
        # 树空
        if not self._root:
            self._root = TreeNode(num)
            return 
        cur, pre = self._root, None
        # 查找插入点
        while cur is not None:
            pre = cur
            if cur.val < num: # 当前值小于插入值:右子树
                cur = cur.right
            elif cur.val > num: # 当前值大于插入值:右子树
                cur = cur.left
            else: # 相等
                print(f'已存在{num}值')
                return
        # 找到插入点后
        node = TreeNode(num)
        if pre.val < num:
            pre.right = node
        else:
            pre.left = node

    def remove(self, num):
        """删除节点"""
        # 数空
        if not self._root:
            return
        cur, pre = self._root, None
        # 查找删除的节点
        while cur is not None:
            # 找到待删除节点,跳出循环
            if cur.val == num:
                break
            pre = cur
            # 待删除节点在 cur 的右子树中
            if cur.val < num:
                cur = cur.right
            # 待删除节点在 cur 的左子树中
            else:
                cur = cur.left
        # 没有找到删除节点,没有值
        if cur is None:
            print(f'未找到{num}节点')
            return

        # 根据删除节点的子节点按情况删除
        if cur.left is None or cur.right is None: # 子节点数量一个或没有
            child = cur.left or cur.right # 选择有效子节点
            if cur != self._root: # 非根节点
                if pre.left == cur:
                    pre.left = child
                else:
                    pre.right = child
            else: # 根节点
                self._root = child
        else: # 子节点个数为2
            # 找到比当前值大的最小数
            temp = cur.right
            while temp.left is not None: # 左子树遍历下去找到大于当前值的最小树
                temp = temp.left
            self.remove(temp)
            cur.val = temp.val
root = TreeNode(10)
root.left = TreeNode(5)
root.right = TreeNode(15)

bst = BinarySearchTree(root)
result = bst.search(5)
print(result.val if result else "Not found")  # Output: 5


result = bst.insert(20)
res = level_order(root)
res

bst.remove(20)
res = level_order(root)
res
5





[10, 5, 15]

常见应用

  • 系统中的多级索引,实现高效查找、插入、删除
  • 某些算法底层数据结构
  • 存储数据流,保持其有序

AVL树

又叫平衡二叉树,同时满树二叉搜索树的条件又再每次操作后进行变形保证高效操作性能

class TreeNode:
    """AVL 树节点类"""
    def __init__(self, val: int):
        self.val= val      # 节点值
        self.height = 0    # 节点高度
        self.left = None   # 左子节点引用
        self.right = None  # 右子节点引用

节点高度

  • 指从该节点到它的最远叶节点的距离,及所经过的‘边’的数量
  • 叶节点高度为0
  • 空节点高度为-1
def get_height(node):
    if node is not None: # 非叶节点
        return node.height
    return -1 # 空节点

def update_height(node):
    # 节点高度等于最高子树高度 + 1
    node.height = max([get_height(node.left), get_height(node.right)]) + 1
n = TreeNode(1)
n1 = TreeNode(2)
n.left = n1
update_height(n)
get_height(n)

节点平衡因子

  • 左子树高度-右子树高度
  • 空节点平衡因子为0
  • 左子树和右子树高度之差的绝对值不超过1
  • 左子树和右子树均为平衡二叉树
def balance_factor(node):
    """获取平衡因子"""
    if node is None:
        return 0
    return get_height(node.left) - get_height(node.right) # 左子树高度-右子树高度
balance_factor(n1)

AVL树旋转

右旋

对于平衡因子首先大于1的进行旋转

    4
   / \
  3   5
 /
1

/ 0 对于值为3的节点,它的平衡因子为2,破坏了平衡

n1 = TreeNode(4)
n2 = TreeNode(3)
n3 = TreeNode(5)
n4 = TreeNode(1)
n5 = TreeNode(0)
n1.left = n2
n1.right = n3
n2.left = n4
n4.left = n5
update_height(n1)
update_height(n2)
update_height(n4)
balance_factor(n2)
def right_rotate(node):
    """右旋操作,只是某个特例"""
    child = node.left
    grand_child = child.right
    child.right = node
    node.left = grand_child
    update_height(node)
    update_height(child)
    return child
左旋
def left_rotate(node):
    """左旋操作"""
    child = node.right
    grand_chile = child.left
    child.left = node
    node.right = grand_chile
    update_height(node)
    update_height(child)
    return child
The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.
先左旋再右旋
先右旋再左旋
汇总,根据节点不同失衡状况进行旋转
def rotate(node):
    """根据不同情况进行不同旋转"""
    balance = balance_factor(node)
    # 左偏树
    if balance > 1:
        # 包括两种方式:右旋和左右旋
        if balance(node.left) >= 0:
            # 右旋
            return right_rotate(node)
        else:
            # 先左再右了
            node.left = left_rotate(node)
            return right_rotate(node)
    # 右偏树
    elif balance < -1:
        # 包括两种:左旋和右左旋
        if balance(node.right) >= 0:
            # 左旋
            return left_rotate(node)
        else:
            # 右左旋
            node.right = right_rotate(node)
            return left_rotate(node)
    # 否者为平衡树无需调整
    return node

常用操作

插入

插入节点与二叉搜索树相似,只是插入某个节点后会使该节点到根节点路径上某些节点失衡,所以在插入节点后需要从这个节点开始自底而上执行旋转操作,使所有节点恢复平衡

def insert(root, val):
    """AVL树插入节点"""
    root = insert_helper(root,val)

def insert_helper(root, val):
    """递归插入节点"""
    if not root:return TreeNode(val)
    if val < root.val: # 左子树中递归
        insert_helper(root.left, val)
    elif val > root.val: # 右子树中递归
        insert_helper(root.right, val)
    else: # 重复节点不插入
        return
    update_height(root) # 更新高度
    rotate(root) # 旋转节点使平衡
删除

删除节点,同样会是插入节点与根节点之间路径上出现一系列的节点失衡,删除节点之后需要自底而上旋转节点至平衡

def delete(root, val):
    if not root:return None
    if val < root.val: # 左子树中递归
        delete(root.left, val)
    elif val > root.val:
        delete(root.right, val)
    else:
        # 节点数为1或0
        if not root.left or not root.right:
            child = root.left or root.right
            if not child: # 子节点为0
                return None
            else: # 子节点为1
                root = child
        else: # 节点为2
            # 首先是=的情况必然在右子树中找值
            temp = root.right
            # 右子树中最小值
            while not temp.left:
                temp = temp.left
            root.right = delete(root.right, val)
            root.val = temp.val
    update_height(root)
    rotate(root)
查找节点

查找节点与二叉搜索树一直,仅是查找不影响AVL树的平衡性

应用

  • 构建数据库中的索引
  • 组织和存储大型数据,高频查找、低频增删