授课语音

线段树的基本操作

1. 线段树的简介

线段树(Segment Tree)是一种树形数据结构,广泛应用于处理区间查询和区间更新问题。它可以在 O(log n) 的时间复杂度内解决很多涉及区间的操作,比如区间求和、区间最小值、区间最大值等问题。线段树可以看作是对数组的一种分治方式,将问题分解为较小的子问题,以提高查询和更新的效率。

1.1 线段树的应用场景

  1. 区间求和问题:给定一个数组,要求在区间 [l, r] 内求和。
  2. 区间最小值/最大值问题:要求在区间 [l, r] 内求最小值或最大值。
  3. 区间更新问题:对数组的某个区间进行修改,比如加值、减值、设置某个数等。

1.2 线段树的结构

线段树是一棵完全二叉树,每个节点存储的是某个区间的某个属性值(如和、最小值、最大值等)。树的叶子节点表示的是数组的元素,而非叶子节点表示的是其子区间的合并结果。

例如,如果我们要构建一个求和的线段树,树的根节点表示整个数组的和,根的左子节点表示数组前一半的和,右子节点表示后一半的和。

2. 线段树的基本操作

线段树主要有以下几个基本操作:

  1. 构建线段树:从数组构建出线段树的结构。
  2. 区间查询:查询一个区间的结果(如求和、求最小值、最大值等)。
  3. 区间更新:更新区间内的元素,比如修改某个区间的值。

2.1 构建线段树

线段树的构建需要一个数组和一个递归过程,构建过程通常是递归分治的方式。每个节点都包含一个区间的相关信息。

2.2 区间查询

查询操作是通过递归的方式合并子节点的结果来完成的。例如,查询一个区间的和,可以通过递归地将区间分成若干子区间,然后将这些子区间的和合并得到最终结果。

2.3 区间更新

更新操作有两种常见的方式:

  • 单点更新:只修改某个位置的值。
  • 区间更新:对某个区间的值进行修改。这个操作可能需要涉及到懒标记(Lazy Propagation)来优化更新过程。

3. 线段树的Java实现

接下来,我们将通过一个简单的例子来演示线段树的基本操作,首先实现线段树的构建、区间查询以及单点更新。

3.1 线段树的实现代码

public class SegmentTree {
    private int[] tree;   // 线段树数组
    private int n;        // 数组的大小

    // 构造函数,构建线段树
    public SegmentTree(int[] arr) {
        this.n = arr.length;
        this.tree = new int[4 * n]; // 线段树的大小是4倍于原数组大小
        build(arr, 0, 0, n - 1);  // 从根节点开始构建线段树
    }

    // 构建线段树
    private void build(int[] arr, int node, int start, int end) {
        if (start == end) {
            tree[node] = arr[start];  // 叶子节点保存数组中的值
        } else {
            int mid = (start + end) / 2;
            int leftChild = 2 * node + 1;
            int rightChild = 2 * node + 2;
            build(arr, leftChild, start, mid);  // 构建左子树
            build(arr, rightChild, mid + 1, end); // 构建右子树
            tree[node] = tree[leftChild] + tree[rightChild]; // 合并左右子树的结果
        }
    }

    // 查询区间 [l, r] 的和
    public int query(int l, int r) {
        return query(0, 0, n - 1, l, r);
    }

    // 区间查询操作
    private int query(int node, int start, int end, int l, int r) {
        if (r < start || end < l) {
            return 0;  // 当前节点与查询区间不重合,返回 0
        }
        if (l <= start && end <= r) {
            return tree[node];  // 当前节点完全在查询区间内,返回节点值
        }
        // 当前节点部分在查询区间内,递归查询左右子树
        int mid = (start + end) / 2;
        int leftChild = 2 * node + 1;
        int rightChild = 2 * node + 2;
        int leftQuery = query(leftChild, start, mid, l, r);
        int rightQuery = query(rightChild, mid + 1, end, l, r);
        return leftQuery + rightQuery;  // 合并结果
    }

    // 更新数组中某个位置的值
    public void update(int idx, int value) {
        update(0, 0, n - 1, idx, value);
    }

    // 更新操作
    private void update(int node, int start, int end, int idx, int value) {
        if (start == end) {
            tree[node] = value;  // 更新叶子节点的值
        } else {
            int mid = (start + end) / 2;
            int leftChild = 2 * node + 1;
            int rightChild = 2 * node + 2;
            if (start <= idx && idx <= mid) {
                update(leftChild, start, mid, idx, value);  // 更新左子树
            } else {
                update(rightChild, mid + 1, end, idx, value); // 更新右子树
            }
            tree[node] = tree[leftChild] + tree[rightChild];  // 合并更新后的结果
        }
    }
}

3.2 代码解释

  1. 构造函数:我们首先创建一个长度为 4 * n 的数组 tree 来存储线段树。线段树的大小一般是原数组大小的 4 倍,因为在最坏的情况下,叶子节点的数量是 n,而根节点到叶子节点之间最多需要 2 次分割,每次都需要 2 个子节点。因此,线段树的节点数是 4 * n

  2. build 函数:这个函数用于递归地构建线段树。叶子节点对应数组中的元素,非叶子节点是左右子节点的合并结果。

  3. query 函数:这是区间查询的核心函数,用于查询区间 [l, r] 的和。它会递归地判断当前节点是否与查询区间有交集,并通过递归调用合并子节点的查询结果。

  4. update 函数:这是单点更新操作。当数组中的某个元素值发生变化时,调用 update 函数对相应节点的值进行更新,并向上更新其父节点的值。

4. 线段树的时间复杂度

  • 构建线段树:构建线段树的时间复杂度是 O(n),因为我们需要对数组进行一次递归处理,树的深度是 O(log n),每个节点的合并操作是 O(1) 的。
  • 区间查询:查询操作的时间复杂度是 O(log n),因为线段树的深度是 O(log n),在查询过程中最多访问到 O(log n) 个节点。
  • 单点更新:更新操作的时间复杂度也是 O(log n),因为在更新过程中,最多需要修改 O(log n) 个节点的值。

5. 总结

线段树是一种高效的数据结构,适用于处理动态区间查询和区间更新问题。它通过将一个区间问题分解成多个子区间,从而实现了高效的查询和更新操作。通过本讲解,我们了解了线段树的基本构建、查询和更新操作,并实现了一个简单的线段树示例。在实际应用中,线段树可以有效地提升许多区间操作问题的效率,尤其是当数据量很大时,传统的暴力方法往往无法满足性能要求。

希望今天的课程能帮助你更好地理解和使用线段树,解决实际问题。

去1:1私密咨询

系列课程: