Ось оптимізована версія коду, перенесена з 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, він відповідно переключиться на перший або другий алгоритм. У цьому випадку перший працює вдвічі швидше, ніж другий у Chrome 80, але в 3 рази повільніше у Firefox 74.