Ecco una versione ottimizzata del codice portato da Python da @Derek, con l'aggiunta dell'opzione distruttiva (sul posto) che lo rende l'algoritmo più veloce possibile se puoi seguirlo. In caso contrario, crea una copia completa o, per un numero limitato di elementi richiesti da un array di grandi dimensioni, passa a un algoritmo basato sulla selezione.
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]);
}
}
Rispetto all'implementazione di Derek, il primo algoritmo è molto più veloce in Firefox mentre è un po 'più lento in Chrome, anche se ora ha l'opzione distruttiva, la più performante. Il secondo algoritmo è semplicemente del 5-15% più veloce. Cerco di non fornire numeri concreti poiché variano a seconda di k e ne probabilmente non significheranno nulla in futuro con le nuove versioni del browser.
L'euristica che fa la scelta tra gli algoritmi proviene dal codice Python. L'ho lasciato così com'è, anche se a volte seleziona quello più lento. Dovrebbe essere ottimizzato per JS, ma è un'attività complessa poiché le prestazioni dei casi d'angolo dipendono dal browser e dalla loro versione. Ad esempio, quando si tenta di selezionare 20 su 1000 o 1050, si passerà di conseguenza al primo o al secondo algoritmo. In questo caso, il primo è 2 volte più veloce del secondo in Chrome 80 ma 3 volte più lento in Firefox 74.