手把手教你从0入门线段树~

news2024/12/25 23:56:26

1. 什么是线段树?

1.1 初探线段树

定义:线段树是一种用于解决区间查询问题的数据结构,是一种广义上的二叉搜索树

原理:将一个区间划分为多个较小的子区间,并为每个子区间存储一些有用的信息,例如最大值、最小值或总和。

可以解决的问题:通过将区间存储的信息逐级向上汇总,线段树可以快速回答各种类型的区间查询,例如求和、最大值、最小值或更新某个区间的值。

**时间复杂度:**线段树的构建和查询操作的时间复杂度都是O(logN),其中 N 是区间的大小。

限制条件:能用线段树解决的问题必须满足区间加法区间加法也就是对于[L,R] 区间,它的答案可以由[L,M][M+1,R] 的答案合并给出。其中,M 是区间的中点

1.2 线段树与二叉树的区别

对于一个数组 [1,2,3,4,5,6],它的二叉树和线段树如下图所示**「区间下标从 0 开始」**:

从图中可以看出,二叉树中单个节点存储的内容是值 val,而线段树中存储的是区间信息

观察线段树,我们还可以快速得出以下结论:

  • 每个节点的左右孩子,分别存储该节点区间的一半
  • 当区间无法划分时,得到叶子节点

1.3 线段树的下标

接着,我们给线段树的每个节点加上数组的下标,得到的结果如下:

可以看到,在线段树中,当节点的下标为 i 时,其左孩子的下标为 2 * i + 1,右孩子的下标为 2 * i + 2。这个时候,我们需要考虑一个问题:当数组大小是 n 时,线段树的空间应该为多少?

答案:2 * n - 1,在一颗完全二叉树中,叶子节点的数量等于非叶子结点的数量减一。在线段树中,叶子节点的个数等于数组大小 n ,非叶子节点的个数为 n - 1,因此线段树的空间应该为 2 * n - 1。注意,为了方便计算以及防止数组越界,我们通常会将线段树的空间大小开到比总节点数更大的最小的2的幂次方,即 4*n 大小的空间。

1.4 线段树的存储内容

1.1 小节中说到:线段树可以快速回答各种类型的区间查询,例如求和、最大值、最小值。那么在求和中,线段树是如何表示的呢?

可以看到,每个叶子节点的存储的是数组下标值 val,每个非叶子节点的求和值等于其左右孩子节点存储值之和;同理,在最大\最小值中,非叶子节点存储的值是其左右孩子节点中的较大\较小值

2. 线段树解决问题的步骤

2.1 建树

虽然线段树中存储的是一段区间的信息,但我们并不需要定义一个类,让它存储区间左值,区间右值以及求和。因为我们可以借助递归+下标关系的方式创建线段树,这样线段树的节点就只需要存储求和。

int nums[] = new int[]{1, 2, 3, 4, 5, 6};
int n = nums.length;
int[] segTree = new int[4 * n]; // 为线段树分配空间


void buildTree(int index, int left, int right) { // index 表示下标,left 表示左区间,right 表示右区间
    if (left == right) {
        segTree[index] = nums[left];
        return; // 到叶子节点就不能继续划分啦~
    }
    int mid = (left + right) / 2; // 一分为 2,例如将 [1,6] 划分为 [1,3] 和 [4,6]
    buildTree(2 * index + 1, left, mid); // 构建左子树,左孩子的下标为 2 * index + 1
    buildTree(2 * index + 2, mid + 1, right); // 构建右子树,右孩子的下标为 2 * index + 2
    segTree[index] = segTree[2 * index + 1] + segTree[2 * index + 2]; // 这里是求和,所以非叶子节点存储的值是左右孩子节点存储的值之和
}

public static void main(String[] args) {
        Solution solution = new Solution();
        solution.buildTree(0, 0, solution.nums.length - 1);
    }

2.2 单点修改

单点修改是区间修改的一种特殊情况,我们先从简单的单点修改看看线段树是如何实现更新的。

思路:假如我们要更新数组的第 i 个值为 x ,那么我们可以从根节点去寻找区间左侧和区间右侧均等于 i 的节点,修改它的值。然后在返回的路上不断更新其祖先节点的值

public void update(int i, int value) {
    update(0, 0, nums.length - 1, i, value);
}

private void update(int index, int left, int right, int i, int value) { // i 表示要更新数组的下标,value 是更改后的值
    if (left == right) { // 当搜寻到叶子节点的时候,就可以修改了,前提是 i 在[0,2 * n - 2] 之间,下标从 0 开始算
        segTree[left] = value;
        return; // 赋值完就结束
    }
    int mid = (left + right) / 2;
    if (i <= mid) update(2 * index + 1, left, mid, i, value);
    else update(2 * index + 2, mid + 1, right, i, value);
    segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 更新祖先节点
}

2.3 仅存在单点修改的区间查询

还记得线段树的使用条件吗?必须满足区间加法

因此,当我们查询一个区间 [a,b] 时,可以将其拆分成满足区间加法的子区间。还是以求和为例sum[1,5] = sum[1,3] + sum[4,5]sum[2,5] = sum[2,2] + sum[3,3] + sum[4,5]

public int query(int x, int y) {
        return query(0, 0, nums.length - 1, x, y);
    }

    private int query(int index, int left, int right, int x, int y) { // x 表示要查询的左区间,y 表示要查询的右区间
        if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0 
        if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
        int mid = (left + right) / 2;
        int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
        int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
        return leftQuery + rightQuery; // 求和 
    }

2.4 区间修改

当我们需要修改的内容是一个区间而不是一个单点,就不能通过 for 循环的方式调用单点循环,因为这样与暴力破解无异。

为了解决这个问题,我们需要引入一个新的概念:延迟标记,你也可以叫它懒标记。这个标记的意义是:被这个标记过的区间值已经被更新,但它的子区间未被更新更新的信息是标记中存储的值

引入延迟标记的区间修改遵循以下规则:

(1)如果要修改的区间完全覆盖当前区间,直接更新这个区间,并打上延迟标记

(2)如果没有完全覆盖,且当前区间有延迟标记,先下传延迟标记到子区间,再清除当前区间的延迟标记。

(3)如果修改区间与左儿子有交集,就搜索左儿子;如果与右儿子有交集,就搜索右儿子

(4)更新当前区间的值

文字太多,是不是感觉头晕了。没关系,我们用一个具体的例子来看看区间修改~

将 nums 数组[0,3] 区间中的每个数加上 1 ,加上后数组将变成 [2,3,4,5,5,6]。在线段树中,我们首先访问根节点 [0,5],修改区间显然不完全覆盖区间[0,5],且当前节点不存在延迟标记;我们再看当前节点的左孩子[0,2],很显然 [0,3][0,2] 存在交集,搜索左孩子:
接着,我们来搜索 [0,2] 左孩子,首先 [0,3] 完全覆盖 [0,2],那么我们更新这个区间,因为节点记录的是求和,而我们要对这个区间的每个数加上 1 ,那么总和 sum = sum + 1 * 区间长度,也就是 sum = 6 + (2 - 0 + 1) * 1 = 9;接下来,我们给节点打上延迟标记 LazyTag = + 1,表示这个节点的子节点都还没进行 +1 操作。


搜索完了[0,2]左孩子,我们又看到[0,3][0,5] 的右孩子[3,5] 存在交集,我们开始搜索右孩子:首先,[0,3] 不覆盖[3,5][3,5] 不存在延迟标记;我们分别查看[3,5] 的左孩子和右孩子,发现其与左孩子[3,4] 有交集,开始搜索 [3,4]


我们发现 [0,3] 不完全覆盖 [3,4],且[3,4] 所在节点不包含延迟标记,我们搜索它的子节点。发现 [0,3][3,3] 有交集,且 [0,3] 完全覆盖[3,3],我们更新该区间:sum = sum + 更新值 * 区间长度,即 sum = 4 + 1 * ( 3 - 3 + 1) = 5 ;然后给节点打上延迟标记 LazyTag = +1「正是因为叶子节点也有延迟标记,还需要继续下放,也就还需要2倍的空间,因此 2 * n - 1 空间不足,需要4 * n 的空间」


至此,我们搜索完毕,开始逐层向上更新区间值:

到这里,细心的你可能会发现延迟标记规则的第二条「如果没有完全覆盖,且当前区间有延迟标记,先下传延迟标记到子区间,再清除当前区间的延迟标记」在刚刚的例子中并没有出现下传延迟标记到子区间的情况。这是因为我们只进行了一次区间更新,当我们进行多次区间更新时,就会出现这个情况~~
在这里插入图片描述

假设我们又要对区间 [0,1] 进行全部加一的更新。首先判断根节点区间与 [0,1] 区间的关系,[0,1] 不覆盖 [0,5],我们搜索 [0,5] 的孩子节点;[0,1] 部分覆盖 [0,2] ,且 [0,2] 所在节点有延迟标记,所以我们执行以下操作:

  1. 下传延迟标记到其左右孩子节点
  2. 更新左右孩子节点的区间值,[0,1] 处更新为 sum = sum + 更新值 * 区间长度,即 sum = 3 + (1 - 0 + 1) * 1 = 5。右孩子节点 sum = 3 + (2 - 2 + 1) * 1 = 4
  3. 将当前节点的延迟标记重置为 0,即不存在延迟标记

下传延迟标记完毕。搜索与[0,2] 有交集的 [0,1] 节点,我们发现 [0,2] 完全覆盖[0,1],直接更新 [0,1] 区间等于 5+(1-0+1)=7,添加延迟标记 LazyTag = + 1,最后向上更新区间值

为了方便记忆,我们把区间更新的步骤简记为:完全覆盖、部分覆盖「标下下传」、搜索孩子、更新区间。下面来看看它的代码实现:

void pushUp(int index) {
    segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 向上更新,用孩子节点更新父节点
}

void pushDown(int index, int left, int right) { // 向下传递延迟标记
    if (lazyTag[index] != 0) {
        int mid = (left + right) / 2;
        lazyTag[index * 2 + 1] += lazyTag[index]; //更新左孩子的延迟标记
        lazyTag[index * 2 + 2] += lazyTag[index];//更新右孩子的延迟标记
        segTree[index * 2 + 1] += lazyTag[index] * (mid - left + 1); // 区间值 = sum + 更新值 *(区间长度)
        segTree[index * 2 + 2] += lazyTag[index] * (right - mid);
        lazyTag[index] = 0; // 清除延迟标记
    }
}
public void intervalUpdate(int x, int y, int value) {
    intervalUpdate(0, 0, nums.length - 1, x, y, value);
}
private void intervalUpdate(int index, int left, int right, int x, int y, int value) {
    if (x <= left && y >= right) { // 完全覆盖
        segTree[index] += value * (right - left + 1); // 更新区间值
        lazyTag[index] += value; // 更新延迟标记
        return;
    }
    pushDown(index, left, right); // 部分覆盖,下传延迟标记
    int mid = (left + right) / 2;
    if (x <= mid) intervalUpdate(index * 2 + 1, left, mid, x, y, value);
    if (y > mid) intervalUpdate(index * 2 + 2, mid + 1, right, x, y, value);
    pushUp(index);
}

2.5 基于区间修改的查询

因为存在延迟标记,所以基于区间修改的查询有所不同。它遵循以下规则:

  • 当我们查询的区间完全覆盖节点区间时,直接返回区间值即可
  • 部分覆盖时,需要先下传延迟标记,再进行查询

接下来,我们看看它的代码实现~

public int query(int x, int y) {
        return query(0, 0, nums.length - 1, x, y);
}
private int query(int index, int left, int right, int x, int y) { // x 表示要查询的左区间,y 表示要查询的右区间
    if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0
    if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
    pushDown(index,left,right); //下传延迟标记
    int mid = (left + right) / 2;
    int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
    int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
    return leftQuery + rightQuery; // 求和
}

与 2.3 小节中的查询相比,我们可是发现它仅仅在完全覆盖这一步之后增加了一个下传延迟标记的操纵~

3. 完整代码

最后附上完整代码和测试数据:

import java.util.List;

public class Solution {
    int nums[] = new int[]{1, 2, 3, 4, 5, 6};
    int n = nums.length;
    int[] segTree = new int[4 * n]; // 为线段树分配空间
    int lazyTag[] = new int[4 * n]; // 为延迟标记分配空间

    void buildTree(int index, int left, int right) { // index 表示下标,left 表示左区间,right 表示右区间
        if (left == right) {
            segTree[index] = nums[left];
            return; // 到叶子节点就不能继续划分啦~
        }
        int mid = (left + right) / 2; // 一分为 2,例如将 [1,6] 划分为 [1,3] 和 [4,6]
        buildTree(2 * index + 1, left, mid); // 构建左子树,左孩子的下标为 2 * index + 1
        buildTree(2 * index + 2, mid + 1, right); // 构建右子树,右孩子的下标为 2 * index + 2
        segTree[index] = segTree[2 * index + 1] + segTree[2 * index + 2]; // 这里是求和,所以非叶子节点存储的值是左右孩子节点存储的值之和
    }

    void pushUp(int index) {
        segTree[index] = segTree[index * 2 + 1] + segTree[index * 2 + 2]; // 向上更新,用孩子节点更新父节点
    }

    void pushDown(int index, int left, int right) { // 向下传递延迟标记
        if (lazyTag[index] != 0) {
            int mid = (left + right) / 2;
            lazyTag[index * 2 + 1] += lazyTag[index]; //更新左孩子的延迟标记
            lazyTag[index * 2 + 2] += lazyTag[index];//更新右孩子的延迟标记
            segTree[index * 2 + 1] += lazyTag[index] * (mid - left + 1); // 区间值 = sum + 更新值 *(区间长度)
            segTree[index * 2 + 2] += lazyTag[index] * (right - mid);
            lazyTag[index] = 0; // 清除延迟标记
        }
    }

    public void intervalUpdate(int x, int y, int value) {
        intervalUpdate(0, 0, nums.length - 1, x, y, value);
    }

    private void intervalUpdate(int index, int left, int right, int x, int y, int value) {
        if (x <= left && y >= right) { // 完全覆盖
            segTree[index] += value * (right - left + 1); // 更新区间值
            lazyTag[index] += value; // 更新延迟标记
            return;
        }
        pushDown(index, left, right); // 部分覆盖,下传延迟标记
        int mid = (left + right) / 2;
        if (x <= mid) intervalUpdate(index * 2 + 1, left, mid, x, y, value);
        if (y > mid) intervalUpdate(index * 2 + 2, mid + 1, right, x, y, value);
        pushUp(index);
    }

    // 区间查询
    public int query(int x, int y) {
        return query(0, 0, nums.length - 1, x, y);
    }

    private int query(int index, int left, int right, int x, int y) { // x 表示要查询的左区间,y 表示要查询的右区间
        if (x > right || y < left) return 0; // 如果查询区间在线段树区间外返回 0
        if (x <= left && y >= right) return segTree[index];  // 当查询区间包含线段树区间,返回节点值
        pushDown(index, left, right); //下传延迟标记
        int mid = (left + right) / 2;
        int leftQuery = query(2 * index + 1, left, mid, x, y); // 计算左孩子
        int rightQuery = query(2 * index + 2, mid + 1, right, x, y); // 计算右孩子
        return leftQuery + rightQuery; // 求和
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        solution.buildTree(0, 0, solution.nums.length - 1);
        solution.intervalUpdate(0,3,1);
        solution.intervalUpdate(1,2,1);
        System.out.println(solution.query(0, 2));
    }
}

能看到最后,你真的很棒,加油~


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

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

相关文章

YOLOv5改进系列(16)——添加EMA注意力机制(ICASSP2023|实测涨点)

【YOLOv5改进系列】前期回顾: YOLOv5改进系列(0)——重要性能指标与训练结果评价及分析 YOLOv5改进系列(1)——添加SE注意力机制 YOLOv5改进系列(2)——添加

android stduio 打开工程后直接报Connection refused解决

报错如下:Connection refused 解决方案: 打开gradle-wrapper.properties修改distributionUrl 将: distributionUrlhttp\://localhost/gradle/distributions/gradle-6.5-bin.zip 替换为: distributionUrlhttps\://services.gradle.org/distributions/gradle-6.5-bin.zip 错…

<C语言> 文件操作

1 文件指针 缓冲文件系统中&#xff0c;关键的概念是“文件类型指针”&#xff0c;简称“文件指针”。 每个被使用的文件都在内存中开辟了一个相应的文件信息区&#xff0c;用来存放文件的相关信息&#xff08;如文件的名 字&#xff0c;文件状态及文件当前的位置等&#xff…

windows系统之WSL 安装 Ubuntu

WSL windows10 以上才有这个wsl功能 WSL&#xff1a; windows Subsystem for Linux 是应用于Windows系统之上的Linux子系统 作用很简单&#xff0c;可以在Windows系统中获取Linux系统环境&#xff0c;并完全直连计算机硬件&#xff0c;无需要通过虚拟机虚拟硬件 Windows10的W…

MD-MTSP:斑马优化算法ZOA求解多仓库多旅行商问题MATLAB(可更改数据集,旅行商的数量和起点)

一、斑马优化算法ZOA 斑马优化算法&#xff08;Zebra Optimization Algorithm&#xff0c;ZOA&#xff09;Eva Trojovsk等人于2022年提出&#xff0c;其模拟斑马的觅食和对捕食者攻击的防御行为。斑马优化算法&#xff08;Zebra Optimization Algorithm&#xff0c;ZOA&#x…

海外ASO优化之应用商店本地化

大多数应用可供世界任何地方的用户使用&#xff0c;所以需要以多种不同语言来展示我们的应用。它能够包含在跨地理区域的搜索结果中&#xff0c;从而提高全球可见性和转化率。 1、关键词的研究&#xff0c;对于确定流行的本地关键词至关重要。 在本地化Google Play的应用页面时…

RTPS规范v2.5(中文版)

实时发布订阅协议 DDS互操作性有线协议 &#xff08;DDSI-RTPS&#xff09; 技术规范 V2.5 &#xff08;2022-04-01正式发布&#xff09; https://www.omg.org/spec/DDSI-RTPS/2.5/PDF   目 录 1 范围 8 2 一致性 8 3 规范性参考文献 8 4 术语和定义 9 5 标识 …

HTML5前端开发工程师的岗位职责说明(合集)

HTML5前端开发工程师的岗位职责说明1 职责 1、根据产品设计文档和视觉文件&#xff0c;利用HTML5相关技术开发移动平台的web前端页面; 2、基于HTML5.0标准进行页面制作&#xff0c;编写可复用的用户界面组件; 3、持续的优化前端体验和页面响应速度&#xff0c;并保证兼容性和…

Godot 4 插件 - Utility AI 研究

今天看到一个视频教学 Godot4 | 实现简单AI | Utility AI 插件_哔哩哔哩_bilibili 就看了一下。吸引我的不是插件&#xff0c;是AI这两个字母。这AI与Godot怎么结合&#xff1f;感觉还是离线使用&#xff0c;值得一看。 视频时间不长&#xff0c;15分钟左右&#xff0c;看得…

无涯教程-jQuery - Highlight方法函数

Highlight 效果可以与effect()方法一起使用。这将以特定的颜色突出显示元素的背景&#xff0c;默认为黄色(yellow)。 Highlight - 语法 selector.effect( "highlight", {arguments}, speed ); 这是所有参数的描述- color - 高亮显示颜色。默认值为"#fff…

比memcpy还要快的内存拷贝,了解一下

前言 朋友们有想过居然还有比memcpy更快的内存拷贝吗&#xff1f; 讲道理&#xff0c;在这之前我没想到过&#xff0c;我也一直觉得memcpy就是最快的内存拷贝方法了。 也不知道老板最近是咋了&#xff0c;天天开会都强调&#xff1a;“我们最近的目标就一个字&#xff0c;性能优…

CompletableFuture生产中使用问题

CompletableFuture生产中使用问题 1 背景2 测试3 原因4. 总结 1 背景 接到一个任务,需要优化下单接口,查看完业务逻辑后发现有一些可以并行或异步查询的地方,于是采用CompletableFuture来做异步优化,提高接口响应速度,伪代码如下 //查询用户信息CompletableFuture<JSONObj…

认识 springboot 并了解它的创建过程 - 1

前言 本篇介绍什么是SpringBoot, SpringBoot项目如何创建&#xff0c;认识创建SpringBoot项目的目录&#xff0c;了解SpringBoo特点如有错误&#xff0c;请在评论区指正&#xff0c;让我们一起交流&#xff0c;共同进步&#xff01; 文章目录 前言1.什么是springboot?2.为什么…

投影仪离线语音识别芯片ic方案,高识别率识别IC,WTK6900H-B-24SS

随着智能科技的不断演进&#xff0c;人工智能已经深入到我们的生活中的方方面面。投影仪作为现代影音娱乐与商务展示的得力工具&#xff0c;为了进一步提升用户与产品的交互体验&#xff0c;深圳唯创知音最新推出WTK6900H-B-24SS离线语音识别芯片IC方案。这项创新技术使得投影仪…

入侵检测——恶意软件、病毒、防病毒、反病毒技术

目录 1. 什么是恶意软件&#xff1f; 2. 恶意软件有哪些特征&#xff1f; 3. 恶意软件的可分为那几类&#xff1f; 3.1.1按照传播方式分类&#xff1a; 3.1.2按照功能分类&#xff1a; 4. 恶意软件的免杀技术有哪些&#xff1f; 5. 反病毒技术有哪些&#xff1f; 5…

左神算法之中级提升(6)

目录 【案例1】 【题目描述】 【思路解析】 【代码实现】 【案例2】 【题目描述】 【思路解析】 【代码实现】 【案例3】 【题目描述】 【思路解析】 【代码实现】 【案例4】 【题目描述】 2018年美团面试题 【思路解析】 【代码实现】 【案例5】 【题目描述】 …

Service Mesh之Istio部署bookinfo

给istio部署插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 rootk8s-master01:/usr/local# cd istio rootk8s-master01:/usr/local/istio# ls samples/addons/ extras grafana.yaml jaeger.yaml kiali.yaml prometheus.yaml RE…

AMD技术泄露?CPU加密技术面临新的危机

许多AMD CPU中的破解加密、泄露密码的漏洞可能需要几个月才能修复。 “Zenbleed”漏洞影响了所有基于Zen 2架构的Ryzen、Threadripper和EPYC处理器。 技术泄露情况 谷歌零项目安全团队成员Tavis Ormandy称&#xff0c;许多AMD新一代消费级、工作站和服务器处理器最近被曝出漏…

招商银行秋招攻略和考试内容详解

招商银行秋招简介 招商银行是一家股份制商业银行&#xff0c;银行的服务理念已经深入人心&#xff0c;在社会竞争愈来愈烈的今天&#xff0c;招商银行的招牌无疑是个香饽饽&#xff0c;很多人也慕名而至&#xff0c;纷纷向招商银行投出了简历。那么秋招银行的秋招开始时间是多…

感受野(Receptive Field)的理解与计算

一、 定义 在卷积神经网络中&#xff0c;感受野&#xff08;Receptive Field&#xff09;是指特征图上的某个点能看到的输入图像的区域&#xff0c;即特征图上的点是由输入图像中感受野大小区域的计算得到的。 神经元感受野的值越大表示其能接触到的原始图像范围就越大&#…