Вот оптимизированная версия кода, перенесенная из Python @Derek, с добавленной деструктивной (на месте) опцией, которая делает его самым быстрым из возможных алгоритмов, если вы можете с ним пойти. В противном случае он либо делает полную копию, либо, для небольшого количества элементов, запрошенных из большого массива, переключается на алгоритм, основанный на выборе.
function sample(pool, k, destructive) {
var n = pool.length;
if (k < 0 || k > n)
throw new RangeError("Sample larger than population or is negative");
if (destructive || n <= (k <= 5 ? 21 : 21 + Math.pow(4, Math.ceil(Math.log(k*3, 4))))) {
if (!destructive)
pool = Array.prototype.slice.call(pool);
for (var i = 0; i < k; i++) {
var j = i + Math.random() * (n - i) | 0;
var x = pool[i];
pool[i] = pool[j];
pool[j] = x;
}
pool.length = k;
return pool;
} else {
var selected = new Set();
while (selected.add(Math.random() * n | 0).size < k) {}
return Array.prototype.map.call(selected, i => population[i]);
}
}
По сравнению с реализацией Дерека, первый алгоритм намного быстрее в Firefox, но немного медленнее в Chrome, хотя теперь у него есть деструктивный вариант - самый производительный. Второй алгоритм просто на 5-15% быстрее. Я стараюсь не называть конкретных цифр, поскольку они различаются в зависимости от k и n и, вероятно, ничего не значат в будущем с новыми версиями браузеров.
Эвристика, которая делает выбор между алгоритмами, исходит из кода Python. Я оставил его как есть, хотя иногда выбирает более медленный. Он должен быть оптимизирован для JS, но это сложная задача, поскольку производительность угловых случаев зависит от браузера и их версии. Например, когда вы пытаетесь выбрать 20 из 1000 или 1050, он переключится на первый или второй алгоритм соответственно. В этом случае первый работает в 2 раза быстрее, чем второй в Chrome 80, но в 3 раза медленнее в Firefox 74.