深入理解线段树(Segment Tree)在LeetCode-Go项目中的应用
线段树(Segment Tree)是一种非常重要的数据结构,特别适合处理区间查询和更新问题。本文将全面介绍线段树的原理、实现以及在算法问题中的应用。
什么是线段树?
线段树是一种二叉树形数据结构,由Jon Louis Bentley在1977年发明。它主要用于存储区间或线段,并允许快速查询结构内包含某一点的所有区间。
线段树的核心特点:
- 每个节点代表一个区间
- 叶子节点代表单个元素区间
- 内部节点代表其子节点区间的并集
- 查询时间复杂度为O(log n + k),其中k是匹配的区间数量
线段树的基本结构
线段树通常使用数组来实现,其结构遵循以下规则:
- 根节点存储在索引0位置
- 对于索引i的节点:
- 左子节点索引为2*i+1
- 右子节点索引为2*i+2
- 叶子节点存储原始数据
- 内部节点存储合并后的数据
线段树的构建过程采用分治思想,递归地将区间一分为二,直到区间长度为1。
线段树的实现
在LeetCode-Go项目中,线段树的基本实现如下:
type SegmentTree struct {
data, tree, lazy []int
left, right int
merge func(i, j int) int
}
func (st *SegmentTree) Init(nums []int, oper func(i, j int) int) {
// 初始化数据
st.merge = oper
data := make([]int, len(nums))
copy(data, nums)
st.data = data
st.tree = make([]int, 4*len(nums))
st.lazy = make([]int, 4*len(nums))
if len(nums) > 0 {
st.buildSegmentTree(0, 0, len(nums)-1)
}
}
func (st *SegmentTree) buildSegmentTree(treeIndex, left, right int) {
if left == right {
st.tree[treeIndex] = st.data[left]
return
}
mid := left + (right-left)/2
leftIndex := 2*treeIndex + 1
rightIndex := 2*treeIndex + 2
st.buildSegmentTree(leftIndex, left, mid)
st.buildSegmentTree(rightIndex, mid+1, right)
st.tree[treeIndex] = st.merge(st.tree[leftIndex], st.tree[rightIndex])
}
线段树的查询操作
线段树支持两种查询方式:
1. 直接查询
func (st *SegmentTree) Query(left, right int) int {
if len(st.data) > 0 {
return st.queryInTree(0, 0, len(st.data)-1, left, right)
}
return 0
}
func (st *SegmentTree) queryInTree(treeIndex, l, r, queryL, queryR int) int {
if l == queryL && r == queryR {
return st.tree[treeIndex]
}
mid := l + (r-l)/2
leftIndex := 2*treeIndex + 1
rightIndex := 2*treeIndex + 2
if queryL > mid {
return st.queryInTree(rightIndex, mid+1, r, queryL, queryR)
} else if queryR <= mid {
return st.queryInTree(leftIndex, l, mid, queryL, queryR)
}
return st.merge(
st.queryInTree(leftIndex, l, mid, queryL, mid),
st.queryInTree(rightIndex, mid+1, r, mid+1, queryR),
)
}
2. 懒查询(配合懒更新)
func (st *SegmentTree) QueryLazy(left, right int) int {
if len(st.data) > 0 {
return st.queryLazyInTree(0, 0, len(st.data)-1, left, right)
}
return 0
}
func (st *SegmentTree) queryLazyInTree(treeIndex, l, r, queryL, queryR int) int {
mid := l + (r-l)/2
leftIndex := 2*treeIndex + 1
rightIndex := 2*treeIndex + 2
// 处理懒标记
if st.lazy[treeIndex] != 0 {
st.tree[treeIndex] += (r-l+1)*st.lazy[treeIndex]
if l != r {
st.lazy[leftIndex] += st.lazy[treeIndex]
st.lazy[rightIndex] += st.lazy[treeIndex]
}
st.lazy[treeIndex] = 0
}
// 查询逻辑
if queryL <= l && queryR >= r {
return st.tree[treeIndex]
}
if queryL > mid {
return st.queryLazyInTree(rightIndex, mid+1, r, queryL, queryR)
}
if queryR <= mid {
return st.queryLazyInTree(leftIndex, l, mid, queryL, queryR)
}
return st.merge(
st.queryLazyInTree(leftIndex, l, mid, queryL, mid),
st.queryLazyInTree(rightIndex, mid+1, r, mid+1, queryR),
)
}
线段树的更新操作
1. 单点更新
func (st *SegmentTree) Update(index, val int) {
if len(st.data) > 0 {
st.updateInTree(0, 0, len(st.data)-1, index, val)
}
}
func (st *SegmentTree) updateInTree(treeIndex, l, r, index, val int) {
if l == r {
st.tree[treeIndex] = val
return
}
mid := l + (r-l)/2
leftIndex := 2*treeIndex + 1
rightIndex := 2*treeIndex + 2
if index > mid {
st.updateInTree(rightIndex, mid+1, r, index, val)
} else {
st.updateInTree(leftIndex, l, mid, index, val)
}
st.tree[treeIndex] = st.merge(st.tree[leftIndex], st.tree[rightIndex])
}
2. 区间更新(使用懒标记)
func (st *SegmentTree) UpdateLazy(updateL, updateR, val int) {
if len(st.data) > 0 {
st.updateLazyInTree(0, 0, len(st.data)-1, updateL, updateR, val)
}
}
func (st *SegmentTree) updateLazyInTree(treeIndex, l, r, updateL, updateR, val int) {
mid := l + (r-l)/2
leftIndex := 2*treeIndex + 1
rightIndex := 2*treeIndex + 2
// 先处理懒标记
if st.lazy[treeIndex] != 0 {
st.tree[treeIndex] += (r-l+1)*st.lazy[treeIndex]
if l != r {
st.lazy[leftIndex] += st.lazy[treeIndex]
st.lazy[rightIndex] += st.lazy[treeIndex]
}
st.lazy[treeIndex] = 0
}
// 当前区间不在更新范围内
if l > r || l > updateR || r < updateL {
return
}
// 当前区间完全在更新范围内
if updateL <= l && updateR >= r {
st.tree[treeIndex] += (r-l+1)*val
if l != r {
st.lazy[leftIndex] += val
st.lazy[rightIndex] += val
}
return
}
// 更新子区间
st.updateLazyInTree(leftIndex, l, mid, updateL, updateR, val)
st.updateLazyInTree(rightIndex, mid+1, r, updateL, updateR, val)
st.tree[treeIndex] = st.merge(st.tree[leftIndex], st.tree[rightIndex])
}
线段树的应用场景
线段树特别适合解决以下类型的问题:
- 区间求和问题:计算数组中某个区间的元素和
- 区间最值问题:查询区间内的最大值或最小值
- 区间更新问题:对区间内的所有元素进行统一操作
- 计数问题:统计满足特定条件的元素数量
- 几何问题:如矩形面积并、矩形周长并等
经典问题示例
1. 区间求和问题
LeetCode上典型的区间求和问题包括:
-
- Range Sum Query - Immutable
-
- Range Sum Query - Mutable
2. 区间最值问题
-
- Sliding Window Maximum (可以用线段树解决)
-
- Maximum Binary Tree
3. 复杂的区间问题
-
- The Skyline Problem
-
- Falling Squares
线段树的变种
除了基本的线段树外,还有一些常见的变种:
- 计数线段树:用于解决统计类问题
- 二维线段树:处理二维平面上的区间问题
- 持久化线段树:支持历史版本查询
- 动态开点线段树:节省空间,适用于值域大的情况
性能分析
线段树的主要操作时间复杂度:
- 构建:O(n)
- 查询:O(log n)
- 更新:O(log n)
- 区间更新:O(log n)(使用懒标记)
空间复杂度:O(4n) ≈ O(n)
总结
线段树是一种功能强大的数据结构,特别适合处理各种区间操作问题。通过本文的介绍,我们了解了:
- 线段树的基本原理和结构
- 线段树的构建、查询和更新操作
- 懒标记技术的应用
- 线段树在实际问题中的应用
掌握线段树可以帮助我们高效解决许多复杂的区间操作问题,是算法学习中的重要数据结构之一。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考