继续在树这一类问题上拓展,线段树也是高级的数据结构,初学者要跳过,深入学习阶段可以适当了解一下,拓宽思维能力。如果要参加竞赛或者其他对数据结构要求比较高的情形,可以仔细研究一番,本文借其他博客和几道力扣题目介绍一下线段树。
第一篇文章我们先从以下两个问题展开:
1. 线段树是什么?是什么样的结构?这样的结构可以解决什么样的问题?
2. 线段树的基本实现及区间查询问题——LeetCode307题
线段树(Segment Tree)是一种二叉树形数据结构,1977 年由 Jon Louis Bentley 发明,用以存储区间或线段,并且允许快速查询结构内包含某一点的所有区间。
我们引出几篇参考博客,本文都有所引用。
线段树从零开始
线段树 从入门到进阶
线段树详解 (原理,实现与应用) (该文比较深入,重实现,浅学的不要看)
Python数据结构与算法(十四、线段树)(以python的实现为主)
线段树所记录的是数组(一般是数组)区间内的信息,比如区间内的元素求和、连乘之类的。下图中根节点记录整棵树的索引范围,依次向下,直到叶子节点只有一个元素。画出来就是下面这个样子的。其中叶子A[0]代表所给数组对应索引的值,所有父节点则汇总其子节点的信息(比如是子节点元素之和,根据你不同的目的而不同)。
这样的数据结构用于解决一类典型的问题,当我们的各种操作,对象是区间(或线段)时,比如修改一个区间的值,比如查找一个区间的值的和、最大值等。下面我们举几个栗子,介绍典型的线段树应用场景。
题目一:
10000个正整数,编号1到10000,用A[1],A[2],A[10000]表示。
修改:无
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.
传统方法,遍历求和L,L+1,...,R;
如果应用线段树,如上图,求2-6的和,我们只需把A[2-4]+A[5-6]两个父节点的值加起来即可,因此将操作的时间复杂度降到O(logN).
即任意给定区间[L,R],线段树在上述子区间中选择约2*log2(R-L+1)个拼成区间[L,R],从而得到相应结果,查询效率加快。
题目二:
10000个正整数,编号从1到10000,用A[1],A[2],A[10000]表示。
修改:1.将第X个数增加C (1 <= X <= 10000)2. 将M到N的数都增加C,其中1<= M <= N <= 10000.
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.
传统方法,修改比较快,但是求和仍然效率不高;
应用线段树的话,这是典型的线段树点修改、区间修改问题。以点修改为例,修改A[2],那么我们依次修改所有包含A[2]的节点,即A[0-9],A[0-4],A[2-4],可以把修改的效果减小到O(logN)。
不知诸位是否对线段树有了初步认识,认识什么是区间操作,认识到线段树在区间操作上的效率优势。下面我们看代码实现和应用。
如何用代码实现线段树?考虑到作为树的基本特征,我相信大家都有初步的实现方案。
看上图可知,线段树并不是完全二叉树,但是也是平衡的,仍然可以使用完全二叉树的数组存储方法,并不会浪费太多空间。
当然可以像一般的树那样写成用指针存储下一级。但是用数组来实现树形结构,可以大大简化代码,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。
想必都还记得数组存储时,父子节点之间的关系吧。
足够的空间?博客里都有介绍:
简单的记法: 足够的空间 = 数组大小n的四倍。
实际上足够的空间 = (n向上扩充到最近的2的某个次方)的两倍。
举例子:假设数组长度为5,就需要5先扩充成8,8*2=16.线段树需要16个元素。如果数组元素为8,那么也需要16个元素。
所以线段树需要的空间是n的两倍到四倍之间的某个数,一般就开4*n的空间就好,如果空间不够,可以自己算好最大值来省点空间。
————————————————
版权声明:本文为CSDN博主「岩之痕」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zearot/article/details/52280189
下面我以一道例题来看一下线段树代码实现和应用。
307. Range Sum Query - Mutable
Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.
The update(i, val) function modifies nums by updating the element at index i to val.
Example:
Given nums = [1, 3, 5]
sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
题目解析:
这是线段树最基础的应用——区间求和问题,涉及到点修改和区间查询。下面我们直接看代码。
class NumArray:
def __init__(self, nums: List[int]):
self._data = nums[:]
self._tree = [None] * 4 * len(self._data)
if not nums:
return
self._build(0, 0, len(self._data)-1)
def _build(self, ix, left, right):
if left == right:
self._tree[ix] = self._data[left]
return
mid = (left + right) // 2
self._build(2*ix+1, left, mid)
self._build(2*ix+2, mid+1, right)
self._tree[ix] = self._tree[2*ix+1] + self._tree[2*ix+2]
def update(self, i: int, val: int) -> None:
if i < 0 or i >= len(self._data):
return
self._data[i] = val
self._set(0, 0, len(self._data) - 1, i, val)
def _set(self, treeIndex, left, right, index, val):
"""
Description: 在以索引treeIndex为根节点的线段树中将索引为index的位置的元素设为e(此时treeIndex索引处所代表的区间范围为:[left, right]
"""
if left == right:
self._tree[treeIndex] = val
return
mid = (left + right) // 2
if index <= mid: # 左子树去修改
self._set(2*treeIndex+1, left, mid, index, val)
else:
self._set(2*treeIndex+2, mid+1, right, index, val)
self._tree[treeIndex] = self._tree[2*treeIndex+1] + self._tree[2*treeIndex+2]
def sumRange(self, i: int, j: int) -> int:
return self._query(0, 0, len(self._data)-1, i, j)
def _query(self, treeIndex, left, right, quaryL, quaryR):
"""
Description: 在根节点索引为treeindex的线段树上查找索引范围为[quaryL, quaryR]上的值,其中left, right值代表该节点所表示的索引范围(左闭右闭)
"""
if left == quaryL and right == quaryR: # 递归到底的情况,区间都对上了,直接返回当前treeIndex索引处的值就好
return self._tree[treeIndex] # 返回当前树上索引为treeIndex的元素值
mid = (right + left) // 2 # 获取TreeIndex索引处所代表的范围的中点
leftChild_index = 2*treeIndex+1 # 获取左孩子的索引,注意上面几处也有用到,只是没有单独列为变量
rightChild_index = 2*treeIndex+2 # 获取右孩子的索引
if quaryL > mid: # 此时要查询的区间完全位于当前treeIndex所带表的区间的右侧
return self._query(rightChild_index, mid + 1, right, quaryL, quaryR) # 直接去右子树找
elif quaryR <= mid: # 此时要查询的区间完全位于当前treIndex所代表的区间的左侧
return self._query(leftChild_index, left, mid, quaryL, quaryR) # 直接去左子树找
# 此时一部分在[left, mid]上,一部分在[mid + 1, right]上
leftResult = self._query(leftChild_index, left, mid, quaryL, mid) # 在左子树找区间
rightResult = self._query(rightChild_index, mid + 1, right, mid + 1, quaryR) # 在右子树找区间
return leftResult + rightResult # 最后在回归的过程中两个子节点进行和操作并返回,得到[quaryL, quaryR]区间上的值
简单来说,self._data是数组本身,在建树和修改时要用到,一些查询操作用不上这玩意;self._tree就是用数组存储的线段树的表示,如上所述,叶子结点的值和self._data对应的,其他的结点的值根据问题本身定义,该题中就是子结点值求和。
其中建树,修改和查询操作都是递归实现的,因此分别实现了递归用函数,希望结合代码和注释,了解线段树的基本实现,这里就不多解释了。
了解一下线段树的话介绍到这儿可以了,更深入的应用看后面,还要多刷几道题才可。