Articulo de referencia

Algoritmos de árbol basados ​​en uniones

En ciencias de la computación , los algoritmos de árbol basados ​​en unión son una clase de algoritmos para árboles de búsqueda binaria autoequilibrados . Este marco tiene como ...

En ciencias de la computación , los algoritmos de árbol basados ​​en unión son una clase de algoritmos para árboles de búsqueda binaria autoequilibrados . Este marco tiene como objetivo diseñar algoritmos altamente paralelizados para varios árboles de búsqueda binaria equilibrados. El marco algorítmico se basa en una única operación de unión . [ 1 ] Bajo este marco, la operación de unión captura todos los criterios de equilibrio de diferentes esquemas de equilibrio, y todas las demás funciones de unión tienen una implementación genérica en diferentes esquemas de equilibrio. Los algoritmos basados ​​en unión se pueden aplicar a al menos cuatro esquemas de equilibrio: árboles AVL , árboles rojo-negro , árboles equilibrados por peso y treaps .

La unión(L,k,R){\displaystyle (L,k,R)}La operación toma como entrada dos árboles binarios equilibrados.L{\displaystyle L}yR{\displaystyle R}del mismo esquema de equilibrio y una clavek{\displaystyle k}y genera un nuevo árbol binario equilibrado.t{\displaystyle t}cuyo recorrido en orden es el recorrido en orden deL{\displaystyle L}, entoncesk{\displaystyle k}luego el recorrido en orden deR{\displaystyle R}. En particular, si los árboles son árboles de búsqueda , lo que significa que el orden de los árboles mantiene un orden total en las claves, debe satisfacer la condición de que todas las claves enL{\displaystyle L}son más pequeños quek{\displaystyle k}y todas las llaves enR{\displaystyle R}son mayores quek{\displaystyle k}.

Historia

La operación de unión fue definida por primera vez por Tarjan [ 2 ] en árboles rojo-negro , que se ejecuta en tiempo logarítmico en el peor de los casos. Posteriormente, Sleator y Tarjan [ 3 ] describieron un algoritmo de unión para árboles splay que se ejecuta en tiempo logarítmico amortizado. Más tarde, Adams [ 4 ] extendió la operación de unión a árboles con peso equilibrado y la utilizó para funciones rápidas de conjuntos, incluyendo unión , intersección y diferencia de conjuntos . En 1998, Blelloch y Reid-Miller extendieron la operación de unión a treaps y demostraron que la cota de las funciones de conjuntos esO(metroregistro(1+nortemetro)){\displaystyle O(m\log(1+{\tfrac {n}{m}}))}para dos árboles de tamañometro{\displaystyle m}ynorte(metro){\displaystyle n(\geq m)}, que es óptimo en el modelo de comparación. También mencionaron el paralelismo en el algoritmo de Adams mediante un esquema de divide y vencerás . En 2016, Blelloch et al. propusieron formalmente los algoritmos basados ​​en la unión y formalizaron el algoritmo de unión para cuatro esquemas de balance diferentes: árboles AVL , árboles rojo-negro , árboles con balance de peso y treaps . En el mismo trabajo demostraron que los algoritmos de Adams sobre unión, intersección y diferencia son óptimos en términos de trabajo en los cuatro esquemas de balance.

Unirse a los algoritmos

La función unir(t1,k,t2){\displaystyle (t_{1},k,t_{2})}Considera reequilibrar el árbol y, por lo tanto, depende del esquema de equilibrio de entrada. Si los dos árboles están equilibrados, join simplemente crea un nuevo nodo con subárbol izquierdo t 1 , raíz k y subárbol derecho t 2 . Supongamos que t 1 es más pesado (este "más pesado" depende del esquema de equilibrio) que t 2 (el otro caso es simétrico). Join sigue la columna vertebral derecha de t 1 hasta un nodo c que está equilibrado con t 2 . En este punto, se crea un nuevo nodo con hijo izquierdo c , raíz k e hijo derecho t 2 para reemplazar a c. El nuevo nodo puede invalidar el invariante de equilibrio. Esto se puede corregir con rotaciones.

A continuación se muestran los algoritmos de unión en diferentes esquemas de balanceo.

El algoritmo de unión para árboles AVL :

función joinRightAVL(T L , k, T R ) (l, k', c) := exponer(T L ) si h(c) ≤ h(T R ) + 1 T' := Nodo(c, k, T R ) si h(T') ≤ h(l) + 1 devolver Nodo(l, k', T') si no devolver rotarIzquierda(Nodo(l, k', rotarDerecha(T'))) si no T' := unirDerechaAVL(c, k, T R ) T : = Nodo(l, k', T') si h(T') ≤ h(l) + 1 devolver T sino devolver rotateLeft(T) función joinLeftAVL(T L , k, T R ) /* simétrico a joinRightAVL */ función join(T L , k, T R ) si h(T L ) > h(T R ) + 1 devolver joinRightAVL(T L , k, T R ) sino si h(T R ) > h(T L ) + 1 devolver joinLeftAVL(T L , k, T R ) sino devolver Node(T L , k, T R )

Dónde:

  • h(v){\displaystyle h(v)}es la altura del nodov{\displaystyle v}.
  • exponer(v){\displaystyle {\text{exponer}}(v)}extrae al niño izquierdol{\displaystyle l}, llavek{\displaystyle k}y el niño derechor{\displaystyle r}del nodov{\displaystyle v}en una tupla(l,k,r){\displaystyle (l,k,r)}.
  • Nodo(l,k,r){\displaystyle {\text{Nodo}}(l,k,r)}crea un nodo con hijo izquierdol{\displaystyle l}, llavek{\displaystyle k}y el niño derechor{\displaystyle r}.

El algoritmo de unión para árboles rojo-negro :

función joinRightRB(T L , k, T R ) si T L .color = negro y ĥ(T L ) = ĥ(T R ) devolver Nodo(T L , ⟨k, rojo⟩, T R ) de lo contrario (L', ⟨k', c'⟩, R') := exponer(T L ) T' := Nodo(L', ⟨k', c'⟩, unirDerechaRB(R', k, T R )) si c' = negro y T'.right.color = T'.right.right.color = rojo T'.right.right.color := negro Devuelve rotateLeft(T'); de lo contrario, devuelve T'. función unirLeftRB(T L , k, T R ) /* simétrico a joinRightRB */ función unir(T L , k, T R ) si ĥ(T L ) > ĥ(T R ) T' := joinRightRB(T L , k, T R ) si (T'.color = rojo) y (T'.right.color = rojo) T'.color := negro devolver T' si no, si ĥ(T R ) > ĥ(T L ) /* simétrico */ else if T L .color = black and T R = black return Node(T L , ⟨k, red⟩, T R ) else return Node(T L , ⟨k, black⟩, T R )

Dónde:

  • h^(v){\displaystyle {\hat {h}}(v)} es la altura negra del nodov{\displaystyle v}.
  • exponer(v){\displaystyle {\text{exponer}}(v)}extrae al niño izquierdol{\displaystyle l}, llavek{\displaystyle k}, colordo{\displaystyle c}y el niño derechor{\displaystyle r}del nodov{\displaystyle v}en una tupla(l,k,do,r){\displaystyle (l,\langle k,c\rangle ,r)}.
  • Nodo(l,k,do,r){\displaystyle {\text{Nodo}}(l,\langle k,c\rangle ,r)}crea un nodo con hijo izquierdol{\displaystyle l}, llavek{\displaystyle k}, colordo{\displaystyle c}y el niño derechor{\displaystyle r}.

El algoritmo de unión para árboles con ponderación equilibrada :

función joinRightWB(T L , k, T R ) (l, k', c) := exponer(T L ) si w(T L ) = α w(T R ) devolver Nodo(T L , k, T R ) de lo contrario T' := unirDerechaWB(c, k, T R ) (l 1 , k 1 , r 1 ) := expose(T') si w(l) = α w(T') devolver Nodo(l, k', T') si no, si w(l) = α w(l 1 ) y w(l)+w(l 1 ) = α w(r 1 ) devolver rotateLeft(Nodo(l, k', T')) si no, devolver rotateLeft(Nodo(l, k', rotateRight(T')) función unirIzquierdaWB(T L , k, T R ) /* simétrico a joinRightWB */ función join(T L , k, T R ) si w(T L ) > α w(T R ) devolver joinRightWB(T L , k, T R ) sino si w(T R ) > α w(T L ) devolver joinLeftWB(T L , k, T R ) sino devolver Node(T L , k, T R )

Dónde:

  • w(v){\displaystyle w(v)}es el peso del nodov{\displaystyle v}.
  • w1=αw2{\displaystyle w_{1}=_{\alpha }w_{2}}significa pesosw1{\displaystyle w_{1}}yw2{\displaystyle w_{2}}están equilibrados en peso α.
  • w1>αw2{\displaystyle w_{1}>_{\alpha }w_{2}}significa pesow1{\displaystyle w_{1}}es más pesado que pesow2{\displaystyle w_{2}}con respecto al equilibrio de peso α.
  • exponer(v){\displaystyle {\text{exponer}}(v)}extrae al niño izquierdol{\displaystyle l}, llavek{\displaystyle k}y el niño derechor{\displaystyle r}del nodov{\displaystyle v}en una tupla(l,k,r){\displaystyle (l,k,r)}.
  • Nodo(l,k,r){\displaystyle {\text{Nodo}}(l,k,r)}crea un nodo con hijo izquierdol{\displaystyle l}, llavek{\displaystyle k}y el niño derechor{\displaystyle r}.

Algoritmos basados ​​en uniones

A continuación,exponer(v){\displaystyle {\text{exponer}}(v)}extrae al niño izquierdol{\displaystyle l}, llavek{\displaystyle k}y el niño derechor{\displaystyle r}del nodov{\displaystyle v}en una tupla(l,k,r){\displaystyle (l,k,r)}.Nodo(l,k,r){\displaystyle {\text{Nodo}}(l,k,r)}crea un nodo con hijo izquierdol{\displaystyle l}, llavek{\displaystyle k}y el niño derechor{\displaystyle r}. "s1||s2{\displaystyle s_{1}||s_{2}}" significa que dos afirmacioness1{\displaystyle s_{1}}ys2{\displaystyle s_{2}}pueden ejecutarse en paralelo.

Dividir

Para dividir un árbol en dos árboles, aquellos menores que la clave x y aquellos mayores que la clave x , primero trazamos un camino desde la raíz insertando x en el árbol. Después de esta inserción, todos los valores menores que x se encontrarán a la izquierda del camino y todos los valores mayores que x se encontrarán a la derecha. Al aplicar Join , todos los subárboles del lado izquierdo se fusionan de abajo hacia arriba utilizando las claves del camino como nodos intermedios de abajo hacia arriba para formar el árbol izquierdo, y la parte derecha es asimétrica. Para algunas aplicaciones, Split también devuelve un valor booleano que indica si x aparece en el árbol. El costo de Split esO(registronorte){\displaystyle O(\log n)}, en orden de la altura del árbol.

El algoritmo de división es el siguiente:

función split(T, k) si (T = nil) devolver (nil, falso, nil) de lo contrario (L, m, R) := exponer(T) si k < m (L', b, R') := split(L, k) devolver (L', b, unir(R', m, R)) de lo contrario si k > m (L', b, R') := split(R, k) Devuelve (join(L, m, L'), b, R')) de lo contrario, devuelve (L, verdadero, R)

Unirse2

Esta función se define de forma similar a join , pero sin la clave intermedia. Primero separa la última clave.k{\displaystyle k}del árbol izquierdo, y luego unir la parte restante del árbol izquierdo con el árbol derecho conk{\displaystyle k}El algoritmo es el siguiente:

función splitLast(T) (L, k, R) := exponer(T) si R = nil devolver (L, k) de lo contrario (T', k') := splitLast(R) devolver (unir(L, k, T'), k') función join2(L, R) si L = nil devolver R sino (L', k) := splitLast(L) devolver unir(L', k, R)

El costo esO(registronorte){\displaystyle O(\log n)}para un árbol de tamañonorte{\displaystyle n}.

Insertar y eliminar

Los algoritmos de inserción y eliminación, al utilizar la operación join, pueden ser independientes de los esquemas de balanceo. Para una inserción, el algoritmo compara la clave que se va a insertar con la clave en la raíz, la inserta en el subárbol izquierdo/derecho si la clave es menor/mayor que la clave en la raíz, y une los dos subárboles con la raíz. Para una eliminación, compara la clave que se va a eliminar con la clave en la raíz. Si son iguales, devuelve join2 en los dos subárboles. De lo contrario, elimina la clave del subárbol correspondiente y une los dos subárboles con la raíz. Los algoritmos son los siguientes:

función insertar(T, k) si T = nil devolver Nodo(nil, k, nil) de lo contrario (L, k', R) := exponer(T) Si k < k', devuelve join(insertar(L,k), k', R) ; de lo contrario, si k > k', devuelve join(L, k', insertar(R, k)) ; de lo contrario , devuelve T. función delete(T, k) si T = nil devolver nil sino (L, k', R) := exponer(T) Si k < k', devuelve join(delete(L, k), k', R) ; de lo contrario, si k > k', devuelve join(L, k', delete(R, k)); de lo contrario, devuelve join2(L, R).

Tanto la inserción como la eliminación requierenO(registronorte){\displaystyle O(\log n)}tiempo si|T|=norte{\displaystyle |T|=n}.

Funciones de conjunto-conjunto

Se han definido varias operaciones de conjuntos en árboles ponderados: unión , intersección y diferencia de conjuntos . La unión de dos árboles ponderados t 1 y t 2 que representan los conjuntos A y B es un árbol t que representa AB. La siguiente función recursiva calcula esta unión:

función unión(t 1 , t 2 ) si t 1 = nil devolver t 2 sino si t 2 = nil devolver t 1 sino (l 1 , k 1 , r 1 ) := exponer(t 1 ) (t < , b, t > ) := split(t 2 , k 1 ) l' := unión(l 1 , t < ) || r' := unión(r 1 , t > ) return unión(l', k 1 , r')

De manera similar, los algoritmos de intersección y diferencia de conjuntos son los siguientes:

función intersección(t 1 , t 2 ) si t 1 = nil o t 2 = nil devolver nil sino (l 1 , k 1 , r 1 ) := exponer(t 1 ) (t < , b, t > ) = split(t 2 , k 1 ) l' := intersección(l 1 , t < ) || r' := intersección(r 1 , t > ) si b devuelve unir(l', k 1 , r') sino devuelve unir2(l', r') función diferencia(t 1 , t 2 ) si t 1 = nil devolver nil sino si t 2 = nil devolver t 1 sino (l 1 , k 1 , r 1 ) := exponer(t 1 ) (t < , b, t > ) := split(t 2 , k 1 ) l' = diferencia(l 1 , t < ) || r' = diferencia(r 1 , t > ) si b devuelve join2(l', r') sino devuelve join(l', k 1 , r')

La complejidad de cada uno de la unión, la intersección y la diferencia esO(metroregistro(nortemetro+1)){\displaystyle O\left(m\log \left({\tfrac {n}{m}}+1\right)\right)}para dos árboles equilibrados por peso de tamañosmetro{\displaystyle m}ynorte(metro){\displaystyle n(\geq m)}Esta complejidad es óptima en términos del número de comparaciones. Más importante aún, dado que las llamadas recursivas a unión, intersección o diferencia son independientes entre sí, pueden ejecutarse en paralelo con una profundidad paralela.O(registrometroregistronorte){\displaystyle O(\log m\log n)}. [ 1 ] Cuandometro=1{\displaystyle m=1}La implementación basada en uniones aplica el mismo cálculo que en una inserción o eliminación de un solo elemento si se utiliza la raíz del árbol más grande para dividir el árbol más pequeño.

Construir

El algoritmo para construir un árbol puede utilizar el algoritmo de unión y el esquema de divide y vencerás:

función construir(A[], n) si n = 0 devolver nil sino si n = 1 devolver Nodo(nil, A[0], nil) sino l' := construir(A, n/2) || r' := (A+n/2, nn/2) devolver unión(L, R)

Este algoritmo cuestaO(norteregistronorte){\displaystyle O(n\log n)}trabajo y tieneO(registro3norte){\displaystyle O(\log ^{3}n)}profundidad. Un algoritmo más eficiente utiliza un algoritmo de ordenación paralela.

función buildSorted(A[], n) si n = 0 devuelve nil sino si n = 1 devuelve Node(nil, A[0], nil) sino l' := construir(A, n/2) || r' := (A+n/2+1, nn/2-1) devolver unir(l', A[n/2], r') función construir(A[], n) A' := sort(A, n) devolver buildSorted(A, n)

Este algoritmo cuestaO(norteregistronorte){\displaystyle O(n\log n)}trabajo y tieneO(registronorte){\displaystyle O(\log n)}profundidad suponiendo que el algoritmo de ordenación tieneO(norteregistronorte){\displaystyle O(n\log n)}trabajo yO(registronorte){\displaystyle O(\log n)}profundidad.

Filtrar

Esta función selecciona todas las entradas de un árbol que satisfacen un predicado.pag{\displaystyle p}y devuelve un árbol que contiene todas las entradas seleccionadas. Filtra recursivamente los dos subárboles y los une con la raíz si la raíz satisfacepag{\displaystyle p}, de lo contrario, unir los dos subárboles.

función filter(T, p) si T = nil devolver nil sino (l, k, r) := exponer(T) l' := filter(l, p) || r' := filter(r, p) si p(k) devuelve join(l', k, r') de lo contrario devuelve join2(l', R)

Este algoritmo cuesta trabajo.O(norte){\displaystyle O(n)}y profundidadO(registro2norte){\displaystyle O(\log ^{2}n)}en un árbol de tamañonorte{\displaystyle n}, suponiendopag{\displaystyle p}tiene un costo constante.

Se utiliza en bibliotecas

Los algoritmos basados ​​en uniones se aplican para admitir interfaces para conjuntos , mapas y mapas aumentados [ 5 ] en bibliotecas como Hackage , SML/NJ y PAM . [ 5 ]

Notas

Referencias

  1. 1 2 Blelloch, Guy E.; Ferizovic, Daniel; Sun, Yihan (2016), "Just Join for Parallel Ordered Sets", Simposio sobre algoritmos y arquitecturas paralelas, Actas del 28.º Simposio ACM sobre algoritmos y arquitecturas paralelas (SPAA 2016) , ACM, págs. 253–264 , arXiv : 1602.02120 , doi : 10.1145/2935764.2935768 , ISBN  978-1-4503-4210-0
  2. Tarjan, Robert Endre (1983), "Estructuras de datos y algoritmos de red", Estructuras de datos y algoritmos de red , Siam, págs . 45–56 
  3. Sleator, Daniel Dominic; Tarjan, Robert Endre (1985), "Árboles de búsqueda binaria autoajustables", Journal of the ACM , Siam
  4. Adams, Stephen (1992), "Implementación eficiente de conjuntos en un lenguaje funcional", Implementación eficiente de conjuntos en un lenguaje funcional , Citeseer, CiteSeerX 10.1.1.501.8427 .
  5. 1 2 Blelloch, Guy E.; Ferizovic, Daniel; Sun, Yihan ( 2018), "PAM: mapas aumentados paralelos", Actas del 23.er Simposio ACM SIGPLAN sobre Principios y Práctica de la Programación Paralela , ACM, págs. 290–304 
  • PAM , la biblioteca de mapas aumentados paralelos
  • Hackage , contenedores en Hackage