Consulte la propuesta MÁS RECIENTE en el repositorio de corefxlab.
Propuesta de https://github.com/dotnet/corefx/issues/574#issuecomment -307971397
Los elementos de la cola de prioridad son únicos. Si no es así, tendríamos que introducir "identificadores" de elementos para permitir su actualización / eliminación. O la semántica de actualización / eliminación tendría que aplicarse a primero / todo, lo cual es extraño.
Modelado a partir de Queue<T>
( enlace MSDN )
`` c #
clase pública PriorityQueue
: IEnumerable,
IEnumerable <(elemento TElement, prioridad TPriority)>,
IReadOnlyCollection <(elemento TElement, prioridad TPriority)>
// IColección no incluida a propósito
{
public PriorityQueue ();
Public PriorityQueue (IComparer
public IComparer<TPriority> Comparer { get; }
public int Count { get; }
public bool IsEmpty { get; }
public bool Contains(TElement element);
// Peek & Dequeue
public (TElement element, TPriority priority) Peek(); // Throws if empty
public (TElement element, TPriority priority) Dequeue(); // Throws if empty
public bool TryPeek(out TElement element, out TPriority priority); // Returns false if empty
public bool TryDequeue(out TElement element, out TPriority priority); // Returns false if empty
// Enqueue & Update
public void Enqueue(TElement element, TPriority priority); // Throws if it is duplicate
public void Update(TElement element, TPriority priority); // Throws if element does not exist
public void EnqueueOrUpdate(TElement element, TPriority priority);
public bool TryEnqueue(TElement element, TPriority priority); // Returns false if it is duplicate (does NOT update it)
public bool TryUpdate(TElement element, TPriority priority); // Returns false if element does not exist (does NOT add it)
public void Remove(TElement element); // Throws if element does not exist
public bool TryRemove(TElement element); // Returns false if element does not exist
public void Clear();
public IEnumerator<(TElement element, TPriority priority)> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
//
// Selector de parte
//
Public PriorityQueue (Func
Public PriorityQueue (Func
public Func<TElement, TPriority> PrioritySelector { get; }
public void Enqueue(TElement element);
public void Update(TElement element);
}
`` ``
Preguntas abiertas:
PriorityQueue
vs. Heap
IHeap
y sobrecarga del constructor? (¿Deberíamos esperar más tarde?)IPriorityQueue
? (¿Deberíamos esperar más tarde - IDictionary
ejemplo)(TElement element, TPriority priority)
frente a KeyValuePair<TPriority, TElement>
Peek
y Dequeue
tener out
argumento Peek
y Dequeue
?El problema https://github.com/dotnet/corefx/issues/163 solicitó la adición de una cola de prioridad a las estructuras de datos de recopilación de .NET centrales.
Esta publicación, aunque es un duplicado, está destinada a actuar como la presentación formal al proceso de revisión de la API de corefx. El contenido del problema es el _speclet_ para un nuevo System.Collections.Generic.PriorityQueue
Contribuiré con el PR, si se aprueba.
Actualmente, las bibliotecas de clases base de .NET (BCL) carecen de soporte para las colecciones de productor-consumidor ordenadas. Un requisito común de muchas aplicaciones de software es la capacidad de generar una lista de elementos a lo largo del tiempo y procesarlos en un orden diferente al orden en el que se recibieron.
Hay tres estructuras de datos genéricas dentro de la jerarquía de espacios de nombres System.Collections que admitían una colección ordenada de elementos; System.Collections.Generic.SortedList, System.Collections.Generic.SortedSet y System.Collections.Generic.SortedDictionary.
De estos, SortedSet y SortedDictionary no son apropiados para patrones de productor-consumidor que generan valores duplicados. La complejidad de SortedList es Θ (n) el peor de los casos tanto para Agregar como para Eliminar.
Una estructura de datos mucho más eficiente en cuanto a memoria y tiempo para colecciones ordenadas con patrones de uso de productor-consumidor es una cola de prioridad. Aparte de cuando es necesario cambiar el tamaño de la capacidad, en el peor de los casos, el rendimiento de inserción (poner en cola) y quitar la parte superior (sacar) es Θ (log n), mucho mejor que las opciones existentes que existen en BCL.
Las colas de prioridad tienen un amplio grado de aplicabilidad en diferentes clases de aplicaciones. La página de Wikipedia sobre colas de prioridad ofrece una lista de muchos casos de uso bien entendidos. Si bien las implementaciones altamente especializadas aún pueden requerir implementaciones de colas de prioridad personalizadas, una implementación estándar cubriría una amplia gama de escenarios de uso. Las colas de prioridad son particularmente útiles para programar la salida de múltiples productores, que es un patrón importante en software altamente paralelizado.
Vale la pena señalar que tanto la biblioteca estándar de C ++ como Java ofrecen funcionalidad de cola de prioridad como parte de sus API básicas.
`` C #
espacio de nombres System.Collections.Generic
{
///
/// Representa una colección de objetos que se eliminan en un orden ordenado.
///
///
[DebuggerDisplay ("Count = {count}")]
[DebuggerTypeProxy (typeof (System_PriorityQueueDebugView <>))]
clase pública PriorityQueue
{
///
/// Inicializa una nueva instancia del
/// que usa un comparador predeterminado.
///
public PriorityQueue ();
/// <summary>
/// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class
/// that has the specified initial capacity.
/// </summary>
/// <param name="capacity">The initial number of elements that the <see cref="PriorityQueue{T}"/> can contain.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
public PriorityQueue(int capacity);
/// <summary>
/// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class
/// that uses a specified comparer.
/// </summary>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IComparer{T}"/> to use when comparing elements.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is null.</exception>
public PriorityQueue(IComparer<T> comparer);
/// <summary>
/// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class
/// that contains elements copied from the specified collection and uses a default comparer.
/// </summary>
/// <param name="collection">The collection whose elements are copied to the new <see cref="PriorityQueue{T}"/>.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="collection"/> is null.</exception>
public PriorityQueue(IEnumerable<T> collection);
/// <summary>
/// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class
/// that contains elements copied from the specified collection and uses a specified comparer.
/// </summary>
/// <param name="collection">The collection whose elements are copied to the new <see cref="PriorityQueue{T}"/>.</param>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IComparer{T}"/> to use when comparing elements.</param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="collection"/> is null. -or-
/// <paramref name="comparer"/> is null.
/// </exception>
public PriorityQueue(IEnumerable<T> collection, IComparer<T> comparer);
/// <summary>
/// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class that is empty,
/// has the specified initial capacity, and uses a specified comparer.
/// </summary>
/// <param name="capacity">The initial number of elements that the <see cref="PriorityQueue{T}"/> can contain.</param>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IComparer{T}"/> to use when comparing elements.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is null.</exception>
public PriorityQueue(int capacity, IComparer<T> comparer);
/// <summary>
/// Gets the <see cref="IComparer{T}"/> for the <see cref="PriorityQueue{T}"/>.
/// </summary>
/// <value>
/// The <see cref="T:System.Collections.Generic.IComparer{T}"/> that is used when
/// comparing elements in the <see cref="PriorityQueue{T}"/>.
/// </value>
public IComparer<T> Comparer
{
get;
}
/// <summary>
/// Gets the number of elements contained in the <see cref="PriorityQueue{T}"/>.
/// </summary>
/// <value>The number of elements contained in the <see cref="PriorityQueue{T}"/>.</value>
public int Count
{
get;
}
/// <summary>
/// Adds an object to the into the <see cref="PriorityQueue{T}"/> by its priority.
/// </summary>
/// <param name="item">
/// The object to add to the <see cref="PriorityQueue{T}"/>.
/// The value can be null for reference types.
/// </param>
public void Enqueue(T item);
/// <summary>
/// Removes and returns the object with the lowest priority in the <see cref="PriorityQueue{T}"/>.
/// </summary>
/// <returns>The object with the lowest priority that is removed from the <see cref="PriorityQueue{T}"/>.</returns>
/// <exception cref="InvalidOperationException">The <see cref="PriorityQueue{T}"/> is empty.</exception>
public T Dequeue();
/// <summary>
/// Returns the object with the lowest priority in the <see cref="PriorityQueue{T}"/>.
/// </summary>
/// <exception cref="InvalidOperationException">The <see cref="PriorityQueue{T}"/> is empty.</exception>
public T Peek();
/// <summary>
/// Removes all elements from the <see cref="PriorityQueue{T}"/>.
/// </summary>
public void Clear();
/// <summary>
/// Determines whether an element is in the <see cref="PriorityQueue{T}"/>.
/// </summary>
/// <param name="item">
/// The object to add to the end of the <see cref="PriorityQueue{T}"/>.
/// The value can be null for reference types.
/// </param>
/// <returns>
/// true if item is found in the <see cref="PriorityQueue{T}"/>; otherwise, false.
/// </returns>
public bool Contains(T item);
/// <summary>
/// Copies the elements of the <see cref="PriorityQueue{T}"/> to an <see cref="T:System.Array"/>,
/// starting at a particular <see cref="T:System.Array"/> index.
/// </summary>
/// <param name="array">
/// The one-dimensional <see cref="T:System.Array">Array</see> that is the
/// destination of the elements copied from the <see cref="PriorityQueue{T}"/>.
/// The <see cref="T:System.Array">Array</see> must have zero-based indexing.
/// </param>
/// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="array"/> is null.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="arrayIndex"/> is less than zero. -or-
/// <paramref name="arrayIndex"/> is equal to or greater than the length of the <paramref name="array"/>
/// </exception>
/// <exception cref="ArgumentException">
/// The number of elements in the source <see cref="T:System.Collections.ICollection"/> is
/// greater than the available space from <paramref name="index"/> to the end of the destination
/// <paramref name="array"/>.
/// </exception>
public void CopyTo(T[] array, int arrayIndex);
/// <summary>
/// Copies the elements of the <see cref="T:System.Collections.ICollection"/> to an
/// <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
/// </summary>
/// <param name="array">
/// The one-dimensional <see cref="T:System.Array">Array</see> that is the
/// destination of the elements copied from the <see cref="PriorityQueue{T}"/>.
/// The <see cref="T:System.Array">Array</see> must have zero-based indexing.
/// </param>
/// <param name="index">The zero-based index in <paramref name="array"/> at which copying begins.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="array"/> is null.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index"/> is less than zero.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="array"/> is multidimensional. -or-
/// <paramref name="array"/> does not have zero-based indexing. -or-
/// <paramref name="index"/> is equal to or greater than the length of the <paramref name="array"/> -or-
/// The number of elements in the source <see cref="T:System.Collections.ICollection"/> is
/// greater than the available space from <paramref name="index"/> to the end of the destination
/// <paramref name="array"/>. -or-
/// The type of the source <see cref="T:System.Collections.ICollection"/> cannot be cast automatically
/// to the type of the destination <paramref name="array"/>.
/// </exception>
void ICollection.CopyTo(Array array, int index);
/// <summary>
/// Copies the elements stored in the <see cref="PriorityQueue{T}"/> to a new array.
/// </summary>
/// <returns>
/// A new array containing a snapshot of elements copied from the <see cref="PriorityQueue{T}"/>.
/// </returns>
public T[] ToArray();
/// <summary>
/// Returns an enumerator that iterates through the <see cref="PriorityQueue{T}"/>
/// </summary>
/// <returns>An enumerator for the contents of the <see cref="PriorityQueue{T}"/>.</returns>
public Enumerator GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the <see cref="PriorityQueue{T}"/>
/// </summary>
/// <returns>An enumerator for the contents of the <see cref="PriorityQueue{T}"/>.</returns>
IEnumerator<T> IEnumerable<T>.GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the <see cref="PriorityQueue{T}"/>.
/// </summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator"/> that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator();
/// <summary>
/// Sets the capacity to the actual number of elements in the <see cref="PriorityQueue{T}"/>,
/// if that number is less than than a threshold value.
/// </summary>
public void TrimExcess();
/// <summary>
/// Gets a value that indicates whether access to the <see cref="ICollection"/> is
/// synchronized with the SyncRoot.
/// </summary>
/// <value>true if access to the <see cref="T:System.Collections.ICollection"/> is synchronized
/// with the SyncRoot; otherwise, false. For <see cref="PriorityQueue{T}"/>, this property always
/// returns false.</value>
bool ICollection.IsSynchronized
{
get;
}
/// <summary>
/// Gets an object that can be used to synchronize access to the
/// <see cref="T:System.Collections.ICollection"/>.
/// </summary>
/// <value>
/// An object that can be used to synchronize access to the
/// <see cref="T:System.Collections.ICollection"/>.
/// </value>
object ICollection.SyncRoot
{
get;
}
public struct Enumerator : IEnumerator<T>
{
public T Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
public void Dispose();
}
}
}
''
| Operación | Complejidad | Notas |
| --- | --- | --- |
| Construir | Θ (1) | |
| Construir con IEnumerable | Θ (n) | |
| Enqueue | Θ (log n) | |
| Dequeue | Θ (log n) | |
| Peek | Θ (1) | |
| Contar | Θ (1) | |
| Limpiar | Θ (N) | |
| Contiene | Θ (N) | |
| CopyTo | Θ (N) | Utiliza Array.Copy, la complejidad real puede ser menor |
| ToArray | Θ (N) | Utiliza Array.Copy, la complejidad real puede ser menor |
| GetEnumerator | Θ (1) | |
| Enumerator.MoveNext | Θ (1) | |
¿Debería ser "estable" la colección? En otras palabras, ¿deberían dos elementos con igual IComparison
Se corrigió la complejidad de 'Construir usando IEnumerable' a Θ (n). Gracias @svick.
| Operación | Complejidad |
| --- | --- |
| Construir con IEnumerable | Θ (log n) |
Creo que debería ser Θ (n). Necesita al menos iterar la entrada.
+1
Rx tiene una clase de cola de prioridad altamente probada en producción:
¿Debería usarse una estructura de enumerador público anidado para evitar una asignación de montón adicional durante las llamadas a GetEnumerator y cuando se usa foreach? Mi suposición es no, ya que enumerar en una cola es una operación poco común.
Me inclinaría por usar el enumerador de estructuras para ser coherente con Queue<T>
que usa un enumerador de estructuras. Además, si no se usa un enumerador de estructuras ahora, no podremos cambiar PriorityQueue<T>
para usar uno en el futuro.
¿También quizás un método para inserciones por lotes? ¿Siempre se puede ordenar y continuar desde el punto de inserción anterior en lugar de comenzar desde el principio si eso ayudaría ?:
public void Enqueue(List<T> items) {
items.Sort(_comparer);
... insertions ...
}
He arrojado una copia de la implementación inicial aquí . La cobertura de la prueba está lejos de ser completa, pero si alguien tiene curiosidad, por favor, eche un vistazo y dígame lo que piensa. Intenté seguir las convenciones de codificación existentes de las clases System.Collections tanto como sea posible.
Frio. Algunos comentarios iniciales:
Queue<T>.Enumerator
implementa IEnumerator.Reset
explícitamente. ¿Debería PriorityQueue<T>.Enumerator
hacer lo mismo?Queue<T>.Enumerator
usa _index == -2
para indicar que el enumerador ha sido eliminado. PriorityQueue<T>.Enumerator
tiene el mismo comentario pero tiene un campo adicional _disposed
. Considere deshacerse del campo adicional _disposed
y use _index == -2
para indicar que se ha dispuesto para hacer la estructura más pequeña y ser consistente con Queue<T>
_emptyArray
se puede eliminar y el uso se puede reemplazar con Array.Empty<T>()
lugar.También...
Dictionary<TKey, TValue>
, HashSet<T>
, SortedList<TKey, TValue>
, SortedDictionary<TKey, TValue>
, SortedSet<T>
, etc.) permiten que nulo sea pasado para el comparador, en cuyo caso se utiliza Comparer<T>.Default
.También...
ToArray
se puede optimizar comprobando _size == 0
antes de asignar la nueva matriz, en cuyo caso simplemente devuelva Array.Empty<T>()
.@justinvp ¡ Excelentes comentarios, gracias!
@ebickle
Array.Empty es una función de F #, ¡así que lamentablemente no puedo usarla aquí!
Ya no: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Array.cs#L1060 -L1069
He migrado el código a ebickle / corefx en la rama del número 574.
Implementó el Array.Empty
Se solucionará una vez que se complete el problema dotnet / corefx # 966.
Un área clave en la que estoy buscando comentarios es cómo se deben manejar ToArray, CopyTo y el enumerador. Actualmente están optimizados para el rendimiento, lo que significa que la matriz de destino es un montón y no está ordenada por el comparador.
Hay tres opciones:
1) Déjelo como está y documente que la matriz devuelta no está ordenada. (es una cola de prioridad, no una cola ordenada)
2) Modifique los métodos para ordenar los elementos / matrices devueltos. Ya no serán operaciones O (n).
3) Elimine los métodos y el soporte enumerable por completo. Esta es la opción "purista", pero elimina la capacidad de tomar rápidamente los elementos restantes de la cola cuando la cola de prioridad ya no es necesaria.
Otra cosa sobre la que me gustaría recibir comentarios es si la cola debe ser estable para dos elementos con la misma prioridad (compare el resultado de 0). En general, las colas de prioridad no garantizan que dos elementos con la misma prioridad se quitarán de la cola en el orden en que se pusieron en cola, pero noté que la implementación de la cola de prioridad interna utilizada en System.Reactive.Core requirió algunos gastos generales adicionales para garantizar esta propiedad. Mi preferencia sería no hacer esto, pero no estoy completamente seguro de qué opción es mejor en términos de las expectativas de los desarrolladores.
Me encontré con este PR porque yo también estaba interesado en agregar una cola de prioridad a .NET. Me alegra ver que alguien hizo el esfuerzo de hacer esta propuesta :). Después de revisar el código, noté lo siguiente:
IComparer
no es consistente con Equals
, el comportamiento de esta implementación Contains
(que usa el IComparer
) puede ser sorprendente para algunos usuarios, ya que es esencialmente _contiene un elemento con la misma prioridad_.Dequeue
. Es típico reducir la matriz de pila a la mitad cuando está llena en un cuarto.Enqueue
aceptar argumentos null
?Clear
debería ser Θ (n), ya que esa es la complejidad de System.Array.Clear
, que utiliza. https://msdn.microsoft.com/en-us/library/system.array.clear%28v=vs.110%29.aspxNo vi código para reducir la matriz en
Dequeue
. Es típico reducir la matriz de pila a la mitad cuando está llena en un cuarto.
Queue<T>
y Stack<T>
tampoco reducen sus matrices (según la Fuente de referencia , todavía no están en CoreFX).
@mbeidler Había considerado agregar alguna forma de reducción automática de matrices en la cola, pero como @svick señaló, no existe en las implementaciones de referencia de estructuras de datos similares. Tendría curiosidad por saber de alguien del equipo de .NET Core / BCL si hay alguna razón en particular por la que eligieron ese estilo de implementación.
Actualización: verifiqué la Lista
Enqueue debe admitir valores nulos y está documentado que los permite. ¿Te encontraste con un error? Todavía no recuerdo cuán robusta es el área de pruebas unitarias en el área.
Interesante, noté en la fuente de referencia vinculada por @svick que Queue<T>
tiene una constante privada no utilizada llamada _ShrinkThreshold
. Quizás ese comportamiento existía en una versión anterior.
Con respecto al uso de IComparer
lugar de Equals
en la implementación de Contains
, escribí la siguiente prueba unitaria, que fallaría actualmente: https: //gist.github. com / mbeidler / 9e9f566ba7356302c57e
@mbeidler Buen punto. Según MSDN, IComparer
Sin embargo, parece que existe el mismo problema en las otras clases de colección. Si modifico el código para operar en SortedList
Podría decirse que también es un error en las clases de colección existentes. Examinaré el resto de las clases de colecciones y veré si hay otras estructuras de datos que acepten un IComparer pero prueben la igualdad por separado. Sin embargo, tiene razón, para una cola de prioridad esperaría un comportamiento de pedido personalizado que sea completamente independiente de la igualdad.
Envié una solución y el caso de prueba en mi rama de bifurcación. La nueva implementación de Contains se basa directamente en el comportamiento de List
Cuando llegue un tiempo más tarde hoy, probablemente enviaré informes de errores para las otras colecciones integradas. Probablemente no se pueda solucionar debido al comportamiento de regresión, pero al menos la documentación debe actualizarse.
Creo que tiene sentido que ContainsKey
use la implementación IComparer<TKey>
, ya que eso es lo que especifica la clave. Sin embargo, creo que sería más lógico que ContainsValue
use Equals
lugar de IComparable<TValue>
en su búsqueda lineal; aunque en este caso el alcance se reduce significativamente ya que es mucho menos probable que el orden natural de un tipo sea inconsistente con iguales.
Parece que en la documentación de MSDN para SortedList<TKey, TValue>
, la sección de comentarios para ContainsValue
indica que el orden de clasificación de TValue se usa en lugar de igualdad.
@terrajobst ¿Qué
: +1:
Gracias por archivar esto. Creo que tenemos suficientes datos para hacer una revisión formal de esta propuesta, por lo tanto, la etiqueté como 'lista para revisión de API'
Como Dequeue
y Peek
son métodos de lanzamiento, la persona que llama debe verificar el recuento antes de cada llamada. ¿Tendría sentido en su lugar (o además) proporcionar TryDequeue
y TryPeek
siguiendo el patrón de las colecciones concurrentes? Hay problemas abiertos para agregar métodos no arrojadizos a colecciones genéricas existentes, por lo que agregar una nueva colección que no tenga estos métodos parece contraproducente.
@andrewgmorris relacionado con https://github.com/dotnet/corefx/issues/4316 "Agregar TryDequeue a la cola"
Tuvimos una revisión básica de esto y estamos de acuerdo en que queremos un ProrityQueue en el marco. Sin embargo, necesitamos que alguien nos ayude a impulsar el diseño y la implementación. Quien se ocupe del problema puede trabajar con
Entonces, ¿qué trabajo queda en la API?
Esto falta en la propuesta de API anterior: PriorityQueue<T>
debería implementar IReadOnlyCollection<T>
para que coincida con Queue<T>
( Queue<T>
ahora implementa IReadOnlyCollection<T>
partir de .NET 4.6).
No sé si las colas de prioridad basadas en matrices son las mejores. La asignación de memoria en .NET es realmente rápida. No tenemos el mismo problema de búsqueda de bloques pequeños con el que se ocupó el antiguo malloc. Puede utilizar mi código de cola de prioridad desde aquí: https://github.com/BrannonKing/Kts.Astar/tree/master/Kts.AStar
@ebickle Una pequeña liendre en el _speclet_. Dice:
/// Adds an object to the end of the <see cref="PriorityQueue{T}"/>.
...
public void Enqueue(T item);
¿No debería decir en su lugar, /// Inserts object into the <see cref="PriorityQueue{T}"> by its priority.
@SunnyWar Se
Hace algún tiempo, creé una estructura de datos con complejidades similares a una cola de prioridad basada en una estructura de datos de lista de omisión que decidí compartir en este momento: https://gist.github.com/bbarry/5e0f3cc1ac7f7521fe6ea25947f48ace
https://en.wikipedia.org/wiki/Skip_list
La lista de omisión coincide con las complejidades de una cola de prioridad anterior en casos promedio, excepto que Contiene es un caso promedio O(log(n))
. Además, el acceso al primer o al último elemento son operaciones de tiempo constante y la iteración tanto en orden directo como inverso coincide con las complejidades del orden directo de una PQ.
Obviamente, esta estructura tiene desventajas en forma de costos más altos en la memoria, y se transfiere a inserciones y eliminaciones de O(n)
peor de los casos, por lo que tiene sus compensaciones ...
¿Esto ya está implementado en alguna parte? ¿Cuándo es el lanzamiento esperado?
También, ¿qué hay de actualizar la prioridad de un elemento existente?
@ Priya91 @ianhays ¿ está esto listo para
Esto falta en la propuesta de API anterior: PriorityQueue
debe implementar IReadOnlyCollection para que coincida con la cola (Cola ahora implementa IReadOnlyCollection a partir de .NET 4.6).
Estoy de acuerdo con @justinvp aquí.
@ Priya91 @ianhays ¿ está esto listo para
Yo diría que sí. Esto ha estado sentado por un tiempo; hagamos movimientos en él.
@justinvp @ianhays He actualizado la especificación para implementar IReadOnlyCollection
Tengo una implementación completa de la clase y su PriorityQueueDebugView asociado que usa una implementación basada en matrices. Las pruebas unitarias aún no tienen una cobertura del 100%, pero si hay algo de interés, puedo trabajar un poco y quitarme el polvo de la horquilla.
@NKnusperer hizo un buen comentario sobre la actualización de la prioridad de un elemento existente. Lo dejaré fuera por ahora, pero es algo a considerar durante la revisión de especificaciones.
Hay 2 implementaciones en full framework, que son posibles alternativas.
https://referencesource.microsoft.com/#q = priorityqueue
Como referencia, aquí hay una pregunta sobre Java PriorityQueue en stackoverflow http://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueue. Es interesante que la prioridad sea manejada por un comparador en lugar de un simple objeto contenedor de prioridad int. Por ejemplo, no hace que sea fácil cambiar la prioridad de un elemento en la cola.
Revisión de API:
Estamos de acuerdo en que es útil tener este tipo en CoreFX, porque esperamos que CoreFX lo use.
Para una revisión final de la forma de la API, nos gustaría ver un código de muestra: PriorityQueue<Thread>
y PriorityQueue<MyClass>
.
T
.Notas:
Remove
y Add
en la cola.Este sería un tipo realmente útil para tener en CoreFX. ¿Alguien está interesado en hacerse con este?
No me gusta la idea de arreglar la cola de prioridad en un montón binario. Eche un vistazo a mi página wiki de AlgoKit para obtener más detalles. Pensamientos rápidos:
El año pasado implementé algunos tipos de montón . En particular, hay una interfaz IHeap
que podría usarse como cola de prioridad.
Estoy totalmente a favor de presentar una interfaz IHeap
y algunas de las implementaciones más eficaces (al menos las basadas en matrices). La API y las implementaciones están en el repositorio que vinculé anteriormente.
Así que no hay _ colas de prioridad_. Montones .
¿Qué piensas?
@karelz @safern @danmosemsft
@pgolebiowski No olvide que estamos diseñando una API fácil de usar y con un PriorityQueue
(que es un término establecido en ciencias de la computación), debería poder encontrar uno en docs / a través de una búsqueda en Internet.
Si la implementación subyacente es montón (que personalmente me pregunto por qué), o algo más, no importa demasiado. Suponiendo que pueda demostrar que la implementación es considerablemente mejor que las alternativas más simples (la complejidad del código también es una métrica que de alguna manera importa).
Por lo tanto, si aún cree que la implementación basada en montón (sin IHeap
API) es mejor que una implementación simple basada en listas o en listas de fragmentos de matriz, explique por qué (idealmente en unas pocas oraciones / párrafos ), para que podamos llegar a un acuerdo sobre el enfoque de implementación (y evitar perder tiempo de su lado en una implementación compleja que puede ser rechazada en el momento de la revisión del RP).
Gota ICollection
, IEnumerable
? Solo tenga las versiones genéricas (aunque genérico IEnumerable<T>
traerá IEnumerable
)
@pgolebiowski cómo se implementa no cambia la API externa. PriorityQueue
define el comportamiento / contrato; mientras que Heap
es una implementación específica.
Si la implementación subyacente es montón (que personalmente me pregunto por qué), o algo más, no importa demasiado. Suponiendo que pueda demostrar que la implementación es considerablemente mejor que las alternativas más simples (la complejidad del código también es una métrica que de alguna manera importa).
Por lo tanto, si aún cree que la implementación basada en montones es mejor que una implementación simple basada en listas o listas de conjuntos de fragmentos, explique por qué (idealmente en unas pocas oraciones / párrafos), para que podamos llegar a un acuerdo sobre el estrategia de implementación [...]
está bien. Una cola de prioridad es una estructura de datos abstracta que luego se puede implementar de alguna manera. Por supuesto, puede implementarlo con una estructura de datos diferente a un montón. Pero de ninguna manera es más eficiente. Como resultado:
Para respaldar mis palabras, comencemos con el soporte teórico. Introducción a los algoritmos , Cormen:
[…] Las colas de prioridad vienen en dos formas: colas de máxima prioridad y colas de mínima prioridad. Nos centraremos aquí en cómo implementar colas de máxima prioridad, que a su vez se basan en max-montones.
Dijo claramente que las colas de prioridad son montones. Este es un atajo, por supuesto, pero entiendes la idea. Ahora, lo que es más importante, echemos un vistazo a cómo otros lenguajes y sus bibliotecas estándar agregan soporte para las operaciones que estamos discutiendo:
PriorityQueue<T>
- cola de prioridad implementada con un montón.BinaryHeap
- montón explícitamente en la API. Dice en los documentos que es una cola de prioridad implementada con un montón binario. Pero la API es muy clara sobre la estructura: un montón.CFBinaryHeap
- nuevamente, dice explícitamente cuál es la estructura de datos, evitando el uso del término abstracto "cola de prioridad". Los documentos que describen la clase: Los montones binarios pueden ser útiles como colas de prioridad. Me gusta el enfoque.priority_queue
- una vez más, implementado canónicamente con un montón binario construido en la parte superior de una matriz.heapq
- el montón se expone explícitamente en la API. La cola de prioridad solo se menciona en los documentos: este módulo proporciona una implementación del algoritmo de cola de pila, también conocido como algoritmo de cola de prioridad. heap package
- incluso hay una interfaz de pila. Sin cola de prioridad explícita, pero nuevamente solo en documentos: un montón es una forma común de implementar una cola de prioridad. Creo firmemente que deberíamos seguir el camino Rust / Swift / Python / Go y exponer un montón explícitamente mientras se indica claramente en los documentos que se puede usar como una cola de prioridad. Creo firmemente que este enfoque es muy limpio y simple.
Espero que a todos les guste el enfoque de exponer claramente una estructura de datos de pila y hacer una referencia de cola de prioridad solo en los documentos.
Necesitamos ponernos de acuerdo sobre el asunto anterior, pero permítanme comenzar con otro tema: cómo implementar esto . Puedo ver dos soluciones aquí:
ArrayHeap
. Al ver el nombre, puede saber inmediatamente con qué tipo de montón está tratando (de nuevo, hay docenas de tipos de montón). Verá que está basado en matrices. De inmediato conoces a la bestia con la que estás tratando.IHeap
y proporcionar una o más implementaciones. El cliente podría escribir código que dependa de las interfaces, lo que permitiría proporcionar implementaciones realmente claras y legibles de algoritmos complejos. Imagina escribir una clase DijkstraAlgorithm
. Simplemente podría depender de la interfaz IHeap
(un constructor parametrizado) o simplemente usar ArrayHeap
(constructor predeterminado). Limpio, simple, explícito, sin ambigüedad debido al uso de un término de "cola de prioridad". Y una interfaz maravillosa que tiene mucho sentido teóricamente.En el enfoque anterior, ArrayHeap
representa un árbol d-ario completo ordenado en montón implícito, almacenado como una matriz. Esto se puede utilizar para crear, por ejemplo, un BinaryHeap
o un QuaternaryHeap
.
Para una mayor discusión, le recomiendo encarecidamente que lea este documento . Sabrás que:
Los montones de 4 arios (cuaternarios) son simplemente más rápidos que los montones de 2 arios (binarios). Hay muchas pruebas realizadas en el documento; le interesaría comparar el rendimiento de implicit_4
y implicit_2
(a veces implicit_simple_4
y implicit_simple_2
).
La elección óptima de implementación depende en gran medida de los insumos. De montones d-arios implícitos, montones emparejados, montones de Fibonacci, montones binomiales, montones d-arios explícitos, montones de emparejamiento de rangos, montones de terremotos, montones de violaciones, montones débiles de rango relajado y montones estrictos de Fibonacci, los siguientes tipos parecen funcionar cubre casi todas las necesidades para varios escenarios:
IHeap
, valdría la pena agregar esto, ya que en muchos casos es realmente rápido y más rápido que la solución basada en arreglos .Para ambos, el esfuerzo de programación es sorprendentemente bajo. Eche un vistazo a mis implementaciones.
@karelz @benaadams
Este sería un tipo realmente útil para tener en CoreFX. ¿Alguien está interesado en hacerse con este?
@safern Estaría muy feliz de tomar este de ahora en adelante.
De acuerdo, fue un problema entre mi teclado y mi silla - PriorityQueue
por supuesto se basa en Heap
- Estaba pensando en solo Queue
donde no tiene sentido y olvidé que el montón está "ordenado" - interrupción del proceso de pensamiento muy vergonzoso para alguien como yo, que ama la lógica, los algoritmos, las máquinas de Turing, etc., mis disculpas. (Por cierto: tan pronto como leí un par de oraciones en su enlace de documentos de Java, la discrepancia hizo clic de inmediato)
Desde esa perspectiva, tiene sentido construir la API sobre Heap
. Sin embargo, no deberíamos hacer pública esa clase todavía; requerirá su propia revisión de API y su propia discusión si es algo que necesitamos en CoreFX. No queremos que la superficie de la API se arrastre debido a la implementación, pero puede ser lo correcto, de ahí la discusión necesaria.
Desde esa perspectiva, no creo que necesitemos crear IHeap
todavía. Puede ser una buena decisión más adelante.
Si hay una investigación de que el montón específico (por ejemplo, 4-ary como mencionaste anteriormente) es mejor para la entrada aleatoria general , entonces deberíamos elegir eso. Esperemos a que @safern @ianhays @stephentoub confirme /
La parametrización del montón subyacente con múltiples opciones implementadas es algo que IMO no pertenece a CoreFX (puedo estar equivocado aquí, nuevamente, veamos lo que piensan los demás).
Mi razón es que, en mi opinión, pronto enviaríamos miles de millones de colecciones especializadas que serían muy difíciles de elegir para las personas (desarrollador promedio sin una sólida experiencia en matices de algoritmos). Sin embargo, dicha biblioteca sería un gran paquete de NuGet, para expertos en el campo, propiedad de usted / comunidad. En el futuro, podemos considerar agregarlo a PowerCollections (estamos discutiendo activamente durante los últimos 4 meses dónde colocar esta biblioteca en GitHub y si deberíamos poseerla, o si deberíamos alentar a la comunidad a que la posea; hay diferentes opiniones al respecto , Espero que finalicemos su destino post 2.0)
Asignándote como quieres trabajar en él ...
Invitación de colaborador de asignártela en ese momento (limitaciones de GitHub).
@benaadams Me quedaría con ICollection
(preferencia leve). Para mantener la coherencia con otros ds en CoreFX. En mi opinión, no vale la pena tener una bestia extraña aquí ... si estuviéramos agregando un puñado de nuevos (por ejemplo, PowerCollections incluso a otro repositorio), no deberíamos incluir los no genéricos ... ¿pensamientos?
De acuerdo, fue un problema entre mi teclado y mi silla.
Jaja 😄 No te preocupes.
tiene sentido construir la API sobre Heap. No deberíamos hacer pública esa clase todavía, aunque [...] no queremos que la superficie de la API se arrastre debido a la implementación, pero puede ser lo correcto, de ahí la discusión necesaria. [...] No creo que necesitemos crear IHeap todavía. Puede ser una buena decisión más adelante.
Si la decisión del grupo es ir con PriorityQueue
, solo ayudaré con el diseño e implementaré esto. Sin embargo, tenga en cuenta el hecho de que si agregamos un PriorityQueue
ahora, será complicado en la API agregar Heap
más adelante , ya que ambos se comportan básicamente de la misma manera. Sería una especie de redundancia en mi opinión. Este sería un olor a diseño para mí. No agregaría la cola de prioridad. No ayuda.
Además, un pensamiento más. En realidad, la estructura de datos del montón de emparejamiento puede resultar útil con bastante frecuencia. Los montones basados en matrices son horribles al fusionarlos. Esta operación es básicamente lineal . Cuando tienes muchos montones de fusiones, estás matando el rendimiento. Sin embargo, si usa un montón de emparejamiento en lugar de un montón de matriz, la operación de fusión es constante (amortizada). Este es otro argumento por el que me gustaría proporcionar una interfaz agradable y dos implementaciones. Uno para información general, el segundo para algunos escenarios específicos, especialmente cuando se trata de la fusión de montones.
¡Votando por IHeap
+ ArrayHeap
+ PairingHeap
! 😄 (como en Rust / Swift / Python / Go)
Si el montón de emparejamiento es demasiado, está bien. Pero vayamos al menos con IHeap
+ ArrayHeap
. ¿No sienten que ir con una clase PriorityQueue
está bloqueando las posibilidades en el futuro y haciendo que la API sea menos clara?
Pero como dije, si todos votan por una clase PriorityQueue
sobre la solución propuesta, está bien.
Invitación de colaborador enviada: cuando acepte enviarme un ping, podré asignárselo en ese momento (limitaciones de GitHub).
@karelz ping :)
tenga en cuenta el hecho de que si agregamos un PriorityQueue ahora, será complicado en la API agregar Heap más adelante, ya que ambos se comportan básicamente de la misma manera. Sería una especie de redundancia en mi opinión. Este sería un olor a diseño para mí. No agregaría la cola de prioridad. No ayuda.
¿Puede explicar más detalles sobre por qué será complicado más adelante? ¿Cuáles son tus preocupaciones?
PriorityQueue
es el concepto que la gente usa. Tener un tipo con ese nombre es útil, ¿verdad?
Creo que las operaciones lógicas (al menos sus nombres) en Heap
podrían ser diferentes. Si son iguales, podemos tener 2 implementaciones diferentes del mismo código en el peor de los casos (no lo ideal, pero no el fin del mundo). O podemos insertar Heap
class como padre de PriorityQueue
, ¿verdad? (suponiendo que esté permitido desde la perspectiva de la revisión de API; en este momento no veo ninguna razón para no hacerlo, pero no tengo tantos años de experiencia con revisiones de API, por lo que esperaré a que otros lo confirmen)
Veamos cómo va la votación y la discusión de diseño adicional ... Me estoy acercando lentamente a la idea de IHeap
+ ArrayHeap
, pero aún no estoy completamente convencido ...
si estuviéramos agregando algunos nuevos ... no deberíamos incluir los no genéricos
Trapo rojo a un toro. ¿Alguien tiene otras colecciones para agregar para que podamos eliminar ICollection
?
Amortiguador circular / anular; ¿Genérico y concurrente?
@karelz Una solución para el problema de los nombres podría ser algo como IPriorityQueue
como lo hace DataFlow para los patrones de productos / consumidores. Hay muchas formas de implementar una cola de prioridad y, si no le importa, use la interfaz. Se preocupan por la implementación o están creando una clase de implementación de uso de instancia.
¿Puede explicar más detalles sobre por qué será complicado más adelante? ¿Cuáles son tus preocupaciones?
PriorityQueue
es el concepto que la gente usa. Tener un tipo con ese nombre es útil, ¿verdad? […] Me estoy acercando lentamente a la idea deIHeap
+ArrayHeap
, pero aún no estoy completamente convencido ...
@karelz Desde mi experiencia, encuentro muy importante tener una abstracción ( IPriorityQueue
o IHeap
). Gracias a este enfoque, un desarrollador puede escribir código desacoplado. Debido a que está escrito contra una interfaz (en lugar de una implementación específica), hay más flexibilidad y espíritu de IoC. Es muy fácil escribir pruebas unitarias para dicho código (con Inyección de dependencia, uno puede inyectar su propio IPriorityQueue
o IHeap
y ver qué métodos se llaman en qué momento y con qué argumentos). Las abstracciones son buenas.
Es cierto que el término "cola de prioridad" se usa comúnmente. El problema es que solo hay una forma de implementar una cola de prioridad de manera efectiva: con un montón. Muchos tipos de montones. Entonces podríamos tener:
class ArrayHeap : IPriorityQueue {}
class PairingHeap : IPriorityQueue {}
class FibonacciHeap : IPriorityQueue {}
class BinomialHeap : IPriorityQueue {}
o
class ArrayHeap : IHeap {}
class PairingHeap : IHeap {}
class FibonacciHeap : IHeap {}
class BinomialHeap : IHeap {}
Para mí, el segundo enfoque se ve mejor. ¿Cómo te sientes al respecto?
Con respecto a la clase PriorityQueue
, creo que sería demasiado vaga y estoy totalmente en contra de dicha clase. Una interfaz vaga es buena, pero no una implementación. Si es un montón binario, simplemente llámelo BinaryHeap
. Si se trata de otra cosa, asígnele el nombre correspondiente.
Estoy a favor de nombrar las clases claramente debido a los problemas con clases como SortedDictionary
. Existe mucha confusión entre los desarrolladores sobre lo que significa y en qué se diferencia de SortedList
. Si solo se llamara BinarySearchTree
, la vida sería más sencilla y podríamos ir a casa y ver a nuestros hijos antes.
Vamos a nombrar un montón de un montón.
@benaadams cada uno de ellos tiene que convertirlo en CoreFX (es decir, tiene que ser valioso para el código CoreFX en sí, o tiene que ser tan básico y ampliamente utilizado como los que ya tenemos); últimamente discutimos bastante esas cosas . Todavía no es un consenso del 100%, pero nadie está ansioso por agregar más colecciones a CoreFX.
El resultado más probable (afirmación sesgada, porque me gusta la solución) es que crearemos otro repositorio y lo prepararemos con PowerCollections y luego dejaremos que la comunidad lo extienda con una guía / supervisión básica de nuestro equipo.
Por cierto: @terrajobst cree que no vale la pena ("tenemos cosas mejores y más impactantes que hacer en el ecosistema") y deberíamos alentar a la comunidad a impulsarlo por completo (incluido comenzar con PowerCollections existentes) y no convertirlo en uno de nuestros repos - algunas discusiones y decisiones frente a nosotros.
// Supongo que hay una oportunidad para que la comunidad aproveche esta solución antes de que tomemos una decisión ;-). Eso haría que la discusión (y mi preferencia) se silenciara ;-)
@pgolebiowski , poco a poco me estás convenciendo de que tener Heap
es mejor que PriorityQueue
; solo necesitaríamos una guía y documentos sólidos "Así es como lo haces PriorityQueue
- usa Heap
"... eso podría funcionar.
Sin embargo, dudo mucho en incluir más de una implementación de montón en CoreFX. Al 98% + de los desarrolladores de C # 'normales' no les importa. Ni siquiera quieren pensar cuál es mejor, solo necesitan algo que haga el trabajo. No todas las piezas de cada software están diseñadas pensando en el alto rendimiento, con razón. Piense en todas las herramientas únicas, aplicaciones de interfaz de usuario, etc. A menos que esté diseñando un sistema de escalamiento horizontal de alto rendimiento donde este ds se encuentra en la ruta crítica, NO debería importarle.
De la misma manera, no me importa cómo se implementen SortedDictionary
o ArrayList
u otros ds: hacen su trabajo decentemente. Yo (como muchos otros) entiendo que si necesito un alto rendimiento de estos ds para mis escenarios , necesito medir el rendimiento y / o verificar la implementación y tengo que decidir si es lo suficientemente bueno para mis escenarios , o si necesito despliegue mi propia implementación especial para obtener el mejor rendimiento, ajustado a mis necesidades.
Deberíamos optimizar la usabilidad para el 98% de los casos de uso, no para el 2%. Si implementamos demasiadas opciones (implementaciones) y obligamos a todos a decidir, simplemente causamos una confusión innecesaria para el 98% de los casos de uso. No creo que valga la pena ...
El ecosistema IMO .NET tiene un gran valor al ofrecer una opción única de muchas API (no solo colecciones) con características de rendimiento muy decentes, útiles para la mayoría de los casos de uso. Y ofrece un ecosistema que permite extensiones de alto rendimiento para aquellos que lo necesitan y están dispuestos a profundizar y aprender más / tomar decisiones informadas y compensaciones.
Dicho esto, tener una interfaz IHeap
(como IDictionary
y IReadOnlyDictionary
), puede tener sentido; tengo que pensarlo un poco más / preguntar a los expertos en revisión de API en el espacio ...
Ya (hasta cierto punto) tenemos lo que @pgolebiowski está hablando con ISet<T>
y HashSet<T>
. Yo digo que lo reflejes. Entonces, la API anterior se cambia a una interfaz ( IPriorityQueue<T>
) y luego tenemos una implementación ( HeapPriorityQueue<T>
) que usa internamente un montón que puede o no estar expuesto públicamente como su propia clase.
¿Debería ( PriorityQueue<T>
) implementar también IList<T>
?
@karelz mi problema con ICollection
es SyncRoot
y IsSynchronized
; o están implementados, lo que significa que hay una asignación adicional para el objeto de bloqueo; o tiran, cuando es un poco inútil tenerlos.
@benaadams Esto sería engañoso. Dado que el 99,99% de las implementaciones de colas de prioridad son montones basados en matrices (y como puedo ver, también vamos con una aquí), ¿significaría exponer el acceso a la estructura interna de la matriz?
Digamos que tenemos un montón con elementos 4, 8, 10, 13, 30, 45. Teniendo en cuenta el orden, se accedería a ellos mediante los índices 0, 1, 2, 3, 4, 5. Sin embargo, la estructura interna del montón es [4, 8, 30, 10, 13, 45] (en binario, en cuaternario sería diferente).
i
realmente no tiene sentido desde el punto de vista del usuario, ya que es casi arbitrario.i
, obtener el elemento i
-th, y luego presionarlos una vez más.IList<T>
es normalmente la solución descarada para: quiero ser flexible con las colecciones que acepta mi api, y quiero enumerarlas pero no quiero asignarlas a través de IEnumerable<T>
Me acabo de dar cuenta de que no hay una interfaz genérica para
ICollection<T>
{
int Count
CopyTo(T[], int index)
}
Así que no importa eso 😢 (Aunque es una especie de IReadOnlyCollection)
Pero Reset on Enumerator debería implementarse explícitamente porque es malo y debería lanzar.
Entonces mis cambios sugeridos
public bool TryDequeue(out T item); // Add
public bool TryPeek(out T item); // Add
public struct Enumerator : IEnumerator<T>
{
public T Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext();
void IEnumerator.Reset(); // Explicit
public void Dispose();
}
Ya que está discutiendo los métodos ... Aún no hemos acordado el IHeap
vs IPriorityQueue
thingy - afecta un poco los nombres de los métodos y la lógica. Sin embargo, de cualquier manera, encuentro que a la propuesta de API actual le falta lo siguiente:
Estas operaciones son bastante cruciales, especialmente la posibilidad de actualizar un elemento. Sin esto, muchos algoritmos simplemente no se pueden implementar. Necesitamos introducir un tipo de asa. En la API aquí , el identificador es IHeapNode
. Lo cual es otro argumento para seguir el camino IHeap
porque de lo contrario tendríamos que introducir el tipo PriorityQueueHandle
que siempre sería un nodo de montón ... 😜 Además, lo que significa es vago ... Considerando que, un nodo de montón: todos conocen las cosas y pueden imaginar con qué cosas están lidiando.
En realidad, en breve, para la propuesta de API, por favor, eche un vistazo a este directorio . Probablemente solo necesitemos un subconjunto. Sin embargo, solo contiene lo que necesitamos en mi opinión, por lo que podría valer la pena echarle un vistazo como punto de partida.
¿Qué piensan, chicos?
IHeapNode
no es muy diferente al tipo de clr KeyValuePair ?
Sin embargo, eso es separar la prioridad y el tipo, por lo que ahora es un PriorityQueue<TKey, TValue>
con un IComparer<TKey> comparer
?
KeyValuePair no es solo una estructura, sino que sus propiedades son de solo lectura. Básicamente, equivaldría a crear un nuevo objeto cada vez que se actualice la estructura.
Usar solo una clave no funciona con claves iguales; se necesita más información para saber qué elemento actualizar / eliminar.
Desde IHeapNode
y ArrayHeapNode
:
/// <summary>
/// Represents a heap node. It is a wrapper around a key and a value.
/// </summary>
public interface IHeapNode<out TKey, out TValue>
{
/// <summary>
/// Gets the key for the value.
/// </summary>
TKey Key { get; }
/// <summary>
/// Gets the value contained in the node.
/// </summary>
TValue Value { get; }
}
/// <summary>
/// Represents a node of an <see cref="ArrayHeap{TKey,TValue}"/>.
/// </summary>
public class ArrayHeapNode<TKey, TValue> : IHeapNode<TKey, TValue>
{
/// <inheritdoc cref="IHeapNode{TKey,TValue}.Key"/>
public TKey Key { get; internal set; }
/// <inheritdoc cref="IHeapNode{TKey,TValue}.Value"/>
public TValue Value { get; }
/// <summary>
/// The index of the node within an <see cref="ArrayHeap{TKey,TValue}"/>.
/// </summary>
public int Index { get; internal set; }
/// <summary>
/// Initializes a new instance of the <see cref="ArrayHeapNode{TKey,TValue}"/> class,
/// containing the specified value.
/// </summary>
public ArrayHeapNode(TKey key, TValue value, int index)
{
this.Key = key;
this.Value = value;
this.Index = index;
}
}
Y el método de actualización dentro de ArrayHeap
:
/// <inheritdoc cref="IHeap{TKey,TValue}.Update"/>
public override void Update(ArrayHeapNode<TKey, TValue> node, TKey key)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
var relation = this.Comparer.Compare(key, node.Key);
node.Key = key;
if (relation < 0)
this.MoveUp(node);
else
this.MoveDown(node);
}
Pero ahora cada elemento del montón es un objeto adicional; ¿Qué pasa si quiero un comportamiento PriorityQueue pero no quiero estas asignaciones adicionales?
Entonces no podrá actualizar / eliminar elementos. Sería una implementación simple que no le permitiría implementar ningún algoritmo dependiente de la operación DecreaseKey
(que es muy común). Por ejemplo, el algoritmo de Dijkstra, el algoritmo de Prim. O si está escribiendo algún tipo de programador y algún proceso (o lo que sea) ha cambiado su prioridad, no puede abordar eso.
Además, en todas las demás implementaciones de montón, los elementos son solo nodos, explícitamente. Es perfectamente natural en otros casos, aquí es un poco artificial, pero necesario para actualizar / eliminar.
En Java, la cola de prioridad no tiene la opción de actualizar elementos. Resultado:
Cuando implementé montones para el proyecto AlgoKit y encontré todos estos problemas de diseño, pensé que esta era la razón por la que los autores de .NET decidieron no agregar una estructura de datos tan básica como un montón (o una cola de prioridad). Porque ninguno de los diseños es bueno. Cada uno tiene sus defectos.
En pocas palabras, si queremos agregar una estructura de datos que respalde el ordenamiento eficiente de los elementos por su prioridad, es mejor que lo hagamos de la manera correcta y agreguemos una funcionalidad como actualizar / eliminar elementos de ella.
Si se siente mal por envolver elementos en una matriz con otro objeto, este no es el único problema. Otro se está fusionando. Con montones basados en matrices, esto es totalmente ineficaz. Sin embargo ... si usamos la estructura de datos del montón de emparejamiento ( El montón de emparejamiento: una nueva forma de montón autoajustable ), entonces:
En realidad, podríamos resolver esto de la siguiente manera:
IHeap
interfaz que admita todos los métodosArrayHeap
y PairingHeap
con todos esos identificadores, actualizando, eliminando, fusionandoPriorityQueue
que es solo un envoltorio alrededor de ArrayHeap
, simplificando la APIPriorityQueue
estaría en System.Collections.Generic
y todos los montones en System.Collections.Specialized
.
¿Obras?
Es bastante poco probable que obtengamos tres nuevas estructuras de datos a través de la revisión de API. En la mayoría de los casos, es mejor una cantidad menor de API. Siempre podemos agregar más después si termina siendo insuficiente, pero no podemos eliminar API.
Esa es una de las razones por las que no soy fanático de la clase HeapNode. Imo, ese tipo de cosas debería ser solo interno y la API debería exponer un tipo ya existente si es posible, en este caso probablemente KVP.
@ianhays Si esto se mantiene solo para uso interno, los usuarios no podrían actualizar las prioridades de los elementos en la estructura de datos. Esto sería bastante inútil y terminaríamos con todos los problemas de Java: la gente vuelve a implementar la estructura de datos que ya está en la biblioteca nativa ... Me suena mal. Mucho peor que tener una clase simple que represente un nodo.
Por cierto: una lista vinculada tiene una clase de nodo para que los usuarios puedan usar la funcionalidad adecuada. Esto es prácticamente un reflejo.
los usuarios no podrían actualizar las prioridades de los elementos en la estructura de datos.
Eso no es necesariamente cierto. La prioridad podría exponerse de una manera que no requiera una estructura de datos adicional, de modo que en lugar de
public override void Update(ArrayHeapNode<TKey, TValue> node, TKey key)
{
}
tendrías por ejemplo
public override void Update(TKey item, TValue priority)
{
}
Tampoco estoy convencido todavía de exponer la prioridad como un parámetro de tipo. Imo, la mayoría de las personas utilizarán el tipo de prioridad predeterminado y TValue será una especificidad innecesaria.
void Update(TKey item, TValue priority)
Primero, ¿qué es TKey
y TValue
en su código? ¿Cómo se supone que funciona? La convención en ciencias de la computación es que:
Segundo:
Imo, la mayoría de las personas utilizarán el tipo de prioridad predeterminado y TValue será una especificidad innecesaria.
Defina "el tipo de prioridad predeterminado". Puedo sentir que solo quieres tener PriorityQueue<T>
, ¿verdad? Si ese es el caso, considere el hecho de que un usuario probablemente tendrá que crear una nueva clase, un envoltorio alrededor de su prioridad y valor, además de implementar algo como IComparable
o proporcionar un comparador personalizado. Bastante pobre.
Primero, ¿qué es TKey y TValue en su código?
El artículo y la prioridad del artículo. Puede cambiarlos para que sean la prioridad y el elemento asociado con esa prioridad, pero luego tiene una colección en la que las TKeys no son necesariamente únicas (es decir, permiten prioridades duplicadas). No estoy en contra de eso, pero para mí, TKey generalmente implica unicidad.
El punto es que exponer una clase de nodo no es un requisito para exponer un método de actualización.
Defina "el tipo de prioridad predeterminado". Puedo sentir que solo quieres tener PriorityQueue
, ¿está bien?
Si. Aunque leo este hilo de nuevo, no estoy completamente convencido de que ese sea el caso.
Si ese es el caso, considere el hecho de que un usuario probablemente tendrá que crear una nueva clase, una envoltura alrededor de su prioridad y valor, además de implementar algo como IComparable o proporcionar un comparador personalizado. Bastante pobre.
Tu no estas equivocado. Me imagino que una buena cantidad de usuarios tendrá que crear un tipo de contenedor con una lógica de comparación personalizada. Me imagino que también hay una buena cantidad de usuarios que ya tienen un tipo comparable que quieren poner en la cola de prioridad o un tipo con un comparador definido. La pregunta es qué campo es el más grande de los dos.
Creo que el tipo debería llamarse PriorityQueue<T>
, no Heap<T>
, ArrayHeap<T>
o incluso QuaternaryHeap<T>
para mantener la coherencia con el resto de .Net:
List<T>
, no ArrayList<T>
.Dictionary<K, V>
, no HashTable<K, V>
.ImmutableList<T>
, no ImmutableAVLTreeList<T>
.Array.Sort()
, no Array.IntroSort()
.A los usuarios de estos tipos normalmente no les importa cómo se implementan, ciertamente no debería ser lo más destacado del tipo.
Si ese es el caso, considere el hecho de que un usuario probablemente tendrá que crear una nueva clase, una envoltura alrededor de su prioridad y valor, además de implementar algo como IComparable o proporcionar un comparador personalizado.
Tu no estas equivocado. Me imagino que una buena cantidad de usuarios tendrá que crear un tipo de contenedor con una lógica de comparación personalizada.
En la API de resumen, la comparación la proporciona el comparador proporcionado IComparer<T> comparer
, no se requiere envoltorio. A menudo, la prioridad será parte del tipo, por ejemplo, un programador de tiempo tendrá el tiempo de ejecución como una propiedad del tipo.
El uso de KeyValuePair
con un IComparer
no agrega asignaciones adicionales; aunque agrega una dirección indirecta adicional para una actualización, ya que los valores deben compararse en lugar del elemento.
Las actualizaciones serían problemáticas para los elementos de estructura a menos que el elemento se obtuviera a través de una devolución de referencia y luego se actualizara y devolviera a un método de actualización mediante la referencia y se compararon las referencias. Pero eso es un poco horrible.
@svick
Creo que el tipo debería llamarse
PriorityQueue<T>
, noHeap<T>
,ArrayHeap<T>
o inclusoQuaternaryHeap<T>
para mantener la coherencia con el resto de .Net.
Estoy perdiendo la fe en la humanidad.
PriorityQueue
es coherente con el resto de .NET y llamarla Heap
¿no? ¿Pasa algo con los montones? Lo mismo debería aplicarse a las pilas y colas entonces.ArrayHeap
-> clase base por QuaternaryHeap
. Gran diferencia.List
es una lista, pero no es una lista. 😜¿Quiere ser coherente con el resto de .NET y tener problemas como este?
Y qué veo allí ... Jon Skeet dice que SortedDictionary
debería llamarse SortedTree
ya que refleja la implementación más de cerca.
@pgolebiowski
¿Pasa algo con los montones?
Sí, describe la implementación, no el uso.
Lo mismo debería aplicarse a las pilas y colas entonces.
Ninguno describe realmente la implementación, por ejemplo, el nombre no le dice que están basados en matrices.
ArrayHeap
-> clase base paraQuaternaryHeap
. Gran diferencia.
Sí, entiendo tu diseño. Lo que estoy diciendo es que nada de esto debería exponerse al usuario.
Hay un montón de cosas que vienen como consecuencia de seguir una determinada ruta de diseño. Vuelva a leer el hilo por favor.
Leí el hilo. No creo que el diseño pertenezca a la BCL. Creo que debería contener algo que sea fácil de usar y comprender, no algo que haga que la gente se pregunte qué es el "montón cuaternario" o si deberían utilizarlo.
Si la implementación predeterminada no es lo suficientemente buena para ellos, ahí es donde entran otras bibliotecas (como la suya).
Hashtable, ArrayList. Parece que existen.
Sí, esas son clases de .Net Framework 1.0 que ya nadie usa. Por lo que puedo decir, sus nombres fueron copiados de Java y los diseñadores de .Net Framework 2.0 decidieron no seguir esa convención. En mi opinión, esa fue la decisión correcta.
Por cierto, una "lista" es una mala elección para nombrar IMO, ya que el primer pensamiento de un usuario es que
List
es una lista, pero no es una lista.
Está. No es una lista vinculada, pero no es lo mismo. Y me gusta no tener que escribir ArrayList
o ResizeArray
(nombre de F # para List<T>
) en todas partes.
Se trata de dar nombres significativos que muestren a los usuarios de inmediato con qué están lidiando.
La mayoría de las personas no tendrán idea de lo que están tratando si ven QuaternaryHeap
. Por otro lado, si ven PriorityQueue
, debe quedar claro con qué están lidiando, incluso si no tienen experiencia en informática. No sabrán cuál es la implementación, pero para eso está la documentación.
@ianhays
Primero, ¿qué es TKey y TValue en su código?
El artículo y la prioridad del artículo. Puede cambiarlos para que sean la prioridad y el elemento asociado con esa prioridad, pero luego tiene una colección en la que las TKeys no son necesariamente únicas (es decir, permiten prioridades duplicadas). No estoy en contra de eso, pero para mí, TKey generalmente implica unicidad.
Claves: objetos que se utilizan para priorizar. Las claves no tienen por qué ser únicas. No podemos hacer tal suposición.
El punto es que exponer una clase de nodo no es un requisito para exponer un método de actualización.
Creo que es: soporte de tecla de disminución / aumento de tecla en bibliotecas nativas . También en este tema, por lo que hay clases auxiliares involucradas para tratar con estructuras de datos:
Me imagino que una buena cantidad de usuarios tendrá que crear un tipo de contenedor con una lógica de comparación personalizada. Me imagino que también hay una buena cantidad de usuarios que ya tienen un tipo comparable que quieren poner en la cola de prioridad o un tipo con un comparador definido. La pregunta es qué campo es el más grande de los dos.
No sé sobre los demás, pero creo que crear un contenedor con una lógica de comparación personalizada cada vez que quiero usar una cola de prioridad es bastante horrible ...
Otro pensamiento, digamos que creé un contenedor:
class Wrapper
{
public int Priority { get; set; }
public string SomeStuff ...
}
Yo alimento PriorityQueue<T>
con este tipo. Por lo que prioriza sus elementos basándose en él. Digamos que ha definido algún selector de teclas. Tal diseño puede bloquear el trabajo interno de un montón. Puede cambiar las prioridades en cualquier momento ya que es el propietario del elemento. No hay ningún mecanismo de notificación para que el montón se actualice. Serías responsable de manejar todo eso y llamarlo. Bastante peligroso y no sencillo para mí.
En el caso del identificador que propuse, el tipo era inmutable desde el punto de vista del usuario. Y no hubo problemas con la singularidad.
En mi opinión, me gusta la idea de tener una interfaz IHeap
y luego la API de clase que implementa esa interfaz. Estoy con @ianhays 'en esto, no vamos a exponer 3 nuevas apis para proporcionar diferentes tipos de montones a los clientes, ya que sería necesario un proceso de revisión de API y nos gustaría centrarnos en PriorityQueue
no importa cuál sea el nombre.
En segundo lugar, creo que no es necesario tener una clase interna / pública de Nodo para almacenar los valores en la cola. Sin necesidad de asignaciones adicionales, podríamos seguir algo parecido a lo que hace IDictionary y al generar los valores para el Enumerable creando KeyValuePairPriorityQueue<T>
(este nombre es solo para darle uno). Con este enfoque, podríamos tener un constructor siguiendo lo que hace todo el BCL con un IComparer<T>
y haciendo la comparación con ese comprador para dar la prioridad. No es necesario exponer las API que pasan una prioridad como parámetro.
Creo que si algunas de las API reciben una prioridad lo haría "menos utilizable" o más complicado para los clientes ordinales que quisieran ser la prioridad predeterminada, entonces estamos haciendo que sea más complicado entender el uso de ellas, al darles la La opción de tener un IComparer<T>
será la más razonable y seguirá las pautas que tenemos en BCL.
Los nombres son lo que hacen, no cómo lo hacen.
Reciben el nombre de los conceptos abstractos y lo que logran para el usuario, en lugar de su implementación y cómo lo logran. (Lo que también significa que su implementación se puede mejorar para usar una implementación diferente si eso resulta mejor)
Lo mismo debería aplicarse a las pilas y colas entonces.
Stack
es una matriz de cambio de tamaño ilimitada y Queue
se implementa como un búfer circular de cambio de tamaño ilimitado. Reciben el nombre de sus conceptos abstractos. Pila (tipo de datos abstractos) : último en entrar, primero en salir (LIFO), cola (tipo de datos abstractos) : primero en entrar, primero en salir (FIFO)
Hashtable, ArrayList. Parece que existen.
Entrada de diccionario
Sí, pero finjamos que no lo hacen; son artefactos de una época menos civilizada. No tienen seguridad de tipo y caja si usa primitivas o estructuras; así que asigne en cada Add.
Puedes usar el Analizador de compatibilidad de plataformas de @terrajobst y te dirá: "Por favor, no lo hagas".
La lista es una lista, pero no es una lista.
Realmente es una Lista (tipo de datos abstracto) también conocida como Secuencia.
Equally Priority Queue es un tipo abstracto que le dice lo que logra en uso; no cómo lo hace en la implementación (ya que puede haber muchas implementaciones diferentes)
¡Mucha discusión!
La especificación original se diseñó teniendo en cuenta algunos principios básicos:
@karelz @pgolebiowski
Cambiar el nombre a "Heap" u otro término alineado con una implementación de estructura de datos no coincidiría con la mayoría de las convenciones de BCL para colecciones. Históricamente, las clases de recopilación de .NET se han diseñado para ser de propósito general en lugar de centrarse en la estructura / patrón de datos específicos. Mi pensamiento original fue que al principio del ecosistema .NET, los diseñadores de API se movieron intencionalmente de "ArrayList" a "List
Si optamos por Heap, podría ocurrir lo mismo: muchos desarrolladores con habilidades intermedias verán (lamentablemente) "Heap" y lo confundirán con el montón de memoria de la aplicación (es decir, el montón y la pila) en lugar de "acumular la estructura de datos generalizada". La prevalencia de System.Collections.Generic hará que aparezca en casi todas las propuestas inteligentes de los desarrolladores de .NET, y se preguntarán por qué pueden asignar un nuevo montón de memoria :)
PriorityQueue, en comparación, es mucho más detectable y menos propenso a la confusión. Puede escribir "Cola" y obtener propuestas para PriorityQueue.
Algunas propuestas y preguntas sobre prioridades enteras o un parámetro genérico de prioridad (TKey, TPriority, etc.). Agregar una prioridad explícita requeriría que los consumidores escribieran su propia lógica para mapear sus prioridades y aumentar la complejidad de la API. Usando el IComparer integrado
Si se requiriera que las entradas sean únicas, Enqueue () requeriría una búsqueda de unicidad para lanzar una ArgumentException. Además, es probable que existan escenarios válidos para permitir que un elemento se ponga en cola más de una vez. Este diseño de no exclusividad hace que proporcionar una operación Update () sea un desafío, ya que no habría forma de saber qué objeto se está actualizando. Como indicaron algunos comentarios, esto comenzaría a ingresar a las API que devuelven referencias de "nodo" que a su vez (probablemente) requerirían asignaciones que deberían ser recolectadas como basura. Incluso si eso se solucionara, aumentaría el consumo de memoria por elemento de la cola de prioridad.
En un momento, tenía una interfaz IPriorityQueue personalizada en la API antes de publicar la especificación. Al final, decidí no hacerlo: el patrón de uso al que aspiraba era Enqueue, Dequeue e Iterate. Ya cubierto por el conjunto de interfaces existente. Pensando en esto como una cola que se ordena internamente; siempre que los elementos mantengan su propia posición (inicial) en la cola según su IComparer
Les debo a todos algunos ejemplos de códigos de clientes: mi plan original era pasar por las implementaciones de BCL existentes de PriorityQueue para usar como base para los ejemplos.
En la API de resumen, la comparación la proporciona el comparador IComparer proporcionado
comparador, no se requiere envoltorio. A menudo, la prioridad será parte del tipo, por ejemplo, un programador de tiempo tendrá el tiempo de ejecución como una propiedad del tipo.
Según la API OP propuesta, se debe cumplir uno de estos para usar la clase:
La API de tipo dual tiene como objetivo reducir la carga de las dos segundas opciones, ¿no? ¿Cómo funciona la construcción de un nuevo PriorityQueue / Heap cuando tienes un tipo que ya contiene la prioridad ? ¿Qué aspecto tiene la API?
Creo que es: soporte de tecla de disminución / aumento de tecla en bibliotecas nativas.
public override void Update(ArrayHeapNode<TKey, TValue> node, TKey key) {}
public override void Update(TKey key, TValue value);
En cualquier caso, se actualiza el elemento asociado con la prioridad dada. ¿Por qué se requiere ArrayHeapNode para la actualización? ¿Qué logra que no se pueda lograr tomando el TKey / TValue directamente?
@ianhays
En cualquier caso, se actualiza el elemento asociado con la prioridad dada. ¿Por qué se requiere ArrayHeapNode para la actualización? ¿Qué logra que no se pueda lograr tomando el TKey / TValue directamente?
Al menos con el montón binario (con el que estoy más familiarizado), si desea actualizar la prioridad de algún valor y conoce su posición en el montón, puede hacerlo rápidamente (en tiempo O (log n)).
Pero si solo conoce el valor y la prioridad, primero deberá encontrar el valor, que es lento (O (n)).
Por otro lado, si no necesita actualizar de manera eficiente, esa asignación por elemento en el montón es pura sobrecarga.
Me gustaría ver una solución en la que pague esos gastos generales solo cuando la necesite, pero es posible que no sea posible implementarla y la API resultante podría no verse bien.
La API de tipo dual tiene como objetivo reducir la carga de las dos segundas opciones, ¿no? ¿Cómo funciona la construcción de un nuevo PriorityQueue / Heap cuando tienes un tipo que ya contiene la prioridad? ¿Qué aspecto tiene la API?
Ese es un buen punto, hay una brecha en la API original para escenarios donde el consumidor ya tiene un valor de prioridad (es decir, un número entero) mantenido por separado del valor. La otra cara es que un
SortedList
Para mí, el mismo principio se aplica a las colas. Históricamente, una cola es una estructura de datos unidimensional.
Es cierto que mi conocimiento moderno de la biblioteca estándar de C ++ está un poco oxidado, pero incluso std :: priority_queue parece tener push, pop y take un comparador como parámetro de plantilla (genérico). El equipo de la biblioteca estándar de C ++ es tan sensible al rendimiento como puede ser :)
Hice un escaneo muy rápido de las implementaciones de cola de prioridad y montón en algunos lenguajes de programación en este momento: C ++, Java, Rust y Go, todos funcionan con un solo tipo (similar a la API original publicada aquí). Un vistazo rápido a las implementaciones de cola de prioridad / montón más populares en NPM muestra lo mismo.
@pgolebiowski , no me malinterpretes, debería haber implementaciones específicas de cosas nombradas explícitamente después de su implementación específica.
Sin embargo, eso es para cuando sepa qué estructura de datos específica desea que coincida con los objetivos de rendimiento que busca y tenga las compensaciones que está dispuesto a aceptar.
Generalmente las colecciones de framework cubren el 90% de usos donde se desea un comportamiento general. Entonces, si desea un comportamiento o implementación muy específico, probablemente elija una biblioteca de terceros; y con suerte recibirán el nombre de la implementación para que sepa que se ajusta a sus necesidades.
Simplemente no quiero vincular los tipos de comportamiento general a una implementación específica; ya que entonces es extraño si la implementación cambia porque el nombre del tipo debe permanecer igual y no coincidirán.
Hay muchas preocupaciones que necesitan discusión, pero comencemos con la que actualmente tiene el mayor impacto en las partes en las que no estamos de acuerdo: actualizar y eliminar elementos .
Entonces, ¿cómo quiere apoyar estas operaciones? Tenemos que incluirlos, son básicos. En Java, los diseñadores los han omitido y, como resultado:
Esto es patético. ¿Hay alguien que realmente quiera seguir ese camino? Me avergonzaría publicar una estructura de datos tan deshabilitada.
@pgolebiowski puede estar seguro de que todos aquí tienen las mejores intenciones para la plataforma. Nadie quiere enviar API rotas. Queremos aprender de los errores de los demás, así que siga trayendo información relevante y útil (como la de la historia de Java).
Sin embargo, quiero señalar un par de cosas:
Solo les pregunté cómo quieren diseñar la actualización / eliminación si no les gusta el enfoque propuesto ... Creo que esto es escuchar a los demás y buscar consenso.
¡No dudo de tus buenas intenciones! A veces importa cómo preguntas, afecta la forma en que las personas perciben el texto del otro lado. El texto está libre de emociones, por lo que las cosas se pueden entender de manera diferente cuando se escriben. El inglés como segundo idioma enturbia aún más las cosas y todos debemos ser conscientes de ello. Estaré encantado de charlar sobre los detalles fuera de línea si está interesado ... dirijamos la discusión aquí de nuevo a la discusión técnica ...
Mis dos centavos en el debate de Heap vs. PriorityQueue: ambos enfoques son válidos y claramente tienen beneficios e inconvenientes.
Dicho esto, "PriorityQueue" parece mucho más consistente con el enfoque de .NET existente. Hoy las colecciones principales son List
Se sentiría extraño que una colección se opusiera a la tendencia aquí.
Creo que PriorityQueue con constructor adicional: PriorityQueue
En ese caso PrioriryQueue
clase ArrayHeap: IHeap {}
clase PairingHeap: IHeap {}
clase FibonacciHeap: IHeap {}
clase BinomialHeap: IHeap {}
está bien. Hay muchas voces diferentes. Repasé la discusión nuevamente y refiné mi enfoque. También abordo preocupaciones comunes. El texto a continuación hace uso de citas de las publicaciones anteriores.
El objetivo final de toda esta discusión es proporcionar un conjunto específico de funcionalidades para hacer feliz al usuario. Es un caso bastante común que un usuario tenga un montón de elementos, donde algunos de ellos tienen una prioridad más alta que otros. Eventualmente, quieren mantener este grupo de elementos en algún orden específico para poder realizar de manera eficiente las siguientes operaciones:
Los autores de otras bibliotecas estándar también han intentado admitir esta funcionalidad. En esta sección, me referiré a cómo se resolvió en Python , Java , C ++ , Go , Swift y Rust .
Ninguno de ellos admite la modificación de elementos ya insertados en la colección. Es sumamente sorprendente, porque es muy probable que, durante la vida de una colección, las prioridades de sus elementos cambien. Especialmente, si dicha colección está destinada a utilizarse durante toda la vida útil de un servicio. También hay una serie de algoritmos que utilizan esta misma funcionalidad de actualización de elementos. Por lo tanto, tal diseño es simplemente incorrecto, porque como resultado, nuestros clientes se pierden: StackOverflow . Esta es una de muchas, muchas preguntas como esa en Internet. Los diseñadores han fracasado.
Otra cosa que vale la pena señalar es que cada biblioteca proporciona esta funcionalidad parcial mediante la implementación de un montón binario . Bueno, se ha demostrado que no es la implementación más eficaz en el caso general (entrada aleatoria). El mejor ajuste para el caso general es un montón cuaternario (un árbol 4-ario completo ordenado por montón implícito, almacenado como una matriz). Es significativamente menos conocido y esta es probablemente la razón por la que los diseñadores optaron por un montón binario. Pero aún así, otra mala elección, aunque de menor gravedad.
¿Qué aprendemos de eso?
Creo firmemente que deberíamos proporcionar:
IHeap<T>
interfazHeap<T>
claseLa interfaz IHeap
obviamente incluiría métodos para lograr todas las operaciones descritas al principio de esta publicación. La clase Heap
, implementada con un montón cuaternario, sería la solución de referencia en el 98% de los casos. El orden de los elementos se basaría en IComparer<T>
pasados al constructor o en el orden predeterminado si un tipo ya es comparable.
IHeap
como entrada. Algunos ejemplos de otros montones que se comportan mejor que el montón cuaternario en algunas condiciones específicas son: montón de emparejamiento , montón binomial y nuestro querido montón binario.Heap
. Esto se está optimizando hacia el 98% de los casos de uso.ISet
y HashSet
IList
y List
IDictionary
y Dictionary
Heap
es de hecho inequívoco y claro. También está en armonía con Stack
, Queue
, List
y Array
. El mismo enfoque con la denominación se adoptó en las bibliotecas estándar más modernas (Go, Swift, Rust): exponen un montón de forma explícita.@pgolebiowski No Heap<T>
/ IHeap<T>
se nombra como Stack<T>
, Queue<T>
y / o List<T>
? Ninguno de esos nombres explica cómo se implementan internamente (una matriz de T como sucede).
@SamuelEnglard
Heap
tampoco dice cómo se implementa internamente. No entiendo por qué para tanta gente un montón sigue inmediatamente a una implementación específica. Hay muchas variantes de montones que comparten la misma API, para empezar:
Decir que se trata de un montón sigue siendo muy abstracto. En realidad, incluso decir que estamos tratando con un montón cuaternario es abstracto: podría implementarse como una estructura de datos implícita basada en una matriz de T
(como nuestro Stack<T>
, Queue<T>
y List<T>
) o explícito (usando nodos y punteros).
En pocas palabras, Heap<T>
es muy similar a Stack<T>
, Queue<T>
y List<T>
, porque es una estructura de datos elemental, un bloque de construcción abstracto básico, que se puede implementar de muchas formas. Además, resulta que todos se implementan usando una matriz de T
debajo. Encuentro esta similitud realmente fuerte.
¿Tiene sentido?
Para que conste, me es indiferente el nombre. Las personas que están acostumbradas a usar la biblioteca estándar de C ++ quizás prefieran _priority_queue_. Las personas que han sido educadas sobre estructuras de datos pueden preferir _Heap_. Si tuviera que votar, elegiría _heap_, aunque para mí está cerca de tirar una moneda.
@pgolebiowski Formulé mal mi pregunta, eso es mi mal. Sí, Heap<T>
no dice cómo se implementa internamente.
Sí, Heap es una estructura de datos válida, pero Heap! = Priority Queue. Ambos exponen diferentes superficies API y se utilizan para diferentes ideas. Heap<T>
/ IHeap<T>
deben ser tipos de datos utilizados internamente por (solo nombres teóricos) PriorityQueue<T>
/ IPriorityQueue<T>
.
@SamuelEnglard
En cuanto a cómo está organizado el mundo de la informática, sí. Estos son los niveles de abstracción:
Y sí, teniendo IHeap
y Heap
, la implementación de PriorityQueue
sería básicamente:
public class PriorityQueue<T>
{
private readonly IHeap<T> heap;
public PriorityQueue(IHeap<T> heap)
{
this.heap = heap;
}
public void Add<T>(T item) => this.heap.Add(item);
public void Remove<T>(T item) => this.heap.Remove(item);
// etc...
}
Ejecutemos un árbol de decisiones aquí.
Una cola de prioridad también podría implementarse con una estructura de datos diferente a alguna forma de montón (teóricamente). Eso hace que el diseño de un PriorityQueue
como el de arriba sea bastante feo, porque está arreglado solo para la familia de montones. También es un envoltorio muy delgado alrededor de IHeap
. Esto genera una pregunta: ¿por qué no usar simplemente la familia de montones en su lugar?
Nos queda una solución: arreglar la cola de prioridad para una implementación específica de un montón cuaternario, sin espacio para la interfaz IHeap
. Siento que está pasando por demasiados niveles de abstracción y mata todos los maravillosos beneficios de tener una interfaz.
Volvemos con la elección de diseño de la mitad de la discusión: tener PriorityQueue
y IPriorityQueue
. Pero entonces básicamente tendríamos:
class BinaryHeap : IPriorityQueue {}
class PairingHeap : IPriorityQueue {}
class FibonacciHeap : IPriorityQueue {}
class BinomialHeap : IPriorityQueue {}
No solo se siente feo, sino también conceptualmente incorrecto: estos no son tipos de colas de prioridad y no comparten la misma API (como ya señaló @SamuelEnglard ). Creo que deberíamos ceñirnos a los montones, una familia lo suficientemente grande como para tener una abstracción para ellos. Obtendríamos:
class BinaryHeap : IHeap {}
class PairingHeap : IHeap {}
class FibonacciHeap : IHeap {}
class BinomialHeap : IHeap {}
Y, proporcionado por nosotros, class Heap : IHeap {}
.
Por cierto, alguien podría encontrar útil lo siguiente:
| Consulta de Google | Resultados |
| : --------------------------------------: | : -----: |
| "estructura de datos" "cola de prioridad" | 172,000 |
| "estructura de datos" "montón" | 430,000 |
| "estructura de datos" "cola" - "cola de prioridad" | 496,000 |
| "estructura de datos" "cola" | 530,000 |
| "estructura de datos" "pila" | 577,000 |
@pgolebiowski Me siento yendo y viniendo aquí, así que concederé
@karelz @safern ¿Qué IHeap
+ Heap
para que pueda presentar una propuesta de API específica?
Cuestiono la necesidad de una interfaz aquí (ya sea IHeap
o IPriorityQueue
). Sí, hay muchos algoritmos diferentes que teóricamente se pueden usar para implementar esta estructura de datos. Sin embargo, parece poco probable que el marco se envíe con más de uno, y la idea de que los escritores de bibliotecas realmente necesiten una interfaz canónica para que varias partes puedan coordinar la escritura de implementaciones de montón compatibles entre sí no parece tan probable.
Además, una vez que se lanza una interfaz, nunca se puede cambiar debido a compatibilidad. Eso significa que la interfaz tiende a quedarse atrás de las clases concretas en términos de funcionalidad (un problema con IList e IDictionary en la actualidad). Por el contrario, si se lanzara una clase Heap y hubiera un gran clamor por una interfaz IHeap, creo que la interfaz podría agregarse en el futuro sin problemas.
@madelson Estoy de acuerdo en que el marco probablemente no necesite enviar más de una implementación única de un montón. Sin embargo, al respaldar esa implementación mediante una interfaz, aquellos de nosotros que nos preocupan por otras implementaciones podemos intercambiar fácilmente en otra implementación (de nuestra propia creación o en una biblioteca externa) y seguir siendo compatibles con el código que acepta la interfaz.
Realmente no veo ningún punto negativo en la codificación de una interfaz. Aquellos a quienes no les importa pueden ignorarlo e ir directamente a la implementación concreta. Aquellos que se preocupan pueden usar cualquier implementación de su elección. Es una eleccion. Y es una elección que quiero.
@pgolebiowski ahora que preguntaste, aquí está mi opinión personal (estoy algo seguro de que otros revisores / arquitectos de API la compartirán, ya que he verificado la opinión con algunos):
El nombre debería ser PriorityQueue
, y no deberíamos introducir la interfaz IHeap
. Debería haber exactamente una implementación (probablemente a través de algún montón).
IHeap
interfaz
Si la biblioteca y la interfaz IHeap
vuelven realmente populares, podemos cambiar de opinión más tarde y agregar IHeap
según la demanda (a través de la sobrecarga de su constructor), pero no creo que sea útil / alineado con el resto de BCL lo suficiente como para justificar la complicación de agregar una nueva interfaz ahora. Empiece de forma simple, crezca hasta convertirse en complicado sólo si es realmente necesario.
... solo mis 2 centavos (personales)
Dada la diferencia de opiniones, propongo el siguiente enfoque para hacer avanzar la propuesta (que sugerí a principios de esta semana a @ianhays y, por lo tanto, indirectamente a @safern):
... tratando de ser completamente transparente sobre mi opinión (que usted pidió), por favor no lo tome como un desánimo ni lo rechace, veamos cómo va la propuesta de API.
@karelz
El nombre debe ser
PriorityQueue
¿Algún argumento? Sería al menos bueno si abordara lo que escribí anteriormente en lugar de simplemente decir que no .
Creo que lo nombró bastante bien anteriormente: _Si tiene una entrada, proporciónela, respaldela con datos y evidencia. También escuche los argumentos de los demás, reconózcalos. Proporcione evidencia en contra de la opinión de otros si no está de acuerdo.
Además, no se trata solo del nombre . No es tan superficial. Por favor lea lo que escribí. Se trata de trabajar entre niveles de abstracción y poder mejorar el código / construir sobre él.
Oh, hubo un argumento en esta discusión por qué:
Si optamos por Heap, muchos desarrolladores con habilidades intermedias verán (tristemente) "Heap" y lo confundirán con el montón de memoria de la aplicación (es decir, montón y pila) en lugar de "montón de la estructura de datos generalizada". [...] se preguntarán por qué pueden asignar un nuevo montón de memoria.
😛
no deberíamos introducir la interfaz
IHeap
.IHeap
interfaz
@bendono escribió un comentario muy agradable sobre este. @safern también quería respaldar la implementación con una interfaz. Y algunos otros.
Otra nota: no estoy seguro de cómo se imagina mover una interfaz a una biblioteca de terceros. ¿Cómo podrían los desarrolladores escribir su código en esa interfaz y usar nuestra funcionalidad? Se verían obligados a ceñirse a nuestra funcionalidad sin interfaz o ignorarla por completo, no hay otra opción, es mutuamente excluyente. En otras palabras, nuestra solución no sería extensible en absoluto, lo que resultaría en que las personas usaran nuestra solución deshabilitada o alguna biblioteca de terceros, en lugar de depender del mismo núcleo arquitectónico en ambos casos . Esto es lo que tenemos en la biblioteca estándar de Java y en las soluciones de terceros.
Pero, de nuevo, comentaste muy bien esto: nadie quiere enviar API rotas.
Dada la diferencia de opiniones, propongo seguir el enfoque para hacer avanzar la propuesta. [...] Hagamos 2 propuestas alternativas [...] llevémoslas a revisión de API, discutamos allí y tomemos una decisión allí. Si veo al menos una persona en ese grupo votando por la propuesta de Heaps, estaré feliz de reconsiderar mi punto de vista.
Hubo mucha discusión sobre varias partes de la API anterior. Abordó:
PriorityQueue
frente a Heap
¿Por qué esas personas que tiene en mente no pueden simplemente contribuir a esta discusión? ¿Por qué deberíamos empezar de nuevo?
Si optamos por Heap, muchos desarrolladores con habilidades intermedias verán (tristemente) "Heap" y lo confundirán con el montón de memoria de la aplicación (es decir, montón y pila) en lugar de "montón de la estructura de datos generalizada".
Si, este soy yo. Soy completamente autodidacta en programación, por lo que no tengo idea de lo que se dijo cuando " Heap
" entró en la discusión. Sin mencionar que incluso en términos de una colección, "un montón de cosas", para mí más intuitivamente implicaría que está desordenada en todos los sentidos.
No puedo creer que esto esté sucediendo realmente ...
¿Algún argumento? Sería al menos bueno si abordara lo que escribí anteriormente en lugar de simplemente decir que no.
Si lee mi respuesta, puede notar que mencioné argumentos clave para mi posición:
Los mismos argumentos que se han repetido en el hilo varias veces en mi opinión. Solo los resumí
. Por favor lea lo que escribí.
Estuve monitoreando activamente este hilo todo el tiempo. Leo todos los argumentos y puntos. Esta es mi opinión resumida, a pesar de leer (y comprender) todos sus puntos y los de los demás.
Aparentemente eres un apasionado del tema. Eso es grandioso. Sin embargo, tengo la sensación de que llegamos a una posición en la que solo estamos repitiendo argumentos similares de cada lado, lo que no va a llevar mucho más lejos; por eso recomendé 2 propuestas y obtuve más comentarios sobre ellas de un grupo más grande de experiencia. Revisores de API de BCL (Por cierto: todavía no me considero un revisor de API con experiencia).
¿Cómo podrían los desarrolladores escribir su código en esa interfaz y usar nuestra funcionalidad?
Los desarrolladores que se preocupan por el escenario avanzado IHeap
harían referencia a la interfaz y la implementación de la biblioteca de terceros. Como dije, si resulta ser popular, podríamos considerar moverlo a CoreFX más adelante.
La buena noticia es que agregar IHeap
más tarde es totalmente factible; básicamente agrega solo una sobrecarga de constructor en PriorityQueue
.
Sí, no es lo ideal desde tu punto de vista, pero no impide la innovación que consideras importante en el futuro. Creo que es un término medio razonable.
¿Por qué esas personas que tiene en mente no pueden simplemente contribuir a esta discusión?
La revisión de API es una reunión con discusión activa, lluvia de ideas y ponderación de todos los ángulos. En muchos casos, es más productivo / eficiente que ir y venir en los problemas de GitHub. Ver dotnet / corefx # 14354 y su predecesor dotnet / corefx # 8034 - discusión demasiado larga, varias opiniones diferentes con miles de ajustes que son difíciles de rastrear, sin conclusión, mientras que una gran discusión, también una pérdida de tiempo no trivial para bastantes personas, hasta que nos sentamos y hablamos de ello y llegamos a un consenso.
Hacer que los revisores de la API supervisen cada uno de los problemas de la API, o incluso los más parlanchines, no se adapta bien.
¿Por qué deberíamos empezar de nuevo?
No comenzaremos de nuevo. ¿Por qué piensas eso?
Terminaremos la revisión de la API en el primer nivel (el nivel de propietario del área) enviando las 2 propuestas más populares con ventajas y desventajas al siguiente nivel de revisión de la API.
Es un enfoque jerárquico de aprobaciones / revisiones. Es similar a las revisiones comerciales: los vicepresidentes / directores ejecutivos con poder de decisión no supervisan todas las discusiones sobre cada proyecto en su empresa, piden a sus equipos / informes que presenten propuestas para las decisiones más impactantes o contagiosas para tener una discusión más profunda sobre ellos. Los equipos / informes deben resumir el problema y presentar los pros / contras de soluciones alternativas.
Si cree que no estamos listos para presentar las 2 propuestas finales con pros y contras, porque hay cosas que aún no se han dicho en este hilo, sigamos discutiendo, hasta que tengamos solo algunos de los principales candidatos para revisar en el próximo. Nivel de revisión de API.
Tengo la sensación de que se ha dicho todo lo que había que decir.
¿Tiene sentido?
El nombre debe ser
PriorityQueue
¿Algún argumento?
Si lee mi respuesta, puede notar que mencioné argumentos clave para mi posición.
Caray ... Me estaba refiriendo a lo que cité ( obviamente ): tú decides ir con una cola de prioridad en lugar de un montón. Y sí, he leído su respuesta, contiene exactamente el 0% de argumentos para esto.
Estuve monitoreando activamente este hilo todo el tiempo. Leo todos los argumentos y puntos. Esta es mi opinión resumida, a pesar de leer (y comprender) todos sus puntos y los de los demás.
Te gusta ser hiperbólico, lo he notado antes. Simplemente reconoce la existencia de puntos.
¿Cómo podrían los desarrolladores escribir su código en esa interfaz y usar nuestra funcionalidad?
Los desarrolladores que se preocupan por el escenario avanzado de IHeap harían referencia a la interfaz y la implementación de la biblioteca de terceros. Como dije, si resulta ser popular, podríamos considerar moverlo a CoreFX más adelante.
¿Es consciente de que está repitiendo mis palabras aquí y no aborda en absoluto el tema que he presentado como consecuencia de lo anterior? Escribí:
Se verían obligados a ceñirse a nuestra funcionalidad sin interfaz o ignorarla por completo, no hay otra opción, es mutuamente excluyente.
Te lo dejaré muy claro. Habría dos grupos de personas
El problema aquí es, como dije, que se trata de grupos de personas inconexos . Y están produciendo código que no funciona en conjunto , porque no existe un núcleo arquitectónico común. _ CODEBASE NO ES COMPATIBLE _. No puede deshacerlo más tarde.
La buena noticia es que agregar IHeap más tarde es totalmente factible; básicamente agrega solo una sobrecarga de constructor en PriorityQueue.
Ya he escrito por qué apesta: mira esta publicación .
¿Por qué esas personas que tiene en mente no pueden simplemente contribuir a esta discusión?
Hacer que los revisores de la API supervisen cada uno de los problemas de la API, o incluso los más parlanchines, no se adapta bien.
Sí, estaba preguntando por qué los revisores de API no pueden monitorear todos y cada uno de los problemas de API. Precisamente, respondió en consecuencia.
¿Tiene sentido?
No. Estoy cansado de esta discusión, de verdad. Algunos de ustedes claramente están participando en él simplemente porque es su trabajo y se les dice que lo hagan. Algunos de ustedes necesitan orientación todo el tiempo, lo cual es muy tedioso. Incluso me pediste que probara por qué debería implementarse una cola de prioridad con un montón internamente, claramente sin experiencia en informática. Algunos de ustedes ni siquiera comprenden qué es realmente un montón, lo que hace que la discusión sea aún más caótica.
Vaya con su PriorityQueue deshabilitada que no permite actualizar y eliminar elementos. Vaya con su diseño que no permita un enfoque OO saludable. Vaya con su solución que no permite reutilizar la biblioteca estándar al escribir una extensión. Sigue el camino de Java.
Y esto ... esto es simplemente alucinante:
Si optamos por Heap, muchos desarrolladores con habilidades intermedias verán (tristemente) "Heap" y lo confundirán con el montón de memoria de la aplicación (es decir, montón y pila) en lugar de "montón de la estructura de datos generalizada".
Presente la API con su enfoque. Soy curioso.
No puedo creer que esto esté sucediendo realmente ...
Bueno, excuuuuuuuuuuuuuuuseme por no haber tenido la oportunidad de obtener una educación adecuada en Ciencias de la Computación para aprender que Heap
es algún tipo de estructura de datos diferente del montón de memoria.
Sin embargo, el punto sigue en pie. Un montón de algo no implica Heap
. PriorityQueue
por otro lado, comunica perfectamente que hace exactamente eso.
¿Como implementación de respaldo? Claro, los detalles de implementación no deberían ser mi preocupación.
¿Alguna IHeap
abstracción? Gran para los autores de la API y las personas que tienen un mayor CS saber para qué se usa para, no hay razón para no tener.
¿Darle a algo un nombre críptico que no declare su intención tan bien y, posteriormente, limite su descubrimiento? 👎
Bueno, excuuuuuuuuuuuuuuuseme por no haber tenido la oportunidad de obtener una educación adecuada en Ciencias de la Computación para aprender que Heap es algún tipo de estructura de datos diferente del montón de memoria.
Esto es ridículo. Al mismo tiempo, desea participar en una discusión sobre cómo agregar una funcionalidad como esa. Suena como trolling.
Un montón de algo no implica nada acerca de que esté ordenado de alguna manera.
Está usted equivocado. Está ordenado, como un montón. Como en las imágenes que vinculó.
¿Como implementación de respaldo? Claro, los detalles de implementación no deberían ser mi preocupación.
Ya me he referido a eso. La familia de montones es enorme y hay dos niveles de abstracción por encima de una implementación. La cola de prioridad es la tercera capa de abstracción.
Si necesitara una colección que me permita almacenar objetos de cosas para procesar, donde algunas instancias que llegan más tarde pueden necesitar ser procesadas antes, no estaría buscando algo llamado Heap. PriorityQueue, por otro lado, comunica perfectamente que hace exactamente eso.
Y sin ningún tipo de antecedentes, ¿le pediría a Google que le proporcione artículos sobre colas de prioridad? Bueno, podemos discutir lo que es más o menos probable en nuestras opiniones. Pero, como muy bien se ha dicho:
Si tiene alguna entrada, proporciónela, respaldela con datos y evidencia. Proporcione evidencia en contra de la opinión de otros si no está de acuerdo.
Y según los datos te equivocas:
Consulta | Golpes
: ----: |: ----: |
| "estructura de datos" "cola de prioridad" | 172,000 |
| "estructura de datos" "montón" | 430,000 |
Es casi 3 veces más probable que se encuentre con un montón mientras lee sobre estructuras de datos. Además, es un nombre con el que los desarrolladores de Swift, Go, Rust y Python están familiarizados, porque sus bibliotecas estándar proporcionan dicha estructura de datos.
Consulta | Golpes
: ----: |: ----: |
| "golang" "cola de prioridad" | 3,390 |
| "óxido" "cola de prioridad" | 8,630 |
| "rápida" "cola de prioridad" | 18,600 |
| "python" "cola de prioridad" | 72,800 |
| "golang" "montón" | 79.000 |
| "óxido" "montón" | 492.000 |
| "rápido" "montón" | 551.000 |
| "python" "montón" | 555.000 |
En realidad, también es similar para C ++, porque se introdujo una estructura de datos de pila en algún momento del siglo anterior.
¿Darle a algo un nombre críptico que no declare su intención tan bien y, posteriormente, limite su descubrimiento? 👎
Sin opiniones. Datos. Véase más arriba. Especialmente sin opiniones de alguien que no tenga antecedentes. Tampoco buscarías en Google una cola de prioridad sin haber leído antes sobre estructuras de datos. Y un montón está cubierto en muchas estructuras de datos 101 .
Es la base de la informática. Es elemental. Cuando tienes varios semestres de algoritmos y estructuras de datos, un montón es algo que ves al principio.
Pero aún:
EDITAR: Ver Tendencias de Google .
Como otro desarrollador autodidacta, no tengo ningún problema con _heap_. Como desarrollador que siempre se esfuerza por mejorar, me he tomado el tiempo de aprender y comprender todo sobre las estructuras de datos. En resumen, no estoy de acuerdo con la implicación de que una convención de nomenclatura debería apuntar a aquellos que no se han tomado el tiempo para comprender el léxico del campo del que forman parte.
Y también estoy totalmente en desacuerdo con afirmaciones como "El nombre debería ser PriorityQueue". Si no desea la opinión de la gente, no la haga de código abierto y no la pida.
Permítanme proporcionar una explicación de cómo pensamos sobre la denominación de API:
Tendemos a favorecer la coherencia dentro de la plataforma .NET por encima de casi cualquier otra cosa. Eso es importante para que las API parezcan familiares y predecibles. A veces, esto significa que aceptamos que un nombre no es 100% correcto si ese es un término que hemos usado antes.
Nuestro objetivo es diseñar una plataforma que sea accesible para una amplia variedad de desarrolladores, algunos de los cuales no han tenido una educación formal en informática. Creemos que parte de la razón por la que .NET se ha percibido generalmente como muy productivo y fácil de usar se debe en parte a ese punto de diseño.
Generalmente empleamos la "prueba del motor de búsqueda" cuando se trata de verificar qué tan conocido y establecido es un nombre o terminología. Así que aprecio mucho la investigación que ha realizado @pgolebiowski . No he hecho la investigación por mí mismo, pero mi intuición es que "montón" no es un término que muchos expertos que no pertenecen al dominio estarían buscando.
Por lo tanto, tiendo a estar de acuerdo con @karelz en que PriorityQueue
parece la mejor opción. Combina un concepto existente (cola) y agrega el giro que expresa la capacidad deseada: recuperación ordenada basada en una prioridad. Pero no estamos inamoviblemente apegados a ese nombre. A menudo hemos cambiado los nombres de las estructuras de datos y las tecnologías en función de los comentarios de los clientes.
Sin embargo, me gustaría señalar que esto:
Si no desea la opinión de la gente, no la haga de código abierto y no la pida.
es una falsa dicotomía. No es que no queramos comentarios de nuestro ecosistema y colaboradores (obviamente los queremos). Pero al mismo tiempo, también debemos apreciar que nuestra base de clientes es bastante diversa y que los colaboradores de GitHub (o desarrolladores de nuestro equipo) no siempre son el mejor proxy para todos nuestros clientes. La usabilidad es difícil y probablemente se necesitarán algunas iteraciones para agregar nuevos conceptos a .NET, especialmente en áreas muy populares como las colecciones.
@pgolebiowski :
Valoro mucho sus conocimientos, datos y sugerencias. Pero no aprecio en absoluto tu estilo de argumentación. Te volviste personal tanto con los miembros de mi equipo como con los miembros de la comunidad en este hilo. El hecho de que no esté de acuerdo con nosotros no le da permiso para acusarnos de no tener experiencia o de no preocuparnos porque es "solo nuestro trabajo". Considere que muchos de nosotros literalmente nos hemos mudado alrededor del mundo, dejando atrás a familias y amigos solo porque queríamos hacer este trabajo. Los comentarios como los suyos no solo son muy injustos, sino que tampoco ayudan a avanzar en el diseño.
Entonces, si bien me gusta pensar que tengo la piel gruesa, no tengo mucha tolerancia para ese tipo de comportamiento. Nuestro dominio ya es lo suficientemente complejo; no necesitamos agregar comunicación de confrontación y confrontación.
Por favor sea respetuoso. Criticar ideas con pasión es un juego limpio, pero atacar a las personas no lo es. Gracias.
Queridos todos los que se sienten desanimados,
Pido disculpas por disminuir sus niveles de felicidad con mi actitud áspera.
@karelz
Me hice el ridículo allí al cometer un error técnico. Me disculpé. Fue aceptado. Sin embargo, me la arrojaron más tarde. En mi opinión no es genial.
Lamento que lo que escribí te haya hecho infeliz. Aunque no fue tan malo como lo describiste, simplemente proporcioné esto como uno de los muchos factores que han contribuido a mi sensación de cansancio . Es de menor gravedad, creo. Pero aún así, lo siento.
Y sí, todos cometemos errores. Está bien. Yo también, por ejemplo, dejándome llevar a veces.
Lo que más me atrapó fue "solo te dicen que hagas esto, no lo crees" - sí, es exactamente por eso que lo hago TAMBIÉN los fines de semana.
Lo siento, puedo ver que trabaja duro y se lo agradezco mucho. Me di cuenta de lo especialmente dedicado que estabas con el hito 5/10.
@terrajobst
El hecho de que no esté de acuerdo con nosotros no le da permiso para acusarnos de no tener experiencia o de no preocuparnos porque es "solo nuestro trabajo".
Comentarios como el suyo son muy injustos y tampoco ayudan a avanzar en el diseño.
Si hay alguien enfermo al que le gustaría "lamerme", no dude en hacerlo.
Hemos encontrado un problema y lo hemos solucionado. Todos han aprendido algo de eso. Puedo ver que todos aquí están preocupados por la calidad del marco, lo cual es absolutamente maravilloso y me motiva a contribuir. Espero seguir trabajando en CoreFX contigo. Habiendo dicho eso, abordaré su nueva aportación técnica probablemente mañana.
@pgolebiowski
Ojalá podamos encontrarnos en persona en algún momento. Sinceramente, creo que parte del desafío de hacer todo en línea es que las personalidades a veces pueden mezclarse de mala manera sin la intención de ninguno de los lados.
Espero seguir trabajando en CoreFX contigo. Habiendo dicho eso, abordaré su nueva aportación técnica probablemente mañana.
Aquí igual. Este es un espacio interesante y hay muchas cosas increíbles que podemos hacer juntos :-)
@pgolebiowski primero, gracias por tu respuesta. Demuestra que te preocupas y que tienes buenas intenciones (que es algo que espero en secreto que hagan todas las personas / desarrolladores del mundo, todo conflicto es solo un malentendido / falta de comunicación). Y eso me hace realmente feliz, me mantiene en movimiento y emocionado.
Sugeriría comenzar de nuevo en nuestra relación. Volvamos la discusión a lo técnico, aprendamos todos de este hilo, cómo manejar situaciones similares en el futuro, supongamos nuevamente que la otra parte solo tiene en mente el mejor interés para la plataforma.
Por cierto: este es uno de los pocos encuentros / discusiones más difíciles sobre el repositorio de CoreFX en los últimos 9 meses, y como puede ver, nosotros (incluido / esp. Yo) todavía estamos aprendiendo cómo manejarlos bien, por lo que esta instancia en particular va para beneficiarnos incluso a nosotros y nos hará ser mejores en el futuro y nos ayudará a comprender mejor los diferentes puntos de vista de los miembros apasionados de la comunidad. Tal vez dé forma a nuestras actualizaciones a los documentos de contribución ...
Mis duros comentarios fueron el resultado de la ineficacia de esta discusión. Ineficiencia = manera caótica de discutir, donde los puntos no se abordan / resuelven y seguimos avanzando a pesar de eso => cansado.
¡Entendido tu frustración! Curiosamente, una frustración similar también estaba en el otro lado por la misma razón 😉 ... es casi gracioso cómo funciona el mundo :).
Desafortunadamente, la discusión difícil es parte del trabajo cuando se toma una decisión de diseño. Es MUCHO trabajo. Mucha gente lo subestima. La habilidad clave requerida es la paciencia con todos y la capacidad de superar su propia opinión y pensar cómo generar consenso incluso si no sale como lo desea. Es por eso que sugerí tener 2 propuestas y "escalar" la discusión técnica al grupo de revisores de API (principalmente porque no sé con certeza si estoy en lo cierto, aunque en secreto espero tener razón como cualquier otro desarrollador del mundo lo haría 😉 ).
Es MUY difícil tener una opinión sobre un tema Y llevar la discusión al CONSENSO en el mismo hilo. Desde esa perspectiva, tú y yo tenemos lo más común en este hilo: ambos tenemos opiniones, pero ambos intentamos llevar la discusión al cierre y la decisión. Así que trabajemos juntos de cerca.
Mi enfoque general es: siempre que pienso que alguien me está atacando, es malvado, vago, me frustra o algo así. Me pregunto primero a mí mismo y también a la persona en particular: ¿Por qué? ¿Por qué dijiste eso? ¿Qué querías decir?
Por lo general, es un signo de falta de comprensión / comunicación de motivos. O señal de leer demasiado entre líneas y ver insultos / acusaciones / malas intenciones donde no lo son.
Ahora que no tengo miedo de seguir discutiendo cuestiones técnicas, esto es lo que quería preguntar antes:
Vaya con su PriorityQueue deshabilitada que no permite actualizar y eliminar elementos.
Esto es algo que no entiendo. Si dejamos fuera IHeap
(en mi / la propuesta original aquí), ¿por qué no sería posible?
En mi opinión, no hay diferencia entre las 2 propuestas desde el punto de vista de las capacidades de la clase, la única diferencia es: ¿agregamos la sobrecarga del constructor PriorityQueue(IHeap)
o no (dejando la disputa del nombre de la clase a un lado como un problema independiente para resolver) .
Descargo de responsabilidad (para evitar errores de comunicación): no tengo tiempo para leer artículos e investigar, espero una respuesta corta, presentando un argumento de alto nivel de quien quiera impulsar la discusión técnica. Nota: No soy yo el trolling. Le haría la misma pregunta a cualquier miembro de nuestro equipo que haga esta afirmación. Si no tiene energía para explicarlo / impulsar la discusión (lo cual sería totalmente comprensible dadas las dificultades y la inversión de tiempo de su parte), dígalo, sin resentimientos. Por favor, no se sienta presionado por mí ni por nadie (y eso se aplica a todos en el hilo).
Sin intentar agregar un comentario innecesario más aquí, este hilo ha sido DEMASIADO LARGO. La regla número uno de la era de Internet es evitar la comunicación de texto si te preocupas por la relación entre las personas. (Bueno, lo acuñé yo). Creo que alguna otra comunidad de código abierto cambiaría a un Hangout de Google para este tipo de discusión si la necesidad es evidente. Cuando miras la cara de otras personas, nunca dirías nada "insultante", y las personas se familiarizan entre sí muy rápidamente. ¿Quizás podamos intentarlo también?
@karelz Debido a la extensión de la discusión anterior, es muy poco probable que alguien nuevo contribuya si no se modifica el flujo. Como tal, me gustaría proponer el siguiente enfoque en este momento:
¿Tiene eso alguna posibilidad de tener sentido?
PriorityQueue que no permite actualizar y eliminar elementos.
Se trataba de la propuesta original, que carecía de estas capacidades :) Lamento no dejarlo claro.
Si no tiene energía para explicarlo / impulsar la discusión (lo cual sería totalmente comprensible dadas las dificultades y la inversión de tiempo de su parte), dígalo, sin resentimientos. Por favor, no se sienta presionado por mí ni por nadie (y eso se aplica a todos en el hilo).
No voy a renunciar. Sin dolor no hay ganancia xD
@ xied75
Creo que alguna otra comunidad de código abierto cambiaría a un Hangout de Google para este tipo de discusión si la necesidad es evidente. ¿Quizás podamos intentarlo también?
Se ve bien ;)
No importa si vamos con Heap
/ IHeap
o PriorityQueue
/ IPriorityQueue
(o algo más), para la funcionalidad que estamos a punto de proporcionar ...
_¿Le gustaría tener una interfaz, junto con la implementación? _
@bendono
Al respaldar esa implementación mediante una interfaz, aquellos de nosotros que nos preocupamos por otras implementaciones podemos intercambiar fácilmente en otra implementación (de nuestra propia creación o en una biblioteca externa) y seguir siendo compatibles con el código que acepta la interfaz.
Aquellos a quienes no les importa pueden ignorarlo e ir directamente a la implementación concreta. Aquellos que se preocupan pueden usar cualquier implementación de su elección.
@madelson
Hay muchos algoritmos diferentes que teóricamente se pueden usar para implementar esta estructura de datos. Sin embargo, parece poco probable que el marco se envíe con más de uno, y la idea de que los escritores de bibliotecas realmente necesiten una interfaz canónica para que varias partes puedan coordinar la escritura de implementaciones de montón compatibles entre sí no parece tan probable.
Además, una vez que se lanza una interfaz, nunca se puede cambiar debido a compatibilidad. Eso significa que la interfaz tiende a quedarse atrás de las clases concretas en términos de funcionalidad (un problema con IList e IDictionary en la actualidad).
@karelz
La interfaz es un escenario experto muy avanzado.
Si la biblioteca y la interfaz
IHeap
vuelven realmente populares, podemos cambiar de opinión más tarde y agregarIHeap
según la demanda (a través de la sobrecarga del constructor), pero no creo que sea útil / alineado con el resto de BCL es suficiente para justificar la complicación de agregar una nueva interfaz ahora. Empiece de forma simple, crezca hasta convertirse en complicado sólo si es realmente necesario.
Utilice 👍 y 👎 para votar por este (a favor y en contra de una interfaz, respectivamente). Alternativamente, escriba un comentario. Idealmente, los revisores de API participarán.
Me gustaría agregar que si bien cambiar las interfaces es difícil, con los métodos de extensión (y las propiedades que vienen), las interfaces son más fáciles de extender y / o trabajar (ver LINQ)
Me gustaría agregar que si bien cambiar las interfaces es difícil, con los métodos de extensión (y las propiedades que vienen), las interfaces son más fáciles de extender y / o trabajar (ver LINQ)
Solo pueden trabajar con los métodos definidos públicamente en la interfaz; por lo que significa hacerlo bien a la primera.
Sugeriría esperar un poco en la interfaz hasta que la clase esté en uso y se haya establecido, luego introduzca una interfaz. (siendo el debate sobre la forma de la interfaz un tema aparte)
Para ser franco, lo único que me importa es la interfaz. Una implementación sólida estaría bien, pero yo (o cualquier otra persona) siempre podría crear la mía propia.
Recuerdo que hace unos años tuvimos exactamente la misma conversación con HashSet<T>
. Microsoft quería HashSet<T>
mientras que la comunidad quería ISet<T>
. Si mal no recuerdo, obtuvimos HashSet<T>
primero y ISet<T>
segundo. Sin una interfaz, el uso de HashSet<T>
era bastante limitado ya que es difícil (si no a menudo imposible) cambiar una API pública.
Debo señalar que también hay SortedSet<T>
ahora, sin mencionar numerosas implementaciones no BCL de ISet<T>
. He usado ISet<T>
en API públicas y estoy agradecido por ello. Mi implementación privada puede usar cualquier implementación concreta que considere correcta. También puedo cambiar fácilmente una implementación por otra sin romper nada. Esto no sería posible sin la interfaz.
Para aquellos que dicen que siempre podemos definir nuestras propias interfaces, consideren esto. Suponga por un momento que ISet<T>
en el BCL nunca sucedió. Ahora puedo crear mi propia interfaz IMySet<T>
así como implementaciones sólidas. Sin embargo, un día se libera el BCL HashSet<T>
. Puede o no implementar ISet<T>
, pero no implementa IMySet<T>
. Como resultado, no puedo intercambiar HashSet<T>
como una implementación de mi IMySet<T>
.
Me temo que volveremos a repetir esta farsa.
Si no está listo para comprometerse con una interfaz, entonces es demasiado pronto para introducir una clase concreta.
Encuentro significativa la disparidad de opiniones. Con solo mirar los números, un poco más de personas están a favor de una interfaz, pero eso no nos dice mucho. Intentaré preguntarle a otras personas que han participado en la discusión anteriormente pero que aún no han expresado sus opiniones con respecto a una interfaz:
@ebickle @svick @horaciojcfilho @paulcbetts @justinvp @akoeplinger @mbeidler @SirCmpwn @andrewgmorris @weshaggard @BrannonKing @NKnusperer @danmosemsft @ianhays @safern @VisualMelon @ Joev4evried @
Para los: _Chicos comentario de este problema (4 publicaciones arriba aquí): estamos discutiendo si proporcionar una interfaz es una buena idea (solo este aspecto por ahora, hasta que se resuelva) ._
Quizás algunas de estas personas sean revisores de API, pero creo que necesitamos su apoyo en esta decisión fundamental antes de seguir adelante. @karelz , @terrajobst , ¿podrías pedirles que nos ayuden a resolver este aspecto? Su aporte sería muy valioso, ya que ellos son los que lo revisarán eventualmente; sería muy útil saberlo en este punto, antes de comprometerse con un determinado enfoque (o ir con más de una propuesta, lo que sería tedioso y un poco inútil, ya que podemos conocer su decisión antes).
Personalmente, estoy a favor de una interfaz, pero si la decisión es diferente, estoy feliz de seguir un camino diferente.
No quiero arrastrar a los revisores de API a la discusión: es largo y complicado, no sería eficiente para los revisores de API volver a leer todo o simplemente decidir cuál es la última respuesta importante (me estoy perdiendo en eso ).
Creo que estamos en el punto en el que podemos crear 2 propuestas API formales (ver el ejemplo 'bueno' allí) y resaltar los pros / contras de cada uno. Luego, podemos revisarlos en nuestra reunión de revisión de API y hacer recomendaciones, teniendo en cuenta los votos. Dependiendo de la discusión allí (si hay varias opiniones), podríamos regresar y comenzar la encuesta de Twitter / votación adicional de GH, etc.
Por cierto: las reuniones de revisión de API ocurren casi todos los martes.
Para ayudar a comenzar, así es como debería verse una propuesta:
`` c #
espacio de nombres System.Collections.Generic
{
clase pública PriorityQueue
: IEnumerable
{
public PriorityQueue ();
Public PriorityQueue (capacidad int);
Public PriorityQueue (IComparer
Public PriorityQueue (IEnumerable
Public PriorityQueue (IEnumerable
public PriorityQueue (int capacidad, IComparer
public IComparer<T> Comparer { get; }
public int Count { get; }
public void Enqueue(T item);
public T Dequeue();
public T Peek();
public void Clear();
public bool Contains(T item);
// Sets the capacity to the actual number of elements
public void TrimExcess();
public void CopyTo(T[] array, int arrayIndex);
void ICollection.CopyTo(Array array, int index);
public T[] ToArray();
public Enumerator GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
bool ICollection.IsSynchronized { get; }
object ICollection.SyncRoot { get; }
public struct Enumerator : IEnumerator<T>
{
public T Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
public void Dispose();
}
}
}
''
HACER:
UpdatePriority
@karelz OK, lo haré, ¡estad atentos! :sonrisa:
Bien. Por lo que puedo decir, una interfaz o un montón no pasarán la revisión de la API. Como tal, me gustaría proponer una solución ligeramente diferente, olvidándome del montón cuaternario, por ejemplo. La estructura de datos a continuación es diferente en algunos aspectos a la que podemos encontrar en Python , Java , C ++ , Go , Swift y Rust (al menos esos).
El enfoque principal es la corrección, la integridad en términos de funcionalidad y la intuición, mientras se mantienen las complejidades óptimas y un gran rendimiento en el mundo real.
@karelz @terrajobst
Es un caso bastante común que un usuario tenga un montón de elementos, donde algunos de ellos tienen una prioridad más alta que otros. Eventualmente, quieren mantener este grupo de elementos en algún orden específico para poder realizar de manera eficiente las siguientes operaciones:
Primero nos enfocaremos en construir la cola de prioridad (solo agregando elementos). La forma de hacerlo depende del tipo de datos del usuario.
TKey
y TValue
son objetos separados.TKey
es comparable.var queue = new PriorityQueue<int, string>();
queue.Enqueue(5, "five");
queue.Enqueue(1, "one");
queue.Enqueue(3, "three");
TKey
y TValue
son objetos separados.TKey
no es comparable.var comparer = Comparer<MyKey>.Create(/* custom logic */);
var queue = new PriorityQueue<MyKey, string>(comparer);
queue.Enqueue(new MyKey(5), "five");
queue.Enqueue(new MyKey(1), "one");
queue.Enqueue(new MyKey(3), "three");
TKey
está contenido dentro de TValue
.TKey
no es comparable.public class MyClass
{
public MyKey Key { get; set; }
}
Además de un comparador de claves, también necesitamos un selector de claves:
var selector = new Func<MyClass, MyKey>(item => item.Key);
var queue = new PriorityQueue<MyKey, MyClass>(selector, comparer);
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
Enqueue
aquí. Esta vez solo se necesita un argumento ( TValue
).Enqueue(TKey, TValue)
debe arrojar InvalidOperationException
.Enqueue(TValue)
debe arrojar InvalidOperationException
.TKey
está contenido dentro de TValue
.TKey
es comparable.var queue = new PriorityQueue<MyKey, MyClass>(selector);
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
TKey
es Comparer<TKey>.Default
, como en el escenario 1 .TKey
y TValue
son objetos separados, pero del mismo tipo.TKey
es comparable.var queue = new PriorityQueue<int, int>();
queue.Enqueue(5, 50);
queue.Enqueue(1, 10);
queue.Enqueue(3, 30);
public class MyClass : IComparable<MyClass>
{
public int CompareTo(MyClass other) => /* custom logic */
}
var queue = new PriorityQueue<MyClass, MyClass>();
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
Al principio, hay una ambigüedad.
MyClass
sea un objeto separado y al usuario simplemente le gustaría tener la clave y el valor separados, como estaba en el Escenario 5 .Así es como PriorityQueue
trata la ambigüedad:
Enqueue(TValue)
. Por lo tanto, una solución alternativa al escenario 6 es simplemente definir un selector y pasarlo al constructor.Enqueue
:Enqueue(TKey, TValue)
la primera vez, la clave y el valor se consideran objetos separados ( Escenario 5 ). A partir de ese momento, el método Enqueue(TValue)
debe arrojar InvalidOperationException
.Enqueue(TValue)
la primera vez, la clave y el valor se consideran el mismo objeto, se infiere el selector de clave ( Escenario 6 ). A partir de ese momento, el método Enqueue(TKey, TValue)
debe arrojar InvalidOperationException
.Ya hemos cubierto la creación de una cola de prioridad. También sabemos cómo agregar elementos a la colección. Ahora nos centraremos en la funcionalidad restante.
Hay dos operaciones que podemos realizar en el elemento con la mayor prioridad: recuperarlo o eliminarlo.
var queue = new PriorityQueue<int, string>();
queue.Enqueue(5, "five");
queue.Enqueue(1, "one");
queue.Enqueue(3, "three");
// retrieve the element with the highest priority
var element = queue.Peek();
// remove that element
queue.Dequeue();
Más adelante discutiremos qué es exactamente lo que devuelven los métodos Peek
y Dequeue
.
Para un elemento arbitrario, nos gustaría modificarlo. Por ejemplo, si se utiliza una cola de prioridad durante toda la vida útil de un servicio, existe una alta probabilidad de que el desarrollador desee tener la posibilidad de actualizar las prioridades.
Sorprendentemente, esta funcionalidad no la proporcionan estructuras de datos equivalentes: en Python , Java , C ++ , Go , Swift y Rust . Posiblemente algunos otros también, pero solo los he comprobado. ¿El resultado? Desarrolladores decepcionados:
Tenemos básicamente dos opciones aquí:
Cada vez que un usuario agrega un elemento a la cola de prioridad, se le asigna un identificador:
var handle = queue.Enqueue(42, "forty two");
Handle es una clase con la siguiente API pública:
public class PriorityQueueHandle<TKey, TValue>
{
public TKey Key { get; }
public TValue Value { get; }
}
Es una referencia a un elemento único en la cola de prioridad. Si le preocupa la eficiencia, consulte las preguntas frecuentes (y ya no se preocupará más).
Este enfoque nos permite modificar fácilmente un elemento único en la cola de prioridad, de una manera muy intuitiva y sencilla:
/*
* User wants to retrieve a server that at any given moment
* has the lowest average response time.
* He doesn't want to maintain a separate key object (it is
* inside his type) and the key is already comparable.
*/
var queue = new PriorityQueue<double, ServerStats>(selector);
/* adding some elements */
var handle = queue.Enqueue(server);
/*
* Server stats are kept along with handles (e.g. in a dictionary).
* Whenever there is a need of updating the priority of a certain
* server, the user simply updates the appropriate ServerStats object
* and then simply uses the handle associated with it:
*/
queue.Update(handle);
En el ejemplo anterior, debido a que se definió un selector específico, un usuario podría actualizar el objeto por sí mismo. La cola de prioridad simplemente necesitaba ser notificada para reorganizarse (no queremos que sea observable).
De manera similar a cómo el tipo de datos del usuario afecta la forma en que una cola de prioridad se construye y se llena de elementos, la forma en que se actualiza también varía. Acortaré los escenarios esta vez, ya que probablemente ya sepas lo que voy a escribir.
TKey
y TValue
son objetos separados.var queue = new PriorityQueue<int, string>();
var handle = queue.Enqueue(1, "three");
queue.Enqueue(1, "three");
queue.Enqueue(2, "three");
queue.Update(handle, 3);
Como puede ver, este enfoque proporciona una forma sencilla de referirse a un elemento único en la cola de prioridad, sin hacer preguntas. Es especialmente útil en escenarios donde las claves pueden estar duplicadas. Además, esto es muy eficiente: conocemos el elemento a actualizar en O (1) y la operación se puede realizar en O (log n).
Alternativamente, el usuario podría usar métodos adicionales que busquen en toda la estructura en O (n) y luego actualicen el primer elemento que coincida con los argumentos. Así es como se elimina un elemento en Java. No es completamente correcto ni eficiente, pero a veces es más simple:
var queue = new PriorityQueue<int, string>();
queue.Enqueue(1, "one");
queue.Enqueue(2, "three");
queue.Enqueue(3, "three");
queue.Update("three", 30);
El método anterior encuentra el primer elemento con su valor igual a "three"
. Su clave se actualizará a 30
. Sin embargo, no sabemos cuál se actualizará si hay más de uno que satisfaga la condición.
Podría ser un poco más seguro con un método Update(TKey oldKey, TValue, TKey newKey)
. Esto agrega otra condición: la clave anterior también debe coincidir. Ambas soluciones son más simples, pero no 100% seguras y de menor rendimiento (O (1 + log n) vs O (n + log n)).
TKey
está contenido dentro de TValue
.var queue = new PriorityQueue<int, MyClass>(selector);
/* adding some elements */
queue.Update(handle);
Este escenario es lo que se dio como ejemplo en la sección de Mangos .
Lo anterior se logra en O (log n). Alternativamente, el usuario podría usar un método Update(TValue)
que encuentra el primer elemento igual al especificado y realiza una reorganización interna. Por supuesto, esto es O (n).
TKey
y TValue
son objetos separados, pero del mismo tipo.Usando un mango, no hay ambigüedad, como siempre. En el caso de otros métodos que permitan la actualización, por supuesto que los hay. Se trata de un compromiso entre simplicidad, rendimiento y corrección (que depende de si los datos se pueden duplicar o no).
Con un asa, de nuevo no hay problema. La actualización a través de otros métodos puede ser más simple, pero nuevamente, menos eficiente y no siempre correcta (entradas iguales).
Se puede hacer de forma sencilla y correcta mediante un asa. Alternativamente, a través de los métodos Remove(TValue)
y Remove(TKey, TValue)
. Los mismos problemas que se describieron anteriormente.
var queue1 = new PriorityQueue<int, string>();
var queue2 = new PriorityQueue<int, string>();
/* add some elements to both */
queue1.Merge(queue2);
InvalidOperationException
.InvalidOperationException
.public class PriorityQueue<TKey, TValue>
: IEnumerable,
IEnumerable<PriorityQueueHandle<TKey, TValue>>,
IReadOnlyCollection<PriorityQueueHandle<TKey, TValue>>
// ICollection not included on purpose
{
public PriorityQueue();
public PriorityQueue(IComparer<TKey> comparer);
public PriorityQueue(Func<TValue, TKey> keySelector);
public PriorityQueue(Func<TValue, TKey> keySelector, IComparer<TKey> comparer);
IComparer<TKey> Comparer { get; }
Func<TValue, TKey> KeySelector { get; }
public int Count { get; }
public void Clear();
public bool Contains(PriorityQueueHandle<TKey, TValue> handle); // O(log n)
public bool Contains(TValue value); // O(n)
public bool Contains(TKey key, TValue value); // O(n)
public PriorityQueueHandle<TKey, TValue> Enqueue(TKey key, TValue value); // O(log n)
public PriorityQueueHandle<TKey, TValue> Enqueue(TValue value); // O(log n)
public PriorityQueueHandle<TKey, TValue> Peek(); // O(1)
public PriorityQueueHandle<TKey, TValue> Dequeue(); // O(log n)
public void Update(PriorityQueueHandle<TKey, TValue> handle); // O(log n)
public void Update(PriorityQueueHandle<TKey, TValue> handle, TKey newKey); // O(log n)
public void Update(TValue value, TKey newKey); // O(n)
public void Update(TKey oldKey, TValue value, TKey newKey); // O(n)
public void Remove(PriorityQueueHandle<TKey, TValue> handle); // O(log n)
public void Remove(TValue value); // O(n)
public void Remove(TKey key, TValue value); // O(n)
public void Merge(PriorityQueue<TKey, TValue> other); // O(1)
public IEnumerator<PriorityQueueHandle<TKey, TValue>> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
}
public class PriorityQueueHandle<TKey, TValue>
{
public TKey Key { get; }
public TValue Value { get; }
}
Estoy fuertemente a favor del enfoque con mangos, porque es eficiente, intuitivo y totalmente correcto . La pregunta es cómo lidiar con métodos más simples pero potencialmente no tan seguros. Una cosa que podríamos hacer es reemplazar esos métodos más simples con algo como esto:
UpdateFirst(Func<PriorityQueueHandle<TKey, TValue>, bool> predicate)
UpdateAll(Func<PriorityQueueHandle<TKey, TValue>, bool> predicate)
El uso sería bastante dulce y poderoso. Y verdaderamente intuitivo. Y legible (muy expresable). Yo estoy a favor de ese.
var queue = new PriorityQueue<int, string>();
/* add some elements */
queue.UpdateAll(x => x.Key < 15, 0);
// or for example
queue.UpdateFirst(x => x.Value.State == State.Idle, 100);
La configuración de la nueva clave también podría ser una función, que toma la clave anterior y la transforma de alguna manera.
Lo mismo para Contains
y Remove
.
Si el selector de clave no está definido (y por lo tanto, la clave y el valor se mantienen por separado), el método podría llamarse UpdateKey
. Probablemente sea más expresable. Sin embargo, cuando se define el selector de clave, Update
es mejor, porque la clave ya está actualizada y lo que hay que hacer es reorganizar algunos elementos dentro de la cola de prioridad.
No hay ningún problema con la eficiencia en lo que respecta al uso de manijas. Un temor común es que requerirá asignaciones adicionales, porque estamos usando un montón basado en una matriz internamente. No temáis. Sigue leyendo.
Sería un enfoque completamente diferente para entregar una cola de prioridad. Por primera vez, una biblioteca estándar no proporcionará esta funcionalidad implementada como un montón binario, que se representa como una matriz debajo. Se implementará con un montón de emparejamiento , que por diseño no usa una matriz, simplemente representa un árbol.
Por cierto, es para la primera iteración de revisión de API. La sección con _preguntas abiertas_ debe resolverse (necesito su opinión [y de los revisores de API si es posible]). Si la primera iteración pasa al menos parcialmente, para el resto de la discusión me gustaría crear un nuevo problema (cerrar y hacer referencia a este).
Por lo que puedo decir, una interfaz o un montón no pasarán la revisión de la API.
@pgolebiowski : ¿Por qué no cerrar el problema entonces? Sin una interfaz, esta clase es casi inútil. El único lugar donde podría usarlo es en implementaciones privadas. En ese momento, puedo crear (o reutilizar) mi propia cola de prioridad cuando sea necesario. No puedo exponer una API pública en mi código con este tipo en la firma, ya que se romperá tan pronto como necesite cambiarla por otra implementación.
@bendono
Bueno, Microsoft tiene la última palabra aquí, no pocas personas comentan en el hilo. Y sabemos que:
Estoy algo seguro de que otros revisores / arquitectos de API lo compartirán, ya que he comprobado la opinión de algunos: el nombre debería ser PriorityQueue, y no deberíamos introducir la interfaz IHeap. Debería haber exactamente una implementación (probablemente a través de algún montón).
Esto lo comparten @karelz , Administrador de ingenieros de software, y @terrajobst , Administrador de programas y el propietario de este repositorio + algunos revisores de API.
Aunque obviamente me gusta el enfoque con interfaces, como se indicó claramente en las publicaciones anteriores, puedo ver que es bastante difícil dado que no tenemos mucho poder en esta discusión. Hemos expresado nuestros puntos, pero somos solo algunos comentaristas. De todos modos, el código no nos pertenece. qué más podemos hacer?
Entonces, ¿por qué no cerrar el problema? Sin una interfaz, esta clase es casi inútil.
He hecho suficiente, en lugar de odiar mi trabajo, haz algo. Haz el trabajo real. ¿Por qué intentas culparme por algo? Cúlpese a sí mismo por no haber logrado persuadir a los demás de su punto de vista.
Y por favor, ahórrame una retórica como esa. Es realmente infantil, hazlo mejor.
Por cierto, la propuesta se enfoca en cosas que son comunes sin importar si introducimos una interfaz o no, o si la clase se llama PriorityQueue
o Heap
. Así que concéntrate en lo que realmente importa aquí y muéstranos algún sesgo de acción si quieres algo.
@pgolebiowski Por supuesto que es decisión de Microsoft. Pero es mejor presentar una API que desee utilizar y que satisfaga sus necesidades. Si es rechazado, que así sea. Simplemente no veo la necesidad de comprometer la propuesta.
Le pido disculpas si interpretó mis comentarios como una culpa. Ciertamente esa no era mi intención.
@pgolebiowski ¿ KeyValuePair<TKey,TValue>
para el identificador?
@SamuelEnglard
¿Por qué no usar
KeyValuePair<TKey,TValue>
para el identificador?
PriorityQueueHandle
es de hecho _ nodo de almacenamiento dinámico de emparejamiento _. Expone dos propiedades: TKey Key
y TValue Value
. Sin embargo, tiene mucha más lógica por dentro, que es solo interna. Consulte mi implementación de esto . Contiene, por ejemplo, también punteros a otros nodos en el árbol (todo sería interno en CoreFX).KeyValuePair
es una estructura, por lo que se copia cada vez + no se puede heredar.PriorityQueueHandle
es una clase bastante complicada que simplemente expone la misma API pública que KeyValuePair
.@bendono
Pero es mejor presentar una API que desee utilizar y que satisfaga sus necesidades. Si es rechazado, que así sea. Simplemente no veo la necesidad de comprometer la propuesta.
TKey
y TValue
separados), esta clase está realmente cerca de ser inútil, entonces - - como está ahora en Java.Ciertamente esa no era mi intención.
Lo siento, mi error entonces.
Mis preguntas sobre el diseño (descargo de responsabilidad: estas preguntas y aclaraciones, sin críticas duras (todavía)):
PriorityQueueHandle
? ¿Qué pasa si solo esperamos valores únicos en la cola?Merge
? ¿Lo tienen otras colecciones? No debemos agregar API, solo porque son fáciles de implementar, debería haber algún caso de uso común para las API.IEnumerable<KeyValuePair<TKey, TValue>>
sería suficiente? + confíe en Linqcomparer
? ¿Podemos siempre volver a los valores predeterminados? (descargo de responsabilidad: me faltan conocimientos / experiencia en este caso, así que solo pregunto)keySelector
? Creo que deberíamos decidir si queremos tener prioridad como parte del valor o como algo separado. La prioridad separada me parece un poco más natural, pero no tengo una opinión sólida. ¿Conocemos los pros y los contras?Puntos de decisión separados / paralelos:
PriorityQueue
vs. Heap
IHeap
y sobrecarga del constructor?¿Introducir IHeap y la sobrecarga del constructor?
Parece que las cosas se han calmado lo suficiente como para tirar mis dos centavos ... Me gusta la interfaz, personalmente. Extrae los detalles de implementación de la API (y la funcionalidad principal descrita por esa API) de una manera que, en mi opinión, simplifica la estructura y permite la mayor usabilidad.
Lo que no tengo una opinión tan fuerte es si hacemos la interfaz al mismo tiempo que PQueue / Heap / ILikeThisItemMoreThanThisItemList o si la agregamos más tarde. El argumento de que la API puede estar "en proceso de cambio" y, como tal, deberíamos publicarlo como una clase primero hasta que obtengamos comentarios es ciertamente válido y no estoy en desacuerdo. La pregunta entonces es cuándo se considera lo suficientemente "estable" como para agregar una interfaz. Mucho más arriba en el hilo IList
y IDictionary
fueron mencionados como rezagados con respecto a las API de sus implementaciones canónicas que agregamos hace muuucho tiempo, entonces, ¿qué período de tiempo se considera un período de descanso aceptable?
Si podemos definir ese período con una certeza razonable y estar seguros de que no está bloqueando de manera inaceptable, entonces no veo ningún problema para enviar esta estructura de datos abultada sin una interfaz. Luego, después de ese período de tiempo, podemos examinar el uso y considerar agregar una interfaz.
Y sí, es realmente sorprendente para mí que la adición de una interfaz sea percibida como una desventaja por Microsoft.
Eso es porque en muchos sentidos es una desventaja. Si enviamos una interfaz, está prácticamente hecho. No hay mucho margen de maniobra para iterar en esa API, por lo que es mejor que nos aseguremos de que sea correcta la primera vez y que seguirá siéndolo durante los próximos años. Faltan algunas funciones-nice-tener una forma mejor lugar para estar en que estar atrapado con una interfaz potencialmente inadecuada en la que sería oh-tan-bueno tener sólo este pequeño cambio que haría todo mejor.
¡Gracias por la entrada, @karelz y @ianhays!
¿Realmente necesitamos
PriorityQueueHandle
? ¿Qué pasa si solo esperamos valores únicos en la cola?Motivación: Parece un concepto bastante complicado. Si lo tenemos, me gustaría entender por qué lo necesitamos. ¿Cómo está ayudando? ¿O es solo un detalle de implementación específico que se filtra en la superficie de la API? ¿Nos va a comprar tanto rendimiento para pagar la complicación en la API?
No, no lo necesitamos. La API de cola de prioridad propuesta anteriormente es bastante poderosa y muy flexible. Se basa en el supuesto de que los elementos y las prioridades podrían duplicarse . Debido a esa suposición, es necesario tener un identificador para poder eliminar o actualizar el nodo correcto. Sin embargo, si ponemos una restricción de que los elementos deben ser únicos, podríamos lograr el mismo resultado que el anterior con una API más simple y sin exponer una clase interna ( PriorityQueueHandle
), que de hecho no es ideal.
Supongamos que solo permitimos elementos únicos. Aún podríamos admitir todos los escenarios anteriores y mantener un rendimiento óptimo. API más simple:
public class PriorityQueue<TElement, TPriority>
: IEnumerable,
IEnumerable<(TElement element, TPriority priority)>,
IReadOnlyCollection<(TElement element, TPriority priority)>
// ICollection not included on purpose
{
public PriorityQueue();
public PriorityQueue(IComparer<TPriority> comparer);
public PriorityQueue(Func<TElement, TPriority> prioritySelector);
public PriorityQueue(Func<TElement, TPriority> prioritySelector, IComparer<TPriority> comparer);
IComparer<TPriority> Comparer { get; }
Func<TElement, TPriority> PrioritySelector { get; }
public int Count { get; }
public void Clear();
public bool Contains(TElement element); // O(1)
public (TElement element, TPriority priority) Peek(); // O(1)
public (TElement element, TPriority priority) Dequeue(); // O(log n)
public void Enqueue(TElement element, TPriority priority); // O(log n)
public void Enqueue(TElement element); // O(log n)
public void Update(TElement element); // O(log n)
public void Update(TElement element, TPriority priority); // O(log n)
public void Remove(TElement element); // O(log n)
public IEnumerator<(TElement element, TPriority priority)> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
}
En breve, habrá un Dictionary<TElement, InternalNode>
subyacente. La prueba de si la cola de prioridad contiene un elemento se puede hacer incluso más rápido que en el enfoque anterior. La actualización y eliminación de elementos se simplifica significativamente, ya que siempre podemos apuntar a un elemento directo en la cola.
Probablemente, permitir duplicados no vale la pena y lo anterior es suficiente. Creo que me gusta. ¿Qué piensas?
¿Necesitamos
Merge
? ¿Lo tienen otras colecciones? No debemos agregar API, solo porque son fáciles de implementar, debería haber algún caso de uso común para las API.
De acuerdo. No lo necesitamos. Siempre podemos agregar API pero no eliminarlo. Estoy de acuerdo con eliminar este método y (potencialmente) agregarlo más tarde (si es necesario).
¿Necesitamos una sobrecarga del comparador? ¿Podemos siempre volver a los valores predeterminados? (descargo de responsabilidad: me faltan conocimientos / experiencia en este caso, así que solo pregunto)
Creo que esto es bastante importante, por dos razones:
IComparable
.Además, es consistente con la API existente. Eche un vistazo a SortedDictionary .
¿Necesitamos sobrecargas de keySelector? Creo que deberíamos decidir si queremos tener prioridad como parte del valor o como algo separado. La prioridad separada me parece un poco más natural, pero no tengo una opinión sólida. ¿Conocemos los pros y los contras?
También me gusta la prioridad separada. Aparte de eso, es más fácil implementar esto, mejor rendimiento y uso de memoria, API más intuitiva, menos trabajo para el usuario (no es necesario implementar IComparable
).
Respecto al selector ahora ...
Esto es lo que hace que esta cola de prioridad sea flexible. Permite a los usuarios tener:
IComparable
Permite prácticamente todas las configuraciones que se me ocurren. Lo encuentro útil, ya que los usuarios pueden simplemente "plug and play". También es bastante intuitivo.
Enqueue
y Update
.Nombre de la clase
PriorityQueue
vs.Heap
Introduzca
IHeap
y la sobrecarga del constructor.
PriorityQueue
debería ser parte de CoreFX, en lugar de Heap
.IHeap
, debido a que una cola de prioridad se puede implementar con algo diferente a un montón, probablemente no nos gustaría exponerlo de esta manera. Sin embargo, es posible que necesitemos IPriorityQueue
.Si enviamos una interfaz, está prácticamente hecho. No hay mucho margen de maniobra para iterar en esa API, por lo que es mejor que nos aseguremos de que sea correcta la primera vez y que seguirá siéndolo durante los próximos años. Perder alguna funcionalidad agradable es un lugar mucho mejor para estar que estar atrapado con una interfaz potencialmente inadecuada donde sería muy bueno tener solo este pequeño cambio que lo haría todo mejor.
¡Totalmente de acuerdo!
Si agregamos otra suposición: que los elementos tienen que ser comparables, entonces la API es aún más simple. Pero nuevamente, es menos flexible.
IComparer
.Enqueue
y Update
.IComparable
antes de poder usar esta cola de prioridad.<TElement, InternalNode>
y tener InternalNode
contener el TPriority
.public class PriorityQueue<T> : IEnumerable, IEnumerable<T>, IReadOnlyCollection<T>
where T : IComparable<T>
// ICollection not included on purpose
{
public PriorityQueue();
// some other constructors like building it from a collection, or initial capacity if we have an array beneath
public int Count { get; }
public void Clear();
public bool Contains(T element);
public T Peek();
public T Dequeue();
public void Enqueue(T element);
public void Update(T element);
public void Remove(T element);
public IEnumerator<T> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
}
Todo es una compensación. Eliminamos algunas funciones, perdemos algo de flexibilidad, pero tal vez no lo necesitemos para satisfacer al 95% de nuestros usuarios.
También me gusta el enfoque de la interfaz porque es más flexible y ofrece más usos, pero también estoy de acuerdo con @karelz y @ianhays en que deberíamos esperar hasta que se use la nueva API de clase y recibamos comentarios hasta que realmente enviemos la interfaz y luego están almacenados con esa versión y no pueden cambiarla sin romper con los clientes que ya la usaron.
También sobre los comparadores, creo que sigue a las otras apis de BCL y no me gusta el hecho de que los usuarios necesiten proporcionar una nueva clase contenedora. Realmente me gusta que la sobrecarga del constructor reciba un enfoque de comparador y use ese comparador dentro de todas las comparaciones que deben hacerse internamente, si no se proporciona el comparador o el comparador es nulo, entonces use el comparador predeterminado.
@pgolebiowski gracias por una propuesta de API tan detallada y descriptiva y por ser tan proactivo y enérgico para que esta API sea aprobada y agregada a CoreFX 👍 cuando termine esta discusión y consideremos que está lista para su revisión, fusionaría todas las entradas y la superficie final de la API en un comentario y actualice el comentario del problema principal en la parte superior, ya que eso facilitaría la vida de los revisores.
De acuerdo, estoy convencido del comparador, tiene sentido y es consistente.
Todavía estoy dividido en el selector, en mi opinión, deberíamos intentar prescindir de él, dividámoslo en 2 variantes.
`` c #
clase pública PriorityQueue
: IEnumerable,
IEnumerable <(elemento TElement, prioridad TPriority)>,
IReadOnlyCollection <(elemento TElement, prioridad TPriority)>
// IColección no incluida a propósito
{
public PriorityQueue ();
Public PriorityQueue (IComparer
public IComparer<TPriority> Comparer { get; }
public int Count { get; }
public void Clear();
public bool Contains(TElement element); // O(1)
public (TElement element, TPriority priority) Peek(); // O(1)
public (TElement element, TPriority priority) Dequeue(); // O(log n)
public void Enqueue(TElement element, TPriority priority); // O(log n)
public void Update(TElement element, TPriority priority); // O(log n)
public void Remove(TElement element); // O(log n)
public IEnumerator<(TElement element, TPriority priority)> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
//
// Selector de parte
//
Public PriorityQueue (Func
Public PriorityQueue (Func
public Func<TElement, TPriority> PrioritySelector { get; }
public void Enqueue(TElement element); // O(log n)
public void Update(TElement element); // O(log n)
}
`` ``
Preguntas abiertas:
PriorityQueue
vs. Heap
IHeap
y sobrecarga del constructor? (¿Deberíamos esperar más tarde?)IPriorityQueue
? (¿Deberíamos esperar más tarde - IDictionary
ejemplo)(TElement element, TPriority priority)
frente a KeyValuePair<TPriority, TElement>
Peek
y Dequeue
tener out
argumento Intentaré ejecutarlo por el grupo de revisión de API mañana para recibir comentarios tempranos.
Se corrigió Peek
y Dequeue
nombre de campo de tupla a priority
(gracias @pgolebiowski por señalarlo).
Publicación superior actualizada con la última propuesta anterior.
Segundo el comentario de @safern: ¡Muchas gracias a @pgolebiowski por impulsarlo!
Preguntas abiertas:
Creo que podríamos tener uno más aquí:
Buen punto, capturado como 'Supuestos' en la publicación superior, en lugar de una pregunta abierta. Lo contrario afectaría bastante a la API.
¿Deberíamos devolver bool
de Remove
? ¿Y también la prioridad como out priority
arg? (Creo que recientemente agregamos sobrecargas similares en otras estructuras de datos)
Similar a Contains
: apuesto a que alguien querrá la prioridad. Es posible que deseemos agregar una sobrecarga con out priority
.
Sigo siendo de la opinión (aunque todavía tengo que expresarlo) de que Heap implicaría una implementación, y apoyaría debidamente llamar a esto PriorityQueue
. (Incluso apoyaría entonces una propuesta para una clase Heap
más delgada que permita elementos duplicados y no permita actualizaciones, etc. (más en línea con la propuesta original) pero no espero eso que suceda).
KeyValuePair<TPriority, TElement>
ciertamente no debe usarse, ya que se esperan prioridades duplicadas, y creo que KeyValuePair<TElement, TPriority>
también es confuso, por lo que apoyaría no usar KeyValuePair
en absoluto, y tampoco usando tuplas simples o parámetros para las prioridades (personalmente, me gustan los parámetros, pero no estoy preocupado).
Si no permitimos los duplicados, debemos decidir el comportamiento de intentar volver a agregarlos con prioridades diferentes / cambiadas.
No apoyo la propuesta del selector, por la simple razón de que implica redundancia, y debidamente se recreará confusión. Si la Prioridad de un Elemento se almacena con él, entonces se almacenará en dos lugares, y si se desincronizan (es decir, alguien se olvida de llamar al método Update(TElement)
apariencia inútil) entonces mucho sufrimiento asegurará. Si el selector es una computadora, entonces estamos abiertos a que las personas agreguen intencionalmente un elemento, luego cambien los valores a partir de los cuales se calculan, y ahora, si intentan volver a agregarlo, hay una gran cantidad de cosas que podrían salir mal dependiendo sobre la decisión de lo que sucede cuando esto ocurre. En un nivel un poco más alto, intentarlo podría resultar en agregar una copia modificada del Elemento, ya que ya no es igual a lo que era antes (este es un problema general con claves mutables, pero creo que separar la Prioridad y el Elemento ayudar a evitar problemas potenciales).
El selector en sí mismo es susceptible de cambiar el comportamiento, que es otra forma en que los usuarios pueden romper todo sin pensar. Creo que es mucho mejor que los usuarios proporcionen explícitamente las prioridades. El gran problema que veo con esto es que transmite que se permiten entradas duplicadas, ya que estamos declarando un par, no un Elemento. Sin embargo, una documentación en línea sensible y un valor de retorno de bool
en Enqueue
deberían remediar esto trivialmente. Mejor que un booleano quizás sería devolver la prioridad anterior / nueva del elemento (por ejemplo, si Enqueue
usa la prioridad recién proporcionada, o el mínimo de las dos, o la prioridad anterior), pero creo que Enqueue
debería fallar si intenta volver a agregar algo y, por lo tanto, debería devolver un bool
indique éxito. Esto mantiene Enqueue
y Update
completamente separados y bien definidos.
Apoyaría no usar KeyValuePair en absoluto, y tampoco usar tuplas simples
Estoy contigo en tuplas.
Si no permitimos los duplicados, debemos decidir el comportamiento de intentar volver a agregarlos con prioridades diferentes / cambiadas.
Dictionary
. Allí, cuando haces dictionary["something"] = 5
, se actualiza si "something"
era una clave allí anteriormente. Si no estaba allí, simplemente se agrega.Enqueue
es análogo para mí al método Add
en el diccionario. Lo que significa que debería lanzar una excepción.Enqueue
debería generar una excepción si alguien quiere agregar un elemento duplicado. De manera similar, el método Update
debería lanzar una excepción si alguien quiere actualizar la prioridad de un elemento que no está presente.TryUpdate
que de hecho devuelve bool
.No apoyo la propuesta del selector, por la simple razón de que implica redundancia
¿No es que la clave no se copia físicamente (si existe), pero el selector sigue siendo una función a la que se llama cuando es necesario evaluar las prioridades? ¿Dónde está la redundancia?
Creo que separar la Prioridad y el Elemento ayudará a evitar problemas potenciales
El único problema es el caso cuando el cliente no tiene la prioridad física. Entonces no puede hacer mucho.
Creo que es mucho mejor que los usuarios proporcionen explícitamente las prioridades. El gran problema que veo con esto es que transmite que se permiten entradas duplicadas, ya que estamos declarando un par, no un Elemento.
Veo algunos problemas con esa solución, pero no necesariamente por qué transmite que se permiten entradas duplicadas. Creo que la lógica de "no duplicados" debería aplicarse solo en TElement
; la prioridad es solo un valor aquí.
var queue = new PriorityQueue<string, double>();
queue.Enqueue("first", 0.1);
queue.Enqueue("first", 0.5); // should be unsuccessful IMO
@VisualMelon, ¿eso tiene sentido?
@pgolebiowski
Estoy totalmente de acuerdo con agregar un TryUpdate
, y el lanzamiento de Update
tiene sentido para mí. Estaba pensando más en la línea de ISet
con respecto a Enqueue
(en lugar de IDictionary
). Lanzar también tendría sentido, debo haber perdido ese punto, pero creo que el retorno bool
transmite la 'configuración' del tipo. ¿Quizás un TryEnqueue
estaría en orden (con Add
arrojando)? También se agradecería un TryRemove
(falla si está vacío). En cuanto a tu último punto, sí, ese es el comportamiento que también tenía en mente. Supongo que una analogía con IDictionary
es mejor que ISet
en la reflexión, y eso debería ser lo suficientemente claro. (Para resumir: apoyaría cualquier lanzamiento según su sugerencia, pero tener Try*
es imprescindible si ese es el caso; también estoy de acuerdo con sus declaraciones con respecto a las condiciones de falla).
Con respecto al indexador, creo que tiene razón en que realmente no se "ajusta" al concepto de cola, no lo apoyaría. En todo caso, un método bien nombrado para poner en cola o actualizar estaría en orden.
¿No es que la clave no se copia físicamente (si existe), pero el selector sigue siendo una función a la que se llama cuando es necesario evaluar las prioridades? ¿Dónde está la redundancia?
Tienes razón, leí mal la actualización de la propuesta (la parte sobre almacenarla en el Elemento). Teniendo en cuenta entonces que el Selector se llama a pedido (lo que tendría que quedar claro en cualquier documentación, ya que podría tener implicaciones en el rendimiento), los puntos todavía están en pie de que el resultado de la función puede cambiar sin que la estructura de datos responda, dejando el dos fuera de sincronización a menos que se llame a Update
. Peor aún, si un usuario cambia la prioridad efectiva de varios Elementos y solo actualiza uno de ellos, la estructura de datos terminará dependiendo de los cambios 'no comprometidos' cuando se seleccionen entre los Elementos 'no actualizados' (no he investigado la implementación propuesta de DataStructure en detalle, pero creo que esto es necesariamente un problema para cualquier Actualización O(<n)
). Obligar al usuario a actualizar explícitamente cualquier prioridad soluciona esto, llevando necesariamente la estructura de datos de un estado consistente a otro.
Tenga en cuenta que todas mis quejas declaradas hasta ahora con la propuesta del Selector se refieren a la solidez de la API: creo que el selector facilita su uso incorrecto. Sin embargo, dejando de lado la usabilidad, conceptualmente, los elementos de la cola no deberían tener que conocer su prioridad. Es potencialmente irrelevante para ellos, y si el usuario termina envolviendo sus Elementos en un struct Queable<T>{}
o algo, entonces parece un error al proporcionar una API sin fricciones (por mucho que me duela usar el término).
Por supuesto, podría argumentar que sin el selector, la carga recae en el usuario para llamar al selector, pero creo que si los Elementos conocen su prioridad, estarán (con suerte) expuestos de manera ordenada (es decir, una propiedad), y si no lo hacen ' t, entonces no hay ningún selector del que preocuparse (generar prioridades sobre la marcha, etc.). Se requieren muchas más tuberías sin sentido para admitir un selector si no lo desea, luego para pasar las prioridades si ya tiene un 'selector' bien definido. El selector atrae al usuario a exponer esta información (quizás de manera inútil), mientras que las prioridades separadas proporcionan una interfaz extremadamente transparente que no puedo imaginar que influya en las decisiones de diseño.
El único argumento en el que realmente puedo pensar a favor del selector es que significa que puede usar pasar un PriorityQueue
alrededor, y otras partes del programa pueden usarlo sin saber cómo se calculan los prioratos. Tendré que pensar en esto un poco más, pero dada la idoneidad del 'nicho', esto me parece una pequeña recompensa por los gastos generales razonablemente pesados en casos más generales.
_Editar: Habiéndolo pensado un poco más, sería muy bueno tener PriorityQueue
s autónomos en los que puedas lanzar Elements, pero mantengo que el costo de solucionar esto sería grande, mientras el costo de introducción sería considerablemente menor.
He estado trabajando un poco buscando a través de bases de código .NET de código abierto para ver ejemplos del mundo real de cómo se usan las colas de prioridad. El problema complicado es filtrar los proyectos de los estudiantes y el código fuente de capacitación.
Uso 1: Servicio de notificación de Roslyn
https://github.com/dotnet/roslyn/
El compilador de Roslyn incluye una implementación de cola de prioridad privada llamada "PriorityQueue" que parece tener una optimización muy específica: una cola de objetos que reutiliza los objetos en la cola para evitar que sean recolectados como basura. El método Enqueue_NoLock realiza una evaluación de current.Value.MinimumRunPointInMS <entry.Value.MinimumRunPointInMS para determinar en qué lugar de la cola colocar el nuevo nodo. Cualquiera de los dos diseños de colas de prioridad principales propuestos aquí (función de comparación / delegado frente a prioridad explícita) encajaría en este escenario de uso.
Uso 2: Lucene.net
https://github.com/apache/lucenenet
Este es posiblemente el ejemplo de uso más grande para PriorityQueue que pude encontrar en .NET. Apache Lucene.net es un puerto .net completo de la popular biblioteca de motores de búsqueda Lucene. Usamos la versión de Java en mi empresa y, según el sitio web de Apache, algunos grandes nombres usan la versión .NET. Hay una gran cantidad de bifurcaciones del proyecto .NET en Github.
Lucene incluye su propia implementación PriorityQueue que está subclasificada por una serie de colas de prioridad "especializadas": HitQueue, TopOrdAndFloatQueue, PhraseQueue y SugerirWordQueue. Además, el proyecto instancia directamente PriorityQueue en varios lugares.
La implementación de PriorityQueue de Lucene, vinculada a la anterior, es muy similar a la API de cola de prioridad original publicada en este número. Se define como "PriorityQueue
La prioridad en PriorityQueue de Lucene está determinada por el comparador pasado al constructor, y si no se pasó ninguno, se supone que los objetos comparados implementan IComparable
Hay una gran cantidad de ejemplos de uso a través de su base de código, siendo SloppyPhraseScorer uno de ellos.
El constructor de SloppyPhraseScorer crea una nueva PhraseQueue (pq), que es una de sus propias subclases personalizadas de PriorityQueue. Se genera una colección de PhrasePositions, que parece ser un contenedor para un conjunto de publicaciones, una posición y un conjunto de términos. El método FillQueue enumera las posiciones de las frases y las pone en cola. PharseFreq () llama a una función AdvancePP y, en un nivel alto, parece retirarse de la cola, actualizar la prioridad de un elemento y luego volver a ponerse en cola. La prioridad se determina relativamente (usando un comparador) en lugar de explícitamente (la prioridad no se "pasa" como segundo parámetro durante la puesta en cola).
Puede ver que en base a su implementación de PhraseQueue, un valor de comparación pasado a través del constructor (por ejemplo, integer) podría no ser suficiente. Su función de comparación ("LessThan") evalúa tres campos diferentes: PhrasePositions.doc, PhrasePositions.position y PhrasePositions.offset.
Uso 3: Desarrollo de juegos
No he terminado de buscar ejemplos de uso en este espacio, pero vi bastantes ejemplos de .NET PriorityQueue personalizados que se utilizan en el desarrollo de juegos. En un sentido muy general, estos tendían a agruparse en torno a la búsqueda de rutas como el caso de uso principal (el de Dijkstra). Puede encontrar mucha gente preguntando cómo implementar algoritmos de búsqueda de rutas en .NET debido a Unity 3D .
Todavía necesito excavar en esta área; vi algunos ejemplos con prioridad explícita en cola y algunos ejemplos usando Comparer / IComparable.
En una nota separada, ha habido una discusión sobre elementos únicos, eliminar elementos explícitos y determinar si existe un elemento específico.
Las colas, como estructura de datos, generalmente admiten la puesta en cola y la eliminación de la cola. Si seguimos el camino de proporcionar otras operaciones similares a conjuntos / listas, me pregunto si en realidad estamos diseñando una estructura de datos completamente diferente, algo parecido a una lista ordenada de tuplas. Si una persona que llama tiene una necesidad que no sea Enqueue, Dequeue, Peek, ¿tal vez necesita algo más que una cola de prioridad? Cola, por definición, implica la inserción en una cola y la eliminación ordenada de la cola; no mucho más.
@ebickle
Agradezco su esfuerzo en revisar otros repositorios y verificar cómo se entregó allí la funcionalidad de la cola de prioridad. Sin embargo, IMO esta discusión se beneficiaría de proporcionar propuestas de diseño específicas . Este hilo ya es difícil de seguir y contar una historia larga sin conclusiones lo hace aún más difícil.
Las colas [...] generalmente admiten la puesta en cola y la retirada de la cola. [...] Si una persona que llama tiene una necesidad que no sea Enqueue, Dequeue, Peek, ¿tal vez necesita algo más que una cola de prioridad? Cola, por definición, implica la inserción en una cola y la eliminación ordenada de la cola; no mucho más.
Gran discusión. ¡La investigación de @ebickle es extremadamente útil en mi opinión!
@ebickle ¿tiene
Parece que necesitamos Try*
variantes de arriba + IsEmpty
+ TryPeek
/ TryDequeue
+ EnqueueOrUpdate
? ¿Pensamientos?
`` c #
public bool IsEmpty ();
public bool TryPeek(ref TElement element, ref TPriority priority); // false if empty
public bool TryDequeue(ref TElement element, ref TPriority priority); // false if empty
public bool TryEnqueue(TElement element, TPriority priority); // false if it is duplicate (doe NOT update it)
public void EnqueueOrUpdate(TElement element, TPriority priority); // TODO: Should return bool status for enqueued vs. updated?
public bool TryUpdate(TElement element, TPriority priority); // false if element does not exist (does NOT add it)
public bool TryRemove(TElement element); // false if element does not exist
''
@karelz
Parece que necesitamos variantes de Try * de arriba
Sí exactamente.
¿Debería devolver el estado bool para en cola frente a actualizado?
Si queremos devolver estados en todas partes, entonces en todas partes. Para este método en particular, creo que debería ser cierto si cualquiera de las operaciones tuvo éxito ( Enqueue
o Update
).
Por lo demás, simplemente estoy de acuerdo: sonríe:
Solo una pregunta: ¿por qué ref
lugar de out
? No estoy seguro:
Si queremos devolver estados en todas partes, entonces en todas partes. Para este método en particular, creo que debería ser cierto si cualquiera de las operaciones tuvo éxito (
Enqueue
oUpdate
).
Siempre volvería cierto, eso no es una buena idea. Deberíamos usar void
en tal caso, en mi opinión. De lo contrario, la gente se confundirá y comprobará el valor de retorno y agregará un código inútil que nunca se ejecutará. (A menos que me haya perdido algo)
ref vs out de acuerdo, lo debatí yo mismo. No tengo una opinión sólida / experiencia suficiente para tomar una decisión por mí mismo. Podemos preguntar a los revisores de API / esperar más comentarios.
Siempre volvería verdad
Tienes razón, mi mal. Perdón.
No tengo una opinión sólida / experiencia suficiente para tomar una decisión por mí mismo.
Tal vez me esté perdiendo algo, pero lo encuentro bastante simple. Si usamos ref
, básicamente estamos diciendo que Peek
y Dequeue
quieren usar de alguna manera los TElement
y TPriority
pasaron ( Me refiero a leer esos campos). Lo cual no es realmente el caso: se supone que nuestros métodos solo asignan valores a esas variables (y de hecho el compilador requiere que lo hagan).
Publicación principal actualizada con mis Try*
API
Se agregaron 2 preguntas abiertas:
Peek
y Dequeue
?TryPeek
y TryDequeue
- ¿deberían usar ref
o out
args?ref vs. out - Tienes razón. Estaba optimizando para evitar la inicialización en caso de que devolviéramos falso. Eso fue estúpido y tomado por sorpresa de mi parte: optimización prematura. Lo cambiaré a out y eliminaré la pregunta.
6: Puede que me esté perdiendo algo, pero si no lanzo una excepción, ¿qué debería hacer Peek
o Dequeue
si la cola está vacía? Supongo que esto comienza a plantear la pregunta de si la estructura de datos debería aceptar null
(preferiría no hacerlo, pero no tengo una opinión firme). Incluso si permitimos null
, los tipos de valor no tienen null
( default
ciertamente no cuentan), por lo que Peek
y Dequeue
tienen no hay forma de transmitir un resultado sin sentido, y debidamente creo que debe lanzar una excepción (¡eliminando así cualquier preocupación sobre los parámetros out
!). No veo ninguna razón para no seguir el ejemplo de los Queue.Dequeue
Solo agregaré que la búsqueda de Dijkstra (también conocida como búsqueda heurística sin una heurística) no debería requerir ningún cambio de prioridad. Nunca he deseado actualizar la prioridad de nada, y hago búsquedas heurísticas todo el tiempo. (el punto del algoritmo es que una vez que ha explorado un estado, sabe que ha explorado la mejor ruta hacia él; de lo contrario, corre el riesgo de no ser óptimo, por lo tanto, nunca podría mejorar la prioridad, y ciertamente no lo hace. Desea disminuirlo (es decir, considerar una ruta peor hacia él). _Ignorando los casos en los que elige intencionalmente la heurística de manera que no sea óptima, en cuyo caso, por supuesto, puede obtener un resultado no óptimo, pero aún así nunca actualizar una prioridad_)
Si no lanza una excepción, ¿qué deben hacer Peek o Dequeue si la cola está vacía?
Verdadero.
¡eliminando así cualquier preocupación sobre los parámetros de salida!
¿Cómo? Bueno, los parámetros out
son para los métodos TryPeek
y TryDequeue
. Los que arrojan una excepción son Peek
y Dequeue
.
Solo agregaré que Dijkstra no debería requerir ningún cambio de prioridad.
Puede que esté equivocado, pero que yo sepa, el algoritmo de Dijkstra hace uso de la operación DecreaseKey
. Vea, por ejemplo, esto . Si esto es eficiente o no, es un aspecto diferente. De hecho, el montón de Fibonacci fue diseñado de una manera que logra la operación DecreaseKey
asintóticamente en O (1) (para mejorar Dijkstra).
Pero aún así, lo que es importante en nuestra discusión: poder actualizar un elemento en una cola de prioridad es muy útil y hay personas que buscan dicha funcionalidad (consulte las preguntas vinculadas anteriormente en StackOverflow). Yo también lo he usado algunas veces.
Lo siento, sí, veo el problema con out
params ahora, malinterpretando las cosas nuevamente. Y parece que una variante de Dijkstra (un nombre que parece aplicarse de manera bastante más amplia de lo que creía ...) se puede implementar quizás de manera más eficiente (si su cola ya contiene los Elementos, presumiblemente hay beneficios para las búsquedas repetibles) con prioridades actualizables. Eso es lo que obtengo por usar palabras y nombres largos. Tenga en cuenta que no estoy proponiendo que eliminemos el Update
, sería bueno también tener un Heap
más delgado u otro (es decir, la propuesta original) sin las restricciones que impone esta capacidad (tan glorioso una capacidad que yo aprecio).
7: Deben ser out
. Cualquier entrada nunca podría tener ningún significado, por lo que no debería existir (es decir, no deberíamos ir con ref
). Con out
params, no vamos a mejorar la devolución de valores default
. Esto es lo que hace Dictionary.TryGetValue
, y no veo ninguna razón para hacer lo contrario. _ Dicho esto, podría tratar el ref
como valor o predeterminado, pero si no tiene un valor predeterminado significativo, frustra las cosas.
Discusión de revisión de API:
Aquí están las notas crudas clave:
ImmutableCollections
en el pasado (el ciclo de lanzamiento de vista previa rápida fue clave y útil para dar forma a las API).MultiValueDictionary
que ya está en CoreFxLab. TODO: Compruebe si tenemos más candidatos, no queremos publicar en el blog cada uno de ellos por separado.TElement
de Peek
& Dequeue
, no devuelva TPriority
(los usuarios pueden usar los métodos Try*
para eso).Update
- el usuario puede Remove
y luego Enqueue
artículo de vuelta con diferente prioridad.Remove
debe eliminar solo el primer elemento encontrado (como lo hace List
).IQueue
para abstraer Queue
y PriorityQueue
( Peek
y Dequeue
devolviendo solo TElement
ayuda aquí)Tuvimos una larga discusión sobre el selector, todavía no estamos decididos (¿puede ser requerido por IQueue
arriba?). A la mayoría no le gustó el selector, pero las cosas pueden cambiar en una futura reunión de revisión de API.
No se discutieron otras cuestiones abiertas.
La última propuesta me parece bastante buena. Mis opiniones / preguntas:
Peek
y Dequeue
usaron out
parámetros para priority
, entonces también podrían tener sobrecargas que no devuelvan prioridad en absoluto, lo que creo que simplificaría uso común. Aunque la prioridad también se puede ignorar utilizando un descarte, lo que hace que esto sea menos importante.PriorityQueue<T>
separado? ¿O un conjunto de métodos estáticos y de extensión que funcionan con PriorityQueue<T, T>
?priorityQueue.Select(t => t.element)
?Dictionary
en el tipo de elemento, ¿debería haber una opción para pasar un IEqualityComparer<TElement>
?Dictionary
es una sobrecarga innecesaria si nunca necesito actualizar las prioridades. ¿Debería haber una opción para desactivarlo? Aunque esto se puede agregar más adelante, si resulta que es útil.@karelz
Estabilidad (para las mismas prioridades): los elementos con las mismas prioridades deben devolverse en el orden en que se insertaron en la cola (comportamiento general de la cola)
Creo que esto significaría que internamente, la prioridad tendría que ser algo así como un par de (priority, version)
, donde version
se incrementa por cada adición. Creo que eso aumentaría significativamente el uso de memoria de la cola de prioridad, especialmente considerando que version
probablemente tendría que ser de 64 bits. No estoy seguro de que valga la pena.
No queremos evitar valores duplicados (similar a otras colecciones):
Deshazte deUpdate
- el usuario puedeRemove
y luegoEnqueue
artículo de vuelta con diferente prioridad.
Dependiendo de la implementación, Update
probablemente será mucho más eficiente que Remove
seguido de Enqueue
. Por ejemplo, con el montón binario (creo que el montón cuaternario tiene las mismas complejidades de tiempo), Update
(con valores únicos y un diccionario) es O (log n ), mientras que Remove
(con valores duplicados y sin diccionario) es O ( n ).
@pgolebiowski
Totalmente de acuerdo en que debemos centrarnos en las propuestas aquí; Hice la propuesta de API original y la publicación original. En enero, @karelz pidió algunos ejemplos de uso específicos; eso abrió una pregunta mucho más amplia sobre la necesidad y el uso específicos de una API PriorityQueue. Tenía mi propia implementación de PriorityQueue y la usé en algunos proyectos y sentí que algo similar sería útil en BCL.
Lo que me faltaba en la publicación original era una amplia encuesta sobre el uso de la cola existente; Los ejemplos del mundo real ayudarán a mantener el diseño bien fundamentado y garantizarán que se pueda utilizar ampliamente.
@karelz
Lucene.net contiene al menos una función de comparación de cola de prioridad (similar a Comparación
Propuesta:
1) PriorityQueue
2) PriorityQueue
Ambos tipos admiten elementos duplicados. Necesidad de determinar si garantizamos FIFO para elementos de la misma prioridad; probablemente no si apoyamos las actualizaciones prioritarias.
Cree ambas variantes en CoreFxLabs como sugirió @karelz y solicite comentarios.
Preguntas:
public void Enqueue(TElement element, TPriority priority); // Throws if it is duplicate`
public bool TryEnqueue(TElement element, TPriority priority); // Returns false if it is duplicate (does NOT update it)
¿Por qué no hay duplicados?
public (TElement element, TPriority priority) Peek(); // Throws if empty
public (TElement element, TPriority priority) Dequeue(); // Throws if empty
public void Remove(TElement element); // Throws if element does not exist
Si estos métodos son deseables, ¿utilizarlos como métodos de extensión?
public void Enqueue(TElement element);
public void Update(TElement element);
Debería tirar; pero ¿por qué no probar la variante, también los métodos de extensión?
¿Enumerador basado en estructuras?
@benaadams throw on dupes fue sugerido originalmente (por mí) para evitar el tipo de asa fea. La última actualización de la revisión de la API revierte eso.
No debemos agregar métodos como métodos de extensión cuando podemos agregarlos en el tipo. Los métodos de extensión son una copia de seguridad si no podemos agregarlos en el tipo (por ejemplo, es una interfaz o queremos entregarlo a .NET Framework más rápido).
Dupes: Si tiene varias entradas que son iguales, ¿solo necesita actualizar una? ¿Entonces se vuelven diferentes?
Métodos de lanzamiento: ¿básicamente son envoltorios y lo serán?
public void Remove(TElement element)
{
if (!TryRemove(element))
{
throw new Exception();
}
}
¿Aunque su control fluye a través de excepciones?
@benaadams verifique mi respuesta con las notas de revisión de API: https://github.com/dotnet/corefx/issues/574#issuecomment -308206064
Update
- el usuario puede Remove
y luego Enqueue
artículo de vuelta con diferente prioridad.Remove
debe eliminar solo el primer elemento encontrado (como lo hace List
).¿Aunque su control fluye a través de excepciones?
No estoy seguro de lo que quieres decir. Parece un patrón bastante común en BCL.
¿Aunque su control fluye a través de excepciones?
No estoy seguro de lo que quieres decir.
Si solo tiene métodos TryX
Sin participación de excepción; pero el nombre te anima a tomar la decisión de tirar la devolución.
Si tiene métodos de lanzamiento de excepciones que no son de prueba
Así que está en el anti-patrón de usar el manejo de excepciones para el control de flujo . Simplemente parecen un poco superfluos; Siempre se puede agregar un tiro si es falso para recrearlo.
Parece un patrón bastante común en BCL.
Los métodos TryX no eran un patrón común en el BCL original; hasta los tipos concurrentes (aunque piensa que Dictionary.TryGetValue
en 2.0 puede haber sido el primer ejemplo?)
por ejemplo, los métodos TryX se acaban de agregar a Core for Queue y Stack y aún no forman parte de Framework.
IComparer
. Si el cliente desea tener estabilidad, puede agregarla fácilmente por sí mismo. Sería fácil construir StablePriorityQueue
sobre nuestra implementación, usando el enfoque ordinario para almacenar la "edad" de un elemento y usarlo durante las comparaciones.Entonces hay conflictos de API. Consideremos ahora un IQueue<T>
simple para que funcione con el Queue<T>
:
public interface IQueue<T> :
IEnumerable,
IEnumerable<T>,
IReadOnlyCollection<T>
{
void Enqueue(T element);
T Peek();
T Dequeue();
bool TryPeek(out T element);
bool TryDequeue(out T element);
}
Tenemos dos opciones para la cola de prioridad:
IQueue<TElement>
.IQueue<(TElement, TPriority)>
.Escribiré las implicaciones de ambos caminos usando simplemente números (1) y (2).
En la cola de prioridad, necesitamos poner en cola tanto un elemento como su prioridad (solución actual). En el montón ordinario, agregamos solo un elemento.
Enqueue(TElement)
, lo cual es bastante extraño para una cola de prioridad si estamos insertando un elemento sin una prioridad. Entonces nos vemos obligados a hacer ... ¿qué? Suponga default(TPriority)
? No ...Enqueue((TElement, TPriority) element)
. Básicamente, agregamos dos argumentos aceptando una tupla creada a partir de ellos. Probablemente no sea la API ideal.Quería que esos métodos devolvieran solo TElement
.
(TElement, TPriority)
.Podríamos mitigar algunos de los problemas proporcionando PriorityQueue<T>
, donde no hay una prioridad física separada.
IComparable
. Lo que agrega mucho código bastante repetitivo (y un nuevo archivo en su código fuente, muy probablemente).Si entregamos dos colas de prioridad, la solución general sería más poderosa y flexible. Hay algunas notas para tomar:
PriorityQueue<T>
potencialmente podría implementar IQueue<T>
.PriorityQueue<TElement, TPriority>
expondría una API extraña si intenta implementar la interfaz IQueue
.Bueno ... funcionaría . Aunque no es ideal.
Dada la suposición de que queremos permitir duplicados y no queremos utilizar el concepto de un identificador:
Básicamente, esto conduce a lo que está en Java, donde los usuarios tienen que proporcionar su propia implementación de toda la estructura de datos si quieren poder actualizar las prioridades en sus colas de prioridad sin iterar ingenuamente a través de toda la colección cada vez.
Suponiendo que no queremos agregar un nuevo tipo de identificador, podemos hacerlo de manera diferente para poder tener el soporte completo para actualizar y eliminar elementos (además de hacerlo de manera eficiente). La solución es agregar métodos alternativos:
void Enqueue(TElement element, TPriority priority, out object handle);
void Update(object handle, TPriority priority);
void Remove(object handle);
Estos serían métodos para aquellos que quieran tener un control adecuado sobre la actualización y eliminación de elementos (y no lo hagan en O (n), como si fuera un List
: stick_out_tongue_winking_eye :).
Pero mejor, consideremos ...
Alternativamente, podemos eliminar esta funcionalidad de la cola de prioridad por completo y agregarla al montón en su lugar. Esto tiene numerosos beneficios:
Heap
.PriorityQueue
- para las personas que desean que su código simplemente funcione .PriorityQueue
estable ahora, ya no es una compensación.Heap
. También serían conscientes de las limitaciones de PriorityQueue
y, por lo tanto, podrían usar Heap
lugar (por ejemplo, si quieren tener más control sobre la actualización / eliminación de elementos o no quieren los datos estructura estable a expensas de la velocidad y el uso de memoria).IQueue
común, porque la potencia y la funcionalidad se incluirían en el campo del montón. La API de PriorityQueue
podría enfocarse en hacerla compatible con Queue
través de una abstracción.IPriorityQueue
(más bien nos concentramos en mantener la funcionalidad de PriorityQueue
y Queue
similar). En su lugar, podemos agregarlo en el área del montón, y esencialmente tener IHeap
allí. Esto es genial, porque permite a las personas construir bibliotecas de terceros sobre lo que está en la biblioteca estándar. Y se siente bien, porque nuevamente, consideramos que los montones son más avanzados que las colas de prioridad , por lo que esa área proporcionaría extensiones. Tales extensiones tampoco sufrirían las opciones que haríamos en PriorityQueue
, porque serían independientes.IHeap
para el PriorityQueue
.Heap
más poderoso y eficiente a nuestro mando!Básicamente, la cola de prioridad se centraría principalmente en la facilidad de uso, a expensas de: rendimiento, potencia y flexibilidad. El montón sería para aquellos que conocen el rendimiento y las implicaciones funcionales de nuestras decisiones en la cola de prioridad. Mitigamos muchos problemas con compensaciones.
Si estamos a favor de hacer un experimento, creo que ahora es posible. Preguntémosle a la comunidad. No todo el mundo lee este hilo; podríamos generar otros comentarios valiosos y escenarios de uso. ¿Qué piensas? Personalmente, me encantaría esa solución. Creo que haría felices a todos.
Nota importante: si queremos un enfoque de este tipo, debemos diseñar la cola de prioridad y el montón juntos . Porque sus propósitos serían diferentes y una solución proporcionaría lo que la otra no proporcionaría.
Con el enfoque presentado anteriormente, para que la cola de prioridad implemente IQueue<T>
(para que tenga sentido), y suponiendo que dejamos de lado el soporte para el selector allí, tendría que tener un tipo genérico. Aunque eso significaría que los usuarios deben proporcionar un contenedor si tienen (user data, priority)
separado, esta solución sigue siendo intuitiva. Y lo más importante, permite todos los formatos de entrada (por eso hay que hacerlo de esta forma si dejamos caer el selector). Sin el selector, Enqueue(TElement, TPriority)
no permitiría tipos que ya sean comparables. Un solo tipo genérico también es crucial para la enumeración, por lo que este método se puede incluir en IQueue<T>
.
@svick
¿Está definido en qué orden se devuelven los elementos al enumerar? Supongo que no está definido para que sea eficaz.
Para tener el orden al enumerar, esencialmente necesitamos ordenar la colección. Es por eso que sí, debería estar indefinido, ya que el cliente simplemente puede ejecutar OrderBy
ellos mismos y lograr aproximadamente el mismo rendimiento (pero algunas personas no lo necesitan).
Idea: en la cola de prioridad esto podría estar ordenado, en el montón desordenado. Se siente mejor. De alguna manera, una cola de prioridad se siente como iterarla en orden. Un montón definitivamente no. Otro beneficio del enfoque anterior.
@pgolebiowski
Todo eso parece muy cuerdo. ¿Le importaría aclarar, en la sección Delivering IQueue then
, está sugiriendo T : IComparable<T>
para "un tipo genérico" como una alternativa al selector dada la tolerancia de elementos duplicados?
Apoyaría tener los dos tipos separados.
No entiendo la razón de ser del uso de object
como tipo de identificador: ¿es esto solo para evitar la creación de un nuevo tipo? Definir un nuevo tipo proporcionaría una sobrecarga de implementación mínima, al tiempo que haría que la API sea más difícil de usar incorrectamente (¿qué me impide intentar pasar un string
a Remove(object)
?), Y más fácil de usar (¿qué me impide intentar para pasar el elemento en sí mismo a Remove(object)
, ¿y quién podría culparme por intentarlo?).
Propongo la adición de un tipo ficticio con el nombre adecuado, para reemplazar object
en los métodos de control, en aras de una interfaz más expresiva.
Si la debugabilidad triunfa sobre la sobrecarga de la memoria, el tipo de identificador podría incluso incluir información sobre a qué cola pertenece (tendría que convertirse en Genérico, lo que aumentaría la seguridad del tipo de la interfaz), proporcionando excepciones útiles en la línea de ("Se creó el identificador suministrado por una cola diferente "), o si ya se ha consumido (" El elemento al que hace referencia el identificador ya se ha eliminado ").
Si la idea del identificador sigue adelante, propondría que si esta información se considera útil, los métodos TryRemove
y TryUpdate
correspondientes también arrojarían un subconjunto de estas excepciones, excepto en el caso de que El elemento ya no está presente, ya sea porque ha sido retirado de la cola o eliminado por el identificador. Esto implicaría un tipo de identificador menos aburrido, genérico y con un nombre adecuado.
@VisualMelon
¿Le importaría aclarar, en la sección
Delivering IQueue then
, está sugiriendoT : IComparable<T>
para "un tipo genérico" como una alternativa al selector dada la tolerancia de elementos duplicados?
Perdón por no dejar esto en claro.
PriorityQueue<T>
, sin restricciones en los T
.IComparer<T>
.T
ya es comparable, entonces simplemente se asumirá Comparer<T>.Default
(y puede llamar al constructor predeterminado de la cola de prioridad sin argumentos).El selector tenía un propósito diferente: poder consumir todo tipo de datos de usuario. Hay varias configuraciones:
El caso raro no sería posible en PriorityQueue<T>
, pero eso no importa mucho. Lo importante es que ahora podemos manejar (1), (2) y (3). Sin embargo, si tuviéramos dos tipos genéricos, tendríamos que tener un método como Enqueue(TElement, TPrioriity)
. Eso nos limitaría a (1) solamente. (2) conduciría a la redundancia. (3) sería increíblemente feo. Hay un poco más de esto en la sección IQueue > Enqueue
anterior (segundo método Enqueue
y default(TPriority
).
Espero que ahora esté más claro.
Por cierto, asumiendo tal solución, diseñar la API de PriorityQueue<T>
y IQueue<T>
sería trivial. Simplemente tome algunos de los métodos en Queue<T>
, tírelos a IQueue<T>
y haga que PriorityQueue<T>
implemente. ¡Tadaa! 😄
No entiendo la razón por la que se usa
object
como tipo de identificador: ¿es esto solo para evitar la creación de un nuevo tipo?
PriorityQueue<T>
residan en System.Collections.Generic
, mientras que la funcionalidad del montón estaría en System.Collections.Specialized
. Allí, tendríamos una mayor probabilidad de introducir un tipo de este tipo, y eventualmente tendríamos la maravillosa detección de errores en tiempo de compilación.Si la debugabilidad triunfa sobre la sobrecarga de la memoria, el tipo de identificador podría incluso incluir información sobre a qué cola pertenece (tendría que convertirse en Genérico, lo que aumentaría la seguridad del tipo de la interfaz), proporcionando excepciones útiles en la línea de ("Se creó el identificador suministrado por una cola diferente "), o si ya se ha consumido (" El elemento al que hace referencia el identificador ya se ha eliminado ").
Si la idea del mango sigue adelante, propondría que si esta información se considera útil ...
Definitivamente más es posible entonces: guiño :
Creo que la idea de crear dos clases tiene mucho sentido y resuelve muchos problemas. Lo único que tengo es que podría tener sentido que PriorityQueue<T>
use Heap<T>
internamente y simplemente "oculte" su funcionalidad avanzada.
Así que básicamente...
IQueue<T>
public interface IQueue<T> :
IEnumerable,
IEnumerable<T>,
IReadOnlyCollection<T>
{
int Count { get; }
void Clear();
bool Contains(T element);
bool IsEmpty();
void Enqueue(T element);
T Peek();
T Dequeue();
bool TryPeek(out T element);
bool TryDequeue(out T element);
}
System.Collections.Generic
.Remove
y TryRemove
). Sin embargo, no está en Queue<T>
. Pero no es necesario.PriorityQueue<T>
public class PriorityQueue<T> : IQueue<T>
{
public PriorityQueue();
public PriorityQueue(IComparer<T> comparer);
public PriorityQueue(IEnumerable<T> collection);
public PriorityQueue(IEnumerable<T> collection, IComparer<T> comparer);
public IComparer<T> Comparer { get; }
public int Count { get; }
public bool IsEmpty();
public void Clear();
public bool Contains(T element);
public void Enqueue(T element);
public T Peek();
public T Dequeue();
public bool TryPeek(out T element);
public bool TryDequeue(out T element);
public void Remove(T element);
public bool TryRemove(T element);
public IEnumerator<T> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
}
System.Collections.Generic
.IComparer<T>
no se entrega, se invoca Comparer<T>.Default
.Remove
y TryRemove
eliminan solo la primera aparición (si la encuentran).No escribo todo aquí ahora, pero:
System.Collections.Specialized
.De acuerdo con el IQueue
La especificación de IQueue
Considere agregar "int Count {get;}" a IQueue
En la valla con respecto a TryPeek, TryDequeue en IQueue
IsEmpty parece un caso atípico; no muchos otros tipos de colecciones en la BCL lo tienen. Para agregarlo a la interfaz, tendríamos que asumir que se agregará a la cola
Suelta TryRemove y cambia Remove a "bool Remove". Mantener la alineación con las otras clases de colección será importante aquí: los desarrolladores tendrán mucha memoria muscular que dice "eliminar () en una colección no arroja". Es un área que muchos desarrolladores no probarán bien y causará muchas sorpresas si se cambia el comportamiento normal.
De su cita anterior @pgolebiowski
- Los datos del usuario están separados de la prioridad (dos instancias físicas).
- Los datos del usuario contienen la prioridad.
- Los datos del usuario son la prioridad.
- Caso raro: la prioridad se puede obtener a través de alguna otra lógica (reside en un objeto diferente de los datos del usuario).
También recomiendo considerar 5. Los datos del usuario contienen la prioridad en múltiples campos (como vimos en Lucene.net)
En la valla con respecto a TryPeek, TryDequeue en IQueue considerando que no están en Queue
Son System / Collections / Generic / Queue.cs # L253-L295
Por otro lado, Queue no tiene
c#
public void Remove(T element);
public bool TryRemove(T element);
Considere agregar "int Count {get;}" a IQueue para que quede claro que se desea Count sin importar si heredamos o no de IReadOnlyCollection.
está bien. Lo modificará.
IsEmpty parece un caso atípico; no muchos otros tipos de colecciones en la BCL lo tienen.
Este fue agregado por @karelz , solo lo copié. Sin embargo, me gusta, podría considerarse en la revisión de la API :)
Suelta TryRemove y cambia Remove a "bool Remove".
Creo que Remove
y TryRemove
es consistente con otros métodos como ese ( Peek
y TryPeek
o Dequeue
y TryDequeue
).
- Los datos del usuario contienen la prioridad en varios campos
Es un punto válido, pero esto de hecho también se puede manejar con un selector (después de todo, es cualquier función), pero eso es por montones.
IsEmpty parece un caso atípico; no muchos otros tipos de colecciones en la BCL lo tienen.
FWIW, bool IsEmpty { get; }
es algo que desearía haber agregado a IProducerConsumerCollection<T>
, y lamenté no haber estado allí en múltiples ocasiones desde entonces. Sin él, los contenedores a menudo necesitan hacer el equivalente a Count == 0
, que para algunas colecciones es significativamente menos eficiente de implementar, en particular en la mayoría de las colecciones concurrentes.
@pgolebiowski ¿Qué
@svick
Si
Peek
yDequeue
usaronout
parámetros para la prioridad, entonces también podrían tener sobrecargas que no devuelvan prioridad en absoluto, lo que creo que simplificaría el uso común
Acordado. Vamos a agregarlos.
¿Debería haber alguna forma de enumerar solo los elementos en la cola de prioridad, ignorando las prioridades?
Buena pregunta. ¿Qué propondrías? IEnumerable<TElement> Elements { get; }
?
Estabilidad: creo que esto significaría que internamente, la prioridad debería ser algo así como un par de
(priority, version)
Creo que podemos evitarlo considerando Update
como Remove
+ Enqueue
lógicos. Siempre agregaríamos elementos al final de los elementos de la misma prioridad (básicamente, considere el resultado del comparador 0 como -1). En mi opinión, eso debería funcionar.
@benaadams
Los métodos TryX no eran un patrón común en el BCL original; hasta los tipos concurrentes (aunque piensa que
Dictionary.TryGetValue
en 2.0 puede haber sido el primer ejemplo?)
por ejemplo, los métodos TryX se acaban de agregar a Core for Queue y Stack y aún no forman parte de Framework.
Admito que todavía soy nuevo en BCL. De las reuniones de revisión de API y del hecho de que agregamos un montón de métodos Try*
recientemente, tuve la impresión de que es un patrón común durante mucho más tiempo 😉.
De cualquier manera, ahora es un patrón común y no debemos tener miedo de usarlo. El hecho de que el patrón aún no esté en .NET Framework no debería detenernos a innovar en .NET Core; ese es su propósito principal, innovar más rápido.
@pgolebiowski
Alternativamente, podemos eliminar esta funcionalidad de la cola de prioridad por completo y agregarla al montón en su lugar. Esto tiene numerosos beneficios.
Hmm, algo me dice que podrías tener una agenda aquí 😆
Ahora, en serio, en realidad es una buena dirección a la que apuntamos todo el tiempo: PriorityQueue
nunca fue una razón para NO hacer Heap
. Si todos estamos de acuerdo con el hecho de que Heap
podría no ingresar a CoreFX y permanecer "solo" en el repositorio de CoreFXExtensions como estructura de datos avanzada junto con PowerCollections, estoy de acuerdo con eso,
Nota importante: si queremos un enfoque de este tipo, debemos diseñar la cola de prioridad y el montón juntos. Porque sus propósitos serían diferentes y una solución proporcionaría lo que la otra no proporcionaría.
No veo por qué tenemos que hacerlo juntos. En mi opinión, podemos centrarnos en PriorityQueue
y agregar Heap
"adecuados" en paralelo / más tarde. No me importa si alguien los hace juntos, pero no veo ninguna razón sólida por la que la existencia de PriorityQueue
fáciles de usar debería afectar el diseño del avanzado Heap
"adecuado / de alto rendimiento"
IQueue
Gracias por escribir. Teniendo en cuenta sus puntos, no creo que debamos salirnos de nuestro camino para agregar IQueue
. En mi opinión, es bueno tenerlo. Si tuviéramos un selector, sería natural. Sin embargo, no me gusta el enfoque del selector, ya que trae rareza y complicación al describir cuándo el selector es llamado por PriorityQueue
(solo en Enqueue
y Update
.
Mango alternativo (objeto)
En realidad, no es una mala idea (aunque un poco fea) tener tales sobrecargas en mi opinión. Tendríamos que poder detectar que el identificador es incorrecto PriorityQueue
, que es O (log n).
Tengo la sensación de que los revisores de API lo rechazarán, pero en mi opinión, vale la pena intentarlo para experimentar ...
Estabilidad
No creo que la estabilidad venga con una sobrecarga de perf / memoria (asumiendo que ya bajamos Update
o que tratamos Update
como lógicos Remove
+ Enqueue
, así que básicamente restablecemos la edad del elemento). Simplemente trate el resultado del comparador 0 como -1 y todo está bien ... ¿O me estoy perdiendo algo?
IQueue<T>
Podría ser una buena idea tener 2 propuestas (y posiblemente aceptemos ambas):
PriorityQueue<T,U>
sin selector y sin IQueue
(lo cual sería engorroso)PriorityQueue<T>
con selector y IQueue
Mucha gente insinuó eso arriba.
Re: Última propuesta de @pgolebiowski - https://github.com/dotnet/corefx/issues/574#issuecomment -308427321
IQueue<T>
- podríamos agregarRemove
yTryRemove
, peroQueue<T>
no los tiene.
¿Tendría sentido añadirlos a Queue<T>
? ( @ebickle está de acuerdo) En caso afirmativo, también deberíamos agrupar la adición.
Agreguémoslo en la interfaz y digamos que son cuestionables / también necesitan Queue<T>
adición.
Lo mismo para IsEmpty
- lo que necesite Queue<T>
y Stack<T>
adiciones, marquémoslo así en la interfaz (será más fácil de revisar y digerir).
@pgolebiowski , ¿puede agregar un comentario a IQueue<T>
con la lista de clases que creemos que implementará?
PriorityQueue<T>
Agreguemos un enumerador de estructuras (creo que es un patrón BCL común últimamente ). Fue llamado un par de veces en el hilo, luego abandonado / olvidado.
Muchísimo
Elección del espacio de nombres: no perdamos tiempo todavía en la decisión del espacio de nombres. Si Heap
termina en CoreFxExtensions, aún no sabemos qué tipo de espacios de nombres permitiremos allí. Quizás Community.*
o algo así. Depende del resultado de las discusiones del modo de operación / propósito de CoreFxExtensions.
Nota: Una idea para el repositorio de CoreFxExtensions es otorgar permisos de escritura probados a los miembros de la comunidad y dejar que sea impulsado principalmente por la comunidad con el equipo .NET brindando solo consejos (incluida la experiencia en revisión de API) y el equipo .NET / MS como árbitro cuando sea necesario. Si ahí es donde aterrizamos, probablemente no lo querríamos en el espacio Microsoft.*
nombres System.*
o Microsoft.*
. (Descargo de responsabilidad: pensamiento temprano, no saque conclusiones precipitadas todavía, hay otras ideas alternativas de gobernanza en vuelo)
Suelta
TryRemove
y cambiaRemove
abool Remove
. Mantener la alineación con las otras clases de colección será importante aquí: los desarrolladores tendrán mucha memoria muscular que dice "eliminar () en una colección no arroja". Es un área que muchos desarrolladores no probarán bien y causará muchas sorpresas si se cambia el comportamiento normal.
👍 Definitivamente deberíamos considerar al menos alinearlo con otras colecciones. ¿Alguien puede escanear otras colecciones cuál es su patrón Remove
?
@ebickle ¿Qué te
Alojémoslo directamente en
@karelz
¿Debería haber alguna forma de enumerar solo los elementos en la cola de prioridad, ignorando las prioridades?
Buena pregunta. ¿Qué propondrías?
IEnumerable<TElement> Elements { get; }
?
Sí, eso o una propiedad que devuelve algún tipo de ElementsCollection
es probablemente la mejor opción. Especialmente porque es similar a Dictionary<K, V>.Values
.
Siempre agregaríamos elementos al final de los elementos de la misma prioridad (básicamente, considere el resultado del comparador 0 como -1). En mi opinión, eso debería funcionar.
No es así como funcionan los montones, no hay "fin de elementos de la misma prioridad". Los elementos con la misma prioridad se pueden distribuir por todo el montón (a menos que estén cerca de ser el mínimo actual).
O, dicho de otra manera, para mantener la estabilidad, debe considerar que el resultado del comparador de 0 es -1 no solo al insertar, sino también cuando los elementos se mueven en el montón después. Y creo que tienes que almacenar algo como version
junto con priority
para hacerlo correctamente.
IQueue
- podríamos agregar Remove y TryRemove, pero Queue no los tiene.
¿Tendrían sentido agregarlos a la cola?
? ( @ebickle está de acuerdo) En caso afirmativo, también deberíamos agrupar la adición.
No creo que Remove
deba agregarse a IQueue<T>
; e incluso sugeriría que Contains
es dudoso; limita la utilidad de la interfaz y para qué tipos de cola se puede utilizar a menos que empiece a lanzar también NotSupportedExceptions. es decir, ¿cuál es el alcance?
¿Es solo para colas de vainilla, colas de mensajes, colas distribuidas, colas de ServiceBus, colas de almacenamiento de Azure, colas confiables de ServiceFabric, etc. (pasando por alto algunos de ellos que prefieren métodos asíncronos)?
No es así como funcionan los montones, no hay "fin de elementos de la misma prioridad". Los elementos con la misma prioridad se pueden distribuir por todo el montón (a menos que estén cerca de ser el mínimo actual).
Punto justo. Estaba pensando en ello como un árbol de búsqueda binario. Tengo que cepillar mis conceptos básicos de DS, supongo :)
Bueno, o lo implementamos con diferentes ds (matriz, árbol de búsqueda binaria (o el nombre oficial que sea) - @stephentoub tiene ideas / sugerencias), o nos conformamos con un orden aleatorio. No creo que el mantenimiento del campo version
o age
valga la garantía de estabilidad.
@ebickle ¿Qué te
@karelz Vamos a alojarlo directamente en CoreFxLabs.
Por favor, eche un vistazo a este documento .
Podría ser una buena idea tener 2 propuestas (y posiblemente aceptemos ambas):
- PriorityQueue
sin selector y sin IQueue (lo cual sería engorroso) - PriorityQueue
con selector y IQueue
Me deshice del selector por completo. Solo es necesario si queremos tener un PriorityQueue<T, U>
unificado. Si tenemos dos clases, bueno, un comparador es suficiente.
Por favor, avíseme si encuentra algo que valga la pena agregar / cambiar / eliminar.
@pgolebiowski
El documento se ve bien. Comenzando a sentirse como una solución sólida que esperaría ver en las bibliotecas centrales de .NET :)
Debería agregar una clase enumeradora anidada. No estoy seguro de si la situación ha cambiado, pero hace años que la estructura secundaria evitó una asignación de recolector de basura causada por encuadrar el resultado de GetEnumerator (). Consulte https://github.com/dotnet/coreclr/issues/1579, por ejemplo.
public class PriorityQueue<T> // ...
{
public struct Enumerator : IEnumerator<T>
{
public T Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
public void Dispose();
}
}
PriorityQueue
Todavía estoy votando para tener "public bool Remove (elemento T)" sin TryRemove, ya que es un patrón de larga data y cambiarlo hace que los desarrolladores cometan errores con mucha probabilidad. Podemos dejar que el equipo de revisión de API intervenga, pero es una pregunta abierta en mi mente.
¿Hay algún valor en especificar la capacidad inicial en el constructor o tener una función TrimExcess, o es una micro-optimización en este momento, especialmente considerando el IEnumerable
Estoy de acuerdo con bool Remove(T element)
y no con TryRemove
. Cf. https://github.com/dotnet/corefx/blob/master/src/System.Collections/src/System/Collections/Generic/CollectionExtensions.cs#L41
@pgolebiowski gracias por ponerlo en un documento. ¿Puede enviar su documento como PR contra la rama maestra de CoreFxLab? Etiqueta a @ianhays @safern , podrán fusionar los RP.
Luego, podemos usar los problemas de CoreFxLab para más discusiones sobre puntos de diseño particulares; debería ser más fácil que este mega problema (acabo de iniciar el correo electrónico para crear el área PriorityQueue allí).
Por favor, ponga un enlace a la solicitud de extracción de CoreFxLab aquí.
Si la API se aprueba en su forma actual (dos clases), crearía otro RP para CoreFXLab con una implementación para ambos usando un montón cuaternario ( ver implementación ). El PriorityQueue<T>
solo usaría una matriz debajo, mientras que PriorityQueue<TElement, TPriority>
usaría dos, y reorganizaría sus elementos juntos. Esto podría ahorrarnos asignaciones adicionales y ser lo más eficiente posible. Agregaré eso una vez que haya luz verde.
¿Hay algún plan para hacer una versión segura para subprocesos como ConcurrentQueue?
Me gustaría: corazón: ver una versión concurrente de esto, implementando IProducerConsumerCollection<T>
, por lo que podría usarse con BlockingCollection<T>
etc.
@aobatact @khellang Suena como un hilo completamente diferente:
Sin embargo, estoy de acuerdo en que es muy valioso: ¡guiño:!
public bool IsEmpty();
¿Por qué es este un método? Todas las demás colecciones del marco que tienen IsEmpty
tienen como propiedad.
Actualicé la propuesta en la publicación superior con IsEmpty { get; }
.
También agregué un enlace al último documento de propuesta en el repositorio de corefxlab.
Hola a todos,
Creo que hay muchas voces que argumentan que sería bueno apoyar la actualización si es posible. Creo que nadie terminó dudando de que es una función útil para las búsquedas de gráficos.
Pero hubo algunas objeciones aquí y allá. Entonces puedo verificar mi comprensión, parece que las principales objeciones son:
-no está claro cómo debería funcionar la actualización cuando hay elementos duplicados.
-Existe algún argumento sobre si los elementos duplicados son incluso deseables para admitir en una cola de prioridad
-Existe cierta preocupación de que podría ser ineficaz tener una estructura de datos de búsqueda adicional para encontrar elementos en la estructura de datos de respaldo solo para actualizar su prioridad. ¡Especialmente para escenarios donde la actualización nunca se realiza! Y / o afectar las garantías de desempeño en el peor de los casos ...
¿Algo que me perdí?
Bien, supongo que uno más fue: se ha afirmado que actualizar / eliminar que significa semánticamente 'updateFirst' o 'removeFirst' podría ser extraño. :)
¿En qué versión de .Net Core podemos empezar a utilizar PriorityQueue?
@memoryfraction .NET Core todavía no tiene PriorityQueue (hay propuestas, pero no hemos tenido tiempo para dedicarle recientemente). Sin embargo, no hay ninguna razón por la que tenga que estar en la distribución de Microsoft .NET Core. Cualquiera en la comunidad puede poner uno en NuGet. Quizás alguien sobre este tema pueda sugerir uno.
@memoryfraction .NET Core todavía no tiene PriorityQueue (hay propuestas, pero no hemos tenido tiempo para dedicarle recientemente). Sin embargo, no hay ninguna razón por la que tenga que estar en la distribución de Microsoft .NET Core. Cualquiera en la comunidad puede poner uno en NuGet. Quizás alguien sobre este tema pueda sugerir uno.
Gracias por tu respuesta y sugerencia.
Cuando uso C # para practicar leetcode, y necesito usar SortedSet para manejar el conjunto con elementos duplicados. Tengo que envolver el elemento con una identificación única para resolver el problema.
Por lo tanto, prefiero tener la cola de prioridad en .NET Core en el futuro porque será más conveniente.
¿Cuál es el estado actual de este problema? Me acabo de encontrar recientemente requiriendo un PriorityQueue
Esta propuesta no habilita la inicialización de la colección de forma predeterminada. PriorityQueue<T>
no tiene un método Add
. Creo que la inicialización de la colección es una característica de lenguaje lo suficientemente grande como para justificar la adición de un método duplicado.
Si alguien tiene un montón de binarios rápidos que le gustaría compartir, me gustaría compararlo con lo que escribí.
Implementé completamente la API como se propuso. Sin embargo, en lugar de un montón, utiliza una matriz ordenada simple. Mantengo el elemento de menor valor al final para que se clasifique en el orden inverso al habitual. Cuando el número de elementos es inferior a 32, utilizo una búsqueda lineal simple para encontrar dónde insertar nuevos valores. Después de eso, utilicé una búsqueda binaria. Usando datos aleatorios, encontré que este enfoque es más rápido que un montón binario en el simple caso de llenar la cola y luego drenarla.
Si tengo tiempo, lo pondré en un repositorio de git público para que la gente pueda criticarlo y actualizar este comentario con la ubicación.
@SunnyWar Estoy usando actualmente: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
que tiene buen rendimiento. Creo que tendría sentido probar su implementación con GenericPriorityQueue allí
Por favor ignore mi publicación anterior. Encontré un error en la prueba. Una vez arreglado, el montón binario funciona mejor.
@karelz ¿Dónde se encuentra actualmente esta propuesta en ICollection<T>
? No entendí por qué eso no iba a ser compatible, a pesar de que la colección, obviamente, no está destinada a ser de solo lectura.
@karelz re la cuestión abierta del orden de eliminación de la cola: argumento a favor de que el valor de prioridad más bajo se elimine primero. Es conveniente para algoritmos de ruta más corta y también es natural para implementaciones de colas de temporizador. Y esos son los dos escenarios principales para los que creo que se utilizará. Realmente no estoy al tanto de cuál sería el argumento opuesto, a menos que sea lingüístico relacionado con prioridades 'altas' y valores numéricos 'altos'.
@karelz re el orden de enumeración ... ¿qué tal tener un método Sort()
en el tipo que pondrá el montón de datos internos de la colección en orden ordenado en el lugar (en O (n log n)). Y llamar a Enumerate()
después de Sort()
enumera la colección en O (n). Y si NO se llama a Sort (), ¿devuelve elementos en orden no ordenado en O (n)?
Triage: avanzando hacia el futuro, ya que no parece haber consenso sobre si deberíamos implementar esto.
Eso es muy decepcionante de escuchar. Todavía tengo que usar soluciones de terceros para esto.
¿Cuáles son las principales razones por las que no le gusta usar una solución de terceros?
La estructura de datos del montón es imprescindible para hacer leetcode
más leetcode, más entrevistas en c # code, lo que significa más desarrolladores de c #.
más desarrolladores significa mejor ecosistema.
mejor ecosistema significa que si todavía podemos programar en c # mañana.
en resumen: esto no es solo una característica, sino también el futuro. es por eso que este tema se etiqueta como "futuro".
¿Cuáles son las principales razones por las que no le gusta usar una solución de terceros?
Porque cuando no hay un estándar, todos inventan el suyo, cada uno con su propio conjunto de peculiaridades.
Ha habido muchas ocasiones en las que he deseado un System.Collections.Generic.PriorityQueue<T>
porque es relevante para algo en lo que estoy trabajando. Esta propuesta existe desde hace 5 años. ¿Por qué todavía no ha sucedido?
¿Por qué todavía no ha sucedido?
Hambruna prioritaria, sin juego de palabras. Diseñar colecciones es trabajo porque si las ponemos en el BCL tenemos que pensar en cómo se intercambian, qué interfaces implementan, cuál es la semántica, etc. Nada de esto es ciencia espacial, pero lleva un tiempo. Además, necesita un conjunto de casos de usuarios y clientes concretos para juzgar el diseño, lo que se vuelve más difícil a medida que las colecciones se vuelven cada vez más especializadas. Hasta ahora, siempre ha habido otro trabajo que se consideró más importante que ese.
Veamos un ejemplo reciente: colecciones inmutables. Fueron diseñados por alguien que no trabajaba en el equipo de BCL para un caso de uso en VS que giraba en torno a la inmutabilidad. Trabajamos con él para obtener las API "BCL-ified". Y cuando Roslyn se conectó, reemplazamos sus copias por las nuestras en tantos lugares como pudimos y modificamos mucho el diseño (y la implementación) basándonos en sus comentarios. Sin un escenario de "héroe" esto es difícil.
Porque cuando no hay un estándar, todos inventan el suyo, cada uno con su propio conjunto de peculiaridades.
@masonwheeler, ¿esto es lo que has visto por PriorityQueue<T>
? ¿Que hay varias opciones de terceros que no son intercambiables y no hay una "mejor biblioteca para la mayoría" claramente aceptada? (No he hecho la investigación así que no sé la respuesta)
@eiriktsarpalis ¿Cómo no hay consenso sobre si implementar?
@terrajobst ¿Realmente necesitas un escenario de héroe cuando se trata de un patrón conocido con aplicaciones conocidas? Aquí debería requerirse muy poca innovación. Incluso hay una especificación de interfaz bastante desarrollada. En mi opinión, la verdadera razón por la que esta utilidad no existe no es que sea demasiado difícil, ni que no exista demanda ni caso de uso para ella. La verdadera razón es que para cualquier proyecto de software real es más fácil simplemente construir uno usted mismo que tratar de emprender una campaña política para que esto se coloque en las bibliotecas marco.
¿Cuáles son las principales razones por las que no le gusta usar una solución de terceros?
@terrajobst
Personalmente, he tenido la experiencia de que las soluciones de terceros no siempre funcionan como se esperaba / no usan las funciones del idioma actual. Con una versión estandarizada, yo (como usuario) podría estar bastante seguro de que el rendimiento es el mejor que puede obtener.
@danmosemsft
@masonwheeler, ¿esto es lo que has visto por
PriorityQueue<T>
? ¿Que hay varias opciones de terceros que no son intercambiables y no hay una "mejor biblioteca para la mayoría" claramente aceptada? (No he hecho la investigación así que no sé la respuesta)
Si. Simplemente busque en Google "cola de prioridad C #"; la primera página está llena de:
@terrajobst ¿Realmente necesitas un escenario de héroe cuando se trata de un patrón conocido con aplicaciones conocidas? Aquí debería requerirse muy poca innovación.
En mi experiencia, sí. El diablo está en los detalles y una vez que enviamos una API, realmente no podemos hacer cambios importantes. Y hay muchas opciones de implementación que son visibles para el usuario. Podemos obtener una vista previa de la API como una OOB por un tiempo, pero también hemos aprendido que, si bien podemos recopilar comentarios, la falta de un escenario de héroe significa que no tiene ningún desempate, lo que a menudo resulta en una situación en la que el héroe llega el escenario, la estructura de datos no cumple con sus requisitos.
Aparentemente necesitamos un héroe. 😛
@masonwheeler Supuse que tu enlace sería a esto 😄 Ahora está en mi cabeza.
Aunque como dice @terrajobst , nuestro problema principal aquí ha sido los recursos / atención (y puede culparnos por eso si lo desea), también nos gustaría fortalecer el ecosistema que no es de Microsoft para que sea más probable que pueda encontrar bibliotecas sólidas para lo que sea. guión.
[editado para mayor claridad]
@danmosemsft No, si tuviera que elegir esa canción, haría la versión de Shrek 2.
Candidato n. ° 1 de la aplicación Hero: ¿qué tal usar una en TimerQueue.Portable?
Candidato n. ° 1 de la aplicación Hero: ¿qué tal usar una en TimerQueue.Portable?
Ya se ha considerado, prototipado y descartado. Hace que el caso muy común de que un temporizador se cree y destruya rápidamente (por ejemplo, para un tiempo de espera) sea menos eficiente.
@stephentoub Supongo que quiere decir que es menos eficiente para algunos escenarios donde hay una pequeña cantidad de temporizadores. Pero, ¿cómo escala?
Supongo que quiere decir que es menos eficiente para algunos escenarios donde hay una pequeña cantidad de temporizadores. Pero, ¿cómo escala?
No, quise decir que el caso común es que tienes muchos temporizadores en cualquier momento, pero muy pocos están disparando. Esto es lo que resulta cuando se utilizan temporizadores para tiempos de espera. Y lo importante es la velocidad a la que puede agregar y eliminar de la estructura de datos ... desea que sea 0 (1) y con una sobrecarga muy baja. Si eso se convierte en O (log N), es un problema.
Tener una cola de prioridad definitivamente hará que C # sea más amigable para las entrevistas.
Tener una cola de prioridad definitivamente hará que C # sea más amigable para las entrevistas.
Sí, eso es verdad.
Lo estoy buscando por la misma razón.
@stephentoub Para los tiempos de espera que en realidad nunca suceden, eso tiene mucho sentido para mí. Pero me pregunto qué le sucede al sistema cuando de repente comienzan a suceder muchos tiempos de espera, porque de repente hay una pérdida de paquetes o un servidor que no responde o lo que sea. ¿También los temporizadores recurrentes de System.Timer utilizan la misma implementación? Allí, el tiempo de espera que expira sería el 'camino feliz'.
Pero me pregunto qué le sucede al sistema cuando de repente comienzan a ocurrir muchos tiempos de espera
Intentalo. :)
Vimos una gran cantidad de cargas de trabajo reales sufriendo con la implementación anterior. En todos los casos que hemos visto, el nuevo (que no usa una cola de prioridad, sino que solo tiene una división simple entre los temporizadores pronto y no pronto) ha solucionado el problema, y no hemos visto nuevos con eso.
¿También los temporizadores recurrentes de System.Timer utilizan la misma implementación?
Si. Pero generalmente se distribuyen, a menudo de manera bastante uniforme, en aplicaciones del mundo real.
Cinco años después, todavía no existe PriorityQueue.
Rx tiene una clase de cola de prioridad altamente probada en producción:
Cinco años después, todavía no existe PriorityQueue.
No debe ser una prioridad lo suficientemente alta ...
Cambiaron el diseño del repositorio. La nueva ubicación es https://github.com/dotnet/reactive/blob/master/Rx.NET/Source/src/System.Reactive/Internal/PriorityQueue.cs
@stephentoub, pero tal vez en realidad @eiriktsarpalis, ¿realmente tenemos algo de tracción en esto en este momento? Si tenemos un diseño de API finalizado, estaría dispuesto a trabajar en él.
No he visto una declaración de que esto esté resuelto todavía y no estoy seguro de si puede haber un diseño de API final sin una aplicación asesina designada. Pero...
Suponiendo que el candidato principal de la aplicación asesina sea concursos de programación / enseñanza / entrevistas, creo que el diseño de Eric en la parte superior parece lo suficientemente útil ... y todavía tengo mi contrapropuesta por ahí (¡recién revisada, aún no retirada!)
Las principales diferencias que estoy notando entre ellos son si debería tratar de hablar como un diccionario y si los selectores deberían ser una cosa.
Estoy empezando a olvidar exactamente por qué quería que fuera un diccionario. Tener un asignador indexado para actualizar las prioridades y poder enumerar las claves con sus prioridades me pareció una bondad y muy parecido a un diccionario. Ser capaz de visualizarlo como un diccionario en el depurador también podría ser muy interesante.
Creo que los selectores en realidad cambian drásticamente la interfaz de clase esperada, FWIW. El selector debe ser algo incorporado en el momento de la construcción, y si lo tiene, elimina la necesidad de que alguien le pase un valor de prioridad a otra persona; solo deben llamar al selector si quieren saber la prioridad. Por lo tanto, no querría tener parámetros de prioridad en las firmas de ningún método, excepto en el selector. Entonces, en ese caso, se convierte en una especie de clase 'PrioritySet' más especializada. [¡Para los cuales los selectores impuros son un posible problema de errores!]
@TimLovellSmith Entiendo el deseo de este comportamiento, pero debido a que esta ha sido una Queue.cs
? Creo que eso es, con mucho, lo que la mayoría de los usuarios necesitan. En mi opinión, esta estructura de datos no se ha implementado porque no podemos ponernos de acuerdo en un diseño de API, pero si estamos haciendo un PriorityQueue
creo que deberíamos emular el diseño de API de Queue
.
@Jlalond De ahí la propuesta aquí, ¿eh? No tengo ninguna objeción a la implementación de una matriz basada en montones. Ahora recuerdo que la mayor objeción que tuve frente a los selectores es que no es lo suficientemente fácil hacer correctamente las actualizaciones de prioridad. Una cola antigua simple no tiene una operación de actualización de prioridad, pero la actualización de la prioridad del elemento es una operación importante para muchos algoritmos de cola de prioridad.
La gente debería estar eternamente agradecida si optamos por una API que no les obligue a depurar estructuras de colas de prioridad dañadas.
@TimLovellSmith Lo siento Tim, debería haber aclarado la herencia de IDictionary
.
¿Tenemos un caso de uso en el que la prioridad cambiaría?
Me gusta su implementación, pero creo que podemos reducir la replicación de IDictionary Behavior. Creo que no deberíamos heredar de IDictionary<>
sino solo de ICollection, porque no creo que los patrones de acceso al diccionario sean intuitivos,
Sin embargo, realmente creo que devolver T y su prioridad asociada tendría sentido, pero no conozco un caso de uso en el que necesite saber la prioridad de un elemento dentro de la estructura de datos al ponerlo en cola o sacarlo de la cola.
@Jlalond
Si tenemos una cola de prioridad, entonces debería admitir todas las operaciones que pueda, de manera simple y eficiente, _y_ que son
'esperado' que esté allí por personas familiarizadas con este tipo de estructura de datos / abstracción de uno.
La prioridad de actualización pertenece a la API porque se incluye en ambas categorías. La prioridad de actualización es una consideración lo suficientemente importante en muchos algoritmos como para que la complejidad de realizar la operación con estructuras de datos de pila afecta la complejidad del algoritmo general, y se mide con regularidad, consulte:
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps
TBH principalmente su clave decreciente, que es interesante para los algoritmos y la eficiencia, y se mide, por lo que personalmente estoy bien porque la API solo tiene DecreasePriority (), y en realidad no tiene UpdatePriority.
Pero por conveniencia , parece más agradable no molestar a los usuarios de la API con preocuparse por si cada cambio es un aumento o una disminución. Para que el desarrollador de Joe tenga una 'actualización' lista para usar sin tener que preguntarse por qué solo admite disminuir y no aumentar (y cuál usar cuando), creo que es mejor tener una API de prioridad de actualización general, pero documente eso al usarlo para disminuir tiene costo X, al usarlo para aumentar tiene costo Y porque se implementa igual que Remover + Agregar.
RE diccionario que estoy de acuerdo. Lo eliminé de mi propuesta en nombre de que lo más simple es mejor.
https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md
No conozco un caso de uso en el que necesite saber la prioridad de un elemento dentro de la estructura de datos al ponerlo en cola o sacarlo de la cola.
No estoy seguro de qué trata este comentario. No es necesario conocer la prioridad de un elemento para quitarlo de la cola. Pero necesita saber su prioridad cuando lo pone en cola. Quizás desee conocer la prioridad final de un elemento cuando lo quita de la cola, ya que ese valor puede tener un significado, por ejemplo, la distancia.
@TimLovellSmith
No estoy seguro de qué trata este comentario. No es necesario conocer la prioridad de un elemento para quitarlo de la cola. Pero necesita saber su prioridad cuando lo pone en cola. Quizás desee conocer la prioridad final de un elemento cuando lo quita de la cola, ya que ese valor puede tener un significado, por ejemplo, la distancia.
Lo siento, entendí mal lo que significaba TPriorty en su par clave-valor.
De acuerdo, las dos últimas preguntas, ¿por qué KeyValuePair con TPriority, y el mejor momento que puedo ver para una repriorización es N log N? Estoy de acuerdo en que es valioso, pero también ¿cómo sería esa API?
el mejor momento que puedo ver para una repriorización es N log N,
Ese es el mejor momento para cambiar las prioridades de N elementos en lugar de un elemento, ¿verdad?
Aclararé el documento: "UpdatePriority | O (log n)" debería leer "UpdatePriority (elemento único) | O (log n)".
@TimLovellSmith Tiene sentido, pero ¿no sería una actualización de prioridad realmente ne O2 * (log n), ya que tendríamos que eliminar el elemento y luego reinsertarlo? Basando mi suposición en que esto es un montón binario
@TimLovellSmith Tiene sentido, pero ¿no sería una actualización de prioridad realmente ne O2 * (log n), ya que tendríamos que eliminar el elemento y luego reinsertarlo? Basando mi suposición en que esto es un montón binario
Los factores constantes como ese 2 generalmente se ignoran en el análisis de complejidad porque se vuelven en su mayoría irrelevantes a medida que aumenta el tamaño de N.
Entendido, sobre todo quería saber si Tim tenía alguna idea interesante que hacer solo
una operación :)
El martes 18 de agosto de 2020 a las 23:51 masonwheeler [email protected] escribió:
@TimLovellSmith https://github.com/TimLovellSmith Tiene sentido, pero
¿No habría ninguna actualización de prioridad en realidad ne O2 * (log n), ya que necesitaríamos
quitar el elemento y luego reinsertarlo? Basando mi suposición en esto
siendo un montón binarioLos factores constantes como ese 2 generalmente se ignoran en el análisis de complejidad
porque se vuelven en su mayoría irrelevantes a medida que crece el tamaño de N.-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/runtime/issues/14032#issuecomment-675887763 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AF76XTI3YK4LRUVTOIQMVHLSBNZARANCNFSM4LTSQI6Q
.
@Jlalond
No hay ideas novedosas, solo estaba ignorando un factor constante, pero las disminuciones también son diferentes de los aumentos.
Si la actualización de prioridad es una disminución, no tiene que eliminarla y volver a agregarla, simplemente aparece, por lo que es 1 * O (log n) = O (log n).
Si la actualización de prioridad es un aumento, probablemente deba eliminarla y volver a agregarla, por lo que su 2 * O (log n) = sigue siendo O (log n).
Alguien más podría haber inventado una mejor estructura de datos / algoritmo para aumentar la prioridad que remove + readd, pero no he intentado encontrarlo, O (log n) parece lo suficientemente bueno.
KeyValuePair
deslizó en la interfaz mientras era un diccionario, pero sobrevivió a la eliminación del diccionario, principalmente para que sea posible iterar la colección de _elementos con sus prioridades_. Y también para que puedas quitar los elementos de la cola con sus prioridades. Pero quizás para simplificar nuevamente, la eliminación de la cola con prioridades solo debería estar en la versión 'avanzada' de la API Dequeue. (TryDequeue: D)
Haré ese cambio.
@TimLovellSmith Genial, espero su propuesta revisada. ¿Podemos enviarlo para revisión para que pueda comenzar a trabajar en esto? Además, sé que hubo cierto impulso para una cola de emparejamiento para mejorar el tiempo de fusión, pero sigo pensando que un montón binario basado en matriz sería el rendimiento más general. ¿Pensamientos?
@Jlalond
Hice los cambios menores para simplificar esa API de Peek + Dequeue.
Algo de ICollection
Mismo enlace de relaciones públicas. ¿Hay alguna otra forma en que deba enviarlo para revisión?
No me importa qué implementación use siempre que sea tan rápido como el montón basado en matrices o mejor.
@TimLovellSmith En realidad, no sé nada sobre la revisión de API, pero imagino que @danmosemsft puede
Mi primera pregunta es ¿qué haría
Y en realidad prefiero los montones binarios, pero quería asegurarme de que todos estamos bien yendo en esa dirección
@eiriktsarpalis @layomia son los propietarios del controlar esto a través de la revisión de API. No he seguido la discusión, ¿hemos llegado a un punto de consenso?
Como hemos discutido en el pasado, las bibliotecas centrales no son el mejor lugar para todas las colecciones, especialmente aquellas que tienen opiniones y / o un nicho. Por ejemplo, enviamos anualmente; no podemos movernos tan rápido como una biblioteca. Tenemos el objetivo de construir una comunidad alrededor de un paquete de colecciones para llenar ese vacío. Pero el 84 pulgar hacia arriba en este sugiere que existe una necesidad generalizada de esto en las bibliotecas centrales.
@Jlalond Lo siento, no entendí bien cuál se supone que debe estar allí la primera pregunta. ¿Se preguntaba por qué TPriority es genérico? ¿O tiene que ser un número? Dudo que mucha gente utilice tipos de prioridad no numéricos. Pero es posible que prefieran usar un byte, int, long, float, double o enum.
@TimLovellSmith Sí, solo quería saber si era genérico
@danmosemsft Gracias Dan. La cantidad de objeciones / comentarios no abordados que veo a mi propuesta [1] es aproximadamente cero, y aborda cualquier pregunta importante que quedó abierta por la propuesta de @ebickle en la parte superior (a la que se está volviendo cada vez más similar).
Así que afirmo que está pasando los controles de cordura hasta ahora. Debe haber algo de revisión requerida todavía, podemos hablar sobre si es útil heredar IReadOnlyCollection (no parece muy útil, pero debería ceder a los expertos) y así sucesivamente, ¡supongo que para eso es el proceso de revisión de API! @eiriktsarpalis @layomia ¿ puedo pedirle que le eche un vistazo?
[PD: basado en el sentimiento del hilo, la aplicación asesina propuesta actualmente es la de codificación de concursos, preguntas de entrevistas, etc., donde solo desea usar una API simple y agradable que funcione en clases o estructuras más su tipo numérico favorito del día, con la O correcta (n) y una implementación no demasiado lenta, y estoy feliz de ser un conejillo de indias para aplicarlo a algunos problemas. Lo siento, no es nada más emocionante.]
Solo hablo porque utilizo colas de prioridad todo el tiempo para proyectos 'reales', principalmente para búsqueda de gráficos (por ejemplo, optimización de problemas, búsqueda de rutas) y extracción ordenada (por fragmentos (por ejemplo, vecinos más cercanos de cuatro árboles), programación (es decir, como la discusión del temporizador anterior)). Tengo un par de proyectos en los que podría conectar una implementación directamente para verificar / probar la cordura, si ayuda. La propuesta actual se ve bien para mí (funcionaría para todos mis propósitos, si no es ideal). Tengo un par de pequeñas observaciones:
TElement
aparece un par de veces donde debería ser TKey
bool EnqueueOrUpdateIfHigherPriority
para facilitar el uso (y en algunos casos eficiencia), que a menudo termina siendo el único método de puesta en cola / actualización que se usa (por ejemplo, en la búsqueda de gráficos). Obviamente, no es esencial y agregaría complejidad adicional a la API, pero es algo muy bueno tenerlo.Enqueue
(no me refiero a Add
): ¿se supone que una sea bool TryEnqueue
?EqualityComparer
es un poco inesperado: hubiera esperado que KeyComparer
fuera con PriorityComparer
.CopyTo
y ToArray
.@VisualMelon
¡Muchas gracias por la revision!
Me gusta la sugerencia de nomenclatura de KeyComparer. La API de prioridad decreciente suena muy útil. ¿Qué tal si agregamos una o ambas API? Dado que estamos usando el modelo de 'la menor prioridad es lo primero', ¿usaría solo la disminución? ¿O desearía un aumento también?
public void EnqueueOrIncreasePriority(TKey key, TPriority priority); // doesn't throw, only updates priority of keys already in the collection if new priority is higher
public void EnqueueOrDeccreasePriority(TKey key, TPriority priority); // doesn't throw, only updates priority of keys already in the collection new priroity is lower
La razón por la que pensé que enumerar devuelve el elemento mínimo primero era para ser coherente con el modelo mental de que Peek () devuelve el elemento al principio de la colección. Y tampoco son necesarias comparaciones para implementarlo si es un montón binario, por lo que parecía un obsequio. Pero tal vez sea menos libre de lo que creo, ¿complejidad injustificada? Estoy feliz de sacarlo por esa razón.
Two Enqueues fue accidental. Tenemos EnqueueOrUpdate, donde la prioridad siempre se actualiza. Supongo que TryUpdate es el inverso lógico de eso, donde la prioridad nunca debe actualizarse para los elementos que ya están en la colección. Puedo ver que llena ese vacío lógico, pero no estoy seguro de si es una API que la gente querrá en la práctica.
Solo puedo imaginar el uso de la variante decreciente: es una operación natural en la búsqueda de rutas querer poner en cola un nodo o reemplazar un nodo existente con uno que tiene una prioridad más baja; No podría sugerir inmediatamente un uso para lo contrario. Un parámetro de retorno boolean
puede ser útil si necesita realizar alguna operación cuando el artículo está en cola / no en cola.
No estaba asumiendo un montón binario, pero si es gratis, entonces no tengo ningún problema con el primer elemento siempre siendo el mínimo, simplemente parecía una restricción extraña.
@VisualMelon @TimLovellSmith ¿
EnqueueOrUpdatePriority
? Me imagino que la capacidad de reposicionar un nodo dentro de la cola es valiosa para los algoritmos de recorrido, incluso si aumenta o disminuye la prioridad
@Jlalond sí, lo menciono porque puede ser útil para la eficiencia dependiendo de la implementación, y codifica directamente el caso de uso de solo querer poner en cola algo si mejora lo que ya tiene en la cola. No presumiré de decir si la complejidad adicional es apropiada para una herramienta de propósito general, pero ciertamente no es necesaria.
Actualizado para eliminar EnqueueOrIncreasePriority, manteniendo EnqueueOrUpdate y EnqueueOrDecrease. Permitirles que devuelvan bool cuando un elemento se agrega recientemente a la colección, como HashSet.Add ().
@Jlalond Me disculpo por el error anterior (eliminación de su último comentario preguntando cuándo se revisará este problema).
Solo quería mencionar que @eiriktsarpalis regresará la próxima semana y discutiremos la priorización de esta función y la revisión de las API.
Esto es algo que estamos considerando, pero aún no estamos comprometidos con .NET 6.
Veo que este hilo se revivió al comenzar desde cero y bifurcarlo, a pesar de que tuvimos discusiones en profundidad sobre este tema en 2017 durante varios meses (la mayoría de este enorme hilo) y produjimos colectivamente esta propuesta : desplácese hacia arriba para ver. Esta es también la propuesta que @karelz vinculó en la primera línea de la primera publicación para la visibilidad:
Consulte la propuesta MÁS RECIENTE en el repositorio de corefxlab.
A partir del 2017-06-16, estábamos esperando una revisión de API que nunca sucedió y el estado era:
Si la API se aprueba en su forma actual (dos clases), crearía otro RP para CoreFXLab con una implementación para ambos usando un montón cuaternario ( ver implementación ). El
PriorityQueue<T>
solo usaría una matriz debajo, mientras quePriorityQueue<TElement, TPriority>
usaría dos, y reorganizaría sus elementos juntos. Esto podría ahorrarnos asignaciones adicionales y ser lo más eficiente posible. Agregaré eso una vez que haya luz verde.
Recomiendo seguir iterando sobre la propuesta que hicimos en 2017, partiendo de una revisión formal que aún no ha sucedido. Esto nos permitirá evitar bifurcaciones e iterar en la parte posterior de una propuesta elaborada después de cientos de publicaciones intercambiadas por miembros de la comunidad, también por respeto al esfuerzo realizado por todos los involucrados. Me complace discutirlo si hay comentarios.
@pgolebiowski Gracias por volver a la discusión. Me gustaría disculparme por seguir avanzando con eficacia las propuestas, que no es la forma más eficaz de colaborar. Fue un movimiento impulsivo de mi parte. (Tenía la impresión de que la discusión se había estancado por completo debido a que había demasiadas preguntas abiertas en las propuestas demasiado lejos, y solo necesitaba una propuesta más obstinada).
¿Qué tal si tratamos de avanzar en esto, al menos temporalmente olvidando el contenido del código de mi RP, y reanudamos la discusión aquí de los 'problemas abiertos' identificados? Me gustaría dar mi opinión sobre todos ellos.
Nombre de clase PriorityQueue vs. Heap <- Creo que ya se discutió y el consenso es que PriorityQueue es mejor.
¿Introducir IHeap y la sobrecarga del constructor? <- No preveo mucho valor. Creo que para el 95% del mundo no hay una razón convincente (por ejemplo, delta de rendimiento) para tener múltiples implementaciones de montón, y el otro 5% probablemente tendrá que escribir las suyas propias.
¿Introducir IPriorityQueue? <- Tampoco creo que sea necesario. Supongo que otros lenguajes y marcos se llevan bien sin estas interfaces en su biblioteca estándar. Avísame si eso es incorrecto.
Use el selector (de prioridad almacenada dentro del valor) o no (diferencia de 5 API) <- No veo un argumento sólido a favor de admitir selectores en la cola de prioridad. Siento que IComparer<>
ya sirve como un mecanismo de extensibilidad mínima realmente bueno para comparar prioridades de elementos, y los selectores no ofrecen ninguna mejora significativa. También es probable que las personas se sientan confundidas acerca de cómo usar selectores con prioridades mutables (o cómo NO usarlos).
Utilice tuplas (elemento TElement, prioridad TPriority) frente a KeyValuePairKeyValuePair
es la opción preferida para una cola de prioridad donde los elementos tienen prioridades actualizables, ya que los elementos se tratan como claves en un conjunto / diccionario.
¿Deberían Peek y Dequeue tener un argumento en lugar de tupla? <- No estoy seguro de todos los pros y los contras, especialmente el rendimiento. ¿Deberíamos elegir el que funcione mejor en una simple prueba de referencia?
¿Son útiles los lanzamientos de Peek y Dequeue? <- Sí ... Es lo que debería suceder con los algoritmos que asumen incorrectamente que todavía hay elementos en la cola . Si no desea que los algoritmos asuman eso, lo mejor que puede hacer es no proporcionar las apis de Peek y Dequeue en absoluto, sino simplemente proporcionar TryPeek y TryDequeue. [Supongo que hay algoritmos que pueden llamar de forma segura a Peek o Dequeue en ciertos casos, y tener Peek / Dequeue es una ligera mejora de rendimiento + usabilidad para ellos].
Aparte de eso, también tengo algunas sugerencias para considerar agregar a la propuesta bajo consideración:
PriorityQueue
PriorityQueue
Sería conveniente para los programadores perezosos si PriorityQueue
La prioridad más pequeña primero es la mejor. Debido a que las colas de prioridad se utilizan mucho, son problemas de optimización, con 'costo mínimo'.
La enumeración no debe proporcionar ninguna garantía de pedido.
Problema abierto - maneja:
PriorityQueue
Un pensamiento para ayudar a mover esto más rápido, puede tener sentido mover este tipo a la biblioteca "Colecciones de la comunidad" que se analiza en https://github.com/dotnet/runtime/discussions/42205
Estoy de acuerdo en que la prioridad más pequeña es lo mejor. Una cola de prioridad también es útil en el desarrollo de juegos por turnos, y poder tener la prioridad como un contador que aumenta monótonamente de cuándo cada elemento tiene su próximo turno es muy útil.
Estamos buscando traer esto para la revisión de API lo antes posible (aunque aún no estamos comprometidos a entregar una implementación dentro del plazo de tiempo de .NET 6).
Pero antes de enviar esto para su revisión, tendremos que resolver algunas de las preguntas abiertas de alto nivel. Creo @TimLovellSmith 's de escritura hasta es un buen punto de partida para alcanzar ese objetivo.
Algunas observaciones sobre esos puntos:
El consenso sobre las preguntas 1 a 3 está establecido desde hace mucho tiempo, creo que podríamos tratarlas como resueltas.
_Utilice selector (de prioridad almacenada dentro del valor) o no_ - De acuerdo con sus comentarios. Otro problema con ese diseño es que los selectores son opcionales, lo que significa que debe tener cuidado de no llamar a la sobrecarga Enqueue
incorrecta (o arriesgarse a obtener un InvalidOperationException
).
Yo prefiero usar una tupla sobre KeyValuePair
. Usar una propiedad .Key
para acceder a un TPriority
me resulta extraño. Una tupla le permite usar .Priority
que tiene mejores propiedades de auto-documentación.
_¿Deberían Peek y Dequeue tener un argumento en lugar de tupla? _ - Creo que deberíamos seguir la convención establecida en métodos similares en otros lugares. Entonces probablemente use un argumento out
.
_¿Es útil el lanzamiento de Peek y Dequeue? _ - Estoy 100% de acuerdo con tus comentarios.
_La prioridad más pequeña primero es la mejor_ - De acuerdo.
_La enumeración no debe proporcionar ninguna garantía de pedido_ - ¿No podría esto violar las expectativas del usuario? ¿Cuáles son las compensaciones? NB, probablemente podamos diferir detalles como este para la discusión de revisión de API.
También me gustaría reformular algunas otras preguntas abiertas:
¿Enviamos PriorityQueue<T>
, PriorityQueue<TElement, TPriority>
o ambos? - Personalmente, creo que solo deberíamos implementar este último, ya que parece proporcionar una solución más limpia y de uso más general. En principio, la prioridad no debería ser una propiedad intrínseca del elemento en cola, por lo que no deberíamos obligar a los usuarios a encapsularlo en tipos de envoltura.
¿Requerimos la unicidad de los elementos, hasta la igualdad? - Corrígeme si me equivoco, pero siento que la singularidad es una restricción artificial, impuesta por el requisito de admitir escenarios de actualización sin recurrir a un enfoque de manejo. También complica la superficie de la API, ya que ahora también debemos preocuparnos por que los elementos tengan la semántica de igualdad correcta (¿cuál es la igualdad apropiada cuando los elementos son DTO grandes?). Puedo ver tres posibles caminos aquí:
Enqueue
para los usuarios que los necesiten. Si las asignaciones de manejo son una preocupación lo suficientemente grande, me pregunto si podrían amortizarse a la ValueTask.¿Admitimos la fusión de colas? - El consenso parece ser no, ya que solo estamos enviando una implementación (presumiblemente usando montones de arreglos) donde la fusión no es eficiente.
¿Qué interfaces debería implementar? - Vi algunas propuestas recomendando IQueue<T>
, pero esto se siente como una abstracción con fugas. Personalmente, preferiría mantenerlo simple e implementar ICollection<T>
.
cc @layomia @safern
@eiriktsarpalis Por el contrario, ¡no hay nada artificial en la restricción para admitir actualizaciones en absoluto!
Los algoritmos con colas de prioridad a menudo actualizan las prioridades de los elementos. La pregunta es, ¿proporciona una API orientada a objetos, que 'simplemente funciona' para actualizar las prioridades de los objetos ordinarios ... o obliga a la gente a
a) entender el modelo de mango
b) mantener datos adicionales en la memoria, como propiedades adicionales o un diccionario externo, para realizar un seguimiento de los identificadores de los objetos, de su lado, solo para que puedan hacer actualizaciones (¿deben ir con el diccionario para las clases que no pueden cambiar? o actualizar los objetos en tuplas, etc.)
c) estructuras externas de 'gestión de memoria' o 'recolección de basura', es decir, limpiar el identificador de los elementos que ya no están en la cola, cuando se utiliza el método de diccionario
d) no confunda los identificadores opacos en contextos con múltiples colas, ya que solo son significativos en el contexto de una sola cola de prioridad
Además, existe esta pregunta filosófica en torno a la razón por la que alguien querría que una cola que realiza un seguimiento de los objetos por prioridad se comporte de esta manera: ¿por qué el mismo objeto (igual a devuelve verdadero) tiene dos _diferentes_ prioridades? Si realmente se supone que tienen diferentes prioridades, ¿por qué no se modelan como objetos diferentes ? (¿o convertidos en tuplas, con distintivos?)
También para los identificadores, tiene que haber alguna tabla interna de identificadores en la cola de prioridad solo para que los identificadores realmente funcionen. Creo que esto es un trabajo equivalente a mantener un diccionario para buscar objetos en la cola.
PD: cada objeto .net ya admite conceptos de igualdad / unicidad, por lo que no es una gran solicitud 'requerirlo'.
Con respecto a KeyValuePair
, estoy de acuerdo en que no es ideal (aunque en la propuesta, Key
es para el elemento, Value
para la prioridad, que es coherente con las distintas Sorted
tipos de datos en el BCL), y su idoneidad dependería de una decisión con respecto a la singularidad. Personalmente, _mucho_ preferiría un struct
dedicado bien nombrado sobre una tupla en cualquier lugar de la API pública, especialmente para la entrada.
Con respecto a la singularidad, esa es una preocupación fundamental, y no creo que se pueda decidir nada más hasta que sea así. Favorecería la unicidad del elemento según lo definido por el comparador (opcional) (según las propuestas existentes y la sugerencia i) si el objetivo es una única API de uso general y fácil de usar. Único vs no único es una gran brecha, y uso ambos tipos para diferentes propósitos. El primero es 'más difícil' de implementar y cubre la mayoría (y más típicos) de los casos de uso (solo mi experiencia) mientras que es más difícil de usar incorrectamente. Esos casos de uso que _requieren_ no exclusividad deberían (IMO) ser atendidos por un tipo diferente (por ejemplo, un montón binario antiguo simple), y agradecería tener ambos disponibles. Esto es esencialmente lo que la propuesta original vinculada por @pgolebiowski proporciona (según tengo entendido) módulo a (simple) envoltorio. _Editar: No, eso no apoyaría prioridades atadas_
Por el contrario, ¡no hay nada artificial en la restricción para admitir actualizaciones!
Lo siento, no quise indicar que el soporte para actualizaciones sea artificial; más bien, el requisito de unicidad se introduce artificialmente para admitir actualizaciones.
PD: cada objeto .net ya admite conceptos de igualdad / unicidad, por lo que no es una gran pregunta 'requerirlo'
Claro, pero ocasionalmente la semántica de igualdad que viene con el tipo puede no ser la deseable (por ejemplo, igualdad de referencia, igualdad estructural en toda regla, etc.). Solo estoy señalando que la igualdad es difícil y forzarla en el diseño trae una clase completamente nueva de errores de usuario potenciales.
@eiriktsarpalis Gracias por aclarar eso. ¿Pero es realmente artificial? No creo que lo sea. Es solo otra solución natural .
La api debe estar _bien definida_. No puede proporcionar una API de actualización sin requerir que el usuario sea específico sobre exactamente lo que desea actualizar . Los identificadores y la igualdad de objetos son solo dos enfoques diferentes para crear una API bien definida.
Enfoque de identificador: cada vez que agrega un objeto a la colección, debe darle un 'nombre', es decir, un 'identificador', para que pueda referirse a ese objeto exacto sin ambigüedad más adelante en la conversación.
Enfoque de unicidad del objeto: cada vez que agrega un objeto a la colección, debe ser un objeto diferente, o debe especificar cómo manejar el caso de que el objeto ya exista.
Desafortunadamente, solo el enfoque de objetos realmente le permite admitir algunas de las propiedades de métodos de API de alto nivel más útiles, como 'EnqueueIfNotExists' o 'EnqueueOrDecreasePriroity (item)'; no tiene sentido proporcionarlas en un diseño centrado en el identificador, porque No puedo no saber si el elemento ya existe en la cola (ya que es su trabajo realizar un seguimiento de eso, con identificadores).
Una de las críticas más contundentes del enfoque del identificador, o de eliminar la restricción de unicidad, es que hace que todo tipo de escenarios con prioridad actualizable sea mucho más complicado de implementar:
p.ej
PD
Claro, pero ocasionalmente la semántica de igualdad que viene con el tipo puede no ser la deseable.
Habrá trampillas de escape, como IEqualityComparer, o conversión ascendente a un tipo más rico.
Gracias por los comentarios 🥳 Actualizaré la propuesta durante el fin de semana, teniendo en cuenta todas las aportaciones nuevas, y compartiremos una nueva revisión para otra ronda. ETA 2020-09-20.
La comunidad .NET Core propone agregar a la biblioteca del sistema la funcionalidad _priority queue_, una estructura de datos en la que cada elemento tiene además una prioridad asociada. Específicamente, proponemos agregar PriorityQueue<TElement, TPriority>
al espacio System.Collections.Generic
nombres
En nuestro diseño, nos guiamos por los siguientes principios (a menos que conozca otros mejores):
Conceptualmente, una cola de prioridad es una colección de elementos, donde cada elemento tiene una prioridad asociada. La funcionalidad más importante de una cola de prioridad es que proporciona un acceso eficiente al elemento con la mayor prioridad en la colección y una opción para eliminar ese elemento. El comportamiento esperado también puede incluir: 1) capacidad para modificar la prioridad de un elemento que ya está en la colección; 2) capacidad para fusionar múltiples colas de prioridad.
Una cola de prioridad es una estructura de datos abstracta, es decir, es un concepto con ciertas características de comportamiento, como se describe en la sección anterior. Las implementaciones más eficientes de una cola de prioridad se basan en montones. Sin embargo, contrariamente a la idea generalizada, un montón es también una estructura de datos abstracta y se puede realizar de varias formas, cada una de las cuales ofrece diferentes ventajas y desventajas.
La mayoría de los ingenieros de software solo están familiarizados con la implementación del montón binario basada en arreglos; es la más simple, pero desafortunadamente no es la más eficiente. Para la entrada aleatoria general, dos ejemplos de tipos de pila más eficientes son: pila cuaternaria y pila de emparejamiento . Para obtener más información sobre montones, consulte Wikipedia y este documento .
Nuestras discusiones han demostrado que el área más desafiante en el diseño, y al mismo tiempo con el mayor impacto en la API, es el mecanismo de actualización. Específicamente, el desafío es determinar si y cómo el producto que queremos ofrecer a los clientes debe apoyar la actualización de las prioridades de los elementos ya presentes en la colección.
Dicha capacidad es necesaria para implementar, por ejemplo, el algoritmo de ruta más corta de Dijkstra o un programador de trabajos que necesita manejar las prioridades cambiantes. Falta el mecanismo de actualización en Java, lo que ha demostrado ser decepcionante para los ingenieros, por ejemplo, en estas tres preguntas de StackOverflow vistas más de 32.000 veces: ejemplo , ejemplo , ejemplo . Para evitar introducir una API con un valor tan limitado, creemos que un requisito fundamental para la funcionalidad de la cola de prioridad que ofrecemos sería admitir la capacidad de actualizar las prioridades para los elementos que ya están presentes en la colección.
Para entregar el mecanismo de actualización, debemos asegurarnos de que el cliente pueda ser específico sobre qué es exactamente lo que desea actualizar. Hemos identificado dos formas de entregar esto: a) a través de identificadores; yb) mediante la aplicación de la unicidad de los elementos de la colección. Cada uno de estos tiene diferentes beneficios y costos.
Opción (a): Asas. En este enfoque, cada vez que se agrega un elemento a la cola, la estructura de datos proporciona su identificador único. Si el cliente desea utilizar el mecanismo de actualización, debe realizar un seguimiento de dichos identificadores para que luego pueda especificar sin ambigüedades qué elemento desea actualizar. El costo principal de esta solución es que los clientes deben administrar estos indicadores. Sin embargo, no significa que deba haber asignaciones internas para admitir identificadores dentro de la cola de prioridad; cualquier montón no basado en arreglos se basa en nodos, donde cada nodo es automáticamente su propio identificador. Como ejemplo, consulte la API del método PairingHeap.Update .
Opción (b): Singularidad. Este enfoque impone dos restricciones adicionales al cliente: i) los elementos dentro de la cola de prioridad deben ajustarse a cierta semántica de igualdad, lo que trae una nueva clase de errores de usuario potenciales; ii) dos elementos iguales no se pueden almacenar en la misma cola. Al pagar este costo, obtenemos el beneficio de admitir el mecanismo de actualización sin recurrir al enfoque de manejo. Sin embargo, cualquier implementación que aproveche la singularidad / igualdad para determinar el elemento a actualizar requerirá un mapeo interno adicional, de modo que se realice en O (1) y no en O (n).
Recomendamos agregar a la biblioteca del sistema una clase PriorityQueue<TElement, TPriority>
que admita el mecanismo de actualización a través de identificadores. La implementación subyacente sería un montón de emparejamiento.
public class PriorityQueueNode<TElement, TPriority>
{
public TElement Element { get; }
public TPriority Priority { get; }
}
public class PriorityQueue<TElement, TPriority> :
IEnumerable<PriorityQueueNode<TElement, TPriority>>
{
public PriorityQueue();
public PriorityQueue(IComparer<TPriority> comparer);
public IComparer<TPriority> Comparer { get; }
public int Count { get; }
public bool IsEmpty { get; }
public void Clear();
public bool Contains(TElement element); // O(n)
public bool TryGetNode(TElement element, out PriorityQueueNode<TElement, TPriority> node); // O(n)
public PriorityQueueNode<TElement, TPriority> Enqueue(TElement element, TPriority priority); //O(log n)
public PriorityQueueNode<TElement, TPriority> Peek(); // O(1)
public bool TryPeek(out PriorityQueueNode<TElement, TPriority> node); // O(1)
public void Dequeue(out TElement element); // O(log n)
public bool TryDequeue(out TElement element, out TPriority priority); // O(log n)
public void Update(PriorityQueueNode<TElement, TPriority> node, TPriority priority); // O(log n)
public void Remove(PriorityQueueNode<TElement, TPriority> node); // O(log n)
public void Merge(PriorityQueue<TElement, TPriority> other) // O(1)
public IEnumerator<PriorityQueueNode<TElement, TPriority>> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
public struct Enumerator : IEnumerator<PriorityQueueNode<TElement, TPriority>>
{
public PriorityQueueNode<TElement, TPriority> Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext() => throw new NotImplementedException();
public void Reset() => throw new NotImplementedException();
public void Dispose() => throw new NotImplementedException();
}
}
var queue = new PriorityQueue<Job, double>();
queue.Enqueue(firstJob, 10);
queue.Enqueue(secondJob, 5);
queue.Enqueue(thirdJob, 40);
var withHighestPriority = queue.Peek(); // { element: secondJob, priority: 5 }
var queue = new PriorityQueue<Job, double>();
var mapping = new Dictionary<Job, PriorityQueueNode<Job, double>>();
mapping[job] = queue.Enqueue(job, priority);
queue.Update(mapping[job], newPriority);
En orden indefinido, para que la enumeración pueda ocurrir en O (n), de manera similar a HashSet
. Ninguna implementación eficiente proporcionaría capacidades para enumerar un montón en tiempo lineal mientras se asegura que los elementos estén enumerados en orden; esto requeriría O (n log n). Debido a que el pedido de una colección se puede lograr trivialmente con .OrderBy(x => x.Priority)
y no a todos los clientes les importa la enumeración con este pedido, creemos que es mejor proporcionar un orden de enumeración indefinido.
| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | PriorityQueueAbstractQueue
e implementa la interfaz Queue
. |
| Óxido | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | Priority_queue | |
| Python | heapq | |
| Ir | montón | Hay una interfaz de montón. |
Tenga en cuenta que cuando se habla de estructuras de datos, el término _heap_ se usa 4 veces más a menudo que _priority queue_.
"array" AND "data structure"
- 17.400.000 resultados"stack" AND "data structure"
- 12.100.000 resultados"queue" AND "data structure"
- 3.850.000 resultados"heap" AND "data structure"
- 1.830.000 resultados"priority queue" AND "data structure"
- 430.000 resultados"trie" AND "data structure"
- 335.000 resultadosPor favor revise, estoy feliz de recibir comentarios y seguir iterando :) ¡Siento que estamos convergiendo! 😄 También estoy feliz de recibir preguntas, ¡las agregaré a las preguntas frecuentes!
@pgolebiowski No
TryDequeue
devuelva un nodo, porque en realidad no es un identificador en ese punto (prefiero dos parámetros de salida separados)Merge
debe ser PriorityQueue'2
no PriorityQueueNode'2
, y ¿puede aclarar el comportamiento? No estoy familiarizado con el montón de emparejamiento, pero presumiblemente los dos montones se superponen a partir de entoncesContains
para el método de 2 parámetros: ese no es un nombre que supongo para un método de estilo TryGet
IEqualityComparer<TElement>
con el propósito de Contains
?Remove
devuelve un bool
; Esperaría que se llamara TryRemove
o que arrojara (lo que supongo que lo hace Update
si el nodo no está en el montón).@VisualMelon ¡muchas gracias por los comentarios! Resolveré rápidamente estos, definitivamente de acuerdo:
- Parece extraño que
TryDequeue
devuelva un nodo, porque en realidad no es un identificador en ese punto (prefiero dos parámetros de salida separados)- El parámetro para
Merge
debe serPriorityQueue'2
noPriorityQueueNode'2
, y ¿puede aclarar el comportamiento? No estoy familiarizado con el montón de emparejamiento, pero presumiblemente los dos montones se superponen a partir de entonces- Es extraño que
Remove
devuelve unbool
; Esperaría que se llamaraTryRemove
o que arrojara (lo que supongo que lo haceUpdate
si el nodo no está en el montón).- No soy fanático del nombre
Contains
para el método de 2 parámetros: ese no es un nombre que supongo para un método de estiloTryGet
Aclaración para estos dos:
- ¿Será estable en el sentido de que si dos elementos se ponen en cola con la misma prioridad, se retiran de la cola en un orden predecible? (es bueno tenerlo para la reproducibilidad; de lo contrario, el consumidor puede implementarlo con bastante facilidad)
- No parece haber una manera de determinar de manera eficiente si un nodo todavía está en el montón (no estoy seguro de cuándo usaría esto, pensó)
Para el primer punto, si el objetivo es la reproducibilidad, entonces la implementación será determinista, sí. Si el objetivo es _si dos elementos que se colocan en la cola con la misma prioridad, se deduce que saldrán en el mismo orden en el que se colocaron en la cola_; no estoy seguro de si la implementación se puede ajustar a lograr esta propiedad, una respuesta rápida sería "probablemente no".
Para el segundo punto, sí, los montones no son buenos para verificar si existe un elemento en la colección, un cliente tendría que realizar un seguimiento de esto por separado para lograrlo en O (1) o reutilizar el mapeo que usan para los identificadores, si utilizan el mecanismo de actualización. De lo contrario, O (n).
- ¿Debería la clase admitir un
IEqualityComparer<TElement>
con el propósito deContains
?
Hmm ... estoy empezando a pensar que quizás poner Contains
en la responsabilidad de esta cola de prioridad puede ser demasiado, y usar el método de Linq
puede ser suficiente ( Contains
debe aplicarse en la enumeración de todos modos).
@pgolebiowski gracias por las aclaraciones
Para el primer punto, si el objetivo es la reproducibilidad, entonces la implementación será determinista, sí.
No tanto determinista, como garantizado para siempre (por ejemplo, incluso la implementación cambia, el comportamiento no lo haría), así que creo que la respuesta que buscaba es "no". Eso está bien: el consumidor puede agregar un ID de secuencia a la prioridad si lo necesita, aunque en ese momento un SortedSet
haría el trabajo.
Para el segundo punto, sí, los montones no son buenos para verificar si existe un elemento en la colección, un cliente tendría que realizar un seguimiento de esto por separado para lograrlo en O (1) o reutilizar el mapeo que usan para los identificadores, si utilizan el mecanismo de actualización. De lo contrario, O (n).
¿No requiere un subconjunto del trabajo necesario para Remove
? Puede que no haya sido claro: me refiero a que me dieron un PriorityQueueNode
, verificando si está en el montón (no un TElement
).
Hmm ... estoy empezando a pensar que quizás poner a Contains en la responsabilidad de esta cola de prioridad puede ser demasiado
No me quejaría si Contains
no estuviera allí: también es una trampa para las personas que no se dan cuenta de que deberían usar identificadores.
@pgolebiowski Pareces estar muy a favor de los tiradores, ¿estoy en lo cierto al pensar que es por razones de eficiencia?
Desde el punto de vista de la eficiencia, sospecho que los manejadores son realmente los mejores, para ambos escenarios con y sin singularidad, por lo que estoy de acuerdo con eso como la solución principal que ofrece el marco.
En absoluto:
Desde el punto de vista de la usabilidad, creo que los elementos duplicados rara vez son lo que quiero, lo que todavía me deja preguntándome si es valioso que el framework brinde soporte para ambos modelos. Pero ... una clase "PrioritySet" compatible con elementos únicos sería al menos fácil de agregar más adelante como envoltorio para el PriorityQueue
, por ejemplo, en respuesta a la demanda continua de una API más amigable. (si existe la demanda. ¡Como creo que podría!)
Para la API actual que se propuso, un par de pensamientos / preguntas:
TryPeek(out TElement element, out TPriority priority)
?Remove(PriorityQueueNode)
si no se encuentra el nodo? o devolver falso?TryRemove()
que no arroje si Eliminar arroja?Contains()
api sea útil la mayor parte del tiempo? ¡'Contiene' parece estar en el ojo del espectador, especialmente para escenarios con elementos 'duplicados' con prioridades distintas u otras características distintas! En cuyo caso, el usuario final probablemente deba hacer su propia búsqueda de todos modos. Pero al menos puede ser útil para el escenario sin duplicados.@pgolebiowski ¡ Gracias por tomarse el tiempo para redactar la nueva propuesta! Algunos comentarios de mi parte:
Contains()
o TryGetNode()
deban existir en absoluto en la API en su forma propuesta actualmente. Implican que la igualdad para TElement
es significativa, que presumiblemente es una de las cosas que un enfoque basado en identificadores estaba tratando de evitar.public void Dequeue(out TElement element);
como public TElement Dequeue();
TryDequeue()
necesita devolver una prioridad?ICollection<T>
o IReadOnlyCollection<T>
?La mayoría de los ingenieros de software solo están familiarizados con la implementación del montón binario basada en arreglos; es la más simple, pero desafortunadamente no es la más eficiente. Para la entrada aleatoria general, dos ejemplos de tipos de pila más eficientes son: pila cuaternaria y pila de emparejamiento.
¿Cuáles son las ventajas y desventajas de elegir los últimos enfoques? ¿Es posible utilizar una implementación basada en matrices para esos?
No puede implementar ICollection<T>
o cuando no tiene las firmas correctas para 'Agregar' y 'Eliminar', etc.IReadOnlyCollection<T>
Es mucho más cercano en espíritu / forma a ICollection<KeyValuePair<T,Priority>>
Es mucho más cercano en espíritu / forma a
ICollection<KeyValuePair<T,Priority>>
¿No sería KeyValuePair<TPriority, TElement>
, ya que el orden lo realiza TPriority, que es efectivamente un mecanismo de codificación?
De acuerdo, parece que en general estamos a favor de eliminar los métodos Contains
y TryGet
. Los eliminará en la próxima revisión y explicará el motivo de la eliminación en las preguntas frecuentes.
En cuanto a las interfaces implementadas, ¿no es suficiente IEnumerable<PriorityQueueNode<TElement, TPriority>>
? ¿Qué tipo de funcionalidad falta?
En el KeyValuePair
, hubo algunas voces en las que una tupla o una estructura con .Element
y .Priority
son más deseables. Creo que estoy a favor de estos.
¿No sería
KeyValuePair<TPriority, TElement>
, ya que el orden lo realiza TPriority, que es efectivamente un mecanismo de codificación?
Hay buenos argumentos para ambas partes. Por un lado, sí, exactamente lo que acaba de decir. Por otro lado, generalmente se espera que las claves en una colección KVP sean únicas, y es perfectamente válido tener múltiples elementos con la misma prioridad.
Por otro lado, generalmente se espera que las claves en una colección KVP sean únicas
No estoy de acuerdo con esta afirmación: una colección de pares clave-valor es solo eso; cualquier requisito de singularidad se superpone a eso.
¿No es
IEnumerable<PriorityQueueNode<TElement, TPriority>>
suficiente? ¿Qué tipo de funcionalidad falta?
Personalmente, esperaría que se implemente IReadOnlyCollection<PQN<TElement, TPriority>>
, ya que la API proporcionada ya satisface en su mayoría esa interfaz. Además, eso sería consistente con otros tipos de colección.
Respecto a la interfaz:
''
public bool TryGetNode (elemento TElement, fuera PriorityQueueNode
What's the point of it if I can just do enumerate collection and do comparison of elements? And why only Try version?
public PriorityQueueNode<TElement, TPriority> Peek(); // O(1)
public void Dequeue(out TElement element); // O(log n)
I find it a bit strange to have discrepancy between Dequeue and Peek. They do pretty much same thing expect one is removing element from queue and other is not, it's looks weird for me if one returns priority and element and other just element.
public void Remove(PriorityQueueNode<TElement, TPriority> node); // O(log n)
`Queue` doesn't have `Remove` method, why `PriorityQueue` should ?
I would also like to see constructor
Public PriorityQueue (IEnumerable
''
También me gustaría que PriorityQueue
implemente la interfaz IReadonlyCollection<>
.
Saltar a otras cosas que no sean la superficie API pública.
Dicha capacidad es necesaria para implementar, por ejemplo, el algoritmo de ruta más corta de Dijkstra o un programador de trabajos que necesita manejar las prioridades cambiantes. Falta el mecanismo de actualización en Java, lo que ha demostrado ser decepcionante para los ingenieros, por ejemplo, en estas tres preguntas de StackOverflow vistas más de 32.000 veces: ejemplo, ejemplo, ejemplo. Para evitar introducir una API con un valor tan limitado, creemos que un requisito fundamental para la funcionalidad de la cola de prioridad que ofrecemos sería admitir la capacidad de actualizar las prioridades para los elementos que ya están presentes en la colección.
Me gustaría estar en desacuerdo. La última vez que escribí Dijkstra en C ++ std :: priority_queue fue suficiente y no maneja la actualización de prioridad. El consenso común de AFAIK para ese caso es agregar un elemento falso a la cola con prioridad y valor alterados y monitorear si procesamos el valor o no. Lo mismo se puede hacer con el programador de trabajos.
Para ser honesto, no estoy seguro de cómo se vería Dijkstra con la propuesta de cola actual. ¿Cómo se supone que debo realizar un seguimiento de los nodos que necesito actualizar con prioridad? ¿Con TryGetNode ()? ¿O tienes otra colección de nodos? Me encantaría ver el código de la propuesta actual.
Si miras en Wikipedia, no hay suposición de prioridades de actualización para la cola de prioridad. Lo mismo para todos los demás idiomas que no tienen esa funcionalidad y se salieron con la suya. Sé "esforzarse por ser mejor", pero ¿existe realmente una demanda para eso?
Para la entrada aleatoria general, dos ejemplos de tipos de pila más eficientes son: pila cuaternaria y pila de emparejamiento. Para obtener más información sobre montones, consulte Wikipedia y este documento.
He examinado el papel y esta es una cita de él:
Los resultados muestran que la elección óptima de implementación depende en gran medida de los insumos. Además, muestra que se debe tener cuidado para optimizar el rendimiento de la caché, principalmente en la barrera L1-L2. Esto sugiere que es poco probable que las estructuras complicadas y ajenas a la memoria caché funcionen bien en comparación con las estructuras más simples y conscientes de la memoria caché.
Por lo que parece, la propuesta de cola actual estaría bloqueada detrás de la implementación del árbol en lugar de la matriz, y algo me dice que existe la posibilidad de que los nodos de árbol dispersos por la memoria no sean tan eficaces como la matriz de elementos.
Creo que tener puntos de referencia para comparar el montón binario simple basado en la matriz y el montón de emparejamiento sería ideal para tomar la decisión adecuada, antes de eso, no creo que sea inteligente bloquear el diseño detrás de una implementación específica (te estoy mirando Merge
método).
Saltando a otro tema, personalmente preferiría tener KeyValuePair
La desventaja son los sentimientos encontrados al mirar TPriority Key
pero creo que una buena documentación puede resolver eso.
'
Como solución alternativa para Dijkstra, agregar elementos falsos en la cola funciona, y el número total de bordes de gráfico que procesa no cambia. Pero el número de nodos temporales que permanecen residentes en el montón generado por el procesamiento de bordes cambia, y eso puede afectar el uso de la memoria y la eficiencia de poner en cola y sacar de cola.
Me equivoqué al no poder hacer IReadOnlyCollection, eso debería estar bien. No Add () y Remove () en esa interfaz, ¡ja! (Qué estaba pensando...)
@ Ivanidzo4ka Tus comentarios me convencen aún más de que tendría sentido tener dos tipos separados: uno, un montón binario simple (es decir, sin actualización), y un segundo como se describe en una de las propuestas:
En el pasado, casi siempre me salí con un montón binario que escribí la mayor parte de hace una década, y una combinación SortedSet
/ Dictionary
, y hay escenarios en los que he usado ambos tipos en roles separados (KSSP me viene a la mente).
Es una estructura en lugar de una clase, por lo que no hay excepciones NullReference
Yo diría que es al revés: si alguien está pasando valores predeterminados, me encantaría que vieran un NRE; sin embargo, creo que debemos tener claro dónde esperamos que se usen estas cosas: los nodos / identificadores probablemente deberían ser clases, pero si solo estamos leyendo / devolviendo un par, estoy de acuerdo en que debería ser una estructura.
Me siento tentado de sugerir que debería ser posible actualizar el elemento, así como la prioridad en la propuesta basada en identificadores. Puede lograr el mismo efecto simplemente quitando un identificador y agregando uno nuevo, pero es una operación útil y puede tener beneficios de rendimiento dependiendo de la implementación (por ejemplo, algunos montones pueden reducir la prioridad de algo relativamente barato). Tal cambio haría que fuera más ordenado implementar muchas cosas (por ejemplo, este ejemplo un tanto inductor de pesadillas basado en el AlgoKit ParingHeap existente), especialmente aquellos que operan en una región desconocida del espacio de estados.
La comunidad .NET Core propone agregar a la biblioteca del sistema la funcionalidad _priority queue_, una estructura de datos en la que cada elemento tiene además una prioridad asociada. Específicamente, proponemos agregar PriorityQueue<TElement, TPriority>
al espacio System.Collections.Generic
nombres
En nuestro diseño, nos guiamos por los siguientes principios (a menos que conozca otros mejores):
Conceptualmente, una cola de prioridad es una colección de elementos, donde cada elemento tiene una prioridad asociada. La funcionalidad más importante de una cola de prioridad es que proporciona un acceso eficiente al elemento con la mayor prioridad en la colección y una opción para eliminar ese elemento. El comportamiento esperado también puede incluir: 1) capacidad para modificar la prioridad de un elemento que ya está en la colección; 2) capacidad para fusionar múltiples colas de prioridad.
Una cola de prioridad es una estructura de datos abstracta, es decir, es un concepto con ciertas características de comportamiento, como se describe en la sección anterior. Las implementaciones más eficientes de una cola de prioridad se basan en montones. Sin embargo, contrariamente a la idea generalizada, un montón es también una estructura de datos abstracta y se puede realizar de varias formas, cada una de las cuales ofrece diferentes ventajas y desventajas.
La mayoría de los ingenieros de software solo están familiarizados con la implementación del montón binario basada en arreglos; es la más simple, pero desafortunadamente no es la más eficiente. Para la entrada aleatoria general, dos ejemplos de tipos de pila más eficientes son: pila cuaternaria y pila de emparejamiento . Para obtener más información sobre montones, consulte Wikipedia y este documento .
Nuestras discusiones han demostrado que el área más desafiante en el diseño, y al mismo tiempo con el mayor impacto en la API, es el mecanismo de actualización. Específicamente, el desafío es determinar si y cómo el producto que queremos ofrecer a los clientes debe apoyar la actualización de las prioridades de los elementos ya presentes en la colección.
Dicha capacidad es necesaria para implementar, por ejemplo, el algoritmo de ruta más corta de Dijkstra o un programador de trabajos que necesita manejar las prioridades cambiantes. Falta el mecanismo de actualización en Java, lo que ha demostrado ser decepcionante para los ingenieros, por ejemplo, en estas tres preguntas de StackOverflow vistas más de 32.000 veces: ejemplo , ejemplo , ejemplo . Para evitar introducir una API con un valor tan limitado, creemos que un requisito fundamental para la funcionalidad de la cola de prioridad que ofrecemos sería admitir la capacidad de actualizar las prioridades para los elementos que ya están presentes en la colección.
Para entregar el mecanismo de actualización, debemos asegurarnos de que el cliente pueda ser específico sobre qué es exactamente lo que desea actualizar. Hemos identificado dos formas de entregar esto: a) a través de identificadores; yb) mediante la aplicación de la unicidad de los elementos de la colección. Cada uno de estos tiene diferentes beneficios y costos.
Opción (a): Asas. En este enfoque, cada vez que se agrega un elemento a la cola, la estructura de datos proporciona su identificador único. Si el cliente desea utilizar el mecanismo de actualización, debe realizar un seguimiento de dichos identificadores para que luego pueda especificar sin ambigüedades qué elemento desea actualizar. El costo principal de esta solución es que los clientes deben administrar estos indicadores. Sin embargo, no significa que deba haber asignaciones internas para admitir identificadores dentro de la cola de prioridad; cualquier montón no basado en arreglos se basa en nodos, donde cada nodo es automáticamente su propio identificador. Como ejemplo, consulte la API del método PairingHeap.Update .
Opción (b): Singularidad. Este enfoque impone dos restricciones adicionales al cliente: i) los elementos dentro de la cola de prioridad deben ajustarse a cierta semántica de igualdad, lo que trae una nueva clase de errores de usuario potenciales; ii) dos elementos iguales no se pueden almacenar en la misma cola. Al pagar este costo, obtenemos el beneficio de admitir el mecanismo de actualización sin recurrir al enfoque de manejo. Sin embargo, cualquier implementación que aproveche la singularidad / igualdad para determinar el elemento a actualizar requerirá un mapeo interno adicional, de modo que se realice en O (1) y no en O (n).
Recomendamos agregar a la biblioteca del sistema una clase PriorityQueue<TElement, TPriority>
que admita el mecanismo de actualización a través de identificadores. La implementación subyacente sería un montón de emparejamiento.
public class PriorityQueueNode<TElement, TPriority>
{
public TElement Element { get; }
public TPriority Priority { get; }
}
public class PriorityQueue<TElement, TPriority> :
IEnumerable<PriorityQueueNode<TElement, TPriority>>,
IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>>
{
public PriorityQueue();
public PriorityQueue(IComparer<TPriority> comparer);
public IComparer<TPriority> Comparer { get; }
public int Count { get; }
public bool IsEmpty { get; }
public void Clear();
public PriorityQueueNode<TElement, TPriority> Enqueue(TElement element, TPriority priority); //O(log n)
public PriorityQueueNode<TElement, TPriority> Peek(); // O(1)
public bool TryPeek(out PriorityQueueNode<TElement, TPriority> node); // O(1)
public bool TryPeek(out TElement element, out TPriority priority); // O(1)
public bool TryPeek(out TElement element); // O(1)
public PriorityQueueNode<TElement, TPriority> Dequeue(); // O(log n)
public void Dequeue(out TElement element, out TPriority priority); // O(log n)
public bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node); // O(log n)
public bool TryDequeue(out TElement element, out TPriority priority); // O(log n)
public bool TryDequeue(out TElement element); // O(log n)
public void Update(PriorityQueueNode<TElement, TPriority> node, TPriority priority); // O(log n)
public void Remove(PriorityQueueNode<TElement, TPriority> node); // O(log n)
public void Merge(PriorityQueue<TElement, TPriority> other) // O(1)
public IEnumerator<PriorityQueueNode<TElement, TPriority>> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
public struct Enumerator : IEnumerator<PriorityQueueNode<TElement, TPriority>>
{
public PriorityQueueNode<TElement, TPriority> Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext() => throw new NotImplementedException();
public void Reset() => throw new NotImplementedException();
public void Dispose() => throw new NotImplementedException();
}
}
var queue = new PriorityQueue<Job, double>();
queue.Enqueue(firstJob, 10);
queue.Enqueue(secondJob, 5);
queue.Enqueue(thirdJob, 40);
var withHighestPriority = queue.Peek(); // { element: secondJob, priority: 5 }
var queue = new PriorityQueue<Job, double>();
var mapping = new Dictionary<Job, PriorityQueueNode<Job, double>>();
mapping[job] = queue.Enqueue(job, priority);
queue.Update(mapping[job], newPriority);
En orden indefinido, para que la enumeración pueda ocurrir en O (n), de manera similar a HashSet
. Ninguna implementación eficiente proporcionaría capacidades para enumerar un montón en tiempo lineal mientras se asegura que los elementos estén enumerados en orden; esto requeriría O (n log n). Debido a que el pedido de una colección se puede lograr trivialmente con .OrderBy(x => x.Priority)
y no a todos los clientes les importa la enumeración con este pedido, creemos que es mejor proporcionar un orden de enumeración indefinido.
Contains
o TryGet
?Proporcionar tales métodos tiene un valor insignificante, porque encontrar un elemento en un montón requiere la enumeración de toda la colección, lo que significa que cualquier método Contains
o TryGet
sería un envoltorio alrededor de la enumeración. Además, para comprobar si existe un elemento en la colección, la cola de prioridad debería saber cómo realizar comprobaciones de igualdad de los objetos TElement
, que no es algo que creemos que deba ser responsabilidad de una cola de prioridad.
Dequeue
y TryDequeue
que devuelven PriorityQueueNode
?Esto es para los clientes que desean utilizar el método Update
o Remove
y realizar un seguimiento de los identificadores. Al retirar un elemento de la cola de prioridad, recibirían un identificador que podrían usar para ajustar el estado de su sistema de seguimiento de identificaciones.
Update
o Remove
recibe un nodo de una cola diferente?La cola de prioridad arrojará una excepción. Cada nodo es consciente de la cola de prioridad a la que pertenece, de forma similar a LinkedListNode<T>
conoce el LinkedList<T>
que pertenece.
| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | PriorityQueueAbstractQueue
e implementa la interfaz Queue
. |
| Óxido | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | Priority_queue | |
| Python | heapq | |
| Ir | montón | Hay una interfaz de montón. |
Tenga en cuenta que cuando se habla de estructuras de datos, el término _heap_ se usa 4 veces más a menudo que _priority queue_.
"array" AND "data structure"
- 17.400.000 resultados"stack" AND "data structure"
- 12.100.000 resultados"queue" AND "data structure"
- 3.850.000 resultados"heap" AND "data structure"
- 1.830.000 resultados"priority queue" AND "data structure"
- 430.000 resultados"trie" AND "data structure"
- 335.000 resultados¡Gracias por toda la retroalimentación! Actualicé la propuesta a la v2.1. Registro de cambios:
Contains
y TryGet
.Contains
o TryGet
? _IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>>
.bool TryPeek(out TElement element, out TPriority priority)
.bool TryPeek(out TElement element)
.void Dequeue(out TElement element, out TPriority priority)
.void Dequeue(out TElement element)
a PriorityQueueNode<TElement, TPriority> Dequeue()
.bool TryDequeue(out TElement element)
.bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node)
.Dequeue
y TryDequeue
sobrecargas que devuelven PriorityQueueNode
? _Update
o Remove
recibe un nodo de una cola diferente? _Gracias por las notas de cambio;)
Pequeñas solicitudes de aclaración:
@pgolebiowski Con respecto al método Merge
:
public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)
Claramente, una operación de este tipo no tendría semántica de copia, por lo que me pregunto si habría algún error al hacer cambios en this
y other
_después_ de que se haya realizado una fusión (por ejemplo, cualquiera de las instancias falla para satisfacer la propiedad del montón).
@eiriktsarpalis @VisualMelon - ¡Gracias! Se abordarán los puntos planteados, ETA 2020-10-04.
Si otros tienen más comentarios / preguntas / inquietudes / pensamientos, comparta 😊
La comunidad .NET Core propone agregar a la biblioteca del sistema la funcionalidad _priority queue_, una estructura de datos en la que cada elemento tiene además una prioridad asociada. Específicamente, proponemos agregar PriorityQueue<TElement, TPriority>
al espacio System.Collections.Generic
nombres
En nuestro diseño, nos guiamos por los siguientes principios (a menos que conozca otros mejores):
Conceptualmente, una cola de prioridad es una colección de elementos, donde cada elemento tiene una prioridad asociada. La funcionalidad más importante de una cola de prioridad es que proporciona un acceso eficiente al elemento con la mayor prioridad en la colección y una opción para eliminar ese elemento. El comportamiento esperado también puede incluir: 1) capacidad para modificar la prioridad de un elemento que ya está en la colección; 2) capacidad para fusionar múltiples colas de prioridad.
Una cola de prioridad es una estructura de datos abstracta, es decir, es un concepto con ciertas características de comportamiento, como se describe en la sección anterior. Las implementaciones más eficientes de una cola de prioridad se basan en montones. Sin embargo, contrariamente a la idea generalizada, un montón es también una estructura de datos abstracta y se puede realizar de varias formas, cada una de las cuales ofrece diferentes ventajas y desventajas.
La mayoría de los ingenieros de software solo están familiarizados con la implementación del montón binario basada en arreglos; es la más simple, pero desafortunadamente no es la más eficiente. Para la entrada aleatoria general, dos ejemplos de tipos de pila más eficientes son: pila cuaternaria y pila de emparejamiento . Para obtener más información sobre montones, consulte Wikipedia y este documento .
Nuestras discusiones han demostrado que el área más desafiante en el diseño, y al mismo tiempo con el mayor impacto en la API, es el mecanismo de actualización. Específicamente, el desafío es determinar si y cómo el producto que queremos ofrecer a los clientes debe apoyar la actualización de las prioridades de los elementos ya presentes en la colección.
Dicha capacidad es necesaria para implementar, por ejemplo, el algoritmo de ruta más corta de Dijkstra o un programador de trabajos que necesita manejar las prioridades cambiantes. Falta el mecanismo de actualización en Java, lo que ha demostrado ser decepcionante para los ingenieros, por ejemplo, en estas tres preguntas de StackOverflow vistas más de 32.000 veces: ejemplo , ejemplo , ejemplo . Para evitar introducir una API con un valor tan limitado, creemos que un requisito fundamental para la funcionalidad de la cola de prioridad que ofrecemos sería admitir la capacidad de actualizar las prioridades para los elementos que ya están presentes en la colección.
Para entregar el mecanismo de actualización, debemos asegurarnos de que el cliente pueda ser específico sobre qué es exactamente lo que desea actualizar. Hemos identificado dos formas de entregar esto: a) a través de identificadores; yb) mediante la aplicación de la unicidad de los elementos de la colección. Cada uno de estos tiene diferentes beneficios y costos.
Opción (a): Asas. En este enfoque, cada vez que se agrega un elemento a la cola, la estructura de datos proporciona su identificador único. Si el cliente desea utilizar el mecanismo de actualización, debe realizar un seguimiento de dichos identificadores para que luego pueda especificar sin ambigüedades qué elemento desea actualizar. El costo principal de esta solución es que los clientes deben administrar estos indicadores. Sin embargo, no significa que deba haber asignaciones internas para admitir identificadores dentro de la cola de prioridad; cualquier montón no basado en arreglos se basa en nodos, donde cada nodo es automáticamente su propio identificador. Como ejemplo, consulte la API del método PairingHeap.Update .
Opción (b): Singularidad. Este enfoque impone dos restricciones adicionales al cliente: i) los elementos dentro de la cola de prioridad deben ajustarse a cierta semántica de igualdad, lo que trae una nueva clase de errores de usuario potenciales; ii) dos elementos iguales no se pueden almacenar en la misma cola. Al pagar este costo, obtenemos el beneficio de admitir el mecanismo de actualización sin recurrir al enfoque de manejo. Sin embargo, cualquier implementación que aproveche la singularidad / igualdad para determinar el elemento a actualizar requerirá un mapeo interno adicional, de modo que se realice en O (1) y no en O (n).
Recomendamos agregar a la biblioteca del sistema una clase PriorityQueue<TElement, TPriority>
que admita el mecanismo de actualización a través de identificadores. La implementación subyacente sería un montón de emparejamiento.
public class PriorityQueueNode<TElement, TPriority>
{
public TElement Element { get; }
public TPriority Priority { get; }
}
public class PriorityQueue<TElement, TPriority> :
IEnumerable<PriorityQueueNode<TElement, TPriority>>,
IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>>
{
public PriorityQueue();
public PriorityQueue(IComparer<TPriority> comparer);
public IComparer<TPriority> Comparer { get; }
public int Count { get; }
public bool IsEmpty { get; }
public void Clear();
public PriorityQueueNode<TElement, TPriority> Enqueue(TElement element, TPriority priority); //O(log n)
public PriorityQueueNode<TElement, TPriority> Peek(); // O(1)
public bool TryPeek(out PriorityQueueNode<TElement, TPriority> node); // O(1)
public bool TryPeek(out TElement element, out TPriority priority); // O(1)
public bool TryPeek(out TElement element); // O(1)
public PriorityQueueNode<TElement, TPriority> Dequeue(); // O(log n)
public void Dequeue(out TElement element, out TPriority priority); // O(log n)
public bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node); // O(log n)
public bool TryDequeue(out TElement element, out TPriority priority); // O(log n)
public bool TryDequeue(out TElement element); // O(log n)
public void Update(PriorityQueueNode<TElement, TPriority> node, TPriority priority); // O(log n)
public void Remove(PriorityQueueNode<TElement, TPriority> node); // O(log n)
public IEnumerator<PriorityQueueNode<TElement, TPriority>> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator();
public struct Enumerator : IEnumerator<PriorityQueueNode<TElement, TPriority>>
{
public PriorityQueueNode<TElement, TPriority> Current { get; }
object IEnumerator.Current { get; }
public bool MoveNext() => throw new NotImplementedException();
public void Reset() => throw new NotImplementedException();
public void Dispose() => throw new NotImplementedException();
}
}
var queue = new PriorityQueue<Job, double>();
queue.Enqueue(firstJob, 10);
queue.Enqueue(secondJob, 5);
queue.Enqueue(thirdJob, 40);
var withHighestPriority = queue.Peek(); // { element: secondJob, priority: 5 }
var queue = new PriorityQueue<Job, double>();
var mapping = new Dictionary<Job, PriorityQueueNode<Job, double>>();
mapping[job] = queue.Enqueue(job, priority);
queue.Update(mapping[job], newPriority);
En orden indefinido, para que la enumeración pueda ocurrir en O (n), de manera similar a HashSet
. Ninguna implementación eficiente proporcionaría capacidades para enumerar un montón en tiempo lineal mientras se asegura que los elementos estén enumerados en orden; esto requeriría O (n log n). Debido a que el pedido de una colección se puede lograr trivialmente con .OrderBy(x => x.Priority)
y no a todos los clientes les importa la enumeración con este pedido, creemos que es mejor proporcionar un orden de enumeración indefinido.
Contains
o TryGet
?Proporcionar tales métodos tiene un valor insignificante, porque encontrar un elemento en un montón requiere la enumeración de toda la colección, lo que significa que cualquier método Contains
o TryGet
sería un envoltorio alrededor de la enumeración. Además, para comprobar si existe un elemento en la colección, la cola de prioridad debería saber cómo realizar comprobaciones de igualdad de los objetos TElement
, que no es algo que creemos que deba ser responsabilidad de una cola de prioridad.
Dequeue
y TryDequeue
que devuelven PriorityQueueNode
?Esto es para los clientes que desean utilizar el método Update
o Remove
y realizar un seguimiento de los identificadores. Al retirar un elemento de la cola de prioridad, recibirían un identificador que podrían usar para ajustar el estado de su sistema de seguimiento de identificaciones.
Update
o Remove
recibe un nodo de una cola diferente?La cola de prioridad arrojará una excepción. Cada nodo es consciente de la cola de prioridad a la que pertenece, de forma similar a LinkedListNode<T>
conoce el LinkedList<T>
que pertenece. Además, si un nodo ha sido eliminado de una cola, intentar invocar Update
o Remove
también resultará con una excepción.
Merge
?La combinación de dos colas de prioridad se puede lograr en un tiempo constante, lo que la convierte en una característica tentadora para ofrecer a los clientes. Sin embargo, no tenemos datos que demuestren que existe demanda de dicha funcionalidad y no podemos justificar su inclusión en la API pública. Además, el diseño de dicha funcionalidad no es trivial y, dado que esta característica puede no ser necesaria, podría complicar innecesariamente la superficie y la implementación de la API.
Sin embargo, no incluir el método Merge
ahora es una puerta de 2 vías: si en el futuro los clientes expresan interés en que se admita la funcionalidad de fusión, será posible extender el tipo PriorityQueue
. Como tal, recomendamos no incluir el método Merge
todavía y seguir adelante con el lanzamiento.
La recogida no ofrecerá una garantía de estabilidad de forma inmediata, es decir, si dos elementos se ponen en cola con la misma prioridad, el cliente no podrá asumir que se retirarán de la cola en un orden determinado. Sin embargo, si un cliente quisiera lograr estabilidad usando nuestro PriorityQueue
, podría definir un TPriority
y el correspondiente IComparer<TPriority>
que lo aseguraría. Además, la recopilación de datos será determinista, es decir, para una secuencia determinada de operaciones, siempre se comportará de la misma manera, lo que permite la reproducibilidad.
| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | PriorityQueueAbstractQueue
e implementa la interfaz Queue
. |
| Óxido | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | Priority_queue | |
| Python | heapq | |
| Ir | montón | Hay una interfaz de montón. |
Tenga en cuenta que cuando se habla de estructuras de datos, el término _heap_ se usa 4 veces más a menudo que _priority queue_.
"array" AND "data structure"
- 17.400.000 resultados"stack" AND "data structure"
- 12.100.000 resultados"queue" AND "data structure"
- 3.850.000 resultados"heap" AND "data structure"
- 1.830.000 resultados"priority queue" AND "data structure"
- 430.000 resultados"trie" AND "data structure"
- 335.000 resultadosRegistro de cambios:
void Merge(PriorityQueue<TElement, TPriority> other) // O(1)
.Merge
?Las nuevas preguntas frecuentes se ven muy bien. Jugué con la codificación de Dijkstra contra la API propuesta, con el diccionario de identificador, y parecía básicamente bien.
El único pequeño aprendizaje que tuve al hacer esto fue que el conjunto actual de nombres / sobrecargas de métodos no funciona tan bien para la escritura implícita de out
variables. Lo que quería hacer con el código C # era TryDequeue(out var node)
, pero desafortunadamente necesitaba dar el tipo explícito de out
variable como PriorityQueueNode<>
contrario, el compilador no sabía si quería un nodo de cola de prioridad o un elemento.
var shortestDistances = new Dictionary<int, int>();
var queue = new PriorityQueue<int, int>();
var handles = new Dictionary<int, PriorityQueueNode<int, int>>();
handles[startNode] = queue.Enqueue(startNode, 0);
while (queue.TryDequeue(out PriorityQueueNode<int, int> nextQueueNode))
{
int nodeToExploreFrom = nextQueueNode.Element, minDistance = nextQueueNode.Priority;
shortestDistances.Add(nodeToExploreFrom, minDistance);
handles[nodeToExploreFrom] = null; // so it is clearly already visited
foreach (int nextNode in nodes)
{
int candidatePathDistance = minDistance + edgeDistances[nodeToExploreFrom, nextNode];
if (handles.TryGetValue(nextNode, out var nextNodeHandle))
{
if (nextNodeHandle != null && candidatePathDistance < nextNodeHandle.Priority)
{
queue.Update(nextNodeHandle, candidatePathDistance);
}
// or else... we already got there
}
else
{
handles[nextNode] = queue.Enqueue(nextNode, candidatePathDistance);
}
}
}
En mi opinión, la principal pregunta de diseño no resuelta es si la implementación debe admitir actualizaciones prioritarias o no. Puedo ver tres posibles caminos aquí:
Estos enfoques son mutuamente excluyentes y tienen sus propios conjuntos de compensaciones, respectivamente:
Es importante que identifiquemos cuál de los enfoques anteriores podría proporcionar el mejor valor para la mayoría de nuestros usuarios. Así que he estado revisando las bases de código .NET, tanto internas como públicas de Microsoft, en busca de instancias de implementaciones de Heap / PriorityQueue con el fin de comprender mejor cuáles son los patrones de uso más comunes:
La mayoría de las aplicaciones implementan variaciones de clasificación de pila o programación de trabajos. Se utilizaron algunos casos para algoritmos como la clasificación topológica o la codificación de Huffman. Se utilizó un número menor para calcular distancias en gráficos. De las 80 implementaciones de PriorityQueue examinadas, se encontró que solo 9 habían implementado algún tipo de actualización de prioridad.
Al mismo tiempo, ninguna de las implementaciones de Heap / PriorityQueue en las bibliotecas centrales de Python, Java, C ++, Go, Swift o Rust admite actualizaciones prioritarias en sus API.
A la luz de estos datos, me queda claro que .ΝΕΤ necesita una implementación de PriorityQueue básica que exponga la API esencial (heapify / push / peek / pop), ofrezca ciertas garantías de rendimiento (por ejemplo, sin asignaciones adicionales por cola) y no aplica restricciones de unicidad. Esto implica que la implementación _no admitiría actualizaciones de prioridad O (log n )_.
También deberíamos considerar realizar un seguimiento con una implementación de montón separada que admita _O (log n) _ actualizaciones / eliminaciones y utilice un enfoque basado en la igualdad. Dado que este sería un tipo especializado, el requisito de unicidad no debería ser un gran problema.
He estado trabajando en la creación de prototipos de ambas implementaciones y, en breve, haré un seguimiento con una propuesta de API.
¡Muchas gracias por el análisis, @eiriktsarpalis! Agradezco en particular el tiempo para analizar la base de código interna de Microsoft para encontrar casos de uso relevantes y respaldar nuestra discusión con datos.
El enfoque basado en el identificador implicaría al menos una asignación adicional por elemento en cola.
Esta suposición es falsa, no necesita asignaciones adicionales en montones basados en nodos. El montón de emparejamiento tiene más rendimiento para una entrada aleatoria general que el montón binario basado en matrices, lo que significa que tendría el incentivo de usar este montón basado en nodos incluso para una cola de prioridad que no admite actualizaciones. Puede ver los puntos de referencia en el documento al que hice referencia anteriormente .
De las 80 implementaciones de PriorityQueue examinadas, se encontró que solo 9 habían implementado algún tipo de actualización de prioridad.
Incluso teniendo en cuenta esta pequeña muestra, eso es del 11 al 12% de todos los usos. Además, esto puede estar subrepresentado en ciertos dominios como, por ejemplo, los videojuegos, donde esperaría que este porcentaje sea mayor.
A la luz de estos datos, me queda claro que [...]
No creo que esa conclusión sea clara, dado que una de las suposiciones clave es falsa y es discutible si el 11-12% de los clientes es un caso de uso lo suficientemente importante o no. Lo que me falta en su evaluación es la evaluación del impacto en los costos de la "estructura de datos que respalda las actualizaciones" para los clientes a los que no les importa este mecanismo, que para mí, este costo es insignificante, ya que podrían usar la estructura de datos. sin verse afectado por el mecanismo de la manija.
Básicamente:
| | 11-12% de casos de uso | 88-89% de casos de uso |
|: -: |: -: |: -: |
| se preocupa por las actualizaciones | si | no |
| se ve afectado negativamente por las asas | N / A (se desean) | no |
| se ve positivamente afectado por las asas | si | no |
Para mí, esto es una obviedad a favor de admitir el 100% de los casos de uso, no solo el 88-89%.
Esta suposición es falsa, no necesita asignaciones adicionales en montones basados en nodos
Si tanto la prioridad como el elemento son tipos de valor (o si ambos son tipos de referencia que no posee y / o no puede cambiar el tipo base), puede vincular a una implementación que demuestre que no se requieren asignaciones adicionales (o solo describe cómo se logra)? Sería útil ver eso. Gracias.
Sería útil si pudiera dar más detalles o simplemente decir lo que está tratando de decir. Necesitaría eliminar la ambigüedad, habría ping-pong y eso se convertiría en una larga discusión. Alternativamente, podríamos concertar una llamada.
Estoy diciendo que queremos evitar cualquier operación de Enqueue que requiera una asignación, ya sea por parte del llamador o por parte de la implementación (una asignación interna amortizada está bien, por ejemplo, para expandir una matriz utilizada en la implementación). Estoy tratando de entender cómo eso es posible con un montón basado en nodos (por ejemplo, si esos objetos de nodo están expuestos a la persona que llama, eso prohíbe la agrupación por la implementación debido a preocupaciones sobre la reutilización / aliasing inapropiados). Quiero poder escribir:
C#
pq.Enqueue(42, 84);
y que no asigne. ¿Cómo las implementaciones a las que te refieres para lograrlo?
o simplemente di lo que intentas decir
Yo pensé que era.
queremos evitar cualquier operación Enqueue que requiera una asignación [...] Quiero poder escribir:
pq.Enqueue(42, 84);
y que no asigne.
¿De dónde viene este deseo? Es bueno tener un efecto secundario de una solución, no un requisito que el 99,9% de los clientes deben satisfacer. No veo por qué elegiría esta dimensión de bajo impacto para tomar decisiones de diseño entre soluciones.
No estamos tomando decisiones de diseño basadas en optimizaciones para el 0.1% de los clientes si estas impactan negativamente al 12% de los clientes en otra dimensión. "preocuparse por no asignaciones" + "tratar con dos tipos de valores" es un caso marginal.
Encuentro la dimensión de comportamiento / funcionalidad soportada mucho más importante, especialmente cuando se diseña una estructura de datos versátil de propósito general para una amplia audiencia y una variedad de casos de uso.
¿De dónde viene este deseo?
Desde querer que los tipos de colecciones principales se puedan usar en escenarios que se preocupan por el rendimiento. Dice que la solución basada en nodos admitiría el 100% de los casos de uso: ese no es el caso si cada cola asigna, al igual que List<T>
, Dictionary<TKey, TValue>
, HashSet<T>
, etc. on se volvería inutilizable en muchas situaciones si asignaran en cada Add.
¿Por qué cree que solo el "0,1%" se preocupa por los gastos generales de asignación de estos métodos? ¿De dónde provienen esos datos?
"preocuparse por no asignaciones" + "tratar con dos tipos de valores" es un caso marginal
No lo es. Tampoco se trata sólo de "dos tipos de valores". Según tengo entendido, la solución propuesta requeriría a) una asignación en cada cola independientemente de los Ts involucrados, ob) requeriría que el tipo de elemento se derive de algún tipo de base conocido que a su vez prohíbe una gran cantidad de usos posibles para Evite la asignación adicional.
@eiriktsarpalis
Para que no olvide ninguna opción, creo que hay una opción 4 factible para agregar a las opciones 1, 2 y 3, en su lista, que es un compromiso:
Todavía podríamos decidir que debido a que hay un rendimiento adicional disponible para el 88% o el 12% de una implementación que no necesita una estructura de datos actualizable, o que está optimizada para una en primer lugar, es mejor proporcionar las opciones 2 y 3, que opción 4. Pero pensé que no deberíamos olvidar que existe otra opción.
[O supongo que podría ver esto como una mejor opción 1 y actualizar la descripción de 1 para decir que la contabilidad no es forzada, sino perezosa, y solo se requiere un comportamiento equitativo correcto cuando se usan actualizaciones ...]
@stephentoub Eso es exactamente lo que tenía en mente acerca de decir simplemente lo que quieres decir, gracias :)
¿Por qué cree que solo el 0,1% se preocupa por los gastos generales de asignación de estos métodos? ¿De dónde provienen esos datos?
Desde la intuición, es decir, la misma fuente basada en la que cree que es más importante priorizar "sin asignaciones adicionales" sobre "la capacidad de realizar actualizaciones". Al menos para el mecanismo de actualización, tenemos los datos que el 11-12% de los clientes necesitan para que este comportamiento sea compatible. No creo que a los clientes ni remotamente cercanos les importe "ninguna asignación adicional".
En cualquier caso, por alguna razón está optando por concentrarse en la dimensión de la memoria, olvidándose de otras dimensiones, por ejemplo, la velocidad bruta, que es otra compensación para su enfoque preferido. Una implementación basada en matrices que proporciona "sin asignaciones adicionales" sería más lenta que una implementación basada en nodos. Nuevamente, creo que aquí es arbitrario priorizar la memoria sobre la velocidad.
Demos un paso atrás y centrémonos en lo que quieren los clientes. Tenemos una opción de diseño que puede o no hacer que la estructura de datos sea inutilizable para el 12% de los clientes. Creo que tendríamos que tener mucho cuidado al proporcionar las razones por las que elegiríamos no apoyarlas.
Una implementación basada en matrices que proporciona "sin asignaciones adicionales" sería más lenta que una implementación basada en nodos.
Comparta las dos implementaciones de C # que está utilizando para realizar esa comparación y los puntos de referencia utilizados para llegar a esa conclusión. Los artículos teóricos son ciertamente valiosos, pero son solo una pequeña pieza del rompecabezas. Lo más importante es cuando el caucho se encuentra con el camino, teniendo en cuenta los detalles de la plataforma dada y las implementaciones dadas, y puede validar en la plataforma específica con la implementación específica y los conjuntos de datos / patrones de uso típicos / esperados. Es muy posible que su afirmación sea correcta. También puede que no lo sea. Me gustaría ver las implementaciones / datos para comprender mejor.
Comparta las dos implementaciones de C # que está utilizando para realizar esa comparación y los puntos de referencia utilizados para llegar a esa conclusión.
Este es un punto válido, el documento que cito solo compara y evalúa implementaciones en C ++. Realiza múltiples evaluaciones comparativas con diferentes conjuntos de datos y patrones de uso. Estoy bastante seguro de que esto sería transferible a C #, pero si cree que esto es algo que debemos redoblar, creo que el mejor curso de acción sería que le pidiera a un colega que realizara dicho estudio.
@pgolebiowski Me interesaría comprender mejor la naturaleza de su objeción. La propuesta aboga por dos tipos separados, ¿no cubriría eso sus requisitos?
- una implementación que admite el caso de uso del 12%, mientras que también casi optimiza para el otro 88% al permitir actualizaciones de elementos que son equiparables, y solo construye de manera perezosa la tabla de búsqueda requerida para hacer esas actualizaciones la primera vez que se llama a un método de actualización ( y actualizándolo en actualizaciones posteriores + eliminaciones). Por lo tanto, incurren en un menor costo para las aplicaciones que no usan la funcionalidad.
Probablemente clasificaría eso como una optimización del rendimiento para la opción 1, sin embargo, veo un par de problemas con ese enfoque en particular:
@eiriktsarpalis Es solo O (n) una vez, y O (1) después, que se amortiza O (1). Y puede posponer la validación de la unicidad hasta la primera actualización. Pero tal vez esto sea demasiado inteligente. Dos clases son más fáciles de explicar.
Pasé los últimos días creando prototipos de dos implementaciones de PriorityQueue: una implementación básica sin soporte de actualización y una implementación que admite actualizaciones utilizando la igualdad de elementos. He nombrado al primero PriorityQueue
y al último, a falta de un nombre mejor, PrioritySet
. Mi objetivo es evaluar la ergonomía de la API y comparar el rendimiento.
Las implementaciones se pueden encontrar en este repositorio . Ambas clases se implementan utilizando montones cuádruples basados en matrices. La implementación actualizable también usa un diccionario que mapea elementos a índices de pila internos.
namespace System.Collections.Generic
{
public class PriorityQueue<TElement, TPriority> : IReadOnlyCollection<(TElement Element, TPriority Priority)>
{
// Constructors
public PriorityQueue();
public PriorityQueue(int initialCapacity);
public PriorityQueue(IComparer<TPriority>? comparer);
public PriorityQueue(int initialCapacity, IComparer<TPriority>? comparer);
public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> values);
public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> values, IComparer<TPriority>? comparer);
// Properties
public int Count { get; }
public IComparer<TPriority> Comparer { get; }
// O(log(n)) push operation
public void Enqueue(TElement element, TPriority priority);
// O(1) peek operations
public TElement Peek();
public bool TryPeek(out TElement element, out TPriority priority);
// O(log(n)) pop operations
public TElement Dequeue();
public bool TryDequeue(out TElement element, out TPriority priority);
// Combined push/pop, generally more efficient than sequential Enqueue();Dequeue() calls.
public TElement EnqueueDequeue(TElement element, TPriority priority);
public void Clear();
public Enumerator GetEnumerator();
public struct Enumerator : IEnumerator<(TElement Element, TPriority Priority)>, IEnumerator;
}
}
Aquí hay una muestra básica usando el tipo
var queue = new PriorityQueue<string, int>();
queue.Enqueue("John", 1940);
queue.Enqueue("Paul", 1942);
queue.Enqueue("George", 1943);
queue.Enqueue("Ringo", 1940);
Assert.Equal("John", queue.Dequeue());
Assert.Equal("Ringo", queue.Dequeue());
Assert.Equal("Paul", queue.Dequeue());
Assert.Equal("George", queue.Dequeue());
namespace System.Collections.Generic
{
public class PrioritySet<TElement, TPriority> : IReadOnlyCollection<(TElement Element, TPriority Priority)> where TElement : notnull
{
// Constructors
public PrioritySet();
public PrioritySet(int initialCapacity);
public PrioritySet(IComparer<TPriority> comparer);
public PrioritySet(int initialCapacity, IComparer<TPriority>? priorityComparer, IEqualityComparer<TElement>? elementComparer);
public PrioritySet(IEnumerable<(TElement Element, TPriority Priority)> values);
public PrioritySet(IEnumerable<(TElement Element, TPriority Priority)> values, IComparer<TPriority>? comparer, IEqualityComparer<TElement>? elementComparer);
// Members shared with baseline PriorityQueue implementation
public int Count { get; }
public IComparer<TPriority> Comparer { get; }
public void Enqueue(TElement element, TPriority priority);
public TElement Peek();
public bool TryPeek(out TElement element, out TPriority priority);
public TElement Dequeue();
public bool TryDequeue(out TElement element, out TPriority priority);
public TElement EnqueueDequeue(TElement element, TPriority priority);
// Update methods and friends
public bool Contains(TElement element); // O(1)
public bool TryRemove(TElement element); // O(log(n))
public bool TryUpdate(TElement element, TPriority priority); // O(log(n))
public void EnqueueOrUpdate(TElement element, TPriority priority); // O(log(n))
public void Clear();
public Enumerator GetEnumerator();
public struct Enumerator : IEnumerator<(TElement Element, TPriority Priority)>, IEnumerator;
}
}
Escribí un banco de pruebas de clasificación de pila simple que compara las dos implementaciones en su aplicación más básica. También incluí un punto de referencia de clasificación que usa Linq para la comparación:
BenchmarkDotNet=v0.12.1, OS=ubuntu 20.04
AMD EPYC 7452, 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=5.0.100-rc.2.20479.15
[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.47505, CoreFX 5.0.20.47505), X64 RyuJIT
DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.47505, CoreFX 5.0.20.47505), X64 RyuJIT
| Método Tamaño | Media | Error | StdDev | Proporción | RatioSD | Gen 0 | Gen 1 | Gen 2 | Asignado |
| -------------- | ------ | -------------: | -----------: | -----------: | ------: | --------: | --------: | -------- : | --------: | ----------: |
| LinqSort | 30 | 1.439 nosotros | 0,0072 nosotros | 0,0064 nosotros | 1,00 | 0,00 | 0,0095 | - | - | 672 B |
| PriorityQueue | 30 | 1.450 us | 0,0085 nosotros | 0,0079 nosotros | 1,01 | 0,01 | - | - | - | - |
| PrioritySet | 30 | 2.778 nosotros | 0.0217 nosotros | 0,0192 nosotros | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 300 | 24.727 nosotros | 0.1032 nosotros | 0.0915 nosotros | 1,00 | 0,00 | 0,0305 | - | - | 3912 B |
| PriorityQueue | 300 | 29.510 nosotros | 0.0995 nosotros | 0.0882 nosotros | 1,19 | 0,01 | - | - | - | - |
| PrioritySet | 300 | 47.715 nosotros | 0.4455 nosotros | 0.4168 nosotros | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015 nosotros | 1.5495 nosotros | 1.3736 nosotros | 1,00 | 0,00 | 0,4883 | - | - | 36312 B |
| PriorityQueue | 3000 | 491.722 nosotros | 4.1463 nosotros | 3.8785 nosotros | 1,19 | 0,01 | - | - | - | - |
| PrioritySet | 3000 | 677.959 nosotros | 3.1996 nosotros | 2.4981 nosotros | 1,64 | 0,01 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 30000 | 5,223.560 nosotros | 11.9077 nosotros | 9,9434 nosotros | 1,00 | 0,00 | 93,7500 | 93,7500 | 93,7500 | 360910 B |
| PriorityQueue | 30000 | 5,688.625 nosotros | 53.0746 nosotros | 49.6460 nosotros | 1,09 | 0,01 | - | - | - | 2 B |
| PrioritySet | 30000 | 8,124.306 nosotros | 39.9498 nosotros | 37.3691 nosotros | 1,55 | 0,01 | - | - | - | 4 B |
Como se puede esperar, la sobrecarga de la ubicación de los elementos de seguimiento agrega un impacto significativo en el rendimiento, aproximadamente un 40-50% más lento en comparación con la implementación de línea de base.
Agradezco todo el esfuerzo, veo que tomó mucho tiempo y energía.
tl; dr:
(priority: ComputePriority(element), element)
en un PQ, y mi función de obtención de prioridad sería simplemente tuple => tuple.priority
.Trabajo en búsqueda de alto rendimiento (planificación de movimiento) y código de geometría computacional (por ejemplo, algoritmos de línea de barrido) que es relevante tanto para la robótica como para los juegos, utilizo muchas colas de prioridad personalizadas enrolladas a mano. El otro caso de uso común que tengo es una consulta Top-K donde la prioridad actualizable no es útil.
Algunos comentarios sobre el debate sobre las dos implementaciones (sí frente a sin soporte de actualización).
Nombrar:
Rendimiento:
API:
Func<TElement, TPriority>
lugar de insertar con prioridad. Si la prioridad informática es cara, simplemente puedo insertar (priority, element)
y proporcionar una función tuple => tuple.priority
Dicho esto, para una cantidad significativa de mis algoritmos, mis elementos de la cola de prioridad contienen sus prioridades, y almacenar eso dos veces (una en el PQ, una vez en los elementos) suena ineficiente. Este es especialmente el caso si estoy ordenando por numerosas claves (caso de uso similar a OrderBy.ThenBy.ThenBy). Esta API también limpia muchas de las inconsistencias donde Insert tiene una prioridad pero Peek no lo devuelve.
Finalmente, vale la pena señalar que a menudo inserto índices de una matriz en una cola de prioridad, en lugar de los elementos de la matriz en sí mismos . Sin embargo, esto es compatible con todas las API discutidas hasta ahora. Como ejemplo, si estoy procesando el comienzo / final de los intervalos en una línea del eje x, podría tener eventos de cola de prioridad (x, isStartElseEnd, intervalId)
y ordenar por x y luego por isStartElseEnd. A menudo, esto se debe a que tengo otras estructuras de datos que se asignan desde un índice a algunos datos calculados.
@pgolebiowski Me tomé la libertad de incluir la implementación de la
| Método Tamaño | Media | Error | StdDev | Mediana | Gen 0 | Gen 1 | Gen 2 | Asignado |
| -------------- | -------- | -------------------: | ---- -------------: | -----------------: | ---------------- ---: | ----------: | ------: | ------: | -----------: |
| PriorityQueue | 10 | 774,7 ns | 3,30 ns | 3,08 ns | 773,2 ns | - | - | - | - |
| PrioritySet | 10 | 1.643,0 ns | 3,89 ns | 3,45 ns | 1.642,8 ns | - | - | - | - |
| PairingHeap | 10 | 1.660,2 ns | 14,11 ns | 12,51 ns | 1.657,2 ns | 0,0134 | - | - | 960 B |
| PriorityQueue | 50 | 6.413,0 ns | 14,95 ns | 13,99 ns | 6.409,5 ns | - | - | - | - |
| PrioritySet | 50 | 12,193.1 ns | 35,41 ns | 29,57 ns | 12.188,3 ns | - | - | - | - |
| PairingHeap | 50 | 13.955,8 ns | 193,36 ns | 180,87 ns | 13.989,2 ns | 0,0610 | - | - | 4800 B |
| PriorityQueue | 150 | 27.402,5 ns | 76,52 ns | 71,58 ns | 27.410,2 ns | - | - | - | - |
| PrioritySet | 150 | 48.485,8 ns | 160,22 ns | 149,87 ns | 48.476,3 ns | - | - | - | - |
| PairingHeap | 150 | 56.951,2 ns | 190,52 ns | 168,89 ns | 56.953,6 ns | 0,1831 | - | - | 14400 B |
| PriorityQueue | 500 | 124.933,7 ns | 429,20 ns | 380,48 ns | 124.824,4 ns | - | - | - | - |
| PrioritySet | 500 | 206,310.0 ns | 433,97 ns | 338,81 ns | 206,319.0 ns | - | - | - | - |
| PairingHeap | 500 | 229.423,9 ns | 3.213,33 ns | 2.848,53 ns | 230,398.7 ns | 0,4883 | - | - | 48000 B |
| PriorityQueue | 1000 | 284.481,8 ns | 475,91 ns | 445,16 ns | 284.445,6 ns | - | - | - | - |
| PrioritySet | 1000 | 454.989,4 ns | 3.712,11 ns | 3.472,31 ns | 455,354.0 ns | - | - | - | - |
| PairingHeap | 1000 | 459.049,3 ns | 1.706,28 ns | 1.424,82 ns | 459.364,9 ns | 0,9766 | - | - | 96000 B |
| PriorityQueue | 10000 | 3.788.802,4 ns | 11.715,81 ns | 10.958,98 ns | 3.787.811,9 ns | - | - | - | 1 B |
| PrioritySet | 10000 | 5.963.100,4 ns | 26,669.04 ns | 22.269,86 ns | 5.950.915,5 ns | - | - | - | 2 B |
| PairingHeap | 10000 | 6.789.719,0 ns | 134,453.01 ns | 265.397,13 ns | 6,918,392.9 ns | 7,8125 | - | - | 960002 B |
| PriorityQueue | 1000000 | 595.059.170,7 ns | 4.001.349,38 ns | 3.547.092,00 ns | 595.716.610,5 ns | - | - | - | 4376 B |
| PrioritySet | 1000000 | 1.592.037.780,9 ns | 13,925,896.05 ns | 12.344.944,12 ns | 1.591.051.886,5 ns | - | - | - | 288 B |
| PairingHeap | 1000000 | 1.858.670.560,7 ns | 36.405.433,20 ns | 59.815.170,76 ns | 1.838.721.629,0 ns | 1000,0000 | - | - | 96000376 B |
~ A la luz de lo anterior, sigo creyendo que existen compensaciones válidas entre el montón de matriz de línea base y el enfoque del montón de emparejamiento ~.
EDITAR: actualicé los resultados después de una corrección de errores en mis puntos de referencia, gracias @VisualMelon
@eiriktsarpalis Su punto de referencia para el PairingHeap
es, creo, incorrecto: los parámetros de Add
están al revés. Cuando los intercambias, es una historia diferente: https://gist.github.com/VisualMelon/00885fe50f7ab0f4ae5cd1307312109f
(Hice exactamente lo mismo cuando lo implementé por primera vez)
Tenga en cuenta que esto no significa que el montón de emparejamiento sea más rápido o más lento, sino que parece depender en gran medida de la distribución / orden de los datos suministrados.
@eiriktsarpalis re: la utilidad de PrioritySet ...
No deberíamos esperar que la actualización sea más lenta para heapsort, ya que no tiene actualizaciones prioritarias en el escenario. (También para heapsort, es probable que incluso desee mantener duplicados, un conjunto simplemente no es apropiado).
La prueba de fuego para ver si PrioritySet es útil deberían ser los algoritmos de evaluación comparativa que utilizan actualizaciones de prioridad, frente a una implementación sin actualización del mismo algoritmo, poniendo en cola los valores duplicados e ignorando los duplicados al retirar de la cola.
Gracias @VisualMelon , actualicé mis resultados y comentarios después de la solución sugerida.
más bien parece depender en gran medida de la distribución / orden de los datos suministrados.
Creo que podría haberse beneficiado del hecho de que las prioridades en cola eran monótonas.
La prueba de fuego para ver si PrioritySet es útil deberían ser los algoritmos de evaluación comparativa que utilizan actualizaciones de prioridad, frente a una implementación sin actualización del mismo algoritmo, poniendo en cola los valores duplicados e ignorando los duplicados al retirar de la cola.
@TimLovellSmith, mi objetivo aquí era medir el rendimiento de la aplicación PriorityQueue más común: en lugar de medir el rendimiento de las actualizaciones, quería ver el impacto en el caso en que las actualizaciones no sean necesarias en absoluto. Sin embargo, podría tener sentido producir un punto de referencia separado que compare el montón de emparejamiento con las actualizaciones de "PrioritySet".
@miyu, gracias por sus comentarios detallados, ¡es muy apreciado!
@TimLovellSmith Escribí un punto de referencia simple que usa actualizaciones:
| Método Tamaño | Media | Error | StdDev | Mediana | Gen 0 | Gen 1 | Gen 2 | Asignado |
| ------------ | -------- | ---------------: | ---------- -----: | ---------------: | ---------------: | -------: | ------: | ------: | -----------: |
| PrioritySet | 10 | 1.052 nosotros | 0.0106 nosotros | 0,0099 nosotros | 1.055 nosotros | - | - | - | - |
| PairingHeap | 10 | 1.055 nosotros | 0,0042 nosotros | 0,0035 nosotros | 1.055 nosotros | 0,0057 | - | - | 480 B |
| PrioritySet | 50 | 7.394 nosotros | 0,0527 nosotros | 0.0493 nosotros | 7.380 nosotros | - | - | - | - |
| PairingHeap | 50 | 8.587 nosotros | 0,1678 nosotros | 0,1570 nosotros | 8.634 nosotros | 0,0305 | - | - | 2400 B |
| PrioritySet | 150 | 27.522 nosotros | 0.0459 nosotros | 0.0359 nosotros | 27.523 nosotros | - | - | - | - |
| PairingHeap | 150 | 32.045 nosotros | 0.1076 nosotros | 0.1007 nosotros | 32.019 nosotros | 0,0610 | - | - | 7200 B |
| PrioritySet | 500 | 109.097 nosotros | 0.6548 nosotros | 0.6125 nosotros | 109.162 nosotros | - | - | - | - |
| PairingHeap | 500 | 131.647 nosotros | 0.5401 nosotros | 0.4510 nosotros | 131.588 nosotros | 0,2441 | - | - | 24000 B |
| PrioritySet | 1000 | 238.184 nosotros | 1.0282 nosotros | 0.9618 nosotros | 238.457 nosotros | - | - | - | - |
| PairingHeap | 1000 | 293.236 nosotros | 0.9396 nosotros | 0.8789 nosotros | 293.257 nosotros | 0,4883 | - | - | 48000 B |
| PrioritySet | 10000 | 3,035.982 nosotros | 12.2952 nosotros | 10.8994 nosotros | 3,036,985 nosotros | - | - | - | 1 B |
| PairingHeap | 10000 | 3,388.685 nosotros | 16.0675 nosotros | 38.1861 nosotros | 3,374.565 nosotros | - | - | - | 480002 B |
| PrioritySet | 1000000 | 841,406.888 nosotros | 16,788.4775 nosotros | 15,703.9522 nosotros | 840,888.389 us | - | - | - | 288 B |
| PairingHeap | 1000000 | 989,966.501 nosotros | 19,722.6687 us | 30,705.8191 us | 996,075.410 us | - | - | - | 48000448 B |
En una nota separada, ¿su discusión / retroalimentación sobre la falta de estabilidad es un problema (o no un problema) para los casos de uso de las personas?
¿Ha habido una discusión / retroalimentación sobre la falta de estabilidad siendo un problema (o no un problema) para el caso de uso de las personas?
Ninguna de las implementaciones garantiza la estabilidad, sin embargo, debería ser bastante sencillo para los usuarios obtener estabilidad aumentando el ordinal con el orden de inserción:
var pq = new PriorityQueue<string, (int priority, int insertionCount)>();
int insertionCount = 0;
foreach (string element in elements)
{
int priority = 42;
pq.Enqueue(element, (priority, insertionCount++));
}
Para resumir algunas de mis publicaciones anteriores, he estado tratando de identificar cómo se vería una cola de prioridad de .NET popular, así que revisé los siguientes datos:
Lo que arrojó las siguientes conclusiones:
De cara al futuro, propongo que tomemos las siguientes acciones para .NET 6:
Introduzca una clase System.Collections.Generic.PriorityQueue
que sea simple, satisfaga la mayoría de los requisitos de nuestros usuarios y sea lo más eficiente posible. Utilizará un montón cuaternario respaldado por una matriz y no admitirá actualizaciones prioritarias. Puede encontrar un prototipo de la implementación aquí . En breve, crearé un problema separado que detalla la propuesta de API.
Reconocemos la necesidad de montones que admitan actualizaciones prioritarias eficientes, por lo que seguiremos trabajando para introducir una clase especializada que aborde este requisito. Estamos evaluando un par de prototipos [ 1 , 2 ], cada uno con sus propios conjuntos de compensaciones. Mi recomendación sería introducir este tipo en una etapa posterior, ya que se necesita más trabajo para finalizar el diseño.
En este momento, me gustaría agradecer a los colaboradores de este hilo, en particular a @pgolebiowski y @TimLovellSmith. Sus comentarios han jugado un papel muy importante en la orientación de nuestro proceso de diseño. Espero seguir recibiendo sus comentarios mientras finalizamos el diseño de la cola de prioridad actualizable.
De cara al futuro, propongo que tomemos las siguientes acciones para .NET 6: [...]
Suena bien :)
Introduzca una clase
System.Collections.Generic.PriorityQueue
que sea simple, satisfaga la mayoría de los requisitos de nuestros usuarios y sea lo más eficiente posible. Utilizará un montón cuaternario respaldado por una matriz y no admitirá actualizaciones prioritarias.
Si tenemos la decisión de los propietarios de la base de código de que esta dirección está aprobada y deseada, ¿puedo continuar liderando el diseño de la API para ese bit y proporcionar la implementación final?
En este momento, me gustaría agradecer a los colaboradores de este hilo, en particular a @pgolebiowski y @TimLovellSmith. Sus comentarios han jugado un papel muy importante en la orientación de nuestro proceso de diseño. Espero seguir recibiendo sus comentarios mientras finalizamos el diseño de la cola de prioridad actualizable.
Fue todo un viaje: D
La API para System.Collections.Generic.PriorityQueue<TElement, TPriority>
acaba de ser aprobada. Creé un problema separado para continuar nuestra conversación sobre una posible implementación de montón que admita actualizaciones prioritarias.
Voy a cerrar este tema, ¡gracias a todos por sus contribuciones!
¡Quizás alguien pueda escribir sobre este viaje! 6 años completos para una API. :) ¿Alguna posibilidad de ganar una Guinness?
Comentario más útil
La estructura de datos del montón es imprescindible para hacer leetcode
más leetcode, más entrevistas en c # code, lo que significa más desarrolladores de c #.
más desarrolladores significa mejor ecosistema.
mejor ecosistema significa que si todavía podemos programar en c # mañana.
en resumen: esto no es solo una característica, sino también el futuro. es por eso que este tema se etiqueta como "futuro".