数组累加和七连7:数组arr中子数组累加和在[a,b]范围内的数量有多少个

数组累加和七连7:数组arr中子数组累加和在[a,b]范围内的数量有多少个?

提示:本题是系统有序表的经典应用,贪心算法,舍弃思想,确实很难想到解决方案
经常在互联网大厂中考,第一道大题,往往就是有序表,或者堆来解决,设计排序的事情。
如果遇到这种题目,那就直接拿基础数据结构改写!

本题涉及有序表的基础知识:
提示:这段时间,讲有序表、跳表的底层数据结构,平衡搜索二叉树:AVL树,SB树,红黑树
基础知识:
【1】求二叉树中节点x的后继节点和前驱结点
【2】二叉树,二叉树的归先序遍历,中序遍历,后序遍历,递归和非递归实现
【3】平衡搜索二叉树BST底层的增删改查原理,左旋右旋的目的
【4】有序表TreeMap/TreeSet底层实现:AVL树、傻逼树SBT、红黑树RBT、跳表SkipMap失衡类型
【5】傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡
【6】跳表SkipList:可二分查找的有序链表,实现有序表,思想先进,操作复杂度O(logn)
【7】二维数组list,每行长度不同,寻找最窄区间[a,b],保证list每行至少有一个数在[a,b]上

本题涉及数组累加和6连得基础知识:
提示:之前见过数组累加和的另外三个重要题目,动态规划,可以认为是数组累加和456连

之前的数组累加和三连的第1连:
【1】数组累加和三连1:arr全大于0,请问累加和为k的子数组最大长度是多少
之前数组累加和三连的第2连:
【2】数组累加和三连2:arr可小于等于大于0,请问累加和为k的子数组最大长度是多少
之前数组累加和三连的第3连:
【3】数组累加和三连3:arr可小于等于大于0,请问累加和<=k的子数组最大长度是多少

重要的数组累加和456连:
【4】数组arr中必须以i位置结尾的子数组,其最大累加和是多少?
【5】数组arr的0–i范围上任选一个子数组的最大累加和是多少?
【6】给定数组arr和k,求3个不重叠的长为k的子数组的累加和之和最大值是多少?

今天讲数组累加和第7连


题目

数组累加和七连7:
给定数组数组arr,元素可小于零,等于零,大于零,请问:
arr中子数组累加和在[a,b]范围内的数量有多少个?


一、审题

示例:arr = {1,2,3,4};
int lower = 3;
int upper = 5;
arr中子数组累加和在[3,5]范围内的数量有多少个?
暴力看看累加和有哪些
外循环:i从0–N-1
内循环:j=i–N-1
求i–j范围内的子数组,累加和,看看其在不在a–b范围内?在count++
返回count

累加和有这几种:
3 5 3 4
4个满足达标条件


暴力解:o(n^3),不可取

以每个i为开头,去求一次子数组,总共n^2个子数组
内部求累加和是o(n)速度
故o(n^3)复杂度,理解题目要旨是可以的
但是暴力解,需要优化!

暴力解手撕代码:

    //暴力看看累加和有哪些
    public static int howMany(int[] arr, int a, int b){
        if (arr == null || arr.length == 0) return 0;

        int N = arr.length;
        int ans = 0;
        //外循环:i从0--N-1
        //	内循环:j=i--N-1
        for (int i = 0; i < N; i++) {
            for (int j = i; j < N; j++) {
                //i--j
                int sum = 0;
                for (int k = i; k <= j; k++) {
                    sum += arr[k];//累加
                }
                if (sum >= a && sum <= b) {
                    System.out.print(sum +" ");
                    ans++;
                }
            }
        }
        //	求i--j范围内的子数组,累加和,看看其在不在a--b范围内?在count++
        //返回count
        System.out.println();

        return ans;
    }

    public static void test(){
        int[] arr = {1,2,3,4};
        int lower = 3;
        int upper = 5;
        //【3-5】:12,3,23,4  答案是4,确实,
        System.out.println(howMany(arr, lower, upper));
    }

    public static void main(String[] args) {
        test();
    }
3 5 3 4 
4

有序表底层傻逼树SBT来加速:o(n*log(n))速度

本题最重要的基础知识,你必须看懂了:
【5】傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡
本题非常重要的基础知识,你也得看懂了,之前数组累加和三连的第2连:
【2】数组累加和三连2:arr可小于等于大于0,请问累加和为k的子数组最大长度是多少

有了以上两个基础知识,你才能综合运用,解除本题来,否则你一脸懵逼的!!!
之前数组累加和三连的第2连咱们说过一个事情,往往累加和这种子数组问题,都是讨论以i开头的子数组的状况,
或者讨论以i结尾的子数组的状况
然后让每一个i开头或者结尾的子数组收集一个合适的答案,N个元素遍历完,自然最优解必在其中。

本题也是一样,之前数组累加和三连的第2连就说过
我们可以讨论以i结尾的子数组的累加和的状况,而且累加和完全可以用前缀累加和sum来加速!
我们有了前缀累加和sum,完全可以以o(1)速度拿到j–i范围上的累加和,看下图
sum[j–i]=sum[i]-sum[j-1]
在这里插入图片描述
现在问你,请问你以i结尾的可能的子数组中?有多少个子数组的累加和s在a–b范围内
在这里插入图片描述
咱们来简单推一个公式出来,转换本题的意思:
完全可以由问你的问题,来到i处,si在a–b范围内,去推知下面图中红色的公式:
等价于问你:来到i处,j-1及之前的所有累加和中,有多少个是在si-b–si-a之间的
在这里插入图片描述
显然j-1<i的,所以其实:
等价于问你:来到i处,i-1及之前的所有累加和中,有多少个是在si-b–si-a之间的

举个例子:
a=10,b=30
请问你以i结尾的可能的子数组中?有多少个子数组的累加和s在10–30范围内
不妨设sumi=100,则等价于问:
来到i处,i-1及之前的所有累加和中,有多少个是在si-b–si-a【100-30,100-10=70–90】之间的
是不是这意思?

这就是本题比价经典的转换的地方,你得慢慢熟悉这个概念

这样的话,咱们从0–i从左往右收集累加和的过程中,把每一个sum放入有序表,收集起来,就能统计咱们究竟哪些s
是在si-b–si-a之间的。如果这个统计速度很快,那么咱们就达到了优化的目的!

如果我给你si-b,和si-a,你根据有序表,能秒杀知道si-b–si-a之间的数量有多少个,岂不美滋滋??
如果有序表能有一个函数lessThanKey(x)
如果它能返回<x的累加和的数量有几个?的话
咱们完全可以知道i-1及之前的所有累加和中,有多少个是在si-b–si-a之间的,调用
k1=lessThanKey(x=si-b)
k2=lessThanKey(x=si-a+1)

注意k2中是x+1哦……
则:i-1及之前的所有累加和中,有多少个是在si-b–si-a之间的k2-k1个
仔细品味一下这个计算,

比如
a=10,b=30
请问你以i结尾的可能的子数组中?有多少个子数组的累加和s在10–30范围内
不妨设sumi=100,则等价于问:
来到i处,i-1及之前的所有累加和中,有多少个是在si-b–si-a【100-30,100-10=70–90】之间的
如果累加和小于70的有k1=10个,小于91的有k2=33个
那i-1及之前的所有累加和中,在si-b–si-a【70–90】之间的就是k2-k1=23个呗!
这不是很简单吗?

然后,咱们让i=0,–N-1,每一个i,都求这么一个k2-k1,全部累加起来,不就是整体arr在a–b之间的达标累加和的个数吗?

因此系统给的有序表TreeMap能干这件事吗???
不能!!!
不能!!!
不能!!!

为毛?
因为咱们0–i从左往右收集累加和的过程中,可能很多sum是重复的,比如上面的案例中的几个达标累加和:3 5 3 4
3 3就是重复的,你将3放入系统给定的有序表的话,不好意思,它不能重复放同一个key,你放重复的就会去重,你只能找到1个3……
那么这就尴尬了……
咱们没法准确统计

但是你别忘了!有序表,底层怎么实现的?
看基础:【4】有序表TreeMap/TreeSet底层实现:AVL树、傻逼树SBT、红黑树RBT、跳表SkipMap失衡类型

可以是AVL树,可以是傻逼树SBT,可以是红黑树,可以是跳表,实现的方式多种多样!

既然系统有序表搞不定统计某个累加和s有多少个?

那咱们就要手动改写有序表了
那咱们就要手动改写有序表了
那咱们就要手动改写有序表了

类似于手改改写堆结构一样,你得知道系统堆的底层堆是怎么实现的
咱们本题就需要你知道有序表的底层是怎么实现的?咱们多的不说,我之前见过了
你必须会傻逼树SBT和跳表实现有序表的底层逻辑和代码,虽然复杂,但是互联网大厂可能就会让你现场手撕代码!

今天咱们要用傻逼树SBT搞定本题!


size balanced tree傻逼树SBT增加all和lessThanKey(x)

本题最重要的基础知识,你必须看懂了:
【5】傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡

原来的SBT长这样【不好意思,size=7,共7个不同的key】
在这里插入图片描述
size统计节点cur极其下面有多少个不同的key,也就是有多少个不同的节点?
目前cur=5,左树3个不同的key,右树3个不同key,加cur自己,共7个不同的key,所以size=7

可是,如果现在cur表示累加和了,可能就会有重复的累加和出现,那同一个key,可能就要另外一个参数来统计了
用all参数,它代表经过cur的所有key的总数量!
比如
在上图基础上,又来了1个3:
在这里插入图片描述
那3节点那个all就应该多增一个,all=2,导致cur=5的all=8,这就是all的意思,拢共出现了多少个累加和,管你key重复与否,统统统计
而size只统计不同key,size的存在是为了正常搞傻逼树SBT的调平衡的。你去学那个文章就知道了。

我们要在size balanced tree(SBT)中,
(1)新增一个参数all:统计经过节点cur的累加和总共有多少个?
这个统计数量的参数,在SBT里面,类似于size一样,在用的时候,我们需要根据all统计<x的累加和到底有多少个?
(2)我们还需要新增一个函数lessThanKey(x),就是系统有序表做不大的统计<x的累加和到底有多少个?这个功能

okay
咱们直接在原来的傻逼树上改代码!

原始的傻逼树SBT数据结构长这样:

傻逼树SBT节点是:

//傻逼树节点
    public static class NodeSBT<K extends Comparable<K>, V>{
        //成员变量key,value,用泛型表达:K,V类型,KV可以是Integer,String等等通用的基础数据类型
        public K key;
        public V value;
        //节点自然要有左右子,才能组成二叉树嘛
        NodeSBT<K, V> l;
        NodeSBT<K, V> r;
        //还有一个重要参数:**不同key节点的个数size**(同一个key是可以通过value计数的)
        public int size;//个数就int类型

        public NodeSBT(K k, V v){
            key = k;
            value = v;
            size = 1;//初始化
        }
    }

傻逼树SBT是:

//傻逼树定义节点:
    public static class SizeBalancedTree<K extends Comparable<K>, V>{
        //咱准备一个root节点,也就是x节点,作为傻逼树的头结点。
        public NodeSBT<K, V> root;//cur=x

        //成员函数有很多,增删改查,调平衡,左右旋……
        //左旋
        public NodeSBT<K, V> leftRotate(NodeSBT<K, V> cur){
            //(1)先记住右子节点rightNode
            NodeSBT<K, V> rightNode = cur.r;
            //(2)让cur=x右子挂rightNode的左子
            cur.r = rightNode.l;
            //(3)再让rightNode左子挂cur=x,完成左旋
            rightNode.r = cur;
            //(4)交换cur=x和rightNode的节点数目,重新统计cur=x的节点数目
            rightNode.size = cur.size;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            //(5)返回rightNode作为头结点,它就是老大

            return rightNode;
        }
        //右旋
        public NodeSBT<K, V> rightRotate(NodeSBT<K, V> cur){
            //(1)记住cur的左子leftNode
            NodeSBT<K, V> leftNode = cur.l;
            //(2)让cur左子挂leftNode的右子
            cur.l = leftNode.r;
            //(3)让leftNode的右子挂cur
            leftNode.r = cur;
            //(4)交换cur和leftNode的size,然后重新统计cur的节点数目
            leftNode.size = cur.size;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            //(5)返回leftNode,完成右旋

            return leftNode;
        }

        //傻逼树SBT的四种失衡情况,调平
        //将四种情况全部综合起来写代码——画个图就知道了,左右旋函数,有了,就调平,检查即可
        public NodeSBT<K, V> maintain(NodeSBT<K, V> cur){
            //给cur为null就没必要调平了
            if (cur == null) return null;
            //(1)x.l.l.size>x.r.size,由左子左树引发的,称为LL型
            //调平方案:**直接将x右旋**
            if (cur.l != null && cur.l.l != null && cur.r != null &&
                    cur.l.l.size > cur.r.size){
                cur = rightRotate(cur.r);
                //由于右子上来替换,重新检查并调平变化的节点
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }
            //(2)x.r.r.size>x.l.size,由右子右树引发的,称为RR型
            //调平方案:**让x左旋**
            else if (cur.r != null && cur.r.r != null && cur.l != null &&
                    cur.r.r.size > cur.l.size){
                cur = leftRotate(cur);
                cur.l = maintain(cur.l);//左子和cur都变化了
                cur = maintain(cur);
            }
            //(3)x.l.r.size>x.r.size,由左子右树引发的,称为LR型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //因此对于LR型:调平方案是:
            //1)是左树右子引发的问题,左子右旋,即把孙子替换左子
            //2)再让x右旋,即把孙子替换x自己
            else if (cur.l != null && cur.l.r != null && cur.r != null &&
                    cur.l.r.size > cur.r.size){
                cur.l = leftRotate(cur.l);
                cur = rightRotate(cur);
                //数量冻过的都要检查重新看看是否平衡
                cur.l = maintain(cur.l);
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }
            //(4)x.r.l.size>x.l.size,由右子左树引发的,称为RL型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //本题是由RL引发的,则调平方案是:
            //1)先让R右旋,让孙子上来替代R
            //2)再让x左旋,让孙子上来替代x自己
            else if (cur.r != null && cur.r.l != null && cur.l != null &&
                    cur.r.l.size > cur.l.size){
                cur.r = rightRotate(cur.r);
                cur = leftRotate(cur);
                //动过的都要检查
                cur.r = maintain(cur.r);
                cur.l = maintain(cur.l);
                cur = maintain(cur);
            }

            return cur;//调完之后的cur就是头,返回
        }

        //查傻逼树SBT中是否有节点key,并返回离key最近的不为null的那个节点
        public NodeSBT<K, V> findLeastKeyNode(K key){
            //(1)最开始让pre和cur都指向头结点root,每次找都是root开始找下去
            NodeSBT<K, V> pre = root;//root就是当前cur
            NodeSBT<K, V> cur = root;
            //(2)只要cur不为null,让pre=cur,cur一直往下找
            while (cur != null){
                //一旦找到key=cur,那退出,pre=cur也就是key
                pre = cur;//一进来就是让pre为cur的父节点
                if (key.compareTo(cur.key) == 0) break;//找到了pre=cur=key
                //如果key<cur,那需要去cur的左边去找
                else if (key.compareTo(cur.key) < 0) cur = cur.l;
                //如果key>cur,那需要去cur的右边找
                else cur = cur.r;
            }

            return pre;//并返回离key最近的不为null的那个节点
        }

        //返回傻逼树SBT整体有多少个节点?
        public int size(){
            return root == null ? 0 : root.size;
        }

        //判断傻逼树是否为空树?
        public boolean isEmpty(){
            return root == null;
        }

        //key确实不存在,就要新增节点,挂到距离key最近的不为null的节点那
        private NodeSBT<K, V> add(NodeSBT<K, V> cur, K key, V value){
            //(1)有距离key最近的不为null的节点pre,新增key,value
            //(2)可能压根root就是null,说明key是首个节点,生成,并挂接root
            if (root == null) return new NodeSBT<>(key, value);
            else {
                //(3)否则,一定要让pre=cur的节点数目新增,size++
                cur.size++;
                //(4)然后判断是挂在cur左边,还是右边?
                if (key.compareTo(cur.key) < 0) cur.l = add(cur.l, key, value);//等价于直接生成节点
                else cur.r = add(cur.r, key, value);//等价于直接生成节点
                //(5)别忘了新增的节点,可能让cur失衡,简单调平,我们有准备好函数的,既然要调平衡,返回新的头
            }
            return maintain(cur);//变动的是cur
        }
        //put是更新,或者新增--没有返回值
        public void put(K key, V value){
            if (key == null) throw new RuntimeException("不能加null");

            NodeSBT<K, V> pre = findLeastKeyNode(key);
            if (pre !=null && key.compareTo(pre.key) == 0) pre.value = value;//更新
            else root = add(pre, key, value);//往pre上直接挂
        }

        //傻逼树SBT是否真的包含某个节点key?
        public boolean continsKey(K key){
            //还是用findLeastKeyNode查询key,得到pre
            //保证pre不是null,且,key相同
            NodeSBT<K, V> pre = findLeastKeyNode(key);

            return pre != null && key.compareTo(pre.key) == 0;
        }

        //傻逼树SBT删除某个节点key的四种情况——从root查找,沿途size--,并删除那个节点key,返回删除之后的root
        public NodeSBT<K, V> delete(NodeSBT<K,V> cur, K key){
            //既然要删除,必然size--
            cur.size--;//root必定减少1个节点key
            //左右查找,删除key
            if (key.compareTo(cur.key) < 0) cur.l = delete(cur.l, key);
            else if (key.compareTo(cur.key) > 0)cur.r = delete(cur.r, key);
            else {
                //找到了key:4种删除的情况
                //(1)x节点没有左子,没有右子,直接废了x,让x=null
                if (cur.l == null && cur.r == null) cur = null;
                //(2)x节点有左子,没有右子,让右子挂接在x的父节点上
                else if (cur.l != null && cur.r == null) cur = cur.l;
                //(3)x节点没有左子,有右子,让x的右子直接挂在x的父节点上
                else if (cur.l == null && cur.r != null) cur = cur.r;
                //(4)x节点有左子,有右子——这是最难搞的,
                //**要让x的后继节点p来接替自己**,操作步骤如下:
                //先把替身p=des的右树挂到替身p的父节点pre上。
                //然后把x=cur的左树和右树挂到替身p=des的左树和右树上,
                //然后让替身p来接替x
                //完成删除x。
                else {
                    NodeSBT<K, V> pre = null;//pre记忆的是des的父节点
                    NodeSBT<K, V> des = cur.r;//去寻找cur右树的最左节点,即后继节点des
                    des.size--;
                    while (des.l != null){
                        //cur右树的最左节点就是后继节点,
                        pre = des;//pre记忆的是des的父节点
                        des = des.l;//往左窜
                        des.size--;//沿途大家size--
                    }
                    //知道des确实就是后继节点了
                    if(pre != null){//保证pre不空,否则没法 玩
                        pre.l = des.r;//让pre右接管des的右,因为后继节点des要溜了
                        des.r = cur.r;//最终des做老大,接管cur的右和左
                        des.l = cur.l;//des左子解cur左子,替换呗
                        //cur废了
                    }
                    //重新统计des的数量
                    des.size = des.l.size + (des.r != null ? des.r.size : 0) + 1;//自己
                    cur = des;//新头
                }
            }

            return cur;//返回最后得root
        }

        //remove傻逼树SBT删除某个节点key
        public void remove(K key){
            //首先查 傻逼树SBT是否真的包含某个节点key?
            if (key == null) throw new RuntimeException("不能加null");

            //真的查到了,又从root查找key,沿途size--,并删除那个节点key,返回删除之后的root
            if (continsKey(key)) root = delete(root, key);
        }

    }

本题新增all和lessThanKey(x)函数的数据结构长这样

本题傻逼树SBT的节点为:

    //用傻逼树解决:
    //傻逼树节点
    public static class NodeSBT<K extends Comparable<K>, V>{
        //成员变量key,value,用泛型表达:K,V类型,KV可以是Integer,String等等通用的基础数据类型
        public K key;
        public V value;
        //节点自然要有左右子,才能组成二叉树嘛
        NodeSBT<K, V> l;
        NodeSBT<K, V> r;
        //还有一个重要参数:**不同key节点的个数size**(同一个key是可以通过value计数的)
        public int size;//个数就int类型
        //(1)新增一个参数**all**:统计经过节点cur的累加和总共有多少个?
        //这个统计数量的参数,在SBT里面,类似于size一样,在用的时候,
        // 我们需要根据all统计<x的累加和到底有多少个?
        public int all;
        

        public NodeSBT(K k, V v){
            key = k;
            value = v;
            size = 1;//初始化,统计不同的key
            all = 1;//初始化时,一个节点,也就来了一个key
        }
    }

你看看区别,多了all是吧!

然后下面就是傻逼树SBT的数据结构了,完全根据上面改编来的
只需要增加一个函数即可:
(2)我们还需要新增一个函数lessThanKey(x),就是系统有序表做不到的统计<x的累加和到底有多少个?这个功能
如何统计这个函数,一会咱们说
先看all怎么更新?

实际上,函数新增,删除,调平啥的,size变,all同步跟这边
唯独在更新key操作时,all遇到了同一个key要统计,所以all要变,而size记录的是不同的key,在更新操作中size不管
这是唯一的区别,所以好办吧!!!

统计<x的累加和到底有多少个?这个功能lessThanKey(x)如何实现呢?
也非常非常的自然
比如:下图累加和2 6 3 5重复有10个
累加和4 8 7重复有20个
最终导致经过3的all=40
经过7的all=50个
经过5的all=100个
问:累加和<6的总数有多少?
在这里插入图片描述
实际上,每次查询,都会从头结点root开始往下查,x=6
不妨设累加和总数ans=0个
(1)当x=6<cur时,往左滑动,个数ans不加
为啥呢?你往左滑动,还没找到=x的点,无法判断<x的个数,你现在找到的都是<cur的,而x<cur,当前cur无法给你结果
下面粉色那cur从7滑到左树去的情况
在这里插入图片描述
(2)当x=6>cur时,往右滑动,起码你知道cur<x的,自然经过cur的all总数,减去cur.r的all,就是包括
cur和左树
在内的小于x的数量
ans+=cur.all-cur.r.all
下面橘色那cur从5滑到右树去的情况
在这里插入图片描述
(1)当x=6=cur时,巧了,你在cur=x,究竟<x的有多少呢?不就是<cur的吗,ans+红色那cur=6,x=6,相等,那看看cur左树的情况,没有就不加,有就加上左树的all
在这里插入图片描述

是不是很巧妙?
也很简单!!!

好,我们来改本题的傻逼树的代码,更新all,然后新增这个函数:lessThanKey(x)

    //傻逼树定义节点:
    public static class SizeBalancedTree<K extends Comparable<K>, V>{
        //咱准备一个root节点,也就是x节点,作为傻逼树的头结点。
        public NodeSBT<K, V> root;//cur=x

        //(2)我们还需要新增一个函数**lessThanKey(x)**,
        // 就是系统有序表做不大的统计<x的累加和到底有多少个?这个功能
        public int lessThanKeyCount(K x){
            NodeSBT<K, V> cur = root;//从头开始查
            int ans = 0;//统计<x的累加和的个数
            while (cur != null){//往下cur遇到叶节点over
                //(3)当x=6=cur时,巧了,你在cur=x,究竟<x的有多少呢?不就是<cur的吗,
                // ans+=cur.l.all【当然,有左树的话,没有左树,自然就是0】
                if (x.compareTo(cur.key) == 0){
                    ans += cur.l != null ? cur.l.all : 0;
                    return ans;//<x的全部收集好了,返回
                }
                //(1)当x=6<cur时,**往左滑动**,个数ans不加
                //为啥呢?你往左滑动,还没找到=x的点,无法判断<x的个数,
                // 你现在找到的都是<cur的,**而x<cur**,当前cur无法给你结果
                else if (x.compareTo(cur.key) < 0) cur = cur.l;//直接滑向左边
                //(2)当x=6>cur时,**往右滑动**,起码你知道cur<x的,
                // 自然经过cur的all总数-cur.r的all,就是包括cur和左树在内的小于x的数量
                //ans+=cur.all-cur.r.all
                else {
                    ans += cur.all - (cur.r != null ? cur.r.all : 0);//有右树才减
                    cur = cur.r;//滑向右边
                }
            }

            return ans;
        }

        //成员函数有很多,增删改查,调平衡,左右旋……
        //size变,all变,更新那,all变,size不管
        //左旋
        public NodeSBT<K, V> leftRotate(NodeSBT<K, V> cur){
            //(1)先记住右子节点rightNode
            NodeSBT<K, V> rightNode = cur.r;
            //(2)让cur=x右子挂rightNode的左子
            cur.r = rightNode.l;
            //(3)再让rightNode左子挂cur=x,完成左旋
            rightNode.r = cur;
            //(4)交换cur=x和rightNode的节点数目,重新统计cur=x的节点数目
            rightNode.size = cur.size;
            rightNode.all = cur.all;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            cur.all = (cur.l != null ? cur.l.all : 0) +
                    (cur.r != null ? cur.r.all : 0) + 1;//自己算上
            //(5)返回rightNode作为头结点,它就是老大

            return rightNode;
        }
        //右旋
        public NodeSBT<K, V> rightRotate(NodeSBT<K, V> cur){
            //(1)记住cur的左子leftNode
            NodeSBT<K, V> leftNode = cur.l;
            //(2)让cur左子挂leftNode的右子
            cur.l = leftNode.r;
            //(3)让leftNode的右子挂cur
            leftNode.r = cur;
            //(4)交换cur和leftNode的size,然后重新统计cur的节点数目
            leftNode.size = cur.size;
            leftNode.all = cur.all;
            cur.size = (cur.l != null ? cur.l.size : 0) +
                    (cur.r != null ? cur.r.size : 0) + 1;//自己算上
            cur.all = (cur.l != null ? cur.l.all : 0) +
                    (cur.r != null ? cur.r.all : 0) + 1;//自己算上
            //(5)返回leftNode,完成右旋

            return leftNode;
        }

        //傻逼树SBT的四种失衡情况,调平
        //调平只看size,与all没关系
        //将四种情况全部综合起来写代码——画个图就知道了,左右旋函数,有了,就调平,检查即可
        public NodeSBT<K, V> maintain(NodeSBT<K, V> cur){
            //给cur为null就没必要调平了
            if (cur == null) return null;
            //(1)x.l.l.size>x.r.size,由左子左树引发的,称为LL型
            //调平方案:**直接将x右旋**
            if (cur.l != null && cur.l.l != null && cur.r != null &&
                    cur.l.l.size > cur.r.size){
                cur = rightRotate(cur.r);
                //由于右子上来替换,重新检查并调平变化的节点
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }
            //(2)x.r.r.size>x.l.size,由右子右树引发的,称为RR型
            //调平方案:**让x左旋**
            else if (cur.r != null && cur.r.r != null && cur.l != null &&
                    cur.r.r.size > cur.l.size){
                cur = leftRotate(cur);
                cur.l = maintain(cur.l);//左子和cur都变化了
                cur = maintain(cur);
            }
            //(3)x.l.r.size>x.r.size,由左子右树引发的,称为LR型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //因此对于LR型:调平方案是:
            //1)是左树右子引发的问题,左子右旋,即把孙子替换左子
            //2)再让x右旋,即把孙子替换x自己
            else if (cur.l != null && cur.l.r != null && cur.r != null &&
                    cur.l.r.size > cur.r.size){
                cur.l = leftRotate(cur.l);
                cur = rightRotate(cur);
                //数量冻过的都要检查重新看看是否平衡
                cur.l = maintain(cur.l);
                cur.r = maintain(cur.r);
                cur = maintain(cur);
            }
            //(4)x.r.l.size>x.l.size,由右子左树引发的,称为RL型
            //LR,RL两种情况都需要完成一个目标:
            //**让引发问题的孙子节点,一步步上来接替x自己,就平衡了**
            //本题是由RL引发的,则调平方案是:
            //1)先让R右旋,让孙子上来替代R
            //2)再让x左旋,让孙子上来替代x自己
            else if (cur.r != null && cur.r.l != null && cur.l != null &&
                    cur.r.l.size > cur.l.size){
                cur.r = rightRotate(cur.r);
                cur = leftRotate(cur);
                //动过的都要检查
                cur.r = maintain(cur.r);
                cur.l = maintain(cur.l);
                cur = maintain(cur);
            }

            return cur;//调完之后的cur就是头,返回
        }

        //查傻逼树SBT中是否有节点key,并返回离key最近的不为null的那个节点
        public NodeSBT<K, V> findLeastKeyNode(K key){
            //(1)最开始让pre和cur都指向头结点root,每次找都是root开始找下去
            NodeSBT<K, V> pre = root;//root就是当前cur
            NodeSBT<K, V> cur = root;
            //(2)只要cur不为null,让pre=cur,cur一直往下找
            while (cur != null){
                //一旦找到key=cur,那退出,pre=cur也就是key
                pre = cur;//一进来就是让pre为cur的父节点
                if (key.compareTo(cur.key) == 0) break;//找到了pre=cur=key
                    //如果key<cur,那需要去cur的左边去找
                else if (key.compareTo(cur.key) < 0) cur = cur.l;
                    //如果key>cur,那需要去cur的右边找
                else cur = cur.r;
            }

            return pre;//并返回离key最近的不为null的那个节点
        }

        //返回傻逼树SBT整体有多少个节点?
        public int size(){
            return root == null ? 0 : root.size;
        }
        //返回傻逼树SBT整体有多少个key,可能重复?
        public int all(){
            return root == null ? 0 : root.all;
        }

        //判断傻逼树是否为空树?
        public boolean isEmpty(){
            return root == null;
        }

        //key确实不存在,就要新增节点,挂到距离key最近的不为null的节点那
        private NodeSBT<K, V> add(NodeSBT<K, V> cur, K key, V value){
            //(1)有距离key最近的不为null的节点pre,新增key,value
            //(2)可能压根cur就是null,说明key是首个节点,生成,并挂接root
            if (cur == null) return new NodeSBT<>(key, value);
            else {
                //(3)否则,一定要让pre=cur的节点数目新增,size++
                //新增的话,all也是别让加的
                cur.size++;
                cur.all++;
                //(4)然后判断是挂在cur左边,还是右边?
                if (key.compareTo(cur.key) < 0) cur.l = add(cur.l, key, value);//等价于直接生成节点
                else cur.r = add(cur.r, key, value);//等价于直接生成节点
                //(5)别忘了新增的节点,可能让cur失衡,简单调平,我们有准备好函数的,既然要调平衡,返回新的头
            }
            return maintain(cur);//变动的是cur
        }
        //put是更新,或者新增--没有返回值
        public void put(K key, V value){
            if (key == null) throw new RuntimeException("不能加null");

            NodeSBT<K, V> pre = findLeastKeyNode(key);
            if (pre !=null && key.compareTo(pre.key) == 0) {
                pre.value = value;//更新
                //同样的key来了,all必然变,我们要总体累加和的个数
                pre.all++;//size是不管的,统计不同的key
            }
            else root = add(pre, key, value);//往pre上直接挂
        }

        //傻逼树SBT是否真的包含某个节点key?
        public boolean continsKey(K key){
            //还是用findLeastKeyNode查询key,得到pre
            //保证pre不是null,且,key相同
            NodeSBT<K, V> pre = findLeastKeyNode(key);

            return pre != null && key.compareTo(pre.key) == 0;
        }

        //傻逼树SBT删除某个节点key的四种情况——从root查找,沿途size--,并删除那个节点key,返回删除之后的root
        public NodeSBT<K, V> delete(NodeSBT<K,V> cur, K key){
            //既然要删除,必然size--
            //删除key,必然伴随着all--
            cur.size--;//root必定减少1个节点key
            cur.all--;//root必定减少1个节点key
            //左右查找,删除key
            if (key.compareTo(cur.key) < 0) cur.l = delete(cur.l, key);
            else if (key.compareTo(cur.key) > 0)cur.r = delete(cur.r, key);
            else {
                //找到了key:4种删除的情况
                //(1)x节点没有左子,没有右子,直接废了x,让x=null
                if (cur.l == null && cur.r == null) cur = null;
                    //(2)x节点有左子,没有右子,让右子挂接在x的父节点上
                else if (cur.l != null && cur.r == null) cur = cur.l;
                    //(3)x节点没有左子,有右子,让x的右子直接挂在x的父节点上
                else if (cur.l == null && cur.r != null) cur = cur.r;
                    //(4)x节点有左子,有右子——这是最难搞的,
                    //**要让x的后继节点p来接替自己**,操作步骤如下:
                    //先把替身p=des的右树挂到替身p的父节点pre上。
                    //然后把x=cur的左树和右树挂到替身p=des的左树和右树上,
                    //然后让替身p来接替x
                    //完成删除x。
                else {
                    NodeSBT<K, V> pre = null;//pre记忆的是des的父节点
                    NodeSBT<K, V> des = cur.r;//去寻找cur右树的最左节点,即后继节点des
                    des.size--;
                    des.all--;
                    while (des.l != null){
                        //cur右树的最左节点就是后继节点,
                        pre = des;//pre记忆的是des的父节点
                        des = des.l;//往左窜
                        des.size--;//沿途大家size--
                        des.all--;//沿途大家all--
                    }
                    //知道des确实就是后继节点了
                    if(pre != null){//保证pre不空,否则没法 玩
                        pre.l = des.r;//让pre右接管des的右,因为后继节点des要溜了
                        des.r = cur.r;//最终des做老大,接管cur的右和左
                        des.l = cur.l;//des左子解cur左子,替换呗
                        //cur废了
                    }
                    //重新统计des的数量
                    des.size = des.l.size + (des.r != null ? des.r.size : 0) + 1;//自己
                    des.all = des.l.all + (des.r != null ? des.r.all : 0) + 1;//自己
                    cur = des;//新头
                }
            }

            return cur;//返回最后得root
        }

        //remove傻逼树SBT删除某个节点key
        public void remove(K key){
            //首先查 傻逼树SBT是否真的包含某个节点key?
            if (key == null) throw new RuntimeException("不能加null");

            //真的查到了,又从root查找key,沿途size--,并删除那个节点key,返回删除之后的root
            if (continsKey(key)) root = delete(root, key);
        }

    }

代码很多,但是你需要搞懂基础的傻逼树SBT原理,改起来就很容易了。


数组arr的子数组中,累加和s在a–b范围内的有几个?o(nlog(n))速度

请问你以i结尾的可能的子数组中?有多少个子数组的累加和s在a–b范围内
不妨设sumi是0–i范围内的累加和,则等价于问:
来到i处,i-1及之前的所有累加和中,有多少个是在si-b–si-a之间的
如果累加和小于si-b的有k1个,
小于si-a的有k2个
那i-1及之前的所有累加和中,在si-b–si-a间的就是k2-k1个呗!

然后,咱们让i=0,–N-1,每一个i,都求这么一个k2-k1,全部累加起来,不就是整体arr在a–b之间的达标累加和的个数吗?

手撕代码:

    //外围调度
    public static int howManySBT(int[] arr, int a, int b){
        if (arr == null || arr.length == 0) return 0;

        int N = arr.length;
        int ans = 0;
        int sum = 0;//0--i内的累加和

        //准备求和,放入SBT:KV都是整型数字
        SizeBalancedTree<Integer, Integer> sizeBalancedTree = new SizeBalancedTree<>();
        int value = 0;//value我们用不着。
        sizeBalancedTree.put(0, value);//累加和为0的放一个进去,value随意
        //这个技巧,经常用,累加和一开始就有个为0的状况

        //调度每一个i位置,以i结尾的子数组大情况
        //咱们让i=0,--N-1,每一个i,都求这么一个k2-k1,
        // 全部累加起来,不就是整体arr在a--b之间的达标累加和的个数吗?
        for (int i = 0; i < N; i++) {
            sum += arr[i];//累加和加入,别忘了
            //请问你以**i结尾的可能的子数组中**?有多少个子数组的**累加和s在a--b范围内**?
            //不妨设sumi是0--i范围内的累加和,则等价于问:
            //来到i处,**i-1及之前的所有累加和中,有多少个是在si-b--si-a之间的**?
            int k1 = sizeBalancedTree.lessThanKeyCount(sum - b);
            int k2 = sizeBalancedTree.lessThanKeyCount(sum - a + 1);//+1多推,a--b是闭区间
            //如果累加和小于si-b的有k1个,
            //小于si-a的有k2个
            //那i-1及之前的所有累加和中,在si-b--si-a间的就是k2-k1个呗!
            ans += k2 - k1;

            sizeBalancedTree.put(sum, value);
        }

        return ans;
    }

测试一把:

    public static void test(){
        int[] arr = {1,2,3,4};
        int lower = 3;
        int upper = 5;
        //【3-5】:12,3,23,4  答案是4,确实,
        System.out.println(howMany(arr, lower, upper));
        System.out.println(howManySBT(arr, lower, upper));

    }

    public static void main(String[] args) {
        test();
    }
3 5 3 4 
4
4

结果绝对没问题!!!
暴力解给你验证过了
都是4

复杂度计算:
外围调度o(n)
傻逼树SBT内部查找o(log(n))
故整体o(nlog(n))
速度贼快吧!!!!


总结

提示:重要经验:

1)有序表的底层实现原理:AVL树,傻逼树size Balanced Tree(SBT),红黑树,跳表,最重要的是傻逼树SBT,和跳表,在互联网大厂考题中,往往你需要手撕代码,或者改写。
2)本题最经典的就是转化题意,让前缀累加和帮你加速算法,同时SBT树中设计all和lessThanKey函数,统计小于x的累加和的数量总数,这样的话,大大加速了算法的运算。
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值