大家好,欢迎阅读周三算法数据结构专题,今天我们来聊聊一个新的数据结构,叫做线段树。

线段树这个数据结构很多人可能会有点蒙,觉得没有听说过,但是它非常非常有名,尤其是在竞赛圈,可以说是竞赛圈的必备技能。所以如果以后遇到有人看了一点算法导论就在你面前装逼,你就可以问他:请问线段树更新的复杂度是多少?

不过如果你会线段树,你也要小心一点,最好不要在面试的时候随便透露你会这个算法。否则面试官一下子就会知道你是圈里人,然后你会发现你后面的面试问题比之前好像难不少。当然也有可能遇到面试官自己不会,为了防止尴尬强行让你用非线段树的解法来完成,比如我就遇到过……

例题

说了这么多废话,那么线段树究竟是什么呢?线段树的英文是segment tree,其实也算是一个直译。因为这个数据结构和线段没有特别大的关系,我个人感觉翻译成区间树可能更贴近一点。

我们先理解到这里,就是这个数据结构大概和区间有点关系。我们先放一放,先来看一道例题,来实际体会一下,为什么需要线段树这个数据结构,以及它的使用场景究竟是什么。这样我们可以对它有一个更加直观的感受,这道题很简单也很经典,我就是在这道题遇到了面试官不让用线段树的突然袭击。

这道题的题面是这样,给定一个长度为n的数组。这个数组当中有n个整数,然后我们会有两种操作。一种操作叫更新,我们指定更新某一个位置的某个数,第二个操作叫query,给定一个区间,要求这个区间里面元素的最小值。n的范围呢是,操作的数量也是,请问我们应该怎么实现?

线段树概念

当然你可能已经知道要用线段树了,只是不知道线段树是什么以及怎么使用。我们先把这些疑惑放在一边,就单纯简单地用最朴素的方法来思考的话,我们会发现我们每次查询都是的操作。最坏的情况下,我们就是要求整个数组的最小值,那么我们需要依次遍历整个区间来求。那么复杂度再乘上操作的数量,整个程序的复杂度会达到。显然这是一个非常巨大的数字,在算法竞赛场景当中一定会超时。

也就是说简单粗暴是做不出来的,如果你有足够多的做题经验,你就会很自然地想到我们也许需要使用一些数据结构来优化这个查询的复杂度。肯定是不能接受的,即使不能优化到,也至少可以试试。线段树就是这样的数据结构,我们直接来看一张图,我们直接就可以搞明白线段树究竟是干嘛的,以及它的工作原理。

这张图当中的a就是我们存数据的数组,这个数组上面的就是线段树。我们从上往下看,给大家解释一下。最上面一条只有一个数字就是1,它代表的是整个数组的最小值是1。也就是说最上层维护的是整个区间的最小值。然后是第二层,在第二层我们看到了两个数,分别是3和1。很明显,3表示的是左半边区间的最小值,1表示的右半边区间的最小值。

到了第三行我们得到了4个数,同理,再下一层有8个数。很明显这是一颗二叉树,并且二叉树当中的每一个节点维护了一个区间的值。它的叶子节点存储的是长度为1的区间,也就是单个元素。我们把两个兄弟节点维护的区间合并起来就得到了父节点的区间。在这道题当中,由于我们维护的是区间的最小值,所以我们可以得到这么一个式子:

node.min = min(node.left.min, node.right.min)

所以线段树就是利用了二叉树这个层次结构对一个区间进行维护的数据结构。

线段树查询

我们已经了解了线段树的结构了,剩下的就只有两个问题,一个是如何更新一个是如何求解。我发先来看求解,我们要求一个区间的最小值。我们来实际看一下,假设我们想要查询下标是[2, 5]这个区间里的最小值怎么办?

我们对照一下上面的数组a,下标[3, 6]这个区间对应的是[7, 9, 6, 4]这四个值。我们会发现不存在刚好只包含这四个值的区间,那怎么办呢?其实很简单,可以拼凑。我们可以发现我们可以把这个完整的区间转化成两个区间连接在一起的结果。比如下图这样。

这样,我们就把原本比较[7, 9, 6, 4]四个值的一个查询行为转化成了只需要比较4和7两个值大小的比较行为了。这可以替我们节约大量的时间。这和记忆化搜索有一点点像,相当于我们制定一个模式,根据这个模式把区间里的最值存储下来。这样我们查询的时候可以利用这些值来快速求解。

如果我们要求[2, 7]区间内的最小值,那么我们可以转而用这两个区间的值求到。

线段树更新

接下来我们来看下线段树的更新,其实更新和查询的原理是一样的,同样是从根节点出发一层层往下,一直到更新到叶子节点为止。假如说我们把数据当中的4更新成0,那么会达成一种怎样的效果呢?

从结果上来看,我们是把发生变更的叶子节点到树根的这一整个链路都更新了。当然这个更新也不是强制发生的,因为如果我们更新的值比它的原值1要大的话,也是不会更新的。

代码实现

关于线段树的原理我们就差不多讲完了,看起来不太长,这是很正常的。因为线段树的原理其实很简单,就是用一棵二叉树来维护各个长度的区间。我们在查询的时候就是要找到可以拼成我们查询的区间的几个子区间,用这些子区间的值来求到我们要查的区间的值。在我们更新的时候,不需要更新整棵树,只需要更新某一条从根节点到叶子节点的路径就可以了。

原理看起来不难,理解起来也不难,但是要用代码实现出来其实不太容易。因为线段树的所有操作都是基于递归和回溯的,所以想要顺利、深入地理解线段树,对于递归以及回溯的掌握一定要过关。否则线段树你写起来很痛苦,写完了调试会更痛苦。

我们会用面向对象的形式来创建一个线段树,当然也有人喜欢用数组来模拟,这也是可以的,本质上都是一样的。首先我们来创建一个节点类。这个节点类存储的值有3个,一个是它维护的区间的值,在这个题目里维护的是区间最小值。一个是区间的范围, 左右边界。另外一个是左右孩子节点。

由于我们在创建节点的时候还不知道它的左右孩子以及维护的值是什么,所以我们先赋值成None。

class Node:
def __init__(self, left_side, right_side):
self.val = None
self.ls, self.rs = left_side, right_side
self.left_child, self.right_child = None, None

Node类有了之后,我们就可以利用它来建树了。我们首先来看看建树的方法,也就是常说的build方法。我们创建线段树的时候最重要的就是让它当中的每一个节点能够存储对应区间的最小值。但是呢由于线段树是有层次结构的,我们在创建区间[a, b]的时候,其实可以利用区间[a, m]和区间[m+1, b]两个区间的最小值来获取整个区间的最小值。也就是说我们可以利用当前节点的左右孩子节点完成,我们之前已经说过这点了。

我们来看代码,通过递归可以很方便地完成这一点。

class SegmentTree:
def __init__(self, arr):
self.n = len(arr)
self.vals = arr[:]
self.root = self.build(0, self.n) def build(self, l, r):
# 传入的l和r表示区间范围,左闭右开
if r - l < 1:
return None
node = Node(l, r)
# 如果区间长度是1,说明是叶子节点了,直接将val赋值成对应的数值
if r - l == 1:
node.val = self.vals[l]
else:
# 否则递归调用
m = (l + r) >> 1
node.left_child = self.build(l, m)
node.right_child = self.build(m, r)
node.val = min(node.left_child.val, node.right_child.val)
return node

当然这个过程也可以用循环实现,只不过用递归实现更加简单。

如果你能看得到build方法,那么update和query对你来说也都不是问题,其实原理都是一样的,只不过一个是通过递归的形式去更新一个是递归去查询而已。我们先来看update:

    def update(self, k, v):
self._update(self.root, k, v) def _update(self, u, k, v):
if u is None:
return
# 如果k在u这个节点维护的区间里
if u.ls <= k < u.rs:
# 更新它的最小值
u.val = min(u.val, v)
m = (u.ls + u.rs) >> 1
# 判断往左还是往右
if k < m:
self._update(u.left_child, k, v)
else:
self._update(u.right_child, k, v)

最后我们再来看query,query同样是通过递归执行的。由于我们查询的是一个区间,所以我们需要判断我们查询区间和节点维护区间之间的关系。只要抓住了这一点,整个逻辑也是很简单的。

    def query(self, l, r):
return self._query(self.root, l, r) def _query(self, u, l, r):
# l和r是查询区间
# 如果查询区间是u节点区间的超集
if l <= u.ls and r >= u.rs:
return u.val
# 如果查询区间只和u节点区间的左半部分有交集
elif r <= u.left_child.rs:
return self._query(u.left_child, l, r)
# 如果查询区间只和u节点右半部分有交集
elif l >= u.right_child.ls:
return self._query(u.right_child, l, r)
# 如果都有交集
return min(self._query(u.left_child, l, r), self._query(u.right_child, l, r))

最后

到这里,我们关于线段树的基本介绍就算是结束了。注意我说的是基本介绍,因为线段树有很多种用法,今天介绍的只是其中最简单的一种:单点更新区间查询。除此之外还有区间更新单点查询,区间更新区间查询,扫描线等等相对高端一些的用法。由于篇幅所限不能一次讲完,准备放在之后的文章当中分享给大家。

另外一点市面上线段树的题目基本上都是用C++写的,所以如果你想要找一道题试一下的话,可能需要用C++重新写一遍。不过我相信这对于你们来说并不是什么大问题。

今天的文章到这里就结束了,如果喜欢本文的话,请给我一波三连支持吧(关注、转发、点赞)。

原文链接,求个关注

本文使用 mdnice 排版

- END -

{{uploading-image-370795.png(uploading...)}}

ACMer不得不会的线段树,究竟是种怎样的数据结构?的更多相关文章

  1. poj-2777(区间线段树,求种类数模板)

    题目链接:http://poj.org/problem?id=2777 参考文章:https://blog.csdn.net/heucodesong/article/details/81038360 ...

  2. 【数据结构模版】可持久化线段树 && 主席树

    浙江集训Day4,从早8:00懵B到晚21:00,只搞懂了可持久化线段树以及主席树的板子.今天只能记个大概,以后详细完善讲解. 可持久化线段树指的是一种基于线段树的可回溯历史状态的数据结构.我们想要保 ...

  3. 线段树 - HDU1166 - 敌兵布阵

    2017-07-29 16:41:00 writer:pprp 线段树跟区间操作相关,想要在题目限定的时间内解决问题就需要用线段树这种数据结构来解决: 线段树是一种二叉平衡树 参考书目:张新华的< ...

  4. HDU 1166 敌兵布阵 【线段树-点修改--计算区间和】

    敌兵布阵 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submis ...

  5. A线段树

    线段树专题 顾琪坤 1.简介: 打acm的时候,经常会碰到一类问题,比方给你n个数的序列,然后动态的更改某些数的值,然后又动态地询问某个区间的值的和或者其它乱七八糟的东西,对于单个更改或者询问,也许很 ...

  6. 【线段树I:母题】hdu 1166 敌兵布阵

    [线段树I:母题]hdu 1166 敌兵布阵 题目链接:hdu 1166 敌兵布阵 题目大意 C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又開始忙乎了.A国在海 ...

  7. HDU 2795 Billboard 线段树活用

    题目大意:在h*w 高乘宽这样大小的 board上要贴广告,每个广告的高均为1,wi值就是数据另给,每组数组给了一个board和多个广告,要你求出,每个广告应该贴在board的哪一行,如果实在贴不上, ...

  8. POJ 3667 Hotel(线段树 区间合并)

    Hotel 转载自:http://www.cnblogs.com/scau20110726/archive/2013/05/07/3065418.html [题目链接]Hotel [题目类型]线段树 ...

  9. 线段树初步&&lazy标记

    线段树 一.概述: 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a, ...

随机推荐

  1. python爬虫数据提取之bs4的使用方法

    Beautiful Soup的使用 1.下载 pip install bs4 pip install lxml # 解析器 官方推荐 2.引用方法 from bs4 import BeautifulS ...

  2. RPC之总体架构

    要完成一个高可用.高性能的RPC框架,需要对其架构的设计进行梳理,这里参考xxl-rpc框架,对整个项目进行梳理. 以上就是项目的整个构架,分为四个部分: 第一个是服务发布与引入,基于JDK动态代理以 ...

  3. Oracle WITH 语句 语法

    With语句可以在查询中做成一个临时表/View,用意是在接下来的SQL中重用,而不需再写一遍. With Clause方法的优点: 增加了SQL的易读性,如果构造了多个子查询,结构会更清晰. 示例: ...

  4. 现象:当指定logback的FileNamePattern为日期2020-01-15后,如果有线程不断的往里写log,过了零点文件不会变成下一日2020-01-16,还是会在2020-01-15里继续写 结论:写log的线程不停,文件不会按日子更换。

    logback版本:1.1.11 这个是我实验验证的,昨天我配置了一个logback,然后用两个线程不断往里写log,结果发现到了今天2020-01-16日,log文件还是昨天的logbackCfg. ...

  5. docker基本操作及介绍

    Docker 简介 Docker 是一个开源项目,诞生于 2013 年初,最初是 dotCloud 公司内部的一个业余项目.它基于 Google 公司推出的 Go 语言实现.项目后来加入了 Linux ...

  6. postgres 无法删除表

    起因 在postgress下删除表的时候报错 解决 简单的百度了一下,有些人说是用户权限的问题,需要切换到库的拥有者下删除,但是切换后还是没有解决··· 最后换了一种方式搜索,不直接搜索报错命令,直接 ...

  7. 一些免费的API

    Github 接口 Github 为我们提供了一些免费的 API 接口,利用这些接口我们可以开发一些工具. 接口文档地址为 https://docs.github.com/en/rest 下面是一个例 ...

  8. Python算法题:有100只大、中、小骆驼,100框土豆,一只大骆驼可以背3框,中骆驼可以背俩框,小骆驼两只背一筐,问大中小各有多少只骆驼?

    1 for x in range(0,100): 2 for y in range(0,100): 3 for z in range(0, 100): 4 if x*3+y*2+0.5*z == 10 ...

  9. Scrapy框架的架构原理解析

    爬虫框架--Scrapy 如果你对爬虫的基础知识有了一定了解的话,那么是时候该了解一下爬虫框架了.那么为什么要使用爬虫框架? 学习框架的根本是学习一种编程思想,而不应该仅仅局限于是如何使用它.从了解到 ...

  10. MYSQL中的where ‘1=1‘ 探讨

    在学习MySQL时候,关于MySQL注入的例子 首先针对以下代码,实现的是关于sql注入时,一个普通登录所产生的的问题 package com.java.lesson02; import com.ja ...