1.概念
树状数组是一个查询和修改复杂度都为log(n)的数据结构。主要用于数组的单点修改&&区间求和
线段树是一个查询和修改复杂度都为log(n)的数据结构。主要用于数组的单点修改&&单点查询&&区间求和&&区间修改. 另外一个拥有类似功能的是树状数组,但是树状数组最常用的是单点修改&&区间求和. 线段树完全涵盖树状数组所有功能
具体区别和联系如下:
- 1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树.
- 2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决.
- 3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树。
图形解析:
1.线段树:是一个二叉树,二个子节点对半分,空间复杂度为o(4n),原因为按照倍数公式可计算所有的节点数为4n
1.树状数组:用一组数组表示一颗树,
更新过程是每次加了个二进制的低位1(101+1 ->110, 110 + 10 -> 1000, 1000 + 1000 -> 10000)
为什么线段树空间复杂度为O(4N)原因如下:
首先线段树是一棵二叉树,最底层有n个叶子节点(n为区间大小) 那么由此可知,此二叉树的高度为
,可证
然后通过等比数列求和
求得二叉树的节点个数,具体公式为,
(x为树的层数,为树的高度+1) 化简可得
,整理之后即为4n(近似计算忽略掉-1) 证毕
2.算法模板:
1.数状数组(可用于数组的单点修改&&区间求和):
int mod = 1e9 + 7;
int n;
//树状数组模板(分治法) :常用来求区间和或者区间个数 算法时间复杂度logn
int tr1[100] = {0};
//求x的最低位1的二进制
int lowbit(int x) { //x最后一个1截断的后半段二进制 如lowbit(10100)=100;
return x & -x;
}
//更新索引为x的值,更新+c;
void add(int x, int c) {
while (x <= 6) { //循环包含x的节点,自底向上
tr1[x]=(tr1[x] + c) % mod;
x += lowbit(x); //更新时向前
}
}
//计算1-x的区间和
int sum(int x) {
int res = 0;
while (x) //
{
res = (tr1[x] + res) % mod;
x -= lowbit(x);//更新时向后
}
return res;
}
//闭合区间和
int sum1(int x,int y){
int res = 0;
res = sum(y) - sum(x - 1);
return res;
}
2.线段树(可用于数组的单点修改&&单点查询&&区间求和&&区间修改):
//最好是全局开long long
#include <bits/stdc++.h>
using namespace std;
#define mem(a, b) memset(a, b, sizeof a)
#define IN freopen("in.txt", "r", stdin)
#define DEBUG(a) cout << (a) << endl
typedef long long ll; //类型声明
int dir8[8][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
int dir4[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
const int INF = 0x3f3f3f3f; //4个3f可代表一个大数
int mod = 1e9 + 7;
const int maxn = 1e5 + 10;
struct node
{
int l, r, w, flag; //l-r的区间 w为总和 flag为暂存标志,等待下一步查询时进行更新
int dis() { return r - l + 1; }
int mid() { return (r + l) / 2; }
} a[maxn * 4];
//建树代码区间长度为l-r,k为结构体数组索引,k为头结点索引 初始化为里面的a[k] = {l, r, 0, 0};
void build(int k, int l, int r)
{ //当前节点的区间
a[k] = {l, r, 0, 0};
if (l == r)
{
cin >> a[k].w;
return;
}
build(k << 1, l, a[k].mid());
build(k << 1 | 1, a[k].mid() + 1, r);
a[k].w = a[k << 1].w + a[k << 1 | 1].w;
}
//基础函数,查询是下方标志位
void down(int k)
{
a[k << 1].w += a[k << 1].dis() * a[k].flag;
a[k << 1 | 1].w += a[k << 1 | 1].dis() * a[k].flag;
a[k << 1].flag += a[k].flag;
a[k << 1 | 1].flag += a[k].flag;
a[k].flag = 0;
}
//单点或者区间更新 头结点开始查询,区间更新范围为l-r,单点的时候为l=r;
// k一般为1 从头结点开始查询(编号一般为1 23 4567) 区间范围为
void update(int k, int l, int r, int w)
{ // 要更新的总区间.(l,r)不变
if (a[k].l >= l && a[k].r <= r)
{
a[k].w += w * a[k].dis();
a[k].flag += w;
return;
}
if (a[k].flag)
down(k);
if (a[k].mid() >= l)
update(k << 1, l, r, w); //左节点
if (a[k].mid() < r)
update(k << 1 | 1, l, r, w); //右节点
a[k].w = a[k << 1].w + a[k << 1 | 1].w;
}
//区间或者单点求和 从头结点k=1开始查询,计算l-r区间的总和
int query(int k, int l, int r)
{ //基本就是二分法的变形
if (a[k].l >= l && a[k].r <= r) //结束条件
return a[k].w;
if (a[k].flag) //中级处理操作
down(k);
int sum = 0;
//开始分治
if (a[k].mid() >= l)
sum += query(k << 1, l, r);
if (a[k].mid() < r)
sum += query(k << 1 | 1, l, r);
a[k].w = a[k << 1].w + a[k << 1 | 1].w; //最后下面的全部回溯回来,总结结果
return sum;
}
int main()
{
//IN;
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
build(1, 1, n);
while (m--)
{
int func;
int l, r, w;
cin >> func;
if (func == 1)
{
cin >> l >> r >> w;
update(1, l, r, w);
}
else if (func == 2)
{
cin >> l >> r;
cout << query(1, l, r) << endl;
}
/* code */
}
return 0;
}
3.总结:
对待不同的问题使用不同的数据结构可很好的降低时间复杂度与代码结构复杂度,对于树状数组与线段树,在单点修改&&区间查询方面优先使用树状数组,其代码结果简单,空间复杂度底。其他树状数组实现不了的在考虑线段树。