Runtime: Agregar PriorityQueue<t>a las colecciones</t>

Creado en 30 ene. 2015  ·  318Comentarios  ·  Fuente: dotnet/runtime

Consulte la propuesta MÁS RECIENTE en el repositorio de corefxlab.

Opciones de la segunda propuesta

Propuesta de https://github.com/dotnet/corefx/issues/574#issuecomment -307971397

Supuestos

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 )

API

`` 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 (IComparercomparador);

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 (FuncPrioritySelector);
Public PriorityQueue (FuncPrioritySelector, IComparercomparador);

public Func<TElement, TPriority> PrioritySelector { get; }

public void Enqueue(TElement element);
public void Update(TElement element);

}
`` ``

Preguntas abiertas:

  1. Nombre de la clase PriorityQueue vs. Heap
  2. ¿Introducir IHeap y sobrecarga del constructor? (¿Deberíamos esperar más tarde?)
  3. Introducir IPriorityQueue ? (¿Deberíamos esperar más tarde - IDictionary ejemplo)
  4. Use el selector (de prioridad almacenada dentro del valor) o no (diferencia de 5 API)
  5. Utilice tuplas (TElement element, TPriority priority) frente a KeyValuePair<TPriority, TElement>

    • ¿Deberían Peek y Dequeue tener out argumento

  6. ¿Es útil el lanzamiento de Peek y Dequeue ?

Propuesta original

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.PriorityQueueescribe.

Contribuiré con el PR, si se aprueba.

Justificación y uso

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.

API propuesta

`` C #
espacio de nombres System.Collections.Generic
{
///


/// Representa una colección de objetos que se eliminan en un orden ordenado.
///

///Especifica el tipo de elementos de la cola.
[DebuggerDisplay ("Count = {count}")]
[DebuggerTypeProxy (typeof (System_PriorityQueueDebugView <>))]
clase pública PriorityQueue: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
///
/// Inicializa una nueva instancia del clase
/// 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();
    }
}

}
''

Detalles

  • La estructura de datos de implementación será un montón binario. Los artículos con un valor de comparación mayor se devolverán primero. (orden descendiente)
  • Complejidades del tiempo:

| 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) | |

  • Sobrecargas adicionales del constructor que toman System.Comparisondelegado se omitieron intencionalmente a favor de una superficie API simplificada. Las personas que llaman pueden usar Comparer.Crear para convertir una función o expresión Lambda en un IComparerinterfaz si es necesario. Esto requiere que la persona que llama incurra en una asignación de montón única.
  • Aunque System.Collections.Generic aún no es parte de corefx, propongo que esta clase se agregue a corefxlab mientras tanto. Se puede mover al repositorio principal de corefx una vez que se agregan System.Collections.Generic y hay consenso en que su estado debe elevarse de experimental a API oficial.
  • No se incluyó una propiedad IsEmpty, ya que no hay una penalización de rendimiento adicional que llame a Count. La mayoría de las estructuras de datos de recopilación no incluyen IsEmpty.
  • Las propiedades IsSynchronized y SyncRoot de ICollection se implementaron explícitamente ya que están efectivamente obsoletas. Esto también sigue el patrón utilizado para las otras estructuras de datos System.Collection.Generic.
  • Dequeue y Peek lanzan una InvalidOperationException cuando la cola está vacía para coincidir con el comportamiento establecido de System.Collections.Queue.
  • IProducerConsumerCollectionno se implementó ya que su documentación indica que solo está destinado a colecciones seguras para subprocesos.

Preguntas abiertas

  • ¿Evitar una asignación de montón adicional durante las llamadas a GetEnumerator cuando se usa foreach es una razón suficientemente sólida para incluir la estructura del enumerador público anidado?
  • ¿CopyTo, ToArray y GetEnumerator deben devolver los resultados en orden de prioridad (ordenado) o en el orden interno utilizado por la estructura de datos? Mi suposición es que la orden interna debe devolverse, ya que no incurrirá en ninguna penalización de desempeño adicional. Sin embargo, este es un problema de usabilidad potencial si un desarrollador piensa en la clase como una "cola ordenada" en lugar de una cola de prioridad.
  • ¿Agregar un tipo llamado PriorityQueue a System.Collections.Generic causa un cambio potencialmente importante? El espacio de nombres se usa mucho y podría causar un problema de compatibilidad de fuente para proyectos que incluyen su propio tipo de cola de prioridad.
  • ¿Deben quitarse los elementos de la cola en orden ascendente o descendente, según la salida de IComparer?? (mi suposición es el orden ascendente, para que coincida con la convención de clasificación normal de IComparer).
  • ¿Debería ser "estable" la colección? En otras palabras, ¿deberían dos elementos con igual IComparisonlos resultados se quitarán de la cola exactamente en el mismo orden en el que se colocaron en la cola? (mi suposición es que esto no es necesario)

    Actualizaciones

  • Se corrigió la complejidad de 'Construir usando IEnumerable' a Θ (n). Gracias @svick.

  • Se agregó otra pregunta de opción con respecto a si la cola de prioridad debe ordenarse en orden ascendente o descendente en comparación con IComparer.
  • Se eliminó NotSupportedException de la propiedad SyncRoot explícita para que coincida con el comportamiento de otros tipos System.Collection.Generic en lugar de usar el patrón más nuevo.
  • Hizo que el método público GetEnumerator devolviera una estructura de enumerador anidada en lugar de IEnumerable, similar a los tipos System.Collections.Generic existentes. Esta es una optimización para evitar una asignación de montón (GC) cuando se usa un bucle foreach.
  • Se eliminó el atributo ComVisible.
  • Se cambió la complejidad de Clear a Θ (n). Gracias @mbeidler.
api-needs-work area-System.Collections wishlist

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".

Todos 318 comentarios

| Operación | Complejidad |
| --- | --- |
| Construir con IEnumerable | Θ (log n) |

Creo que debería ser Θ (n). Necesita al menos iterar la entrada.

+1

¿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>
  • Creo que el campo estático _emptyArray se puede eliminar y el uso se puede reemplazar con Array.Empty<T>() lugar.

También...

  • Otras colecciones que toman un comparador (por ejemplo, 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!

  • Implementé Enumerator.Reset explícitamente ya que es la funcionalidad principal y no obsoleta de un enumerador. Si está expuesto o no, parece inconsistente en todos los tipos de colección, y solo unos pocos usan la variante explícita.
  • Se eliminó el campo _disposed a favor de _index, ¡gracias! Lancé eso en el último minuto esa noche y se perdió lo obvio. Decidió mantener la ObjectDisposedException para que sea correcta con los tipos de colección más nuevos, aunque los tipos System.Collections.Generic antiguos no la usan.
  • Array.Emptyes una función de F #, ¡así que lamentablemente no puedo usarla aquí!
  • Se modificaron los parámetros del comparador para aceptar nulo, ¡buen hallazgo!
  • La optimización de ToArray es complicada. _Técnicamente_ hablando, las matrices son mutables en C #, incluso cuando tienen una longitud de cero. En realidad, tiene razón, la asignación no es necesaria y se puede optimizar. Me estoy inclinando hacia una implementación más cautelosa, en caso de que haya efectos secundarios en los que no estoy pensando. Semánticamente, la persona que llama seguirá esperando esa asignación, y es menor.

@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() cambió y conectó todo en la tubería de compilación normal. Un pequeño inconveniente temporal que tuve que introducir fue que el proyecto System.Collections dependiera del paquete nuget System.Collections, como Compareraún no es de código abierto.

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:

  • Cuando el orden 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_.
  • No 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.
  • ¿Debería el método Enqueue aceptar argumentos null ?
  • Creo que la complejidad de 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.aspx

No 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, Cola, Queue y ArrayList: ninguno de ellos reduce el tamaño de la matriz interna en un remove / dequeue.

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/ IComparablesolo garantiza que un valor de cero tenga el mismo orden de clasificación.

Sin embargo, parece que existe el mismo problema en las otras clases de colección. Si modifico el código para operar en SortedList, el caso de prueba aún falla en ContainsKey. La implementación de SortedList.ContainsKey llama a Array.BinarySearch, que se basa en IComparer para verificar la igualdad. Lo mismo es válido para SortedSet.

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.Contiene. Desde listano acepta IEqualityComparer, el comportamiento es funcionalmente equivalente.

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: PriorityQueuedebe implementar IReadOnlyCollectionpara que coincida con la cola(Colaahora implementa IReadOnlyCollectiona 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. ¡Gracias!

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> .

  1. ¿Cómo mantenemos la prioridad? En este momento, solo está implícito en T .
  2. ¿Queremos que sea posible pasar la prioridad cuando añades una entrada? (que parece bastante útil)

Notas:

  • Esperamos que la prioridad no cambie por sí sola; necesitaríamos una API para ello o esperaríamos Remove y Add en la cola.
  • Dado que no tenemos un código de cliente claro aquí (solo deseo general que queramos el tipo), es difícil decidir para qué optimizar: ¿rendimiento, usabilidad o algo más?

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:

  • Si estamos arreglando la implementación para un cierto tipo de montón, conviértalo en un montón de 4 arios.
  • No deberíamos arreglar la implementación del montón, ya que en algunos escenarios algunos tipos de montones son más eficaces que otros. Por lo tanto, dado que arreglamos la implementación a un cierto tipo de montón y el cliente necesita uno diferente para obtener más rendimiento, necesitaría implementar toda la cola de prioridad desde cero. Eso está mal. Deberíamos ser más flexibles aquí y dejar que reutilice parte del código CoreFX (al menos algunas interfaces).

El año pasado implementé algunos tipos de montón . En particular, hay una interfaz IHeap que podría usarse como cola de prioridad.

  • ¿Por qué no llamarlo un montón ...? ¿Conoce alguna forma sensata de implementar una cola de prioridad que no sea usando un montón?

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.

Colas de prioridad frente a montones

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:

  • La cola de prioridad y el montón se utilizan a menudo como sinónimos (consulte el documento de Python a continuación como el ejemplo más claro de esto)
  • siempre que tenga un soporte de "cola de prioridad" en alguna biblioteca, usa un montón (vea todos los ejemplos a continuación)

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:

  • Java: PriorityQueue<T> - cola de prioridad implementada con un montón.
  • Rust: 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.
  • Swift: 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.
  • C ++: priority_queue - una vez más, implementado canónicamente con un montón binario construido en la parte superior de una matriz.
  • Python: 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.
  • Vaya: 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.

  • Tenemos claro la estructura de datos y permitimos que la API se mejore en el futuro, si a alguien se le ocurre una forma nueva y revolucionaria de implementar una cola de prioridad que sea mejor en algunos aspectos (y la elección del montón frente al nuevo tipo dependería de el escenario), nuestra API aún puede estar intacta. Simplemente podríamos agregar el nuevo tipo revolucionario mejorando la biblioteca, y la clase de pila todavía existiría allí.
  • Imagine que un usuario se pregunta si la estructura de datos que elegimos es estable. Cuando la solución que seguimos es una cola de prioridad , esto no es obvio. El término es abstracto y cualquier cosa puede estar debajo. Por lo tanto, el usuario pierde algo de tiempo para buscar los documentos y descubrir que utiliza un montón internamente y, como tal, no es estable. Esto podría haberse evitado simplemente indicando explícitamente a través de la API que esto es un montón.

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.

API e implementación

Necesitamos ponernos de acuerdo sobre el asunto anterior, pero permítanme comenzar con otro tema: cómo implementar esto . Puedo ver dos soluciones aquí:

  • Solo proporcionamos una clase 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.
  • Otra posibilidad que me gusta mucho más es proporcionar una interfaz 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 .

Línea de fondo

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:

    • Montones d-ary implícitos (ArrayHeap), <- seguramente necesitamos esto
    • Emparejamiento de montones, <- si adoptamos el enfoque agradable y limpio 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 de IHeap + 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).

  • Devolver un número interno en el índice i realmente no tiene sentido desde el punto de vista del usuario, ya que es casi arbitrario.
  • Devolver un número en orden (por prioridad) es demasiado costoso; esto es lineal.
  • Devolver cualquiera de estos no tiene sentido en otras implementaciones. A menudo se trata simplemente de hacer estallar los elementos 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:

  • Posibilidad de eliminar un determinado elemento de la cola.
  • Capacidad para actualizar la prioridad de un elemento.
  • Capacidad para fusionar estas estructuras

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:

  • los identificadores de los elementos son nodos maravillosos; estos deben asignarse de todos modos, por lo que no hay cosas desordenadas
  • la fusión es constante (vs lineal en una solución basada en matrices)

En realidad, podríamos resolver esto de la siguiente manera:

  • Agregue IHeap interfaz que admita todos los métodos
  • Agregue ArrayHeap y PairingHeap con todos esos identificadores, actualizando, eliminando, fusionando
  • Agregue PriorityQueue que es solo un envoltorio alrededor de ArrayHeap , simplificando la API

PriorityQueue 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:

  • clave = prioridad
  • valor = lo que quieras almacenar en un elemento

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:

  • Tenemos List<T> , no ArrayList<T> .
  • Tenemos Dictionary<K, V> , no HashTable<K, V> .
  • Tenemos ImmutableList<T> , no ImmutableAVLTreeList<T> .
  • Tenemos 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> , no Heap<T> , ArrayHeap<T> o incluso QuaternaryHeap<T> para mantener la coherencia con el resto de .Net.

Estoy perdiendo la fe en la humanidad.

  • Llamar a una estructura de datos 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.
  • La discusión no es cuestión de elegir un nombre genial. 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.
  • Hashtable , ArrayList . Parece que existen. 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. 😜
  • No se trata de divertirse y decir cómo se implementa una cosa. Se trata de dar nombres significativos que muestren a los usuarios de inmediato con qué están lidiando.

¿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 para QuaternaryHeap . 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 KeyValuePairobjetos si optamos por la opción de tener una prioridad para cada artículo que almacenamos (que no creo que sea la mejor manera de hacerlo, no estoy del todo convencido de almacenar una prioridad por artículo). El mejor enfoque que me gustaría sería tener PriorityQueue<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:

  • Propósito general: cubre la mayoría de los casos de uso con un equilibrio entre el consumo de CPU y memoria.
  • Alinee, tanto como sea posible, con los patrones y convenciones existentes en System.Collections.Generic.
  • Permitir múltiples entradas del mismo elemento.
  • Utilice las interfaces y clases de comparación de BCL existentes (es decir, Comparer). *
  • Alto nivel de abstracción: la especificación está diseñada para proteger a los consumidores de la tecnología de implementación subyacente.

@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". El cambio probablemente se debió a una confusión con una matriz; su desarrollador promedio habría pensado" ¿ArrayList? Solo quiero una lista, no una matriz ".

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 integradoaprovecha la funcionalidad BCL existente, y también he considerado agregar sobrecargas a Compareren la especificación para que sea más fácil proporcionar expresiones lambda ad-hoc / funciones anónimas como comparaciones de prioridad. Lamentablemente, esa no es una convención común en la BCL.

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, la persona que llama nunca debe preocuparse por cómo se representa la prioridad. En la implementación de referencia anterior que hice, (¡si recuerdo bien!) La prioridad no está representada en absoluto. Todo es relativo basado en IComparero comparación.

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 proporcionadocomparador, 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:

  • Tenga un tipo que ya sea comparable de la forma que desea que sea
  • Envuelva un tipo con otra clase que contenga el valor de prioridad y compare la forma en que lo desea
  • Cree un IComparer personalizado para su tipo y páselo a través del constructor. Asume que el valor que desea representar como prioridad ya está expuesto públicamente por su tipo.

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 unEl estilo de API complicaría todos los tipos intrínsecamente comparables. Hace años escribí un componente de programador muy liviano que tenía su propia clase interna ScheduledTask. Cada ScheduledTask implementó IComparer. Tírelos en una cola de prioridad, listo.

SortedListutiliza un diseño de clave / valor y, como resultado, he evitado usarlo. Requiere que las claves sean únicas, y mi requisito habitual es "Tengo N valores que necesito mantener ordenados en una lista y asegurarme de que los valores ocasionales que agregue permanezcan ordenados". Una "clave" no es parte de esa ecuación, una lista no es un diccionario.

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:

  1. Hay muchas preguntas en los foros sobre cómo solucionar problemas debido a la falta de funciones.
  2. Hay implementaciones de terceros de una cola de prioridad / montón para reemplazar la implementación de origen, porque es bastante inútil.

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:

  • No espere cambios de la noche a la mañana. Esta es una discusión de diseño. Necesitamos encontrar consenso. No apresuramos las API. Todas las opiniones deben ser escuchadas y consideradas, pero no hay garantía de que la opinión de todos sea implementada / aceptada. Si tiene alguna 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. A veces, esté de acuerdo en el hecho de que hay desacuerdo en algunos puntos. Tenga en cuenta que SW, incl. El diseño de API no es blanco o negro / correcto o incorrecto.
  • Mantengamos la discusión civilizada. No usemos palabras y declaraciones fuertes. No estemos de acuerdo con la gracia y mantengamos la discusión técnica. Todos pueden consultar también el código de conducta de los colaboradores . Reconocemos y fomentamos la pasión por .NET, pero asegurémonos de no ofendernos entre nosotros.
  • Si tiene alguna inquietud / pregunta sobre la velocidad de la discusión del diseño, reacciones, etc., no dude en comunicarse conmigo directamente (mi correo electrónico está en mi perfil de GH). Puedo ayudar a aclarar expectativas, suposiciones e inquietudes públicamente o fuera de línea si es necesario.

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, Diccionario, Pila, Cola, HashSet, SortedDictionaryy SortedSet. Estos reciben el nombre de la funcionalidad y la semántica, no del algoritmo. HashSetes el único valor atípico, pero incluso esto se puede racionalizar en relación con la semántica de igualdad de conjuntos (en comparación con SortedSet). Después de todo, ahora tenemos ImmutableHashSetque se basa en un árbol debajo del capó.

Se sentiría extraño que una colección se opusiera a la tendencia aquí.

Creo que PriorityQueue con constructor adicional: PriorityQueue(IHeap) puede ser una solución. Los constructores sin el parámetro IHeap pueden usar el montón predeterminado.
En ese caso PrioriryQueuerepresentará el tipo de datos abstracto (como la mayoría de las colecciones de C #) e implementará IPriorityQueueinterfaz pero puede usar diferentes implementaciones de montón como sugirió @pgolebiowski :

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.

Nuestro objetivo

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:

  • Recupera el elemento con mayor prioridad (y poder eliminarlo).
  • Agrega un nuevo elemento a la colección.
  • Elimina un elemento de la colección.
  • Modifica un elemento de la colección.
  • Fusiona dos colecciones.

Otras bibliotecas estándar

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?

  • Si queremos que nuestros clientes estén contentos y evitar que implementen esta funcionalidad ellos mismos mientras ignoran nuestra maravillosa estructura de datos, debemos proporcionar un soporte para modificar elementos ya insertados en la colección.
  • No deberíamos asumir que debido a que

Enfoque propuesto

Creo firmemente que deberíamos proporcionar:

  • IHeap<T> interfaz
  • Heap<T> clase

La 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.

Justificación

  • Los desarrolladores pueden escribir su lógica en una interfaz. Supongo que todo el mundo sabe lo importante que es y no entraré en detalles. Leer: principio de inversión de dependencia , inyección de dependencia , diseño por contrato , inversión de control .
  • Los desarrolladores pueden ampliar esta funcionalidad para satisfacer sus necesidades personalizadas proporcionando otras implementaciones de montón. Dichas implementaciones podrían incluirse en bibliotecas de terceros como PowerCollections . Simplemente haciendo referencia a dicha biblioteca, podría simplemente inyectar su montón personalizado en cualquier lógica que tome 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.
  • Si un desarrollador solo necesita una herramienta que haga el trabajo sin la necesidad de que piense qué tipo es mejor, simplemente puede usar la implementación de uso general Heap . Esto se está optimizando hacia el 98% de los casos de uso.
  • Agregamos al gran valor del ecosistema .NET en lo que respecta a ofrecer una opción única con características de rendimiento muy decentes, útiles para la mayoría de los casos de uso, al mismo tiempo que permiten extensiones de alto rendimiento para aquellos que lo necesitan y están dispuestos a excavar. profundizar y aprender más / tomar decisiones informadas y compensaciones.
  • El enfoque propuesto refleja las convenciones actuales:

    • ISet y HashSet

    • IList y List

    • IDictionary y Dictionary

  • Algunas personas han declarado que deberíamos nombrar las clases en función de lo que hacen sus instancias, no de cómo lo hacen. Esto no es enteramente verdad. Es un atajo común decir que debemos nombrar las clases según su comportamiento. De hecho, se aplica en numerosos casos. Sin embargo, hay casos en los que este no es el enfoque adecuado. Los ejemplos más notables son bloques de construcción básicos, como tipos primitivos, tipos de enumeración o estructuras de datos. El principio es simplemente elegir nombres que sean significativos (es decir, que no sean ambiguos para el usuario). Tenga en cuenta el hecho de que la funcionalidad que estamos discutiendo siempre se proporciona como un montón, ya sea Python, Java, C ++, Go, Swift o Rust. Heap es una de las estructuras de datos más elementales. 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:

  • montones d-arios,
  • 2-3 montones,
  • montones de izquierdas,
  • montones suaves,
  • montones débiles,
  • B-montones,
  • montones de radix,
  • sesgar montones,
  • emparejamiento de montones,
  • Montones de Fibonacci,
  • montones binomiales,
  • montones de emparejamiento de rangos,
  • montones de terremotos,
  • montones de violaciones.

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:

  • implementación : montón cuaternario implícito basado en una matriz
  • abstracción : montón cuaternario
  • abstracción : familia de montones
  • abstracción : familia de colas de prioridad

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):

  • Haga 2 propuestas alternativas: una simple como describí anteriormente y otra con los Heaps como propuso, llevémosla a la revisión de la 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.

... 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
  • Agregar una interfaz
  • Apoyar la actualización / eliminación de elementos de la colección

¿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:

  • La interfaz de IHeap es un escenario experto muy avanzado
  • 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

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

  1. Un grupo que está usando nuestra funcionalidad. No tiene una interfaz y no se puede ampliar, por lo que no está conectado a lo que se proporciona en bibliotecas de terceros.
  2. Segundo grupo que ignora por completo nuestra funcionalidad y utiliza soluciones puramente de terceros.

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:

  • Primero, mire los números de arriba.
  • En segundo lugar, piense en todos los demás lenguajes en los que un montón es parte de la biblioteca estándar.

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:

  1. 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.

  2. 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".

  • No tener experiencia: dirigido a personas sin experiencia en informática y que no comprenden el concepto de montones / colas de prioridad. Si tal descripción se aplica a alguien, bueno, se aplica, no es mi culpa.
  • No importa: dirigido a aquellos con una tendencia a ignorar algunos de los puntos técnicos, lo que obliga a repetir los argumentos, lo que hace que la discusión sea caótica y más difícil de seguir por otras personas (lo que a su vez conduce a menos aportaciones).

Comentarios como el suyo son muy injustos y tampoco ayudan a avanzar en el diseño.

  • 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.
  • Además, como uno de los impulsores clave en la discusión, creo firmemente que he hecho mucho para ayudar a avanzar en el diseño. Por favor, no me demonices, como intentas hacer aquí y en las redes sociales.

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:

  • Realizaré votaciones sobre aspectos fundamentales, uno tras otro. Tendremos un aporte claro de la comunidad. Idealmente, los revisores de API también vendrán aquí y comentarán en breve.
  • Las "publicaciones de votación" contendrán suficiente información para poder ignorar todo el texto de arriba.
  • Una vez finalizada esta sesión de votación, sabremos qué esperar de los revisores de API y podremos proceder con un enfoque determinado. Cuando nos pongamos de acuerdo en los aspectos fundamentales, se cerrará este tema y se abrirá otro (que hará referencia a éste). En el nuevo número, resumiré nuestras conclusiones y proporcionaré una propuesta de API que refleje estas decisiones. Y lo tomaremos de ahí.

¿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 ;)

Proporcionar una interfaz

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? _

Para

@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.

Contra

@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 agregar IHeap 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.

Impacto potencial de la decisión

  • Incluir la interfaz significa que no podemos cambiarla en el futuro.
  • No incluir la interfaz significa que las personas escriben código que utiliza nuestra solución de biblioteca estándar o está escrito en una solución proporcionada por una biblioteca de terceros (no hay una interfaz común que permita la compatibilidad cruzada).

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:

Ejemplo de propuesta / Semilla

`` c #
espacio de nombres System.Collections.Generic
{
clase pública PriorityQueue
: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
public PriorityQueue ();
Public PriorityQueue (capacidad int);
Public PriorityQueue (IComparercomparador);
Public PriorityQueue (IEnumerablecolección);
Public PriorityQueue (IEnumerablecolección, IComparercomparador);
public PriorityQueue (int capacidad, IComparercomparador);

    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:

  • Falta un ejemplo de uso (no me queda claro cómo expreso la prioridad de los elementos). ¿Quizás deberíamos rediseñarlo para tener 'int' como entrada de valor de prioridad? ¿Quizás alguna abstracción sobre ello? (Creo que se discutió anteriormente, pero el hilo es demasiado largo para leer, por eso necesitamos propuestas concretas y no más discusión)
  • Falta el escenario UpdatePriority
  • ¿Algo más discutido anteriormente que falte aquí?

@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

Propuesta

Razón fundamental

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:

  1. Agrega un nuevo elemento a la colección.
  2. Recupera el elemento con mayor prioridad (y poder eliminarlo).
  3. Elimina un elemento de la colección.
  4. Modifica un elemento de la colección.
  5. Fusiona dos colecciones.

Uso

Glosario

  • Valor : datos del usuario.
  • Clave : un objeto que se utiliza para realizar pedidos.

Varios tipos de datos de usuario

Primero nos enfocaremos en construir la cola de prioridad (solo agregando elementos). La forma de hacerlo depende del tipo de datos del usuario.

escenario 1

  • 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");

Escenario 2

  • 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");

Escenario 3

  • 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 */ ));
Notas
  • Usamos un método Enqueue aquí. Esta vez solo se necesita un argumento ( TValue ).
  • Si el selector de clave está definido, el método Enqueue(TKey, TValue) debe arrojar InvalidOperationException .
  • Si el selector de clave no está definido, el método Enqueue(TValue) debe arrojar InvalidOperationException .

Escenario 4

  • 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 */ ));
Notas
  • Se supone que el comparador de TKey es Comparer<TKey>.Default , como en el escenario 1 .

Escenario 5

  • 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);

Escenario 6

  • Los datos del usuario son un solo objeto, que es comparable.
  • No hay una clave física o un usuario no quiere usarla.
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 */ ));
Notas

Al principio, hay una ambigüedad.

  • Es posible que 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 .
  • Sin embargo, también puede ser un solo objeto (como en este caso).

Así es como PriorityQueue trata la ambigüedad:

  • Si el selector de llave está definido, entonces no hay ambigüedad. Solo se permiten Enqueue(TValue) . Por lo tanto, una solución alternativa al escenario 6 es simplemente definir un selector y pasarlo al constructor.
  • Si el selector de teclas no está definido, la ambigüedad se resuelve con el primer uso de un método Enqueue :

    • Si se llama 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 .

    • Si se llama 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 .

Otra funcionalidad

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.

Elemento de máxima prioridad

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 .

Modificar un elemento

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í:

  • Hágalo a la manera de Java y no proporcione esta funcionalidad. Estoy firmemente en contra de eso. Obliga al usuario a eliminar un elemento de la colección (que por sí solo no funciona bien, pero llegaré a eso más adelante) y luego agregarlo una vez más. Es muy feo, no funciona en todos los casos y es ineficiente.
  • Introducir un nuevo concepto de tiradores .

Manejas

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.

Escenarios

Escenario 7
  • 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)).

Escenario 8
  • 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).

Escenario 9
  • 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).

Escenario 10
  • Los datos del usuario son un solo objeto.

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).

Eliminar un elemento

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.

Fusionando dos colecciones

var queue1 = new PriorityQueue<int, string>();
var queue2 = new PriorityQueue<int, string>();

/* add some elements to both */

queue1.Merge(queue2);

Notas

  • Una vez realizada la fusión, las colas comparten la misma representación interna. El usuario puede utilizar cualquiera de los dos.
  • Los tipos deben coincidir (comprobados estáticamente).
  • Los comparadores deben ser iguales, de lo contrario InvalidOperationException .
  • Los selectores deben ser iguales, de lo contrario InvalidOperationException .

API propuesta

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; }
}

Preguntas abiertas

Predica en su lugar

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 .

UpdateKey

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.

Preguntas más frecuentes

¿No son los mangos ineficientes?

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.

¿Cómo lo vas a implementar entonces?

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.

¿Qué rendimiento tendría?

  • Para una entrada aleatoria general, un montón cuaternario sería un poco más rápido.
  • Sin embargo, tendría que basarse en una matriz para ser más rápido. Entonces, los identificadores no podrían basarse simplemente en nodos: se necesitarían asignaciones adicionales. Entonces, no pudimos actualizar y eliminar elementos de manera razonable.
  • De la forma en que está diseñado en este momento, tenemos una API fácil de usar y una implementación bastante eficiente.
  • Un beneficio adicional de tener un montón de emparejamiento debajo es la capacidad de fusionar dos colas de prioridad en O (1), en lugar de O (n) como en los montones binarios / cuaternarios.
  • Emparejar el montón sigue siendo increíblemente rápido. Ver referencias. A veces es más rápido que los montones cuaternarios (depende de los datos de entrada y las operaciones realizadas, no solo de la fusión).

Referencias

  • Michael L. Fredman, Robert Sedgewick, Daniel D. Sleator y Robert E. Tarjan (1986), The pairing heap: A new form of self-ajuste heap , Algorithmica 1: 111-129 .
  • Daniel H. Larkin, Siddhartha Sen y Robert E. Tarjan (2014), Un estudio empírico de vuelta a los fundamentos de las colas de prioridad , arXiv: 1403.0252v1 [cs.DS].

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?

  • Bueno, el 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.
  • Pero lo principal es que 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.

  • Es cierto, lo tendré en cuenta y veré qué pasa. @karelz , junto con la propuesta, ¿podrías pasar también algunos de los post anteriores (están justo antes de la propuesta), donde votamos por las interfaces? Podríamos volver a esto más adelante, después de la primera iteración de la revisión de la API.
  • Aún así, no importa qué solución usemos, la funcionalidad será muy similar y sería útil revisarla. Porque si se rechaza la idea de los identificadores y realmente no podemos actualizar / eliminar elementos correctamente (o no podemos tener TKey y TValue separados), esta clase está realmente cerca de ser inútil, entonces - - como está ahora en Java.
  • En particular, si no tenemos una interfaz, mi biblioteca AlgoKit no podrá tener un núcleo común para montones con CoreFX, lo que sería realmente triste para mí.
  • Y sí, es realmente sorprendente para mí que la adición de una interfaz sea percibida como una desventaja por Microsoft.

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)):

  1. ¿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?

  2. ¿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.

    • ¿Quizás solo agregar algo de inicialización desde IEnumerable<KeyValuePair<TKey, TValue>> sería suficiente? + confíe en Linq

  3. ¿Necesitamos una sobrecarga de comparer ? ¿Podemos siempre volver a los valores predeterminados? (descargo de responsabilidad: me faltan conocimientos / experiencia en este caso, así que solo pregunto)
  4. ¿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?

Puntos de decisión separados / paralelos:

  1. Nombre de la clase PriorityQueue vs. Heap
  2. ¿Introducir 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!

Permitir duplicados

¿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?

Unir

¿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).

Comparador

¿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:

  • A un usuario le gustaría tener sus elementos ordenados en orden ascendente o descendente. La prioridad más alta no nos dice cómo se debe realizar el pedido.
  • Si omitimos un comparador, obligamos al usuario a que siempre nos entregue una clase que implemente IComparable .

Además, es consistente con la API existente. Eche un vistazo a SortedDictionary .

Selector

¿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 ...

Pros

Esto es lo que hace que esta cola de prioridad sea flexible. Permite a los usuarios tener:

  • elementos y sus prioridades como elementos separados
  • elementos (clases complejas) que tienen prioridades en algún lugar dentro de ellos
  • una lógica externa que recupera la prioridad para un elemento dado
  • elementos que implementan 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.

Contras

  • Hay más que aprender.
  • Más API. Dos constructores adicionales, un método adicional Enqueue y Update .
  • Si decidimos tener el elemento y la prioridad separados o unidos, obligamos a algunos de los usuarios (que tienen sus datos en un formato diferente) a adaptar su código para usar esta estructura de datos.

Puntos de decisión separados / paralelos

Nombre de la clase PriorityQueue vs. Heap

Introduzca IHeap y la sobrecarga del constructor.

  • Se siente que PriorityQueue debería ser parte de CoreFX, en lugar de Heap .
  • Con respecto a la interfaz 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!

Elementos comparables

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.

Pros

  • No es necesario IComparer .
  • No es necesario el selector.
  • Un método Enqueue y Update .
  • Un tipo genérico en lugar de dos.

Contras

  • No podemos tener elemento y prioridad como objetos separados. Los usuarios deben proporcionar una nueva clase contenedora si la tienen en ese formato.
  • Los usuarios siempre deben implementar IComparable antes de poder usar esta cola de prioridad.
  • Si tenemos elementos y prioridades separados, podríamos implementarlo más fácilmente, con mejor rendimiento y uso de memoria - diccionario interno <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 (IComparercomparador);

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 (FuncPrioritySelector);
Public PriorityQueue (FuncPrioritySelector, IComparercomparador);

public Func<TElement, TPriority> PrioritySelector { get; }

public void Enqueue(TElement element); // O(log n)
public void Update(TElement element); // O(log n)

}
`` ``

Preguntas abiertas:

  1. Nombre de la clase PriorityQueue vs. Heap
  2. ¿Introducir IHeap y sobrecarga del constructor? (¿Deberíamos esperar más tarde?)
  3. Introducir IPriorityQueue ? (¿Deberíamos esperar más tarde - IDictionary ejemplo)
  4. Use el selector (de prioridad almacenada dentro del valor) o no (diferencia de 5 API)
  5. Utilice tuplas (TElement element, TPriority priority) frente a KeyValuePair<TPriority, TElement>

    • ¿Deberían 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í:

  1. Limitarnos solo a elementos únicos (no permitir duplicados).

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.

  • Veo una similitud con el indexador dentro de un Dictionary . Allí, cuando haces dictionary["something"] = 5 , se actualiza si "something" era una clave allí anteriormente. Si no estaba allí, simplemente se agrega.
  • Sin embargo, el método Enqueue es análogo para mí al método Add en el diccionario. Lo que significa que debería lanzar una excepción.
  • Teniendo en cuenta los puntos anteriores, podríamos considerar agregar un indexador a la cola de prioridad para respaldar el comportamiento que tiene en mente.
  • Pero, a su vez, un indexador podría no ser algo que funcione con el concepto de colas.
  • Lo que nos lleva a la conclusión de que el método 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.
  • Lo que nos lleva a una nueva solución: agregue el método 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"y acepta un IComparerparámetro en su constructor. Los métodos y propiedades incluyen Count, Clear, Offer (poner en cola / empujar), Poll (sacar / pop), Peek, Remove (elimina el primer elemento coincidente encontrado en la cola), Add (sinónimo de Offer). Curiosamente, también brindan soporte de enumeración para la cola.

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 IComparabley usa esa interfaz para hacer la comparación. El diseño de API original publicado aquí es similar, excepto que también funciona con tipos de valor.

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.

  • Cual es la conclusion? ¿Propuesta?
  • Está muy lejos de la verdad. Incluso el algoritmo de Dijkstra que mencionaste hace uso de las prioridades de actualización de los elementos. Y lo que se necesita para actualizar elementos específicos también se necesita para eliminar elementos específicos.

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:

  • No es necesario que lo inicialicemos antes de ingresar a la función.
  • No se usa para el método (simplemente se apaga).

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 ).

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:

  • [6] ¿Son útiles los lanzamientos de Peek y Dequeue ?
  • [7] 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:

  • Necesitaremos tener una reunión de diseño adecuada (2h) para discutir todos los pros / contras, los 30min que invertimos hoy no fueron suficientes para llegar a un consenso / cerrar las preguntas abiertas.

    • Es probable que invitemos a los miembros de la comunidad más @pgolebiowski , ¿alguien más?

Aquí están las notas crudas clave:

  • Experimente : deberíamos hacer un experimento (en CoreFxLabs), lanzarlo como paquete NuGet previo al lanzamiento y pedir comentarios a los consumidores (a través de la publicación del blog). No creemos que podamos clavar la API sin un ciclo de retroalimentación.

    • Hicimos algo similar por ImmutableCollections en el pasado (el ciclo de lanzamiento de vista previa rápida fue clave y útil para dar forma a las API).

    • Podemos agrupar este experimento junto con 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.

    • El paquete experimental NuGet será destruido después de que finalice el experimento y trasladaremos el código fuente a CoreFX o CoreFXExtensions (que se decidirá más adelante).

  • Idea: Devuelva solo TElement de Peek & Dequeue , no devuelva TPriority (los usuarios pueden usar los métodos Try* para eso).
  • 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)
  • Queremos habilitar duplicados (similar a otras colecciones):

    • Deshazte de 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 ).

  • Idea: ¿Podemos abstraer la interfaz IQueue para abstraer Queue y PriorityQueue ( Peek y Dequeue devolviendo solo TElement ayuda aquí)

    • Nota: Puede resultar imposible, pero deberíamos explorarlo antes de decidirnos por la API.

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:

  1. Si 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.
  2. No me gusta que la versión del selector se distinga por usar un constructor diferente y habilite un conjunto diferente de métodos. ¿Quizás debería haber un PriorityQueue<T> separado? ¿O un conjunto de métodos estáticos y de extensión que funcionan con PriorityQueue<T, T> ?
  3. ¿Está definido en qué orden se devuelven los elementos al enumerar? Supongo que no está definido para que sea eficaz.
  4. ¿Debería haber alguna forma de enumerar solo los elementos en la cola de prioridad, ignorando las prioridades? ¿O es aceptable tener que usar algo como priorityQueue.Select(t => t.element) ?
  5. Si la cola de prioridad usa internamente un Dictionary en el tipo de elemento, ¿debería haber una opción para pasar un IEqualityComparer<TElement> ?
  6. El seguimiento de los elementos usando 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 de Update - el usuario puede Remove y luego Enqueue 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) que evalúa varios campos para determinar la prioridad. El patrón de comparación "implícito" de Lucene no se correlacionará bien con el parámetro TPriority explícito de la propuesta de API actual. Se necesitaría algún tipo de mapeo, combinando los múltiples campos en uno o en una estructura de datos 'comparable' que se puede pasar como TPriority.

Propuesta:
1) PriorityQueueclase basada en mi propuesta original anterior (enumerada bajo el encabezado Propuesta original). Potencialmente, agregue las funciones de conveniencia Actualizar (T), Eliminar (T) y Contiene (T). Se adapta a la mayoría de los ejemplos de uso existentes de la comunidad de código abierto.
2) PriorityQueuevariante de dotnet / corefx # 1. Dequeue () y Peek () devuelven TElement en lugar de tuple. No Try * functiuons, Remove () devuelve bool en lugar de lanzar para encajar en Listpatrón. Actúa como un "tipo de conveniencia", por lo que los desarrolladores que tienen un valor de prioridad explícito no necesitan crear su propio tipo comparable.

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

  • Queremos habilitar duplicados (similar a otras colecciones):

    • Deshazte de 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

  • si te importa el resultado; tu lo revisas
  • si no te importa el resultado, no lo verificas

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

  • si te importa el resultado; tiene que verificar previamente o usar un try / catch para detectar
  • si no le importa el resultado; o tiene que vaciar la captura del método o obtener excepciones inesperadas

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.

Estabilidad

  • La estabilidad no es gratuita. Esto viene con una sobrecarga en el rendimiento y en la memoria. Para un uso normal, la estabilidad en las colas de prioridad no es importante.
  • Exponemos un 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.

IQueue

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:

  1. Implemente IQueue<TElement> .
  2. Implemente IQueue<(TElement, TPriority)> .

Escribiré las implicaciones de ambos caminos usando simplemente números (1) y (2).

Enqueue

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.

  1. En el primer caso, exponemos 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 ...
  2. En el segundo caso, exponemos Enqueue((TElement, TPriority) element) . Básicamente, agregamos dos argumentos aceptando una tupla creada a partir de ellos. Probablemente no sea la API ideal.

Peek & Dequeue

Quería que esos métodos devolvieran solo TElement .

  1. Funciona con lo que quiere lograr.
  2. No funciona con lo que desea lograr: devuelve (TElement, TPriority) .

Enumerar

  1. El cliente no puede escribir ninguna consulta LINQ que haga uso de las prioridades de los elementos. Esto está mal.
  2. Funciona.

Podríamos mitigar algunos de los problemas proporcionando PriorityQueue<T> , donde no hay una prioridad física separada.

  • El usuario se ve esencialmente obligado a escribir una clase contenedora para que su código pueda usar nuestra clase. Esto significa que en muchos casos también desearían implementar IComparable . Lo que agrega mucho código bastante repetitivo (y un nuevo archivo en su código fuente, muy probablemente).
  • Obviamente, podemos volver a discutir esto, si lo desea. También proporciono un enfoque alternativo a continuación.

Dos colas de prioridad

Si entregamos dos colas de prioridad, la solución general sería más poderosa y flexible. Hay algunas notas para tomar:

  • Exponemos dos clases para proporcionar la misma funcionalidad, pero solo para varios tipos de formato de entrada. No se siente mejor.
  • 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.

Actualización y eliminación

Dada la suposición de que queremos permitir duplicados y no queremos utilizar el concepto de un identificador:

  • Es imposible proporcionar la funcionalidad de actualizar las prioridades de los elementos de forma totalmente correcta (actualizar solo el nodo específico). De manera análoga, se aplica a la eliminación de elementos.
  • Además, ambas operaciones deben realizarse en O (n), lo cual es bastante triste, porque es posible hacer ambas en O (log n).

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.

Mango alternativo

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 ...

Enfoque alternativo

Alternativamente, podemos eliminar esta funcionalidad de la cola de prioridad por completo y agregarla al montón en su lugar. Esto tiene numerosos beneficios:

  • Las operaciones más potentes y eficientes están disponibles mediante el uso de Heap .
  • La API simple y el uso directo están disponibles mediante el uso de PriorityQueue - para las personas que desean que su código simplemente funcione .
  • No nos encontramos con los problemas de Java.
  • Podríamos hacer que el PriorityQueue estable ahora, ya no es una compensación.
  • La solución está en armonía con la sensación de que las personas con una sólida formación en informática estarían al tanto de la existencia de 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).
  • Tanto la cola de prioridad como el montón podrían permitir duplicados fácilmente sin comprometer sus funcionalidades (porque sus propósitos son diferentes).
  • Sería más fácil hacer una interfaz 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.
  • Ya no necesitamos proporcionar la interfaz 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.
  • Ya no necesitamos considerar el constructor IHeap para el PriorityQueue .
  • La cola de prioridad sería una clase útil para usar internamente en CoreFX. Sin embargo, si agregamos características como estabilidad y descartamos algunas otras funcionalidades, es probable que terminemos con la necesidad de algo más poderoso que esa solución. Afortunadamente, ¡estaría el 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.

Entregando IQueue entonces

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> .

Diverso

@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á sugiriendo T : 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.

  • Me refiero a entregar PriorityQueue<T> , sin restricciones en los T .
  • Todavía haría uso de un IComparer<T> .
  • Si sucede que 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:

    1. Los datos del usuario están separados de la prioridad (dos instancias físicas).
    2. Los datos del usuario contienen la prioridad.
    3. Los datos del usuario son la prioridad.
    4. 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).

    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?

  • Sí, precisamente. Dada la suposición de que no queremos exponer tal tipo, es la única solución para seguir usando el concepto de asa (y como tal tener más potencia y velocidad). Sin embargo, estoy de acuerdo en que no es ideal, fue más bien una aclaración de lo que tendría que suceder si nos atenemos al enfoque actual y queremos tener más potencia y eficiencia (lo que estoy en contra).
  • Dado que optaríamos por el enfoque alternativo (cola de prioridad separada y montones), esto es más fácil. Podríamos dejar que 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.
  • Pero una vez más, es muy importante diseñar la cola de prioridad y la funcionalidad del montón juntos si quisiéramos tener un enfoque de este tipo. Porque una solución proporciona lo que la otra no proporciona.

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 :

@karelz @safern @ianhays @terrajobst @bendono @svick @ alexey-dvortsov @SamuelEnglard @ xied75 y otros, ¿crees que este enfoque tendría sentido (como se describe en esta y esta publicación)? ¿Satisfaría todas sus expectativas y necesidades?

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);
}

Notas

  • En System.Collections.Generic .
  • Idea: también podríamos agregar métodos para eliminar elementos aquí ( 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();
}

Notas

  • En System.Collections.Generic .
  • Si el IComparer<T> no se entrega, se invoca Comparer<T>.Default .
  • Es estable.
  • Permite duplicados.
  • Remove y TryRemove eliminan solo la primera aparición (si la encuentran).
  • La enumeración está ordenada.

Muchísimo

No escribo todo aquí ahora, pero:

  • En System.Collections.Specialized .
  • No sería estable (y como tal más rápido y eficiente en términos de memoria).
  • Mango de soporte, correcta actualización y extracción.

    • hecho rápidamente en O (log n) en lugar de O (n)

    • hecho correctamente

  • La enumeración no está ordenada (más rápido).
  • Permite duplicados.

De acuerdo con el IQueuepropuesta. Hoy iba a lanzar lo mismo, se siente como el nivel correcto de abstracción en el nivel de la interfaz. "Una interfaz para una estructura de datos a la que se le pueden agregar elementos y ordenar que se eliminen elementos".

  • La especificación de IQueuese ve bien.

  • Considere agregar "int Count {get;}" a IQueuepara que quede claro que se desea Count independientemente de si heredamos o no de IReadOnlyCollection.

  • En la valla con respecto a TryPeek, TryDequeue en IQueueconsiderando que no están en la cola, pero esos ayudantes probablemente también deberían agregarse a Queue y Stack.

  • 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, y parece un poco extraño agregarlo a la colay nada más. Recomendar que lo eliminemos de la interfaz, y quizás también de la clase.

  • 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

  1. Los datos del usuario están separados de la prioridad (dos instancias físicas).
  2. Los datos del usuario contienen la prioridad.
  3. Los datos del usuario son la prioridad.
  4. 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 ).

  1. 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 y Dequeue usaron out 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?

Selector y 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 agregar Remove y TryRemove , pero Queue<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 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.

👍 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 Queueno 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):

  • PriorityQueuesin selector y sin IQueue (lo cual sería engorroso)
  • PriorityQueuecon 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(); } }

  • PriorityQueuetambién debería tener una estructura de enumerador anidada.

  • 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 IEnumerableparámetros del constructor?

@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í.

  • @ebickle @ jnm2 - agregado
  • @karelz @SamuelEnglard - referenciado

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:

  1. Implementaciones de cola de prioridad en Github y otros sitios de alojamiento
  2. Personas que preguntan por qué en el mundo no hay una cola de prioridad oficial en Colecciones.
  3. Tutoriales sobre cómo crear su propia implementación de cola de prioridad

@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?

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.

Cinco años después, todavía no existe PriorityQueue.

No debe ser una prioridad lo suficientemente alta ...

@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!)

https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

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 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 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 ICollectionLas apis relacionadas con KeyValuePair aún permanecen, porque no veo nada obviamente mejor con lo que reemplazarlas.
Mismo enlace de relaciones públicas. ¿Hay alguna otra forma en que deba enviarlo para revisión?

https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

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¿ser? Supongo que sería un número entero o de coma flotante, pero para mí declarar el número que es "interno" a la cola me parece extraño

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?

[1] https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

[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
  • Mis propias implementaciones a menudo incluyen un 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.
  • Hay dos copias de Enqueue (no me refiero a Add ): ¿se supone que una sea bool TryEnqueue ?
  • "// enumera la colección en orden arbitrario, pero con el menor elemento primero": No creo que el último bit sea útil; Preferiría que el enumerador no tuviera que realizar ninguna comparación, pero supongo que será 'gratis', por lo que no me molestará.
  • El nombre de EqualityComparer es un poco inesperado: hubiera esperado que KeyComparer fuera con PriorityComparer .
  • No entiendo las notas sobre la complejidad de 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 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.

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.

  1. Nombre de clase PriorityQueue vs. Heap <- Creo que ya se discutió y el consenso es que PriorityQueue es mejor.

  2. ¿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.

  3. ¿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.

  4. 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).

  5. Utilice tuplas (elemento TElement, prioridad TPriority) frente a KeyValuePair<- Personalmente, creo que KeyValuePair 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.

  6. ¿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?

  7. ¿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:

  1. PriorityQueuesolo debe funcionar con elementos únicos, que son su propio identificador. Debería admitir IEqualityComparer personalizado, en caso de que quiera comparar objetos no extensibles (como cadenas) de formas específicas para realizar actualizaciones de prioridad.

  2. PriorityQueuedebe admitir una variedad de operaciones como 'eliminar', 'disminuir la prioridad si es más pequeño' y especialmente 'poner en cola el elemento o disminuir la prioridad si es una operación más pequeña', todo en O (log n), para implementar de manera eficiente y sencilla escenarios de búsqueda de gráficos.

  3. Sería conveniente para los programadores perezosos si PriorityQueueproporciona el operador de índice [] para obtener / establecer la prioridad de los elementos existentes. Debería ser O (1) para obtener, O (log n) para conjunto.

  4. 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'.

  5. La enumeración no debe proporcionar ninguna garantía de pedido.

Problema abierto - maneja:
PriorityQueueLos elementos y prioridades parecen estar destinados a permitir trabajar con valores duplicados. Esos valores deben considerarse como 'inmutables' o debe haber un método para llamar para notificar a la colección cuando las prioridades de los elementos cambiaron, posiblemente con identificadores, para que pueda ser específico sobre la prioridad de qué duplicados cambiaron (qué escenario debe hacer esto ??) ... Tengo dudas sobre todo lo útil que es esto, y si es una buena idea, pero si tenemos prioridades de actualización en una colección de este tipo, sería bueno si los costos incurridos por ese método se pueden incurrir de manera perezosa, por lo que que cuando solo trabajas con elementos inmutables y no quieres usarlos no hay ningún costo adicional ... ¿cómo resolverlo? ¿Puede ser por tener métodos sobrecargados que usan identificadores y otros que no? ¿O simplemente no deberíamos tener identificadores de elementos que sean distintos de los elementos en sí mismos (utilizados como clave hash para la búsqueda)?

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í:

    1. Requiere unicidad / igualdad y admite escenarios de actualización pasando el elemento original (hasta la igualdad).
    2. No requiere unicidad / igualdad y admite escenarios de actualización mediante identificadores. Estos se pueden obtener utilizando variantes de método opcionales 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.
    3. No requieren unicidad / igualdad y no admiten la actualización de prioridades.
  • ¿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

  1. usar una PriorityQueuepara las cadenas que representan mensajes / sentimientos / etiquetas / nombre de usuario que se votan a favor / se actualizan en la puntuación, los valores únicos tienen prioridad cambiante
  2. usar PriorityQueue, doble> para ordenar tuplas únicas [ya sea que tengan cambios de prioridad o no] - debe realizar un seguimiento de los identificadores adicionales en alguna parte
  3. utilizar PriroityQueuepara priorizar índices de gráficos, o ID de objetos de base de datos, ahora tiene que esparcir identificadores a través de su implementación

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.

Propuesta de cola de prioridad (v2.0)

Resumen

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

Principios

En nuestro diseño, nos guiamos por los siguientes principios (a menos que conozca otros mejores):

  • Cobertura amplia. Queremos ofrecer a los clientes de .NET Core una estructura de datos valiosa que sea lo suficientemente versátil como para admitir una amplia variedad de casos de uso.
  • Aprenda de los errores conocidos. Nos esforzamos por ofrecer una funcionalidad de cola prioritaria que esté libre de los problemas de cara al cliente presentes en otros marcos y lenguajes, por ejemplo, Java, Python, C ++, Rust. Evitaremos tomar decisiones de diseño que se sabe que hacen descontentos a los clientes y reducen la utilidad de las colas de prioridad.
  • Cuidado extremo con decisiones de puertas de 1 vía. Una vez que se introduce una API, no se puede modificar ni eliminar, solo ampliar. Analizaremos cuidadosamente las opciones de diseño para evitar soluciones subóptimas con las que nuestros clientes se quedarán atrapados para siempre.
  • Evite la parálisis del diseño. Aceptamos que puede que no haya una solución perfecta. Equilibraremos las compensaciones y avanzaremos con la entrega, para finalmente entregar a nuestros clientes la funcionalidad que han estado esperando durante años.

Fondo

Desde la perspectiva de un cliente

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.

Fondo de ciencias de la computación

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 .

El mecanismo de actualización es el desafío clave del diseño

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).

Recomendació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();
    }
}

Uso de ejemplo

1) Cliente al que no le importa el mecanismo de actualización

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 }

2) Cliente que se preocupa por el mecanismo de actualización

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);

Preguntas más frecuentes

¿En qué orden enumera los elementos la cola de prioridad?

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.

Apéndices

Apéndice A: Otros idiomas con funcionalidad de cola de prioridad

| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | PriorityQueue | Extiende la clase abstracta AbstractQueue e implementa la interfaz Queue . |
| Óxido | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | Priority_queue | |
| Python | heapq | |
| Ir | montón | Hay una interfaz de montón. |

Apéndice B: Descubribilidad

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

Por 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

  • 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)
  • ¿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)
  • El parámetro para 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 entonces
  • 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 estilo TryGet
  • ¿Debería la clase admitir un IEqualityComparer<TElement> con el propósito de Contains ?
  • 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ó)
  • Es extraño que 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 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 entonces
  • Es extraño que 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).
  • 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 estilo TryGet

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 de Contains ?

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:

  • ¿Qué tal si también se proporciona una sobrecarga de TryPeek(out TElement element, out TPriority priority) ?
  • Si tiene claves duplicadas actualizables, me preocupa que si 'dequeue' no devuelve el nodo de la cola de prioridad, ¿cómo comprobaría si está eliminando el nodo exacto correcto de su sistema de seguimiento de controles? Dado que podría tener más de una copia de elemento con la misma prioridad.
  • ¿Se lanza Remove(PriorityQueueNode) si no se encuentra el nodo? o devolver falso?
  • ¿Debería haber una versión TryRemove() que no arroje si Eliminar arroja?
  • ¿No estoy seguro de que 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:

  • Haciendo eco del comentario de @TimLovellSmith , no estoy seguro de que 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.
  • Probablemente reformularía public void Dequeue(out TElement element); como public TElement Dequeue();
  • ¿Por qué el método TryDequeue() necesita devolver una prioridad?
  • ¿No debería la clase implementar también ICollection<T> o IReadOnlyCollection<T> ?
  • ¿Qué debería suceder si intento actualizar un PriorityQueueNode devuelto desde una instancia de PriorityQueue diferente?
  • ¿Queremos apoyar una operación de fusión eficiente? AFAICT esto implica que no podemos usar una representación basada en matrices. ¿Cómo afectaría esto a la implementación en términos de asignaciones?

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 IReadOnlyCollection<T> cuando no tiene las firmas correctas para 'Agregar' y 'Eliminar', etc.

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 PriorityQueueNodenodo); // Sobre)

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> colección);
''

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 KeyValuePairque la nueva clase personalizada.

  • Menos superficie API
  • Puedo hacer algo como esto: `new PriorityQueue(nuevo diccionario() {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}); Sé que está limitado por la singularidad de la clave. También creo que aportaría una buena sinergia consumir colecciones basadas en IDictionary.
  • Es una estructura en lugar de una clase, por lo que no hay excepciones NullReference
  • En algún momento, PrioirtyQueue necesitaría ser serializado / deserializado y creo que sería más fácil hacer qué con el objeto ya existente.

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:

  • Los Binary Heaps son simples, fáciles de implementar, tienen una pequeña API y se sabe que funcionan bien en la práctica en muchos escenarios importantes.
  • Una cola de prioridad completa proporciona beneficios teóricos para algunos escenarios (es decir, complejidad de espacio reducida y complejidad de tiempo reducida para la búsqueda en gráficos densamente conectados) y proporciona una API natural para más algoritmos / escenarios.

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.

Propuesta de cola de prioridad (v2.1)

Resumen

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

Principios

En nuestro diseño, nos guiamos por los siguientes principios (a menos que conozca otros mejores):

  • Cobertura amplia. Queremos ofrecer a los clientes de .NET Core una estructura de datos valiosa que sea lo suficientemente versátil como para admitir una amplia variedad de casos de uso.
  • Aprenda de los errores conocidos. Nos esforzamos por ofrecer una funcionalidad de cola prioritaria que esté libre de los problemas de cara al cliente presentes en otros marcos y lenguajes, por ejemplo, Java, Python, C ++, Rust. Evitaremos tomar decisiones de diseño que se sabe que hacen descontentos a los clientes y reducen la utilidad de las colas de prioridad.
  • Cuidado extremo con decisiones de puertas de 1 vía. Una vez que se introduce una API, no se puede modificar ni eliminar, solo ampliar. Analizaremos cuidadosamente las opciones de diseño para evitar soluciones subóptimas con las que nuestros clientes se quedarán atrapados para siempre.
  • Evite la parálisis del diseño. Aceptamos que puede que no haya una solución perfecta. Equilibraremos las compensaciones y avanzaremos con la entrega, para finalmente entregar a nuestros clientes la funcionalidad que han estado esperando durante años.

Fondo

Desde la perspectiva de un cliente

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.

Fondo de ciencias de la computación

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 .

El mecanismo de actualización es el desafío clave del diseño

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).

Recomendació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();
    }
}

Uso de ejemplo

1) Cliente al que no le importa el mecanismo de actualización

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 }

2) Cliente que se preocupa por el mecanismo de actualización

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);

Preguntas más frecuentes

1. ¿En qué orden enumera los elementos la cola de prioridad?

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.

2. ¿Por qué no existe el método 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.

3. ¿Por qué hay sobrecargas 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.

4. ¿Qué sucede cuando el método 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.

Apéndices

Apéndice A: Otros idiomas con funcionalidad de cola de prioridad

| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | PriorityQueue | Extiende la clase abstracta AbstractQueue e implementa la interfaz Queue . |
| Óxido | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | Priority_queue | |
| Python | heapq | |
| Ir | montón | Hay una interfaz de montón. |

Apéndice B: Descubribilidad

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:

  • Se eliminaron los métodos Contains y TryGet .
  • Se agregó una pregunta frecuente n. ° 2: _ ¿Por qué no existe el método Contains o TryGet ? _
  • Añadida la interfaz IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>> .
  • Se agregó una sobrecarga de bool TryPeek(out TElement element, out TPriority priority) .
  • Se agregó una sobrecarga de bool TryPeek(out TElement element) .
  • Se agregó una sobrecarga de void Dequeue(out TElement element, out TPriority priority) .
  • Se cambió void Dequeue(out TElement element) a PriorityQueueNode<TElement, TPriority> Dequeue() .
  • Se agregó una sobrecarga de bool TryDequeue(out TElement element) .
  • Se agregó una sobrecarga de bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node) .
  • Se agregó una pregunta frecuente n. ° 3: _¿Por qué hay Dequeue y TryDequeue sobrecargas que devuelven PriorityQueueNode ? _
  • Se agregó una pregunta frecuente n. ° 4: _¿Qué sucede cuando el método Update o Remove recibe un nodo de una cola diferente? _

Gracias por las notas de cambio;)

Pequeñas solicitudes de aclaración:

  • ¿Podemos calificar la Pregunta frecuente 4 de manera que se aplique a los elementos que no están en la cola? (es decir, los eliminados)
  • ¿Podemos agregar una pregunta frecuente sobre estabilidad, es decir, las garantías (si las hay) al retirar elementos de la cola con la misma prioridad (tengo entendido que no hay ningún plan para hacer garantías, lo cual es importante saber, por ejemplo, para la programació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 😊

Propuesta de cola de prioridad (v2.2)

Resumen

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

Principios

En nuestro diseño, nos guiamos por los siguientes principios (a menos que conozca otros mejores):

  • Cobertura amplia. Queremos ofrecer a los clientes de .NET Core una estructura de datos valiosa que sea lo suficientemente versátil como para admitir una amplia variedad de casos de uso.
  • Aprenda de los errores conocidos. Nos esforzamos por ofrecer una funcionalidad de cola prioritaria que esté libre de los problemas de cara al cliente presentes en otros marcos y lenguajes, por ejemplo, Java, Python, C ++, Rust. Evitaremos tomar decisiones de diseño que se sabe que hacen descontentos a los clientes y reducen la utilidad de las colas de prioridad.
  • Cuidado extremo con decisiones de puertas de 1 vía. Una vez que se introduce una API, no se puede modificar ni eliminar, solo ampliar. Analizaremos cuidadosamente las opciones de diseño para evitar soluciones subóptimas con las que nuestros clientes se quedarán atrapados para siempre.
  • Evite la parálisis del diseño. Aceptamos que puede que no haya una solución perfecta. Equilibraremos las compensaciones y avanzaremos con la entrega, para finalmente entregar a nuestros clientes la funcionalidad que han estado esperando durante años.

Fondo

Desde la perspectiva de un cliente

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.

Fondo de ciencias de la computación

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 .

El mecanismo de actualización es el desafío clave del diseño

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).

Recomendació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();
    }
}

Uso de ejemplo

1) Cliente al que no le importa el mecanismo de actualización

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 }

2) Cliente que se preocupa por el mecanismo de actualización

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);

Preguntas más frecuentes

1. ¿En qué orden enumera los elementos la cola de prioridad?

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.

2. ¿Por qué no existe el método 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.

3. ¿Por qué hay sobrecargas 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.

4. ¿Qué sucede cuando el método 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.

5. ¿Por qué no existe el método 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.

6. ¿Ofrece la colección una garantía de estabilidad?

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.

Apéndices

Apéndice A: Otros idiomas con funcionalidad de cola de prioridad

| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | PriorityQueue | Extiende la clase abstracta AbstractQueue e implementa la interfaz Queue . |
| Óxido | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | Priority_queue | |
| Python | heapq | |
| Ir | montón | Hay una interfaz de montón. |

Apéndice B: Descubribilidad

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

Registro de cambios:

  • Se eliminó el método void Merge(PriorityQueue<TElement, TPriority> other) // O(1) .
  • Se agregó una pregunta frecuente n. ° 5: ¿Por qué no existe el método Merge ?
  • Se modificó la pregunta frecuente n. ° 4 para que también se mantenga en los nodos que se han eliminado de la cola de prioridad.
  • Se agregó una pregunta frecuente n. ° 6: ¿La colección proporciona una garantía de estabilidad?

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í:

  1. Requiere actualizaciones de singularidad / igualdad y soporte pasando el elemento original.
  2. Apoye las actualizaciones de prioridad usando manijas.
  3. No admite actualizaciones prioritarias.

Estos enfoques son mutuamente excluyentes y tienen sus propios conjuntos de compensaciones, respectivamente:

  1. El enfoque basado en la igualdad complica el contrato de API al forzar la singularidad y requiere una contabilidad adicional bajo el capó. Esto sucede tanto si el usuario necesita actualizaciones prioritarias como si no.
  2. El enfoque basado en el identificador implicaría al menos una asignación adicional por elemento en cola. Si bien no impone la unicidad, mi impresión es que para los escenarios que necesitan actualizaciones, este invariante es casi con certeza implícito (por ejemplo, observe cómo ambos ejemplos enumerados anteriormente almacenan los manejadores en diccionarios externos indexados por los elementos mismos).
  3. Las actualizaciones no se admiten en absoluto o requerirían un recorrido lineal del montón. En caso de elementos duplicados, las actualizaciones pueden ser ambiguas.

Identificación de aplicaciones PriorityQueue comunes

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.

Recomendaciones

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:

  1. 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 _lazily_ construyendo 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.

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?

  1. 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:

  • La actualización ahora se convierte en _O (n) _, lo que puede resultar en un rendimiento impredecible según los patrones de uso.
  • La tabla de búsqueda también es necesaria para validar la unicidad. Poner en cola el mismo elemento dos veces _antes_ de llamar a Update sería aceptado y podría decirse que llevaría la cola a un estado inconsistente.

@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.

PriorityQueue básica

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());

PriorityQueue actualizable

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;
    }
}

Comparación de rendimiento

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.

  1. Realmente no veo la razón de 2 estructuras de datos casi idénticas, donde una es una versión inferior de la otra.
  2. Además, incluso si quisiera tener estas 2 versiones de una cola de prioridad, no veo cómo la versión "superior" es mejor que la propuesta de cola de prioridad (v2.2) de hace 20 días.

tl; dr:

  • ¡¡Esta propuesta es emocionante !! Pero aún no se ajusta a mis casos de uso de alto rendimiento.
  • > 90% de mi carga de rendimiento de planificación de movimiento / geometría computacional es PQ enqueue / dequeue porque ese es el N ^ m LogN dominante en un algoritmo.
  • Estoy a favor de implementaciones de PQ separadas. Por lo general, no necesito actualizaciones de prioridad, y el rendimiento 2 veces peor es inaceptable.
  • PrioritySet es un nombre confuso y no se puede descubrir frente a PriorityQueue
  • Almacenar la prioridad dos veces (una en mi elemento, una vez en la cola) parece caro. Estructura de copias y uso de memoria.
  • Si la prioridad informática fuera cara, simplemente almacenaría una tupla (priority: ComputePriority(element), element) en un PQ, y mi función de obtención de prioridad sería simplemente tuple => tuple.priority .
  • El rendimiento debe ser evaluado por operación o, alternativamente, en casos de uso del mundo real (por ejemplo, búsqueda optimizada de múltiples inicios y múltiples extremos en un gráfico)
  • El comportamiento de enumeración desordenado es inesperado. ¿Prefiere Dequeue () similar a una cola - semántica de orden?
  • Considere la posibilidad de admitir la operación de clonación y fusión.
  • Las operaciones básicas deben ser de asignación 0 en el uso de estado estable. Reuniré estas colas.
  • Considere la posibilidad de admitir EnqueueMany, que realiza heapify para ayudar a la agrupación.

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:

  • PrioritySet implica establecer semántica, pero la cola no implementa ISet.
  • Estás usando el nombre UpdatablePriorityQueue, que es más fácil de descubrir si busco PriorityQueue.

Rendimiento:

  • El rendimiento de la cola de prioridad es casi siempre mi cuello de botella de rendimiento (> 90%) en mis algoritmos de geometría / planificación
  • Considere pasar un Funco comparaciónto ctor en lugar de copiar TPriority (¡caro!). Si la prioridad informática es cara, insertaré (prioridad, elemento) en un PQ y pasaré una comparación que analiza mi prioridad almacenada en caché.
  • Un número significativo de mis algoritmos no necesita actualizaciones de PQ. Consideraría usar un PQ integrado que no admita actualizaciones, pero si algo tiene un costo de rendimiento de 2x para admitir una función que no necesito (actualización), entonces no me sirve.
  • Para el análisis de rendimiento / compensaciones, sería importante conocer el costo relativo de puesta en cola / salida de cola por operación @eiriktsarpalis : "el seguimiento es 2
  • Me alegré de ver a sus constructores realizar Heapify. Considere un constructor que toma IList, List o Array para evitar asignaciones de enumeración.
  • Considere exponer un EnqueueMany que realiza Heapify si el PQ está inicialmente vacío, ya que en alto rendimiento es común agrupar colecciones.
  • Considere hacer una matriz Clear not zero si los elementos no contienen referencias.
  • Las asignaciones en poner en cola / quitar de cola son inaceptables. Mis algoritmos son de asignación cero por razones de rendimiento, con colecciones locales de subprocesos agrupadas.

API:

  • Clonar una cola de prioridad es una operación trivial con sus implementaciones y frecuentemente útil.

    • Relacionado: ¿La enumeración de una cola prioritaria debe tener una semántica similar a una cola? Esperaría una colección de orden de salida de cola, similar a lo que hace Queue. Espero que la nueva List (myPriorityQueue) no mute el priorityQueue, sino que funcione como acabo de describir.

  • Como se mencionó anteriormente, es preferible tomar un 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
  • A veces resulta útil fusionar dos colas de prioridad.
  • Es extraño que Peek devuelva un TItem pero Enumeration & Enqueue tengan (TItem, TPriority).

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 |

Conclusiones clave

  • ~ La implementación del montón de emparejamiento funciona asintóticamente mucho mejor que sus contrapartes respaldadas por arreglos. Sin embargo, puede ser hasta 2 veces más lento para montones pequeños (<50 elementos), alcanza alrededor de 1000 elementos, pero es hasta 2 veces más rápido para montones de tamaño 10 ^ 6 ~.
  • Como se esperaba, el montón de emparejamiento produce una cantidad significativa de asignaciones de montón.
  • La implementación de "PrioritySet" es consistentemente lenta ~ la más lenta de los tres contendientes ~, por lo que es posible que no queramos seguir ese enfoque después de todo.

~ 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:

  • Patrones comunes de uso de la cola de prioridad en el código fuente .NET.
  • Implementaciones de PriorityQueue en bibliotecas centrales de marcos de la competencia.
  • Benchmarks de varios prototipos de colas de prioridad .NET.

Lo que arrojó las siguientes conclusiones:

  • El 90% de los casos de uso de colas de prioridad no requieren actualizaciones de prioridad.
  • El soporte de actualizaciones de prioridad da como resultado un contrato de API más complicado (que requiere identificadores o elementos únicos).
  • En mis puntos de referencia, las implementaciones que admiten actualizaciones prioritarias son 2-3 veces más lentas en comparación con las que no lo hacen.

Próximos pasos

De cara al futuro, propongo que tomemos las siguientes acciones para .NET 6:

  1. 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.

  2. 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?

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

chunseoklee picture chunseoklee  ·  3Comentarios

yahorsi picture yahorsi  ·  3Comentarios

omajid picture omajid  ·  3Comentarios

noahfalk picture noahfalk  ·  3Comentarios

jamesqo picture jamesqo  ·  3Comentarios