Nie sądzę, można to zrobić lepiej niż O (N) złożoności czasowej - w najgorszym wypadku będziesz musiał dotknąć każdy element zbioru danych, aby znaleźć duplikaty
Jednym ze sposobów poprawy Zużycie przestrzeni (kosztem wymagającego odrobiny twiddlingu, a także dwóch przebiegów przez zbiór danych) jest użycie Bloom Filter. Pomysł polega na pierwszym przejściu przez zbiór danych: jeśli znajdziesz możliwy duplikat, usuń go z zestawu danych i dodaj go do tablicy mieszającej (jeśli filtr kwitnący działa poprawnie, to tylko około 1% elementów zostanie oznaczonych flagą w miarę możliwości duplikaty). Następnie przeprowadź drugie przejście przez przefiltrowany zbiór danych, testując elementy pod kątem małej tablicy mieszania możliwych duplikatów.
Mój kod będzie w Javie, ponieważ jest to język, który jestem najbardziej znany.
Class DupFinder {
BloomFilter filter = new BloomFilter();
HashTable hashTable = new HashTable();
int start = 0;
int run(int[] dataset) {
// first pass
for(int i = 0; i < dataset.length; i++) {
if(filter.contains(dataset[i]) {
// check if element is in hashTable, else add it
if(hashTable.contains(dataset[i]) {
return dataset[i]; // duplicate found
} else {
hashTable.add(dataset[i]);
}
// remove element from dataset
int temp = dataset[start];
dataset[start] = dataset[i];
dataset[i] = temp;
start++;
} else filter.add(dataset[i]);
}
// second pass
for(int i = start; i < dataset.length; i++) {
if(hashTable.contains(dataset[i]) {
return dataset[i]; // duplicate found
}
}
return NULL; // no duplicate found
}
}
Alternatywą do tabeli hash jest użycie Radix Sort, algorytm sortowania czasie liniowym. Sortowanie Radix będzie miało lepszą wydajność w najgorszym przypadku (O (n) dla sortowania radix, w porównaniu do O (n^2) dla tabeli mieszania w mało prawdopodobnym przypadku, gdy napotkasz śmieszną liczbę kolizji), ale gorsza wydajność średniej wielkości (Implementacja tablicy hash będzie zwykle znajdować duplikat po zeskanowaniu tylko połowy zestawu danych, podczas gdy sortowanie radix zawsze będzie wymagało wielu przejść przez zbiór danych). Sortowanie Radix będzie również nieznacznie bardziej wydajne pod względem wykorzystania przestrzeni, jeśli użyjesz przestrzennej struktury danych dla wiader, np. lista porcji.
Można dokonać równoległej implementacji tabeli mieszania bez ponoszenia nadmiernych kosztów synchronizacji. Używając wątków t, każdy wątek przetwarza elementy zbioru danych (np.jeśli masz 32 elementy w zestawie danych i 2 wątki, wątek0 przetwarza elementy 0-15, a wątek1 przetwarza elementy 16-31), umieszczając każdy element w wiadrze z indeksem absoluteValue (x modulo t). Następnie każdy wątek będzie odpowiedzialny za przetwarzanie wszystkich elementów z danym indeksem wiadra, np. thread0 przetworzy wszystkie segmenty z indeksem 0. Do synchronizacji używam BlockingQueue - umożliwia to wątkowi wywołanie take() w kolejce, powodując, że wątek usuwa pierwszy element kolejki lub blokuje do momentu element staje się dostępny; wszystkie inne struktury danych są wątkowo lokalne. Będziesz musiał zainicjować zmienną dupFinders, aby wystąpienie DupFinder pojawiło się w tym samym indeksie każdej zmiennej DupFindera dupFinders (np. Thread0 zawsze pojawia się w indeksie 0, zapewniając w ten sposób, że wszystkie elementy w jego klasie BlockingQue mają absoluteValue (x modulo t) == 0).
Class DupFinder implements Callable<Integer> {
private Class Chunk {
int size = 0;
int chunk = new int[64];
boolean add(int x) {
if(size < 64) {
chunk[size] = x;
size++;
return true;
} else return false;
}
}
int t = ??? // number of threads
private BlockingQueue<Stack<Chunk>> queue = new LinkedBlockingQueue()
private DupFinder[] dupFinders = new DupFinder[t];
private Stack<Chunk>[] stacks = new Stack<Chunk>[t];
void add(Stack<Chunk> stack) {
queue.add(stack);
}
// the thread only receives n/t elements of the dataset
int call(int[] partialDataset) {
// partition dataset elements by their modulus(t)
for(int i = 0; i < partialDataset.length; i++) {
tempStack = stacks[Math.absoluteValue(partialDataset[i] modulo t)];
if(!tempStack.peek().add(partialDataset[i])) {
Chunk chunk = new Chunk();
chunk.add(partialDataset[i]);
tempStack.push(chunk);
}
}
// distribute chunk stacks to the appropriate threads
for(int i = 0; i < t; i++) {
dupFinders[i].add(stacks[i]);
}
HashTable hashTable = new HashTable();
for(int i = 0; i < t; i++) {
// wait for a chunk stack to become available
Stack<Chunk> tempStack = queue.take();
while(!tempStack.isEmpty) {
tempChunk = tempStack.pop();
for(int i = 0; i < tempChunk.size; i++) {
if(hashTable.contains(tempChunk.chunk[i]) {
return tempChunk.chunk[i]; // duplicate found
} else {
hashTable.add(tempChunk.chunk[i]);
}
}
}
}
return NULL; // no duplicate found
}
}
Numery te są wystarczająco małe (i wszystkie> = 0), aby zmieścić się jako bity w pojedynczej krótkiej liczbie całkowitej. Nie hash, a następnie * set * - i będąc nieco ustawionym, możesz efektywnie testować każdy nowy przedmiot. – usr2564301
Jak duża jest tablica i jaki jest zakres elementów w tablicy? Czy jest to arbitralnie duża tablica liczb całkowitych? A może jest to mały zestaw małych wartości? –
@JimMischel Jest to arbitralnie duża tablica i nie możemy pozwolić sobie na sortowanie tablicy. – CRM