线段树

1. 背景

   假设现在有一个非常大的数组 arr,对数组里面的数字需要反复做两个操作:

       1)随机的选择一块区间,然后对区间里面的所有数字求和。

       2)随机修改数组里面的某一个值,即更新操作。

   因为数组是随机存取的,所以更新操作很容易,其时间复杂度是 $O(1)$。

   对于求和操作,其时间复杂度取决于区间的长度,假设区间长度为 $L$,则求和操作的时间复杂度为 $O(L)$。

   那有没有办法降低求和操作的时间复杂度呢?

   首先我们想到的是另外建立一个数组 sumArr,这个数组存储的是 arr 数组的前缀和,即

$$sumArr[i] = \sum_{j=0}^{i}arr[j]$$

   要求某段的和,只需在 sumArr 数组里找到对应下标做一次减法操作就可以了,时间复杂度变成了 $O(1)$。

   这样做的话更新操作的时间复杂度上升了,因为每次更新 arr 数组中的一个元素,都得更新数组 sumArr。

   那有没有办法平衡一下求和更新两个操作的时间复杂度呢?

   下面介绍线段树。

 

2. 线段树

   以下面数组进行举例:

       

   线段树本身是一个二叉树,每个结点代表一段区间,结点值就是区间内所有元素的和。

   对于线段树中的每一个非叶子节点 $[a,b]$,它的左儿子表示的区间为 $[a,\frac{a+b}{2}]$,右儿子表示的区间为 $[\frac{a+b}{2}+1,b]$,

   因此线段树是平衡二叉树。

   针对上面的数组,可以建立如下线段树:

       

   可以看出每个叶子结点就是单个元素。

   我们将根节点标号为 0,那么对于任意一个结点 $i$,其左孩子的结点 $id$ 为 $left = 2i + 1$,右孩子的结点 $id$ 为 $right = 2i + 2$。

   因为其本身是一个平衡二叉树,所以可以用数组存储,浪费的空间小。

#include <stdio.h>

#define MAX_LEN 1000

void BuildTree(int arr[], int tree[], int node, int start, int end)
{
    if(start == end) {
        tree[node] = arr[start];
    }
    else {
        int mid = (start + end) / 2;
        int leftNode = node * 2 + 1;
        int rightNode = node * 2 + 2;

        BuildTree(arr, tree, leftNode, start, mid);
        BuildTree(arr, tree, rightNode, mid + 1, end);

        tree[node] = tree[leftNode] + tree[rightNode];
    }
}

void UpdateTree(int arr[], int tree[], int node, int start, int end, int idx, int value)
{
    if (start == end) {
        arr[idx] = value;
        tree[node] = value;
    }
    else {
        int mid = (start + end) / 2;
        int leftNode = node * 2 + 1;
        int rightNode = node * 2 + 2;

        if (idx >= start && idx <= mid) {
            UpdateTree(arr, tree, leftNode, start, mid, idx, value);
        }
        else {
            UpdateTree(arr, tree, rightNode, mid + 1, end, idx, value);
        }

        tree[node] = tree[leftNode] + tree[rightNode];
    }
}

int QueryTree(int arr[], int tree[], int node, int start, int end, int L, int R)
{
    if (R < start || L > end) return 0;
    if (start == end) return tree[node];
    if(L <= start && end <= R) return tree[node];

    int mid = (start + end) / 2;
    int leftNode = node * 2 + 1;
    int rightNode = node * 2 + 2;
    int sumLeft = QueryTree(arr, tree, leftNode, start, mid, L, R);
    int sunRight = QueryTree(arr, tree, rightNode, mid + 1, end, L, R);

    return sumLeft + sunRight;
}

int main()
{
    int arr[] = {1,3,7,9,11};
    int size = 6;
    int tree[MAX_LEN] = {0};

    BuildTree(arr, tree, 0 , 0, size - 1);

    return 0;
}

 

posted @ 2020-09-13 10:55  _yanghh  阅读(314)  评论(0编辑  收藏  举报