/* eslint no-bitwise: 0 */
/**
* 本函数计算给定值在已排序数组中的分位数排名。通过二分查找算法,可在对数时间内完成计算。
*
* @param {Array<number>} x 已排序输入数组
* @param {number} value 目标数值
* @returns {number} 分位数排名(范围[0,1])
* @example
* quantileRankSorted([1, 2, 3, 4], 3); // => 0.75
* quantileRankSorted([1, 2, 3, 3, 4], 3); // => 0.7
* quantileRankSorted([1, 2, 3, 4], 6); // => 1
* quantileRankSorted([1, 2, 3, 3, 5], 4); // => 0.8
*/
function quantileRankSorted(x, value) {
// 处理边界情况:值小于数组最小值
if (value < x[0]) {
return 0;
}
// 处理边界情况:值大于数组最大值
if (value > x[x.length - 1]) {
return 1;
}
// 获取目标值在数组中的下界索引
let l = lowerBound(x, value);
// 值不存在于数组中时直接返回比例位置
if (x[l] !== value) {
return l / x.length;
}
l++;
// 获取目标值在数组中的上界索引
const u = upperBound(x, value);
// 唯一存在情况:上下界索引重合
if (u === l) {
return l / x.length;
}
/*
* 计算包含目标值的连续索引范围的平均位置:
* 1. 计算连续索引的数量(r = 范围长度)
* 2. 使用等差数列求和公式计算总位置和
* 3. 求平均位置后转换为比例值
*/
const r = u - l + 1;
const sum = (r * (u + l)) / 2; // 等差数列求和公式
const mean = sum / r; // 计算平均位置
return mean / x.length;
}
/**
* 二分查找下界索引(第一个大于等于目标值的位置)
* @param {Array<number>} x 已排序数组
* @param {number} value 目标值
* @returns {number} 下界索引
*/
function lowerBound(x, value) {
let mid = 0;
let lo = 0;
let hi = x.length;
while (lo < hi) {
mid = (lo + hi) >>> 1; // 无符号右移实现取整
if (value <= x[mid]) {
hi = mid; // 向左收缩范围
} else {
lo = -~mid; // 等价于 mid + 1(位运算技巧)
}
}
return lo;
}
/**
* 二分查找上界索引(第一个大于目标值的位置)
* @param {Array<number>} x 已排序数组
* @param {number} value 目标值
* @returns {number} 上界索引
*/
function upperBound(x, value) {
let mid = 0;
let lo = 0;
let hi = x.length;
while (lo < hi) {
mid = (lo + hi) >>> 1; // 无符号右移实现取整
if (value >= x[mid]) {
lo = -~mid; // 等价于 mid + 1(位运算技巧)
} else {
hi = mid; // 向右收缩范围
}
}
return lo;
}
export default quantileRankSorted;