最大余额法

当开发一些图表类页面时,经常需要对一组数据求百分比,而像是常用的 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 ]

完美 ~~~

相关推荐
阿伟来咯~26 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端31 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱34 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai43 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨44 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry3 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端4 小时前
Content Security Policy (CSP)
前端·javascript·面试