深入解析线段树 (Segment Tree):从基础到实际应用

336 阅读3分钟

线段树(Segment Tree)是一种强大的数据结构,特别适合用于解决动态区间问题,比如区间查询和更新。本文通过一个简单的示例详细介绍线段树的核心概念、实现以及运行过程,帮助你轻松掌握其原理。

什么是线段树?

线段树是一种二叉树结构,常用于分治法,将数组分为多个区间,以便高效处理以下操作:

  1. 区间查询:快速获取数组某一范围内的统计信息,如和、最大值或最小值。
  2. 区间更新:动态修改数组中的元素并维护统计信息。

示例:区间和问题

假设我们有一个数组 data = [1, 3, 5, 7, 9, 11],需要支持以下操作:

  1. 查询任意区间的和。
  2. 更新数组中的某个元素。

线段树的实现与运行过程

以下是用 TypeScript 实现的线段树代码,并结合示例数据逐步解析每行代码的作用和表现值。

构建线段树

class SegmentTree {
  private tree: number[]; // 线段树存储数组
  private n: number;      // 原数组长度

  constructor(data: number[]) {
    this.n = data.length;                      // 保存数组长度
    this.tree = new Array(4 * this.n).fill(0); // 分配足够空间
    this.build(0, 0, this.n - 1, data);        // 调用递归构建函数
  }

  private build(node: number, start: number, end: number, data: number[]): void {
    if (start === end) {
      this.tree[node] = data[start]; // 叶节点存储原数组值
    } else {
      const mid = Math.floor((start + end) / 2);
      const leftChild = 2 * node + 1;
      const rightChild = 2 * node + 2;
      this.build(leftChild, start, mid, data);   // 构建左子树
      this.build(rightChild, mid + 1, end, data); // 构建右子树
      this.tree[node] = this.tree[leftChild] + this.tree[rightChild]; // 合并子树值
    }
  }
}

示例解析

  • 初始化:

    • 输入数据 data = [1, 3, 5, 7, 9, 11]
    • 构建的线段树数组 tree,大小为 24(足够存储节点)。
  • 构建过程:

    • 根节点表示整个区间 [0,5] 的和 36。

    • 左子树表示区间 [0,2] 的和 9,右子树表示区间 [3,5] 的和 27。

    • 叶节点依次存储数组元素:

      • '36,9,27,4,5,16,11,1,3,0,0,7,9,0,0,0,0,0,0,0,0,0,0,0'

      • tree[7]=1,tree[8]=3,tree[4]=5,tree[11]=7,tree[12]=9,tree[6]=11

查询区间和

query(l: number, r: number): number {
  return this.queryRange(0, 0, this.n - 1, l, r);
}

private queryRange(
  node: number,
  start: number,
  end: number,
  l: number,
  r: number
): number {
  if (r < start || l > end) {
    return 0; // 查询范围与节点区间无交集
  }
  if (l <= start && r >= end) {
    return this.tree[node]; // 查询范围完全覆盖节点区间
  }
  const mid = Math.floor((start + end) / 2);
  const leftChild = 2 * node + 1;
  const rightChild = 2 * node + 2;
  const leftSum = this.queryRange(leftChild, start, mid, l, r);
  const rightSum = this.queryRange(rightChild, mid + 1, end, l, r);
  return leftSum + rightSum;
}

示例解析

  • 查询 [1,3] :

    1. 根节点 [0,5]:值为 36,范围部分重叠,递归查询左右子树。

    2. 左子树 [0,2]:值为 9,继续递归:

      • tree[7]=1 不在范围内,跳过。
      • tree[8]=3,完全在范围内,贡献值为 3。
      • tree[4]=5,完全在范围内,贡献值为 5。
    3. 右子树 [3,5]]:值为 27,继续递归:

      • tree[11]=7,完全在范围内,贡献值为 7。
      • tree[6]=11, tree[12]=9 不在范围内,跳过。
    4. 最终结果:3+5+7=15。

更新操作

update(index: number, value: number): void {
  this.updateValue(0, 0, this.n - 1, index, value);
}

private updateValue(
  node: number,
  start: number,
  end: number,
  index: number,
  value: number
): void {
  if (start === end) {
    this.tree[node] = value; // 更新叶节点值
  } else {
    const mid = Math.floor((start + end) / 2);
    const leftChild = 2 * node + 1;
    const rightChild = 2 * node + 2;
    if (index <= mid) {
      this.updateValue(leftChild, start, mid, index, value); // 更新左子树
    } else {
      this.updateValue(rightChild, mid + 1, end, index, value); // 更新右子树
    }
    this.tree[node] = this.tree[leftChild] + this.tree[rightChild]; // 更新父节点值
  }
}

示例解析

  • 更新索引 1 的值为 10:

    1. 修改叶节点:tree[8]=10。
    2. 更新父节点:tree[3]=tree[7]+tree[8]=1+10=11
    3. 更新父节点:tree[1]=tree[3]+tree[4]=11+5=16
    4. 更新根节点:tree[0]=tree[1]+tree[2]=16+27=43

总结

线段树是一种高效的区间操作工具,适用于动态查询与更新的场景。通过递归分治和二叉树结构,线段树可以快速处理大规模数据。希望本文的代码与示例能帮助你彻底掌握线段树的核心原理!