1. 数据结构双雄:树状数组与线段树实战解析
在算法竞赛和工程开发中,树状数组(Fenwick Tree)和线段树(Segment Tree)是处理区间查询问题的两大利器。最近在准备算法面试时,我重新梳理了这两个数据结构的经典应用场景,包括逆序对统计、区间异或计算和差分数组优化。本文将结合LeetCode和ACWing上的高频考题,分享一套经过实战检验的代码模板和解题思路。
1.1 为什么选择这两种数据结构?
树状数组以其简洁的实现和高效的区间求和著称,时间复杂度为O(log n)。而线段树虽然实现稍复杂,但支持更丰富的区间操作(如最大值、最小值、区间更新等)。两者在解决逆序对问题时各有优劣:树状数组代码量少适合笔试,线段树扩展性强适合面试深度考察。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 逆序对问题的三种解法对比
2.1 归并排序解法(经典分治)
cpp复制int mergeSort(vector<int>& nums, int l, int r) {
if (l >= r) return 0;
int mid = l + (r - l)/2;
int cnt = mergeSort(nums, l, mid) + mergeSort(nums, mid+1, r);
vector<int> temp(r-l+1);
int i = l, j = mid+1, k = 0;
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) temp[k++] = nums[i++];
else {
temp[k++] = nums[j++];
cnt += mid - i + 1; // 关键统计点
}
}
while (i <= mid) temp[k++] = nums[i++];
while (j <= r) temp[k++] = nums[j++];
for (int p = 0; p < k; p++) nums[l+p] = temp[p];
return cnt;
}
注意事项:当数组元素值域很大时(如1e9),需要先进行离散化处理,将原始值映射到紧凑的整数区间。
2.2 树状数组实现(离散化+动态统计)
cpp复制class FenwickTree {
private:
vector<int> tree;
public:
FenwickTree(int n) : tree(n+1, 0) {}
void update(int idx, int delta) {
while (idx < tree.size()) {
tree[idx] += delta;
idx += idx & -idx;
}
}
int query(int idx) {
int sum = 0;
while (idx > 0) {
sum += tree[idx];
idx -= idx & -idx;
}
return sum;
}
};
int countInversions(vector<int>& nums) {
// 离散化处理
vector<int> sorted = nums;
sort(sorted.begin(), sorted.end());
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
FenwickTree ft(sorted.size());
int res = 0;
for (int i = nums.size()-1; i >= 0; i--) {
int rank = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin() + 1;
res += ft.query(rank-1);
ft.update(rank, 1);
}
return res;
}
2.3 线段树解法(动态开点优化)
对于值域特别大的情况(如1e18),动态开点线段树可以避免离散化:
cpp复制class DynamicSegmentTree {
private:
struct Node {
int l, r;
int sum;
Node *left, *right;
Node(int l, int r) : l(l), r(r), sum(0), left(nullptr), right(nullptr) {}
};
Node* root;
const int L = 1, R = 1e9;
void update(Node* node, int pos, int val) {
if (node->l == node->r) {
node->sum += val;
return;
}
int mid = node->l + (node->r - node->l)/2;
if (pos <= mid) {
if (!node->left) node->left = new Node(node->l, mid);
update(node->left, pos, val);
} else {
if (!node->right) node->right = new Node(mid+1, node->r);
update(node->right, pos, val);
}
node->sum = (node->left ? node->left->sum : 0) +
(node->right ? node->right->sum : 0);
}
int query(Node* node, int ql, int qr) {
if (!node || qr < node->l || ql > node->r) return 0;
if (ql <= node->l && node->r <= qr) return node->sum;
return query(node->left, ql, qr) + query(node->right, ql, qr);
}
public:
DynamicSegmentTree() : root(new Node(L, R)) {}
void update(int pos, int val) { update(root, pos, val); }
int query(int l, int r) { return query(root, l, r); }
};
int countInversions(vector<int>& nums) {
DynamicSegmentTree dst;
int res = 0;
for (int i = nums.size()-1; i >= 0; i--) {
res += dst.query(1, nums[i]-1);
dst.update(nums[i], 1);
}
return res;
}
3. 区间异或问题的线段树特化实现
3.1 问题建模
给定一个数组,需要支持两种操作:
- 区间异或:将区间[l,r]内的每个元素异或一个值x
- 单点查询:查询某个位置的值
cpp复制class XorSegmentTree {
private:
struct Node {
int l, r;
int val, lazy;
Node *left, *right;
Node(int l, int r) : l(l), r(r), val(0), lazy(0), left(nullptr), right(nullptr) {}
};
Node* root;
void pushDown(Node* node) {
if (node->lazy && node->l != node->r) {
if (!node->left) node->left = new Node(node->l, (node->l+node->r)/2);
if (!node->right) node->right = new Node((node->l+node->r)/2+1, node->r);
node->left->val ^= node->lazy;
node->left->lazy ^= node->lazy;
node->right->val ^= node->lazy;
node->right->lazy ^= node->lazy;
node->lazy = 0;
}
}
void rangeXor(Node* node, int l, int r, int val) {
if (l > node->r || r < node->l) return;
if (l <= node->l && node->r <= r) {
node->val ^= val;
node->lazy ^= val;
return;
}
pushDown(node);
rangeXor(node->left, l, r, val);
rangeXor(node->right, l, r, val);
}
int pointQuery(Node* node, int pos) {
if (node->l == node->r) return node->val;
pushDown(node);
if (pos <= node->left->r) return pointQuery(node->left, pos);
else return pointQuery(node->right, pos);
}
public:
XorSegmentTree(int n) { root = new Node(1, n); }
void rangeXor(int l, int r, int val) { rangeXor(root, l, r, val); }
int pointQuery(int pos) { return pointQuery(root, pos); }
};
实战技巧:异或操作的特性使得我们可以直接合并多次操作,这与加法操作不同。例如连续异或同一个值两次等于没有操作,这个性质可以简化某些场景下的逻辑判断。
4. 差分数组与树状数组的高阶应用
4.1 区间更新+单点查询(差分思想)
cpp复制class RangeUpdatePointQuery {
private:
FenwickTree ft;
public:
RangeUpdatePointQuery(int n) : ft(n) {}
void rangeAdd(int l, int r, int val) {
ft.update(l, val);
ft.update(r+1, -val);
}
int pointQuery(int idx) {
return ft.query(idx);
}
};
4.2 区间更新+区间查询(双重差分)
cpp复制class RangeUpdateRangeQuery {
private:
FenwickTree ft1, ft2;
void add(int l, int r, int val) {
ft1.update(l, val);
ft1.update(r+1, -val);
ft2.update(l, val*(l-1));
ft2.update(r+1, -val*r);
}
int prefixSum(int idx) {
return ft1.query(idx)*idx - ft2.query(idx);
}
public:
RangeUpdateRangeQuery(int n) : ft1(n), ft2(n) {}
void rangeAdd(int l, int r, int val) { add(l, r, val); }
int rangeSum(int l, int r) { return prefixSum(r) - prefixSum(l-1); }
};
4.3 数线段问题实战
问题描述:给定若干线段[start, end],查询某个点x被多少线段覆盖
cpp复制class SegmentCover {
private:
FenwickTree ft;
const int OFFSET = 1e6 + 10; // 处理负数坐标
public:
SegmentCover() : ft(2*OFFSET) {}
void addSegment(int l, int r) {
ft.update(l + OFFSET, 1);
ft.update(r + OFFSET + 1, -1);
}
int queryPoint(int x) {
return ft.query(x + OFFSET);
}
};
5. 性能对比与工程实践建议
| 数据结构 | 构建时间 | 点更新 | 区间查询 | 区间更新 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|---|---|
| 树状数组 | O(n) | O(logn) | O(logn) | 不支持 | O(n) | 前缀和、点更新+区间查询 |
| 标准线段树 | O(n) | O(logn) | O(logn) | O(logn) | O(4n) | 通用区间操作 |
| 动态开点线段树 | - | O(logn) | O(logn) | O(logn) | O(mlogn) | 值域大且稀疏 |
| ZKW线段树 | O(n) | O(logn) | O(logn) | O(logn) | O(2n) | 竞赛高频场景 |
工程实践中的选择建议:
- 优先考虑树状数组:当问题可以转化为前缀和且不需要区间更新时
- 值域超过1e6:考虑动态开点线段树避免离散化
- 需要支持多种区间操作:选择标准线段树
- 极致性能要求:尝试非递归实现的ZKW线段树
6. 常见踩坑点与调试技巧
-
边界条件处理:
- 线段树查询时注意区间完全包含的判断条件
- 树状数组的下标通常从1开始
-
离散化陷阱:
cpp复制// 错误示例(未去重) vector<int> temp = nums; sort(temp.begin(), temp.end()); // 正确做法 sort(temp.begin(), temp.end()); temp.erase(unique(temp.begin(), temp.end()), temp.end()); -
延迟传播错误:
- 忘记在查询和更新时调用pushDown
- 多个延迟标记合并时处理不当
-
内存优化:
- 动态开点线段树及时释放内存
- 使用内存池预分配节点
调试时可以添加可视化打印函数:
cpp复制void printTree(Node* node, int depth = 0) {
if (!node) return;
printTree(node->right, depth + 1);
cout << string(depth*4, ' ') << "[" << node->l << "," << node->r << "]: "
<< node->val << " (lazy: " << node->lazy << ")" << endl;
printTree(node->left, depth + 1);
}
在实际比赛中,建议准备3个版本的模板:
- 精简版:用于编码速度要求高的场合
- 调试版:包含完整性检查和日志输出
- 扩展版:支持多种操作的综合模板
