跳转至

Chapter 5 Segment Tree

§1 Motivation

1.1Application Scenario

【场景】给定一个长度可达百万级的数组A[1000000],需要频繁执行任意区间[L, R]的聚合查询(以区间和为例)

1.2Naive Implementation

最直接的实现方式是遍历区间内的元素逐个累加,代码实现如下:

ElementType  Query ( ElementType A[ ],  int L, int R ) 
{ 
      ElementType sum = 0;
      for  ( int i = L; i <= R; ++i )
          sum += A[i];
      return sum;
} 
- 时间复杂度:T(N) = O(N)

  • 缺陷:对于高频次的区间查询操作,线性时间复杂度的性能极差,无法满足大规模数据的处理需求。

§2 Structure

2.1 Definition

【定义】线段树是一棵完全二叉树,树中每个节点对应原数组的一个区间[start, end]: - 叶子节点:对应区间长度为1,即原数组的单个元素A[i],为直接存储的数值

  • 非叶子节点:对应区间被划分为左右两个子区间,分别对应其左、右孩子节点,存储该区间的聚合结果(以区间和为例,即左右孩子节点的值之和,为动态计算值)

2.2Example Structure

以长度为5的数组A[5] = {7,2,5,3,8}为例,对应的线段树结构如下: - 根节点:对应区间[0,4],存储区间和25 - 左子树根节点:对应区间[0,2],存储区间和14;右子树根节点:对应区间[3,4],存储区间和11 - 递归拆分至叶子节点,分别对应[0](7)、[1](2)、[2](5)、[3](3)、[4](8)

                     [0,4]=25
                    /        \
                   /          \
            [0,2]=14          [3,4]=11
           /       \          /       \
     [0,1]=9     [2,2]=5   [3,3]=3  [4,4]=8
      /     \
 [0,0]=7  [1,1]=2

2.3Array Representation

线段树作为完全二叉树,可通过数组高效存储,与二叉堆的存储逻辑一致: - 定义数组tree[]存储线段树的节点值

  • 对于数组中索引为node的节点:

  • 左孩子节点索引:2 * node

  • 右孩子节点索引:2 * node + 1

  • 空间复杂度:S(N) = O(2N-1) = O(N),对于长度为N的原数组,线段树仅需线性级别的存储空间。

将上述树映射到 tree[] 数组中(根节点从索引 1 开始):

节点索引:     1     2     3     4     5     6     7     8     9
tree[node]:  25    14    11    9     5     3     8     7     2
对应区间:    [0,4] [0,2] [3,4] [0,1] [2,2] [3,3] [4,4] [0,0] [1,1]

§3 Operations

线段树的核心操作包括构建(Build)区间查询(Query)单点更新(Update),所有操作均基于递归实现。·

3.1 Build Operation

递归构建完全二叉树:自顶向下拆分区间,直到区间长度为1(叶子节点),直接赋值为原数组对应元素;自底向上合并左右子节点的聚合值,完成非叶子节点的赋值。该操作仅需执行一次,作为预处理步骤。

void Build ( int node, int start, int end )
{
    // Base Case: Leaf Node(叶子节点,区间长度为1)
    if ( start == end ) {
        tree[ node ] = A[ start ];
        return;
    }

    // Recursive Step: Split and Build Children(递归拆分区间,构建左右子树)
    int mid = ( start + end ) / 2;
    Build ( 2 * node, start, mid );
    Build ( 2 * node + 1, mid + 1, end );

    // Merge Logic: Sum of children(合并结果,当前节点值为左右孩子之和)
    tree[ node ] = tree[ 2 * node ] + tree[ 2 * node + 1 ];
}
  • 初始调用:通常从根节点开始,即Build(1, 0, n-1)(n为原数组长度,根节点索引为1,原数组下标从0到n-1)

  • 时间复杂度:T(N) = O(N),线段树共有约2N个节点,每个节点仅被访问并赋值一次,构建操作仅需执行一次。

3.2 Query Operation

根据当前节点对应的区间与查询区间[L, R]的重叠关系,分三类情况处理,避免遍历原数组的所有元素,实现对数时间复杂度的区间查询: 1. 无重叠(No Overlap):当前节点区间完全在查询区间外,对查询结果无贡献,返回聚合操作的单位元(求和操作的单位元为0) 2. 完全重叠(Total Overlap):当前节点区间完全在查询区间内,直接返回当前节点存储的聚合值,无需继续递归 3. 部分重叠(Partial Overlap):当前节点区间与查询区间部分相交,递归查询左右子树,合并左右子树的查询结果作为当前节点的返回值

int Query ( int node, int start, int end, int L, int R )
{
    // Case 1: No Overlap (Node is completely outside query range)
    if ( R < start || end < L ) {
        return 0;
    }

    // Case 2: Total Overlap (Node is completely inside query range)
    if ( L <= start && end <= R ) {
        return tree[ node ];
    }

    // Case 3: Partial Overlap (We need to go deeper into both children)
    int mid = ( start + end ) / 2;
    int left_sum = Query ( 2 * node, start, mid, L, R );
    int right_sum = Query ( 2 * node + 1, mid + 1, end, L, R );

    return left_sum + right_sum;
}
  • 初始调用:Query(1, 0, n-1, L, R),从根节点开始查询区间[L, R]的和

  • 时间复杂度:T(N) = O(log N),每次递归最多访问树的两层,线段树的高度为⌊log N⌋,因此单次查询仅需对数级别的时间,适合高频次区间查询。

Query(1, 3) 为例,查询 A[1..3] 的区间和(预期结果: 2+5+3=10):

【T】完全重叠直接返回  【P】部分重叠继续递归  【N】无重叠返回0

              [0,4]=25 【P】
             /           \
      [0,2]=14 【P】     [3,4]=11 【P】
      /        \         /        \
[0,1]=9【P】  [2,2]=5【T】 [3,3]=3【T】 [4,4]=8【N】
  /       \
[0,0]=7【N】 [1,1]=2【T】

结果 = 标记【T】的节点值相加 = 5 + 3 + 2 = 10

3.3 Update Operation

定位到原数组索引idx对应的线段树叶子节点,更新其值为新值val;随后自底向上回溯,更新所有包含该索引的祖先节点的聚合值,保证整棵线段树的聚合结果正确。

void Update ( int node, int start, int end, int idx, int val )
{
    // Base Case: Leaf Node found(找到对应索引的叶子节点,完成更新)
    if ( start == end ) {
        tree[ node ] = val;
        return;
    }

    // Recursive Step: Go left or right depending on where “idx” is(递归定位到对应子树)
    int mid = (start + end) / 2;
    if ( start <= idx && idx <= mid ) {
        // Index is in the left child(索引在左子树,递归更新左子树)
        Update ( 2 * node, start, mid, idx, val );
    } else {
        // Index is in the right child(索引在右子树,递归更新右子树)
        Update ( 2 * node + 1, mid + 1, end, idx, val );
    }

    // Backtracking Step: Update current node based on new children values(回溯更新当前节点值)
    tree[ node ] = tree[ 2 * node ] + tree[ 2 * node + 1 ];
}
  • 初始调用:Update(1, 0, n-1, idx, val),从根节点开始,将原数组idx位置的元素更新为val
  • 时间复杂度:T(N) = O(log N),仅需遍历从根节点到目标叶子节点的一条路径(长度为树的高度log N),回溯更新祖先节点同样仅需log N步,单次更新为对数时间复杂度。

Update(idx=1, val=10) 为例,将 A[1] 从 2 更新为 10:

                   [0,4]
                 25 → 33        ← 回溯: 22+11=33
                /        \
         [0,2]            [3,4]
       14 → 22   ←回溯      11 (不变)
      /       \            /      \
   [0,1]      [2,2]     [3,3]    [4,4]
  9 → 17  ←回溯 5(不变)  3(不变)  8(不变)
   /   \
[0,0]  [1,1]
7(不变)  2→10 ★ 更新点

§4 Additional Notes

  1. 通用聚合操作支持 线段树不仅限于区间和查询,可适用于任意满足区间聚合性质的操作,例如区间最小值(min)、区间最大值(max)、区间平均值等。只需对应修改合并逻辑与无重叠场景的返回值(如区间最小值查询,无重叠时返回无穷大)即可。

  2. 区间更新与懒传播 对于更通用的区间更新操作(例如给区间[2, 1000]内的所有元素统一加10),直接逐点更新的时间复杂度会退化为O(N log N),此时可使用懒传播(Lazy Propagation) 策略优化,将区间更新的时间复杂度也降至O(log N),该部分内容建议自主学习。