了解一下线段树

线段树是什么?

线段树是一种二叉树 结构,用于存储和处理区间相关的信息。它的每个节点都代表一个区间:

  • 叶子节点:存储原始数组的单个元素。
  • 非叶子节点:存储其子节点所代表区间的某种聚合信息(例如,和、最大值、最小值等)。

通过这种结构,线段树可以将复杂的区间操作分解为对树的递归操作,从而提高效率。

例如,假设我们有一个数组 arr = [1, 3, 5, 7, 9, 11],我们可以用线段树来快速计算任意区间的和,比如 [1, 3] 的和(即 3+5+7=15)。


线段树的基本结构

以数组 [1, 3, 5, 7, 9, 11] 为例,线段树会将整个数组划分为多个区间,并通过树形结构存储这些区间的聚合信息(这里以"和"为例):

  • 根节点:代表整个区间 [0, 5],值为 1+3+5+7+9+11=36。
  • 根的左子节点:代表左半部分 [0, 2],值为 1+3+5=9。
  • 根的右子节点:代表右半部分 [3, 5],值为 7+9+11=27。
  • 依此类推,直到叶子节点,每个叶子节点存储单个元素。

线段树的操作

线段树支持以下三种主要操作:

(1) 构建线段树
  • 从原始数组开始,递归地将区间一分为二,直到每个叶子节点代表一个元素。
  • 每个非叶子节点的值是其左右子节点值的聚合(例如求和)。
(2) 区间查询
  • 查询某个区间 [left, right] 的聚合值(比如和)。
  • 根据当前节点的区间与查询区间的关系,决定是直接返回节点值、跳过,还是递归查询子节点。
(3) 单点更新
  • 更新数组中某个位置的值,然后同步更新线段树中所有受影响的节点。

这些操作的时间复杂度均为 O(log n),因为线段树的高度是对数组长度 n 取对数。


Java 实现

下面是一个完整的 Java 代码示例,展示如何实现线段树,包括构建、查询和更新操作。我们以计算区间和为例。

线段树的构建

java 复制代码
class SegmentTree {
    private int[] tree; // 线段树数组
    private int n;      // 原始数组长度

    // 构造函数:传入原始数组,初始化线段树
    public SegmentTree(int[] arr) {
        n = arr.length;
        tree = new int[4 * n]; // 线段树的空间通常需要 4 倍原始数组大小
        buildTree(arr, 1, 0, n - 1); // 从节点 1 开始构建
    }

    // 递归构建线段树
    private void buildTree(int[] arr, int node, int start, int end) {
        if (start == end) {
            // 叶子节点,直接存储数组元素
            tree[node] = arr[start];
        } else {
            int mid = (start + end) / 2;
            // 递归构建左子树和右子树
            buildTree(arr, 2 * node, start, mid);
            buildTree(arr, 2 * node + 1, mid + 1, end);
            // 父节点值为左右子节点之和
            tree[node] = tree[2 * node] + tree[2 * node + 1];
        }
    }
}

查询操作

java 复制代码
    // 公开方法:查询区间 [left, right] 的和
    public int query(int left, int right) {
        return query(1, 0, n - 1, left, right);
    }

    // 递归查询区间和
    private int query(int node, int start, int end, int left, int right) {
        if (right < start || left > end) {
            return 0; // 无交集,返回 0
        }
        if (left <= start && end <= right) {
            return tree[node]; // 完全包含,直接返回节点值
        }
        // 部分重叠,递归查询左右子树
        int mid = (start + end) / 2;
        int leftSum = query(2 * node, start, mid, left, right);
        int rightSum = query(2 * node + 1, mid + 1, end, left, right);
        return leftSum + rightSum;
    }

更新操作

java 复制代码
// 公开方法:更新位置 index 的值为 val
    public void update(int index, int val) {
        update(1, 0, n - 1, index, val);
    }

    // 递归更新
    private void update(int node, int start, int end, int index, int val) {
        if (start == end) {
            // 到达叶子节点,更新值
            tree[node] = val;
        } else {
            int mid = (start + end) / 2;
            if (index <= mid) {
                update(2 * node, start, mid, index, val); // 更新左子树
            } else {
                update(2 * node + 1, mid + 1, end, index, val); // 更新右子树
            }
            // 更新父节点
            tree[node] = tree[2 * node] + tree[2 * node + 1];
        }
    }
代码说明
  • tree 数组:用一维数组表示线段树,节点编号从 1 开始,左子节点为 2 * node,右子节点为 2 * node + 1。
  • 空间需求:线段树通常需要 4n 的空间,以确保能容纳所有节点。
  • 构建:从根节点递归划分区间,计算每个节点的和。
  • 查询:根据区间关系递归计算结果。
  • 更新:从叶子节点向上更新所有受影响的节点。

图解(ASCII 表示)

以下是以数组 [1, 3, 5, 7, 9, 11] 为例的线段树结构(表示区间和):

java 复制代码
          [0,5]:36
        /          \
  [0,2]:9         [3,5]:27
  /     \         /     \
[0,1]:4 [2,2]:5 [3,4]:16 [5,5]:11
 / \              / \       /\
[0]:1 [1]:3   [3]:7 [4]:9 [5]:11
解释
  • 0,5\]:36 是根节点,表示整个区间的和。

  • 0\]:1、\[1\]:3 等是叶子节点,表示单个元素。