以下是一个简单易懂的 C++ 线段树教程,用生活化的比喻和分步解释,帮你快速掌握核心思想:


🌳 线段树:像切蛋糕一样处理区间问题

一、什么是线段树?

想象你要管理一家奶茶店的订单,需要快速回答这些问题:

  • "下午2点到5点卖了多少杯?"(区间求和
  • "今天销量最高的是哪一小时?"(区间最大值

如果用普通方法计算,每次都要遍历所有数据,效率低。线段树就像把时间轴切分成多个小段,预先记录每个时间段的信息,查询时直接拼装结果。

二、线段树的本质

把数组像切蛋糕一样不断二分,直到每个小块不能再分割。例如数组 [1,3,5,7] 的切分过程:

        [0-3](总和16)
       /      \
  [0-1](4)   [2-3](12)
  /   \       /   \
0(1) 1(3)  2(5) 3(7)

三、手把手实现线段树(区间求和版)

1. 准备工作

const int MAXN = 1e5;  // 假设最大数据量
int arr[MAXN];         // 原始数组(建议从0开始存)
int tree[4 * MAXN];    // 线段树数组(需要4倍空间)

2. 建树:搭建信息金字塔(⚡时间复杂度 O(n))

// node: 当前节点编号(像门牌号)
// start/end: 当前节点管理的区间
void build(int node, int start, int end) {
    if (start == end) { // 到达叶子节点
        tree[node] = arr[start];
        return;
    }
    
    int mid = (start + end) / 2;
    int leftChild = 2 * node + 1;  // 左孩子(类似数组堆的存储方式)
    int rightChild = 2 * node + 2; // 右孩子
    
    build(leftChild, start, mid);   // 建左半子树
    build(rightChild, mid + 1, end);// 建右半子树
    
    tree[node] = tree[leftChild] + tree[rightChild]; // 合并子节点结果
}

3. 查询:快速获取区间和(⚡时间复杂度 O(log n))

// [l, r]是查询区间,[start, end]是当前节点的管理区间
int query(int node, int start, int end, int l, int r) {
    if (r < start || end < l) // 完全没交集
        return 0;
    
    if (l <= start && end <= r) // 完全在查询区间内
        return tree[node];
    
    // 部分重叠,继续向下查
    int mid = (start + end) / 2;
    int leftSum = query(2*node+1, start, mid, l, r);
    int rightSum = query(2*node+2, mid+1, end, l, r);
    
    return leftSum + rightSum;
}

4. 更新:修改某个元素的值(⚡时间复杂度 O(log n))

void update(int node, int start, int end, int index, int value) {
    if (start == end) { // 找到目标位置
        arr[index] = value;
        tree[node] = value;
        return;
    }
    
    int mid = (start + end) / 2;
    if (index <= mid) // 要更新的位置在左子树
        update(2*node+1, start, mid, index, value);
    else              // 在右子树
        update(2*node+2, mid+1, end, index, value);
    
    tree[node] = tree[2*node+1] + tree[2*node+2]; // 更新父节点
}

四、举个栗子 🌰

int main() {
    // 初始化数组 [1,3,5,7](下标0~3)
    arr[0] = 1; arr[1] = 3; 
    arr[2] = 5; arr[3] = 7;

    // 建树:根节点是0,管理整个数组[0,3]
    build(0, 0, 3);

    // 查询区间[1,2]的和:3+5=8
    cout << query(0, 0, 3, 1, 2) << endl;

    // 更新arr[1]为10
    update(0, 0, 3, 1, 10);

    // 再次查询[1,2]:10+5=15
    cout << query(0, 0, 3, 1, 2) << endl;

    return 0;
}

五、重点总结

要点 说明
为什么叫线段树 每个节点代表一个区间(线段)
为什么需要4倍空间 最坏情况需要2^(h+1)个节点,h是树的高度
索引从0还是1开始 都可以,但代码逻辑要一致
怎么改求最大值 +改成max(),初始化时叶子节点赋值

六、常见问题

Q1:为什么查询时判断条件那么复杂?
A1:就像查快递时,包裹可能在左仓库、右仓库,或分散在两个仓库,需要分别查找。

Q2:线段树和前缀数组哪个好?
A2:前缀数组查询快(O(1)),但更新慢(O(n))。线段树适合频繁更新的场景。

Q3:什么时候用线段树?
A3:当需要同时满足 区间查询单点/区间修改 的高效操作时。


通过这个简化版的教程,你已经掌握了线段树的核心思想!下一步可以尝试实现求区间最大值,或者学习进阶技巧——延迟标记(Lazy Propagation)。

8 条评论

  • @ 2025-2-1 11:37:14

    • @ 2025-2-1 11:35:36

      • @ 2025-2-1 11:34:43

        🌟 线段树延迟标记(Lazy Propagation)通俗教程

        延迟标记是线段树的一种优化技术,用于高效处理区间更新操作。它的核心思想是:推迟更新,只有在需要时才将更新应用到具体的节点上,从而避免不必要的重复操作。


        一、为什么要用延迟标记?

        想象你在管理一个图书馆的书架,每次有人借书或还书时,你需要更新书架上的书籍数量。如果每次借还书都更新所有书架,效率会很低。延迟标记就像在书架上贴一张便签:“这些书被借走了”,等到真正需要查询时再更新。


        二、延迟标记的核心思想

        1. 标记(Lazy Tag)

          • 每个节点维护一个标记,表示该节点的子节点需要进行的更新操作。
          • 标记可以是区间加、区间赋值等。
        2. 延迟更新

          • 更新时,只更新当前节点,并将更新操作记录在标记中。
          • 查询时,如果当前节点有标记,则将标记下传给子节点,并更新子节点的值。

        三、手把手实现延迟标记

        1. 准备工作

        const int MAXN = 1e5;  // 假设最大数据量
        int arr[MAXN];         // 原始数组
        int tree[4 * MAXN];    // 线段树数组
        int lazy[4 * MAXN];    // 延迟标记数组
        

        2. 建树(Build)

        void build(int node, int start, int end) {
            if (start == end) { // 到达叶子节点
                tree[node] = arr[start];
                return;
            }
            int mid = (start + end) / 2;
            build(2 * node, start, mid);   // 建左子树
            build(2 * node + 1, mid + 1, end); // 建右子树
            tree[node] = tree[2 * node] + tree[2 * node + 1]; // 合并结果
        }
        

        3. 标记下传(Push Down)

        void pushDown(int node, int start, int end) {
            if (lazy[node] != 0) { // 如果有标记
                int mid = (start + end) / 2;
                // 更新左子树
                tree[2 * node] += lazy[node] * (mid - start + 1);
                lazy[2 * node] += lazy[node];
                // 更新右子树
                tree[2 * node + 1] += lazy[node] * (end - mid);
                lazy[2 * node + 1] += lazy[node];
                // 清除当前节点的标记
                lazy[node] = 0;
            }
        }
        

        4. 区间更新(Update)

        void update(int node, int start, int end, int l, int r, int val) {
            if (r < start || end < l) // 完全无交集
                return;
            if (l <= start && end <= r) { // 完全包含
                tree[node] += val * (end - start + 1); // 更新当前节点
                lazy[node] += val; // 标记延迟更新
                return;
            }
            pushDown(node, start, end); // 下传标记
            int mid = (start + end) / 2;
            update(2 * node, start, mid, l, r, val); // 更新左子树
            update(2 * node + 1, mid + 1, end, l, r, val); // 更新右子树
            tree[node] = tree[2 * node] + tree[2 * node + 1]; // 合并结果
        }
        

        5. 区间查询(Query)

        int query(int node, int start, int end, int l, int r) {
            if (r < start || end < l) // 完全无交集
                return 0;
            if (l <= start && end <= r) // 完全包含
                return tree[node];
            pushDown(node, start, end); // 下传标记
            int mid = (start + end) / 2;
            int leftSum = query(2 * node, start, mid, l, r); // 查询左子树
            int rightSum = query(2 * node + 1, mid + 1, end, l, r); // 查询右子树
            return leftSum + rightSum; // 合并结果
        }
        

        四、举个栗子 🌰

        1. 初始化

        int n = 5;
        arr[1] = 1; arr[2] = 3; arr[3] = 5; arr[4] = 7; arr[5] = 9;
        build(1, 1, n);
        

        2. 区间更新

        // 将区间 [2,4] 的所有值加 2
        update(1, 1, n, 2, 4, 2);
        

        3. 区间查询

        // 查询区间 [1,5] 的和
        cout << query(1, 1, n, 1, 5) << endl; // 1 + 5 + 7 + 9 + 9 = 31
        

        五、总结

        操作 时间复杂度 空间复杂度
        建树 O(n)
        区间更新 O(log n) O(1)
        区间查询

        六、常见问题

        Q1:为什么需要延迟标记?
        A1:延迟标记可以避免不必要的重复更新,将区间更新的时间复杂度优化到 O(log n)

        Q2:延迟标记适用于哪些操作?
        A2:适用于区间加、区间赋值等区间更新操作。

        Q3:延迟标记的实现难点是什么?
        A3:难点在于正确下传标记,确保更新操作不会遗漏或重复。


        通过这个通俗易懂的教程,你应该已经掌握了延迟标记的核心思想和实现方法!快去试试解决一些区间更新问题吧!

        • @ 2025-2-1 11:17:19

          延迟标记(Lazy Propagation)是线段树的一种优化技术,用于高效处理区间更新操作。它的核心思想是:推迟更新,只有在需要时才将更新应用到具体的节点上,从而避免不必要的重复操作。


          一、为什么需要延迟标记?

          在普通的线段树中,区间更新(如将区间 [L, R] 内的所有值加上 x)需要递归更新所有相关节点,时间复杂度为 O(n)。延迟标记通过延迟更新,将时间复杂度优化到 O(log n)

          示例

          假设需要对区间 [1, 5] 的所有值加 2

          • 普通线段树:递归更新 [1,5] 内的所有节点。
          • 延迟标记:只在 [1,5] 的父节点上标记“需要加 2”,等到查询时再实际更新。

          二、延迟标记的核心思想

          1. 标记(Lazy Tag)

            • 每个节点维护一个标记,表示该节点的子节点需要进行的更新操作。
            • 标记可以是区间加、区间赋值等。
          2. 延迟更新

            • 更新时,只更新当前节点,并将更新操作记录在标记中。
            • 查询时,如果当前节点有标记,则将标记下传给子节点,并更新子节点的值。

          三、实现步骤

          1. 数据结构

          const int MAXN = 1e5;  // 假设最大数据量
          int arr[MAXN];         // 原始数组
          int tree[4 * MAXN];    // 线段树数组
          int lazy[4 * MAXN];    // 延迟标记数组
          

          2. 初始化

          void build(int node, int start, int end) {
              if (start == end) {
                  tree[node] = arr[start];
                  return;
              }
              int mid = (start + end) / 2;
              build(2 * node, start, mid);
              build(2 * node + 1, mid + 1, end);
              tree[node] = tree[2 * node] + tree[2 * node + 1];
          }
          

          3. 标记下传(Push Down)

          void pushDown(int node, int start, int end) {
              if (lazy[node] != 0) { // 如果有标记
                  int mid = (start + end) / 2;
                  // 更新左子树
                  tree[2 * node] += lazy[node] * (mid - start + 1);
                  lazy[2 * node] += lazy[node];
                  // 更新右子树
                  tree[2 * node + 1] += lazy[node] * (end - mid);
                  lazy[2 * node + 1] += lazy[node];
                  // 清除当前节点的标记
                  lazy[node] = 0;
              }
          }
          

          4. 区间更新(Update)

          void update(int node, int start, int end, int l, int r, int val) {
              if (r < start || end < l) // 完全无交集
                  return;
              if (l <= start && end <= r) { // 完全包含
                  tree[node] += val * (end - start + 1); // 更新当前节点
                  lazy[node] += val; // 标记延迟更新
                  return;
              }
              pushDown(node, start, end); // 下传标记
              int mid = (start + end) / 2;
              update(2 * node, start, mid, l, r, val); // 更新左子树
              update(2 * node + 1, mid + 1, end, l, r, val); // 更新右子树
              tree[node] = tree[2 * node] + tree[2 * node + 1]; // 合并结果
          }
          

          5. 区间查询(Query)

          int query(int node, int start, int end, int l, int r) {
              if (r < start || end < l) // 完全无交集
                  return 0;
              if (l <= start && end <= r) // 完全包含
                  return tree[node];
              pushDown(node, start, end); // 下传标记
              int mid = (start + end) / 2;
              int leftSum = query(2 * node, start, mid, l, r); // 查询左子树
              int rightSum = query(2 * node + 1, mid + 1, end, l, r); // 查询右子树
              return leftSum + rightSum; // 合并结果
          }
          

          四、示例

          1. 初始化

          int n = 5;
          arr[1] = 1; arr[2] = 3; arr[3] = 5; arr[4] = 7; arr[5] = 9;
          build(1, 1, n);
          

          2. 区间更新

          // 将区间 [2,4] 的所有值加 2
          update(1, 1, n, 2, 4, 2);
          

          3. 区间查询

          // 查询区间 [1,5] 的和
          cout << query(1, 1, n, 1, 5) << endl; // 1 + 5 + 7 + 9 + 9 = 31
          

          五、复杂度分析

          操作 时间复杂度 空间复杂度
          建树 O(n)
          区间更新 O(log n) O(1)
          区间查询

          六、总结

          延迟标记的核心思想是推迟更新,通过标记下传机制,将区间更新的时间复杂度优化到 O(log n)。它的实现需要:

          1. 标记数组:记录每个节点的延迟更新操作。
          2. 标记下传:在查询和更新时,将标记下传给子节点。
          3. 区间更新:只更新当前节点,并标记延迟操作。

          掌握延迟标记后,线段树可以高效处理动态区间更新和查询问题!

          • @ 2025-2-1 9:47:52

            线段树需要开 4N 的空间,主要是为了确保在最坏情况下有足够的空间存储所有节点。以下是详细解释:


            一、线段树的空间需求

            线段树是一棵完全二叉树,它的叶子节点存储原始数组的元素,而非叶子节点存储区间的统计值(如区间和、最大值等)。

            1. 完全二叉树的性质

            • 一棵高度为 h 的完全二叉树,最多有 2^h - 1 个节点。
            • 线段树的叶子节点数等于原始数组的长度 N,因此树的高度为 h = ceil(log2(N)) + 1

            2. 最坏情况下的空间需求

            • N 不是 2 的幂时,线段树并不是一棵满二叉树,最后一层会有空节点。
            • 在最坏情况下,线段树的节点数会接近 2 * N,但实际需要更多空间来存储这些节点。

            二、为什么是 4N?

            1. 理论分析

            • 线段树的高度为 h = ceil(log2(N)) + 1
            • 节点总数最多为 2^(h+1) - 1
            • N 不是 2 的幂时,2^(h+1) - 1 会接近 4N

            2. 实际例子

            假设 N = 5

            • 线段树的高度 h = ceil(log2(5)) + 1 = 4
            • 节点总数最多为 2^(4+1) - 1 = 31
            • 4N = 20,已经足够存储所有节点。

            三、数学证明

            1. 满二叉树的情况

              • N 是 2 的幂时,线段树是一棵满二叉树。
              • 节点总数为 2N - 1
            2. 非满二叉树的情况

              • N 不是 2 的幂时,线段树的最后一层会有空节点。
              • 节点总数最多为 2 * 2^ceil(log2(N)) - 1
              • 由于 2^ceil(log2(N)) < 2N,因此节点总数最多为 4N - 1

            四、代码实现中的空间分配

            在实际代码中,我们通常直接分配 4N 的空间,以避免复杂的计算和边界问题。

            const int MAXN = 1e5;  // 假设原始数组最大长度为 1e5
            int tree[4 * MAXN];    // 线段树数组
            

            五、总结

            情况 节点总数 空间需求
            N 是 2 的幂 2N - 1 2N
            N 不是 2 的幂 最多 4N - 1 4N

            因此,线段树开 4N 的空间是为了覆盖最坏情况,确保有足够的空间存储所有节点,同时避免复杂的边界处理。

            • @ 2025-2-1 9:24:40

              二叉树的存储方式主要有两种:链式存储顺序存储。以下是它们的详细说明和代码实现:


              一、链式存储(最常用)

              链式存储通过节点对象指针来表示二叉树的结构。每个节点包含:

              • 数据域(存储数据)
              • 左指针(指向左孩子)
              • 右指针(指向右孩子)

              1. 节点定义

              struct TreeNode {
                  int val;           // 数据域
                  TreeNode* left;    // 左孩子指针
                  TreeNode* right;   // 右孩子指针
              
                  // 构造函数
                  TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
              };
              

              2. 创建二叉树

              // 创建一棵简单的二叉树
              //       1
              //      / \
              //     2   3
              //    / \
              //   4   5
              TreeNode* createTree() {
                  TreeNode* root = new TreeNode(1);
                  root->left = new TreeNode(2);
                  root->right = new TreeNode(3);
                  root->left->left = new TreeNode(4);
                  root->left->right = new TreeNode(5);
                  return root;
              }
              

              3. 优点

              • 灵活:适合动态变化的二叉树。
              • 节省空间:只存储实际存在的节点。

              4. 缺点

              • 指针开销:每个节点需要额外的空间存储指针。
              • 访问效率:访问父节点需要从根节点遍历。

              二、顺序存储(基于数组)

              顺序存储通过数组来表示二叉树,利用数组下标的关系来定位父子节点。

              1. 存储规则

              • 根节点存储在索引 0
              • 对于索引为 i 的节点:
                • 左孩子索引:2*i + 1
                • 右孩子索引:2*i + 2
                • 父节点索引:(i-1)/2

              2. 示例

              二叉树:
                     1
                    / \
                   2   3
                  / \
                 4   5
              
              数组存储:[1, 2, 3, 4, 5, null, null]
              

              3. 代码实现

              #include <vector>
              #include <iostream>
              
              using namespace std;
              
              // 顺序存储的二叉树
              vector<int> tree = {1, 2, 3, 4, 5, -1, -1}; // -1 表示空节点
              
              // 获取左孩子
              int getLeftChild(int index) {
                  int left = 2 * index + 1;
                  return (left < tree.size() && tree[left] != -1) ? left : -1;
              }
              
              // 获取右孩子
              int getRightChild(int index) {
                  int right = 2 * index + 2;
                  return (right < tree.size() && tree[right] != -1) ? right : -1;
              }
              
              // 前序遍历
              void preOrder(int index) {
                  if (index == -1 || tree[index] == -1)
                      return;
                  cout << tree[index] << " "; // 访问根节点
                  preOrder(getLeftChild(index)); // 访问左子树
                  preOrder(getRightChild(index)); // 访问右子树
              }
              
              int main() {
                  cout << "前序遍历结果:";
                  preOrder(0); // 从根节点开始遍历
                  return 0;
              }
              

              4. 优点

              • 访问效率高:通过下标直接访问父子节点。
              • 适合完全二叉树:空间利用率高。

              5. 缺点

              • 空间浪费:对于非完全二叉树,数组中会有大量空节点。
              • 动态性差:插入和删除节点时需要移动大量元素。

              三、两种存储方式的对比

              特性 链式存储 顺序存储
              存储结构 节点 + 指针 数组
              空间效率 只存储实际节点 可能有空间浪费
              访问效率 需要遍历指针 通过下标直接访问
              适用场景 动态变化的二叉树 完全二叉树或静态二叉树
              实现难度 较简单 需要处理空节点

              四、选择存储方式的建议

              1. 链式存储:适合大多数场景,尤其是动态变化的二叉树。
              2. 顺序存储:适合完全二叉树或节点数量固定的场景。

              五、扩展:其他存储方式

              1. 线索二叉树:在链式存储的基础上,利用空指针指向遍历顺序的前驱或后继节点。
              2. 孩子兄弟表示法:将二叉树转化为多叉树,每个节点存储第一个孩子和右兄弟的指针。

              掌握这两种存储方式后,你可以根据具体问题选择最合适的实现方法!

              • @ 2025-2-1 9:18:56

                二叉平衡树(Balanced Binary Tree)是一种特殊的二叉搜索树(BST),它通过自动调整树的结构,确保树的高度始终保持在 O(log n) 级别,从而保证插入、删除和查找操作的高效性。


                一、为什么需要二叉平衡树?

                普通的二叉搜索树(BST)在最坏情况下会退化成链表,导致操作的时间复杂度从 O(log n) 变成 O(n)。例如:

                普通BST(退化成链表):
                1
                 \
                  2
                   \
                    3
                     \
                      4
                

                二叉平衡树通过旋转操作保持树的平衡,避免退化:

                平衡的BST:
                   2
                  / \
                 1   3
                      \
                       4
                

                二、常见的二叉平衡树

                1. AVL 树:通过严格的平衡因子(左右子树高度差不超过1)保持平衡。
                2. 红黑树:通过颜色标记和旋转操作,保持近似平衡。
                3. Splay 树:通过伸展操作,将最近访问的节点移动到根节点。
                4. Treap:结合二叉搜索树和堆的性质,通过随机优先级保持平衡。

                三、AVL 树:严格平衡的代表

                1. 核心思想

                • 每个节点的平衡因子(Balance Factor)定义为:左子树高度 - 右子树高度
                • 平衡因子必须为 -1、0 或 1,否则需要通过旋转调整。

                2. 旋转操作

                AVL 树通过四种旋转操作保持平衡:

                • 左旋(Left Rotation):解决右子树过高。
                • 右旋(Right Rotation):解决左子树过高。
                • 左右旋(Left-Right Rotation):先左旋再右旋。
                • 右左旋(Right-Left Rotation):先右旋再左旋。

                示例:右旋

                不平衡树:
                  3
                 /
                2
                /
                1
                
                右旋后:
                  2
                 / \
                1   3
                

                四、红黑树:近似平衡的代表

                1. 核心规则

                1. 每个节点是红色或黑色。
                2. 根节点是黑色。
                3. 每个叶子节点(NIL)是黑色。
                4. 如果一个节点是红色,则它的两个子节点都是黑色。
                5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

                2. 特点

                • 红黑树通过颜色标记和旋转操作,保持树的高度近似平衡。
                • 插入和删除操作的时间复杂度为 O(log n)。

                五、C++ 实现:AVL 树

                1. 节点定义

                struct Node {
                    int key;
                    Node* left;
                    Node* right;
                    int height; // 节点高度
                };
                

                2. 计算高度

                int height(Node* node) {
                    if (node == nullptr)
                        return 0;
                    return node->height;
                }
                

                3. 计算平衡因子

                int getBalance(Node* node) {
                    if (node == nullptr)
                        return 0;
                    return height(node->left) - height(node->right);
                }
                

                4. 右旋操作

                Node* rightRotate(Node* y) {
                    Node* x = y->left;
                    Node* T2 = x->right;
                
                    // 旋转
                    x->right = y;
                    y->left = T2;
                
                    // 更新高度
                    y->height = max(height(y->left), height(y->right)) + 1;
                    x->height = max(height(x->left), height(x->right)) + 1;
                
                    return x;
                }
                

                5. 插入操作

                Node* insert(Node* node, int key) {
                    // 1. 标准BST插入
                    if (node == nullptr)
                        return new Node{key, nullptr, nullptr, 1};
                
                    if (key < node->key)
                        node->left = insert(node->left, key);
                    else if (key > node->key)
                        node->right = insert(node->right, key);
                    else
                        return node; // 不允许重复键
                
                    // 2. 更新高度
                    node->height = 1 + max(height(node->left), height(node->right));
                
                    // 3. 获取平衡因子
                    int balance = getBalance(node);
                
                    // 4. 根据平衡因子调整
                    // 左左情况
                    if (balance > 1 && key < node->left->key)
                        return rightRotate(node);
                
                    // 右右情况
                    if (balance < -1 && key > node->right->key)
                        return leftRotate(node);
                
                    // 左右情况
                    if (balance > 1 && key > node->left->key) {
                        node->left = leftRotate(node->left);
                        return rightRotate(node);
                    }
                
                    // 右左情况
                    if (balance < -1 && key < node->right->key) {
                        node->right = rightRotate(node->right);
                        return leftRotate(node);
                    }
                
                    return node;
                }
                

                六、二叉平衡树的应用

                1. 数据库索引:B+ 树(基于平衡树)用于数据库索引。
                2. C++ STLstd::mapstd::set 基于红黑树实现。
                3. 文件系统:文件目录结构常用平衡树组织。

                七、总结

                特性 普通 BST AVL 树 红黑树
                平衡性 可能退化 严格平衡 近似平衡
                插入/删除 O(n)(最坏情况) O(log n)
                查找
                适用场景 数据静态 频繁查找 频繁插入/删除

                掌握二叉平衡树后,可以轻松应对动态数据的插入、删除和查找问题!

                • @ 2025-2-1 9:06:06

                  以下是C++线段树的详细教程,包含核心概念、代码实现和示例:

                  C++线段树教程

                  一、什么是线段树?

                  线段树(Segment Tree)是一种二叉树数据结构,用于高效处理区间查询单点/区间更新操作,典型时间复杂度为O(log n)

                  常见应用场景

                  • 区间求和/求积
                  • 区间最大值/最小值
                  • 区间GCD/LCM
                  • 区间覆盖统计

                  二、线段树结构

                           [1-8]
                          /     \
                      [1-4]    [5-8]
                      /   \     /   \
                   [1-2][3-4][5-6][7-8]
                   ...       ...       ...
                  

                  三、核心操作实现

                  1. 数据结构准备

                  const int MAXN = 1e5;  // 数据最大长度
                  int arr[MAXN];         // 原始数组
                  int tree[4*MAXN];      // 线段树数组(需要4倍空间)
                  

                  2. 建树(Build)

                  // 参数:当前节点,节点区间[left, right]
                  void build(int node, int left, int right) {
                      if (left == right) {
                          tree[node] = arr[left];
                          return;
                      }
                      
                      int mid = (left + right) / 2;
                      int leftChild = 2 * node;
                      int rightChild = 2 * node + 1;
                      
                      build(leftChild, left, mid);
                      build(rightChild, mid+1, right);
                      
                      // 合并子节点结果(求和示例)
                      tree[node] = tree[leftChild] + tree[rightChild];
                  }
                  

                  3. 区间查询(Query)

                  // 查询区间[qLeft, qRight]
                  int query(int node, int curLeft, int curRight, int qLeft, int qRight) {
                      // 当前区间与查询区间无交集
                      if (curRight < qLeft || curLeft > qRight)
                          return 0;
                      
                      // 当前区间完全包含在查询区间内
                      if (qLeft <= curLeft && curRight <= qRight)
                          return tree[node];
                      
                      // 部分重叠,继续递归查询
                      int mid = (curLeft + curRight) / 2;
                      int leftSum = query(2*node, curLeft, mid, qLeft, qRight);
                      int rightSum = query(2*node+1, mid+1, curRight, qLeft, qRight);
                      
                      return leftSum + rightSum;
                  }
                  

                  4. 单点更新(Update)

                  void update(int node, int curLeft, int curRight, int index, int newValue) {
                      if (curLeft == curRight) {
                          tree[node] = newValue;
                          return;
                      }
                      
                      int mid = (curLeft + curRight) / 2;
                      if (index <= mid)
                          update(2*node, curLeft, mid, index, newValue);
                      else
                          update(2*node+1, mid+1, curRight, index, newValue);
                      
                      tree[node] = tree[2*node] + tree[2*node+1];
                  }
                  

                  四、使用示例

                  int main() {
                      int n = 6;
                      arr[1] = 1; arr[2] = 3; arr[3] = 5;
                      arr[4] = 7; arr[5] = 9; arr[6] = 11;
                  
                      // 建树(根节点为1,区间[1,6])
                      build(1, 1, n);
                  
                      // 查询区间[2,5]的和
                      cout << query(1, 1, 6, 2, 5) << endl;  // 3+5+7+9=24
                  
                      // 更新arr[3]为10
                      update(1, 1, 6, 3, 10);
                  
                      // 再次查询
                      cout << query(1, 1, 6, 2, 5) << endl;  // 3+10+7+9=29
                  
                      return 0;
                  }
                  

                  五、变种实现

                  区间最小值版本

                  // 修改建树时的合并操作
                  tree[node] = min(tree[leftChild], tree[rightChild]);
                  
                  // 修改查询时的合并操作
                  return min(leftMin, rightMin);
                  
                  // 初始化时叶节点赋值为arr[i]
                  

                  六、注意事项

                  1. 数组大小:线段树数组需要开4倍原始数据大小
                  2. 索引起点:通常使用1-based索引更方便计算
                  3. 区间划分:注意处理mid的归属问题(左闭右开或全闭区间)
                  4. 延迟标记:进阶实现需要添加lazy propagation处理区间更新

                  七、复杂度分析

                  操作 时间复杂度 空间复杂度
                  建树 O(n)
                  查询 O(log n) O(1)
                  更新

                  掌握线段树后,可以轻松解决LeetCode上多种区间操作问题,如:307. 区域和检索 - 数组可修改、715. Range 模块等经典题目。

                  • 1