最近刚学习了两个数据结构,线段树与树状数组,现在来记录一下。
数组数组是一个原理比较复杂的结构,这边的话我的老师y总在蓝桥杯的辅导课里面没有具体介绍它的原理,因为确实很复杂然后也没必要讲,因为只是针对于蓝桥杯的话确实没必要弄清楚原理,会用就行。
结构类型:一棵结构很神奇的树
存储方式:一维数组
用途:能够在O(log n) 的时间复杂度内处理前缀和,支持单点修改操作(目前只学了这么多)
性能:很高很高
先上一张y总上课时画树状数组的结构图的图:
一看这棵树的结构就感觉很头大有木有!
所以说我们目前先不需要搞懂它是什么原理,会用它的操作就行。
第一个操作那就是求前缀和了。
我们用 a[N] 来表示我们原来的数组, c[N] 这个数组来表示我们的树状数组,那么 c[i] 的含义是什么呢?
c[i]的存的是,区间 (lowbit(i),i] 这个左开右闭的区间内的a[i] 的和。(切记,这个是关键)
lowbit函数:
int lowbit(int x)
{
return x&-x;
}//这个函数求的是 x 的二进制状态下的倒数第一个 1 以及它后面的 0
//比如 x 的二进制为 10010101100 ,则lowbit(x)=100 然后再转化成十进制
不要问为什么,就是这样,背过就好了。
下面给一下几个操作的代码。
int query(int x)
{
int ans=0;
for(int i=x;i>=1;i-=lowbit(i))ans+=a[i];
return ans;
}
int query(int l,int r)
{
int res1=0;
int res2=0;
for(int i=l-1;i;i-=lowbit(i))res1+=c[i];
//由于我们求的区间是包含左端点的,所以要减去 [1,l-1] 这个区间
for(int i=r;i;i-=lowbit(i))res2+=c[i];
//原理是用 [1,r] 区间的和减去 [1,l-1] 区间的和
return res2-res1;
}
void change(int x,int v)
{
for(int i=x;i<=n;i+=lowbit(i))c[i]+=-a[i]+v;
//原理是,改成某个值,相当于给这个值加上 -a[i]+v;
//比如 6 改成 9,相当于 6-6+9;
}
void add(int x,int v)
{
for(int i=x;i<=n;i+=lowbit(i))c[i]+=v;
}
(上面所有的操作都是转化成间接对于树状数组进行操作的)
还是那句话,树状数组的原理挺复杂的但是我们只需要知道操作就可以了。
线段树的话相当于树状数组来说原理就十分简单了。线段树是一棵类似于完全二叉树的情况,(最后一层可能不完全)。原理很简单,存储方式就和手写堆一样。线段树可以支持很多操作,它其实覆盖了很多算法,它是一个功能很全的算法。
不过它当然也不可能是一个完美的算法,它的效率比较低。
结构类型:完全二叉树
存储方式:一维数组,类似于手写堆
基本用途:维护区间的属性(总和,最值,等等,这里仅仅是基本用途,因为作者本身也才刚开始接触线段树)
性能:较低,但是也不低,部分题目可能会TLE(超时)
先来两张y总上课画的关于线段树结构和应用的图:
线段树的结构:
当然了这个一维数组是结构体类型的。
数组中的每一个成员,至少要存两个信息:
第一、当前成员维护的是哪一段区间
第二、当前区间的某一个属性(比如总和,最值等)
一般来说,数组的第一个成员(线段树的数组小标约定从 1 开始)即a[1] 维护的就是整个区间,以及整个区间的属性。
然后我们再根据树的特性,来继续划分。
a[1] 也就是树的根,那么它的两个儿子 就是 a[1x2] (左儿子) 和 a[1x2+1] (右儿子),同时,我们对于该区间一分为二,分别分配给两个儿子,以此类推,递归处理,知道我们当前的区间无法再继续分为止。(即每一个节点的 a[i] 的两个儿子就是 a[2 x i] (左儿子) 和 a[2 x i+1] (右儿子),并且两个儿子各自的区间等于自身的区间一分为二,然后分配给两个儿子)
上面就是线段树大概的存储方式,我们要始终明确一点:
数组中的每一个成员,都是维护一个区间的,并且会维护这个区间内的属性,并且父子之间有密切的关系。
下面来看一下线段树的几个基本操作。(假设我们当前维护的属性是区间内的最大值)
void build(int u,int l,int r)//建立线段树
{
/*注意当前函数是建立线段树的过程*/
if(l==r)tr[u]={l,r,w[r]};//如果说我们目前的区间里面只有一个值,那么我们最大值就是我们自己
else
{
tr[u]={l,r};//如果不是只有一个元素的话,那我们先把当前节点维护的区间赋值为l ,r
int mid=l+r>>1;//那我们就把区间一分为二,递归一下我们的左右儿子
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
tr[u].maxv=max(tr[u<<1].maxv,tr[u<<1|1].maxv);//那么我们当前的最大值,那就是我们的左右儿子里面的最大值
}
}
void push_up(int u)//利用它的两个儿子来算一下它的当前节点信息
{
tr[u].maxv=max(tr[u<<1].maxv,tr[u<<1|1].maxv);//那么我们当前的最大值,那就是我们的左右儿子里面的最大值
//这个函数就是用来更新节点信息的
}
int query(int u,int l,int r)
{
if(l<=tr[u].l&&tr[u].r<=r)return tr[u].maxv;//如果我们当前树中的区间已经包含在了我们所要求的区间的话,那么我们就可以直接
//返回我们的最大值了,因为这就是我们所需要的,这一步是不会出现错的,因为我们总是第一次达到这个状态,所以就也是我们所求的
int mid=tr[u].l+tr[u].r>>1;
int maxv=-5454887;
if(l<=mid)//如果我们所需要查询的左端点在我们目前节点所维护的区间中点的左边的话,那么说明有交集,那么就要查询一下
maxv=max(query(u<<1,l,r),maxv);
if(r>=mid+1)//如果我们所需要查询的右端点在我们目前节点所维护的区间的右边的话,那么说明有交集,那么就要查询一下
maxv=max(query(u<<1|1,l,r),maxv);
return maxv;//如果都不满足那就只能返回这个了,但是一般来说这个不会实现
}
别看线段树好像就这么点操作,其实线段树的功能是很多很多的,再次强调。(因为它维护的是一个区间,那么这个区间内的所有属性我们都能维护)
不过线段树最大的弊端就是性能稍微有点差。
这个是跑了十个数据加起来的时间:
但是其实用到线段树的地方还是挺多的,有时候我感觉应该是可以加一些优化然后加速的。
感觉线段树还是挺强的,只不过我现在还是没学到。加油吧!
现在学的算法知识难度已经慢慢上来了,学一个知识点还是要学的扎实一点,多总结总结,多复习复习,才能够真正的掌握这么一个算法!
好了,每篇博客后面都附上一句话。
做错并不可怕,可怕的是错过。