如何生成包含重复元素的排列组合数组?

3

我很了解这个网站,但是我没有找到包含重复元素的答案。例如,给定以下数组:

[1,2,3,4]

有一个长度为3的函数,应该生成使用每个数字超过一次的这些数字的每个可能组合的列表:

[
  [1,1,1],
  [1,1,2],
  [1,1,3],
  ...
  [4,4,2],
  [4,4,3],
  [4,4,4]
]

我只是无法理解我应该使用哪个算法。 我不期望得到源代码的答案,但是希望能得到正确方向的指引。

我已经尝试使用reduce函数,像这样:

const arr = [1, 2, 3, 4]
const len = 3

arr.reduce((acc, n) => {
    for (let i = 0; i < len; i++) {
        acc.push(/* ???? */)
    }
    return acc
}, [])

但我真的不知道该如何继续。

值得一提的是,理想情况下,我希望尽可能高效地完成这件事。


1
这个回答解决了你的问题吗?JavaScript中的排列组合? - Aksen P
2
@AksenP 不行。就像我之前提到的那样,这些答案不使用重复项,而是每个元素只使用一次。 - KEKW
你可以检查他们的算法并打开重复项。或者你不能读别人的代码? - Aksen P
5个回答

3

一种方法是使用数组的长度作为基础。然后,您可以从0开始访问数组的元素,并计数到组合的数量(数组长度**长度)。如果您正在处理小数据集,则性能不应成为问题,但本答案非常注重性能:

const getCombos = (arr, len) => {
  const base = arr.length
  const counter = Array(len).fill(base === 1 ? arr[0] : 0)
  if (base === 1) return [counter]
  const combos = []
  const increment = i => {
    if (counter[i] === base - 1) {
      counter[i] = 0
      increment(i - 1)
    } else {
      counter[i]++
    }
  }

  for (let i = base ** len; i--;) {
    const combo = []
    for (let j = 0; j < counter.length; j++) {
      combo.push(arr[counter[j]])
    }
    combos.push(combo)
    increment(counter.length - 1)
  }

  return combos
}

const combos = getCombos([1, 2, 3, 4], 3)
console.log(combos)


谢谢答复,你赋值计数器的那一行是哪一行? - KEKW
它将计数器分配给一个填充了零的数组,或者如果给定的数组只有一个元素,则会用该元素填充它,并在下一行返回它。 - Kobe

2

您可以使用算法来获取具有所需值的三个数组的笛卡尔积。

var values = [1, 2, 3, 4],
    length = 3,
    result = Array
        .from({ length }, _ => values)
        .reduce((a, b) => a.reduce((r, v) => r.concat(b.map(w => [].concat(v, w))), []));

console.log(result.map(a => a.join(' ')));
.as-console-wrapper { max-height: 100% !important; top: 0; }


2
想象一下一个古老的机械旋转计数器

enter image description here

0000099999 的计数方法是,将最右边的轮子旋转到 9,然后它会重置为 0,第二个右边的轮子就会向前进 1,以此类推。
在代码中:找到最右边的数字小于最大数字的轮子。将该轮子向前进 1,并将其右侧所有轮子重置为 0。重复此操作,直到没有这样的轮子为止。

function counter(digits, size) {
    let wheels = Array(size).fill(0),
        len = digits.length,
        res = [];

    while (1) {
        res.push(wheels.map(n => digits[n]));
        for (let i = size - 1; i >= 0; i--) {
            if (wheels[i] < len - 1) {
                wheels[i]++;
                wheels.fill(0, i + 1);
                break;
            }
            if (i === 0)
                return res;
        }
    }
}

all = counter('1234', 3)
console.log(...all.map(c => c.join('')))

表现度量:

const kobe = (arr, len) => {
    const base = arr.length
    const counter = Array(len).fill(base === 1 ? arr[0] : 0)
    if (base === 1) return [counter]
    const combos = []
    const increment = i => {
        if (counter[i] === base - 1) {
            counter[i] = 0
            increment(i - 1)
        } else {
            counter[i]++
        }
    }

    for (let i = base ** len; i--;) {
        const combo = []
        for (let j = 0; j < counter.length; j++) {
            combo.push(arr[counter[j]])
        }
        combos.push(combo)
        increment(counter.length - 1)
    }

    return combos
}

function nina(values, length) {
    return Array
        .from({length}, _ => values)
        .reduce((a, b) => a.reduce((r, v) => r.concat(b.map(w => [].concat(v, w))), []));
}

function trincot(digits, size) {
    if (!size) return [[]]; // base case
    let shorter = trincot(digits, size - 1); // get all solutions for smaller size
    // ...and prefix each of those with each possible digit
    return Array.from(digits, dig => shorter.map(arr => [dig, ...arr])).flat();
}

function georg(digits, size) {
    let wheels = Array(size).fill(0),
        len = digits.length,
        res = [];

    while (1) {
        res.push(wheels.map(n => digits[n]));
        for (let i = size - 1; i >= 0; i--) {
            if (wheels[i] < len - 1) {
                wheels[i]++;
                wheels.fill(0, i + 1);
                break;
            }
            if (i === 0)
                return res;
        }
    }
}

const gilad = (A, k) =>
  k ? gilad(A, k - 1).reduce((a, s) => a.concat(A.map(e => s.concat(e))), []) : [[]]


//////////////////////////////////////////////////////////////

fns = [kobe, nina, trincot, georg, gilad];
ary = [0, 1, 2, 3, 4, 5, 6, 7, 8]
size = 5
res = []

for (f of fns) {
    console.time(f.name);
    res.push(f(ary, size));
    console.timeEnd(f.name)
}

使用相同的技术来生成笛卡尔积:

function product(...arrays) {
    let size = arrays.length,
        wheels = Array(size).fill(0),
        lens = arrays.map(a => a.length),
        res = [];

    while (1) {
        res.push(wheels.map((n, w) => arrays[w][n]));
        for (let w = size - 1; w >= 0; w--) {
            if (wheels[w] < lens[w] - 1) {
                wheels[w]++;
                wheels.fill(0, w + 1);
                break;
            }
            if (w === 0)
                return res;
        }
    }
}

// demo

p = product('12', 'abcde', 'XYZ')
console.log(...p.map(q => q.join('')))

// some timings

// https://dev59.com/xWct5IYBdhLWcg3wKqQd#43053803
const f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e))));
const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a);

arrays = Array(7).fill(0).map(_ => Array(5).fill(0)) // 5**7=78125 combinations

console.time('func')
cartesian(...arrays)
console.timeEnd('func')

console.time('iter')
product(...arrays)
console.timeEnd('iter')


我给这个点赞是因为代码应该简单易懂,只需将值放在选择器(轮子)上,为每个值添加更多的轮子,然后通过它们运行是任何初学者程序员最容易理解的。如果我作为一个真正的程序员看到其他两个答案的代码,我必须停下来查找一些含义以确保它有意义并且可以工作。KISS原则适用于此。 - Guy Coder

1
也许还可以添加递归解决方案:

function counter(digits, size) {
    if (!size) return [[]]; // base case
    let shorter = counter(digits, size-1); // get all solutions for smaller size
    // ...and prefix each of those with each possible digit
    return Array.from(digits, dig => shorter.map(arr => [dig, ...arr])).flat();
}
// demo
let all = counter("1234", 3);
console.log(...all.map(c => c.join("")));


我提供了一个类似于你的递归,但不使用“flat”函数。(目前为止,我的回答是唯一得到赞同的) - גלעד ברקן

1
这被称为“n选k”,并具有以下递推关系

function f(ns, n, k){
  if (n == 0)
    return []
  if (k == 0)
    return [[]]
    
  return f(ns, n - 1, k).concat(
    f(ns, n, k - 1).map(s => s.concat(ns[n-1])))
}

var multiset = [1, 2, 3, 4]
var k = 3
console.log(JSON.stringify(f(multiset, multiset.length, k)))

作为其他人已经回答过的一种替代方案,是同时包含每个组合的每个排列。一种方法是在构建到最终长度时将每个元素附加到每个组合中。(这个想法类似于trincot的想法。)

const f = (A, k) =>
  k ? f(A, k - 1).reduce((a, s) => a.concat(A.map(e => s.concat(e))), []) : [[]]

var A = [1, 2, 3, 4]
var k = 3
console.log(JSON.stringify(f(A, k)))


不幸的是,这似乎不正确,例如缺少 1,2,11,3,1 等。 - georg
@georg 你可以要求原帖作者澄清一下。按照目前的描述,这个答案符合原帖的要求:“函数应该生成使用每个数字超过一次的所有可能组合的列表。”也许你的回答是不正确的? - גלעד ברקן
顺便说一下,我的是你回答中两个赞成票之一 :) - גלעד ברקן
谢谢 ;) 我认为这个描述非常清晰:给定一个由N个数字组成的集合,生成所有长度为L的N进制数字。 - georg
@georg,那是你的话,而不是原帖作者的话。但我引用的话确实是原帖作者的话。他们的示例也没有更清晰,因为它缺少了121。 - גלעד ברקן
@georg(我添加了其他人已经解释的问题的替代方案。) - גלעד ברקן

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接