Wydajna struktura danych obsługująca wstawianie, usuwanie i większość częstotliwości


14

Załóżmy, że mamy zestaw a każdy element jest parą danych i kluczy. Chcemy struktury danych, która obsługiwałaby następujące operacje:DD

  • Wstaw do ,(d,k)D
  • Usuń członka , (nie trzeba szukać, aby znaleźć , np. wskazuje na członka w ),eeeD
  • MostFrequent, który zwraca element członkowski dzięki czemu jest jednym z najczęstszych kluczy w (należy pamiętać, że najczęściej używany klucz nie musi być unikalny).e∈De.keyD

Jaka byłaby efektywna implementacja tej struktury danych?

Moim rozwiązaniem jest sterta kluczy i ich częstotliwości, dla których priorytetem są częstotliwości oraz tabela skrótów, w której funkcja skrótu odwzorowuje elementy z tym samym kluczem do tego samego miejsca w tablicy skrótów (ze wskaźnikami od każdej części do drugiej).

Może to dać dla pierwszych dwóch operacji i dla trzeciego (najgorszy czas działania).Θ(lg⁡n)Θ(1)

Zastanawiam się, czy istnieje bardziej wydajne rozwiązanie? (lub prostsze rozwiązanie o tej samej wydajności?)


Jeśli chcesz, możesz użyć prostego, zbalansowanego drzewa wyszukiwania binarnego zamiast tabeli skrótów.
— Joe

Tabela skrótów zajmuje dużo nieprzyzwoitej przestrzeni, zaproponowałbym kolejkę priorytetową. Zapewniłoby to tę samą złożoność czasu przy wstawianiu i usuwaniu, ale złożoność pamięci byłaby lepsza.
— Bartosz Przybylski,

@Joe, użycie BST zamiast tabeli skrótów sprawiłoby, że operacje MostFrequent byłyby mniej wydajne, ale może to być rozsądny kompromis dla pamięci.
— Kaveh

2
Jeśli używasz tylko porównań, przynajmniej jedna z wartości Insert / MostFrequent musi zostać zamortyzowana , ze względu na dolne granice problemu rozróżnienia elementów. Ω(log⁡n)
— Aryabhata

1
Istnieje również kilka interesujących struktur w modelu przesyłania strumieniowego. springerlink.com/content/t17nhd9hwwry909p
— Joe

Odpowiedzi:


7

W modelu obliczeń opartym na porównaniu można zaimplementować kolejkę priorytetową za pomocą sterty Fibonacciego zamiast zwykłej sterty. To da ci następujące granice: zamortyzowany czas wstawiania i zamortyzowany czas operacji usuwania.O(1)O(log⁡n)

Jeśli odejdziesz od modelu opartego na porównaniu i przyjmiesz model pamięci RAM, w którym klucze są traktowane jako ciągi binarne, z których każdy zawiera jedno lub więcej słów maszynowych, możesz zaimplementować swoją kolejkę priorytetową w . Rzeczywiście, możesz osiągnąć zarówno operacje wstawiania, jak i usuwania i czas na operację findMin. Thorup to udowodniło(log⁡n)O(log⁡log⁡n)O(1)

Jeśli potrafimy posortować kluczy według czasu według klucza, możemy zaimplementować kolejkę priorytetową obsługującą find-min w stałym czasie oraz aktualizacje (wstawianie i usuwanie) w czasie .nS(n)S(n)

Zobacz M. Thorup. Równoważność między kolejkami priorytetowymi a sortowaniem, 2002. w Proc. FOCS 2002

Ponieważ możemy sortować w oczekiwany czas i przestrzeń liniową, jak pokazano za pomocąO(nlog⁡log⁡n)

Y. Han i M. Thorup. Sortowanie liczb całkowitych w oczekiwany czas i przestrzeń liniowa. w Proc. FOCS 2002O(nlog⁡log⁡n)

granica jest udowodniona.


1

Możesz to zrobić w oczekiwanym zamortyzowanym czasie. Podstawową sztuczką jest to, że nie potrzebujemy pełnej mocy kolejki priorytetowej, ponieważ częstotliwość klawiszy zmienia się tylko o 1 podczas każdego wstawiania lub usuwania.O(1)

Moje rozwiązanie poniżej to tak naprawdę twoje rozwiązanie z „nieefektywną” kolejką priorytetową, która akurat działa dobrze w tym przypadku: kolejka maksimum priorytetu zaimplementowana jako podwójnie połączone listy segmentów kluczy ma O (1) insertMin, deleteMax, removeFromBucket i wzrostKlucz.


Zachowaj podwójnie połączoną listę segmentów, gdzie każdy segment ma niepusty zestaw kluczy mieszających (które nazywam kohortą) i dodatnią liczbę całkowitą (które nazywam ValCount). W segmencie b każdy klucz k w kohorcie b ma taką samą liczbę unikalnych wartości związanych z nim w zestawie, który utrzymujesz. Na przykład, jeśli twój zestaw zawiera pary (a, jabłko), (a, awokado), (b, banan), (c, ogórek), (d, owoc smoka), gdzie pojedyncze litery to klucze, a owoce to wartości, wtedy miałbyś dwa Wiadra: Jeden Wiadro miałby ValCount 2 i Kohortę składającą się tylko z jednego klucza: a. Drugi segment miałby wartość ValCount równą 1 i kohortę złożoną z trzech kluczy b, c i d.

ValCount powinna przechowywać uporządkowaną podwójnie listę Bucket. Ważne będzie, abyśmy mogli znaleźć głowę i ogon listy w czasie i abyśmy mogli podzielić na nowy segment Wiadra w czasie jeśli znamy jego sąsiadów. Niewyobrażalnie nazywam listę Buckets listą BucketList.O(1)O(1)

Oprócz BucketList potrzebujemy SetMap, który jest kluczem mapującym mapę skrótów do ValueBuckets. ValueBucket to para składająca się z ValueSet (niepustego zestawu wartości skrótu) i niepustego wskaźnika do segmentu. ValueSet powiązany z kluczem k zawiera wszystkie unikalne wartości związane z k. Wskaźnik segmentu skojarzony z ValueSet ma kohortę równą wielkości ValueSet. Wiadro powiązane z kluczem k w SetMap jest również powiązane z kluczem k w BucketList.

W C ++:

struct Bucket {
    unsigned ValCount;
    unordered_set<Key> Cohort;
    Bucket * heavier;
    Bucket * lighter;
};
Bucket * BucketListHead;
Bucket * BucketListTail;

struct ValueBucket {
  unordered_set<Value> ValueSet;
  Bucket * bucket;
};
unordered_map<Key, ValueBucket> SetMap;

Aby znaleźć parę klucz-wartość o maksymalnej częstotliwości, wystarczy spojrzeć na nagłówek BucketList, znaleźć klucz w kohorcie, wyszukać ten klucz w SetMap i znaleźć wartość w ValueSet jego ValueBucket. (uff!)

Wstawianie i usuwanie par klucz-wartość jest trudniejsze.

Aby wstawić lub usunąć parę klucz-wartość, najpierw wstawiamy lub usuwamy ją w SetMap. Spowoduje to zmianę rozmiaru ValueSet, dlatego musimy zmodyfikować segment powiązany z kluczem. Jedynymi wiadrami, na które musimy spojrzeć, aby dokonać tej zmiany, będą najbliżsi sąsiedzi wiadra, w którym kiedyś był klucz. Jest tu kilka przypadków i prawdopodobnie nie warto ich w pełni opisywać, chociaż byłbym szczęśliwy rozwinąć, jeśli nadal masz problemy.


Dzięki. Właściwie mieliśmy podobny pomysł na rozwiązanie, ale był z tym problem. Muszę sprawdzić, na czym polega problem, ponieważ nie pamiętam go teraz. Sprawdzę to dokładniej w przyszłym tygodniu, aby sprawdzić, czy pozwoli to uniknąć problemu, jaki miało nasze rozwiązanie.
— Kaveh

Oto inny sposób myślenia o moim rozwiązaniu: tak naprawdę to tylko twoje rozwiązanie z „nieefektywną” kolejką priorytetową, która akurat działa dobrze w tym przypadku. Kolejka o maksymalnym priorytecie zaimplementowana jako podwójnie połączone listy segmentów kluczy ma O (1) insertMin, deleteMax, removeFromBucket i zwiększKey.
— jbapple 13.01.2013

Najskuteczniejszym sposobem (w najgorszym przypadku) utrzymania mapowania z kluczy dla ValueBuckets jest prawdopodobnie drzewo wyszukiwania.
— Raphael

Raphael - Nie jestem pewien, o co ci chodzi. Czy mówisz, że tabele skrótów są w praktyce drogie, czy też mają najgorszą wydajność w najgorszym przypadku, czy może trzecią rzecz?
— jbapple 15.01.2013

-3

Złożoność najgorszego przypadku

O(1)

O(1)

O(1)

O(log⁡log⁡n)

O(n)

Dowód

[0,N) O(log log min{n,N})

Który ustalono za pomocą kombinacji:

τ(n,N) n[0,N) τ(n,N)≤τ(N,N)τ

i:

n[0,N)O(1+log log n−log log q)q

Odniesienie

Thorup, Mikkel. „Kolejki całkowite z malejącym kluczem w stałym czasie i problem z najkrótszymi ścieżkami z jednego źródła”. W materiałach z trzydziestego piątego dorocznego sympozjum ACM na temat teorii obliczeń, 149–158. STOC '03. Nowy Jork, NY, USA: ACM, 2003.


Zauważ, że we wszystkich kolejkach priorytetowych przejście do struktury obsługującej „get-min” i „extract-min” jest trywialne, a do struktury obsługującej „get-max” i „extract-max”.
— AT

Ping ...: @Kaveh
— AT
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.