关于组合算法的 JS 实现


之前在写一个小页面时,遇到一个实际问题,这是一个关于双色球兑奖的小功能,输入本期开奖号码和自己买的号,实现自动兑奖。自己的号跟样本号码一个个检查,通过累计命中个数来判断奖项。但问题来了,如果我是复式投注的话,我们首先要将购买组合号码拆分出来。大家都读过高中代数,学习过排列组合的一些基本知识,通过它我们能很快计算出复式投注实际的注数。但如何用 JS 将这些组合号码都一个不漏地显示出来呢?

排列英文名叫 Permutation,如果与排列顺序有关的组合,我们用数学表达式记做 \(P_{m}^{n}\),与顺序无关的话,就叫做组合了,英文名叫 Combination,数学表达式记做 \(C_{m}^{n}\),而今天我们先来了解一下用 JS 如何来实现 \(C_{m}^{n}\)。

在着手解决这个问题时,深感自己的算法水平匮乏,于是上网搜索了一下,希望能得到前辈们的启发,果不其然,让我找到一篇多年前的博客,其中的思路让我大受启发,具体算法请移驾该博客自行观赏,但其中精髓,作者称之为“01转换法”。

假设有一个长度为 m 的数组,我每次选取 n 个数组成一组,将所有的可能列举出来:

  1. 创建一个由 “0” 和 “1” 组成的数组,长度为 m,数组中 “1” 表示其下标所在的数被选中,为 “0” 则没选中;
  2. 初始化该数组,将数组前 n 个元素置 “1” ,表示第一个组合为前 n 个数;
  3. 然后从左到右扫描数组元素值的 “10” 组合,找到第一个 “10” 组合后将其变为 “01” 组合;
  4. 同时将其左边的所有 “1” 全部移动到数组的最左端;
  5. 当 n 个 “1” 全部移动到最右端时,就得到了最后一个组合,组合结束。

举个列子,数组 [‘a’, ‘b’, ‘c’, ‘d’] 4 选 3:

[1, 1, 1, 0] // ["a", "b", "c"]
[1, 1, 0, 1] // ["a", "b", "d"]
[1, 0, 1, 1] // ["a", "c", "d"]
[0, 1, 1, 1] // ["b", "c", "d"]

仔细分析了一下博客里前辈的代码,发现他用了很多循环来实现 \(C_{m}^{n}\),我想是否可以不用这么多 for 循环来简化代码,实现同样的效果呢?突然灵机一动,不就是一堆 “0011” 的操作吗,把它当做字符串处理不就得了?用一个 while 循环不断的将字符串中的 “10” replace 成 “01”,同时整理前面的字符串,保证所有的 “1” 在字符串最前端,一直到字符串中再也找不出 “10” 为止。

思路有了,写了个 demo 代码实验了一下,果然可行啊,那就开干了!现在新一代 ES 语法早已大行其道,很多之前写起来有点麻烦的代码已经有了原生支持的方法,所以,前辈兄的代码是该优化一下了。

以下就是我琢磨出来的代码,为了能方便和原代码做对比,一些必要函数和变量名我都尽量保持和原来一致,得罪了前辈哥:

/**
 * 获得指定数组的所有组合
 */
function arrayCombine(targetArr = [], count = 1) {
  if (!Array.isArray(targetArr)) return []

  const resultArrs = []
  // 所有组合的 01 排列
  const flagArrs = getFlagArrs(targetArr.length, count)
  while (flagArrs.length) {
    const flagArr = flagArrs.shift()
    resultArrs.push(targetArr.filter((_, idx) => flagArr[idx]))
  }
  return resultArrs
}
/**
 * 获得从 m 中取 n 的所有组合
 * 思路如下:
 * 生成一个长度为 m 的数组,
 * 数组元素的值为 1 表示其下标代表的数被选中,为 0 则没选中。
 *
 * 1. 初始化数组,前 n 个元素置 1,表示第一个组合为前 n 个数;
 * 2. 从左到右扫描数组元素值的 “10” 组合,找到第一个 “10” 组合后将其变为 “01” 组合;
 * 3. 将其左边的所有 “1” 全部移动到数组的最左端
 * 4. 当 n 个 “1” 全部移动到最右端时(没有 “10” 组合了),得到了最后一个组合。
 */
function getFlagArrs(m, n = 1) {
  if (n < 1 || m < n)  return []

  // 先生成一个长度为 m 字符串,开头为 n 个 1, 例如“11100”
  let str = '1'.repeat(n) + '0'.repeat(m-n)
  let pos
  // 1
  const resultArrs = [Array.from(str, x => Number(x))]
  const keyStr = '10'

  while(str.indexOf(keyStr) > -1) {
    pos = str.indexOf(keyStr)
    // 2
    str = str.replace(keyStr, '01')
    // 3
    str = Array.from(str.slice(0, pos))
      .sort((a, b) => b-a)
      .join('') + str.slice(pos)
    // 4
    resultArrs.push(Array.from(str, x => Number(x)))
  }
  return resultArrs
}

我们看到,我在组合时就用了一个 while 作了一个不定循环,其他的都用隐式迭代暗度陈仓了。看一下实际效果:

// 数组中 5 选 3
arrayCombine(['a', 'b', 'c', 'd', 'e'], 3)

返回结果为:[["a", "b", "c"], ["a", "b", "d"], ["a", "c", "d"], ["b", "c", "d"], ["a", "b", "e"], ["a", "c", "e"], ["b", "c", "e"], ["a", "d", "e"], ["b", "d", "e"], ["c", "d", "e"]],完美,哈哈。


文章版权本站所有,您可以转载,但请注明出处,谢谢!