En informática , una cola de prioridad es un tipo de datos abstracto similar a una cola normal o un tipo de datos abstracto de pila . Cada elemento de una cola de prioridad tiene una prioridad asociada. En una cola de prioridad, los elementos con alta prioridad se sirven antes que los elementos con baja prioridad. En algunas implementaciones, si dos elementos tienen la misma prioridad, se sirven en el mismo orden en el que se pusieron en cola. En otras implementaciones, el orden de los elementos con la misma prioridad no está definido.
Si bien las colas de prioridad suelen implementarse mediante heaps , conceptualmente son distintas de los heaps. Una cola de prioridad es un tipo de datos abstracto como una lista o un mapa ; así como una lista se puede implementar con una lista enlazada o con una matriz , una cola de prioridad se puede implementar con un heap u otro método como una matriz ordenada.
Una cola de prioridad debe admitir al menos las siguientes operaciones:
Además, la operación peek (en este contexto, a menudo denominada find-max o find-min ), que devuelve el elemento de mayor prioridad pero no modifica la cola, se implementa con mucha frecuencia y casi siempre se ejecuta en tiempo O (1) . Esta operación y su rendimiento O (1) son cruciales para muchas aplicaciones de colas de prioridad.
Las implementaciones más avanzadas pueden soportar operaciones más complicadas, como pull_lowest_priority_element , inspeccionar los primeros elementos de mayor o menor prioridad, limpiar la cola, limpiar subconjuntos de la cola, realizar una inserción por lotes, fusionar dos o más colas en una, incrementar la prioridad de cualquier elemento, etc.
Las pilas y las colas se pueden implementar como tipos particulares de colas de prioridad, en las que la prioridad se determina por el orden en el que se insertan los elementos. En una pila, la prioridad de cada elemento insertado aumenta de forma monótona; por lo tanto, el último elemento insertado es siempre el primero en recuperarse. En una cola, la prioridad de cada elemento insertado disminuye de forma monótona; por lo tanto, el primer elemento insertado es siempre el primero en recuperarse.
Existen diversas formas sencillas, generalmente ineficientes, de implementar una cola de prioridad. Estas proporcionan una analogía que ayuda a comprender qué es una cola de prioridad.
Por ejemplo, se pueden mantener todos los elementos en una lista desordenada ( tiempo de inserción O (1)). Siempre que se solicite el elemento de mayor prioridad, se busca entre todos los elementos el que tenga mayor prioridad. ( tiempo de extracción O ( n )).
insertar (nodo) { lista.append(nodo)}
jalar () { más alto = lista.obtener_primer_elemento() foreach nodo en lista { si mayor prioridad < nodo.prioridad { más alto = nodo } } lista.eliminar(más alta) volver más alto}
En otro caso, se pueden mantener todos los elementos en una lista ordenada por prioridad ( tiempo de ordenación por inserción O (n)), siempre que se solicite el elemento de mayor prioridad, se puede devolver el primero de la lista. ( Tiempo de extracción O (1))
insertar (nodo) { foreach (índice, elemento) en lista { si nodo.prioridad < elemento.prioridad { lista.insert_at_index(nodo,índice) romper } }}
jalar () { más alto = lista.obtener_en_índice(lista.longitud-1) lista.eliminar(más alta) volver más alto}
Para mejorar el rendimiento, las colas de prioridad se basan normalmente en un montón , lo que proporciona un rendimiento de O (log n ) para inserciones y eliminaciones, y O ( n ) para construir el montón inicialmente a partir de un conjunto de n elementos. Las variantes de la estructura de datos del montón básico, como los montones de emparejamiento o los montones de Fibonacci, pueden proporcionar mejores límites para algunas operaciones. [1]
Alternativamente, cuando se utiliza un árbol binario de búsqueda autoequilibrado , la inserción y la eliminación también toman tiempo O (log n ), aunque la construcción de árboles a partir de secuencias existentes de elementos toma tiempo O ( n log n ); esto es típico donde uno ya podría tener acceso a estas estructuras de datos, como con bibliotecas de terceros o estándar. Desde un punto de vista de complejidad espacial, el uso de un árbol binario de búsqueda autoequilibrado con lista enlazada requiere más almacenamiento, ya que requiere almacenar referencias adicionales a otros nodos.
Desde el punto de vista de la complejidad computacional, las colas de prioridad son congruentes con los algoritmos de ordenamiento. La sección sobre la equivalencia de las colas de prioridad y los algoritmos de ordenamiento, que aparece a continuación, describe cómo los algoritmos de ordenamiento eficientes pueden crear colas de prioridad eficientes.
Existen varias estructuras de datos de montón especializadas que proporcionan operaciones adicionales o superan a las implementaciones basadas en montón para tipos específicos de claves, específicamente claves enteras. Supongamos que el conjunto de claves posibles es {1, 2, ..., C}.
Para las aplicaciones que realizan muchas operaciones de " vista previa " por cada operación de "extracción mínima", la complejidad temporal de las acciones de vista previa se puede reducir a O (1) en todas las implementaciones de árboles y montones almacenando en caché el elemento de mayor prioridad después de cada inserción y eliminación. Para la inserción, esto agrega como máximo un costo constante, ya que el elemento recién insertado se compara solo con el elemento mínimo almacenado previamente en caché. Para la eliminación, esto agrega como máximo un costo de "vista previa" adicional, que generalmente es más económico que el costo de eliminación, por lo que la complejidad temporal general no se ve afectada significativamente.
Las colas de prioridad monótona son colas especializadas que están optimizadas para el caso en el que nunca se inserta ningún elemento que tenga una prioridad menor (en el caso de min-heap) que cualquier elemento extraído previamente. Esta restricción se cumple en varias aplicaciones prácticas de las colas de prioridad.
Aquí se muestran las complejidades temporales [5] de varias estructuras de datos de montón. La abreviatura am. indica que la complejidad dada se amortiza, de lo contrario es una complejidad del peor caso. Para el significado de " O ( f )" y " Θ ( f )", consulte la notación Big O. Los nombres de las operaciones suponen un montón mínimo.
Operación | encontrar-min | eliminar-min | tecla de disminución | insertar | fusionarse | crear montón [a] |
---|---|---|---|---|---|---|
Binario [5] | Θ (1) | Θ (log n ) | Θ (log n ) | Θ (log n ) | Θ ( n ) | Θ ( n ) |
Inclinación [6] | Θ (1) | O (log n ) soy. | O (log n ) soy. | O (log n ) soy. | O (log n ) soy. | Θ ( n ) soy. |
Izquierdista [7] | Θ (1) | Θ (log n ) | Θ (log n ) | Θ (log n ) | Θ (log n ) | Θ ( n ) |
Binomio [5] [9] | Θ (1) | Θ (log n ) | Θ (log n ) | Yo (1) soy. | Θ (log n ) [b] | Θ ( n ) |
Binomio sesgado [10] | Θ (1) | Θ (log n ) | Θ (log n ) | Θ (1) | Θ (log n ) [b] | Θ ( n ) |
Montón de 2 a 3 [12] | Θ (1) | O (log n ) soy. | Θ (1) | Yo (1) soy. | O (log n ) [b] | Θ ( n ) |
Sesgo de abajo hacia arriba [6] | Θ (1) | O (log n ) soy. | O (log n ) soy. | Yo (1) soy. | Yo (1) soy. | Θ ( n ) soy. |
Emparejamiento [13] | Θ (1) | O (log n ) soy. | o (log n ) soy. [c] | Θ (1) | Θ (1) | Θ ( n ) |
Emparejamiento de rangos [16] | Θ (1) | O (log n ) soy. | Yo (1) soy. | Θ (1) | Θ (1) | Θ ( n ) |
Fibonacci [5] [17] | Θ (1) | O (log n ) soy. | Yo (1) soy. | Θ (1) | Θ (1) | Θ ( n ) |
Fibonacci estricto [18] [d] | Θ (1) | Θ (log n ) | Θ (1) | Θ (1) | Θ (1) | Θ ( n ) |
Brodal [19] [d] | Θ (1) | Θ (log n ) | Θ (1) | Θ (1) | Θ (1) | Θ ( n ) [20] |
La semántica de las colas de prioridad sugiere naturalmente un método de ordenación: introducir todos los elementos que se van a ordenar en una cola de prioridad y eliminarlos secuencialmente; saldrán en orden ordenado. Este es en realidad el procedimiento que utilizan varios algoritmos de ordenación , una vez que se elimina la capa de abstracción proporcionada por la cola de prioridad. Este método de ordenación es equivalente a los siguientes algoritmos de ordenación:
Nombre | Implementación de cola de prioridad | Mejor | Promedio | El peor |
---|---|---|---|---|
Ordenamiento por montículos | Montón | |||
Ordenación suave | Montón de Leonardo | |||
Ordenación por selección | Matriz desordenada | |||
Ordenación por inserción | Matriz ordenada | |||
Ordenación de árboles | Árbol de búsqueda binario autoequilibrado |
También se puede utilizar un algoritmo de ordenación para implementar una cola de prioridad. En concreto, Thorup afirma: [21]
Presentamos una reducción espacial lineal determinista general desde colas de prioridad hasta ordenación, lo que implica que si podemos ordenar hasta n claves en tiempo S ( n ) por clave, entonces hay una cola de prioridad que admite eliminación e inserción en tiempo O ( S ( n )) y búsqueda mínima en tiempo constante.
Es decir, si hay un algoritmo de ordenamiento que puede ordenar en O ( S ) tiempo por clave, donde S es alguna función de n y tamaño de palabra , [22] entonces uno puede usar el procedimiento dado para crear una cola de prioridad donde extraer el elemento de mayor prioridad es O (1) tiempo, e insertar nuevos elementos (y eliminar elementos) es O ( S ) tiempo. Por ejemplo, si uno tiene un algoritmo de ordenamiento O ( n log n ), uno puede crear una cola de prioridad con O (1) extracción e O ( log n ) inserción.
Una cola de prioridad a menudo se considera una " estructura de datos contenedora ".
La biblioteca de plantillas estándar (STL) y el estándar C++ 1998 especifica std::priority_queue como una de las plantillas de clase de adaptador de contenedor STL . Sin embargo, no especifica cómo se deben servir dos elementos con la misma prioridad y, de hecho, las implementaciones comunes no los devolverán según su orden en la cola. Implementa una cola de máxima prioridad y tiene tres parámetros: un objeto de comparación para ordenar, como un objeto de función (el valor predeterminado es less<T> si no se especifica), el contenedor subyacente para almacenar las estructuras de datos (el valor predeterminado es std::vector<T>) y dos iteradores para el comienzo y el final de una secuencia. A diferencia de los contenedores STL reales, no permite la iteración de sus elementos (se adhiere estrictamente a su definición de tipo de datos abstractos). STL también tiene funciones de utilidad para manipular otro contenedor de acceso aleatorio como un montón máximo binario. Las bibliotecas Boost también tienen una implementación en el montón de bibliotecas.
El módulo heapq de Python implementa un min-heap binario en la parte superior de una lista.
La biblioteca de JavaPriorityQueue
contiene una clase que implementa una cola de prioridad mínima como un montón binario.
La biblioteca de .NET contiene una clase PriorityQueue, que implementa un min-heap cuaternario respaldado por una matriz.
La biblioteca de Scala contiene una clase PriorityQueue, que implementa una cola de máxima prioridad.
La biblioteca de Go contiene un módulo contenedor/montón, que implementa un min-montón sobre cualquier estructura de datos compatible.
La extensión de la biblioteca PHP estándar contiene la clase SplPriorityQueue.
El marco Core Foundation de Apple contiene una estructura CFBinaryHeap, que implementa un min-heap.
La cola de prioridad se puede utilizar para gestionar recursos limitados, como el ancho de banda en una línea de transmisión desde un enrutador de red . En caso de que el tráfico saliente se ponga en cola debido a un ancho de banda insuficiente, todas las demás colas se pueden detener para enviar el tráfico desde la cola de mayor prioridad a su llegada. Esto garantiza que el tráfico priorizado (como el tráfico en tiempo real, por ejemplo, un flujo RTP de una conexión VoIP ) se reenvíe con el menor retraso y la menor probabilidad de ser rechazado debido a que una cola alcanza su capacidad máxima. Todo el resto del tráfico se puede gestionar cuando la cola de mayor prioridad está vacía. Otro enfoque utilizado es enviar desproporcionadamente más tráfico desde colas de mayor prioridad.
Muchos protocolos modernos para redes de área local también incluyen el concepto de colas de prioridad en la subcapa de control de acceso al medio (MAC) para garantizar que las aplicaciones de alta prioridad (como VoIP o IPTV ) experimenten una latencia menor que otras aplicaciones que pueden recibir un servicio de máximo esfuerzo . Algunos ejemplos incluyen IEEE 802.11e (una modificación de IEEE 802.11 que proporciona calidad de servicio ) e ITU-T G.hn (una norma para redes de área local de alta velocidad que utilizan el cableado doméstico existente ( líneas eléctricas , líneas telefónicas y cables coaxiales ).
Generalmente se establece una limitación (policía) para limitar el ancho de banda que puede utilizar el tráfico de la cola de mayor prioridad, con el fin de evitar que los paquetes de alta prioridad obstruyan el resto del tráfico. Este límite normalmente nunca se alcanza debido a instancias de control de alto nivel como Cisco Callmanager, que se pueden programar para inhibir las llamadas que excedan el límite de ancho de banda programado.
Otro uso de una cola de prioridad es gestionar los eventos en una simulación de eventos discretos . Los eventos se añaden a la cola y su tiempo de simulación se utiliza como prioridad. La ejecución de la simulación se lleva a cabo extrayendo repetidamente la parte superior de la cola y ejecutando el evento que se encuentra allí.
Véase también : Programación (informática) , teoría de colas
Cuando el gráfico se almacena en forma de lista o matriz de adyacencia, se puede utilizar una cola de prioridad para extraer el mínimo de manera eficiente al implementar el algoritmo de Dijkstra , aunque también se necesita la capacidad de alterar la prioridad de un vértice particular en la cola de prioridad de manera eficiente.
Si, en cambio, un gráfico se almacena como objetos de nodo y se insertan pares de nodos prioritarios en un montón, no es necesario modificar la prioridad de un vértice en particular si se hace un seguimiento de los nodos visitados. Una vez que se visita un nodo, si vuelve a aparecer en el montón (habiendo tenido un número de prioridad más bajo asociado con él anteriormente), se lo elimina y se lo ignora.
La codificación de Huffman requiere que se obtengan repetidamente los dos árboles de frecuencia más baja. Una cola de prioridad es un método para lograrlo .
Los algoritmos de búsqueda de mejor primero , como el algoritmo de búsqueda A* , encuentran la ruta más corta entre dos vértices o nodos de un gráfico ponderado , probando primero las rutas más prometedoras. Se utiliza una cola de prioridad (también conocida como franja ) para realizar un seguimiento de las rutas inexploradas; aquella para la que la estimación (un límite inferior en el caso de A*) de la longitud total de la ruta es más pequeña recibe la mayor prioridad. Si las limitaciones de memoria hacen que la búsqueda de mejor primero sea poco práctica, se pueden utilizar en su lugar variantes como el algoritmo SMA* , con una cola de prioridad de doble extremo para permitir la eliminación de elementos de baja prioridad.
El algoritmo ROAM (Real-time Optimally Adapting Meshes ) calcula una triangulación de un terreno que cambia dinámicamente. Funciona dividiendo los triángulos donde se necesitan más detalles y fusionándolos donde se necesitan menos detalles. El algoritmo asigna a cada triángulo del terreno una prioridad, generalmente relacionada con la disminución del error si ese triángulo se dividiera. El algoritmo utiliza dos colas de prioridad, una para los triángulos que se pueden dividir y otra para los triángulos que se pueden fusionar. En cada paso, se divide el triángulo de la cola de división con la prioridad más alta, o se fusiona el triángulo de la cola de fusión con la prioridad más baja con sus vecinos.
Al utilizar la cola de prioridad de montón mínimo en el algoritmo de Prim para encontrar el árbol de expansión mínimo de un gráfico conectado y no dirigido , se puede lograr un buen tiempo de ejecución. Esta cola de prioridad de montón mínimo utiliza la estructura de datos de montón mínimo que admite operaciones como insertar , mínimo , extraer-mínimo , disminuir-clave . [23] En esta implementación, el peso de los bordes se utiliza para decidir la prioridad de los vértices . Cuanto menor sea el peso, mayor será la prioridad y cuanto mayor sea el peso, menor será la prioridad. [24]
La paralelización se puede utilizar para acelerar las colas de prioridad, pero requiere algunos cambios en la interfaz de la cola de prioridad. La razón de estos cambios es que una actualización secuencial normalmente solo tiene un costo y no hay ninguna ganancia práctica en paralelizar una operación de este tipo. Un cambio posible es permitir el acceso simultáneo de varios procesadores a la misma cola de prioridad. El segundo cambio posible es permitir operaciones por lotes que funcionen sobre elementos, en lugar de solo sobre un elemento. Por ejemplo, extractMin eliminará los primeros elementos con la prioridad más alta.
Si la cola de prioridad permite el acceso simultáneo, varios procesos pueden realizar operaciones simultáneamente en esa cola de prioridad. Sin embargo, esto plantea dos problemas. En primer lugar, la definición de la semántica de las operaciones individuales ya no es obvia. Por ejemplo, si dos procesos quieren extraer el elemento con la prioridad más alta, ¿deberían obtener el mismo elemento o diferentes? Esto restringe el paralelismo en el nivel del programa que utiliza la cola de prioridad. Además, debido a que varios procesos tienen acceso al mismo elemento, esto conduce a la contención.
El acceso concurrente a una cola de prioridad se puede implementar en un modelo PRAM de lectura concurrente, escritura concurrente (CRCW). A continuación, la cola de prioridad se implementa como una lista de omisión . [25] [26] Además, se utiliza una primitiva de sincronización atómica, CAS , para hacer que la lista de omisión esté libre de bloqueos . Los nodos de la lista de omisión consisten en una clave única, una prioridad, una matriz de punteros , para cada nivel, a los siguientes nodos y una marca de eliminación . La marca de eliminación marca si el nodo está a punto de ser eliminado por un proceso. Esto garantiza que otros procesos puedan reaccionar a la eliminación de manera apropiada.
Si se permite el acceso simultáneo a una cola de prioridad, pueden surgir conflictos entre dos procesos. Por ejemplo, surge un conflicto si un proceso intenta insertar un nuevo nodo, pero al mismo tiempo otro proceso está a punto de eliminar el predecesor de ese nodo. [25] Existe el riesgo de que el nuevo nodo se agregue a la lista de omisiones, pero ya no sea accesible. ( Ver imagen )
En esta configuración, las operaciones en una cola de prioridad se generalizan a un lote de elementos. Por ejemplo, k_extract-min elimina los elementos más pequeños de la cola de prioridad y los devuelve.
En un entorno de memoria compartida , la cola de prioridad paralela se puede implementar fácilmente utilizando árboles de búsqueda binarios paralelos y algoritmos de árboles basados en uniones . En particular, k_extract-min corresponde a una división en el árbol de búsqueda binario que tiene un costo y produce un árbol que contiene los elementos más pequeños. k_insert se puede aplicar mediante una unión de la cola de prioridad original y el lote de inserciones. Si el lote ya está ordenado por la clave, k_insert tiene un costo. De lo contrario, primero debemos ordenar el lote, por lo que el costo será . Otras operaciones para la cola de prioridad se pueden aplicar de manera similar. Por ejemplo, k_decrease-key se puede realizar aplicando primero difference y luego union , que primero elimina los elementos y luego los vuelve a insertar con las claves actualizadas. Todas estas operaciones son altamente paralelas, y la eficiencia teórica y práctica se puede encontrar en artículos de investigación relacionados. [27] [28]
El resto de esta sección analiza un algoritmo basado en colas en memoria distribuida. Suponemos que cada procesador tiene su propia memoria local y una cola de prioridad local (secuencial). Los elementos de la cola de prioridad global (paralela) se distribuyen entre todos los procesadores.
Una operación k_insert asigna los elementos de manera aleatoria uniforme a los procesadores que insertan los elementos en sus colas locales. Nótese que aún se pueden insertar elementos individuales en la cola. Al utilizar esta estrategia, los elementos globales más pequeños se encuentran en la unión de los elementos locales más pequeños de cada procesador con alta probabilidad. Por lo tanto, cada procesador tiene una parte representativa de la cola de prioridad global.
Esta propiedad se utiliza cuando se ejecuta k_extract-min , ya que los elementos más pequeños de cada cola local se eliminan y se recopilan en un conjunto de resultados. Los elementos del conjunto de resultados aún están asociados con su procesador original. La cantidad de elementos que se eliminan de cada cola local depende de y la cantidad de procesadores . [29] Mediante la selección paralela, se determinan los elementos más pequeños del conjunto de resultados. Con alta probabilidad, estos son los elementos globales más pequeños. Si no, los elementos se eliminan nuevamente de cada cola local y se colocan en el conjunto de resultados. Esto se hace hasta que los elementos globales más pequeños estén en el conjunto de resultados. Ahora se pueden devolver estos elementos. Todos los demás elementos del conjunto de resultados se insertan nuevamente en sus colas locales. El tiempo de ejecución de k_extract-min es esperado , donde y es el tamaño de la cola de prioridad. [29]
La cola de prioridad se puede mejorar aún más si no se mueven los elementos restantes del conjunto de resultados directamente de nuevo a las colas locales después de una operación k_extract-min . Esto ahorra tener que mover elementos de un lado a otro todo el tiempo entre el conjunto de resultados y las colas locales.
Al eliminar varios elementos a la vez, se puede lograr una aceleración considerable. Pero no todos los algoritmos pueden usar este tipo de cola de prioridad. El algoritmo de Dijkstra, por ejemplo, no puede funcionar en varios nodos a la vez. El algoritmo toma el nodo con la menor distancia de la cola de prioridad y calcula nuevas distancias para todos sus nodos vecinos. Si se eliminaran nodos, trabajar en un nodo podría cambiar la distancia de otro de los nodos. Por lo tanto, el uso de operaciones de k elementos destruye la propiedad de configuración de etiquetas del algoritmo de Dijkstra.
{{cite web}}
: CS1 maint: copia archivada como título ( enlace )