『数据结构』线段树
线段树原理线段树,类似区间树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为\(O(logn)\)。 线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如父亲的区间是\([a,b]\),那么\((c=(a+b)/2)\)左儿子的区间是\([a,c]\),右儿子的区间是\([c+1,b]\)。 下面我们从一个经典的例子来了解线段树,问题描述如下:从数组a[0...n-1]中查找某个数组某个区间内的最小值,其中数组大小固定,但是数组中的元素的值可以随时更新。 对这个问题一个简单的解法是:遍历数组区间找到最小值,时间复杂度是\(O(n)\),额外的空间复杂度\(O(1)\)。当数据量特别大,而查询操作很频繁的时候,耗时可能会不满足需求。 另一种解法:使用一个二维数组来保存提前计算好的区间\([i,j]\)内的最小值,那么预处理时间为\(O(n^2)\),查询耗时\(O(1)\),但是需要额外的\(O(n^2)\)空间,当数据量很大时,这个空间消耗是庞大的,而且当改变了数组中的某一个值时,更新二维数组中的最小值也很麻烦。 我们可以用线段树来解决这个问题:预处理耗时\(O(n)\),查询、更新操作\(O(logn)\),需要额外的空间\(O(n)\)。根据这个问题我们构造如下的二叉树 叶子节点是原始组数\(a\)中的元素 由于线段树的父节点区间是平均分割到左右子树,因此线段树是完全二叉树,对于包含\(n\)个叶子节点的完全二叉树,它一定有\(n-1\)个非叶节点,总共\(2n-1\)个节点,因此存储线段是需要的空间复杂度是\(O(n)\)。那么线段树的操作:创建线段树、查询、节点更新 是如何运作的呢(以下所有代码都是针对求区间最小值问题)? 对于线段树我们可以选择和普通二叉树一样的链式结构。由于线段树是完全二叉树,我们也可以用数组来存储,下面的讨论及代码都是数组来存储线段树,节点结构如下(注意到用数组存储时,有效空间为\(2n-1\),实际空间确不止这么多,比如上面的线段树中叶子节点\(1\)、\(3\)虽然没有左右子树,但是的确占用了数组空间,实际空间是满二叉树的节点数目。 线段树的代码实现建线段树的过程void build(int l,int r,int root) { if (l==r) { sum[root]=a[l]; return; } int mid=(l+r)>>1; build(l,mid,root<<1); build(mid+1,r,root<<1|1); pushup(root); } pushup过程void push(int root) { sum[root]=sum[root<<1]+sum[root<<1|1]; } 查询线段树int query(int ansl,int ansr,int l,int root) { if (ansl<=l && r<=ansr) return sum[root]; int mid=(l+r)>>1; int ans=0; if (ansl<=mid) ans+=query(ansl,ansr,l,root<<1); if (ansr>mid) ans+=query(ansl,mid+1,root<<1|1); return ans; } 单节点更新void update(int pos,int c,int root) { if (l==r) { sum[root]+=c; return; } int mid=(l+r)/2; if (pos<=mid) update(pos,c,root<<1); else update(pos,root<<1|1); pushup(root); } (编辑:ASP站长网) |