C++高级数据结构之线段树

 更新时间:2022年05月24日 08:45:30   作者:下一站不是永远  
这篇文章主要介绍了C++高级数据结构之线段树,文章围绕主题的相关资料展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下

前言:

  • 高级数据结构(Ⅲ)线段树(Segment Tree)
  • 线段树的原理
  • 树的创建
  • 单点修改
  • 区间查找
  • 完整代码及测试

高级数据结构(Ⅲ)线段树(Segment Tree)

线段树的原理

线段树是一种二叉搜索树 , 对于线段树中的每一个非叶子结点[a,b],它的左儿子表示的区间为[a, (a+b)/2],右儿子表示的区间为[(a+b)/2+1, b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度,其空间复杂度为O(n)。

若对于一个数组使用前缀和数组保存它的区间和,那么查找区间和时间复杂度为O(1),而区间修改时间复杂度为O(n)。使用线段树可以快速查找或修改某个区间的值,时间复杂度为O(logN)。

线段树中每一个结点代表一个区间,对于区间【1, 7】线段树的结构如下图

实例:

class SegmentTree{
private static final int[] tree = new int[1000];
int[] arr;
SegmentTree() {
}
SegmentTree(int[] arr) {
this.arr = arr;
}
//创建树···
public void buildTree(){}
//单点修改更新树
public void updateTree(){}
//区间查找
public void queryTree(){}
}

树的创建

给定一个数组arr = [6, 4, 7, 5, 8, 3 , 9],创建它对应的线段树数组。

对于一个结点k,它的左孩子为2 * k,右孩子为 2 * k + 1,此公式适用于根结点从1开始。但我们在数组中存储元素时下标通常是从0开始的,即根结点从0开始,此时它的左孩子为 2 * k + 1, 右孩子为 2 * k + 2。

如下图所示,数组arr的长度为7,由于树是以2为基数的,且线段树中叶子结点保存的是arr中单个结点的值,我们可以将数组arr的长度设想为8 (2 ^ 3 = 8,理解为新添了一个元素0,这对区间和并不影响),此时它对应的线段树就是一棵结点数为15(1 + 2 + 4 + 8)的满二叉树。相应的结点值,映射到数组tree的值在图中可清晰的看出。

那么,如何用处程序来创建一棵树呢?

由于线段树叶子结点都是数组arr中的某一元素,所以我们可以使用两个变量low和high来标记数组arr的区间,

  • 若low == high,此时令tree[node] = arr[low],并终止递归
  • 否则,将区间二分,分别计算左区间[low, mid]和右区间[mid +1, high],并在最后更新tree[node]

实现:

//创建树
public void buildTree() {
this.buildTree(0, 0, arr.length - 1);
}
private void buildTree(int node, int low, int high) {
if(low == high) {
tree[node] = arr[low];
return;
}
int mid = low + (high - low) / 2;
int lnode = 2 * node + 1;
int rnode = 2 * node + 2;
buildTree(lnode, low, mid);
buildTree(rnode, mid + 1, high);
tree[node] = tree[lnode] + tree[rnode];
}

单点修改

若现在将arr[4]的值修改为1,需要怎样操作呢?

从下图绿色标出的结点不难看出其更新过程,先将其所对应的叶子结点修改,然后继续向上修改其父节点即可。

当long==high&&low==index时更新两个数组的值,否则,缩小区间,在相应的区间搜索,最后更新结点和即可,相应代码如下

//单点修改更新树
public void updateTree(int index, int val) {
this.updateTree(0, 0, arr.length - 1, index, val);
}
private void updateTree(int node, int low, int high, int index, int val) {
if(low == high && low == index) {
arr[index] = val;
tree[node] = val;
return;
}
int mid = low + (high - low) / 2;
int lnode = 2 * node + 1;
int rnode = 2 * node + 2;
if(index >= low && index <= mid) {
updateTree(lnode, low, mid, index, val);
}else {
updateTree(rnode, mid + 1, high, index, val);
}
tree[node] = tree[lnode] + tree[rnode];
}

区间查找

若现在查找数组arr区间和[3,6],如何利用线段树呢?

在线段树中,我们将它的和划分为两个区间[3]和[4,6],如图中的黄色结点

下面来看看相关代码如何实现,给定一个查找区间[L, R],同样使用变量low和high维护对数组arr的二分查找边界

  • 若当前区间low > R 或者 high < L,说明已超出查找范围,返回0
  • 若[low, high]处于区间[L, R]内,返回当前结点的值tree[node]

然后继续在左右区间查找并保存左右区间的值sumLeft和sumRight,最后返回sumLeft + sumRight即可

//区间查找
public int queryTree(int L, int R) {
return this.queryTree(0, 0, arr.length - 1, L, R);
}
private int queryTree(int node, int low,
int high, int L, int R) {
if(low > R || high < L) {
return 0;
}else if(low >= L && high <= R) {
return tree[node];
}
int mid = low + (high - low) / 2;
int lnode = 2 * node + 1;
int rnode = 2 * node + 2;
int sumleft = queryTree(lnode, low, mid, L, R);
int sumRight = queryTree(rnode, mid + 1, high, L, R);
return sumleft + sumRight;
}

完整代码及测试

class SegmentTree{
private static final int[] tree = new int[1000];
int[] arr;
SegmentTree() {
}
SegmentTree(int[] arr) {
this.arr = arr;
}
//创建树
public void buildTree() {
this.buildTree(0, 0, arr.length - 1);
}
private void buildTree(int node, int low, int high) {
if(low == high) {
tree[node] = arr[low];
return;
}
int mid = low + (high - low) / 2;
int lnode = 2 * node + 1;
int rnode = 2 * node + 2;
buildTree(lnode, low, mid);
buildTree(rnode, mid + 1, high);
tree[node] = tree[lnode] + tree[rnode];
}
//单点修改更新树
public void updateTree(int index, int val) {
this.updateTree(0, 0, arr.length - 1, index, val);
}
private void updateTree(int node, int low, int high, int index, int val) {
if(low == high && low == index) {
arr[index] = val;
tree[node] = val;
return;
}
int mid = low + (high - low) / 2;
int lnode = 2 * node + 1;
int rnode = 2 * node + 2;
if(index >= low && index <= mid) {
updateTree(lnode, low, mid, index, val);
}else {
updateTree(rnode, mid + 1, high, index, val);
}
tree[node] = tree[lnode] + tree[rnode];
}
//区间查找
public int queryTree(int L, int R) {
return this.queryTree(0, 0, arr.length - 1, L, R);
}
private int queryTree(int node, int low, int high, int L, int R) {
if(low > R || high < L) {
return 0;
}else if(low >= L && high <= R) {
return tree[node];
}
int mid = low + (high - low) / 2;
int lnode = 2 * node + 1;
int rnode = 2 * node + 2;
int sumleft = queryTree(lnode, low, mid, L, R);
int sumRight = queryTree(rnode, mid + 1, high, L, R);
return sumleft + sumRight;
}
//输出线段树的值
public void printTree() {
int size = 15; //size值的大小由arr数组的大小而定
for (int i = 0; i < size; i++) {
System.out.print(tree[i] + " ");
}
System.out.println();
}
}
public class SegmentTreeTest {
public static void main(String[] args) {
int[] arr = {6, 4, 7, 5, 8, 3, 9};
SegmentTree st = new SegmentTree(arr);
//创建线段树
st.buildTree();
st.printTree();
//>>>42 22 20 10 12 11 9 6 4 7 5 8 3 0 0
//查找区间[3, 6]
int sum = st.queryTree(3, 6);
System.out.println(sum);
//>>>25
//单点修改更新树, 令arr[4] = 1
st.updateTree(4, 1);
st.printTree();
//>>>35 22 13 10 12 4 9 6 4 7 5 1 3 0 0
}
}

树结点版本:

此版本不使用数组保存,而是以结点来保存值,相应代码不难实现,如下:

import java.util.ArrayDeque;
import java.util.Deque;
class SegNode{
int val;
SegNode lnode;
SegNode rnode;
SegNode(){}
SegNode(int val) {
this.val = val;
}
}
class SegTree{
SegNode root;
int[] arr;
SegTree() {}
SegTree(int[] arr) {
this.arr = arr;
this.bulidTree();
}
//创建树
public void bulidTree() {
root = this.buildTree(0, arr.length - 1);
}
private SegNode buildTree(int low, int high) {
if(low == high) {
return new SegNode(arr[low]);
}
SegNode node = new SegNode();
int mid = low + (high - low) / 2;
node.lnode = buildTree(low, mid);
node.rnode = buildTree(mid + 1, high);
node.val = node.lnode.val + node.rnode.val;
return node;
}
//单点修改更新树
public void updateTree(int index, int val) {
root = updateTree(root ,0, arr.length - 1, index, val);
}
private SegNode updateTree(SegNode node, int low, int high, int index, int val) {
if(low == high && low == index) {
arr[index] = val;
node.val = val;
return node;
}
int mid = low + (high - low) / 2;
if(index >= low && index <= mid) {
node.lnode = updateTree(node.lnode, low, mid, index, val);
}else {
node.rnode = updateTree(node.rnode, mid + 1, high, index, val);
}
node.val = node.lnode.val + node.rnode.val;
return node;
}
//查找区间
public int queryTree(int L, int R) {
return queryTree(root, 0, arr.length - 1, L, R);
}
private int queryTree(SegNode node, int low, int high, int L ,int R) {
if(low > R || high < L) {
return 0;
}else if(low >= L && high <= R) {
return node.val;
}
int mid = low + (high - low) / 2;
int sumLeft = queryTree(node.lnode, low, mid, L, R);
int sumRight = queryTree(node.rnode, mid + 1, high, L, R);
return sumLeft + sumRight;
}
//输出树(层次遍历)
public void printTree() {
Deque<SegNode> queue = new ArrayDeque<SegNode>();
queue.offer(this.root);
while(!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
SegNode node = queue.poll();
System.out.print(node.val + " ");
if(node.lnode != null) queue.offer(node.lnode);
if(node.rnode != null) queue.offer(node.rnode);
}
}
}
}
public class SegmentTreeNodeTest {
public static void main(String[] args) {
int[] arr = {6, 4, 7, 5, 8, 3, 9};
//创建线段树
SegTree st = new SegTree(arr);
st.printTree();
System.out.println("");
//>>>42 22 20 10 12 11 9 6 4 7 5 8 3
//查找区间[3, 6]
int sum = st.queryTree(3, 6);
System.out.println(sum);
//>>>25
//单点修改更新树, 令arr[4] = 1
st.updateTree(4, 1);
st.printTree();
System.out.println("");
>>>35 22 13 10 12 4 9 6 4 7 5 1 3
}
}

到此这篇关于C++高级数据结构之线段树的文章就介绍到这了,更多相关C++线段树内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • c语言单词搜索的实现

    c语言单词搜索的实现

    本文主要介绍了c语言单词搜索的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • 利用Matlab复刻两款粒子爱心动画效果

    利用Matlab复刻两款粒子爱心动画效果

    最近电视剧《点燃我,温暖你》大火,蹭一下热度,发一下MATLAB画爱心的代码,写的比较随意,大家可以自行调整粒子大小和颜色啥的
    2022-11-11
  • C语言例题之输出1000以内的所有完数

    C语言例题之输出1000以内的所有完数

    完数是一些特殊的自然数,它所有的真因子(即除了自身以外的约数)的和(即因子函数),恰好等于它本身,如果一个数恰好等于它的因子之和,则称该数为“完数”,这篇文章主要给大家介绍了关于C语言例题之输出1000以内的所有完数的相关资料,需要的朋友可以参考下
    2022-11-11
  • VS2022 Git提交代码的实现

    VS2022 Git提交代码的实现

    本文主要介绍了VS2022 Git提交代码的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • C++使用curl库进行http请求的方法详解

    C++使用curl库进行http请求的方法详解

    这篇文章主要为大家详细介绍了C++如何使用curl库进行http请求,并且实现获取返回的头信息的时间,也就是获取后台服务的当前时间,感兴趣的可以了解一下
    2023-07-07
  • 深入解析unsigned int 和 int

    深入解析unsigned int 和 int

    以下是对unsigned int和int进行了详细的分析介绍,需要的朋友可以过来参考下
    2013-08-08
  • break的使用for循环嵌套示例

    break的使用for循环嵌套示例

    这篇文章主要介绍了break的使用for循环嵌套示例,需要的朋友可以参考下
    2014-02-02
  • Linux网络编程之UDP Socket程序示例

    Linux网络编程之UDP Socket程序示例

    这篇文章主要介绍了Linux网络编程之UDP Socket程序示例,有助于读者在实践中掌握UDP协议的原理及应用方法,需要的朋友可以参考下
    2014-08-08
  • 简述c++ 发展史

    简述c++ 发展史

    这篇文章主要介绍了c++ 发展的过程,帮助大家更好的了解和学习c++,感兴趣的朋友可以了解下
    2020-08-08
  • OpenCV实现图像边缘检测

    OpenCV实现图像边缘检测

    这篇文章主要为大家详细介绍了OpenCV实现图像边缘检测,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01

最新评论