从数组中获取 n 个不重叠的 m 大小的样本

Get n non overlapping m-sized samples from an array

给定一个数组,我如何从中提取 n 个大小为 m 的非重叠随机样本?

例如,给定数组:

const arr = [1, 2, 3, 4, 5, 6, 7, 8];

调用 sample(arr, 3, 2) 例如 return [[7, 8], [4, 5], [2, 3]],调用 sample(arr, 2, 4) 必然 return [[1, 2, 3, 4], [5, 6, 7, 8],调用 sample(arr, 5, 2) 会抛出错误。

编辑 - 也许这在最初的问题中并不清楚:样本应该是连续元素的列表。这就是为什么 sample(arr, 2, 4) 只能 return [[1, 2, 3, 4], [5, 6, 7, 8] 而不能 [[2, 3, 1, 6], [5, 4, 7, 8],例如

你可以使用贪心算法,从打乱后的数组中取出 m-sized n 个元组:

const arr = [2, 1, 3, 4, 5, 6, 7, 8];
function sample(arr, length, size){
  if(arr.length < length*size)
    throw new Error("too short");
  arr.sort(() => Math.random() - 0.5);
  let res = [];
  for(let i = 0; i < length; i++) res.push(arr.slice(i*size, i*size+size));
  return res;
}
console.log(sample(arr, 2, 4));

我认为最好的实现方式是先洗牌。这是我的两分钱:

function shuffle(array){
  let a = array.slice(), i = a.length, n, h;
  while(i){
    n = Math.floor(Math.random()*i--); h = a[i]; a[i] = a[n]; a[n] = h;
  }
  return a;
}
function sample(array, chunks, count){
  const r = [], a = shuffle(array);
  for(let n=0; n<chunks; n++){
    r.push(a.splice(0, count));
  }
  return r;
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(sample(arr, 3, 2)); console.log(sample(arr, 2, 4));

您可以先创建一个格式为 return 值的列表:

[ 1,  2,  3,  4,  5,  6,  7,  8]
[<---->, <---->, <---->, <>, <>] // sample(array, 3, 2)
[<------------>, <------------>] // sample(array, 2, 4)

这些格式数组可以使用以下长度写出:

[1, 2, 3, 4, 5, 6, 7, 8]
[   2,    2,    2, 1, 1] // sample(array, 3, 2)
[         4,          4] // sample(array, 2, 4)

然后打乱格式数组以获得随机样本选择:

[1, 2, 3, 4, 5, 6, 7, 8]
[   2, 1,    2,    2, 1] // sample(array, 3, 2)
[         4,          4] // sample(array, 2, 4)

然后对于格式数组的每个元素,从输入数组中删除前 n 个元素。然后存储它们,除非它是填充物(放入一个大小的块以达到数组长度)。

[1, 2, 3, 4, 5, 6, 7, 8]
[[1,2], [4,5], [6,7]]  // sample(array, 3, 2)
[[1,2,3,4], [5,6,7,8]] // sample(array, 2, 4)

最后洗牌结果样本。

[1, 2, 3, 4, 5, 6, 7, 8]
[[4,5], [1,2], [6,7]]  // sample(array, 3, 2)
[[5,6,7,8], [1,2,3,4]] // sample(array, 2, 4)

const arr = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(sample(arr, 3, 2));
console.log(sample(arr, 2, 4));
console.log(sample(arr, 5, 2));

function randomInt(limit) {
  return Math.floor(Math.random() * limit);
}

function shuffle(array) {
  for (let limit = array.length; limit > 0; --limit)
    array.push(...array.splice(randomInt(limit), 1));
}

function sample(array, sampleCount, sampleLength) {
  let elementCount = sampleCount * sampleLength;
  if (elementCount > array.length)
    throw "invalid sampleCount/sampleLength arguments";
    
  const filler = {valueOf: () => 1};
  const fillerCount = array.length - elementCount;
  const lengths = Array.from(
    {length: sampleCount + fillerCount},
    (_, i) => i < sampleCount ? sampleLength : filler
  );

  shuffle(lengths);
  const samples = Array.from(array);
  for (const length of lengths) {
    const sample = samples.splice(0, length);
    if (length === filler) continue;
    samples.push(sample);
  }
  shuffle(samples);
  
  return samples;
}

请注意 ===length === filler 中很重要。如果您使用 ==filler 也等于 1。这将与 sample(array, 5, 1) 之类的调用冲突,其中每个样本长度为 1.

const filler = {valueOf: () => 1};

console.log("1 == filler       //=>", 1 == filler);
console.log("2 == filler       //=>", 2 == filler);
console.log("filler == filler  //=>", filler == filler);
console.log("1 === filler      //=>", 1 === filler);
console.log("2 === filler      //=>", 2 === filler);
console.log("filler === filler //=>", filler == filler);

您可以使用 Rando.js (which is cryptographically secure), map, and splice 轻松做到这一点。只需使用 randojs 的 randoSequence 函数来打乱提供的数组并从打乱后的数组中拼接 n 大小-m 数组以获得我们需要的一切 return。如果提供的数组值太少,我们 return 后面的数组会更短。

function sample(arr, n, m){
  arr = randoSequence(arr).map(i => i.value), sample = [];
  for(var i = 0; i < n; i++) sample[i] = arr.splice(-m);
  return sample;
}

console.log(sample([1, 2, 3, 4, 5, 6, 7, 8], 3, 2));
<script src="https://randojs.com/2.0.0.js"></script>