• 力扣LeetCode,区域和检索


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

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

    示例:

    1 Given nums = [1, 3, 5]
    2 
    3 sumRange(0, 2) -> 9
    4 update(1, 2)
    5 sumRange(0, 2) -> 8

    说明:

    a)、数组仅可以在 update 函数下进行修改。

    b)、你可以假设 update 函数与 sumRange 函数的调用次数是均匀分布的。


    2、如果使用线段树解决这个问题的话,需要先将融合器创建好,方便自己实现自己求和或者最大值或者最小值,等等需求。

     1 package com.tree;
     2 
     3 /**
     4  * 融合器,可以处理任意类型
     5  *
     6  * @param <E>
     7  */
     8 public interface Merger<E> {
     9 
    10     /**
    11      * 就是将两个元素a,b转换成一个元素E
    12      *
    13      * @param a
    14      * @param b
    15      * @return
    16      */
    17     E merger(E a, E b);
    18 }

    可以实现创建自己的线段树的功能。

      1 package com.tree;
      2 
      3 
      4 /**
      5  * 线段树
      6  */
      7 public class SegmentTree<E> {
      8 
      9     // 对于区间的每一个元素,有可能需要通过线段树来进行获取
     10     private E[] data;
     11 
     12     private E[] tree;// 这里面的变量tree,是用于将数组转换为二叉树的,看作是满二叉树.
     13 
     14     private Merger<E> merger;//融合器
     15 
     16 
     17     /**
     18      * 含参构造函数
     19      *
     20      * @param arr    要考察的整个区间的范围.
     21      * @param merger 融合器
     22      */
     23     public SegmentTree(E[] arr, Merger<E> merger) {
     24 
     25         this.merger = merger;
     26 
     27         // 通过创建Object的方式来创建E类型的数组
     28         data = (E[]) new Object[arr.length];
     29         // 循环遍历,将数组元素的值赋值给创建的数组
     30         for (int i = 0; i < arr.length; i++) {
     31             data[i] = arr[i];
     32         }
     33 
     34         // 此时,也要初始化一个线段树,数组长度是4倍的数组的大小,就可以存储所有的节点
     35         tree = (E[]) new Object[4 * arr.length];
     36 
     37         // 创建线段树,初始的时候根几点所对应的索引为0,
     38         // 初始的时候根节点即0索引位置的节点的左端点是0,右断点是尾部
     39         // 初始的时候根节点对应的区间就是我们data这个数组从头到尾,从最左端到最右端.
     40         buildSegmentTree(0, 0, arr.length - 1);
     41     }
     42 
     43     /**
     44      * 创建线段树,在treeIndex的位置创建表示区间[left...right]的线段树
     45      *
     46      * @param treeIndex 当前要创建的这个线段树的根节点所对应的索引
     47      * @param left      对于这个节点它所表示的那个线段或者区间的左右断点是什么,
     48      * @param right
     49      */
     50     private void buildSegmentTree(int treeIndex, int left, int right) {
     51         // 首先,考虑递归到底的情况
     52         // 如果区间长度为1的时候,只有一个元素,就是递归到底了
     53         if (left == right) {
     54             // 此时tree[treeIndex]线段树节点存储的就是元素的本身data[left]
     55             tree[treeIndex] = data[left];
     56             // tree[treeIndex] = data[right];
     57             return;
     58         }
     59 
     60         // 递归的第二部分
     61         // 如果此时left不等于right的时候,此时,left一定是小于right的,要表示的是一个区间的话
     62         // 首先要计算出表示一个区间的节点,这个节点一定会有左右孩子的.
     63         // 左孩子所对应的在线段树中的索引是leftTreeIndex
     64         int leftTreeIndex = this.leftChild(treeIndex);
     65         // 右孩子所对应的在线段树中的索引是rightTreeIndex
     66         int rightTreeIndex = this.rightChild(treeIndex);
     67 
     68         // 此时,要创建好这个节点的左右子树,对于创建这个节点的左右子树,我们已经知道了左右子树所对应的在数组中的
     69         // 那个索引,还需要知道对于这个左右子树来说,它相应的表示的区间范围,
     70 
     71         // 区间范围的计算
     72 //        int mid = (left + right) / 2;// 避免整形溢出
     73         int mid = left + (right - left) / 2;// 左边界,加上左右边界之间的距离除以2,得到的位置也是中间的位置.
     74         // 比如1-5, 1  2  3  4  5中间元素是3,那么就是1 + (5 - 1) /2 = 3.
     75 
     76         // 此时,有了中间位置之后,那么对于当前的treeIndex所在的这个节点,他表示从left到right这个区间
     77         // 它的左右子树表示的是什么区间呢,其实就是left 到 mid,mid + 1 到right.
     78         // 基于这两个区间再创建线段树,这是一个递归的过程
     79         // 创建左子树,从leftTreeIndex这个索引上创建从left 到 mid这个区间对应的线段树
     80         buildSegmentTree(leftTreeIndex, left, mid);
     81         // 创建右子树从rightTreeIndex这个索引上创建从mid + 1 到 right这个区间对应的线段树
     82         buildSegmentTree(rightTreeIndex, mid + 1, right);
     83 
     84         // 此时将treeIndex的左右子树创建好,最后在创建好两个子树之后
     85         // 此时考虑tree[treeIndex]的值应该是多少,是和业务有关系的,这里是求和
     86         // 这里的信息综合左右两个线段相应的信息来得到当前的这个更大的这个线段相应的信息.
     87         // 怎么综合是根据业务逻辑决定的.
     88         // 由于此时类型E不一定定义了加法,只能做加法还是减法还是求出最大值还是最小值等等.
     89         // 所以此时新增了融合器的功能,使用融合器进行操作
     90         tree[treeIndex] = merger.merger(tree[leftTreeIndex], tree[rightTreeIndex]);
     91         // 此时,递归创建线段树就完成了
     92     }
     93 
     94 
     95     /**
     96      * 获取指定索引的元素内容
     97      *
     98      * @param index
     99      * @return
    100      */
    101     public E get(int index) {
    102         if (index < 0 || index >= data.length) {
    103             throw new IllegalArgumentException("Index is illegal.");
    104         }
    105         return data[index];
    106     }
    107 
    108     /**
    109      * 关注的线段树区间一共有多少个元素
    110      *
    111      * @return
    112      */
    113     public int getSize() {
    114         return data.length;
    115     }
    116 
    117 
    118     /**
    119      * 返回完全二叉树的数组表示中,一个索引表示的元素的左孩子节点的索引.
    120      *
    121      * @param index
    122      * @return
    123      */
    124     private int leftChild(int index) {
    125         return 2 * index + 1;
    126     }
    127 
    128     /**
    129      * 返回完全二叉树的数组表示中,一个索引表示的元素的右孩子节点的索引.
    130      *
    131      * @param index
    132      * @return
    133      */
    134     private int rightChild(int index) {
    135         return 2 * index + 2;
    136     }
    137 
    138     @Override
    139     public String toString() {
    140         StringBuilder stringBuilder = new StringBuilder();
    141         stringBuilder.append('[');
    142         for (int i = 0; i < tree.length; i++) {
    143             if (tree[i] != null) {
    144                 stringBuilder.append(tree[i]);
    145             } else {
    146                 stringBuilder.append("null");
    147             }
    148 
    149             if (i != tree.length - 1) {
    150                 stringBuilder.append(", ");
    151             }
    152         }
    153         stringBuilder.append(']');
    154         return stringBuilder.toString();
    155     }
    156 
    157     /**
    158      * 返回区间[queryLeft,queryRight]的值
    159      *
    160      * @param queryLeft  前闭区间,用户期望查询的区间左右两个边界
    161      * @param queryRight 后闭区间
    162      * @return
    163      */
    164     public E query(int queryLeft, int queryRight) {
    165         // 进行边界检查
    166         if (queryLeft < 0 || queryLeft >= data.length || queryRight < 0 || queryRight >= data.length || queryLeft > queryRight) {
    167             throw new IllegalArgumentException("Index is illegal.");
    168         }
    169 
    170         // 递归函数调用。根节点的索引是0,区间是从0开始,到data.lenght-1这个范围里面
    171         // 还需要查询一个区间,这个区间是queryLeft到queryRight
    172         return query(0, 0, data.length - 1, queryLeft, queryRight);
    173     }
    174 
    175 
    176     /**
    177      * 在以treeId为根的线段树中[l...r]的范围里面,搜索区间[queryLeft...queryRight]的值
    178      * <p>
    179      * <p>
    180      * 从线段树的根节点开始,根节点所在的索引treeIndex
    181      * 相应的节点表示的是一个区间,节点表示的区间用left、right表示。
    182      *
    183      * @param treeIndex
    184      * @param left      节点所表示的区间左边界left
    185      * @param right     节点所表示的区间右边界left
    186      * @return
    187      */
    188     private E query(int treeIndex, int left, int right, int queryLeft, int queryRight) {
    189         // 需要注意的是我们做的线段树相关的操作,对于每一个treeIndex都传了对于这个treeIndex所在的节点
    190         // 它所表示的区间范围是哪里left-right,对于这个区间范围,其实也可以包装成一个线段树中的一个节点类。
    191         // 对于每一个节点存储它所对应的区间范围left-right,在这种情况下,直接传入treeIndex就行了,
    192         // 通过这个索引就可以访问到这个节点,进而就可以访问到这个节点所表示的区间范围。
    193 
    194 
    195         // 此处实现的方法是,在treeIndex所表示的这个区间范围,以参数的形式进行传递。
    196         // int treeIndex, int left, int right三个参数都在表示当前的节点表示的相应的信息,
    197         // 在这个节点中去查询queryLeft到queryRight这个用户关心区间。
    198 
    199 
    200         // 递归的第一部分,终止条件,递归到底的情况
    201         // 如果这个节点的左边界left和用户想要查询的左边界queryLeft、同时这个节点的有边界right和用户想要查询的有边界queryRight
    202         // 重合的时候,就是递归到底的情况,这个节点的信息就是用户想要的信息。
    203         if (left == queryLeft && right == queryRight) {
    204             // 如果是的话,直接返回
    205             return tree[treeIndex];
    206         }
    207 
    208         // 递归的第二部分
    209         // 计算中间的位置,此时,要继续到当前的这个节点的孩子节点去查找
    210         int mid = left + (right - left) / 2;
    211         // 到底是去到左孩子查找还是右孩子查找呢,还是两个孩子都要找一找呢,
    212         // 首先计算左右两个孩子所对应的索引
    213         int leftTreeChild = this.leftChild(treeIndex);
    214         int rightTreeChild = this.rightChild(treeIndex);
    215 
    216         // 如果用户查询的左边界区间大于等于中间的位置即mid + 1,
    217         // 对于当前的这个节点它将left-right这个范围分成了两个部分,两部分的中间是mid,
    218         // 第一部分是left-mid,第二部分是mid-right。
    219         // 如果用户关心的区间的左边界是大于等于中间的位置即mid + 1的话,
    220         // 也就是用户关心的这个区间和这个节点的左孩子一点关系都没有的时候,左边这部分完全可以忽略了,
    221         if (queryLeft >= mid + 1) {
    222             // 此时去mid-right这个区间去查找queryLeft-queryRight这个区间的相应的结果。
    223             return query(rightTreeChild, mid + 1, right, queryLeft, queryRight);
    224         } else if (queryRight <= mid) {
    225             // 用户关心的右边界小于中间的位置mid。把当前这个节点的区间一分为二,得到的这个mid中间位置。
    226             // 此时,用户关心的这个区间和当前这个节点一分为二,和右边这一半,后半部分一点关系都没有的。
    227             return query(leftTreeChild, left, mid, queryLeft, queryRight);
    228         }
    229 
    230         // 如果,这两种情况都不是的话,意味着用户关注的区间queryLeft-queryRight既没有落到当前节点treeIndex
    231         // 这个节点的左孩子所代表的那个节点,也没有完全落在右孩子所代表的那个节点中,
    232         // 事实上,它有一部分落在左孩子那边,另外一部分落在右孩子那边。
    233         // 在这种情况下,两边都需要查询一下。
    234 
    235         // 先去左孩子那边去查找,此时将queryLeft-queryRight这个区间拆分成了queryLeft-mid和
    236         // mid + 1到queryRight这两个区间了。
    237         // 此时,查找的queryLeft到mid的结果,存储到queryResult中。
    238         E leftResult = query(leftTreeChild, left, mid, queryLeft, mid);
    239         E rightResult = query(rightTreeChild, mid + 1, right, mid + 1, queryRight);
    240 
    241         // 如果用户关系的这部分区间有一部分在左节点,有另外一部分在右节点,此时,左右两边都需要进行查询。
    242         // 查询完成以后就可以进行融合了。调用融合器进行融合。
    243         return merger.merger(leftResult, rightResult);
    244     }
    245 
    246 
    247     /**
    248      * 对线段树进行更新操作。
    249      *
    250      * <p>
    251      * 将index位置的值,更新为e
    252      *
    253      * @param index
    254      * @param e
    255      */
    256     public void set(int index, E e) {
    257         // 对index合法性进行检测
    258         if (index < 0 || index >= data.length) {
    259             throw new IllegalArgumentException("Index is illegal.");
    260         }
    261 
    262         // 索引index正常
    263         // 此时,将线段树的data[index]这个index索引的值换成是e
    264         data[index] = e;
    265 
    266         // 接下来对tree这个变量进行相应的更新操作了。
    267         // 从根节点开始,所对应的这个区间从0到data.length-1,来更新index索引位置的元素更新为e。
    268         this.set(0, 0, data.length - 1, index, e);
    269     }
    270 
    271     /**
    272      * 在以treeIndex为根的线段树中更新index的值为e
    273      * <p>
    274      * 递归函数修改,修改tree
    275      * <p>
    276      * 时间复杂度是O(logn)级别的。
    277      * 从根节点开始,一直找到index这个位置的元素所在的那个叶子节点,这个高度是logn级别的。
    278      *
    279      * @param treeIndex 以treeIndex为根进行的线段树
    280      * @param left      对于这个节点表示的是从left到right的值
    281      * @param right
    282      * @param index     在这个线段树中更新将index这个位置的元素为e
    283      * @param e
    284      */
    285     public void set(int treeIndex, int left, int right, int index, E e) {
    286         // 递归函数,递归到底的情况
    287         if (left == right) {
    288             // 此时,已经递归到底了,已经找到了要更新的节点了。
    289             tree[treeIndex] = e;
    290             return;
    291         }
    292 
    293         // 否则,就是在线段树中去找index这个位置它对应的叶子的位置在哪里。
    294         // 只有找到叶子节点才可以计算出根节点的值的。
    295         // 线段树中每一个节点都是对应一个区间。
    296         int mid = left + (right - left) / 2;
    297         // 计算出对于treeIndex这个节点的左右孩子
    298         // treeIndex这个左孩子的索引
    299         int leftTreeIndex = leftChild(treeIndex);
    300         // treeIndex这个右孩子的索引
    301         int rightTreeIndex = rightChild(treeIndex);
    302 
    303         // 如果我们要找的index >= mid + 1的时候,此时只要去右子树继续寻找就行了
    304         if (index >= mid + 1) {
    305             // 此时,index在该节点对应的右子树所对应的区间
    306             set(rightTreeIndex, mid + 1, right, index, e);
    307         } else if (index <= mid) {
    308             // 去左子树的所对应的区间去更新操作
    309             set(leftTreeIndex, left, mid, index, e);
    310         }
    311 
    312         // 对于线段树来说,我们最终找到了叶子节点,更新完了tree[treeIndex]之后,是不够的
    313         // 相应的在返回去的过程中,这个叶子节点的父亲节点,这个父亲节点的父亲节点等一系列节点
    314         // 都会受到牵连,因为这些节点描述的都是一个区间内相应的统计的值,而这些区间包含index这个
    315         // 索引的元素,一旦index这个位置的元素发生了改变,那么它的祖辈节点相应所表示的区间包含这个index
    316         // 其统计结果也要发生改变,所以最后的时候,在对每一个节点更新的时候,对这个节点的值tree[treeIndex]
    317         // 也要进行一下更新,之间调用融合器进行更新。
    318 
    319         // 每次递归操作都进行了更新操作的。
    320         tree[treeIndex] = merger.merger(tree[leftTreeIndex], tree[rightTreeIndex]);
    321     }
    322 
    323 
    324     public static void main(String[] args) {
    325         Integer[] nums = {2, 0, 3, 5, 2, 1};
    326 //        SegmentTree<Integer> segmentTree = new SegmentTree<Integer>(nums, new Merger<Integer>() {
    327 //
    328 //            /**
    329 //             * 使用匿名内部类,来实现融合器
    330 //             * @param a
    331 //             * @param b
    332 //             * @return
    333 //             */
    334 //            @Override
    335 //            public Integer merger(Integer a, Integer b) {
    336 //                return a + b;
    337 //            }
    338 //        });
    339 
    340 
    341         //
    342         SegmentTree<Integer> segmentTree = new SegmentTree<Integer>(nums,
    343                 (a, b) -> a + b
    344         );
    345         System.out.println(segmentTree.toString());
    346 
    347         System.out.println();
    348         // 线段树的查询
    349         Integer query = segmentTree.query(0, 3);
    350         System.out.println(query);
    351     }
    352 
    353 }

    解决力扣LeetCode,力扣LeetCode,区域和检索 - 数组可修改,代码,如下所示:

     1 package com.leetcode;
     2 
     3 import com.tree.SegmentTree;
     4 
     5 /**
     6  * 线段树解决此问题
     7  * <p>
     8  * <p>
     9  * <p>
    10  * 给定一个整数数组  nums,求出数组从索引 i 到 j  (i ≤ j) 范围内元素的总和,包含 i,  j 两点。
    11  * <p>
    12  * <p>
    13  * 线段树的查询操作和修改操作的时间复杂度是O(logn)级别的。O(logn)远远快于O(n)级别的时间复杂度
    14  * <p>
    15  * <p>
    16  * 线段树的创建的过程时间复杂度是O(n)的复杂度,更准确的说是O(4*n)的复杂度。
    17  * <p>
    18  * <p>
    19  * 对于线段树来说,对于要考虑区间的这一种数据,尤其是想要查询区间的相关统计的信息的时候,
    20  * 同时数据是动态的,不时要更新数据,线段树是一种很好的数据结构。
    21  */
    22 public class NumArray {
    23 
    24     // 线段树
    25     private SegmentTree<Integer> segmentTree;
    26 
    27 
    28     /**
    29      * 给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
    30      * <p>
    31      * <p>
    32      * sumRange(0, 2) -> 1
    33      * sumRange(2, 5) -> -1
    34      * sumRange(0, 5) -> -3
    35      *
    36      * @param nums
    37      */
    38     public NumArray(int[] nums) {
    39         // 判断数组长度是否大于0,如果大于0就进行创建线段树
    40         if (nums.length > 0) {
    41             // 将int[]数组转换为Integer[]类型
    42             Integer[] data = new Integer[nums.length];
    43             for (int i = 0; i < data.length; i++) {
    44                 data[i] = nums[i];
    45             }
    46 
    47             // 开始初始化线段树
    48             segmentTree = new SegmentTree<>(data, (a, b) -> a + b);
    49         }
    50     }
    51 
    52     /**
    53      * 查询区间之间的和
    54      * 查询操作,时间复杂度是O(logn)
    55      *
    56      * @param i
    57      * @param j
    58      * @return
    59      */
    60     public int sumRange(int i, int j) {
    61         if (segmentTree == null) {
    62             throw new IllegalArgumentException("Segment Tree is null.");
    63         }
    64 
    65         return segmentTree.query(i, j);
    66     }
    67 
    68 
    69     /**
    70      * <p>
    71      * <p>
    72      * 更新操作,时间复杂度是O(logn)
    73      *
    74      * @param i
    75      * @param val
    76      */
    77     public void update(int i, int val) {
    78         if (segmentTree == null) {
    79             throw new IllegalArgumentException("Segment Tree is null.");
    80         }
    81 
    82         // 直接调用线段树的更新操作即可
    83         segmentTree.set(i, val);
    84     }
    85 
    86     public static void main(String[] args) {
    87         int[] nums = {-2, 0, 3, -5, 2, -1};
    88         NumArray numArray = new NumArray(nums);
    89         int sumRange = numArray.sumRange(0, 3);
    90         System.out.println(sumRange);
    91     }
    92 
    93 }
  • 相关阅读:
    Scons 三
    Scons 二
    vs code插件
    Scons一
    实例演示 C# 中 Dictionary<Key, Value> 的检索速度远远大于 hobbyList.Where(c => c.UserId == user.Id)
    ASP.NET Core 如何用 Cookie 来做身份验证
    如何设计出和 ASP.NET Core 中 Middleware 一样的 API 方法?
    小记编程语言的设计
    解决 VS2019 打开 edmx 文件时没有 Diagram 视图的 Bug
    一款回到顶部的 jQuery 插件,支持 Div 中的滚动条回到顶部
  • 原文地址:https://www.cnblogs.com/biehongli/p/12526819.html
Copyright © 2020-2023  润新知