您当前的位置:首页 > 算法研究

C++树状数组 vs 线段树:傻傻分不清?

时间:2026-05-03 17:01:39  来源:  作者:

在C++算法竞赛中,处理区间问题(比如求区间和、区间最值、区间修改)时,有两个结构总是如影随形:树状数组(Binary Indexed Tree, BIT)线段树(Segment Tree)。初学者往往陷入纠结:这俩到底有什么区别?我什么时候用树状数组,什么时候用线段树?为什么有人说树状数组能做的线段树都能做,反之则不然?

本文将带你从原理、代码实现、功能边界、常数性能等多个维度,彻底理清这对“孪生兄弟”。全文约2000字,配有C++代码片段,适合已经学过二者但总是混淆的读者。

一、从名字说起:它们长什么样?

树状数组:紧凑的二进制魔法

树状数组利用整数的二进制分解思想,用一个数组 BIT[] 来维护前缀信息。它的核心操作是 lowbit(x) = x & -x,通过不断加减 lowbit 实现查询和更新。

  • 空间:O(n),仅需一个额外数组。

  • 代码:核心函数不超过10行,堪称“代码量最少的区间数据结构”。

  • 支持操作:单点加、前缀查询(如前缀和)。通过差分,可以支持区间加、单点查询。但难以直接支持区间加、区间查询(需要维护两个树状数组),也无法直接处理区间最值(除非有特殊性质)。

线段树:二叉树的全能战士

线段树把区间递归地分成左右两半,每个节点存储对应区间的聚合信息(和、最大值、最小值等)。它是一棵平衡二叉树,通常用数组存储(类似堆)。

  • 空间:O(4n)(一般开4倍大小)。

  • 代码:基本操作(建树、更新、查询)约40~50行,加上懒标记要达到80~100行。

  • 支持操作:理论上可以维护任何满足结合律的信息(和、积、最值、最大子段和、区间赋值、区间加乘混合等),配合懒标记实现区间修改。

一句话总结:树状数组是“专用工具”,线段树是“瑞士军刀”。

二、核心操作对比:代码见真章

我们以最常见的“区间求和 + 单点修改”为例,对比两者的代码。

树状数组实现

cpp
int BIT[N], n;

void add(int i, int delta) {
    while (i <= n) {
        BIT[i] += delta;
        i += i & -i;
    }
}

int sum(int i) {  // 前缀和 [1..i]
    int s = 0;
    while (i > 0) {
        s += BIT[i];
        i -= i & -i;
    }
    return s;
}

int range_sum(int l, int r) {
    return sum(r) - sum(l - 1);
}

简洁至极。建树只需对原数组每个位置调用 add(i, a[i]),O(n log n)。

线段树实现(不含懒标记)

cpp
int tree[4 * N], a[N];

void build(int p, int l, int r) {
    if (l == r) { tree[p] = a[l]; return; }
    int mid = (l + r) / 2;
    build(p*2, l, mid);
    build(p*2+1, mid+1, r);
    tree[p] = tree[p*2] + tree[p*2+1];
}

void update(int p, int l, int r, int pos, int val) {
    if (l == r) { tree[p] = val; return; }
    int mid = (l + r) / 2;
    if (pos <= mid) update(p*2, l, mid, pos, val);
    else update(p*2+1, mid+1, r, pos, val);
    tree[p] = tree[p*2] + tree[p*2+1];
}

int query(int p, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) return tree[p];
    int mid = (l + r) / 2, res = 0;
    if (ql <= mid) res += query(p*2, l, mid, ql, qr);
    if (qr > mid) res += query(p*2+1, mid+1, r, ql, qr);
    return res;
}

明显更冗长。但它的优势在区间修改时会体现出来。

三、功能边界:树状数组不能做什么?

很多同学听说“线段树能做的树状数组都能做”,这是不准确的。准确的说法是:对于可减的运算(如加法、异或),树状数组可以通过差分技巧实现区间加、区间查;对于不可减的运算(如最大值、最小值),树状数组很难处理区间查询。具体来说:

 
 
操作 树状数组 线段树
单点加 + 前缀查询 ✅ 天然支持 ✅ 杀鸡用牛刀
区间加 + 单点查 ✅ 用差分BIT ✅ 懒标记
区间加 + 区间查 ⚠️ 需维护两个BIT(加法可行) ✅ 懒标记
区间赋值 + 区间查 ❌ 难以支持 ✅ 懒标记(赋值标记)
区间最值查询 ❌ 不可行(除非无修改或特殊性质)
区间最大子段和 ❌ 不可行 ✅(需维护多个信息)
第k小/大 ✅ 可配合二分+树状数组(权值BIT) ✅ 线段树上二分

结论:树状数组本质上是前缀和思想的扩展,适合处理前缀可合并且可逆(即存在逆运算,比如加法的逆是减法)的信息。而线段树只需要结合律(可合并),不要求可逆,因此适用范围广得多。

四、性能与常数:写代码时的实际感受

时间常数

  • 树状数组:每次操作大约执行 log n 次循环,每次循环只有几次整数运算和内存访问。常数极小,实测通常比线段树快 3~5 倍。

  • 线段树:每次操作递归 log n 层,每层有函数调用开销、多次数组访问。即使迭代版线段树(非递归)常数也明显大于树状数组。

在 n = 2e5 且操作次数 2e5 的极限数据下,树状数组能轻松跑进 0.1 秒,而线段树可能需要 0.3~0.5 秒。常数敏感型题目(如卡常的 OI 题)中,树状数组是首选。

空间消耗

  • 树状数组:恰好 n+1 大小。

  • 线段树:通常 4n,对于 n=1e6,线段树需要约 4MB 存 int,加上 lz 数组就是 8MB,仍可接受;但 n=1e7 时,树状数组 40MB,线段树 160MB 可能 MLE。

编码复杂度

  • 树状数组:适合五分钟写完的场合,不容易写错。

  • 线段树:初学者平均调试半小时,尤其是懒标记下传时机、区间长度计算等细节。

五、实战选型指南:我到底该用哪个?

根据题目需求,按以下流程决策:

  1. 是否只需要前缀查询(或单点查询)和单点更新?
    → 树状数组足够,没必要上线段树。

  2. 是否涉及区间修改和区间查询,且运算为加法(或其它可减运算)?
    → 可以用“树状数组维护差分”的扩展(两个BIT),代码仍比线段树简洁。推荐学一下这个技巧。

  3. 查询运算不可减(如最大值、最小值),或有区间赋值操作?
    → 直接线段树,树状数组无能为力。

  4. 需要同时维护多个信息(如区间和、区间最大子段和)?
    → 线段树。

  5. 对常数时间要求极高,且功能树状数组刚好满足?
    → 树状数组。

  6. 题目只是“区间加+区间求和”模板题?
    → 两者皆可,但树状数组更短,推荐树状数组;如果你想练习线段树,也可以。

一个经典误区

Q: “我用线段树做区间求和,过了,但换树状数组是不是更快?”
A: 是的,但前提是你需要重新写差分树状数组。而如果你已经写了线段树,且不卡常,没必要重构。

六、进阶:树状数组的“超纲”用法

很多选手不知道,树状数组其实能做一些“看起来很线段树”的事情:

1. 区间加 + 区间求和(无需线段树)

维护两个BIT:B1B2。区间加 [l, r] 时:

text
add(B1, l, delta); add(B1, r+1, -delta);
add(B2, l, delta*(l-1)); add(B2, r+1, -delta*r);

前缀查询 sum(i) = sum(B1, i)*i - sum(B2, i)。区间和再用前缀和相减。代码量仍然远小于线段树。

2. 权值树状数组求第k小

在值域上建立树状数组(值域离散化),每个位置存储该值出现的次数。求第k小时,在BIT上二分(倍增法),复杂度 O(log V)。这是动态求第k小的简洁解法。

3. 二维树状数组

扩展成二维,支持子矩阵和、单点更新。二维线段树代码极其复杂,而二维BIT依然简洁。

七、线段树的“不可替代”场景

尽管树状数组能“曲线救国”实现部分区间操作,但有些场合线段树是唯一选择:

  • 区间取模、区间开方:这类操作无法通过差分简单实现,需要线段树的“暴力剪枝”思想(每个数开方几次后会变成1,记录最大值)。

  • 区间赋值 + 各种查询:懒标记必须支持覆盖操作,树状数组无法做到。

  • 维护复杂信息:比如区间最大连续子段和,需要节点存储前缀最大、后缀最大、总和、答案四个值,pushUp需要自定义合并逻辑——这正是线段树的拿手好戏。

八、总结:两张表帮你记住区别

 
 
维度 树状数组 线段树
代码长度 ~10行 ~50~100行
时间复杂度 O(log n) O(log n)
常数 极小 中等偏大
空间 n 4n
可维护信息 可减信息(和、异或) 任意结合信息(最值、和、乘积、自定义)
区间修改 需差分技巧(仅限加性) 懒标记(支持赋值、加乘等)
调试难度 中高

一句话建议

  • 如果你的需求是“单点改、区间查”或“区间加、单点查”,无脑树状数组。

  • 如果涉及到“区间最值”、“区间赋值”或复杂合并信息,别挣扎,上线段树。

  • 如果不确定,可以先写出树状数组,发现功能不够再换成线段树——毕竟重构代码也是学习过程。


希望这篇文章能让你彻底分清这对“孪生兄弟”。下次遇到区间问题,不要再凭感觉盲目选择,而是要理性分析:我的运算可逆吗?我需要懒标记吗?卡常数吗? 回答完这三个问题,答案自然浮出水面。

淮安信息学奥赛CSP J/S 培训网, 一个专注于信息学奥赛学习的网站,专注于信息学奥赛培优,助力于孩子在NOIP/NOI/CSP/IOI中取得好成绩。