0基础学习线段树

前言:

线段树:用树来表示一个一个的线段区间。

1、为什么要使用线段树?

题目:给定一个数组nums,我们有两种下面两种操作

1、查询nums数组下标i到下标j的和;

2、将nums数组指定下标的值改为指定的一个新值;

如果上面两种操作频繁交叉进行,如何使整体效率更高。

方案一:更新操作O1

每次查询操作都从i加到j,每次更新直接更新对应下标。

缺点:如果每次查询操作都是查整个数组的和,一下子来了几万个查询操作,则每次都是O(n),几万次O(n),比较浪费时间。

方案二:查询操作O1

新建一个同等大小数组dp,dp【i】用于记录从nums[0]加到nums[i]的值。

缺点:虽然查询操作从O(n),变成O1(dp[j]-dp[i-1])。但是更新操作需要变成了O(n),每次更新都需要维护dp数组,从更新处开始往后面的值都需要重新计算,如果一次性来了大量的更新操作请求,则比较浪费时间。

方案三:线段树(查询Ologn,更新Ologn)

2、线段树为什么开4n空间?

1、假设n刚好是2的整数次阶乘,则n的满二叉树具有下面特征:

1)树的层高:h=log2^n+1;

2)树的最后一层的节点数:an=2^(h-1);

3)树的节点数总和:sn=2n-1;

那为什么要开4n空间呢?因为如果n=9,呢?即n不是刚好2的某个整数的阶乘,这时候会在满二叉树的基础上多出一层来。

多出来一层的节点数为:an+1=2^h;

那么树的节点数总和就变成了:sn=2n-1+2^h,h=log2^n+1,所以:sn=4n-1。

因为存储的时候下标从1开始,所以我们给4n的空间。

3、构造线段树

1、节点数组下标分配:从上到下,从左到右

2、父子节点间下标关系:

l = fa*2 (左子树下标为父节点下标的两倍)
r = fa*2+1(右子树下标为父节点下标的两倍+1)

3、代码如下:

class Tree{
	int[] nums;
	int[] tree;
	public Tree(int[] nums){
		this.nums = nums;
		int n = nums.length;
		tree = new int[4*n];
		build(nums,0,0,0);
		
	}
//	l = fa*2 (左子树下标为父节点下标的两倍)
//	r = fa*2+1(右子树下标为父节点下标的两倍+1)	
	public void build(int[] nums,int id,int l,int r){
		if(l==r){
			tree[id] = nums[l];
			return;
		}
		int mid = (l + r)/2;
		build(nums,2*id,l,mid);
		build(nums,2*id+1,mid,r);
		tree[id] = tree[2*id]+tree[2*id+1];			
	}
}

4、线段树区间查询

我们知道线段树的每个结点存储的都是一段区间的信息 ,如果我们刚好要查询这个区间,那么则直接返回这个结点的信息即可,比如对于上面线段树,如果我直接查询[1,6]这个区间的最值,那么直接返回根节点信息返回13即可,但是一般我们不会凑巧刚好查询那些区间,比如现在我要查询[2,5]区间的最值,这时候该怎么办呢,我们来看看哪些区间被[2,5]包含了。

一共有5个区间,而且我们可以发现[4,5]这个区间已经包含了两个子树的信息([4,4],[5,5]),所以我们需要查询的区间只有三个,分别是[2,2],[3,3],[4,5],我们从根节点开始往下递归,如果当前结点是被要查询的区间包含了的,则返回这个结点的信息,这样从根节点往下递归,时间复杂度也是O(logN)。

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/weq2011/article/details/128791426

//	查询指定区间内的值的和
	public int find(int id,int l,int r,int x,int y){
//		当区间完全在要求查找区间范围内时,就是我们要的值
		if(l>=x && r<=y){
			return tree[id];
		}
		int mid = (l + r)/2;
		int sum = 0;
//		如果值存在于左子树,则需要进行查找
		if(x<=mid){
			sum+=find(2*id,l,mid,x,y);
		}
//		如果值存在于右子树,则需要进行查找
		if(y>mid){
			sum+=find(2*id+1,mid+1,r,x,y);
		}
		return sum;
	}

5、线段树更新

id:从线段树哪个下标开始检索

l:数值区间左

r:数值区间右

index:需要更新的dp下标

val:更新的值

//	更新指定下标的值
//	指定下标
	public void update(int id,int l,int r,int index,int val){
		tree[id] = tree[id]+val;
		if(l == index && r == index){
			return;
		}
		int mid = (l + r)/2;
		if(index<=mid){
			update(2*id,l,mid,index,val);
		}
		if(index>mid){
			update(2*id+1,mid+1,r,index,val);
		}
		
	}

6、完整案例及代码

数组 arr 中 大于 前面和后面相邻元素的元素被称为 峰值 元素。

给你一个整数数组 nums 和一个二维整数数组 queries 。

你需要处理以下两种类型的操作:

  • queries[i] = [1, li, ri] ,求出子数组 nums[li..ri] 中 峰值 元素的数目。
  • queries[i] = [2, indexi, vali] ,将 nums[indexi] 变为 vali 。

请你返回一个数组 answer ,它依次包含每一个第一种操作的答案。

注意:

  • 子数组中 第一个 和 最后一个 元素都 不是 峰值元素。

 

示例 1:

输入:nums = [3,1,4,2,5], queries = [[2,3,4],[1,0,4]]

输出:[0]

解释:

第一个操作:我们将 nums[3] 变为 4 ,nums 变为 [3,1,4,4,5] 。

第二个操作:[3,1,4,4,5] 中峰值元素的数目为 0 。

示例 2:

输入:nums = [4,1,4,2,1,5], queries = [[2,2,4],[1,0,2],[1,0,4]]

输出:[0,1]

解释:

第一个操作:nums[2] 变为 4 ,它已经是 4 了,所以保持不变。

第二个操作:[4,1,4] 中峰值元素的数目为 0 。

第三个操作:第二个 4 是 [4,1,4,2,1] 中的峰值元素。 

提示:

  • 3 <= nums.length <= 105
  • 1 <= nums[i] <= 105
  • 1 <= queries.length <= 105
  • queries[i][0] == 1 或者 queries[i][0] == 2
  • 对于所有的 i ,都有:
    • queries[i][0] == 1 :0 <= queries[i][1] <= queries[i][2] <= nums.length - 1
    • queries[i][0] == 2 :0 <= queries[i][1] <= nums.length - 11 <= queries[i][2] <= 105

1)线段树求解代码: 

package algorithm.temp;

import java.util.ArrayList;
import java.util.List;

class Test2 {
	static final int COUNT_PEAK = 1, UPDATE = 2;

	public static void main(String[] args) {
		int[][] queyy = new int[4][3];
		queyy[0][0] = 2;
		queyy[0][1] = 0;
		queyy[0][2] = 2;
		queyy[1][0] = 1;
		queyy[1][1] = 0;
		queyy[1][2] = 3;
		
//		queyy[0][0] = 1;
//		queyy[0][1] = 2;
//		queyy[0][2] = 4;
//		queyy[1][0] = 1;
//		queyy[1][1] = 0;
//		queyy[1][2] = 1;
		queyy[2][0] = 1;
		queyy[2][1] = 3;
		queyy[2][2] = 3;
		queyy[3][0] = 2;
		queyy[3][1] = 3;
		queyy[3][2] = 5;
		System.out.println(countOfPeaks(new int[] {9,7,5,8,9}, queyy));
		;
	}
	    public static List<Integer> countOfPeaks(int[] nums, int[][] queries) {
	        List<Integer> li = new ArrayList<Integer>();
	        int[] dp = new int[nums.length];
	        dp[0] = 0;
	        for(int i=1;i<nums.length;i++){
	            if(isTopElement(nums,i)){
	                dp[i]= 1;
	                dp[++i] = 0;
	            };
	        }
	        Tree tree = new Tree(dp);
	        
	        for(int i=0;i<queries.length;i++){
	            int[] temp = queries[i];
	            if(temp[0] == 1){
	                int x = temp[1];
	                int y = temp[2];
	                li.add(y-x>1?tree.find(1, 0, dp.length-1, x+1, y-1):0);
	            }else{
	            	int index = temp[1];
	            	int val = temp[2];
	            	nums[index] = val;
	            	for(int k=-1;k<=1;k++){
	            		int num=0;
	            		if(index+k>0 && index+k<nums.length-1){
	            			num=isTopElement(nums,index+k)?1-dp[index+k]:0-dp[index+k];
	            			tree.update(1,0, dp.length-1, index+k, num);
	            			dp[index+k] = dp[index+k]+num;
	            		}
	            	}
	            }
	        }
	        return li;
	    }

	    public static boolean isTopElement(int[] nums,int index) {
	        if(index-1>=0 && index+1<=nums.length-1 && nums[index]>nums[index-1] && nums[index]>nums[index+1]){
	            return true;
	        }
	        return false;
	    }
	}

class Tree{
	int[] nums;
	int[] tree;
	public Tree(int[] nums){
		this.nums = nums;
		int n = nums.length;
		tree = new int[4*n];
		build(nums,1,0,n-1);
		
	}
//	l = fa*2 (左子树下标为父节点下标的两倍)
//	r = fa*2+1(右子树下标为父节点下标的两倍+1)	
	public void build(int[] nums,int id,int l,int r){
		if(l==r){
			tree[id] = nums[l];
			return;
		}
		int mid = (l + r)/2;
		build(nums,2*id,l,mid);
		build(nums,2*id+1,mid+1,r);
		tree[id] = tree[2*id]+tree[2*id+1];			
	}
//	查询指定区间内的值的和
	public int find(int id,int l,int r,int x,int y){
//		当区间完全在要求查找区间范围内时,就是我们要的值
		if(l>=x && r<=y){
			return tree[id];
		}
		int mid = (l + r)/2;
		int sum = 0;
//		如果值存在于左子树,则需要进行查找
		if(x<=mid){
			sum+=find(2*id,l,mid,x,y);
		}
//		如果值存在于右子树,则需要进行查找
		if(y>mid){
			sum+=find(2*id+1,mid+1,r,x,y);
		}
		return sum;
	}
//	更新指定下标的值
//	指定下标
	public void update(int id,int l,int r,int index,int val){
		tree[id] = tree[id]+val;
		if(l == index && r == index){
			return;
		}
		int mid = (l + r)/2;
		if(index<=mid){
			update(2*id,l,mid,index,val);
		}
		if(index>mid){
			update(2*id+1,mid+1,r,index,val);
		}
		
	}
}

2)暴力求解代码

(执行效率低下,本人第一次就是写的这段代码,案例跑超时):

class Solution {
    public List<Integer> countOfPeaks(int[] nums, int[][] queries) {
        List<Integer> li = new ArrayList<Integer>();
        int[] dp = new int[nums.length];
        dp[0] = 0;
        for(int i=1;i<nums.length;i++){
            if(isTopElement(nums,i)){
                dp[i]= 1;
                dp[++i] = 0;
            };
        }

        for(int i=0;i<queries.length;i++){
            int[] temp = queries[i];
            if(temp[0] == 1){
                int sum = 0;
                for(int k=temp[1]+1;k<temp[2]&&k<nums.length;k++){
                    sum+=dp[k];
                }
                li.add(sum);
            }else{
            	nums[temp[1]] = temp[2];
                for(int k=temp[1]-1;k<=temp[1]+1;k++){
                    if(isTopElement(nums,k)){
                    	dp[k] = 1;
                    }else if(k>0&&k<nums.length){
                    	dp[k] = 0;
                    }
                }
            }
        }
        return li;
    }

    public boolean isTopElement(int[] nums,int index) {
        if(index-1>=0 && index+1<=nums.length-1 && nums[index]>nums[index-1] && nums[index]>nums[index+1]){
            return true;
        }
        return false;
    }
}

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无声游子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值