最大余额法

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

完美 ~~~

相关推荐
叫我:松哥7 分钟前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
好名字082142 分钟前
monorepo基础搭建教程(从0到1 pnpm+monorepo+vue)
前端·javascript
c#上位机1 小时前
C#事件的用法
java·javascript·c#
万物得其道者成1 小时前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白1 小时前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风2 小时前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom2 小时前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan2 小时前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^2 小时前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang2 小时前
vue3中开发引导页的方法
开发语言·前端·javascript