L'ho già pubblicato una volta su SO, ma lo riprodurrò qui perché è piuttosto interessante. Usa l'hashing, costruendo qualcosa come un set di hash sul posto. È garantito che sia O (1) nello spazio ascellare (la ricorsione è una chiamata di coda), ed è tipicamente O (N) complessità temporale. L'algoritmo è il seguente:
- Prendi il primo elemento dell'array, questa sarà la sentinella.
- Riordina il resto dell'array, per quanto possibile, in modo che ogni elemento si trovi nella posizione corrispondente al suo hash. Al termine di questo passaggio, verranno rilevati i duplicati. Impostali uguali a sentinella.
- Sposta tutti gli elementi per cui l'indice è uguale all'hash all'inizio della matrice.
- Sposta tutti gli elementi che sono uguali a sentinel, tranne il primo elemento della matrice, alla fine della matrice.
- Ciò che rimane tra gli elementi correttamente hash e gli elementi duplicati saranno gli elementi che non è stato possibile posizionare nell'indice corrispondente al loro hash a causa di una collisione. Ricorso per affrontare questi elementi.
Questo può essere dimostrato essere O (N) a condizione che non vi sia alcuno scenario patologico nell'hashing: anche se non ci sono duplicati, circa 2/3 degli elementi verranno eliminati ad ogni ricorsione. Ogni livello di ricorsione è O (n) dove piccolo n è la quantità di elementi rimasti. L'unico problema è che, in pratica, è più lento di un ordinamento rapido quando ci sono pochi duplicati, cioè molte collisioni. Tuttavia, quando ci sono enormi quantità di duplicati, è incredibilmente veloce.
Modifica: nelle attuali implementazioni di D, hash_t è di 32 bit. Tutto ciò che riguarda questo algoritmo presuppone che ci saranno pochissime, se non nessuna, collisioni hash nello spazio completo a 32 bit. Tuttavia, le collisioni possono verificarsi frequentemente nello spazio del modulo. Tuttavia, questa ipotesi sarà con ogni probabilità vera per qualsiasi set di dati di dimensioni ragionevoli. Se la chiave è minore o uguale a 32 bit, può essere il proprio hash, il che significa che una collisione nello spazio completo di 32 bit è impossibile. Se è più grande, semplicemente non ne puoi inserire abbastanza nello spazio degli indirizzi di memoria a 32 bit perché sia un problema. Presumo che hash_t verrà aumentato a 64 bit nelle implementazioni a 64 bit di D, dove i set di dati possono essere più grandi. Inoltre, se questo dovesse mai rivelarsi un problema, si potrebbe cambiare la funzione hash ad ogni livello di ricorsione.
Ecco un'implementazione nel linguaggio di programmazione D:
void uniqueInPlace(T)(ref T[] dataIn) {
uniqueInPlaceImpl(dataIn, 0);
}
void uniqueInPlaceImpl(T)(ref T[] dataIn, size_t start) {
if(dataIn.length - start < 2)
return;
invariant T sentinel = dataIn[start];
T[] data = dataIn[start + 1..$];
static hash_t getHash(T elem) {
static if(is(T == uint) || is(T == int)) {
return cast(hash_t) elem;
} else static if(__traits(compiles, elem.toHash)) {
return elem.toHash;
} else {
static auto ti = typeid(typeof(elem));
return ti.getHash(&elem);
}
}
for(size_t index = 0; index < data.length;) {
if(data[index] == sentinel) {
index++;
continue;
}
auto hash = getHash(data[index]) % data.length;
if(index == hash) {
index++;
continue;
}
if(data[index] == data[hash]) {
data[index] = sentinel;
index++;
continue;
}
if(data[hash] == sentinel) {
swap(data[hash], data[index]);
index++;
continue;
}
auto hashHash = getHash(data[hash]) % data.length;
if(hashHash != hash) {
swap(data[index], data[hash]);
if(hash < index)
index++;
} else {
index++;
}
}
size_t swapPos = 0;
foreach(i; 0..data.length) {
if(data[i] != sentinel && i == getHash(data[i]) % data.length) {
swap(data[i], data[swapPos++]);
}
}
size_t sentinelPos = data.length;
for(size_t i = swapPos; i < sentinelPos;) {
if(data[i] == sentinel) {
swap(data[i], data[--sentinelPos]);
} else {
i++;
}
}
dataIn = dataIn[0..sentinelPos + start + 1];
uniqueInPlaceImpl(dataIn, start + swapPos + 1);
}