Algorytmy drzewa oparte na łączeniach

W informatyce algorytmy drzewa oparte na łączeniach są klasą algorytmów samobalansujących drzew wyszukiwania binarnego . Ramy te mają na celu zaprojektowanie wysoce równoległych algorytmów dla różnych zrównoważonych drzew wyszukiwania binarnego. Ramy algorytmiczne opierają się na pojedynczej operacji łączenia . W tych ramach łączenia obejmuje wszystkie kryteria równoważenia różnych schematów równoważenia, a wszystkie inne łączenie funkcji ma ogólną implementację w różnych schematach równoważenia. Algorytmy oparte na łączeniach można zastosować do co najmniej czterech schematów równoważenia: drzew AVL , drzew czerwono-czarnych , drzew zbalansowanych wagowo i treapów .

( Displaystyle ( przyjmuje jako dane wejściowe dwa zrównoważone drzewa binarne samego schematu równoważenia i wyprowadza nowe zrównoważone drzewo binarne, przechodzenie w kolejności jest przechodzeniem w kolejności , a następnie a następnie przechodzenie w kolejności . W szczególności, jeśli drzewa są drzewami wyszukiwania , co oznacza, że ​​kolejność drzew zachowuje całkowite uporządkowanie kluczy, musi spełniać warunek, że wszystkie klucze w są mniejsze niż i wszystkie klucze w większe niż .

Historia

Operacja łączenia została po raz pierwszy zdefiniowana przez Tarjana na czerwono-czarnych drzewach , która przebiega w czasie logarytmicznym najgorszego przypadku. Później Sleator i Tarjan opisali łączenia dla drzew splay , który działa w amortyzowanym czasie logarytmicznym. Później Adams rozszerzył łączenie na drzewa z wyważoną wagą i użył go do szybkich funkcji zestawu-ustawienia, w tym sumy , przecięcia i różnicy zestawów . W 1998 r. Blelloch i Reid-Miller przedłużyli współpracę na treaps i udowodnił, że granica ustawionych funkcji wynosi dla dwóch drzew o rozmiarze n , co jest optymalne w modelu porównawczym Podnieśli również równoległość w algorytmie Adamsa, stosując schemat dziel i zwyciężaj . W 2016 roku Blelloch i in. formalnie zaproponował algorytmy łączenia i sformalizował łączenia dla czterech różnych schematów równoważenia: drzewa AVL , drzewa czerwono-czarne , drzewa o zrównoważonej wadze i treaps . W tej samej pracy udowodnili, że algorytmy Adamsa dotyczące sumy, przecięcia i różnicy są optymalne pod względem pracy we wszystkich czterech schematach równoważenia.

Dołącz do algorytmów

Funkcja join schematu Jeśli oba drzewa są zrównoważone, polecenie join po prostu tworzy nowy węzeł z lewym poddrzewem t 1 , korzeniem k i prawym poddrzewem t 2 . Załóżmy, że t 1 jest cięższy (ten „cięższy” zależy od schematu równoważenia) niż t 2 (drugi przypadek jest symetryczny). Join podąża za prawym grzbietem t 1 aż do węzła c , który jest zrównoważony z t 2 . W tym momencie tworzony jest nowy węzeł z lewym dzieckiem c , korzeniem k i prawym dzieckiem t 2 w celu zastąpienia c. Nowy węzeł może unieważnić niezmiennik bilansujący. Można to naprawić za pomocą obrotów.

Poniżej przedstawiono algorytmy łączenia w różnych schematach równoważenia.

łączenia dla drzew AVL :

             funkcja  joinRightAVL(T  L  , k, T  R  ) (l, k', c) := ujawnij(T  L  )  jeśli  h(c) ≤ h(T  R  ) + 1 T' := Węzeł(c, k, T  R  )  if  h(T') ≤ h(l) + 1  return  Node(l, k', T')  else  return  obróćw lewo(węzeł(l, k', obróćw prawo(T')))  else  T' := joinRightAVL (c, k, T  R  ) T :  =  Węzeł(l, k', T') 
             if  h(T') ≤ h(l) + 1  return  T  else  return  funkcji  joinLeft(T)  joinLeftAVL(T  L  , k, T  R  ) /* symetrycznie do joinRightAVL */  function  join(T  L  , k, T  R  )  if  h(T  L  ) > h(T  R  ) + 1  return  joinRightAVL(T  L  , k, T  R  )  inaczej if  h(T  R  ) > h(T  L 
         ) + 1  return  joinLeftAVL(TL  ,  k, T  R  )  inaczej  return  Node(TL  ,  k,  TR  ) 

Gdzie:

  • to wysokość węzła .
  • wyodrębnia lewe dziecko , klucz i prawe dziecko węzła w krotkę .
  • tworzy węzeł z lewym dzieckiem i prawym dzieckiem , klucz .

Algorytm łączenia drzew czerwono-czarnych dla :

 funkcja  joinRightRB(T  L  , k, T  R  )  if  r(T  L  ) = ⌊r(T  R  )/2⌋ × 2  powrót  Node(T  L  , ⟨k, red⟩, T  R  )  else  (L', ⟨ k', c'⟩, R') := odsłonić(T  L  ) T' := Węzeł(L', ⟨k', c'⟩, joinRightRB(R', k, T  R  ))  if  c' = czarny  I 
             T'.right.color = T'.right.right.color = red T'.right.right.color := czarny  return  obróćw lewo(T')  else  return  T'  funkcja  joinLeftRB(T  L  , k, T  R  ) / * symetrycznie do joinRightRB */  funkcja  join(T  L  , k, T  R  )  jeśli  ⌊r(T  L  )/2⌋ > ⌊r(T  R  )/2⌋ × 2 T' := joinRightRB(T  L  , k, T  R  )  jeśli 
         (T'.kolor = czerwony)  i  (T'.prawy.kolor = czerwony) T'.kolor := czarny  zwróć  T'  inaczej, jeśli  ⌊r(T  R  )/2⌋ > ⌊r(T  L  )/2⌋ × 2 /* symetryczny */  inaczej jeśli  T  L  .color = czarny  i  T  R  = czarny  powrót  Node(T  L  , ⟨k, red⟩, T  R  )  inaczej  powrót  Node(T  L  , ⟨k, czarny⟩, T  R  ) 

Gdzie:

  • dwukrotność czarnej wysokości czarnego węzła i dwukrotność czarnej wysokości czerwonego węzła.
  • wyodrębnia lewe dziecko , klucz , kolor i prawe dziecko węzła w krotkę .
  • tworzy węzeł z lewym dzieckiem , klucz , kolor i prawe dziecko .

Algorytm łączenia dla drzew o zrównoważonej wadze :

 funkcja  joinRightWB(T  L  , k, T  R  ) (l, k', c) := odsłonić(T  L  )  if  w(T  L  ) =  α  w(T  R  )  powrót  Węzeł(T  L  , k, T  R  )  else  T' := joinRightWB(c, k, T  R  ) (l  1  , k  1  , r  1  ) := odsłonić(T')  if  w(l) =  α  w(T') 
             zwróć  Węzeł(l, k', T')  inaczej jeśli  w(l) =  α  w(l  1  )  i  w(l)+w(l  1  ) =  α  w(r  1  )  zwróć  obróćw lewo(węzeł(l, k ', T'))  else  return  obróćw lewo(węzeł(l, k', obróćw prawo(T'))  funkcja  joinLeftWB(T  L  , k, T  R  ) /* symetrycznie do joinRightWB */  funkcja  join(T  L  , k, T  R 
         )  if  w(T  L  ) >  α  w(T  R  )  return  joinRightWB(TL  ,  k,  TR  )  else if  w(T  R  ) >  α  w(T  L  )  return  joinLeftWB(TL  ,  k,  TR  )  w przeciwnym razie  zwróć  Węzeł(T  L  , k, T  R  ) 

Gdzie:

  • to waga węzła .
  • oznacza wagi i są α- zrównoważony wagowo.
  • oznacza wagę jest cięższa niż waga w odniesieniu do bilansu wagowego α.
  • wyodrębnia lewe dziecko , klucz i prawe dziecko węzła w krotkę .
  • tworzy węzeł z lewym dzieckiem kluczem i prawym dzieckiem .

Algorytmy oparte na łączeniach

Poniżej dziecko , klucz prawe dziecko węzła krotkę . tworzy węzeł z lewym dzieckiem kluczem prawym dzieckiem . „ " oznacza, że ​​dwie instrukcje działać w równoległy.

Podział

Aby podzielić drzewo na dwa drzewa, mniejsze niż klucz x i większe niż klucz x , najpierw rysujemy ścieżkę od korzenia, wstawiając x do drzewa. Po tym wstawieniu wszystkie wartości mniejsze niż x zostaną znalezione po lewej stronie ścieżki, a wszystkie wartości większe niż x zostaną znalezione po prawej stronie. Stosując Join , wszystkie poddrzewa po lewej stronie są scalane od dołu do góry za pomocą kluczy na ścieżce jako węzłów pośrednich od dołu do góry, tworząc lewe drzewo, a prawa część jest asymetryczna. W przypadku niektórych aplikacji Split zwraca również wartość logiczną oznaczającą, czy x występuje w drzewie. Koszt podziału to , kolejność wysokości drzewa.

Algorytm podziału jest następujący:

             funkcja  split(T, k)  if  (T = nil)  return  (nil, false, nil)  else  (L, m, R) := ujawnij(T)  if  k < m (L', b, R') := split(L, k)  return  (L', b, join(R', m, R))  else if  k > m (L', b, R') := split(R, k)  return  (join(L , m, L'), b, R'))  else  return  (L, true, R) 

Dołącz2

Ta funkcja jest zdefiniowana podobnie jak join , ale bez środkowego klawisza. Najpierw rozdziela ostatni klucz łączy pozostałą część lewego drzewa z prawym drzewem za pomocą . Algorytm jest następujący:

 funkcja  splitLast(T) (L, k, R) := wyeksponuj(T)  if  R = zero  return  (L, k)  else  (T', k') := splitLast(R)  return  (join(L, k, T'), k')  funkcja  join2(L, R)  if  L = zero  return  R  else  (L', k) := splitLast(L)  return  join(L', k, R) 

Koszt wynosi dla drzewa o rozmiarze .

Wstaw i usuń

Algorytmy wstawiania i usuwania przy użyciu łączenia mogą być niezależne od schematów równoważenia. W celu wstawienia algorytm porównuje klucz, który ma zostać wstawiony, z kluczem w korzeniu, wstawia go do lewego/prawego poddrzewa, jeśli klucz jest mniejszy/większy niż klucz w korzeniu, i ponownie łączy dwa poddrzewa z korzeniem . Usunięcie porównuje klucz do usunięcia z kluczem w katalogu głównym. Jeśli są równe, zwróć join2 na dwóch poddrzewach. W przeciwnym razie usuń klucz z odpowiedniego poddrzewa i połącz oba poddrzewa z korzeniem. Algorytmy są następujące:

             funkcja  insert(T, k)  if  T = nil  return  Node(nil, k, nil)  else  (L, k', R) := wyeksponuj(T)  if  k < k'  return  join(insert(L,k), k', R)  else if  k > k'  return  join(L, k', insert(R, k))  else  return  T  funkcja  delete(T, k)  if  T = zero  return  zero  else 
             (L, k', R) := ujawnij(T)  if  k < k'  return  join(delete(L, k), k', R)  else if  k > k'  return  join(L, k', delete( R, k))  w przeciwnym razie  zwróć  sprzężenie2(L, R) 

i usuwanie jeśli .

Funkcje zestaw-ustaw

Na drzewach o zrównoważonych wagach zdefiniowano kilka operacji na zbiorach: suma , przecięcie i różnica zbiorów . Suma dwóch zrównoważonych wagowo drzew t 1 i t 2 reprezentujących zbiory A i B , jest drzewem t reprezentującym A B . Poniższa funkcja rekurencyjna oblicza tę sumę:

    
     funkcja  suma(t  1  , t  2  )  jeśli  t  1  = zero  zwróć  t  2  w przeciwnym razie jeśli  t  2  = zero  zwróć  t  1  w przeciwnym razie  (l  1  , k  1  , r  1  ) := ujawnij (t  1  ) (t  <  , b, t  >  ) := podział(t  2  , k  1  ) l' := suma (l  1  , t  <  ) || r' := suma(r   1  , t  >  )  return  join(l', k  1  , r') 

Podobnie algorytmy przecięcia i różnicy zbiorów są następujące:

 funkcji  (t  1  , t  2  )  jeśli  t  1  = zero  lub  t  2  = zero  zwróć  zero  w przeciwnym razie  (l  1  , k  1  , r  1  ) := ujawnij (t  1  ) (t  <  , b, t  >  ) = podziel (t  2  , k  1  ) l' := punkt przecięcia (l  1  , t  <  ) || r' := przecięcie(r  
            
     1  , t  >  )  jeśli  b  zwróć  sprzężenie(l', k  1  , r')  w przeciwnym razie  zwróć  sprzężenie2(l', r')  funkcja  różnica(t  1  , t  2  )  jeśli  t  1  = zero  zwróć  zero  w przeciwnym razie jeśli  t  2  = nil  return  t  1  else  (l  1  , k  1  , r 
             1  ) := wyeksponuj(t  1  ) (t  <  , b, t  >  ) := podziel(t  2  , k  1  ) l' = różnica (l  1  , t  <  ) || r' = różnica(r   1  , t  >  )  jeśli  b  return  join2(l', r')  w przeciwnym razie  return  join(l', k  1  , r') 

Złożoność każdego związku, przecięcia i różnicy wynosi dwóch zrównoważonych wagowo drzew o rozmiarach n . Ta złożoność jest optymalna pod względem liczby porównań. Co ważniejsze, ponieważ rekurencyjne wywołania sumy, przecięcia lub różnicy są od siebie niezależne, można je wykonywać równolegle z równoległą głębokością . m , implementacja oparta na łączeniu stosuje te same obliczenia, co w przypadku wstawiania lub usuwania pojedynczego elementu, jeśli korzeń większego drzewa jest używany do podziału mniejszego drzewa.

Zbudować

Algorytm budowania drzewa może wykorzystywać algorytm łączenia i schemat dziel i zwyciężaj:

 funkcja  build(A[], n)  if  n = 0  return  nil  else if  n = 1  return  Node(nil, A[0], nil)  else  l' := build(A, n/2) || r' := (A+n/2, nn/2)   powrót  suma(L, R) 

kosztuje pracę i ma głębokość Bardziej wydajny algorytm wykorzystuje algorytm sortowania równoległego.

 funkcja  buildSorted(A[], n)  if  n = 0  return  nil  else if  n = 1  return  Node(nil, A[0], nil)  else  l' := build(A, n/2) || r' := (A+n/2+1, nn/2-1)   return  join(l', A[n/2], r') budowanie   funkcji  (A[], n) A' := sort( A, n)  return  buildSorted(A, n) 

Ten ma _ praca i głębokość.

Filtr

wybiera wszystkie wpisy w drzewie spełniające predykat zwraca drzewo zawierające wszystkie wybrane wpisy. Rekurencyjnie filtruje dwa poddrzewa i łączy je z korzeniem, jeśli korzeń spełnia przeciwnym razie łączy dwa poddrzewa.

             funkcja  filter(T, p)  if  T = nil  return  nil  else  (l, k, r) := wyeksponuj(T) l' := filter(l, p) || r' := filter(r, p)   if  p(k)  return  join(l', k, r')  w przeciwnym razie  return  join2(l', R) 

Ten algorytm kosztuje pracę i głębokość o rozmiarze , zakładając, stały koszt.

Stosowany w bibliotekach

Algorytmy oparte na łączeniach są stosowane do obsługi interfejsu dla zbiorów , map i rozszerzonych map w bibliotekach takich jak Hackage , SML/NJ i PAM .

Notatki

Linki zewnętrzne

  • PAM , równoległa rozszerzona biblioteka map
  • Hackage , Kontenery w Hackage