当前位置:   article > 正文

线段树:讲解与模板_线段树 前缀和

线段树 前缀和

本章内容介绍另一种数据结构—线段树,线段树和前文所提到的前缀和所能解决的问题相似,但使用场景不同。
并且在下一章《线段树:例题讲解》会针对相关例题做出讨论。

1 前缀和的局限性

在使用前缀和时,我们必须保证所生成前缀和的原始数组不能发生元素的更新操作。以一维数组为例,一旦前缀和数组已经生成完毕,此时若改变原始数组中的某个值,那么其后面的前缀和都要发生更改。
在这里插入图片描述
可以看出,前缀和方法重新生成前缀和数组的时间复杂度为O(N)

2 线段树与前缀和的比较

树是一种比较灵活的数据结构,可以用来解决某个范围内的数据聚合问题,使用线段树,我们可以在O(logN) 的时间里找到数据的聚合信息(如最大值,最小值,总和等),当然,当原始数组发生变更时,更新线段树的时间复杂度也是O(logN)。
在这里插入图片描述
数组A[0, 1, … , n-1]的线段树是一颗二叉树,其中每个节点包含数组下标在 [i, j]范围内的聚合信息(如最大值,最小值,总和等),其左右节点分别包含范围 [i, (i+j)/2] , [(i+j)/2+1, j]上的信息

在上图所给出的示例中,每个叶节点都是数组{2,4,12,17}的元素 。非叶节点包含范围内相应元素的总和 ,例如(6) 是从索引 0 到索引 1 的元素之和。而根节点 (35) 是它的两个子节点 (6) 和 (29) 的和,也是整个数组的元素之和。

线段树与前缀和的区别:

  • 前缀和查询区间的时间复杂度为O(1), 而线段树查询区间的时间复杂度为O(logN),因此,当数组没有修改需求时,前缀和的效率较高。
  • 前缀和修改元素而引起更改的时间复杂度为O(N),而使用线段树修改元素并更改线段树数组的时间复杂度为O(logN), 在面对有修改需求的数组时,线段树可更快完成修改。

3 线段树的实现

线段树通常基于数组来实现,由二叉树的性质可知,如果索引为i的元素不是叶节点,那么它的左右节点分别存储在索引2i和2i+1处。

以求和操作为例,线段树的操作可以分为以下三部分:

  • 从原始数组生成线段树数组
  • 修改元素时更新线段树数组
  • 使用线段树数组进行区域查询

3.1 生成线段树

由线段树的性质可知,如果需要求某个节点p的值,则必须先求出其左右子节点的值,因为节点p的值等于其左右子节点的元素值之和,因此,我们使用自下而上的方法来生成线段树,因此使用后序遍历的方式,递归函数buildTree(root, l, r)的参数含义:

  • root:根元素在tree数组中的索引;
  • l,r:tree元素表示的左右范围。
int[] tree;
int[] nums;
int n;
public NumArray(int[] nums){
  if(nums.length > 0){
    this.nums = nums;
    n = nums.length;
    tree = new int[2 * n];
    buildTree(1, 0, nums.length - 1);
  }
}
public void buildTree(int root, int l, int r){
  if(l == r){
    tree[root] = nums[l];
    return;
  }
  int mid = l + (r - l) / 2;
  buildTree(nums, root * 2, l, mid);
  buildTree(nums, root * 2 + 1, mid + 1, r);
  tree[root] = tree[root * 2] + tree[root * 2 + 1];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

时间复杂度:O(N)
空间复杂度:O(N)

3.2 更新线段树

与生成线段树相似,当更新原数组的元素时,总是会修改叶节点的值,然后自下而上更新非叶节点的元素值,以满足非叶节点的元素值等于子节点的元素值之和。递归函数update(root, l, r, key, value)的参数含义:

  • root:根元素在tree数组中的索引;
  • l,r:tree元素表示的左右范围;
  • key, value:要修改的元素下标以及新元素值。
public void update(int root,int l,int r,int key,int value)
    {
     if(l == r)
     {
      if(l == key)
       Tree[root] = value;
      return;
     }
     
     if(l > key || r < key)
      return;
     
     int mid = l + (r - l) / 2;
     update(root * 2, l, mid, key, value);
     update(root * 2 + 1, mid+1, r, key, value);
     Tree[root] = Tree[root * 2] + Tree[root * 2 + 1];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

时间复杂度:O(logN)
空间复杂度:O(1)

3.3 范围查询

处理步骤与上述操作类似,递归函数query(root, l, r, ql, qr)的参数含义:

  • root:根元素在tree数组中的索引;
  • l,r:tree元素表示的左右范围;
  • ql,qr:要查询范围的左右边界
public int query(int root, int l, int r, int ql, int qr){
    if(l >= ql && r <= qr) return Tree[root];
    if(ql > r || qr < l) return 0;
    int mid = l + (r - l) / 2;
    int left_sum = query(root * 2, l, mid, ql, qr);
    int right_sum = query(root * 2 + 1, mid + 1, r, ql, qr);
    return left_sum + right_sum;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

时间复杂度:O(logN)
空间复杂度:O(1)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/654248
推荐阅读
相关标签
  

闽ICP备14008679号