7.12.1 线段树原理及应用(上)

继续在树这一类问题上拓展,线段树也是高级的数据结构,初学者要跳过,深入学习阶段可以适当了解一下,拓宽思维能力。如果要参加竞赛或者其他对数据结构要求比较高的情形,可以仔细研究一番,本文借其他博客和几道力扣题目介绍一下线段树。


第一篇文章我们先从以下两个问题展开:

1. 线段树是什么?是什么样的结构?这样的结构可以解决什么样的问题?

2. 线段树的基本实现及区间查询问题——LeetCode307题

 

概述

线段树(Segment Tree)是一种二叉树形数据结构,1977 年由 Jon Louis Bentley 发明,用以存储区间或线段,并且允许快速查询结构内包含某一点的所有区间。

我们引出几篇参考博客,本文都有所引用。

线段树从零开始

线段树 从入门到进阶

线段树详解 (原理,实现与应用)  (该文比较深入,重实现,浅学的不要看)

Python数据结构与算法(十四、线段树)(以python的实现为主)

 

线段树所记录的是数组(一般是数组)区间内的信息,比如区间内的元素求和、连乘之类的。下图中根节点记录整棵树的索引范围,依次向下,直到叶子节点只有一个元素。画出来就是下面这个样子的。其中叶子A[0]代表所给数组对应索引的值,所有父节点则汇总其子节点的信息(比如是子节点元素之和,根据你不同的目的而不同)。

在这里插入图片描述

这样的数据结构用于解决一类典型的问题,当我们的各种操作,对象是区间(或线段)时,比如修改一个区间的值,比如查找一个区间的值的和、最大值等。下面我们举几个栗子,介绍典型的线段树应用场景。

题目一:
10000个正整数,编号1到10000,用A[1],A[2],A[10000]表示。
修改:无
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

传统方法,遍历求和L,L+1,...,R;

如果应用线段树,如上图,求2-6的和,我们只需把A[2-4]+A[5-6]两个父节点的值加起来即可,因此将操作的时间复杂度降到O(logN).

即任意给定区间[L,R],线段树在上述子区间中选择约2*log2(R-L+1)个拼成区间[L,R],从而得到相应结果,查询效率加快。

题目二:
10000个正整数,编号从1到10000,用A[1],A[2],A[10000]表示。
修改:1.将第X个数增加C (1 <= X <= 10000)2. 将M到N的数都增加C,其中1<= M <= N <= 10000.
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

传统方法,修改比较快,但是求和仍然效率不高;

应用线段树的话,这是典型的线段树点修改、区间修改问题。以点修改为例,修改A[2],那么我们依次修改所有包含A[2]的节点,即A[0-9],A[0-4],A[2-4],可以把修改的效果减小到O(logN)。

 

不知诸位是否对线段树有了初步认识,认识什么是区间操作,认识到线段树在区间操作上的效率优势。下面我们看代码实现和应用。

 

实现

 如何用代码实现线段树?考虑到作为树的基本特征,我相信大家都有初步的实现方案。

看上图可知,线段树并不是完全二叉树,但是也是平衡的,仍然可以使用完全二叉树的数组存储方法,并不会浪费太多空间。

当然可以像一般的树那样写成用指针存储下一级。但是用数组来实现树形结构,可以大大简化代码,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。

想必都还记得数组存储时,父子节点之间的关系吧。

足够的空间?博客里都有介绍:

简单的记法: 足够的空间 = 数组大小n的四倍。
实际上足够的空间 =  (n向上扩充到最近的2的某个次方)的两倍。
举例子:假设数组长度为5,就需要5先扩充成8,8*2=16.线段树需要16个元素。如果数组元素为8,那么也需要16个元素。
所以线段树需要的空间是n的两倍到四倍之间的某个数,一般就开4*n的空间就好,如果空间不够,可以自己算好最大值来省点空间。
————————————————
版权声明:本文为CSDN博主「岩之痕」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zearot/article/details/52280189

下面我以一道例题来看一下线段树代码实现和应用。

307. Range Sum Query - Mutable

Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.

The update(i, val) function modifies nums by updating the element at index i to val.

Example:

Given nums = [1, 3, 5]

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

题目解析:

这是线段树最基础的应用——区间求和问题,涉及到点修改和区间查询。下面我们直接看代码。

class NumArray:

    def __init__(self, nums: List[int]):
        self._data = nums[:]
        self._tree = [None] * 4 * len(self._data)
        if not nums:
            return
        self._build(0, 0, len(self._data)-1)
    
    def _build(self, ix, left, right):
        if left == right:
            self._tree[ix] = self._data[left]
            return
        mid = (left + right) // 2
        self._build(2*ix+1, left, mid)
        self._build(2*ix+2, mid+1, right)
        self._tree[ix] = self._tree[2*ix+1] + self._tree[2*ix+2]    

    def update(self, i: int, val: int) -> None:
        if i < 0 or i >= len(self._data):
            return
        self._data[i] = val
        self._set(0, 0, len(self._data) - 1, i, val)       
    
    def _set(self, treeIndex, left, right, index, val):
        """
        Description: 在以索引treeIndex为根节点的线段树中将索引为index的位置的元素设为e(此时treeIndex索引处所代表的区间范围为:[left, right]
        """
        if left == right:
            self._tree[treeIndex] = val
            return
        mid = (left + right) // 2
        if index <= mid:    # 左子树去修改
            self._set(2*treeIndex+1, left, mid, index, val)
        else:
            self._set(2*treeIndex+2, mid+1, right, index, val)
        self._tree[treeIndex] = self._tree[2*treeIndex+1] + self._tree[2*treeIndex+2]

    def sumRange(self, i: int, j: int) -> int:
        return self._query(0, 0, len(self._data)-1, i, j)

    def _query(self, treeIndex, left, right, quaryL, quaryR):
        """
        Description: 在根节点索引为treeindex的线段树上查找索引范围为[quaryL, quaryR]上的值,其中left, right值代表该节点所表示的索引范围(左闭右闭)
        """
        if left == quaryL and right == quaryR:      # 递归到底的情况,区间都对上了,直接返回当前treeIndex索引处的值就好
            return self._tree[treeIndex]            # 返回当前树上索引为treeIndex的元素值

        mid = (right + left) // 2            # 获取TreeIndex索引处所代表的范围的中点
        leftChild_index = 2*treeIndex+1    # 获取左孩子的索引,注意上面几处也有用到,只是没有单独列为变量
        rightChild_index = 2*treeIndex+2  # 获取右孩子的索引

        if quaryL > mid:        # 此时要查询的区间完全位于当前treeIndex所带表的区间的右侧
            return self._query(rightChild_index, mid + 1, right, quaryL, quaryR)    # 直接去右子树找
        elif quaryR <= mid:     # 此时要查询的区间完全位于当前treIndex所代表的区间的左侧
            return self._query(leftChild_index, left, mid, quaryL, quaryR)      # 直接去左子树找

        # 此时一部分在[left, mid]上,一部分在[mid + 1, right]上
        leftResult = self._query(leftChild_index, left, mid, quaryL, mid)   # 在左子树找区间
        rightResult = self._query(rightChild_index, mid + 1, right, mid + 1, quaryR)   # 在右子树找区间
        return leftResult + rightResult      # 最后在回归的过程中两个子节点进行和操作并返回,得到[quaryL, quaryR]区间上的值

简单来说,self._data是数组本身,在建树和修改时要用到,一些查询操作用不上这玩意;self._tree就是用数组存储的线段树的表示,如上所述,叶子结点的值和self._data对应的,其他的结点的值根据问题本身定义,该题中就是子结点值求和。

其中建树,修改和查询操作都是递归实现的,因此分别实现了递归用函数,希望结合代码和注释,了解线段树的基本实现,这里就不多解释了。

了解一下线段树的话介绍到这儿可以了,更深入的应用看后面,还要多刷几道题才可。

你可能感兴趣的:(python学习,python数据结构与算法,线段树,数据结构,区间查询)