[leetcode刷题笔记]线段树Segment Tree

在做两道区间检索题中接触到了线段树的概念,本文将其进行整理,文末几篇参考对线段树做了系统的介绍,结合两道leetcode题,对线段树的创建、查找、更新有了更深地掌握。文中不足,还望多多指正。

区域和检索 - 数组不可变

给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

线段树(Segment Tree)也是一棵树,只不过元素的值代表一个区间。常用区间的统计操作,比如一个区间的最大值(max),最小值(min),和(sum)等等,线段树是一个平衡二叉树,但不一定是完全二叉树。

在本题中,数组nums=[-2, 0, 3, -5, 2, -1]构建一棵线段树如下,节点为区间和(sum)

根节点就是 0~lenght-1 的和,根节点的左右子树平分根节点的区间,然后依次类推,直到只有一个元素不能划分为止,该元素也就是二叉树的叶子节点。

求线段树的区间统计,时间复杂度和二叉树的高度有关系,和元素的个数没关系,它的时间复杂度为 O(log n)。

如果我们用数组来存储线段树的话,我们大致需要开辟多大的数组空间呢?
h层的满二叉树总共有 2^h-1 个节点,第h-1层有2^(h-1)个节点,它们大概是两倍的关系。也就是说对于满二叉树 最后一层的节点数乘以2 大致就是整棵树的节点数。但是线段树并不一定是满二叉树,但是一定是平衡二叉树,所以需要多冗余一层。也就是 乘以4 就足以盛放所有的节点数,但是会浪费一定的内存空间。

class SegmentTree:
    def __init__(self, data:List[int]): 
        '''
        data:传入的数组
        merge:处理的业务逻辑,例如求和/最大值/最小值,lambda表达式
        '''
        self.data = data
        self.n = len(data)
        #  申请4倍data长度的空间来存线段树节点
        self.tree = [None] * (4 * self.n) # 索引i的左孩子索引为2i+1,右孩子为2i+2
        if self.n:
            self.build(0, 0, self.n-1)

    def querySum(self, ql:int, qr:int) -> int:
        '''
        返回区间[ql,..,qr]的值
        '''
        return self.query(0, 0, self.n-1, ql, qr)

    def build(self, tree_index:int, l:int, r:int):
        '''
        递归创建线段树
        tree_index : 线段树节点在数组中位置
        l, r : 该节点表示的区间的左,右边界
        '''
        if l == r:
            self.tree[tree_index] = self.data[l]
            return
        mid = (l+r) // 2 # 区间中点,对应左孩子区间结束,右孩子区间开头
        left, right = 2 * tree_index + 1, 2 * tree_index + 2 # tree_index的左右子树索引
        self.build(left, l, mid)
        self.build(right, mid+1, r)
        self.tree[tree_index] = self.tree[left] + self.tree[right]

查找查找分为四种情况

  • 查询的区间在刚好就是当前节点表示区间
    查询结果即为当前节点值

  • 查询区间全在左子树

  • 查询区间全在右子树

  • 查询区间部分在左子树,部分在右子树
    需到左右子树分别查询,并将查询结果求和(本题构造求和的线段树)

     def query(self, tree_index:int, l:int, r:int, ql:int, qr:int) -> int:
         '''
         递归查询区间[ql,..,qr]的值
         tree_index : 某个根节点的索引
         l, r : 该节点表示的区间的左右边界
         ql, qr: 待查询区间的左右边界
         '''
         if l == ql and r == qr:
             return self.tree[tree_index]
    
         # 区间中点,对应左孩子区间结束,右孩子区间开头
         mid = (l+r) // 2 
         left, right = tree_index * 2 + 1, tree_index * 2 + 2
         if qr <= mid:
             # 查询区间全在左子树
             return self.query(left, l, mid, ql, qr)
         elif ql > mid:
             # 查询区间全在右子树
             return self.query(right, mid+1, r, ql, qr)
    
         # 查询区间一部分在左子树一部分在右子树
         return self.query(left, l, mid, ql, mid) + self.query(right, mid+1, r, mid+1, qr)
    

构建好线段树后,本题操作

class NumArray:

    def __init__(self, nums: List[int]):
        self.segment_tree = SegmentTree(nums)
    

    def sumRange(self, i: int, j: int) -> int:
        return self.segment_tree. querySum(i, j)

区域和检索 - 数组可修改

给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

update(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。

示例:

Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8

说明:

数组仅可以在 update 函数下进行修改。
你可以假设 update 函数与 sumRange 函数的调用次数是均匀分布的。

相比上一题增加了update操作,其更新操作需找到相应的叶子结点,将其值更新,然后将包括其大区间的值更新。

def update(self, tree_index, l, r, index):
    '''
    tree_index:某个根节点索引
    l, r : 此根节点代表区间的左右边界
    index : 更新的值的索引
    '''
    if l == r==index :
        self.tree[tree_index] = self.data[index]
        return
    mid = (l+r)//2
    left, right = 2 * tree_index + 1, 2 * tree_index + 2
    if index > mid:
        # 要更新的区间在右子树
        self.update(right, mid+1, r, index)
    else:
        # 要更新的区间在左子树index<=mid
        self.update(left, l, mid, index)

    self.tree[tree_index] = self.tree[left] + self.tree[right]

def updateSum(self,index:int,value:int):
    self.data[index] = value
    self.update(0, 0, self.n-1, index)

Reference

1.数据结构与算法(十)线段树(Segment Tree)入门
2.线段树SegmentTree-数组实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343