数组累加和七连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,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。