Runtime: Adicionar PriorityQueue<t>para coleções</t>

Criado em 30 jan. 2015  ·  318Comentários  ·  Fonte: dotnet/runtime

Veja a última proposta no repositório corefxlab.

Opções de segunda proposta

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

Premissas

Os elementos na fila de prioridade são exclusivos. Se não forem, teremos que introduzir 'alças' de itens para permitir sua atualização / remoção. Ou a semântica de atualização / remoção teria que ser aplicada em primeiro lugar, o que é estranho.

Modelado após Queue<T> ( link do MSDN )

API

`` `c #
public class PriorityQueue
: IEnumerable,
IEnumerable <(elemento TElement, prioridade TPriority)>,
IReadOnlyCollection <(elemento TElement, prioridade TPriority)>
// ICollection não incluída propositalmente
{
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();

//
// Parte do seletor
//
public PriorityQueue (FuncprioritySelector);
public PriorityQueue (FuncprioritySelector, IComparercomparador);

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

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

}
`` ``

Perguntas abertas:

  1. Nome da classe PriorityQueue vs. Heap
  2. Introduzir IHeap e sobrecarga do construtor? (Devemos esperar mais tarde?)
  3. Apresente IPriorityQueue ? (Devemos esperar mais tarde - IDictionary exemplo)
  4. Use seletor (de prioridade armazenada dentro do valor) ou não (diferença de 5 APIs)
  5. Use tuplas (TElement element, TPriority priority) vs. KeyValuePair<TPriority, TElement>

    • Deveriam Peek e Dequeue ter out argumento em vez de tupla?

  6. O arremesso de Peek e Dequeue útil?

Proposta Original

O problema https://github.com/dotnet/corefx/issues/163 solicitou a adição de uma fila de prioridade às estruturas de dados de coleta .NET principais.

Esta postagem, embora seja uma duplicata, tem como objetivo atuar na submissão formal ao Processo de Revisão da API corefx. O conteúdo do problema é o _speclet_ para um novo System.Collections.Generic.PriorityQueuemodelo.

Estarei contribuindo com o PR, se aprovado.

Justificativa e uso

O .NET Base Class Libraries (BCL) atualmente carece de suporte para coleções ordenadas produtor-consumidor. Um requisito comum de muitos aplicativos de software é a capacidade de gerar uma lista de itens ao longo do tempo e processá-los em uma ordem diferente da ordem em que foram recebidos.

Existem três estruturas de dados genéricas na hierarquia System.Collections de namespaces que oferecem suporte a uma coleção classificada de itens; System.Collections.Generic.SortedList, System.Collections.Generic.SortedSet e System.Collections.Generic.SortedDictionary.

Destes, SortedSet e SortedDictionary não são apropriados para padrões produtor-consumidor que geram valores duplicados. A complexidade de SortedList é Θ (n) o pior caso para Adicionar e Remover.

Uma estrutura de dados com muito mais memória e eficiência de tempo para coleções ordenadas com padrões de uso produtor-consumidor é uma fila de prioridade. Exceto quando o redimensionamento da capacidade é necessário, o pior caso de inserção (enfileirar) e remover o desempenho superior (retirar da fila) é Θ (log n) - muito melhor do que as opções existentes no BCL.

As filas prioritárias têm um amplo grau de aplicabilidade em diferentes classes de aplicativos. A página da Wikipedia sobre Filas prioritárias oferece uma lista de muitos casos de uso bem conhecidos. Embora implementações altamente especializadas ainda possam exigir implementações de fila de prioridade personalizada, uma implementação padrão cobriria uma ampla gama de cenários de uso. As filas prioritárias são particularmente úteis na programação da saída de vários produtores, o que é um padrão importante em software altamente paralelizado.

É importante notar que tanto a biblioteca padrão C ++ quanto o Java oferecem funcionalidade de fila prioritária como parte de suas APIs básicas.

API proposta

`` `C #
namespace System.Collections.Generic
{
///


/// Representa uma coleção de objetos que são removidos em uma ordem de classificação.
///

///Especifica o tipo de elementos na fila.
[DebuggerDisplay ("Count = {count}")]
[DebuggerTypeProxy (typeof (System_PriorityQueueDebugView <>))]
public class PriorityQueue: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
///
/// Inicializa uma nova instância do classe
/// que usa um comparador padrão.
///

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

}
`` `

Detalhes

  • A estrutura de dados de implementação será um heap binário. Os itens com um valor de comparação maior serão retornados primeiro. (ordem decrescente)
  • Complexidades temporais:

| Operação | Complexidade | Notas |
| --- | --- | --- |
| Construir | Θ (1) | |
| Construir usando IEnumerable | Θ (n) | |
| Enqueue | Θ (log n) | |
| Dequeue | Θ (log n) | |
| Peek | Θ (1) | |
| Count | Θ (1) | |
| Limpar | Θ (N) | |
| Contém | Θ (N) | |
| CopyTo | Θ (N) | Usa Array.Copy, a complexidade real pode ser menor |
| ToArray | Θ (N) | Usa Array.Copy, a complexidade real pode ser menor |
| GetEnumerator | Θ (1) | |
| Enumerator.MoveNext | Θ (1) | |

  • Sobrecargas de construtor adicionais que levam o System.Comparisondelegado foram intencionalmente omitidos em favor de uma área de superfície API simplificada. Os chamadores podem usar o Comparer.Crie para converter uma função ou expressão Lambda em um IComparerinterface, se necessário. Isso exige que o chamador incorra em uma alocação de heap única.
  • Embora System.Collections.Generic ainda não faça parte do corefx, proponho que essa classe seja adicionada ao corefxlab nesse meio tempo. Ele pode ser movido para o repositório corefx principal assim que System.Collections.Generic for adicionado e houver consenso de que seu status deve ser elevado de experimental a uma API oficial.
  • Uma propriedade IsEmpty não foi incluída, uma vez que não há nenhuma penalidade de desempenho adicional chamando Count. A maioria das estruturas de dados de coleta não inclui IsEmpty.
  • As propriedades IsSynchronized e SyncRoot de ICollection foram implementadas explicitamente, pois são efetivamente obsoletas. Isso também segue o padrão usado para as outras estruturas de dados System.Collection.Generic.
  • Dequeue e Peek lançam uma InvalidOperationException quando a fila está vazia para corresponder ao comportamento estabelecido de System.Collections.Queue.
  • IProducerConsumerCollectionnão foi implementado, pois sua documentação afirma que ele se destina apenas a coleções thread-safe.

Perguntas abertas

  • Evitar uma alocação de heap adicional durante chamadas para GetEnumerator ao usar foreach é uma razão forte o suficiente para incluir a estrutura de enumerador público aninhado?
  • CopyTo, ToArray e GetEnumerator devem retornar resultados em ordem priorizada (classificada) ou a ordem interna usada pela estrutura de dados? Minha suposição é que a ordem interna deve ser retornada, pois não incorre em nenhuma penalidade de desempenho adicional. No entanto, esse é um problema de usabilidade potencial se um desenvolvedor pensar na classe como uma "fila classificada" em vez de uma fila de prioridade.
  • Adicionar um tipo denominado PriorityQueue a System.Collections.Generic causa uma alteração potencialmente significativa? O namespace é muito usado e pode causar um problema de compatibilidade de origem para projetos que incluem seu próprio tipo de fila de prioridade.
  • Os itens devem ser retirados da fila em ordem crescente ou decrescente, com base na saída de IComparer? (minha suposição é de ordem crescente, para corresponder à convenção de classificação normal de IComparer)
  • A coleção deve ser 'estável'? Em outras palavras, se dois itens com igual IComparisonos resultados devem ser retirados da fila exatamente na mesma ordem em que foram colocados na fila? (suponho que isso não seja necessário)

    Atualizações

  • Correção da complexidade de 'Construct Using IEnumerable' para Θ (n). Obrigado @svick.

  • Adicionada outra questão de opção sobre se a fila de prioridade deve ser ordenada em ordem crescente ou decrescente em comparação com o IComparer.
  • Removido NotSupportedException da propriedade SyncRoot explícita para corresponder ao comportamento de outros tipos System.Collection.Generic em vez de usar o padrão mais recente.
  • Tornou o método GetEnumerator público que retornou uma estrutura Enumerator aninhada em vez de IEnumerable, semelhante aos tipos System.Collections.Generic existentes. Esta é uma otimização para evitar uma alocação de heap (GC) ao usar um loop foreach.
  • Atributo ComVisible removido.
  • Complexidade alterada de Clear para Θ (n). Obrigado @mbeidler.
api-needs-work area-System.Collections wishlist

Comentários muito úteis

a estrutura de dados heap é OBRIGATÓRIA para fazer leetcode
mais leetcode, mais entrevista de código c #, o que significa mais desenvolvedores c #.
mais desenvolvedores significa melhor ecossistema.
melhor ecossistema significa que se ainda podemos programar em c # amanhã.

em suma: este não é apenas um recurso, mas também o futuro. é por isso que o problema é rotulado como 'futuro'.

Todos 318 comentários

| Operação | Complexidade |
| --- | --- |
| Construir usando IEnumerable | Θ (log n) |

Acho que deveria ser Θ (n). Você precisa pelo menos iterar a entrada.

+1

Deve uma estrutura de enumerador público aninhada ser usada para evitar uma alocação de heap adicional durante chamadas para GetEnumerator e ao usar foreach? Minha suposição é não, já que enumerar em uma fila é uma operação incomum.

Eu tenderia a usar o enumerador de estrutura para ser consistente com Queue<T> que usa um enumerador de estrutura. Além disso, se um enumerador de estrutura não for usado agora, não poderemos alterar PriorityQueue<T> para usar um no futuro.

Também, talvez, um método para inserções em lote? Sempre será possível classificar e continuar a partir do ponto de inserção anterior em vez de começar do início, se isso ajudar ?:

    public void Enqueue(List<T> items) {
         items.Sort(_comparer);
         ... insertions ...
    }

Joguei uma cópia da implementação inicial aqui . A cobertura do teste está longe de ser completa, mas se alguém estiver curioso, dê uma olhada e me diga o que você achou. Tentei seguir as convenções de codificação existentes das classes System.Collections tanto quanto possível.

Legal. Alguns comentários iniciais:

  • Queue<T>.Enumerator implementa IEnumerator.Reset explicitamente. PriorityQueue<T>.Enumerator fazer o mesmo?
  • Queue<T>.Enumerator usa _index == -2 para indicar que o enumerador foi eliminado. PriorityQueue<T>.Enumerator tem o mesmo comentário, mas tem um campo _disposed extra. Considere se livrar do campo _disposed extra e use _index == -2 para indicar que ele foi descartado para tornar a estrutura menor e ser consistente com Queue<T>
  • Acho que o campo estático _emptyArray pode ser removido e o uso substituído por Array.Empty<T>() .

Também...

  • Outras coleções que usam um comparador (por exemplo, Dictionary<TKey, TValue> , HashSet<T> , SortedList<TKey, TValue> , SortedDictionary<TKey, TValue> , SortedSet<T> , etc.) permitem que nulo seja passado para o comparador, caso em que Comparer<T>.Default é usado.

Também...

  • ToArray pode ser otimizado verificando-se _size == 0 antes de alocar o novo array, nesse caso apenas retorne Array.Empty<T>() .

@justinvp Ótimo feedback, obrigado!

  • Eu implementei Enumerator.Reset explicitamente, pois é a funcionalidade principal e não obsoleta de um enumerador. Se está ou não exposto parece inconsistente entre os tipos de coleção, e apenas alguns usam a variante explícita.
  • Removido o campo _disposed em favor de _index, obrigado! Joguei isso no último minuto naquela noite e perdi o óbvio. Decidiu manter a ObjectDisposedException para correção com os tipos de coleção mais recentes, embora os antigos tipos System.Collections.Generic não a usem.
  • Array.Emptyé um recurso do F #, portanto, infelizmente, não posso usá-lo aqui!
  • Modificados os parâmetros do comparador para aceitar nulo, bom achado!
  • A otimização do ToArray é complicada. Matrizes _Técnicamente_ falando são mutáveis ​​em C #, mesmo quando têm comprimento zero. Na verdade, você está certo, a alocação não é necessária e pode ser otimizada. Estou inclinado a uma implementação mais cautelosa, caso haja efeitos colaterais nos quais não estou pensando. Semanticamente, o chamador ainda espera essa alocação, e é pequena.

@ebickle

Array.Empty é um recurso do F #, portanto, infelizmente, não podemos usá-lo aqui!

Não mais: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Array.cs#L1060 -L1069

Migrei o código para branch issue-574.

Implementado o Array.Empty() alterou e conectou tudo no pipeline de compilação regular. Um pequeno erro temporário que tive de introduzir foi fazer com que o projeto System.Collections dependesse do pacote nuget System.Collections, como Comparerainda não é de código aberto.

Será corrigido assim que o problema dotnet / corefx # 966 for concluído.

Uma área importante na qual estou procurando feedback é como ToArray, CopyTo e o enumerador devem ser tratados. Atualmente, eles estão otimizados para desempenho, o que significa que a matriz de destino é um heap e não é classificada pelo comparador.

Existem três opções:
1) Deixe como está e documente que o array retornado não está classificado. (é uma fila prioritária, não uma fila classificada)
2) Modifique os métodos para classificar os itens / matrizes retornados. Eles não serão mais operações O (n).
3) Remova os métodos e o suporte enumerável completamente. Esta é a opção "purista", mas remove a capacidade de pegar rapidamente os itens restantes na fila quando a fila de prioridade não for mais necessária.

Outra coisa sobre a qual gostaria de receber feedback é se a fila deve ser estável para dois itens com a mesma prioridade (compare o resultado de 0). Geralmente, as filas de prioridade não garantem que dois itens com a mesma prioridade serão retirados da fila na ordem em que foram enfileirados, mas percebi que a implementação da fila de prioridade interna usada em System.Reactive.Core exigiu alguma sobrecarga adicional para garantir essa propriedade. Minha preferência seria não fazer isso, mas não tenho certeza de qual opção é melhor em termos de expectativas dos desenvolvedores.

Aconteceu neste PR porque eu também estava interessado em adicionar uma fila prioritária ao .NET. Fico feliz em ver que alguém se esforçou para fazer esta proposta :). Depois de revisar o código, percebi o seguinte:

  • Quando a ordem IComparer não é consistente com Equals , o comportamento desta implementação Contains (que usa IComparer ) pode ser surpreendente para alguns usuários, como é essencialmente _contém um item com igual prioridade_.
  • Não vi código para reduzir o array em Dequeue . Reduzir a matriz de heap pela metade quando um quarto cheio é típico.
  • O método Enqueue aceitar null argumentos?
  • Acho que a complexidade de Clear deve ser Θ (n), já que essa é a complexidade de System.Array.Clear , que ele usa. https://msdn.microsoft.com/en-us/library/system.array.clear%28v=vs.110%29.aspx

Não vi código para reduzir o array em Dequeue . Reduzir a matriz de heap pela metade quando um quarto cheio é típico.

Queue<T> e Stack<T> também não reduzem seus arrays (com base na fonte de referência , eles ainda não estão no CoreFX).

@mbeidler Eu considerei adicionar alguma forma de redução automática de array no desenfileiramento, mas como @svick apontou, isso não existe nas implementações de referência de estruturas de dados semelhantes. Eu ficaria curioso para ouvir de alguém da equipe .NET Core / BCL se há algum motivo específico para a escolha desse estilo de implementação.

Atualização: Eu verifiquei a lista, Fila, Queue e ArrayList - nenhum deles reduz o tamanho da matriz interna em uma remoção / remoção da fila.

O enfileiramento deve oferecer suporte a nulos e está documentado como permitindo-os. Você encontrou um bug? Não me lembro o quão robusta a área de testes de unidade na área ainda.

Interessante, notei na fonte de referência linkada por @svick que Queue<T> tem uma constante privada não usada chamada _ShrinkThreshold . Talvez esse comportamento existisse em uma versão anterior.

Com relação ao uso de IComparer vez de Equals na implementação de Contains , escrevi o seguinte teste de unidade, que falharia atualmente: https: //gist.github. com / mbeidler / 9e9f566ba7356302c57e

@mbeidler Bom argumento. De acordo com MSDN, IComparer/ IComparableapenas garante que um valor igual a zero tenha a mesma ordem de classificação.

No entanto, parece que o mesmo problema existe nas outras classes de coleção. Se eu modificar o código para operar em SortedList, o caso de teste ainda falha em ContainsKey. A implementação de SortedList.ContainsKey chama Array.BinarySearch, que depende de IComparer para verificar a igualdade. O mesmo se aplica a SortedSet.

Sem dúvida, é um bug nas classes de coleção existentes também. Vou vasculhar o restante das classes de coleções e ver se há outras estruturas de dados que aceitam um IComparer, mas testam a igualdade separadamente. Você está certo, porém, para uma fila de prioridade você esperaria um comportamento de pedido personalizado que é completamente independente da igualdade.

Cometi uma correção e o caso de teste em meu branch fork. A nova implementação de Contains é baseada diretamente no comportamento de List.Contains. Desde Listanão aceita um IEqualityComparer, o comportamento é funcionalmente equivalente.

Quando eu chegar algum tempo mais tarde hoje, provavelmente irei enviar relatórios de bug para as outras coleções integradas. Provavelmente não pode ser corrigido devido ao comportamento de regressão, mas pelo menos a documentação precisa ser atualizada.

Acho que faz sentido ContainsKey usar a implementação IComparer<TKey> , já que é isso que especifica a chave. No entanto, acho que seria mais lógico para ContainsValue usar Equals vez de IComparable<TValue> em sua busca linear; embora, neste caso, o escopo seja significativamente reduzido, uma vez que a ordem natural de um tipo tem muito menos probabilidade de ser inconsistente com iguais.

Parece que na documentação do MSDN para SortedList<TKey, TValue> , a seção de comentários para ContainsValue indica que a ordem de classificação do TValue é usada no lugar da igualdade.

@terrajobst O que você acha da proposta da API até agora? Você acha que este é um bom ajuste para CoreFX?

: +1:

Obrigado por preencher isso. Acredito que temos dados suficientes para fazer uma revisão formal desta proposta, portanto, eu a rotulei como 'pronta para revisão da API'

Como Dequeue e Peek são métodos de lançamento, o chamador precisa verificar a contagem antes de cada chamada. Faria sentido em vez disso (ou além) fornecer TryDequeue e TryPeek seguindo o padrão das coleções simultâneas? Existem questões em aberto para adicionar métodos não lançados a coleções genéricas existentes, portanto, adicionar uma nova coleção que não tenha esses métodos parece contraproducente.

@andrewgmorris related https://github.com/dotnet/corefx/issues/4316 "Adicionar TryDequeue à fila"

Fizemos uma revisão básica disso e concordamos que queremos um ProrityQueue na estrutura. No entanto, precisamos contratar alguém para ajudar a conduzir o design e a implementação dele. Quem perceber o problema pode trabalhar

Então, que trabalho resta na API?

Isso está faltando na proposta de API acima: PriorityQueue<T> deve implementar IReadOnlyCollection<T> para corresponder a Queue<T> ( Queue<T> agora implementa IReadOnlyCollection<T> como do .NET 4.6).

Não sei se as filas de prioridade baseadas em array são as melhores. A alocação de memória no .NET é muito rápida. Não temos o mesmo problema de busca de pequenos blocos que o velho malloc tratou. Você está convidado a usar meu código de fila de prioridade aqui: https://github.com/BrannonKing/Kts.Astar/tree/master/Kts.AStar

@ebickle Um minúsculo nit no _speclet_. Diz:

/// Adds an object to the end of the <see cref="PriorityQueue{T}"/>. ... public void Enqueue(T item);

Não deveria dizer, em vez disso, /// Inserts object into the <see cref="PriorityQueue{T}"> by its priority.

@SunnyWar Corrigida a documentação do método Enqueue, obrigado!

Algum tempo atrás, criei uma estrutura de dados com complexidades semelhantes a uma fila de prioridade baseada em uma estrutura de dados Skip List que decidi compartilhar neste momento: https://gist.github.com/bbarry/5e0f3cc1ac7f7521fe6ea25947f48ace

https://en.wikipedia.org/wiki/Skip_list

A lista de pular corresponde às complexidades de uma fila de prioridade acima em casos médios, exceto que Contém é um caso médio O(log(n)) . Além disso, o acesso ao primeiro ou ao último elemento são operações de tempo constante e a iteração em ordem direta e reversa corresponde às complexidades de ordem direta de um PQ.

Obviamente, há desvantagens em tal estrutura na forma de custos mais altos de memória, e ela retorna para O(n) inserções e remoções de pior caso, então tem suas compensações ...

Isso já está implementado em algum lugar? Quando é o lançamento esperado?
Também que tal atualizar a prioridade de um item existente?

@ Priya91 @ianhays está pronto para ser marcado como pronto para revisão?

Isso está faltando na proposta de API acima: PriorityQueuedeve implementar IReadOnlyCollectionpara coincidir com a fila(Filaagora implementa IReadOnlyCollectiona partir do .NET 4.6).

Eu concordo com @justinvp aqui.

@ Priya91 @ianhays está pronto para ser marcado como pronto para revisão?

Eu diria que sim. Isso está parado há um tempo; vamos fazer movimentos nisso.

@justinvp @ianhays Eu atualizei a especificação para implementar IReadOnlyCollection. Obrigado!

Eu tenho uma implementação completa da classe e está associada PriorityQueueDebugView que usa uma implementação baseada em array. Os testes de unidade ainda não estão com 100% de cobertura, mas se houver algum interesse, posso trabalhar um pouco e tirar o pó do garfo.

@NKnusperer fez uma boa observação sobre como atualizar a prioridade de um item existente. Vou deixar de fora por enquanto, mas é algo a se considerar durante a revisão das especificações.

Existem 2 implementações em full framework, que são alternativas possíveis.
https://referencesource.microsoft.com/#q = priorityqueue

Para referência, aqui está uma pergunta sobre o Java PriorityQueue em stackoverflow http://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueue. É interessante que a prioridade seja tratada por um comparador em vez de apenas um objeto wrapper de prioridade simples int. Por exemplo, não facilita alterar a prioridade de um item já na fila.

Revisão da API:
Concordamos que é útil ter este tipo no CoreFX, porque esperamos que o CoreFX o use.

Para a revisão final da forma da API, gostaríamos de ver o código de amostra: PriorityQueue<Thread> e PriorityQueue<MyClass> .

  1. Como mantemos a prioridade? No momento, só está implícito em T .
  2. Queremos que seja possível passar a prioridade ao adicionar uma entrada? (o que parece muito útil)

Notas:

  • Esperamos que a prioridade não mude por conta própria - precisaríamos de uma API para ela ou esperaríamos Remove e Add na fila.
  • Dado que não temos um código de cliente claro aqui (apenas desejo geral que queiramos o tipo), é difícil decidir o que otimizar - perf, vs. usabilidade vs. outra coisa?

Esse seria um tipo realmente útil de se ter no CoreFX. Alguém está interessado em pegar este?

Não gosto da ideia de fixar a fila de prioridade em um heap binário. Dê uma olhada na minha página wiki do AlgoKit para detalhes. Pensamentos rápidos:

  • Se estivermos corrigindo a implementação para um certo tipo de heap, torne-o um heap 4 ário.
  • Não devemos corrigir a implementação do heap, pois em alguns cenários alguns tipos de heap têm mais desempenho do que outros. Assim, dado que corrigimos a implementação para um determinado tipo de heap e o cliente precisa de um diferente para obter mais desempenho, ele precisaria implementar toda a fila de prioridade desde o início. Isto é errado. Devemos ser mais flexíveis aqui e deixá-lo reutilizar parte do código CoreFX (pelo menos algumas interfaces).

No ano passado, implementei alguns tipos de heap . Em particular, há IHeap interface que pode ser usada como a fila de prioridade.

  • Por que não chamar de heap ...? Você conhece alguma maneira sensata de implementar uma fila de prioridade diferente de usar um heap?

Sou totalmente a favor da introdução de uma interface IHeap e algumas implementações de maior desempenho (pelo menos aquelas baseadas em array). A API e as implementações estão no repositório I linkado acima.

Portanto, não há _filas de prioridade_. Pilhas .

O que você acha?

@karelz @safern @danmosemsft

@pgolebiowski Não se esqueça de que estamos projetando uma API fácil de usar e com PriorityQueue (que é um termo consagrado na ciência da computação), deverá encontrar um em documentos / por meio de pesquisa na Internet.
Se a implementação subjacente for heap (que eu pessoalmente me pergunto por quê), ou outra coisa, não importa muito. Supondo que você possa demonstrar que a implementação é mensuravelmente melhor do que alternativas mais simples (a complexidade do código também é uma métrica que importa um pouco).

Portanto, se você ainda acredita que a implementação baseada em heap (sem IHeap API) é melhor do que uma implementação simples baseada em lista ou lista de blocos de matriz, explique o porquê (de preferência em algumas frases / parágrafos ), para que possamos chegar a um acordo sobre a abordagem de implementação (e evitar perda de tempo da sua parte com implementações complexas que podem ser rejeitadas no momento da revisão de RP).

Drop ICollection , IEnumerable ? Apenas tenha as versões genéricas (embora IEnumerable<T> genéricos trarão IEnumerable )

@pgolebiowski como é implementado não altera a API externa. PriorityQueue define o comportamento / contrato; enquanto Heap é uma implementação específica.

Filas prioritárias vs heaps

Se a implementação subjacente for heap (que eu pessoalmente me pergunto por quê), ou outra coisa, não importa muito. Supondo que você possa demonstrar que a implementação é mensuravelmente melhor do que alternativas mais simples (a complexidade do código também é uma métrica que importa um pouco).

Portanto, se você ainda acredita que a implementação baseada em heap é melhor do que uma implementação simples baseada em lista ou lista de blocos de matriz, explique o porquê (de preferência em algumas frases / parágrafos), para que possamos chegar a um acordo sobre o abordagem de implementação [...]

OK. Uma fila de prioridade é uma estrutura de dados abstrata que pode ser implementada de alguma forma. Claro, você pode implementá-lo com uma estrutura de dados diferente de um heap. Mas de jeito nenhum é mais eficiente. Como resultado:

  • fila de prioridade e heap são frequentemente usados ​​como sinônimos (consulte a documentação do Python abaixo como o exemplo mais claro disso)
  • sempre que você tiver um suporte de "fila de prioridade" em alguma biblioteca, ele usa um heap (veja todos os exemplos abaixo)

Para apoiar minhas palavras, vamos começar com o suporte teórico. Introdução aos algoritmos , Cormen:

[…] As filas de prioridade vêm em duas formas: filas de prioridade máxima e filas de prioridade mínima. Vamos nos concentrar aqui em como implementar filas de prioridade máxima, que por sua vez são baseadas em heaps máximos.

Declarado claramente que as filas prioritárias são montes. Este é um atalho, é claro, mas essa é a ideia. Agora, mais importante, vamos dar uma olhada em como outras linguagens e suas bibliotecas padrão adicionam suporte para as operações que estamos discutindo:

  • Java: PriorityQueue<T> - fila de prioridade implementada com um heap.
  • Ferrugem: BinaryHeap - heap explicitamente na API. Diz na documentação que é uma fila prioritária implementada com um heap binário. Mas a API é muito clara na estrutura - um heap.
  • Swift: CFBinaryHeap - novamente, diz explicitamente qual é a estrutura de dados, evitando o uso do termo abstrato "fila de prioridade". Os documentos que descrevem a classe: heaps binários podem ser úteis como filas prioritárias. Gosto da abordagem.
  • C ++: priority_queue - mais uma vez, implementado canonicamente com um heap binário construído no topo de um array.
  • Python: heapq - heap é explicitamente exposto na API. A fila de prioridade é mencionada apenas na documentação: Este módulo fornece uma implementação do algoritmo de fila de heap, também conhecido como algoritmo de fila de prioridade.
  • Vá: heap package - há até uma interface de heap. Nenhuma fila de prioridade explícita, mas novamente apenas em documentos: um heap é uma maneira comum de implementar uma fila de prioridade.

Acredito fortemente que devemos seguir o caminho Rust / Swift / Python / Go e expor um heap explicitamente, ao mesmo tempo em que declaramos claramente na documentação que ele pode ser usado como uma fila de prioridade. Eu acredito fortemente que essa abordagem é muito limpa e simples.

  • Temos clareza sobre a estrutura de dados e deixamos a API ser aprimorada no futuro - se alguém vier com uma maneira nova e revolucionária de implementar uma fila de prioridades que seja melhor em alguns aspectos (e a escolha de heap versus o novo tipo dependeria de o cenário), nossa API ainda pode estar intacta. Poderíamos simplesmente adicionar o novo tipo revolucionário aprimorando a biblioteca - e a classe de heap ainda existiria lá.
  • Imagine um usuário se perguntando se a estrutura de dados que escolhemos é estável. Quando a solução que seguimos é uma fila prioritária , isso não é óbvio. O termo é abstrato e qualquer coisa pode estar abaixo. Assim, o usuário perde algum tempo para pesquisar os documentos e descobrir que ele usa um heap internamente e, como tal, não é estável. Isso poderia ter sido evitado apenas declarando explicitamente por meio da API que isso é um heap.

Espero que todos gostem da abordagem de apenas expor claramente uma estrutura de dados heap - e fazer uma referência de fila de prioridade apenas nos documentos.

API e implementação

Precisamos concordar sobre o assunto acima, mas deixe-me começar outro tópico - como implementar isso . Posso ver duas soluções aqui:

  • Acabamos de fornecer uma classe ArrayHeap . Vendo o nome, você pode dizer imediatamente com que tipo de heap está lidando (novamente, existem dezenas de tipos de heap). Você vê que é baseado em array. Você conhece imediatamente a besta com a qual está lidando.
  • Outra possibilidade da qual gosto muito mais é fornecer uma interface IHeap e uma ou mais implementações. O cliente pode escrever código que depende de interfaces - isso permitiria fornecer implementações realmente claras e legíveis de algoritmos complexos. Imagine escrever uma aula de DijkstraAlgorithm . Ele poderia simplesmente depender da interface IHeap (um construtor parametrizado) ou apenas usar o ArrayHeap (construtor padrão). Limpo, simples, explícito, sem ambigüidade envolvida devido ao uso de um termo de "fila de prioridade". E uma interface maravilhosa que faz muito sentido teoricamente.

Na abordagem acima, ArrayHeap representa uma árvore d-ária completa ordenada por heap implícita, armazenada como um array. Isso pode ser usado para criar, por exemplo, um BinaryHeap ou um QuaternaryHeap .

Resultado

Para uma discussão mais aprofundada, eu recomendo fortemente que você dê uma olhada neste artigo . Você saberá que:

  • Montes de 4 ários (quaternários) são simplesmente mais rápidos do que pilhas de 2 ários (binários). Existem muitos testes feitos no papel - você estaria interessado em comparar o desempenho de implicit_4 e implicit_2 (às vezes implicit_simple_4 e implicit_simple_2 ).

  • A escolha ideal de implementação depende fortemente dos insumos. De pilhas d-árias implícitas, pilhas de emparelhamento, pilhas de Fibonacci, pilhas binomiais, pilhas d-árias explícitas, pilhas de emparelhamento de classificação, pilhas de terremoto, pilhas de violação, pilhas de fraqueza relaxadas e pilhas de Fibonacci estritos, os seguintes tipos parecem cobrem quase todas as necessidades para vários cenários:

    • Montagens d-árias implícitas (ArrayHeap), <- certamente precisamos disso
    • Montes de emparelhamento, <- se adotarmos a abordagem IHeap agradável e limpa, valeria a pena adicionar isso, pois em muitos casos é muito rápido e mais rápido do que a solução baseada em array .

    Para ambos, o esforço de programação é surpreendentemente baixo. Dê uma olhada em minhas implementações.


@karelz @benaadams

Esse seria um tipo realmente útil de se ter no CoreFX. Alguém está interessado em pegar este?

@safern eu ficaria muito feliz em pegar este de agora em diante.

OK, era um problema entre o meu teclado e a minha cadeira - PriorityQueue baseia-se, claro, em Heap - estava a pensar apenas em Queue onde não faz sentido e esqueci que a pilha está 'classificada' - uma interrupção do processo de pensamento muito embaraçosa para alguém como eu, que adora lógica, algoritmos, máquinas de Turing, etc., minhas desculpas. (A propósito: assim que li algumas frases no seu link de documentos Java, a discrepância imediatamente apareceu)

Dessa perspectiva, faz sentido construir a API em cima de Heap . Não devemos tornar essa classe pública ainda - ela exigirá sua própria revisão de API e sua própria discussão se for algo de que precisamos no CoreFX. Não queremos o deslocamento da superfície da API devido à implementação, mas pode ser a coisa certa a fazer - daí a discussão necessária.
Dessa perspectiva, não acho que precisamos criar IHeap ainda. Pode ser uma boa decisão mais tarde.
Se houver pesquisas que demonstrem que heap específico (por exemplo, 4-ary, como você mencionou acima) é melhor para a entrada aleatória geral , devemos escolher essa. Vamos esperar que @safern @ianhays @stephentoub confirme /

A parametrização do heap subjacente com várias opções implementadas é algo que IMO não pertence ao CoreFX (posso estar errado aqui, novamente - vamos ver o que os outros pensam).
Minha razão é que a IMO em breve enviaríamos zilhões de coleções especializadas, o que seria muito difícil para as pessoas (desenvolvedor médio sem grande conhecimento em nuances de algoritmos) escolher. Essa biblioteca, no entanto, seria um ótimo pacote NuGet, para especialistas na área - de propriedade de você / da comunidade. No futuro, podemos considerar adicioná-la ao PowerCollections (estamos discutindo ativamente nos últimos 4 meses onde colocar esta biblioteca no GitHub e se devemos possuí-la, ou se devemos encorajar a comunidade a possuí-la - há opiniões diferentes sobre o assunto , Espero que finalizaremos seu destino após 2.0)

Atribuindo a você como você deseja trabalhar nisso ...

@pgolebiowski convite de colaborador enviado - quando você aceitar o ping, poderei atribuí-lo a você (limitações do GitHub).

@benaadams Eu manteria ICollection (preferência moderada). Para consistência com outros ds no CoreFX. IMO, não vale a pena ter um animal estranho aqui ... se estivéssemos adicionando um punhado de novos (por exemplo, PowerCollections mesmo para outro repo), não deveríamos incluir os não genéricos ... pensamentos?

OK, houve um problema entre meu teclado e minha cadeira.

Haha 😄 Não se preocupe.

faz sentido construir a API em cima do Heap. Não devemos tornar essa classe pública ainda, embora [...] Não queremos o aumento da superfície da API devido à implementação, mas pode ser a coisa certa a se fazer - daí a discussão necessária. [...] acho que não precisamos criar o IHeap ainda. Pode ser uma boa decisão mais tarde.

Se a decisão do grupo for PriorityQueue , vou apenas ajudar no design e na implementação. No entanto, leve em consideração o fato de que se adicionarmos PriorityQueue agora, será complicado adicionar Heap mais tarde na API - já que ambos se comportam basicamente da mesma forma. Seria uma espécie de redundância IMO. Isso seria um cheiro de design para mim. Eu não adicionaria a fila de prioridade. Isso não ajuda.

Além disso, mais um pensamento. Na verdade, a estrutura de dados do heap de emparelhamento pode ser útil com bastante frequência. Os heaps baseados em array são horríveis em mesclá-los. Esta operação é basicamente linear . Quando você tem muitos montes de mesclagem, está acabando com o desempenho. No entanto, se você usar um heap de emparelhamento em vez de um heap de array - a operação de fusão é constante (amortizada). Este é outro argumento pelo qual eu gostaria de fornecer uma boa interface e duas implementações. Um para a entrada geral, o segundo para alguns cenários específicos, especialmente quando a fusão de heaps está envolvida.

Votação para IHeap + ArrayHeap + PairingHeap ! 😄 (como em Rust / Swift / Python / Go)

Se a pilha de emparelhamento for muito grande - OK. Mas vamos pelo menos usar IHeap + ArrayHeap . Vocês não acham que usar uma classe PriorityQueue está bloqueando as possibilidades no futuro e tornando a API menos clara?

Mas como eu disse - se todos vocês votarem em uma classe PriorityQueue sobre a solução proposta - OK.

convite de colaborador enviado - quando você aceitar o ping, poderei atribuí-lo a você (limitações do GitHub).

@karelz ping :)

leve em consideração o fato de que, se adicionarmos um PriorityQueue agora, será complicado adicionar Heap na API mais tarde - já que ambos se comportam basicamente da mesma forma. Seria uma espécie de redundância IMO. Isso seria um cheiro de design para mim. Eu não adicionaria a fila de prioridade. Isso não ajuda.

Você pode explicar mais detalhes sobre por que ficará bagunçado mais tarde? Quais são suas preocupações?
PriorityQueue é o conceito que as pessoas usam. Ter um tipo com esse nome é útil, certo?
Acho que as operações lógicas (pelo menos seus nomes) em Heap podem ser diferentes. Se eles forem iguais, podemos ter 2 implementações diferentes do mesmo código no pior caso (não ideal, mas não o fim do mundo). Ou podemos inserir Heap class como pai de PriorityQueue , certo? (presumindo que seja permitido do ponto de vista de revisão de API - no momento não vejo razão para não, mas não tenho tantos anos de experiência com revisões de API, então vou esperar que outros confirmem)

Vamos ver como vão a votação e outras discussões sobre o design ... Estou lentamente começando a gostar da ideia de IHeap + ArrayHeap , mas ainda não estou totalmente convencido ...

se estivéssemos adicionando alguns novos ... não deveríamos incluir os não genéricos

Pano vermelho para um touro. Alguém tem outras coleções para adicionar para que possamos descartar ICollection ?

Buffer circular / anel; Genérico e simultâneo?

@karelz Uma solução para o problema de nomenclatura poderia ser algo como IPriorityQueue como o DataFlow faz para padrões de produção / consumo. Muitas maneiras de implementar uma fila de prioridade e se você não se importar com isso, use a interface. Se preocupam com a implementação ou estão criando uma classe de implementação de uso de instância.

Você pode explicar mais detalhes sobre por que ficará bagunçado mais tarde? Quais são suas preocupações?
PriorityQueue é o conceito que as pessoas usam. Ter um tipo com esse nome é útil, certo? […] Estou lentamente começando a gostar da ideia de IHeap + ArrayHeap , mas ainda não estou totalmente convencido ...

@karelz Pela minha experiência, acho muito importante ter uma abstração ( IPriorityQueue ou IHeap ). Graças a essa abordagem, um desenvolvedor pode escrever código desacoplado. Por ser escrito em uma interface (em vez de uma implementação específica), há mais flexibilidade e espírito de IoC. É muito fácil escrever testes de unidade para tal código (tendo injeção de dependência pode-se injetar seus próprios IPriorityQueue ou IHeap simulados e ver quais métodos são chamados em que horas e com quais argumentos). Abstrações são boas.

É verdade que o termo "fila de prioridade" é comumente usado. O problema é que só existe uma maneira de implementar uma fila de prioridades com eficiência - com um heap. Muitos tipos de montes. Então, poderíamos ter:

class ArrayHeap : IPriorityQueue {}
class PairingHeap : IPriorityQueue {}
class FibonacciHeap : IPriorityQueue {}
class BinomialHeap : IPriorityQueue {}

ou

class ArrayHeap : IHeap {}
class PairingHeap : IHeap {}
class FibonacciHeap : IHeap {}
class BinomialHeap : IHeap {}

Para mim, a segunda abordagem parece melhor. Como você se sente com isso?

Com relação à classe PriorityQueue - acho que seria muito vago e sou totalmente contra essa classe. Uma interface vaga é boa, mas não uma implementação. Se for um heap binário, basta chamá-lo de BinaryHeap . Se for outra coisa, nomeie-o de acordo.

Sou totalmente a favor de nomear classes claramente por causa dos problemas com classes como SortedDictionary . Há muita confusão entre os desenvolvedores sobre o que ele representa e como ele difere de SortedList . Se se chamasse apenas BinarySearchTree , a vida seria mais simples e poderíamos ir para casa e ver nossos filhos mais cedo.

Vamos nomear um heap um heap.

@benaadams cada um deles tem que ser transformado em CoreFX (ou seja, tem que ser valioso para o próprio código CoreFX, ou tem que ser tão básico e amplamente usado quanto os que já temos) - discutimos muito essas coisas ultimamente . Ainda não é 100% consenso, mas ninguém está ansioso para apenas adicionar mais coleções ao CoreFX.

O resultado mais provável (afirmação tendenciosa, porque gosto da solução) é que vamos criar outro repo e prepará-lo com PowerCollections e, em seguida, deixar a comunidade estendê-lo com alguma orientação / supervisão básica de nossa equipe.
BTW: @terrajobst acha que não vale a pena ("temos coisas melhores e mais impactantes para fazer no ecossistema") e devemos encorajar a comunidade a conduzi-lo totalmente (incluindo começar com PowerCollections existentes) e não torná-lo um de nossos repos - alguns embora discussões e decisões à nossa frente.
// Acho que há uma oportunidade para a comunidade adotar essa solução antes de nos decidirmos ;-). Isso tornaria a discussão (e minha preferência) muda ;-)

@pgolebiowski, você está me convencendo aos poucos de que ter Heap é melhor do que PriorityQueue - precisaríamos apenas de orientação e documentos fortes "É assim que você faz PriorityQueue - use Heap "... isso pode funcionar.

No entanto, estou muito hesitante em incluir mais de 1 implementação de heap no CoreFX. 98% + dos desenvolvedores C # 'normais' por aí não se importam. Eles nem mesmo querem pensar qual é o melhor, eles só precisam de algo que faça o trabalho. Nem todas as peças de software são projetadas com o alto desempenho em mente, com razão. Pense em todas as ferramentas únicas, aplicativos de interface do usuário, etc. A menos que você esteja projetando um sistema de escalonamento de alto desempenho onde este ds está no caminho crítico, você NÃO deve se preocupar.

Da mesma forma, não me importa como SortedDictionary ou ArrayList ou outros ds são implementados - eles fazem seu trabalho decentemente. Eu (como muitos outros) entendo que se eu precisar de alto desempenho desses ds para meus cenários , preciso medir o desempenho e / ou verificar a implementação e tenho que decidir se é bom o suficiente para meus cenários ou se preciso lançar minha própria implementação especial para obter o melhor desempenho, ajustado para minhas necessidades.

Devemos otimizar a usabilidade para 98% dos casos de uso, não para 2%. Se lançarmos muitas opções (implementações) e forçarmos todos a decidir, apenas causamos confusão desnecessária para 98% dos casos de uso. Acho que nao vale a pena ...
O ecossistema IMO .NET tem grande valor em oferecer uma escolha única de muitas APIs (não apenas coleções) com características de desempenho muito decentes, úteis para a maioria dos casos de uso. E oferece um ecossistema que permite extensões de alto desempenho para aqueles que precisam e estão dispostos a se aprofundar e aprender mais / fazer escolhas e compensações informadas.

Dito isso, ter uma interface IHeap (como IDictionary e IReadOnlyDictionary ) pode fazer sentido - tenho que pensar um pouco mais / perguntar aos especialistas em revisão de API no espaço ...

Nós já (até certo ponto) temos o que @pgolebiowski está falando com ISet<T> e HashSet<T> . Eu digo apenas espelhe. Portanto, a API acima é alterada para uma interface ( IPriorityQueue<T> ) e então temos uma implementação ( HeapPriorityQueue<T> ) que usa internamente um heap que pode ou não ser exposto publicamente como sua própria classe.

Deve ( PriorityQueue<T> ) também implementar IList<T> ?

@karelz meu problema com ICollection é SyncRoot e IsSynchronized ; eles são implementados, o que significa que há uma alocação extra para o objeto de bloqueio; ou eles jogam, quando é um pouco inútil tê-los.

@benaadams Isso seria enganoso. Visto que 99,99% das implementações de filas de prioridade são heaps baseados em matrizes (e como posso ver, faremos um aqui também), isso significaria expor o acesso à estrutura interna da matriz?

Digamos que tenhamos um heap com os elementos 4, 8, 10, 13, 30, 45. Considerando a ordem, eles seriam acessados ​​pelos índices 0, 1, 2, 3, 4, 5. Porém, a estrutura interna do heap é [4, 8, 30, 10, 13, 45] (em binário, em quaternário seria diferente).

  • Retornar um número interno no índice i realmente não faz sentido do ponto de vista do usuário, pois é quase arbitrário.
  • Retornar um número em ordem (por prioridade) é muito caro - isso é linear.
  • Retornar qualquer um desses não faz sentido em outras implementações. Freqüentemente, é apenas popping i elementos, obtendo o i -th elemento e, em seguida, empurrando-os novamente.

IList<T> normalmente é a solução atrevida para: quero ser flexível com as coleções que minha API aceita e quero enumerá-las, mas não quero alocar por meio de IEnumerable<T>

Acabei de perceber que não há interface genérica para

ICollection<T>
{
    int Count
    CopyTo(T[], int index)
}

Portanto, não se preocupe com isso 😢 (embora seja uma espécie de IReadOnlyCollection)

Mas Reset on Enumerator deve ser explicitamente implementado porque é ruim e deve apenas lançar.

Então, minhas mudanças sugeridas

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

Já que você está discutindo os métodos ... Ainda não concordamos com a IHeap vs IPriorityQueue thingy - ela afeta um pouco os nomes dos métodos e a lógica. No entanto, de qualquer maneira, acho que falta o seguinte na proposta de API atual:

  • Capacidade de remover um determinado elemento da fila
  • Capacidade de atualizar a prioridade de um elemento
  • Capacidade de mesclar essas estruturas

Essas operações são muito importantes, especialmente a possibilidade de atualizar um elemento. Sem isso, muitos algoritmos simplesmente não podem ser implementados. Precisamos apresentar um tipo de alça. Na API aqui , o identificador é IHeapNode . O que é outro argumento para seguir o caminho IHeap porque, caso contrário, teríamos que introduzir o tipo PriorityQueueHandle que sempre seria apenas um nó de heap ... 😜 Além disso, é apenas vago o que significa ... Considerando que, um nó de heap - todos conhecem as coisas e podem imaginar com que estão lidando.

Na verdade, falando resumidamente, para a proposta de API, por favor, dê uma olhada neste diretório . Provavelmente precisaríamos apenas de um subconjunto dele. Mas, no entanto - ele contém apenas o que precisamos da IMO, então pode valer a pena dar uma olhada nele como um ponto de partida.

Quais são seus pensamentos, pessoal?

IHeapNode não é muito diferente do tipo de clr KeyValuePair ?

No entanto, isso está separando a prioridade e o tipo, então agora é um PriorityQueue<TKey, TValue> com um IComparer<TKey> comparer ?

KeyValuePair não é apenas uma estrutura, mas suas propriedades são somente leitura. Basicamente, seria igual a criar um novo objeto toda vez que a estrutura for atualizada.

Usar apenas uma chave não funciona com chaves iguais - são necessárias mais informações para saber qual elemento atualizar / remover.

De IHeapNode e 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;
        }
    }

E o método de atualização em 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);
        }

Mas agora cada elemento no Heap é um objeto adicional; e se eu quiser um comportamento PriorityQueue, mas não quiser essas alocações extras?

Então você não seria capaz de atualizar / remover elementos. Seria uma implementação simples, não permitindo a implementação de nenhum algoritmo dependente da operação DecreaseKey (que é extremamente comum). Por exemplo, o algoritmo de Dijkstra, o algoritmo de Prim. Ou se você está escrevendo algum tipo de agendador e algum processo (ou qualquer outro) mudou sua prioridade, você não pode resolver isso.

Além disso, em todas as outras implementações de heap - os elementos são apenas nós, explicitamente. É perfeitamente natural em outros casos, aqui é um pouco artificial, mas necessário para atualização / remoção.

Em Java, a fila de prioridade não tem a opção de atualizar elementos. Resultado:

Quando implementei heaps para o projeto AlgoKit e me deparei com todos esses problemas de design, pensei que essa fosse a razão pela qual os autores do .NET decidiram não adicionar uma estrutura de dados tão básica como um heap (ou uma fila de prioridade). Porque nenhum dos dois designs é bom. Cada um tem suas deficiências.

Resumindo - se quisermos adicionar uma estrutura de dados que suporte a ordenação eficiente de elementos por sua prioridade, é melhor fazermos isso da maneira certa e adicionar uma funcionalidade como atualizar / remover elementos dela.

Se você se sente mal por agrupar elementos em uma matriz com outro objeto - este não é o único problema. Outra é a fusão. Com heaps baseados em array, isso é totalmente ineficiente. No entanto ... se usarmos a estrutura de dados do heap de emparelhamento ( O heap de emparelhamento: uma nova forma de heap de autoajuste ), então:

  • alças para os elementos são nós maravilhosos - eles devem ser alocados de qualquer maneira, então nada de bagunçado
  • a fusão é constante (vs linear em solução baseada em array)

Na verdade, poderíamos resolver isso da seguinte maneira:

  • Adicionar interface IHeap que suporte todos os métodos
  • Adicione ArrayHeap e PairingHeap com todas as alças, atualizando, removendo, mesclando
  • Adicione PriorityQueue que é apenas um invólucro em torno de ArrayHeap , simplificando a API

PriorityQueue estaria em System.Collections.Generic e todos os montes em System.Collections.Specialized .

Trabalho?

É bastante improvável que obtenhamos três novas estruturas de dados por meio da revisão da API. Na maioria dos casos, uma quantidade menor de API é melhor. Sempre podemos adicionar mais tarde se acabar sendo insuficiente, mas não podemos remover a API.

Esse é um dos motivos pelos quais não sou fã da classe HeapNode. Imo, esse tipo de coisa deve ser apenas interno e a API deve expor um tipo já existente, se possível - neste caso, provavelmente KVP.

@ianhays Se for mantido apenas interno, os usuários não poderão atualizar as prioridades dos elementos na estrutura de dados. Isso seria praticamente inútil e acabaríamos com todos os problemas do Java - pessoas reimplementando a estrutura de dados que já está na biblioteca nativa ... Parece ruim para mim. Muito pior do que ter uma classe simples representando um nó.

BTW: uma lista vinculada possui uma classe de nó para que os usuários possam usar a funcionalidade adequada. Isso é basicamente um espelhamento.

os usuários não seriam capazes de atualizar as prioridades dos elementos na estrutura de dados.

Isso não é necessariamente verdade. A prioridade pode ser exposta de uma forma que não exija uma estrutura de dados adicional de forma que, em vez de

        public override void Update(ArrayHeapNode<TKey, TValue> node, TKey key)
        {

        }

você teria, por exemplo

        public override void Update(TKey item, TValue priority)
        {

        }

Ainda não estou convencido de expor a prioridade como um parâmetro de tipo. Imo, a maioria das pessoas usará o tipo de prioridade padrão e o TValue será uma especificidade desnecessária.

void Update(TKey item, TValue priority)

Primeiro - o que é TKey e TValue em seu código? Como isso deveria funcionar? A convenção em ciência da computação é que:

  • chave = prioridade
  • valor = tudo o que você deseja armazenar em um elemento

Segundo:

Imo, a maioria das pessoas usará o tipo de prioridade padrão e o TValue será uma especificidade desnecessária.

Defina "o tipo de prioridade padrão". Posso sentir que você só quer PriorityQueue<T> , certo? Se for esse o caso, considere o fato de que um usuário provavelmente terá que criar uma nova classe, um invólucro em torno de sua prioridade e valor, além de implementar algo como IComparable ou fornecer um comparador personalizado. Muito pobre.

Primeiro - o que é TKey e TValue em seu código?

O item e a prioridade do item. Você pode alterá-los para serem a prioridade e o item associado a essa prioridade, mas então você tem uma coleção na qual as TKeys não são necessariamente exclusivas (ou seja, permitindo prioridades duplicadas). Não sou contra isso, mas para mim TKey geralmente implica em exclusividade.

A questão é que expor uma classe Node não é um requisito para expor um método Update.

Defina "o tipo de prioridade padrão". Eu posso sentir que você só quer ter PriorityQueue, Isso está certo?

sim. Apesar de ler este tópico novamente, não estou completamente convencido de que seja esse o caso.

Se for esse o caso, considere o fato de que um usuário provavelmente terá que criar uma nova classe, um invólucro em torno de sua prioridade e valor, além de implementar algo como IComparable ou fornecer um comparador personalizado. Muito pobre.

Você não está errado. Imagino que uma boa quantidade de usuários terá que criar um tipo de invólucro com lógica de comparação personalizada. Imagino que também haja uma boa quantidade de usuários que já têm um tipo comparável que desejam colocar na fila de prioridade ou um tipo com um comparador definido. A questão é qual campo é o maior dos dois.

Acho que o tipo deve ser chamado de PriorityQueue<T> , não Heap<T> , ArrayHeap<T> ou mesmo QuaternaryHeap<T> para permanecer consistente com o resto do .Net:

  • Temos List<T> , não ArrayList<T> .
  • Temos Dictionary<K, V> , não HashTable<K, V> .
  • Temos ImmutableList<T> , não ImmutableAVLTreeList<T> .
  • Temos Array.Sort() , não Array.IntroSort() .

Usuários de tais tipos geralmente não se importam como eles são implementados, certamente não deve ser a coisa mais proeminente sobre o tipo.

Se for esse o caso, considere o fato de que um usuário provavelmente terá que criar uma nova classe, um invólucro em torno de sua prioridade e valor, além de implementar algo como IComparable ou fornecer um comparador personalizado.

Você não está errado. Imagino que uma boa quantidade de usuários terá que criar um tipo de invólucro com lógica de comparação personalizada.

Na API de resumo, a comparação é fornecida pelo comparador fornecido IComparer<T> comparer , nenhum wrapper é necessário. Freqüentemente, a prioridade fará parte do tipo, por exemplo, um programador de tempo terá o tempo de execução como uma propriedade do tipo.

Usar um KeyValuePair com um IComparer não adiciona alocações extras; embora isso adicione uma indireção extra para uma atualização, pois os valores precisam ser comparados em vez do item.

As atualizações seriam problemáticas para itens de estrutura, a menos que o item fosse obtido por meio de um retorno ref e, em seguida, atualizado e passado de volta para um método de atualização por ref e refs fossem comparados. Mas isso é um pouco horrível.

@svick

Eu acho que o tipo deve ser chamado de PriorityQueue<T> , não Heap<T> , ArrayHeap<T> ou mesmo QuaternaryHeap<T> para ficar consistente com o resto do .Net.

Estou perdendo minha fé na humanidade.

  • Chamar uma estrutura de dados de PriorityQueue é ser consistente com o resto do .NET e chamá-la de Heap não é? Há algo de errado com os montes? O mesmo deve se aplicar a pilhas e filas.
  • ArrayHeap -> classe base para QuaternaryHeap . Enorme diferença.
  • A discussão não é uma questão de escolher um nome legal. Muitas coisas acontecem como consequência de seguir um determinado caminho de design. Releia o tópico, por favor.
  • Hashtable , ArrayList . Eles parecem existir. BTW, uma "lista" é uma escolha muito ruim de nomear IMO, já que o primeiro pensamento de um usuário é que List é uma lista, mas não é uma lista. 😜
  • Não se trata de se divertir e dizer como uma coisa é implementada. Trata-se de dar nomes significativos que mostrem aos usuários imediatamente com o que estão lidando.

Quer ficar consistente com o resto do .NET e ter problemas como este?

E o que eu vejo lá ... Jon Skeet dizendo que SortedDictionary deve ser chamado de SortedTree porque isso reflete a implementação mais de perto.

@pgolebiowski

Há algo de errado com os montes?

Sim, ele descreve a implementação, não o uso.

O mesmo deve se aplicar a pilhas e filas.

Nenhum dos dois realmente descreve a implementação, por exemplo, o nome não indica que eles são baseados em array.

ArrayHeap -> classe base para QuaternaryHeap . Enorme diferença.

Sim, eu entendo seu projeto. O que estou dizendo é que nada disso deve ser exposto ao usuário.

Muitas coisas acontecem como consequência de seguir um determinado caminho de design. Releia o tópico, por favor.

Eu li o tópico. Não creio que esse design pertença ao BCL. Acho que deve conter algo que seja simples de usar e entender, não algo que faça com que as pessoas se perguntem o que é "heap quaternário" ou se devem usá-lo.

Se a implementação padrão não for boa o suficiente para eles, é aí que entram outras bibliotecas (como a sua).

Hashtable, ArrayList. Eles parecem existir.

Sim, essas são classes .Net Framework 1.0 que ninguém mais usa. Pelo que eu posso dizer, seus nomes foram copiados de Java e os designers do .Net Framework 2.0 decidiram não seguir essa convenção. Na minha opinião, essa foi a decisão certa.

BTW, uma "lista" é uma escolha muito ruim de nomear IMO, já que o primeiro pensamento de um usuário é que List é uma lista, mas não é uma lista.

Isto é. Não é uma lista vinculada, mas não é a mesma coisa. E eu gosto de não ter que escrever ArrayList ou ResizeArray (nome de F # para List<T> ) em todos os lugares.

Trata-se de dar nomes significativos que mostrem aos usuários imediatamente com o que estão lidando.

A maioria das pessoas não terá idéia do que está lidando se vir QuaternaryHeap . Por outro lado, se eles virem PriorityQueue , deve ficar claro com o que estão lidando, mesmo que não tenham nenhum histórico de CS. Eles não saberão o que é a implementação, mas é para isso que serve a documentação.

@ianhays

Primeiro - o que é TKey e TValue em seu código?

O item e a prioridade do item. Você pode alterá-los para serem a prioridade e o item associado a essa prioridade, mas então você tem uma coleção na qual as TKeys não são necessariamente exclusivas (ou seja, permitindo prioridades duplicadas). Não sou contra isso, mas para mim TKey geralmente implica em exclusividade.

Chaves - coisinhas usadas para fazer a priorização. As chaves não precisam ser exclusivas. Não podemos fazer tal suposição.

A questão é que expor uma classe Node não é um requisito para expor um método Update.

Acredito que seja: Suporte para chave de redução / chave de aumento em bibliotecas nativas . Também neste tópico, portanto, há classes auxiliares envolvidas para lidar com estruturas de dados:

Imagino que uma boa quantidade de usuários terá que criar um tipo de invólucro com lógica de comparação personalizada. Imagino que também haja uma boa quantidade de usuários que já têm um tipo comparável que desejam colocar na fila de prioridade ou um tipo com um comparador definido. A questão é qual campo é o maior dos dois.

Não sei sobre os outros, mas acho horrível criar um wrapper com lógica comparadora personalizada toda vez que quero usar uma fila de prioridade ...

Outro pensamento - digamos que criei um wrapper:

class Wrapper
{
    public int Priority { get; set; }
    public string SomeStuff ...
}

Eu alimento PriorityQueue<T> com este tipo. Portanto, ele prioriza seus elementos com base nele. Digamos que tenha definido algum seletor de chave. Tal design pode travar o trabalho interno de um heap. Você pode alterar as prioridades a qualquer momento, pois você é o proprietário do elemento. Não há mecanismo de notificação para atualização do heap. Você seria responsável por lidar com tudo isso e chamá-lo. Muito perigoso e não direto para mim.

No caso do identificador que propus - o tipo era imutável do ponto de vista do usuário. E não houve problemas com exclusividade.

IMO Eu gosto da ideia de ter uma interface IHeap e, em seguida, a API de classe implementando essa interface. Estou com @ianhays 'sobre isso que não vamos expor 3 novas apis para fornecer diferentes tipos de pilhas aos clientes, pois seria necessário um processo de revisão de API e gostaríamos de nos concentrar em PriorityQueue não importa qual seja o nome.

Em segundo lugar, acho que não há necessidade de ter uma classe Node interna / pública para armazenar os valores na fila. Não há necessidade de alocações extras, poderíamos seguir algo como o que IDictionary faz e ao produzir os valores para Enumerable criando KeyValuePairobjetos se optarmos pela opção de ter uma prioridade para cada item que armazenamos (o que não acho que seja a melhor maneira de fazer isso, não estou totalmente convencido de armazenar uma prioridade por item). A melhor abordagem que eu gostaria seria ter PriorityQueue<T> (este nome é apenas para lhe dar um). Com essa abordagem, poderíamos ter um construtor seguindo o que todo o BCL faz com um IComparer<T> e fazendo a comparação com esse comprador para dar a prioridade. Não há necessidade de expor APIs que passam uma prioridade como parâmetro.

Acho que ter algumas APIs recebendo uma prioridade tornaria "menos utilizável" ou mais complicado para clientes comuns que gostariam de ser a prioridade padrão, então estamos tornando mais complicado entender o uso disso, ao dar-lhes o a opção de ter um IComparer<T> será a mais razoável e seguirá as diretrizes que temos em toda a BCL.

Os nomes são o que eles fazem, não como o fazem.

Eles são nomeados de acordo com os conceitos abstratos e o que eles alcançam para o usuário, ao invés de sua implementação e como eles o conseguem. (O que também significa que sua implementação pode ser melhorada para usar uma implementação diferente, se for melhor)

O mesmo deve se aplicar a pilhas e filas.

Stack é um array de redimensionamento ilimitado e Queue é implementado como um buffer circular de redimensionamento ilimitado. Eles são nomeados de acordo com seus conceitos abstratos. Pilha (tipo de dados abstratos) : Último a entrar, primeiro a sair (LIFO), Fila (tipo de dados abstratos) : Primeiro a entrar, primeiro a sair (FIFO)

Hashtable, ArrayList. Eles parecem existir.

Entrada do dicionário

Sim, mas vamos fingir que não; são artefatos de uma época menos civilizada. Eles não têm segurança de tipo e caixa se você usar primitivas ou structs; então aloque em cada Add.

Você pode usar o Platform Compatibility Analyzer do @terrajobst e ele dirá: "Por favor, não"

Lista é uma lista, mas não é uma lista

Na verdade, é uma Lista (tipo de dado abstrato) também conhecido como Sequência.

Igualmente Priority Queue é um tipo abstrato que informa o que ela consegue em uso; não como ele faz na implementação (pois pode haver muitas implementações diferentes)

Muita boa discussão!

A especificação original foi projetada com alguns princípios básicos em mente:

  • Finalidade geral - cobre a maioria dos casos de uso com um equilíbrio entre o consumo de CPU e memória.
  • Alinhe, tanto quanto possível, com os padrões e convenções existentes em System.Collections.Generic.
  • Permite várias entradas do mesmo elemento.
  • Utilize as interfaces e classes de comparação BCL existentes (ou seja, Comparer). *
  • Alto nível de abstração - A especificação é projetada para proteger os consumidores da tecnologia de implementação subjacente.

@karelz @pgolebiowski
Renomear para "Heap" ou outro termo alinhado a uma implementação de estrutura de dados não corresponderia à maioria das convenções BCL para coleções. Historicamente, as classes de coleção .NET foram projetadas para propósitos gerais, em vez de focadas na estrutura / padrão de dados específicos. Meu pensamento original era que no início do ecossistema .NET, os designers de API intencionalmente mudaram de "ArrayList" para "List". A mudança foi provavelmente devido a uma confusão com um Array - o desenvolvedor médio teria pensado" ArrayList? Eu só quero uma lista, não um array ".

Se formos com Heap, o mesmo pode ocorrer - muitos desenvolvedores com habilidades intermediárias irão (infelizmente) ver "Heap" e confundi-lo com o heap de memória do aplicativo (isto é, heap e pilha) em vez de "heap a estrutura de dados generalizada". A prevalência de System.Collections.Generic fará com que ele apareça em quase todas as propostas inteligentes de desenvolvedores .NET, e eles se perguntarão por que podem alocar um novo heap de memória :)

PriorityQueue, em comparação, é muito mais detectável e menos sujeito a confusão. Você pode digitar "Fila" e obter propostas para PriorityQueue.

Algumas propostas e perguntas sobre prioridades inteiras ou um parâmetro genérico para prioridade (TKey, TPriority, etc). Adicionar uma prioridade explícita exigiria que os consumidores escrevessem sua própria lógica para mapear suas prioridades e aumentar a complexidade da API. Usando o IComparer integradoaproveita a funcionalidade BCL existente, e também considerei adicionar sobrecargas ao Comparerna especificação para tornar mais fácil fornecer expressões lambda ad-hoc / funções anônimas como comparações de prioridade. Essa não é uma convenção comum na BCL, infelizmente.

Se as entradas precisassem ser exclusivas, Enqueue () exigiria uma pesquisa de exclusividade para lançar uma ArgumentException. Além disso, existem provavelmente cenários válidos que permitem que um item seja colocado na fila mais de uma vez. Esse design de não exclusividade torna o fornecimento de uma operação Update () um desafio, pois não haveria como saber qual objeto está sendo atualizado. Como alguns comentários indicaram, isso começaria a entrar em APIs que retornam referências de "nó" que, por sua vez, (provavelmente) exigiriam alocações que precisariam ser coletadas como lixo. Mesmo se isso fosse contornado, aumentaria o consumo de memória por elemento da fila de prioridade.

Em um ponto, eu tinha uma interface IPriorityQueue personalizada na API antes de postar a especificação. No final das contas, decidi não fazer isso - o padrão de uso que eu queria era Enqueue, Dequeue e Iterate. Já coberto pelo conjunto de interface existente. Pensando nisso como uma Fila que é classificada internamente; contanto que os itens mantenham sua própria posição (inicial) na fila com base em seu IComparer, o chamador nunca precisa se preocupar com a forma como a prioridade é representada. Na implementação de referência antiga que eu fiz, (se bem me lembro!) A prioridade não é representada de forma alguma. É tudo relativo com base no IComparerou comparação.

Devo a todos vocês alguns exemplos de código de cliente - meu plano original era passar pelas implementações BCL existentes de PriorityQueue para usar como base para os exemplos.

Na API de resumo, a comparação é fornecida pelo comparador IComparer fornecidocomparador, nenhum invólucro é necessário. Freqüentemente, a prioridade fará parte do tipo, por exemplo, um programador de tempo terá o tempo de execução como uma propriedade do tipo.

De acordo com a API OP proposta, um destes precisa ser atendido para usar a classe:

  • Tenha um tipo que já seja comparável da maneira que você gostaria que fosse
  • Envolva um tipo com outra classe que contenha o valor de prioridade e compare da maneira que você deseja
  • Crie um IComparer personalizado para o seu tipo e passe-o por meio do construtor. Presume que o valor que você deseja representar a prioridade já está exposto publicamente por seu tipo.

A API de tipo duplo visa diminuir o fardo das duas segundas opções, certo? Como funciona a construção de um novo PriorityQueue / Heap quando você tem um tipo que já contém a prioridade ? Qual é a aparência da API?

Acredito que seja: Suporte para chave de redução / chave de aumento em bibliotecas nativas.

public override void Update(ArrayHeapNode<TKey, TValue> node, TKey key) {}
public override void Update(TKey key, TValue value);

Em qualquer um dos casos, o item associado à prioridade dada é atualizado. Por que o ArrayHeapNode é necessário para atualização? O que ele realiza que não pode ser realizado tomando o TKey / TValue diretamente?

@ianhays

Em qualquer um dos casos, o item associado à prioridade dada é atualizado. Por que o ArrayHeapNode é necessário para atualização? O que ele realiza que não pode ser realizado tomando o TKey / TValue diretamente?

Pelo menos com heap binário (aquele com o qual estou mais familiarizado), se você deseja atualizar a prioridade de algum valor e sabe sua posição no heap, pode fazer isso rapidamente (em tempo O (log n)).

Mas se você souber apenas o valor e a prioridade, primeiro precisará encontrar o valor, que é lento (O (n)).

Por outro lado, se você não precisar atualizar com eficiência, essa alocação por item no heap é pura sobrecarga.

Gostaria de ver uma solução em que você pague essa sobrecarga apenas quando precisar, mas isso pode não ser possível implementar e a API resultante pode não parecer boa.

A API de tipo duplo visa diminuir o fardo das duas segundas opções, certo? Como funciona a construção de um novo PriorityQueue / Heap quando você tem um tipo que já contém a prioridade? Qual é a aparência da API?

Esse é um bom ponto, há uma lacuna na API original para cenários em que o consumidor já tem um valor de prioridade (ou seja, inteiro) mantido separadamente do valor. O outro lado é que umestilo de API complicaria todos os tipos intrinsecamente comparáveis. Anos atrás, escrevi um componente agendador muito leve que tinha sua própria classe interna ScheduledTask. Cada ScheduledTask implementou IComparer. Jogue-os em uma fila prioritária, pronto.

SortedListusa um design de chave / valor e, como resultado, acabei evitando usá-lo. Exige que as chaves sejam exclusivas e meu requisito usual é "Tenho N valores que preciso manter classificados em uma lista e garantir que os valores ocasionais adicionados permaneçam classificados". Uma "chave" não faz parte dessa equação, uma lista não é um dicionário.

Para mim, o mesmo princípio se aplica às filas. Uma fila, historicamente, é uma estrutura de dados unidimensional.

Meu conhecimento moderno da biblioteca std C ++ está um pouco enferrujado, mas mesmo std :: priority_queue parece ter push, pop e take a comparer como um parâmetro modelado (genérico). A equipe da biblioteca padrão C ++ é tão sensível ao desempenho quanto você pode imaginar :)

Eu fiz uma varredura muito rápida da fila de prioridade e implementações de heap em algumas linguagens de programação agora - C ++, Java, Rust e Go, todos funcionam com um único tipo (semelhante à API original postada aqui). Uma rápida olhada nas implementações mais populares de heap / fila de prioridade no NPM mostra o mesmo.

@pgolebiowski não me entenda mal, deve haver implementações específicas de coisas nomeadas explicitamente após sua implementação específica.

No entanto, isso é para quando você sabe qual estrutura de dados específica deseja que corresponda às metas de desempenho que você busca e tenha as compensações que você está disposto a aceitar.

Geralmente, as coleções de frameworks cobrem 90% dos usos em que você deseja um comportamento geral. Então, se você quiser um comportamento ou implementação muito específico, provavelmente irá para uma biblioteca de terceiros; e esperamos que eles recebam o nome da implementação para que você saiba que ela atende às suas necessidades.

Só não quero amarrar os tipos de comportamento geral a uma implementação específica; pois então é estranho se a implementação muda porque o nome do tipo tem que permanecer o mesmo e eles não irão combinar.

Há muitas preocupações que precisam ser discutidas, mas vamos começar com aquela que atualmente tem mais impacto nas partes sobre as quais discordamos: atualizar e remover elementos .

Como você deseja apoiar essas operações, então? Temos que incluí-los, eles são básicos. Em Java, os designers os omitiram e, como resultado:

  1. Há muitas perguntas nos fóruns sobre como fazer soluções alternativas devido à falta de recursos.
  2. Existem implementações de terceiros de uma fila de heap / prioridade para substituir a implementação original, porque é praticamente inútil.

Isso é simplesmente patético. Há alguém que realmente queira seguir esse caminho? Eu teria vergonha de lançar uma estrutura de dados desabilitada.

@pgolebiowski tenha certeza de que todos aqui têm as melhores intenções para a plataforma. Ninguém quer enviar APIs quebradas. Queremos aprender com os erros dos outros, portanto, continue trazendo informações úteis e relevantes (como aquela sobre a história do Java).

No entanto, quero destacar algumas coisas:

  • Não espere mudanças durante a noite. Esta é uma discussão sobre design. Precisamos encontrar um consenso. Não temos pressa em APIs. Todas as opiniões devem ser ouvidas e consideradas, mas não há garantia de que a opinião de todos será implementada / aceita. Se você tiver contribuições, forneça-as, apoie-as com dados e evidências. Também ouça os argumentos dos outros, reconheça-os. Forneça evidências contra a opinião dos outros se você discordar. Às vezes, concorde no fato de que há desacordo em alguns pontos. Lembre-se de que SW, incl. O design da API não é uma coisa preta ou branca / certa ou errada.
  • Vamos manter a discussão civilizada. Não vamos usar palavras e declarações fortes. Vamos discordar com graça e vamos manter a discussão técnica. Todos podem consultar também o código de conduta do colaborador . Reconhecemos e encorajamos a paixão pelo .NET, mas vamos ter certeza de não ofender uns aos outros.
  • Se você tiver quaisquer preocupações / perguntas sobre a velocidade da discussão do design, reações, etc., sinta-se à vontade para entrar em contato comigo diretamente (meu e-mail está no meu perfil GH). Posso ajudar a esclarecer expectativas, suposições e preocupações publicamente ou offline, se necessário.

Eu só perguntei como vocês querem projetar a atualização / remoção se vocês não gostam da abordagem proposta ... Isso é ouvir os outros e buscar consenso, eu acredito.

Não duvido de suas boas intenções! Às vezes, é importante como você pergunta - afeta a forma como as pessoas percebem o texto do outro lado. O texto está livre de emoções, então as coisas podem ser entendidas de forma diferente quando escritas. O inglês como segunda língua confunde ainda mais as coisas e todos nós precisamos estar cientes disso. Ficarei feliz em conversar sobre os detalhes off-line se você estiver interessado ... vamos direcionar a discussão aqui de volta para a discussão técnica ...

Meus dois centavos no debate Heap vs. PriorityQueue: ambas as abordagens são válidas e claramente têm vantagens e desvantagens.

Dito isso, "PriorityQueue" parece muito mais consistente com a abordagem .NET existente. Hoje, as coleções principais são Lista, Dicionário, Pilha, Fila, HashSet, SortedDictionary, e SortedSet. Eles são nomeados de acordo com a funcionalidade e semântica, não o algoritmo. HashSeté o único valor atípico, mas mesmo isso pode ser racionalizado como relacionado à semântica de igualdade do conjunto (em comparação com SortedSet) Afinal, agora temos ImmutableHashSetque se baseia em uma árvore sob o capô.

Seria estranho para uma coleção contrariar a tendência aqui.

Acho que PriorityQueue com construtor adicional: PriorityQueue(IHeap) pode ser uma solução. Os construtores sem o parâmetro IHeap podem usar o Heap padrão.
Nesse caso, PrioriryQueuerepresentará o tipo de dados abstratos (como a maioria das coleções C #) e implementará IPriorityQueueinterface, mas pode usar diferentes implementações de heap como @pgolebiowski sugerido:

classe ArrayHeap: IHeap {}
classe PairingHeap: IHeap {}
classe FibonacciHeap: IHeap {}
classe BinomialHeap: IHeap {}

OK. Existem muitas vozes diferentes. Repassei a discussão e refinei minha abordagem. Eu também trato de preocupações comuns. O texto abaixo faz uso de citações das postagens acima.

Nosso objetivo

O objetivo final de toda essa discussão é fornecer um conjunto específico de funcionalidades para deixar o usuário feliz. É um caso muito comum que um usuário tenha vários elementos, onde alguns deles têm uma prioridade mais alta do que outros. Eventualmente, eles querem manter esse grupo de elementos em alguma ordem específica para poder executar com eficiência as seguintes operações:

  • Recupere o elemento com a prioridade mais alta (e seja capaz de removê-lo).
  • Adicione um novo elemento à coleção.
  • Remova um elemento da coleção.
  • Modifique um elemento da coleção.
  • Mescle duas coleções.

Outras bibliotecas padrão

Os autores de outras bibliotecas padrão também tentaram oferecer suporte a essa funcionalidade. Nesta seção, vou me referir a como isso foi resolvido em Python , Java , C ++ , Go , Swift e Rust .

Nenhum deles suporta a modificação de elementos já inseridos na coleção. É extremamente surpreendente, porque é muito provável que, durante a vida de uma coleção, as prioridades de seus elementos mudem. Especialmente se essa coleção se destinar a ser usada durante toda a vida útil de um serviço. Existem também vários algoritmos que utilizam essa mesma funcionalidade de atualização de elementos. Esse design é simplesmente errado, porque, como resultado, nossos clientes estão perdidos: StackOverflow . Essa é uma das muitas perguntas como essa na Internet. Os designers falharam.

Outra coisa que vale a pena notar é que cada biblioteca fornece essa funcionalidade parcial por meio da implementação de um heap binário . Bem, foi demonstrado que não é a implementação de melhor desempenho no caso geral (entrada aleatória). O melhor ajuste para o caso geral é um heap quaternário (uma árvore 4-ária completa ordenada por heap implícita, armazenada como um array). É significativamente menos conhecido e esta é provavelmente a razão pela qual os designers optaram por um heap binário. Mas ainda assim - outra escolha ruim, embora de menor gravidade.

O que aprendemos com isso?

  • Se queremos que nossos clientes fiquem satisfeitos e evitem que eles próprios implementem essa funcionalidade, ao mesmo tempo que ignoram nossa maravilhosa estrutura de dados, devemos fornecer um suporte para modificar os elementos já inseridos na coleção.
  • Não devemos presumir que, devido ao

Abordagem proposta

Acredito fortemente que devemos fornecer:

  • Interface de IHeap<T>
  • Heap<T> classe

A interface IHeap obviamente incluiria métodos para realizar todas as operações descritas no início deste post. A classe Heap , implementada com um heap quaternário, seria a solução ideal em 98% dos casos. A ordem dos elementos seria baseada em IComparer<T> passados ​​para o construtor ou na ordem padrão se um tipo já for comparável.

Justificação

  • Os desenvolvedores podem escrever sua lógica em uma interface. Presumo que todos saibam o quanto isso é importante e não entraremos em detalhes. Leia: princípio de inversão de dependência , injeção de dependência , projeto por contrato , inversão de controle .
  • Os desenvolvedores podem estender essa funcionalidade para atender às suas necessidades personalizadas, fornecendo outras implementações de heap. Essas implementações podem ser incluídas em bibliotecas de terceiros, como PowerCollections . Apenas referenciando essa biblioteca, você poderia simplesmente injetar seu heap personalizado em qualquer lógica que receba IHeap como entrada. Alguns exemplos de outros heaps que se comportam melhor do que o heap quaternário em algumas condições específicas são: heap de emparelhamento , heap binomial e nosso amado heap binário.
  • Se um desenvolvedor precisar apenas de uma ferramenta que execute o trabalho sem que ele precise pensar sobre qual tipo é o melhor, ele pode simplesmente usar a implementação Heap uso geral. Isso é otimizado para 98% dos casos de uso.
  • Acrescentamos ao grande valor do ecossistema .NET no que diz respeito a oferecer uma escolha única com características de desempenho muito decentes, úteis para a maioria dos casos de uso, ao mesmo tempo permitindo extensões de alto desempenho para aqueles que precisam e estão dispostos a explorar aprofundar e aprender mais / fazer escolhas e compensações informadas.
  • A abordagem proposta reflete as convenções atuais:

    • ISet e HashSet

    • IList e List

    • IDictionary e Dictionary

  • Algumas pessoas afirmaram que devemos nomear as classes com base no que suas instâncias fazem, não como o fazem. Isso não é inteiramente verdade. É um atalho comum dizer que devemos nomear classes de acordo com seu comportamento. Na verdade, se aplica em vários casos. No entanto, há casos em que essa não é a abordagem apropriada. Os exemplos mais notáveis ​​são blocos de construção básicos - como tipos primitivos, tipos enum ou estruturas de dados. O princípio é simplesmente escolher nomes que sejam significativos (ou seja, inequívocos para o usuário). Leve em consideração o fato de que a funcionalidade que estamos discutindo é sempre fornecida como um heap - seja Python, Java, C ++, Go, Swift ou Rust. Heap é uma das estruturas de dados mais elementares. Heap é de fato inequívoco e claro. Também está em harmonia com Stack , Queue , List e Array . A mesma abordagem com a nomenclatura foi adotada nas bibliotecas padrão mais modernas (Go, Swift, Rust) - elas expõem um heap explicitamente.

@pgolebiowski Não estou vendo como Heap<T> / IHeap<T> é nomeado como Stack<T> , Queue<T> e / ou List<T> ? Nenhum desses nomes explica como eles são implementados internamente (um array de T conforme acontece).

@SamuelEnglard

Heap também não diz como é implementado internamente. Não consigo entender por que, para tantas pessoas, uma pilha segue imediatamente uma implementação específica. Existem muitas variantes de heaps que compartilham a mesma API, para começar:

  • montes d-ários,
  • 2-3 pilhas,
  • montes de esquerdistas,
  • pilhas macias,
  • pilhas fracas,
  • B-heaps,
  • pilhas radix,
  • skew heaps,
  • montes de emparelhamento,
  • Pilhas de Fibonacci,
  • pilhas binomiais,
  • montes de emparelhamento de classificação,
  • montes de terremotos,
  • pilhas de violação.

Dizer que estamos lidando com uma pilha ainda é muito abstrato. Na verdade, mesmo dizer que estamos lidando com um heap quaternário é abstrato - pode ser implementado como uma estrutura de dados implícita baseada em um array de T (como nosso Stack<T> , Queue<T> e List<T> ) ou explícito (usando nós e ponteiros).

Resumindo, Heap<T> é muito semelhante a Stack<T> , Queue<T> e List<T> , porque é uma estrutura de dados elementar, um bloco de construção abstrato básico, que pode ser implementado de várias maneiras. Além disso, por acaso, todos eles são implementados usando uma matriz de T abaixo. Acho essa semelhança muito forte.

Isso faz sentido?

Só para constar, sou indiferente quanto à nomenclatura. Pessoas que estão acostumadas a usar a biblioteca padrão C ++ irão, talvez, preferir _priority_queue_. Pessoas que foram informadas sobre estruturas de dados podem preferir _Heap_. Se eu tivesse que votar, escolheria _heap_, embora seja quase um lance de moeda para mim.

@pgolebiowski Eu Heap<T> não diz como é implementado internamente.

Sim Heap é uma estrutura de dados válida, mas Heap! = Fila de prioridade. Ambos expõem superfícies de API diferentes e são usados ​​para ideias diferentes. Heap<T> / IHeap<T> devem ser tipos de dados usados ​​internamente por (apenas nomes teóricos) PriorityQueue<T> / IPriorityQueue<T> .

@SamuelEnglard
Em termos de como o mundo da Ciência da Computação está organizado, sim. Aqui estão os níveis de abstração:

  • implementação : heap quaternário implícito com base em uma matriz
  • abstração : heap quaternário
  • abstração : família de pilhas
  • abstração : família de filas prioritárias

E sim, tendo IHeap e Heap , a implementação de PriorityQueue seria basicamente:

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

Vamos fazer uma árvore de decisão aqui.

Uma fila de prioridade também pode ser implementada com uma estrutura de dados diferente de alguma forma de heap (teoricamente). Isso torna o design de um PriorityQueue como o acima bastante feio, porque ele é voltado apenas para a família de pilhas. É também um invólucro muito fino em torno de IHeap . Isso gera uma pergunta - _por que não usar simplesmente a família de heaps em seu lugar_?

Ficamos com uma solução - fixar a fila de prioridade para uma implementação específica de um heap quaternário, sem espaço para a interface IHeap . Eu sinto que está passando por muitos níveis de abstração e mata todos os benefícios maravilhosos de ter uma interface.

Estamos de volta com a escolha do design do meio da discussão - temos PriorityQueue e IPriorityQueue . Mas então teríamos basicamente:

class BinaryHeap : IPriorityQueue {}
class PairingHeap : IPriorityQueue {}
class FibonacciHeap : IPriorityQueue {}
class BinomialHeap : IPriorityQueue {}

Parece não apenas feio, mas também conceitualmente errado - esses não são tipos de filas prioritárias e não compartilham a mesma API (como @SamuelEnglard já observou). Acho que devemos nos limitar a pilhas, uma família grande o suficiente para ter uma abstração para eles. Nós obteríamos:

class BinaryHeap : IHeap {}
class PairingHeap : IHeap {}
class FibonacciHeap : IHeap {}
class BinomialHeap : IHeap {}

E, fornecido por nós, class Heap : IHeap {} .


BTW, alguém pode achar o seguinte útil:

| Consulta do Google | Resultados
| : --------------------------------------: | : -----: |
| "estrutura de dados" "fila prioritária" | 172.000 |
| "estrutura de dados" "heap" | 430.000 |
| "estrutura de dados" "fila" - "fila prioritária" | 496.000 |
| "estrutura de dados" "fila" | 530.000 |
| "estrutura de dados" "pilha" | 577.000 |

@pgolebiowski sinto que estou indo e voltando aqui, então vou admitir

@karelz @safern O que você acha disso? Podemos nos fixar na abordagem IHeap + Heap para que eu possa apresentar uma proposta de API específica?

Eu questiono a necessidade de uma interface aqui (seja IHeap ou IPriorityQueue ). Sim, existem muitos algoritmos diferentes que podem, teoricamente, ser usados ​​para implementar essa estrutura de dados. No entanto, parece improvável que a estrutura seja enviada com mais de um, e a ideia de que os escritores de bibliotecas realmente precisam de uma interface canônica para que várias partes possam se coordenar na gravação de implementações de heap compatíveis entre si não parece muito provável.

Além disso, uma vez que uma interface é lançada, ela nunca pode ser alterada devido à compatibilidade. Isso significa que a interface tende a ficar para trás das classes concretas em termos de funcionalidade (um problema com IList e IDictionary hoje). Em contraste, se uma classe Heap fosse lançada e houvesse um forte clamor por uma interface IHeap, acho que a interface poderia ser adicionada posteriormente sem problemas.

@madelson : Concordo que a estrutura provavelmente não precisa enviar mais do que uma única implementação de um heap. No entanto, ao apoiar essa implementação por uma interface, aqueles de nós que se preocupam com outras implementações podem facilmente trocar em outra implementação (de nossa própria criação ou em uma biblioteca externa) e ainda ser compatível com o código que aceita a interface.

Eu realmente não vejo nenhum ponto negativo na codificação para uma interface. Aqueles que não se importam com isso podem ignorá-lo e ir diretamente para a implementação concreta. Aqueles que se importam podem usar qualquer implementação de sua escolha. É uma escolha. E é uma escolha que eu quero.

@pgolebiowski agora que você perguntou, aqui está minha opinião pessoal (tenho certeza de que outros revisores / arquitetos de API irão compartilhá-la, pois verifiquei a opinião de alguns):
O nome deve ser PriorityQueue e não devemos introduzir a interface IHeap . Deve haver exatamente uma implementação (provavelmente por meio de algum heap).
IHeap interface é um cenário de especialista muito avançado - eu sugeriria movê-lo para a biblioteca PowerCollections (a ser criada eventualmente) ou qualquer outra biblioteca.
Se a biblioteca e a interface IHeap se tornarem realmente populares, podemos mudar de ideia mais tarde e adicionar IHeap base na demanda (por meio de sua sobrecarga de construtor), mas não acho que seja útil / alinhado com o resto do BCL o suficiente para justificar a complicação de adicionar uma nova interface agora. Comece simples e torne-se complicado apenas se for realmente necessário.
... apenas meus 2 centavos (pessoais)

Dada a diferença de opiniões, proponho a seguinte abordagem para levar a proposta adiante (que sugeri no início desta semana para @ianhays e, portanto, indiretamente para @safern):

  • Faça 2 propostas alternativas - uma simples como descrevi acima e uma com os Heaps como você propôs, vamos trazê-la para a revisão da API, discuti-la lá e tomar uma decisão lá.
  • Se eu vir pelo menos uma pessoa naquele grupo votando na proposta de Heaps, ficarei feliz em reconsiderar meu ponto de vista.

... tentando ser totalmente transparente sobre minha opinião (que você pediu), por favor, não tome isso como desânimo ou recue - vamos ver como vai a proposta da API.

@karelz

O nome deve ser PriorityQueue

Algum argumento? Seria pelo menos bom se você abordasse o que escrevi acima em vez de apenas dizer não .

Acho que você o nomeou muito bem anteriormente: _Se você tem uma entrada, forneça-a, apoie-a com dados e evidências. Também ouça os argumentos dos outros, reconheça-os. Forneça evidências contra a opinião de outras pessoas se você discordar._

Além disso, não se trata apenas do nome . Não é tão superficial. Por favor, leia o que escrevi. Trata-se de trabalhar entre níveis de abstração e ser capaz de aprimorar o código / compilar em cima dele.

Oh, houve um argumento nesta discussão porque:

Se escolhermos Heap, muitos desenvolvedores com habilidades intermediárias verão (infelizmente) "Heap" e o confundirão com o heap de memória do aplicativo (ou seja, heap e pilha) em vez de "heap a estrutura de dados generalizada". eles vão se perguntar por que podem alocar um novo heap de memória.

😛

não devemos introduzir a interface IHeap . IHeap interface é um cenário de especialista muito avançado - eu sugeriria movê-lo para a biblioteca PowerCollections.

@bendono escreveu um comentário muito bom sobre este. @safern também queria apoiar a implementação com uma interface. E alguns outros.

Outra observação - não tenho certeza de como você imagina mover uma interface para uma biblioteca de terceiros. Como os desenvolvedores poderiam escrever seu código nessa interface e usar nossa funcionalidade? Eles seriam forçados a manter nossa funcionalidade sem interface ou ignorá-la completamente, não há outra opção, é mutuamente exclusivo. Em outras palavras - nossa solução não seria extensível de forma alguma, resultando em pessoas usando nossa solução desabilitada ou alguma biblioteca de terceiros, em vez de depender do mesmo núcleo arquitetônico em ambos os casos . Isso é o que temos na biblioteca padrão do Java e em soluções de terceiros.

Mas, novamente, você comentou muito bem sobre este: Ninguém quer enviar APIs quebradas.

Dada a diferença de opiniões, proponho a seguinte abordagem para fazer a proposta avançar. [...] Faça 2 propostas alternativas [...] vamos trazê-lo para a revisão da API, discuti-lo lá e tomar uma decisão lá. Se eu vir pelo menos uma pessoa naquele grupo votando na proposta de Heaps, ficarei feliz em reconsiderar meu ponto de vista.

Houve muita discussão em várias partes da API acima. Ele abordou:

  • PriorityQueue vs Heap
  • Adicionando uma interface
  • Apoiar a atualização / remoção de elementos da coleção

Por que as pessoas que você tem em mente não podem simplesmente contribuir para essa discussão? Por que deveríamos começar de novo?

Se escolhermos Heap, muitos desenvolvedores com habilidades intermediárias verão (infelizmente) "Heap" e o confundirão com o heap de memória do aplicativo (ou seja, heap e pilha) em vez de "heap a estrutura de dados generalizada".

Sim, sou eu. Sou totalmente autodidata em programação, então não tenho ideia do que foi dito quando " Heap " entrou na discussão. Sem falar que, mesmo em termos de coleção, "um monte de coisas", para mim, significaria mais intuitivamente que ela fosse desordenada em todos os sentidos.

Eu não posso acreditar que isso está realmente acontecendo ...

Algum argumento? Seria pelo menos bom se você abordasse o que escrevi acima em vez de apenas dizer não.

Se você leu minha resposta, pode notar que mencionei os principais argumentos para minha posição:

  • A interface IHeap é um cenário de especialista muito avançado
  • Não acho que seja útil / alinhado com o resto do BCL o suficiente para justificar a complicação de adicionar nova interface agora

Os mesmos argumentos que se repetiram no tópico várias vezes IMO. Eu apenas os resumi

. Por favor, leia o que escrevi.

Eu estava monitorando ativamente esse tópico o tempo todo. Eu li todos os argumentos e pontos. Esta é minha opinião resumida, apesar de ler (e compreender) todos os seus pontos e os de outros.
Você aparentemente é apaixonado pelo assunto. Isso é ótimo. No entanto, tenho a sensação de que chegamos a uma posição quando estamos apenas repetindo argumentos semelhantes de cada lado, o que não levará muito mais longe - é por isso que recomendei 2 propostas e obtive mais feedback sobre elas de um grupo maior de experiência Revisores BCL API (BTW: Eu não me considero um revisor API experiente ainda).

Como os desenvolvedores poderiam escrever seu código nessa interface e usar nossa funcionalidade?

Os desenvolvedores que se preocupam com o cenário avançado IHeap farão referência à interface e à implementação da biblioteca de terceiros. Como eu disse, se ele provar ser popular, poderíamos considerar movê-lo para o CoreFX mais tarde.
A boa notícia é que adicionar IHeap mais tarde é totalmente viável - basicamente adiciona apenas uma sobrecarga de construtor em PriorityQueue .
Sim, não é o ideal do seu ponto de vista, mas não impede a inovação que você considera importante no futuro. Acho que é um meio-termo razoável.

Por que as pessoas que você tem em mente não podem simplesmente contribuir para esta discussão?

A revisão da API é uma reunião com discussão ativa, brainstorming, ponderando todos os ângulos. Em muitos casos, é mais produtivo / eficiente do que ir e vir nos problemas do GitHub. Veja dotnet / corefx # 14354 e seu predecessor dotnet / corefx # 8034 - discussão muito longa, várias opiniões diferentes com zilhões de ajustes que são difíceis de rastrear, sem conclusão, enquanto grande discussão, também perda de tempo não trivial para algumas pessoas, até que nos sentamos e conversamos sobre isso e chegamos a um consenso.
Ter revisores de API monitorando todos os problemas de API, ou mesmo apenas os mais tagarelas, não é um bom escalonamento.

Por que deveríamos começar de novo?

Não vamos começar de novo. Porque você pensaria isso?
Concluiremos a revisão da API no primeiro nível (o nível do proprietário da área) enviando 2 propostas mais populares com prós / contras para o próximo nível de revisão da API.
É uma abordagem hierárquica de aprovações / revisões. É semelhante a análises de negócios - VPs / CEOs com poder de decisão não supervisionam todas as discussões sobre todos os projetos em sua empresa, eles pedem a suas equipes / relatórios que apresentem propostas para as decisões mais impactantes ou contagiosas para uma discussão mais aprofundada sobre elas. As equipes / relatórios devem resumir o problema e apresentar os prós / contras das soluções alternativas.

Se você acredita que não estamos prontos para apresentar as 2 propostas finais com prós e contras, porque há coisas que ainda não foram ditas neste tópico, vamos continuar discutindo, até que tenhamos apenas alguns candidatos principais para revisar no próximo Nível de revisão da API.
Tive a sensação de que tudo o que precisava ser dito foi dito.
Faz sentido?

O nome deve ser PriorityQueue

Algum argumento?

Se você leu minha resposta, pode notar que mencionei os principais argumentos para minha posição.

Nossa ... Eu estava me referindo ao que citei ( obviamente ) - você decidiu ir com uma fila prioritária em vez de um heap. E sim, eu li sua resposta - ela contém exatamente 0% dos argumentos para isso.

Eu estava monitorando ativamente esse tópico o tempo todo. Eu li todos os argumentos e pontos. Esta é minha opinião resumida, apesar de ler (e compreender) todos os seus pontos e os de outros.

Você gosta de ser hiperbólico, já percebi isso antes. Você meramente reconhece a existência de pontos.

Como os desenvolvedores poderiam escrever seu código nessa interface e usar nossa funcionalidade?

Os desenvolvedores que se preocupam com o cenário avançado IHeap consultariam a interface e a implementação da biblioteca de terceiros. Como eu disse, se ele provar ser popular, poderíamos considerar movê-lo para o CoreFX mais tarde.

Você está ciente de que está apenas repetindo minhas palavras aqui e de forma alguma abordando o problema que apresentei como consequência do acima? Escrevi:

Eles seriam forçados a manter nossa funcionalidade sem interface ou ignorá-la completamente, não há outra opção, é mutuamente exclusivo.

Vou deixar isso bem claro para você. Haveria dois grupos

  1. Um grupo que está usando nossa funcionalidade. Não possui interface e não pode ser estendido, portanto, não está conectado ao que é fornecido em bibliotecas de terceiros.
  2. Segundo grupo que está ignorando completamente nossa funcionalidade e usando soluções puramente de terceiros.

O problema aqui é, como eu disse, que esses são grupos separados de pessoas . E eles estão produzindo código que não funciona em conjunto , porque não existe um núcleo arquitetônico comum. _ O CODEBASE NÃO É COMPATÍVEL _. Você não pode desfazer isso mais tarde.

A boa notícia é que adicionar IHeap posteriormente é totalmente viável - basicamente adiciona apenas uma sobrecarga de construtor em PriorityQueue.

Já escrevi porque é uma merda: veja este post .

Por que as pessoas que você tem em mente não podem simplesmente contribuir para esta discussão?

Ter revisores de API monitorando todos os problemas de API, ou mesmo apenas os mais tagarelas, não é um bom escalonamento.

Sim, eu estava perguntando por que os revisores de API não podem monitorar todos os problemas de API. Precisamente, você realmente respondeu de acordo.

Faz sentido?

Não. Estou cansado dessa discussão, realmente. Alguns de vocês estão claramente participando disso simplesmente porque é o seu trabalho e você deve fazer isso. Alguns de vocês precisam de orientação o tempo todo, o que é muito cansativo. Você até me pediu para provar por que uma fila de prioridade deveria ser implementada com um heap internamente, claramente sem formação em ciência da computação. Alguns de vocês nem mesmo entendem o que realmente é um heap, tornando a discussão ainda mais caótica.

Vá com o PriorityQueue desativado que não permite atualizar e remover elementos. Vá com seu design que não permite uma abordagem OO saudável. Vá com sua solução que não permite reutilizar a biblioteca padrão ao escrever uma extensão. Siga o caminho do Java.

E isso ... isso é simplesmente alucinante:

Se escolhermos Heap, muitos desenvolvedores com habilidades intermediárias verão (infelizmente) "Heap" e o confundirão com o heap de memória do aplicativo (ou seja, heap e pilha) em vez de "heap a estrutura de dados generalizada".

Apresente a API com sua abordagem. Estou curioso.

Eu não posso acreditar que isso está realmente acontecendo ...

Bem, desculpe-me por não ter tido a oportunidade de obter uma educação adequada em Ciência da Computação para aprender que Heap é algum tipo de estrutura de dados diferente do heap de memória.

O ponto ainda permanece, no entanto. Uma pilha de algo não implica Heap . PriorityQueue por outro lado, comunica perfeitamente que faz exatamente isso.

Como uma implementação de apoio? Claro, os detalhes da implementação não devem ser minha preocupação.
Alguma abstração de IHeap ? Ótimo para autores API e as pessoas que têm um CS major para saber o que ele é usado para, nenhuma razão para não ter.
Dar a algo um nome enigmático que não afirma sua intenção muito bem e, subsequentemente, limita a descoberta? 👎

Bem, desculpe-me por não ter tido a oportunidade de obter uma educação adequada em Ciência da Computação para aprender que Heap é algum tipo de estrutura de dados diferente do heap de memória.

Isto é ridículo. Ao mesmo tempo, você deseja participar de uma discussão sobre como adicionar uma funcionalidade como essa. Parece trollagem.

Uma pilha de algo não implica nada sobre ser ordenada de algum tipo.

Você está errado. Está ordenado, como uma pilha. Como nas fotos que você vinculou.

Como uma implementação de apoio? Claro, os detalhes da implementação não devem ser minha preocupação.

Eu já abordei isso. A família de heaps é enorme e há dois níveis de abstração acima de uma implementação. A fila de prioridade é a terceira camada de abstração.

Se eu precisasse de uma coleção que me permitisse armazenar objetos de coisas para processar, onde algumas instâncias que vêm depois podem precisar ser processadas mais cedo, eu não estaria procurando por algo chamado Heap. Por outro lado, PriorityQueue comunica perfeitamente que faz exatamente isso.

E sem qualquer histórico, você pediria ao Google para fornecer artigos sobre filas prioritárias? Bem, podemos argumentar o que é mais ou menos provável em nossas opiniões. Mas, como foi dito muito bem:

Se você tiver contribuições, forneça-as, apoie-as com dados e evidências. Forneça evidências contra a opinião dos outros se você discordar.

E de acordo com os dados você está errado:

Query | Exitos
: ----: |: ----: |
| "estrutura de dados" "fila prioritária" | 172.000 |
| "estrutura de dados" "heap" | 430.000 |

É quase 3 vezes mais provável que você encontre um heap ao ler sobre estruturas de dados. Além disso, é um nome com o qual os desenvolvedores de Swift, Go, Rust e Python estão familiarizados, porque suas bibliotecas padrão fornecem essa estrutura de dados.

Query | Exitos
: ----: |: ----: |
| "golang" "fila prioritária" | 3,390 |
| "ferrugem" "fila prioritária" | 8,630 |
| "rápida" "fila de prioridade" | 18,600 |
| "python" "fila de prioridade" | 72,800 |
| "Golang" "heap" | 79.000 |
| "ferrugem" "heap" | 492.000 |
| "pilha" "rápida" | 551.000 |
| "python" "heap" | 555.000 |

Na verdade, também é semelhante para C ++, porque uma estrutura de dados heap foi introduzida lá em algum momento no século anterior.

Dar a algo um nome enigmático que não afirma sua intenção muito bem e, subsequentemente, limita a descoberta? 👎

Sem opiniões. Dados. Veja acima. Especialmente sem opiniões de alguém que não tem formação. Você também não pesquisaria uma fila prioritária no Google sem ter lido sobre estruturas de dados antes. E uma pilha é abordada em muitas estruturas de dados 101 .

É a base da Ciência da Computação. É elementar. Quando você tem vários semestres de algoritmos e estruturas de dados, um heap é algo que você vê no início.

Mas ainda:

  • Primeiro - veja os números acima.
  • Segundo - pense em todas as outras linguagens em que um heap faz parte da biblioteca padrão.

EDIT: Veja Google Trends .

Como outro desenvolvedor autodidata, não tenho problemas com _heap_. Como um desenvolvedor que está sempre se esforçando para melhorar, dediquei um tempo para aprender e entender tudo sobre estruturas de dados. Em suma, não concordo com a implicação de que uma convenção de nomenclatura deva ter como alvo aqueles que não se deram ao trabalho de entender o léxico do campo do qual fazem parte.

E também discordo veementemente de afirmações como "O nome deve ser PriorityQueue". Se você não quer a opinião das pessoas, não torne o código-fonte aberto e não peça por ele.

Deixe-me fornecer uma explicação de como pensamos sobre a nomenclatura de API:

  1. Temos a tendência de favorecer a consistência na plataforma .NET acima de quase qualquer outra coisa. Isso é importante para fazer com que as APIs pareçam familiares e previsíveis. Às vezes, isso significa que aceitamos que um nome não é 100% correto se for um termo que usamos antes.

  2. Nosso objetivo é projetar uma plataforma que seja acessível a uma ampla variedade de desenvolvedores, alguns dos quais não tiveram uma educação formal em ciência da computação. Acreditamos que parte do motivo pelo qual o .NET geralmente é considerado muito produtivo e fácil de usar é parcialmente devido a esse ponto de design.

Geralmente empregamos o "teste do mecanismo de busca" quando se trata de verificar se um nome ou terminologia é bem conhecido e estabelecido. Portanto, agradeço muito a pesquisa que @pgolebiowski fez. Eu não fiz a pesquisa sozinho, mas meu pressentimento é que "pilha" não é um termo que muitos especialistas fora do domínio estariam procurando.

Portanto, tendo a concordar com @karelz que PriorityQueue parece a melhor escolha. Ele combina um conceito existente (fila) e adiciona o toque que expressa a capacidade desejada: recuperação ordenada com base em uma prioridade. Mas não estamos inamovivelmente apegados a esse nome. Freqüentemente, mudamos os nomes das estruturas de dados e tecnologias com base no feedback do cliente.

No entanto, gostaria de salientar que:

Se você não quer a opinião das pessoas, não torne o código-fonte aberto e não peça por ele.

é uma falsa dicotomia. Não é que não queiramos feedback de nosso ecossistema e colaboradores (obviamente queremos). Mas, ao mesmo tempo, também temos que reconhecer que nossa base de clientes é bastante diversificada e que os contribuidores do GitHub (ou desenvolvedores em nossa equipe) nem sempre são o melhor proxy para todos os nossos clientes. A usabilidade é difícil e provavelmente levará algumas iterações para adicionar novos conceitos ao .NET, especialmente em áreas altamente populares como coleções.

@pgolebiowski :

Eu valorizo ​​muito sua visão, dados e sugestões. Mas eu absolutamente não aprecio seu estilo de argumentação. Você foi pessoal tanto com os membros da minha equipe quanto com os membros da comunidade neste tópico. Só porque você discorda de nós, não dá a você permissão para nos acusar de não ter experiência ou não nos importar porque é "apenas nosso trabalho". Considere que muitos de nós literalmente nos mudamos ao redor do mundo, deixando famílias e amigos para trás apenas porque queríamos fazer este trabalho. Comentários como os seus não são apenas muito injustos, mas também não ajudam a desenvolver o design.

Então, embora eu goste de pensar que tenho a pele dura, não tenho muita tolerância para esse tipo de comportamento. Nosso domínio já é complexo o suficiente; não precisamos adicionar comunicação de confronto e adversário.

Por favor, seja respeitoso. Criticar ideias apaixonadamente é um jogo justo, mas atacar as pessoas não. Obrigada.

Caro todos que se sentem desanimados,

Peço desculpas por diminuir seus níveis de felicidade com minha atitude áspera.

@karelz

Fiz papel de idiota cometendo um erro técnico. Eu me desculpei. Foi aceito. Ainda assim, atirado em mim mais tarde. Não é legal IMO.

Lamento que o que escrevi o tenha deixado infeliz. Embora não tenha sido tão ruim como você descreveu - meramente citei isso como um dos muitos fatores que contribuíram para a minha sensação de cansaço . É de uma gravidade menor, eu acho. Mesmo assim, sinto muito.

E sim, todo mundo comete erros. Está tudo bem. Eu também, por exemplo, me deixando levar às vezes.

O que mais me pegou foi "apenas disseram para você fazer isso, você não acredita" - sim, é exatamente por isso que eu TAMBÉM faço isso nos fins de semana.

Sinto muito, posso ver que você trabalha muito e agradeço muito isso. Era visível para mim como você era especialmente dedicado no marco 5/10.

@terrajobst

Só porque você discorda de nós, não dá a você permissão para nos acusar de não ter experiência ou não nos importar porque é "apenas nosso trabalho".

  • Não ter especialização - dirigido a pessoas sem formação em ciência da computação e que não entendem o conceito de heaps / filas prioritárias. Se tal descrição se aplica a alguém - bem, se aplica, não é minha culpa.
  • Não se importando - dirigido a quem tem tendência a ignorar alguns dos pontos técnicos, obrigando a argumentos repetitivos, tornando a discussão caótica e mais difícil de seguir por outras pessoas (o que por sua vez leva a menos input).

Comentários como os seus são muito injustos e também não ajudam a avançar o design.

  • Meus comentários ásperos foram resultado da ineficiência nesta discussão. Ineficiência = forma caótica de discussão, onde os pontos não são abordados / resolvidos e avançamos apesar disso => ​​cansativo.
  • Além disso, como um dos principais impulsionadores da discussão, sinto fortemente que fiz muito para ajudar no avanço do design. Por favor, não me demonize, como você tenta fazer aqui e nas redes sociais.

Se houver alguém doente que queira "me lamber", fique à vontade.


Encontramos um problema e o resolvemos. Todo mundo aprendeu algo com isso. Percebo que todos aqui estão preocupados com a qualidade do framework, o que é absolutamente maravilhoso e me motiva a contribuir. Estou ansioso para continuar trabalhando no CoreFX com você. Dito isso, irei abordar suas novas sugestões técnicas provavelmente amanhã.

@pgolebiowski

Espero que possamos nos encontrar pessoalmente em algum momento. Sinceramente, acredito que parte do desafio de fazer tudo online é que as personalidades às vezes podem se misturar de maneiras ruins, sem nenhuma intenção de nenhum dos lados.

Estou ansioso para continuar trabalhando no CoreFX com você. Dito isso, irei abordar suas novas sugestões técnicas provavelmente amanhã.

Mesmo aqui. Este é um espaço interessante e há muitas coisas incríveis que podemos fazer juntos :-)

@pgolebiowski primeiro, obrigado pela sua resposta. Isso mostra que você se preocupa e tem boas intenções (o que eu secretamente espero que cada pessoa / desenvolvedor no mundo faça, todo conflito é apenas mal-entendido / falha de comunicação). E isso me deixa muito feliz - me mantém ativo e animado.
Eu sugeriria que recomeçássemos nosso relacionamento. Vamos voltar a discussão técnica, vamos todos aprender com este tópico, como lidar com situações semelhantes no futuro, vamos todos assumir novamente que a outra parte tem apenas o melhor interesse para a plataforma em mente.
BTW: Este é um dos poucos encontros / discussões mais difíceis sobre o repositório CoreFX nos últimos 9 meses, e como você pode ver, nós (incl./esp. Eu) ainda estamos aprendendo a lidar bem com eles - então esta instância em particular está acontecendo para beneficiar até mesmo a nós e nos fará ser melhores no futuro e nos ajudará a entender melhor os diferentes pontos de vista de membros da comunidade apaixonados. Talvez isso moldará nossas atualizações de documentos de contribuição ...

Meus comentários ásperos foram resultado da ineficiência nesta discussão. Ineficiência = forma caótica de discussão, onde os pontos não são abordados / resolvidos e avançamos apesar disso => ​​cansativo.

Compreendeu sua frustração! Curiosamente, uma frustração semelhante também estava do outro lado pelo mesmo motivo 😉 ... é quase engraçado como o mundo funciona :).
Infelizmente, a difícil discussão faz parte do trabalho quando você conduz uma decisão de design. É muito trabalho. Muitas pessoas o subestimam. A habilidade-chave necessária é paciência com todos e capacidade de se colocar acima de sua própria opinião e pensar em como gerar consenso, mesmo que isso não aconteça do seu jeito. É por isso que sugeri ter 2 propostas e "escalar" a discussão técnica para o grupo de revisores da API (principalmente porque não tenho certeza se estou certo, embora secretamente espere estar certo como qualquer outro desenvolvedor no mundo faria 😉 )

É MUITO difícil ter opinião sobre um assunto E conduzir a discussão ao CONSENSO no mesmo tópico. Dessa perspectiva, você e eu temos o mais comum neste tópico - ambos temos opiniões, mas ambos tentamos conduzir a discussão ao encerramento e à decisão. Portanto, vamos trabalhar juntos de perto.

Minha abordagem geral é: Sempre que penso que alguém está me atacando, está sendo mau, preguiçoso, me frustra ou algo assim. Eu me pergunto primeiro a mim mesmo e também à pessoa em particular: Por quê? Porque você disse isso? O que você quis dizer?
Normalmente é o sinal de falta de compreensão / comunicação dos motivos. Ou sinal de ler muito nas entrelinhas e ver insultos / acusações / más intenções onde não estão.


Agora que não tenho medo de continuar discutindo questões técnicas, eis o que eu queria perguntar antes:

Vá com o PriorityQueue desativado que não permite atualizar e remover elementos.

Isso é algo que não entendo. Se deixarmos de fora IHeap (na minha / na proposta original aqui), por que isso não seria possível?
IMO, não há diferença entre as 2 propostas do ponto de vista das capacidades da classe, a única diferença é - adicionamos a sobrecarga do construtor PriorityQueue(IHeap) ou não (deixando a disputa pelo nome da classe de lado como um problema independente para resolver) .

Isenção de responsabilidade (para evitar falhas de comunicação): Não tenho tempo para ler artigos e fazer pesquisas, espero uma resposta curta, apresentando argumentos de elevador de quem quer conduzir a discussão técnica. Nota: Não sou eu trollando. Eu faria a mesma pergunta a qualquer pessoa de nossa equipe que fizesse essa afirmação. Se você não tem energia para explicar / conduzir a discussão (o que seria totalmente compreensível dadas as dificuldades e o investimento de tempo de sua parte), é só dizer, sem ressentimentos. Por favor, não se sinta pressionado por mim ou por ninguém (e isso se aplica a todos no tópico).

Não tentando adicionar mais comentários desnecessários aqui, este tópico está MUITO LONGO. A regra nº 1 da era da Internet é evitar a comunicação de texto se você se preocupa com o relacionamento com as pessoas. (Bem, eu inventei). Eu acredito que alguma outra comunidade de código aberto mudaria para um Google Hangout para este tipo de discussão se a necessidade fosse aparente. Quando você olha para o rosto de outras pessoas, você nunca diria nada "insultuoso", e as pessoas se familiarizam umas com as outras muito rápido. Talvez possamos tentar também?

@karelz Devido à extensão da discussão acima, é altamente improvável que alguém novo contribua se o fluxo não for modificado. Como tal, gostaria de propor a seguinte abordagem agora:

  • Vou conduzir a votação sobre os aspectos fundamentais, um após o outro. Teremos contribuições claras da comunidade. O ideal é que os revisores da API também venham aqui e façam comentários em breve.
  • "Postagens de votação" conterá informações suficientes para ignorar todo o texto acima.
  • Após esta sessão de votação, saberemos o que esperar dos revisores da API e poderemos prosseguir com uma determinada abordagem. Quando chegarmos a um acordo sobre os aspectos fundamentais, este assunto será encerrado e outro aberto (que fará referência a este). Na nova edição, vou resumir nossas conclusões e fornecer uma proposta de API que reflete essas decisões. E vamos continuar a partir daí.

Isso tem alguma chance de fazer sentido?

PriorityQueue que não permite atualizar e remover elementos.

Foi em relação à proposta original, que não tinha esses recursos :) Desculpe por não ter deixado isso claro.

Se você não tem energia para explicar / conduzir a discussão (o que seria totalmente compreensível dadas as dificuldades e o investimento de tempo de sua parte), é só dizer, sem ressentimentos. Por favor, não se sinta pressionado por mim ou por ninguém (e isso se aplica a todos no tópico).

Eu não vou desistir. Sem dor sem ganho xD

@ xied75

Eu acredito que alguma outra comunidade de código aberto mudaria para um Google Hangout para este tipo de discussão se a necessidade fosse aparente. Talvez possamos tentar também?

Parece bom ;)

Fornecendo uma interface

Não importa se vamos com Heap / IHeap ou PriorityQueue / IPriorityQueue (ou outra coisa), para a funcionalidade que estamos prestes a fornecer ...

_você gostaria de ter uma interface, junto com a implementação? _

Para

@bendono

Apoiando essa implementação por uma interface, aqueles de nós que se preocupam com outras implementações podem facilmente trocar em outra implementação (de nossa própria criação ou em uma biblioteca externa) e ainda ser compatível com o código que aceita a interface.

Aqueles que não se importam com isso podem ignorá-lo e ir diretamente para a implementação concreta. Aqueles que se importam podem usar qualquer implementação de sua escolha.

Contra

@madelson

Existem muitos algoritmos diferentes que podem, teoricamente, ser usados ​​para implementar essa estrutura de dados. No entanto, parece improvável que a estrutura seja enviada com mais de um, e a ideia de que os escritores de bibliotecas realmente precisam de uma interface canônica para que várias partes possam se coordenar na gravação de implementações de heap compatíveis entre si não parece muito provável.

Além disso, uma vez que uma interface é lançada, ela nunca pode ser alterada devido à compatibilidade. Isso significa que a interface tende a ficar para trás das classes concretas em termos de funcionalidade (um problema com IList e IDictionary hoje).

@karelz

Interface é um cenário de especialista muito avançado.

Se a biblioteca e a interface IHeap se tornarem realmente populares, podemos mudar de ideia mais tarde e adicionar IHeap base na demanda (via sobrecarga do construtor), mas não acho que seja útil / alinhado com o resto do BCL é suficiente para justificar a complicação de adicionar uma nova interface agora. Comece simples e torne-se complicado apenas se for realmente necessário.

Impacto potencial da decisão

  • Incluir a interface significa que não podemos alterá-la no futuro.
  • Não incluir a interface significa que as pessoas escrevem código que usa nossa solução de biblioteca padrão ou é escrito em uma solução fornecida por uma biblioteca de terceiros (não há uma interface comum que possibilite a compatibilidade cruzada).

Use 👍 e 👎 para votar neste (a favor e contra uma interface, respectivamente). Alternativamente, escreva um comentário. O ideal é que os revisores da API participem.

Eu gostaria de acrescentar que embora a mudança de interfaces seja difícil, com métodos de extensão (e propriedades chegando) as interfaces são mais fáceis de estender e / ou trabalhar (consulte LINQ)

Eu gostaria de acrescentar que embora a mudança de interfaces seja difícil, com métodos de extensão (e propriedades chegando) as interfaces são mais fáceis de estender e / ou trabalhar (consulte LINQ)

Eles só podem funcionar com os métodos definidos publicamente na interface; então significa acertar da primeira vez.

Eu sugeriria esperar um pouco na interface até que a aula esteja em uso e se estabilize - então introduza uma interface. (com o debate sobre a forma da interface sendo uma questão separada)

Para ser franco, a única coisa que me interessa é a interface. Uma implementação sólida seria legal, mas eu (ou qualquer outra pessoa) sempre poderia criar a minha própria.

Lembro-me de alguns anos atrás, como tivemos exatamente essa mesma conversa com HashSet<T> . A Microsoft queria HashSet<T> enquanto a comunidade queria ISet<T> . Se bem me lembro, obtivemos HashSet<T> primeiro e ISet<T> segundo. Sem uma interface, o uso de HashSet<T> era bastante limitado, pois é difícil (senão frequentemente impossível) alterar uma API pública.

Devo observar que também existe SortedSet<T> agora, sem mencionar as inúmeras implementações não-BCL de ISet<T> . Usei ISet<T> em APIs públicas e sou grato por isso. Minha implementação privada pode usar qualquer implementação concreta que eu achar correta. Também posso trocar facilmente uma implementação por outra sem quebrar nada. Isso não seria possível sem a interface.

Para aqueles que dizem que sempre podemos definir nossas próprias interfaces, considere isso. Suponha por um momento que ISet<T> no BCL nunca aconteceu. Agora posso criar minha própria interface IMySet<T> , bem como implementações sólidas. Porém, um dia o BCL HashSet<T> é lançado. Pode ou não implementar ISet<T> , mas não implementa IMySet<T> . Como resultado, não posso trocar HashSet<T> como uma implementação do meu IMySet<T> .

Temo que vamos repetir essa farsa novamente.
Se você não está pronto para se comprometer com uma interface, então é muito cedo para introduzir uma classe concreta.

Acho a disparidade de opiniões significativa. Só de olhar para os números, um pouco mais de pessoas querem uma interface, mas isso não nos diz muito. Vou tentar perguntar a outras pessoas que já participaram da discussão antes, mas ainda não expressaram suas opiniões em relação a uma interface:

@ebickle @svick @horaciojcfilho @paulcbetts @justinvp @akoeplinger @mbeidler @SirCmpwn @andrewgmorris @weshaggard @BrannonKing @NKnusperer @danmosemsft @ianhays @safern @VisualMelon @ Joe4evr @jcouv @ xied75

Para os notificados: _guys, vocês poderiam fornecer sua opinião? Seria muito útil, mesmo se você apenas votar com: +1 :,: -1 :. Você pode começar a ler neste comentário de problema (4 posts acima aqui) - estamos discutindo se fornecer uma interface é uma boa ideia (apenas este aspecto por enquanto, até que seja resolvido) ._

Talvez algumas dessas pessoas sejam revisores de API, mas acredito que precisamos de seu apoio nesta decisão fundamental antes de prosseguirmos. @karelz , @terrajobst , seria possível pedir a eles que nos ajudassem a resolver esse aspecto? A contribuição deles seria muito valiosa, pois são eles que irão revisá-lo eventualmente - seria muito útil saber neste momento, antes de se comprometer com uma determinada abordagem (ou ir com mais de uma proposta, o que seria cansativo e um pouco sem sentido, pois podemos saber a sua decisão anterior).

Pessoalmente, sou a favor de uma interface, mas se a decisão for diferente, fico feliz em seguir um caminho diferente.

Não quero arrastar revisores de API para a discussão - é longo e confuso, não seria eficiente para revisores de API reler tudo ou mesmo apenas decidir qual é a última resposta importante (estou me perdendo nisso )
Acho que estamos no ponto em que podemos criar 2 propostas formais de API (veja o 'bom' exemplo aqui) e destacar os prós / contras de cada uma. Podemos analisá-los em nossa reunião de revisão de API e fazer recomendações, levando os votos em consideração. Dependendo da discussão lá (se houver várias opiniões), podemos voltar e lançar a enquete do Twitter / votação adicional de GH, etc.

BTW: reuniões de revisão de API acontecem quase todas as terças-feiras.

Para ajudar a começar, veja como uma proposta deve ser:

Exemplo de proposta / semente

`` `c #
namespace System.Collections.Generic
{
public class PriorityQueue
: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
public PriorityQueue ();
public PriorityQueue (capacidade interna);
public PriorityQueue (IComparercomparador);
public PriorityQueue (IEnumerablecoleção);
public PriorityQueue (IEnumerablecoleção, IComparercomparador);
public PriorityQueue (capacidade interna, 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();
    }
}

}
`` `

PENDÊNCIA:

  • Falta de exemplo de uso (não está claro para mim como expresso a prioridade dos itens) - talvez devêssemos redesenhar para ter 'int' como entrada de valor de prioridade? Talvez alguma abstração sobre isso? (Acho que foi discutido acima, mas o tópico é muito longo para ler, é por isso que precisamos de propostas concretas e não mais discussão)
  • Cenário UpdatePriority ausente
  • Mais alguma coisa discutida acima que está faltando aqui?

@karelz OK, vou

Tudo bem. Pelo que eu posso dizer, uma interface ou um heap não passará na revisão da API. Como tal, gostaria de propor uma solução ligeiramente diferente, esquecendo a pilha quaternária, por exemplo. A estrutura de dados abaixo é diferente em alguns aspectos do que podemos encontrar em Python , Java , C ++ , Go , Swift e Rust (pelo menos esses).

O foco principal é a exatidão, integridade em termos de funcionalidade e intuitividade, mantendo complexidades ideais e excelente desempenho no mundo real.

@karelz @terrajobst

Proposta

Justificativa

É um caso muito comum que um usuário tenha vários elementos, onde alguns deles têm uma prioridade mais alta do que outros. Eventualmente, eles querem manter esse grupo de elementos em alguma ordem específica para poder executar com eficiência as seguintes operações:

  1. Adicione um novo elemento à coleção.
  2. Recupere o elemento com a prioridade mais alta (e seja capaz de removê-lo).
  3. Remova um elemento da coleção.
  4. Modifique um elemento da coleção.
  5. Mescle duas coleções.

Uso

Glossário

  • Valor - dados do usuário.
  • Chave - um objeto que é usado para fins de pedido.

Vários tipos de dados do usuário

Primeiro, vamos focar na construção da fila de prioridade (apenas adicionando elementos). A forma como isso é feito depende do tipo de dados do usuário.

Cenário 1

  • TKey e TValue são objetos separados.
  • TKey é comparável.
var queue = new PriorityQueue<int, string>();

queue.Enqueue(5, "five");
queue.Enqueue(1, "one");
queue.Enqueue(3, "three");

Cenário 2

  • TKey e TValue são objetos separados.
  • TKey não é comparável.
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");

Cenário 3

  • TKey está contido em TValue .
  • TKey não é comparável.
public class MyClass
{
    public MyKey Key { get; set; }
}

Além de um comparador de chave, também precisamos de um seletor de chave:

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 um método Enqueue aqui. Desta vez, leva apenas um argumento ( TValue ).
  • Se o seletor de chave for definido, o método Enqueue(TKey, TValue) deve lançar InvalidOperationException .
  • Se o seletor de chave não estiver definido, o método Enqueue(TValue) deve lançar InvalidOperationException .

Cenário 4

  • TKey está contido em TValue .
  • TKey é comparável.
var queue = new PriorityQueue<MyKey, MyClass>(selector);

queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
Notas
  • O comparador para TKey é considerado Comparer<TKey>.Default , como no Cenário 1 .

Cenário 5

  • TKey e TValue são objetos separados, mas do mesmo tipo.
  • TKey é comparável.
var queue = new PriorityQueue<int, int>();

queue.Enqueue(5, 50);
queue.Enqueue(1, 10);
queue.Enqueue(3, 30);

Cenário 6

  • Os dados do usuário são um único objeto, que é comparável.
  • Não há chave física ou o usuário não deseja usá-la.
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

No início, existe uma ambigüidade.

  • É possível que MyClass seja um objeto separado e o usuário gostaria de simplesmente ter a chave e o valor separados, como no Cenário 5 .
  • No entanto, também pode ser um único objeto (como neste caso).

Veja como PriorityQueue lida com a ambigüidade:

  • Se o seletor de chave for definido, não haverá ambigüidade. Apenas Enqueue(TValue) é permitido. Portanto, uma solução alternativa para o Cenário 6 é simplesmente definir um seletor e passá-lo para o construtor.
  • Se o seletor de chave não for definido, a ambigüidade será resolvida com o primeiro uso de um método Enqueue :

    • Se Enqueue(TKey, TValue) for chamado pela primeira vez, a chave e o valor serão considerados objetos separados ( Cenário 5 ). A partir de então, o método Enqueue(TValue) deve lançar InvalidOperationException .

    • Se Enqueue(TValue) for chamado pela primeira vez, a chave e o valor são considerados o mesmo objeto, o seletor de chave é inferido ( Cenário 6 ). A partir de então, o método Enqueue(TKey, TValue) deve lançar InvalidOperationException .

Outras funcionalidades

Já abordamos a criação de uma fila prioritária. Também sabemos como adicionar elementos à coleção. Agora vamos nos concentrar na funcionalidade restante.

Elemento de maior prioridade

Existem duas operações que podemos fazer no elemento com a prioridade mais alta - recuperá-lo ou removê-lo.

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

O que exatamente está sendo retornado pelos métodos Peek e Dequeue será discutido posteriormente.

Modificando um elemento

Para um elemento arbitrário, podemos querer modificá-lo. Por exemplo, se uma fila de prioridade for usada durante todo o tempo de vida de um serviço, há uma grande chance de que o desenvolvedor gostaria de ter a possibilidade de atualizar as prioridades.

Surpreendentemente, essa funcionalidade não é fornecida por estruturas de dados equivalentes: em Python , Java , C ++ , Go , Swift e Rust . Possivelmente alguns outros também, mas eu apenas verifiquei aqueles. O resultado? Desenvolvedores decepcionados:

Temos basicamente duas opções aqui:

  • Faça do jeito Java e não forneça essa funcionalidade. Eu sou fortemente contra isso. Ele força o usuário a remover um elemento da coleção (o que por si só não funciona bem, mas abordarei isso mais tarde) e a adicioná-lo novamente. É muito feio, não funciona em todos os casos e é ineficiente.
  • Apresente um novo conceito de puxadores .

Alças

Cada vez que um usuário adiciona um elemento à fila de prioridade, ele recebe um identificador:

var handle = queue.Enqueue(42, "forty two");

Handle é uma classe com a seguinte API pública:

public class PriorityQueueHandle<TKey, TValue>
{
    public TKey Key { get; }
    public TValue Value { get; }
}

É uma referência a um elemento único na fila de prioridade. Se você está preocupado com a eficiência, consulte as perguntas frequentes (e não se preocupará mais).

Essa abordagem nos permite modificar facilmente um elemento único na fila de prioridade, de uma forma muito intuitiva e simples:

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

No exemplo acima, como havia um seletor específico definido, o próprio usuário poderia atualizar o objeto. A fila de prioridade simplesmente precisava ser notificada para se reorganizar (não queremos torná-la observável).

Da mesma forma como o tipo de dados do usuário impacta a maneira como uma fila de prioridade é construída e preenchida com elementos, a maneira como ela é atualizada também varia. Vou encurtar os cenários desta vez, pois você provavelmente já sabe o que vou escrever.

Cenários

Cenário 7
  • TKey e TValue são 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 você pode ver, essa abordagem fornece uma maneira simples de se referir a um elemento único na fila de prioridade, sem fazer perguntas. É especialmente útil em cenários onde as chaves podem ser duplicadas. Além disso, isso é muito eficiente - sabemos o elemento a ser atualizado em O (1), e a operação pode ser realizada em O (log n).

Como alternativa, o usuário pode usar métodos adicionais que pesquisam toda a estrutura em O (n) e, em seguida, atualizam o primeiro elemento que corresponde aos argumentos. É assim que a remoção de um elemento é feita em Java. Não é totalmente correto nem eficiente, mas às vezes mais simples:

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

queue.Enqueue(1, "one");
queue.Enqueue(2, "three");
queue.Enqueue(3, "three");

queue.Update("three", 30);

O método acima encontra o primeiro elemento com seu valor igual a "three" . Sua chave será atualizada para 30 . Não sabemos qual será atualizado se houver mais de um que satisfaça a condição.

Poderia ser um pouco mais seguro com um método Update(TKey oldKey, TValue, TKey newKey) . Isso adiciona outra condição - a chave antiga também deve corresponder. Ambas as soluções são mais simples, mas não 100% seguras e têm menos desempenho (O (1 + log n) vs O (n + log n)).

Cenário 8
  • TKey está contido em TValue .
var queue = new PriorityQueue<int, MyClass>(selector);

/* adding some elements */

queue.Update(handle);

Este cenário é o que foi dado como exemplo na seção Handles .

O acima é obtido em O (log n). Alternativamente, o usuário pode usar um método Update(TValue) que encontra o primeiro elemento igual ao especificado e faz uma reorganização interna. Claro, isso é O (n).

Cenário 9
  • TKey e TValue são objetos separados, mas do mesmo tipo.

Usando uma alça, não há ambigüidade, como sempre. No caso de outros métodos que permitem a atualização - existe, é claro. Esta é uma compensação entre simplicidade, desempenho e correção (que depende se os dados podem ser duplicados ou não).

Cenário 10
  • Os dados do usuário são um único objeto.

Com uma alça, novamente não há problema. A atualização por meio de outros métodos pode ser mais simples - mas, novamente, tem menos desempenho e nem sempre é correta (entradas iguais).

Removendo um elemento

Pode ser feito de forma simples e correta por meio de uma alça. Alternativamente, por meio dos métodos Remove(TValue) e Remove(TKey, TValue) . Mesmos problemas descritos anteriormente.

Mesclando duas coleções

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

/* add some elements to both */

queue1.Merge(queue2);

Notas

  • Após a fusão, as filas compartilham a mesma representação interna. O usuário pode usar qualquer um dos dois.
  • Os tipos devem corresponder (verificados estaticamente).
  • Os comparadores devem ser iguais, caso contrário InvalidOperationException .
  • Os seletores devem ser iguais, caso contrário InvalidOperationException .

API proposta

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

Perguntas abertas

Em vez disso, predicados

Sou fortemente a favor da abordagem com alças, porque é eficiente, intuitiva e totalmente correta . A questão é como lidar com métodos mais simples, mas potencialmente não tão seguros. Uma coisa que poderíamos fazer é substituir esses métodos mais simples por algo assim:

  • UpdateFirst(Func<PriorityQueueHandle<TKey, TValue>, bool> predicate)
  • UpdateAll(Func<PriorityQueueHandle<TKey, TValue>, bool> predicate)

O uso seria muito doce e poderoso. E verdadeiramente intuitivo. E legível (muito expressivo). Eu sou a favor disso.

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

Definir a nova chave também pode ser uma função, que pega a chave antiga e a transforma de alguma forma.

O mesmo vale para Contains e Remove .

UpdateKey

Se o seletor de chave não for definido (e, portanto, a chave e o valor são mantidos separadamente), o método pode ser denominado UpdateKey . Provavelmente é mais expressivo. Quando o seletor de chave é definido, Update é melhor, porque a chave já está atualizada e o que precisa ser feito é o rearranjo de alguns elementos dentro da fila de prioridade.

Perguntas frequentes

Os cabos não são ineficientes?

Não há nenhum problema com a eficiência no que diz respeito ao uso de alças. Um medo comum é que isso exija alocações adicionais, porque estamos usando um heap com base em um array internamente. Não temas. Leia.

Como você vai implementá-lo então?

Seria uma abordagem completamente diferente para entregar uma fila prioritária. Pela primeira vez, uma biblioteca padrão não fornecerá essa funcionalidade implementada como um heap binário, que é representado como uma matriz embaixo. Ele será implementado com um heap de emparelhamento , que por design não usa um array - ele simplesmente representa uma árvore.

Qual seria o desempenho?

  • Para entrada aleatória geral, um heap quaternário seria um pouco mais rápido.
  • No entanto, ele teria que ser baseado em um array para ser mais rápido. Assim, as alças não poderiam ser baseadas simplesmente em nós - seriam necessárias alocações adicionais. Então - não pudemos atualizar e remover elementos de maneira razoável.
  • Da forma como está projetado agora, temos uma API fácil de usar e uma implementação de bom desempenho.
  • Um benefício adicional de ter um heap de emparelhamento abaixo é a capacidade de mesclar duas filas de prioridade em O (1), em vez de O (n) como em heaps binários / quaternários.
  • O heap de emparelhamento ainda é extremamente rápido. Veja as referências. Às vezes, é mais rápido do que heaps quaternários (depende dos dados de entrada e das operações realizadas, não apenas da fusão).

Referências

  • Michael L. Fredman, Robert Sedgewick, Daniel D. Sleator e Robert E. Tarjan (1986), The pairing heap: A new form of self- Adjusting heap , Algorithmica 1: 111-129 .
  • Daniel H. Larkin, Siddhartha Sen e Robert E. Tarjan (2014), A back-to-basics empírico study of priority queues , arXiv: 1403.0252v1 [cs.DS].

BTW, é para a 1ª iteração de revisão da API. A seção com _perguntas abertas_ precisa ser resolvida (preciso da sua opinião [e dos revisores da API, se possível]). Se a 1ª iteração passar pelo menos parcialmente, para o resto da discussão, gostaria de criar um novo problema (feche e faça referência a este).

Pelo que eu posso dizer, uma interface ou um heap não passará na revisão da API.

@pgolebiowski : Por que não encerrar o problema então? Sem uma interface, essa classe é quase inútil. O único lugar que posso usar é em implementações privadas. Nesse ponto, posso apenas criar (ou reutilizar) minha própria fila de prioridade quando necessário. Não posso expor uma API pública em meu código com esse tipo de assinatura, pois ela será interrompida assim que eu precisar trocá-la por outra implementação.

@bendono
Bem, a Microsoft tem a palavra final aqui, não poucas pessoas comentando no tópico. E sabemos que:

Tenho certeza de que outros revisores / arquitetos de API irão compartilhá-lo, já que verifiquei a opinião de alguns: o nome deve ser PriorityQueue e não devemos apresentar a interface IHeap. Deve haver exatamente uma implementação (provavelmente por meio de algum heap).

Isso é compartilhado por @karelz , Gerente de Engenharia de Software, e @terrajobst , Gerente de Programa e o proprietário deste repositório + alguns revisores de API.

Embora eu obviamente goste da abordagem com interfaces, conforme claramente afirmado nos posts anteriores, posso ver que é muito difícil, uma vez que não temos muito poder nesta discussão. Defendemos os nossos pontos de vista, mas somos apenas alguns comentadores. O código não pertence a nós de qualquer maneira. O que mais podemos fazer?

Por que não encerrar o problema então? Sem uma interface, essa classe é quase inútil.

Já fiz o suficiente - em vez de odiar meu trabalho, faça algo. Faça o trabalho real. Por que você tenta me culpar por alguma coisa? Culpe a si mesmo por não ter conseguido persuadir os outros a seu ponto de vista.

E, por favor, poupe-me de retórica como essa. É realmente infantil - faça melhor.

A propósito, a proposta foca em coisas que são comuns, não importa se apresentamos uma interface ou não, ou se a classe se chama PriorityQueue ou Heap . Portanto, concentre-se no que realmente importa aqui e nos mostre alguma tendência para a ação, se você quiser algo.

@pgolebiowski Claro que a decisão é da Microsoft. Mas é melhor apresentar uma API que você deseja usar e que atenda às suas necessidades. Se for rejeitado, então que seja. Só não vejo necessidade de comprometer a proposta.

Peço desculpas se você interpretou meus comentários como culpando você. Essa certamente não era minha intenção.

@pgolebiowski por que não usar KeyValuePair<TKey,TValue> para o identificador?

@SamuelEnglard

Por que não usar KeyValuePair<TKey,TValue> para a alça?

  • Bem, o PriorityQueueHandle é na verdade _ nó de pilha de emparelhamento _. Ele expõe duas propriedades - TKey Key e TValue Value . No entanto, tem muito mais lógica interna, que é apenas interna. Por favor, consulte a minha implementação disso . Ele contém, por exemplo, também ponteiros para outros nós na árvore (tudo seria interno no CoreFX).
  • KeyValuePair é uma estrutura, então ela é copiada toda vez + não pode ser herdada.
  • Mas o principal é que PriorityQueueHandle é uma classe bastante complicada que simplesmente expõe a mesma API pública de KeyValuePair .

@bendono

Mas é melhor apresentar uma API que você deseja usar e que atenda às suas necessidades. Se for rejeitado, então que seja. Só não vejo necessidade de comprometer a proposta.

  • É verdade, terei isso em mente e verei o que acontece. @karelz , junto com a proposta, você também poderia aprovar um pouco do post anterior (eles estão logo antes da proposta), onde votamos nas interfaces? Podemos voltar a isso mais tarde, após a primeira iteração da revisão da API.
  • Ainda assim, não importa a solução que escolhermos, a funcionalidade será muito semelhante e seria útil revisá-la. Porque se a ideia de alças for rejeitada e não pudermos realmente atualizar / remover elementos adequadamente (ou não podemos separar TKey e TValue ), esta classe está realmente perto de ser inútil então - - como agora em Java.
  • Em particular, se não tivermos uma interface, minha biblioteca AlgoKit não será capaz de ter um núcleo comum para heaps com CoreFX, o que seria realmente triste para mim.
  • E sim, é realmente surpreendente para mim que adicionar uma interface seja percebido como uma desvantagem pela Microsoft.

Essa certamente não era minha intenção.

Desculpe, meu erro então.

Minhas perguntas sobre o design (isenção de responsabilidade: essas perguntas e esclarecimentos, sem contestações difíceis (ainda)):

  1. Nós realmente precisamos de PriorityQueueHandle ? E se esperarmos apenas valores únicos na fila?

    • Motivação: Parece um conceito bastante complicado. Se tivermos, gostaria de entender por que precisamos? Como isso está ajudando? Ou são apenas detalhes de implementação específicos vazando para a superfície da API? Isso vai nos comprar tanto perf para pagar pela complicação na API?

  2. Precisamos de Merge ? Outras coleções têm? Não devemos adicionar APIs, apenas porque são fáceis de implementar, deve haver algum caso de uso comum para as APIs.

    • Talvez apenas adicionar alguma inicialização de IEnumerable<KeyValuePair<TKey, TValue>> seja suficiente? + confiar no Linq

  3. Precisamos de sobrecarga de comparer ? Podemos sempre voltar ao padrão? (isenção de responsabilidade: estou faltando conhecimento / experiência neste caso, então estou apenas perguntando)
  4. Precisamos de sobrecargas de keySelector ? Acho que devemos decidir se queremos ter prioridade como parte do valor, ou uma coisa separada. A prioridade separada parece um pouco mais natural para mim, mas não tenho uma opinião forte. Conhecemos os prós e os contras?

Pontos de decisão separados / paralelos:

  1. Nome da classe PriorityQueue vs. Heap
  2. Introduzir IHeap e sobrecarga do construtor?

Introduzir IHeap e sobrecarga de construtor?

Parece que as coisas se acalmaram o suficiente para eu adicionar meus 2 centavos ... Eu gosto da interface, pessoalmente. Ele abstrai os detalhes de implementação da API (e a funcionalidade central descrita por essa API) de uma forma que, em minha opinião, simplifica a estrutura e permite o máximo de usabilidade.

O que não tenho uma opinião tão forte é se fazemos a interface ao mesmo tempo que PQueue / Heap / ILikeThisItemMoreThanThisItemList ou se a adicionamos posteriormente. O argumento de que a API pode estar "em fluxo" e, como tal, devemos liberá-la como uma classe primeiro, até obtermos feedback, é certamente válido, do qual não discordo. A questão então é quando é considerado "estável" o suficiente para adicionar uma interface. Muito acima, no tópico IList e IDictionary foram mencionados como atrasados ​​em relação às APIs de suas implementações canônicas, que adicionamos loooongo tempo atrás, então, qual período de tempo é considerado um período de descanso aceitável?

Se pudermos definir esse período com uma certeza razoável e ter certeza de que não está bloqueando de forma inaceitável, então não vejo nenhum problema em enviar essa coisa de estrutura de dados enorme sem uma interface. Então, após esse período de tempo, podemos examinar o uso e considerar a adição de uma interface.

E sim, é realmente surpreendente para mim que adicionar uma interface seja percebido como uma desvantagem pela Microsoft.

Isso porque, de várias maneiras, é uma desvantagem. Se enviarmos uma interface, está praticamente pronto. Não há muito espaço para iterar nessa API, então é melhor ter certeza de que está certo na primeira vez e continuará a ser certo por muitos anos que virão. Perder alguma funcionalidade interessante é uma maneira melhor de estar do que ficar preso a uma interface potencialmente inadequada, onde seria muito bom ter apenas uma pequena mudança que tornaria tudo melhor.

Obrigado pela contribuição, @karelz e @ianhays!

Permitindo duplicatas

Nós realmente precisamos de PriorityQueueHandle ? E se esperarmos apenas valores únicos na fila?

Motivação: Parece um conceito bastante complicado. Se tivermos, gostaria de entender por que precisamos? Como isso está ajudando? Ou são apenas detalhes de implementação específicos vazando para a superfície da API? Isso vai nos comprar tanto perf para pagar pela complicação na API?

Não, não precisamos disso. A API de fila de prioridade proposta acima é bastante poderosa e muito flexível. Baseia-se no pressuposto de que os elementos e prioridades podem ser duplicados . Por causa dessa suposição, é necessário ter um identificador para poder remover ou atualizar o nó correto. No entanto, se colocarmos uma restrição de que os elementos devem ser únicos, poderíamos obter o mesmo resultado acima com uma API mais simples e sem expor uma classe interna ( PriorityQueueHandle ), o que de fato não é o ideal.

Vamos supor que permitimos apenas elementos únicos. Ainda poderíamos oferecer suporte a todos os cenários anteriores e manter o desempenho ideal. API mais simples:

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

Em breve, haveria um Dictionary<TElement, InternalNode> subjacente. Testar se a fila de prioridade contém um elemento pode ser feito ainda mais rápido do que na abordagem anterior. Atualizar e remover elementos é significativamente simplificado, pois sempre podemos apontar para um elemento direto na fila.

Provavelmente, permitir duplicatas não compensa o incômodo e o acima exposto é suficiente. Eu acho que gosto disso. O que você acha?

Unir

Precisamos de Merge ? Outras coleções têm? Não devemos adicionar APIs, apenas porque são fáceis de implementar, deve haver algum caso de uso comum para as APIs.

Aceita. Nós não precisamos disso. Sempre podemos adicionar API, mas não removê-la. Estou bem em remover este método e (potencialmente) adicioná-lo mais tarde (se houver necessidade).

Comparer

Precisamos de sobrecarga do comparador? Podemos sempre voltar ao padrão? (isenção de responsabilidade: estou faltando conhecimento / experiência neste caso, então estou apenas perguntando)

Acho isso muito importante, por dois motivos:

  • Um usuário gostaria que seus elementos fossem ordenados em ordem crescente ou decrescente. A prioridade mais alta não nos diz como o pedido deve ser feito.
  • Se pularmos um comparador, obrigamos o usuário a sempre nos entregar uma classe que implemente IComparable .

Além disso, é consistente com a API existente. Dê uma olhada em SortedDictionary .

Seletor

Precisamos de sobrecargas de keySelector? Acho que devemos decidir se queremos ter prioridade como parte do valor, ou uma coisa separada. A prioridade separada parece um pouco mais natural para mim, mas não tenho uma opinião forte. Conhecemos os prós e os contras?

Eu também gosto de prioridades separadas. Além disso, é mais fácil de implementar, melhor desempenho e uso de memória, API mais intuitiva, menos trabalho para o usuário (não há necessidade de implementar IComparable ).

Em relação ao seletor agora ...

Prós

Isso é o que torna essa fila de prioridade flexível. Ele permite que os usuários tenham:

  • elementos e suas prioridades como elementos separados
  • elementos (classes complexas) que têm prioridades em algum lugar dentro deles
  • uma lógica externa que recupera a prioridade para um determinado elemento
  • elementos que implementam IComparable

Ele permite quase todas as configurações que posso imaginar. Acho que é útil, pois os usuários podem apenas "ligar e jogar". Também é bastante intuitivo.

Contras

  • Há mais para aprender.
  • Mais API. Dois construtores adicionais, um método Enqueue e Update .
  • Se decidirmos ter o elemento e a prioridade separados ou unidos, obrigamos alguns dos usuários (que têm seus dados em um formato diferente) a adaptar seu código para usar essa estrutura de dados.

Pontos de decisão separados / paralelos

Nome da classe PriorityQueue vs. Heap

Apresente IHeap e sobrecarga do construtor.

  • Parece que PriorityQueue deve fazer parte do CoreFX, em vez de Heap .
  • Com relação à interface IHeap - porque uma fila de prioridade pode ser implementada com algo diferente de um heap, provavelmente não gostaríamos de expô-la dessa forma. No entanto, podemos precisar de IPriorityQueue .

Se enviarmos uma interface, está praticamente pronto. Não há muito espaço para iterar nessa API, então é melhor ter certeza de que está certo na primeira vez e continuará a ser certo por muitos anos que virão. Perder alguma funcionalidade interessante é uma maneira melhor de estar do que ficar preso a uma interface potencialmente inadequada, onde seria muito bom ter apenas uma pequena mudança que tornaria tudo melhor.

Concordo plenamente!

Elementos comparáveis

Se adicionarmos outra suposição: que os elementos devem ser comparáveis, a API é ainda mais simples. Mas, novamente, é menos flexível.

Prós

  • Não há necessidade de IComparer .
  • Não há necessidade do seletor.
  • Um método Enqueue e Update .
  • Um tipo genérico em vez de dois.

Contras

  • Não podemos ter elemento e prioridade como objetos separados. Os usuários precisam fornecer uma nova classe de invólucro se a tiverem nesse formato.
  • Os usuários sempre precisam implementar IComparable antes de usar essa fila de prioridade.
  • Se tivermos elementos e prioridades separados, poderíamos implementá-lo de maneira mais fácil, com melhor desempenho e uso de memória - dicionário interno <TElement, InternalNode> e ter InternalNode contendo 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();
}

Tudo é uma troca. Removemos alguns recursos, perdemos alguma flexibilidade, mas talvez não precisemos disso para satisfazer 95% dos nossos usuários.

Eu também gosto da abordagem da interface porque é mais flexível e oferece mais utilizações, mas também concordo com @karelz e @ianhays que devemos esperar até que a nova API de classe seja usada e

Também sobre comparadores, acho que segue as outras apis do BCL e não gosto do fato de os usuários precisarem fornecer uma nova classe de wrapper. Eu realmente gosto da sobrecarga do construtor recebendo uma abordagem de comparador e usando esse comparador dentro de todas as comparações que precisam ser feitas internamente, se não for fornecido o comparador ou o comparador for nulo, então use o comparador padrão.

@pgolebiowski obrigado pela proposta de API detalhada e descritiva e por ser tão pró-ativo e enérgico em obter esta API aprovada e adicionada ao CoreFX 👍 quando esta discussão for concluída e considerarmos que está pronto para revisão, eu mesclaria todas as entradas e a superfície final da API em um comentário e atualize o comentário do problema principal no topo, pois isso tornaria a vida dos revisores mais fácil.

OK, estou convencido sobre o comparador, faz sentido e é consistente.
Ainda estou indeciso sobre o seletor - IMO, devemos tentar ir sem ele - vamos dividi-lo em 2 variantes.

`` `c #
public class PriorityQueue
: IEnumerable,
IEnumerable <(elemento TElement, prioridade TPriority)>,
IReadOnlyCollection <(elemento TElement, prioridade TPriority)>
// ICollection não incluída propositalmente
{
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();

//
// Parte do seletor
//
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)

}
`` ``

Perguntas abertas:

  1. Nome da classe PriorityQueue vs. Heap
  2. Introduzir IHeap e sobrecarga do construtor? (Devemos esperar mais tarde?)
  3. Apresente IPriorityQueue ? (Devemos esperar mais tarde - IDictionary exemplo)
  4. Use seletor (de prioridade armazenada dentro do valor) ou não (diferença de 5 APIs)
  5. Use tuplas (TElement element, TPriority priority) vs. KeyValuePair<TPriority, TElement>

    • Deveriam Peek e Dequeue ter out argumento em vez de tupla?

Vou tentar executá-lo pelo grupo de revisão de API amanhã para feedback antecipado.

Corrigido Peek e Dequeue nome de campo de tupla para priority (obrigado @pgolebiowski por apontar isso).
Postagem principal atualizada com a proposta mais recente acima.

Eu concordo com o comentário de @pgolebiowski por levar isso adiante!

Perguntas abertas:

Acho que poderíamos ter mais um aqui:

  1. Limitemo-nos apenas a elementos únicos (não permita duplicatas).

Bom ponto, capturado como 'Suposições' na postagem superior, em vez de pergunta aberta. O oposto complicaria um pouco a API.

Devemos retornar bool de Remove ? E também a prioridade como out priority arg? (Acredito que adicionamos sobrecargas semelhantes recentemente em outras estruturas de dados)

Semelhante a Contains - aposto que alguém vai querer a prioridade fora disso. Podemos querer adicionar uma sobrecarga com out priority .

Continuo com a opinião (embora ainda não a expresse) que o Heap implicaria em uma implementação e apoiaria devidamente a chamada de PriorityQueue . (Eu até apoiaria então uma proposta para uma classe Heap estreita que permite elementos duplicados e não permite atualização, etc. (mais de acordo com a proposta original), mas não espero isso acontecer).

KeyValuePair<TPriority, TElement> certamente não deve ser usado, visto que prioridades duplicadas são esperadas, e eu acho que KeyValuePair<TElement, TPriority> também é confuso, então eu apoiaria não usar KeyValuePair alguma, e usando tuplas simples ou parâmetros de saída para as prioridades (pessoalmente, gosto de parâmetros de saída, mas não estou preocupado).

Se não permitirmos duplicatas, precisamos decidir o comportamento de tentar adicioná-las novamente com prioridades diferentes / alteradas.

Não apoio a proposta do seletor, pela simples razão de que implica redundância, e devidamente irá recriar confusão. Se a prioridade de um elemento for armazenada com ele, então ele será armazenado em dois lugares, e se eles ficarem fora de sincronia (ou seja, alguém se esquece de chamar o método de aparência inútil Update(TElement) ), então muito sofrimento Irá garantir. Se o seletor for um computador, então estamos abertos para que as pessoas adicionem intencionalmente um elemento, alterem os valores a partir dos quais são calculados e, agora, se tentarem adicioná-lo novamente, há uma série de coisas que podem dar errado, dependendo na decisão do que acontece quando isso ocorre. Em um nível um pouco mais alto, tentar pode resultar na adição de uma cópia alterada do Elemento, já que ele não é mais igual ao que era (este é um problema geral com chaves mutáveis, mas acho que separar a Prioridade e o Elemento irá ajudar a evitar problemas em potencial).

O próprio seletor está sujeito a mudar o comportamento, que é outra maneira que os usuários podem quebrar tudo sem pensar. Muito melhor, eu acho, que os usuários forneçam explicitamente as prioridades. O grande problema que vejo com isso é que transmite que entradas duplicadas são permitidas, pois estamos declarando um par, não um Elemento. No entanto, uma documentação embutida sensata e um valor de retorno de bool em Enqueue devem remediar isso trivialmente. Melhor do que um booleano seria talvez retornar a prioridade antiga / nova do elemento (por exemplo, se Enqueue usa a prioridade fornecida recentemente, ou o mínimo dos dois, ou a prioridade antiga), mas eu acho que Enqueue deve apenas falhar se você tentar adicionar algo novamente e, portanto, deve apenas retornar um bool indicando sucesso. Isso mantém Enqueue e Update completamente separados e bem definidos.

Eu apoiaria não usar KeyValuePair de forma alguma e usar tuplas simples

Estou com você nas tuplas.

Se não permitirmos duplicatas, precisamos decidir o comportamento de tentar adicioná-las novamente com prioridades diferentes / alteradas.

  • Vejo uma semelhança com o indexador em Dictionary . Lá, quando você faz dictionary["something"] = 5 então ele é atualizado se "something" era uma chave lá anteriormente. Se não estava lá, ele apenas é adicionado.
  • No entanto, o método Enqueue é analógico para mim ao método Add do dicionário. O que significa que deve lançar uma exceção.
  • Levando em consideração os pontos acima, podemos considerar a adição de um indexador à fila de prioridade para oferecer suporte ao comportamento que você tem em mente.
  • Mas, por sua vez, um indexador pode não ser algo que funcione com o conceito de filas.
  • O que nos leva à conclusão de que o método Enqueue deve apenas lançar uma exceção se alguém quiser adicionar um elemento duplicado. Da mesma forma, o método Update deve lançar uma exceção se alguém quiser atualizar a prioridade de um elemento que não está presente.
  • O que nos leva a uma nova solução - adicionar o método TryUpdate que realmente retorna bool .

Não apoio a proposta do seletor, pela simples razão de que implica redundância

Não é que a chave não é copiada fisicamente (se existir), mas o seletor permanece apenas uma função que é chamada quando as prioridades precisam ser avaliadas? Onde está a redundância?

Acho que separar a Prioridade e o Elemento ajudará a evitar possíveis problemas

O único problema é quando o cliente não tem a prioridade física. Ele não pode fazer muito então.

Muito melhor, eu acho, que os usuários forneçam explicitamente as prioridades. O grande problema que vejo com isso é que transmite que entradas duplicadas são permitidas, pois estamos declarando um par, não um Elemento.

Vejo alguns problemas com essa solução, mas não necessariamente por que ela indica que entradas duplicadas são permitidas. Acho que a lógica "sem duplicatas" deve ser aplicada em TElement apenas - a prioridade é apenas um valor aqui.

var queue = new PriorityQueue<string, double>();

queue.Enqueue("first", 0.1);
queue.Enqueue("first", 0.5); // should be unsuccessful IMO

@VisualMelon isso faz sentido?

@pgolebiowski

Eu concordo totalmente em adicionar TryUpdate , e Update arremessar faz sentido para mim. Eu estava pensando mais na linha de ISet em relação a Enqueue (ao invés de IDictionary ). Jogar também faria sentido, devo ter perdido esse ponto, mas acho que o retorno bool transmite a 'definição' do tipo. Talvez um TryEnqueue também esteja em ordem (com Add jogando)? A TryRemove também seria apreciado (falha se estiver vazio). Em relação ao seu último ponto, sim, esse é o comportamento que eu tinha em mente também. Suponho que uma analogia com IDictionary seja melhor do que ISet refletindo, e isso deve ser suficientemente claro. (Para resumir: eu apoiaria tudo que fosse lançado de acordo com sua sugestão, mas ter Try* é uma obrigação, se for o caso; também concordo com suas afirmações sobre as condições de falha).

Com relação ao indexador, acho que você está certo de que ele realmente não 'se encaixa' no conceito de fila, eu não apoiaria isso. Se houver alguma coisa, um método bem nomeado para Enfileirar ou Atualizar estaria em ordem.

Não é que a chave não é copiada fisicamente (se existir), mas o seletor permanece apenas uma função que é chamada quando as prioridades precisam ser avaliadas? Onde está a redundância?

Você está certo, eu interpretei mal a atualização da proposta (a parte sobre como armazená-la no Elemento). Partindo do pressuposto de que o Seletor é acionado sob demanda (o que teria que ficar claro em qualquer documentação, pois poderia ter implicações de desempenho), os pontos ainda permanecem que o resultado da função pode mudar sem que a estrutura de dados responda, deixando o dois fora de sincronia, a menos que Update seja chamado. Pior ainda, se um usuário alterar a prioridade efetiva de vários elementos, e apenas atualizar um deles, a estrutura de dados vai acabar dependendo das alterações 'não confirmadas' quando elas são selecionadas de elementos 'não atualizados' (eu não olhei para a implementação da DataStructure proposta em detalhes, mas acho que isso é necessariamente um problema para qualquer atualização O(<n) ). Forçar o usuário a atualizar explicitamente quaisquer prioridades corrige isso, necessariamente levando a estrutura de dados de um estado consistente para outro.

Observe que todas as minhas queixas declaradas até agora com a proposta do Seletor são sobre a robustez da API: Eu acho que o seletor facilita o uso incorreto. No entanto, usabilidade à parte, conceitualmente, os Elementos na Fila não deveriam ter que saber sua prioridade. É potencialmente irrelevante para eles, e se o usuário acabar envolvendo seus Elementos em um struct Queable<T>{} ou algo assim, isso parece uma falha no fornecimento de uma API sem atrito (por mais que me machuque usar o termo).

Você poderia, é claro, argumentar que sem o seletor, o fardo recai sobre o usuário em chamar o seletor, mas acho que se os Elementos souberem sua prioridade, eles serão (com sorte) expostos organizadamente (ou seja, uma propriedade), e se não souberem t, então não há seletor com o qual se preocupar (geração de Prioridades em tempo real, etc.). Há muito mais encanamentos sem sentido necessários para suportar um seletor se você não quiser, do que passar as prioridades se você já tiver um 'seletor' bem definido. O seletor estimula o usuário a expor essas informações (talvez inutilmente), enquanto as prioridades separadas fornecem uma interface extremamente transparente que não posso imaginar que influenciará as decisões de design.

O único argumento que posso realmente pensar em favor do seletor é que isso significa que você pode passar PriorityQueue e outras partes do programa podem usá-lo sem saber como as prioridades são calculadas. Terei que pensar sobre isso um pouco mais, mas dada a adequação do 'nicho', isso me parece uma pequena recompensa pela sobrecarga razoavelmente pesada em casos mais gerais.

_Editar: Tendo pensado mais um pouco, seria muito bom ter PriorityQueue s autossuficientes que você pode simplesmente jogar no Elements, mas eu mantenho que o custo de trabalhar em torno disso seria ótimo, embora o custo de introdução seria consideravelmente menor._

Tenho trabalhado um pouco olhando as bases de código .NET de código aberto para ver exemplos do mundo real de como as filas prioritárias são usadas. O problema complicado é filtrar os projetos dos alunos e o código-fonte do treinamento.

Uso 1: Serviço de Notificação Roslyn
https://github.com/dotnet/roslyn/
O compilador Roslyn inclui uma implementação de fila de prioridade privada chamada "PriorityQueue" que parece ter uma otimização muito específica - uma fila de objetos que reutiliza objetos na fila para evitar que sejam coletados no lixo. O método Enqueue_NoLock executa uma avaliação em current.Value.MinimumRunPointInMS <entry.Value.MinimumRunPointInMS para determinar onde na fila colocar o novo nó. Qualquer um dos dois designs de fila de prioridade principais propostos aqui (função de comparação / delegado vs prioridade explícita) se encaixaria neste cenário de uso.

Uso 2: Lucene.net
https://github.com/apache/lucenenet
Este é sem dúvida o maior exemplo de uso de PriorityQueue que encontrei no .NET. Apache Lucene.net é uma porta .net completa da popular biblioteca do mecanismo de pesquisa Lucene. Usamos a versão Java na minha empresa e, de acordo com o site da Apache, alguns grandes nomes usam a versão .NET. Há um grande número de bifurcações do projeto .NET no Github.

Lucene inclui sua própria implementação PriorityQueue que é subclassificada por várias filas de prioridade "especializadas": HitQueue, TopOrdAndFloatQueue, PhraseQueue e SuggestWordQueue. Da mesma forma, o projeto instancia diretamente PriorityQueue em vários lugares.

A implementação de PriorityQueue da Lucene, vinculada acima, é muito semelhante à API de fila de prioridade original publicada nesta edição. É definido como "PriorityQueue"e aceita um IComparerparâmetro em seu construtor. Métodos e propriedades incluem Count, Clear, Offer (enfileirar / push), Poll (retirar da fila / pop), Peek, Remove (remove o primeiro item correspondente encontrado na fila), Add (sinônimo de Offer). Curiosamente, eles também fornecem suporte de enumeração para a fila.

A prioridade em PriorityQueue da Lucene é determinada pelo comparador passado para o construtor e, se nenhum foi passado, assume que os objetos comparados implementam IComparablee usa essa interface para fazer a comparação. O design da API original postado aqui é semelhante, exceto que também funciona com tipos de valor.

Há um grande número de exemplos de uso através de sua base de código, SloppyPhraseScorer sendo um deles.

O construtor de SloppyPhraseScorer instancia um novo PhraseQueue (pq), que é uma de suas próprias subclasses personalizadas de PriorityQueue. Uma coleção de PhrasePositions é gerada, que parece ser um invólucro para um conjunto de postagens, uma posição e um conjunto de termos. O método FillQueue enumera as posições da frase e as enfileira. PharseFreq () chama uma função AdvancePP e, em um nível alto, parece desenfileirar, atualizar a prioridade de um item e, em seguida, enfileirar novamente. A prioridade é determinada relativamente (usando um comparador) em vez de explicitamente (a prioridade não é "passada" como um segundo parâmetro durante o enfileiramento).

Você pode ver que, com base na implementação de PhraseQueue, um valor de comparação passado por meio do construtor (por exemplo, inteiro) pode não funcionar. Sua função de comparação ("LessThan") avalia três campos diferentes: PhrasePositions.doc, PhrasePositions.position e PhrasePositions.offset.

Uso 3: desenvolvimento de jogos
Não terminei de pesquisar exemplos de uso neste espaço, mas vi alguns exemplos de PriorityQueue do .NET personalizado sendo usado no desenvolvimento de jogos. Em um sentido muito geral, eles tendiam a se agrupar em torno do pathfinding como o caso de uso principal (de Dijkstra). Você pode encontrar muitas pessoas perguntando como implementar algoritmos de pathfinding no .NET devido ao Unity 3D .

Ainda preciso cavar nesta área; vi alguns exemplos com prioridade explícita sendo enfileirados e alguns exemplos usando Comparer / IComparable.

Em uma nota separada, houve alguma discussão em torno de elementos únicos, removendo elementos explícitos e determinando se um elemento específico existe.

Filas, como uma estrutura de dados, geralmente oferecem suporte a Enfileiramento e Retirada da Fila. Se seguirmos o caminho de fornecer outras operações do tipo conjunto / lista, me pergunto se estamos realmente projetando uma estrutura de dados completamente diferente - algo semelhante a uma lista classificada de tuplas. Se um chamador tiver uma necessidade diferente de Enfileirar, Retirar da fila, Peek, talvez ele precise de algo diferente de uma fila prioritária? Fila, por definição, implica a inserção em uma fila e a remoção ordenada da fila; não muito mais.

@ebickle

Agradeço seu esforço em passar por outros repositórios e verificar como a funcionalidade de fila de prioridade foi entregue lá. No entanto, a IMO esta discussão se beneficiaria com o fornecimento de propostas de design específicas . Este segmento já é difícil de seguir e contar uma história longa sem nenhuma conclusão torna ainda mais difícil.

As filas geralmente [...] suportam Enfileiramento e Retirada da Fila. [...] Se um chamador tiver uma necessidade diferente de Enfileirar, Retirar da fila, Peek, talvez ele precise de algo diferente de uma fila prioritária? Fila, por definição, implica a inserção em uma fila e a remoção ordenada da fila; não muito mais.

  • Qual é a conclusão? Proposta?
  • Está muito longe da verdade. Até mesmo o algoritmo de Dijkstra que você mencionou faz uso da atualização de prioridades de elementos. E o que é necessário para atualizar elementos específicos também é necessário para remover elementos específicos.

Ótima discussão. A pesquisa de @ebickle é extremamente útil IMO!
@ebickle , você tem uma conclusão sobre [2] lucene.net - nossa última proposta se encaixa nos usos ou não? (Espero não ter perdido em sua descrição detalhada)

Parece que precisamos de Try* variantes de cima + IsEmpty + TryPeek / TryDequeue + EnqueueOrUpdate ? Pensamentos?
`` `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 precisamos das variantes Try * de cima

Sim, exatamente.

Deve retornar o status bool para enfileirado vs. atualizado?

Se quisermos retornar status em todos os lugares, então em todos os lugares. Para este método específico, acho que deveria ser verdade se qualquer uma das operações fosse bem-sucedida ( Enqueue ou Update ).

Quanto ao resto - eu simplesmente concordo: sorria:

Apenas uma pergunta - por que ref vez de out ? Não tenho certeza:

  • Não precisamos inicializá-lo antes de entrar na função.
  • Não é usado para o método (apenas sai).

Se quisermos retornar status em todos os lugares, então em todos os lugares. Para este método específico, acho que deveria ser verdade se qualquer uma das operações fosse bem-sucedida ( Enqueue ou Update ).

Sempre retornaria verdadeiro, isso não é uma boa ideia. Devemos usar void nesse caso IMO. Caso contrário, as pessoas ficarão confusas e verificarão o valor de retorno e adicionarão um código inútil que nunca será executado. (A menos que eu tenha perdido algo)

ref vs. fora de acordo, eu debati sozinho. Não tenho opinião forte / experiência suficiente para tomar decisões sozinho. Podemos pedir aos revisores da API / aguardar mais comentários.

Sempre retornaria verdadeiro

Você está certo, meu mal. Desculpa.

Não tenho opinião forte / experiência suficiente para tomar decisões sozinho.

Talvez eu esteja faltando alguma coisa, mas acho muito simples. Se usarmos ref , estamos basicamente dizendo que Peek e Dequeue desejam usar de alguma forma os TElement e TPriority passados ​​para ele ( Quero dizer, leia esses campos). O que não é realmente o caso - nossos métodos devem apenas atribuir valores a essas variáveis ​​(e eles são, de fato, solicitados a fazer isso pelo compilador).

Postagem principal atualizada com minhas Try* APIs
Adicionadas 2 questões abertas:

  • [6] O arremesso de Peek e Dequeue útil?
  • [7] TryPeek e TryDequeue - deve usar ref ou out args?

ref vs. out - Você está certo. Eu estava otimizando para evitar a inicialização caso retornássemos falso. Isso foi estúpido e pego de surpresa para mim - otimização prematura. Vou mudar para fora e remover a pergunta.

6: Posso estar faltando alguma coisa, mas se não estiver lançando uma exceção, o que Peek ou Dequeue fazer se a fila estiver vazia? Suponho que isso comece a questionar se a estrutura de dados deve aceitar null (preferiria não, mas sem opinião firme). Mesmo se permitirmos null , os Tipos de Valor não têm null ( default certamente não conta), então Peek e Dequeue têm nenhuma maneira de transmitir um resultado sem sentido e, devidamente, acho que devo lançar uma exceção (removendo assim quaisquer preocupações sobre os parâmetros out !). Não vejo razão para não seguir o exemplo do existente Queue.Dequeue

Vou apenas acrescentar que o Dijkstra (também conhecido como pesquisa heurística sem uma heurística) não deve exigir nenhuma alteração de prioridade. Nunca desejei atualizar a prioridade de nada, e sempre rejeito pesquisas heurísticas. (o ponto do algoritmo é que uma vez que você tenha explorado um estado, você sabe que explorou a melhor rota para ele, caso contrário, corre o risco de não ser o ideal, portanto, você nunca poderia melhorar a prioridade, e certamente não deseja diminuí-lo (ou seja, considere uma rota pior para isso). _Ignorar casos em que você escolhe intencionalmente a heurística de forma que não seja a ideal, caso em que você pode, é claro, obter um resultado não ideal, mas ainda assim nunca atualizar uma prioridade_)

se não estiver lançando uma exceção, o que Peek ou Dequeue deve fazer se a fila estiver vazia?

Verdade.

removendo, assim, quaisquer preocupações sobre os parâmetros de saída!

Como? Bem, os parâmetros out são para os métodos TryPeek e TryDequeue . Os que lançam uma exceção são Peek e Dequeue .

Vou apenas acrescentar que o Dijkstra não deve exigir nenhuma alteração de prioridade.

Posso estar errado, mas até onde sei, o algoritmo de Dijkstra usa a operação DecreaseKey . Veja por exemplo este . Se isso é eficiente ou não, é um aspecto diferente. Na verdade, o heap de Fibonacci foi projetado de forma a atingir a operação DecreaseKey assintoticamente em O (1) (para melhorar Dijkstra).

Mas, ainda assim, o que é importante em nossa discussão - ser capaz de atualizar um elemento em uma fila de prioridade é muito útil e há pessoas que procuram por tal funcionalidade (consulte as perguntas vinculadas anteriormente no StackOverflow). Eu também usei algumas vezes.

Desculpe, sim, vejo o problema com out params agora, leitura incorreta novamente. E parece que uma variante de Dijkstra (um nome que parece ser aplicado de forma mais ampla do que eu acreditava ...) pode ser implementada talvez de forma mais eficiente (se sua fila já contém os Elementos, provavelmente há benefícios para pesquisas repetíveis) com prioridades atualizáveis. É isso que ganho por usar palavras e nomes longos. Observe que não estou propondo descartar Update , seria bom também ter um Heap mais fino ou outro (ou seja, a proposta original) sem as restrições que esse recurso impõe (tão glorioso uma capacidade que eu aprecio).

7: Devem ser out . Qualquer entrada nunca poderia ter qualquer significado, então não deveria existir (ou seja, não deveríamos usar ref ). Com out params, não vamos melhorar o retorno de default valores. Isso é o que Dictionary.TryGetValue faz, e não vejo razão para fazer o contrário. _Dito isso, você poderia tratar ref como valor ou padrão, mas se você não tiver um padrão significativo, isso frustra as coisas._

Discussão de revisão da API:

  • Teremos que ter uma reunião de projeto adequada (2h) para discutir todos os prós / contras, os 30min que investimos hoje não foram suficientes para chegar a um consenso / fechamento nas questões abertas.

    • Provavelmente convidaremos a maioria dos membros da comunidade - @pgolebiowski , mais alguém?

Aqui estão as principais notas brutas:

  • Experimente - devemos fazer experimentos (no CoreFxLabs), lançá-lo como um pacote NuGet de pré-lançamento e pedir feedback aos consumidores (via postagem no blog). Não acreditamos que possamos acertar a API sem loop de feedback.

    • Fizemos algo semelhante para ImmutableCollections no passado (o ciclo de lançamento de visualização rápida foi fundamental e útil para moldar as APIs).

    • Podemos agrupar este experimento com MultiValueDictionary que já está no CoreFxLab. TODO: Verifique se temos mais candidatos, não queremos postar no blog de cada um deles separadamente.

    • O pacote NuGet experimental será nukado após o término do experimento e moveremos o código-fonte para CoreFX ou CoreFXExtensions (a ser decidido posteriormente).

  • Ideia: Retorne apenas TElement de Peek & Dequeue , não retorne TPriority (os usuários podem usar métodos Try* para isso).
  • Estabilidade (para as mesmas prioridades) - itens com as mesmas prioridades devem ser devolvidos na ordem em que foram inseridos na fila (comportamento geral da fila)
  • Queremos habilitar duplicatas (semelhantes a outras coleções):

    • Livre-se de Update - o usuário pode Remove e então Enqueue item de volta com prioridade diferente.

    • Remove deve remover apenas o primeiro item encontrado (como List faz).

  • Ideia: Podemos abstrair IQueue interface para abstrair Queue e PriorityQueue ( Peek e Dequeue retornando apenas TElement ajuda aqui)

    • Observação: pode ser impossível, mas devemos explorá-lo antes de decidirmos pela API

Tivemos uma longa discussão sobre o seletor - ainda não estamos decididos (pode ser exigido por IQueue acima?). A maioria não gostou do seletor, mas as coisas podem mudar na futura reunião de revisão da API.

Outras questões em aberto não foram discutidas.

A última proposta parece muito boa para mim. Minhas opiniões / dúvidas:

  1. Se Peek e Dequeue usassem out parâmetros para priority , então eles também poderiam ter sobrecargas que não retornam prioridade, o que eu acho que simplificaria uso comum. Embora a prioridade também possa ser ignorada usando um descarte, o que torna isso menos importante.
  2. Não gosto que a versão do seletor seja diferenciada por usar um construtor diferente e permitir um conjunto diferente de métodos. Talvez devesse haver um PriorityQueue<T> separado? Ou um conjunto de métodos estáticos e de extensão trabalhando com PriorityQueue<T, T> ?
  3. Está definido em que ordem os elementos são retornados durante a enumeração? Meu palpite é que é indefinido, para torná-lo eficiente.
  4. Deve haver alguma maneira de enumerar apenas os itens na fila de prioridade, ignorando as prioridades? Ou ter que usar algo como priorityQueue.Select(t => t.element) aceitável?
  5. Se a fila de prioridade estiver usando internamente um Dictionary no tipo de elemento, deve haver uma opção para passar um IEqualityComparer<TElement> ?
  6. Rastrear os elementos usando Dictionary é uma sobrecarga desnecessária se eu nunca precisar atualizar as prioridades. Deve haver uma opção para desativá-lo? Embora isso possa ser adicionado mais tarde, se for útil.

@karelz

Estabilidade (para as mesmas prioridades) - itens com as mesmas prioridades devem ser devolvidos na ordem em que foram inseridos na fila (comportamento geral da fila)

Acho que isso significaria que, internamente, a prioridade teria que ser algo como um par de (priority, version) , onde version é incrementado a cada adição. Acho que isso aumentaria significativamente o uso de memória da fila de prioridade, especialmente considerando que version provavelmente teria que ter 64 bits. Não tenho certeza se valeria a pena.

Não queremos evitar valores duplicados (semelhantes a outras coleções):
Livre-se de Update - o usuário pode Remove e então Enqueue item de volta com prioridade diferente.

Dependendo da implementação, Update provavelmente será muito mais eficiente do que Remove seguido por Enqueue . Por exemplo, com heap binário ( acho que heap quaternário tem as mesmas complexidades de tempo), Update (com valores únicos e um dicionário) é O (log n ), enquanto Remove (com valores duplicados e nenhum dicionário) é O ( n ).

@pgolebiowski
Concordo totalmente que precisamos nos concentrar nas propostas aqui; Fiz a proposta de API original e a postagem original. Em janeiro, @karelz pediu alguns exemplos de uso específicos; isso abriu uma questão muito mais ampla em relação à necessidade e uso específicos de uma API PriorityQueue. Tive minha própria implementação de PriorityQueue e usei-a em alguns projetos e senti que algo semelhante seria útil no BCL.

O que faltou na postagem original foi uma ampla pesquisa sobre o uso da fila existente; exemplos do mundo real ajudarão a manter o design aterrado e garantir que ele possa ser amplamente utilizado.

@karelz
Lucene.net contém pelo menos uma função de comparação de fila de prioridade (semelhante a Comparison) que avalia vários campos para determinar a prioridade. O padrão de comparação "implícito" do Lucene não mapeia bem no parâmetro TPriority explícito da proposta de API atual. Seria necessário algum tipo de mapeamento - combinar os vários campos em um ou em uma estrutura de dados 'comparável' que pode ser passada como TPriority.

Proposta:
1) PriorityQueueaula baseada em minha proposta original acima (listada no título Proposta Original). Potencialmente adicione as funções de conveniência Atualizar (T), Remover (T) e Contém (T). Se encaixa na maioria dos exemplos de uso existentes da comunidade de código aberto.
2) PriorityQueuevariante de dotnet / corefx # 1. Dequeue () e Peek () retornam TElement em vez de tupla. Não Try * functiuons, Remove () retorna bool em vez de jogar para ajustar a listapadronizar. Atua como um 'tipo de conveniência' para que os desenvolvedores que tenham um valor de prioridade explícito não precisem criar seu próprio tipo comparável.

Ambos os tipos oferecem suporte a elementos duplicados. Necessidade de determinar se garantimos ou não o PEPS para elementos com a mesma prioridade; provavelmente não se apoiarmos atualizações prioritárias.

Construa ambas as variantes no CoreFxLabs como @karelz sugeriu e solicite feedback.

Perguntas:

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 que não há duplicatas?

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

Se esses métodos são desejáveis, eles são métodos de extensão?

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

Deve jogar; mas por que nenhuma variante Try, também métodos de extensão?

Enumerador baseado em Struct?

@benaadams jogar em

Não devemos adicionar métodos como métodos de extensão quando podemos adicioná-los ao tipo. Os métodos de extensão são backup se não pudermos adicioná-los ao tipo (por exemplo, é interface ou queremos entregá-lo ao .NET Framework mais rápido).

Dupes: Se você tiver várias entradas iguais, você só precisa atualizar uma? Então eles se tornam diferentes?

Métodos de lançamento: basicamente são invólucros e serão?

public void Remove(TElement element)
{
    if (!TryRemove(element))
    {
        throw new Exception();
    }
}

Embora seu controle flua por meio de exceções?

@benaadams verifique minha resposta com notas de revisão de API: https://github.com/dotnet/corefx/issues/574#issuecomment -308206064

  • Queremos habilitar duplicatas (semelhantes a outras coleções):

    • Livre-se de Update - o usuário pode Remove e então Enqueue item de volta com prioridade diferente.

    • Remove deve remover apenas o primeiro item encontrado (como List faz).

Embora seu controle flua por meio de exceções?

Não tenho certeza do que você quer dizer. Parece um padrão bastante comum em BCL.

Embora seu controle flua por meio de exceções?

Não tenho certeza do que você quer dizer.

Se você tiver apenas métodos TryX

  • se você se preocupa com o resultado; você verifica isso
  • se você não se preocupa com o resultado você não verifica

Sem envolvimento de exceção; mas o nome o incentiva a tomar a decisão de jogar fora o retorno.

Se você tiver métodos de lançamento de exceção não-Try

  • se você se preocupa com o resultado; você tem que verificar previamente ou usar um try / catch para detectar
  • se você não se preocupa com o resultado; você tem que esvaziar a captura do método ou obter exceções inesperadas

Portanto, você está no antipadrão de usar tratamento de exceção para controle de fluxo . Eles parecem um pouco supérfluos; sempre pode apenas adicionar um lance se falso para recriar.

Parece um padrão bastante comum em BCL.

Os métodos TryX não eram um padrão comum no BCL original; até os tipos simultâneos (embora pense que Dictionary.TryGetValue em 2.0 pode ter sido o primeiro exemplo?)

por exemplo, os métodos TryX acabaram de ser adicionados ao Core for Queue e Stack e ainda não fazem parte do Framework.

Estabilidade

  • A estabilidade não vem de graça. Isso vem com uma sobrecarga no desempenho e na memória. Para um uso comum, a estabilidade nas filas de prioridade não é importante.
  • Expomos um IComparer . Se o cliente deseja estabilidade, ele pode adicioná-la facilmente. Seria fácil construir StablePriorityQueue em cima de nossa implementação - usando a abordagem comum com o armazenamento da "idade" de um elemento e usando-a durante as comparações.

IQueue

Existem conflitos de API então. Vamos agora considerar um IQueue<T> simples para que funcione com o 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);
}

Temos duas opções para a fila prioritária:

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

Vou escrever as implicações de ambos os caminhos usando simplesmente os números (1) e (2).

Enqueue

Na fila de prioridade, precisamos enfileirar um elemento e sua prioridade (solução atual). Na pilha comum, adicionamos apenas um elemento.

  1. No primeiro caso, expomos Enqueue(TElement) , o que é muito estranho para uma fila de prioridade se estivermos inserindo um elemento sem prioridade. Somos então forçados a fazer ... o quê? Suponha que default(TPriority) ? Nah ...
  2. No segundo caso, expomos Enqueue((TElement, TPriority) element) . Essencialmente, adicionamos dois argumentos aceitando uma tupla criada a partir deles. Provavelmente não é uma API ideal.

Peek & Dequeue

Você queria que esses métodos retornassem TElement apenas.

  1. Trabalha com o que você deseja alcançar.
  2. Não funciona com o que você deseja alcançar - retorna (TElement, TPriority) .

Enumerar

  1. O cliente não pode escrever nenhuma consulta LINQ que faça uso das prioridades dos elementos. Isto está errado.
  2. Funciona.

Poderíamos mitigar alguns dos problemas fornecendo PriorityQueue<T> , onde não há prioridade física separada.

  • O usuário é essencialmente forçado a escrever uma classe de invólucro para que seu código seja capaz de usar nossa classe. Isso significa que, em muitos casos, eles também desejariam implementar IComparable . O que adiciona muito código padrão (e um novo arquivo em seu código-fonte, provavelmente).
  • Obviamente, podemos rediscutir isso, se você quiser. Eu também forneço uma abordagem alternativa abaixo.

Duas filas prioritárias

Se entregarmos duas filas prioritárias, a solução geral será mais poderosa e flexível. Existem algumas notas a serem tomadas:

  • Expomos duas classes para fornecer a mesma funcionalidade, mas apenas para vários tipos de formato de entrada. Não parece melhor.
  • PriorityQueue<T> poderia implementar potencialmente IQueue<T> .
  • PriorityQueue<TElement, TPriority> exporia uma API estranha se tentar implementar a interface IQueue .

Bem ... funcionaria . Embora não seja o ideal.

Atualizando e removendo

Partindo do pressuposto de que queremos permitir duplicatas e não queremos usar o conceito de alça:

  • É impossível fornecer a funcionalidade de atualização de prioridades de elementos de forma totalmente correta (atualizar apenas o nó específico). Analogicamente, aplica-se à remoção de elementos.
  • Além disso, ambas as operações devem ser feitas em O (n), o que é muito triste, porque é possível fazer as duas em O (log n).

Isso basicamente leva ao Java, onde os usuários precisam fornecer sua própria implementação de toda a estrutura de dados se quiserem ser capazes de atualizar as prioridades em suas filas de prioridade sem iterar ingenuamente por toda a coleção todas as vezes.

Alça alternativa

Partindo do pressuposto de que não queremos adicionar um novo tipo de identificador, podemos fazer isso de forma diferente para ter o suporte completo para atualizar e remover elementos (além de fazê-lo de forma eficiente). A solução é adicionar métodos alternativos:

void Enqueue(TElement element, TPriority priority, out object handle);

void Update(object handle, TPriority priority);

void Remove(object handle);

Estes seriam métodos para quem deseja ter um controle adequado na atualização e remoção de elementos (e não o faça em O (n), como se fosse um List : stick_out_tongue_winking_eye :).

Mas melhor, vamos considerar ...

Abordagem alternativa

Como alternativa, podemos remover totalmente essa funcionalidade da fila de prioridade e adicioná-la ao heap . Isso tem vários benefícios:

  • As operações mais poderosas e eficientes estão disponíveis usando Heap .
  • API simples e de uso direto estão disponíveis usando PriorityQueue - para pessoas que desejam que seu código funcione .
  • Não encontramos os problemas de Java.
  • Poderíamos tornar o PriorityQueue estável agora - não é mais uma troca.
  • A solução está em harmonia com a sensação de que pessoas com formação mais sólida em ciência da computação estariam cientes da existência de Heap . Eles também estariam cientes das limitações de PriorityQueue e, portanto, poderiam usar Heap (por exemplo, se quiserem ter mais controle sobre a atualização / remoção de elementos ou não quiserem os dados estrutura para ser estável em detrimento da velocidade e uso de memória).
  • Tanto a fila de prioridade quanto o heap podem facilmente permitir duplicatas sem comprometer suas funcionalidades (porque seus objetivos são diferentes).
  • Seria mais fácil fazer uma interface IQueue - porque o poder e a funcionalidade seriam lançados no campo de heap. A API de PriorityQueue poderia se concentrar em torná-la compatível com Queue por meio de uma abstração.
  • Não precisamos mais fornecer a interface IPriorityQueue (preferimos nos concentrar em manter a funcionalidade de PriorityQueue e Queue semelhantes). Em vez disso, podemos adicioná-lo na área do heap - e essencialmente ter IHeap lá. Isso é ótimo, porque permite que as pessoas construam bibliotecas de terceiros além do que está na biblioteca padrão. E parece certo - porque, novamente, consideramos heaps mais avançados do que filas prioritárias , portanto, extensões seriam fornecidas por essa área. Essas extensões também não sofreriam com as escolhas que faríamos em PriorityQueue , porque seriam separadas.
  • Não precisamos mais considerar o construtor IHeap para o PriorityQueue .
  • A fila de prioridade seria uma classe útil para usar internamente no CoreFX. No entanto, se adicionarmos recursos como estabilidade e eliminarmos algumas outras funcionalidades, provavelmente acabaremos com a necessidade de algo mais poderoso do que essa solução. Felizmente, haveria o mais poderoso e melhor desempenho Heap ao nosso comando!

Basicamente, a fila de prioridade se concentraria principalmente na facilidade de uso, em detrimento de: desempenho, potência e flexibilidade. O heap seria para aqueles que estão cientes do desempenho e das implicações funcionais de nossas decisões na fila de prioridade. Mitigamos muitos problemas com compensações.

Se quisermos fazer um experimento, acho que agora é possível. Vamos apenas perguntar à comunidade. Nem todo mundo lê este tópico - podemos gerar outros comentários valiosos e cenários de uso. O que você acha? Pessoalmente, adoraria essa solução. Acho que faria todo mundo feliz.

Observação importante: se quisermos tal abordagem, precisamos projetar a fila de prioridades e o heap juntos . Porque seus propósitos seriam diferentes e uma solução forneceria o que a outra não.

Entregando IQueue então

Com a abordagem apresentada acima, para que a fila de prioridade implemente IQueue<T> (de forma que faça sentido), e supondo que abandonemos o suporte para o seletor lá, ela teria que ter um tipo genérico. Embora isso signifique que os usuários precisem fornecer um invólucro se tiverem (user data, priority) separadamente, essa solução ainda é intuitiva. E o mais importante, ele permite todos os formatos de entrada (é por isso que tem que ser feito dessa forma se largarmos o seletor). Sem o seletor, Enqueue(TElement, TPriority) não permitiria tipos que já são comparáveis. Um único tipo genérico também é crucial para a enumeração - de forma que esse método possa ser incluído em IQueue<T> .

Diversos

@svick

Está definido em que ordem os elementos são retornados durante a enumeração? Meu palpite é que é indefinido, para torná-lo eficiente.

Para ter a ordem enquanto enumeramos, precisamos essencialmente classificar a coleção. É por isso que sim, deve ser indefinido, já que o cliente pode simplesmente executar OrderBy própria e obter aproximadamente o mesmo desempenho (mas algumas pessoas não precisam disso).

Idéia: na fila de prioridade isso poderia ser ordenado, no heap não ordenado. É uma sensação melhor. De alguma forma, parece que uma fila de prioridades está iterando-a em ordem. Um monte definitivamente não. Outro benefício da abordagem acima.

@pgolebiowski
Isso tudo parece muito lógico. Você se importaria de esclarecer, na seção Delivering IQueue then , você está sugerindo T : IComparable<T> para o "um tipo genérico" como uma alternativa ao seletor, considerando a permissão de elementos duplicados?

Eu apoiaria ter os dois tipos separados.

Não entendo a razão por trás do uso de object como o tipo de identificador: isso é apenas para evitar a criação de um novo tipo? Definir um novo tipo forneceria sobrecarga de implementação mínima, enquanto tornaria a API mais difícil de usar indevidamente (o que está me impedindo de passar um string para Remove(object) ?) E mais fácil de usar (o que está me impedindo de tentar para passar o próprio Elemento para Remove(object) , e quem poderia me culpar por tentar?).

Proponho a adição de um tipo de manequim apropriadamente denominado, para substituir object nos métodos de manipulação, no interesse de uma interface mais expressiva.

Se a depuração supera a sobrecarga de memória, o tipo de identificador pode até incluir informações sobre a qual fila ele pertence (teria que se tornar Genérico, o que aumentaria a segurança de tipo da interface), fornecendo exceções úteis ao longo das linhas de ("O identificador fornecido foi criado por uma fila diferente "), ou se já foi consumido (" Elemento referenciado por Handle já foi removido ").

Se a ideia do manipulador for adiante, eu proporia que, se esta informação for considerada útil, um subconjunto dessas exceções também seria lançado pelos métodos TryRemove e TryUpdate , exceto aquele em que o elemento não está mais presente, porque foi retirado da fila ou removido pelo identificador. Isso implicaria em um tipo de identificador menos enfadonho, genérico e adequadamente denominado.

@VisualMelon

Você se importaria de esclarecer, na seção Delivering IQueue then , você está sugerindo T : IComparable<T> para o "um tipo genérico" como uma alternativa ao seletor, considerando a permissão de elementos duplicados?

Desculpe por não deixar isso claro.

  • Eu quis dizer entregar PriorityQueue<T> , sem restrições sobre T .
  • Ele ainda faria uso de IComparer<T> .
  • Se acontecer de T já ser comparável, então simplesmente Comparer<T>.Default seria assumido (e você pode chamar o construtor padrão da fila de prioridade sem argumentos).
  • O seletor tinha uma finalidade diferente - ser capaz de consumir todos os tipos de dados do usuário. Existem várias configurações:

    1. Os dados do usuário são separados da prioridade (duas instâncias físicas).
    2. Os dados do usuário contêm a prioridade.
    3. Os dados do usuário são a prioridade.
    4. Caso raro: a prioridade pode ser obtida por meio de alguma outra lógica (reside em um objeto diferente dos dados do usuário).

    O caso raro não seria possível em PriorityQueue<T> , mas isso não importa muito. O importante é que agora podemos lidar com (1), (2) e (3). No entanto, se tivéssemos dois tipos genéricos, teríamos que ter um método como Enqueue(TElement, TPrioriity) . Isso nos limitaria a apenas (1). (2) levaria à redundância. (3) seria incrivelmente feio. Há um pouco mais sobre isso na seção IQueue > Enqueue acima (segundo método Enqueue e default(TPriority ).

Espero que esteja mais claro agora.

BTW, assumindo tal solução, projetar a API de PriorityQueue<T> e IQueue<T> seria trivial. Basta pegar alguns dos métodos em Queue<T> , jogá-los em IQueue<T> e fazer com que PriorityQueue<T> implemente. Tadaa! 😄

Não entendo a razão por trás do uso de object como o tipo de identificador: isso é apenas para evitar a criação de um novo tipo?

  • Sim, exatamente. Supondo que não queremos expor esse tipo, é a única solução que ainda usa o conceito de alça (e, como tal, tem mais potência e velocidade). Mas concordo que não é o ideal - foi antes um esclarecimento sobre o que teria de acontecer se nos mantivéssemos na abordagem atual e quiséssemos ter mais potência e eficiência (o que sou contra).
  • Considerando que iríamos com a abordagem alternativa (fila de prioridade separada e heaps), isso é mais fácil. Poderíamos deixar PriorityQueue<T> residir em System.Collections.Generic , enquanto a funcionalidade de heap ficaria em System.Collections.Specialized . Lá, teríamos uma chance maior de introduzir tal tipo, eventualmente tendo a maravilhosa detecção de erros em tempo de compilação.
  • Mas, mais uma vez - é muito importante projetar a fila de prioridade e a funcionalidade de heap juntos se quisermos ter essa abordagem. Porque uma solução oferece o que a outra não.

Se a depuração supera a sobrecarga de memória, o tipo de identificador pode até incluir informações sobre a qual fila ele pertence (teria que se tornar Genérico, o que aumentaria a segurança de tipo da interface), fornecendo exceções úteis ao longo das linhas de ("O identificador fornecido foi criado por uma fila diferente "), ou se já foi consumido (" Elemento referenciado por Handle já foi removido ").

Se a ideia do identificador for adiante, eu proporia que, se essa informação for considerada útil ...

Definitivamente mais é possível então: wink :

@karelz @safern @ianhays @terrajobst @bendono @svick @ alexey- dvortsov @SamuelEnglard @ xied75 e outros - você acha que tal abordagem faria sentido (conforme descrito neste e neste post)? Isso satisfaria todas as suas expectativas e necessidades?

Acho que a ideia de criar duas classes faz muito sentido e resolve muitos problemas. Embora eu tenha feito isso, pode fazer sentido PriorityQueue<T> usar Heap<T> internamente e apenas "ocultar" sua funcionalidade avançada.

Então, basicamente...

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

  • Em System.Collections.Generic .
  • Idéia: também poderíamos adicionar métodos para remover elementos aqui ( Remove e TryRemove ). Porém, não está em Queue<T> . Mas não é necessário.

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

  • Em System.Collections.Generic .
  • Se IComparer<T> não for entregue, Comparer<T>.Default é convocado.
  • É estável.
  • Permite duplicatas.
  • Remove e TryRemove removem apenas a primeira ocorrência (se a encontrarem).
  • A enumeração é ordenada.

Montes

Não estou escrevendo tudo aqui agora, mas:

  • Em System.Collections.Specialized .
  • Não seria estável (e como tal mais rápido e eficiente em termos de memória).
  • Lidar com o suporte, atualização e remoção adequadas

    • feito rapidamente em O (log n) em vez de O (n)

    • feito corretamente

  • A enumeração não é ordenada (mais rápida).
  • Permite duplicatas.

Concordo com o IQueueproposta. Eu ia propor a mesma coisa hoje, parece o nível certo de abstração para se ter no nível da interface. "Uma interface para uma estrutura de dados que pode ter itens adicionados e itens solicitados removidos."

  • A especificação para IQueueparece bom.

  • Considere adicionar "int Count {get;}" a IQueuepara que fique claro, a contagem é desejada, independentemente de herdarmos ou não de IReadOnlyCollection.

  • Em cima do muro em relação ao TryPeek, TryDequeue in IQueueconsiderando que eles não estão na fila, mas esses auxiliares provavelmente também devem ser adicionados à Fila e à Pilha.

  • IsEmpty parece um outlier; não há muitos outros tipos de coleção no BCL. Para adicioná-lo à interface, teríamos que assumir que ele será adicionado à fila, e parece um pouco estranho adicioná-lo à filae nada mais. Recomendando que o retiremos da interface e talvez da classe também.

  • Largue TryRemove e altere Remove para "bool Remove". Manter o alinhamento com as outras classes de coleção será importante aqui - os desenvolvedores terão muita memória muscular que diz "remove () em uma coleção não joga". É uma área que muitos desenvolvedores não testarão bem e causará muitas surpresas se o comportamento normal for alterado.

De sua citação anterior @pgolebiowski

  1. Os dados do usuário são separados da prioridade (duas instâncias físicas).
  2. Os dados do usuário contêm a prioridade.
  3. Os dados do usuário são a prioridade.
  4. Caso raro: a prioridade pode ser obtida por meio de alguma outra lógica (reside em um objeto diferente dos dados do usuário).

Também recomendamos considerar 5. Os dados do usuário contêm a prioridade em vários campos (como vimos em Lucene.net)

Em dúvida sobre TryPeek, TryDequeue em IQueue, considerando que eles não estão na fila

Eles são System / Collections / Generic / Queue.cs # L253-L295

Por outro lado, a fila não tem
c# public void Remove(T element); public bool TryRemove(T element);

Considere adicionar "int Count {get;}" a IQueue para que fique claro que Count é desejado, independentemente de herdarmos ou não de IReadOnlyCollection.

OK. Irá modificá-lo.

IsEmpty parece um outlier; não há muitos outros tipos de coleção no BCL.

Este foi adicionado por @karelz , acabei de copiá-lo. Eu gosto disso, porém, pode ser considerado na análise da API :)

Largue TryRemove e altere Remove para "bool Remove".

Acho que Remove e TryRemove é consistente com outros métodos como esse ( Peek e TryPeek ou Dequeue e TryDequeue )

  1. Os dados do usuário contêm a prioridade em vários campos

É um ponto válido, mas na verdade também pode ser tratado com um seletor (afinal, é qualquer função) - mas isso é para muitos.

IsEmpty parece um outlier; não há muitos outros tipos de coleção no BCL.

FWIW, bool IsEmpty { get; } é algo que eu gostaria de ter adicionado a IProducerConsumerCollection<T> , e me arrependi de não ter estado lá várias vezes desde então. Sem ele, os wrappers geralmente precisam fazer o equivalente a Count == 0 , que para algumas coleções é significativamente menos eficiente de implementar, em particular na maioria das coleções simultâneas.

@pgolebiowski O que você acha da ideia de criar um repositório github para hospedar os contratos de API atuais e um ou dois arquivos .md contendo a lógica do design. Depois de estabilizado, ele pode ser usado como um local para construir a implementação inicial antes de fazer um PR até o CoreFxLabs quando estiver pronto?

@svick

Se Peek e Dequeue usassem out parâmetros para prioridade, então eles também poderiam ter sobrecargas que não retornam prioridade, o que eu acho que simplificaria o uso comum

Concordou. Vamos adicioná-los.

Deve haver alguma maneira de enumerar apenas os itens na fila de prioridade, ignorando as prioridades?

Boa pergunta. O que você propõe? IEnumerable<TElement> Elements { get; } ?

Estabilidade - acho que isso significaria que, internamente, a prioridade teria que ser algo como um par de (priority, version)

Acho que podemos evitar isso considerando Update tão lógico Remove + Enqueue . Gostaríamos de adicionar itens sempre ao final dos itens de mesma prioridade (considere basicamente o resultado 0 do comparador como -1). IMO que deve funcionar.


@benaadams

Os métodos TryX não eram um padrão comum no BCL original; até os tipos simultâneos (embora pense que Dictionary.TryGetValue em 2.0 pode ter sido o primeiro exemplo?)
por exemplo, os métodos TryX acabaram de ser adicionados ao Core for Queue e Stack e ainda não fazem parte do Framework.

Admito que ainda sou novo no BCL. Pelas reuniões de revisão da API e pelo fato de adicionarmos vários métodos Try* recentemente, tive a impressão de que é um padrão comum por muito mais tempo 😉.
De qualquer forma, é um padrão comum agora e não devemos ter medo de usá-lo. O fato de o padrão ainda não estar no .NET Framework não deve nos impedir de inovar no .NET Core - esse é seu objetivo principal, inovar mais rápido.


@pgolebiowski

Como alternativa, podemos remover totalmente essa funcionalidade da fila de prioridade e adicioná-la ao heap. Isso tem vários benefícios

Hmm, algo me diz que você pode ter uma agenda aqui 😆
Agora, falando sério, é na verdade uma boa direção que buscamos o tempo todo - PriorityQueue nunca teve a intenção de ser uma razão para NÃO fazer Heap . Se estivermos todos ok com o fato de que Heap pode não entrar no CoreFX e pode ficar "apenas" no repositório CoreFXExtensions como estrutura de dados avançada junto com PowerCollections, estou bem com isso,

Observação importante: se quisermos tal abordagem, precisamos projetar a fila de prioridades e o heap juntos. Porque seus propósitos seriam diferentes e uma solução forneceria o que a outra não.

Não vejo por que precisamos fazer isso juntos. IMO, podemos nos concentrar em PriorityQueue e adicionar Heap "apropriados" em paralelo / mais tarde. Não me importo se alguém os fizer junto, mas não vejo nenhuma razão forte para que a existência de PriorityQueue fáceis de usar deva afetar o design do avançado Heap "adequado / alto desempenho"

IQueue

Obrigado por escrever. Dados seus pontos, não acho que devemos sair do nosso caminho para adicionar IQueue . IMO, é bom ter. Se tivéssemos um seletor, seria natural. No entanto, não gosto da abordagem do seletor, pois traz estranheza e complicação ao descrever quando o seletor é chamado pelo PriorityQueue (somente em Enqueue e Update .

Identificador alternativo (objeto)

Na verdade, não é uma má ideia (embora um pouco feia) ter tais sobrecargas IMO. Teríamos que ser capazes de detectar que o identificador está errado PriorityQueue , que é O (log n).
Tenho a sensação de que os revisores da API irão rejeitá-lo, mas IMO, vale a pena tentar ...

Estabilidade

Não acho que a estabilidade venha com qualquer sobrecarga de desempenho / memória (assumindo que já perdemos Update ou que tratamos Update como Remove + Enqueue lógico, então basicamente redefinimos a idade do elemento). Apenas trate o resultado 0 do comparador como -1 e tudo estará bem ... Ou estou perdendo alguma coisa?

Seletor e IQueue<T>

Pode ser uma boa ideia ter 2 propostas (e podemos potencialmente aceitar ambas):

  • PriorityQueue<T,U> sem seletor e sem IQueue (o que seria complicado)
  • PriorityQueue<T> com seletor e IQueue

Algumas pessoas sugeriram isso acima.

Re: última proposta de @pgolebiowski - https://github.com/dotnet/corefx/issues/574#issuecomment -308427321

IQueue<T> - poderíamos adicionar Remove e TryRemove , mas Queue<T> não os tem.

Eles fariam sentido serem adicionados a Queue<T> ? ( @ebickle concorda) Se sim, devemos agrupar a adição também.
Vamos adicioná-lo na interface e dizer que eles também são questionáveis ​​/ precisam de Queue<T> adição.
O mesmo para IsEmpty - o que quer que precise de Queue<T> e Stack<T> adições, vamos marcá-lo assim na interface (será mais fácil de revisar e digerir).
@pgolebiowski você pode adicionar um comentário a IQueue<T> com a lista de classes pelas quais achamos que será implementado?

PriorityQueue<T>

Vamos adicionar o enumerador de estrutura (acho que é um padrão BCL comum ultimamente ). Ele foi chamado algumas vezes na lista de discussão e, em seguida, descartado / esquecido.

Montes

Escolha de namespace: não vamos perder tempo com decisões de namespace ainda. Se Heap acabar em CoreFxExtensions, não sabemos ainda que tipo de namespaces permitiremos lá. Talvez Community.* ou algo parecido. Depende do resultado das discussões de propósito / modo de operação do CoreFxExtensions.
Observação: uma ideia para o repositório CoreFxExtensions é dar aos membros da comunidade permissões de gravação e deixá-lo ser conduzido principalmente pela comunidade com a equipe .NET fornecendo apenas conselhos (incluindo experiência em revisão de API) e a equipe .NET / MS sendo o árbitro quando necessário. Se for onde pousamos, provavelmente não o quereríamos no namespace System.* ou Microsoft.* . (Aviso: pensamento inicial, por favor, não tire conclusões precipitadas ainda, existem outras ideias de governança alternativas em andamento)

Largue TryRemove e altere Remove para bool Remove . Manter o alinhamento com as outras classes de coleção será importante aqui - os desenvolvedores terão muita memória muscular que diz "remove () em uma coleção não joga". É uma área que muitos desenvolvedores não testarão bem e causará muitas surpresas se o comportamento normal for alterado.

👍 Devemos definitivamente considerar pelo menos alinhá-lo com outras coleções. Alguém pode escanear outras coleções - qual é seu padrão Remove ?

@ebickle O que você acha da ideia de criar um repositório github para hospedar os contratos de API atuais e um ou dois arquivos .md contendo a lógica do design.

Vamos hospedá-lo diretamente no CoreFxLabs . 👍

@karelz

Deve haver alguma maneira de enumerar apenas os itens na fila de prioridade, ignorando as prioridades?

Boa pergunta. O que você propõe? IEnumerable<TElement> Elements { get; } ?

Sim, isso ou uma propriedade devolvendo algum tipo de ElementsCollection é provavelmente a melhor opção. Especialmente porque é semelhante a Dictionary<K, V>.Values .

Gostaríamos de adicionar itens sempre ao final dos itens de mesma prioridade (considere basicamente o resultado 0 do comparador como -1). IMO que deve funcionar.

Não é assim que os heaps funcionam, não há "fim dos itens com a mesma prioridade". Itens com a mesma prioridade podem ser espalhados por todo o heap (a menos que estejam próximos de ser o mínimo atual).

Ou, dito de outra forma, para manter a estabilidade, você deve considerar o resultado do comparador de 0 como -1 não apenas ao inserir, mas também quando os itens são movidos posteriormente na pilha. E eu acho que você tem que armazenar algo como version junto com priority para fazer isso corretamente.

IQueue- poderíamos adicionar Remove e TryRemove, mas Queuenão os tem.

Eles fariam sentido para serem adicionados à fila? ( @ebickle concorda) Se sim, devemos agrupar a adição também.

Não acho que Remove deva ser adicionado a IQueue<T> ; e até sugeriria que Contains é duvidoso; ele limita a utilidade da interface e para quais tipos de Fila ela pode ser usada, a menos que você também comece a lançar NotSupportedExceptions. ou seja, qual é o escopo?

É apenas para filas vanilla, filas de mensagens, filas distribuídas, filas ServiceBus, filas de armazenamento do Azure, filas confiáveis ​​ServiceFabric etc ... (ignorando alguns deles preferindo métodos assíncronos)

Não é assim que os heaps funcionam, não há "fim dos itens com a mesma prioridade". Itens com a mesma prioridade podem ser espalhados por todo o heap (a menos que estejam próximos de ser o mínimo atual).

Ponto justo. Eu estava pensando nisso como uma árvore de pesquisa binária. Tenho que escovar meu ds básico, eu acho :)
Bem, ou nós o implementamos com diferentes ds (array, árvore de busca binária (ou qualquer que seja o nome oficial) - @stephentoub tem idéias / sugestões), ou optamos por uma ordem aleatória. Não acho que manter o campo version ou age valha a garantia de estabilidade.

@ebickle O que você acha da ideia de criar um repositório github para hospedar os contratos de API atuais e um ou dois arquivos .md contendo a lógica do design.

@karelz Vamos hospedá-lo diretamente no CoreFxLabs.

Por favor, dê uma olhada neste documento .

Pode ser uma boa ideia ter 2 propostas (e podemos potencialmente aceitar ambas):

  • Fila de prioridadesem seletor e sem IQueue (o que seria complicado)
  • Fila de prioridadecom seletor e IQueue

Eu me livrei completamente do seletor. É necessário apenas se quisermos ter um PriorityQueue<T, U> unificado. Se tivermos duas classes - bem, um comparador é o suficiente.


Informe-nos se encontrar algo que valha a pena adicionar / alterar / remover.

@pgolebiowski

O documento parece bom. Começando a parecer uma solução sólida que eu esperava ver nas bibliotecas do núcleo .NET :)

  • Deve adicionar uma classe Enumerator aninhada. Não tenho certeza se a situação mudou, mas anos atrás, a estrutura filha evitou uma alocação do coletor de lixo causada pelo boxing do resultado de GetEnumerator (). Consulte https://github.com/dotnet/coreclr/issues/1579 por exemplo.
    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(); } }

  • Fila de prioridadedeve ter uma estrutura Enumerator aninhada também.

  • Ainda estou votando para ter "public bool Remove (elemento T)" sem o TryRemove, pois é um padrão de longa data e alterá-lo torna os erros do desenvolvedor altamente prováveis. Podemos deixar a equipe de revisão da API entrar na conversa, mas é uma questão em aberto em minha mente.

  • Existe algum valor em especificar a capacidade inicial no construtor ou ter uma função TrimExcess, ou isso é uma micro-otimização agora - especialmente considerando o IEnumerableparâmetros do construtor?

@pgolebiowski obrigado por colocá-lo em um documento. Você pode enviar seu documento como PR no branch master do CoreFxLab? Marque @ianhays @safern, eles poderão mesclar os PRs.
Podemos então usar os problemas do CoreFxLab para discussões adicionais sobre pontos de design específicos - deve ser mais fácil do que este mega-problema (acabei de enviar o e-mail para criar o area-PriorityQueue lá).

Por favor, coloque um link para a solicitação de pull CoreFxLab aqui?

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

Se a API for aprovada em sua forma atual (duas classes), eu criaria outro PR para CoreFXLab com uma implementação para ambos usando um heap quaternário ( ver implementação ). O PriorityQueue<T> usaria apenas um array abaixo, enquanto PriorityQueue<TElement, TPriority> usaria dois - e reorganizaria seus elementos juntos. Isso pode nos poupar de alocações adicionais e ser o mais eficiente possível. Vou acrescentar isso quando houver luz verde.

Existe algum plano de fazer uma versão thread-safe como ConcurrentQueue?

Eu: heart: para ver uma versão simultânea disso, implementando IProducerConsumerCollection<T> , para que possa ser usado com BlockingCollection<T> etc.

@aobatact @khellang Parece um tópico completamente diferente:

Eu concordo que é muito valioso: wink :!

public bool IsEmpty();

Por que isso é um método? Todas as outras coleções no framework que possuem IsEmpty possuem como uma propriedade.

Eu atualizei a proposta na postagem superior com IsEmpty { get; } .
Também adicionei um link para o documento da proposta mais recente no repositório corefxlab.

Olá a todos,
Eu acho que há muitas vozes argumentando que seria bom apoiar a atualização, se possível. Acho que ninguém acabou duvidando que é um recurso útil para pesquisas gráficas.

Mas houve algumas objeções levantadas aqui e ali. Para verificar meu entendimento, parece que as principais objeções são:
-não está claro como a atualização deve funcionar quando há elementos duplicados.
- há algum argumento sobre se elementos duplicados são até desejáveis ​​para suporte em uma fila de prioridade
- há alguma preocupação de que pode ser ineficiente ter uma estrutura de dados de pesquisa adicional para localizar elementos na estrutura de dados de apoio apenas para atualizar sua prioridade. Especialmente para cenários onde a atualização nunca é realizada! E / ou afetar as garantias de desempenho de pior caso ...

Algo que eu perdi?

OK, acho que mais um foi - foi afirmado que atualizar / remover semanticamente significando 'updateFirst' ou 'removeFirst' pode ser estranho. :)

Em qual versão do .Net Core podemos começar a usar o PriorityQueue?

@memoryfraction .NET Core propriamente dito não tem um PriorityQueue ainda (há propostas - mas não tivemos tempo para dedicar a isso recentemente). No entanto, não há razão para que ele esteja na distribuição do Microsoft .NET Core. Qualquer pessoa na comunidade pode colocar um no NuGet. Talvez alguém neste assunto possa sugerir um.

@memoryfraction .NET Core propriamente dito não tem um PriorityQueue ainda (há propostas - mas não tivemos tempo para dedicar a isso recentemente). No entanto, não há razão para que ele esteja na distribuição do Microsoft .NET Core. Qualquer pessoa na comunidade pode colocar um no NuGet. Talvez alguém neste assunto possa sugerir um.

Obrigado pela sua resposta e sugestão.
Quando eu uso o C # para praticar o leetcode e preciso usar SortedSet para lidar com o conjunto com elementos duplicados. Tenho que envolver o elemento com uma identificação exclusiva para resolver o problema.
Portanto, prefiro ter a Fila de prioridade no .NET Core no futuro porque será mais conveniente.

Qual é o estado atual desse problema? Acabei de me descobrir recentemente exigindo um PriorityQueue

Esta proposta não habilita a inicialização da coleção por padrão. PriorityQueue<T> não tem um método Add . Acho que a inicialização da coleção é um recurso de linguagem grande o suficiente para garantir a adição de um método duplicado.

Se alguém tiver um heap binário rápido que gostaria de compartilhar, gostaria de compará-lo com o que escrevi.
Implementei totalmente a API conforme proposto. No entanto, em vez de um heap, ele usa uma matriz classificada simples. Eu mantenho o item de menor valor no final para que seja classificado no reverso da ordem normal. Quando o número de itens é inferior a 32, uso uma pesquisa linear simples para descobrir onde inserir novos valores. Depois disso, usei uma pesquisa binária. Usando dados aleatórios, descobri que essa abordagem é mais rápida do que um heap binário no caso simples de preencher a fila e, em seguida, esvaziá-la.
Se eu tiver tempo, vou colocá-lo em um repositório git público para que as pessoas possam criticá-lo e atualizar este comentário com a localização.

@SunnyWar Estou usando: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
que tem bom desempenho. Acho que faria sentido testar sua implementação com o GenericPriorityQueue

Por favor, desconsidere minha postagem anterior. Encontrei um erro no teste. Uma vez corrigido, o heap binário tem o melhor desempenho.

@karelz Onde está esta proposta atualmente em ICollection<T> ? Não entendi por que isso não seria compatível, embora a coleção obviamente não tenha o objetivo de ser somente leitura.

@karelz re a questão em aberto da ordem de

@karelz re a ordem de enumeração ... que tal ter um método Sort() no tipo que colocará o heap de dados internos da coleção em ordem de classificação local (em O (n log n)). E chamar Enumerate() após Sort() enumera a coleção em O (n). E se Sort () NÃO for chamado, ele retornará elementos em ordem não classificada em O (n)?

Triagem: avançar para o futuro, pois não parece haver consenso sobre se devemos implementá-la.

É muito decepcionante ouvir isso. Eu ainda tenho que usar soluções de terceiros para isso.

Quais são os principais motivos pelos quais você não gosta de usar uma solução de terceiros?

a estrutura de dados heap é OBRIGATÓRIA para fazer leetcode
mais leetcode, mais entrevista de código c #, o que significa mais desenvolvedores c #.
mais desenvolvedores significa melhor ecossistema.
melhor ecossistema significa que se ainda podemos programar em c # amanhã.

em suma: este não é apenas um recurso, mas também o futuro. é por isso que o problema é rotulado como 'futuro'.

Quais são os principais motivos pelos quais você não gosta de usar uma solução de terceiros?

Porque quando não há um padrão, cada um inventa o seu, cada um com seu conjunto de peculiaridades.

Muitas vezes desejei um System.Collections.Generic.PriorityQueue<T> porque é relevante para algo em que estou trabalhando. Esta proposta já existe há 5 anos. Por que ainda não aconteceu?

Por que ainda não aconteceu?

Inanição prioritária, sem trocadilhos. Projetar coleções é um trabalho porque, se as colocarmos no BCL, temos que pensar em como elas são trocadas, quais interfaces implementam, quais são as semânticas etc. Nada disso é ciência do foguete, mas leva um tempo. Além disso, você precisa de um conjunto de casos de usuários e clientes concretos para avaliar o design, o que fica mais difícil à medida que as coleções se tornam cada vez mais especializadas. Até agora, sempre houve outro trabalho que foi considerado mais importante do que esse.

Vejamos um exemplo recente: coleções imutáveis. Eles foram projetados por alguém que não trabalha na equipe BCL para um caso de uso no VS que era em torno da imutabilidade. Trabalhamos com ele para obter as APIs "BCL-ified". E quando Roslyn ficou online, substituímos suas cópias pelas nossas em tantos lugares quanto pudemos e ajustamos o design (e implementação) muito com base em seus comentários. Sem um cenário de "herói", isso é difícil.

Porque quando não há um padrão, cada um inventa o seu, cada um com seu conjunto de peculiaridades.

@masonwheeler é isso que você viu por PriorityQueue<T> ? Que existem várias opções de terceiros que não são intercambiáveis ​​e nenhuma "melhor biblioteca para a maioria" claramente aceita? (Eu não fiz a pesquisa, então não sei a resposta)

@eiriktsarpalis Como não há consenso sobre a implementação?

@terrajobst Você realmente precisa de um cenário de herói quando este é um padrão bem conhecido com aplicativos bem conhecidos? Muito pouca inovação deve ser exigida aqui. Já existe até uma especificação de interface totalmente desenvolvida. Na minha opinião, a verdadeira razão pela qual este utilitário não existe não é que seja muito difícil, nem que não haja demanda ou caso de uso para ele. A verdadeira razão é que, para qualquer projeto de software real, é mais fácil simplesmente construir um você mesmo do que tentar travar uma campanha política para que isso seja colocado nas bibliotecas do framework.

Quais são os principais motivos pelos quais você não gosta de usar uma solução de terceiros?

@terrajobst
Eu pessoalmente fiz a experiência de que as soluções de terceiros nem sempre funcionam conforme o esperado / não usam os recursos de linguagem atuais. Com uma versão padronizada, i (como usuário) pode ter certeza de que o desempenho é o melhor que você pode obter.

@danmosemsft

@masonwheeler é isso que você viu por PriorityQueue<T> ? Que existem várias opções de terceiros que não são intercambiáveis ​​e nenhuma "melhor biblioteca para a maioria" claramente aceita? (Eu não fiz a pesquisa, então não sei a resposta)

sim. Apenas google "fila de prioridade C #"; a primeira página está cheia de:

  1. Implementações de fila prioritárias no Github e outros sites de hospedagem
  2. Pessoas perguntando por que diabos não há fila oficial de prioridade em Collections.Generic
  3. Tutoriais sobre como construir sua própria implementação de fila prioritária

@terrajobst Você realmente precisa de um cenário de herói quando este é um padrão bem conhecido com aplicativos bem conhecidos? Muito pouca inovação deve ser exigida aqui.

Na minha experiência, sim. O diabo está nos detalhes e, uma vez que enviamos uma API, não podemos realmente fazer alterações significativas. E há muitas opções de implementação visíveis ao usuário. Podemos visualizar a API como um OOB por um tempo, mas também aprendemos que, embora possamos certamente obter feedback, a falta de um cenário de herói significa que você não tem nenhum desempate, o que geralmente resulta em uma situação em que o herói cenário chega, a estrutura de dados não está atendendo aos seus requisitos.

Aparentemente , precisamos de um herói. 😛

@masonwheeler Presumi que seu link seria para isso 😄 Agora está na minha cabeça.

Embora, como diz @terrajobst , nosso principal problema aqui tenha sido recursos / atenção (e você pode nos culpar por isso, se quiser), também gostaríamos de fortalecer o ecossistema não-Microsoft para que seja mais provável que você possa encontrar bibliotecas fortes para qualquer coisa cenário.

[editado para maior clareza]

@danmosemsft Nah, se eu fosse escolher essa música, faria a versão Shrek 2.

Candidato do aplicativo Hero nº 1: que tal usar um em TimerQueue.Portable?

Já foi considerado, prototipado e descartado. Isso torna o caso muito comum de um cronômetro sendo rapidamente criado e destruído (por exemplo, para um tempo limite) menos eficiente.

@stephentoub Presumo que você queira dizer que é menos eficiente para alguns cenários em que há um pequeno número de temporizadores. Mas como isso aumenta?

Presumo que você queira dizer que é menos eficiente para alguns cenários em que há um pequeno número de temporizadores. Mas como isso aumenta?

Não, eu quis dizer que o caso comum é que você tem muitos temporizadores a qualquer momento, mas muito poucos estão disparando. Isso é o que ocorre quando os temporizadores são usados ​​para tempos limite. E o importante aí é a velocidade na qual você pode adicionar e remover da estrutura de dados ... você quer que seja 0 (1) e com sobrecarga muito baixa. Se isso se tornar O (log N), é um problema.

Ter uma fila de prioridade definitivamente tornará o C # mais amigável para entrevistas.

Ter uma fila de prioridade definitivamente tornará o C # mais amigável para entrevistas.

Sim, é verdade.
Estou procurando pelo mesmo motivo.

@stephentoub Para os tempos limite que nunca acontecem, isso faz todo o sentido para mim. Mas eu me pergunto o que acontece com o sistema quando de repente muitos timeouts começam a acontecer, porque de repente há perda de pacotes ou um servidor que não responde ou algo assim? Também os temporizadores recorrentes ala System.Timer usam a mesma implementação? Lá, o tempo limite expirando seria o 'caminho feliz'.

Mas eu me pergunto o que acontece com o sistema quando, de repente, muitos timeouts começam a acontecer

Tente. :)

Vimos muitas cargas de trabalho reais sofrendo com a implementação anterior. Em todos os casos que vimos, o novo (que não usa uma fila de prioridade, mas em vez disso apenas tem uma divisão simples entre temporizadores para breve e não para breve) corrigiu o problema, e não vimos novos com isto.

Também os temporizadores recorrentes ala System.Timer usam a mesma implementação?

sim. Mas eles geralmente estão espalhados, muitas vezes de forma bastante uniforme, em aplicativos do mundo real.

5 anos depois, ainda não há PriorityQueue.

5 anos depois, ainda não há PriorityQueue.

Não deve ser uma prioridade alta o suficiente ...

@stephentoub mas talvez na verdade @eiriktsarpalis , nós realmente temos alguma tração nisso agora? Se tivermos um design de API finalizado, estaria disposto a trabalhar nele

Eu não vi uma declaração sobre isso ainda e não tenho certeza se pode haver um design de API final sem um aplicativo killer designado. Mas...
presumindo que o principal candidato a aplicativo seja a programação de concursos / ensino / entrevistas, acho que o design de Eric no topo parece útil o suficiente ... e ainda tenho minha contra-proposta em andamento (recentemente revisada, ainda não retirada!)

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

As principais diferenças que estou notando entre eles são se deve tentar falar como um dicionário e se os seletores devem ser uma coisa.

Estou começando a esquecer exatamente por que queria que fosse um dicionário. Ter um atribuidor indexado para atualizar as prioridades e ser capaz de enumerar as chaves com suas prioridades parecia uma coisa boa e muito semelhante a um dicionário para mim. Poder visualizá-lo como um dicionário no depurador também pode ser ótimo.

Acho que os seletores realmente mudam drasticamente a interface de classe esperada, FWIW. O seletor deve ser algo embutido no momento da construção e, se você o tiver, elimina a necessidade de qualquer um passar um valor de prioridade para outra pessoa - eles devem apenas chamar o seletor se quiserem saber a prioridade. Portanto, você não gostaria de ter parâmetros de prioridade em nenhuma assinatura de método, exceto basicamente no seletor. Nesse caso, torna-se uma espécie de classe 'PrioritySet' mais especializada. [Para os quais seletores impuros são um possível problema de bug!]

@TimLovellSmith Eu entendo o desejo por esse comportamento, mas como esta foi uma pergunta de 5 anos, não seria razoável apenas implementar um heap binário baseado em array com um seletor e compartilhando a mesma área de superfície de API de Queue.cs ? Acho que é de longe o que a maioria dos usuários precisa. Na minha opinião, essa estrutura de dados não foi implementada porque não podemos concordar com um design de API, mas se estivermos fazendo um PriorityQueue , acredito que devemos apenas emular o design de API de Queue .

@Jlalond Daí a proposta aqui, hein? Não tenho nenhuma objeção à implementação de array com base em heap. Eu agora me lembro da maior objeção que eu tinha contra os seletores, é que não é fácil fazer atualizações de prioridade certas . Uma fila simples e antiga não tem uma operação de atualização de prioridade, mas atualizar a prioridade do elemento é uma operação importante para muitos algoritmos de fila de prioridade.
As pessoas ficarão eternamente gratas se optarmos por uma API que não exija que depurem estruturas de fila de prioridade corrompidas.

@TimLovellSmith Desculpe, Tim, eu deveria ter esclarecido a herança de IDictionary .

Temos um caso de uso em que a prioridade mudaria?

Gosto da sua implementação, mas acho que podemos reduzir a replicação do IDictionary Behavior. Acho que não devemos herdar de IDictionary<> mas apenas ICollection, porque não acho que os padrões de acesso do dicionário sejam intuitivos,

No entanto, eu realmente acho que retornar T e sua prioridade associada faria sentido, mas não conheço um caso de uso em que eu precisaria saber a prioridade de um elemento dentro da estrutura de dados ao enfileirá-lo ou retirá-lo da fila.

@Jlalond
Se tivermos uma fila de prioridade, ela deve oferecer suporte a todas as operações que pode ser simples e eficiente, _e_ que são
'esperado' estar lá por pessoas familiarizadas com este tipo de estrutura de dados / abstração de um.

A prioridade de atualização pertence à API porque se enquadra nessas duas categorias. A prioridade de atualização é consideração suficiente em muitos algoritmos de forma que a complexidade de fazer a operação com estruturas de dados heap afeta a complexidade do algoritmo geral e é medido regularmente, consulte:
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps

TBH principalmente seu diminui_key que é interessante para algoritmos e eficiência, e medido, então estou pessoalmente bem porque a API só tem DecreasePriority (), e não tem UpdatePriority.
Mas, por conveniência , parece melhor não incomodar os usuários da API com a preocupação de que cada mudança seja um aumento ou uma diminuição. Para que o desenvolvedor joe tenha 'atualização' pronto para uso, sem ter que se perguntar por que ele suporta apenas diminuição e não aumento (e qual usar quando), acho que é melhor ter uma API de prioridade de atualização geral, mas documentar isso ao usar para diminuir tem custo X, ao usar para aumentar tem custo Y porque é implementado da mesma forma que Remover + Adicionar.

Dicionário RE Eu concordei. Removi da minha proposta em nome de mais simples é melhor.
https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

Não conheço um caso de uso em que precisaria saber a prioridade de um elemento dentro da estrutura de dados ao enfileirá-lo ou retirá-lo da fila.

Não tenho certeza do que se trata esse comentário. Você não precisa saber a prioridade de um elemento para retirá-lo da fila. Mas você precisa saber sua prioridade ao colocá-lo na fila. Talvez você queira aprender a prioridade final de um elemento ao retirá-lo da fila, pois esse valor pode ter significado, por exemplo, distância.

@TimLovellSmith

Não tenho certeza do que se trata esse comentário. Você não precisa saber a prioridade de um elemento para retirá-lo da fila. Mas você precisa saber sua prioridade ao colocá-lo na fila. Talvez você queira aprender a prioridade final de um elemento ao retirá-lo da fila, pois esse valor pode ter significado, por exemplo, distância.

Desculpe, não entendi o que TPriorty significava em seu par de valores-chave.

Ok, as duas últimas perguntas, por que eles fazem KeyValuePair com TPriority, e o melhor momento que posso ver para uma repriorização é N log N, concordo que é valioso, mas também como seria essa API?

o melhor momento que posso ver para uma repriorização é N log N,

Esse é o melhor momento para redefinir a prioridade de N itens em vez de um item, certo?
Vou esclarecer o documento: "UpdatePriority | O (log n)" deve ser "UpdatePriority (item único) | O (log n)".

@TimLovellSmith Faz sentido, mas nenhuma atualização de prioridade seria ne O2 * (log n), já que precisaríamos remover o elemento e, em seguida, reinseri-lo? Baseando minha suposição em ser um heap binário

@TimLovellSmith Faz sentido, mas nenhuma atualização de prioridade seria ne O2 * (log n), já que precisaríamos remover o elemento e, em seguida, reinseri-lo? Baseando minha suposição em ser um heap binário

Fatores constantes como o 2 são geralmente ignorados na análise de complexidade porque se tornam irrelevantes conforme o tamanho de N aumenta.

Entendido, principalmente queria saber se Tim tinha ideias interessantes para apenas fazer
uma operação :)

Na terça - feira, 18 de agosto de 2020, 23:51 masonwheeler

@TimLovellSmith https://github.com/TimLovellSmith Faz sentido, mas
não haveria nenhuma atualização de prioridade ne O2 * (log n), pois precisaríamos
remover o elemento e, em seguida, inseri-lo novamente? Baseando minha suposição nisso
sendo um heap binário

Fatores constantes como esse 2 são geralmente ignorados na análise de complexidade
porque eles se tornam principalmente irrelevantes à medida que o tamanho de N aumenta.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/dotnet/runtime/issues/14032#issuecomment-675887763 ,
ou cancelar
https://github.com/notifications/unsubscribe-auth/AF76XTI3YK4LRUVTOIQMVHLSBNZARANCNFSM4LTSQI6Q
.

@Jlalond
Sem ideias novas, estava apenas ignorando um fator constante, mas diminuições também são diferentes de aumentos

Se a atualização de prioridade for uma diminuição, você não precisa removê-la e adicioná-la novamente, ela apenas borbulha, então é 1 * O (log n) = O (log n).
Se a atualização de prioridade for um aumento, você provavelmente terá que removê-la e adicioná-la novamente, de modo que seja 2 * O (log n) = ainda O (log n).

Alguém pode ter inventado uma estrutura de dados / algoritmo melhor para aumentar a prioridade do que remove + readd, mas eu não tentei encontrar, O (log n) parece bom o suficiente.

KeyValuePair entrou sorrateiramente na interface enquanto era um dicionário, mas sobreviveu à remoção do dicionário, principalmente para que fosse possível iterar a coleção de _elementos com suas prioridades_. E também para que você possa desenfileirar os elementos com suas prioridades. Mas talvez para simplificar novamente, retirar da fila com prioridades deve estar apenas na versão 'avançada' da API de retirar da fila. (TryDequeue: D)

Vou fazer essa mudança.

@TimLovellSmith Cool, aguardo sua proposta revisada. Podemos enviá-lo para revisão para que eu possa começar a trabalhar nisso? Além disso, sei que houve algum ímpeto para que uma fila de emparelhamento melhorasse o tempo de mesclagem, mas ainda acho que um heap binário baseado em array seria o melhor desempenho geral. Pensamentos?

@Jlalond
Fiz pequenas alterações para simplificar a API Peek + Dequeue.
Alguma ICollectionapis relacionados a KeyValuePair ainda permanecem, porque não vejo nada obviamente melhor para substituí-los.
Mesmo link de relações públicas. Existe outra maneira de enviá-lo para revisão?

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

Não me importo com qual implementação ele usa, contanto que seja tão rápido quanto um heap baseado em array ou melhor.

@TimLovellSmith Na verdade, não sei nada sobre revisão de API, mas imagino que @danmosemsft possa nos apontar a direção certa.

Minha primeira pergunta é o que seriaser? Suponho que seria um int ou um número de ponto flutuante, mas para mim, declarar o número que é "interno" para a fila parece estranho para mim

E eu, na verdade, prefiro heaps binários, mas queria ter certeza de que estamos todos bem indo nessa direção

@eiriktsarpalis @layomia são os proprietários da

Como discutimos no passado, as bibliotecas centrais não são o melhor lugar para todas as coleções, especialmente aquelas que são opinativas e / ou de nicho. Por exemplo, nós enviamos anualmente - não podemos nos mover tão rapidamente quanto uma biblioteca pode. Temos o objetivo de construir uma comunidade em torno de um pacote de coleções para preencher essa lacuna. Mas 84 votos positivos sobre este sugere que há uma necessidade generalizada disso nas bibliotecas centrais.

@Jlalond Desculpe, não entendi bem qual deveria ser a primeira pergunta. Você estava perguntando por que o TPriority é genérico? Ou tem que ser um número? Duvido que muitas pessoas usem tipos de prioridade não numéricos. Mas eles podem preferir usar byte, int, long, float, double ou enum.

@TimLovellSmith Sim, só queria saber se era genérico

@danmosemsft Obrigado Dan. O número de objeções / comentários não resolvidos que vejo à minha proposta [1] é quase zero e aborda todas as questões importantes que foram deixadas em aberto pela proposta de @ebickle no topo (à qual está se tornando cada vez mais semelhante).

Então eu afirmo que ele está passando nos testes de sanidade até agora. Ainda deve haver alguma revisão necessária, podemos conversar sobre se é útil herdar IReadOnlyCollection (não parece muito útil, mas devo encaminhar para os especialistas) e assim por diante - acho que é para isso que serve o processo de revisão da API! @eiriktsarpalis @layomia posso pedir para você dar uma olhada nisso?

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

[PS - com base no sentimento do tópico, o aplicativo matador proposto atual é codificação de competições, perguntas de entrevista, etc, onde você só quer uma API simples e agradável para usar que funcione tanto classes quanto estruturas mais seu tipo numérico favorito do dia, com o O certo (n) e uma implementação não muito lenta - e estou feliz por ser a cobaia para aplicá-la a alguns problemas. Desculpe, não é nada mais emocionante.]

Apenas falando porque eu uso filas de prioridade o tempo todo para projetos 'reais', principalmente para pesquisa de gráfico (por exemplo, otimização de problema, localização de rota) e extração ordenada (por blocos (por exemplo, quatro vizinhos de árvore mais próximos), programação (ou seja, como a discussão do temporizador acima)). Tenho alguns projetos nos quais poderia conectar uma implementação diretamente para verificação / teste de integridade, se ajudar. A proposta atual parece boa para mim (funcionaria para todos os meus propósitos, se não fosse um ajuste ideal). Eu tenho algumas pequenas observações:

  • TElement aparece algumas vezes onde deveria estar TKey
  • Minhas próprias implementações geralmente incluem bool EnqueueOrUpdateIfHigherPriority para facilidade de uso (e em alguns casos eficiência), que muitas vezes acaba sendo o único método de enfileiramento / atualização que é usado (por exemplo, na pesquisa de gráfico). Obviamente, não é essencial e adicionaria complexidade adicional à API, mas é uma coisa muito boa de se ter.
  • Existem duas cópias de Enqueue (não quero dizer Add ): uma delas deve ser bool TryEnqueue ?
  • "// enumera a coleção em ordem arbitrária, mas com o mínimo de elemento primeiro": Não acho que a última parte seja útil; Eu preferiria que o enumerador não tivesse que fazer nenhuma comparação, mas presumo que isso seja 'gratuito', então não me incomodará
  • A nomenclatura de EqualityComparer é um pouco inesperada: eu esperava que KeyComparer fosse igual a PriorityComparer .
  • Não entendo as notas sobre a complexidade de CopyTo e ToArray .

@VisualMelon
Muito obrigado pela revisão!

Eu gosto da sugestão de nomenclatura de KeyComparer. A API de diminuição de prioridade parece muito útil. Que tal adicionarmos uma ou ambas as APIs. Já que estamos usando o modelo 'menos prioridade vem primeiro', você usaria apenas redução? Ou você gostaria de aumentar também?

    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

A razão pela qual pensei que enumerar retornasse o mínimo de elemento primeiro era para ser consistente com o modelo mental de que Peek () retorna o elemento na frente da coleção. E também nenhuma comparação é necessária para implementá-lo se for um heap binário, então parecia um brinde. Mas talvez seja menos gratuito do que penso - complexidade injustificada? Estou feliz em retirá-lo por esse motivo.

Dois Enqueues foram acidentais. Temos EnqueueOrUpdate, onde a prioridade sempre é atualizada. Eu acho que TryUpdate é o inverso lógico disso, onde a prioridade nunca deve ser atualizada para itens já na coleção. Posso ver que preenche essa lacuna lógica, mas não tenho certeza se é uma API que as pessoas vão querer na prática.

Só posso imaginar o uso da variante decrescente: é uma operação natural na localização de rotas querer enfileirar um nó ou substituir um nó existente por outro que tenha uma prioridade mais baixa; Não pude sugerir imediatamente um uso para o oposto. Um parâmetro de retorno boolean pode ser útil se você precisar realizar qualquer operação quando o item está na fila / não na fila.

Eu não estava presumindo um heap binário, mas se for livre, não tenho problema com o primeiro elemento sendo sempre o mínimo, apenas parecia uma restrição estranha.

@VisualMelon @TimLovellSmith Uma API funcionaria?
EnqueueOrUpdatePriority ? Imagino que apenas a capacidade de reposicionar um nó dentro da fila é valiosa para algoritmos de travessia, mesmo que aumente ou diminua a prioridade

@Jlalond sim, eu mencionei isso porque pode ser útil para a eficiência dependendo da implementação e codifica diretamente o caso de uso de apenas querer enfileirar algo se melhorar o que você já tem na fila. Não pretendo dizer se a complexidade adicional é apropriada para uma ferramenta de uso geral, mas certamente não é necessária.

Atualizado para remover EnqueueOrIncreasePriority, mantendo EnqueueOrUpdate e EnqueueOrDecrease. Permitindo que eles retornem bool quando um item for adicionado à coleção, como HashSet.Add ().

@Jlalond Peço desculpas pelo erro acima (exclusão de seu comentário mais recente perguntando quando este problema será analisado).

Só queria mencionar que @eiriktsarpalis estará de volta na próxima semana e discutiremos a priorização desse recurso e a revisão das APIs em seguida.

Estamos considerando isso, mas ainda não estamos comprometidos com o .NET 6.

Vejo que este tópico foi revivido começando do zero e bifurcando, embora tivéssemos discussões aprofundadas sobre esse tópico em 2017 por vários meses (a maior parte deste enorme tópico) e produzisse coletivamente essa proposta - role acima para ver. Essa também é a proposta que @karelz vinculou na primeira linha do primeiro post para visibilidade:

Veja a última proposta no repositório corefxlab.

Em 2016/06/16, estávamos esperando por uma revisão da API que nunca aconteceu e o status era:

Se a API for aprovada em sua forma atual (duas classes), eu criaria outro PR para CoreFXLab com uma implementação para ambos usando um heap quaternário ( ver implementação ). O PriorityQueue<T> usaria apenas um array abaixo, enquanto PriorityQueue<TElement, TPriority> usaria dois - e reorganizaria seus elementos juntos. Isso pode nos poupar de alocações adicionais e ser o mais eficiente possível. Vou acrescentar isso quando houver luz verde.

Recomendo continuar iterando a proposta que fizemos em 2017, a partir de uma revisão formal que ainda não aconteceu. Isso nos permitirá evitar bifurcações e iterações no verso de uma proposta elaborada após centenas de postagens trocadas por membros da comunidade, também por respeito ao esforço despendido por todos os envolvidos. Fico feliz em discutir isso se houver feedback.

@pgolebiowski Obrigado por voltar à discussão. Gostaria de pedir desculpas por efetivamente bifurcar ainda mais as propostas, o que não é a forma mais eficaz de colaborar. Foi um movimento impulsivo da minha parte. (Tive a impressão de que a discussão havia paralisado completamente devido ao excesso de questões em aberto nas propostas e apenas precisava de uma proposta mais opinativa.)

Que tal tentarmos seguir em frente, esquecendo pelo menos temporariamente o conteúdo do código do meu PR, e retomarmos a discussão aqui dos 'problemas em aberto' identificados? Eu gostaria de dar minha opinião sobre todos eles.

  1. Nome da classe PriorityQueue vs. Heap <- Acho que já foi discutido e o consenso é que PriorityQueue é melhor.

  2. Introduzir IHeap e sobrecarga de construtor? <- Não prevejo muito valor. Acho que para 95% do mundo não há uma razão convincente (por exemplo, delta de desempenho) para ter várias implementações de heap, e os outros 5% provavelmente terão que escrever suas próprias.

  3. Apresentar IPriorityQueue? <- Eu também não acho que seja necessário. Estou assumindo que outras linguagens e estruturas se dão muito bem sem essas interfaces em sua biblioteca padrão. Deixe-me saber se isso estiver incorreto.

  4. Use seletor (de prioridade armazenada dentro do valor) ou não (diferença de 5 APIs) <- Não vejo um argumento forte a favor de seletores de suporte na fila de prioridade. Acho que IComparer<> já serve como um mecanismo de extensibilidade mínima realmente bom para comparar prioridades de itens, e os seletores não oferecem nenhuma melhoria significativa. Além disso, as pessoas provavelmente ficarão confusas sobre como usar seletores com prioridades mutáveis ​​(ou como NÃO usá-los).

  5. Use tuplas (elemento TElement, prioridade TPriority) vs. KeyValuePair<- Pessoalmente, acho que KeyValuePair é a opção preferida para uma fila de prioridade onde os itens têm prioridades atualizáveis, uma vez que os itens estão sendo tratados como chaves em um conjunto / dicionário.

  6. Peek e Dequeue deveriam ter um argumento em vez de uma tupla? <- Não tenho certeza de todos os prós e contras, especialmente o desempenho. Devemos apenas escolher aquele que tem melhor desempenho em um teste de benchmark simples?

  7. O lançamento de Peek and Dequeue é útil? <- Sim ... É o que deveria acontecer para algoritmos que assumem incorretamente que ainda há itens na fila . Se você não quiser que os algoritmos jamais presumam isso, a melhor coisa a fazer é não fornecer as apis Peek e Dequeue, mas apenas TryPeek e TryDequeue. [Eu diria que existem algoritmos que podem, com segurança, chamar Peek ou Dequeue em certos casos, e ter Peek / Dequeue é uma pequena melhoria de desempenho + usabilidade para eles.]

Além disso, também tenho algumas sugestões a serem adicionadas à proposta em consideração:

  1. Fila de prioridadesó deve funcionar com itens exclusivos, que são seu próprio identificador. Ele deve suportar IEqualityComparer personalizado, caso queira comparar objetos não extensíveis (como strings) de maneiras específicas para fazer atualizações prioritárias.

  2. Fila de prioridadedeve oferecer suporte a uma variedade de operações como 'remover', 'diminuir a prioridade se for menor' e, especialmente, 'enfileirar o item ou diminuir a prioridade se a operação for menor', tudo em O (log n) - para implementar de forma eficiente e simples os cenários de pesquisa de gráfico.

  3. Seria conveniente para programadores preguiçosos se PriorityQueuefornece operador de índice [] para obter / definir prioridade de itens existentes. Deve ser O (1) para obter, O (log n) para definir.

  4. A menor prioridade primeiro é a melhor. Como as filas prioritárias são usadas para muitos, são problemas de otimização, com 'custo mínimo'.

  5. A enumeração não deve fornecer nenhuma garantia de pedido.

Problema aberto - trata:
Fila de prioridadeitens e prioridades parecem ter a intenção de permitir trabalhar com valores duplicados. Esses valores precisam ser considerados como 'imutáveis', ou deve haver um método para chamar para notificar a coleção quando as prioridades dos itens forem alteradas, possivelmente com identificadores, para que possa ser específico sobre a prioridade de quais duplicatas alteradas (que cenário precisa fazer isso ??) ... Tenho dúvidas sobre tudo isso é útil, e se isso é uma boa idéia, mas se tivermos prioridades de atualização em tal coleção, pode ser bom se os custos incorridos por esse método podem ser incorridos preguiçosamente, então que quando você está trabalhando apenas com itens imutáveis, e não quer usar, não há nenhum custo extra ... como resolver? Pode ser por ter métodos sobrecarregados que usam identificadores e outros que não usam? Ou simplesmente não deveríamos ter nenhum identificador de item diferente dos próprios itens (usado como chave hash para pesquisa)?

Uma ideia para ajudar a agilizar isso, pode fazer sentido mover esse tipo para a biblioteca "Coleções da comunidade" discutida em https://github.com/dotnet/runtime/discussions/42205

Eu concordo que a menor prioridade primeiro é a melhor. Uma fila de prioridade também é útil no desenvolvimento de jogos baseados em turnos, e ser capaz de ter a prioridade ser um contador monotonicamente crescente de quando cada elemento obtém sua próxima jogada é muito útil.

Estamos pensando em trazer isso para revisão da API o mais rápido possível (embora ainda não estejamos comprometidos em entregar uma implementação dentro do prazo do .NET 6).

Mas antes de enviarmos isso para revisão, precisaremos resolver algumas das questões em aberto de alto nível. Eu acho que 's @TimLovellSmith escrita acima é um bom ponto de partida para alcançar esse objetivo.

Algumas observações sobre esses pontos:

  • O consenso sobre as questões 1-3 está estabelecido há muito tempo, acho que poderíamos tratá-las como resolvidas.

  • _Use seletor (de prioridade armazenado dentro do valor) ou não_ - Concordo com suas observações. Outro problema com esse design é que os seletores são opcionais, o que significa que você precisa ter cuidado para não chamar Enqueue sobrecarga errada (ou correr o risco de receber InvalidOperationException ).

  • Eu prefiro usar uma tupla acima de KeyValuePair . Usar uma propriedade .Key para acessar um TPriority parece estranho para mim. Uma tupla permite que você use .Priority que tem melhores propriedades de autodocumentação.

  • _Deve Peek e Dequeue preferem ter um argumento em vez de tupla? _ - Acho que devemos apenas seguir a convenção estabelecida em métodos semelhantes em outros lugares. Portanto, provavelmente use um argumento out .

  • _O lançamento de Peek and Dequeue é útil? _ - Concordou 100% com seus comentários.

  • _A menor prioridade primeiro é a melhor_ - Concordo.

  • _A enumeração não deve fornecer nenhuma garantia de pedido_ - Isso não pode violar as expectativas do usuário? Quais são as vantagens e desvantagens? NB, provavelmente podemos adiar detalhes como este para a discussão de revisão da API.

Eu gostaria de reformular algumas outras questões em aberto também:

  • Enviamos PriorityQueue<T> , PriorityQueue<TElement, TPriority> ou ambos? - Pessoalmente, acho que devemos apenas implementar o último, pois parece fornecer uma solução mais limpa e de uso geral. Em princípio, a prioridade não deve ser uma propriedade intrínseca ao elemento enfileirado, portanto, não devemos forçar os usuários a encapsulá-lo em tipos de invólucro.

  • Exigimos a exclusividade do elemento, até a igualdade? - Corrija-me se eu estiver errado, mas eu sinto que a exclusividade é uma restrição artificial, forçada sobre nós pela necessidade de suportar cenários de atualização sem recorrer a uma abordagem de manipulação. Isso também complica a superfície da API, já que agora também precisamos nos preocupar com os elementos que têm a semântica de igualdade correta (qual é a igualdade apropriada quando os elementos são DTOs grandes?). Posso ver três caminhos possíveis aqui:

    1. Exigir exclusividade / igualdade e suportar cenários de atualização passando o elemento original (até igualdade).
    2. Não requer exclusividade / igualdade e suporta cenários de atualização usando alças. Eles podem ser obtidos usando variantes opcionais do método Enqueue para usuários que precisam deles. Se as alocações de identificadores são uma preocupação grande o suficiente, estou me perguntando se elas poderiam ser amortizadas à la ValueTask?
    3. Não exigem exclusividade / igualdade e não oferecem suporte para atualização de prioridades.
  • Oferecemos suporte para a fusão de filas? - O consenso parece ser não, já que estamos enviando apenas uma implementação (provavelmente usando heaps de array) em que a mesclagem não é eficiente.

  • Quais interfaces ele deve implementar? - Eu vi algumas propostas recomendando IQueue<T> , mas isso parece uma abstração que vaza. Eu pessoalmente prefiro mantê-lo simples e apenas implementar ICollection<T> .

cc @layomia @safern

@eiriktsarpalis Pelo contrário - não há nada artificial na restrição para oferecer suporte a atualizações!

Os algoritmos com filas de prioridade geralmente atualizam as prioridades dos elementos. A questão é: você fornece uma API orientada a objetos, que 'simplesmente funciona' para atualizar as prioridades de objetos comuns ... ou você força as pessoas a
a) entender o modelo do cabo
b) manter dados adicionais na memória, como propriedades extras ou um dicionário externo, para acompanhar as alças dos objetos, ao seu lado, apenas para que possam fazer atualizações (deve ir com o dicionário para as classes que não podem alterar? ou atualize os objetos em tuplas, etc.)
c) estruturas externas de 'gerenciamento de memória', ou 'coleta de lixo', ou seja, controle de limpeza para itens que não estão mais na fila, ao usar a abordagem de dicionário
d) não confunda alças opacas em contextos com múltiplas filas, uma vez que eles só são significativos no contexto de uma única fila de prioridade

Além disso, há esta questão filosófica em torno de todo o motivo pelo qual alguém iria querer que uma fila que _mantenha controle de objetos por prioridade_ se comporte desta forma: por que o mesmo objeto (igual a retorna verdadeiro) tem duas prioridades _diferentes_? Se eles realmente deveriam ter prioridades diferentes, por que não são modelados como objetos diferentes ? (ou convertido em tuplas, com distinguidores?)

Também para identificadores, deve haver alguma tabela interna de identificadores na fila de prioridade para que os identificadores realmente funcionem. Acho que é um trabalho equivalente a manter um dicionário para pesquisar objetos na fila.

PS: todo objeto .net já oferece suporte a conceitos de igualdade / exclusividade, portanto, não é uma questão de 'exigir' isso.

Em relação a KeyValuePair , concordo que não é o ideal (embora na proposta, Key seja para o elemento, Value para a prioridade, que é consistente com os vários Sorted tipos de dados no BCL), e sua adequação dependeria de uma decisão quanto à exclusividade. Pessoalmente, eu _much_ preferiria um dedicado e bem nomeado struct vez de uma tupla em qualquer lugar na API pública, especialmente para entrada.

Quanto à exclusividade, essa é uma preocupação fundamental, e não acho que mais nada possa ser decidido até que seja. Eu favoreceria a exclusividade do elemento, conforme definido pelo comparador (opcional) (de acordo com as propostas existentes e a sugestão i) se o objetivo for uma única API fácil de usar e de uso geral. Único versus não único é uma grande brecha, e eu uso os dois tipos para finalidades diferentes. O primeiro é 'mais difícil' de implementar e cobre a maioria (e mais típico) dos casos de uso (apenas a minha experiência), embora seja mais difícil de usar incorretamente. Os casos de uso que _requerem_ não-exclusividade devem (IMO) ser atendidos por um tipo diferente (por exemplo, um heap binário antigo simples), e eu gostaria de ter ambos disponíveis. Isso é essencialmente o que a proposta original vinculada por @pgolebiowski fornece (pelo que entendi) módulo um invólucro (simples). _Editar: Não, isso não apoiaria prioridades empatadas_

Pelo contrário - não há nada artificial na restrição para oferecer suporte a atualizações!

Desculpe, não quis indicar que o suporte para atualizações é artificial; em vez disso, o requisito de exclusividade é introduzido artificialmente para oferecer suporte a atualizações.

PS: todo objeto .net já oferece suporte a conceitos de igualdade / exclusividade, então não é um grande pedido 'exigi-lo'

Claro, mas ocasionalmente a semântica de igualdade que vem com o tipo pode não ser a desejável (por exemplo, igualdade de referência, igualdade estrutural completa, etc.). Só estou apontando que a igualdade é difícil e forçá-la no design traz uma nova classe de possíveis bugs do usuário.

@eiriktsarpalis Obrigado por esclarecer isso. Mas é realmente artificial? Eu não acho que seja. É apenas mais uma solução

A API deve ser _bem definida_. Você não pode fornecer uma API de atualização sem exigir que o usuário seja específico sobre exatamente o que deseja atualizar . Alças e igualdade de objetos são apenas duas abordagens diferentes para construir uma API bem definida.

Abordagem de manuseio: toda vez que você adiciona um objeto à coleção, você deve dar a ele um 'nome', ou seja, um 'manuseio', para que você possa se referir a esse objeto exato sem ambigüidade posteriormente na conversa.

Abordagem da exclusividade do objeto: cada vez que você adiciona um objeto à coleção, ele deve ser um objeto diferente ou você deve especificar como lidar com o caso de o objeto já existir.

Infelizmente, apenas a abordagem de objeto realmente permite que você suporte algumas das propriedades de métodos de API de alto nível mais úteis, como 'EnqueueIfNotExists' ou 'EnqueueOrDecreasePriroity (item)' - eles não fazem sentido fornecer em um design centrado em alça, porque você não posso não saber se o item já existe na fila (já que é seu trabalho controlá-lo, com alças).

Uma das críticas mais contundentes da abordagem de manipulação, ou descartando a restrição de exclusividade para mim, é que torna todos os tipos de cenários com prioridade atualizável muito mais complicados de implementar:

por exemplo

  1. use um PriorityQueuepara strings que representam mensagens / sentimentos / tags / nome de usuário que são votados / atualizados, os valores únicos têm prioridade variável
  2. usar PriorityQueue, double> para ordenar tuplas únicas [quer tenham mudanças de prioridade ou não] - deve manter o controle de identificadores extras em algum lugar
  3. usar PriroityQueuepara priorizar índices de gráfico, ou ids de objeto de banco de dados, agora você tem que polvilhar alças por meio de sua implementação

PS

Claro, mas ocasionalmente a semântica de igualdade que vem com o tipo pode não ser a desejável

Deve haver escotilhas de escape, como IEqualityComparer, ou conversão para um tipo mais rico.

Obrigado pelo feedback 🥳 Atualizarei a proposta no fim de semana, levando em consideração todas as novas contribuições, e compartilharei uma nova revisão para outra rodada. ETA 2020-09-20.

Proposta de fila prioritária (v2.0)

Resumo

A comunidade do .NET Core propõe adicionar à funcionalidade _priority queue_ da biblioteca do sistema uma estrutura de dados na qual cada elemento adicionalmente tem uma prioridade associada a ele. Especificamente, propomos adicionar PriorityQueue<TElement, TPriority> ao namespace System.Collections.Generic .

Princípios

Em nosso projeto, fomos guiados pelos seguintes princípios (a menos que você conheça outros):

  • Ampla cobertura. Queremos oferecer aos clientes do .NET Core uma estrutura de dados valiosa que seja versátil o suficiente para oferecer suporte a uma ampla variedade de casos de uso.
  • Aprenda com os erros conhecidos. Nós nos esforçamos para fornecer funcionalidade de fila de prioridade que estaria livre dos problemas enfrentados pelo cliente presentes em outras estruturas e linguagens, por exemplo, Java, Python, C ++, Rust. Evitaremos fazer escolhas de design que sabidamente deixam os clientes insatisfeitos e reduzem a utilidade das filas prioritárias.
  • Cuidado extremo com decisões de porta unilateral. Depois que uma API é introduzida, ela não pode ser modificada ou excluída, apenas estendida. Analisaremos cuidadosamente as opções de design para evitar soluções abaixo do ideal que nossos clientes usarão para sempre.
  • Evite a paralisia do design. Aceitamos que pode não haver solução perfeita. Iremos equilibrar as trocas e seguir em frente com a entrega, para finalmente entregar aos nossos clientes a funcionalidade que eles esperam há anos.

Fundo

Da perspectiva de um cliente

Conceitualmente, uma fila de prioridade é uma coleção de elementos, onde cada elemento tem uma prioridade associada. A funcionalidade mais importante de uma fila de prioridade é que ela fornece acesso eficiente ao elemento com a prioridade mais alta na coleção e uma opção para remover esse elemento. O comportamento esperado também pode incluir: 1) capacidade de modificar a prioridade de um elemento que já está na coleção; 2) capacidade de mesclar várias filas de prioridade.

Formação em ciência da computação

Uma fila de prioridade é uma estrutura de dados abstrata, ou seja, é um conceito com certas características comportamentais, conforme descrito na seção anterior. As implementações mais eficientes de uma fila de prioridade são baseadas em heaps. No entanto, ao contrário do equívoco geral, um heap também é uma estrutura de dados abstrata e pode ser realizada de várias maneiras, cada uma oferecendo diferentes benefícios e desvantagens.

A maioria dos engenheiros de software está familiarizada apenas com a implementação de heap binário baseado em array - é a mais simples, mas infelizmente não é a mais eficiente. Para entrada aleatória geral, dois exemplos de tipos de heap mais eficientes são: heap quaternário e heap de emparelhamento . Para obter mais informações sobre heaps, consulte a Wikipedia e este documento .

O mecanismo de atualização é o principal desafio do design

Nossas discussões demonstraram que a área mais desafiadora no design e, ao mesmo tempo, com maior impacto na API, é o mecanismo de atualização. Especificamente, o desafio é determinar se e como o produto que queremos oferecer aos clientes deve suportar as prioridades de atualização dos elementos já presentes na coleção.

Essa capacidade é necessária para implementar, por exemplo, o algoritmo de caminho mais curto de Dijkstra ou um agendador de tarefas que precisa lidar com a mudança de prioridades. O mecanismo de atualização está faltando no Java, o que se mostrou decepcionante para os engenheiros, por exemplo, nessas três questões StackOverflow visualizadas mais de 32 mil vezes: exemplo , exemplo , exemplo . Para evitar a introdução de API com valor limitado, acreditamos que um requisito fundamental para a funcionalidade de fila de prioridade que oferecemos seria oferecer suporte à capacidade de atualização de prioridades para elementos já presentes na coleção.

Para entregar o mecanismo de atualização, devemos garantir que o cliente possa ser específico sobre o que exatamente deseja atualizar. Identificamos duas maneiras de fazer isso: a) por meio de alças; eb) por meio da aplicação de exclusividade de elementos na coleção. Cada um deles vem com diferentes benefícios e custos.

Opção (a): Alças. Nessa abordagem, cada vez que um elemento é adicionado à fila, a estrutura de dados fornece seu identificador exclusivo. Se o cliente deseja usar o mecanismo de atualização, ele precisa manter o controle de tais identificadores para que possa posteriormente especificar de forma inequívoca qual elemento deseja atualizar. O principal custo dessa solução é que os clientes precisam gerenciar esses indicadores. No entanto, isso não significa que precisa haver qualquer alocação interna para suportar identificadores na fila de prioridade - qualquer heap não baseado em array é baseado em nós, onde cada nó é automaticamente seu próprio identificador. Como exemplo, consulte a API do método PairingHeap.Update .

Opção (b): Exclusividade. Essa abordagem impõe duas restrições adicionais ao cliente: i) os elementos da fila de prioridade devem estar em conformidade com certas semânticas de igualdade, o que traz uma nova classe de possíveis bugs do usuário; ii) dois elementos iguais não podem ser armazenados na mesma fila. Ao pagar esse custo, temos o benefício de oferecer suporte ao mecanismo de atualização sem recorrer à abordagem de manipulação. No entanto, qualquer implementação que aproveite a exclusividade / igualdade para determinar o elemento a ser atualizado exigirá um mapeamento interno extra, de forma que seja feito em O (1) e não em O (n).

Recomendação

Recomendamos adicionar à biblioteca do sistema uma classe PriorityQueue<TElement, TPriority> que suporte o mecanismo de atualização por meio de identificadores. A implementação subjacente seria um heap de emparelhamento.

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

Exemplo de uso

1) Cliente que não se preocupa com o mecanismo de atualização

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 com o mecanismo de atualização

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

Perguntas frequentes

Em que ordem a fila de prioridade enumera os elementos?

Em ordem indefinida, para que a enumeração possa acontecer em O (n), da mesma forma que com um HashSet . Nenhuma implementação eficiente forneceria recursos para enumerar um heap em tempo linear, garantindo que os elementos sejam enumerados em ordem - isso exigiria O (n log n). Como o pedido em uma coleção pode ser alcançado trivialmente com .OrderBy(x => x.Priority) e nem todo cliente se preocupa com a enumeração com este pedido, acreditamos que seja melhor fornecer uma ordem de enumeração indefinida.

Apêndices

Apêndice A: Outros idiomas com funcionalidade de fila prioritária

| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | Fila de prioridade | Estende a classe abstrata AbstractQueue e implementa a interface Queue . |
| Rust | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| Go | heap | Existe uma interface de heap. |

Apêndice B: Detectabilidade

Observe que, ao discutir estruturas de dados, o termo _heap_ é usado 4 vezes mais do que _fila de prioridade_.

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

Reveja, fico feliz em receber feedback e continuar iterando :) Sinto que estamos convergindo! 😄 Também fico feliz em receber perguntas - vou adicioná-las ao FAQ!

@pgolebiowski Eu não costumo usar APIs baseadas em identificadores (eu esperaria embrulhar isso conforme o exemplo 2, se eu fosse reformar qualquer código existente), mas na maioria das vezes parece bom para mim. Posso tentar a implementação que você mencionou para ver como é, mas aqui estão alguns comentários iniciais:

  • Parece estranho que TryDequeue retorne um nó, porque não é realmente um identificador naquele ponto (eu prefiro dois parâmetros separados)
  • Será estável no sentido de que, se dois elementos forem enfileirados com a mesma prioridade, eles serão retirados da fila em uma ordem previsível? (bom ter para reprodutibilidade; pode ser implementado facilmente pelo consumidor de outra forma)
  • O parâmetro para Merge deve ser PriorityQueue'2 não PriorityQueueNode'2 , e você pode esclarecer o comportamento? Não estou familiarizado com a pilha de emparelhamento, mas presumivelmente as duas pilhas se sobrepõem depois
  • Não sou fã do nome Contains para o método de 2 parâmetros: esse não é um nome que eu imagino para um método de estilo TryGet
  • A classe deve oferecer suporte a um IEqualityComparer<TElement> para o propósito de Contains ?
  • Não parece haver uma maneira de determinar com eficiência se um nó ainda está no heap (não tenho certeza de quando eu usaria isso, pensei)
  • É estranho que Remove retorna um bool ; Eu esperaria que fosse chamado de TryRemove ou que jogasse (o que estou assumindo que Update faz se o nó não estiver no heap).

@VisualMelon muito obrigado pelo feedback! Resolverá isso rapidamente, definitivamente concordo:

  • Parece estranho que TryDequeue retorne um nó, porque não é realmente um identificador naquele ponto (eu prefiro dois parâmetros separados)
  • O parâmetro para Merge deve ser PriorityQueue'2 não PriorityQueueNode'2 , e você pode esclarecer o comportamento? Não estou familiarizado com a pilha de emparelhamento, mas presumivelmente as duas pilhas se sobrepõem depois
  • É estranho que Remove retorna um bool ; Eu esperaria que fosse chamado de TryRemove ou que jogasse (o que estou assumindo que Update faz se o nó não estiver no heap).
  • Não sou fã do nome Contains para o método de 2 parâmetros: esse não é um nome que eu diria para um método de estilo TryGet

Esclarecimento para estes dois:

  • Será estável no sentido de que, se dois elementos forem enfileirados com a mesma prioridade, eles serão retirados da fila em uma ordem previsível? (bom ter para reprodutibilidade; pode ser implementado facilmente pelo consumidor de outra forma)
  • Não parece haver uma maneira de determinar com eficiência se um nó ainda está no heap (não tenho certeza de quando eu usaria isso, pensei)

Para o primeiro ponto, se o objetivo é a reprodutibilidade, então a implementação será determinística, sim. Se o objetivo for _se dois elementos são colocados na fila com a mesma prioridade, segue-se que eles sairão na mesma ordem em que foram colocados na fila_ - não tenho certeza se a implementação pode ser ajustada para atingir essa propriedade, uma resposta rápida seria "provavelmente não".

Para o segundo ponto, sim, heaps não são bons para verificar se um elemento existe na coleção, um cliente teria que manter o controle disso separadamente para conseguir isso em O (1) ou reutilizar o mapeamento que eles usam para as alças, se eles usam o mecanismo de atualização. Caso contrário, O (n).

  • A classe deve apoiar um IEqualityComparer<TElement> para o propósito de Contains ?

Hmm ... Estou começando a pensar que talvez colocar Contains na responsabilidade desta fila de prioridade pode ser demais, e usar o método de Linq pode ser suficiente ( Contains tem de ser aplicado na enumeração de qualquer maneira).

@pgolebiowski obrigado pelos esclarecimentos

Para o primeiro ponto, se o objetivo é reprodutibilidade, então a implementação será determinística, sim

Não tanto determinístico, mas garantido para sempre (por exemplo, até mesmo a implementação muda, o comportamento não), então acho que a resposta que eu queria era 'não'. Tudo bem: o consumidor pode adicionar um ID de sequência à prioridade se precisar, embora, nesse ponto, um SortedSet faça o trabalho.

Para o segundo ponto, sim, heaps não são bons para verificar se um elemento existe na coleção, um cliente teria que manter o controle disso separadamente para conseguir isso em O (1) ou reutilizar o mapeamento que eles usam para as alças, se eles usam o mecanismo de atualização. Caso contrário, O (n).

Não requer um subconjunto do trabalho necessário para Remove ? Posso não ter sido claro: quis dizer que foi dado um PriorityQueueNode , verificando se ele está na pilha (não um TElement ).

Hmm ... estou começando a pensar que talvez colocar o Contains na responsabilidade dessa fila de prioridade seja demais

Eu não reclamaria se Contains não estivesse lá: também é uma armadilha para pessoas que não percebem que deveriam usar alças.

@pgolebiowski Você parece fortemente a favor dos puxadores, estou certo em pensar que é por razões de eficiência?

Do ponto de vista da eficiência, suspeito que os identificadores são realmente os melhores, para ambos os cenários com exclusividade e sem, então estou bem com isso como sendo a solução primária oferecida pelo framework.

Em absoluto:
Do ponto de vista da usabilidade, acho que elementos duplicados raramente são o que eu quero, o que ainda me deixa pensando se é valioso para o framework fornecer suporte para ambos os modelos. Mas ... uma classe "PrioritySet" amigável a um elemento único seria pelo menos fácil de adicionar posteriormente como um wrapper para o PriorityQueue proposto, por exemplo, em resposta à demanda contínua por uma API mais amigável. (se houver demanda. Como eu acho que pode!)

Para a API atual se propôs, algumas reflexões / perguntas:

  • que tal fornecer também uma sobrecarga de TryPeek(out TElement element, out TPriority priority) ?
  • se você tiver chaves duplicadas atualizáveis, tenho uma preocupação de que, se 'dequeue' não retornar o nó da fila de prioridade, como você verificaria se está removendo o nó exato correto do seu sistema de rastreamento de alças? Uma vez que você pode ter mais de uma cópia do elemento com a mesma prioridade.
  • Remove(PriorityQueueNode) atira se o nó não for encontrado? ou retornar falso?
  • Deveria haver uma versão TryRemove() que não jogue se Remover lançar?
  • Não tenho certeza se Contains() api é útil na maioria das vezes? 'Contém' parece estar no olho do observador, especialmente para cenários com elementos 'duplicados' com prioridades distintas ou outros recursos distintos! Nesse caso, o usuário final provavelmente deve fazer sua própria pesquisa de qualquer maneira. Mas pelo menos pode ser útil para o cenário sem duplicatas.

@pgolebiowski Obrigado por

  • Ecoando o comentário de @TimLovellSmith , não tenho certeza se Contains() ou TryGetNode() devem existir na API em sua forma proposta atualmente. Eles implicam que a igualdade para TElement é significativa, o que é presumivelmente uma das coisas que uma abordagem baseada em identificadores estava tentando evitar.
  • Eu provavelmente reformularia public void Dequeue(out TElement element); como public TElement Dequeue();
  • Por que o método TryDequeue() precisa retornar uma prioridade?
  • A classe também não deveria implementar ICollection<T> ou IReadOnlyCollection<T> ?
  • O que deve acontecer se eu tentar atualizar um PriorityQueueNode retornado de uma instância diferente de PriorityQueue?
  • Queremos oferecer suporte a uma operação de mesclagem eficiente? AFAICT, isso significa que não podemos usar uma representação baseada em array. Como isso impactaria a implementação em termos de alocações?

A maioria dos engenheiros de software está familiarizada apenas com a implementação de heap binário baseado em array - é a mais simples, mas infelizmente não é a mais eficiente. Para entrada aleatória geral, dois exemplos de tipos de heap mais eficientes são: heap quaternário e heap de emparelhamento.

Quais são os trade-offs para escolher as últimas abordagens? É possível usar uma implementação baseada em array para eles?

Ele não pode implementar ICollection<T> ou IReadOnlyCollection<T> quando não tem as assinaturas corretas para 'Adicionar' e 'Remover' etc.

É muito mais próximo em espírito / forma de ICollection<KeyValuePair<T,Priority>>

É muito mais próximo em espírito / forma de ICollection<KeyValuePair<T,Priority>>

Não seria KeyValuePair<TPriority, TElement> , já que a ordenação é feita por TPriority, que é efetivamente um mecanismo de codificação?

OK, parece que em geral somos a favor de descartar os métodos Contains e TryGet . Irá removê-los na próxima revisão e explicar o motivo da remoção no FAQ.

Quanto às interfaces implementadas - IEnumerable<PriorityQueueNode<TElement, TPriority>> suficiente? Que tipo de funcionalidade está faltando?

No KeyValuePair - havia algumas vozes que uma tupla ou uma estrutura com .Element e .Priority são mais desejáveis. Acho que sou a favor disso.

Não seria KeyValuePair<TPriority, TElement> , já que a ordenação é feita por TPriority, que é efetivamente um mecanismo de codificação?

Existem bons argumentos para ambos os lados. Por um lado, sim, exatamente o que você acabou de dizer. Por outro lado, geralmente espera-se que as chaves em uma coleção KVP sejam únicas e é perfeitamente válido ter vários elementos com a mesma prioridade.

Por outro lado, geralmente se espera que as chaves em uma coleção KVP sejam exclusivas

Eu discordo dessa afirmação - uma coleção de pares de valores-chave é apenas isso; quaisquer requisitos de exclusividade são colocados em cima disso.

IEnumerable<PriorityQueueNode<TElement, TPriority>> suficiente? Que tipo de funcionalidade está faltando?

Eu pessoalmente espero que IReadOnlyCollection<PQN<TElement, TPriority>> seja implementado, uma vez que a API fornecida já satisfaz essa interface em grande parte. Além disso, isso seria consistente com outros tipos de coleção.

Em relação à interface:

`` `
public bool TryGetNode (elemento TElement, out PriorityQueueNodenó); // 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> coleção);
`` `

Também gostaria que PriorityQueue implementasse a interface IReadonlyCollection<> .

Saltando para outras coisas além da superfície da API pública.

Essa capacidade é necessária para implementar, por exemplo, o algoritmo de caminho mais curto de Dijkstra ou um agendador de tarefas que precisa lidar com a mudança de prioridades. O mecanismo de atualização está faltando em Java, o que se mostrou decepcionante para os engenheiros, por exemplo, nessas três questões StackOverflow visualizadas mais de 32 mil vezes: exemplo, exemplo, exemplo. Para evitar a introdução de API com valor limitado, acreditamos que um requisito fundamental para a funcionalidade de fila de prioridade que oferecemos seria oferecer suporte à capacidade de atualização de prioridades para elementos já presentes na coleção.

Eu gostaria de discordar. A última vez que escrevi Dijkstra em C ++ std :: priority_queue foi suficiente e não lida com atualização de prioridade. O consenso comum da AFAIK para esse caso é adicionar um elemento falso na fila com prioridade e valor alterados e monitorar se processamos o valor ou não. O mesmo pode ser feito com o agendador de tarefas.

Para ser honesto, não tenho certeza de como Dijkstra ficaria com a proposta de fila atual. Como devo manter o controle dos nós para os quais preciso atualizar a prioridade? Com TryGetNode ()? Ou tem outra coleção de nós? Adoraria ver o código da proposta atual.

Se você olhar para a Wikipedia, não há nenhuma suposição de atualização de prioridades para a fila de prioridade. O mesmo para todas as outras linguagens que não têm essa funcionalidade e conseguiram isso. Eu sei "se esforçar para ser melhor", mas há demanda para isso?

Para entrada aleatória geral, dois exemplos de tipos de heap mais eficientes são: heap quaternário e heap de emparelhamento. Para obter mais informações sobre heaps, consulte a Wikipedia e este documento.

Eu olhei no papel e esta é uma citação dele:

Os resultados mostram que a escolha ótima de implementação depende fortemente dos insumos. Além disso, mostra que é preciso ter cuidado para otimizar o desempenho do cache, principalmente na barreira L1-L2. Isso sugere que estruturas complicadas e ignorantes do cache têm pouca probabilidade de ter um bom desempenho em comparação com estruturas mais simples que reconhecem o cache.

Pelo que parece, a proposta da fila atual estaria bloqueada por trás da implementação de árvore em vez de array, e algo me diz que há chance de que nós de árvore espalhados pela memória não tenham o mesmo desempenho de array de elementos.

Acho que ter benchmarks para comparar heap binário simples com base em array e heap de emparelhamento seria ideal para tomar a decisão adequada, antes disso, não acho que seja inteligente bloquear o design atrás de implementação específica (estou olhando para você Merge método).

Pulando para outro tópico, eu pessoalmente prefiro ter KeyValuePairdo que a nova classe personalizada.

  • Menos superfície de API
  • Posso fazer algo assim: `new PriorityQueue(novo dicionário() {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}); Eu sei que é limitado pela exclusividade da chave. Também acho que traria uma boa sinergia para consumir coleções baseadas em IDictionary.
  • É uma estrutura em vez de uma classe, portanto, nenhuma exceção NullReference
  • Em algum ponto, PrioirtyQueue precisaria ser serializado / desserializado e acho que seria mais fácil fazer o que com um objeto já existente.

A desvantagem é a mistura de sentimentos em relação a TPriority Key mas acho que uma boa documentação pode resolver isso.
`

Como uma solução alternativa para Dijkstra, adicionar elementos falsos na fila funciona e o número total de arestas do gráfico que você processa não muda. Mas o número de nós temporários que permanecem residentes no heap gerado pelo processamento das mudanças nas bordas, e isso pode impactar o uso da memória e a eficiência do enfileiramento e desenfileiramento.

Eu estava errado sobre não ser capaz de fazer IReadOnlyCollection, isso deve servir. Sem Add () e Remove () nessa interface, hah! (O que eu estava pensando...)

@ Ivanidzo4ka Seus comentários me convencem ainda mais de que faria sentido ter dois tipos separados: um, um heap binário simples (ou seja, sem atualização) e um segundo conforme descrito por uma das propostas:

  • Heaps binários são simples, fáceis de implementar, têm uma pequena API e são conhecidos por ter um bom desempenho na prática em muitos cenários importantes.
  • Uma fila de prioridade totalmente desenvolvida fornece benefícios teóricos para alguns cenários (ou seja, complexidade de espaço reduzida e complexidade de tempo reduzida para pesquisa em gráficos densamente conectados) e fornece uma API natural para muitos algoritmos / cenários.

No passado, eu geralmente escapei com um heap binário que escrevi na melhor parte de uma década atrás e uma combinação de SortedSet / Dictionary , e há cenários onde usei ambos os tipos em funções separadas (KSSP vem à mente).

É uma estrutura em vez de uma classe, portanto, nenhuma exceção NullReference

Eu diria que é o contrário: se alguém está passando valores padrão, adoraria que eles vissem um NRE; no entanto, acho que precisamos deixar claro onde esperamos que essas coisas sejam usadas: nós / identificadores provavelmente devem ser classes, mas se estivermos apenas lendo / retornando um par, concordo que deve ser uma estrutura.


Estou tentado a sugerir que deve ser possível atualizar o elemento, bem como a prioridade na proposta baseada em alças. Você pode obter o mesmo efeito apenas removendo um identificador e adicionando um novo, mas é uma operação útil e pode ter benefícios de desempenho dependendo da implementação (por exemplo, alguns heaps podem reduzir a prioridade de algo relativamente barato). Essa mudança tornaria mais organizado a implementação de muitas coisas (por exemplo, este exemplo indutor de pesadelo baseado no AlgoKit ParingHeap existente), especialmente aqueles que operam em uma região desconhecida do espaço do estado.

Proposta de fila prioritária (v2.1)

Resumo

A comunidade do .NET Core propõe adicionar à funcionalidade _priority queue_ da biblioteca do sistema uma estrutura de dados na qual cada elemento adicionalmente tem uma prioridade associada a ele. Especificamente, propomos adicionar PriorityQueue<TElement, TPriority> ao namespace System.Collections.Generic .

Princípios

Em nosso projeto, fomos guiados pelos seguintes princípios (a menos que você conheça outros):

  • Ampla cobertura. Queremos oferecer aos clientes do .NET Core uma estrutura de dados valiosa que seja versátil o suficiente para oferecer suporte a uma ampla variedade de casos de uso.
  • Aprenda com os erros conhecidos. Nós nos esforçamos para fornecer funcionalidade de fila de prioridade que estaria livre dos problemas enfrentados pelo cliente presentes em outras estruturas e linguagens, por exemplo, Java, Python, C ++, Rust. Evitaremos fazer escolhas de design que sabidamente deixam os clientes insatisfeitos e reduzem a utilidade das filas prioritárias.
  • Cuidado extremo com decisões de porta unilateral. Depois que uma API é introduzida, ela não pode ser modificada ou excluída, apenas estendida. Analisaremos cuidadosamente as opções de design para evitar soluções abaixo do ideal que nossos clientes usarão para sempre.
  • Evite a paralisia do design. Aceitamos que pode não haver solução perfeita. Iremos equilibrar as trocas e seguir em frente com a entrega, para finalmente entregar aos nossos clientes a funcionalidade que eles esperam há anos.

Fundo

Da perspectiva de um cliente

Conceitualmente, uma fila de prioridade é uma coleção de elementos, onde cada elemento tem uma prioridade associada. A funcionalidade mais importante de uma fila de prioridade é que ela fornece acesso eficiente ao elemento com a prioridade mais alta na coleção e uma opção para remover esse elemento. O comportamento esperado também pode incluir: 1) capacidade de modificar a prioridade de um elemento que já está na coleção; 2) capacidade de mesclar várias filas de prioridade.

Formação em ciência da computação

Uma fila de prioridade é uma estrutura de dados abstrata, ou seja, é um conceito com certas características comportamentais, conforme descrito na seção anterior. As implementações mais eficientes de uma fila de prioridade são baseadas em heaps. No entanto, ao contrário do equívoco geral, um heap também é uma estrutura de dados abstrata e pode ser realizada de várias maneiras, cada uma oferecendo diferentes benefícios e desvantagens.

A maioria dos engenheiros de software está familiarizada apenas com a implementação de heap binário baseado em array - é a mais simples, mas infelizmente não é a mais eficiente. Para entrada aleatória geral, dois exemplos de tipos de heap mais eficientes são: heap quaternário e heap de emparelhamento . Para obter mais informações sobre heaps, consulte a Wikipedia e este documento .

O mecanismo de atualização é o principal desafio do design

Nossas discussões demonstraram que a área mais desafiadora no design e, ao mesmo tempo, com maior impacto na API, é o mecanismo de atualização. Especificamente, o desafio é determinar se e como o produto que queremos oferecer aos clientes deve suportar as prioridades de atualização dos elementos já presentes na coleção.

Essa capacidade é necessária para implementar, por exemplo, o algoritmo de caminho mais curto de Dijkstra ou um agendador de tarefas que precisa lidar com a mudança de prioridades. O mecanismo de atualização está faltando no Java, o que se mostrou decepcionante para os engenheiros, por exemplo, nessas três questões StackOverflow visualizadas mais de 32 mil vezes: exemplo , exemplo , exemplo . Para evitar a introdução de API com valor limitado, acreditamos que um requisito fundamental para a funcionalidade de fila de prioridade que oferecemos seria oferecer suporte à capacidade de atualização de prioridades para elementos já presentes na coleção.

Para entregar o mecanismo de atualização, devemos garantir que o cliente possa ser específico sobre o que exatamente deseja atualizar. Identificamos duas maneiras de fazer isso: a) por meio de alças; eb) por meio da aplicação de exclusividade de elementos na coleção. Cada um deles vem com diferentes benefícios e custos.

Opção (a): Alças. Nessa abordagem, cada vez que um elemento é adicionado à fila, a estrutura de dados fornece seu identificador exclusivo. Se o cliente deseja usar o mecanismo de atualização, ele precisa manter o controle de tais identificadores para que possa posteriormente especificar de forma inequívoca qual elemento deseja atualizar. O principal custo dessa solução é que os clientes precisam gerenciar esses indicadores. No entanto, isso não significa que precisa haver qualquer alocação interna para suportar identificadores na fila de prioridade - qualquer heap não baseado em array é baseado em nós, onde cada nó é automaticamente seu próprio identificador. Como exemplo, consulte a API do método PairingHeap.Update .

Opção (b): Exclusividade. Essa abordagem impõe duas restrições adicionais ao cliente: i) os elementos da fila de prioridade devem estar em conformidade com certas semânticas de igualdade, o que traz uma nova classe de possíveis bugs do usuário; ii) dois elementos iguais não podem ser armazenados na mesma fila. Ao pagar esse custo, temos o benefício de oferecer suporte ao mecanismo de atualização sem recorrer à abordagem de manipulação. No entanto, qualquer implementação que aproveite a exclusividade / igualdade para determinar o elemento a ser atualizado exigirá um mapeamento interno extra, de forma que seja feito em O (1) e não em O (n).

Recomendação

Recomendamos adicionar à biblioteca do sistema uma classe PriorityQueue<TElement, TPriority> que suporte o mecanismo de atualização por meio de identificadores. A implementação subjacente seria um heap de emparelhamento.

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

Exemplo de uso

1) Cliente que não se preocupa com o mecanismo de atualização

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 com o mecanismo de atualização

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

Perguntas frequentes

1. Em que ordem a fila de prioridade enumera os elementos?

Em ordem indefinida, para que a enumeração possa acontecer em O (n), da mesma forma que com um HashSet . Nenhuma implementação eficiente forneceria recursos para enumerar um heap em tempo linear, garantindo que os elementos sejam enumerados em ordem - isso exigiria O (n log n). Como o pedido em uma coleção pode ser alcançado trivialmente com .OrderBy(x => x.Priority) e nem todo cliente se preocupa com a enumeração com este pedido, acreditamos que seja melhor fornecer uma ordem de enumeração indefinida.

2. Por que não há método Contains ou TryGet ?

Fornecer tais métodos tem valor insignificante, porque encontrar um elemento em um heap requer a enumeração de toda a coleção, o que significa que qualquer método Contains ou TryGet seria um invólucro em torno da enumeração. Além disso, para verificar se um elemento existe na coleção, a fila de prioridade deve estar ciente de como conduzir verificações de igualdade de TElement objetos, o que não é algo que acreditamos que deva cair na responsabilidade de uma fila de prioridade.

3. Por que existem sobrecargas de Dequeue e TryDequeue que retornam PriorityQueueNode ?

Isso é para clientes que desejam usar o método Update ou Remove e controlar os identificadores. Ao retirar da fila um elemento da fila de prioridade, eles receberiam uma alça que poderiam usar para ajustar o estado de seu sistema de rastreamento de alça.

4. O que acontece quando o método Update ou Remove recebe um nó de uma fila diferente?

A fila de prioridade lançará uma exceção. Cada nó está ciente da fila de prioridade a que pertence, da mesma forma que um LinkedListNode<T> está ciente da LinkedList<T> que pertence.

Apêndices

Apêndice A: Outros idiomas com funcionalidade de fila prioritária

| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | Fila de prioridade | Estende a classe abstrata AbstractQueue e implementa a interface Queue . |
| Rust | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| Go | heap | Existe uma interface de heap. |

Apêndice B: Detectabilidade

Observe que, ao discutir estruturas de dados, o termo _heap_ é usado 4 vezes mais do que _fila de prioridade_.

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

Obrigado a todos pelo feedback! Eu atualizei a proposta para v2.1. Changelog:

  • Removidos os métodos Contains e TryGet .
  • Adicionada uma FAQ # 2: _Por que não existe um método Contains ou TryGet ? _
  • Adicionada a interface IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>> .
  • Adicionada uma sobrecarga de bool TryPeek(out TElement element, out TPriority priority) .
  • Adicionada uma sobrecarga de bool TryPeek(out TElement element) .
  • Adicionada uma sobrecarga de void Dequeue(out TElement element, out TPriority priority) .
  • Alterado void Dequeue(out TElement element) para PriorityQueueNode<TElement, TPriority> Dequeue() .
  • Adicionada uma sobrecarga de bool TryDequeue(out TElement element) .
  • Adicionada uma sobrecarga de bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node) .
  • Adicionada uma FAQ # 3: _Por que existem sobrecargas de Dequeue e TryDequeue que retornam PriorityQueueNode ? _
  • Adicionada uma FAQ # 4: _O que acontece quando o método Update ou Remove recebe um nó de uma fila diferente? _

Obrigado pelas notas de mudança;)

Pequenos pedidos de esclarecimento:

  • Podemos qualificar a FAQ 4 de forma que ela seja válida para os elementos que não estão na fila? (ou seja, removidos)
  • Podemos adicionar um FAQ sobre estabilidade, ou seja, as garantias (se houver) ao retirar da fila elementos com a mesma prioridade (meu entendimento é que não há nenhum plano para fazer quaisquer garantias, o que é importante saber para, por exemplo, programação).

@pgolebiowski Em relação ao método Merge proposto:

public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)

Claramente, tal operação não teria semântica de cópia, então eu estou me perguntando se haveria alguma pegadinha em torno de fazer alterações em this e other _após_ uma fusão ter sido realizada (por exemplo, falha em qualquer instância para satisfazer a propriedade heap).

@eiriktsarpalis @VisualMelon - Obrigado! Abordará os pontos levantados, ETA 2020-10-04.

Se outras pessoas tiverem mais comentários / perguntas / preocupações / pensamentos - compartilhe 😊

Proposta de fila prioritária (v2.2)

Resumo

A comunidade do .NET Core propõe adicionar à funcionalidade _priority queue_ da biblioteca do sistema uma estrutura de dados na qual cada elemento adicionalmente tem uma prioridade associada a ele. Especificamente, propomos adicionar PriorityQueue<TElement, TPriority> ao namespace System.Collections.Generic .

Princípios

Em nosso projeto, fomos guiados pelos seguintes princípios (a menos que você conheça outros):

  • Ampla cobertura. Queremos oferecer aos clientes do .NET Core uma estrutura de dados valiosa que seja versátil o suficiente para oferecer suporte a uma ampla variedade de casos de uso.
  • Aprenda com os erros conhecidos. Nós nos esforçamos para fornecer funcionalidade de fila de prioridade que estaria livre dos problemas enfrentados pelo cliente presentes em outras estruturas e linguagens, por exemplo, Java, Python, C ++, Rust. Evitaremos fazer escolhas de design que sabidamente deixam os clientes insatisfeitos e reduzem a utilidade das filas prioritárias.
  • Cuidado extremo com decisões de porta unilateral. Depois que uma API é introduzida, ela não pode ser modificada ou excluída, apenas estendida. Analisaremos cuidadosamente as opções de design para evitar soluções abaixo do ideal que nossos clientes usarão para sempre.
  • Evite a paralisia do design. Aceitamos que pode não haver solução perfeita. Iremos equilibrar as trocas e seguir em frente com a entrega, para finalmente entregar aos nossos clientes a funcionalidade que eles esperam há anos.

Fundo

Da perspectiva de um cliente

Conceitualmente, uma fila de prioridade é uma coleção de elementos, onde cada elemento tem uma prioridade associada. A funcionalidade mais importante de uma fila de prioridade é que ela fornece acesso eficiente ao elemento com a prioridade mais alta na coleção e uma opção para remover esse elemento. O comportamento esperado também pode incluir: 1) capacidade de modificar a prioridade de um elemento que já está na coleção; 2) capacidade de mesclar várias filas de prioridade.

Formação em ciência da computação

Uma fila de prioridade é uma estrutura de dados abstrata, ou seja, é um conceito com certas características comportamentais, conforme descrito na seção anterior. As implementações mais eficientes de uma fila de prioridade são baseadas em heaps. No entanto, ao contrário do equívoco geral, um heap também é uma estrutura de dados abstrata e pode ser realizada de várias maneiras, cada uma oferecendo diferentes benefícios e desvantagens.

A maioria dos engenheiros de software está familiarizada apenas com a implementação de heap binário baseado em array - é a mais simples, mas infelizmente não é a mais eficiente. Para entrada aleatória geral, dois exemplos de tipos de heap mais eficientes são: heap quaternário e heap de emparelhamento . Para obter mais informações sobre heaps, consulte a Wikipedia e este documento .

O mecanismo de atualização é o principal desafio do design

Nossas discussões demonstraram que a área mais desafiadora no design e, ao mesmo tempo, com maior impacto na API, é o mecanismo de atualização. Especificamente, o desafio é determinar se e como o produto que queremos oferecer aos clientes deve suportar as prioridades de atualização dos elementos já presentes na coleção.

Essa capacidade é necessária para implementar, por exemplo, o algoritmo de caminho mais curto de Dijkstra ou um agendador de tarefas que precisa lidar com a mudança de prioridades. O mecanismo de atualização está faltando no Java, o que se mostrou decepcionante para os engenheiros, por exemplo, nessas três questões StackOverflow visualizadas mais de 32 mil vezes: exemplo , exemplo , exemplo . Para evitar a introdução de API com valor limitado, acreditamos que um requisito fundamental para a funcionalidade de fila de prioridade que oferecemos seria oferecer suporte à capacidade de atualização de prioridades para elementos já presentes na coleção.

Para entregar o mecanismo de atualização, devemos garantir que o cliente possa ser específico sobre o que exatamente deseja atualizar. Identificamos duas maneiras de fazer isso: a) por meio de alças; eb) por meio da aplicação de exclusividade de elementos na coleção. Cada um deles vem com diferentes benefícios e custos.

Opção (a): Alças. Nessa abordagem, cada vez que um elemento é adicionado à fila, a estrutura de dados fornece seu identificador exclusivo. Se o cliente deseja usar o mecanismo de atualização, ele precisa manter o controle de tais identificadores para que possa posteriormente especificar de forma inequívoca qual elemento deseja atualizar. O principal custo dessa solução é que os clientes precisam gerenciar esses indicadores. No entanto, isso não significa que precisa haver qualquer alocação interna para suportar identificadores na fila de prioridade - qualquer heap não baseado em array é baseado em nós, onde cada nó é automaticamente seu próprio identificador. Como exemplo, consulte a API do método PairingHeap.Update .

Opção (b): Exclusividade. Essa abordagem impõe duas restrições adicionais ao cliente: i) os elementos da fila de prioridade devem estar em conformidade com certas semânticas de igualdade, o que traz uma nova classe de possíveis bugs do usuário; ii) dois elementos iguais não podem ser armazenados na mesma fila. Ao pagar esse custo, temos o benefício de oferecer suporte ao mecanismo de atualização sem recorrer à abordagem de manipulação. No entanto, qualquer implementação que aproveite a exclusividade / igualdade para determinar o elemento a ser atualizado exigirá um mapeamento interno extra, de forma que seja feito em O (1) e não em O (n).

Recomendação

Recomendamos adicionar à biblioteca do sistema uma classe PriorityQueue<TElement, TPriority> que suporte o mecanismo de atualização por meio de identificadores. A implementação subjacente seria um heap de emparelhamento.

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

Exemplo de uso

1) Cliente que não se preocupa com o mecanismo de atualização

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 com o mecanismo de atualização

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

Perguntas frequentes

1. Em que ordem a fila de prioridade enumera os elementos?

Em ordem indefinida, para que a enumeração possa acontecer em O (n), da mesma forma que com um HashSet . Nenhuma implementação eficiente forneceria recursos para enumerar um heap em tempo linear, garantindo que os elementos sejam enumerados em ordem - isso exigiria O (n log n). Como o pedido em uma coleção pode ser alcançado trivialmente com .OrderBy(x => x.Priority) e nem todo cliente se preocupa com a enumeração com este pedido, acreditamos que seja melhor fornecer uma ordem de enumeração indefinida.

2. Por que não há método Contains ou TryGet ?

Fornecer tais métodos tem valor insignificante, porque encontrar um elemento em um heap requer a enumeração de toda a coleção, o que significa que qualquer método Contains ou TryGet seria um invólucro em torno da enumeração. Além disso, para verificar se um elemento existe na coleção, a fila de prioridade deve estar ciente de como conduzir verificações de igualdade de TElement objetos, o que não é algo que acreditamos que deva cair na responsabilidade de uma fila de prioridade.

3. Por que existem sobrecargas de Dequeue e TryDequeue que retornam PriorityQueueNode ?

Isso é para clientes que desejam usar o método Update ou Remove e controlar os identificadores. Ao retirar da fila um elemento da fila de prioridade, eles receberiam uma alça que poderiam usar para ajustar o estado de seu sistema de rastreamento de alça.

4. O que acontece quando o método Update ou Remove recebe um nó de uma fila diferente?

A fila de prioridade lançará uma exceção. Cada nó está ciente da fila de prioridade a que pertence, da mesma forma que um LinkedListNode<T> está ciente da LinkedList<T> que pertence. Além disso, se um nó foi removido de uma fila, tentar invocar Update ou Remove nele também resultará em uma exceção.

5. Por que não existe um método Merge ?

A fusão de duas filas prioritárias pode ser alcançada em tempo constante, o que a torna um recurso tentador de oferecer aos clientes. No entanto, não temos dados que demonstrem que haja demanda por tal funcionalidade, e não podemos justificar sua inclusão na API pública. Além disso, o design de tal funcionalidade não é trivial e, dado que esse recurso pode não ser necessário, pode complicar desnecessariamente a superfície e a implementação da API.

No entanto, não incluir o método Merge agora é uma porta de 2 vias - se no futuro os clientes expressarem interesse em ter suporte para a funcionalidade de mesclagem, será possível estender o tipo PriorityQueue . Portanto, recomendamos não incluir o método Merge ainda e prosseguir com o lançamento.

6. A coleção oferece garantia de estabilidade?

A coleta não fornecerá uma garantia de estabilidade fora da caixa, ou seja, se dois elementos forem enfileirados com a mesma prioridade, o cliente não poderá presumir que eles serão retirados da fila em uma determinada ordem. No entanto, se um cliente quiser obter estabilidade usando nosso PriorityQueue , ele pode definir um TPriority e os IComparer<TPriority> que garantem isso. Além disso, a coleta de dados será determinística, ou seja, para uma determinada sequência de operações, ela sempre se comportará da mesma forma, permitindo a reprodutibilidade.

Apêndices

Apêndice A: Outros idiomas com funcionalidade de fila prioritária

| Idioma | Tipo | Notas |
|: -: |: -: |: -: |
| Java | Fila de prioridade | Estende a classe abstrata AbstractQueue e implementa a interface Queue . |
| Rust | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| Go | heap | Existe uma interface de heap. |

Apêndice B: Detectabilidade

Observe que, ao discutir estruturas de dados, o termo _heap_ é usado 4 vezes mais do que _fila de prioridade_.

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

Changelog:

  • Removido o método void Merge(PriorityQueue<TElement, TPriority> other) // O(1) .
  • Adicionada uma FAQ # 5: Por que não existe um método Merge ?
  • A FAQ # 4 modificada também se aplica aos nós que foram removidos da fila de prioridade.
  • Adicionada uma FAQ # 6: A coleção fornece uma garantia de estabilidade?

O novo FAQ parece ótimo. Eu brinquei com a codificação de Dijkstra contra a API proposta, com dicionário de alças, e parecia basicamente bom.

O único pequeno aprendizado que tive ao fazer isso foi que o conjunto atual de nomes de métodos / sobrecargas não funciona tão bem para a digitação implícita de out variáveis. O que eu queria fazer com o código C # era TryDequeue(out var node) - mas infelizmente eu precisava fornecer o tipo explícito da variável out como PriorityQueueNode<> caso contrário, o compilador não saberia se eu queria um nó de fila de prioridade ou um 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);
            }
        }
    }

A principal questão não resolvida de design, em minha opinião, é se a implementação deve ou não suportar atualizações prioritárias. Posso ver três caminhos possíveis aqui:

  1. Requer exclusividade / igualdade e atualizações de suporte passando o elemento original.
  2. Suporte a atualizações prioritárias usando alças.
  3. Não suporta atualizações prioritárias.

Essas abordagens são mutuamente exclusivas e têm seus próprios conjuntos de compensações, respectivamente:

  1. A abordagem baseada em igualdade complica o contrato de API ao forçar exclusividade e requer contabilidade adicional nos bastidores. Isso acontece independentemente de o usuário precisar ou não de atualizações prioritárias.
  2. A abordagem baseada em manuseio implicaria em pelo menos uma alocação adicional por elemento enfileirado. Embora não imponha exclusividade, minha impressão é que, para cenários que precisam de atualizações, essa invariante é quase certamente implícita (por exemplo, observe como ambos os exemplos listados acima armazenam manipuladores em dicionários externos indexados pelos próprios elementos).
  3. As atualizações não são suportadas de forma alguma ou exigiriam uma passagem linear do heap. No caso de elementos duplicados, as atualizações podem ser ambíguas.

Identificar aplicativos PriorityQueue comuns

É importante identificarmos quais das abordagens acima podem fornecer o melhor valor para a maioria de nossos usuários. Então, tenho pesquisado as bases de código .NET, tanto internas quanto públicas da Microsoft, para instâncias de implementações Heap / PriorityQueue a fim de entender melhor quais são os padrões de uso mais comuns:

A maioria dos aplicativos implementa variações de classificação de heap ou agendamento de trabalho. Algumas instâncias foram usadas para algoritmos como classificação topológica ou codificação de Huffman. Um número menor foi usado para calcular as distâncias nos gráficos. Das 80 implementações de PriorityQueue examinadas, apenas 9 foram encontradas para implementar alguma forma de atualizações prioritárias.

Ao mesmo tempo, nenhuma das implementações Heap / PriorityQueue nas bibliotecas centrais Python, Java, C ++, Go, Swift ou Rust oferecem suporte a atualizações prioritárias em suas APIs.

Recomendações

À luz desses dados, está claro para mim que .ΝΕΤ precisa de uma implementação de PriorityQueue de linha de base que expõe a API essencial (heapify / push / peek / pop), oferece certas garantias de desempenho (por exemplo, nenhuma alocação adicional por enfileiramento) e não impõe restrições de exclusividade. Isso implica que a implementação _não suportaria atualizações de prioridade O (log n )_.

Devemos também considerar o acompanhamento com uma implementação de heap separada que suporte atualizações / remoções _O (log n) _ e use uma abordagem baseada em igualdade. Como esse seria um tipo especializado, o requisito de exclusividade não deveria ser um grande problema.

Tenho trabalhado na criação de protótipos de ambas as implementações e irei acompanhar com uma proposta de API em breve.

Muito obrigado pela análise, @eiriktsarpalis! Agradeço em particular o tempo para analisar a base de código interna da Microsoft para encontrar casos de uso relevantes e apoiar nossa discussão com dados.

A abordagem baseada em manuseio implicaria em pelo menos uma alocação adicional por elemento enfileirado.

Essa suposição é falsa, você não precisa de alocações adicionais em heaps baseados em nó. O heap de emparelhamento tem mais desempenho para uma entrada aleatória geral do que o heap binário baseado em array, o que significa que você teria o incentivo para usar esse heap baseado em nó mesmo para uma fila de prioridade que não suporta atualizações. Você pode ver os benchmarks no artigo que mencionei anteriormente .

Das 80 implementações de PriorityQueue examinadas, apenas 9 foram encontradas para implementar alguma forma de atualizações prioritárias.

Mesmo com essa pequena amostra, isso representa 11-12% de todos os usos. Além disso, isso pode ser sub-representado em certos domínios, como videogames, onde eu esperaria que essa porcentagem fosse maior.

Diante desses dados, é claro para mim que [...]

Não acho que tal conclusão seja clara, visto que uma das principais suposições é falsa e é discutível se 11-12% dos clientes é um caso de uso importante o suficiente ou não. O que estou faltando na sua avaliação é a avaliação do impacto do custo da "estrutura de dados que suporta atualizações" para clientes que não se importam com esse mecanismo - que para mim, esse custo é insignificante, pois eles poderiam usar a estrutura de dados sem ser afetado pelo mecanismo da alça.

Basicamente:

| | 11-12% de casos de uso | 88-89% casos de uso |
|: -: |: -: |: -: |
| se preocupa com atualizações | sim | não |
| é afetado negativamente pelas alças | N / A (são desejados) | não |
| é afetado positivamente pelas alças | sim | não |

Para mim, isso é um acéfalo a favor de apoiar 100% dos casos de uso, não apenas 88-89%.

Essa suposição é falsa, você não precisa de alocações adicionais em heaps baseados em nó

Se a prioridade e o item forem tipos de valor (ou se ambos forem tipos de referência que você não possui e / ou não pode alterar o tipo de base), você pode vincular a uma implementação que demonstra que nenhuma alocação adicional é necessária (ou apenas descreva como é alcançado)? Seria útil ver isso. Obrigado.

Seria útil se você pudesse elaborar mais ou apenas dizer o que está tentando dizer. Eu precisaria esclarecer a ambigüidade, haveria pingue-pongue e isso se transformaria em uma longa discussão. Como alternativa, podemos combinar uma chamada.

Estou dizendo que queremos evitar qualquer operação Enqueue que exija uma alocação, seja por parte do chamador ou da implementação (alocação interna amortizada é adequada, por exemplo, para expandir um array usado na implementação). Estou tentando entender como isso é possível com um heap baseado em nó (por exemplo, se esses objetos de nó são expostos ao chamador, isso proíbe o agrupamento pela implementação devido a preocupações em torno de reutilização / aliasing inadequado). Eu quero ser capaz de escrever:
C# pq.Enqueue(42, 84);
e não alocar. Como as implementações que você se refere para conseguir isso?

ou apenas diga o que você está tentando dizer

Eu pensei que eu era.

queremos evitar qualquer operação de enfileiramento que requeira uma alocação [...] eu quero ser capaz de escrever: pq.Enqueue(42, 84); e não alocar.

De onde vem esse desejo? É bom ter o efeito colateral de uma solução, não um requisito que 99,9% dos clientes precisam satisfazer. Não vejo por que você escolheria essa dimensão de baixo impacto para fazer escolhas de design entre as soluções.

Não estamos fazendo escolhas de design com base em otimizações para 0,1% dos clientes se isso impactar negativamente 12% dos clientes em outra dimensão. "preocupar-se com nenhuma alocação" + "lidar com dois tipos de valor" é um caso extremo.

Acho a dimensão do comportamento / funcionalidade com suporte muito mais importante, especialmente ao projetar uma estrutura de dados versátil de uso geral para um público amplo e uma variedade de casos de uso.

De onde vem esse desejo?

De querer que os tipos de coleção principais sejam utilizáveis ​​em cenários que se preocupam com o desempenho. Você diz que a solução baseada em nó suportaria 100% dos casos de uso: esse não é o caso se cada enfileiramento alocar, assim como List<T> , Dictionary<TKey, TValue> , HashSet<T> e assim on se tornaria inutilizável em muitas situações se eles fossem alocados em cada Add.

Por que você acredita que apenas "0,1%" se preocupa com as despesas gerais de alocação desses métodos? De onde vêm esses dados?

"preocupar-se com nenhuma alocação" + "lidar com dois tipos de valor" é um caso extremo

Não é. Também não se trata apenas de "dois tipos de valor". Pelo que entendi, a solução proposta exigiria a) uma alocação em cada enfileiramento, independentemente dos Ts envolvidos, ou b) exigiria que o tipo de elemento derivasse de algum tipo de base conhecido que, por sua vez, proíbe um grande número de usos possíveis para evitar alocação extra.

@eiriktsarpalis
Para que você não esqueça de nenhuma opção, acho que há uma opção viável 4 para adicionar às opções 1, 2 e 3, em sua lista, que é um meio-termo:

  1. uma implementação que suporta o caso de uso de 12%, enquanto também otimiza quase para os outros 88%, permitindo atualizações em elementos que são equacionáveis, e apenas _lazily_ construindo a tabela de pesquisa necessária para fazer essas atualizações na primeira vez que um método de atualização é chamado ( e atualizá-lo em atualizações + remoções subsequentes). Portanto, incorrendo em menos custo para aplicativos que não usam a funcionalidade.

Podemos ainda decidir que, porque há desempenho extra disponível para 88% ou 12% de uma implementação que não precisa de uma estrutura de dados atualizável, ou é otimizada para uma em primeiro lugar, é melhor fornecer as opções 2 e 3, do que opção 4. Mas pensei que não devemos esquecer que existe outra opção.

[Ou eu suponho que você poderia apenas ver isso como uma opção melhor 1 e atualizar a descrição de 1 para dizer que a contabilidade não é forçada, mas preguiçosa, e o comportamento equatável correto só é necessário quando as atualizações são usadas ...]

@stephentoub Isso é exatamente o que eu tinha em mente sobre dizer simplesmente o que você quer dizer, obrigado :)

Por que você acredita que apenas 0,1% se preocupa com as despesas gerais de alocação desses métodos? De onde vêm esses dados?

Da intuição, ou seja, a mesma fonte com base na qual você acredita ser mais importante priorizar "nenhuma alocação adicional" em vez de "capacidade de realizar atualizações". Pelo menos para o mecanismo de atualização, temos os dados de que 11-12% dos clientes precisam para ter suporte para esse comportamento. Não acho que clientes remotamente próximos se importem com "nenhuma alocação adicional".

Em ambos os casos, você está, por algum motivo, escolhendo se fixar na dimensão da memória, esquecendo-se das outras dimensões, por exemplo, velocidade bruta, que é outra compensação para sua abordagem preferida. Uma implementação baseada em array fornecendo "nenhuma alocação adicional" seria mais lenta do que uma implementação baseada em nó. Novamente, acho que é arbitrário aqui priorizar a memória sobre a velocidade.

Vamos dar um passo atrás e nos concentrar no que os clientes desejam. Temos uma escolha de design que pode ou não tornar a estrutura de dados inutilizável para 12% dos clientes. Acho que precisaríamos ser muito cuidadosos ao fornecer razões pelas quais escolheríamos não apoiá-los.

Uma implementação baseada em array fornecendo "nenhuma alocação adicional" seria mais lenta do que uma implementação baseada em nó.

Compartilhe as duas implementações C # que você está usando para realizar essa comparação e os benchmarks usados ​​para chegar a essa conclusão. Os artigos teóricos são certamente valiosos, mas são apenas uma pequena peça do quebra-cabeça. O mais importante é quando a borracha encontra a estrada, levando em consideração os detalhes de determinada plataforma e implementações, e você pode validar na plataforma específica com a implementação específica e conjuntos de dados / padrões de uso típicos / esperados. Pode muito bem ser que sua afirmação esteja correta. Também pode não ser. Gostaria de ver as implementações / dados para entender melhor.

Compartilhe as duas implementações C # que você está usando para realizar essa comparação e os benchmarks usados ​​para chegar a essa conclusão

Este é um ponto válido, o artigo que cito apenas compara e avalia implementações em C ++. Ele conduz vários benchmarks com diferentes conjuntos de dados e padrões de uso. Tenho certeza de que isso seria transferível para C #, mas se você acredita que isso é algo que precisamos dobrar, acho que a melhor ação seria pedir a um colega para conduzir esse estudo.

@pgolebiowski Eu estaria interessado em entender melhor a natureza de sua objeção. A proposta defende dois tipos distintos, isso não cobriria seus requisitos?

  1. uma implementação que suporta o caso de uso de 12%, enquanto também otimiza quase para os outros 88%, permitindo atualizações para elementos que são equacionáveis, e apenas preguiçosamente construindo a tabela de pesquisa necessária para fazer essas atualizações na primeira vez que um método de atualização é chamado ( e atualizá-lo em atualizações + remoções subsequentes). Portanto, incorrendo em menos custo para aplicativos que não usam a funcionalidade.

Eu provavelmente classificaria isso como uma otimização de desempenho para a opção 1, no entanto, vejo alguns problemas com essa abordagem em particular:

  • Atualizar agora se torna _O (n) _, o que pode resultar em desempenho imprevisível dependendo dos padrões de uso.
  • A tabela de pesquisa também é necessária para validar a exclusividade. Enfileirar o mesmo elemento duas vezes _antes_ de chamar Update seria aceito e, sem dúvida, colocaria a fila em um estado inconsistente.

@eiriktsarpalis É apenas O (n) uma vez, e O (1) depois, que é O (1) amortizado. E você pode adiar a validação da exclusividade até a primeira atualização. Mas talvez isso seja muito inteligente. Duas classes são mais fáceis de explicar.

Passei os últimos dias criando um protótipo de duas implementações de PriorityQueue: uma implementação básica sem suporte de atualização e uma implementação que oferece suporte a atualizações usando igualdade de elemento. Nomeei o primeiro PriorityQueue e o último, por falta de um nome melhor, PrioritySet . Meu objetivo é avaliar a ergonomia da API e comparar o desempenho.

As implementações podem ser encontradas neste repo . Ambas as classes são implementadas usando heaps quádruplos baseados em array. A implementação atualizável também usa um dicionário que mapeia elementos para índices de heap internos.

PriorityQueue básico

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

Aqui está um exemplo básico usando o 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 atualizável

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

Comparação de Desempenho

Eu escrevi um benchmark de heapsort simples que compara as duas implementações em seus aplicativos mais básicos. Também incluí um benchmark de classificação que usa o Linq para comparação:

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 | Tamanho | Média | Erro | StdDev | Razão | RatioSD | Gen 0 | Gen 1 | Gen 2 | Alocado |
| -------------- | ------ | -------------: | -----------: | -----------: | ------: | --------: | --------: | -------- : | --------: | ----------: |
| LinqSort | 30 1.439 us | 0,0072 us | 0,0064 us | 1,00 | 0,00 | 0,0095 | - | - | 672 B |
| PriorityQueue | 30 1.450 nós | 0,0085 us | 0,0079 us | 1,01 | 0,01 | - | - | - | - |
| PrioritySet | 30 2,778 us | 0,0217 us | 0,0192 us | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 300 24.727 us | 0,1032 us | 0,0915 us | 1,00 | 0,00 | 0,0305 | - | - | 3912 B |
| PriorityQueue | 300 29.510 us | 0,0995 us | 0,0882 us | 1,19 | 0,01 | - | - | - | - |
| PrioritySet | 300 47.715 us | 0,4455 us | 0.4168 us | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015 us | 1,5495 us | 1,3736 us | 1,00 | 0,00 | 0,4883 | - | - | 36312 B |
| PriorityQueue | 3000 | 491.722 us | 4.1463 us | 3,8785 us | 1,19 | 0,01 | - | - | - | - |
| PrioritySet | 3000 | 677.959 us | 3.1996 us | 2.4981 us | 1,64 | 0,01 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 30000 | 5.223.560 us | 11,9077 us | 9,9434 us | 1,00 | 0,00 | 93,7500 | 93,7500 | 93,7500 | 360910 B |
| PriorityQueue | 30000 | 5.688.625 us | 53.0746 us | 49,6460 us | 1,09 | 0,01 | - | - | - | 2 B |
| PrioritySet | 30000 | 8.124,306 us | 39,9498 us | 37.3691 us | 1,55 | 0,01 | - | - | - | 4 B |

Como pode ser esperado, a sobrecarga de localização dos elementos de rastreamento adiciona um impacto significativo no desempenho, cerca de 40-50% mais lento em comparação com a implementação da linha de base.

Agradeço todo o esforço, vejo que consumiu muito tempo e energia.

  1. Eu realmente não vejo a razão para 2 estruturas de dados quase idênticas, onde uma é uma versão inferior da outra.
  2. Além disso, mesmo se você quiser ter essas 2 versões de uma fila prioritária, não vejo como a versão "superior" é melhor do que a proposta da fila Prioritária (v2.2) de 20 dias atrás.

tl; dr:

  • Esta proposta é emocionante !! Mas ainda não se encaixa em meus casos de uso de alto desempenho.
  • > 90% da minha carga de desempenho de planejamento de geometria computacional / movimento é enfileirar / retirar da fila PQ porque esse é o N ^ m LogN dominante em um algoritmo.
  • Sou a favor de implementações PQ separadas. Normalmente não preciso de atualizações prioritárias, e 2x pior desempenho é inaceitável.
  • PrioritySet é um nome confuso e não pode ser descoberto em comparação com PriorityQueue
  • Armazenar a prioridade duas vezes (uma vez no meu elemento, uma vez na fila) parece caro. Cópias estruturais e uso de memória.
  • Se a prioridade de computação fosse cara, eu simplesmente armazenaria uma tupla (priority: ComputePriority(element), element) em um PQ, e minha função de obtenção de prioridade seria simplesmente tuple => tuple.priority .
  • O desempenho deve ser avaliado por operação ou alternativamente em casos de uso do mundo real (por exemplo, pesquisa multi-início multi-fim otimizada em um gráfico)
  • O comportamento de enumeração não ordenado é inesperado. Prefere Dequeue semelhante a fila () - semântica da ordem?
  • Considere o suporte à operação Clone e operação de mesclagem.
  • As operações básicas devem ser alocadas em 0 em uso em estado estacionário. Vou agrupar essas filas.
  • Considere oferecer suporte a EnqueueMany, que executa heapify para ajudar no pool.

Eu trabalho em pesquisa de alto desempenho (planejamento de movimento) e código de geometria computacional (por exemplo, algoritmos de linha de varredura) que é relevante para robótica e jogos, eu uso muitas filas de prioridade roladas manualmente. O outro caso de uso comum que tenho é uma consulta Top-K em que a prioridade atualizável não é útil.

Algum feedback sobre o debate de duas implementações (sim vs não suporte a atualização).

Nomenclatura:

  • PrioritySet implica definir semântica, mas a fila não implementa ISet.
  • Você está usando o nome UpdatablePriorityQueue, que é mais fácil de descobrir se eu pesquisar PriorityQueue.

Atuação:

  • O desempenho da fila de prioridade é quase sempre meu gargalo de desempenho (> 90%) em meus algoritmos de geometria / planejamento
  • Considere passar um Funcou comparaçãopara criar em vez de copiar o TPriority (caro!). Se a prioridade de computação for cara, irei inserir (prioridade, elemento) em um PQ e passar uma comparação que examina minha prioridade em cache.
  • Um número significativo de meus algoritmos não precisa de atualizações de QP. Eu consideraria usar um PQ integrado que não oferece suporte a atualizações, mas se algo tem um custo de desempenho 2x para oferecer suporte a um recurso que não preciso (atualização), então é inútil para mim.
  • Para análise de desempenho / compensações, seria importante saber o custo relativo do tempo de espera de enfileirar / retirar da fila por operação
  • Fiquei feliz em ver seus construtores realizando Heapify. Considere um construtor que usa um IList, List ou Array para evitar alocações de enumeração.
  • Considere expor um EnqueueMany que executa Heapify se o PQ estiver inicialmente vazio, uma vez que em alto desempenho é comum as coleções de pool.
  • Considere fazer um array Clear diferente de zero se os elementos não contiverem referências.
  • As alocações no enfileiramento / desenfileiramento são inaceitáveis. Meus algoritmos têm alocação zero por motivos de desempenho, com coleções locais de thread em pool.

APIs:

  • A clonagem de uma fila de prioridade é uma operação trivial com suas implementações e frequentemente útil.

    • Relacionado: a enumeração de uma fila prioritária deve ter semântica semelhante à de fila? Eu esperaria uma coleção de ordem de desenfileiramento, semelhante ao que Queue faz. Espero que a nova List (myPriorityQueue) não mude a priorityQueue, mas funcione como acabei de descrever.

  • Como mencionado acima, é preferível incluir Func<TElement, TPriority> vez de inserir com prioridade. Se a prioridade de computação for cara, posso simplesmente inserir (priority, element) e fornecer uma função tuple => tuple.priority
  • Mesclar duas filas de prioridade às vezes é útil.
  • É estranho que Peek retorne um TItem, mas Enumeration & Enqueue (TItem, TPriority).

Dito isso, para um número significativo de meus algoritmos, meus itens de fila de prioridade contêm suas prioridades, e armazenar isso duas vezes (uma vez no PQ, uma vez nos itens) parece ineficiente. Esse é especialmente o caso se eu estiver ordenando por várias chaves (caso de uso semelhante a OrderBy.ThenBy.ThenBy). Essa API também limpa muitas das inconsistências em que Insert tem prioridade, mas Peek não a retorna.

Por fim, é importante notar que frequentemente insiro índices de um array em uma Priority Queue, em vez dos próprios elementos do array . No entanto, isso é suportado por todas as APIs discutidas até agora. Por exemplo, se estou processando o início / fim dos intervalos em uma linha do eixo x, posso ter eventos de fila de prioridade (x, isStartElseEnd, intervalId) e ordenar por x e depois por isStartElseEnd. Geralmente, isso ocorre porque tenho outras estruturas de dados que são mapeadas de um índice para alguns dados computados.

@pgolebiowski Tomei a liberdade de incluir sua implementação de heap de emparelhamento proposta para os benchmarks, apenas para que possamos comparar instâncias de todas as três abordagens diretamente. Aqui estão os resultados:

| Método | Tamanho | Média | Erro | StdDev | Mediana | Gen 0 | Gen 1 | Gen 2 | Alocado |
| -------------- | -------- | -------------------: | ---- -------------: | -----------------: | ---------------- ---: | ----------: | ------: | ------: | -----------: |
| 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 |

Principais conclusões

  • ~ A implementação do heap de emparelhamento assintoticamente tem um desempenho muito melhor do que suas contrapartes baseadas em array. No entanto, pode ser até 2x mais lento para tamanhos de heap pequenos (<50 elementos), alcança cerca de 1000 elementos, mas é até 2x mais rápido para pilhas de tamanho 10 ^ 6 ~.
  • Conforme esperado, o heap de emparelhamento produz uma quantidade significativa de alocações de heap.
  • A implementação de "PrioritySet" é consistentemente lenta ~ o mais lento dos três contendores ~, portanto, talvez não desejemos seguir essa abordagem, afinal.

~ À luz do que foi dito acima, ainda acredito que haja compensações válidas entre o heap da matriz de linha de base e a abordagem de heap de emparelhamento ~.

EDIT: atualizei os resultados após uma correção de bug em meus benchmarks, obrigado @VisualMelon

@eiriktsarpalis Seu benchmark para PairingHeap está, eu acho, errado: os parâmetros para Add estão errados. Quando você os troca, é uma história diferente: https://gist.github.com/VisualMelon/00885fe50f7ab0f4ae5cd1307312109f

(Fiz exatamente a mesma coisa quando o implementei pela primeira vez)

Observe que isso não significa que o heap de emparelhamento é mais rápido ou mais lento, ao invés disso, parece depender muito da distribuição / ordem dos dados fornecidos.

@eiriktsarpalis re: a utilidade do PrioritySet ...
Não devemos esperar que o atualizável seja senão mais lento para o heapsort, uma vez que ele não tem atualizações prioritárias no cenário. (Também para o heapsort, é provável que você queira manter duplicatas, um conjunto simplesmente não é apropriado.)

O teste decisivo para ver se PrioritySet é útil deve ser algoritmos de benchmarking que usam atualizações de prioridade, em vez de uma implementação sem atualização do mesmo algoritmo, enfileirando os valores duplicados e ignorando as duplicatas ao retirar da fila.

Obrigado @VisualMelon , atualizei meus resultados e comentários após sua correção sugerida.

em vez disso, parece depender fortemente da distribuição / ordem dos dados fornecidos.

Eu acredito que pode ter se beneficiado do fato de que as prioridades enfileiradas eram monotônicas.

O teste decisivo para ver se PrioritySet é útil deve ser algoritmos de benchmarking que usam atualizações de prioridade, em vez de uma implementação sem atualização do mesmo algoritmo, enfileirando os valores duplicados e ignorando as duplicatas ao retirar da fila.

@TimLovellSmith, meu objetivo aqui era medir o desempenho do aplicativo PriorityQueue mais comum: em vez de medir o desempenho das atualizações, eu queria ver o impacto no caso em que as atualizações não são necessárias. No entanto, pode fazer sentido produzir um benchmark separado que compare a pilha de emparelhamento com as atualizações "PrioritySet".

@miyu obrigado por seus comentários detalhados, é muito apreciado!

@TimLovellSmith Escrevi um benchmark simples que usa atualizações:

| Método | Tamanho | Média | Erro | StdDev | Mediana | Gen 0 | Gen 1 | Gen 2 | Alocado |
| ------------ | -------- | ---------------: | ---------- -----: | ---------------: | ---------------: | -------: | ------: | ------: | -----------: |
| PrioritySet | 10 1.052 us | 0,0106 nós | 0,0099 us | 1.055 us | - | - | - | - |
| PairingHeap | 10 1.055 us | 0,0042 us | 0,0035 us | 1.055 us | 0,0057 | - | - | 480 B |
| PrioritySet | 50 7.394 us | 0,0527 us | 0,0493 us | 7.380 us | - | - | - | - |
| PairingHeap | 50 8.587 us | 0,1678 us | 0,1570 us | 8.634 us | 0,0305 | - | - | 2400 B |
| PrioritySet | 150 27.522 us | 0,0459 us | 0,0359 us | 27.523 us | - | - | - | - |
| PairingHeap | 150 32.045 us | 0,1076 us | 0,1007 us | 32.019 us | 0,0610 | - | - | 7200 B |
| PrioritySet | 500 109.097 us | 0,6548 us | 0,6125 us | 109.162 us | - | - | - | - |
| PairingHeap | 500 131.647 us | 0,5401 us | 0,4510 us | 131.588 us | 0,2441 | - | - | 24000 B |
| PrioritySet | 1000 238.184 us | 1.0282 us | 0,9618 us | 238.457 us | - | - | - | - |
| PairingHeap | 1000 293.236 us | 0,9396 us | 0,8789 us | 293.257 us | 0,4883 | - | - | 48000 B |
| PrioritySet | 10000 3.035,982 us | 12.2952 us | 10.8994 us | 3.036,985 us | - | - | - | 1 B |
| PairingHeap | 10000 3,388,685 us | 16.0675 us | 38.1861 us | 3,374,565 us | - | - | - | 480002 B |
| PrioritySet | 1000000 | 841.406.888 us | 16.788,4775 us | 15.703,9522 us | 840.888.389 us | - | - | - | 288 B |
| PairingHeap | 1000000 | 989.966.501 us | 19.722,6687 us | 30,705,8191 us | 996.075.410 us | - | - | - | 48000448 B |

Em uma nota separada, a discussão / feedback deles sobre a falta de estabilidade ser um problema (ou não) para os casos de uso das pessoas?

tem havido discussão / feedback sobre a falta de estabilidade ser um problema (ou não) para o caso de uso das pessoas

Nenhuma das implementações garante estabilidade, no entanto, deve ser bastante simples para os usuários obterem estabilidade aumentando o ordinal com a ordem de inserção:

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 algumas das minhas postagens anteriores, estou tentando identificar como seria uma fila de prioridade popular do .NET, então analisei os seguintes dados:

  • Padrões de uso de fila de prioridade comum no código-fonte .NET.
  • Implementações de PriorityQueue em bibliotecas centrais de estruturas concorrentes.
  • Benchmarks de vários protótipos de fila de prioridade .NET.

Que resultou nas seguintes conclusões:

  • 90% dos casos de uso de fila de prioridade não requerem atualizações de prioridade.
  • O suporte a atualizações de prioridade resulta em um contrato de API mais complicado (exigindo identificadores ou exclusividade de elemento).
  • Em meus benchmarks, as implementações que suportam atualizações prioritárias são 2 a 3 vezes mais lentas em comparação com aquelas que não suportam.

Próximos passos

No futuro, proponho que tomemos as seguintes ações para .NET 6:

  1. Apresente uma classe System.Collections.Generic.PriorityQueue que seja simples, atenda à maioria dos nossos requisitos de usuário e seja o mais eficiente possível. Ele usará um heap quaternário baseado em array e não oferecerá suporte a atualizações prioritárias. Um protótipo da implementação pode ser encontrado aqui . Estarei criando uma edição separada detalhando a proposta da API em breve.

  2. Reconhecemos a necessidade de heaps que ofereçam suporte a atualizações prioritárias eficientes, portanto, continuaremos trabalhando para introduzir uma classe especializada que atenda a esse requisito. Estamos avaliando alguns protótipos [ 1 , 2 ], cada um com seus próprios conjuntos de compensações. Minha recomendação seria introduzir este tipo em um estágio posterior, já que mais trabalho é necessário para finalizar o design.

Neste momento, gostaria de agradecer aos contribuidores deste tópico, em particular @pgolebiowski e @TimLovellSmith. Seu feedback desempenhou um papel importante na orientação de nosso processo de design. Espero continuar recebendo sua opinião enquanto desenhamos o design da fila de prioridade atualizável.

No futuro, proponho que tomemos as seguintes ações para .NET 6: [...]

Soa bem :)

Apresente uma classe System.Collections.Generic.PriorityQueue que seja simples, atenda à maioria dos nossos requisitos de usuário e seja o mais eficiente possível. Ele usará um heap quaternário baseado em array e não oferecerá suporte a atualizações prioritárias.

Se tivermos a decisão dos proprietários da base de código de que essa direção é aprovada e desejada, posso continuar liderando o design da API para essa parte e fornecer a implementação final?

Neste momento, gostaria de agradecer aos contribuidores deste tópico, em particular @pgolebiowski e @TimLovellSmith. Seu feedback desempenhou um papel importante na orientação de nosso processo de design. Espero continuar recebendo sua opinião enquanto desenhamos o design da fila de prioridade atualizável.

Foi uma viagem e tanto: D

A API para System.Collections.Generic.PriorityQueue<TElement, TPriority> acaba de ser aprovada. Eu criei um problema separado para continuar nossa conversa sobre uma possível implementação de heap que oferece suporte a atualizações prioritárias.

Vou encerrar esta edição, obrigado a todos por suas contribuições!

Talvez alguém possa escrever sobre essa jornada! Um total de 6 anos para uma API. :) Alguma chance de ganhar um Guinness?

Esta página foi útil?
0 / 5 - 0 avaliações