数据结构——树

如果说数组、链表是"线性"世界的代表,那么树(Tree)则开启了"层次化"结构的大门。树是一种非线性 的数据结构,它通过节点之间的父子关系,优雅地表达了层次分明的数据组织方式。从文件系统到网页的 DOM 树,从数据库索引到人工智能的决策树,树的影子无处不在。

本文将从树的基本概念入手,逐步深入到二叉树、二叉搜索树、树的遍历方式,并简要介绍平衡树和堆等变体,最后总结树的应用场景和复杂度。让我们一起走进这个层次化的递归世界。

一、什么是树?

树是由 n(n ≥ 0)个节点 组成的有限集合。当 n = 0 时,称为空树。当 n > 0 时,存在一个特殊的节点称为根节点 (Root),其余节点被划分为 m(m ≥ 0)个互不相交的有限集合,每个集合本身又是一棵树,称为根节点的子树(Subtree)。

这种定义具有递归性:树由根和若干子树组成,而子树又是树。正是这种递归特性,使得很多树相关的算法天然适合用递归实现。

树的基本术语

  • 节点:树中的基本元素,包含数据以及指向子节点的引用。

  • 根节点:树的最顶层节点,没有父节点。

  • 叶子节点:没有子节点的节点。

  • 父节点、子节点、兄弟节点:节点之间的相对关系。

  • 节点的度:节点拥有的子树的个数。

  • 树的度:树中所有节点的度的最大值。

  • 深度(Depth):从根节点到该节点所经过的边的数量(根深度为 0)。

  • 高度(Height):从该节点到最远叶子节点的边的数量(叶子高度为 0)。

  • 层(Level):根节点在第 1 层(或第 0 层,视定义而定),其子节点在第 2 层,依此类推。

二、二叉树(Binary Tree)

二叉树是树结构中最重要、最基础的一种,它的每个节点最多只有两个子节点,分别称为左孩子右孩子。二叉树的性质使其在计算机科学中占据了核心地位。

二叉树的分类

  • 满二叉树:所有叶子节点都在同一层,且每个非叶子节点都有两个子节点。

  • 完全二叉树:除了最后一层外,其余层节点数达到最大,且最后一层的节点都靠左排列。

  • 平衡二叉树:任意节点的左右子树高度差不超过 1(AVL 树、红黑树等)。

二叉树的存储方式

链式存储

每个节点是一个对象,包含数据域和左右指针。这是最常用的方式。

python 复制代码
class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None
顺序存储(数组)

将二叉树按层序编号,存入数组。对于下标 i 的节点:

  • 左子节点下标:2i + 1

  • 右子节点下标:2i + 2

  • 父节点下标:(i - 1) // 2

这种方式适合完全二叉树,对普通二叉树会有空间浪费。

三、二叉搜索树(BST)

二叉搜索树(Binary Search Tree)是一种特殊的二叉树,它满足左小右大的性质:

  • 左子树上所有节点的值均小于根节点的值。

  • 右子树上所有节点的值均大于根节点的值。

  • 左右子树也分别为二叉搜索树。

这个性质使得 BST 支持高效的查找、插入和删除操作。

BST 的基本操作

1. 查找
python 复制代码
def search(root, target):
    if root is None or root.value == target:
        return root
    if target < root.value:
        return search(root.left, target)
    else:
        return search(root.right, target)
2. 插入
python 复制代码
def insert(root, value):
    if root is None:
        return TreeNode(value)
    if value < root.value:
        root.left = insert(root.left, value)
    elif value > root.value:
        root.right = insert(root.right, value)
    # 若相等,通常不插入或根据需求处理
    return root
3. 删除

删除节点较为复杂,分三种情况:

  • 叶子节点:直接删除。

  • 只有一个子节点:用子节点替代。

  • 有两个子节点:用右子树的最小节点(或左子树的最大节点)替换,并删除该最小节点。

python 复制代码
def delete(root, value):
    if root is None:
        return None
    if value < root.value:
        root.left = delete(root.left, value)
    elif value > root.value:
        root.right = delete(root.right, value)
    else:
        # 找到要删除的节点
        if root.left is None:
            return root.right
        if root.right is None:
            return root.left
        # 有两个孩子,找到右子树的最小节点
        min_node = find_min(root.right)
        root.value = min_node.value
        root.right = delete(root.right, min_node.value)
    return root

def find_min(root):
    while root.left:
        root = root.left
    return root

BST 的复杂度分析

  • 查找、插入、删除:平均 O(log n) ,最坏 O(n)(当树退化为链表时)。

  • 空间复杂度:O(n)。

四、树的遍历

遍历是树最核心的操作之一。根据访问根节点的顺序,分为深度优先遍历(DFS)和广度优先遍历(BFS)。

深度优先遍历(DFS)

前序遍历(Preorder):根 → 左 → 右
python 复制代码
def preorder(root):
    if root:
        print(root.value, end=' ')
        preorder(root.left)
        preorder(root.right)
中序遍历(Inorder):左 → 根 → 右

对 BST 进行中序遍历可以得到有序序列。

python 复制代码
def inorder(root):
    if root:
        inorder(root.left)
        print(root.value, end=' ')
        inorder(root.right)
后序遍历(Postorder):左 → 右 → 根

常用于释放树节点或计算表达式树。

python 复制代码
def postorder(root):
    if root:
        postorder(root.left)
        postorder(root.right)
        print(root.value, end=' ')

广度优先遍历(BFS):层序遍历

按层从上到下、从左到右依次访问。通常使用队列实现。

python 复制代码
from collections import deque

def level_order(root):
    if not root:
        return
    queue = deque([root])
    while queue:
        node = queue.popleft()
        print(node.value, end=' ')
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)

遍历的时间复杂度

所有遍历方式都访问每个节点一次,因此时间复杂度均为 O(n),空间复杂度取决于递归栈或队列的大小,最坏 O(n)。

五、平衡二叉树

BST 的性能依赖于树的平衡程度。为了解决最坏情况下的 O(n) 复杂度,人们发明了自平衡二叉搜索树,在插入和删除后通过旋转操作保持树的平衡。

AVL 树

AVL 树是最早发明的自平衡 BST,它通过维护每个节点的平衡因子(左子树高度 - 右子树高度)来确保任意节点的平衡因子绝对值 ≤ 1。插入或删除后,如果平衡被破坏,则通过左旋右旋等操作恢复平衡。

  • 查找、插入、删除均为 O(log n)

  • 实现相对复杂,但提供了严格平衡。

红黑树

红黑树是一种近似平衡的 BST,它通过为节点着色(红或黑)并满足五条性质来保证树的高度大致为 2log n。与 AVL 相比,红黑树的插入删除旋转次数更少,因此在实际应用中更广泛(如 Java 的 TreeMap、C++ 的 std::map 等)。

  • 查找、插入、删除均为 O(log n)

六、堆(Heap)

堆是一种特殊的完全二叉树,它满足堆序性

  • 最大堆:每个节点的值 ≥ 其子节点的值(根最大)。

  • 最小堆:每个节点的值 ≤ 其子节点的值(根最小)。

堆通常用数组存储,因为完全二叉树可以紧凑地映射到数组。堆常用于实现优先队列,支持在 O(log n) 时间内插入和删除最大(或最小)元素,在 O(1) 时间内获取最大(或最小)元素。

堆的基本操作(以最大堆为例)

  • 上浮(sift up):插入元素时,将其放在末尾,然后不断与父节点比较并交换,直到满足堆序。

  • 下沉(sift down):删除堆顶时,将末尾元素移到堆顶,然后不断与较大的子节点交换,直到满足堆序。

python 复制代码
class MaxHeap:
    def __init__(self):
        self.heap = []

    def push(self, val):
        self.heap.append(val)
        self._sift_up(len(self.heap) - 1)

    def pop(self):
        if not self.heap:
            return None
        self.heap[0], self.heap[-1] = self.heap[-1], self.heap[0]
        val = self.heap.pop()
        if self.heap:
            self._sift_down(0)
        return val

    def _sift_up(self, idx):
        parent = (idx - 1) // 2
        while idx > 0 and self.heap[idx] > self.heap[parent]:
            self.heap[idx], self.heap[parent] = self.heap[parent], self.heap[idx]
            idx = parent
            parent = (idx - 1) // 2

    def _sift_down(self, idx):
        n = len(self.heap)
        while True:
            left = 2 * idx + 1
            right = 2 * idx + 2
            largest = idx
            if left < n and self.heap[left] > self.heap[largest]:
                largest = left
            if right < n and self.heap[right] > self.heap[largest]:
                largest = right
            if largest == idx:
                break
            self.heap[idx], self.heap[largest] = self.heap[largest], self.heap[idx]
            idx = largest

堆的应用

  • 优先队列(操作系统任务调度、Dijkstra 算法等)

  • 堆排序(O(n log n) 的不稳定排序)

  • 求 Top K 问题(维护大小为 K 的堆)

  • 中位数查找(使用两个堆)

七、树的经典应用

树形结构因其天然的层次性和递归特性,在计算机科学中应用极广:

  1. 文件系统:目录和文件的层级结构。

  2. 数据库索引:B 树、B+ 树是关系型数据库的核心索引结构。

  3. 编译原理:语法树(AST)表示程序的结构。

  4. 网络路由:路由表可以用树结构组织。

  5. 人工智能:决策树、博弈树(如 AlphaGo 的蒙特卡洛树搜索)。

  6. 数据压缩:哈夫曼树用于构建最优前缀编码(哈夫曼编码)。

  7. 表达式求值:表达式树可以方便地计算中缀表达式。

  8. XML/HTML 解析:DOM 树是文档对象模型的基础。

八、树与递归

树的结构天然与递归绑定。几乎所有的树操作都可以用简洁的递归算法实现,例如遍历、查找、插入、删除等。递归的魅力在于它直接反映了树的定义:对根操作,然后递归地对子树操作。

然而,递归也有栈溢出的风险(深度过大时)。对于深度很大的树,可以考虑使用迭代(栈或队列)来实现遍历。

九、复杂度总结

数据结构 查找平均 查找最坏 插入平均 插入最坏 删除平均 删除最坏
普通 BST O(log n) O(n) O(log n) O(n) O(log n) O(n)
AVL 树 O(log n) O(log n) O(log n) O(log n) O(log n) O(log n)
红黑树 O(log n) O(log n) O(log n) O(log n) O(log n) O(log n)
堆(优先队列) O(1)(取最值) O(1) O(log n) O(log n) O(log n) O(log n)
遍历(任意树) O(n) O(n) - - - -

十、总结

树是数据结构中"分治"思想的完美体现。通过这篇文章,我们了解了:

  • 树的基本概念和术语,以及递归定义。

  • 二叉树及其常见类型(满、完全、平衡)。

  • 二叉搜索树的查找、插入、删除操作及复杂度。

  • 四种遍历方式(前序、中序、后序、层序)及其实现。

  • 平衡树(AVL、红黑树)如何解决 BST 的退化问题。

  • 堆作为特殊树的应用和实现。

  • 树在计算机科学中的广泛应用。

树的学习不仅有助于解决特定问题,更能培养抽象思维和递归思考能力。如果你正在学习数据结构,不妨从实现一个简单的二叉搜索树开始,然后尝试将其改写为 AVL 树,这将是一次绝佳的练习。

相关推荐
Book思议-1 小时前
【数据结构实战】川剧 “扯脸” 与栈的 LIFO 特性 :用 C 语言实现 3 种栈结构
c语言·数据结构·算法·
chudonghao2 小时前
[UE学习笔记][基于源码] 理解 Gameplay
c++·笔记·学习·ue5
云淡风轻~窗明几净3 小时前
关于TSP的海岸线猜想:SeaLine算法的逐层法(不同于逐点法)
数据结构·算法·动态规划·模拟退火算法
菜鸟小九3 小时前
hot100(91-100)
数据结构·算法·排序算法
平常心cyk4 小时前
Python基础快速复习——集合和字典
开发语言·数据结构·python
左左右右左右摇晃4 小时前
数据结构——数组
数据结构·笔记·算法
左左右右左右摇晃4 小时前
数据结构——队列
数据结构
nainaire4 小时前
速通LeetCode hot100——(1~9 哈希,双指针,滑动窗口)
c++·笔记·算法·leetcode
hmbbcsm4 小时前
动手学习深度学习学习笔记(一)
笔记·学习