C++ 树进阶系列之线段树和它的延迟更新

news2024/11/20 20:29:44

1. 前言

线段树树状数组有相似之处,可以用于解决区间类型的问题。

但两者又各个千秋,树状数组本质是数组,有着树的形,可以借用树的一些概念。线段树是典型的二叉树结构,无论神和形都是树,可以应用树的所有理论。

本文将详细聊聊线段树。

2. 问题驱动

与树状数组一样,线段树可以缓存区间内具有特殊性质的数据(如:区间和,区间最值、…),以提高操作性能。

现通过一个案例理解线段树的初衷。

如有如下数组,现有求任意区间内最大值的需求。最简单的解决方案是使用穷举法求最值,时间复杂度O(n)

x1.png

如果求某个区间中的最值是一个高频率操作,每次都使用穷举法计算,累积的时间代价是非常大的。

在代码中,当需要对相同的计算频繁调用时,首当其冲的想法必然是缓存机制。针对本题可以使用简单动态规划思想,缓存原数组中每一个位置的最大值。

#include <bits/stdc++.h>
using namespace std;
int main() {
	//原数组
	int nums[8]= {3,6,1,9,7,11,8,5};
	//最大值缓存数组
	int cache[8]= {0};
	cout<<"原数组中数据"<<endl;
	for(int i=0; i<8; i++)
		cout<<nums[i]<<"\t";
	cout<<endl;
	cout<<"缓存数据"<<endl;
	for(int i=0; i<8; i++) {
		if(i==0)
			cache[i]=nums[i];
		else
			cache[i]=max(cache[i-1],nums[i]);
		cout<<cache[i]<<"\t";
	}
	cout<<endl;
	//求前 5 位数中的最大值
	cout<<"求前 5 位数中的最大值"<<endl;
	cout<<cache[4]<<endl;
	cout<<"求前 [3~7] 区间中的最大值"<<endl;
	cout<<max(cache[2],cache[6]  )<<endl;
	return 0;
}

输出结果:

x1_1.png

缓存时间复杂度是O(n),求最值时间复杂度为O(1),如果原数组中的数据是稳定的,不失为一种良好的方案。

但是,如果原数组中的数据有频繁更新需求时,则需要随时联动更新整个缓存数组,时间性能会变得较大。

线段树的基本思路和树状数组一样,仅对区间信息缓存,更新也仅针对区间进行,线段树的时间复杂度为O(logn)

3. 线段树的构建流程

在探讨线段树的构建之前,先看一下最终线段树的形状。

x5.png

分析结果图可知:

  • 原数组arr中的每一个数据都是线段树的叶结点。
  • 非叶结点的值是在其左、右子结点的值中选择了较大哪个。
  • 结点至少包含 3 个信息(值或称权重,左、右边界值)。且叶结点的区间特征是左、右边界值相同。
  • 整个数组就是一个大区间,区间边界从07,可统一描述格式为[0,7]

根据分析,构建的基本思路:

  • 父结点向左、右子结点发送请求,获取左、右子结点上的值。
  • 如果左、右子结点不是叶结点,则继续向自己的左、右子结点发送值的请求。
  • 如果左、右子结点是叶结点,则把值回送给父结点。
  • 父结点获取到左、右子结点值后,求两者中的最大值作为自己的值。

上面的过程显然符合递归的向后请求、向上回溯的执行模式。下面根据原数组提供的信息,使用递归思想构建出完整的线段树。

  • 构建根结点,区间信息为 [0,7],值未知。

x2.png

  • 构建根结点的左、右子结点。对根结点的区间[0,7]使用二分思想,划分成左、右 2 个子区间,左区间范围[0,7/2],右区间范围[7/2+1,7],此时,左、右子结点点值任然未知。

x3.png

  • 以此类推,继续对非叶结点的区间信息采用二分思想,以子区间信息构建子结点。

x4.png

  • 直到区间不能再分(左、右边界相同),此时构建出来的结点是叶结点。以叶结点的区间值为索引号,从原数组中获取值。

x6.png

  • 然后把值向父结点提供,父结点会选择较大的值作为自己的值。

x7.png

  • 一路向上,直到根结点的值被填充。

x8.png

最终可以看到构建出了一个满二叉树。

是不是对于任意一个数列中的数据都能构建出满二叉树?

不一定,只能说是一棵近似的完全二叉树。

因本例中数组恰好有 8 个数据。

根据二叉树的原理。树的深度为logn+1n=8时,深度h=4

而又知,二叉树的最后一层的结点数最多为 2h-1,把h=4代入后可知值为8。当最后一层达到最大数量时,此二叉树方为满二叉树。如果原数组中的数据只有 5 或其它个数,最后一层是不可能达到满二叉树所要的数量。

4. 线段树的 API

原数组中的数据个数不同,所构建出来的线段树不一定是满二叉树,或者说一定是完全二叉树,但也是一棵近似完全二叉树。因为完全二叉树中父结点和子结点的存在如下的位置关系。

  • 如果父结点的位置为 i
  • 如果存在左、右子结点,则左子结点的位置为 i*2、右子结点的位置为 2*i+1
  • 如果已知子结点的位置i,则父结点的位置是 i/2。根结点的父结点位置为 0

有了这个良好的数学关系,线段树常使用数组方式进行存储。

4.1 结点类

结点类中有一个lazy属性,称为延迟更新值,延迟更新是线段树的一个显著的特点。暂且不表,在线段树的区间更新时再深聊。

#include <bits/stdc++.h>
#include <cmath>
using namespace std;
struct TreeNode {
	//编号,与结点存储位置对应
	int code;
	//结点的值(权重)
	int value;
	//左边界
	int left;
	//右边界
	int right;
	//延迟更新值
	int lazy; 
	/*
	*无参构造 
	*/
	TreeNode() {
		this->code=0;
		this->lazy=0;
	}
	/*
	*有参构造 
	*/ 
	TreeNode(int code,int value,int left,int right) {
		this->code=code;
		this->value=value;
		this->left=left;
		this->right=right;
		this->lazy=0;
	}
	/*
	*自我显示 
	*/ 
	void desc() {
		cout<<"结点存储位置:"<<this->code<<",区间:["<<this->left<<","<<this->right<<"],值:"<<this->value<<endl;
	}
};

4.2 线段树类

class SegmentTree {
	private:
		//使用数组存储线段树的结点
		TreeNode** st;
         //线段树大小
		int size;
	public:
		SegmentTree(int size):size(size) {
			//树的深度
			int h=ceil(log2(size)) +1;
             //数组的大小
			this->size=pow(2,h);
			this->st=new TreeNode*[this->size] {NULL};
		}
		/*
		* 初始化线段树
		* arr: 原数组
		* pos: 线段树中的位置
		* left:左区间
		* right:右区间
		*/
		int initTree(int* arr,int pos, int left,int right);
		/*
		* 查找指定区间的最大值
		*/
		int getMax(int left,int right);
		/*
		*单点更新
		*/
		int update(int pos,int index,int val);
		/*
		*区间更新
		*/
		int queryUpdate(int pos,int left,int right,int val);
        /*
        *显示树结点
        */
		void showAll() {
			for(int i=0; i<this->size; i++) {
				if(this->st[i]!=NULL)
					this->st[i]->desc();
			}
		}
};

4.2.1 初始化函数

使用递归初始化整个线段树。

int SegmentTree::initTree(int* arr,int pos, int left,int right) {
	if(left==right) {
		//如果左、右边界相同
		this->st[pos]=new TreeNode(pos,arr[left],left,right);
        //叶结点是递归出口
		return arr[left];
	}
    //二分思想划分左右区间
	int mid=(right+left)/2;
	//初始左子结点 
	int lVal= initTree(arr,2*pos,left,mid);
	//初始右子结点
	int rVal= initTree(arr,2*pos+1,mid+1,right);
	//找左、右子结点中的较大值
	int val=max(lVal,rVal);
	//以较大值创建结点
	this->st[pos]=new TreeNode(pos,val,left,right);
	return val;
}

测试构建线段树:

x9.png

4.2.2 区间查询

查询指定区间中的最大值,需分几种情况讨论。

  • 无效区间。如下图所示,[left,right]中的left>7right<0时。返回无解。

x10.png

  • 完整包含。当[left,right]中的left<=0 and right>=7时。返回[0,7]区间的最大值。

x11.png

  • 匹配左或右子区间。查找左、或右子空间中的最大值。
    x13.png

  • 与左、右子空间相集。为左、右子空间中较大的值,即为[0,7]区间最大值。

x14.png

/*
*区间查找
*/
int SegmentTree::getMax(int left,int right) {
    //从根结点开始查找
    int pos=1;
    //移动指针
    TreeNode* move=NULL;
    while(1) {
        move=this->st[pos];
        if (left>move->right || right<move->left )
            //无效区间
            return 1>>31;
        else if( left<=move->left && right>=move->right )
            //查找区间恰好包含在此区间
            return move->value;
        else {
            //中间位置
            int mid=(move->left+move->right)/2;
            if( right<=mid )
                //左边查找
                pos=move->code*2;
            else if(left>=mid )
                //右边查找
                pos=move->code*2+1;
            else
                return move->value;
        }
    }
}

测试区间查找:

int main() {
	//省略……
	int res= segmentTree.getMax(2,7);
	cout<<"区间[2,7]最大值:"<<endl;
	cout<<res<<endl;
	res= segmentTree.getMax(1,3);
	cout<<"区间[1,3]最大值:"<<endl;
	cout<<res<<endl;
	return 0;
}

输出结果:

x15.png

4.2.3 单点更新

单点更新某一个叶结点上的值。使用递归方案一路向下查询到叶结点,再在回溯过程中更新非叶结点。和初始线段树的逻辑相似。

/*
*单点更新
*/
int update(int pos,int index,int val) {
    TreeNode* move=this->st[pos];
    if( move->left== move->right ) {
        //如果是叶结点,直接更新
        this->st[pos]->value+=val;
        return 	this->st[pos]->value;
    }
    //不是叶结点
    int mid= (move->left+move->right)/2;
    int lVal=0;
    int rVal=0;
    int mx=0;
    if( index<=mid ) {
        //更新左边子区间
        lVal=update(pos*2,index,val);
        //在更新后的左子区间和右子区间中找出较大的值
        mx=max(lVal,this->st[pos*2+1]->value );
    } else {
        //更新右子空间
        rVal=update(pos*2+1,index,val);
        //在更新后的右子区间和左子区间中找出较大的值
        mx=max(this->st[pos*2]->value, rVal);
    }
    //更新当前位置
    this->st[pos]->value=mx;
    return mx;
}

测试单点更新:

int main() {
    //省略……
   cout<<"\n索引号为 3 位置的值增加 5(原来是 9,增加后为 14) \n"<<endl; 
	segmentTree.update(1,3,5);
	segmentTree.showAll();
	return 0;
}

输出结果:

x16.png

当同时需要更新的叶结点较多时,因为单点更新的时间复杂度为O(logn),如果逐次调用单点更新函数,需要能达到最终结点,时间复杂度为O(n*logn)

线段树提供了延迟更新策略,算是线段树最高光之处。

4.2.4 区间更新

区间更新并不要求一步到位,而是利用了积累的力量。基本思想是边查询边更新,查询到哪里更新到哪里。

如下图所示,线段树上的每一个结点都有一个lazy延迟更新属性,初始值为 0

x17.png

  • 当需要让[0,3]区间内所有叶结点值+5时。更新会延迟到当某次需要查询[0,3]区间的最大值时,这时从根结点向下查询到[0,3]结点9。让结点 9的值增加为 14,且结点 9 的 lazy属性存储增量5后再把 14 返回给根结点,让根结点更新为14

x18.png

  • 当下次需要查询[0,1]区间内最大值时。当查询到[0,3]且发现其lazy属性值不等于0。则会把此值向左、右子结点传递。

x19.png

/*
*区间更新
*/
int SegmentTree::queryUpdate(int pos,int left,int right,int val) {
    //移动指针
    TreeNode* move=this->st[pos];
    if (left>move->right || right<move->left )
        //无效区间
        return 1>>31;
    if( left<=move->left && right>=move->right ) {
        //查找区间恰好包含在此区间
        move->lazy+=val;
        move->value+=move->lazy;
        //叶结点,清除延迟值
        move->lazy=move->left==move->right?0:move->lazy;
        return move->value;
    }
    //中间位置
    int mid=(move->left+move->right)/2;
    int lVal=0;
    int rVal=0;
    int mx=0;
    if(move->lazy!=0) {
        //延迟值向左、右子结点传递
        this->st[pos*2]->lazy=move->lazy;
        this->st[pos*2+1]->lazy=move->lazy;
        //清零
        move->lazy=0;
    }
    if(right<=mid ) {
        //查询左边
        lVal= queryUpdate(pos*2,left,right,val);
        this->st[pos]->value=max(move->value, lVal);
        return lVal;
    } else if(left>=mid ) {
        //右边查找
        rVal=queryUpdate(pos*2+1,left,right,val);
        this->st[pos]->value=max(move->value, rVal);
        return rVal;
    } else {
        //查找区间恰好包含在此区间
        move->lazy+=val;
        move->value+=move->lazy;
        //叶结点,清除延迟值
        move->lazy=move->left==move->right?0:move->lazy;
        return move->value;
    }
}

测试区间更新:

int main() {
	//省略……
	cout<<"\n对区间[0,3]的结点值加 5,查询时更新: \n"<<endl;
	//segmentTree.update(1,3,5);
	int res= segmentTree.queryUpdate(1,0,3,5);
	cout<<"[0,3]最大值"<<res<<endl;
	segmentTree.showAll();
	cout<<"\n对区间[0,1]的结点值加 9,查询时更新: \n"<<endl;
	res=segmentTree.queryUpdate(1,0,1,9);
	cout<<"[0,1]最大值"<<res<<endl;
	segmentTree.showAll();
	cout<<"\n对区间[0,0]的结点值加 9,查询时更新: \n"<<endl;
	res=segmentTree.queryUpdate(1,0,0,9);
	cout<<"[0,0]最大值"<<res<<endl;
	segmentTree.showAll();
    return 0;
}

输出结果: 可以看到[,当查询到[0,0]结点时,些结点才一次性全部更新。

x20.png

5. 总结

线段树是很有个性的数据结构,常用于解决区间类型问题。线段树有一个延迟更新理念,通过查询深度不同,更新到的深度也不一样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/194289.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用 Python 的 tkinter 模块编写一个好看又强大的中国象棋

继上次我的第一版的《中国象棋》程序之后&#xff0c;我又编写了第二版的《中国象棋》程序&#xff0c;关注我的粉丝知道&#xff0c;我在第一篇《中国象棋》的文章末尾说了&#xff0c;我会出第二版的&#xff0c;对第一版感兴趣的朋友们&#xff0c;可以去看看&#xff0c;也…

VueJS 之样式冲突与样式穿透

文章目录参考描述样式冲突现象scoped原理样式穿透深度选择器使用原理顶层元素局限性参考 项目描述搜索引擎Bing哔哩哔哩黑马程序员 描述 项目描述Edge109.0.1518.70 (正式版本) (64 位)操作系统Windows 10 专业版vue/cli5.0.8npm8.19.3VueJS2.6.14 样式冲突 在使用 Vue 进行…

大文件上传/下载

一、前言 大文件上传下载一直以来是前端常用且常考的热门话题。本文将分别介绍大文件上传/下载的思路和前端实现代码。 二、分片上传 整体流程 对文件做切片&#xff0c;选择文件后&#xff0c;对获取到的file对象使用slice方法可以将其按照制定的大小进行切片&#xff0c;…

使用matplotlib,pylab进行python绘图

一提到python绘图&#xff0c;matplotlib是不得不提的python最著名的绘图库&#xff0c;它里面包含了类似matlab的一整套绘图的API。因此&#xff0c;作为想要学习python绘图的童鞋们就得在自己的python环境中安装matplotlib库了&#xff0c;安装方式这里就不多讲&#xff0c;方…

openmmlab学习打卡1

openmmlab学习打卡1通用视觉框架 OpenMMLab通过 conda 安装通用视觉框架 OpenMMLab 基于pytorch实现 其中&#xff1a; 分类算法在 mmclassification 模块下 目标检测在 mmdetection 模块下 分割模型在 mmsegmentation 模块下&#xff08;openmmlab 2.0 版本中加入&#xff09…

洛谷P1885 Moo —— 搜索

This way 题意&#xff1a; 奶牛 Bessie 最近在学习字符串操作&#xff0c;它用如下的规则逐一的构造出新的字符串&#xff1a; S(0)S(0) S(0) moo S(1)S(0)S(1) S(0) S(1)S(0) m ooo S(0) S(0) S(0) moo m ooo moo moomooomoo S(2)S(1)S(2) S(1) S(2)S(1) m oooo S(…

无js实现拖拽边框改变大小的笔记

前言 最近刷抖音看到一款游戏"拣爱",看到这个人手动拖动的很有意思,就想着能不能前端实现,来学习学习,虽然说最终的效果没有gif图片那么好,但是也算实现了,吧… 具体原理 利用resize属性所出现的小拖拽条 再配合::-webkit-scrollbar设置拖拽区域宽度,高度,结合opac…

手动签发证书配置nginx

openssl和ssh基本用法 通过OpenSSL工具生成证书 创建私钥 openssl genrsa -des3 -out server.key 2048 注意&#xff0c;centos版本如果是CentOS Linux release 8.0.1905 (Core)版本&#xff0c;私钥长度不能设置成1024位&#xff0c;必须2048位。不然再最后启动nginx时会出…

java之数组模块

数组定义格式1.1数组概述一次性声明大量的用于存储数据的变量要存储的数据通常都是同类型数据&#xff0c;例如&#xff1a;考试成绩1.2什么是数组数组(array)是一种用于存储多个相同类型数据的存储模型1.3数组的定义格式格式一&#xff1a;数据类型[] 变量名范例&#xff1a; …

h5实现相机

什么是取景器 取景器是什么&#xff1f;取景器是相机的一个专业术语&#xff0c;在前端就是扫描拍照 取景器的实现原理 请求手机的一个媒体类型的视频轨道&#xff0c;利用一个div或者图片作为上层蒙层&#xff0c;然后在利用canvas绘制视频中某一帧的画面绘制为图片。 前期…

HTML基础知识

一个网站由两部分组成&#xff1a;前端和后端。前端主流语言目前是HTML、CSS、JS等。HTML只是描述了页面的内容&#xff08;骨架&#xff09;&#xff0c;CSS才是描述了页面的样式。HTML结构HTML标签HTML代码是由“标签”构成的&#xff0c;HTML描述了页面上有什么东西&#xf…

数字化转型导师坚鹏:银行数字化转型为什么需要融合王阳明心学

在BLM银行数字化转型方法论中&#xff0c;我之所以融合BLM模型与王阳明心学&#xff0c;作为一个工科背景并拥有多年软硬件产品研发经验的人来说&#xff0c;深刻地知道很多人利用了科技的力量做了大量的恶事&#xff0c;而不是善事&#xff0c;如黑客大量盗取、泄漏、贩卖客户…

ESLint 的一些理解

ESLint ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具&#xff0c;它的目标是保证代码的一致性和避免错误。 为什么要使用ESLint 有的可以帮我们避免错误&#xff1b;有的可以帮我们写出最佳实践的代码&#xff1b;有的可以帮我们规范变量的使用方式&a…

Docker入门之使用Dockerfile 构建镜像(七)

文章目录1. 前言2. Docker file 核心要点2.1 注意事项2.2 Docker file 执行流程2.3 Docker Image、Docker file、Docker Container区别2.4 Dockerfile常用保留字指令2.4.1 FROM2.4.2 MAINTAINER2.4.3 RUN2.4.4 EXPOSE2.4.5 WORKDIR2.4.6 USER2.4.7 ENV2.4.8 ADD2.4.9 COPY2.4.1…

ansible 简单使用

运行过程 1.加载自己的配置文件&#xff0c;默认/etc/ansible/ansible.cfg&#xff1b; 2.查找对应的主机配置文件&#xff0c;找到要执行的主机或者组&#xff1b; 3.加载自己对应的模块文件&#xff0c;如 command&#xff1b; 4.通过ansible将模块或命令生成对应的临时py文…

OpenMMLab 实战营打卡 - 第 一 课

OpenMMLab 实战营打卡 - 第 一 课 复习下总忘的基础知识 卷积的通道数变化 前一层特征纬度&#xff08;通道数&#xff09;决定核的通道数 当前层输出的特征纬度&#xff0c;由核的数量决定 图像尺寸变化 padding 公式&#xff1a;H′H−K12pH^{\prime}H-K12 pH′H−K12p…

电源技术中的安森美 单通道电压电平转换器件FXLP34P5X 适合便携式应用方案

电源技术中的安森美 单通道电压电平转换器件FXLP34P5X 适合便携式应用方案 &#xff1a;输入转换器电源电压为VCC1&#xff0c;输出转换器电源电压为VCC。 该器件使用1.0V至3.6V的VCC值运行&#xff0c;主要用于要求超低功耗的便携式应用。内部电路由最小量的缓冲器级组成&…

普通大学生自学 JAVA 怎样才能进大厂?

前言 可以看一下现在大厂对于Java方面的要求 阿里 百度 腾讯 从上面可以看出&#xff0c;无论是阿里、百度亦或是腾讯对于Java方面的要求是比较高的&#xff0c;可以说要求的是一个全面&#xff0c;所以想要进入大厂&#xff0c;不能操之过急&#xff0c;需要先从基础做起&am…

php报错SERVER SENT CHARSET (255) UNKNOWN

配置文件PHP.ini修改打开; extension_dir "ext"&#xff0c;修改成; extension_dir "./" ; On windows: extension_dir "自己php的存放路径\ext"2.打开extensionmsql.dll; For example, on Windows: ;extensionmsql.dll3.修改配置&#xff08…

五、Linux 用户管理常用命令

一、用户管理命令 - useradd 命令名称&#xff1a;useradd 命令所在路径&#xff1a;/usr/sbin/useradd 执行权限&#xff1a;root 功能描述&#xff1a;添加新用户 语法&#xff1a;useradd 用户名 二、用户管理命令 - userdel 命令名称&#xff1a;userdel 命令所在路…