算法之树的详解(C++)

简介:

在算法与数据结构的浩瀚宇宙中,树结构宛如一颗璀璨的明星,以其独特的层次化组织和高效的数据处理能力,在众多领域熠熠生辉。从经典的二叉树、红黑树,到应用广泛的 B 树、Trie 树,每一种树结构都承载着独特的设计思想与算法逻辑。它们不仅是解决搜索、排序、存储等问题的 “秘密武器”,更在数据库索引优化、自然语言处理、文件系统管理等场景中发挥着不可替代的作用。本文将带您深入树结构的奇妙世界,一同领略其精妙设计与无限潜力。

二叉树

满二叉树

满二叉树(Full Binary Tree)是一种特殊的二叉树结构,其定义如下:

  1. 定义
    满二叉树是指一棵二叉树中,除了叶子节点外,每一个节点都有恰好两个子节点(左子节点和右子节点),且所有叶子节点都在同一层上。

  2. 特点

    • 如果满二叉树的深度为 h,则它的节点总数为 2^h - 1
    • 叶子节点的数量为 2^(h-1)
    • 非叶子节点的数量为 2^(h-1) - 1
  3. 示例
    以下是一个深度为 3 的满二叉树:

          A
        /     \
       B       C
      / \     / \
     D   E   F   G
    

    其中,节点 ABC 都有两个子节点,而叶子节点 DEFG 都在同一层。

  4. 与其他树的区别

    • 满二叉树不同于完全二叉树,完全二叉树允许最后一层的叶子节点不完全填满,但必须从左到右填充。
    • 满二叉树也不同于完美二叉树(Perfect Binary Tree),完美二叉树是满二叉树的特殊情况,要求所有层都完全填满。
  5. 应用场景
    满二叉树常用于堆(Heap)结构、哈夫曼编码(Huffman Coding)等算法中,因为其结构规整,便于计算和操作。


完全二叉树

定义
完全二叉树(Complete Binary Tree)是一种特殊的二叉树结构,其中除了最后一层外,每一层都被完全填满,并且最后一层的所有节点都尽可能集中在左侧。

特点

  1. 层序填充:节点按从上到下、从左到右的顺序依次填充。
  2. 最后一层不要求满:但最后一层的节点必须连续集中在左侧,右侧可以有空缺。
  3. 高度差:若最后一层未满,树的高度为 ( h ),则前 ( h-1 ) 层必须完全填满。

性质

  1. 节点编号:若根节点编号为 1,对于任意节点 ( i ):
    • 左子节点编号为 ( 2i )。
    • 右子节点编号为 ( 2i+1 )。
    • 父节点编号为 ( \lfloor i/2 \rfloor )(向下取整)。
  2. 节点数范围:高度为 ( h ) 的完全二叉树,节点数 ( n ) 满足:
    [
    2^{h-1} \leq n \leq 2^h - 1
    ]
  3. 存储效率:适合用数组存储,无需指针,空间利用率高。

示例

       1
     /   \
    2     3
   / \   /
  4   5 6
  • 满足完全二叉树:前两层填满,最后一层节点靠左。

与非完全二叉树的区别

  • 完全二叉树:最后一层左侧连续,无间隔空缺。
  • 非完全二叉树:可能存在右侧节点空缺或中间层未填满的情况。

应用场景

  1. 堆(Heap):优先队列的实现基于完全二叉树结构。
  2. 内存管理:某些内存分配算法利用完全二叉树的紧凑性。
  3. 高效存储:数组存储时无需额外空间记录子节点指针。

平衡二叉树(AVL树)

定义

平衡二叉树(AVL树)是一种自平衡的二叉搜索树(BST),其中任何节点的两个子树的高度差最多为1。AVL树得名于其发明者Adelson-Velsky和Landis。

特性
  1. 平衡条件:对于AVL树中的每个节点,其左子树和右子树的高度差(平衡因子)的绝对值不超过1。

    • 平衡因子 = 左子树高度 - 右子树高度
    • 平衡因子 ∈ {-1, 0, 1}
  2. 高度平衡:由于严格的平衡条件,AVL树的高度始终保持在O(log n),其中n是树中的节点数。

基本操作
  1. 插入

    • 按照二叉搜索树的规则插入新节点。
    • 从插入点向上回溯,检查每个祖先节点的平衡因子。
    • 如果发现某个节点的平衡因子绝对值大于1,则通过旋转操作恢复平衡。
  2. 删除

    • 按照二叉搜索树的规则删除节点。
    • 从删除点向上回溯,检查每个祖先节点的平衡因子。
    • 如果发现不平衡,通过旋转操作恢复平衡。
  3. 查找

    • 与普通二叉搜索树相同,时间复杂度为O(log n)。
旋转操作

当插入或删除导致树不平衡时,需要通过旋转恢复平衡。主要有四种旋转情况:

  1. 左旋(Left Rotation)

    • 适用于右子树比左子树高的情况。
    • 将当前节点的右子节点提升为新的根节点,原根节点成为新根节点的左子节点。
  2. 右旋(Right Rotation)

    • 适用于左子树比右子树高的情况。
    • 将当前节点的左子节点提升为新的根节点,原根节点成为新根节点的右子节点。
  3. 左右旋(Left-Right Rotation)

    • 先对左子节点左旋,再对当前节点右旋。
  4. 右左旋(Right-Left Rotation)

    • 先对右子节点右旋,再对当前节点左旋。
时间复杂度
  • 插入:O(log n),可能需要旋转操作。
  • 删除:O(log n),可能需要旋转操作。
  • 查找:O(log n)。
应用场景
  • 需要频繁查找、插入和删除操作,且对性能要求较高的场景。
  • 数据库索引、内存中的有序数据结构等。

二叉搜索树(BST)

定义

二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树数据结构,其中每个节点最多有两个子节点(左子节点和右子节点),并且满足以下性质:

  • 对于树中的任意节点,其左子树中的所有节点的值都小于该节点的值。
  • 对于树中的任意节点,其右子树中的所有节点的值都大于该节点的值。
  • 左右子树也分别是二叉搜索树。
特点
  1. 有序性:BST的中序遍历(左-根-右)会得到一个升序排列的序列。
  2. 查找效率:在平衡的BST中,查找、插入和删除操作的时间复杂度为O(log n),其中n是树中节点的数量。但在最坏情况下(如退化为链表),时间复杂度会退化为O(n)。
  3. 动态结构:BST支持动态插入和删除操作,同时保持有序性。
基本操作
  1. 查找(Search)

    • 从根节点开始,比较目标值与当前节点的值。
    • 如果目标值等于当前节点的值,返回该节点。
    • 如果目标值小于当前节点的值,递归查找左子树。
    • 如果目标值大于当前节点的值,递归查找右子树。
    • 如果子树为空,则查找失败。
  2. 插入(Insert)

    • 从根节点开始,比较插入值与当前节点的值。
    • 如果插入值小于当前节点的值且左子节点为空,则插入为左子节点;否则递归插入左子树。
    • 如果插入值大于当前节点的值且右子节点为空,则插入为右子节点;否则递归插入右子树。
    • 如果插入值等于当前节点的值,通常根据具体需求处理(如忽略或替换)。
  3. 删除(Delete)

    • 叶子节点:直接删除。
    • 只有一个子节点:用子节点替换被删除的节点。
    • 有两个子节点
      • 找到被删除节点的中序后继(右子树中的最小节点)或中序前驱(左子树中的最大节点)。
      • 用中序后继(或前驱)的值替换被删除节点的值。
      • 删除中序后继(或前驱)节点。
应用场景
  1. 数据库索引:BST可用于实现数据库的索引结构,加速查询。
  2. 字典实现:存储键值对,支持快速查找、插入和删除。
  3. 排序:通过中序遍历BST可以得到有序数据。
  4. 范围查询:高效查找某个范围内的所有值。
注意事项
  • 平衡性:普通的BST可能会因为插入顺序不当而退化为链表(如按升序或降序插入)。此时,操作的时间复杂度会退化为O(n)。为了避免这种情况,通常使用平衡二叉搜索树(如AVL树、红黑树)来保持树的平衡。
  • 重复值处理:BST通常不允许重复值,但可以通过在节点中增加计数器或扩展为允许重复值的变体来处理。

红黑树

红黑树是一种自平衡的二叉查找树,它在计算机科学中应用广泛,尤其是在关联容器(如C++中的std::mapstd::set)的实现中。红黑树通过特定的规则确保树在插入和删除操作后保持近似平衡,从而保证操作的时间复杂度为O(log n)。

红黑树的特性

红黑树必须满足以下五个性质:

  1. 节点颜色:每个节点要么是红色,要么是黑色。
  2. 根节点:根节点必须是黑色。
  3. 叶子节点(NIL节点):所有叶子节点(NIL节点,即空节点)都是黑色。
  4. 红色节点的子节点:如果一个节点是红色,那么它的两个子节点必须是黑色(即不能有两个连续的红色节点)。
  5. 黑高平衡:从任一节点到其每个叶子节点的所有路径上,黑色节点的数量必须相同(称为“黑高”)。
红黑树的操作

红黑树的核心操作包括插入、删除和查找,并通过旋转和重新着色来维护红黑树的性质。

  1. 插入

    • 新插入的节点初始为红色。
    • 插入后可能违反红黑树的性质(如连续红色节点),需要通过旋转和重新着色修复。
    • 修复操作包括:左旋、右旋、重新着色。
  2. 删除

    • 删除节点后可能破坏红黑树的性质(如黑高不平衡),需要通过旋转和重新着色修复。
    • 修复操作比插入更复杂,可能涉及多次旋转和重新着色。
  3. 查找

    • 红黑树的查找操作与普通二叉查找树相同,时间复杂度为O(log n)。
红黑树的优势
  • 平衡性:红黑树通过严格的规则确保树的高度始终保持在O(log n),从而保证高效的操作性能。
  • 高效操作:插入、删除和查找的时间复杂度均为O(log n)。
  • 广泛应用:常用于实现高效的动态数据结构,如C++的std::mapstd::set
红黑树与AVL树的对比
  • 平衡严格性:AVL树的平衡性更严格(左右子树高度差不超过1),而红黑树的平衡性稍弱(通过黑高平衡保证)。
  • 操作效率:AVL树的查找更快(更严格的平衡),但插入和删除可能需要更多的旋转操作;红黑树的插入和删除效率更高,适合频繁修改的场景。
示例代码(C++)

以下是红黑树的简单实现框架(仅展示插入和旋转操作):

enum Color { RED, BLACK };

struct Node {
    int data;
    Color color;
    Node *left, *right, *parent;
    Node(int data) : data(data), color(RED), left(nullptr), right(nullptr), parent(nullptr) {}
};

class RedBlackTree {
private:
    Node *root;
    void leftRotate(Node *x);
    void rightRotate(Node *y);
    void fixInsert(Node *z);
public:
    RedBlackTree() : root(nullptr) {}
    void insert(int data);
};

void RedBlackTree::leftRotate(Node *x) {
    Node *y = x->right;
    x->right = y->left;
    if (y->left != nullptr) {
        y->left->parent = x;
    }
    y->parent = x->parent;
    if (x->parent == nullptr) {
        root = y;
    } else if (x == x->parent->left) {
        x->parent->left = y;
    } else {
        x->parent->right = y;
    }
    y->left = x;
    x->parent = y;
}

void RedBlackTree::rightRotate(Node *y) {
    Node *x = y->left;
    y->left = x->right;
    if (x->right != nullptr) {
        x->right->parent = y;
    }
    x->parent = y->parent;
    if (y->parent == nullptr) {
        root = x;
    } else if (y == y->parent->left) {
        y->parent->left = x;
    } else {
        y->parent->right = x;
    }
    x->right = y;
    y->parent = x;
}

void RedBlackTree::fixInsert(Node *z) {
    while (z != root && z->parent->color == RED) {
        // 修复操作的具体实现
    }
    root->color = BLACK;
}

void RedBlackTree::insert(int data) {
    Node *z = new Node(data);
    // 插入逻辑
    fixInsert(z);
}

多路查找树

B树

B树(B-Tree)是一种自平衡的树数据结构,用于在磁盘或其他直接存取的存储设备上高效地存储和检索数据。它广泛应用于数据库和文件系统中,因为它能够减少磁盘I/O操作的次数。

特点
  1. 平衡性:所有叶子节点位于同一层,确保查找、插入和删除操作的时间复杂度稳定。
  2. 多路分支:每个节点可以有多个子节点(通常远多于二叉树的2个子节点),这减少了树的高度。
  3. 有序性:节点内的键值按升序排列,便于快速查找。
  4. 节点容量:每个非根节点至少包含 t-1 个键,最多包含 2t-1 个键(t 是B树的最小度数)。
操作
  1. 查找:从根节点开始,递归或迭代地在节点内部进行二分查找,直到找到目标键或到达叶子节点。
  2. 插入:可能涉及节点的分裂(当一个节点已满时),分裂会向上传递,可能导致树的高度增加。
  3. 删除:可能涉及从兄弟节点借键或合并节点,以保持树的平衡性。
应用场景
  • 数据库索引(如MySQL的InnoDB存储引擎)。
  • 文件系统(如NTFS、ReiserFS)。
复杂度
  • 查找、插入、删除的时间复杂度均为 O(log n),其中 n 是键的数量。
  • 空间复杂度为 O(n)

B+树

定义

B+树是一种多路平衡查找树,是B树的一种变体。它主要用于数据库和文件系统中,能够高效地进行查找、顺序访问、插入和删除操作。

特点
  1. 多路平衡:每个节点可以有多个子节点,保持树的平衡。
  2. 叶子节点存储数据:所有数据都存储在叶子节点中,内部节点仅存储键值用于索引。
  3. 叶子节点链表连接:所有叶子节点通过指针连接成一个有序链表,便于范围查询。
  4. 高度平衡:所有叶子节点位于同一层,保证查找效率稳定。
结构
  • 内部节点:存储键值和指向子节点的指针,不存储实际数据。
  • 叶子节点:存储键值和对应的数据(或数据指针),并通过指针连接成链表。
操作
  1. 查找

    • 从根节点开始,逐层比较键值,直到找到目标叶子节点。
    • 由于所有数据都在叶子节点,查找时间复杂度为O(log n)。
  2. 插入

    • 找到合适的叶子节点插入数据。
    • 如果叶子节点已满,则进行分裂操作,并将中间键值提升到父节点。
    • 递归检查父节点是否需要分裂,直到根节点。
  3. 删除

    • 找到目标叶子节点并删除数据。
    • 如果叶子节点数据过少,可能需要合并或借用相邻节点的数据。
    • 递归调整父节点,直到根节点。
优点
  1. 高效的范围查询:叶子节点链表结构使得范围查询非常高效。
  2. 稳定的查询性能:所有查询都需要从根节点到叶子节点,路径长度相同。
  3. 更适合磁盘存储:节点通常设计为磁盘块大小,减少IO操作。
应用场景
  1. 数据库索引:如MySQL的InnoDB存储引擎使用B+树作为索引结构。
  2. 文件系统:某些文件系统使用B+树管理文件和目录。
与B树的区别
  1. 数据存储位置:B+树数据仅在叶子节点,B树数据在所有节点。
  2. 叶子节点连接:B+树叶子节点通过链表连接,B树没有。
  3. 查询效率:B+树范围查询更高效,B树随机查询可能更快。

B*树

B*树是B树的一种变体,旨在提高节点的空间利用率和查询效率。它通过调整节点的分裂策略和兄弟节点之间的数据重新分配来优化性能。

特点
  1. 节点填充率更高:B*树要求非根节点的填充率至少为2/3,而普通B树通常为1/2。
  2. 分裂策略不同:当节点满时,B*树会尝试将部分数据转移到兄弟节点(若兄弟节点未满),而不是直接分裂。仅当兄弟节点也满时,才执行分裂操作。
  3. 减少分裂次数:通过数据重新分配,B*树减少了节点分裂的频率,从而降低了树的高度和I/O操作次数。
结构规则
  • 阶数(m):每个节点最多包含m个关键字和m+1个子节点指针。
  • 关键字数量
    • 根节点:至少1个关键字。
    • 非根节点:至少⌈(2m-1)/3⌉个关键字(确保2/3填充率)。
  • 分裂条件:仅在当前节点和兄弟节点均满时,将当前节点、兄弟节点及新数据合并后分裂为3个节点。
操作示例
  • 插入
    1. 定位到目标叶子节点。
    2. 若节点未满,直接插入。
    3. 若节点满,检查兄弟节点:
      • 兄弟节点未满:重新分配数据。
      • 兄弟节点满:分裂为3个节点(原节点、兄弟节点、新节点各含约1/3数据)。
优势
  • 更高的空间利用率:减少存储浪费。
  • 更稳定的性能:减少分裂操作带来的开销。
应用场景

主要用于需要高磁盘I/O效率的场景,如数据库索引和文件系统。


其他树结构

哈夫曼树(Huffman Tree)

定义

哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。它主要用于数据压缩领域,如哈夫曼编码。

特点
  1. 带权路径长度(WPL)最小:所有叶子节点的权值乘以其到根节点的路径长度之和最小。
  2. 没有度为1的节点:哈夫曼树是严格的二叉树,每个非叶子节点都有两个子节点。
  3. 权值越大的节点离根越近:权值较小的节点通常位于树的较深层。
构建步骤
  1. 初始化:将每个数据及其权值作为一个单独的节点,构成森林。
  2. 合并节点:每次选择权值最小的两个节点,合并为一个新节点,新节点的权值为这两个节点的权值之和。
  3. 重复合并:直到森林中只剩下一棵树,即为哈夫曼树。
示例

假设有字符集 {A, B, C, D},对应的权值(频率)为 {5, 1, 6, 3}

  1. 初始节点:A(5)B(1)C(6)D(3)
  2. 合并 B(1)D(3),生成新节点 BD(4)
  3. 合并 A(5)BD(4),生成新节点 ABD(9)
  4. 最后合并 ABD(9)C(6),生成根节点 ABCD(15)
应用
  • 哈夫曼编码:用于无损数据压缩,高频字符用短编码,低频字符用长编码。
  • 优先级队列优化:通过哈夫曼树实现高效的优先级队列。

字典树(Trie树)

基本概念

字典树,又称前缀树或Trie树,是一种树形数据结构,用于高效地存储和检索字符串集合中的键。字典树的核心思想是利用字符串的公共前缀来减少查询时间,通常用于搜索引擎、拼写检查、自动完成等场景。

结构特点
  1. 节点结构:每个节点代表一个字符,从根节点到某一节点的路径上经过的字符连接起来,构成该节点对应的字符串。
  2. 根节点:根节点不包含字符,通常为空或表示字符串的起始。
  3. 子节点:每个节点的子节点数量取决于字符集的大小(如小写字母为26个)。
  4. 标记结束:某些节点会被标记为字符串的结束节点(通常用布尔值表示)。
操作
  1. 插入

    • 从根节点开始,逐个字符插入。
    • 如果字符对应的子节点不存在,则创建新节点。
    • 插入完成后,标记最后一个节点为结束节点。
  2. 查找

    • 从根节点开始,逐个字符匹配。
    • 如果路径上的字符全部匹配且最后一个节点被标记为结束,则字符串存在。
  3. 前缀匹配

    • 类似于查找,但不需要检查结束标记,只需确认前缀存在即可。
优缺点
  • 优点
    • 查询效率高,时间复杂度为O(L),其中L为字符串长度。
    • 适合处理大量具有公共前缀的字符串。
  • 缺点
    • 空间消耗较大,尤其是字符集较大时。
    • 不适合处理动态变化的字符集。
应用场景
  • 自动补全和拼写检查。
  • IP路由的最长前缀匹配。
  • 字符串排序(按字典序输出所有字符串)。

线段树(Segment Tree)

定义

线段树是一种二叉树数据结构,用于高效处理区间查询(如区间求和、区间最小值/最大值等)和区间更新操作。它将一个区间划分为若干个子区间,每个节点存储对应区间的信息。

核心特性
  1. 完全二叉树结构(通常用数组实现)。
  2. 每个节点代表一个区间 [l, r]
    • 叶子节点:区间长度为 1(即 l = r)。
    • 非叶子节点:区间 [l, r] 的左子节点为 [l, mid],右子节点为 [mid+1, r]mid = (l + r) / 2)。
  3. 支持区间查询单点/区间更新,时间复杂度均为 O(log n)
基本操作
  1. 构建(Build)

    • 递归划分区间,从根节点开始构建子树,直到叶子节点。
    • 示例代码(C++):
      void build(int node, int l, int r, vector<int>& data) {
          if (l == r) {
              tree[node] = data[l];
              return;
          }
          int mid = (l + r) / 2;
          build(2*node, l, mid, data);
          build(2*node+1, mid+1, r, data);
          tree[node] = tree[2*node] + tree[2*node+1]; // 以区间求和为例
      }
      
  2. 区间查询(Query)

    • 若当前节点区间 [l, r] 完全包含于查询区间 [ql, qr],则直接返回节点值。
    • 否则递归查询左右子树,合并结果。
    • 示例代码(求和查询):
      int query(int node, int l, int r, int ql, int qr) {
          if (qr < l || ql > r) return 0; // 无交集
          if (ql <= l && qr >= r) return tree[node]; // 完全包含
          int mid = (l + r) / 2;
          return query(2*node, l, mid, ql, qr) + query(2*node+1, mid+1, r, ql, qr);
      }
      
  3. 更新(Update)

    • 单点更新:递归找到叶子节点并更新值,回溯时更新父节点。
      void update(int node, int l, int r, int pos, int val) {
          if (l == r) {
              tree[node] = val;
              return;
          }
          int mid = (l + r) / 2;
          if (pos <= mid) update(2*node, l, mid, pos, val);
          else update(2*node+1, mid+1, r, pos, val);
          tree[node] = tree[2*node] + tree[2*node+1];
      }
      
    • 区间更新:需结合**懒惰标记(Lazy Propagation)**优化,避免重复操作。
应用场景
  1. 动态维护区间统计量(如求和、最值)。
  2. 解决**RMQ(Range Minimum Query)**问题。
  3. 处理区间覆盖、区间染色等问题。
复杂度分析
  • 空间复杂度O(4n)(保守估计,实际为 2n-1 但通常开 4 倍空间)。
  • 时间复杂度
    • 构建:O(n)
    • 查询/更新:O(log n)

并查集树

并查集树(Disjoint Set Union Tree,简称DSU Tree)是一种用于处理不相交集合合并与查询问题的数据结构。它主要用于解决动态连通性问题,比如判断两个元素是否属于同一集合,或者将两个集合合并为一个集合。

核心操作
  1. 查找(Find):确定某个元素属于哪个集合(通常返回集合的代表元素)。
  2. 合并(Union):将两个集合合并为一个集合。
实现方式

通常通过数组结构实现,每个节点指向其父节点,根节点指向自己(代表集合的标识)。

优化技术
  1. 路径压缩(Path Compression):在查找操作时,将路径上的所有节点直接指向根节点,减少后续查找的时间。
    • 示例:find(x)时,将x到根节点的路径上的所有节点的父节点设为根。
  2. 按秩合并(Union by Rank):合并时,将较小的树合并到较大的树下,避免树的高度过高。
    • 示例:用rank数组记录树的深度,合并时比较rank
时间复杂度
  • 未优化:最坏情况为 O ( n ) O(n) O(n)(退化为链表)。
  • 优化后(路径压缩 + 按秩合并):接近 O ( 1 ) O(1) O(1)(均摊时间复杂度)。
代码示例(C++)
int parent[MAX_N], rank[MAX_N];

void init(int n) {
    for (int i = 0; i < n; ++i) {
        parent[i] = i;  // 初始时每个元素自成一集合
        rank[i] = 0;    // 初始秩为0
    }
}

int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);  // 路径压缩
    }
    return parent[x];
}

void unionSets(int x, int y) {
    int rootX = find(x), rootY = find(y);
    if (rootX == rootY) return;  // 已属于同一集合

    // 按秩合并
    if (rank[rootX] > rank[rootY]) {
        parent[rootY] = rootX;
    } else {
        parent[rootX] = rootY;
        if (rank[rootX] == rank[rootY]) {
            rank[rootY]++;
        }
    }
}
应用场景
  • 动态连通性问题(如网络连接、社交网络好友关系)。
  • 最小生成树算法(Kruskal算法)。
  • 图的连通分量统计。
注意事项
  • 并查集树不支持集合的拆分操作。
  • 需要合理选择优化策略以避免性能退化。

后缀树(Suffix Tree)

定义

后缀树是一种压缩的字典树(Trie),用于存储一个字符串的所有后缀。它是一种线性空间的数据结构,能够高效地解决许多字符串处理问题。

结构特点
  1. 节点:每个节点代表一个子字符串(通常是字符串的某个子串)。
  2. :边上的标签表示子字符串的一部分。
  3. 根到叶子路径:每条从根到叶子的路径对应字符串的一个后缀。
  4. 压缩:如果某个节点只有一个子节点,它会被压缩为一个边,以减少空间占用。
构建方法

后缀树可以通过Ukkonen算法在线性时间内构建(时间复杂度为O(n),其中n是字符串长度)。该算法通过逐步扩展字符并维护隐式后缀树来实现高效构建。

主要用途
  1. 字符串匹配:快速查找子串是否存在(O(m)时间,m为子串长度)。
  2. 最长重复子串:找到字符串中最长的重复子串。
  3. 最长公共子串:解决多个字符串的最长公共子串问题。
  4. 后缀数组构造:后缀树可以用于高效构造后缀数组。
示例

对于字符串"banana",其后缀树会包含以下后缀:

  • banana
  • anana
  • nana
  • ana
  • na
  • a
优缺点
  • 优点
    • 高效的字符串操作(如匹配、搜索)。
    • 线性空间复杂度(经过压缩优化)。
  • 缺点
    • 构建算法较复杂(如Ukkonen算法)。
    • 在实际实现中可能占用较多内存。
代码示例(伪代码)
struct SuffixTreeNode {
    map<char, SuffixTreeNode*> children; // 子节点
    int start; // 边的起始索引
    int end;   // 边的结束索引
    SuffixTreeNode* suffixLink; // 后缀链接
};

SuffixTreeNode* buildSuffixTree(string s) {
    // 实现Ukkonen算法构建后缀树
    // ...
}

你可能感兴趣的:(C和CPP,算法,c++,数据结构)