今天刚接触这种数据结构,学完后把自己的思路整理一下,这篇博客就当做总结吧,有错误日后发现了修改。
分界线 |
为什么要用这种数据结构?
现在,就当我们还没听过这种数据结构,也不知道它的作用是什么,我们来思考以下几个问题。
现在我有一串数组,我要实现下面两个操作:
1.查询:我想知道L–R区间内的元素的和是多少。
2.更新:我要实现更新某一下标对应元素的值。
以上两个步骤会交叉进行多次。
那么,以我们现有的知识,我们会对上面问题进行这样的操作。
1.查询:用循环计算L–R区间内的元素累加和。时间复杂度O(n)。
2.更新:只需要对应arr[idx] = val(要修改的值)即可。时间复杂度O(1)。
我们知道,更新的操作只需要一步,非常快,而查询操作却要线性的时间复杂度,在两种操作并行的时候,查询操作会占去大部分时间。从而影响整体的效率。
那么,怎么把查询操作也变成O(1)的时间复杂度呢?
很简单,大家都知道,以前缀和的形式记录就行啦。
这样查询操作就变成sum[R] - sum[L-1]了,表示的是L到R区间内的元素和。
看起来变成了O(1),整体变快了,但是确实如此吗?
我们再来看看更新:这时候更新某一位置的元素,其后面的位置的前缀和也会跟着发生改变,也就是修改当前一个,要多修改后面n个。
这又变成了O(n)的时间复杂度了!
如图:
看来,鱼和熊掌不可兼得,不能将两者都维持到一个很低的复杂度上。那么,我想知道有没有一种方法,可以将整体的时间复杂度维持到一个比线性更快的水平上呢?这就切入正题,抛出线段树这一概念,解决的就是查询与更新并行时整体效率的问题
分界线 |
何为线段树?
首先概念是:
1.线段树是一颗二叉搜索树。
2.每个结点包含着数组对应区间的信息,并必有左右分支。
如何构建这样一颗树?
因为我们查询的时候无非是获取区间内的和,那么我就可以将数的端点用来存储对应区间的和,等到要用的时候找到搜索区间就好了,时间复杂度O(logn)。那么便顺着思路将根节点定义为 [0, a.size() -1 ]内的元素和,左右分支对半分割当前结点区间,左结点记录[ 0,mid ]和,右节点记录[ mid+1, a,size()-1 ]和。对于这两个分支,也是同理,不断分割区间,知道叶结点表示区间只有一个元素时到底。
理想的树结构应该是这样(图中结点内数字表示区间)
我这里以数组的方法建立树结构,那么标上下标就是
可以看到每次在建立左右分支的时候,其下标分别是当前node2+1和node2+2。
这样,我们就把树建好了。我们看看代码如何实现
详细的操作见代码及注释。
//树的生成
void build_tree(int arr[],int tree[], int node, int start, int end)//node表示当前结点,start and end表示当前结点对应在数组上的区间。
{
if(start==end) //递归出口,当对应区间只含有一个元素时,返回当前元素
{
tree[node] = arr[start];
return;
}
int mid = (start+end)/2;
int left_node = 2*node + 1; //左结点下标
int right_node = 2*node + 2;//右节点下标
build_tree(arr,tree,left_node,start,mid);//建立左树
build_tree(arr,tree,right_node,mid+1,end); //建立右树</