目录
《【算法笔记】有序表------AVL树》
《【算法笔记】有序表------SB树》
《【算法笔记】有序表------跳表》
《【算法笔记】有序表------相关题目》
1、有序表的改造思路
- 有序表,有序表是指在插入元素时,会根据元素的大小,自动将元素插入到合适的位置,从而保证了表中元素的有序性的结构。
- 面试场景中主要用到的有序表是:AVL树、红黑树、跳表、SB树等。
- 但是基础的这些结构只是说明了其实如何保持平衡和查找元素的,如果遇到特殊的需求,就要改写有序表,从而让其适配新的需求。
- 有序表的改造思路主要有以下几个方面:
1.1、如何处理重复元素的问题
- 正常的有序表中是不容许插入重复元素的,但是很多的题目中是有重复元素的,如果要用有序表的方式来做题,就要改造有序表,让其能够支持重复元素。
- 支持重复元素有两个方法,其一是给有序表的节点加入一个字段,用来记录当前节点的个数;其二是在SB树中,复用size字段,用一个封装结构来代替原来的key。
- 给节点加入一个字段的方式很容易理解,这里我们单独讲一个SB树中复用size字段的方式。
- 在SB树中,原来的size是指的以当前节点为头的子树的节点个数,并不支持重复字段,这个时候我们可以在调用SBT的时候,将key做一个封装,让其支持重复元素,
- 比如我们可以将数组中的元素封装成一个类,类中包含数组的下标index和值value,这样就可以先用value来排序,
- 在value相等的情况下用index排序,这样就可以支持重复元素了。
1.2、如何获取第i个元素,即用数组的方式操作有序表
- 正常的有序表是通过元素的大小比较来确定元素的位置的,但是如果要按照插入顺序的方式来操作有序表,我们就可以在SB树上进行改写,
- 具体的思路就是让原来节点大小的size字段,用来记录插入的顺序,这样的话,按照中序遍历的方式,节点的遍历顺序就代表了index。
- 对于某一个节点a,其左子树上都是比a插入早的元素,右子树上都是比a插入晚的元素,因为原来的size代表了节点的大小(包含重复元素),
- 这样右侧元素的下标就是当前节点的size + 右侧节点的size,而且左旋和右旋操作不影响这个顺序。
- 在插入元素的时候,对比需要操作的index,如果小于当前节点的size,就递归插入到左子树,否则递归插入到右子树。
1.3、如何获取小于某个值的元素个数
- 这个可以直接服用SB树的size,因为SBT是直接支持用index来获取第index个元素的,只是原始的不支持重复元素,
- 如果要支持重复元素,我们可以直接将添加到SBT中的元素做一个封装,比如数组中的元素,我们可以将下标index和值value做一个封装,封装类先用value排序,
- 在value相等的情况下用index排序,这样就利用SBT的结构天然支持重复元素和获取指定下标的元素了。,
- 对于需要查找小于某个树的节点个数,如果当前数小于当前节点的size,直接在左树上找即可,
- 如果当前数大于某个节点的size,则将当前的size累加,然后在右子树上找小于当前数-size的节点个数即可
2、题目一:区间和的个数
- 题目一:区间和的个数
- 给定一个数组arr,和两个整数a和b(a<=b)。求arr中有多少个子数组,累加和在[a,b]这个范围上。返回达标的子数组数量
- 测试链接:https://leetcode.cn/problems/count-of-range-sum
- 题目解析:
- 这种子数组的题目,有两种求解思路,算出来的结果是一样的:
- 1、从某个下标i开始,到结束统计有多少个子数组,
- 2、以某个下标i结束,从0开始统计有多少个子数组
- 两种思路只是考虑问题的方向不同,但是结果是一样的。
2.1、暴力方法加累加和数组的求法
- 暴力方法加累加和数组的求法
- 思路:
-
像这种求累加和的方式,可以将原来的数组做成一个累加和,然后根据公式求解:
-
累加和算法:
-
1、sum[j]表示从0到j的累加和
-
2、sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0...j] - sum[0...i-1]
-
提交时方法名改为:countRangeSum --> 会超时
-
java
/**
* 暴力方法加累加和数组的求法
* 思路:
* 像这种求累加和的方式,可以将原来的数组做成一个累加和,然后根据公式求解:
* 累加和算法:
* 1、sum[j]表示从0到j的累加和
* 2、sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1]
* <br>
* 提交时方法名改为:countRangeSum --> 会超时
*/
public static int countRangeSumWithSumArr(int[] arr, int lower, int upper) {
if (arr == null || arr.length == 0) {
return 0;
}
int res = 0;
// 构建前缀和数组,为了防止溢出,要用long
long[] sumArr = new long[arr.length];
sumArr[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
sumArr[i] = sumArr[i - 1] + arr[i];
}
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
long sum = 0;
if (i == 0) {
sum = sumArr[j];
} else {
sum = sumArr[j] - sumArr[i - 1];
}
if (sum >= lower && sum <= upper) {
res++;
}
}
}
return res;
}
2.2、改写SB树的求法
-
改写SB树的求法:
-
思路:
- 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0...j] - sum[0...i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
- 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
- 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0...j] - sum[0...i-1]也在[lower,upper]范围,
- 4.所以如果sum[0...j]为x,为了能让sum[0...j]满足[lower,upper]范围,那么sum[0...i-1]必须在[x-upper,x-lower]范围
- 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
- 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
-
利用SB树解决j前面有多少个累加和在[x-upper,x-lower]上的思路:
- 同样是利用累加和数组的方法,将累加和的数组依次加入到SB树中,然后求出小于等于某个值的个数,利用累加和数组的计算公式,就可以求出整体的差值,然后判断区间。
- 因为累加和数组不需要删除,所以只需要提供add和查询小于某个值的个数的方法即可。
- 同时,因为这里需要考虑重复值,我们可以用新增一个字段的方式,来记录真个树的值,方便查询。
- 这里用新增一个字段allCount的方式,和原来的size字段区分开,这样方便理解改造的过程。
- 某个节点的allCount就表示以该节点为根的树,所有节点的累加和的个数。
- 这样,这个节点的值的个数就是allCount - (左子树的allCount) - (右子树的allCount)
-
过程:
-
1、新建一个改造的SB树结构,用来插入和查询小于某个值的元素
-
2、用一个变量sum来记录累计到当前位置的累加和,到了某个位置i,此时的问题就变成了必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
-
3、从0开始遍历数组,每到一个位置,累加和sun累计当前的值arr[i],然后计算出新的需要判断的边界sum-upper和sum-lower
-
4、用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
-
因为要求的是[sum-upper,sum-lower]这个闭区间的范围,这个范围要想包含sum-upper的边界,就要减去小于sum-upper的值,即不能包含sum-upper,否则就会少算相等的一部分值。
-
因为SB树查询的就是小于某个树的个数,所以问题就转成了从SB中查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
-
提交时方法名改为:countRangeSum
-
java
/**
* 改写SB树的求法:
* 思路:
* 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
* 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
* 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
* 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
* 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
* 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
* <br>
* 利用SB树解决j前面有多少个累加和在[x-upper,x-lower]上的思路:
* 同样是利用累加和数组的方法,将累加和的数组依次加入到SB树中,然后求出小于等于某个值的个数,利用累加和数组的计算公式,就可以求出整体的差值,然后判断区间。
* 因为累加和数组不需要删除,所以只需要提供add和查询小于某个值的个数的方法即可。
* 同时,因为这里需要考虑重复值,我们可以用新增一个字段的方式,来记录真个树的值,方便查询。
* 这里用新增一个字段allCount的方式,和原来的size字段区分开,这样方便理解改造的过程。
* 某个节点的allCount就表示以该节点为根的树,所有节点的累加和的个数。
* 这样,这个节点的值的个数就是allCount - (左子树的allCount) - (右子树的allCount)
* <br>
* 过程:
* 1、新建一个改造的SB树结构,用来插入和查询小于某个值的元素
* 2、用一个变量sum来记录累计到当前位置的累加和,到了某个位置i,此时的问题就变成了必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
* 3、从0开始遍历数组,每到一个位置,累加和sun累计当前的值arr[i],然后计算出新的需要判断的边界sum-upper和sum-lower
* 4、用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
* 因为要求的是[sum-upper,sum-lower]这个闭区间的范围,这个范围要想包含sum-upper的边界,就要减去小于sum-upper的值,即不能包含sum-upper,否则就会少算相等的一部分值。
* 因为SB树查询的就是小于某个树的个数,所以问题就转成了从SB中查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
* <br>
* 提交时方法名改为:countRangeSum
*/
public static int countRangeSumWithSizeBalanced(int[] arr, int lower, int upper) {
if (arr == null || arr.length == 0 || lower > upper) {
return 0;
}
// SB的改造结构
SizeBalancedTreeSet sbt = new SizeBalancedTreeSet();
// 累计和的变量
long sum = 0;
// 记录最终的结果
int ans = 0;
// 一个数都没有的时候,就已经有一个前缀和累加和为0
sbt.add(0);
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
// 计算出新的需要判断的边界sum-upper和sum-lower
long lowerBound = sum - upper;
long upperBound = sum - lower + 1;
// 用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
ans += sbt.lessKeySize(upperBound) - sbt.lessKeySize(lowerBound);
// 将当前的累加和加入到SB树中
sbt.add(sum);
}
return ans;
}
/**
* 改写的SB树,新增一个字段allCount,记录以该节点为根的树,所有节点的累加和的个数。
*/
private static class SizeBalancedTreeSet {
// root节点
private Node root;
// 用一个set来快速判断某个值有没有添加过
private Set<Long> set = new HashSet<>();
/**
* 右旋
*/
private Node rightRotate(Node cur) {
if (cur == null || cur.left == null) {
return cur;
}
// 先记录当前节点的相同的个数
int sameCount = cur.allCount - cur.left.allCount - (cur.right != null ? cur.right.allCount : 0);
Node leftNode = cur.left;
cur.left = leftNode.right;
leftNode.right = cur;
// 更新size,旋转对总的size不变,所以此时根节点的size就是原来cur的size
leftNode.size = cur.size;
cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
// 更新allCount,旋转对节点数量不变,所以此时的根节点leftNode的allCount是原来的cur的allCount
leftNode.allCount = cur.allCount;
cur.allCount = (cur.left != null ? cur.left.allCount : 0) + sameCount + (cur.right != null ? cur.right.allCount : 0);
return leftNode;
}
/**
* 左旋
*/
private Node leftRotate(Node cur) {
if (cur == null || cur.right == null) {
return cur;
}
int sameCount = cur.allCount - cur.right.allCount - (cur.left != null ? cur.left.allCount : 0);
Node rightNode = cur.right;
cur.right = rightNode.left;
rightNode.left = cur;
// 更新size
rightNode.size = cur.size;
cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
// 更新allCount
rightNode.allCount = cur.allCount;
cur.allCount = sameCount + (cur.right != null ? cur.right.allCount : 0) + (cur.left != null ? cur.left.allCount : 0);
return rightNode;
}
/**
* 平衡调整
*/
private Node maintain(Node cur) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
if (leftLeftSize > rightSize) {
// LL
cur = rightRotate(cur);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) {
// LR
cur.left = leftRotate(cur.left);
cur = rightRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) {
// RR
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) {
// RL
cur.right = rightRotate(cur.right);
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
}
return cur;
}
/**
* 递归添加元素
*/
private Node add(Node cur, long key, boolean contains) {
if (cur == null) {
return new Node(key);
}
cur.allCount++;
// key相同,直接返回
if (key == cur.key) {
return cur;
}
// 递归添加,先判断是否已经存在,如果已经存在,添加的时候只需要更新allCount字段,不需要更新size字段
if (!contains) {
cur.size++;
}
if (key < cur.key) {
cur.left = add(cur.left, key, contains);
} else {
cur.right = add(cur.right, key, contains);
}
return maintain(cur);
}
/**
* 添加元素
*/
public void add(long key) {
boolean contains = set.contains(key);
// 调用递归add方法添加
this.root = add(this.root, key, contains);
if (!contains) {
this.set.add(key);
}
}
/**
* 获得小于某个key的个数
*/
public int lessKeySize(long key) {
Node cur = root;
int ans = 0;
while (cur != null) {
if (key == cur.key) {
// 相等,累计上左侧的值即可
return ans + (cur.left != null ? cur.left.allCount : 0);
} else if (key < cur.key) {
// 小于的话,从左侧查找
cur = cur.left;
} else {
// 累计上左侧和当前的值,往右查找,
// 累计左侧和当前的重复值,就是用当前的减去右侧的值
ans += cur.allCount - (cur.right != null ? cur.right.allCount : 0);
cur = cur.right;
}
}
return ans;
}
class Node {
private long key;
private Node left;
private Node right;
private int size;
// 以该节点为根的树,所有节点的累加和的个数
private int allCount;
public Node(long key) {
this.key = key;
this.size = 1;
this.allCount = 1;
}
}
}
2.3、改写归并排序的方法
-
改写归并排序的方法:
-
思路:
- 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0...j] - sum[0...i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
- 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
- 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0...j] - sum[0...i-1]也在[lower,upper]范围,
- 4.所以如果sum[0...j]为x,为了能让sum[0...j]满足[lower,upper]范围,那么sum[0...i-1]必须在[x-upper,x-lower]范围
- 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
- 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
-
- 用归并排序方法求解,对sum数组进行排序,在分解前,就可以求出arr[0...i]是否满足,即sum[i]是不是在[lower,upper]范围,
-
- 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
-
- 最后返回所有的个数和,就是求解的个数
-
过程:
*- 先求前缀和数组sum,sum[i]表示arr[0...i]的累加和
-
- 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围,结束递归的条件L==R,判断sum[L]是否达标,代表了arr[0...L]这个数组上的累加和是不是达标
-
- 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
-
- 最后返回所有的个数和,就是求解的个数
-
提交时方法名改为:countRangeSum
java
/**
* 改写归并排序的方法:
* 思路:
* 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
* 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
* 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
* 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
* 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
* 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
* 7. 用归并排序方法求解,对sum数组进行排序,在分解前,就可以求出arr[0...i]是否满足,即sum[i]是不是在[lower,upper]范围,
* 8. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
* 9. 最后返回所有的个数和,就是求解的个数
* <br>
* 过程:
* 1. 先求前缀和数组sum,sum[i]表示arr[0...i]的累加和
* 2. 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围,结束递归的条件L==R,判断sum[L]是否达标,代表了arr[0...L]这个数组上的累加和是不是达标
* 3. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
* 4. 最后返回所有的个数和,就是求解的个数
* <br>
* 提交时方法名改为:countRangeSum
*/
public static int countRangeSumWithMergeSort(int[] arr, int lower, int upper) {
if (arr == null || arr.length == 0) {
return 0;
}
// 构建前缀和数组,为了防止溢出,要用long类型
long[] sum = new long[arr.length];
sum[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
sum[i] = sum[i - 1] + arr[i];
}
// 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
return process(sum, 0, sum.length - 1, lower, upper);
}
/**
* 递归处理函数:
* 求在arr[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
* 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
*/
private static int process(long[] sum, int L, int R, int lower, int upper) {
if (L == R) {
// 代表arr[0...L]这个数组上的累加和是不是达标
return sum[L] >= lower && sum[L] <= upper ? 1 : 0;
}
// 递归处理左右两边
int M = L + ((R - L) >> 1);
return process(sum, L, M, lower, upper) + process(sum, M + 1, R, lower, upper)
+ merge(sum, L, M, R, lower, upper);
}
/**
* 合并函数:
* 求在arr[L...M]和arr[M+1...R]合并的过程中,求出多少个累加和在[lower,upper]范围
* 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
* 在合并前,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
*/
public static int merge(long[] sum, int L, int M, int R, int lower, int upper) {
int ans = 0;
int windowL = L;
int windowR = L;
/**
* 先求出右组中每个数x,左组中有多少个数,位于[x-upper,x-lower]范围
* [windowL, windowR)
* 因为左组和右组都是有序的,可以用滑动窗口的方法,求左组中有多少个数,位于[x-upper,x-lower]范围
*/
for (int i = M + 1; i <= R; i++) {
long min = sum[i] - upper;
long max = sum[i] - lower;
// 先移动windowR,找到第一个大于max的位置
while (windowR <= M && sum[windowR] <= max) {
windowR++;
}
// 再移动windowL,找到第一个大于等于min的位置
while (windowL <= M && sum[windowL] < min) {
windowL++;
}
ans += windowR - windowL;
}
// 正常合并
long[] help = new long[R - L + 1];
int index = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[index++] = sum[p1] <= sum[p2] ? sum[p1++] : sum[p2++];
}
while (p1 <= M) {
help[index++] = sum[p1++];
}
while (p2 <= R) {
help[index++] = sum[p2++];
}
for (int i = 0; i < help.length; i++) {
sum[L + i] = help[i];
}
return ans;
}
整体代码和测试如下:
java
import java.util.HashSet;
import java.util.Set;
/**
* 题目一:区间和的个数
* 给定一个数组arr,和两个整数a和b(a<=b)。求arr中有多少个子数组,累加和在[a,b]这个范围上。返回达标的子数组数量
* 测试链接:https://leetcode.cn/problems/count-of-range-sum
* 题目解析:
* 这种子数组的题目,有两种求解思路,算出来的结果是一样的:
* 1、从某个下标i开始,到结束统计有多少个子数组,
* 2、以某个下标i结束,从0开始统计有多少个子数组
* 两种思路只是考虑问题的方向不同,但是结果是一样的。
*/
public class Q1_CountofRangeSum {
/**
* 暴力方法加累加和数组的求法
* 思路:
* 像这种求累加和的方式,可以将原来的数组做成一个累加和,然后根据公式求解:
* 累加和算法:
* 1、sum[j]表示从0到j的累加和
* 2、sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1]
* <br>
* 提交时方法名改为:countRangeSum --> 会超时
*/
public static int countRangeSumWithSumArr(int[] arr, int lower, int upper) {
if (arr == null || arr.length == 0) {
return 0;
}
int res = 0;
// 构建前缀和数组,为了防止溢出,要用long
long[] sumArr = new long[arr.length];
sumArr[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
sumArr[i] = sumArr[i - 1] + arr[i];
}
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
long sum = 0;
if (i == 0) {
sum = sumArr[j];
} else {
sum = sumArr[j] - sumArr[i - 1];
}
if (sum >= lower && sum <= upper) {
res++;
}
}
}
return res;
}
/**
* 改写SB树的求法:
* 思路:
* 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
* 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
* 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
* 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
* 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
* 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
* <br>
* 利用SB树解决j前面有多少个累加和在[x-upper,x-lower]上的思路:
* 同样是利用累加和数组的方法,将累加和的数组依次加入到SB树中,然后求出小于等于某个值的个数,利用累加和数组的计算公式,就可以求出整体的差值,然后判断区间。
* 因为累加和数组不需要删除,所以只需要提供add和查询小于某个值的个数的方法即可。
* 同时,因为这里需要考虑重复值,我们可以用新增一个字段的方式,来记录真个树的值,方便查询。
* 这里用新增一个字段allCount的方式,和原来的size字段区分开,这样方便理解改造的过程。
* 某个节点的allCount就表示以该节点为根的树,所有节点的累加和的个数。
* 这样,这个节点的值的个数就是allCount - (左子树的allCount) - (右子树的allCount)
* <br>
* 过程:
* 1、新建一个改造的SB树结构,用来插入和查询小于某个值的元素
* 2、用一个变量sum来记录累计到当前位置的累加和,到了某个位置i,此时的问题就变成了必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
* 3、从0开始遍历数组,每到一个位置,累加和sun累计当前的值arr[i],然后计算出新的需要判断的边界sum-upper和sum-lower
* 4、用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
* 因为要求的是[sum-upper,sum-lower]这个闭区间的范围,这个范围要想包含sum-upper的边界,就要减去小于sum-upper的值,即不能包含sum-upper,否则就会少算相等的一部分值。
* 因为SB树查询的就是小于某个树的个数,所以问题就转成了从SB中查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
* <br>
* 提交时方法名改为:countRangeSum
*/
public static int countRangeSumWithSizeBalanced(int[] arr, int lower, int upper) {
if (arr == null || arr.length == 0 || lower > upper) {
return 0;
}
// SB的改造结构
SizeBalancedTreeSet sbt = new SizeBalancedTreeSet();
// 累计和的变量
long sum = 0;
// 记录最终的结果
int ans = 0;
// 一个数都没有的时候,就已经有一个前缀和累加和为0
sbt.add(0);
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
// 计算出新的需要判断的边界sum-upper和sum-lower
long lowerBound = sum - upper;
long upperBound = sum - lower + 1;
// 用SB树查询小于等于sum-lower + 1的个数,减去小于等于sum-upper的个数,就是必须以i为结尾的子数组中,有多少个前缀和在[sum-upper,sum-lower]上
ans += sbt.lessKeySize(upperBound) - sbt.lessKeySize(lowerBound);
// 将当前的累加和加入到SB树中
sbt.add(sum);
}
return ans;
}
/**
* 改写的SB树,新增一个字段allCount,记录以该节点为根的树,所有节点的累加和的个数。
*/
private static class SizeBalancedTreeSet {
// root节点
private Node root;
// 用一个set来快速判断某个值有没有添加过
private Set<Long> set = new HashSet<>();
/**
* 右旋
*/
private Node rightRotate(Node cur) {
if (cur == null || cur.left == null) {
return cur;
}
// 先记录当前节点的相同的个数
int sameCount = cur.allCount - cur.left.allCount - (cur.right != null ? cur.right.allCount : 0);
Node leftNode = cur.left;
cur.left = leftNode.right;
leftNode.right = cur;
// 更新size,旋转对总的size不变,所以此时根节点的size就是原来cur的size
leftNode.size = cur.size;
cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
// 更新allCount,旋转对节点数量不变,所以此时的根节点leftNode的allCount是原来的cur的allCount
leftNode.allCount = cur.allCount;
cur.allCount = (cur.left != null ? cur.left.allCount : 0) + sameCount + (cur.right != null ? cur.right.allCount : 0);
return leftNode;
}
/**
* 左旋
*/
private Node leftRotate(Node cur) {
if (cur == null || cur.right == null) {
return cur;
}
int sameCount = cur.allCount - cur.right.allCount - (cur.left != null ? cur.left.allCount : 0);
Node rightNode = cur.right;
cur.right = rightNode.left;
rightNode.left = cur;
// 更新size
rightNode.size = cur.size;
cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
// 更新allCount
rightNode.allCount = cur.allCount;
cur.allCount = sameCount + (cur.right != null ? cur.right.allCount : 0) + (cur.left != null ? cur.left.allCount : 0);
return rightNode;
}
/**
* 平衡调整
*/
private Node maintain(Node cur) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
if (leftLeftSize > rightSize) {
// LL
cur = rightRotate(cur);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) {
// LR
cur.left = leftRotate(cur.left);
cur = rightRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) {
// RR
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) {
// RL
cur.right = rightRotate(cur.right);
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
}
return cur;
}
/**
* 递归添加元素
*/
private Node add(Node cur, long key, boolean contains) {
if (cur == null) {
return new Node(key);
}
cur.allCount++;
// key相同,直接返回
if (key == cur.key) {
return cur;
}
// 递归添加,先判断是否已经存在,如果已经存在,添加的时候只需要更新allCount字段,不需要更新size字段
if (!contains) {
cur.size++;
}
if (key < cur.key) {
cur.left = add(cur.left, key, contains);
} else {
cur.right = add(cur.right, key, contains);
}
return maintain(cur);
}
/**
* 添加元素
*/
public void add(long key) {
boolean contains = set.contains(key);
// 调用递归add方法添加
this.root = add(this.root, key, contains);
if (!contains) {
this.set.add(key);
}
}
/**
* 获得小于某个key的个数
*/
public int lessKeySize(long key) {
Node cur = root;
int ans = 0;
while (cur != null) {
if (key == cur.key) {
// 相等,累计上左侧的值即可
return ans + (cur.left != null ? cur.left.allCount : 0);
} else if (key < cur.key) {
// 小于的话,从左侧查找
cur = cur.left;
} else {
// 累计上左侧和当前的值,往右查找,
// 累计左侧和当前的重复值,就是用当前的减去右侧的值
ans += cur.allCount - (cur.right != null ? cur.right.allCount : 0);
cur = cur.right;
}
}
return ans;
}
class Node {
private long key;
private Node left;
private Node right;
private int size;
// 以该节点为根的树,所有节点的累加和的个数
private int allCount;
public Node(long key) {
this.key = key;
this.size = 1;
this.allCount = 1;
}
}
}
/**
* 改写归并排序的方法:
* 思路:
* 1.累加和算法:sum[i...j]表示从i到j的累加和,则sum[i...j] = sum[0..j] - sum[0..i-1],所以用一个辅助数组,i位置表示0...i的累加和,就可以计算出任何两个位置的累加和
* 2.子数组的转换,原先的求法是用i开始,到n-1,有多少子数组,可以转化为以n-1为结尾,从0开始有多少个子数组,在整个数组上的子数组是一样的
* 3.经过转换,如果sum[i...j]在[lower,upper]范围,那么sum[0..j] - sum[0..i-1]也在[lower,upper]范围,
* 4.所以如果sum[0..j]为x,为了能让sum[0..j]满足[lower,upper]范围,那么sum[0..i-1]必须在[x-upper,x-lower]范围
* 5.假设sum[0...j]的值为x,所以问题就转化为:必须以j位置结尾的子数组中,j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上
* 6.所以问题就转化为在辅助数组sum上,求j之前的所有前缀和中有多少个前缀和在[x-upper,x-lower]上的问题。
* 7. 用归并排序方法求解,对sum数组进行排序,在分解前,就可以求出arr[0...i]是否满足,即sum[i]是不是在[lower,upper]范围,
* 8. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
* 9. 最后返回所有的个数和,就是求解的个数
* <br>
* 过程:
* 1. 先求前缀和数组sum,sum[i]表示arr[0...i]的累加和
* 2. 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围,结束递归的条件L==R,判断sum[L]是否达标,代表了arr[0...L]这个数组上的累加和是不是达标
* 3. 在合并[L...M]和[M+1...R]时,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
* 4. 最后返回所有的个数和,就是求解的个数
* <br>
* 提交时方法名改为:countRangeSum
*/
public static int countRangeSumWithMergeSort(int[] arr, int lower, int upper) {
if (arr == null || arr.length == 0) {
return 0;
}
// 构建前缀和数组,为了防止溢出,要用long类型
long[] sum = new long[arr.length];
sum[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
sum[i] = sum[i - 1] + arr[i];
}
// 递归处理sum数组,求在sum[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
return process(sum, 0, sum.length - 1, lower, upper);
}
/**
* 递归处理函数:
* 求在arr[L...R]范围上,有多少个子数组的累加和在[lower,upper]范围
* 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
*/
private static int process(long[] sum, int L, int R, int lower, int upper) {
if (L == R) {
// 代表arr[0...L]这个数组上的累加和是不是达标
return sum[L] >= lower && sum[L] <= upper ? 1 : 0;
}
// 递归处理左右两边
int M = L + ((R - L) >> 1);
return process(sum, L, M, lower, upper) + process(sum, M + 1, R, lower, upper)
+ merge(sum, L, M, R, lower, upper);
}
/**
* 合并函数:
* 求在arr[L...M]和arr[M+1...R]合并的过程中,求出多少个累加和在[lower,upper]范围
* 因为已经求了累加和sum数组,所以直接用传入sum就可以了,不需要再传arr数组
* 在合并前,对于右组中的每个数x,求左组中有多少个数,位于[x-upper,x-lower]范围,然后正常merge
*/
public static int merge(long[] sum, int L, int M, int R, int lower, int upper) {
int ans = 0;
int windowL = L;
int windowR = L;
/**
* 先求出右组中每个数x,左组中有多少个数,位于[x-upper,x-lower]范围
* [windowL, windowR)
* 因为左组和右组都是有序的,可以用滑动窗口的方法,求左组中有多少个数,位于[x-upper,x-lower]范围
*/
for (int i = M + 1; i <= R; i++) {
long min = sum[i] - upper;
long max = sum[i] - lower;
// 先移动windowR,找到第一个大于max的位置
while (windowR <= M && sum[windowR] <= max) {
windowR++;
}
// 再移动windowL,找到第一个大于等于min的位置
while (windowL <= M && sum[windowL] < min) {
windowL++;
}
ans += windowR - windowL;
}
// 正常合并
long[] help = new long[R - L + 1];
int index = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[index++] = sum[p1] <= sum[p2] ? sum[p1++] : sum[p2++];
}
while (p1 <= M) {
help[index++] = sum[p1++];
}
while (p2 <= R) {
help[index++] = sum[p2++];
}
for (int i = 0; i < help.length; i++) {
sum[L + i] = help[i];
}
return ans;
}
public static void main(String[] args) {
// 测试次数
int testTime = 500000;
// 数组最大长度
int maxSize = 100;
// 数组最大值
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr = generateRandomArr(maxSize, maxValue);
int temp1 = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
int temp2 = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
int lower = Math.min(temp1, temp2);
int upper = Math.max(temp1, temp2);
int countSum1 = countRangeSumWithSumArr(copyArr(arr), lower, upper);
int countSum2 = countRangeSumWithSizeBalanced(copyArr(arr), lower, upper);
int countSum3 = countRangeSumWithMergeSort(copyArr(arr), lower, upper);
if (countSum1 != countSum2 || countSum1 != countSum3) {
succeed = false;
System.out.println("原数组:");
printArray(arr);
System.out.printf("lowper:%d,upper:%d,暴力累加和方法:%d,SB方法:%d,归并排序方法:%d \n", lower, upper, countSum1, countSum2, countSum3);
break;
}
}
System.out.println(succeed ? "successful!" : "error!");
}
public static int[] generateRandomArr(int maxSize, int maxValue) {
// Math.random() -> [0,1) 所有的小数,等概率返回一个
// Math.random() * N -> [0,N) 所有小数,等概率返回一个
// (int)(Math.random() * N) -> [0,N-1] 所有的整数,等概率返回一个
// (int)(Math.random() * (maxSize + 1)) -> [0,maxSize] 所有的整数,等概率返回一个
int size = (int) (Math.random() * (maxSize + 1));
int[] arr = new int[size];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
}
return arr;
}
public static int[] copyArr(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
3、题目二:窗口的中位数
- 题目二:窗口的中位数
- 给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。
- 窗口中有 k 个数,每次窗口向右移动 1 位。
- 你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
- 中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
- 测试链接:https://leetcode.cn/problems/sliding-window-median
3.1、使用SBT的方法求解
- 使用SBT的方法求解
- 思路:
- 本题的要求是在一个窗口里面,求中位数,这就需要两个过程:
- 1、加入和删除某个数字的时候,要排序
- 2、要能根据下标取到对应位置的值,这样的话,就能求到中位数。
- 大顶堆和小顶堆能根据元素来维护有序性,但是无法取到指定位置的数,所以无法满足要求,
- 我们可以根据SBT来改造成这样的结构。
- 首先,SBT本来就天生支持根据某个index来取值,因为通过其对size的计算,就可以获取到第index的位置,
- 其次,SBT中的数据是有序的,我们只需要中序遍历数组,就能获得有序的数组。
- 所以SBT是天然支持这两个要求的,我们需要的是如何将本题目的要求和SBT的结构相匹配。
- 我们可以将题目中的数组的下标index和值value封装成一个类,
- 这个类实现Comparable接口,在value不同的时候用value排序,在value相同的时候,用index排序,
- 这样既实现了排序的问题,也适配了可能出现的重复value的情况。
- 然后我们在SBT中只维护k个元素,每次都是取其中间的位置的值(如果是奇数取1个,偶数取2个),
- 然后就可以根据取出的值的value计算出平均数,就是需要的中位数了。
整体代码如下:
java
/**
* 题目二:窗口的中位数
* 给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。
* 窗口中有 k 个数,每次窗口向右移动 1 位。
* 你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
* 中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
* 测试链接:https://leetcode.cn/problems/sliding-window-median
*/
public class Q2_SlidingWindowMedian {
/**
* 使用SBT的方法求解
* 思路:
* 本题的要求是在一个窗口里面,求中位数,这就需要两个过程:
* 1、加入和删除某个数字的时候,要排序
* 2、要能根据下标取到对应位置的值,这样的话,就能求到中位数。
* 大顶堆和小顶堆能根据元素来维护有序性,但是无法取到指定位置的数,所以无法满足要求,
* 我们可以根据SBT来改造成这样的结构。
* 首先,SBT本来就天生支持根据某个index来取值,因为通过其对size的计算,就可以获取到第index的位置,
* 其次,SBT中的数据是有序的,我们只需要中序遍历数组,就能获得有序的数组。
* 所以SBT是天然支持这两个要求的,我们需要的是如何将本题目的要求和SBT的结构相匹配。
* 我们可以将题目中的数组的下标index和值value封装成一个类,
* 这个类实现Comparable接口,在value不同的时候用value排序,在value相同的时候,用index排序,
* 这样既实现了排序的问题,也适配了可能出现的重复value的情况。
* 然后我们在SBT中只维护k个元素,每次都是取其中间的位置的值(如果是奇数取1个,偶数取2个),
* 然后就可以根据取出的值的value计算出平均数,就是需要的中位数了。
*/
public static double[] medianSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || k <= 0) {
return new double[0];
}
// 新定义一个根据value和index比较的节点类
class QNode implements Comparable<QNode> {
private final int index;
private final int value;
public QNode(int index, int value) {
this.index = index;
this.value = value;
}
@Override
public int compareTo(QNode o) {
// 先用value排序,后用index排序
return value != o.value ? Integer.compare(value, o.value)
: Integer.compare(index, o.index);
}
}
// 新建SBT结构,并将前k-1个数加入到SBT中
SizeBalancedTree<QNode> sbt = new SizeBalancedTree<>();
for (int i = 0; i < k - 1; i++) {
sbt.add(new QNode(i, nums[i]));
}
double[] ans = new double[nums.length - k + 1];
int index = 0;
// 从k-1下标开始,获取中位数并加到ans中
// 因为前面已经加了k-1个数,再加一个数就满足k个数的区间了
for (int i = k - 1; i < nums.length; i++) {
sbt.add(new QNode(i, nums[i]));
// 根据个数计算中位数
int size = sbt.size();
if (size % 2 == 0) {
// 偶数个,取中间的2个数的平均数,getIndex是从0开始的,所以偶数个的上中位数下标为 size/2,下中位数下标为 size/2-1
QNode downMid = sbt.getIndexKey(size / 2 - 1);
QNode upMid = sbt.getIndexKey(size / 2);
ans[index++] = ((double) downMid.value + (double) upMid.value) / 2.0;
} else {
// 奇数个,取中间的数
QNode mid = sbt.getIndexKey(size / 2);
ans[index++] = mid.value;
}
// 删除掉最前面的数,准备下次添加
sbt.remove(new QNode(i - k + 1, nums[i - k + 1]));
}
return ans;
}
/**
* 改写的SB树结构
* 因为使用了泛型,所以这个结构和平常的SBT没有区别
*/
static class SizeBalancedTree<K extends Comparable<K>> {
private Node<K> root;
public int size() {
return root == null ? 0 : root.size;
}
public boolean containsKey(K key) {
if (key == null) {
return false;
}
Node<K> lastIndex = findLastIndex(key);
return lastIndex != null && key.compareTo(lastIndex.key) == 0;
}
public void add(K key) {
if (key == null) {
return;
}
// 不存在加
if (!containsKey(key)) {
root = add(root, key);
}
}
public void remove(K key) {
if (key == null) {
return;
}
if (containsKey(key)) {
root = delete(root, key);
}
}
/**
* 获取第index(从0开始)的元素
*/
public K getIndexKey(int index) {
if (index < 0 || index >= size()) {
return null;
}
return getIndex(root, index + 1).key;
}
private Node<K> rightRotate(Node<K> cur) {
if (cur == null || cur.left == null) {
return cur;
}
Node<K> leftNode = cur.left;
cur.left = leftNode.right;
leftNode.right = cur;
leftNode.size = cur.size;
cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
return leftNode;
}
private Node<K> leftRotate(Node<K> cur) {
if (cur == null || cur.right == null) {
return cur;
}
Node<K> rightNode = cur.right;
cur.right = rightNode.left;
rightNode.left = cur;
rightNode.size = cur.size;
cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
return rightNode;
}
private Node<K> maintain(Node<K> cur) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
if (leftLeftSize > rightSize) {
// LL
cur = rightRotate(cur);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) {
// LR
cur.left = leftRotate(cur.left);
cur = rightRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) {
// RR
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) {
// RL
cur.right = rightRotate(cur.right);
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
}
return cur;
}
private Node<K> findLastIndex(K key) {
if (root == null) {
return null;
}
Node<K> pre = root;
Node<K> cur = root;
while (cur != null) {
pre = cur;
if (key.compareTo(cur.key) == 0) {
break;
} else if (key.compareTo(cur.key) < 0) {
cur = cur.left;
} else {
cur = cur.right;
}
}
return pre;
}
private Node<K> add(Node<K> cur, K key) {
if (cur == null) {
return new Node<>(key);
}
cur.size++;
if (key.compareTo(cur.key) < 0) {
cur.left = add(cur.left, key);
} else {
cur.right = add(cur.right, key);
}
return maintain(cur);
}
private Node<K> delete(Node<K> cur, K key) {
cur.size--;
if (key.compareTo(cur.key) < 0) {
cur.left = delete(cur.left, key);
} else if (key.compareTo(cur.key) > 0) {
cur.right = delete(cur.right, key);
} else {
// 当前节点就是要删除的节点
if (cur.left == null && cur.right == null) {
cur = null;
} else if (cur.left == null) {
cur = cur.right;
} else if (cur.right == null) {
cur = cur.left;
} else {
// 左右子树都有节点,找到右侧最小的节点
// 前一个节点,因为要重新计算size,所以需要有前一个节点的变量
Node<K> pre = null;
Node<K> des = cur.right;
des.size--;
while (des.left != null) {
pre = des;
des = des.left;
des.size--;
}
if (pre != null) {
// 删掉des的left,因为pre肯定没有left节点了
pre.left = des.right;
// 这一句要放到这里,因为如果pre为null,代表cur.right没有左节点,直接接cur.left即可
des.right = cur.right;
}
des.left = cur.left;
des.size = des.left.size + (des.right == null ? 0 : des.right.size) + 1;
cur = des;
}
}
return cur;
}
/**
* 从当前子树获得第k个节点
*/
private Node<K> getIndex(Node<K> cur, int kth) {
if (cur == null) {
return null;
}
if (kth == (cur.left != null ? cur.left.size : 0) + 1) {
// 要的是左子树+1的节点,也就是当前节点
return cur;
} else if (kth <= (cur.left != null ? cur.left.size : 0)) {
// 要的是左子树的第kth个节点
return getIndex(cur.left, kth);
} else {
// 要的是右子树的第k个节点,需要转换右侧的索引
return getIndex(cur.right, kth - (cur.left != null ? cur.left.size : 0) - 1);
}
}
/**
* SBT树的内部节点
*/
static class Node<K extends Comparable<K>> {
private final K key;
private int size;
private Node<K> left;
private Node<K> right;
public Node(K key) {
this.key = key;
this.size = 1;
}
}
}
public static void main(String[] args) {
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
double[] ans = medianSlidingWindow(nums, k);
for (double v : ans) {
System.out.println(v);
}
}
}
4、题目三:设计一个保持时序的列表
- 题目三:设计一个保持时序的列表
- 设计一个结构包含如下两个方法:
- void add(int index, int num):把num加入到index位置
- int get(int index) :取出index位置的值
- void remove(int index) :把index位置上的值删除
- 要求三个方法时间复杂度O(logN)
4.1、使用SBT的方法求解
- 用SBT改写的支持用所以增加、查询、删除操作的方法
- 思路:
- SB树是支持index查询的,但是不支持index插入和删除,这是因为SB树是根据key的值来做比较的,
- 如果我们用插入顺序来做逻辑的key,就可以直接复用size字段来做已经有的节点的index,
- 在插入和删除的时候,根据传入的index字段和size进行对比,就能计算出对应的逻辑key,
- 同时,因为是用的逻辑key做的比较,相同的value是作为不同的节点存在于树中的,所以改写的SBT就天然支持重复元素。
- 具体参照实现的类SbtList
java
/**
* 用SBT改写的支持用所以增加、查询、删除操作的方法
* 思路:
* SB树是支持index查询的,但是不支持index插入和删除,这是因为SB树是根据key的值来做比较的,
* 如果我们用插入顺序来做逻辑的key,就可以直接复用size字段来做已经有的节点的index,
* 在插入和删除的时候,根据传入的index字段和size进行对比,就能计算出对应的逻辑key,
* 同时,因为是用的逻辑key做的比较,相同的value是作为不同的节点存在于树中的,所以改写的SBT就天然支持重复元素。
* 具体参照实现的类SbtList
*/
public static class SbtList<V> {
private Node<V> root;
public void add(int index, V num) {
Node<V> node = new Node<>(num);
if (root == null) {
root = node;
} else {
if (index <= root.size) {
// 只有符合条件的index才能添加
root = add(root, index, node);
}
}
}
public V get(int index) {
if (index >= 0 && size() > index) {
Node<V> ans = get(root, index);
return ans.value;
}
return null;
}
public void remove(int index) {
if (index >= 0 && size() > index) {
root = delete(root, index);
}
}
public int size() {
return root == null ? 0 : root.size;
}
/**
* 递归给当前节点cur的子树添加index(从0开始)位置的node节点
* 当要替换掉一个已经存在的index位置的时候,新加入的节点实际上还是会加到合适的最底层,
* 但是由于新加入的节点的存在,在中序遍历中会占据index的位置,让后面的节点往后移动,所以自然是符合时序的
*/
private Node<V> add(Node<V> cur, int index, Node<V> node) {
// 到最后为null的节点了,直接将node放到这个位置
if (cur == null) {
return node;
}
cur.size++;
// 利用size计算出当前cur节点的左侧和本身的长度
int leftAndHeadSize = (cur.left != null ? cur.left.size : 0) + 1;
// 用index判断需要插入到什么位置
if (index < leftAndHeadSize) {
// 插入到左子树
cur.left = add(cur.left, index, node);
} else {
// 插入到右子树,需要重新计算下个位置的坐标,
cur.right = add(cur.right, index - leftAndHeadSize, node);
}
// 维护平衡性
return maintain(cur);
}
/**
* 递归删除当前节点cur的子树中index(从0开始)位置的节点
*/
private Node<V> delete(Node<V> cur, int index) {
if (cur == null) {
return null;
}
cur.size--;
// 当前头结点的序号
int rootIndex = (cur.left != null ? cur.left.size : 0);
if (index != rootIndex) {
// 删除的不是当前的cur节点的数,按照序号递归即可
if (index < rootIndex) {
cur.left = delete(cur.left, index);
} else {
cur.right = delete(cur.right, index - rootIndex - 1);
}
return cur;
}
// 删除的是当前的cur节点
if (cur.left == null && cur.right == null) {
return null;
}
if (cur.left == null) {
return cur.right;
}
if (cur.right == null) {
return cur.left;
}
// 两边都有子树,找到右侧最小的替换
Node<V> pre = null;
Node<V> des = cur.right;
des.size--;
while (des.left != null) {
pre = des;
des = des.left;
des.size--;
}
if (pre != null) {
pre.left = des.right;
des.right = cur.right;
}
des.left = cur.left;
des.size = 1 + (des.left != null ? des.left.size : 0) + (des.right != null ? des.right.size : 0);
return des;
}
/**
* 递归查询当前节点cur的子树中index(从0开始)位置的节点
*/
private Node<V> get(Node<V> cur, int index) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
if (index < leftSize) {
return get(cur.left, index);
} else if (index == leftSize) {
return cur;
} else {
return get(cur.right, index - leftSize - 1);
}
}
private Node<V> rightRotate(Node<V> cur) {
if (cur == null || cur.left == null) {
return cur;
}
Node<V> leftNode = cur.left;
cur.left = leftNode.right;
leftNode.right = cur;
leftNode.size = cur.size;
cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
return leftNode;
}
private Node<V> leftRotate(Node<V> cur) {
if (cur == null || cur.right == null) {
return cur;
}
Node<V> rightNode = cur.right;
cur.right = rightNode.left;
rightNode.left = cur;
rightNode.size = cur.size;
cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
return rightNode;
}
private Node<V> maintain(Node<V> cur) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
if (leftLeftSize > rightSize) {
// LL
cur = rightRotate(cur);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) {
// LR
cur.left = leftRotate(cur.left);
cur = rightRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) {
// RR
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) {
// RL
cur.right = rightRotate(cur.right);
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
}
return cur;
}
/**
* 节点类
* 因为是按照插入顺序来做逻辑key的,可以复用size字段,
* 所以节点就没有必要再存储一个key了,只需要存储value即可。
*/
static class Node<V> {
private V value;
private Node<V> left;
private Node<V> right;
private int size;
public Node(V value) {
this.value = value;
this.size = 1;
}
}
}
整体代码和测试如下:
java
import java.util.ArrayList;
/**
* 题目三:设计一个保持时序的列表
* 设计一个结构包含如下两个方法:
* void add(int index, int num):把num加入到index位置
* int get(int index) :取出index位置的值
* void remove(int index) :把index位置上的值删除
* 要求三个方法时间复杂度O(logN)
*/
public class Q3_AddRemoveGetWithIndex {
/**
* 用SBT改写的支持用所以增加、查询、删除操作的方法
* 思路:
* SB树是支持index查询的,但是不支持index插入和删除,这是因为SB树是根据key的值来做比较的,
* 如果我们用插入顺序来做逻辑的key,就可以直接复用size字段来做已经有的节点的index,
* 在插入和删除的时候,根据传入的index字段和size进行对比,就能计算出对应的逻辑key,
* 同时,因为是用的逻辑key做的比较,相同的value是作为不同的节点存在于树中的,所以改写的SBT就天然支持重复元素。
* 具体参照实现的类SbtList
*/
public static class SbtList<V> {
private Node<V> root;
public void add(int index, V num) {
Node<V> node = new Node<>(num);
if (root == null) {
root = node;
} else {
if (index <= root.size) {
// 只有符合条件的index才能添加
root = add(root, index, node);
}
}
}
public V get(int index) {
if (index >= 0 && size() > index) {
Node<V> ans = get(root, index);
return ans.value;
}
return null;
}
public void remove(int index) {
if (index >= 0 && size() > index) {
root = delete(root, index);
}
}
public int size() {
return root == null ? 0 : root.size;
}
/**
* 递归给当前节点cur的子树添加index(从0开始)位置的node节点
* 当要替换掉一个已经存在的index位置的时候,新加入的节点实际上还是会加到合适的最底层,
* 但是由于新加入的节点的存在,在中序遍历中会占据index的位置,让后面的节点往后移动,所以自然是符合时序的
*/
private Node<V> add(Node<V> cur, int index, Node<V> node) {
// 到最后为null的节点了,直接将node放到这个位置
if (cur == null) {
return node;
}
cur.size++;
// 利用size计算出当前cur节点的左侧和本身的长度
int leftAndHeadSize = (cur.left != null ? cur.left.size : 0) + 1;
// 用index判断需要插入到什么位置
if (index < leftAndHeadSize) {
// 插入到左子树
cur.left = add(cur.left, index, node);
} else {
// 插入到右子树,需要重新计算下个位置的坐标,
cur.right = add(cur.right, index - leftAndHeadSize, node);
}
// 维护平衡性
return maintain(cur);
}
/**
* 递归删除当前节点cur的子树中index(从0开始)位置的节点
*/
private Node<V> delete(Node<V> cur, int index) {
if (cur == null) {
return null;
}
cur.size--;
// 当前头结点的序号
int rootIndex = (cur.left != null ? cur.left.size : 0);
if (index != rootIndex) {
// 删除的不是当前的cur节点的数,按照序号递归即可
if (index < rootIndex) {
cur.left = delete(cur.left, index);
} else {
cur.right = delete(cur.right, index - rootIndex - 1);
}
return cur;
}
// 删除的是当前的cur节点
if (cur.left == null && cur.right == null) {
return null;
}
if (cur.left == null) {
return cur.right;
}
if (cur.right == null) {
return cur.left;
}
// 两边都有子树,找到右侧最小的替换
Node<V> pre = null;
Node<V> des = cur.right;
des.size--;
while (des.left != null) {
pre = des;
des = des.left;
des.size--;
}
if (pre != null) {
pre.left = des.right;
des.right = cur.right;
}
des.left = cur.left;
des.size = 1 + (des.left != null ? des.left.size : 0) + (des.right != null ? des.right.size : 0);
return des;
}
/**
* 递归查询当前节点cur的子树中index(从0开始)位置的节点
*/
private Node<V> get(Node<V> cur, int index) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
if (index < leftSize) {
return get(cur.left, index);
} else if (index == leftSize) {
return cur;
} else {
return get(cur.right, index - leftSize - 1);
}
}
private Node<V> rightRotate(Node<V> cur) {
if (cur == null || cur.left == null) {
return cur;
}
Node<V> leftNode = cur.left;
cur.left = leftNode.right;
leftNode.right = cur;
leftNode.size = cur.size;
cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
return leftNode;
}
private Node<V> leftRotate(Node<V> cur) {
if (cur == null || cur.right == null) {
return cur;
}
Node<V> rightNode = cur.right;
cur.right = rightNode.left;
rightNode.left = cur;
rightNode.size = cur.size;
cur.size = 1 + (cur.left == null ? 0 : cur.left.size) + (cur.right == null ? 0 : cur.right.size);
return rightNode;
}
private Node<V> maintain(Node<V> cur) {
if (cur == null) {
return null;
}
int leftSize = cur.left != null ? cur.left.size : 0;
int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
if (leftLeftSize > rightSize) {
// LL
cur = rightRotate(cur);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) {
// LR
cur.left = leftRotate(cur.left);
cur = rightRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) {
// RR
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) {
// RL
cur.right = rightRotate(cur.right);
cur = leftRotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
}
return cur;
}
/**
* 节点类
* 因为是按照插入顺序来做逻辑key的,可以复用size字段,
* 所以节点就没有必要再存储一个key了,只需要存储value即可。
*/
static class Node<V> {
private V value;
private Node<V> left;
private Node<V> right;
private int size;
public Node(V value) {
this.value = value;
this.size = 1;
}
}
}
// 通过以下这个测试,
// 可以很明显的看到LinkedList的插入、删除、get效率不如SbtList
// LinkedList需要找到index所在的位置之后才能插入或者读取,时间复杂度O(N)
// SbtList是平衡搜索二叉树,所以插入或者读取时间复杂度都是O(logN)
public static void main(String[] args) {
// 功能测试
int test = 50000;
int max = 1000000;
boolean pass = true;
ArrayList<Integer> list = new ArrayList<>();
SbtList<Integer> sbtList = new SbtList<>();
for (int i = 0; i < test; i++) {
if (list.size() != sbtList.size()) {
pass = false;
break;
}
if (list.size() > 1 && Math.random() < 0.5) {
int removeIndex = (int) (Math.random() * list.size());
list.remove(removeIndex);
sbtList.remove(removeIndex);
} else {
int randomIndex = (int) (Math.random() * (list.size() + 1));
int randomValue = (int) (Math.random() * (max + 1));
list.add(randomIndex, randomValue);
sbtList.add(randomIndex, randomValue);
}
}
for (int i = 0; i < list.size(); i++) {
if (!list.get(i).equals(sbtList.get(i))) {
pass = false;
break;
}
}
System.out.println("功能测试是否通过 : " + pass);
// 性能测试
test = 500000;
list = new ArrayList<>();
sbtList = new SbtList<>();
long start = 0;
long end = 0;
start = System.currentTimeMillis();
for (int i = 0; i < test; i++) {
int randomIndex = (int) (Math.random() * (list.size() + 1));
int randomValue = (int) (Math.random() * (max + 1));
list.add(randomIndex, randomValue);
}
end = System.currentTimeMillis();
System.out.println("ArrayList插入总时长(毫秒) : " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < test; i++) {
int randomIndex = (int) (Math.random() * (i + 1));
list.get(randomIndex);
}
end = System.currentTimeMillis();
System.out.println("ArrayList读取总时长(毫秒) : " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < test; i++) {
int randomIndex = (int) (Math.random() * list.size());
list.remove(randomIndex);
}
end = System.currentTimeMillis();
System.out.println("ArrayList删除总时长(毫秒) : " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < test; i++) {
int randomIndex = (int) (Math.random() * (sbtList.size() + 1));
int randomValue = (int) (Math.random() * (max + 1));
sbtList.add(randomIndex, randomValue);
}
end = System.currentTimeMillis();
System.out.println("SbtList插入总时长(毫秒) : " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < test; i++) {
int randomIndex = (int) (Math.random() * (i + 1));
sbtList.get(randomIndex);
}
end = System.currentTimeMillis();
System.out.println("SbtList读取总时长(毫秒) : " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < test; i++) {
int randomIndex = (int) (Math.random() * sbtList.size());
sbtList.remove(randomIndex);
}
end = System.currentTimeMillis();
System.out.println("SbtList删除总时长(毫秒) : " + (end - start));
}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷