L'uso di Random e OrderBy è un buon algoritmo shuffle?


164

Ho letto un articolo su vari algoritmi shuffle su Coding Horror . Ho visto che da qualche parte le persone hanno fatto questo per mescolare un elenco:

var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());

È un buon algoritmo shuffle? Come funziona esattamente? È un modo accettabile per farlo?

Risposte:


205

Non è un modo di mescolare che mi piace, principalmente per il fatto che è O (n log n) senza una buona ragione quando è facile implementare un O (n) shuffle. Il codice nella domanda "funziona" in sostanza dando un numero casuale (si spera univoco!) A ciascun elemento, quindi ordinando gli elementi in base a quel numero.

Preferisco la variante di Durstenfield dello shuffle Fisher-Yates che scambia elementi.

L'implementazione di un semplice Shufflemetodo di estensione consisterebbe sostanzialmente nel chiamare ToListo ToArraynell'input quindi usando un'implementazione esistente di Fisher-Yates. (Passa Randomcome parametro per rendere la vita generalmente più piacevole.) Ci sono molte implementazioni in giro ... Probabilmente ne ho una in una risposta da qualche parte.

La cosa bella di un tale metodo di estensione è che sarebbe quindi molto chiaro al lettore cosa stai effettivamente cercando di fare.

EDIT: ecco una semplice implementazione (nessun controllo degli errori!):

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    // Note i > 0 to avoid final pointless iteration
    for (int i = elements.Length-1; i > 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        int swapIndex = rng.Next(i + 1);
        T tmp = elements[i];
        elements[i] = elements[swapIndex];
        elements[swapIndex] = tmp;
    }
    // Lazily yield (avoiding aliasing issues etc)
    foreach (T element in elements)
    {
        yield return element;
    }
}

EDIT: i commenti sulla performance di seguito mi hanno ricordato che possiamo effettivamente restituire gli elementi mentre li mescoliamo:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        // ... except we don't really need to swap it fully, as we can
        // return it immediately, and afterwards it's irrelevant.
        int swapIndex = rng.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

Questo ora farà solo tutto il lavoro necessario.

Si noti che in entrambi i casi, è necessario prestare attenzione all'istanza Randomutilizzata come:

  • La creazione di due istanze Randomall'incirca nello stesso momento produrrà la stessa sequenza di numeri casuali (se usati allo stesso modo)
  • Random non è thread-safe.

Ho un articolo suRandom cui si approfondisce questo argomento e fornisce soluzioni.


5
Bene, le implementazioni per piccole, ma importanti, cose come questa, direi, sono sempre belle da trovare qui su StackOverflow. Quindi sì, per favore, se vuoi =)
Svish

9
Jon - la tua spiegazione di Fisher-Yates è equivalente all'implementazione data nella domanda (la versione ingenua). Durstenfeld / Knuth ottengono O (n) non per incarico, ma per selezione da un set decrescente e scambio. In questo modo il numero casuale selezionato può ripetersi e l'algoritmo prende solo O (n).
Tvanfosson,

8
Probabilmente ti stai stancando di sentirmi parlare di questo, ma ho riscontrato un leggero problema nei test delle unità che potresti voler conoscere. C'è una stranezza con ElementAt che lo fa invocare l'estensione ogni volta, dando risultati inaffidabili. Nei miei test sto materializzando il risultato prima di verificare per evitarlo.
tvanfosson,

3
@tvanfosson: non è affatto malato :) Ma sì, i chiamanti dovrebbero essere consapevoli che è valutato pigramente.
Jon Skeet,

4
Un po 'in ritardo, ma tieni presente source.ToArray();che devi avere using System.Linq;lo stesso file. In caso contrario, viene visualizzato l'errore:'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
Powerlord,

70

Questo si basa sulla risposta di Jon Skeet .

In quella risposta, l'array viene mischiato, quindi restituito usando yield. Il risultato netto è che l'array viene tenuto in memoria per la durata di foreach, così come gli oggetti necessari per l'iterazione, eppure il costo è tutto all'inizio - il rendimento è sostanzialmente un ciclo vuoto.

Questo algoritmo è molto utilizzato nei giochi, in cui vengono scelti i primi tre elementi e gli altri saranno necessari solo in seguito, se non del tutto. Il mio suggerimento è ai yieldnumeri non appena vengono scambiati. Ciò ridurrà il costo di avvio, mantenendo il costo di iterazione su O (1) (sostanzialmente 5 operazioni per iterazione). Il costo totale rimarrebbe lo stesso, ma il mescolamento stesso sarebbe più rapido. Nei casi in cui questo viene chiamato in collection.Shuffle().ToArray()quanto teoricamente non farà alcuna differenza, ma nei casi d'uso sopra menzionati accelererà l'avvio. Inoltre, ciò renderebbe l'algoritmo utile per i casi in cui sono necessari solo alcuni elementi unici. Ad esempio, se devi estrarre tre carte da un mazzo di 52, puoi chiamare deck.Shuffle().Take(3)e avranno luogo solo tre swap (anche se l'intero array dovrebbe essere copiato per primo).

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
    T[] elements = source.ToArray();
    // Note i > 0 to avoid final pointless iteration
    for (int i = elements.Length - 1; i > 0; i--)
    {
        // Swap element "i" with a random earlier element it (or itself)
        int swapIndex = rng.Next(i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
        // we don't actually perform the swap, we can forget about the
        // swapped element because we already returned it.
    }

    // there is one item remaining that was not returned - we return it now
    yield return elements[0]; 
}

Ahia! Questo probabilmente non restituirà tutti gli articoli nella fonte. Non puoi fare affidamento sul fatto che un numero casuale sia univoco per N iterazioni.
P Daddy,

2
Intelligente! (E odio questa roba da 15 personaggi ...)
Svish,

@P Papà: Eh? Ti interessa elaborare?
Svish,

1
Oppure potresti sostituire lo> 0 con> = 0 e non è necessario (anche se, un colpo RNG extra più un incarico ridondante)
FryGuy,

4
Il costo di avvio è O (N) come costo di source.ToArray ();
Dave Hillier,

8

A partire da questa citazione di Skeet:

Non è un modo di mescolare che mi piace, principalmente per il fatto che è O (n log n) senza una buona ragione quando è facile implementare un O (n) shuffle. Il codice nella domanda "funziona" in sostanza dando un numero casuale (si spera univoco! ) A ciascun elemento, quindi ordinando gli elementi in base a quel numero.

Continuerò un po 'spiegando il motivo per cui si spera sia unico!

Ora, da Enumerable.OrderBy :

Questo metodo esegue un ordinamento stabile; cioè se le chiavi di due elementi sono uguali, l'ordine degli elementi viene preservato

Questo è molto importante! Cosa succede se due elementi "ricevono" lo stesso numero casuale? Accade che rimangano nello stesso ordine in cui si trovano nell'array. Ora, qual è la possibilità che ciò accada? È difficile calcolare esattamente, ma c'è il problema del compleanno che è esattamente questo problema.

Adesso è reale? È vero?

Come sempre, in caso di dubbi, scrivi alcune righe di programma: http://pastebin.com/5CDnUxPG

Questo piccolo blocco di codice mescola un array di 3 elementi un certo numero di volte usando l'algoritmo Fisher-Yates fatto al contrario, l'algoritmo Fisher-Yates fatto in avanti (nella pagina wiki ci sono due algoritmi pseudo-codice ... Producono equivalenti risultati, ma uno viene eseguito dal primo all'ultimo elemento, mentre l'altro viene eseguito dall'ultimo al primo elemento), l'ingenuo algoritmo errato di http://blog.codinghorror.com/the-danger-of-naivete/ e l'utilizzo di .OrderBy(x => r.Next())e il .OrderBy(x => r.Next(someValue)).

Ora, Random.Next è

Un numero intero con segno a 32 bit che è maggiore o uguale a 0 e inferiore a MaxValue.

quindi è equivalente a

OrderBy(x => r.Next(int.MaxValue))

Per verificare se questo problema esiste, potremmo allargare l'array (qualcosa di molto lento) o semplicemente ridurre il valore massimo del generatore di numeri casuali ( int.MaxValuenon è un numero "speciale" ... È semplicemente un numero molto grande). Alla fine, se l'algoritmo non è influenzato dalla stabilità di OrderBy, allora qualsiasi intervallo di valori dovrebbe dare lo stesso risultato.

Il programma quindi verifica alcuni valori, nell'intervallo 1 ... 4096. Guardando il risultato, è abbastanza chiaro che per valori bassi (<128), l'algoritmo è molto distorto (4-8%). Con 3 valori è necessario almeno r.Next(1024). Se ingrandisci l'array (4 o 5), r.Next(1024)non è nemmeno sufficiente. Non sono un esperto di mescolanza e matematica, ma penso che per ogni ulteriore bit di lunghezza dell'array, siano necessari 2 bit extra di valore massimo (perché il paradosso del compleanno è collegato allo sqrt (valori numerici)), quindi che se il valore massimo è 2 ^ 31, dirò che dovresti essere in grado di ordinare le matrici fino a 2 ^ 12/2 ^ 13 bit (4096-8192 elementi)


Ben affermato e mostra perfettamente un problema con la domanda originale. Questo dovrebbe essere unito alla risposta di Jon.
TheSoftwareJedi,

6

Probabilmente va bene per la maggior parte degli scopi, e quasi sempre genera una distribuzione veramente casuale (tranne quando Random.Next () produce due interi casuali identici).

Funziona assegnando a ciascun elemento della serie un numero intero casuale, quindi ordinando la sequenza con questi numeri interi.

È totalmente accettabile per il 99,9% delle applicazioni (a meno che non sia assolutamente necessario gestire la custodia per bordi sopra). Inoltre, l'obiezione di Skeet al suo tempo di esecuzione è valida, quindi se stai mescolando un lungo elenco potresti non volerlo usare.


4

Questo è successo molte volte prima. Cerca Fisher-Yates su StackOverflow.

Ecco un esempio di codice C # che ho scritto per questo algoritmo. Puoi parametrizzarlo su un altro tipo, se preferisci.

static public class FisherYates
{
        //      Based on Java code from wikipedia:
        //      http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
        static public void Shuffle(int[] deck)
        {
                Random r = new Random();
                for (int n = deck.Length - 1; n > 0; --n)
                {
                        int k = r.Next(n+1);
                        int temp = deck[n];
                        deck[n] = deck[k];
                        deck[k] = temp;
                }
        }
}

2
Non dovresti usare Randomcome una variabile statica come questa - Randomnon è thread-safe. Vedi csharpindepth.com/Articles/Chapter12/Random.aspx
Jon Skeet il

@Jon Skeet: certo, questo è un argomento legittimo. OTOH, l'OP stava chiedendo di un algoritmo che era completamente sbagliato mentre questo è corretto (diverso dal caso d'uso multithreading di carte che mescola le carte).
Hughdbrown,

1
Ciò significa semplicemente che questo è "meno sbagliato" dell'approccio del PO. Non significa che sia il codice che dovrebbe essere usato senza capire che non può essere usato in sicurezza in un contesto multi-thread ... che è qualcosa che non hai menzionato. C'è una ragionevole aspettativa che i membri statici possano essere usati in sicurezza da più thread.
Jon Skeet,

@Jon Skeet: certo, posso cambiarlo. Fatto. Tendo a pensare che tornare a una domanda con risposta tre anni e mezzo fa e dicendo: "Non è corretto perché non gestisce il caso d'uso multithread" quando l'OP non ha mai chiesto nulla di più di quanto l'algoritmo sia eccessivo. Rivedi le mie risposte nel corso degli anni. Spesso ho dato risposte ai PO che andavano oltre i requisiti dichiarati. Sono stato criticato per questo. Tuttavia, non mi aspetto che i PO ottengano risposte adeguate a tutti gli usi possibili.
Hughdbrown,

Ho visitato questa risposta solo perché qualcun altro mi ha indicato la chat. Mentre l'OP non ha menzionato specificamente il threading, penso che valga la pena menzionare quando un metodo statico non è thread-safe, poiché è insolito e rende il codice inadatto per molte situazioni senza modifiche. Il tuo nuovo codice è thread-safe - ma non è l'ideale come se lo chiamassi da più thread contemporaneamente "approssimativamente" allo stesso tempo per mescolare due raccolte della stessa dimensione, i shuffle saranno equivalenti. Fondamentalmente, Randomè un dolore da usare, come notato nel mio articolo.
Jon Skeet,

3

Sembra un buon algoritmo di mescolamento, se non sei troppo preoccupato per le prestazioni. L'unico problema che vorrei sottolineare è che il suo comportamento non è controllabile, quindi potresti avere difficoltà a testarlo.

Una possibile opzione è avere un seme da passare come parametro al generatore di numeri casuali (o al generatore casuale come parametro), in modo da poter avere un maggiore controllo e testarlo più facilmente.


3

Ho trovato la risposta di Jon Skeet del tutto soddisfacente, ma il robo-scanner del mio cliente segnalerà qualsiasi istanza Randomcome un difetto di sicurezza. Quindi l'ho scambiato per System.Security.Cryptography.RNGCryptoServiceProvider. Come bonus, risolve il problema di sicurezza del thread menzionato. D'altra parte, RNGCryptoServiceProviderè stato misurato 300 volte più lentamente dell'uso Random.

Uso:

using (var rng = new RNGCryptoServiceProvider())
{
    var data = new byte[4];
    yourCollection = yourCollection.Shuffle(rng, data);
}

Metodo:

/// <summary>
/// Shuffles the elements of a sequence randomly.
/// </summary>
/// <param name="source">A sequence of values to shuffle.</param>
/// <param name="rng">An instance of a random number generator.</param>
/// <param name="data">A placeholder to generate random bytes into.</param>
/// <returns>A sequence whose elements are shuffled randomly.</returns>
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, RNGCryptoServiceProvider rng, byte[] data)
{
    var elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        rng.GetBytes(data);
        var swapIndex = BitConverter.ToUInt32(data, 0) % (i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}

3

Alla ricerca di un algoritmo? Puoi usare la mia ShuffleListclasse:

class ShuffleList<T> : List<T>
{
    public void Shuffle()
    {
        Random random = new Random();
        for (int count = Count; count > 0; count--)
        {
            int i = random.Next(count);
            Add(this[i]);
            RemoveAt(i);
        }
    }
}

Quindi, usalo in questo modo:

ShuffleList<int> list = new ShuffleList<int>();
// Add elements to your list.
list.Shuffle();

Come funziona?

Facciamo un elenco ordinato iniziale dei primi 5 numeri interi: { 0, 1, 2, 3, 4 }.

Il metodo inizia contando il numero di elementi e lo chiama count. Quindi, con la countriduzione su ogni passaggio, prende un numero casuale tra 0ecount e lo sposta alla fine dell'elenco.

Nel seguente esempio dettagliato, gli elementi che possono essere spostati sono in corsivo , l'elemento selezionato è in grassetto :

0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2


Questo non è O (n). RemoveAt alone è O (n).
paparazzo,

Hmm, sembra che tu abbia ragione, mia cattiva! Rimuoverò quella parte.
SteeveDroz,

1

Questo algoritmo viene mischiato generando un nuovo valore casuale per ciascun valore in un elenco, quindi ordinando l'elenco in base a tali valori casuali. Pensa a come aggiungere una nuova colonna a una tabella in memoria, quindi riempirla con GUID e quindi ordinare per quella colonna. A me sembra un modo efficiente (specialmente con lo zucchero lambda!)


1

Leggermente non correlato, ma qui è un metodo interessante (che sebbene sia davvero eccessivo, è stato REALMENTE implementato) per la generazione di lanci di dadi davvero casuali!

Dice-O-Matic

Il motivo per cui sto pubblicando questo qui è che fa alcuni punti interessanti su come i suoi utenti hanno reagito all'idea di usare gli algoritmi per mescolare, oltre i dadi reali. Ovviamente, nel mondo reale, una soluzione del genere è solo per i limiti estremi dello spettro in cui la casualità ha un impatto così grande e forse l'impatto influisce sul denaro;).


1

Direi che molte risposte qui come "Questo algoritmo si mescola generando un nuovo valore casuale per ciascun valore in un elenco, quindi ordinando l'elenco con quei valori casuali" potrebbe essere molto sbagliato!

Penso che questo NON assegna un valore casuale a ciascun elemento della raccolta di sorgenti. Invece potrebbe esserci un algoritmo di ordinamento in esecuzione come Quicksort che chiamerebbe una funzione di confronto circa n log n volte. Alcuni algortihm si aspettano davvero che questa funzione di confronto sia stabile e restituisca sempre lo stesso risultato!

Non potrebbe essere che IEnumerableSorter chiama una funzione di confronto per ogni fase dell'algoritmo, ad esempio quicksort e ogni volta chiama la funzione x => r.Next()per entrambi i parametri senza memorizzarli nella cache!

In tal caso, potresti davvero rovinare l'algoritmo di ordinamento e renderlo molto peggio delle aspettative su cui si basa l'algoritmo. Naturalmente, alla fine diventerà stabile e restituirà qualcosa.

Potrei verificarlo in seguito inserendo l'output di debug all'interno di una nuova funzione "Next", quindi guarda cosa succede. In Reflector non sono riuscito a scoprire immediatamente come funziona.


1
Non è il caso: override interno void ComputeKeys (elementi TElement [], int count); Tipo di dichiarazione: System.Linq.EnumerableSorter <TElement, TKey> Assembly: System.Core, Version = 3.5.0.0 Questa funzione crea prima un array con tutte le chiavi che consumano memoria, prima che quicksort le ordina
Christian,

Buono a sapersi, comunque solo un dettaglio dell'implementazione, che potrebbe plausibilmente cambiare nelle versioni future!
Blorgbeard esce il

-5

Tempo di avvio per l'esecuzione su codice con cancella tutti i thread e memorizza nella cache ogni nuovo test,

Primo codice non riuscito. Funziona su LINQPad. Se segui per testare questo codice.

Stopwatch st = new Stopwatch();
st.Start();
var r = new Random();
List<string[]> list = new List<string[]>();
list.Add(new String[] {"1","X"});
list.Add(new String[] {"2","A"});
list.Add(new String[] {"3","B"});
list.Add(new String[] {"4","C"});
list.Add(new String[] {"5","D"});
list.Add(new String[] {"6","E"});

//list.OrderBy (l => r.Next()).Dump();
list.OrderBy (l => Guid.NewGuid()).Dump();
st.Stop();
Console.WriteLine(st.Elapsed.TotalMilliseconds);

list.OrderBy (x => r.Next ()) utilizza 38,6528 ms

list.OrderBy (x => Guid.NewGuid ()) utilizza 36.7634 ms (è consigliato da MSDN.)

la seconda volta dopo entrambi usano nello stesso tempo.

MODIFICA: CODICE DI PROVA su Intel Core i7 4@2.1GHz, RAM 8 GB DDR3 @ 1600, HDD SATA 5200 rpm con [Data: www.dropbox.com/s/pbtmh5s9lw285kp/data]

using System;
using System.Runtime;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Threading;

namespace Algorithm
{
    class Program
    {
        public static void Main(string[] args)
        {
            try {
                int i = 0;
                int limit = 10;
                var result = GetTestRandomSort(limit);
                foreach (var element in result) {
                    Console.WriteLine();
                    Console.WriteLine("time {0}: {1} ms", ++i, element);
                }
            } catch (Exception e) {
                Console.WriteLine(e.Message);
            } finally {
                Console.Write("Press any key to continue . . . ");
                Console.ReadKey(true);
            }
        }

        public static IEnumerable<double> GetTestRandomSort(int limit)
        {
            for (int i = 0; i < 5; i++) {
                string path = null, temp = null;
                Stopwatch st = null;
                StreamReader sr = null;
                int? count = null;
                List<string> list = null;
                Random r = null;

                GC.Collect();
                GC.WaitForPendingFinalizers();
                Thread.Sleep(5000);

                st = Stopwatch.StartNew();
                #region Import Input Data
                path = Environment.CurrentDirectory + "\\data";
                list = new List<string>();
                sr = new StreamReader(path);
                count = 0;
                while (count < limit && (temp = sr.ReadLine()) != null) {
//                  Console.WriteLine(temp);
                    list.Add(temp);
                    count++;
                }
                sr.Close();
                #endregion

//              Console.WriteLine("--------------Random--------------");
//              #region Sort by Random with OrderBy(random.Next())
//              r = new Random();
//              list = list.OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with OrderBy(Guid)
//              list = list.OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with Parallel and OrderBy(random.Next())
//              r = new Random();
//              list = list.AsParallel().OrderBy(l => r.Next()).ToList();
//              #endregion

//              #region Sort by Random with Parallel OrderBy(Guid)
//              list = list.AsParallel().OrderBy(l => Guid.NewGuid()).ToList();
//              #endregion

//              #region Sort by Random with User-Defined Shuffle Method
//              r = new Random();
//              list = list.Shuffle(r).ToList();
//              #endregion

//              #region Sort by Random with Parallel User-Defined Shuffle Method
//              r = new Random();
//              list = list.AsParallel().Shuffle(r).ToList();
//              #endregion

                // Result
//              
                st.Stop();
                yield return st.Elapsed.TotalMilliseconds;
                foreach (var element in list) {
                Console.WriteLine(element);
            }
            }

        }
    }
}

Descrizione del risultato: https://www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
Stat dei risultati: https://www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG

Conclusione: si
supponga che LINQ OrderBy (r.Next ()) e OrderBy (Guid.NewGuid ()) non siano peggiori del metodo Shuffle definito dall'utente nella prima soluzione.

Risposta: sono contraddizioni.


1
La seconda opzione non è corretta e pertanto le prestazioni sono irrilevanti . Anche questo non risponde ancora alla domanda se l'ordinazione tramite un numero casuale sia accettabile, efficiente o come funzioni. La prima soluzione ha anche problemi di correttezza, ma sono non come grande di un affare.
Servito il

Siamo spiacenti, vorrei sapere qual è il tipo di parametro migliore di Quicksort di Linq OrderBy? Ho bisogno di testare le prestazioni. Tuttavia, penso che il tipo int abbia solo una velocità migliore della stringa di Guid, ma non lo è. Ho capito perché MSDN ha raccomandato. La prestazione della prima soluzione modificata è la stessa di OrderBy con istanza casuale.
GMzo,

Qual è lo scopo di misurare le prestazioni del codice che non risolve il problema? Le prestazioni sono solo una considerazione da fare tra due soluzioni che funzionano entrambe . Quando si dispone di soluzioni di lavoro, allora si può iniziare a confrontarli.
Servito il

Devo avere il tempo di testare più dati, quindi se è finito, prometto di postare di nuovo. Supponiamo: penso che Linq OrderBy non sia peggio della prima soluzione. Opinione: è facile da usare e da capire.
GMzo,

È notevolmente meno efficiente degli algoritmi di shuffle casuale molto semplici, ma, ancora una volta, le prestazioni sono irrilevanti . Non stanno mescolando in modo affidabile i dati, oltre ad essere meno performanti.
Servito il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.