线段树,它是一种工具,能把一些对于区间(或者线段)的修改、维护,把时间复杂度降低O(logN)。如果一个数组下标分段,直到划分为一个元素,我们会发现可以构成一棵树。
这样看就是一颗满二叉树,但是不是每次都是满二叉树,因为会有奇偶的情况,所以其实是一个平衡二叉树。因为这里我们的树是使用数组实现的,就类似于堆排序中的树。
它其实类似于归并排序的思想,每次都划分子区间,这个树区间左孩子和右孩子的计算公式和堆排序创建的树是一样的。线段树的数组长度可能很大,所以我们定义一个较大值来初始化树的数组长度。这里我们用一个简单的例子来举例。
为了方便起见我们用三个元素举例:
#define TREE_MAX 1000
//构建树
//arr[6] = { 1,3,5,7,9,11 };
//tree[TREE_MAX] = {0};
//build_tree(arr, tree, 0, 5, 0)
//从0下标开始构建树,代码里关于node(L_N, R_N)都是树的下标
//L_N是树左节点的下标
//R_N是树右节点的下标
void build_tree(int* arr, int* tree, int left, int right, int node)
{
if (left == right)
{
tree[node] = arr[left];
}
else
{
//注意,带有node的都是和树的下标有关的
//左右区间中间值
int mid = (left + right) >> 1;
int L_N = 2 * node + 1;//左节点
int R_N = 2 * node + 2;//右节点
//划分区域
//将左节点划分区域
build_tree(arr, tree, left, mid, L_N);
//将右节点划分区域
build_tree(arr, tree, mid + 1, right, R_N);
//合并
tree[node] = tree[L_N] + tree[R_N];
}
}
我们来看看构建好的线段树。
之后因为是平衡二叉树,并不是完全二叉树,所以我们来把它补全,并看建好的树中的所有元素:
void update_tree(int* arr, int* tree, int left, int right, int node, int idex, int val)
{
if (left == right)
{
tree[node] = val;
arr[idex] = val;
}
else
{
int mid = (left + right) >> 1;
int L_N = 2 * node + 1;
int R_N = 2 * node + 2;
if (idex <= mid)
{//去左分支
update_tree(arr, tree, left, mid, L_N, idex, val);
}
else
{//去右分支
update_tree(arr, tree, mid + 1, right, R_N, idex, val);
}
tree[node] = tree[L_N] + tree[R_N];
}
}
//更新arr[4] 的值为6
update_tree(arr, tree, 0, sz - 1, 0, 4, 6);
当然线段树肯定不知只有这一点将修改数组的效率提高,还可以完成区间求和: 此时需要判断范围覆盖,毕竟本就是为了提高效率,增加空间提升时间的。如果查询范围没有任何交集直接返回0;如果当前求和范围完全覆盖了树节点的范围,那么直接返回这个树节点的值。比如我们目前求arr[6] = { 1,3,5,7,6,11 }下标为[2-5]的区间和。
注:原数组arr[6] = { 1,3,5,7,9,11 },我们利用update_tree(arr, tree, 0, sz - 1, 0, 4, 6)函数将原数组下标为4的值改为了6,所以更新为arr[6] = { 1,3,5,7,6,11 }。
此时就要看覆盖的范围了,如果全部覆盖,则直接返回当前树节点即可:
int query_tree(int* arr, int* tree, int left, int right, int node, int L, int R)
//L代表左区间, R代表右区间
如果各位不相信效率提升了,我们可以把调查的区间打印出来,看其中一个完全对覆盖的区间[3-5]看其是否还往下面继续搜索:
此时我们就已经完成所有操作,接下来附上全部代码:
#include<stdio.h>
#include<stdlib.h>
//定义一个最大树数组的长度
#define MAX_LEN 1000
//构建树
void build_tree(int* arr, int* tree, int left, int right, int node)
{
//当区间只有一个值(就是左右区间的值相等)
if (left == right)
{
tree[node] = arr[left];
}
else
{
int mid = (left + right) >> 1;
int L_N = node * 2 + 1;//找左孩子节点
int R_N = node * 2 + 2;//找右孩子节点
//划分区间
//给左孩子节点划分区间
build_tree(arr, tree, left, mid, L_N);
//给右孩子节点划分区间
build_tree(arr, tree, mid + 1, right, R_N);
//当前节点为左右孩子节点值的和
tree[node] = tree[L_N] + tree[R_N];
}
}
void update_tree(int* arr, int* tree, int left, int right, int node, int idex, int val)
{
if (left == right)
{
arr[idex] = val;
tree[node] = val;
}
else
{
int mid = (left + right) >> 1;
int L_N = node * 2 + 1;
int R_N = node * 2 + 2;
//因为是单个位置,我们需要判断区间
if (idex <= mid)
{
//去左节点
update_tree(arr, tree, left, mid, L_N, idex, val);
}
else
{
update_tree(arr, tree, mid + 1, right, R_N, idex, val);
}
//还是需要合并
tree[node] = tree[L_N] + tree[R_N];
}
}
//求和 首个树节点下标 求和左区间 求和右区间
int sum_tree(int* arr, int* tree, int left, int right, int node, int L, int R)
{
//这里需要判断是否覆盖
if (right < L || left > R)
{//没有公共部分
return 0;
}
else if (left >= L && right <= R)
{//完全覆盖
return tree[node];
}
else
{
int mid = (left + right) >> 1;
int L_N = node * 2 + 1;
int R_N = node * 2 + 2;
int sum1 = sum_tree(arr, tree, left, mid, L_N, L, R);
int sum2 = sum_tree(arr, tree, mid + 1, right, R_N, L, R);
return sum1 + sum2;
}
}
int main()
{
//线段树其实就是方便对数组的查询和修改
//它会降低暴力遍历的时间复杂度,是一种很好用的数据结构
//这里带有node的数据都是和tree数组下标有关的
int arr[] = { 5,7,0,8,4,2 };
int sz = sizeof(arr) / sizeof(arr[0]);
int segment_tree[MAX_LEN] = { 0 };
build_tree(arr, segment_tree, 0, sz - 1, 0);
int i = 0;
for (i = 0; i < 15; i++)
{
printf("tree[%-2d] = %-2d\n", i, segment_tree[i]);
}
printf("\n");
//修改
update_tree(arr, segment_tree, 0, sz - 1, 0, 2, 1);
for (i = 0; i < 15; i++)
{
printf("tree[%-2d] = %-2d\n", i, segment_tree[i]);
}
//求和
int ret = sum_tree(arr, segment_tree, 0, sz - 1, 0, 0, 2);
printf("ret = %d\n", ret);
return 0;
}
本人能力有限,有不足之处请在评论区指出,希望各位能认真看一看。