当开发一些图表类页面时,经常需要对一组数据求百分比,而像是常用的 Echarts 图表,在内部已经计算妥当了,保证一组数据在计算完百分比之后,这些百分比相加后仍然等于 100% 。而这种计算百分比的算法之一就是 ------ 最大余额法。
核心思想就是,根据每部分所占比例的大小,按照从高到低的顺序去分配剩余部分。
比如,一组数据 [ 4, 4, 3 ]
,所占百分比为 [ 36.36363636363636687, 36.36363636363636687, 27.2727272727272734 ]
。先取出整数部分,得到 [ 36, 36, 27 ]
,累加后总和为 99
,还剩余 1
。接下来再取出小数部分,得到 [ 0.36363636363636687, 0.36363636363636687, 0.2727272727272734 ]
。把最后剩余的 1
根据小数部分的大小,优先加到最大的部分,如果有多个,则索引在前的优先级高。这里就是加到索引为 0
的位置上,最终得到结果 [ 37, 36, 27 ]
。如果有多个剩余,则同理,每次找最大的小数部分,然后追加 1。需要注意:已经追加过 1 的部分不能再次追加。
有同学可能会有疑惑,会不会出现:有 N 个数,剩余为 M,且 M > N ?答案是 否定 的。因为 M 等于 N 个数的小数部分之和,而且小数部分都小于 1,N 个小于 1 的数之和一定小于 N 。所以,M < N 恒成立。也就意味着,每个部分最多追加一次余额,不会出现追加两次的情况。
理清思路后,先来实现一个简单版本。
js
/**
* 计算各个数值所占百分比
* @param {number[]} data 源数据
* @returns {number[]}
*/
function getPercentValue(data) {
if (!data.length) {
return []
}
// 求和
const sum = getSum(data)
// 初始化剩余为 100
let remainSum = 100
// 记录整数部分
const integerPart = []
// 记录小数部分
const decimalPart = []
for (const v of data) {
// 因为要计算百分比,需要先乘以 100
const newVal = v * 100
// 计算实际的百分比
const percent = newVal / sum
// 得到整数部分
const integer = Math.floor(percent)
// 得到小数部分
const decimal = percent - integer
// 将整数、小数部分分别存入对应的数组中
integerPart.push(integer)
decimalPart.push(decimal)
// 更新剩余,减去当前的整数部分
remainSum -= integer
}
// 如果剩余大于 0,循环去消减剩余
while (remainSum > 0) {
// 找到小数部分数组中最大值的索引
const maxIdx = findMaxValIndex(decimalPart)
// 将整数部分对应索引的值加 1
integerPart[maxIdx] += 1
// 将本次找到的小数部分置为负数,防止重复查找
decimalPart[maxIdx] *= -1
// 剩余减 1
remainSum--
}
// 整数部分就是最后的结果
return integerPart
// 计算当前数组之和
function getSum(arr) {
return arr.reduce((prev, sum) => prev + sum, 0)
}
// 查找当前数组中最大值索引
function findMaxValIndex(arr) {
let max = 0
let maxIdx = -1
for (let i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
maxIdx = i
}
}
return maxIdx
}
}
小测一下:
js
getPercentValue([6, 6, 8]) // [ 30, 30, 40 ]
getPercentValue([6, 6, 8, 6, 8]) // [ 18, 18, 23, 18, 23 ]
getPercentValue([4, 4, 3]) // [ 37, 36, 27 ]
getPercentValue([4]) // [ 100 ]
getPercentValue([3, 3, 3, 3, 3, 3]) // [ 17, 17, 17, 17, 16, 16 ]
getPercentValue([30, 20, 6, 1]) // [ 53, 35, 10, 2 ]
效果不错,very nice ~
接下来,再加一个功能,因为有时候百分比不一定都是整数,也需要保留到小数点后几位。新增一个参数 precision
,默认为 0
,即保留到整数位,若为 2
,则保留到小数点后 2
位。
js
/**
* 计算各个数值所占百分比
* @param {number[]} data 源数据
* @param {number} precision 精度,默认为 0,即保留到整数位
* @returns {number[]}
*/
function getPercentValue(data, precision = 0) {
if (!data.length) {
return []
}
// 除了基本的需要乘以 100 之外,还需要根据精度大小,再乘以 10^n 次方
const base = 100 * Math.pow(10, precision)
// 初始化剩余为基数值
let remainSum = base
const sum = getSum(data)
const integerPart = []
const decimalPart = []
for (const v of data) {
// 不再乘以 100,这里乘以基数
const newVal = v * base
const percent = newVal / sum
const integer = Math.floor(percent)
const decimal = percent - integer
integerPart.push(integer)
decimalPart.push(decimal)
remainSum -= integer
}
while (remainSum > 0) {
const maxIdx = findMaxValIndex(decimalPart)
integerPart[maxIdx] += 1
decimalPart[maxIdx] *= -1
remainSum--
}
// 根据精度,挪动小数点位置
return integerPart.map(v => v / Math.pow(10, precision))
// 计算当前数组之和
function getSum(arr) {
return arr.reduce((prev, sum) => prev + sum, 0)
}
// 查找当前数组中最大值索引
function findMaxValIndex(arr) {
let max = 0
let maxIdx = -1
for (let i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
maxIdx = i
}
}
return maxIdx
}
}
最后测试一下:
js
console.log(getPercentValue([6, 6, 8], 1)) // [ 30, 30, 40 ]
console.log(getPercentValue([6, 6, 8, 6, 8], 2)) // [ 17.65, 17.65, 23.53, 17.64, 23.53 ]
console.log(getPercentValue([4, 4, 3], 3)) // [ 36.364, 36.363, 27.273 ]
console.log(getPercentValue([4])) // [ 100 ]
console.log(getPercentValue([3, 3, 3, 3, 3, 3], 2)) // [ 16.67, 16.67, 16.67, 16.67, 16.66, 16.66 ]
console.log(getPercentValue([30, 20, 6, 1], 3)) // [ 52.632, 35.088, 10.526, 1.754 ]
完美 ~~~