Já postei isso uma vez no SO, mas vou reproduzir aqui porque é muito legal. Ele usa hashing, criando algo como um conjunto de hash no local. É garantido que é O (1) no espaço axilar (a recursão é uma chamada final) e é tipicamente O (N) complexidade de tempo. O algoritmo é o seguinte:
- Pegue o primeiro elemento da matriz, este será o sentinela.
- Reordene o resto da matriz, tanto quanto possível, de forma que cada elemento fique na posição correspondente ao seu hash. Quando esta etapa for concluída, duplicatas serão descobertas. Defina-os como sentinela.
- Mova todos os elementos para os quais o índice é igual ao hash para o início da matriz.
- Mova todos os elementos iguais a sentinela, exceto o primeiro elemento da matriz, para o final da matriz.
- O que resta entre os elementos com hash adequado e os elementos duplicados são os elementos que não puderam ser colocados no índice correspondente ao seu hash devido a uma colisão. Recurse para lidar com esses elementos.
Isso pode ser mostrado como O (N), desde que não haja cenário patológico no hashing: Mesmo se não houver duplicatas, aproximadamente 2/3 dos elementos serão eliminados a cada recursão. Cada nível de recursão é O (n), onde n pequeno é a quantidade de elementos restantes. O único problema é que, na prática, é mais lento do que uma classificação rápida quando há poucas duplicatas, ou seja, muitas colisões. No entanto, quando há grandes quantidades de duplicatas, é incrivelmente rápido.
Edit: Nas implementações atuais de D, hash_t é de 32 bits. Tudo sobre esse algoritmo pressupõe que haverá muito poucas, se houver, colisões de hash no espaço de 32 bits completo. As colisões podem, no entanto, ocorrer freqüentemente no espaço do módulo. No entanto, essa suposição será provavelmente verdadeira para qualquer conjunto de dados de tamanho razoável. Se a chave for menor ou igual a 32 bits, ela pode ser seu próprio hash, o que significa que uma colisão em todo o espaço de 32 bits é impossível. Se for maior, você simplesmente não conseguirá colocar o suficiente deles no espaço de endereço da memória de 32 bits para que seja um problema. Presumo que hash_t será aumentado para 64 bits em implementações de D de 64 bits, onde os conjuntos de dados podem ser maiores. Além disso, se isso se provar um problema, pode-se alterar a função hash em cada nível de recursão.
Esta é uma implementação na linguagem de programação 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);
}