Runtime: Ajouter une file d'attente prioritaire<t>aux collections</t>

Créé le 30 janv. 2015  ·  318Commentaires  ·  Source: dotnet/runtime

Voir la DERNIÈRE proposition dans le référentiel corefxlab.

Options de la deuxième proposition

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

Hypothèses

Les éléments de la file d'attente prioritaire sont uniques. S'ils ne le sont pas, nous devrons introduire des « poignées » d'éléments pour permettre leur mise à jour/suppression. Ou la sémantique de mise à jour/suppression devrait s'appliquer à tout d'abord, ce qui est étrange.

Modélisé d'après Queue<T> ( lien MSDN )

API

```c#
classe publique PriorityQueue
: IEnumerable,
IEnumerable<(élément TElement, priorité TPriority)>,
IReadOnlyCollection<(élément TElement, priorité TPriority)>
// Collection non incluse exprès
{
public PriorityQueue();
public PriorityQueue(IComparercomparateur);

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

//
// Pièce sélecteur
//
public PriorityQueue(FuncPrioritySelector);
public PriorityQueue(FuncPrioritySelector, IComparercomparateur);

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

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

}
````

Questions ouvertes:

  1. Nom de la classe PriorityQueue vs Heap
  2. Introduire IHeap et la surcharge du constructeur ? (doit-on attendre plus tard ?)
  3. Présentez IPriorityQueue ? (Faut-il attendre plus tard - exemple IDictionary )
  4. Utiliser le sélecteur (de priorité stocké à l'intérieur de la valeur) ou non (différence 5 API)
  5. Utilisez des tuples (TElement element, TPriority priority) contre KeyValuePair<TPriority, TElement>

    • Peek et Dequeue devraient-ils plutôt avoir out argument

  6. Le lancer de Peek et Dequeue vraiment utile ?

Proposition originale

Le problème https://github.com/dotnet/corefx/issues/163 a demandé l'ajout d'une file d'attente prioritaire aux structures de données de collecte de base .NET.

Ce message, bien qu'il s'agisse d'un doublon, est destiné à constituer la soumission formelle au processus d'examen de l'API corefx. Le contenu du problème est le _speclet_ pour un nouveau System.Collections.Generic.PriorityQueuetaper.

Je contribuerai au PR, si approuvé.

Justification et utilisation

Les bibliothèques de classes de base .NET (BCL) ne prennent actuellement pas en charge les collections ordonnées producteur-consommateur. Une exigence commune à de nombreuses applications logicielles est la capacité de générer une liste d'articles au fil du temps et de les traiter dans un ordre différent de celui dans lequel ils ont été reçus.

Il existe trois structures de données génériques dans la hiérarchie System.Collections des espaces de noms qui prennent en charge une collection triée d'éléments ; System.Collections.Generic.SortedList, System.Collections.Generic.SortedSet et System.Collections.Generic.SortedDictionary.

Parmi ceux-ci, SortedSet et SortedDictionary ne conviennent pas aux modèles producteur-consommateur qui génèrent des valeurs en double. La complexité de SortedList est Θ(n) le pire des cas pour Add et Remove.

Une structure de données beaucoup plus efficace en termes de mémoire et de temps pour les collections ordonnées avec des modèles d'utilisation producteur-consommateur est une file d'attente prioritaire. Sauf lorsque le redimensionnement de la capacité est nécessaire, les performances d'insertion (mise en file d'attente) et de suppression (sortie de file d'attente) dans le pire des cas sont (log n) - bien meilleures que les options existantes qui existent dans la BCL.

Les files d'attente prioritaires ont un large degré d'applicabilité dans différentes classes d'applications. La page Wikipedia sur les files d'attente prioritaires propose une liste de nombreux cas d'utilisation bien compris. Alors que des implémentations hautement spécialisées peuvent encore nécessiter des implémentations de file d'attente prioritaire personnalisée, une implémentation standard couvrirait un large éventail de scénarios d'utilisation. Les files d'attente prioritaires sont particulièrement utiles pour planifier la sortie de plusieurs producteurs, ce qui est un modèle important dans les logiciels hautement parallélisés.

Il convient de noter que la bibliothèque standard C++ et Java offrent une fonctionnalité de file d'attente prioritaire dans le cadre de leurs API de base.

API proposée

``` C#
espace de noms System.Collections.Generic
{
///


/// Représente une collection d'objets qui sont supprimés dans un ordre trié.
///

///Spécifie le type d'éléments dans la file d'attente.
[DebuggerDisplay("Count = {count}")]
[DebuggerTypeProxy(typeof(System_PriorityQueueDebugView<>))]
classe publique PriorityQueue: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
///
/// Initialise une nouvelle instance du classer
/// qui utilise un comparateur par défaut.
///

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

}
```

Des détails

  • La structure de données d'implémentation sera un tas binaire. Les articles avec une valeur de comparaison supérieure seront renvoyés en premier. (Ordre décroissant)
  • Complexités temporelles :

| Opération | Complexité | Remarques |
| --- | --- | --- |
| Construire | (1) | |
| Construire à l'aide de IEnumerable | (n) | |
| Mettre en file d'attente | (log n) | |
| Sortir de la file d'attente | (log n) | |
| Coup d'oeil | (1) | |
| Compte | (1) | |
| Effacer | (N) | |
| Contient | (N) | |
| Copier vers | (N) | Utilise Array.Copy, la complexité réelle peut être inférieure |
| ToArray | (N) | Utilise Array.Copy, la complexité réelle peut être inférieure |
| GetEnumerator | (1) | |
| Enumerator.MoveNext | (1) | |

  • Surcharges de constructeur supplémentaires qui prennent le System.Comparisondélégué ont été volontairement omis au profit d'une surface API simplifiée. Les appelants peuvent utiliser le comparateur.Create pour convertir une fonction ou une expression Lambda en IComparerinterface si nécessaire. Cela nécessite que l'appelant encoure une allocation de tas unique.
  • Bien que System.Collections.Generic ne fasse pas encore partie de corefx, je propose que cette classe soit ajoutée à corefxlab en attendant. Il peut être déplacé vers le référentiel corefx principal une fois que System.Collections.Generic est ajouté et il existe un consensus sur le fait que son statut devrait être élevé d'expérimental à une API officielle.
  • Une propriété IsEmpty n'a pas été incluse, car il n'y a pas de pénalité de performance supplémentaire en appelant Count. La majorité des structures de données de collecte n'incluent pas IsEmpty.
  • Les propriétés IsSynchronized et SyncRoot de ICollection ont été implémentées explicitement car elles sont effectivement obsolètes. Cela suit également le modèle utilisé pour les autres structures de données System.Collection.Generic.
  • Dequeue et Peek lèvent une InvalidOperationException lorsque la file d'attente est vide pour correspondre au comportement établi de System.Collections.Queue.
  • IProducerConsumerCollectionn'a pas été implémenté car sa documentation indique qu'il est uniquement destiné aux collections thread-safe.

Questions ouvertes

  • Le fait d'éviter une allocation de tas supplémentaire lors des appels à GetEnumerator lors de l'utilisation de foreach est-il une justification suffisamment solide pour inclure la structure d'énumérateur public imbriquée ?
  • CopyTo, ToArray et GetEnumerator doivent-ils renvoyer les résultats dans l'ordre prioritaire (trié) ou dans l'ordre interne utilisé par la structure de données ? Mon hypothèse est que l'ordre interne doit être retourné, car il n'encourt aucune pénalité de performance supplémentaire. Cependant, il s'agit d'un problème potentiel d'utilisation si un développeur considère la classe comme une « file d'attente triée » plutôt qu'une file d'attente prioritaire.
  • L'ajout d'un type nommé PriorityQueue à System.Collections.Generic provoque-t-il un changement potentiellement décisif ? L'espace de noms est fortement utilisé et peut provoquer un problème de compatibilité des sources pour les projets qui incluent leur propre type de file d'attente prioritaire.
  • Les éléments doivent-ils être retirés de la file d'attente par ordre croissant ou décroissant, en fonction de la sortie d'IComparer? (mon hypothèse est l'ordre croissant, pour correspondre à la convention de tri normale d'IComparer).
  • La collection doit-elle être « stable » ? En d'autres termes, si deux éléments avec IComparison égalles résultats soient-ils retirés de la file d'attente exactement dans le même ordre dans lequel ils sont mis en file d'attente ? (je suppose que ce n'est pas nécessaire)

    Mises à jour

  • Correction de la complexité de 'Construire en utilisant IEnumerable' à (n). Merci @svick.

  • Ajout d'une autre question optionnelle indiquant si la file d'attente prioritaire doit être classée par ordre croissant ou décroissant par rapport à IComparer.
  • Suppression de NotSupportedException de la propriété SyncRoot explicite pour correspondre au comportement des autres types System.Collection.Generic au lieu d'utiliser le modèle plus récent.
  • La méthode publique GetEnumerator a renvoyé une structure Enumerator imbriquée au lieu de IEnumerable, similaire aux types System.Collections.Generic existants. Il s'agit d'une optimisation pour éviter une allocation de tas (GC) lors de l'utilisation d'une boucle foreach.
  • Suppression de l'attribut ComVisible.
  • Modification de la complexité de Clear en (n). Merci @mbeidler.
api-needs-work area-System.Collections wishlist

Commentaire le plus utile

la structure de données de tas est un MUST pour faire leetcode
plus de leetcode, plus d'interview de code c#, ce qui signifie plus de développeurs c#.
plus de développeurs signifie un meilleur écosystème.
un meilleur écosystème signifie que si nous pouvons encore programmer en c# demain.

en somme : ce n'est pas seulement une caractéristique, mais aussi l'avenir. c'est pourquoi ce problème est étiqueté « futur ».

Tous les 318 commentaires

| Opération | Complexité |
| --- | --- |
| Construire à l'aide de IEnumerable | (log n) |

Je pense que cela devrait être Θ(n). Vous devez au moins itérer l'entrée.

+1

Une structure d'énumérateur public imbriquée doit-elle être utilisée pour éviter une allocation de tas supplémentaire lors des appels à GetEnumerator et lors de l'utilisation de foreach ? Mon hypothèse est non, car l'énumération sur une file d'attente est une opération rare.

Je pencherais vers l'utilisation de l'énumérateur struct pour être cohérent avec Queue<T> qui utilise un énumérateur struct. De plus, si un énumérateur de structure n'est pas utilisé maintenant, nous ne pourrons pas modifier PriorityQueue<T> pour en utiliser un à l'avenir.

Peut-être aussi une méthode pour les insertions par lots ? Peut toujours trier et continuer à partir du point d'insertion précédent plutôt que de commencer au début si cela peut aider ? :

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

J'ai jeté une copie de la mise en œuvre initiale ici . La couverture des tests est loin d'être complète, mais si quelqu'un est curieux, veuillez jeter un œil et me faire savoir ce que vous en pensez. J'ai essayé de suivre autant que possible les conventions de codage existantes des classes System.Collections.

Frais. Quelques premiers retours :

  • Queue<T>.Enumerator implémente IEnumerator.Reset explicitement. PriorityQueue<T>.Enumerator devrait-il faire de même ?
  • Queue<T>.Enumerator utilise _index == -2 pour indiquer que l'énumérateur a été supprimé. PriorityQueue<T>.Enumerator a le même commentaire mais a un champ _disposed supplémentaire. Envisagez de vous débarrasser du champ _disposed supplémentaire et utilisez _index == -2 pour indiquer qu'il a été supprimé pour réduire la structure et être cohérent avec Queue<T>
  • Je pense que le champ statique _emptyArray peut être supprimé et l'utilisation remplacée par Array.Empty<T>() place.

Aussi...

  • D'autres collections qui prennent un comparateur (par exemple Dictionary<TKey, TValue> , HashSet<T> , SortedList<TKey, TValue> , SortedDictionary<TKey, TValue> , SortedSet<T> , etc.) permettent à null d'être transmis pour le comparateur, auquel cas Comparer<T>.Default est utilisé.

Aussi...

  • ToArray peut être optimisé en vérifiant _size == 0 avant d'allouer le nouveau tableau, auquel cas il suffit de retourner Array.Empty<T>() .

@justinvp Excellent retour, merci !

  • J'ai implémenté Enumerator.Reset explicitement car il s'agit de la fonctionnalité principale et non obsolète d'un énumérateur. Qu'elle soit ou non exposée semble incohérente d'un type de collection à l'autre, et seuls quelques-uns utilisent la variante explicite.
  • Suppression du champ _disposed en faveur de _index, merci ! Je l'ai jeté à la dernière minute ce soir-là et j'ai raté l'évidence. Décidé de conserver l'ObjectDisposedException pour l'exactitude avec les nouveaux types de collection, même si les anciens types System.Collections.Generic ne l'utilisent pas.
  • Tableau.Videest une fonctionnalité F#, donc je ne peux malheureusement pas l'utiliser ici !
  • Modification des paramètres du comparateur pour accepter null, bonne recherche !
  • L'optimisation ToArray est délicate. Les tableaux _techniquement_ sont mutables en C#, même s'ils ont une longueur de zéro. En réalité vous avez raison, l'allocation n'est pas nécessaire et elle peut être optimisée. Je penche pour une mise en œuvre plus prudente, au cas où il y aurait des effets secondaires auxquels je ne pense pas. Sémantiquement, l'appelant attendra toujours cette allocation, et elle est mineure.

@ebickle

Array.Empty est une fonctionnalité F#, donc je ne peux malheureusement pas l'utiliser ici !

Plus maintenant : https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Array.cs#L1060 -L1069

J'ai migré le code vers ebicle/corefx dans la branche issue-574.

Implémentation de Array.Empty() change et a tout branché dans le pipeline de construction normal. Un léger problème temporaire que j'ai dû introduire était que le projet System.Collections dépende du package nuget System.Collections, en tant que comparateurn'est pas encore open source.

Sera corrigé une fois le problème dotnet/corefx#966 terminé.

Un domaine clé dans lequel je recherche des commentaires est la façon dont ToArray, CopyTo et l'énumérateur doivent être gérés. Actuellement, ils sont optimisés pour les performances, ce qui signifie que le tableau cible est un tas et non trié par le comparateur.

Il y a trois options :
1) Laissez-le tel quel et documentez que le tableau renvoyé n'est pas trié. (c'est une file d'attente prioritaire, pas une file d'attente triée)
2) Modifiez les méthodes de tri des éléments/tableaux renvoyés. Ce ne seront plus des opérations O(n).
3) Supprimez complètement les méthodes et le support énumérable. Il s'agit de l'option "puriste" mais supprime la possibilité de récupérer rapidement les éléments restants en file d'attente lorsque la file d'attente prioritaire n'est plus nécessaire.

Une autre chose sur laquelle j'aimerais avoir des commentaires est de savoir si la file d'attente doit être stable pour deux éléments avec la même priorité (comparer le résultat de 0). En général, les files d'attente prioritaires ne garantissent pas que deux éléments ayant la même priorité seront retirés de la file d'attente dans l'ordre dans lequel ils ont été mis en file d'attente, mais j'ai remarqué que l'implémentation de la file d'attente prioritaire interne utilisée dans System.Reactive.Core prenait une charge supplémentaire pour garantir cette propriété. Ma préférence serait de ne pas le faire, mais je ne sais pas exactement quelle option est la meilleure en termes d'attentes des développeurs.

Je suis tombé sur ce PR parce que moi aussi j'étais intéressé par l'ajout d'une file d'attente prioritaire à .NET. Heureux de voir que quelqu'un a fait l'effort de faire cette proposition :). Après avoir examiné le code, j'ai remarqué ce qui suit :

  • Lorsque l'ordre IComparer n'est pas cohérent avec Equals , le comportement de cette implémentation Contains (qui utilise le IComparer ) peut surprendre certains utilisateurs, car il s'agit essentiellement de _contient un élément de priorité égale_.
  • Je n'ai pas vu de code pour réduire le tableau dans Dequeue . Réduire de moitié le tas au quart lorsqu'il est plein est typique.
  • La méthode Enqueue accepter les arguments null ?
  • Je pense que la complexité de Clear devrait être Θ(n), puisque c'est la complexité de System.Array.Clear , qu'il utilise. https://msdn.microsoft.com/en-us/library/system.array.clear%28v=vs.110%29.aspx

Je n'ai pas vu de code pour réduire le tableau dans Dequeue . Réduire de moitié le tas au quart lorsqu'il est plein est typique.

Queue<T> et Stack<T> ne réduisent pas non plus leurs tableaux (basés sur Reference Source , ils ne sont pas encore dans CoreFX).

@mbeidler J'avais envisagé d'ajouter une forme de rétrécissement automatique des tableaux dans la file d' @svick l'a souligné, cela n'existe pas dans les implémentations de référence de structures de données similaires. Je serais curieux d'entendre des membres de l'équipe .NET Core/BCL s'il y a une raison particulière pour laquelle ils ont choisi ce style d'implémentation.

Mise à jour : j'ai vérifié la liste, File d'attente, Queue et ArrayList - aucun d'entre eux ne réduit la taille du tableau interne lors d'une suppression/sortie de la file d'attente.

La mise en file d'attente doit prendre en charge les valeurs NULL et est documentée comme les autorisant. Avez-vous rencontré un bug ? Je ne me souviens pas encore de la robustesse de la zone de tests unitaires dans la région.

Intéressant, j'ai remarqué dans la source de référence liée par @svick que Queue<T> a une constante privée inutilisée nommée _ShrinkThreshold . Peut-être que ce comportement existait dans une version précédente.

Concernant l'utilisation de IComparer au lieu de Equals dans l'implémentation de Contains , j'ai rédigé le test unitaire suivant, qui échouerait actuellement : https://gist.github. com/mbeidler/9e9f566ba7356302c57e

@mbeidler Bon point. Selon MSDN, IComparer/ IComparablegarantit seulement qu'une valeur de zéro a le même ordre de tri.

Cependant, il semble que le même problème existe dans les autres classes de collection. Si je modifie le code pour opérer sur SortedList, le scénario de test échoue toujours sur ContainsKey. L'implémentation de SortedList.ContainsKey appelle Array.BinarySearch, qui s'appuie sur IComparer pour vérifier l'égalité. Il en va de même pour SortedSet.

On peut dire que c'est aussi un bogue dans les classes de collection existantes. Je vais fouiller dans le reste des classes de collections et voir s'il existe d'autres structures de données qui acceptent un IComparer mais testent l'égalité séparément. Vous avez raison cependant, pour une file d'attente prioritaire, vous vous attendriez à un comportement de commande personnalisé totalement indépendant de l'égalité.

J'ai commis un correctif et le cas de test dans ma branche fork. La nouvelle implémentation de Contains est basée directement sur le comportement de List.Contient. Depuis la listen'accepte pas un IEqualityComparer, le comportement est fonctionnellement équivalent.

Quand j'aurai un peu de temps plus tard dans la journée, je soumettrai probablement des rapports de bogues pour les autres collections intégrées. Le problème ne peut probablement pas être corrigé en raison d'un comportement de régression, mais au moins la documentation doit être mise à jour.

Je pense qu'il est logique que ContainsKey utilise l'implémentation IComparer<TKey> , puisque c'est ce qui spécifie la clé. Cependant, je pense qu'il serait plus logique que ContainsValue utilise Equals au lieu de IComparable<TValue> dans sa recherche linéaire ; bien que dans ce cas, la portée soit considérablement réduite car l'ordre naturel d'un type est beaucoup moins susceptible d'être incompatible avec des égaux.

Il semble que dans la documentation MSDN pour SortedList<TKey, TValue> , la section des remarques pour ContainsValue indique que l'ordre de tri de TValue est utilisé au lieu de l'égalité.

@terrajobst Que

:+1:

Merci d'avoir déposé ceci. Je pense que nous avons suffisamment de données pour procéder à un examen formel de cette proposition, c'est pourquoi je l'ai qualifiée de « prête pour l'examen de l'API »

Comme Dequeue et Peek sont des méthodes de lancement, l'appelant doit vérifier le nombre avant chaque appel. Serait-il judicieux de fournir à la place (ou en plus) TryDequeue et TryPeek suivant le modèle des collectes simultanées ? Il y a des problèmes ouverts pour ajouter des méthodes de non-lancement aux collections génériques existantes, donc l'ajout d'une nouvelle collection qui n'a pas ces méthodes semble contre-productif.

@andrewgmorris lié https://github.com/dotnet/corefx/issues/4316 "Ajouter TryDequeue à la file d'attente"

Nous avons eu un examen de base de cela et nous convenons que nous voulons une ProrityQueue dans le cadre. Cependant, nous avons besoin de quelqu'un pour aider à conduire la conception et la mise en œuvre de celui-ci. Celui qui saisit le problème peut travailler @terrajobst sur la finalisation de l'API.

Alors, quel travail reste-t-il sur l'API ?

Cela manque dans la proposition d'API ci-dessus : PriorityQueue<T> devrait implémenter IReadOnlyCollection<T> pour correspondre à Queue<T> ( Queue<T> implémente maintenant IReadOnlyCollection<T> partir de .NET 4.6).

Je ne sais pas si les files d'attente prioritaires basées sur les tableaux sont les meilleures. L'allocation de mémoire dans .NET est vraiment rapide. Nous n'avons pas le même problème de recherche de petits blocs que l'ancien malloc traitait. Vous pouvez utiliser mon code de file d'attente prioritaire à partir d'ici : https://github.com/BrannonKing/Kts.Astar/tree/master/Kts.AStar

@ebicle Une petite

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

Ne devrait-il pas dire à la place, /// Inserts object into the <see cref="PriorityQueue{T}"> by its priority.

@SunnyWar Correction de la documentation de la méthode Enqueue, merci !

Il y a quelque temps, j'ai créé une structure de données avec des complexités similaires à une file d'attente prioritaire basée sur une structure de données Skip List que j'ai décidé à ce stade de partager : https://gist.github.com/bbarry/5e0f3cc1ac7f7521fe6ea25947f48ace

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

La liste de sauts correspond aux complexités d'une file d'attente prioritaire ci-dessus dans les cas moyens, sauf que Contient est un cas moyen O(log(n)) . De plus, l'accès au premier ou au dernier élément est une opération à temps constant et l'itération dans l'ordre avant et arrière correspond aux complexités de l'ordre avant d'un PQ.

Il y a évidemment des inconvénients à une telle structure sous la forme de coûts plus élevés en mémoire, et cela se traduit par des insertions et des suppressions dans le pire des cas de O(n) , donc elle a ses compromis...

Est-ce déjà implémenté quelque part ? Quand est la sortie prévue ?
Qu'en est-il également de la mise à jour de la priorité d'un élément existant ?

@Priya91 @ianhays est-ce prêt à être marqué et prêt à être révisé ?

Ceci est manquant dans la proposition d'API ci-dessus : PriorityQueuedevrait implémenter IReadOnlyCollectionpour correspondre à la file d'attente(File d'attenteimplémente maintenant IReadOnlyCollectionà partir de .NET 4.6).

Je suis d'accord avec @justinvp ici.

@Priya91 @ianhays est-ce prêt à être marqué et prêt à être révisé ?

Je le dirais. Cela a été assis pendant un certain temps; faisons des mouvements dessus.

@justinvp @ianhays J'ai mis à jour la spécification pour implémenter IReadOnlyCollection. Merci!

J'ai une implémentation complète de la classe et son PriorityQueueDebugView associé qui utilise une implémentation basée sur un tableau. Les tests unitaires ne sont pas encore à 100% de couverture, mais s'il y a un certain intérêt, je peux faire un peu de travail et dépoussiérer ma fourche.

@NKnusperer a fait un bon point sur la mise à jour de la priorité d'un élément existant. Je vais le laisser de côté pour le moment, mais c'est quelque chose à considérer lors de l'examen des spécifications.

Il existe 2 implémentations dans le framework complet, qui sont des alternatives possibles.
https://referencesource.microsoft.com/#q =priorityqueue

Pour référence, voici une question sur Java PriorityQueue sur stackoverflow http://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueue. Il est intéressant de noter que la priorité est gérée par un comparateur au lieu d'un simple objet wrapper de priorité int. Par exemple, cela ne facilite pas la modification de la priorité d'un élément déjà dans la file d'attente.

Examen de l'API :
Nous convenons qu'il est utile d'avoir ce type dans CoreFX, car nous nous attendons à ce que CoreFX l'utilise.

Pour l'examen final de la forme de l'API, nous aimerions voir un exemple de code : PriorityQueue<Thread> et PriorityQueue<MyClass> .

  1. Comment maintenir la priorité ? Pour le moment, cela n'est implicite que par T .
  2. Voulons-nous qu'il soit possible de passer la priorité lorsque vous ajoutez une entrée ? (ce qui semble assez pratique)

Remarques:

  • Nous nous attendons à ce que la priorité ne mute pas d'elle-même - nous aurions besoin de l'une ou l'autre API pour cela, ou nous nous attendrions Remove ce que Add soient dans la file d'attente.
  • Étant donné que nous n'avons pas de code client clair ici (juste le souhait général que nous voulions le type), il est difficile de décider pour quoi optimiser - perf, vs convivialité vs autre chose ?

Ce serait un type vraiment utile à avoir dans CoreFX. Est-ce que quelqu'un est intéressé à saisir celui-ci?

Je n'aime pas l'idée de fixer la file d'attente prioritaire à un tas binaire. Jetez un œil à ma page wiki AlgoKit pour plus de détails. Réflexions rapides :

  • Si nous fixons l'implémentation à un certain type de tas, en faire un tas 4-aire.
  • Nous ne devons pas corriger l'implémentation du tas, car dans certains scénarios, certains types de tas sont plus performants que d'autres. Ainsi, étant donné que nous fixons l'implémentation à un certain type de tas et que le client a besoin d'un autre pour plus de performances, il devrait implémenter l'intégralité de la file d'attente prioritaire à partir de zéro. C'est faux. Nous devrions être plus flexibles ici et le laisser réutiliser une partie du code CoreFX (au moins certaines interfaces).

L'année dernière, j'ai implémenté quelques types de tas . En particulier, il existe IHeap interface

  • Pourquoi ne pas simplement appeler ça un tas...? Connaissez-vous un moyen sensé d'implémenter une file d'attente prioritaire autre que l'utilisation d'un tas ?

Je suis tout à fait favorable à l'introduction d'une interface IHeap et de certaines implémentations les plus performantes (au moins celles basées sur les tableaux). L'API et les implémentations se trouvent dans le référentiel que j'ai lié ci-dessus.

Donc pas de files d'attente prioritaires. Des tas .

Qu'est-ce que tu penses?

@karelz @safern @danmosemsft

@pgolebiowski N'oubliez pas que nous PriorityQueue (qui est un terme informatique établi), vous devriez pouvoir en trouver un dans docs / via une recherche sur Internet.
Si l'implémentation sous-jacente est un tas (ce que je me demande personnellement pourquoi), ou autre chose, cela n'a pas trop d'importance. En supposant que vous puissiez démontrer que la mise en œuvre est nettement meilleure que des alternatives plus simples (la complexité du code est également une métrique qui compte quelque peu).

Donc, si vous pensez toujours que l'implémentation basée sur le tas (sans IHeap API) est meilleure qu'une simple implémentation basée sur une liste ou une liste de morceaux de tableau, veuillez expliquer pourquoi (idéalement en quelques phrases/paragraphes ), afin que nous puissions nous mettre d'accord sur l'approche de mise en œuvre (et éviter de perdre du temps de votre côté sur une mise en œuvre complexe qui peut être refusée au moment de l'examen des relations publiques).

Déposer ICollection , IEnumerable ? Ayez juste les versions génériques (bien que IEnumerable<T> générique apportera IEnumerable )

@pgolebiowski comment sa mise en œuvre ne change pas l'api externe. PriorityQueue définit le comportement/contrat ; alors qu'un Heap est une implémentation spécifique.

Files d'attente prioritaires vs tas

Si l'implémentation sous-jacente est un tas (ce que je me demande personnellement pourquoi), ou autre chose, cela n'a pas trop d'importance. En supposant que vous puissiez démontrer que la mise en œuvre est nettement meilleure que des alternatives plus simples (la complexité du code est également une métrique qui compte quelque peu).

Donc, si vous pensez toujours que l'implémentation basée sur le tas est meilleure qu'une simple implémentation basée sur une liste ou une liste de morceaux de tableau, veuillez expliquer pourquoi (idéalement en quelques phrases/paragraphes), afin que nous puissions nous mettre d'accord sur le approche de mise en œuvre [...]

D'ACCORD. Une file d'attente prioritaire est une structure de données abstraite qui peut ensuite être implémentée d' une manière ou d'

  • file d'attente prioritaire et tas sont souvent utilisés comme synonymes (voir la documentation Python ci-dessous comme exemple le plus clair)
  • chaque fois que vous avez un support de "file d'attente prioritaire" dans une bibliothèque, il utilise un tas (voir tous les exemples ci-dessous)

Pour étayer mes propos, commençons par le support théorique. Introduction aux algorithmes , Cormen :

[…] les files d'attente prioritaires se présentent sous deux formes : les files d'attente à priorité maximale et les files d'attente à priorité minimale. Nous nous concentrerons ici sur la façon d'implémenter des files d'attente de priorité maximale, qui sont à leur tour basées sur des tas max.

A clairement indiqué que les files d'attente prioritaires sont des tas. C'est un raccourci bien sûr, mais vous voyez l'idée. Maintenant, plus important encore, voyons comment d'autres langages et leurs bibliothèques standard ajoutent la prise en charge des opérations dont nous discutons :

  • Java : PriorityQueue<T> — file d'attente prioritaire implémentée avec un tas.
  • Rouille : BinaryHeap — tas explicitement dans l'API. Il est dit dans la documentation qu'il s'agit d' une file d'attente prioritaire implémentée avec un tas binaire. Mais l'API est très claire sur la structure — un tas.
  • Swift : CFBinaryHeap — encore une fois, indique explicitement quelle est la structure des données, en évitant l'utilisation du terme abstrait « file d'attente prioritaire ». La doc décrivant la classe : Les tas binaires peuvent être utiles comme files d'attente prioritaires. J'aime l'approche.
  • C++ : priority_queue — encore une fois, implémenté de manière canonique avec un tas binaire construit au sommet d'un tableau.
  • Python : heapq — le tas est explicitement exposé dans l'API. La file d'attente prioritaire n'est mentionnée que dans la doc : Ce module fournit une implémentation de l'algorithme de file d'attente de tas, également connu sous le nom d'algorithme de file d'attente prioritaire.
  • Allez : heap package — il y a même une interface de tas. Pas de file d'attente prioritaire explicite, mais encore une fois uniquement dans les documents : un tas est un moyen courant d'implémenter une file d'attente prioritaire.

Je crois fermement que nous devrions suivre la voie Rust / Swift / Python / Go et exposer explicitement un tas tout en indiquant clairement dans la documentation qu'il peut être utilisé comme file d'attente prioritaire. Je crois fermement que cette approche est très propre et simple.

  • Nous sommes clairs sur la structure des données et laissons l'API être améliorée à l'avenir - si quelqu'un propose une nouvelle façon révolutionnaire d'implémenter une file d'attente prioritaire qui est meilleure à certains égards (et le tas de choix par rapport au nouveau type dépendrait de le scénario), notre API peut encore être intacte. Nous pourrions simplement ajouter le nouveau type révolutionnaire améliorant la bibliothèque - et la classe de tas existerait toujours là-bas.
  • Imaginez qu'un utilisateur se demande si la structure de données que nous choisissons est stable. Lorsque la solution que nous suivons est une file d'attente prioritaire , ce n'est pas évident. Le terme est abstrait et tout peut être en dessous. Ainsi, l'utilisateur perd un peu de temps pour rechercher les documents et découvrir qu'il utilise un tas en interne et qu'en tant que tel, il n'est pas stable. Cela aurait pu être évité en déclarant simplement explicitement via l'API qu'il s'agit d'un tas.

J'espère que vous aimez tous l'approche consistant à simplement exposer clairement une structure de données de tas - et à faire une référence de file d'attente prioritaire dans les documents uniquement.

API et implémentation

Nous devons nous mettre d'accord sur la question ci-dessus, mais permettez-moi de commencer un autre sujet : comment mettre en œuvre cela . Je vois ici deux solutions :

  • Nous proposons juste un cours de ArrayHeap . En voyant le nom, vous pouvez immédiatement savoir à quel type de tas vous avez affaire (encore une fois, il existe des dizaines de types de tas). Vous voyez qu'il est basé sur un tableau. Vous savez immédiatement à quelle bête vous avez affaire.
  • Une autre possibilité que j'aime beaucoup plus est de fournir une interface IHeap et de fournir une ou plusieurs implémentations. Le client pourrait écrire du code qui dépend des interfaces - cela permettrait de fournir des implémentations vraiment claires et lisibles d'algorithmes complexes. Imaginez que vous écriviez un cours de DijkstraAlgorithm . Cela pourrait simplement dépendre de l'interface IHeap (un constructeur paramétré) ou simplement utiliser le ArrayHeap (constructeur par défaut). Propre, simple, explicite, sans ambiguïté en raison de l'utilisation d'un terme de "file d'attente prioritaire". Et une interface merveilleuse qui a beaucoup de sens théoriquement.

Dans l'approche ci-dessus, le ArrayHeap représente un arbre d-aire complet implicitement ordonné par tas, stocké sous forme de tableau. Cela peut être utilisé pour créer par exemple un BinaryHeap ou un QuaternaryHeap .

En bout de ligne

Pour une discussion plus approfondie, je vous encourage fortement à jeter un œil à ce document . Vous saurez que :

  • Les tas 4-aires (quaternaires) sont simplement plus rapides que les tas 2-aires (binaires). Il existe de nombreux tests effectués dans le document - vous seriez intéressé à comparer les performances de implicit_4 et implicit_2 (parfois implicit_simple_4 et implicit_simple_2 ).

  • Le choix optimal de la mise en œuvre dépend fortement des intrants. Parmi les tas d-aires implicites, les tas d'appariement, les tas de Fibonacci, les tas binomiaux, les tas d-aires explicites, les tas d'appariement de rangs, les tas de tremblements de terre, les tas de violation, les tas faibles à classement relaxé et les tas de Fibonacci stricts, les types suivants semblent couvrir presque tous les besoins pour divers scénarios :

    • Tas d-aires implicites (ArrayHeap), <- nous en avons sûrement besoin
    • Associer des tas, <- si nous adoptons l'approche agréable et propre IHeap , cela vaudrait la peine de l'ajouter, car dans de nombreux cas, c'est vraiment rapide et plus rapide que la solution basée sur les tableaux .

    Pour les deux, l'effort de programmation est étonnamment faible. Jetez un œil à mes implémentations.


@karelz @benaadams

Ce serait un type vraiment utile à avoir dans CoreFX. Est-ce que quelqu'un est intéressé à saisir celui-ci?

@safern Je serais très heureux de prendre celui-ci à partir de maintenant.

OK, c'était un problème entre mon clavier et ma chaise - PriorityQueue est bien sûr basé sur Heap -- je pensais juste à Queue où ça n'a pas de sens et j'ai oublié que le tas est «trié» - panne de processus de réflexion très embarrassante pour quelqu'un comme moi, qui aime la logique, les algorithmes, les machines de Turing, etc., toutes mes excuses. (BTW : dès que j'ai lu quelques phrases dans votre lien de documentation Java, l'écart a immédiatement cliqué)

De ce point de vue, il est logique de construire l'API au-dessus de Heap . Cependant, nous ne devrions pas encore rendre cette classe publique - elle nécessitera sa propre revue d'API et sa propre discussion si c'est quelque chose dont nous avons besoin dans CoreFX. Nous ne voulons pas de fluage de la surface de l'API en raison de la mise en œuvre, mais cela peut être la bonne chose à faire - d'où la discussion nécessaire.
De ce point de vue, je ne pense pas que nous ayons besoin de créer IHeap l'instant. Ce sera peut-être une bonne décision plus tard.
S'il y a des recherches selon lesquelles un tas spécifique (par exemple 4-ary comme vous l'avez mentionné ci-dessus) est le meilleur pour une entrée aléatoire générale , alors nous devrions choisir cela. Attendons @safern @ianhays @stephentoub pour confirmer/exprimer son opinion.

La paramétrisation du tas sous-jacent avec plusieurs options implémentées est quelque chose dont IMO n'appartient pas à CoreFX (je me trompe peut-être ici, voyons ce que les autres pensent).
Ma raison est que, selon l'OMI, nous expédierions bientôt des milliards de collections spécialisées, ce qui serait très difficile pour les personnes (développeur moyen sans solide expérience dans les nuances d'algorithmes) de choisir. Une telle bibliothèque, cependant, ferait un excellent package NuGet, pour les experts dans le domaine - appartenant à vous/à la communauté. À l'avenir, nous pourrions envisager de l'ajouter dans PowerCollections (nous discutons activement depuis 4 mois où sur GitHub pour mettre cette bibliothèque et si nous devrions la posséder, ou si nous devrions encourager la communauté à la posséder - il y a différentes opinions sur la question , j'espère que nous finaliserons son sort post 2.0)

Vous assigner comme vous voulez y travailler...

Invitation de collaborateur contacter , je pourrai alors vous l'attribuer (limitations de GitHub).

@benaadams Je garderais ICollection (préférence légère). Pour la cohérence avec les autres DS de CoreFX. OMI, cela ne vaut pas la peine d'avoir une bête étrange ici ... si nous en ajoutions une poignée de nouvelles (par exemple, PowerCollections même dans un autre référentiel), nous ne devrions pas inclure les non génériques ... pensées?

OK, c'était un problème entre mon clavier et ma chaise.

Haha 😄 Pas de soucis.

il est logique de construire l'API au-dessus de Heap. Nous ne devrions pas encore rendre cette classe publique, cependant [...] Nous ne voulons pas de fluage de la surface de l'API en raison de l'implémentation, mais cela peut être la bonne chose à faire -- d'où la discussion nécessaire. [...] Je ne pense pas que nous ayons besoin de créer IHeap pour le moment. Ce sera peut-être une bonne décision plus tard.

Si la décision du groupe est d'utiliser PriorityQueue , je vais juste aider à la conception et à la mise en œuvre. Cependant, veuillez prendre en considération le fait que si nous ajoutons un PriorityQueue maintenant, il sera compliqué dans l'API d'ajouter plus tard Heap -- car les deux se comportent fondamentalement de la même manière. Ce serait une sorte de redondance OMI. Ce serait une odeur de design pour moi. Je n'ajouterais pas la file d'attente prioritaire. Cela n'aide pas.

Aussi, encore une pensée. En fait, la structure de données de tas d'appariement pourrait s'avérer utile assez souvent. Les tas basés sur des tableaux sont horribles à les fusionner. Cette opération est fondamentalement linéaire . Lorsque vous rencontrez de nombreux tas de fusion, vous tuez la performance. Cependant, si vous utilisez un tas d'appariement au lieu d'un tas de tableau, l'opération de fusion est constante (amortie). C'est un autre argument pour lequel je voudrais fournir une interface agréable et deux implémentations. Un pour l'entrée générale, le second pour certains scénarios spécifiques, en particulier lorsque la fusion de tas est impliquée.

Votez pour IHeap + ArrayHeap + PairingHeap ! (comme dans Rust / Swift / Python / Go)

Si le tas d'appariement est trop important, OK. Mais allons au moins avec IHeap + ArrayHeap . Ne pensez-vous pas qu'aller avec une classe PriorityQueue verrouille les possibilités à l'avenir et rend l'API moins claire ?

Mais comme je l'ai dit -- si vous votez tous pour une classe PriorityQueue sur la solution proposée -- OK.

invitation collaborateur envoyée - lorsque vous accepterez de me pinger, je pourrai alors vous l'attribuer (limitations GitHub).

@karelz ping :)

veuillez prendre en considération le fait que si nous ajoutons une PriorityQueue maintenant, il sera plus compliqué dans l'API d'ajouter Heap -- car les deux se comportent fondamentalement de la même manière. Ce serait une sorte de redondance OMI. Ce serait une odeur de design pour moi. Je n'ajouterais pas la file d'attente prioritaire. Cela n'aide pas.

Pouvez-vous expliquer plus en détail pourquoi ce sera désordonné plus tard? Quelles sont vos préoccupations ?
PriorityQueue est un concept utilisé par les gens. Avoir un type nommé de cette façon est utile, non ?
Je pense que les opérations logiques (au moins leurs noms) sur Heap pourraient être différentes. Si ce sont les mêmes, on peut avoir 2 implémentations différentes du même code dans le pire des cas (pas idéal, mais pas la fin du monde). Ou nous pouvons insérer la classe Heap comme parent de PriorityQueue , n'est-ce pas ? (en supposant que cela soit autorisé du point de vue de l'examen des API - pour le moment, je ne vois aucune raison de ne pas le faire, mais je n'ai pas autant d'années d'expérience avec les examens d'API, j'attendrai donc que les autres le confirment)

Voyons comment se déroulent le vote et la discussion sur la conception ... Je me réchauffe lentement à l'idée de IHeap + ArrayHeap , mais pas encore complètement convaincu ...

si nous en ajoutions une poignée de nouveaux ... nous ne devrions pas inclure les non génériques

Chiffon rouge à un taureau. Quelqu'un a-t-il d'autres collections à ajouter pour que nous puissions déposer ICollection ?

Tampon circulaire/anneau ; Générique et simultané ?

@karelz Une solution au problème de nommage pourrait être quelque chose comme IPriorityQueue comme le fait DataFlow pour les modèles de production/consommation. De nombreuses façons d'implémenter une file d'attente prioritaire et si vous ne vous en souciez pas, utilisez l'interface. Souciez-vous de l'implémentation ou créez une classe d'implémentation d'utilisation d'instance.

Pouvez-vous expliquer plus en détail pourquoi ce sera désordonné plus tard? Quelles sont vos préoccupations ?
PriorityQueue est un concept utilisé par les gens. Avoir un type nommé de cette façon est utile, non ? […] Je m'échauffe lentement à l'idée de IHeap + ArrayHeap , mais pas encore totalement convaincu...

@karelz D'après mon expérience, je trouve vraiment important d'avoir une abstraction ( IPriorityQueue ou IHeap ). Grâce à une telle approche, un développeur peut écrire du code découplé. Parce qu'il est écrit contre une interface (plutôt qu'une implémentation spécifique), il y a plus de flexibilité et d'esprit IoC. Il est très facile d'écrire des tests unitaires pour un tel code (avec l'injection de dépendances, on peut injecter ses propres IPriorityQueue ou IHeap et voir quelles méthodes sont appelées à quel moment et avec quels arguments). Les abstractions sont bonnes.

Il est vrai que le terme « file d'attente prioritaire » est couramment utilisé. Le problème est qu'il n'y a qu'une seule façon d'implémenter efficacement une file d'attente prioritaire - avec un tas. Beaucoup de types de tas. On pourrait donc avoir :

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

Pour moi, la deuxième approche semble meilleure. Comment te sens-tu à propos de ça?

Concernant la classe PriorityQueue — je pense que ce serait trop vague et je suis totalement contre une telle classe. Une interface vague est bonne, mais pas une implémentation. S'il s'agit d'un tas binaire, appelez-le simplement BinaryHeap . S'il s'agit d'autre chose, nommez-le en conséquence.

Je suis tout à fait pour nommer les classes clairement à cause des problèmes avec les classes comme SortedDictionary . Il y a beaucoup de confusion parmi les développeurs quant à ce qu'il représente et en quoi il diffère de SortedList . Si cela s'appelait simplement BinarySearchTree , la vie serait plus simple et nous pourrions rentrer à la maison et voir nos enfants plus tôt.

Nommons un tas un tas.

@benaadams chacun d'eux doit le faire dans CoreFX (c'est-à-dire qu'il doit être utile pour le code CoreFX lui-même, ou il doit être aussi basique et largement utilisé que ceux que nous avons déjà) - nous avons beaucoup discuté de ces choses ces derniers temps . Toujours pas de consensus à 100%, mais personne n'est désireux d'ajouter simplement plus de collections dans CoreFX.

Le résultat le plus probable (affirmation biaisée, car j'aime la solution) est que nous allons créer un autre référentiel et l'amorcer avec PowerCollections, puis laisser la communauté l'étendre avec quelques conseils/supervision de base de notre équipe.
BTW : @terrajobst pense que cela n'en vaut pas la peine ("nous avons des choses meilleures et plus percutantes dans l'écosystème à faire") et nous devrions encourager la communauté à le piloter pleinement (y compris commencer avec les PowerCollections existantes) et à ne pas en faire l'un de nos repos - certaines discussions et décisions devant nous.
// Je suppose qu'il y a une opportunité pour la communauté de sauter sur cette solution avant que nous nous décidions ;-). Cela rendrait la discussion (et ma préférence) muette ;-)

@pgolebiowski, vous me convainquez lentement qu'avoir Heap vaut mieux que PriorityQueue -- nous aurions juste besoin de conseils et de documents solides "Voici comment vous faites PriorityQueue - utilisez Heap " ... ça pourrait marcher.

Cependant, j'hésite beaucoup à inclure plus d'une implémentation de tas dans CoreFX. Plus de 98 % des développeurs C# « normaux » s'en moquent. Ils ne veulent même pas penser lequel est le meilleur, ils ont juste besoin de quelque chose qui fasse le travail. Chaque pièce de chaque logiciel n'est pas conçue avec des performances élevées à l'esprit, à juste titre. Pensez à tous les outils uniques, applications d'interface utilisateur, etc. À moins que vous ne conceviez un système de mise à l'échelle hautes performances où ce DS est sur le chemin critique, vous ne devriez PAS vous en soucier.

De la même manière, peu m'importe comment SortedDictionary ou ArrayList ou d'autres ds sont implémentés -- ils font leur travail décemment. Je (comme beaucoup d'autres) comprends que si j'ai besoin de hautes performances de ces ds pour mes scénarios , je dois mesurer les performances et/ou vérifier la mise en œuvre et décider si c'est assez bon pour mes scénarios , ou si j'ai besoin de déployer ma propre implémentation spéciale pour obtenir les meilleures performances, adaptées à mes besoins.

Nous devrions optimiser la convivialité pour 98 % des cas d'utilisation, pas pour les 2 %. Si nous déployons trop d'options (implémentations) et forçons tout le monde à décider, nous venons de créer une confusion inutile pour 98% des cas d'utilisation. Je pense que ça ne vaut pas le coup...
L'écosystème IMO .NET a une grande valeur en offrant un choix unique de nombreuses API (pas seulement des collections) avec des caractéristiques de performances très décentes, utiles dans la plupart des cas d'utilisation. Et offrir un écosystème qui permet des extensions hautes performances pour ceux qui en ont besoin et qui sont prêts à approfondir et à en savoir plus / faire des choix et des compromis éclairés.

Cela dit, avoir une interface IHeap (comme IDictionary et IReadOnlyDictionary ), peut avoir du sens - je dois y réfléchir un peu plus / demander aux experts en revue d'API dans le espacer ...

Nous avons déjà (dans une certaine mesure) ce dont parle ISet<T> et HashSet<T> . Je dis juste le refléter. L'API ci-dessus est donc changée en une interface ( IPriorityQueue<T> ), puis nous avons une implémentation ( HeapPriorityQueue<T> ) qui utilise en interne un tas qui peut ou non être exposé publiquement comme sa propre classe.

Doit-il ( PriorityQueue<T> ) également implémenter IList<T> ?

@karelz mon problème avec ICollection est SyncRoot et IsSynchronized ; soit ils sont implémentés, ce qui signifie qu'il y a une allocation supplémentaire pour l'objet verrou ; ou ils jettent, quand c'est un peu inutile de les avoir.

@benaadams Ce serait trompeur. Étant donné que 99,99 % des implémentations de files d'attente prioritaires sont des tas basés sur des tableaux (et comme je peux le voir, nous en utilisons un ici également), cela signifierait-il exposer l'accès à la structure interne du tableau ?

Disons que nous avons un tas avec les éléments 4, 8, 10, 13, 30, 45. Compte tenu de l'ordre, ils seraient accessibles par les index 0, 1, 2, 3, 4, 5. Cependant, la structure interne du tas est [4, 8, 30, 10, 13, 45] (en binaire, en quaternaire ce serait différent).

  • Renvoyer un numéro interne à l'index i n'a pas vraiment de sens du point de vue de l'utilisateur, car c'est presque arbitraire.
  • Renvoyer un nombre dans l'ordre (par priorité) est trop coûteux - c'est linéaire.
  • Renvoyer l'un ou l'autre n'a pas vraiment de sens dans d'autres implémentations. Souvent, il s'agit simplement de faire apparaître des éléments i , d'obtenir le i -ième élément, puis de les pousser à nouveau.

IList<T> est normalement la solution de contournement effrontée pour : je veux être flexible avec les collections que mon API accepte, et je veux les énumérer mais je ne veux pas les allouer via IEnumerable<T>

Je viens de réaliser qu'il n'y a pas d'interface générique pour

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

Alors peu importe (Bien que ce soit une sorte de IReadOnlyCollection)

Mais Reset on Enumerator doit être explicitement implémenté car c'est mauvais et devrait simplement être lancé.

Donc mes changements suggérés

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

Puisque vous discutez des méthodes... Nous ne nous sommes pas encore mis d'accord sur le IHeap vs IPriorityQueue -- cela affecte un peu les noms des méthodes et la logique. Cependant, dans tous les cas, je trouve que la proposition d'API actuelle manque les éléments suivants :

  • Possibilité de supprimer un certain élément de la file d'attente
  • Possibilité de mettre à jour la priorité d'un élément
  • Possibilité de fusionner ces structures

Ces opérations sont assez cruciales, notamment la possibilité de mettre à jour un élément. Sans cela, de nombreux algorithmes ne peuvent tout simplement pas être implémentés. Nous devons introduire un type de poignée. Dans l' API ici , le handle est IHeapNode . Ce qui est un autre argument pour aller dans le sens de IHeap car sinon nous aurions à introduire le type PriorityQueueHandle qui serait toujours juste un nœud de tas... 😜 De plus, ce que cela signifie est vague... Alors qu'un nœud de tas - tout le monde connaît le truc et peut imaginer le truc auquel il a affaire.

En fait, en bref, pour la proposition d'API, veuillez jeter un œil à ce répertoire . Nous n'en aurions probablement besoin que d'un sous-ensemble. Mais néanmoins - il contient juste ce dont nous avons besoin pour l'OMI, il peut donc valoir la peine d'y jeter un coup d'œil comme point de départ.

Quelles sont vos pensées, les gars?

IHeapNode n'est pas très différent du type clr KeyValuePair ?

Cependant, cela sépare ensuite la priorité et le type, donc maintenant c'est un PriorityQueue<TKey, TValue> avec un IComparer<TKey> comparer ?

KeyValuePair n'est pas seulement une structure, mais ses propriétés sont en lecture seule. Cela équivaudrait essentiellement à créer un nouvel objet chaque fois que la structure est mise à jour.

L'utilisation d'une clé uniquement ne fonctionne pas avec des clés égales - plus d'informations sont nécessaires pour savoir quel élément mettre à jour/supprimer.

À partir de IHeapNode et 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;
        }
    }

Et la méthode de mise à jour dans un 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);
        }

Mais maintenant, chaque élément du tas est un objet supplémentaire ; et si je veux un comportement PriorityQueue mais que je ne veux pas de ces allocations supplémentaires ?

Vous ne pourrez alors pas mettre à jour/supprimer des éléments. Ce serait une implémentation nue ne vous permettant pas d'implémenter un algorithme dépendant de l'opération DecreaseKey (ce qui est extrêmement courant). Par exemple l'algorithme de Dijkstra, l'algorithme de Prim. Ou si vous écrivez une sorte de planificateur et qu'un processus (ou autre) a changé sa priorité, vous ne pouvez pas régler cela.

De plus, dans toutes les autres implémentations de tas, les éléments ne sont que des nœuds, explicitement. C'est parfaitement naturel dans d'autres cas, ici c'est un peu artificiel, mais nécessaire pour mettre à jour/supprimer.

En Java, la file d'attente prioritaire n'a pas la possibilité de mettre à jour les éléments. Résultat:

Lorsque j'ai implémenté des tas pour le projet AlgoKit et que j'ai rencontré tous ces problèmes de conception, j'ai pensé que c'était la raison pour laquelle les auteurs de .NET avaient décidé de ne pas ajouter une structure de données aussi basique qu'un tas (ou une file d'attente prioritaire). Parce qu'aucun des deux modèles n'est bon. Chacun a ses défauts.

En bref, si nous voulons ajouter une structure de données prenant en charge un classement efficace des éléments par priorité, nous ferions mieux de le faire correctement et d'ajouter une fonctionnalité telle que la mise à jour / la suppression d'éléments.

Si vous vous sentez mal à l'idée d'envelopper des éléments dans un tableau avec un autre objet, ce n'est pas le seul problème. Un autre est en train de fusionner. Avec des tas basés sur des tableaux, cela est totalement inefficace. Cependant... si nous utilisons la structure de données du tas d'appariement ( Le tas d'appariement : une nouvelle forme de tas auto-ajustable ), alors :

  • les poignées des éléments sont de merveilleux nœuds - ceux-ci devraient être alloués de toute façon, donc pas de choses compliquées
  • la fusion est constante (vs linéaire dans une solution basée sur un tableau)

En fait, nous pourrions résoudre cela de la manière suivante :

  • Ajouter IHeap interface
  • Ajoutez ArrayHeap et PairingHeap avec toutes ces poignées, mise à jour, suppression, fusion
  • Ajoutez PriorityQueue qui est juste un wrapper autour d'un ArrayHeap , simplifiant l'API

PriorityQueue serait dans System.Collections.Generic et tous les tas dans System.Collections.Specialized .

Travaux?

Il est assez peu probable que nous obtenions trois nouvelles structures de données grâce à l'examen de l'API. Dans la plupart des cas, une plus petite quantité d'API est préférable. Nous pouvons toujours en ajouter plus tard si cela s'avère insuffisant, mais nous ne pouvons pas supprimer l'API.

C'est l'une des raisons pour lesquelles je ne suis pas fan de la classe HeapNode. Imo, ce genre de chose devrait être interne uniquement et l'API devrait exposer un type déjà existant si possible - dans ce cas, KVP probablement.

@ianhays Si cela est conservé uniquement en interne, les utilisateurs ne pourraient pas mettre à jour les priorités des éléments dans la structure de données. Ce serait à peu près inutile et nous nous retrouverions avec tous les problèmes Java -- les gens ré-implémentant la structure de données qui est déjà dans la bibliothèque native... Cela me semble mauvais. Bien pire que d'avoir une simple classe représentant un nœud.

BTW : une liste chaînée a une classe de nœuds afin que les utilisateurs puissent utiliser la fonctionnalité appropriée. C'est à peu près du miroir.

les utilisateurs ne seraient pas en mesure de mettre à jour les priorités des éléments dans la structure de données.

Ce n'est pas nécessairement vrai. La priorité pourrait être exposée d'une manière qui ne nécessite pas de structure de données supplémentaire telle qu'au lieu de

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

        }

tu aurais par exemple

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

        }

Je ne suis pas encore convaincu d'exposer la priorité en tant que paramètre de type non plus. Imo, la majorité des gens vont utiliser le type de priorité par défaut et le TValue sera une spécificité inutile.

void Update(TKey item, TValue priority)

Premièrement, qu'est-ce que TKey et TValue dans votre code ? Comment c'est censé fonctionner? La convention en informatique est que :

  • clé = priorité
  • value = tout ce que vous voulez stocker dans un élément

Seconde:

Imo, la majorité des gens vont utiliser le type de priorité par défaut et le TValue sera une spécificité inutile.

Veuillez définir "le type de priorité par défaut". Je sens que vous voulez juste avoir PriorityQueue<T> , n'est-ce pas ? Si tel est le cas, considérez le fait qu'un utilisateur devra probablement créer une nouvelle classe, un wrapper autour de sa priorité et de sa valeur, et implémenter quelque chose comme IComparable ou fournir un comparateur personnalisé. Assez pauvre.

Premièrement, qu'est-ce que TKey et TValue dans votre code ?

L'élément et la priorité de l'élément. Vous pouvez les changer pour qu'ils soient la priorité et l'élément associé à cette priorité, mais vous avez alors une collection dans laquelle les TKeys ne sont pas nécessairement uniques (c'est-à-dire autorisant les priorités en double). Je ne suis pas contre cela, mais pour moi, TKey implique généralement l' unicité.

Le fait est que l'exposition d'une classe Node n'est pas obligatoire pour exposer une méthode Update.

Veuillez définir "le type de priorité par défaut". Je peux sentir que vous voulez juste avoir PriorityQueue, Est-ce correct?

Oui. Bien que relisant ce fil, je ne suis pas complètement convaincu que ce soit le cas.

Si tel est le cas, considérez le fait qu'un utilisateur devra probablement créer une nouvelle classe, un wrapper autour de sa priorité et de sa valeur, et implémenter quelque chose comme IComparable ou fournir un comparateur personnalisé. Assez pauvre.

Vous n'avez pas tort. J'imagine qu'un bon nombre d'utilisateurs devront créer un type de wrapper avec une logique de comparateur personnalisée. J'imagine qu'il y a aussi pas mal d'utilisateurs qui ont déjà un type comparable qu'ils veulent mettre dans la file d'attente prioritaire ou un type avec un comparateur défini. La question est de savoir quel camp est le plus grand des deux.

Je pense que le type devrait s'appeler PriorityQueue<T> , pas Heap<T> , ArrayHeap<T> ou même QuaternaryHeap<T> pour rester cohérent avec le reste de .Net :

  • Nous avons List<T> , pas ArrayList<T> .
  • Nous avons Dictionary<K, V> , pas HashTable<K, V> .
  • Nous avons ImmutableList<T> , pas ImmutableAVLTreeList<T> .
  • Nous avons Array.Sort() , pas Array.IntroSort() .

Les utilisateurs de ces types ne se soucient généralement pas de la façon dont ils sont implémentés, cela ne devrait certainement pas être la chose la plus importante à propos du type.

Si tel est le cas, considérez le fait qu'un utilisateur devra probablement créer une nouvelle classe, un wrapper autour de sa priorité et de sa valeur, et implémenter quelque chose comme IComparable ou fournir un comparateur personnalisé.

Vous n'avez pas tort. J'imagine qu'un bon nombre d'utilisateurs devront créer un type de wrapper avec une logique de comparateur personnalisée.

Dans l'API de résumé, la comparaison est fournie par le comparateur fourni IComparer<T> comparer , aucun wrapper n'est requis. Souvent, la priorité fera partie du type, par exemple un programmateur de temps aura le temps d'exécution comme propriété du type.

L'utilisation d'un KeyValuePair avec un IComparer n'ajoute aucune allocation supplémentaire ; bien qu'il ajoute une indirection supplémentaire pour une mise à jour car les valeurs doivent être comparées plutôt que l'élément.

Les mises à jour seraient problématiques pour les éléments de structure à moins que l'élément n'ait été obtenu via un retour de référence, puis mis à jour et renvoyé dans une méthode de mise à jour par ref et que les références aient été comparées. Mais c'est un peu horrible.

@svick

Je pense que le type devrait s'appeler PriorityQueue<T> , pas Heap<T> , ArrayHeap<T> ou même QuaternaryHeap<T> pour rester cohérent avec le reste de .Net.

Je perds confiance en l'humanité.

  • Appeler une structure de données PriorityQueue est cohérent avec le reste de .NET et l'appeler Heap ne l'est pas ? Quelque chose ne va pas avec les tas? La même chose devrait alors s'appliquer aux piles et aux files d'attente.
  • ArrayHeap -> classe de base pour QuaternaryHeap . Énorme différence.
  • La discussion n'est pas une question de choisir un nom cool. Il y a beaucoup de choses à venir en conséquence du suivi d'un certain chemin de conception. Relisez le fil s'il vous plait.
  • Table de hachage , ArrayList . Ils semblent exister. BTW, une "liste" est un très mauvais choix pour nommer IMO, car la première pensée d'un utilisateur est que List est une liste, mais ce n'est pas une liste. ??
  • Il ne s'agit pas de s'amuser et de dire comment une chose est mise en œuvre. Il s'agit de donner des noms significatifs qui montrent immédiatement aux utilisateurs à quoi ils ont affaire.

Vous voulez rester cohérent avec le reste de .NET et rencontrer des problèmes comme celui-ci ?

Et qu'est-ce que je vois là-bas... Jon Skeet dit que SortedDictionary devrait s'appeler SortedTree car cela reflète de plus près l'implémentation.

@pgolebiowski

Quelque chose ne va pas avec les tas?

Oui, il décrit la mise en œuvre, pas l'utilisation.

La même chose devrait alors s'appliquer aux piles et aux files d'attente.

Ni l'un ni l'autre ne décrit vraiment l'implémentation, par exemple le nom ne vous dit pas qu'ils sont basés sur un tableau.

ArrayHeap -> classe de base pour QuaternaryHeap . Énorme différence.

Oui, je comprends votre conception. Ce que je dis, c'est que rien de tout cela ne devrait être exposé à l'utilisateur.

Il y a beaucoup de choses à venir en conséquence du suivi d'un certain chemin de conception. Relisez le fil s'il vous plait.

J'ai lu le fil. Je ne pense pas que le design appartienne à la BCL. Je pense qu'il devrait contenir quelque chose de simple à utiliser et à comprendre, pas quelque chose qui amène les gens à se demander ce qu'est le "tas quaternaire" ou s'ils devraient l'utiliser.

Si l'implémentation par défaut n'est pas assez bonne pour eux, c'est là qu'interviennent d'autres bibliothèques (comme la vôtre).

Table de hachage, ArrayList. Ils semblent exister.

Oui, ce sont des classes .Net Framework 1.0 que plus personne n'utilise. Pour autant que je sache, leurs noms ont été copiés de Java et les concepteurs de .Net Framework 2.0 ont décidé de ne pas suivre cette convention. À mon avis, c'était la bonne décision.

BTW, une "liste" est un très mauvais choix pour nommer IMO, car la première pensée d'un utilisateur est que List est une liste, mais ce n'est pas une liste.

Il est. Ce n'est pas une liste chaînée, mais ce n'est pas la même chose. Et j'aime ne pas avoir à écrire ArrayList ou ResizeArray (le nom de F# pour List<T> ) partout.

Il s'agit de donner des noms significatifs qui montrent immédiatement aux utilisateurs à quoi ils ont affaire.

La plupart des gens n'auront aucune idée de ce à quoi ils ont affaire s'ils voient QuaternaryHeap . D'un autre côté, s'ils voient PriorityQueue , il devrait être clair à quoi ils ont affaire, même s'ils n'ont aucune expérience en CS. Ils ne sauront pas quelle est la mise en œuvre, mais c'est à cela que sert la documentation.

@ianhays

Premièrement, qu'est-ce que TKey et TValue dans votre code ?

L'élément et la priorité de l'élément. Vous pouvez les changer pour qu'ils soient la priorité et l'élément associé à cette priorité, mais vous avez alors une collection dans laquelle les TKeys ne sont pas nécessairement uniques (c'est-à-dire autorisant les priorités en double). Je ne suis pas contre cela, mais pour moi, TKey implique généralement l'unicité.

Clés -- trucs utilisés pour établir les priorités. Les clés n'ont pas besoin d'être uniques. Nous ne pouvons pas faire une telle hypothèse.

Le fait est que l'exposition d'une classe Node n'est pas obligatoire pour exposer une méthode Update.

Je crois que c'est : prise en charge de Decrease-key/Augmenter-key dans les bibliothèques natives . Également sur ce sujet, il existe donc des classes d'assistance impliquées pour traiter les structures de données :

J'imagine qu'un bon nombre d'utilisateurs devront créer un type de wrapper avec une logique de comparateur personnalisée. J'imagine qu'il y a aussi pas mal d'utilisateurs qui ont déjà un type comparable qu'ils veulent mettre dans la file d'attente prioritaire ou un type avec un comparateur défini. La question est de savoir quel camp est le plus grand des deux.

Je ne sais pas pour les autres, mais je trouve que créer un wrapper avec une logique de comparaison personnalisée à chaque fois que je veux utiliser une file d'attente prioritaire est assez horrible...

Une autre idée -- disons que j'ai créé un wrapper :

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

Je nourris PriorityQueue<T> avec ce type. Il priorise donc ses éléments en fonction de celui-ci. Disons qu'il a défini un sélecteur de clé. Une telle conception peut écraser le travail interne d'un tas. Vous pouvez modifier les priorités à tout moment car vous êtes le propriétaire de l'élément. Il n'y a pas de mécanisme de notification pour la mise à jour du tas. Vous seriez responsable de gérer tout cela et de l'appeler. Assez dangereux et pas simple pour moi.

Dans le cas de la poignée que j'ai proposée, le type était immuable du point de vue de l'utilisateur. Et il n'y avait aucun problème avec l'unicité.

IMO J'aime l'idée d'avoir une interface IHeap puis l'API de classe implémentant cette interface. Je suis avec @ianhays ' sur ce que nous PriorityQueue quel que soit le nom.

Deuxièmement, je pense qu'il n'est pas nécessaire d'avoir une classe interne/publique de nœud pour stocker les valeurs dans la file d'attente. Pas besoin d'allocations supplémentaires, nous pourrions suivre quelque chose comme ce que fait IDictionary et lors de la production des valeurs pour l'Enumerable en créant KeyValuePairobjets si nous optons pour l'option d'avoir une priorité pour chaque élément que nous stockons (ce que je ne pense pas que ce soit la meilleure façon de procéder, je ne suis pas entièrement convaincu de stocker une priorité par élément). La meilleure approche que j'aimerais serait d'avoir PriorityQueue<T> (ce nom est juste pour lui en donner un). Avec cette approche, nous pourrions avoir un constructeur suivant ce que l'ensemble de la BCL fait avec un IComparer<T> et faisant la comparaison avec ce compresseur pour donner la priorité. Pas besoin d'exposer des API qui passent une priorité en paramètre.

Je pense que certaines des API reçoivent une priorité le rendrait "moins utilisable" ou plus compliqué pour les clients ordinaux qui voudraient être la priorité par défaut, alors nous rendons plus compliqué la compréhension de son utilisation, en leur donnant le L'option d'avoir un IComparer<T> sera la plus raisonnable et suivra les directives que nous avons à travers BCL.

Les noms sont ce qu'ils font, pas comment ils le font.

Ils sont nommés d'après les concepts abstraits et ce qu'ils permettent à l'utilisateur, plutôt que leur mise en œuvre et la manière dont ils y parviennent. (Ce qui signifie également que leur implémentation peut être améliorée pour utiliser une implémentation différente si cela s'avère meilleur)

La même chose devrait alors s'appliquer aux piles et aux files d'attente.

Stack est un tableau de redimensionnement illimité et Queue est implémenté en tant que tampon circulaire de redimensionnement illimité. Ils sont nommés d'après leurs concepts abstraits. Pile (type de données abstrait) : Last-In-First-Out (LIFO), Queue (type de données abstrait) : First-In-First-Out (FIFO)

Table de hachage, ArrayList. Ils semblent exister.

Entrée du dictionnaire

Ouais, mais faisons comme si non ; ce sont des artefacts d'une époque moins civilisée. Ils n'ont pas de sécurité de type et de boîte si vous utilisez des primitives ou des structures ; donc allouer sur chaque Add.

Vous pouvez utiliser l' analyseur de compatibilité de plate-forme de @terrajobst et il vous dira : "Ne le faites pas"

La liste est une liste, mais ce n'est pas une liste

C'est vraiment une liste (type de données abstrait) également connue sous le nom de séquence.

La file d'attente à priorité égale est un type abstrait qui vous indique ce qu'il obtient en cours d'utilisation ; pas comment il le fait dans la mise en œuvre (car il peut y avoir de nombreuses implémentations différentes)

Beaucoup de belles discussions !

La spécification originale a été conçue avec quelques principes de base à l'esprit :

  • Usage général - Couvre la plupart des cas d'utilisation avec un équilibre entre la consommation du processeur et de la mémoire.
  • Alignez, autant que possible, sur les modèles et conventions existants dans System.Collections.Generic.
  • Autoriser plusieurs entrées du même élément.
  • Utiliser les interfaces et classes de comparaison BCL existantes (c'est-à-dire Comparer).*
  • Haut niveau d'abstraction - La spécification est conçue pour protéger les consommateurs de la technologie de mise en œuvre sous-jacente.

@karelz @pgolebiowski
Renommer en « Heap » ou en un autre terme aligné sur une implémentation de structure de données ne correspondrait pas à la plupart des conventions BCL pour les collections. Historiquement, les classes de collection .NET ont été conçues pour un usage général plutôt que pour se concentrer sur la structure/le modèle de données spécifiques. Ma pensée initiale était qu'au début de l'écosystème .NET, les concepteurs d'API sont intentionnellement passés de « ArrayList » à « List". Le changement était probablement dû à une confusion avec un tableau - votre développeur moyen aurait pensé "ArrayList? Je veux juste une liste, pas un tableau".

Si nous utilisons Heap, la même chose pourrait se produire - de nombreux développeurs intermédiaires verront (malheureusement) "Heap" et le confondront avec le tas de mémoire de l'application (c'est-à-dire le tas et la pile) au lieu de "taper la structure de données généralisée". La prévalence de System.Collections.Generic le fera apparaître dans à peu près toutes les propositions intelligentes de développeurs .NET, et ils se demanderont pourquoi ils peuvent allouer un nouveau tas de mémoire :)

PriorityQueue, en comparaison, est beaucoup plus détectable et moins sujet à confusion. Vous pouvez taper "Queue" et obtenir des propositions pour PriorityQueue.

Quelques propositions et questions posées sur les priorités entières ou un paramètre générique de priorité (TKey, TPriority, etc). L'ajout d'une priorité explicite obligerait les consommateurs à écrire leur propre logique pour cartographier leurs priorités et augmenter la complexité de l'API. Utilisation de l'IComparer intégréexploite la fonctionnalité BCL existante, et j'ai également envisagé d'ajouter des surcharges au comparateurdans la spécification pour faciliter la fourniture d'expressions lambda ad hoc/de fonctions anonymes en tant que comparaisons prioritaires. Ce n'est malheureusement pas une convention courante dans la BCL.

Si les entrées devaient être uniques, Enqueue() nécessiterait une recherche d'unicité pour lever une ArgumentException. De plus, il existe probablement des scénarios valides pour permettre à un élément d'être mis en file d'attente plus d'une fois. Cette conception de non-unicité rend difficile la fourniture d'une opération Update() car il n'y aurait aucun moyen de savoir quel objet est mis à jour. Comme l'ont indiqué quelques commentaires, cela commencerait à entrer dans les API renvoyant des références de « nœuds » qui à leur tour nécessiteraient (probablement) des allocations qui devraient être nettoyées. Même si cela était contourné, cela augmenterait la consommation de mémoire par élément de la file d'attente prioritaire.

À un moment donné, j'avais une interface IPriorityQueue personnalisée dans l'API avant de publier la spécification. En fin de compte, j'ai décidé de ne pas le faire - le modèle d'utilisation que je visais était Enqueue, Dequeue et Iterate. Déjà couvert par l'ensemble d'interfaces existant. Considérer cela comme une file d'attente triée en interne ; tant que les éléments conservent leur propre position (initiale) dans la file d'attente en fonction de leur IComparer, l'appelant n'a jamais à se soucier de la façon dont la priorité est représentée. Dans l'ancienne implémentation de référence que je faisais, (si je me souviens bien !) la priorité n'est pas du tout représentée. Tout est relatif basé sur IComparerou Comparaison.

Je vous dois à tous quelques exemples de code client - mon plan initial était de passer en revue les implémentations BCL existantes de PriorityQueue à utiliser comme base pour les exemples.

Dans l'API de résumé, la comparaison est fournie par le comparateur fourni IComparercomparateur, aucun wrapper n'est requis. Souvent, la priorité fera partie du type, par exemple un programmateur de temps aura le temps d'exécution comme propriété du type.

Dans le cadre de l'API OP proposée, l'un de ces besoins doit être rempli pour utiliser la classe :

  • Avoir un type qui est déjà comparable comme vous le souhaitez
  • Enveloppez un type avec une autre classe qui contient la valeur de priorité et compare la façon dont vous le souhaitez
  • Créez un IComparer personnalisé pour votre type et transmettez-le via le constructeur. Suppose que la valeur que vous voulez représenter la priorité est déjà publiquement exposée par votre type.

L'API à double type vise à alléger le fardeau des deux deuxièmes options, n'est-ce pas ? Comment fonctionne la construction d'un nouveau PriorityQueue/Heap lorsque vous avez un type qui contient déjà la priorité ? A quoi ressemble l'API ?

Je crois que c'est : prise en charge de la diminution de la clé/de l'augmentation de la clé dans les bibliothèques natives.

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

Dans les deux cas, l'élément associé à la priorité donnée est mis à jour. Pourquoi ArrayHeapNode est-il requis pour la mise à jour ? Qu'accomplit-il qui ne peut pas être accompli en prenant directement la TKey/TValue ?

@ianhays

Dans les deux cas, l'élément associé à la priorité donnée est mis à jour. Pourquoi ArrayHeapNode est-il requis pour la mise à jour ? Qu'accomplit-il qui ne peut pas être accompli en prenant directement la TKey/TValue ?

Au moins avec le tas binaire (celui que je connais le mieux), si vous voulez mettre à jour la priorité d'une valeur et que vous connaissez sa position dans le tas, vous pouvez le faire rapidement (en temps O(log n)).

Mais si vous ne connaissez que la valeur et la priorité, vous devrez d'abord trouver la valeur, qui est lente (O(n)).

D'un autre côté, si vous n'avez pas besoin de mettre à jour efficacement, cette allocation par élément dans le tas est une pure surcharge.

J'aimerais voir une solution dans laquelle vous ne payez ces frais généraux que lorsque vous en avez besoin, mais cela pourrait ne pas être possible à mettre en œuvre et l'API résultante pourrait ne pas sembler bonne.

L'API à double type vise à alléger le fardeau des deux deuxièmes options, n'est-ce pas ? Comment fonctionne la construction d'un nouveau PriorityQueue/Heap lorsque vous avez un type qui contient déjà la priorité ? A quoi ressemble l'API ?

C'est un bon point, il y a une lacune dans l'API d'origine pour les scénarios où le consommateur a déjà une valeur prioritaire (c'est-à-dire un entier) maintenue séparément de la valeur. Le revers de la médaille est qu'unstyle d'API compliquerait tous les types intrinsèquement comparables. Il y a des années, j'ai écrit un composant de planificateur très léger qui avait sa propre classe interne ScheduledTask. Chaque ScheduledTask a implémenté IComparer. Jetez-les dans une file d'attente prioritaire, c'est bon.

Liste triéeutilise une conception clé/valeur et je me suis retrouvé à éviter de l'utiliser en conséquence. Cela nécessite que les clés soient uniques, et mon exigence habituelle est "J'ai N valeurs que je dois garder triées dans une liste et m'assurer que les valeurs occasionnelles que j'ajoute restent triées". Une "clé" ne fait pas partie de cette équation, une liste n'est pas un dictionnaire.

Pour moi, le même principe s'applique aux files d'attente. Une file d'attente, historiquement, est une structure de données unidimensionnelle.

Ma connaissance de la bibliothèque std C++ moderne est certes un peu rouillée, mais même std::priority_queue semble avoir push, pop et take un comparateur comme paramètre de modèle (générique). L'équipe de la bibliothèque standard C++ est à peu près aussi sensible aux performances que possible :)

J'ai fait une analyse très rapide des implémentations de file d'attente prioritaire et de tas dans quelques langages de programmation - C++, Java, Rust et Go fonctionnent tous avec un seul type (similaire à l'API d'origine publiée ici). Un coup d'œil rapide sur les implémentations de file d'attente de tas/priorité les plus populaires dans NPM montre la même chose.

@pgolebiowski ne vous méprenez pas, il devrait y avoir des implémentations spécifiques de choses nommées explicitement d'après leur implémentation spécifique.

Cependant, c'est pour quand vous savez quelle structure de données spécifique vous voulez qui correspond aux objectifs de performance que vous recherchez et a les compromis que vous êtes prêt à accepter.

Généralement les collections de framework couvrent les 90% des usages où l'on souhaite un comportement général. Ensuite, si vous voulez un comportement ou une implémentation très spécifique, vous opterez probablement pour une bibliothèque tierce ; et, espérons-le, ils porteront le nom de l'implémentation afin que vous sachiez qu'elle correspond à vos besoins.

Je ne veux tout simplement pas lier les types de comportement généraux à une implémentation spécifique ; car alors c'est bizarre si l'implémentation change car le nom du type doit rester le même et ils ne correspondront pas.

Il existe de nombreuses préoccupations qui nécessitent une discussion, mais commençons par celle qui a actuellement le plus d'impact sur les parties sur lesquelles nous sommes en désaccord : la mise à jour et la suppression d'éléments .

Comment voulez-vous alors soutenir ces opérations ? Nous devons les inclure, ils sont basiques. En Java, les concepteurs les ont omis, et par conséquent :

  1. Il y a beaucoup de questions sur les forums pour savoir comment faire des solutions de contournement en raison de fonctionnalités manquantes.
  2. Il existe des implémentations tierces d'une file d'attente de tas/priorité pour remplacer l'implémentation première, car c'est à peu près inutile.

C'est juste pathétique. Y a-t-il quelqu'un qui veut vraiment poursuivre une telle voie? J'aurais honte de publier une structure de données aussi désactivée.

@pgolebiowski soyez assuré que tout le monde ici a les meilleures intentions pour la plateforme. Personne ne veut expédier des API cassées. Nous voulons apprendre des erreurs des autres, alors continuez à apporter des informations pertinentes et utiles (comme celle sur l'histoire de Java).

Cependant, je tiens à souligner deux choses :

  • Ne vous attendez pas à des changements du jour au lendemain. Il s'agit d'une discussion sur la conception. Nous devons trouver un consensus. Nous ne précipitons pas les API. Chaque opinion doit être entendue et prise en compte, mais rien ne garantit que l'opinion de chacun sera mise en œuvre/acceptée. Si vous avez des commentaires, fournissez-les, soutenez-les avec des données et des preuves. Écoutez également les arguments des autres, reconnaissez-les. Fournissez des preuves contre l'opinion des autres si vous n'êtes pas d'accord. Parfois, s'accorder sur le fait qu'il y a désaccord sur certains points. Gardez à l'esprit que SW, incl. La conception de l'API n'est pas une chose en noir ou blanc / juste ou faux.
  • Gardons la discussion civile. N'utilisons pas de mots et de déclarations forts. Ne sommes pas d'accord avec la grâce et gardons la discussion technique. Chacun peut également se référer au code de conduite des contributeurs . Nous reconnaissons et encourageons la passion pour .NET, mais veillons à ne pas nous offenser les uns les autres.
  • Si vous avez des préoccupations/questions concernant la vitesse de la discussion sur la conception, les réactions, etc., n'hésitez pas à me contacter directement (mon e-mail est sur mon profil GH). Je peux aider à clarifier les attentes, les hypothèses et les préoccupations publiquement ou hors ligne si nécessaire.

Je viens de demander comment voulez-vous concevoir la mise à jour / suppression si vous n'aimez pas l'approche proposée... C'est écouter les autres et rechercher un consensus, je crois.

Je ne doute pas de vos bonnes intentions ! Parfois, la façon dont vous posez la question est importante - cela affecte la façon dont les gens perçoivent le texte de l'autre côté. Le texte est exempt d'émotions, donc les choses peuvent être comprises différemment lorsqu'elles sont écrites. L'anglais comme langue seconde brouille encore plus les choses et nous devons tous en être conscients. Je serai heureux de discuter des détails hors ligne si cela vous intéresse... ramenons la discussion ici vers la discussion technique...

Mes deux cents sur le débat Heap vs PriorityQueue : les deux approches sont valables et présentent clairement des avantages et des inconvénients.

Cela dit, "PriorityQueue" semble beaucoup plus cohérent avec l'approche .NET existante. Aujourd'hui, les collections de base sont List, Dictionnaire, Empiler, File d'attente, HashSet, TriéDictionnaire, et SortedSet. Ceux-ci sont nommés d'après la fonctionnalité et la sémantique, pas l'algorithme. Jeu de hachageest la seule valeur aberrante, mais même cela peut être rationalisé comme se rapportant à la sémantique d'égalité des ensembles (par rapport à SortedSet). Après tout, nous avons maintenant ImmutableHashSetqui repose sur un arbre sous le capot.

Il serait étrange qu'une collection inverse la tendance ici.

Je pense PriorityQueue avec un constructeur supplémentaire : PriorityQueue(IHeap) peut être une solution. Les constructeurs sans paramètre IHeap peuvent utiliser le tas par défaut.
Dans ce cas PrioriryQueuereprésentera le type de données abstrait (comme la plupart des collections C#) et implémentera IPiorityQueueinterface mais peut utiliser différentes implémentations de tas comme @pgolebiowski suggéré :

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

D'ACCORD. Il y a beaucoup de voix différentes. J'ai repris la discussion et affiné mon approche. J'aborde également les préoccupations communes. Le texte ci-dessous utilise des citations des messages ci-dessus.

Notre objectif

Le but ultime de toute cette discussion est de fournir un ensemble spécifique de fonctionnalités pour rendre l'utilisateur heureux. Il est assez courant qu'un utilisateur dispose d'un ensemble d'éléments, certains d'entre eux ayant une priorité plus élevée que d'autres. Finalement, ils veulent garder ce groupe d'éléments dans un ordre spécifique pour pouvoir effectuer efficacement les opérations suivantes :

  • Récupérer l'élément ayant la priorité la plus élevée (et pouvoir le supprimer).
  • Ajouter un nouvel élément à la collection.
  • Supprimer un élément de la collection.
  • Modifier un élément de la collection.
  • Fusionner deux collections.

Autres bibliothèques standards

Les auteurs d'autres bibliothèques standard ont également essayé de prendre en charge cette fonctionnalité. Dans cette section, je ferai référence à la façon dont il a été résolu en Python , Java , C++ , Go , Swift et Rust .

Aucun d'entre eux ne prend en charge la modification d'éléments déjà insérés dans la collection. C'est extrêmement surprenant, car il est très probable qu'au cours de la vie d'une collection, les priorités de ses éléments changent. Surtout si une telle collection est destinée à être utilisée pendant toute la durée de vie d'un service. Il existe également un certain nombre d'algorithmes qui utilisent cette fonctionnalité même de mise à jour des éléments. Une telle conception est donc tout simplement erronée, car du coup, nos clients sont perdus : StackOverflow . C'est l'une des nombreuses questions comme celle-là sur Internet. Les concepteurs ont échoué.

Une autre chose à noter est que chaque bibliothèque fournit cette fonctionnalité partielle en implémentant un tas binaire . Eh bien, il a été démontré que ce n'est pas l'implémentation la plus performante dans le cas général (entrée aléatoire). Le meilleur ajustement pour le cas général est un tas quaternaire (un arbre 4-aire complet ordonné par tas implicite, stocké sous forme de tableau). Il est nettement moins connu et c'est probablement la raison pour laquelle les concepteurs ont plutôt opté pour un tas binaire. Mais encore — un autre mauvais choix, bien que d'une moindre gravité.

Qu'apprenons-nous de cela?

  • Si nous voulons que nos clients soient satisfaits et les empêchent d'implémenter eux-mêmes cette fonctionnalité tout en ignorant notre merveilleuse structure de données, nous devons fournir un support pour modifier les éléments déjà insérés dans la collection.
  • Nous ne devrions pas supposer que parce que

Approche proposée

Je pense fortement que nous devrions fournir :

  • IHeap<T> interface
  • Heap<T> cours

L'interface IHeap inclurait évidemment des méthodes pour réaliser toutes les opérations décrites au début de cet article. La classe Heap , implémentée avec un tas quaternaire, serait la solution de référence dans 98% des cas. L'ordre des éléments serait basé sur IComparer<T> passé dans le constructeur ou l'ordre par défaut si un type est déjà comparable.

Justification

  • Les développeurs peuvent écrire leur logique sur une interface. Je suppose que tout le monde sait à quel point c'est important et n'entrera pas dans les détails. Lire : principe d'inversion de dépendances , injection de dépendances , design by contract , inversion de contrôle .
  • Les développeurs peuvent étendre cette fonctionnalité pour répondre à leurs besoins personnalisés en fournissant d'autres implémentations de tas. De telles implémentations pourraient être incluses dans des bibliothèques tierces telles que PowerCollections . En référençant simplement une telle bibliothèque, vous pouvez simplement injecter votre tas personnalisé dans n'importe quelle logique qui prend un IHeap comme entrée. Voici quelques exemples d'autres tas qui se comportent mieux que le tas quaternaire dans certaines conditions spécifiques : tas d'appariement , tas binomial et notre tas binaire bien-aimé.
  • Si un développeur a juste besoin d'un outil qui fait le travail sans qu'il ait besoin de réfléchir au type le mieux adapté, il peut simplement utiliser l'implémentation générale Heap . Ceci s'optimise vers 98% des cas d'utilisation.
  • Nous ajoutons à la grande valeur de l'écosystème .NET en offrant un choix unique avec des caractéristiques de performances très décentes, utiles à la plupart des cas d'utilisation, tout en permettant des extensions hautes performances pour ceux qui en ont besoin et sont prêts à creuser approfondir et en savoir plus / faire des choix et des compromis éclairés.
  • L'approche proposée reflète les conventions actuelles :

    • ISet et HashSet

    • IList et List

    • IDictionary et Dictionary

  • Certaines personnes ont déclaré que nous devrions nommer les classes en fonction de ce que font leurs instances, et non de la manière dont elles le font. Ce n'est pas tout à fait vrai. C'est un raccourci courant pour dire que nous devons nommer les classes d'après leur comportement. Elle s'applique en effet dans de nombreux cas. Cependant, il y a des cas où cette approche n'est pas appropriée. Les exemples les plus notables sont les blocs de construction de base, comme les types primitifs, les types enum ou les structures de données. Le principe est simplement de choisir des noms qui ont du sens (c'est-à-dire sans ambiguïté pour l'utilisateur). Tenez compte du fait que les fonctionnalités dont nous discutons sont toujours fournies sous forme de tas, qu'il s'agisse de Python, Java, C++, Go, Swift ou Rust. Le tas est l'une des structures de données les plus élémentaires. Heap est en effet clair et sans ambiguïté. Il est également en harmonie avec Stack , Queue , List , et Array . La même approche avec le nommage a été adoptée dans les bibliothèques standard les plus modernes (Go, Swift, Rust) - elles exposent explicitement un tas.

@pgolebiowski Je ne vois pas comment Heap<T> / IHeap<T> est nommé comme Stack<T> , Queue<T> , et/ou List<T> ? Aucun de ces noms n'explique comment ils sont implémentés en interne (un tableau de T en l'occurrence).

@SamuelEnglard

Heap ne dit pas non plus comment il est implémenté en interne. Je ne comprends pas pourquoi pour tant de gens un tas suit immédiatement une implémentation spécifique. Il existe de nombreuses variantes de tas qui partagent la même API, pour commencer :

  • tas d-aires,
  • 2-3 tas,
  • tas de gauche,
  • tas mous,
  • tas faibles,
  • B-tas,
  • tas de base,
  • tas obliques,
  • appariement des tas,
  • tas de Fibonacci,
  • tas binomiaux,
  • tas d'appariement de rangs,
  • des tas de tremblements,
  • tas de violations.

Dire qu'il s'agit d' un tas est encore très abstrait. En fait, même dire que nous avons affaire à un tas quaternaire est abstrait -- il pourrait être implémenté soit comme une structure de données implicite basée sur un tableau de T (comme notre Stack<T> , Queue<T> , et List<T> ) ou explicite (en utilisant des nœuds et des pointeurs).

En bref, Heap<T> est très similaire à Stack<T> , Queue<T> et List<T> , car c'est une structure de données élémentaire, un bloc de construction abstrait de base, qui peut être mis en œuvre de plusieurs manières. De plus, en l'occurrence, ils sont tous implémentés à l'aide d'un tableau de T dessous. Je trouve cette similitude vraiment forte.

Cela a-t-il du sens?

Pour mémoire, je suis indifférent à la dénomination. Les personnes habituées à utiliser la bibliothèque standard C++ préféreront peut-être _priority_queue_. Les personnes qui ont été formées aux structures de données peuvent préférer _Heap_. Si je devais voter, je choisirais _heap_, bien que ce soit presque un tirage au sort pour moi.

@pgolebiowski J'ai mal formulé ma question, c'est mon mauvais. Oui, Heap<T> ne dit pas comment il est implémenté en interne.

Oui Heap est une structure de données valide, mais Heap != Priority Queue. Ils exposent tous deux différentes surfaces d'API et sont utilisés pour différentes idées. Heap<T> / IHeap<T> doivent être des types de données qui sont utilisés en interne par (seulement des noms théoriques) PriorityQueue<T> / IPriorityQueue<T> .

@SamuelEnglard
En termes d'organisation du monde de l'informatique, oui. Voici les niveaux d'abstraction :

  • implémentation : tas quaternaire implicite basé sur un tableau
  • abstraction : tas quaternaire
  • abstraction : famille de tas
  • abstraction : famille de files d'attente prioritaires

Et oui, ayant IHeap et Heap , l'implémentation de PriorityQueue serait essentiellement :

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

Exécutons un arbre de décision ici.

Une file d'attente prioritaire pourrait également être implémentée avec une structure de données différente d'une certaine forme de tas (théoriquement). Cela rend la conception d'un PriorityQueue comme celle ci-dessus assez moche, car elle est uniquement orientée vers la famille des tas. C'est aussi un emballage très fin autour d'un IHeap . Cela conduit à une question -- _pourquoi ne pas simplement utiliser la famille des tas à la place_ ?

Il nous reste une solution : fixer la file d'attente prioritaire à une implémentation spécifique d'un tas quaternaire, sans espace pour l'interface IHeap . Je pense que cela passe par trop de niveaux d'abstraction et tue tous les merveilleux avantages d'avoir une interface.

Nous sommes de retour avec le choix de conception du milieu de la discussion - ont PriorityQueue et IPriorityQueue . Mais alors nous aurions essentiellement:

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

Se sent non seulement moche, mais aussi conceptuellement faux - ce ne sont pas des types de files d'attente prioritaires et ne partagent pas la même API (comme @SamuelEnglard déjà noté). Je pense que nous devrions nous en tenir à des tas, une famille assez nombreuse pour avoir une abstraction pour eux. On obtiendrait :

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

Et, fourni par nous, class Heap : IHeap {} .


BTW, quelqu'un pourrait trouver les éléments suivants utiles :

| requête Google | Résultats |
| :---------------------------------------: | :-----: |
| "structure de données" "file d'attente prioritaire" | 172 000 |
| "structure de données" "tas" | 430 000 |
| "structure de données" "file d'attente" -"file d'attente prioritaire" | 496 000 |
| "structure de données" "file d'attente" | 530 000 |
| "structure de données" "pile" | 577 000 |

@pgolebiowski je me sens aller et venir ici donc je concède

@karelz @safern Que IHeap + Heap afin que je puisse présenter une proposition d'API spécifique ?

Je remets en question la nécessité d'une interface ici (que ce soit IHeap ou IPriorityQueue ). Oui, il existe de nombreux algorithmes différents qui peuvent théoriquement être utilisés pour implémenter cette structure de données. Cependant, il semble peu probable que le framework soit livré avec plus d'un, et l'idée que les rédacteurs de bibliothèques aient vraiment besoin d'une interface canonique pour que diverses parties puissent se coordonner dans l'écriture d'implémentations de tas compatibles ne semble pas si probable.

De plus, une fois qu'une interface est publiée, elle ne peut plus être modifiée en raison de la compatibilité. Cela signifie que l'interface a tendance à prendre du retard par rapport aux classes concrètes en termes de fonctionnalité (un problème avec IList et IDictionary aujourd'hui). En revanche, si une classe Heap était publiée et qu'il y avait de sérieuses demandes pour une interface IHeap, je pense que l'interface pourrait être ajoutée sans problème.

@madelson Je conviens que le framework n'a probablement pas besoin d'expédier plus d'une seule implémentation d'un tas. Cependant, en soutenant cette implémentation par une interface, ceux d'entre nous qui se soucient d'autres implémentations peuvent facilement échanger dans une autre implémentation (de notre propre création ou dans une bibliothèque externe) et toujours être compatible avec le code qui accepte l'interface.

Je ne vois pas vraiment de points négatifs à coder sur une interface. Ceux qui s'en moquent peuvent l'ignorer et passer directement à la mise en œuvre concrète. Ceux qui s'en soucient peuvent utiliser n'importe quelle implémentation de leur choix. C'est un choix. Et c'est un choix que je veux.

@pgolebiowski maintenant que vous avez demandé, voici mon opinion personnelle (je suis un peu sûr que d'autres examinateurs/architectes d'API la partageront car j'ai vérifié l'opinion de quelques-uns):
Le nom doit être PriorityQueue , et nous ne devons pas introduire l'interface IHeap . Il devrait y avoir exactement une implémentation (probablement via un tas).
IHeap interface
Si la bibliothèque et l'interface IHeap deviennent vraiment populaires, nous pouvons changer d'avis plus tard et ajouter IHeap fonction de la demande (via la surcharge de votre constructeur), mais je ne pense pas que ce soit utile/aligné avec le reste de BCL assez pour justifier la complication d'ajouter une nouvelle interface maintenant. Commencez simplement, devenez compliqué uniquement si cela est vraiment nécessaire.
... juste mes 2 cents (personnels)

Compte tenu de la différence d'opinions, je propose l'approche suivante pour faire avancer la proposition (ce que j'ai suggéré plus tôt cette semaine à @ianhays , et donc indirectement à @safern):

  • Faites 2 propositions alternatives - une simple comme je l'ai décrit ci-dessus et une avec les tas comme vous l'avez proposé, amenons-la à l'examen de l'API, discutons-en là-bas et prenons une décision là-bas.
  • Si je vois au moins une personne dans ce groupe voter pour la proposition de Heaps, je serai heureux de reconsidérer mon point de vue.

... en essayant d'être totalement transparent sur mon opinion (ce que vous avez demandé), veuillez ne pas le prendre comme un découragement ou un recul - voyons comment se déroule la proposition de l'API.

@karelz

Le nom doit être PriorityQueue

Un argument ? Ce serait au moins bien si vous répondiez à ce que j'ai écrit ci-dessus au lieu de simplement dire non .

Je pense que vous l'avez assez bien nommé précédemment : _Si vous avez une contribution, fournissez-la, soutenez-la avec des données et des preuves. Écoutez également les arguments des autres, reconnaissez-les. Fournissez des preuves contre l'opinion des autres si vous n'êtes pas d'accord._

De plus, il ne s'agit pas que du nom . Ce n'est pas si superficiel. Veuillez lire ce que j'ai écrit. Il s'agit de travailler entre les niveaux d'abstraction et de pouvoir améliorer le code / construire dessus.

Oh, il y avait un argument dans cette discussion pourquoi:

Si nous utilisons Heap, de nombreux développeurs intermédiaires verront (malheureusement) "Heap" et le confondront avec le tas de mémoire de l'application (c'est-à-dire le tas et la pile) au lieu de "taper la structure de données généralisée". [...] ils se demanderont pourquoi ils peuvent allouer un nouveau tas mémoire.

??

nous ne devrions pas introduire l'interface IHeap . IHeap interface

@bendono a écrit un très bon commentaire sur celui-ci. @safern voulait également soutenir l'implémentation avec une interface. Et quelques autres.

Une autre note -- je ne sais pas comment vous imaginez déplacer une interface dans une bibliothèque tierce. Comment les développeurs pourraient-ils écrire leur code sur cette interface et utiliser nos fonctionnalités ? Ils seraient obligés soit de s'en tenir à notre fonctionnalité sans interface, soit de l'ignorer complètement, il n'y a pas d'autre option, c'est mutuellement exclusif. En d'autres termes, notre solution ne serait pas du tout extensible, ce qui entraînerait des personnes utilisant soit notre solution désactivée, soit une bibliothèque tierce, au lieu de s'appuyer sur le même noyau architectural dans les deux cas . C'est ce que nous avons dans la bibliothèque standard de Java et les solutions tierces.

Mais encore une fois, vous avez gentiment commenté celui-ci : personne ne veut expédier des API cassées.

Compte tenu de la différence d'opinions, je propose de suivre l'approche pour faire avancer la proposition. [...] Faites 2 propositions alternatives [...] amenons-le à l'examen de l'API, discutons-en là-bas et prenons une décision là-bas. Si je vois au moins une personne dans ce groupe voter pour la proposition de Heaps, je serai heureux de reconsidérer mon point de vue.

Il y a eu beaucoup de discussions sur diverses parties de l'API ci-dessus. Il a abordé :

  • PriorityQueue vs Heap
  • Ajout d'une interface
  • Prise en charge de la mise à jour/suppression d'éléments de la collection

Pourquoi les personnes que vous avez en tête ne peuvent-elles pas simplement contribuer à cette discussion ? Pourquoi devrions-nous plutôt recommencer?

Si nous utilisons Heap, de nombreux développeurs intermédiaires verront (malheureusement) "Heap" et le confondront avec le tas de mémoire de l'application (c'est-à-dire le tas et la pile) au lieu de "taper la structure de données généralisée".

Ouais, c'est moi. Je suis entièrement autodidacte en programmation, donc je n'ai aucune idée de ce qui s'est dit quand " Heap " est entré dans la discussion. Sans compter que même en termes de collection, "un tas de choses", pour moi, cela impliquerait plus intuitivement qu'elle soit désordonnée de toutes les manières.

Je n'arrive pas à croire que cela se produise vraiment ...

Un argument ? Ce serait au moins bien si vous répondiez à ce que j'ai écrit ci-dessus au lieu de simplement dire non.

Si vous lisez ma réponse, vous remarquerez que j'ai mentionné des arguments clés pour ma position :

  • L'interface IHeap est un scénario expert très avancé
  • Je ne pense pas que ce soit suffisamment utile/aligné sur le reste de la BCL pour justifier la complication d'ajouter une nouvelle interface maintenant

Mêmes arguments qui ont été répétés plusieurs fois sur le fil OMI. je viens de les résumer

. Veuillez lire ce que j'ai écrit.

Je surveillais activement ce fil tout le temps. J'ai lu tous les arguments et points. Ceci est mon opinion résumée, malgré la lecture (et la compréhension) de tous vos points et de ceux des autres.
Vous êtes apparemment passionné par le sujet. C'est super. Cependant, j'ai le sentiment que nous nous sommes retrouvés dans une position où nous ne faisons que répéter des arguments similaires de chaque côté, ce qui ne mènera pas beaucoup plus loin - c'est pourquoi j'ai recommandé 2 propositions et obtenir plus de commentaires sur elles d'un plus grand groupe d'expérience Réviseurs d'API BCL (BTW : je ne me considère pas encore comme un réviseur d'API expérimenté).

Comment les développeurs pourraient-ils écrire leur code sur cette interface et utiliser nos fonctionnalités ?

Les développeurs qui se soucient du scénario avancé IHeap référenceraient l'interface et l'implémentation à partir de la bibliothèque tierce. Comme je l'ai dit, s'il s'avère être populaire, nous pourrions envisager de le déplacer plus tard dans CoreFX.
La bonne nouvelle est que l'ajout de IHeap plus tard est tout à fait faisable - cela n'ajoute fondamentalement qu'une seule surcharge de constructeur sur PriorityQueue .
Oui, ce n'est pas idéal de votre point de vue, mais cela n'empêche pas l'innovation que vous jugez importante à l'avenir. Je pense que c'est un juste milieu.

Pourquoi les personnes que vous avez en tête ne peuvent-elles pas simplement contribuer à cette discussion ?

L'examen de l'API est une réunion avec une discussion active, un brainstorming, pondérant tous les angles. Dans de nombreux cas, il est plus productif/efficace que les allers-retours sur les problèmes GitHub. Voir dotnet/corefx#14354 et son prédécesseur dotnet/corefx#8034 - discussion trop longue, plusieurs opinions différentes avec des milliards de réglages difficiles à suivre, pas de conclusion, alors qu'une excellente discussion, également une perte de temps non négligeable pour pas mal de gens, jusqu'à ce que nous nous asseyions pour en parler et que nous parvenions à un consensus.
Le fait que les réviseurs d'API surveillent chaque problème d'API, ou même les plus bavards, ne s'adaptent pas bien.

Pourquoi devrions-nous plutôt recommencer?

Nous ne recommencerons pas. Pourquoi penseriez-vous que?
Nous terminerons l'examen de l'API au premier niveau (le niveau du propriétaire de la zone) en envoyant les 2 propositions les plus populaires avec des avantages/inconvénients au prochain niveau d'examen de l'API.
Il s'agit d'une approche hiérarchique des approbations/examens. C'est similaire aux revues d'entreprise - les VP/PDG avec des pouvoirs de décision ne supervisent pas toutes les discussions sur chaque projet dans leur entreprise, ils demandent à leurs équipes/rapports de faire des propositions pour les décisions les plus percutantes ou contagieuses pour avoir une discussion plus approfondie à leur sujet. Les équipes/rapports doivent résumer le problème et présenter les avantages/inconvénients des solutions alternatives.

Si vous pensez que nous ne sommes pas prêts à présenter les 2 propositions finales avec le pour et le contre, car il y a des choses qui n'ont pas encore été dites sur ce fil, continuons à discuter, jusqu'à ce que nous n'ayons que quelques meilleurs candidats à revoir à la prochaine Niveau d'examen de l'API.
J'ai l'impression que tout ce qui devait être dit a été dit.
Logique?

Le nom doit être PriorityQueue

Un argument ?

Si vous lisez ma réponse, vous remarquerez que j'ai mentionné des arguments clés pour ma position.

Bon sang... Je faisais référence à ce que j'ai cité ( évidemment ) - vous avez décidé d'opter pour une file d'attente prioritaire au lieu d'un tas. Et oui, j'ai lu votre réponse - elle contient exactement 0% d'arguments pour cela.

Je surveillais activement ce fil tout le temps. J'ai lu tous les arguments et points. Ceci est mon opinion résumée, malgré la lecture (et la compréhension) de tous vos points et de ceux des autres.

Vous aimez être hyperbolique, je l'ai déjà remarqué. Vous reconnaissez simplement l'existence de points.

Comment les développeurs pourraient-ils écrire leur code sur cette interface et utiliser nos fonctionnalités ?

Les développeurs qui se soucient du scénario avancé IHeap référenceraient l'interface et l'implémentation à partir de la bibliothèque tierce. Comme je l'ai dit, s'il s'avère être populaire, nous pourrions envisager de le déplacer plus tard dans CoreFX.

Êtes-vous conscient que vous ne faites que répéter mes paroles ici et que vous n'abordez pas du tout le problème que j'ai présenté en conséquence de ce qui précède ? J'ai écrit:

Ils seraient obligés soit de s'en tenir à notre fonctionnalité sans interface, soit de l'ignorer complètement, il n'y a pas d'autre option, c'est mutuellement exclusif.

Je vais vous expliquer cela très clairement. Il y aurait deux groupes

  1. Un groupe qui utilise notre fonctionnalité. Il n'a pas d'interface et ne peut pas être étendu, il n'est donc pas connecté à ce qui est fourni dans les bibliothèques tierces.
  2. Deuxième groupe qui ignore complètement nos fonctionnalités et utilise des solutions purement tierces.

Le problème ici, comme je l'ai dit, c'est qu'il s'agit de groupes de personnes disjoints . Et ils produisent du code qui ne fonctionne pas ensemble , car il n'y a pas de noyau architectural commun. _ LA BASE DE CODE N'EST PAS COMPATIBLE _. Vous ne pouvez pas l'annuler plus tard.

La bonne nouvelle est que l'ajout ultérieur d'IHeap est tout à fait faisable - il n'ajoute fondamentalement qu'une seule surcharge de constructeur sur PriorityQueue.

J'ai déjà écrit pourquoi ça craint : voir ce post .

Pourquoi les personnes que vous avez en tête ne peuvent-elles pas simplement contribuer à cette discussion ?

Le fait que les réviseurs d'API surveillent chaque problème d'API, ou même les plus bavards, ne s'adaptent pas bien.

Oui, je demandais pourquoi les réviseurs d'API ne peuvent pas surveiller chaque problème d'API. Justement, vous avez bien répondu en conséquence.

Logique?

Non. J'en ai vraiment marre de cette discussion. Certains d'entre vous y participent clairement simplement parce que c'est votre travail et qu'on vous le dit. Certains d'entre vous ont besoin d'être guidés tout le temps, ce qui est très fastidieux. Vous m'avez même demandé de prouver pourquoi une file d'attente prioritaire devrait être implémentée avec un tas en interne, manquant clairement de connaissances en informatique. Certains d'entre vous ne comprennent même pas ce qu'est vraiment un tas, ce qui rend la discussion encore plus chaotique.

Allez avec votre PriorityQueue désactivée qui ne permet pas de mettre à jour et de supprimer des éléments. Allez avec votre conception qui ne permet pas une approche OO saine. Allez avec votre solution qui ne permet pas de réutiliser la bibliothèque standard lors de l'écriture d'une extension. Suivez la voie Java.

Et ça... c'est juste époustouflant :

Si nous utilisons Heap, de nombreux développeurs intermédiaires verront (malheureusement) "Heap" et le confondront avec le tas de mémoire de l'application (c'est-à-dire le tas et la pile) au lieu de "taper la structure de données généralisée".

Présentez l'API avec votre approche. Je suis curieux.

Je n'arrive pas à croire que cela se produise vraiment...

Eh bien, excusez-moi de ne pas avoir eu l'opportunité d'avoir une bonne formation en informatique pour apprendre que Heap est une sorte de structure de données différente du tas de mémoire.

Le point tient toujours, cependant. Un tas de quelque chose n'implique rien qu'il soit commandé d'une manière ou d'une autre. Si j'avais besoin d'une collection qui me permette de stocker des objets à traiter, où certaines instances qui arrivent plus tard pourraient devoir être traitées plus tôt, je ne chercherais pas quelque chose appelé Heap . PriorityQueue d'autre part, communique parfaitement qu'il fait exactement cela.

Comme implémentation de support ? Bien sûr, les détails de mise en œuvre ne devraient pas être ma préoccupation.
Quelques IHeap abstraction ? Idéal pour les auteurs de l' API et les personnes qui ont un CS Major de savoir ce qu'il est utilisé pour, aucune raison de ne pas avoir.
Donner à quelque chose un nom énigmatique qui n'énonce pas très bien son intention et limite par la suite la découvrabilité ? ??

Eh bien, excusez-moi de ne pas avoir eu l'occasion d'avoir une bonne formation en informatique pour apprendre que Heap est une sorte de structure de données différente du tas de mémoire.

C'est ridicule. En même temps, vous souhaitez participer à une discussion concernant l'ajout d'une fonctionnalité comme celle-ci. Cela ressemble à de la pêche à la traîne.

Un tas de quelque chose n'implique rien qu'il soit commandé d'une manière ou d'une autre.

Vous avez tort. Il est commandé, comme un tas. Comme dans les images que vous avez liées.

Comme implémentation de support ? Bien sûr, les détails de mise en œuvre ne devraient pas être ma préoccupation.

J'en ai déjà parlé. La famille des tas est immense et il existe deux niveaux d'abstraction au-dessus d'une implémentation. La file d'attente prioritaire est la troisième couche d'abstraction.

Si j'avais besoin d'une collection qui me permette de stocker des objets à traiter, où certaines instances qui arrivent plus tard pourraient devoir être traitées plus tôt, je ne chercherais pas quelque chose appelé un tas. PriorityQueue, d'autre part, communique parfaitement qu'il fait exactement cela.

Et sans aucun arrière-plan, vous demanderiez à Google de vous fournir des articles sur les files d'attente prioritaires ? Eh bien, nous pouvons discuter de ce qui est plus ou moins probable dans nos opinions. Mais, comme cela a été dit très gentiment :

Si vous avez des commentaires, fournissez-les, soutenez-les avec des données et des preuves. Fournissez des preuves contre l'opinion des autres si vous n'êtes pas d'accord.

Et d'après les données vous vous trompez :

Requête | Les coups
:----: |:----:|
| "structure de données" "file d'attente prioritaire" | 172 000 |
| "structure de données" "tas" | 430 000 |

Il est presque 3 fois plus probable que vous rencontriez un tas en lisant sur les structures de données. De plus, c'est un nom que les développeurs Swift, Go, Rust et Python connaissent bien, car leurs bibliothèques standard fournissent une telle structure de données.

Requête | Les coups
:----: |:----:|
| "golang" "file d'attente prioritaire" | 3.390 |
| "rouille" "file d'attente prioritaire" | 8.630 |
| "swift" "file d'attente prioritaire" | 18.600 |
| "python" "file d'attente prioritaire" | 72.800 |
| "golang" "tas" | 79.000 |
| "rouille" "tas" | 492.000 |
| "rapide" "tas" | 551.000 |
| "python" "tas" | 555.000 |

En fait, c'est également similaire pour C++, car une structure de données en tas y a été introduite au cours du siècle précédent.

Donner à quelque chose un nom énigmatique qui n'énonce pas très bien son intention et limite par la suite la découvrabilité ? ??

Pas d'avis. Données. Voir au dessus. Surtout pas d'avis de quelqu'un qui n'a pas d'expérience. Vous ne voudriez pas non plus rechercher une file d'attente prioritaire sur Google sans avoir préalablement lu des informations sur les structures de données. Et un tas est couvert dans de nombreuses structures de données 101 .

C'est le fondement de l'informatique. C'est élémentaire. Lorsque vous avez plusieurs semestres d'algorithmes et de structures de données, un tas est quelque chose que vous voyez au début.

Mais reste:

  • Tout d'abord, regardez les chiffres ci-dessus.
  • Deuxièmement, pensez à toutes les autres langues où un tas fait partie de la bibliothèque standard.

EDIT : voir Google Tendances .

En tant qu'autre développeur autodidacte, je n'ai aucun problème avec _heap_. En tant que développeur qui cherche toujours à s'améliorer, j'ai pris le temps d'apprendre et de tout comprendre sur les structures de données. Bref, je ne suis pas d'accord avec l'implication selon laquelle une convention de nommage devrait cibler ceux qui n'ont pas pris le temps de comprendre le lexique du domaine dont ils font partie.

Et je suis également fortement en désaccord avec des déclarations telles que "Le nom devrait être PriorityQueue". Si vous ne voulez pas que les gens y contribuent, ne le rendez pas open source et ne le demandez pas.

Permettez-moi de fournir quelques explications sur la façon dont nous concevons le nommage des API :

  1. Nous avons tendance à privilégier la cohérence au sein de la plate-forme .NET avant presque toute autre chose. C'est important pour que les API paraissent familières et prévisibles. Parfois, cela signifie que nous acceptons qu'un nom n'est pas à 100% correct si c'est un terme que nous avons déjà utilisé.

  2. Notre objectif est de concevoir une plate-forme accessible à une grande variété de développeurs, dont certains n'ont pas eu de formation formelle en informatique. Nous pensons qu'une partie de la raison pour laquelle .NET a généralement été perçu comme très productif et facile à utiliser est en partie due à ce point de conception.

Nous utilisons généralement le « test des moteurs de recherche » lorsqu'il s'agit de vérifier à quel point un nom ou une terminologie est connu et établi. J'apprécie donc grandement les recherches effectuées par @pgolebiowski . Je n'ai pas fait la recherche moi-même, mais mon intuition est que "tas" n'est pas un terme que de nombreux experts non spécialisés rechercheraient.

Par conséquent, j'ai tendance à être d'accord avec @karelz pour dire que PriorityQueue semble être le meilleur choix. Il combine un concept existant (file d'attente) et ajoute la touche qui exprime la capacité souhaitée : la récupération ordonnée basée sur une priorité. Mais nous ne sommes pas indéfectiblement attachés à ce nom. Nous avons souvent changé les noms des structures de données et des technologies en fonction des commentaires des clients.

Cependant, je tiens à préciser que ceci :

Si vous ne voulez pas que les gens y contribuent, ne le rendez pas open source et ne le demandez pas.

est une fausse dichotomie. Ce n'est pas que nous ne voulons pas de retours de notre écosystème et de nos contributeurs (c'est évidemment le cas). Mais en même temps, nous devons également comprendre que notre base de clients est assez diversifiée et que les contributeurs GitHub (ou les développeurs de notre équipe) ne sont pas toujours le meilleur proxy pour tous nos clients. La convivialité est difficile et il faudra probablement quelques itérations pour ajouter de nouveaux concepts dans .NET, en particulier dans les domaines très populaires comme les collections.

@pgolebiowski :

J'apprécie grandement votre perspicacité, vos données et vos suggestions. Mais je n'apprécie absolument pas votre style d'argumentation. Vous êtes devenu personnel envers les membres de mon équipe ainsi que les membres de la communauté sur ce fil. Ce n'est pas parce que vous n'êtes pas d'accord avec nous la permission de nous accuser de ne pas avoir d'expertise ou de ne pas nous soucier parce que c'est "juste notre travail". Considérez que beaucoup d'entre nous ont littéralement déménagé dans le monde entier, laissant derrière nous des familles et des amis uniquement parce que nous voulions faire ce travail. Non seulement les commentaires comme le vôtre sont très injustes, mais ils n'aident pas non plus à faire avancer le design.

Donc, même si j'aime penser que j'ai une peau épaisse, je n'ai pas beaucoup de tolérance pour ce genre de comportement. Notre domaine est déjà assez complexe ; nous n'avons pas besoin d'ajouter une communication conflictuelle et accusatoire.

S'il vous plaît soyez respectueux. Critiquer des idées avec passion est un jeu équitable, mais attaquer les gens ne l'est pas. Merci.

Cher tous ceux qui se sentent découragés,

Je m'excuse d'avoir diminué votre niveau de bonheur par mon attitude dure.

@karelz

Je me suis ridiculisé là-bas en faisant une erreur technique. Je me suis excusé. Il a été accepté. Pourtant jeté sur moi plus tard. Pas cool OMI.

Je suis désolé que ce que j'ai écrit vous ait rendu malheureux. Même si ce n'était pas si grave que vous l'avez décrit - je l'ai simplement fourni comme l'un des nombreux facteurs qui ont contribué à mon sentiment de fatigue . C'est d'une moindre gravité, je pense. Mais quand même, je suis désolé.

Et oui, tout le monde fait des erreurs. C'est bon. Moi aussi, par exemple en m'emballant parfois.

Ce qui m'a le plus attiré, c'est "on vous dit juste de faire ça, vous n'y croyez pas" - ouais, c'est exactement pourquoi je le fais AUSSI le week-end.

Je suis désolé, je vois que vous travaillez dur et je l'apprécie beaucoup. J'ai pu voir à quel point vous étiez particulièrement dévoué à l'étape 5/10.

@terrajobst

Ce n'est pas parce que vous n'êtes pas d'accord avec nous la permission de nous accuser de ne pas avoir d'expertise ou de ne pas nous soucier parce que c'est "juste notre travail".

  • Ne pas avoir d'expertise -- adressé aux personnes sans formation en informatique et ne comprenant pas le concept de tas / files d'attente prioritaires. Si une telle description s'applique à quelqu'un -- eh bien, elle s'applique, ce n'est pas de ma faute.
  • Ne s'en soucie pas - adressé à ceux qui ont tendance à ignorer certains des points techniques, appliquant ainsi des arguments répétés, rendant ainsi la discussion chaotique et plus difficile à suivre par d'autres personnes (ce qui à son tour conduit à moins d'entrées).

Des commentaires comme le vôtre sont très injustes et n'aident pas non plus à faire avancer la conception.

  • Mes commentaires acerbes résultaient de l'inefficacité de cette discussion. Inefficacité = manière chaotique de discuter, où les points ne sont pas abordés / résolus et on avance malgré cela => fastidieux.
  • De plus, en tant que l'un des principaux moteurs de la discussion, j'ai le sentiment d'avoir beaucoup fait pour aider à faire avancer la conception. S'il vous plaît, ne me diabolisez pas, comme vous essayez de le faire ici et dans les médias sociaux.

S'il y a quelqu'un de malade qui voudrait "me lécher", n'hésitez pas.


Nous avons rencontré un problème et nous l'avons résolu. Tout le monde en a appris quelque chose. Je vois que tout le monde ici est soucieux de la qualité du cadre, ce qui est absolument magnifique et me motive à contribuer. J'ai hâte de continuer à travailler sur CoreFX avec vous. Cela étant dit, j'aborderai probablement votre nouvelle contribution technique demain.

@pgolebiowski

J'espère que nous pourrons nous rencontrer en personne à un moment donné. Honnêtement, je crois qu'une partie du défi de tout faire en ligne est que les personnalités peuvent parfois se mélanger de mauvaises manières sans aucune intention de part et d'autre.

J'ai hâte de continuer à travailler sur CoreFX avec vous. Cela étant dit, j'aborderai probablement votre nouvelle contribution technique demain.

Pareil ici. C'est un espace intéressant et il y a plein de choses incroyables que nous pouvons faire ensemble :-)

@pgolebiowski d' abord, merci pour votre réponse. Cela montre que vous vous souciez de vous et que vous avez de bonnes intentions (ce que j'espère secrètement que chaque personne/développeur dans le monde fait, tout conflit n'est qu'un malentendu/une mauvaise communication). Et cela me rend vraiment heureux - cela me permet de continuer et d'être excité.
Je suggérerais de recommencer dans notre relation. Revenons à la discussion technique, apprenons tous de ce fil, comment gérer des situations similaires à l'avenir, supposons tous à nouveau que l'autre partie n'a en tête que le meilleur intérêt pour la plate-forme.
BTW : C'est l'une des quelques rencontres/discussions les plus difficiles sur le repo CoreFX au cours des 9 derniers mois, et comme vous le voyez, nous (y compris/en particulier moi) apprenons toujours à bien les gérer - donc cette instance particulière va pour profiter même à nous et cela nous rendra meilleurs à l'avenir et nous aidera à mieux comprendre les différents points de vue des membres passionnés de la communauté. Peut-être que cela façonnera nos mises à jour des documents de contribution ...

Mes commentaires acerbes résultaient de l'inefficacité de cette discussion. Inefficacité = manière chaotique de discuter, où les points ne sont pas abordés / résolus et on avance malgré cela => fastidieux.

J'ai compris votre frustration ! De manière intéressante, une frustration similaire était également de l'autre côté pour la même raison 😉 ... c'est presque drôle comment le monde fonctionne :).
Malheureusement, la discussion difficile fait partie du travail lorsque vous prenez une décision de conception. C'est BEAUCOUP de travail. Beaucoup de gens le sous-estiment. La compétence clé requise est la patience avec tout le monde et la capacité de dépasser votre propre opinion et de réfléchir à la manière de parvenir à un consensus même si cela ne vous convient pas. C'est pourquoi j'ai suggéré d'avoir 2 propositions et de "faire remonter" la discussion technique au groupe de réviseurs d'API (principalement parce que je ne sais pas avec certitude si j'ai raison, même si j'espère secrètement que j'ai raison comme tous les autres développeurs dans le monde le feraient 😉 ).

Il est TRÈS difficile d'avoir une opinion sur un sujet ET de conduire la discussion au CONSENSUS sur le même fil. De ce point de vue, vous et moi avons le plus de points communs sur ce fil - nous avons tous les deux des opinions, mais nous essayons tous les deux de mener la discussion à la clôture et à la décision. Alors travaillons en étroite collaboration.

Mon approche générale est la suivante : chaque fois que je pense que quelqu'un m'attaque, est méchant, paresseux, me frustre ou quelque chose du genre. Je me demande d'abord à moi-même et aussi à la personne en particulier : Pourquoi ? Pourquoi as-tu dis cela? Que voulais-tu dire?
Habituellement, c'est le signe d'un manque de compréhension/communication des motifs. Ou signe de trop lire entre les lignes et de voir des insultes/accusations/mauvaises intentions là où elles ne le sont pas.


Maintenant que je n'ai pas peur de continuer à discuter de questions techniques, voici ce que je voulais demander plus tôt :

Allez avec votre PriorityQueue désactivée qui ne permet pas de mettre à jour et de supprimer des éléments.

C'est quelque chose que je ne comprends pas. Si nous omettons IHeap (dans ma / la proposition originale ici), pourquoi cela ne serait-il pas possible ?
IMO, il n'y a pas de différence entre les 2 propositions du point de vue des capacités de classe, la seule différence est -- ajoutons-nous la surcharge du constructeur PriorityQueue(IHeap) ou non (en laissant de côté le conflit de nom de classe comme problème indépendant à résoudre) .

Avis de non-responsabilité (pour éviter les problèmes de communication) : je n'ai pas le temps de lire des articles et de faire des recherches, j'attends une réponse courte, présentant des arguments de hauteur de la part de quiconque veut conduire la discussion technique. Remarque : ce n'est pas moi qui trolle. Je poserais la même question à tous les membres de notre équipe faisant cette réclamation. Si vous n'avez pas l'énergie pour l'expliquer / animer la discussion (ce qui serait tout à fait compréhensible compte tenu des difficultés et de l'investissement en temps de votre part), dites-le simplement, pas de rancune. S'il vous plaît, ne vous sentez pas sous pression par moi ou par qui que ce soit (et cela s'applique à tout le monde sur le fil).

N'essayant pas d'ajouter un commentaire inutile de plus ici, ce fil a été BEAUCOUP TROP LONG. La règle n°1 de l'ère d'Internet est d'éviter la communication par texte si vous vous souciez des relations avec les gens. (Eh bien, je l'ai inventé). Je pense qu'une autre communauté open source passerait à un Google Hangout pour ce genre de discussion si le besoin s'en fait sentir. Quand vous regardez le visage des autres, vous ne dites jamais rien d'"insultant", et les gens se connaissent très vite. Peut-être qu'on peut essayer aussi ?

@karelz En raison de la longueur de la discussion ci-dessus, il est très peu probable que quelqu'un de nouveau contribue si le flux n'est pas modifié. En tant que tel, je voudrais proposer l'approche suivante dès maintenant :

  • Je procéderai au vote sur les aspects fondamentaux, l'un après l'autre. Nous aurons une contribution claire de la communauté. Idéalement, les réviseurs d'API viendront également ici et commenteront sous peu.
  • Les « messages de vote » contiendront suffisamment d'informations pour pouvoir ignorer tout le mur de texte ci-dessus.
  • Une fois cette session de vote terminée, nous saurons à quoi nous attendre des réviseurs d'API et pourrons procéder avec une certaine approche. Lorsque nous serons d'accord sur les aspects fondamentaux, ce numéro sera clos, et un autre ouvert (qui fera référence à celui-ci). Dans le nouveau numéro, je vais résumer nos conclusions et fournir une proposition d'API qui reflète ces décisions. Et nous le prendrons à partir de là.

Cela a-t-il une chance d'avoir un sens ?

PriorityQueue qui ne permet pas de mettre à jour et de supprimer des éléments.

C'était à propos de la proposition originale, qui manquait de ces capacités :) Désolé de ne pas avoir été clair.

Si vous n'avez pas l'énergie pour l'expliquer / animer la discussion (ce qui serait tout à fait compréhensible compte tenu des difficultés et de l'investissement en temps de votre part), dites-le simplement, pas de rancune. S'il vous plaît, ne vous sentez pas sous pression par moi ou par qui que ce soit (et cela s'applique à tout le monde sur le fil).

Je ne vais pas abandonner. Pas de douleur pas de gain xD

@xied75

Je pense qu'une autre communauté open source passerait à un Google Hangout pour ce genre de discussion si le besoin s'en fait sentir. Peut-être qu'on peut essayer aussi ?

Cela semble bon ;)

Fournir une interface

Peu importe si nous optons pour Heap / IHeap ou PriorityQueue / IPriorityQueue (ou autre chose), pour la fonctionnalité que nous sommes sur le point de fournir...

_voulez-vous avoir une interface, avec l'implémentation ?_

Pour

@bendono

En soutenant cette implémentation par une interface, ceux d'entre nous qui se soucient d'autres implémentations peuvent facilement échanger dans une autre implémentation (de notre propre création ou dans une bibliothèque externe) et toujours être compatible avec le code qui accepte l'interface.

Ceux qui s'en moquent peuvent l'ignorer et passer directement à la mise en œuvre concrète. Ceux qui s'en soucient peuvent utiliser n'importe quelle implémentation de leur choix.

Contre

@madelson

Il existe de nombreux algorithmes différents qui peuvent théoriquement être utilisés pour implémenter cette structure de données. Cependant, il semble peu probable que le framework soit livré avec plus d'un, et l'idée que les rédacteurs de bibliothèques aient vraiment besoin d'une interface canonique pour que diverses parties puissent se coordonner dans l'écriture d'implémentations de tas compatibles ne semble pas si probable.

De plus, une fois qu'une interface est publiée, elle ne peut plus être modifiée en raison de la compatibilité. Cela signifie que l'interface a tendance à prendre du retard par rapport aux classes concrètes en termes de fonctionnalité (un problème avec IList et IDictionary aujourd'hui).

@karelz

L'interface est un scénario expert très avancé.

Si la bibliothèque et l'interface IHeap deviennent vraiment populaires, nous pouvons changer d'avis plus tard et ajouter IHeap fonction de la demande (via la surcharge du constructeur), mais je ne pense pas que ce soit utile/aligné avec le reste de BCL suffit à justifier la complication d'ajouter une nouvelle interface maintenant. Commencez simplement, devenez compliqué uniquement si cela est vraiment nécessaire.

Impact potentiel sur la décision

  • L'inclusion de l'interface signifie que nous ne pouvons pas la modifier à l'avenir.
  • Ne pas inclure l'interface signifie que les gens écrivent du code qui utilise notre solution de bibliothèque standard ou qui est écrit par rapport à une solution fournie par une bibliothèque tierce (il n'y a pas d'interface commune qui permettrait une compatibilité croisée).

Utilisez 👍 et 👎 pour voter sur celui-ci (respectivement pour et contre une interface). Sinon, écrivez un commentaire. Idéalement, les réviseurs d'API participeront.

Je voudrais ajouter que même si changer d'interface est difficile, avec les méthodes d'extension (et les propriétés à venir), les interfaces sont plus faciles à étendre et/ou à utiliser (voir LINQ)

Je voudrais ajouter que même si changer d'interface est difficile, avec les méthodes d'extension (et les propriétés à venir), les interfaces sont plus faciles à étendre et/ou à utiliser (voir LINQ)

Ils ne peuvent fonctionner qu'avec les méthodes définies publiquement sur l'interface ; cela signifie donc bien faire les choses du premier coup.

Je suggérerais de patienter un peu sur l'interface jusqu'à ce que la classe soit utilisée et se soit installée - puis introduisez une interface. (le débat sur la forme de l'interface étant une question distincte)

Pour être franc, la seule chose qui m'importe, c'est l'interface. Une implémentation solide serait bien, mais moi (ou n'importe qui d'autre) pourrais toujours créer la mienne.

Je me souviens qu'il y a quelques années, nous avions eu exactement la même conversation avec HashSet<T> . Microsoft voulait HashSet<T> tandis que la communauté voulait ISet<T> . Si je me souviens bien, nous avons eu HashSet<T> premier et ISet<T> second. Sans interface, l'utilisation de HashSet<T> était assez limitée car il est difficile (voire souvent impossible) de changer une API publique.

Je dois noter qu'il y a aussi SortedSet<T> maintenant, sans parler de nombreuses implémentations non BCL de ISet<T> . J'ai utilisé ISet<T> dans des API publiques et j'en suis reconnaissant. Mon implémentation privée peut utiliser n'importe quelle implémentation concrète qui me semble correcte. Je peux également facilement remplacer une implémentation par une autre sans rien casser. Cela ne serait pas possible sans l'interface.

Pour ceux qui disent que nous pouvons toujours définir nos propres interfaces, considérez ceci. Supposons un instant que ISet<T> dans la BCL ne se soient jamais produits. Maintenant, je peux créer ma propre interface IMySet<T> ainsi que des implémentations solides. Cependant, un jour, le BCL HashSet<T> sort. Il peut ou non implémenter ISet<T> , mais il n'implémente pas IMySet<T> . Par conséquent, je ne peux pas échanger HashSet<T> en tant qu'implémentation de mon IMySet<T> .

Je crains que nous ne recommencions cette parodie.
Si vous n'êtes pas prêt à vous engager dans une interface, il est alors trop tôt pour introduire une classe concrète.

Je trouve la disparité des opinions importante. Rien qu'en regardant les chiffres, un peu plus de gens sont pour une interface, mais cela ne nous dit pas grand-chose. Je vais essayer de demander à d'autres personnes qui ont déjà participé à la discussion mais qui n'ont pas encore exprimé leur avis sur une interface :

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

Pour les notifiés : _les gars, pourriez-vous s'il vous plaît donner votre avis ? Ce serait très utile, même si vous votez simplement avec :+1:, :-1:. Vous pouvez commencer à lire ce commentaire sur

Certaines de ces personnes sont peut-être des réviseurs d'API, mais je pense que nous avons besoin de leur soutien dans cette décision fondamentale avant d'aller de l'avant. @karelz , @terrajobst , vous serait-il possible de leur demander de nous aider à résoudre cet aspect ? Leur contribution serait très précieuse, car ce sont eux qui l'examineront éventuellement. et un peu inutile, car nous pouvons connaître leur décision plus tôt).

Personnellement, je suis pour une interface, mais si la décision est différente, je suis heureux de suivre un chemin différent.

Je ne veux pas entraîner les réviseurs d'API dans la discussion - c'est long et désordonné, il ne serait pas efficace pour les réviseurs d'API de tout relire ou même de simplement décider quelle est la dernière réponse importante (je m'y perds ).
Je pense que nous en sommes au point où nous pouvons créer 2 propositions d'API formelles (voir le «bon» exemple ici) et mettre en évidence les avantages/inconvénients de chacune. Nous pouvons ensuite les examiner lors de notre réunion d'examen de l'API et faire des recommandations, en tenant compte des votes. En fonction de la discussion là-bas (s'il y a plusieurs opinions), nous pourrions revenir et lancer un sondage Twitter / un vote GH supplémentaire, etc.

BTW : les réunions d'examen de l'API ont lieu presque tous les mardis.

Pour vous aider à démarrer, voici à quoi devrait ressembler une proposition :

Exemple de proposition / Seed

```c#
espace de noms System.Collections.Generic
{
classe publique PriorityQueue
: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
public PriorityQueue();
public PriorityQueue (capacité int);
public PriorityQueue(IComparercomparateur);
public PriorityQueue(IEnumerablecollection);
public PriorityQueue(IEnumerablecollection, IComparercomparateur);
public PriorityQueue(int capacité, IComparercomparateur);

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

}
```

À FAIRE:

  • Exemple d'utilisation manquant (je ne sais pas comment j'exprime la priorité des éléments) - peut-être devrions-nous le reconcevoir pour avoir « int » comme entrée de valeur de priorité ? Peut-être une abstraction là-dessus ? (Je pense que cela a été discuté ci-dessus, mais le fil est beaucoup trop long à lire, c'est pourquoi nous avons besoin de propositions concrètes et non de plus de discussion)
  • Scénario UpdatePriority manquant
  • Quelque chose d'autre discuté ci-dessus qui manque ici?

@karelz OK, va le faire, restez connectés ! :le sourire:

Bien. Pour autant que je sache, une interface ou un tas ne passera pas l'examen de l'API. A ce titre je voudrais proposer une solution un peu différente, en oubliant le tas quaternaire par exemple. La structure de données ci-dessous est différente à plusieurs égards de ce que nous pouvons trouver dans Python , Java , C++ , Go , Swift et Rust (au moins ceux-ci).

L'accent est mis sur l'exactitude, l'exhaustivité en termes de fonctionnalité et d'intuitivité, tout en maintenant des complexités optimales et d'excellentes performances dans le monde réel.

@karelz @terrajobst

Proposition

Raisonnement

Il est assez courant qu'un utilisateur dispose d'un ensemble d'éléments, certains d'entre eux ayant une priorité plus élevée que d'autres. Finalement, ils veulent garder ce groupe d'éléments dans un ordre spécifique pour pouvoir effectuer efficacement les opérations suivantes :

  1. Ajouter un nouvel élément à la collection.
  2. Récupérer l'élément ayant la priorité la plus élevée (et pouvoir le supprimer).
  3. Supprimer un élément de la collection.
  4. Modifier un élément de la collection.
  5. Fusionner deux collections.

Usage

Glossaire

  • Valeur — données utilisateur.
  • Clé — un objet qui est utilisé à des fins de commande.

Différents types de données utilisateur

Tout d'abord, nous allons nous concentrer sur la construction de la file d'attente prioritaire (en ajoutant uniquement des éléments). La manière dont cela est fait dépend du type de données utilisateur.

Scénario 1

  • TKey et TValue sont des objets séparés.
  • TKey est comparable.
var queue = new PriorityQueue<int, string>();

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

Scénario 2

  • TKey et TValue sont des objets séparés.
  • TKey n'est pas comparable.
var comparer = Comparer<MyKey>.Create(/* custom logic */);

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

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

Scénario 3

  • TKey est contenu dans TValue .
  • TKey n'est pas comparable.
public class MyClass
{
    public MyKey Key { get; set; }
}

Outre un comparateur à clé, nous avons également besoin d'un sélecteur à clé :

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 */ ));
Remarques
  • Nous utilisons ici une autre méthode Enqueue . Cette fois, il ne prend qu'un seul argument ( TValue ).
  • Si le sélecteur de clé est défini, la méthode Enqueue(TKey, TValue) doit lancer InvalidOperationException .
  • Si le sélecteur de clé n'est pas défini, la méthode Enqueue(TValue) doit lancer InvalidOperationException .

Scénario 4

  • TKey est contenu dans TValue .
  • TKey est comparable.
var queue = new PriorityQueue<MyKey, MyClass>(selector);

queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
Remarques
  • Le comparateur pour TKey est supposé être Comparer<TKey>.Default , comme dans le scénario 1 .

Scénario 5

  • TKey et TValue sont des objets distincts, mais du même type.
  • TKey est comparable.
var queue = new PriorityQueue<int, int>();

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

Scénario 6

  • Les données utilisateur sont un objet unique, qui est comparable.
  • Il n'y a pas de clé physique ou un utilisateur ne veut pas l'utiliser.
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 */ ));
Remarques

Au départ, il y a une ambiguïté.

  • Il est possible que MyClass soit un objet distinct et que l'utilisateur souhaite simplement séparer la clé et la valeur, comme c'était le cas dans le scénario 5 .
  • Cependant, il peut aussi s'agir d'un seul objet (comme dans ce cas).

Voici comment PriorityQueue gère l'ambiguïté :

  • Si le sélecteur de clé est défini, alors il n'y a pas d'ambiguïté. Seul le Enqueue(TValue) est autorisé. Par conséquent, une solution alternative au scénario 6 consiste simplement à définir un sélecteur et à le transmettre au constructeur.
  • Si le sélecteur de clé n'est pas défini, l'ambiguïté est résolue avec la première utilisation d'une méthode Enqueue :

    • Si Enqueue(TKey, TValue) est appelé la première fois, la clé et la valeur sont considérées comme des objets distincts ( Scénario 5 ). A partir de là, la méthode Enqueue(TValue) doit renvoyer InvalidOperationException .

    • Si Enqueue(TValue) est appelé la première fois, la clé et la valeur sont considérées comme le même objet, le sélecteur de clé est déduit ( Scénario 6 ). A partir de là, la méthode Enqueue(TKey, TValue) doit renvoyer InvalidOperationException .

Autre fonctionnalité

Nous avons déjà couvert la création d'une file d'attente prioritaire. Nous savons également ajouter des éléments à la collection. Nous allons maintenant nous concentrer sur les fonctionnalités restantes.

Élément de priorité la plus élevée

Nous pouvons effectuer deux opérations sur l'élément ayant la priorité la plus élevée : le récupérer ou le supprimer.

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

Ce qui est exactement renvoyé par les méthodes Peek et Dequeue sera discuté plus tard.

Modifier un élément

Pour un élément arbitraire, nous pouvons souhaiter le modifier. Par exemple, si une file d'attente prioritaire est utilisée pendant toute la durée de vie d'un service, il y a de fortes chances que le développeur souhaite avoir la possibilité de mettre à jour les priorités.

Étonnamment, une telle fonctionnalité n'est pas fournie par des structures de données équivalentes : en Python , Java , C++ , Go , Swift et Rust . Peut-être d'autres aussi, mais je n'ai vérifié que ceux-là. Le résultat? Développeurs déçus :

Nous avons essentiellement deux options ici :

  • Faites-le à la manière Java et ne fournissez pas cette fonctionnalité. Je suis fortement contre cela. Cela oblige l'utilisateur à supprimer un élément de la collection (ce qui seul ne fonctionne pas bien, mais j'y reviendrai plus tard), puis à l'ajouter à nouveau. C'est très moche, ça ne marche pas dans tous les cas, et c'est inefficace.
  • Introduire un nouveau concept de poignées .

Poignées

Chaque fois qu'un utilisateur ajoute un élément à la file d'attente prioritaire, il reçoit un handle :

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

Handle est une classe avec l'API publique suivante :

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

Il s'agit d'une référence à un élément unique dans la file d'attente prioritaire. Si vous êtes préoccupé par l'efficacité, veuillez consulter la FAQ (et vous ne serez plus inquiet).

Une telle approche nous permet de modifier facilement un élément unique dans la file d'attente prioritaire, de manière très intuitive et simple :

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

Dans l'exemple ci-dessus, comme un sélecteur spécifique était défini, un utilisateur pouvait mettre à jour l'objet lui-même. La file d'attente prioritaire avait simplement besoin d'être notifiée pour se réorganiser (nous ne voulons pas la rendre observable).

De la même manière que le type de données utilisateur affecte la façon dont une file d'attente prioritaire est créée et remplie d'éléments, la façon dont elle est mise à jour varie également. Je vais raccourcir les scénarios cette fois, car vous savez probablement déjà ce que je vais écrire.

Scénarios

Scénario 7
  • TKey et TValue sont des objets séparés.
var queue = new PriorityQueue<int, string>();

var handle = queue.Enqueue(1, "three");
queue.Enqueue(1, "three");
queue.Enqueue(2, "three");

queue.Update(handle, 3);

Comme vous pouvez le voir, une telle approche fournit un moyen simple de se référer à un élément unique dans la file d'attente prioritaire, sans poser de questions. Il est particulièrement utile dans les scénarios où les clés peuvent être dupliquées. De plus, c'est très efficace — nous connaissons l'élément à mettre à jour en O(1), et l'opération peut être effectuée en O(log n).

Alternativement, l'utilisateur peut utiliser des méthodes supplémentaires qui recherchent dans toute la structure en O(n), puis mettent à jour le premier élément qui correspond aux arguments. C'est ainsi que la suppression d'un élément se fait en Java. Ce n'est ni tout à fait correct ni efficace, mais parfois plus simple :

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

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

queue.Update("three", 30);

La méthode ci-dessus trouve le premier élément avec sa valeur égale à "three" . Sa clé sera mise à jour en 30 . Cependant, nous ne savons pas lequel sera mis à jour s'il y en a plusieurs qui satisfont à la condition.

Cela pourrait être légèrement plus sûr avec une méthode Update(TKey oldKey, TValue, TKey newKey) . Cela ajoute une autre condition : l'ancienne clé doit également correspondre. Les deux solutions sont plus simples, mais pas 100% sûres et moins performantes (O(1 + log n) vs O(n + log n)).

Scénario 8
  • TKey est contenu dans TValue .
var queue = new PriorityQueue<int, MyClass>(selector);

/* adding some elements */

queue.Update(handle);

Ce scénario est ce qui a été donné comme exemple dans la section Poignées .

Ce qui précède est réalisé en O(log n). Alternativement, l'utilisateur peut utiliser une méthode Update(TValue) qui trouve le premier élément égal à celui spécifié et effectue une réorganisation interne. Bien sûr, c'est O(n).

Scénario 9
  • TKey et TValue sont des objets distincts, mais du même type.

A l'aide d'une poignée, il n'y a pas d'ambiguïté, comme toujours. Dans le cas d'autres méthodes permettant la mise à jour, il y en a bien sûr. Il s'agit d'un compromis entre la simplicité, les performances et l'exactitude (ce qui dépend si les données peuvent être dupliquées ou non).

Scénario 10
  • Les données utilisateur sont un objet unique.

Avec une poignée, il n'y a de nouveau aucun problème. La mise à jour via d'autres méthodes peut être plus simple - mais encore une fois, moins performante et pas toujours correcte (entrées égales).

Supprimer un élément

Peut être fait simplement et correctement via une poignée. Alternativement, via les méthodes Remove(TValue) et Remove(TKey, TValue) . Mêmes problèmes que ceux décrits précédemment.

Fusion de deux collections

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

/* add some elements to both */

queue1.Merge(queue2);

Remarques

  • Une fois la fusion terminée, les files d'attente partagent la même représentation interne. L'utilisateur peut utiliser n'importe lequel des deux.
  • Les types doivent correspondre (vérifiés statiquement).
  • Les comparateurs doivent être égaux, sinon InvalidOperationException .
  • Les sélecteurs doivent être égaux, sinon InvalidOperationException .

API proposée

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

Questions ouvertes

Prédicats à la place

Je suis fortement pour l'approche avec des poignées, car elle est efficace, intuitive et parfaitement correcte . La question est de savoir comment traiter des méthodes plus simples mais potentiellement pas si sûres. Une chose que nous pourrions faire est de remplacer ces méthodes plus simples par quelque chose comme ceci :

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

L'utilisation serait assez douce et puissante. Et vraiment intuitif. Et lisible (très expressible). Je suis pour celui-là.

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

/* add some elements */

queue.UpdateAll(x => x.Key < 15, 0);

// or for example

queue.UpdateFirst(x => x.Value.State == State.Idle, 100);

La définition de la nouvelle clé pourrait également être une fonction, qui prend l'ancienne clé et la transforme d'une manière ou d'une autre.

Idem pour Contains et Remove .

Clé de mise à jour

Si le sélecteur de clé n'est pas défini (et donc la clé et la valeur sont conservées séparément), la méthode peut être nommée UpdateKey . C'est probablement plus exprimable. Cependant, lorsque le sélecteur de clé est défini, Update est meilleur, car la clé est déjà mise à jour et ce qu'il faut faire, c'est réorganiser certains éléments dans la file d'attente prioritaire.

FAQ

Les poignées ne sont-elles pas inefficaces ?

Il n'y a pas de problème d'efficacité en ce qui concerne l'utilisation des poignées. Une crainte commune est que cela nécessitera des allocations supplémentaires, car nous utilisons un tas basé sur un tableau en interne. N'aie pas peur. Continuer à lire.

Comment allez-vous le mettre en œuvre alors ?

Ce serait une approche complètement différente pour fournir une file d'attente prioritaire. Pour la première fois, une bibliothèque standard ne fournira pas cette fonctionnalité implémentée sous forme de tas binaire, qui est représenté sous la forme d'un tableau. Il sera implémenté avec un tas d'appariement , qui par conception n'utilise pas de tableau - il représente simplement un arbre à la place.

À quel point serait-il performant ?

  • Pour une entrée aléatoire générale, un tas quaternaire serait légèrement plus rapide.
  • Cependant, il devrait être basé sur un tableau pour être plus rapide. Ensuite, les descripteurs ne pouvaient pas être basés sur des nœuds simplement - des allocations supplémentaires seraient nécessaires. Ensuite, nous ne pouvions pas mettre à jour et supprimer des éléments de manière raisonnable.
  • La façon dont il est conçu actuellement, nous avons une API facile à utiliser tout en étant implémentée assez performante.
  • Un avantage supplémentaire d'avoir un tas d'appariement en dessous est la possibilité de fusionner deux files d'attente prioritaires en O(1), au lieu de O(n) comme dans les tas binaires/quaternaires.
  • Le tas d'appariement est toujours incroyablement rapide. Voir références. Parfois, c'est plus rapide que les tas quaternaires (dépend des données d'entrée et des opérations effectuées, pas seulement de la fusion).

Les références

  • Michael L. Fredman, Robert Sedgewick, Daniel D. Sleator et Robert E. Tarjan (1986), The pairing heap: A new form of self- adjusting heap , Algorithmica 1:111-129 .
  • Daniel H. Larkin, Siddhartha Sen et Robert E. Tarjan (2014), Une étude empirique de retour aux sources des files d'attente prioritaires , arXiv:1403.0252v1 [cs.DS].

BTW, c'est pour la 1ère itération de révision de l'API. La section avec les _questions ouvertes_ doit être résolue (j'ai besoin de votre avis [et des examinateurs de l'API si possible]). Si la 1ère itération réussit au moins partiellement, pour le reste de la discussion, j'aimerais créer un nouveau problème (fermer et référencer celui-ci).

Pour autant que je sache, une interface ou un tas ne passera pas l'examen de l'API.

@pgolebiowski : Pourquoi ne pas simplement clore le problème alors ? Sans interface, cette classe est presque inutile. Le seul endroit où je pourrais l'utiliser est dans les implémentations privées. À ce stade, je peux simplement créer (ou réutiliser) ma propre file d'attente prioritaire en cas de besoin. Je ne peux pas exposer une API publique dans mon code avec ce type dans la signature car elle se brisera dès que je devrai la remplacer par une autre implémentation.

@bendono
Eh bien, Microsoft a le dernier mot ici, pas quelques personnes commentant le fil. Et on sait que :

Je suis à peu près sûr que d'autres réviseurs/architectes d'API le partageront, car j'ai vérifié l'opinion de quelques-uns : le nom devrait être PriorityQueue, et nous ne devrions pas introduire l'interface IHeap. Il devrait y avoir exactement une implémentation (probablement via un tas).

Ceci est partagé par @karelz , Software Engineer Manager, et @terrajobst , Program Manager et le propriétaire de ce référentiel + quelques réviseurs d'API.

Bien que j'aime évidemment l'approche avec les interfaces, comme indiqué clairement dans les messages précédents, je peux voir que c'est assez difficile étant donné que nous n'avons pas beaucoup de pouvoir dans cette discussion. Nous avons fait valoir nos points, mais nous ne sommes que quelques commentateurs. Le code ne nous appartient de toute façon pas. Que pouvons-nous faire d'autre?

Pourquoi ne pas simplement clore le problème alors ? Sans interface, cette classe est presque inutile.

J'en ai assez fait -- au lieu de détester mon travail, fais quelque chose. Faites le travail proprement dit. Pourquoi essayez-vous de me blâmer pour quoi que ce soit? Blâmez-vous de ne pas réussir à convaincre les autres de votre point de vue.

Et s'il vous plaît, épargnez-moi une rhétorique comme ça. C'est vraiment enfantin - faire mieux.

BTW, la proposition se concentre sur les choses qui sont communes, peu importe si nous introduisons une interface ou non, ou si la classe s'appelle PriorityQueue ou Heap . Alors concentrez-vous sur ce qui compte vraiment ici et montrez-nous un certain biais pour l'action si vous voulez quelque chose.

@pgolebiowski Bien sûr, c'est la décision de Microsoft. Mais il vaut mieux présenter une API que vous souhaitez utiliser et qui répond à vos besoins. S'il est rejeté, qu'il en soit ainsi. Je ne vois tout simplement pas la nécessité de compromettre la proposition.

Je m'excuse si vous avez interprété mes commentaires comme un blâme. Ce n'était certainement pas mon intention.

@pgolebiowski pourquoi ne pas utiliser KeyValuePair<TKey,TValue> pour la poignée ?

@SamuelEnglard

Pourquoi ne pas utiliser KeyValuePair<TKey,TValue> pour la poignée ?

  • Eh bien, le PriorityQueueHandle est en fait _ le nœud de tas d'appariement _. Il expose deux propriétés -- TKey Key et TValue Value . Cependant, il y a beaucoup plus de logique à l'intérieur, qui est juste interne. Veuillez vous référer à ma mise en œuvre de cela . Il contient par exemple aussi des pointeurs vers d'autres nœuds de l'arborescence (tout serait interne à CoreFX).
  • KeyValuePair est une structure, elle est donc copiée à chaque fois + elle ne peut pas être héritée.
  • Mais l'essentiel est que PriorityQueueHandle est une classe assez compliquée qui se trouve juste à exposer la même API publique que KeyValuePair .

@bendono

Mais il vaut mieux présenter une API que vous souhaitez utiliser et qui répond à vos besoins. S'il est rejeté, qu'il en soit ainsi. Je ne vois tout simplement pas la nécessité de compromettre la proposition.

  • C'est vrai, je vais avoir ça en tête et voir ce qui se passe. @karelz , avec la proposition, pourriez-vous également passer une partie du post précédent (ils sont juste avant la proposition), où nous avons voté pour les interfaces ? Nous y reviendrons peut-être plus tard, après la 1ère itération de l'examen de l'API.
  • Néanmoins, quelle que soit la solution choisie, la fonctionnalité sera très similaire et il serait utile de la revoir. Parce que si l'idée des poignées est rejetée et que nous ne pouvons pas vraiment mettre à jour/supprimer des éléments correctement (ou nous ne pouvons pas séparer TKey et TValue ), cette classe est vraiment proche d'être inutile alors - - comme c'est maintenant en Java.
  • En particulier, si nous n'avons pas d'interface, ma bibliothèque AlgoKit ne pourra pas avoir de noyau commun pour les tas avec CoreFX, ce qui serait vraiment triste pour moi.
  • Et oui, c'est vraiment surprenant pour moi que l'ajout d'une interface soit perçu comme un inconvénient par Microsoft.

Ce n'était certainement pas mon intention.

Désolé, mon erreur alors.

Mes questions sur la conception (avertissement : ces questions et clarifications, pas de recul (encore) :

  1. Avons-nous vraiment besoin de PriorityQueueHandle ? Et si nous attendions simplement des valeurs uniques dans la file d'attente ?

    • Motivation : Il semble plutôt impliqué concept. Si nous l'avons, j'aimerais comprendre pourquoi nous en avons besoin? Comment ça aide? Ou s'agit-il simplement de détails d'implémentation spécifiques qui fuient dans la surface de l'API ? Est-ce que ça va nous acheter autant de perf pour payer la complication dans l'API ?

  2. Avons-nous besoin de Merge ? Est-ce que d'autres collections l'ont ? Nous ne devrions pas ajouter d'API, simplement parce qu'elles sont faciles à implémenter, il devrait y avoir des cas d'utilisation courants pour les API.

    • Peut-être qu'ajouter une initialisation à partir de IEnumerable<KeyValuePair<TKey, TValue>> serait suffisant ? + compter sur Linq

  3. Avons-nous besoin d' comparer surcharge de
  4. Avons-nous besoin keySelector surcharges de

Points de décision séparés/parallèles :

  1. Nom de la classe PriorityQueue vs Heap
  2. Introduire IHeap et la surcharge du constructeur ?

Introduire IHeap et la surcharge du constructeur ?

On dirait que les choses se sont suffisamment arrangées pour que je jette mes 2 cents... J'aime l'interface, personnellement. Il fait abstraction des détails de mise en œuvre de l'API (et de la fonctionnalité de base décrite par cette API) d'une manière qui, à mon avis, simplifie la structure et permet la plus grande convivialité.

Ce que je n'ai pas d'opinion aussi forte, c'est si nous faisons l'interface en même temps que le PQueue/Heap/ILikeThisItemMoreThanThisItemList ou si nous l'ajoutons plus tard. L'argument selon lequel l'API peut être "en évolution" et en tant que tel, nous devrions d'abord la publier en tant que classe jusqu'à ce que nous obtenions des commentaires est certainement un argument valable avec lequel je ne suis pas en désaccord. La question devient alors quand il est considéré comme suffisamment "stable" pour ajouter une interface. Bien au-dessus dans le fil, IList et IDictionary ont été mentionnés comme étant à la traîne par rapport aux API de leurs implémentations canoniques que nous avons ajoutées il y a très longtemps, alors quelle période est considérée comme une période de repos acceptable ?

Si nous pouvons définir cette période avec une certitude raisonnable et être sûrs qu'elle ne bloque pas de manière inacceptable, alors je ne vois aucun problème à expédier cette structure de données volumineuse sans interface. Ensuite, après cette période, nous pouvons examiner l'utilisation et envisager d'ajouter une interface.

Et oui, c'est vraiment surprenant pour moi que l'ajout d'une interface soit perçu comme un inconvénient par Microsoft.

En effet , dans beaucoup de façons , il est un inconvénient. Si nous expédions une interface, c'est à peu près fait. Il n'y a pas beaucoup de marge de manœuvre pour itérer sur cette API, nous ferions donc mieux de nous assurer qu'elle est correcte la première fois et continuera de l'être pour les nombreuses années à venir. Manque quelques nice-to-have fonctionnalité est un moyen meilleur endroit pour être que d' être coincé avec une interface potentiellement inadéquate où il serait oh-so-agréable d'avoir tout ce petit changement qui ferait tout mieux.

Merci pour la contribution, @karelz et @ianhays !

Autoriser les doublons

Avons-nous vraiment besoin de PriorityQueueHandle ? Et si nous attendions simplement des valeurs uniques dans la file d'attente ?

Motivation : Il semble plutôt impliqué concept. Si nous l'avons, j'aimerais comprendre pourquoi nous en avons besoin? Comment ça aide? Ou s'agit-il simplement de détails d'implémentation spécifiques qui fuient dans la surface de l'API ? Est-ce que ça va nous acheter autant de perf pour payer la complication dans l'API ?

Non, nous n'avons pas besoin de ça. L'API de file d'attente prioritaire proposée ci-dessus est assez puissante et très flexible. Il repose sur l'hypothèse que les éléments et les priorités pourraient être dupliqués . En raison de cette hypothèse, il est nécessaire d'avoir un handle pour pouvoir supprimer ou mettre à jour le nœud correct. Cependant, si nous imposons une restriction selon laquelle les éléments doivent être uniques, nous pourrions obtenir le même résultat que ci-dessus avec une API plus simple et sans exposer une classe interne ( PriorityQueueHandle ), ce qui n'est effectivement pas idéal.

Supposons que nous n'autorisons que des éléments uniques. Nous pourrions toujours prendre en charge tous les scénarios précédents et conserver des performances optimales. API plus simple :

public class PriorityQueue<TElement, TPriority>
    : IEnumerable,
    IEnumerable<(TElement element, TPriority priority)>,
    IReadOnlyCollection<(TElement element, TPriority priority)>
    // ICollection not included on purpose
{
    public PriorityQueue();
    public PriorityQueue(IComparer<TPriority> comparer);
    public PriorityQueue(Func<TElement, TPriority> prioritySelector);
    public PriorityQueue(Func<TElement, TPriority> prioritySelector, IComparer<TPriority> comparer);

    IComparer<TPriority> Comparer { get; }
    Func<TElement, TPriority> PrioritySelector { get; }
    public int Count { get; }

    public void Clear();
    public bool Contains(TElement element); // O(1)

    public (TElement element, TPriority priority) Peek(); // O(1)
    public (TElement element, TPriority priority) Dequeue(); // O(log n)

    public void Enqueue(TElement element, TPriority priority); // O(log n)
    public void Enqueue(TElement element); // O(log n)

    public void Update(TElement element); // O(log n)
    public void Update(TElement element, TPriority priority); // O(log n)

    public void Remove(TElement element); // O(log n)

    public IEnumerator<(TElement element, TPriority priority)> GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator();
}

Bientôt, il y aurait un sous-jacent Dictionary<TElement, InternalNode> . Tester si la file d'attente prioritaire contient un élément peut être fait encore plus rapidement que dans l'approche précédente. La mise à jour et la suppression d'éléments sont considérablement simplifiées, car nous pouvons toujours pointer vers un élément direct dans la file d'attente.

Autoriser les doublons ne vaut probablement pas la peine et ce qui précède est suffisant. Je pense que j'aime ça. Qu'est-ce que tu penses?

Fusionner

Avons-nous besoin de Merge ? Est-ce que d'autres collections l'ont ? Nous ne devrions pas ajouter d'API, simplement parce qu'elles sont faciles à implémenter, il devrait y avoir des cas d'utilisation courants pour les API.

Se mettre d'accord. Nous n'en avons pas besoin. Nous pouvons toujours ajouter une API mais pas la supprimer. Je suis d'accord pour supprimer cette méthode et (potentiellement) l'ajouter plus tard (s'il y a un besoin).

Comparateur

Avons-nous besoin d'une surcharge de comparateur ? Pouvons-nous toujours revenir à la valeur par défaut ? (avertissement : il me manque des connaissances/expertise dans ce cas, alors je demande simplement)

Je pense que c'est assez important, pour deux raisons :

  • Un utilisateur souhaite que ses éléments soient classés par ordre croissant ou décroissant. La priorité la plus élevée ne nous dit pas comment la commande doit être effectuée.
  • Si nous sautons un comparateur, nous obligeons l'utilisateur à toujours nous fournir une classe qui implémente IComparable .

De plus, il est cohérent avec l'API existante. Jetez un œil à SortedDictionary .

Sélecteur

Avons-nous besoin de surcharges keySelector ? Je pense que nous devrions décider si nous voulons avoir la priorité dans le cadre de la valeur, ou une chose distincte. La priorité séparée me semble un peu plus naturelle, mais je n'ai pas d'opinion tranchée. Connaît-on le pour et le contre ?

J'aime aussi la priorité séparée. En dehors de cela, il est plus facile à implémenter, de meilleures performances et une meilleure utilisation de la mémoire, une API plus intuitive, moins de travail pour l'utilisateur (pas besoin d'implémenter IComparable ).

Concernant le sélecteur maintenant...

Avantages

C'est ce qui rend cette file d'attente prioritaire flexible. Il permet aux utilisateurs d'avoir :

  • éléments et leurs priorités en tant qu'éléments séparés
  • éléments (classes complexes) qui ont des priorités quelque part à l'intérieur d'eux
  • une logique externe qui récupère la priorité pour un élément donné
  • éléments qui implémentent IComparable

Il permet à peu près toutes les configurations auxquelles je peux penser. Je trouve cela utile, car les utilisateurs peuvent simplement "plug n play". C'est aussi assez intuitif.

Les inconvénients

  • Il y a plus à apprendre.
  • Plus d'API. Deux constructeurs supplémentaires, une méthode Enqueue et Update supplémentaire.
  • Si nous décidons de séparer ou de joindre l'élément et la priorité, nous obligeons certains utilisateurs (qui ont leurs données dans un format différent) à adapter leur code pour utiliser cette structure de données.

Points de décision séparés/parallèles

Nom de la classe PriorityQueue vs Heap

Introduisez IHeap et la surcharge du constructeur.

  • Il semble que PriorityQueue devrait faire partie de CoreFX, plutôt que Heap .
  • Concernant l'interface IHeap — parce qu'une file d'attente prioritaire peut être implémentée avec quelque chose de différent d'un tas, nous aimerions probablement ne pas l'exposer de cette façon. Cependant, nous pourrions avoir besoin de IPriorityQueue .

Si nous expédions une interface, c'est à peu près fait. Il n'y a pas beaucoup de marge de manœuvre pour itérer sur cette API, nous ferions donc mieux de nous assurer qu'elle est correcte la première fois et continuera de l'être pour les nombreuses années à venir. Manquer certaines fonctionnalités agréables est un bien meilleur endroit où être que d'être coincé avec une interface potentiellement inadéquate où il serait tellement agréable d'avoir juste ce petit changement qui rendrait tout meilleur.

Entièrement d'accord!

Éléments comparables

Si nous ajoutons une autre hypothèse : que les éléments doivent être comparables, alors l'API est encore plus simple. Mais encore une fois, c'est moins flexible.

Avantages

  • Pas besoin de IComparer .
  • Pas besoin de sélecteur.
  • Une méthode Enqueue et Update .
  • Un type générique au lieu de deux.

Les inconvénients

  • Nous ne pouvons pas avoir l'élément et la priorité en tant qu'objets séparés. Les utilisateurs doivent fournir une nouvelle classe wrapper s'ils l'ont dans ce format.
  • Les utilisateurs doivent toujours implémenter IComparable avant de pouvoir utiliser cette file d'attente prioritaire.
  • Si nous avons des éléments et des priorités séparés, nous pourrions l'implémenter plus facilement, avec de meilleures performances et une meilleure utilisation de la mémoire - le dictionnaire interne <TElement, InternalNode> et que InternalNode contienne le 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();
}

Tout est un compromis. Nous supprimons certaines fonctionnalités, perdons un peu de flexibilité, mais peut-être n'en avons-nous pas besoin pour satisfaire 95% de nos utilisateurs.

J'aime aussi l'approche de l'interface car elle est plus flexible et donne plus d'utilisations, mais je suis également d'accord avec @karelz et @ianhays que nous devrions attendre que la nouvelle API de classe soit utilisée et que nous obtenions des commentaires jusqu'à ce que nous expédions réellement l'interface, puis nous sont stockés avec cette version et ne peuvent pas la changer sans casser les clients qui l'ont déjà utilisé.

En ce qui concerne également les comparateurs, je pense qu'il suit les autres API BCL et je n'aime pas le fait que les utilisateurs aient besoin de fournir une nouvelle classe wrapper. J'aime beaucoup la surcharge du constructeur recevant une approche de comparateur et utilisant ce comparateur dans toutes les comparaisons qui doivent être effectuées en interne, si le comparateur n'est pas fourni ou si le comparateur est null, alors utilisez le comparateur par défaut.

@pgolebiowski merci pour cette proposition d'API détaillée et descriptive et d'être si proactif et énergique pour faire approuver cette API et l'ajouter à CoreFX 👍 lorsque cette discussion est terminée et que nous considérons qu'elle est prête à être examinée, je fusionnerais toutes les entrées et la surface finale de l'API en un seul commentaire et mettez à jour le commentaire sur le problème principal en haut, car cela faciliterait la vie des évaluateurs.

OK, je suis convaincu de comparer, ça a du sens et c'est cohérent.
Je suis toujours déchiré sur le sélecteur - IMO, nous devrions essayer de nous en passer - divisons-le en 2 variantes.

```c#
classe publique PriorityQueue
: IEnumerable,
IEnumerable<(élément TElement, priorité TPriority)>,
IReadOnlyCollection<(élément TElement, priorité TPriority)>
// Collection non incluse exprès
{
public PriorityQueue();
public PriorityQueue(IComparercomparateur);

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

//
// Pièce sélecteur
//
public PriorityQueue(FuncPrioritySelector);
public PriorityQueue(FuncPrioritySelector, IComparercomparateur);

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

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

}
````

Questions ouvertes:

  1. Nom de la classe PriorityQueue vs Heap
  2. Introduire IHeap et la surcharge du constructeur ? (doit-on attendre plus tard ?)
  3. Présentez IPriorityQueue ? (Faut-il attendre plus tard - exemple IDictionary )
  4. Utiliser le sélecteur (de priorité stocké à l'intérieur de la valeur) ou non (différence 5 API)
  5. Utilisez des tuples (TElement element, TPriority priority) contre KeyValuePair<TPriority, TElement>

    • Peek et Dequeue devraient-ils plutôt avoir out argument

J'essaierai de l'exécuter par le groupe d'examen des API demain pour obtenir des commentaires précoces.

Correction du nom du champ de tuple Peek et Dequeue en priority (merci @pgolebiowski de l' avoir signalé).
Top-post mis à jour avec la dernière proposition ci-dessus.

Je seconde le commentaire de @safern - Merci beaucoup à @pgolebiowski pour l'avoir fait avancer !

Questions ouvertes:

Je pense que nous pourrions en avoir un de plus ici :

  1. Limitons-nous aux éléments uniques uniquement (n'autorisez pas les doublons).

Bon point, capturé comme « hypothèses » dans le premier message, plutôt que comme question ouverte. L'inverse dégraderait un peu l'API.

Doit-on retourner bool partir de Remove ? Et aussi la priorité en tant que out priority arg ? (Je crois que nous avons récemment ajouté des surcharges similaires dans d'autres structures de données)

Similaire à Contains - Je parie que quelqu'un voudra en retirer la priorité. Nous pourrions vouloir ajouter une surcharge avec out priority .

Je reste d'avis (bien que je ne l'aie pas encore exprimé) que Heap impliquerait une implémentation et prendrait dûment en charge l'appel de PriorityQueue . (Je soutiendrais même alors une proposition pour une classe Heap plus mince qui autorise les éléments en double, et ne permet pas la mise à jour, etc. (plus conforme à la proposition originale) mais je ne m'attends pas à cela se passer).

KeyValuePair<TPriority, TElement> ne devrait certainement pas être utilisé, car des priorités en double sont attendues, et je pense que KeyValuePair<TElement, TPriority> est également déroutant, donc je soutiendrais de ne pas utiliser du tout KeyValuePair , et soit en utilisant des tuples simples, ou des paramètres de sortie pour les priorités (personnellement, j'aime les paramètres de sortie, mais je ne suis pas inquiet).

Si nous interdisons les doublons, nous devons décider du comportement consistant à tenter de les rajouter avec des priorités différentes/modifiées.

Je ne soutiens pas la proposition du sélecteur, pour la simple raison qu'elle implique une redondance et recréera dûment la confusion. Si la priorité d'un élément est stockée avec lui, alors il sera stocké à deux endroits, et s'ils deviennent désynchronisés (c'est-à-dire que quelqu'un oublie d'appeler la méthode inutile Update(TElement) ) alors beaucoup de souffrance assurera. Si le sélecteur est un ordinateur, alors nous sommes ouverts aux personnes qui ajoutent intentionnellement un élément, puis modifient les valeurs à partir desquelles ils sont calculés, et maintenant s'ils essaient de le rajouter, il y a une multitude de choses qui pourraient mal tourner selon sur la décision de ce qui se passe lorsque cela se produit. À un niveau légèrement supérieur, essayer pourrait entraîner l'ajout d'une copie modifiée de l'élément, car il n'est plus égal à ce qu'il était autrefois (il s'agit d'un problème général avec les clés modifiables, mais je pense que séparer la priorité et l'élément sera aider à éviter un problème potentiel).

Le sélecteur lui-même est susceptible de changer de comportement, ce qui est une autre façon pour les utilisateurs de tout casser sans réfléchir. Bien mieux, je pense, que les utilisateurs fournissent explicitement les priorités. Le gros problème que je vois avec cela, c'est qu'il indique que les entrées en double sont autorisées, car nous déclarons une paire, pas un élément. Cependant, une documentation en ligne raisonnable et une valeur de retour bool sur Enqueue devraient trivialement remédier à cela. Mieux qu'un booléen serait peut-être de renvoyer l'ancienne/nouvelle priorité de l'élément (par exemple si Enqueue utilise la priorité nouvellement fournie, ou le minimum des deux, ou l'ancienne priorité), mais je pense que Enqueue devrait simplement échouer si vous essayez de rajouter quelque chose, et devrait donc simplement renvoyer un bool indiquant le succès. Cela permet Enqueue garder Update complètement séparés et bien définis.

Je soutiendrais de ne pas utiliser du tout KeyValuePair, et soit d'utiliser des tuples simples

Je suis avec toi sur tuples.

Si nous interdisons les doublons, nous devons décider du comportement consistant à tenter de les rajouter avec des priorités différentes/modifiées.

  • Je vois une similitude avec l'indexeur dans un Dictionary . Là, lorsque vous faites dictionary["something"] = 5 il est mis à jour si "something" était une clé auparavant. S'il n'y était pas, il est simplement ajouté.
  • Cependant, la méthode Enqueue est pour moi analogue à la méthode Add du dictionnaire. Ce qui signifie qu'il devrait lever une exception.
  • Compte tenu des points ci-dessus, nous pourrions envisager d'ajouter un indexeur à la file d'attente prioritaire pour prendre en charge le comportement que vous avez en tête.
  • Mais à son tour, un indexeur peut ne pas être quelque chose qui fonctionne avec le concept de files d'attente.
  • Ce qui nous amène à la conclusion que la méthode Enqueue devrait simplement lever une exception si quelqu'un veut ajouter un élément dupliqué. De même, la méthode Update devrait lever une exception si quelqu'un veut mettre à jour la priorité d'un élément qui n'est pas présent.
  • Ce qui nous amène à une nouvelle solution - ajoutez la méthode TryUpdate qui renvoie effectivement bool .

Je ne soutiens pas la proposition du sélecteur, pour la simple raison qu'elle implique une redondance

N'est-ce pas que la clé n'est pas physiquement copiée (si elle existe), mais le sélecteur reste juste une fonction qui est appelée lorsque les priorités doivent être évaluées ? Où est la redondance ?

Je pense que séparer la priorité et l'élément aidera à éviter un problème potentiel

Le seul problème est le cas où le client n'a pas la priorité physique. Il ne peut pas faire grand-chose alors.

Bien mieux, je pense, que les utilisateurs fournissent explicitement les priorités. Le gros problème que je vois avec cela, c'est qu'il indique que les entrées en double sont autorisées, car nous déclarons une paire, pas un élément.

Je vois quelques problèmes avec cette solution, mais pas nécessairement pourquoi elle indique que les entrées en double sont autorisées. Je pense que la logique "pas de doublons" devrait être appliquée sur TElement uniquement -- la priorité n'est qu'une valeur ici.

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

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

@VisualMelon est-ce que cela a du sens ?

@pgolebiowski

Je suis tout à fait d'accord avec l'ajout d'un TryUpdate , et le lancer de Update sens pour moi. Je pensais plutôt à ISet par rapport à Enqueue (plutôt qu'à IDictionary ). Le lancer aurait également du sens, j'ai dû rater ce point, mais je pense que le retour bool transmet le « setness » du type. Peut-être qu'un TryEnqueue serait dans l'ordre aussi (avec Add lancer) ? Un TryRemove serait également apprécié (en cas d'échec si vide). Concernant votre dernier point, oui, c'est le comportement que j'avais en tête aussi. Je suppose qu'une analogie avec IDictionary est meilleure que ISet à la réflexion, et cela devrait être assez clair. (Pour résumer : je soutiendrais tout lancer selon votre suggestion, mais avoir Try* est un must si tel est le cas ; je suis également d'accord avec vos déclarations concernant les conditions d'échec).

En ce qui concerne l'indexeur, je pense que vous avez raison de dire qu'il ne correspond pas vraiment au concept de file d'attente, je ne le soutiendrais pas. Si quoi que ce soit, une méthode bien nommée pour la file d'attente ou la mise à jour serait en ordre.

N'est-ce pas que la clé n'est pas physiquement copiée (si elle existe), mais le sélecteur reste juste une fonction qui est appelée lorsque les priorités doivent être évaluées ? Où est la redondance ?

Vous avez raison, j'ai mal lu la mise à jour de la proposition (le peu sur son stockage dans l'élément). En supposant alors que le sélecteur est appelé à la demande (ce qui devrait être précisé dans toute documentation, car cela pourrait avoir des implications sur les performances), il reste que le résultat de la fonction peut changer sans que la structure de données ne réponde, laissant le deux désynchronisés sauf si Update est appelé. Pire encore, si un utilisateur modifie la priorité effective de plusieurs éléments et ne met à jour qu'un seul d'entre eux, la structure des données finira par dépendre des modifications « non validées » lorsqu'elles sont sélectionnées parmi les éléments « non mis à jour » (je n'ai pas examiné l'implémentation de DataStructure proposée en détail, mais je pense que c'est nécessairement un problème pour toute mise O(<n) jour

Notez que tous mes reproches déclarés jusqu'à présent avec la proposition du sélecteur concernent la robustesse de l'API : je pense que le sélecteur le rend facile à utiliser de manière incorrecte. Cependant, mise à part la convivialité, conceptuellement, les éléments de la file d'attente ne devraient pas avoir à connaître leur priorité. Cela n'est potentiellement pas pertinent pour eux, et si l'utilisateur finit par envelopper ses éléments dans un struct Queable<T>{} ou quelque chose, cela semble être un échec dans la fourniture d'une API sans friction (autant que cela me fait mal d'utiliser le terme).

Vous pourriez bien sûr affirmer que sans le sélecteur, il incombe à l'utilisateur d'appeler le sélecteur, mais je pense que si les éléments connaissent leur priorité, ils seront (espérons-le) exposés proprement (c'est-à-dire une propriété), et s'ils ne le font pas t, alors il n'y a pas de sélecteur à se soucier (génération de priorités à la volée, etc.). Il y a beaucoup plus de plomberie dénuée de sens nécessaire pour prendre en charge un sélecteur si vous ne le voulez pas, puis pour transmettre les priorités si vous avez déjà un « sélecteur » bien défini. Le sélecteur incite l'utilisateur à exposer ces informations (peut-être inutilement), tandis que des priorités distinctes fournissent une interface extrêmement transparente qui, je ne peux pas imaginer, influencera les décisions de conception.

Le seul argument auquel je peux vraiment penser en faveur du sélecteur est que cela signifie que vous pouvez utiliser un PriorityQueue , et que d'autres parties du programme peuvent l'utiliser sans savoir comment les priorités sont calculées. Je vais devoir y réfléchir un peu plus longtemps, mais étant donné l'adéquation de la « niche », cela me semble être une petite récompense pour les frais généraux raisonnablement lourds dans des cas plus généraux.

_Edit: Après y avoir réfléchi un peu plus, il serait plutôt très agréable d'avoir des PriorityQueue autonomes sur lesquels vous pouvez simplement lancer des éléments, mais je maintiens que le coût de contourner cela serait énorme, tout en le coût d'introduction serait considérablement moindre._

J'ai fait un peu de travail en parcourant les bases de code open source .NET pour voir des exemples du monde réel de la façon dont les files d'attente prioritaires sont utilisées. Le problème délicat consiste à filtrer les projets des étudiants et le code source de la formation.

Utilisation 1 : Service de notification Roslyn
https://github.com/dotnet/roslyn/
Le compilateur Roslyn comprend une implémentation de file d'attente prioritaire privée appelée "PriorityQueue" qui semble avoir une optimisation très spécifique - une file d'attente d'objets qui réutilise les objets dans la file d'attente pour éviter qu'ils ne soient ramassés. La méthode Enqueue_NoLock effectue une évaluation sur current.Value.MinimumRunPointInMS < entry.Value.MinimumRunPointInMS pour déterminer où dans la file d'attente placer le nouveau nœud. L'une ou l'autre des deux conceptions principales de file d'attente prioritaire proposées ici (fonction de comparaison/délégué vs priorité explicite) conviendrait à ce scénario d'utilisation.

Utilisation 2 : Lucene.net
https://github.com/apache/lucenenet
C'est sans doute le plus grand exemple d'utilisation d'un PriorityQueue que j'ai pu trouver dans .NET. Apache Lucene.net est un portage .net complet de la célèbre bibliothèque de moteurs de recherche Lucene. Nous utilisons la version Java dans mon entreprise, et selon le site Web d'Apache, quelques grands noms utilisent la version .NET. Il existe un grand nombre de forks du projet .NET dans Github.

Lucene inclut sa propre implémentation PriorityQueue qui est sous-classée par un certain nombre de files d'attente prioritaires « spécialisées » : HitQueue, TopOrdAndFloatQueue, PhraseQueue et SuggestWordQueue. De plus, le projet instancie directement PriorityQueue à plusieurs endroits.

L'implémentation PriorityQueue de Lucene, liée à ci-dessus, est très similaire à l'API de file d'attente prioritaire d'origine publiée dans ce numéro. Il est défini comme "PriorityQueue" et accepte un IComparerparamètre dans son constructeur. Les méthodes et propriétés incluent Count, Clear, Offer (enqueue/push), Poll (dequeue/pop), Peek, Remove (supprime le premier élément correspondant trouvé dans la file d'attente), Add (synonyme de Offer). Fait intéressant, ils fournissent également un support d'énumération pour la file d'attente.

La priorité dans PriorityQueue de Lucene est déterminée par le comparateur passé au constructeur, et si aucun n'a été passé, il suppose que les objets comparés implémentent IComparableet utilise cette interface pour faire la comparaison. La conception originale de l'API publiée ici est similaire, sauf qu'elle fonctionne également avec les types de valeur.

Il existe un grand nombre d'exemples d'utilisation à travers leur base de code, SloppyPhraseScorer étant l'un d'entre eux.

Le constructeur de SloppyPhraseScorer instancie une nouvelle PhraseQueue (pq), qui est l'une de leurs propres sous-classes personnalisées de PriorityQueue. Une collection de PhrasePositions est générée, qui semble être un wrapper pour un ensemble de publications, une position et un ensemble de termes. La méthode FillQueue énumère les positions des phrases et les met en file d'attente. PharseFreq() appelle une fonction AdvancePP et, à un niveau élevé, semble sortir de la file d'attente, mettre à jour la priorité d'un élément, puis se remettre en file d'attente. La priorité est déterminée relativement (à l'aide d'un comparateur) plutôt qu'explicitement (la priorité n'est pas « transmise » en tant que deuxième paramètre lors de la mise en file d'attente).

Vous pouvez voir que sur la base de leur implémentation de PhraseQueue, une valeur de comparaison transmise via le constructeur (par exemple, un entier) peut ne pas la couper. Leur fonction de comparaison ("LessThan") évalue trois champs différents : PhrasePositions.doc, PhrasePositions.position et PhrasePositions.offset.

Utilisation 3 : Développement de jeux
Je n'ai pas fini de chercher des exemples d'utilisation dans cet espace, mais j'ai vu pas mal d'exemples de files d'attente prioritaires .NET personnalisées utilisées dans le développement de jeux. Dans un sens très général, ceux-ci avaient tendance à être regroupés autour de la recherche de chemin en tant que cas d'utilisation principal (celui de Dijkstra). Vous pouvez trouver de nombreuses personnes qui demandent comment implémenter des algorithmes de recherche de chemin dans .NET grâce à Unity 3D .

Encore faut-il creuser dans cette zone; vu quelques exemples avec une priorité explicite mis en file d'attente et quelques exemples utilisant Comparer/IComparable.

Sur une note distincte, il y a eu des discussions sur les éléments uniques, la suppression des éléments explicites et la détermination de l'existence d'un élément spécifique.

Les files d'attente, en tant que structure de données, prennent généralement en charge la mise en file d'attente et la suppression de la file d'attente. Si nous nous dirigeons vers la fourniture d'autres opérations de type ensemble/liste, je me demande si nous concevons réellement une structure de données complètement différente - quelque chose qui s'apparente à une liste triée de tuples. Si un appelant a un besoin autre que Enqueue, Dequeue, Peek, peut-être a-t-il besoin de quelque chose d'autre qu'une file d'attente prioritaire ? La file d'attente, par définition, implique l'insertion dans une file d'attente et le retrait ordonné de la file d'attente ; pas grand chose d'autre.

@ebickle

J'apprécie vos efforts pour parcourir d'autres référentiels et vérifier comment la fonctionnalité de file d'attente prioritaire y a été fournie. Cependant, l'OMI bénéficierait de cette discussion en fournissant des propositions de conception spécifiques . Ce fil est déjà difficile à suivre et raconter une longue histoire sans aucune conclusion le rend encore plus difficile.

Les files d'attente [...] prennent généralement en charge la mise en file d'attente et la suppression de la file d'attente. [...] Si un appelant a un besoin autre que Enqueue, Dequeue, Peek, peut-être a-t-il besoin de quelque chose d'autre qu'une file d'attente prioritaire ? La file d'attente, par définition, implique l'insertion dans une file d'attente et le retrait ordonné de la file d'attente ; pas grand chose d'autre.

  • Quelle est la conclusion? Proposition?
  • C'est très loin de la vérité. Même l'algorithme de Dijkstra que vous avez mentionné utilise la mise à jour des priorités des éléments. Et ce qui est nécessaire pour mettre à jour des éléments spécifiques est également nécessaire pour supprimer des éléments spécifiques.

Grand débat. La recherche de @ebicle est extrêmement utile IMO !
@ebicle avez-vous une conclusion sur [2] lucene.net - notre dernière proposition correspond-elle aux usages ou non ? (J'espère que je ne l'ai pas manqué dans votre description détaillée)

On dirait que nous avons besoin Try* variantes IsEmpty + TryPeek / TryDequeue + EnqueueOrUpdate ? Les pensées?
```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

Il semble que nous ayons besoin des variantes Try* ci-dessus

Oui, exactement.

Doit-on retourner le statut bool pour mis en file d'attente ou mis à jour ?

Si nous voulons rendre les statuts partout, alors partout. Pour cette méthode particulière, je pense que cela devrait être vrai si l'une des opérations a réussi ( Enqueue ou Update ).

Pour le reste -- je suis tout simplement d'accord :sourire:

Juste une question -- pourquoi ref au lieu de out ? Je ne suis pas sûr:

  • Nous n'avons pas besoin de l'initialiser avant d'entrer dans la fonction.
  • Il n'est pas utilisé pour la méthode (il s'éteint simplement).

Si nous voulons rendre les statuts partout, alors partout. Pour cette méthode particulière, je pense que cela devrait être vrai si l'une des opérations a réussi ( Enqueue ou Update ).

Cela reviendrait toujours vrai, ce n'est pas une bonne idée. Nous devrions utiliser void dans ce cas IMO. Sinon, les gens seront confus et vérifieront la valeur de retour et ajouteront du code inutile à ne jamais exécuter. (sauf si j'ai raté quelque chose)

ref vs out d'accord, j'en ai débattu moi-même. Je n'ai pas une opinion bien arrêtée / assez d'expérience pour prendre une décision moi-même. Nous pouvons demander aux examinateurs d'API / attendre plus de commentaires.

ça reviendrait toujours vrai

Vous avez raison, mon mauvais. Désolé.

Je n'ai pas une opinion bien arrêtée / assez d'expérience pour prendre une décision moi-même.

Peut-être qu'il me manque quelque chose, mais je trouve cela assez simple. Si nous utilisons ref , nous disons essentiellement que Peek et Dequeue veulent utiliser d'une manière ou d'une autre les TElement et TPriority transmis ( Je veux dire lire ces champs). Ce qui n'est pas vraiment le cas -- nos méthodes sont censées n'affecter des valeurs qu'à ces variables (et elles sont en fait requises par le compilateur).

Top-post mis à jour avec mes API Try*
Ajout de 2 questions ouvertes :

  • [6] Les lancers de Peek et Dequeue vraiment utiles ?
  • [7] TryPeek et TryDequeue - devriez-vous utiliser les ref ou out ?

ref vs out - Vous avez raison. J'optimisais pour éviter l'initialisation au cas où nous renverrions false. C'était stupide et aveuglé de ma part - une optimisation prématurée. Je vais le remplacer par et supprimer la question.

6 : Il se peut que je manque quelque chose, mais si je ne lance pas d'exception, que doit faire Peek ou Dequeue si la file d'attente est vide ? Je suppose que cela commence à poser la question de savoir si la structure de données doit accepter null (je préférerais pas, mais pas d'opinion ferme). Même si nous autorisons null , les types de valeur n'ont pas de null ( default ne compte certainement pas), donc Peek et Dequeue ont aucun moyen de transmettre un résultat dénué de sens, et dûment, je pense, doit lever une exception (éliminant ainsi toute préoccupation concernant les paramètres out !). Je ne vois aucune raison de ne pas suivre l'exemple du Queue.Dequeue existant

J'ajouterai simplement que Dijkstra (alias. Recherche heuristique sans heuristique) ne devrait nécessiter aucun changement de priorité. Je n'ai jamais souhaité mettre à jour la priorité de quoi que ce soit, et je lance des recherches heuristiques tout le temps. (le but de l'algorithme est qu'une fois que vous avez exploré un état, vous savez que vous avez exploré le meilleur chemin pour y accéder, sinon il risque d'être non optimal, vous ne pourrez donc jamais améliorer la priorité, et vous ne le ferez certainement pas voulez le diminuer (c'est-à-dire envisager un chemin plus mauvais pour y parvenir). _Ignorer les cas où vous choisissez intentionnellement l'heuristique de telle sorte qu'elle ne soit pas optimale, auquel cas vous pouvez bien sûr obtenir un résultat non optimal, mais vous ne mettre à jour une priorité_)

s'il ne lance pas d'exception, que doit faire Peek ou Dequeue si la file d'attente est vide ?

Vrai.

supprimant ainsi toute préoccupation concernant nos paramètres !

Comment? Eh bien, les paramètres out sont pour les méthodes TryPeek et TryDequeue . Ceux qui lèvent une exception sont Peek et Dequeue .

J'ajouterai simplement que Dijkstra ne devrait nécessiter aucun changement de priorité.

Je me trompe peut-être, mais à ma connaissance, l'algorithme de Dijkstra utilise l'opération DecreaseKey . Voir par exemple ceci . Que ce soit efficace ou non est un aspect différent. En fait, le tas de Fibonacci a été conçu de manière à réaliser l'opération DecreaseKey asymptotiquement en O(1) (pour améliorer Dijkstra).

Mais quand même, ce qui est important dans notre discussion -- pouvoir mettre à jour un élément dans une file d'attente prioritaire est très utile et il y a des gens qui recherchent une telle fonctionnalité (voir les questions précédemment liées sur StackOverflow). Je l'ai moi-même utilisé quelques fois aussi.

Désolé, oui, je vois le problème avec les paramètres out maintenant, je lis encore mal les choses. Et il semble qu'une variante de celle de Dijkstra (un nom qui semble être appliqué un peu plus largement que je ne le pensais...) priorités actualisables. C'est ce que j'obtiens en utilisant des mots et des noms longs. Notez que je ne propose pas de supprimer le Update , ce serait juste bien aussi d'avoir un Heap plus mince ou autre (c'est-à-dire la proposition originale) sans les restrictions que cette capacité impose (aussi glorieuse une capacité telle que je l'apprécie).

7 : Ceux-ci devraient être de out . Toute entrée ne pourrait jamais avoir de sens, donc ne devrait pas exister (c'est-à-dire que nous ne devrions pas utiliser ref ). Avec les paramètres out , nous n'allons pas améliorer le retour des valeurs default . C'est ce que fait Dictionary.TryGetValue , et je ne vois aucune raison de faire autrement. _Cela dit, vous pouvez traiter le ref comme valeur ou valeur par défaut, mais si vous n'avez pas de valeur par défaut significative, cela frustre les choses._

Discussion sur l'examen de l'API :

  • Nous aurons besoin d'avoir une réunion de conception appropriée (2h) pour discuter de tous les avantages / inconvénients, les 30 minutes que nous avons investies aujourd'hui n'ont pas suffi pour parvenir à un consensus / clôturer sur des questions ouvertes.

    • Nous inviterons probablement les membres de la communauté les plus investis - @pgolebiowski , quelqu'un d'autre ?

Voici les notes brutes clés :

  • Expérimenter - nous devrions faire des expériences (dans CoreFxLabs), le publier en tant que package NuGet de pré-version et demander aux consommateurs leurs commentaires (via un article de blog). Nous ne pensons pas pouvoir clouer l'API sans boucle de rétroaction.

    • Nous avons fait la même chose pour ImmutableCollections dans le passé (un cycle de publication de prévisualisation rapide était essentiel et utile pour façonner les API).

    • Nous pouvons regrouper cette expérience avec MultiValueDictionary qui est déjà dans CoreFxLab. A FAIRE : Vérifiez si nous avons plus de candidats, nous ne souhaitons pas publier de blog chacun d'eux séparément.

    • Le package expérimental NuGet sera nuked après la fin de l'expérience et nous déplacerons le code source dans CoreFX ou CoreFXExtensions (à décider ultérieurement).

  • Idée : ne retournez que TElement partir de Peek & Dequeue , ne retournez pas TPriority (les utilisateurs peuvent utiliser les méthodes Try* pour cela).
  • Stabilité (pour les mêmes priorités) - les éléments avec les mêmes priorités doivent être renvoyés dans l'ordre dans lequel ils ont été insérés dans la file d'attente (comportement général de la file d'attente)
  • Nous souhaitons activer les doublons (similaire à d'autres collections) :

    • Débarrassez-vous de Update - l'utilisateur peut Remove puis Enqueue article avec une priorité différente.

    • Remove ne devrait supprimer que le 1er élément trouvé (comme le fait List ).

  • Idée : Pouvons-nous abstrait IQueue interface pour abstrait Queue et PriorityQueue ( Peek et Dequeue retourner juste TElement aide ici)

    • Remarque : Cela peut s'avérer impossible, mais nous devrions l'explorer avant de nous installer sur l'API

Nous avons eu une longue discussion sur le sélecteur - nous ne sommes toujours pas décidés (peut être requis par IQueue ci-dessus ?). La majorité n'a pas aimé le sélecteur, mais les choses pourraient changer lors d'une future réunion d'examen de l'API.

Les autres questions ouvertes n'ont pas été abordées.

La dernière proposition me semble plutôt bonne. Mes avis/questions :

  1. Si Peek et Dequeue utilisaient des paramètres out pour priority , alors ils pourraient également avoir des surcharges qui ne renvoient pas du tout la priorité, ce qui, je pense, simplifierait usage courant. Bien que la priorité puisse également être ignorée en utilisant un écart, ce qui rend cela moins important.
  2. Je n'aime pas que la version du sélecteur se distingue par l'utilisation d'un constructeur différent et permette un ensemble de méthodes différent. Peut-être qu'il devrait y avoir un PriorityQueue<T> séparé ? Ou un ensemble de méthodes statiques et d'extension fonctionnant avec PriorityQueue<T, T> ?
  3. Est-il défini dans quel ordre les éléments sont-ils renvoyés lors de l'énumération ? Je suppose que ce n'est pas défini, pour le rendre efficace.
  4. Devrait-il y avoir un moyen d'énumérer uniquement les éléments dans la file d'attente prioritaire, en ignorant les priorités ? Ou est-ce que l'utilisation de quelque chose comme priorityQueue.Select(t => t.element) acceptable ?
  5. Si la file d'attente prioritaire utilise en interne un Dictionary sur le type d'élément, devrait-il y avoir une option pour passer un IEqualityComparer<TElement> ?
  6. Le suivi des éléments à l'aide d'un Dictionary est une surcharge inutile si je n'ai jamais besoin de mettre à jour les priorités. Devrait-il y avoir une option pour le désactiver? Bien que cela puisse être ajouté plus tard, s'il s'avère que c'est utile.

@karelz

Stabilité (pour les mêmes priorités) - les éléments avec les mêmes priorités doivent être renvoyés dans l'ordre dans lequel ils ont été insérés dans la file d'attente (comportement général de la file d'attente)

Je pense que cela signifierait qu'en interne, la priorité devrait être quelque chose comme une paire de (priority, version) , où version est incrémenté pour chaque ajout. Je pense que cela augmenterait considérablement l'utilisation de la mémoire de la file d'attente prioritaire, d'autant plus que le version devrait probablement être de 64 bits. Je ne suis pas sûr que cela en vaille la peine.

Nous ne voulons pas empêcher les valeurs en double (similaire à d'autres collections) :
Débarrassez-vous de Update - l'utilisateur peut Remove puis l'article Enqueue avec une priorité différente.

Selon l'implémentation, Update sera probablement beaucoup plus efficace que Remove suivi de Enqueue . Par exemple, avec un tas binaire (je pense que le tas quaternaire a les mêmes complexités temporelles), Update (avec des valeurs uniques et un dictionnaire) est O(log n ), tandis que Remove (avec des valeurs en double et pas de dictionnaire) est O( n ).

@pgolebiowski
Entièrement d'accord, nous devons rester concentrés sur les propositions ici ; J'ai fait la proposition d'API d'origine et le message d'origine. En janvier, @karelz a demandé des exemples d'utilisation spécifiques ; cela a ouvert une question beaucoup plus large concernant le besoin et l'utilisation spécifiques d'une API PriorityQueue. J'avais ma propre implémentation PriorityQueue et je l'ai utilisée dans quelques projets et j'ai senti que quelque chose de similaire serait utile dans la BCL.

Ce qui me manquait dans le message d'origine, c'était une vaste enquête sur l'utilisation des files d'attente existantes; des exemples du monde réel aideront à garder la conception à la terre et à garantir qu'elle peut être largement utilisée.

@karelz
Lucene.net contient au moins une fonction de comparaison de file d'attente prioritaire (similaire à Comparison) qui évalue plusieurs champs pour déterminer la priorité. Le modèle de comparaison "implicite" de Lucene ne correspondra pas bien au paramètre TPriority explicite de la proposition d'API actuelle. Une sorte de mappage serait nécessaire - combinant les multiples champs en un seul ou dans une structure de données « comparable » qui peut être transmise en tant que TPriority.

Proposition:
1) File d'attente prioritaireclasse basée sur ma proposition originale ci-dessus (répertoriée sous le titre Proposition originale). Ajoutez potentiellement les fonctions de commodité Mettre à jour (T), Supprimer (T) et Contient (T). Convient à la majorité des exemples d'utilisation existants de la communauté open source.
2) File d'attente prioritairevariante de dotnet/corefx#1. Dequeue() et Peek() renvoient TElement au lieu de tuple. Aucune fonction Try*, Remove() renvoie bool au lieu de lancer pour s'adapter à la listemodèle. Agit comme un « type pratique » afin que les développeurs qui ont une valeur de priorité explicite n'aient pas besoin de créer leur propre type comparable.

Les deux types prennent en charge les éléments en double. Besoin de déterminer si nous garantissons ou non FIFO pour les éléments de même priorité ; probablement pas si nous prenons en charge les mises à jour prioritaires.

Construisez les deux variantes dans les CoreFxLabs comme @karelz l'a suggéré et sollicitez des commentaires.

Des questions:

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)

Pourquoi pas de doublons ?

public (TElement element, TPriority priority) Peek(); // Throws if empty
public (TElement element, TPriority priority) Dequeue(); // Throws if empty
public void Remove(TElement element); // Throws if element does not exist

Si ces méthodes sont souhaitables, les avez-vous comme méthodes d'extension ?

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

Devrait jeter ; mais pourquoi pas de variante Try, aussi des méthodes d'extension ?

Énumérateur basé sur la structure ?

@benaadams jeter sur les dupes a été initialement suggéré (par moi) pour éviter le type de poignée moche. La dernière mise à jour de l'examen de l'API inverse cela.

Nous ne devons pas ajouter de méthodes en tant que méthodes d'extension lorsque nous pouvons les ajouter sur le type. Les méthodes d'extension sont de sauvegarde si nous ne pouvons pas les ajouter sur le type (par exemple, il s'agit d'une interface ou nous voulons le livrer plus rapidement à .NET Framework).

Dupes : Si vous avez plusieurs entrées identiques, vous n'avez besoin d'en mettre qu'une ? Alors deviennent-ils différents ?

Méthodes de lancer : fondamentalement, ce sont des emballages et le seront-ils ?

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

Bien que son flux de contrôle via des exceptions ?

@benaadams vérifie ma réponse avec les notes de révision de l'API : https://github.com/dotnet/corefx/issues/574#issuecomment-308206064

  • Nous souhaitons activer les doublons (similaire à d'autres collections) :

    • Débarrassez-vous de Update - l'utilisateur peut Remove puis Enqueue article avec une priorité différente.

    • Remove ne devrait supprimer que le 1er élément trouvé (comme le fait List ).

Bien que son flux de contrôle via des exceptions ?

Pas sûr de ce que vous voulez dire. Cela semble être un modèle assez courant en BCL.

Bien que son flux de contrôle via des exceptions ?

Pas sûr de ce que vous voulez dire.

Si vous n'avez que des méthodes TryX

  • si vous vous souciez du résultat ; tu le vérifies
  • si vous ne vous souciez pas du résultat, vous ne le vérifiez pas

Aucune implication d'exception ; mais le nom vous encourage à prendre la décision de jeter le retour.

Si vous avez des méthodes de levée d'exception non-Try

  • si vous vous souciez du résultat ; vous devez soit effectuer une pré-vérification, soit utiliser un try/catch pour détecter
  • si vous ne vous souciez pas du résultat ; vous devez soit vider la méthode, soit obtenir des exceptions inattendues

Vous êtes donc dans l'anti-modèle d' utilisation de la gestion des exceptions pour le contrôle de flux . Ils semblent juste un peu superflus; peut toujours simplement ajouter un lancer si faux pour recréer.

Cela semble être un modèle assez courant en BCL.

Les méthodes TryX n'étaient pas un modèle commun dans la BCL d'origine ; jusqu'à ce que les types Concurrent (bien que pensez que Dictionary.TryGetValue en 2.0 ait pu être le premier exemple ?)

Par exemple, les méthodes TryX viennent juste d'être ajoutées à Core for Queue and Stack et ne font pas encore partie de Framework.

Stabilité

  • La stabilité n'est pas gratuite. Cela s'accompagne d'un surcoût en termes de performances et de mémoire. Pour une utilisation ordinaire, la stabilité des files d'attente prioritaires n'est pas importante.
  • Nous exposons un IComparer . Si le client veut avoir de la stabilité, il peut facilement l'ajouter lui-même. Il serait facile de construire StablePriorityQueue en plus de notre implémentation — en utilisant l'approche ordinaire avec le stockage de "l'âge" d'un élément et son utilisation lors des comparaisons.

IQueue

Il y a alors des conflits d'API. Considérons maintenant un simple IQueue<T> pour qu'il fonctionne avec le Queue<T> existant :

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

Nous avons deux options pour la file d'attente prioritaire :

  1. Mettre en œuvre IQueue<TElement> .
  2. Implémentez IQueue<(TElement, TPriority)> .

J'écrirai les implications des deux chemins en utilisant simplement les nombres (1) et (2).

Mettre en file d'attente

Dans la file d'attente prioritaire, nous devons mettre en file d'attente à la fois un élément et sa priorité (solution actuelle). Dans le tas ordinaire, nous ajoutons un seul élément.

  1. Dans le premier cas, nous exposons Enqueue(TElement) , ce qui est assez étrange pour une file d'attente prioritaire si nous insérons un élément sans priorité. On est alors obligé de faire... quoi ? Supposons default(TPriority) ? Nan...
  2. Dans le second cas, nous exposons Enqueue((TElement, TPriority) element) . Nous ajoutons essentiellement deux arguments en acceptant un tuple créé à partir d'eux. API pas idéale probablement.

Aperçu et sortie de la file d'attente

Vous vouliez que ces méthodes renvoient uniquement TElement .

  1. Fonctionne avec ce que vous voulez atteindre.
  2. Ne fonctionne pas avec ce que vous voulez atteindre — renvoie (TElement, TPriority) .

Énumérer

  1. Le client ne peut pas écrire de requête LINQ qui utilise les priorités des éléments. C'est faux.
  2. Ça marche.

Nous pourrions atténuer certains des problèmes en fournissant PriorityQueue<T> , où il n'y a pas de priorité physique distincte.

  • L'utilisateur est essentiellement obligé d'écrire une classe wrapper pour que son code puisse utiliser notre classe. Cela signifie que dans de nombreux cas, ils voudront également implémenter IComparable . Ce qui ajoute beaucoup de code assez passe-partout (et un nouveau fichier dans leur code source, très probablement).
  • Nous pouvons évidemment en rediscuter, si vous le souhaitez. Je propose également une approche alternative ci-dessous.

Deux files d'attente prioritaires

Si nous livrons deux files d'attente prioritaires, la solution globale serait plus puissante et flexible. Il y a quelques notes à prendre :

  • Nous exposons deux classes pour fournir la même fonctionnalité, mais uniquement pour différents types de format d'entrée. Ne se sent pas mieux.
  • PriorityQueue<T> pourrait potentiellement implémenter IQueue<T> .
  • PriorityQueue<TElement, TPriority> exposerait une API étrange si elle tentait d'implémenter l'interface IQueue .

Eh bien… ça marcherait . Pas idéal cependant.

Mise à jour et suppression

Étant donné que nous voulons autoriser les doublons et que nous ne voulons pas utiliser le concept de poignée :

  • Il est impossible de fournir la fonctionnalité de mise à jour des priorités des éléments complètement correctement (mettre à jour le nœud spécifique uniquement). Analogiquement, elle s'applique à la suppression d'éléments.
  • De plus, les deux opérations doivent être effectuées en O(n), ce qui est assez triste, car il est possible de faire les deux en O(log n).

Cela conduit essentiellement à ce qu'il y a dans Java, où les utilisateurs doivent fournir leur propre implémentation de l'ensemble de la structure de données s'ils veulent pouvoir mettre à jour les priorités dans leurs files d'attente prioritaires sans parcourir naïvement l'ensemble de la collection à chaque fois.

Poignée alternative

En partant du principe que nous ne voulons pas ajouter un nouveau type de poignée, nous pouvons le faire différemment pour pouvoir bénéficier de la prise en charge complète de la mise à jour et de la suppression d'éléments (et le faire efficacement). La solution est d'ajouter des méthodes alternatives :

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

void Update(object handle, TPriority priority);

void Remove(object handle);

Ce seraient des méthodes pour ceux qui veulent avoir un contrôle approprié sur la mise à jour et la suppression d'éléments (et ne le font pas en O(n), comme s'il s'agissait d'un List :stuck_out_tongue_winking_eye :).

Mais mieux, considérons...

Approche alternative

Alternativement, nous pouvons supprimer complètement cette fonctionnalité de la file d'attente prioritaire et l'ajouter à la place dans le tas . Cela présente de nombreux avantages :

  • Les opérations les plus puissantes et les plus efficaces sont disponibles en utilisant un Heap .
  • Une API simple et une utilisation directe sont disponibles en utilisant un PriorityQueue - pour les personnes qui souhaitent que leur code fonctionne .
  • Nous ne rencontrons pas les problèmes de Java.
  • Nous pourrions rendre le PriorityQueue stable maintenant -- ce n'est plus un compromis.
  • La solution est en harmonie avec le sentiment que les personnes ayant une solide formation en informatique seraient au courant de l'existence de Heap . Ils seraient également conscients des limitations du PriorityQueue et pourraient donc utiliser le Heap place (par exemple s'ils veulent avoir plus de contrôle sur la mise à jour/supprimer des éléments ou ne veulent pas que les données structure stable au détriment de la vitesse et de l'utilisation de la mémoire).
  • La file d'attente prioritaire et le tas pourraient facilement autoriser les doublons sans compromettre leurs fonctionnalités (car leurs objectifs sont différents).
  • Il serait plus facile de créer une interface IQueue -- parce que la puissance et la fonctionnalité seraient jetées dans le champ de tas. L'API de PriorityQueue pourrait se concentrer sur la rendre compatible avec Queue via une abstraction.
  • Nous n'avons plus besoin de fournir l'interface IPriorityQueue (nous nous concentrons plutôt sur le maintien des fonctionnalités de PriorityQueue et Queue similaires). Au lieu de cela, nous pouvons l'ajouter dans la zone de tas - et y avoir essentiellement IHeap . C'est génial, car cela permet aux gens de créer des bibliothèques tierces en plus de ce qui se trouve dans la bibliothèque standard. Et cela semble juste - car encore une fois, nous considérons que les tas sont plus avancés que les files d'attente prioritaires , donc les extensions seraient fournies par cette zone. De telles extensions ne souffriraient pas non plus des choix que nous ferions dans PriorityQueue , car elles seraient séparées.
  • Nous n'avons plus besoin de considérer le constructeur IHeap pour le PriorityQueue .
  • La file d'attente prioritaire serait une classe utile à utiliser en interne dans CoreFX. Cependant, si nous ajoutons des fonctionnalités telles que la stabilité et supprimons d'autres fonctionnalités, nous aurons probablement besoin de quelque chose de plus puissant que cette solution. Heureusement, il y aurait le Heap le plus puissant et le plus performant à notre disposition !

Fondamentalement, la file d'attente prioritaire serait principalement axée sur la facilité d'utilisation, au détriment des performances, de la puissance et de la flexibilité. Le tas serait pour ceux qui sont conscients des performances et des implications fonctionnelles de nos décisions dans la file d'attente prioritaire. Nous atténuons de nombreux problèmes avec des compromis.

Si nous voulons faire une expérience, je pense que c'est possible maintenant. Demandons simplement à la communauté. Tout le monde ne lit pas ce fil -- nous pourrions générer d'autres commentaires et scénarios d'utilisation utiles. Qu'est-ce que tu penses? Personnellement, j'aimerais une telle solution. Je pense que ça ferait plaisir à tout le monde.

Remarque importante : si nous voulons une telle approche, nous devons concevoir une file d'attente prioritaire et un tas ensemble . Parce que leurs objectifs seraient différents et qu'une solution fournirait ce que l'autre ne fournirait pas.

Livrer IQueue ensuite

Avec l'approche présentée ci-dessus, pour que la file d'attente prioritaire implémente IQueue<T> (afin que cela ait du sens), et en supposant que nous supprimions le support du sélecteur là-bas, il faudrait qu'il ait un type générique. Bien que cela signifie que les utilisateurs doivent fournir un wrapper s'ils ont (user data, priority) séparément, une telle solution est toujours intuitive. Et surtout, il autorise tous les formats d'entrée (c'est pourquoi cela doit être fait de cette façon si nous laissons tomber le sélecteur). Sans le sélecteur, Enqueue(TElement, TPriority) n'autoriserait pas les types déjà comparables. Un seul type générique est également crucial pour l'énumération - afin que cette méthode puisse être incluse dans IQueue<T> .

Divers

@svick

Est-il défini dans quel ordre les éléments sont-ils renvoyés lors de l'énumération ? Je suppose que ce n'est pas défini, pour le rendre efficace.

Pour avoir l'ordre lors de l'énumération, nous devons essentiellement trier la collection. C'est pourquoi oui, il ne doit pas être défini, car le client peut simplement exécuter lui-même OrderBy et obtenir à peu près les mêmes performances (mais certaines personnes n'en ont pas besoin).

Idée : dans la file d'attente prioritaire, cela pourrait être ordonné, dans le tas non ordonné. Il se sent mieux. D'une manière ou d'une autre, une file d'attente prioritaire donne l'impression de l'itérer dans l'ordre. Un tas certainement pas. Un autre avantage de l'approche ci-dessus.

@pgolebiowski
Tout cela semble très sain. Pourriez-vous clarifier, dans la section Delivering IQueue then , suggérez-vous T : IComparable<T> pour le « type générique » comme alternative au sélecteur compte tenu de la tolérance des éléments en double ?

Je soutiendrais avoir les deux types distincts.

Je ne comprends pas la raison d'être de l'utilisation de object comme type de poignée : est-ce juste pour éviter de créer un nouveau type ? Définir un nouveau type fournirait une surcharge d'implémentation minimale, tout en rendant l'API plus difficile à utiliser (qu'est-ce qui m'empêche d'essayer de passer un string à Remove(object) ?), et plus facile à utiliser (qu'est-ce qui m'empêche d'essayer passer l'élément lui-même à Remove(object) , et qui pourrait me reprocher d'avoir essayé ?).

Je propose l'ajout d'un type factice convenablement nommé, pour remplacer object dans les méthodes handle, dans l'intérêt d'une interface plus expressive.

Si le débogage l'emporte sur la surcharge de mémoire, le type de handle pourrait même inclure des informations sur la file d'attente à laquelle il appartient (devrait devenir générique, ce qui renforcerait la sécurité de type de l'interface), fournissant des exceptions utiles du type ("Le handle fourni a été créé par une file d'attente différente"), ou s'il a déjà été consommé ("L'élément référencé par Handle a déjà été supprimé").

Si l'idée du handle va de l'avant, je proposerais que si cette information est jugée utile, alors un sous-ensemble de ces exceptions serait également lancé par les méthodes correspondantes TryRemove et TryUpdate , à l'exception de celle où le L'élément n'est plus présent, soit parce qu'il a été retiré de la file d'attente ou supprimé par le handle. Cela entraînerait un type de poignée moins ennuyeux, générique et nommé de manière appropriée.

@VisualMelon

Pourriez-vous clarifier, dans la section Delivering IQueue then , suggérez-vous T : IComparable<T> pour le « type générique » comme alternative au sélecteur compte tenu de la tolérance des éléments en double ?

Désolé de ne pas avoir été clair.

  • Je voulais dire livrer PriorityQueue<T> , sans aucune contrainte sur le T .
  • Il utiliserait toujours un IComparer<T> .
  • S'il arrive que le T soit déjà comparable, alors simplement Comparer<T>.Default serait supposé (et vous pouvez appeler le constructeur par défaut de la file d'attente prioritaire sans arguments).
  • Le sélecteur avait un objectif différent : être en mesure de consommer tous les types de données utilisateur. Il existe plusieurs configurations :

    1. Les données utilisateur sont distinctes de la priorité (deux instances physiques).
    2. Les données utilisateur contiennent la priorité.
    3. Les données utilisateur sont la priorité.
    4. Cas rare : la priorité peut être obtenue via une autre logique (réside dans un objet différent des données utilisateur).

    Le cas rare ne serait pas possible dans PriorityQueue<T> , mais cela n'a pas beaucoup d'importance. Ce qui est important, c'est que nous pouvons maintenant gérer (1), (2) et (3). Cependant, si nous avions deux types génériques, nous devrions avoir une méthode comme Enqueue(TElement, TPrioriity) . Cela nous limiterait à (1) seulement. (2) conduirait à des licenciements. (3) serait incroyablement laid. Il y a un peu plus à ce sujet dans la section IQueue > Enqueue ci-dessus (deuxième méthode Enqueue et default(TPriority ).

J'espère que c'est plus clair maintenant.

BTW, en supposant une telle solution, concevoir l'API de PriorityQueue<T> et IQueue<T> serait trivial. Prenez simplement certaines des méthodes de Queue<T> , jetez-les dans IQueue<T> et faites en sorte que PriorityQueue<T> implémente. Tadaa ! ??

Je ne comprends pas la raison d'être de l'utilisation de object comme type de poignée : est-ce juste pour éviter de créer un nouveau type ?

  • Oui, justement. Étant donné que nous ne voulons pas exposer un tel type, c'est la seule solution de continuer à utiliser le concept de poignée (et en tant que tel d'avoir plus de puissance et de vitesse). Je suis d'accord que ce n'est pas idéal cependant - c'était plutôt une clarification de ce qui devrait se passer si nous nous en tenons à l'approche actuelle et voulons avoir plus de puissance et d'efficacité (ce que je suis contre).
  • Étant donné que nous opterions pour l'approche alternative (file d'attente prioritaire et tas séparés), c'est plus facile. Nous pourrions laisser le PriorityQueue<T> résider dans System.Collections.Generic , tandis que la fonctionnalité de tas serait dans System.Collections.Specialized . Là, nous aurions plus de chances d'introduire un tel type, avec éventuellement la merveilleuse détection d'erreur à la compilation.
  • Mais encore une fois, il est très important de concevoir ensemble les fonctionnalités de file d'attente prioritaire et de tas si nous souhaitons avoir une telle approche. Parce qu'une solution fournit ce que l'autre n'apporte pas.

Si le débogage l'emporte sur la surcharge de mémoire, le type de handle pourrait même inclure des informations sur la file d'attente à laquelle il appartient (devrait devenir générique, ce qui renforcerait la sécurité de type de l'interface), fournissant des exceptions utiles du type ("Le handle fourni a été créé par une file d'attente différente"), ou s'il a déjà été consommé ("L'élément référencé par Handle a déjà été supprimé").

Si l'idée de la poignée avance, je proposerais que si cette information est jugée utile...

Certainement plus est possible alors : wink:. Surtout pour étendre tout cela par notre communauté dans des bibliothèques tierces.

@karelz @safern @ianhays @terrajobst @bendono @svick @alexey-dvortsov @SamuelEnglard @xied75 et d'autres -- pensez-vous qu'une telle approche aurait du sens (comme décrit dans cet article et cet article) ? Répondrait-il à toutes vos attentes et besoins ?

Je pense que l'idée de créer deux classes a beaucoup de sens et résout beaucoup de problèmes. Seulement si j'ai, c'est qu'il pourrait être logique pour PriorityQueue<T> d'utiliser le Heap<T> interne et de simplement "cacher" ses fonctionnalités avancées.

Donc en gros...

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

Remarques

  • En System.Collections.Generic .
  • Idée : nous pourrions également ajouter des méthodes pour supprimer des éléments ici ( Remove et TryRemove ). Ce n'est pas dans Queue<T> cependant. Mais ce n'est pas nécessaire.

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

Remarques

  • En System.Collections.Generic .
  • Si le IComparer<T> n'est pas livré, Comparer<T>.Default est invoqué.
  • C'est stable.
  • Autorise les doublons.
  • Remove et TryRemove suppriment uniquement la première occurrence (s'ils la trouvent).
  • L'énumération est ordonnée.

des tas

Je n'écris pas tout ici maintenant, mais :

  • En System.Collections.Specialized .
  • Il ne serait pas stable (et donc plus rapide et plus efficace en termes de mémoire).
  • Prise en charge de la prise en charge, mise à jour et suppression appropriées

    • fait rapidement en O(log n) au lieu de O(n)

    • fait correctement

  • L'énumération n'est pas ordonnée (plus rapide).
  • Autorise les doublons.

D'accord avec IQueueproposition. J'allais présenter la même chose aujourd'hui, cela semble être le bon niveau d'abstraction à avoir au niveau de l'interface. "Une interface vers une structure de données qui peut avoir des éléments ajoutés et des éléments commandés supprimés."

  • La spécification pour IQueueCela semble bon.

  • Envisagez d'ajouter "int Count { get; }" à IQueueafin qu'il soit clair que Count est souhaité, que nous héritions ou non de IReadOnlyCollection.

  • Sur la clôture concernant TryPeek, TryDequeue dans IQueueétant donné qu'ils ne sont pas dans la file d'attente, mais ces aides devraient probablement être également ajoutées à la file d'attente et à la pile.

  • IsEmpty semble être une valeur aberrante ; peu d'autres types de collection dans la BCL l'ont. Pour l'ajouter à l'interface, nous devons supposer qu'il va être ajouté à la file d'attente, et cela semble un peu étrange de l'ajouter à la file d'attenteet rien d'autre. Nous recommandons de le supprimer de l'interface, et peut-être aussi de la classe.

  • Déposez TryRemove et remplacez Remove par "bool Remove". Rester en alignement avec les autres classes de collection sera important ici - les développeurs auront beaucoup de mémoire musculaire qui dit "remove() dans une collection ne jette pas". C'est un domaine que de nombreux développeurs ne testeront pas bien et qui causera beaucoup de surprises si le comportement normal est modifié.

D'après votre citation précédente @pgolebiowski

  1. Les données utilisateur sont distinctes de la priorité (deux instances physiques).
  2. Les données utilisateur contiennent la priorité.
  3. Les données utilisateur sont prioritaires.
  4. Cas rare : la priorité peut être obtenue via une autre logique (réside dans un objet différent des données utilisateur).

Recommande également de considérer 5. Les données utilisateur contiennent la priorité dans plusieurs champs (comme nous l'avons vu sur Lucene.net)

Sur la clôture concernant TryPeek, TryDequeue dans IQueue étant donné qu'ils ne sont pas dans la file d'attente

Ce sont System/Collections/Generic/Queue.cs#L253-L295

D'un autre côté, la file d'attente n'a pas
c# public void Remove(T element); public bool TryRemove(T element);

Envisagez d'ajouter "int Count { get; }" à IQueue afin qu'il soit clair que Count est souhaité, que nous héritions ou non de IReadOnlyCollection.

D'ACCORD. Le modifiera.

IsEmpty semble être une valeur aberrante ; peu d'autres types de collection dans la BCL l'ont.

Celui-ci a été ajouté par @karelz , je viens de le copier. Je l'aime bien, pourrait être pris en compte dans l'examen de l'API :)

Déposez TryRemove et remplacez Remove par "bool Remove".

Je pense que Remove et TryRemove est compatible avec d'autres méthodes comme celle-ci ( Peek et TryPeek ou Dequeue et TryDequeue ).

  1. Les données utilisateur contiennent la priorité dans plusieurs champs

C'est un point valable, mais cela peut également être géré avec un sélecteur (c'est n'importe quelle fonction après tout) - mais c'est pour les tas.

IsEmpty semble être une valeur aberrante ; peu d'autres types de collection dans la BCL l'ont.

FWIW, bool IsEmpty { get; } est quelque chose que j'aurais aimé ajouter à IProducerConsumerCollection<T> , et j'ai regretté qu'il ne soit pas là à plusieurs reprises depuis. Sans cela, les wrappers ont souvent besoin de faire l'équivalent de Count == 0 , ce qui pour certaines collections est nettement moins efficace à implémenter, en particulier sur la plupart des collections concurrentes.

@pgolebiowski Que

@svick

Si Peek et Dequeue utilisaient des paramètres out pour la priorité, alors ils pourraient également avoir des surcharges qui ne renvoient pas du tout la priorité, ce qui, je pense, simplifierait l'utilisation courante

D'accord. Ajoutons-les.

Devrait-il y avoir un moyen d'énumérer uniquement les éléments dans la file d'attente prioritaire, en ignorant les priorités ?

Bonne question. Que proposeriez-vous ? IEnumerable<TElement> Elements { get; } ?

Stabilité - Je pense que cela signifierait qu'en interne, la priorité devrait être quelque chose comme une paire de (priority, version)

Je pense que nous pouvons éviter cela en considérant Update comme logique Remove + Enqueue . Nous ajouterions toujours des éléments à la fin des éléments de même priorité (considérons essentiellement le résultat 0 du comparateur comme -1). OMI qui devrait fonctionner.


@benaadams

Les méthodes TryX n'étaient pas un modèle commun dans la BCL d'origine ; jusqu'à ce que les types Concurrent (bien que pensez que Dictionary.TryGetValue en 2.0 ait pu être le premier exemple ?)
Par exemple, les méthodes TryX viennent juste d'être ajoutées à Core for Queue and Stack et ne font pas encore partie de Framework.

J'avoue que je suis encore novice en BCL. D'après les réunions d'examen de l'API et du fait que nous avons récemment ajouté un tas de méthodes Try* , j'avais l'impression que c'était un modèle courant pendant beaucoup plus longtemps 😉.
Quoi qu'il en soit, c'est un modèle courant maintenant et nous ne devrions pas avoir peur de l'utiliser. Le fait que le modèle ne soit pas encore dans .NET Framework ne devrait pas nous empêcher d'innover dans .NET Core - c'est son objectif principal, innover plus rapidement.


@pgolebiowski

Alternativement, nous pouvons supprimer complètement cette fonctionnalité de la file d'attente prioritaire et l'ajouter à la place dans le tas. Cela a de nombreux avantages

Hmm, quelque chose me dit que vous pourriez avoir un agenda ici 😆
Maintenant sérieusement, c'est en fait une bonne direction que nous visions tout le temps - PriorityQueue n'a jamais été censé être une raison pour NE PAS faire Heap . Si nous sommes tous d'accord avec le fait que Heap pourrait ne pas être intégré à CoreFX et rester "juste" dans le référentiel CoreFXExtensions en tant que structure de données avancée aux côtés de PowerCollections, cela me convient,

Remarque importante : si nous voulons une telle approche, nous devons concevoir une file d'attente prioritaire et un tas ensemble. Parce que leurs objectifs seraient différents et qu'une solution fournirait ce que l'autre ne fournirait pas.

Je ne vois pas pourquoi nous devons le faire ensemble. IMO, nous pouvons nous concentrer sur PriorityQueue et ajouter "correct" Heap en parallèle/plus tard. Cela ne me dérange pas si quelqu'un les fait ensemble, mais je ne vois aucune raison sérieuse pour laquelle l'existence de PriorityQueue faciles à utiliser devrait affecter la conception de "proper/high-perf" avancé Heap famille.

IQueue

Merci pour le compte rendu. Compte tenu de vos points, je ne pense pas que nous devrions faire tout notre possible pour ajouter IQueue . OMI, c'est agréable à avoir. Si nous avions un sélecteur, ce serait naturel. Cependant, je n'aime pas l'approche du sélecteur car elle apporte de l'étrangeté et de la complication dans la description du moment où le sélecteur est appelé par le PriorityQueue (uniquement sur Enqueue et Update .

Autre poignée (d'objet)

Ce n'est en fait pas une mauvaise idée (bien qu'un peu moche) d'avoir de telles surcharges IMO. Nous devrions être capables de détecter que le descripteur provient du mauvais PriorityQueue , qui est O(log n).
J'ai le sentiment que les examinateurs de l'API le rejetteront, mais IMO, cela vaut la peine d'essayer de l'expérimenter ...

Stabilité

Je ne pense pas que la stabilité vienne avec une surcharge de performances/mémoire (en supposant que nous ayons déjà abandonné Update ou que nous traitions Update comme logique Remove + Enqueue , donc nous réinitialisons essentiellement l'âge de l'élément). Traitez simplement le résultat 0 du comparateur comme -1 et tout va bien... Ou est-ce que j'ai raté quelque chose ?

Sélecteur et IQueue<T>

Ce peut être une bonne idée d'avoir 2 propositions (et nous pouvons potentiellement prendre les deux) :

  • PriorityQueue<T,U> sans sélecteur et sans IQueue (ce qui serait encombrant)
  • PriorityQueue<T> avec sélecteur et IQueue

Un certain nombre de personnes l'ont laissé entendre ci-dessus.

Re : Dernière proposition de @pgolebiowski - https://github.com/dotnet/corefx/issues/574#issuecomment -308427321

IQueue<T> - nous pourrions ajouter Remove et TryRemove , mais Queue<T> ne les a pas.

Auraient-ils du sens à être ajoutés à Queue<T> ? ( @ebicle est d' accord) Si oui, nous devrions également regrouper l'ajout.
Ajoutons-le dans l'interface et disons qu'ils sont discutables / nécessitent également Queue<T> ajout de
Idem pour IsEmpty -- tout ce qui a besoin d'ajouts de Queue<T> et Stack<T> , marquons-le ainsi dans l'interface (ce sera plus facile à revoir et à digérer).
@pgolebiowski pouvez-vous s'il vous plaît ajouter un commentaire à IQueue<T> avec la liste des classes par lesquelles nous pensons qu'il sera implémenté ?

PriorityQueue<T>

Ajoutons l'énumérateur de structure (je pense que c'est un modèle BCL commun ces derniers temps ). Il a été appelé plusieurs fois sur le fil, puis abandonné/oublié.

des tas

Choix de l'espace de noms : ne perdons pas de temps sur la décision d'espace de noms pour le moment. Si Heap se retrouve dans CoreFxExtensions, nous ne savons pas encore quel type d'espaces de noms nous autoriserons là-bas. Peut-être Community.* ou quelque chose comme ça. Cela dépend du résultat des discussions sur le but/le mode de fonctionnement de CoreFxExtensions.
Remarque : une idée pour le référentiel CoreFxExtensions est de donner aux membres de la communauté éprouvés des autorisations d'écriture, et de le laisser être conduit principalement par la communauté avec l'équipe .NET fournissant uniquement des conseils (y compris l'expertise de révision des API) et l'équipe .NET/MS étant l'arbitre en cas de besoin. Si c'est là que nous atterrissons, nous ne le voudrions probablement pas dans l'espace Microsoft.* noms System.* ou Microsoft.* . (Avertissement : réflexion précoce, s'il vous plaît ne sautez pas aux conclusions pour le moment, il y a d'autres idées de gouvernance alternatives en vol)

Supprimez TryRemove et remplacez Remove par bool Remove . Rester en alignement avec les autres classes de collection sera important ici - les développeurs auront beaucoup de mémoire musculaire qui dit "remove() dans une collection ne jette pas". C'est un domaine que de nombreux développeurs ne testeront pas bien et qui causera beaucoup de surprises si le comportement normal est modifié.

👍 Nous devrions certainement au moins envisager de l'aligner avec d'autres collections. Quelqu'un peut-il scanner d'autres collections quel est leur modèle Remove ?

@ebicle Que

Hébergons-le directement dans

@karelz

Devrait-il y avoir un moyen d'énumérer uniquement les éléments dans la file d'attente prioritaire, en ignorant les priorités ?

Bonne question. Que proposeriez-vous ? IEnumerable<TElement> Elements { get; } ?

Oui, cela ou une propriété renvoyant une sorte de ElementsCollection est probablement la meilleure option. D'autant plus que c'est similaire à Dictionary<K, V>.Values .

Nous ajouterions toujours des éléments à la fin des éléments de même priorité (considérons essentiellement le résultat 0 du comparateur comme -1). OMI qui devrait fonctionner.

Ce n'est pas ainsi que fonctionnent les tas, il n'y a pas de "fin des éléments de même priorité". Les éléments avec la même priorité peuvent être répartis sur tout le tas (à moins qu'ils ne soient proches du minimum actuel).

Ou, en d'autres termes, pour maintenir la stabilité, vous devez considérer le résultat du comparateur de 0 comme étant -1 non seulement lors de l'insertion, mais également lorsque les éléments sont déplacés dans le tas par la suite. Et je pense que vous devez stocker quelque chose comme version avec les priority pour le faire correctement.

IQueue- nous pourrions ajouter Remove et TryRemove, mais File d'attentene les a pas.

Auraient-ils du sens à être ajoutés à la file d'attente? ( @ebicle est d' accord) Si oui, nous devrions également regrouper l'ajout.

Je ne pense pas que Remove devrait être ajouté à IQueue<T> ; et je dirais même que Contains est louche ; cela limite l'utilité de l'interface et les types de file d'attente pour lesquels elle peut être utilisée, à moins que vous ne commenciez également à lancer des NotSupportedExceptions. c'est-à-dire quelle est la portée?

Est-ce uniquement pour les files d'attente vanille, les files d'attente de messages, les files d'attente distribuées, les files d'attente ServiceBus, les files d'attente de stockage Azure, les files d'attente fiables ServiceFabric, etc.

Ce n'est pas ainsi que fonctionnent les tas, il n'y a pas de "fin des éléments de même priorité". Les éléments avec la même priorité peuvent être répartis sur tout le tas (à moins qu'ils ne soient proches du minimum actuel).

Point juste. J'y pensais comme un arbre de recherche binaire. Je dois brosser mes bases de DS je suppose :)
Eh bien, soit nous l'implémentons avec différents ds (tableau, arbre de recherche binaire (ou quel que soit le nom officiel) - @stephentoub a des idées/suggestions), soit nous nous contentons d'un ordre aléatoire. Je ne pense pas que le maintien du champ version ou age vaut la garantie de stabilité.

@ebicle Que

@karelz Hébergons -le directement dans CoreFxLabs.

Veuillez consulter ce document .

Ce peut être une bonne idée d'avoir 2 propositions (et nous pouvons potentiellement prendre les deux) :

  • File d'attente de prioritésans sélecteur et sans IQueue (ce qui serait encombrant)
  • File d'attente de prioritéavec sélecteur et IQueue

Je me suis complètement débarrassé du sélecteur. Cela n'est nécessaire que si nous voulons avoir un PriorityQueue<T, U> unifié. Si nous avons deux classes -- eh bien, un comparateur suffit.


S'il vous plaît laissez-moi savoir si vous trouvez quelque chose qui mérite d'être ajouté / modifié / supprimé.

@pgolebiowski

Le document a l'air bien. Je commence à me sentir comme une solution solide que je m'attendrais à voir dans les bibliothèques principales .NET :)

  • Devrait ajouter une classe Enumerator imbriquée. Je ne sais pas si la situation a changé, mais il y a des années, le fait d'avoir la structure enfant a évité une allocation de ramasse-miettes causée par la mise en boîte du résultat de GetEnumerator(). Voir https://github.com/dotnet/coreclr/issues/1579 par exemple.
    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(); } }

  • File d'attente de prioritédevrait également avoir une structure Enumerator imbriquée.

  • Je vote toujours pour avoir "public bool Remove (T element)" sans TryRemove car c'est un modèle de longue date et le changer rend les erreurs de développeur très probables. Nous pouvons laisser l'équipe d'examen de l'API intervenir, mais c'est une question ouverte dans mon esprit.

  • Y a-t-il une valeur à spécifier la capacité initiale dans le constructeur ou à avoir une fonction TrimExcess, ou est-ce une micro-optimisation en ce moment - en particulier compte tenu de l'IEnumerableparamètres constructeur ?

@pgolebiowski merci de l'avoir mis dans un document. Pouvez-vous s'il vous plaît soumettre votre doc en tant que PR contre la branche principale de CoreFxLab ? Taguez @ianhays @safern, ils pourront fusionner les PR.
Nous pouvons ensuite utiliser les problèmes CoreFxLab pour d'autres discussions sur des points de conception particuliers - cela devrait être plus facile que ce méga-problème (je viens de lancer l'e-mail pour créer la zone PriorityQueue là-bas).

Veuillez mettre un lien vers la demande d'extraction CoreFxLab ici ?

  • @ebicle @jnm2 -- ajouté
  • @karelz @SamuelEnglard -- référencé

Si l'API est approuvée dans sa forme actuelle (deux classes), je créerais un autre PR à CoreFXLab avec une implémentation pour les deux en utilisant un tas quaternaire ( voir implémentation ). Le PriorityQueue<T> n'utiliserait qu'un seul tableau en dessous, tandis que PriorityQueue<TElement, TPriority> utiliserait deux -- et réorganiserait leurs éléments ensemble. Cela pourrait nous épargner des allocations supplémentaires et être aussi efficace que possible. J'ajouterai qu'une fois qu'il y aura le feu vert.

Existe-t-il un plan pour créer une version thread-safe comme ConcurrentQueue ?

Je voudrais :heart: voir une version concurrente de ceci, implémentant IProducerConsumerCollection<T> , afin qu'il puisse être utilisé avec BlockingCollection<T> etc.

@aobatact @khellang Cela ressemble à un fil complètement différent :

Je suis d'accord que c'est très précieux cependant :wink:!

public bool IsEmpty();

Pourquoi est-ce une méthode ? Toutes les autres collections du framework qui ont IsEmpty ont comme propriété.

J'ai mis à jour la proposition dans le top-post avec IsEmpty { get; } .
J'ai également ajouté un lien vers le dernier document de proposition dans le référentiel corefxlab.

Salut tout le monde,
Je pense qu'il y a beaucoup de voix qui soutiennent qu'il serait bon de prendre en charge la mise à jour si possible. Je pense que personne n'a fini par douter qu'il s'agisse d'une fonctionnalité utile pour les recherches de graphiques.

Mais des objections ont été soulevées ici et là. Je peux donc vérifier ma compréhension, il semble que les principales objections sont :
-il n'est pas clair comment la mise à jour doit fonctionner lorsqu'il y a des éléments en double.
-il y a un argument pour savoir s'il est même souhaitable de prendre en charge les éléments en double dans une file d'attente prioritaire
-il y a une certaine inquiétude qu'il pourrait être inefficace d'avoir une structure de données de recherche supplémentaire pour trouver des éléments dans la structure de données de sauvegarde juste pour mettre à jour leur priorité. Surtout pour les scénarios où la mise à jour n'est jamais effectuée ! Et/ou affecter les pires garanties de performance...

Quelque chose que j'ai raté ?

OK, je suppose qu'un autre était - il a été affirmé que la mise à jour/suppression qui signifie sémantiquement « mettre à jour d'abord » ou « supprimer d'abord » pourrait être étrange. :)

Dans quelle version de .Net Core pouvons-nous commencer à utiliser PriorityQueue ?

@memoryfraction .NET Core lui-même n'a pas encore de PriorityQueue (il y a des propositions - mais nous n'avons pas eu le temps d'y consacrer récemment). Cependant, il n'y a aucune raison pour qu'il soit dans la distribution Microsoft .NET Core. Tout membre de la communauté peut en mettre un sur NuGet. Peut-être que quelqu'un sur cette question peut en suggérer un.

@memoryfraction .NET Core lui-même n'a pas encore de PriorityQueue (il y a des propositions - mais nous n'avons pas eu le temps d'y consacrer récemment). Cependant, il n'y a aucune raison pour qu'il soit dans la distribution Microsoft .NET Core. Tout membre de la communauté peut en mettre un sur NuGet. Peut-être que quelqu'un sur cette question peut en suggérer un.

Merci pour votre réponse et suggestion.
Lorsque j'utilise C# pour m'entraîner au leetcode et que j'ai besoin d'utiliser SortedSet pour gérer l'ensemble avec des éléments en double. Je dois envelopper l'élément avec un ID unique pour résoudre le problème.
Donc, je préfère avoir la file d'attente prioritaire dans .NET Core à l'avenir car ce sera plus pratique.

Quel est l'état actuel de ce problème ? Je viens de me retrouver récemment nécessitant un PriorityQueue

Cette proposition n'active pas l'initialisation de la collection par défaut. PriorityQueue<T> n'a pas Add méthode

Si quelqu'un a un tas binaire rapide qu'il souhaite partager, j'aimerais le comparer avec ce que j'ai écrit.
J'ai entièrement implémenté l'API comme proposé. Cependant, plutôt qu'un tas, il utilise un simple tableau trié. Je garde l'élément de valeur la plus faible à la fin afin qu'il soit trié dans l'ordre inverse de l'ordre habituel. Lorsque le nombre d'éléments est inférieur à 32, j'utilise une simple recherche linéaire pour trouver où insérer de nouvelles valeurs. Après cela, j'ai utilisé une recherche binaire. En utilisant des données aléatoires, j'ai trouvé que cette approche était plus rapide qu'un tas binaire dans le cas simple de remplir la file d'attente puis de la vider.
Si j'ai le temps, je le mettrai dans un dépôt git public afin que les gens puissent le critiquer et mettre à jour ce commentaire avec l'emplacement.

@SunnyWar que j'utilise actuellement : https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
qui a de bonnes performances. Je pense qu'il serait logique de tester votre implémentation par rapport à GenericPriorityQueue là-bas

Merci de ne pas tenir compte de mon message précédent. J'ai trouvé une erreur dans le test. Une fois corrigé, le tas binaire fonctionne le mieux.

@karelz Où en est cette proposition actuellement sur ICollection<T> ? Je n'ai pas compris pourquoi cela n'allait pas être pris en charge, même si la collection n'est évidemment pas destinée à être en lecture seule ?

@karelz concernant la question ouverte de l'ordre de mise en

@karelz concernant l'ordre d'énumération... que diriez-vous d'avoir une méthode Sort() sur le type qui mettra le tas de données internes de la collection dans l'ordre trié sur place (en O (n log n)). Et appeler Enumerate() après Sort() énumère la collection en O(n). Et si Sort() n'est PAS appelé, il renvoie les éléments dans un ordre non trié en O(n) ?

Triage : passer à l'avenir car il ne semble pas y avoir de consensus sur la question de savoir si nous devons mettre cela en œuvre.

C'est très décevant à entendre. Je dois encore utiliser des solutions tierces pour cela.

Quelles sont les principales raisons pour lesquelles vous n'aimez pas utiliser une solution tierce ?

la structure de données de tas est un MUST pour faire leetcode
plus de leetcode, plus d'interview de code c#, ce qui signifie plus de développeurs c#.
plus de développeurs signifie un meilleur écosystème.
un meilleur écosystème signifie que si nous pouvons encore programmer en c# demain.

en somme : ce n'est pas seulement une caractéristique, mais aussi l'avenir. c'est pourquoi ce problème est étiqueté « futur ».

Quelles sont les principales raisons pour lesquelles vous n'aimez pas utiliser une solution tierce ?

Parce que quand il n'y a pas de standard, chacun invente le sien, chacun avec son lot de bizarreries.

Il y a eu de nombreuses fois où j'ai souhaité un System.Collections.Generic.PriorityQueue<T> parce que c'est pertinent pour quelque chose sur lequel je travaille. Cette proposition existe depuis 5 ans maintenant. Pourquoi n'est-il toujours pas arrivé ?

Pourquoi n'est-il toujours pas arrivé ?

Famine prioritaire, sans jeu de mots. Concevoir des collections est un travail car si nous les mettons dans la BCL, nous devons réfléchir à la façon dont elles échangent, aux interfaces qu'elles implémentent, à la sémantique, etc. Rien de tout cela n'est sorcier, mais cela prend du temps. De plus, vous avez besoin d'un ensemble de cas d'utilisateurs et de clients concrets pour juger le design, ce qui devient de plus en plus difficile à mesure que les collections deviennent de plus en plus spécialisées. Jusqu'à présent, il y a toujours eu d'autres travaux qui ont été considérés comme plus importants que cela.

Prenons un exemple récent : les collections immuables. Ils ont été conçus par quelqu'un ne travaillant pas dans l'équipe BCL pour un cas d'utilisation dans VS qui tournait autour de l'immuabilité. Nous avons travaillé avec lui pour obtenir les API « BCL-ified ». Et lorsque Roslyn a été mis en ligne, nous avons remplacé leurs copies par les nôtres dans autant d'endroits que possible et nous avons beaucoup peaufiné la conception (et la mise en œuvre) en fonction de leurs commentaires. Sans un scénario de "héros", c'est difficile.

Parce que quand il n'y a pas de standard, chacun invente le sien, chacun avec son lot de bizarreries.

@masonwheeler est-ce ce que vous avez vu pour PriorityQueue<T> ? Qu'il existe plusieurs options tierces qui ne sont pas interchangeables et qu'il n'y a pas de « meilleure bibliothèque pour la plupart » clairement acceptée ? (je n'ai pas fait de recherche donc je ne connais pas la réponse)

@eiriktsarpalis Comment n'y a-t-il pas de consensus sur la mise en œuvre ?

@terrajobst Avez-vous vraiment besoin d'un scénario de héros alors qu'il s'agit d'un modèle bien connu avec des applications bien connues ? Très peu d'innovation devrait être nécessaire ici. Il existe même déjà une spécification d'interface assez complète. À mon avis, la vraie raison pour laquelle cet utilitaire n'existe pas n'est pas qu'il est trop difficile, ni qu'il n'y a pas de demande ni de cas d'utilisation pour cela. La vraie raison est que pour tout projet logiciel réel, il est plus facile d'en créer un vous-même que d'essayer de mener une campagne politique pour qu'il soit mis dans les bibliothèques du framework.

Quelles sont les principales raisons pour lesquelles vous n'aimez pas utiliser une solution tierce ?

@terrajobst
J'ai personnellement fait l'expérience que les solutions tierces ne fonctionnent pas toujours comme prévu/n'utilisent pas les fonctionnalités linguistiques actuelles. Avec une version standardisée, je (en tant qu'utilisateur) peut être tout à fait certain que les performances sont les meilleures que vous puissiez obtenir.

@danmosemsft

@masonwheeler est-ce ce que vous avez vu pour PriorityQueue<T> ? Qu'il existe plusieurs options tierces qui ne sont pas interchangeables et qu'il n'y a pas de « meilleure bibliothèque pour la plupart » clairement acceptée ? (je n'ai pas fait de recherche donc je ne connais pas la réponse)

Oui. Juste google "file d'attente prioritaire C#" ; la première page est pleine de :

  1. Implémentations prioritaires de file d'attente sur Github et d'autres sites d'hébergement
  2. Les gens demandent pourquoi dans le monde il n'y a pas de file d'attente prioritaire officielle dans Collections.Generic
  3. Tutoriels sur la façon de créer votre propre implémentation de file d'attente prioritaire

@terrajobst Avez-vous vraiment besoin d'un scénario de héros alors qu'il s'agit d'un modèle bien connu avec des applications bien connues ? Très peu d'innovation devrait être nécessaire ici.

D'après mon expérience oui. Le diable est dans les détails et une fois que nous avons expédié une API, nous ne pouvons pas vraiment faire de changements de rupture. Et il existe de nombreux choix de mise en œuvre qui sont visibles par l'utilisateur. Nous pouvons prévisualiser l'API en tant qu'OOB pendant un certain temps, mais nous avons également appris que même si nous pouvons certainement recueillir des commentaires, l'absence d'un scénario de héros signifie que vous n'avez pas de bris d'égalité, ce qui entraîne souvent une situation où lorsque le héros scénario arrive, la structure de données ne répond pas à ses exigences.

@masonwheeler J'ai supposé que votre lien serait vers ceci 😄 Maintenant, c'est dans ma tête.

Bien que, comme le dit @terrajobst , notre principal problème ici ait été les ressources/l'attention (et vous pouvez nous en vouloir si vous le souhaitez), nous aimerions également renforcer l'écosystème non-Microsoft afin qu'il soit plus probable que vous puissiez trouver des bibliothèques solides pour quoi que ce soit. scénario.

[édité pour plus de clarté]

@danmosemsft Nah, si je devais choisir cette chanson, je ferais la version Shrek 2.

Candidate n°1 de l'application Hero : que diriez-vous d'en utiliser une dans TimerQueue.Portable ?

Déjà été considéré, prototypé et mis au rebut. Cela rend le cas très courant d'un timer rapidement créé et détruit (par exemple pour un timeout) moins efficace.

@stephentoub Je suppose que vous voulez dire que c'est moins efficace pour certains scénarios où il y a un petit nombre de minuteries. Mais comment évolue-t-il ?

Je suppose que vous voulez dire que c'est moins efficace pour certains scénarios où il y a un petit nombre de minuteries. Mais comment évolue-t-il ?

Non, je voulais dire que le cas courant est que vous avez beaucoup de minuteries à tout moment, mais très peu se déclenchent. C'est ce qui se produit lorsque les minuteries sont utilisées pour les délais d'attente. Et ce qui est important, c'est la vitesse à laquelle vous pouvez ajouter et supprimer de la structure de données... vous voulez que ce soit 0 (1) et avec une très faible surcharge. Si cela devient O(log N), c'est un problème.

Avoir une file d'attente prioritaire rendra certainement C# plus convivial pour les entretiens.

Avoir une file d'attente prioritaire rendra certainement C# plus convivial pour les entretiens.

Oui c'est vrai.
Je le cherche pour la même raison.

@stephentoub Pour les délais d'attente qui ne se produisent jamais, cela me semble parfaitement logique. Mais je me demande ce qui arrive au système lorsque soudainement de nombreux délais d'attente commencent à se produire, parce qu'il y a soudainement une perte de paquets ou un serveur qui ne répond pas ou quoi que ce soit d'autre ? Est-ce que les minuteries récurrentes ala System.Timer utilisent la même implémentation ? Là, le délai d'expiration serait le « chemin heureux ».

Mais je me demande ce qui arrive au système quand soudain, beaucoup de délais d'attente commencent à se produire

Essayez-le. :)

Nous avons vu beaucoup de charges de travail réelles souffrir avec la mise en œuvre précédente. Dans tous les cas que nous avons examinés, le nouveau (qui n'utilise pas de file d'attente prioritaire mais a simplement une séparation simple entre les minuteries prochainement et prochainement) a résolu le problème, et nous n'en avons pas vu de nouveaux avec ce.

Est-ce que les minuteries récurrentes ala System.Timer utilisent la même implémentation ?

Oui. Mais ils sont généralement répartis, souvent assez uniformément, dans les applications du monde réel.

5 ans plus tard, il n'y a toujours pas de PriorityQueue.

5 ans plus tard, il n'y a toujours pas de PriorityQueue.

Ne doit pas être une priorité assez élevée...

Ils ont changé autour de la disposition du repo. Le nouvel emplacement est https://github.com/dotnet/reactive/blob/master/Rx.NET/Source/src/System.Reactive/Internal/PriorityQueue.cs

@stephentoub mais peut-être en fait @eiriktsarpalis avons-nous réellement une

Je n'ai pas encore vu de déclaration que cela soit réglé et je ne sais pas s'il peut y avoir une conception finale de l'API sans une application tueuse désignée. Mais...
en supposant que le meilleur candidat de l'application tueuse programme des concours / enseignement / entretiens, je pense que la conception d'Eric en haut semble assez utilisable ... et j'ai toujours ma contre-proposition assise (récemment révisée, toujours pas retirée !)

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

Les principales différences que je remarque entre eux sont de savoir si cela devrait essayer de parler comme un dictionnaire et si les sélecteurs devraient être une chose.

Je commence à oublier exactement pourquoi je voulais que ce soit un dictionnaire. Avoir un assignateur indexé pour mettre à jour les priorités et être capable d'énumérer les clés avec leurs priorités, me semblait être une bonté et très semblable à un dictionnaire. Être capable de le visualiser comme un dictionnaire dans le débogueur pourrait également être agréable.

Je pense que les sélecteurs modifient en quelque sorte radicalement l'interface de classe attendue, FWIW. Le sélecteur doit être quelque chose de cuit au moment de la construction, et si vous l'avez, cela élimine le besoin pour quiconque de transmettre une valeur de priorité à quelqu'un d'autre - ils doivent simplement appeler le sélecteur s'ils veulent connaître la priorité. Vous ne voudriez donc pas du tout avoir de paramètres prioritaires dans les signatures de méthode, à l'exception du sélecteur. Donc, dans ce cas, cela devient une sorte de classe 'PrioritySet' plus spécialisée. [Pour quels sélecteurs impurs sont un problème de bogue possible !]

@TimLovellSmith Je comprends le désir de ce comportement, mais comme cela a été une demande de 5 ans, ne serait-il pas raisonnable de simplement implémenter un tas binaire basé sur un tableau avec un sélecteur et partageant la même surface d'API que Queue.cs ? Je pense que c'est de loin ce dont la plupart des utilisateurs ont besoin. À mon avis, cette structure de données n'a pas été implémentée car nous ne pouvons pas nous mettre d'accord sur une conception d'API, mais si nous faisons un PriorityQueue je pense que nous devrions simplement émuler la conception d'API de Queue .

@Jlalond D'où la proposition ici, hein? Je n'ai aucune objection à l'implémentation d'un tableau basé sur le tas. Je me souviens maintenant de la plus grande objection que j'ai eue contre les sélecteurs, c'est qu'il n'est pas assez facile de faire correctement les mises à jour prioritaires. Une vieille file d'attente simple n'a pas d'opération de mise à jour prioritaire, mais la mise à jour de la priorité des éléments est une opération importante pour de nombreux algorithmes de file d'attente prioritaire.
Les gens devraient être éternellement reconnaissants si nous optons pour une API qui ne les oblige pas à déboguer les structures de file d'attente prioritaires corrompues.

@TimLovellSmith Désolé Tim, j'aurais dû clarifier l'héritage de IDictionary .

Avons-nous un cas d'utilisation où la priorité changerait ?

J'aime votre implémentation, mais je pense que nous pouvons réduire la réplication de IDictionary Behavior. Je pense que nous ne devrions pas hériter de IDictionary<> mais seulement de ICollection, car je ne pense pas que les modèles d'accès au dictionnaire soient intuitifs,

Cependant, je pense vraiment que renvoyer T et sa priorité associée aurait du sens, mais je ne connais pas de cas d'utilisation où j'aurais besoin de connaître la priorité d'un élément dans la structure de données lors de sa mise en file d'attente ou de sa suppression.

@Jlalond
Si nous avons une file d'attente prioritaire, alors elle devrait prendre en charge toutes les opérations qu'elle peut simplement et efficacement, _et_ qui sont
« attendu » d'être là par des personnes familières avec ce type de structure de données / abstraction d'un.

La priorité de mise à jour appartient à l'API car elle appartient à ces deux catégories. La mise à jour de la priorité est une considération suffisamment importante dans de nombreux algorithmes pour que la complexité de l'opération avec des structures de données de tas affecte la complexité de l'algorithme global, et elle est régulièrement mesurée, voir :
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps

TBH principalement sa diminu_key qui est intéressante pour les algorithmes et l'efficacité, et mesurée, donc je vais personnellement bien pour l'API n'a que DecreasePriority(), et n'a pas réellement UpdatePriority.
Mais pour plus de

dictionnaire RE J'ai accepté. Je l'ai retiré de ma proposition au nom de plus simple, c'est mieux.
https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

Je ne connais pas de cas d'utilisation où j'aurais besoin de connaître la priorité d'un élément dans la structure de données lors de sa mise en file d'attente ou de sa suppression.

Je ne sais pas de quoi parle ce commentaire. Vous n'avez pas besoin de connaître la priorité d'un élément pour le retirer de la file d'attente. Mais vous devez connaître sa priorité lorsque vous le placez en file d'attente. Vous voulez peut-être connaître la priorité finale d'un élément lorsque vous le retirez de la file d'attente, car cette valeur peut avoir une signification, par exemple la distance.

@TimLovellSmith

Je ne sais pas de quoi parle ce commentaire. Vous n'avez pas besoin de connaître la priorité d'un élément pour le retirer de la file d'attente. Mais vous devez connaître sa priorité lorsque vous le placez en file d'attente. Vous voulez peut-être connaître la priorité finale d'un élément lorsque vous le retirez de la file d'attente, car cette valeur peut avoir une signification, par exemple la distance.

Désolé, j'ai mal compris ce que TPriorty voulait dire dans votre paire clé-valeur.

D'accord, deux dernières questions, pourquoi ils KeyValuePair avec TPriority, et le meilleur moment que je peux voir pour une redéfinition des priorités est N log N, je suis d'accord que c'est précieux, mais aussi à quoi ressemblerait cette API ?

le meilleur moment que je vois pour une redéfinition des priorités est N log N,

C'est le meilleur moment pour redéfinir les priorités de N éléments par opposition à un élément, n'est-ce pas ?
Je vais clarifier la doc : "UpdatePriority | O(log n)" devrait se lire "UpdatePriority (single item) | O(log n)".

@TimLovellSmith Cela a du sens, mais une mise à jour de la priorité ne serait-elle pas réellement O2 * (log n), car nous aurions besoin de supprimer l'élément puis de le réinsérer? Baser mon hypothèse sur le fait qu'il s'agit d'un tas binaire

@TimLovellSmith Cela a du sens, mais une mise à jour de la priorité ne serait-elle pas réellement O2 * (log n), car nous aurions besoin de supprimer l'élément puis de le réinsérer? Baser mon hypothèse sur le fait qu'il s'agit d'un tas binaire

Les facteurs constants comme celui-ci 2 sont généralement ignorés dans l'analyse de complexité car ils deviennent pour la plupart hors de propos à mesure que la taille de N augmente.

Compris, je voulais surtout savoir si Tim avait des idées excitantes à faire seulement
une opération :)

Le mar 18 août 2020, 23:51 masonwheeler [email protected] a écrit :

@TimLovellSmith https://github.com/TimLovellSmith C'est logique, mais
n'y aurait-il pas de mise à jour de la priorité en fait ne O2 * (log n), car nous aurions besoin de
retirer l'élément puis le réinsérer ? Baser mon hypothèse sur ça
être un tas binaire

Les facteurs constants comme celui-ci 2 sont généralement ignorés dans l'analyse de complexité
car ils deviennent pour la plupart hors de propos à mesure que la taille de N augmente.

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/dotnet/runtime/issues/14032#issuecomment-675887763 ,
ou se désinscrire
https://github.com/notifications/unsubscribe-auth/AF76XTI3YK4LRUVTOIQMVHLSBNZARANCNFSM4LTSQI6Q
.

@Jlalond
Pas d'idées nouvelles, j'ignorais simplement un facteur constant, mais les diminutions sont également différentes des augmentations

Si la mise à jour prioritaire est une diminution, vous n'avez pas besoin de la supprimer et de la rajouter, elle ne fait que bouillonner, donc son 1 * O(log n) = O(log n).
Si la mise à jour prioritaire est une augmentation, vous devrez probablement la supprimer et la rajouter, donc son 2 * O(log n) = toujours O (log n).

Quelqu'un d'autre aurait peut-être inventé une meilleure structure de données/un meilleur algorithme pour augmenter la priorité que remove + readd, mais je n'ai pas essayé de le trouver, O(log n) semble assez bon.

KeyValuePair s'est glissé dans l'interface alors qu'il s'agissait d'un dictionnaire, mais a survécu à la suppression du dictionnaire, principalement pour qu'il soit possible d'itérer la collection d'éléments avec leurs priorités. Et aussi pour que vous puissiez retirer des éléments de la file d'attente avec leurs priorités. Mais peut-être pour plus de simplicité encore, la suppression de la file d'attente avec priorités ne devrait être que dans la version « avancée » de l'API Dequeue. (EssayezDequeue :D)

Je vais faire ce changement.

@TimLovellSmith Cool, j'attends avec impatience votre proposition révisée. Pouvons-nous le soumettre pour examen afin que je puisse commencer à travailler dessus ? De plus, je sais qu'il y avait une certaine impulsion pour une file d'attente d'appariement pour améliorer le temps de fusion, mais je pense toujours qu'un tas binaire basé sur un tableau serait la performance la plus globale. Les pensées?

@Jlalond
J'ai apporté des modifications mineures pour simplifier cette API Peek + Dequeue.
Certains ICCollectionles apis liés à KeyValuePair restent, car je ne vois évidemment rien de mieux pour les remplacer.
Même lien RP. Existe-t-il un autre moyen de le soumettre pour examen ?

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

L'implémentation qu'il utilise ne me dérange pas tant qu'elle est à peu près aussi rapide que le tas basé sur un tableau ou mieux.

@TimLovellSmith En fait, je ne sais rien de l'examen des API, mais j'imagine que @danmosemsft peut nous

Ma première question est qu'est-ce quiêtre? Je suppose que ce serait un entier ou un nombre à virgule flottante, mais pour moi, déclarer le nombre "interne" à la file d'attente me semble étrange

Et je préfère en fait les tas binaires, mais je voulais m'assurer que tout allait bien dans cette direction

@eiriktsarpalis @layomia sont les propriétaires de la gérer cela via l'examen de l'API. Je n'ai pas suivi la discussion, avons-nous atteint un point de consensus ?

Comme nous en avons discuté dans le passé, les bibliothèques de base ne sont pas le meilleur endroit pour toutes les collections, en particulier celles qui sont opiniâtres et/ou spécialisées. Par exemple, nous expédions chaque année - nous ne pouvons pas nous déplacer aussi rapidement qu'une bibliothèque peut le faire. Nous avons pour objectif de créer une communauté autour d'un pack de collections pour combler cette lacune. Mais 84 pouces vers le haut sur celui-ci suggèrent qu'il y a un besoin généralisé pour cela dans les bibliothèques de base.

@Jlalond Désolé, je n'ai pas tout à fait compris ce que la première question est censée être là. Vous demandiez pourquoi TPriority est générique ? Ou doit être un nombre ? Je doute que beaucoup de gens utilisent des types de priorité non numériques. Mais ils peuvent préférer utiliser un octet, un entier, un long, un flottant, un double ou un enum.

@TimLovellSmith Oui, je voulais juste savoir si c'était générique

@danmosemsft Merci Dan. Le nombre d'objections/commentaires sans réponse que je vois à ma proposition[1] est à peu près nul, et il aborde toutes les questions importantes qui ont été laissées ouvertes par la proposition de @ebicle en haut (à laquelle elle devient de plus en plus similaire).

Donc, je prétends qu'il passe les contrôles de santé mentale jusqu'à présent. Il doit encore y avoir une révision requise, nous pouvons discuter de l'utilité d'hériter de IReadOnlyCollection (cela ne semble pas très utile mais je devrais m'en remettre aux experts) et ainsi de suite - je suppose que c'est à cela que sert le processus de révision de l'API ! @eiriktsarpalis @layomia puis-je vous demander d'y jeter un œil ?

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

[PS - basé sur le sentiment du fil, l'application killer proposée actuellement est de coder des compétitions, des questions d'entretien, etc. (n) et une implémentation pas trop lente - et je suis heureux d'être un cobaye pour l'appliquer à quelques problèmes. Désolé ce n'est rien de plus excitant.]

Juste pour parler parce que j'utilise tout le temps des files d'attente prioritaires pour de "vrais" projets, principalement pour la recherche de graphes (par exemple, l'optimisation de problèmes, la recherche d'itinéraires) et l'extraction ordonnée (par morceaux (par exemple, les voisins les plus proches de l'arbre quad), la planification (c'est-à-dire comme la discussion sur la minuterie ci-dessus)). J'ai quelques projets auxquels je pourrais brancher une implémentation directement pour vérifier/tester l'intégrité, si cela peut aider. La proposition actuelle me semble bonne (elle fonctionnerait pour tous mes objectifs, si ce n'est une solution idéale). J'ai quelques petites remarques :

  • TElement apparaît plusieurs fois là où il devrait être TKey
  • Mes propres implémentations incluent souvent un bool EnqueueOrUpdateIfHigherPriority pour la facilité d'utilisation (et dans certains cas l'efficacité), qui finit souvent par être la seule méthode de mise en file d'attente/mise à jour utilisée (par exemple dans la recherche de graphiques). Évidemment, ce n'est pas essentiel et ajouterait une complexité supplémentaire à l'API, mais c'est une très bonne chose à avoir.
  • Il y a deux exemplaires de Enqueue (je ne veux pas dire Add ) : est-ce que l'un est censé être bool TryEnqueue ?
  • "// énumère la collection dans un ordre arbitraire, mais avec le moindre élément en premier" : je ne pense pas que le dernier bit soit utile ; Je préférerais que l'enquêteur n'ait pas à effectuer de comparaisons, mais je suppose que ce sera « gratuit » donc cela ne me dérangera pas
  • Le nom de EqualityComparer est un peu inattendu : je me serais attendu KeyComparer ce que PriorityComparer .
  • Je ne comprends pas les notes sur la complexité de CopyTo et ToArray .

@VisualMelon
Merci beaucoup pour la revue !

J'aime la suggestion de nommage de KeyComparer. L'API de priorité de diminution semble très utile. Que diriez-vous d'ajouter une ou les deux API. Étant donné que nous utilisons le modèle « la moindre priorité vient en premier », utiliseriez-vous uniquement la diminution ? Ou voudriez-vous augmenter aussi?

    public void EnqueueOrIncreasePriority(TKey key, TPriority priority); // doesn't throw, only updates priority of keys already in the collection if new priority is higher
    public void EnqueueOrDeccreasePriority(TKey key, TPriority priority); // doesn't throw, only updates priority of keys already in the collection new priroity is lower

La raison pour laquelle je pensais que l'énumération renvoyait le moins d'éléments en premier était d'être cohérent avec le modèle mental selon lequel Peek() renvoie l'élément au début de la collection. De plus, aucune comparaison n'est nécessaire pour l'implémenter s'il s'agit d'un tas binaire, cela semblait donc être un cadeau. Mais peut-être est-ce moins gratuit que je ne le pense - complexité injustifiée ? Je suis heureux de le retirer pour cette raison.

Deux files d'attente étaient accidentelles. Nous avons EnqueueOrUpdate, où la priorité est toujours mise à jour. Je suppose que TryUpdate est l'inverse logique de cela, où la priorité ne devrait jamais être mise à jour pour les éléments déjà dans la collection. Je peux voir que cela comble cette lacune logique, mais je ne sais pas si c'est une API que les gens voudront dans la pratique.

Je ne peux qu'imaginer utiliser la variante décroissante : c'est une opération naturelle dans la recherche d'itinéraire de vouloir mettre un nœud en file d'attente ou remplacer un nœud existant par un autre qui a une priorité inférieure ; Je ne pouvais pas suggérer immédiatement une utilisation pour le contraire. Un paramètre de retour boolean peut être utile si vous devez effectuer une opération lorsque l'élément est en file d'attente/non en file d'attente.

Je ne supposais pas un tas binaire, mais s'il est gratuit, je n'ai aucun problème avec le premier élément étant toujours le minimum, cela semblait juste une contrainte étrange.

@VisualMelon @TimLovellSmith Une API conviendrait-elle ?
EnqueueOrUpdatePriority ? J'imagine que la simple possibilité de repositionner un nœud dans la file d'attente est précieuse pour les algorithmes de traversée même si elle augmente ou diminue la priorité

@Jlalond oui, je le mentionne car cela peut être utile pour l'efficacité en fonction de l'implémentation, et encode directement le cas d'utilisation

Mise à jour pour supprimer EnqueueOrIncreasePriority, en conservant EnqueueOrUpdate et EnqueueOrDecrease. Les laisser retourner bool lorsqu'un élément est nouvellement ajouté à la collection, comme HashSet.Add().

@Jlalond Je m'excuse pour l'erreur ci-dessus (suppression de votre dernier commentaire demandant quand ce problème sera examiné).

Je voulais juste mentionner que @eiriktsarpalis sera de retour la semaine prochaine et nous discuterons ensuite de la priorisation de cette fonctionnalité et de la révision des API.

C'est quelque chose que nous envisageons mais qui n'est pas encore engagé pour .NET 6.

Je vois que ce fil a été relancé en partant de zéro et en bifurquant, même si nous avons eu des discussions approfondies sur ce sujet en 2017 pendant plusieurs mois (la majorité de cet énorme fil) et avons collectivement produit cette proposition - faites défiler ci-dessus pour voir. C'est aussi la proposition que @karelz a liée dans la première ligne du premier post pour la visibilité :

Voir la DERNIÈRE proposition dans le référentiel corefxlab.

Au 16/06/2017, nous attendions une révision de l'API qui n'a jamais eu lieu et le statut était :

Si l'API est approuvée dans sa forme actuelle (deux classes), je créerais un autre PR à CoreFXLab avec une implémentation pour les deux en utilisant un tas quaternaire ( voir implémentation ). Le PriorityQueue<T> n'utiliserait qu'un seul tableau en dessous, tandis que PriorityQueue<TElement, TPriority> utiliserait deux -- et réorganiserait leurs éléments ensemble. Cela pourrait nous épargner des allocations supplémentaires et être aussi efficace que possible. J'ajouterai qu'une fois qu'il y aura le feu vert.

Je recommande de continuer à répéter la proposition que nous avons faite en 2017, à partir d'un examen formel qui n'a pas encore eu lieu. Cela nous permettra d'éviter de bifurquer et d'itérer au dos d'une proposition composée après des centaines de messages échangés par les membres de la communauté, également par respect pour l'effort fourni par toutes les personnes impliquées. Je suis heureux d'en discuter s'il y a des commentaires.

@pgolebiowski Merci d'être revenu sur la discussion. Je voudrais m'excuser d'avoir poussé les propositions encore plus loin, ce qui n'est pas le moyen le plus efficace de collaborer. C'était un geste impulsif de ma part. (J'avais l'impression que la discussion était complètement bloquée en raison du trop grand nombre de questions ouvertes dans les propositions, et j'avais juste besoin d'une proposition plus avisée.)

Et si nous essayions d'aller de l'avant sur ce point, en oubliant au moins temporairement le contenu du code de mon RP, et reprenions la discussion ici des « problèmes en suspens » identifiés ? Je voudrais donner mon avis sur chacun d'eux.

  1. Nom de classe PriorityQueue vs. Heap <-- Je pense que cela a déjà été discuté et le consensus est que PriorityQueue est meilleur.

  2. Introduire IHeap et la surcharge du constructeur ? <-- Je ne prévois pas beaucoup de valeur. Je pense que pour 95% du monde, il n'y a pas de raison impérieuse (par exemple dans le delta de performance) d'avoir plusieurs implémentations de tas, et les 5% restants devront probablement écrire les leurs.

  3. Introduire IPriorityQueue ? <-- Je ne pense pas non plus que ce soit nécessaire. Je suppose que les autres langages et frameworks s'entendent très bien sans ces interfaces dans leur bibliothèque standard. Faites-moi savoir si c'est incorrect.

  4. Utiliser le sélecteur (de priorité stocké dans la valeur) ou non (différence de 5 API) <-- Je ne vois pas d'argument solide en faveur de la prise en charge des sélecteurs dans la file d'attente prioritaire. Je pense que IComparer<> sert déjà de très bon mécanisme d'extensibilité minimale pour comparer les priorités des éléments, et les sélecteurs n'offrent aucune amélioration significative. De plus, les gens sont susceptibles de ne pas savoir comment utiliser les sélecteurs avec des priorités modifiables (ou comment NE PAS les utiliser).

  5. Utiliser des tuples (élément TElement, priorité TPriority) par rapport à KeyValuePair<-- Personnellement, je pense que KeyValuePair est l'option préférée pour une file d'attente prioritaire où les éléments ont des priorités pouvant être mises à jour, car les éléments sont traités comme des clés dans un ensemble/dictionnaire.

  6. Peek et Dequeue devraient-ils plutôt avoir un argument au lieu d'un tuple ? <-- Je ne suis pas sûr de tous les avantages et inconvénients, en particulier les performances. Devrions-nous simplement choisir celui qui fonctionne le mieux dans un simple test de référence ?

  7. Le lancer Peek and Dequeue est-il vraiment utile ? <-- Oui... C'est ce qui devrait arriver pour les algorithmes qui supposent à tort qu'il y a encore des éléments dans la file d'attente . Si vous ne voulez pas que les algorithmes supposent cela, la meilleure chose à faire est de ne pas fournir du tout les API Peek et Dequeue, mais simplement de fournir TryPeek et TryDequeue. [Je suppose qu'il existe des algorithmes qui peuvent appeler Peek ou Dequeue en toute sécurité dans certains cas, et avoir Peek/Dequeue est une légère amélioration des performances et de la convivialité pour ceux-ci.]

En dehors de cela, j'ai également quelques suggestions à envisager d'ajouter à la proposition à l'étude :

  1. File d'attente de prioriténe devrait fonctionner qu'avec des éléments uniques, qui sont leur propre poignée. Il doit prendre en charge IEqualityComparer personnalisé, au cas où il souhaite comparer des objets non extensibles (comme des chaînes) de manière spécifique pour effectuer des mises à jour prioritaires.

  2. File d'attente de prioritédevrait prendre en charge une variété d'opérations telles que « supprimer », « diminuer la priorité si plus petit », et en particulier « mettre un élément en file d'attente ou réduire la priorité si une opération plus petite », le tout en O(log n) - pour mettre en œuvre efficacement et simplement des scénarios de recherche de graphique.

  3. Ce serait pratique pour les programmeurs paresseux si PriorityQueuefournit l'opérateur d'index [] pour obtenir/définir la priorité des éléments existants. Il devrait être O(1) pour get, O(log n) pour set.

  4. La plus petite priorité est la meilleure. Parce que les files d'attente prioritaires sont utilisées pour beaucoup, il y a des problèmes d'optimisation, avec un « coût minimal ».

  5. Le dénombrement ne doit fournir aucune garantie d'ordre.

Problème ouvert - gère :
File d'attente de prioritéles éléments et les priorités semblent être destinés à permettre de travailler avec des valeurs en double. Ces valeurs doivent soit être considérées comme « immuables », ou il doit y avoir une méthode à appeler pour notifier la collection lorsque les priorités des éléments ont changé, éventuellement avec des descripteurs, afin qu'il puisse être précis sur la priorité des doublons modifiés (quel scénario doit faire ceci ??)... J'ai des doutes sur tout ce qui est utile, et si c'est une bonne idée, mais si nous avons des priorités de mise à jour dans une telle collection, il serait bien que les coûts encourus par cette méthode puissent être engagés paresseusement, donc que lorsque vous ne travaillez que sur des objets immuables et que vous ne voulez pas les utiliser, il n'y a pas de coût supplémentaire... comment résoudre le problème ? Est-ce dû au fait d'avoir des méthodes surchargées qui utilisent des poignées et d'autres qui n'en utilisent pas ? Ou ne devrions-nous tout simplement pas avoir de descripteurs d'élément distincts des éléments eux-mêmes (utilisés comme clé de hachage pour la recherche) ?

Une pensée pour aider à déplacer cela plus rapidement, il peut être judicieux de déplacer ce type vers la bibliothèque "Collections communautaires" en cours de discussion dans https://github.com/dotnet/runtime/discussions/42205

Je suis d'accord que la plus petite priorité d'abord est la meilleure. Une file d'attente prioritaire est également utile dans le développement du jeu au tour par tour, et pouvoir avoir la priorité soit un compteur monotone croissant du moment où chaque élément obtient son prochain tour est très utile.

Nous envisageons de soumettre cela pour examen de l'API dès que possible (bien que nous ne soyons pas encore déterminés à fournir une implémentation dans le délai de .NET 6).

Mais avant d'envoyer ceci pour examen, nous devrons aplanir certaines des questions ouvertes de haut niveau. Je pense que l » @TimLovellSmith en écriture est un bon point de départ pour atteindre cet objectif.

Quelques remarques sur ces points :

  • Le consensus sur les questions 1 à 3 est établi depuis longtemps, je pense que nous pourrions les traiter comme résolues.

  • _Utiliser le sélecteur (de priorité stocké à l'intérieur de la valeur) ou non_ -- D'accord avec vos remarques. Un autre problème avec cette conception est que les sélecteurs sont facultatifs, ce qui signifie que vous devez faire attention à ne pas appeler la mauvaise surcharge Enqueue (ou risquer d'obtenir un InvalidOperationException ).

  • Je préfère utiliser un tuple plutôt que KeyValuePair . Utiliser une propriété .Key pour accéder à une TPriority me semble étrange. Un tuple vous permet d'utiliser .Priority qui a de meilleures propriétés d'auto-documentation.

  • _Est-ce que Peek et Dequeue devraient plutôt avoir un argument au lieu d'un tuple?_ -- Je pense que nous devrions simplement suivre la convention établie dans des méthodes similaires ailleurs. Utilisez donc probablement un argument out .

  • _Est-ce que le lancement de Peek and Dequeue est utile ?_ -- D'accord à 100 % avec vos commentaires.

  • _La plus petite priorité est la meilleure_ -- D'accord.

  • _L'énumération ne doit fournir aucune garantie de commande_ -- Cela ne pourrait-il pas violer les attentes des utilisateurs ? Quels sont les compromis ? NB, nous pouvons probablement reporter des détails comme celui-ci pour la discussion sur l'examen de l'API.

Je voudrais également reformuler quelques autres questions ouvertes :

  • Expédions-nous PriorityQueue<T> , PriorityQueue<TElement, TPriority> ou les deux ? -- Personnellement, je pense que nous ne devrions implémenter que ce dernier, car il semble fournir une solution plus propre et plus générale. En principe, la priorité ne devrait pas être une propriété intrinsèque à l'élément mis en file d'attente, nous ne devrions donc pas forcer les utilisateurs à l'encapsuler dans des types de wrapper.

  • Exigeons-nous l'unicité des éléments, jusqu'à l'égalité ? -- Corrigez-moi si je me trompe, mais je pense que l'unicité est une contrainte artificielle, imposée par l'exigence de prendre en charge les scénarios de mise à jour sans recourir à une approche de gestion. Cela complique également la surface de l'API, car nous devons maintenant également nous soucier des éléments ayant la bonne sémantique d'égalité (quelle est l'égalité appropriée lorsque les éléments sont de grands DTO ?). Je vois ici trois chemins possibles :

    1. Exiger l'unicité/l'égalité et prendre en charge les scénarios de mise à jour en passant l'élément d'origine (jusqu'à l'égalité).
    2. Ne nécessite pas l'unicité/l'égalité et prend en charge les scénarios de mise à jour à l'aide de descripteurs. Ceux-ci peuvent être obtenus en utilisant des variantes de méthode optionnelles Enqueue pour les utilisateurs qui en ont besoin. Si les allocations de poignées sont une préoccupation suffisamment importante, je me demande si celles-ci pourraient être amorties à la ValueTask ?
    3. N'exige pas l'unicité/l'égalité et ne prend pas en charge la mise à jour des priorités.
  • Prenons-nous en charge la fusion des files d'attente ? -- Le consensus semble être non, puisque nous n'envoyons qu'une seule implémentation (probablement en utilisant des tas de tableaux) où la fusion n'est pas efficace.

  • Quelles interfaces doit-il implémenter ? -- J'ai vu quelques propositions recommandant IQueue<T> , mais cela ressemble à une abstraction qui fuit. Personnellement, je préférerais rester simple et simplement implémenter ICollection<T> .

cc @layomia @safern

@eiriktsarpalis Au contraire, la contrainte de prendre en charge les mises à jour n'a rien d'artificiel !

Les algorithmes avec des files d'attente prioritaires mettent souvent à jour les priorités des éléments. La question est de savoir si vous fournissez une API orientée objet, qui "fonctionne simplement" pour mettre à jour les priorités des objets ordinaires... ou forcez-vous les gens à
a) comprendre le modèle de poignée
b) conserver des données supplémentaires en mémoire, telles que des propriétés supplémentaires ou un dictionnaire externe, pour garder une trace des poignées des objets, de leur côté, juste pour qu'ils puissent faire des mises à jour (doit aller avec le dictionnaire pour les classes qu'ils ne peuvent pas changer ? ou mettre à niveau les objets en tuples, etc.)
c) les structures externes « gestion de la mémoire » ou « récupération de la mémoire », c'est-à-dire le traitement de nettoyage pour les éléments qui ne sont plus dans la file d'attente, lors de l'utilisation de l'approche par dictionnaire
d) ne pas confondre les descripteurs opaques dans des contextes avec plusieurs files d'attente car ils n'ont de sens que dans le contexte d'une seule file d'attente prioritaire

De plus, il y a cette question philosophique sur toute la raison pour laquelle quelqu'un voudrait qu'une file d'attente qui _garde la trace des objets par priorité_ se comporte de cette façon : pourquoi le même objet (égale renvoie vrai) aurait-il deux priorités _différentes_ ? S'ils sont vraiment censés avoir des priorités différentes, pourquoi ne sont-ils pas modélisés comme des objets

Aussi pour les descripteurs, il doit y avoir une table interne de descripteurs dans la file d'attente prioritaire juste pour que les descripteurs fonctionnent réellement. Je pense que c'est un travail équivalent à garder un dictionnaire pour rechercher des objets dans la file d'attente.

PS : chaque objet .net prend déjà en charge les concepts d'égalité/unicité, ce n'est donc pas une grande demande de l'"exiger".

En ce qui concerne KeyValuePair , je conviens que ce n'est pas idéal (bien que dans la proposition, Key soit pour l'élément, Value pour la priorité, ce qui est cohérent avec les différents Sorted types struct dédié bien nommé

En ce qui concerne l'unicité, c'est une préoccupation fondamentale, et je ne pense pas qu'on puisse décider quoi que ce soit d'autre jusqu'à ce que ce soit le cas. Je favoriserais l'unicité des éléments telle que définie par le comparateur (facultatif) (conformément aux propositions existantes et à la suggestion i) si l'objectif est une seule API facile à utiliser et à usage général. Unique vs non unique est une grande faille, et j'utilise les deux types à des fins différentes. Le premier est «plus difficile» à mettre en œuvre et couvre les cas d'utilisation les plus (et les plus typiques) (juste mon expérience) tout en étant plus difficile à mal utiliser. Ces cas d'utilisation qui _require_ la non-unicité devraient (IMO) être desservis par un type différent (par exemple, un ancien tas binaire simple), et j'apprécierais d'avoir les deux disponibles. C'est essentiellement ce que la proposition originale liée par @pgolebiowski fournit (si je comprends bien) modulo un (simple) wrapper. _Edit : Non, cela ne prendrait pas en charge les priorités liées_

Au contraire, il n'y a rien d'artificiel dans la contrainte de prendre en charge les mises à jour !

Je suis désolé, je ne voulais pas dire que la prise en charge des mises à jour est artificielle ; l'exigence d'unicité est plutôt introduite artificiellement pour prendre en charge les mises à jour.

PS : chaque objet .net prend déjà en charge les concepts d'égalité/unicité, ce n'est donc pas une grande demande de l'"exiger"

Bien sûr, mais parfois, la sémantique d'égalité qui accompagne le type peut ne pas être la plus souhaitable (par exemple, égalité de référence, égalité structurelle à part entière, etc.). Je signale simplement que l'égalité est difficile et que l'imposer de force dans la conception entraîne une toute nouvelle classe de bogues utilisateurs potentiels.

@eiriktsarpalis Merci d'avoir clarifié cela. Mais est-ce vraiment artificiel ? Je ne pense pas que ce soit le cas. C'est juste une autre solution naturelle .

L'API doit être _bien définie_. Vous ne pouvez pas fournir une API de mise à jour sans demander à l'utilisateur d'être précis sur ce qu'il souhaite mettre à jour . Les poignées et l'égalité des objets ne sont que deux approches différentes pour créer une API bien définie.

Approche par handle : chaque fois que vous ajoutez un objet à la collection, vous devez lui donner un 'nom', c'est-à-dire un 'handle', afin que vous puissiez vous référer à cet objet exact sans ambiguïté plus tard dans la conversation.

Approche d'unicité d'objet : chaque fois que vous ajoutez un objet à la collection, il doit s'agir d'un objet différent, ou vous devez spécifier comment gérer le cas où l'objet existe déjà.

Malheureusement, seule l'approche objet vous permet vraiment de prendre en charge certaines des propriétés de méthodes d'API de haut niveau les plus utiles, comme 'EnqueueIfNotExists' ou 'EnqueueOrDecreasePriroity(item)' - elles n'ont aucun sens à fournir dans une conception centrée sur la poignée, car vous ne peut pas savoir si l'élément existe déjà dans la file d'attente (puisque c'est votre travail de garder une trace de cela, avec des poignées).

L'une des critiques les plus révélatrices de l'approche du handle, ou de l'abandon de la contrainte d'unicité pour moi, est qu'elle rend toutes sortes de scénarios avec une priorité de mise à jour beaucoup plus compliqués à mettre en œuvre :

par exemple

  1. utiliser une file d'attente prioritairepour les chaînes représentant des messages/sentiments/tags/nom d'utilisateur qui sont votés/score mis à jour, les valeurs uniques ont une priorité changeante
  2. utiliser PriorityQueue, double> pour commander des tuples uniques [qu'ils aient des changements de priorité ou non] - doit garder une trace des poignées supplémentaires quelque part
  3. utiliser PriorityQueuepour hiérarchiser les index de graphes ou les identifiants d'objets de base de données, vous devez maintenant saupoudrer de poignées dans votre implémentation

PS

Bien sûr, mais parfois la sémantique d'égalité qui accompagne le type peut ne pas être la plus souhaitable

Il doit y avoir des trappes d'échappement, comme IEqualityComparer, ou une conversion ascendante vers un type plus riche.

Merci pour les commentaires 🥳 Mettra à jour la proposition au cours du week-end, en tenant compte de toutes les nouvelles entrées, et partagera une nouvelle révision pour un autre tour. ETA 2020-09-20.

Proposition de file d'attente prioritaire (v2.0)

Sommaire

La communauté .NET Core propose d'ajouter à la bibliothèque système la fonctionnalité _priority queue_, une structure de données dans laquelle chaque élément a en plus une priorité qui lui est associée. Plus précisément, nous proposons d'ajouter PriorityQueue<TElement, TPriority> à l'espace System.Collections.Generic noms

Principes

Dans notre conception, nous avons été guidés par les principes suivants (à moins que vous n'en connaissiez de meilleurs) :

  • Large couverture. Nous voulons offrir aux clients .NET Core une structure de données précieuse qui est suffisamment polyvalente pour prendre en charge une grande variété de cas d'utilisation.
  • Apprenez des erreurs connues. Nous nous efforçons de fournir une fonctionnalité de file d'attente prioritaire qui serait exempte des problèmes rencontrés par les clients présents dans d'autres frameworks et langages, par exemple Java, Python, C++, Rust. Nous éviterons de faire des choix de conception connus pour rendre les clients mécontents et réduire l'utilité des files d'attente prioritaires.
  • Un soin extrême avec les décisions de porte à sens unique. Une fois qu'une API est introduite, elle ne peut pas être modifiée ou supprimée, seulement étendue. Nous analyserons soigneusement les choix de conception pour éviter les solutions sous-optimales avec lesquelles nos clients seront coincés pour toujours.
  • Évitez la paralysie de la conception. Nous acceptons qu'il n'y ait pas de solution parfaite. Nous équilibrerons les compromis et avancerons dans la livraison, pour enfin offrir à nos clients les fonctionnalités qu'ils attendent depuis des années.

Fond

Du point de vue d'un client

Conceptuellement, une file d'attente prioritaire est une collection d'éléments, où chaque élément a une priorité associée. La fonctionnalité la plus importante d'une file d'attente prioritaire est qu'elle fournit un accès efficace à l'élément ayant la priorité la plus élevée dans la collection et une option pour supprimer cet élément. Le comportement attendu peut également inclure : 1) la possibilité de modifier la priorité d'un élément qui est déjà dans la collection ; 2) possibilité de fusionner plusieurs files d'attente prioritaires.

Formation en informatique

Une file d'attente prioritaire est une structure de données abstraite, c'est-à-dire un concept avec certaines caractéristiques comportementales, comme décrit dans la section précédente. Les implémentations les plus efficaces d'une file d'attente prioritaire sont basées sur des tas. Cependant, contrairement à l'idée fausse générale, un tas est également une structure de données abstraite et peut être réalisé de différentes manières, chacune offrant des avantages et des inconvénients différents.

La plupart des ingénieurs logiciels ne connaissent que l'implémentation de tas binaire basée sur un tableau - c'est la plus simple, mais malheureusement pas la plus efficace. Pour une entrée aléatoire générale, deux exemples de types de tas plus efficaces sont : le tas quaternaire et le tas d'appariement . Pour plus d'informations sur les tas, veuillez vous référer à Wikipedia et à ce document .

Le mécanisme de mise à jour est le défi de conception clé

Nos discussions ont démontré que le domaine le plus difficile dans la conception, et en même temps avec le plus grand impact sur l'API, est le mécanisme de mise à jour. Concrètement, l'enjeu est de déterminer si et comment le produit que nous souhaitons proposer aux clients doit prendre en charge la mise à jour des priorités des éléments déjà présents dans la collection.

Une telle capacité est nécessaire pour implémenter, par exemple, l'algorithme du chemin le plus court de Dijkstra ou un planificateur de tâches qui doit gérer les priorités changeantes. Le mécanisme de mise à jour est absent de Java, ce qui s'est avéré décevant pour les ingénieurs, par exemple dans ces trois questions StackOverflow vues plus de 32 000 fois : example , example , example . Pour éviter d'introduire une API avec une valeur aussi limitée, nous pensons qu'une exigence fondamentale pour la fonctionnalité de file d'attente prioritaire que nous proposons serait de prendre en charge la capacité de mettre à jour les priorités pour les éléments déjà présents dans la collection.

Pour fournir le mécanisme de mise à jour, nous devons nous assurer que le client peut être précis sur ce qu'il souhaite exactement mettre à jour. Nous avons identifié deux manières de fournir ceci : a) via des poignées ; et b) en imposant l'unicité des éléments de la collection. Chacun d'entre eux présente des avantages et des coûts différents.

Option (a) : Poignées. Dans cette approche, chaque fois qu'un élément est ajouté à la file d'attente, la structure de données fournit son handle unique. Si le client souhaite utiliser le mécanisme de mise à jour, il doit garder une trace de ces descripteurs afin de pouvoir ultérieurement spécifier sans ambiguïté l'élément qu'il souhaite mettre à jour. Le coût principal de cette solution est que les clients doivent gérer ces pointeurs. Cependant, cela ne signifie pas qu'il doit y avoir des allocations internes pour prendre en charge les descripteurs dans la file d'attente prioritaire - tout tas non basé sur un tableau est basé sur des nœuds, où chaque nœud est automatiquement son propre descripteur. A titre d'exemple, voir l'API de la méthode PairingHeap.Update .

Option (b) : Unicité. Cette approche impose deux contraintes supplémentaires au client : i) les éléments dans la file d'attente prioritaire doivent se conformer à une certaine sémantique d'égalité, ce qui amène une nouvelle classe de bogues utilisateurs potentiels ; ii) deux éléments égaux ne peuvent pas être stockés dans la même file d'attente. En payant ce coût, nous obtenons l'avantage de prendre en charge le mécanisme de mise à jour sans recourir à l'approche du handle. Cependant, toute implémentation qui exploite l'unicité/l'égalité pour déterminer l'élément à mettre à jour nécessitera un mappage interne supplémentaire, afin qu'il soit effectué en O(1) et non en O(n).

Recommandation

Nous vous recommandons d'ajouter à la bibliothèque système une classe PriorityQueue<TElement, TPriority> qui prend en charge le mécanisme de mise à jour via des handles. L' implémentation sous-jacente serait un tas d'appariement.

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

Exemple d'utilisation

1) Client qui ne se soucie pas du mécanisme de mise à jour

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) Client soucieux du mécanisme de mise à jour

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

FAQ

Dans quel ordre la file d'attente prioritaire énumère-t-elle les éléments ?

Dans un ordre indéfini, de sorte que l'énumération puisse se produire en O(n), de la même manière qu'avec un HashSet . Aucune implémentation efficace ne fournirait des capacités pour énumérer un tas en temps linéaire tout en garantissant que les éléments sont énumérés dans l'ordre - cela nécessiterait O(n log n). Étant donné que la commande sur une collection peut être réalisée de manière triviale avec .OrderBy(x => x.Priority) et que tous les clients ne se soucient pas de l'énumération avec cette commande, nous pensons qu'il est préférable de fournir un ordre d'énumération non défini.

Annexes

Annexe A : Autres langues avec fonctionnalité de file d'attente prioritaire

| Langue | Type | Remarques |
|:-:|:-:|:-:|
| Java | File d'attente de priorité | Étend la classe abstraite AbstractQueue et implémente l'interface Queue . |
| Rouille | BinaireHeap | |
| Rapide | CFBinaryHeap | |
| C++ | file_priorité | |
| Python | heapq | |
| Aller | tas | Il y a une interface de tas. |

Annexe B : découvrabilité

Notez que lors de l'examen des structures de données, le terme _heap_ est utilisé 4 fois plus souvent que _file d'attente prioritaire_.

  • "array" AND "data structure" — 17.400.000 résultats
  • "stack" AND "data structure" — 12.100.000 résultats
  • "queue" AND "data structure" — 3.850.000 résultats
  • "heap" AND "data structure" — 1.830.000 résultats
  • "priority queue" AND "data structure" — 430.000 résultats
  • "trie" AND "data structure" — 335.000 résultats

Veuillez revoir, je suis heureux de recevoir des commentaires et continuez d'itérer :) Je sens que nous sommes en train de converger ! 😄 Je suis également heureux de recevoir des questions — je les ajouterai à la FAQ !

@pgolebiowski Je n'ai pas tendance à utiliser des API basées sur des poignées (je m'attendrais à l'envelopper comme dans l'exemple 2 si je réorganisais un code existant), mais cela me semble généralement bien. Je pourrais essayer l'implémentation que vous mentionnez pour voir ce que cela donne, mais voici quelques commentaires d'emblée :

  • Il semble étrange que TryDequeue renvoie un nœud, car ce n'est pas vraiment une poignée à ce stade (je préférerais deux paramètres de sortie séparés)
  • Sera-t-il stable en ce sens que si deux éléments sont mis en file d'attente avec la même priorité, ils sortent de la file d'attente dans un ordre prévisible ? (agréable à avoir pour la reproductibilité ; peut être mis en œuvre assez facilement par le consommateur sinon)
  • Le paramètre pour Merge doit être PriorityQueue'2 non PriorityQueueNode'2 , et pouvez-vous clarifier le comportement ? Je ne connais pas le tas d'appariement, mais les deux tas se chevauchent probablement par la suite
  • Je ne suis pas fan du nom Contains pour la méthode à 2 paramètres : ce n'est pas un nom que je suppose pour une méthode de style TryGet
  • La classe doit-elle prendre en charge un IEqualityComparer<TElement> dans le but de Contains ?
  • Il ne semble pas y avoir de moyen de déterminer efficacement si un nœud est toujours dans le tas (je ne sais pas quand je l'utiliserais, pensa-t-il)
  • C'est bizarre que Remove retourne un bool ; Je m'attendrais à ce qu'il s'appelle TryRemove ou qu'il soit lancé (ce que je suppose que Update fait si le nœud n'est pas dans le tas).

@VisualMelon merci beaucoup pour les commentaires ! Résoudra rapidement ceux-ci, certainement d'accord :

  • Il semble étrange que TryDequeue renvoie un nœud, car ce n'est pas vraiment une poignée à ce stade (je préférerais deux paramètres de sortie séparés)
  • Le paramètre pour Merge doit être PriorityQueue'2 non PriorityQueueNode'2 , et pouvez-vous clarifier le comportement ? Je ne connais pas le tas d'appariement, mais les deux tas se chevauchent probablement par la suite
  • C'est bizarre que Remove retourne un bool ; Je m'attendrais à ce qu'il s'appelle TryRemove ou qu'il soit lancé (ce que je suppose que Update fait si le nœud n'est pas dans le tas).
  • Je ne suis pas fan du nom Contains pour la méthode à 2 paramètres : ce n'est pas un nom que je suppose pour une méthode de style TryGet

Clarification pour ces deux :

  • Sera-t-il stable en ce sens que si deux éléments sont mis en file d'attente avec la même priorité, ils sortent de la file d'attente dans un ordre prévisible ? (agréable à avoir pour la reproductibilité ; peut être mis en œuvre assez facilement par le consommateur sinon)
  • Il ne semble pas y avoir de moyen de déterminer efficacement si un nœud est toujours dans le tas (je ne sais pas quand je l'utiliserais, pensa-t-il)

Pour le premier point, si le but est la reproductibilité, alors l'implémentation sera déterministe, oui. Si le but est _si deux éléments qui sont placés dans la file d'attente avec la même priorité, il s'ensuit qu'ils sortiront dans le même ordre dans lequel ils ont été placés dans la file d'attente_ — je ne sais pas si l'implémentation peut être ajustée à atteindre cette propriété, une réponse rapide serait "probablement non".

Pour le deuxième point, oui, les tas ne sont pas bons pour vérifier si un élément existe dans la collection, un client devrait le suivre séparément pour y parvenir en O (1) ou réutiliser le mappage qu'il utilise pour les poignées, si ils utilisent le mécanisme de mise à jour. Sinon, O(n).

  • La classe doit-elle prendre en charge un IEqualityComparer<TElement> dans le but de Contains ?

Hmm... Je commence à penser que peut-être mettre Contains dans la responsabilité de cette file d'attente prioritaire peut être trop, et utiliser la méthode de Linq peut suffire ( Contains doit de toute façon être appliqué au dénombrement).

@pgolebiowski merci pour les éclaircissements

Pour le premier point, si le but est la reproductibilité, alors l'implémentation sera déterministe, oui

Pas tellement déterministe, car garanti pour toujours (par exemple, même la mise en œuvre change, le comportement ne changera pas), donc je pense que la réponse que je cherchais est «non». C'est bien : le consommateur peut ajouter un ID de séquence à la priorité s'il en a besoin, bien qu'à ce stade, un SortedSet fasse l'affaire.

Pour le deuxième point, oui, les tas ne sont pas bons pour vérifier si un élément existe dans la collection, un client devrait le suivre séparément pour y parvenir en O (1) ou réutiliser le mappage qu'il utilise pour les poignées, si ils utilisent le mécanisme de mise à jour. Sinon, O(n).

Ne nécessite-t-il pas un sous-ensemble du travail nécessaire pour Remove ? Je n'ai peut-être pas été clair : je voulais dire donner un PriorityQueueNode , vérifier s'il est dans le tas (pas un TElement ).

Hmm... Je commence à penser que peut-être que confier la responsabilité de Contient à cette file d'attente prioritaire est peut-être trop

Je ne me plaindrais pas si Contains n'était pas là : c'est aussi un piège pour les personnes qui ne réalisent pas qu'elles devraient utiliser des poignées.

@pgolebiowski Vous semblez assez fortement en faveur des poignées, ai-je raison de penser que c'est pour des raisons d'efficacité ?

Du point de vue de l'efficacité, je soupçonne que les poignées sont vraiment les meilleures, pour les deux scénarios avec unicité et sans, donc je suis d'accord avec cela comme étant la principale solution offerte par le cadre.

Du tout:
Du point de vue de la convivialité, je pense que les éléments dupliqués sont rarement ce que je veux, ce qui me laisse encore me demander s'il est utile pour le framework de prendre en charge les deux modèles. Mais... une classe "PrioritySet" conviviale pour les éléments uniques serait au moins facile à ajouter plus tard en tant que wrapper pour le PriorityQueue proposé, par exemple en réponse à la demande continue d'une API plus conviviale. (si la demande existe. Comme je pense que cela pourrait le faire !)

Pour l'API actuelle proposée elle-même, quelques réflexions/questions :

  • que diriez-vous de fournir également une surcharge de TryPeek(out TElement element, out TPriority priority) ?
  • si vous avez des clés en double pouvant être mises à jour, je crains que si « dequeue » ne renvoie pas le nœud de file d'attente prioritaire, alors comment vérifieriez-vous que vous supprimez le bon nœud exact de votre système de suivi de poignée ? Puisque vous pourriez avoir plus d'une copie d'élément avec la même priorité.
  • Est-ce que Remove(PriorityQueueNode) lance si le nœud n'est pas trouvé ? ou retourner faux?
  • Devrait-il y avoir une version TryRemove() qui ne se lance pas si Remove se lance ?
  • Je ne suis pas sûr que Contains() api soit utile la plupart du temps ? « Contient » semble être dans l'œil du spectateur, en particulier pour les scénarios avec des éléments « en double » avec des priorités distinctes, ou d'autres caractéristiques distinctes ! Dans ce cas, l'utilisateur final doit probablement faire sa propre recherche de toute façon. Mais au moins, cela peut être utile pour le scénario sans doublons.

@pgolebiowski Merci d'avoir pris le temps de rédiger la nouvelle proposition ! Quelques commentaires de mon côté :

  • Faisant écho au commentaire de Contains() ou TryGetNode() devraient exister dans l'API sous sa forme actuellement proposée. Ils impliquent que l'égalité pour TElement est significative, ce qui est probablement l'une des choses qu'une approche basée sur le handle essayait d'éviter.
  • Je reformulerais probablement public void Dequeue(out TElement element); comme public TElement Dequeue();
  • Pourquoi la méthode TryDequeue() doit-elle renvoyer une priorité ?
  • La classe ne devrait-elle pas aussi implémenter ICollection<T> ou IReadOnlyCollection<T> ?
  • Que se passe-t-il si j'essaie de mettre à jour un PriorityQueueNode renvoyé par une autre instance PriorityQueue ?
  • Voulons-nous soutenir une opération de fusion efficace ? AFAICT cela implique que nous ne pouvons pas utiliser une représentation basée sur un tableau. Quel impact cela aurait-il sur la mise en œuvre en termes d'allocations ?

La plupart des ingénieurs logiciels ne connaissent que l'implémentation de tas binaire basée sur un tableau - c'est la plus simple, mais malheureusement pas la plus efficace. Pour une entrée aléatoire générale, deux exemples de types de tas plus efficaces sont : le tas quaternaire et le tas d'appariement.

Quels sont les compromis pour choisir ces dernières approches ? Est-il possible d'utiliser une implémentation basée sur un tableau pour ceux-ci ?

Il ne peut pas implémenter ICollection<T> ou IReadOnlyCollection<T> lorsqu'il n'a pas les bonnes signatures pour "Ajouter" et "Supprimer", etc.

Son esprit/forme est beaucoup plus proche de ICollection<KeyValuePair<T,Priority>>

Son esprit/forme est beaucoup plus proche de ICollection<KeyValuePair<T,Priority>>

Ne serait-ce pas KeyValuePair<TPriority, TElement> , puisque la commande est effectuée par TPriority, qui est en fait un mécanisme de saisie ?

OK, il semble que nous soyons globalement en faveur de l'abandon des méthodes Contains et TryGet . Les supprimera dans la prochaine révision et expliquera la raison de la suppression dans la FAQ.

Quant aux interfaces implémentées, IEnumerable<PriorityQueueNode<TElement, TPriority>> n'est-il pas suffisant ? Quel type de fonctionnalité manque ?

Sur le KeyValuePair - il y avait quelques voix qu'un tuple ou une structure avec .Element et .Priority sont plus souhaitables. Je pense que je suis en faveur de ceux-ci.

Ne serait-ce pas KeyValuePair<TPriority, TElement> , puisque la commande est effectuée par TPriority, qui est en fait un mécanisme de saisie ?

Il y a de bons arguments pour les deux côtés. D'une part, oui, exactement ce que vous venez de dire. D'un autre côté, les clés d'une collection KVP sont généralement censées être uniques, et il est parfaitement valable d'avoir plusieurs éléments avec la même priorité.

D'autre part, les clés d'une collection KVP sont généralement censées être uniques

Je ne suis pas d'accord avec cette affirmation - une collection de paires clé-valeur n'est que cela ; toutes les exigences d'unicité sont superposées à cela.

IEnumerable<PriorityQueueNode<TElement, TPriority>> suffit-il pas ? Quel type de fonctionnalité manque ?

Personnellement, je m'attendrais à ce que IReadOnlyCollection<PQN<TElement, TPriority>> soit implémenté, car l'API fournie satisfait déjà en grande partie cette interface. De plus, cela serait cohérent avec d'autres types de collecte.

Concernant l'interface :

```
public bool TryGetNode (élément TElement, out PriorityQueueNodenœud); // Au)

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

J'aimerais également que PriorityQueue implémente l'interface IReadonlyCollection<> .

Passer à autre chose que la surface de l'API publique.

Une telle capacité est nécessaire pour implémenter, par exemple, l'algorithme du chemin le plus court de Dijkstra ou un planificateur de tâches qui doit gérer les priorités changeantes. Le mécanisme de mise à jour est absent de Java, ce qui s'est avéré décevant pour les ingénieurs, par exemple dans ces trois questions StackOverflow vues plus de 32 000 fois : exemple, exemple, exemple. Pour éviter d'introduire une API avec une valeur aussi limitée, nous pensons qu'une exigence fondamentale pour la fonctionnalité de file d'attente prioritaire que nous proposons serait de prendre en charge la capacité de mettre à jour les priorités pour les éléments déjà présents dans la collection.

Je voudrais être en désaccord. La dernière fois que j'ai écrit Dijkstra en C++, std::priority_queue était suffisant et ne gère pas la mise à jour prioritaire. AFAIK, le consensus commun pour ce cas est d'ajouter un faux élément dans la file d'attente avec une priorité et une valeur modifiées et de surveiller si nous traitons la valeur ou non. La même chose peut être faite avec le planificateur de tâches.

Pour être honnête, je ne sais pas à quoi ressemblerait Dijkstra avec la proposition de file d'attente actuelle. Comment suis-je supposé garder une trace des nœuds dont j'ai besoin de mettre à jour la priorité ? Avec TryGetNode() ? Ou avez-vous une autre collection de nœuds ? J'adorerais voir le code de la proposition actuelle.

Si vous regardez dans Wikipédia, il n'y a aucune hypothèse de mise à jour des priorités pour la file d'attente prioritaire. Idem pour toutes les autres langues qui n'ont pas cette fonctionnalité et qui s'en sont tirées. Je sais « s'efforcer d'être meilleur », mais y a-t-il réellement une demande pour cela ?

Pour une entrée aléatoire générale, deux exemples de types de tas plus efficaces sont : le tas quaternaire et le tas d'appariement. Pour plus d'informations sur les tas, veuillez vous référer à Wikipedia et à cet article.

J'ai regardé dans le papier et voici une citation de celui-ci :

Les résultats montrent que le choix optimal de mise en œuvre dépend fortement des intrants. De plus, cela montre qu'il faut veiller à optimiser les performances du cache, principalement au niveau de la barrière L1-L2. Cela suggère qu'il est peu probable que les structures compliquées et inconscientes du cache fonctionnent bien par rapport aux structures plus simples, conscientes du cache.

D'après ce qu'il semble, la proposition de file d'attente actuelle serait verrouillée derrière l'implémentation de l'arbre plutôt que le tableau, et quelque chose me dit quelle est la probabilité que les nœuds d'arbre dispersés dans la mémoire ne soient pas aussi performants que le tableau d'éléments.

Je pense qu'avoir des références pour comparer un tas binaire simple basé sur un tableau et un tas d'appariement serait idéal pour prendre la bonne décision, avant cela, je ne pense pas qu'il soit intelligent de verrouiller la conception derrière une implémentation spécifique (je vous regarde Merge méthode).

Passant à un autre sujet, je préférerais personnellement avoir KeyValuePairque la nouvelle classe personnalisée.

  • Moins de surface API
  • Je peux faire quelque chose comme ceci : `new PriorityQueue(nouveau dictionnaire() { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 } }); Je sais que c'est limité par l'unicité de la clé. Aussi, je pense que cela apporterait une belle synergie pour consommer des collections basées sur IDictionary.
  • C'est une structure plutôt qu'une classe donc pas d'exceptions NullReference
  • À un moment donné, PrioirtyQueue devrait être sérialisé/désérialisé et je pense qu'il serait plus facile de faire quoi avec un objet déjà existant.

L'inconvénient, ce sont des sentiments mitigés à la recherche de TPriority Key mais je pense qu'une bonne documentation peut résoudre ce problème.
`

En guise de solution de contournement pour Dijkstra, l'ajout de faux éléments dans la file d'attente fonctionne et le nombre total d'arêtes de graphique que vous traitez ne change pas. Mais le nombre de nœuds temporaires restant résidents dans le tas généré par le traitement des bords change, ce qui peut avoir un impact sur l'utilisation de la mémoire et l'efficacité de la mise en file d'attente et de la suppression de la file d'attente.

J'avais tort de ne pas pouvoir faire IReadOnlyCollection, ça devrait aller. Pas d'ajout () et de suppression () dans cette interface, hah ! (À quoi je pensais...)

@Ivanidzo4ka Vos commentaires me convainquent encore plus qu'il serait logique d'avoir deux types distincts : un simple tas binaire (c'est-à-dire sans mise à jour), et un second tel que décrit par l'une des propositions :

  • Les tas binaires sont simples, faciles à implémenter, ont une petite API et sont connus pour bien fonctionner dans la pratique dans de nombreux scénarios importants.
  • Une file d'attente prioritaire à part entière offre des avantages théoriques pour certains scénarios (c'est-à-dire une complexité spatiale réduite et une complexité temporelle réduite pour la recherche sur des graphiques densément connectés), et fournit une API naturelle pour un plus grand nombre d'algorithmes/scénarios.

Dans le passé, je m'en suis surtout tiré avec un tas binaire que j'ai écrit la meilleure partie il y a une décennie, et une combinaison SortedSet / Dictionary , et il y a des scénarios où j'ai utilisé les deux types dans des rôles séparés (KSSP vient à l'esprit).

C'est une structure plutôt qu'une classe donc pas d'exceptions NullReference

Je dirais que c'est l'inverse : si quelqu'un transmet des valeurs par défaut, j'aimerais qu'il voie un NRE ; cependant, je pense que nous devons être clairs sur l'endroit où nous nous attendons à ce que ces choses soient utilisées : les nœuds/descripteurs devraient probablement être des classes, mais si nous ne faisons que lire/retourner une paire, alors je suis d'accord que cela devrait être une structure.


Je suis tenté de suggérer qu'il devrait être possible de mettre à jour l'élément ainsi que la priorité dans la proposition basée sur les poignées. Vous pouvez obtenir le même effet en supprimant simplement un handle et en en ajoutant un nouveau, mais c'est une opération utile et peut avoir des avantages en termes de performances selon l'implémentation (par exemple, certains tas peuvent réduire la priorité de quelque chose à relativement bon marché). Un tel changement rendrait plus ordonné la mise en œuvre de nombreuses choses (par exemple, cet exemple induisant quelque peu cauchemardesque basé sur l'AlgoKit ParingHeap existant), en particulier ceux qui fonctionnent dans une région inconnue de l'espace d'état.

Proposition de file d'attente prioritaire (v2.1)

Sommaire

La communauté .NET Core propose d'ajouter à la bibliothèque système la fonctionnalité _priority queue_, une structure de données dans laquelle chaque élément a en plus une priorité qui lui est associée. Plus précisément, nous proposons d'ajouter PriorityQueue<TElement, TPriority> à l'espace System.Collections.Generic noms

Principes

Dans notre conception, nous avons été guidés par les principes suivants (à moins que vous n'en connaissiez de meilleurs) :

  • Large couverture. Nous voulons offrir aux clients .NET Core une structure de données précieuse qui est suffisamment polyvalente pour prendre en charge une grande variété de cas d'utilisation.
  • Apprenez des erreurs connues. Nous nous efforçons de fournir une fonctionnalité de file d'attente prioritaire qui serait exempte des problèmes rencontrés par les clients présents dans d'autres frameworks et langages, par exemple Java, Python, C++, Rust. Nous éviterons de faire des choix de conception connus pour rendre les clients mécontents et réduire l'utilité des files d'attente prioritaires.
  • Un soin extrême avec les décisions de porte à sens unique. Une fois qu'une API est introduite, elle ne peut pas être modifiée ou supprimée, seulement étendue. Nous analyserons soigneusement les choix de conception pour éviter les solutions sous-optimales avec lesquelles nos clients seront coincés pour toujours.
  • Évitez la paralysie de la conception. Nous acceptons qu'il n'y ait pas de solution parfaite. Nous équilibrerons les compromis et avancerons dans la livraison, pour enfin offrir à nos clients les fonctionnalités qu'ils attendent depuis des années.

Fond

Du point de vue d'un client

Conceptuellement, une file d'attente prioritaire est une collection d'éléments, où chaque élément a une priorité associée. La fonctionnalité la plus importante d'une file d'attente prioritaire est qu'elle fournit un accès efficace à l'élément ayant la priorité la plus élevée dans la collection et une option pour supprimer cet élément. Le comportement attendu peut également inclure : 1) la possibilité de modifier la priorité d'un élément qui est déjà dans la collection ; 2) possibilité de fusionner plusieurs files d'attente prioritaires.

Formation en informatique

Une file d'attente prioritaire est une structure de données abstraite, c'est-à-dire un concept avec certaines caractéristiques comportementales, comme décrit dans la section précédente. Les implémentations les plus efficaces d'une file d'attente prioritaire sont basées sur des tas. Cependant, contrairement à l'idée fausse générale, un tas est également une structure de données abstraite et peut être réalisé de différentes manières, chacune offrant des avantages et des inconvénients différents.

La plupart des ingénieurs logiciels ne connaissent que l'implémentation de tas binaire basée sur un tableau - c'est la plus simple, mais malheureusement pas la plus efficace. Pour une entrée aléatoire générale, deux exemples de types de tas plus efficaces sont : le tas quaternaire et le tas d'appariement . Pour plus d'informations sur les tas, veuillez vous référer à Wikipedia et à ce document .

Le mécanisme de mise à jour est le défi de conception clé

Nos discussions ont démontré que le domaine le plus difficile dans la conception, et en même temps avec le plus grand impact sur l'API, est le mécanisme de mise à jour. Concrètement, l'enjeu est de déterminer si et comment le produit que nous souhaitons proposer aux clients doit prendre en charge la mise à jour des priorités des éléments déjà présents dans la collection.

Une telle capacité est nécessaire pour implémenter, par exemple, l'algorithme du chemin le plus court de Dijkstra ou un planificateur de tâches qui doit gérer les priorités changeantes. Le mécanisme de mise à jour est absent de Java, ce qui s'est avéré décevant pour les ingénieurs, par exemple dans ces trois questions StackOverflow vues plus de 32 000 fois : example , example , example . Pour éviter d'introduire une API avec une valeur aussi limitée, nous pensons qu'une exigence fondamentale pour la fonctionnalité de file d'attente prioritaire que nous proposons serait de prendre en charge la capacité de mettre à jour les priorités pour les éléments déjà présents dans la collection.

Pour fournir le mécanisme de mise à jour, nous devons nous assurer que le client peut être précis sur ce qu'il souhaite exactement mettre à jour. Nous avons identifié deux manières de fournir ceci : a) via des poignées ; et b) en imposant l'unicité des éléments de la collection. Chacun d'entre eux présente des avantages et des coûts différents.

Option (a) : Poignées. Dans cette approche, chaque fois qu'un élément est ajouté à la file d'attente, la structure de données fournit son handle unique. Si le client souhaite utiliser le mécanisme de mise à jour, il doit garder une trace de ces descripteurs afin de pouvoir ultérieurement spécifier sans ambiguïté l'élément qu'il souhaite mettre à jour. Le coût principal de cette solution est que les clients doivent gérer ces pointeurs. Cependant, cela ne signifie pas qu'il doit y avoir des allocations internes pour prendre en charge les descripteurs dans la file d'attente prioritaire - tout tas non basé sur un tableau est basé sur des nœuds, où chaque nœud est automatiquement son propre descripteur. A titre d'exemple, voir l'API de la méthode PairingHeap.Update .

Option (b) : Unicité. Cette approche impose deux contraintes supplémentaires au client : i) les éléments dans la file d'attente prioritaire doivent se conformer à une certaine sémantique d'égalité, ce qui amène une nouvelle classe de bogues utilisateurs potentiels ; ii) deux éléments égaux ne peuvent pas être stockés dans la même file d'attente. En payant ce coût, nous obtenons l'avantage de prendre en charge le mécanisme de mise à jour sans recourir à l'approche du handle. Cependant, toute implémentation qui exploite l'unicité/l'égalité pour déterminer l'élément à mettre à jour nécessitera un mappage interne supplémentaire, afin qu'il soit effectué en O(1) et non en O(n).

Recommandation

Nous vous recommandons d'ajouter à la bibliothèque système une classe PriorityQueue<TElement, TPriority> qui prend en charge le mécanisme de mise à jour via des handles. L' implémentation sous-jacente serait un tas d'appariement.

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

Exemple d'utilisation

1) Client qui ne se soucie pas du mécanisme de mise à jour

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) Client soucieux du mécanisme de mise à jour

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

FAQ

1. Dans quel ordre la file d'attente prioritaire énumère-t-elle les éléments ?

Dans un ordre indéfini, de sorte que l'énumération puisse se produire en O(n), de la même manière qu'avec un HashSet . Aucune implémentation efficace ne fournirait des capacités pour énumérer un tas en temps linéaire tout en garantissant que les éléments sont énumérés dans l'ordre - cela nécessiterait O(n log n). Étant donné que la commande sur une collection peut être réalisée de manière triviale avec .OrderBy(x => x.Priority) et que tous les clients ne se soucient pas de l'énumération avec cette commande, nous pensons qu'il est préférable de fournir un ordre d'énumération non défini.

2. Pourquoi n'y a-t-il pas TryGet méthode Contains ou TryGet ?

Fournir de telles méthodes a une valeur négligeable, car la recherche d'un élément dans un tas nécessite l'énumération de la collection entière, ce qui signifie que toute méthode Contains ou TryGet serait un wrapper autour de l'énumération. De plus, pour vérifier si un élément existe dans la collection, la file d'attente prioritaire devrait savoir comment effectuer des contrôles d'égalité des objets TElement , ce qui, selon nous, ne devrait pas relever de la responsabilité d'une file d'attente prioritaire.

3. Pourquoi y a-t-il des surcharges Dequeue et TryDequeue qui renvoient PriorityQueueNode ?

Ceci est destiné aux clients qui souhaitent utiliser la méthode Update ou Remove et garder une trace des poignées. Lors de la suppression d'un élément de la file d'attente prioritaire, ils recevraient un descripteur qu'ils pourraient utiliser pour ajuster l'état de leur système de suivi de descripteurs.

4. Que se passe-t-il lorsque la méthode Update ou Remove reçoit un nœud d'une file d'attente différente ?

La file d'attente prioritaire lèvera une exception. Chaque nœud connaît la file d'attente prioritaire à laquelle il appartient, de la même manière qu'un LinkedListNode<T> connaît les LinkedList<T> auxquels il appartient.

Annexes

Annexe A : Autres langues avec fonctionnalité de file d'attente prioritaire

| Langue | Type | Remarques |
|:-:|:-:|:-:|
| Java | File d'attente de priorité | Étend la classe abstraite AbstractQueue et implémente l'interface Queue . |
| Rouille | BinaireHeap | |
| Rapide | CFBinaryHeap | |
| C++ | file_priorité | |
| Python | heapq | |
| Aller | tas | Il y a une interface de tas. |

Annexe B : découvrabilité

Notez que lors de l'examen des structures de données, le terme _heap_ est utilisé 4 fois plus souvent que _file d'attente prioritaire_.

  • "array" AND "data structure" — 17.400.000 résultats
  • "stack" AND "data structure" — 12.100.000 résultats
  • "queue" AND "data structure" — 3.850.000 résultats
  • "heap" AND "data structure" — 1.830.000 résultats
  • "priority queue" AND "data structure" — 430.000 résultats
  • "trie" AND "data structure" — 335.000 résultats

Merci à tous pour les commentaires! J'ai mis à jour la proposition vers la v2.1. Journal des modifications :

  • Suppression des méthodes Contains et TryGet .
  • Ajout d'une FAQ #2 : _Pourquoi n'y a-t-il pas de méthode Contains ou TryGet ?_
  • Ajout de l'interface IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>> .
  • Ajout d'une surcharge de bool TryPeek(out TElement element, out TPriority priority) .
  • Ajout d'une surcharge de bool TryPeek(out TElement element) .
  • Ajout d'une surcharge de void Dequeue(out TElement element, out TPriority priority) .
  • Changé void Dequeue(out TElement element) en PriorityQueueNode<TElement, TPriority> Dequeue() .
  • Ajout d'une surcharge de bool TryDequeue(out TElement element) .
  • Ajout d'une surcharge de bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node) .
  • Ajout d'une FAQ n°3 : _Pourquoi y a-t-il des surcharges Dequeue et TryDequeue qui renvoient PriorityQueueNode ?_
  • Ajout d'une FAQ n°4 : _Que se passe-t-il lorsque la méthode Update ou Remove reçoit un nœud d'une file d'attente différente ?_

Merci pour les notes de changement ;)

Petites demandes d'éclaircissements :

  • Pouvons-nous qualifier la FAQ 4 de telle sorte qu'elle soit valable pour les éléments qui ne sont pas dans la file d'attente ? (c'est-à-dire ceux supprimés)
  • Pouvons-nous ajouter une FAQ sur la stabilité, c'est-à-dire les garanties (le cas échéant) lors de la suppression d'éléments avec la même priorité (je crois comprendre qu'il n'est pas prévu de garantir, ce qui est important à savoir pour, par exemple, la planification).

@pgolebiowski Concernant la méthode Merge proposée :

public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)

Il est clair qu'une telle opération n'aurait pas de sémantique de copie, donc je me demande s'il y aurait des pièges autour de la modification de this et other _après_ une fusion a été effectuée (par exemple, l'une ou l'autre instance échoue pour satisfaire la propriété du tas).

@eiriktsarpalis @VisualMelon — Merci ! Abordera les points soulevés, ETA 2020-10-04.

Si d'autres ont d'autres commentaires/questions/préoccupations/réflexions — veuillez partager 😊

Proposition de file d'attente prioritaire (v2.2)

Sommaire

La communauté .NET Core propose d'ajouter à la bibliothèque système la fonctionnalité _priority queue_, une structure de données dans laquelle chaque élément a en plus une priorité qui lui est associée. Plus précisément, nous proposons d'ajouter PriorityQueue<TElement, TPriority> à l'espace System.Collections.Generic noms

Principes

Dans notre conception, nous avons été guidés par les principes suivants (à moins que vous n'en connaissiez de meilleurs) :

  • Large couverture. Nous voulons offrir aux clients .NET Core une structure de données précieuse qui est suffisamment polyvalente pour prendre en charge une grande variété de cas d'utilisation.
  • Apprenez des erreurs connues. Nous nous efforçons de fournir une fonctionnalité de file d'attente prioritaire qui serait exempte des problèmes rencontrés par les clients présents dans d'autres frameworks et langages, par exemple Java, Python, C++, Rust. Nous éviterons de faire des choix de conception connus pour rendre les clients mécontents et réduire l'utilité des files d'attente prioritaires.
  • Un soin extrême avec les décisions de porte à sens unique. Une fois qu'une API est introduite, elle ne peut pas être modifiée ou supprimée, seulement étendue. Nous analyserons soigneusement les choix de conception pour éviter les solutions sous-optimales avec lesquelles nos clients seront coincés pour toujours.
  • Évitez la paralysie de la conception. Nous acceptons qu'il n'y ait pas de solution parfaite. Nous équilibrerons les compromis et avancerons dans la livraison, pour enfin offrir à nos clients les fonctionnalités qu'ils attendent depuis des années.

Fond

Du point de vue d'un client

Conceptuellement, une file d'attente prioritaire est une collection d'éléments, où chaque élément a une priorité associée. La fonctionnalité la plus importante d'une file d'attente prioritaire est qu'elle fournit un accès efficace à l'élément ayant la priorité la plus élevée dans la collection et une option pour supprimer cet élément. Le comportement attendu peut également inclure : 1) la possibilité de modifier la priorité d'un élément qui est déjà dans la collection ; 2) possibilité de fusionner plusieurs files d'attente prioritaires.

Formation en informatique

Une file d'attente prioritaire est une structure de données abstraite, c'est-à-dire un concept avec certaines caractéristiques comportementales, comme décrit dans la section précédente. Les implémentations les plus efficaces d'une file d'attente prioritaire sont basées sur des tas. Cependant, contrairement à l'idée fausse générale, un tas est également une structure de données abstraite et peut être réalisé de différentes manières, chacune offrant des avantages et des inconvénients différents.

La plupart des ingénieurs logiciels ne connaissent que l'implémentation de tas binaire basée sur un tableau - c'est la plus simple, mais malheureusement pas la plus efficace. Pour une entrée aléatoire générale, deux exemples de types de tas plus efficaces sont : le tas quaternaire et le tas d'appariement . Pour plus d'informations sur les tas, veuillez vous référer à Wikipedia et à ce document .

Le mécanisme de mise à jour est le défi de conception clé

Nos discussions ont démontré que le domaine le plus difficile dans la conception, et en même temps avec le plus grand impact sur l'API, est le mécanisme de mise à jour. Concrètement, l'enjeu est de déterminer si et comment le produit que nous souhaitons proposer aux clients doit prendre en charge la mise à jour des priorités des éléments déjà présents dans la collection.

Une telle capacité est nécessaire pour implémenter, par exemple, l'algorithme du chemin le plus court de Dijkstra ou un planificateur de tâches qui doit gérer les priorités changeantes. Le mécanisme de mise à jour est absent de Java, ce qui s'est avéré décevant pour les ingénieurs, par exemple dans ces trois questions StackOverflow vues plus de 32 000 fois : example , example , example . Pour éviter d'introduire une API avec une valeur aussi limitée, nous pensons qu'une exigence fondamentale pour la fonctionnalité de file d'attente prioritaire que nous proposons serait de prendre en charge la capacité de mettre à jour les priorités pour les éléments déjà présents dans la collection.

Pour fournir le mécanisme de mise à jour, nous devons nous assurer que le client peut être précis sur ce qu'il souhaite exactement mettre à jour. Nous avons identifié deux manières de fournir ceci : a) via des poignées ; et b) en imposant l'unicité des éléments de la collection. Chacun d'entre eux présente des avantages et des coûts différents.

Option (a) : Poignées. Dans cette approche, chaque fois qu'un élément est ajouté à la file d'attente, la structure de données fournit son handle unique. Si le client souhaite utiliser le mécanisme de mise à jour, il doit garder une trace de ces descripteurs afin de pouvoir ultérieurement spécifier sans ambiguïté l'élément qu'il souhaite mettre à jour. Le coût principal de cette solution est que les clients doivent gérer ces pointeurs. Cependant, cela ne signifie pas qu'il doit y avoir des allocations internes pour prendre en charge les descripteurs dans la file d'attente prioritaire - tout tas non basé sur un tableau est basé sur des nœuds, où chaque nœud est automatiquement son propre descripteur. A titre d'exemple, voir l'API de la méthode PairingHeap.Update .

Option (b) : Unicité. Cette approche impose deux contraintes supplémentaires au client : i) les éléments dans la file d'attente prioritaire doivent se conformer à une certaine sémantique d'égalité, ce qui amène une nouvelle classe de bogues utilisateurs potentiels ; ii) deux éléments égaux ne peuvent pas être stockés dans la même file d'attente. En payant ce coût, nous obtenons l'avantage de prendre en charge le mécanisme de mise à jour sans recourir à l'approche du handle. Cependant, toute implémentation qui exploite l'unicité/l'égalité pour déterminer l'élément à mettre à jour nécessitera un mappage interne supplémentaire, afin qu'il soit effectué en O(1) et non en O(n).

Recommandation

Nous vous recommandons d'ajouter à la bibliothèque système une classe PriorityQueue<TElement, TPriority> qui prend en charge le mécanisme de mise à jour via des handles. L' implémentation sous-jacente serait un tas d'appariement.

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

Exemple d'utilisation

1) Client qui ne se soucie pas du mécanisme de mise à jour

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) Client soucieux du mécanisme de mise à jour

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

FAQ

1. Dans quel ordre la file d'attente prioritaire énumère-t-elle les éléments ?

Dans un ordre indéfini, de sorte que l'énumération puisse se produire en O(n), de la même manière qu'avec un HashSet . Aucune implémentation efficace ne fournirait des capacités pour énumérer un tas en temps linéaire tout en garantissant que les éléments sont énumérés dans l'ordre - cela nécessiterait O(n log n). Étant donné que la commande sur une collection peut être réalisée de manière triviale avec .OrderBy(x => x.Priority) et que tous les clients ne se soucient pas de l'énumération avec cette commande, nous pensons qu'il est préférable de fournir un ordre d'énumération non défini.

2. Pourquoi n'y a-t-il pas TryGet méthode Contains ou TryGet ?

Fournir de telles méthodes a une valeur négligeable, car la recherche d'un élément dans un tas nécessite l'énumération de la collection entière, ce qui signifie que toute méthode Contains ou TryGet serait un wrapper autour de l'énumération. De plus, pour vérifier si un élément existe dans la collection, la file d'attente prioritaire devrait savoir comment effectuer des contrôles d'égalité des objets TElement , ce qui, selon nous, ne devrait pas relever de la responsabilité d'une file d'attente prioritaire.

3. Pourquoi y a-t-il des surcharges Dequeue et TryDequeue qui renvoient PriorityQueueNode ?

Ceci est destiné aux clients qui souhaitent utiliser la méthode Update ou Remove et garder une trace des poignées. Lors de la suppression d'un élément de la file d'attente prioritaire, ils recevraient un descripteur qu'ils pourraient utiliser pour ajuster l'état de leur système de suivi de descripteurs.

4. Que se passe-t-il lorsque la méthode Update ou Remove reçoit un nœud d'une file d'attente différente ?

La file d'attente prioritaire lèvera une exception. Chaque nœud connaît la file d'attente prioritaire à laquelle il appartient, de la même manière qu'un LinkedListNode<T> connaît les LinkedList<T> auxquels il appartient. De plus, si un nœud a été supprimé d'une file d'attente, essayer d'invoquer Update ou Remove dessus entraînera également une exception.

5. Pourquoi n'y a-t-il pas Merge méthode

La fusion de deux files d'attente prioritaires peut être réalisée en un temps constant, ce qui en fait une fonctionnalité tentante à offrir aux clients. Cependant, nous n'avons aucune donnée pour démontrer qu'il existe une demande pour une telle fonctionnalité, et nous ne pouvons justifier de l'inclure dans l'API publique. De plus, la conception d'une telle fonctionnalité n'est pas triviale et, étant donné que cette fonctionnalité n'est peut-être pas nécessaire, elle pourrait inutilement compliquer la surface et la mise en œuvre de l'API.

Néanmoins, ne pas inclure la méthode Merge est maintenant une porte à double sens — si à l'avenir les clients expriment leur intérêt à ce que la fonctionnalité de fusion soit prise en charge, il sera possible d'étendre le type PriorityQueue . En tant que tel, nous vous recommandons de ne pas encore inclure la méthode Merge et d'aller de l'avant avec le lancement.

6. La collection offre-t-elle une garantie de stabilité ?

La collecte n'offrira pas une garantie de stabilité en sortie de boîte, c'est-à-dire que si deux éléments sont mis en file d'attente avec la même priorité, le client ne pourra pas supposer qu'ils seront retirés de la file d'attente dans un certain ordre. Cependant, si un client souhaite atteindre la stabilité en utilisant notre PriorityQueue , il peut définir un TPriority et le IComparer<TPriority> qui le garantirait. De plus, la collecte des données sera déterministe, c'est-à-dire que pour une séquence d'opérations donnée, elle se comportera toujours de la même manière, permettant une reproductibilité.

Annexes

Annexe A : Autres langues avec fonctionnalité de file d'attente prioritaire

| Langue | Type | Remarques |
|:-:|:-:|:-:|
| Java | File d'attente de priorité | Étend la classe abstraite AbstractQueue et implémente l'interface Queue . |
| Rouille | BinaireHeap | |
| Rapide | CFBinaryHeap | |
| C++ | file_priorité | |
| Python | heapq | |
| Aller | tas | Il y a une interface de tas. |

Annexe B : découvrabilité

Notez que lors de l'examen des structures de données, le terme _heap_ est utilisé 4 fois plus souvent que _file d'attente prioritaire_.

  • "array" AND "data structure" — 17.400.000 résultats
  • "stack" AND "data structure" — 12.100.000 résultats
  • "queue" AND "data structure" — 3.850.000 résultats
  • "heap" AND "data structure" — 1.830.000 résultats
  • "priority queue" AND "data structure" — 430.000 résultats
  • "trie" AND "data structure" — 335.000 résultats

Journal des modifications :

  • Suppression de la méthode void Merge(PriorityQueue<TElement, TPriority> other) // O(1) .
  • Ajout d'une FAQ #5 : Pourquoi n'y a-t-il pas Merge méthode
  • Modification de la FAQ n°4 à conserver également pour les nœuds qui ont été supprimés de la file d'attente prioritaire.
  • Ajout d'une FAQ n°6 : La collection offre-t-elle une garantie de stabilité ?

La nouvelle FAQ a fière allure. J'ai joué avec le codage de Dijkstra par rapport à l'API proposée, avec un dictionnaire de poignées, et cela semblait fondamentalement bien.

Le seul petit apprentissage que j'ai tiré de cette opération est que l'ensemble actuel de noms de méthodes/surcharges ne fonctionne pas aussi bien pour le typage implicite des variables out . Ce que je voulais faire avec le code C# était TryDequeue(out var node) - mais malheureusement je devais donner le type explicite de la variable out comme PriorityQueueNode<> sinon le compilateur ne savait pas si je voulait un nœud de file d'attente prioritaire ou un élément.

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

À mon avis, la principale question de conception non résolue est de savoir si la mise en œuvre doit ou non prendre en charge les mises à jour prioritaires. Je vois ici trois chemins possibles :

  1. Exiger l'unicité/l'égalité et prendre en charge les mises à jour en transmettant l'élément d'origine.
  2. Prise en charge des mises à jour prioritaires à l'aide de poignées.
  3. Ne prend pas en charge les mises à jour prioritaires.

Ces approches sont mutuellement exclusives et ont leurs propres ensembles de compromis, respectivement :

  1. L'approche basée sur l'égalité complique le contrat API en forçant l'unicité et nécessite une comptabilité supplémentaire sous le capot. Cela se produit que l'utilisateur ait besoin ou non de mises à jour prioritaires.
  2. L'approche basée sur le handle impliquerait au moins une allocation supplémentaire par élément mis en file d'attente. Bien qu'il n'impose pas l'unicité, j'ai l'impression que pour les scénarios nécessitant des mises à jour, cet invariant est presque certainement implicite (par exemple, notez comment les deux exemples répertoriés ci-dessus stockent les descripteurs dans des dictionnaires externes indexés par les éléments eux-mêmes).
  3. Les mises à jour ne sont pas du tout prises en charge ou nécessiteraient une traversée linéaire du tas. En cas d'éléments en double, les mises à jour peuvent être ambiguës.

Identification des applications PriorityQueue courantes

Il est important que nous identifiions laquelle des approches ci-dessus pourrait offrir la meilleure valeur pour la plupart de nos utilisateurs. J'ai donc parcouru les bases de code .NET, à la fois internes et publiques de Microsoft, pour les instances d'implémentations Heap/PriorityQueue afin de mieux comprendre quels sont les modèles d'utilisation les plus courants :

La plupart des applications implémentent des variantes de tri par tas ou de planification des tâches. Quelques instances ont été utilisées pour des algorithmes tels que le tri topologique ou le codage de Huffman. Un nombre plus petit a été utilisé pour calculer les distances dans les graphiques. Sur les 80 implémentations PriorityQueue examinées, seules 9 ont mis en œuvre une forme de mise à jour prioritaire.

Dans le même temps, aucune des implémentations Heap/PriorityQueue dans les bibliothèques principales Python, Java, C++, Go, Swift ou Rust ne prend en charge les mises à jour prioritaires dans leurs API.

Recommandations

À la lumière de ces données, il est clair pour moi que .ΝΕΤ a besoin d'une implémentation de base de PriorityQueue qui expose l'API essentielle (heapify/push/peek/pop), offre certaines garanties de performances (par exemple, aucune allocation supplémentaire par mise en file d'attente) et n'applique pas contraintes d'unicité. Cela implique que la mise en œuvre _ne prendrait pas en charge les mises à jour prioritaires O(log n)_.

Nous devrions également envisager de poursuivre avec une implémentation de tas distincte qui prend en charge les mises à jour/suppressions _O(log n)_ et utilise une approche basée sur l'égalité. Puisqu'il s'agirait d'un type spécialisé, l'exigence d'unicité ne devrait pas être un gros problème.

J'ai travaillé sur le prototypage des deux implémentations et je ferai bientôt une proposition d'API.

Merci beaucoup pour l'analyse, @eiriktsarpalis ! J'apprécie en particulier le temps passé à analyser la base de code interne de Microsoft pour trouver des cas d'utilisation pertinents et soutenir notre discussion avec des données.

L'approche basée sur le handle impliquerait au moins une allocation supplémentaire par élément mis en file d'attente.

Cette hypothèse est fausse, vous n'avez pas besoin d'allocations supplémentaires dans les tas basés sur les nœuds. Le tas de couplage est plus performant pour une entrée aléatoire générale que le tas binaire basé sur un tableau, ce qui signifie que vous auriez intérêt à utiliser ce tas basé sur des nœuds même pour une file d'attente prioritaire qui ne prend pas en charge les mises à jour. Vous pouvez voir des repères dans l'article auquel j'ai fait référence plus tôt .

Sur les 80 implémentations PriorityQueue examinées, seules 9 ont mis en œuvre une forme de mise à jour prioritaire.

Même compte tenu de ce petit échantillon, cela représente 11 à 12 % de toutes les utilisations. En outre, cela peut être sous-représenté dans certains domaines comme par exemple les jeux vidéo, où je m'attendrais à ce que ce pourcentage soit plus élevé.

À la lumière de ces données, il est clair pour moi que [...]

Je ne pense pas qu'une telle conclusion soit claire, étant donné que l'une des hypothèses clés est fausse et que l'on peut se demander si 11 à 12 % des clients constituent un cas d'utilisation suffisamment important ou non. Ce qui me manque dans votre évaluation, c'est l'évaluation de l'impact sur les coûts de la "structure de données prenant en charge les mises à jour" pour les clients qui ne se soucient pas de ce mécanisme - ce qui pour moi, ce coût est négligeable, car ils pourraient utiliser la structure de données sans être affecté par le mécanisme de la poignée.

Essentiellement:

| | 11-12% cas d'utilisation | 88 à 89 % des cas d'utilisation |
|:-:|:-:|:-:|
| se soucie des mises à jour | oui | non |
| est affecté négativement par les poignées | N/A (ils sont souhaités) | non |
| est positivement affecté par les poignées | oui | non |

Pour moi, c'est une évidence en faveur de la prise en charge de 100 % des cas d'utilisation, et pas seulement de 88 à 89 %.

Cette hypothèse est fausse, vous n'avez pas besoin d'allocations supplémentaires dans les tas basés sur les nœuds

Si la priorité et l'élément sont tous deux des types de valeur (ou si les deux sont des types de référence que vous ne possédez pas et/ou ne pouvez pas modifier le type de base), pouvez-vous établir un lien vers une implémentation qui démontre qu'aucune allocation supplémentaire n'est requise (ou décrivez simplement comment il est atteint) ? Ce serait utile de voir. Merci.

Ce serait utile si vous pouviez développer davantage, ou simplement dire ce que vous essayez de dire. Il faudrait que je lève l'ambiguïté, il y aurait du ping-pong, et cela deviendrait une longue discussion. Alternativement, nous pourrions organiser un appel.

Je dis que nous voulons éviter toute opération de mise en file d'attente nécessitant une allocation, que ce soit de la part de l'appelant ou de la part de l'implémentation (une allocation interne amortie convient, par exemple pour étendre un tableau utilisé dans l'implémentation). J'essaie de comprendre comment cela est possible avec un tas basé sur des nœuds (par exemple, si ces objets de nœud sont exposés à l'appelant, cela interdit la mise en commun par l'implémentation en raison de problèmes de réutilisation / alias inappropriés). Je veux pouvoir écrire :
C# pq.Enqueue(42, 84);
et ne pas allouer. Comment les implémentations auxquelles vous faites référence y parviennent-elles ?

ou dites simplement ce que vous essayez de dire

Je pensais que je l'étais.

nous voulons éviter toute opération de mise en file d'attente nécessitant une allocation [...] Je veux pouvoir écrire : pq.Enqueue(42, 84); et que cela ne soit pas alloué.

D'où vient ce désir ? C'est bien d'avoir un effet secondaire d'une solution, pas une exigence que 99,9% des clients doivent satisfaire. Je ne vois pas pourquoi vous choisiriez cette dimension à faible impact pour faire des choix de conception entre les solutions.

Nous ne faisons pas de choix de conception basés sur des optimisations pour les 0,1 % de clients si celles-ci ont un impact négatif sur 12 % des clients dans une autre dimension. « se soucier de l'absence d'allocations » + «                                                                                                                                                                                                                                                                                        .

Je trouve la dimension du comportement/fonctionnalité pris en charge beaucoup plus importante, en particulier lors de la conception d'une structure de données polyvalente à usage général pour un large public et une variété de cas d'utilisation.

D'où vient ce désir ?

De vouloir que les types de collection de base soient utilisables dans des scénarios soucieux des performances. Vous dites que la solution basée sur les nœuds prendrait en charge 100 % des cas d'utilisation : ce n'est pas le cas si chaque mise en file d'attente alloue, tout comme List<T> , Dictionary<TKey, TValue> , HashSet<T> , et ainsi de suite on deviendrait inutilisable dans de nombreuses situations s'ils étaient alloués à chaque Add.

Pourquoi pensez-vous que seuls « 0,1 % » se soucient des frais généraux d'allocation de ces méthodes ? D'où viennent ces données ?

"se soucier de l'absence d'allocations" + "s'occuper de deux types de valeurs" est un cas limite

Ce n'est pas. Il ne s'agit pas non plus seulement de « deux types de valeur ». D'après ce que je comprends, la solution proposée nécessiterait soit a) une allocation sur chaque file d'attente indépendamment des T impliqués, soit b) exigerait que le type d'élément dérive d'un type de base connu qui à son tour interdit un grand nombre d'utilisations possibles pour éviter une allocation supplémentaire.

@eiriktsarpalis
Donc vous n'oubliez aucune option, je pense qu'il y a une option faisable 4 à ajouter aux options 1, 2 et 3, dans votre liste qui est un compromis :

  1. une implémentation qui prend en charge le cas d'utilisation de 12 %, tout en optimisant presque les 88 % restants en autorisant les mises à jour d'éléments équivalents, et en ne construisant _paresseusement_ que la table de recherche requise pour effectuer ces mises à jour la première fois qu'une méthode de mise à jour est appelée ( et le mettre à jour sur les mises à jour + suppressions de sous-séquences). Par conséquent, encourir moins de coûts pour les applications qui n'utilisent pas la fonctionnalité.

Nous pourrions toujours décider que parce qu'il y a des performances supplémentaires disponibles pour les 88% ou 12% d'une implémentation qui n'a pas besoin d'une structure de données actualisable, ou est optimisée pour une en premier lieu, il est préférable de fournir les options 2 et 3, que option 4. Mais j'ai pensé que nous ne devrions pas oublier qu'une autre option existe.

[Ou je suppose que vous pourriez simplement voir cela comme une meilleure option 1 et mettre à jour la description de 1 pour dire que la comptabilité n'est pas forcée, mais paresseuse et qu'un comportement équivalent correct n'est requis que lorsque des mises à jour sont utilisées...]

@stephentoub C'est exactement ce que j'avais en tête de dire simplement ce que vous voulez dire, merci :)

Pourquoi pensez-vous que seuls 0,1 % se soucient des frais généraux d'allocation de ces méthodes ? D'où viennent ces données ?

De l'intuition, c'est-à-dire de la même source sur la base de laquelle vous pensez qu'il est plus important de prioriser "aucune allocation supplémentaire" plutôt que "capacité à effectuer des mises à jour". Au moins pour le mécanisme de mise à jour, nous avons les données dont 11 à 12% des clients ont besoin pour que ce comportement soit pris en charge. Je ne pense pas que les clients proches à distance se soucieraient de "aucune allocation supplémentaire".

Dans les deux cas, vous choisissez pour une raison quelconque de vous concentrer sur la dimension mémoire, en oubliant d'autres dimensions, par exemple la vitesse brute, ce qui est un autre compromis pour votre approche préférée. Une implémentation basée sur un tableau ne fournissant « aucune allocation supplémentaire » serait plus lente qu'une implémentation basée sur des nœuds. Encore une fois, je pense qu'il est arbitraire ici de privilégier la mémoire à la vitesse.

Prenons du recul et concentrons-nous sur ce que veulent les clients. Nous avons un choix de conception qui peut ou non rendre la structure de données inutilisable pour 12% des clients. Je pense que nous devrions être très prudents en fournissant des raisons pour lesquelles nous choisirions de ne pas les soutenir.

Une implémentation basée sur un tableau ne fournissant « aucune allocation supplémentaire » serait plus lente qu'une implémentation basée sur des nœuds.

Veuillez partager les deux implémentations C# que vous utilisez pour effectuer cette comparaison et les références utilisées pour arriver à cette conclusion. Les articles théoriques ont certainement de la valeur, mais ils ne sont qu'une petite pièce du puzzle. La chose la plus importante est lorsque le caoutchouc rencontre la route, en tenant compte des détails de la plate-forme donnée et des implémentations données, et que vous êtes en mesure de valider sur la plate-forme spécifique avec l'implémentation spécifique et les ensembles de données / modèles d'utilisation typiques/attendus. Il se peut très bien que votre affirmation soit correcte. Ce n'est peut-être pas le cas non plus. J'aimerais voir les implémentations/données pour mieux comprendre.

Veuillez partager les deux implémentations C# que vous utilisez pour effectuer cette comparaison et les références utilisées pour arriver à cette conclusion

C'est un point valable, l'article que je cite ne fait que comparer et comparer les implémentations en C++. Il effectue plusieurs benchmarks avec différents ensembles de données et modèles d'utilisation. Je suis assez confiant que cela serait transférable à C#, mais si vous pensez que c'est quelque chose que nous devons doubler, je pense que la meilleure chose à faire serait que vous demandiez à un collègue de mener une telle étude.

@pgolebiowski Je serais intéressé à mieux comprendre la nature de votre objection. La proposition préconise deux types distincts, cela ne couvrirait-il pas vos besoins ?

  1. une implémentation qui prend en charge le cas d'utilisation de 12 %, tout en optimisant presque les 88 % restants en autorisant les mises à jour d'éléments équivalents et en ne créant que paresseusement la table de recherche requise pour effectuer ces mises à jour la première fois qu'une méthode de mise à jour est appelée ( et le mettre à jour sur les mises à jour + suppressions de sous-séquences). Par conséquent, encourir moins de coûts pour les applications qui n'utilisent pas la fonctionnalité.

Je classerais probablement cela comme une optimisation des performances pour l'option 1, mais je vois quelques problèmes avec cette approche particulière :

  • La mise à jour devient désormais _O(n)_, ce qui peut entraîner des performances imprévisibles en fonction des modèles d'utilisation.
  • La table de recherche est également nécessaire pour valider l'unicité. Mettre en file d'attente le même élément deux fois _avant_ d'appeler Update serait accepté et amènerait sans doute la file d'attente à un état incohérent.

@eiriktsarpalis C'est seulement O (n) une fois, et O (1) ensuite, qui est O (1) amorti. Et vous pouvez reporter la validation de l'unicité jusqu'à la première mise à jour. Mais c'est peut-être trop intelligent. Deux classes est plus facile à expliquer.

J'ai passé les derniers jours à prototyper deux implémentations PriorityQueue : une implémentation de base sans prise en charge des mises à jour et une implémentation qui prend en charge les mises à jour en utilisant l'égalité des éléments. J'ai nommé le premier PriorityQueue et le dernier, faute d'un meilleur nom, PrioritySet . Mon objectif est de jauger l'ergonomie des API et de comparer les performances.

Les implémentations peuvent être trouvées dans ce référentiel . Les deux classes sont implémentées à l'aide de tas quads basés sur des tableaux. L'implémentation pouvant être mise à jour utilise également un dictionnaire qui mappe les éléments aux indices de tas internes.

File d'attente prioritaire de base

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

Voici un exemple de base utilisant le type

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 modifiable

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

Comparaison des performances

J'ai écrit un simple benchmark de tri de tas qui compare les deux implémentations dans leur application la plus basique. J'ai également inclus un benchmark de tri qui utilise Linq à des fins de comparaison :

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éthode | Taille | moyenne | Erreur | DevStd | Rapport | RatioSD | Génération 0 | Génération 1 | Gen 2 | Attribué |
|-------------- |------ |-------------:|-----------: |-----------:|------:|--------:|--------:|-------- :|--------:|----------:|
| LinqSort | 30 | 1.439 us | 0,0072 us | 0,0064 nous | 1,00 | 0,00 | 0,0095 | - | - | 672B |
| PrioritéQueue | 30 | 1.450 us | 0,0085 us | 0,0079 nous | 1.01 | 0,01 | - | - | - | - |
| PrioritySet | 30 | 2,778 us | 0,0217 nous | 0,0192 us | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 300 | 24.727 us | 0,1032 nous | 0,0915 us | 1,00 | 0,00 | 0,0305 | - | - | 3912 B |
| PrioritéQueue | 300 | 29.510 nous | 0,0995 us | 0,0882 us | 1.19 | 0,01 | - | - | - | - |
| PrioritySet | 300 | 47.715 us | 0.4455 nous | 0,4168 us | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015 us | 1.5495 nous | 1.3736 nous | 1,00 | 0,00 | 0,4883 | - | - | 36312 B |
| PrioritéQueue | 3000 | 491.722 us | 4.1463 nous | 3,8785 us | 1.19 | 0,01 | - | - | - | - |
| PrioritySet | 3000 | 677.959 us | 3.1996 nous | 2.4981 nous | 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 | 360910B |
| PrioritéQueue | 30000 | 5,688,625 us | 53.0746 nous | 49.6460 nous | 1.09 | 0,01 | - | - | - | 2B |
| PrioritySet | 30000 | 8 124,306 us | 39.9498 us | 37.3691 us | 1,55 | 0,01 | - | - | - | 4B |

Comme on peut s'y attendre, la surcharge des emplacements des éléments de suivi ajoute un impact significatif sur les performances, environ 40 à 50 % plus lent par rapport à la mise en œuvre de base.

J'apprécie tous les efforts, je vois que cela a pris beaucoup de temps et d'énergie.

  1. Je ne vois vraiment pas la raison de 2 structures de données presque identiques, où l'une est une version inférieure de l'autre.
  2. De plus, même si vous voudriez avoir ces 2 versions d'une file d'attente prioritaire, je ne vois pas en quoi la version "supérieure" est meilleure que la proposition de file d'attente prioritaire (v2.2) d'il y a 20 jours.

tl;dr :

  • Cette proposition est passionnante !! Mais cela ne correspond pas encore à mes cas d'utilisation à hautes performances.
  • > 90 % de ma charge de performance de planification de géométrie/mouvement de calcul est mise en file d'attente/sortie PQ car c'est le N^m LogN dominant dans un algorithme.
  • Je suis en faveur d'implémentations PQ séparées. Je n'ai généralement pas besoin de mises à jour prioritaires, et des performances 2x pires sont inacceptables.
  • PrioritySet est un nom déroutant et n'est pas détectable par rapport à PriorityQueue
  • Stocker la priorité deux fois (une fois dans mon élément, une fois dans la file d'attente) semble coûteux. Copies de structure et utilisation de la mémoire.
  • Si la priorité de calcul était chère, je stockerais simplement un tuple (priority: ComputePriority(element), element) dans un PQ, et ma fonction de recherche de priorité serait simplement tuple => tuple.priority .
  • Les performances doivent être évaluées par opération ou alternativement sur des cas d'utilisation réels (par exemple, recherche optimisée multi-start multi-end sur un graphique)
  • Le comportement d'énumération non ordonnée est inattendu. Vous préférez la sémantique d'ordre Dequeue() de type file d'attente ?
  • Envisagez de prendre en charge les opérations de clonage et de fusion.
  • Les opérations de base doivent être allouées à 0 en utilisation en régime permanent. Je vais mettre en commun ces files d'attente.
  • Envisagez de prendre en charge EnqueueMany qui effectue heapify pour faciliter la mise en commun.

Je travaille dans la recherche haute performance (planification de mouvement) et le code de géométrie computationnelle (par exemple, les algorithmes de balayage) qui sont pertinents à la fois pour la robotique et les jeux, j'utilise beaucoup de files d'attente prioritaires personnalisées. L'autre cas d'utilisation courant que j'ai est une requête Top-K où la priorité de mise à jour n'est pas utile.

Quelques retours concernant le débat sur les deux implémentations (oui vs pas de support de mise à jour).

Appellation:

  • PrioritySet implique une sémantique définie, mais la file d'attente n'implémente pas ISet.
  • Vous utilisez le nom UpdatablePriorityQueue qui est plus facile à découvrir si je recherche PriorityQueue.

Performance:

  • Les performances de la file d'attente prioritaire sont presque toujours mon goulot d'étranglement des performances (> 90 %) dans mes algorithmes de géométrie/planification
  • Envisagez de passer un Funcou Comparaisonà ctor plutôt que de copier TPriority (coûteux !). Si la priorité de calcul est chère, je vais insérer (priorité, élément) dans un PQ et passer une comparaison qui regarde ma priorité en cache.
  • Un nombre important de mes algorithmes n'ont pas besoin de mises à jour PQ. J'envisagerais d'utiliser un PQ intégré qui ne prend pas en charge les mises à jour, mais si quelque chose a un coût de performance 2x pour prendre en charge une fonctionnalité dont je n'ai pas besoin (mise à jour), cela m'est inutile.
  • Pour l'analyse des performances / les compromis, il serait important de connaître le coût relatif de mise en file d'attente/de mise en file d'attente par opération @eiriktsarpalis -- "le suivi est 2 fois plus lent" n'est pas suffisant pour évaluer si un PQ est utile.
  • J'étais heureux de voir vos constructeurs effectuer Heapify. Considérez un constructeur qui prend un IList, une liste ou un tableau pour éviter les allocations d'énumération.
  • Envisagez d'exposer un EnqueueMany qui exécute Heapify si le PQ est initialement vide, car dans les hautes performances, il est courant de regrouper les collections.
  • Envisagez de faire Clear not zero array si les éléments ne contiennent pas de références.
  • Les allocations dans la file d'attente/retrait de la file d'attente sont inacceptables. Mes algorithmes sont à allocation zéro pour des raisons de performances, avec des collections locales de threads regroupées.

Apis:

  • Le clonage d'une file d'attente prioritaire est une opération triviale avec vos implémentations et fréquemment utile.

    • Connexe : l'énumération d'une file d'attente prioritaire devrait-elle avoir une sémantique semblable à une file d'attente ? Je m'attendrais à une collection de commandes de mise en file d'attente, similaire à ce que fait la file d'attente. Je m'attends à ce que la nouvelle liste (myPriorityQueue) ne mute pas la priorité de file d'attente, mais fonctionne comme je viens de le décrire.

  • Comme mentionné ci-dessus, il est préférable de rentrer un Func<TElement, TPriority> plutôt que de l'insérer en priorité. Si la priorité de calcul est chère, je peux simplement insérer (priority, element) et fournir une fonction tuple => tuple.priority
  • La fusion de deux files d'attente prioritaires est parfois utile.
  • Il est étrange que Peek renvoie un TItem mais que Enumeration & Enqueue ont (TItem, TPriority).

Cela étant dit, pour un nombre important de mes algorithmes, mes éléments de file d'attente prioritaires contiennent leurs priorités, et le stockage deux fois (une fois dans le PQ, une fois dans les éléments) semble inefficace. C'est particulièrement le cas si je commande par plusieurs clés (cas d'utilisation similaire à OrderBy.ThenBy.ThenBy). Cette API nettoie également de nombreuses incohérences où Insert est prioritaire mais Peek ne le renvoie pas.

Enfin, il convient de noter que j'insère souvent des indices d'un tableau dans une file d'attente prioritaire, plutôt que des éléments de tableau eux-mêmes . Ceci est pris en charge par toutes les API discutées jusqu'à présent, cependant. Par exemple, si je traite le début/la fin des intervalles sur une ligne d'axe des x, je pourrais avoir des événements de file d'attente prioritaires (x, isStartElseEnd, intervalId) et commander par x puis par isStartElseEnd. C'est souvent parce que j'ai d'autres structures de données qui mappent d'un index à certaines données calculées.

@pgolebiowski J'ai pris la liberté d'inclure votre implémentation de tas d'appariement proposée dans les références, juste pour que nous puissions comparer directement les instances des trois approches. Voici les résultats:

| Méthode | Taille | moyenne | Erreur | DevStd | Médiane | Génération 0 | Génération 1 | Gen 2 | Attribué |
|-------------- |-------- |-------------------:|---- --------------:|-----------------:|---------------- ---:|----------:|------:|------:|-----------:|
| PrioritéQueue | 10 | 774,7 ns | 3,30 secondes | 3,08 secondes | 773,2 secondes | - | - | - | - |
| PrioritySet | 10 | 1 643,0 ns | 3,89 ns | 3,45 secondes | 1 642,8 ns | - | - | - | - |
| CouplageHeap | 10 | 1 660,2 ns | 14,11 secondes | 12,51 ns | 1 657,2 ns | 0,0134 | - | - | 960B |
| PrioritéQueue | 50 | 6 413,0 ns | 14,95 ns | 13,99 secondes | 6 409,5 ns | - | - | - | - |
| PrioritySet | 50 | 12 193,1 ns | 35,41 secondes | 29,57 secondes | 12 188,3 ns | - | - | - | - |
| CouplageHeap | 50 | 13 955,8 ns | 193,36 ns | 180,87 ns | 13 989,2 ns | 0,0610 | - | - | 4800B |
| PrioritéQueue | 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 | - | - | - | - |
| CouplageHeap | 150 | 56 951,2 ns | 190,52 ns | 168,89 ns | 56 953,6 ns | 0,1831 | - | - | 14400 B |
| PrioritéQueue | 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 | - | - | - | - |
| CouplageHeap | 500 | 229 423,9 ns | 3 213,33 ns | 2 848,53 ns | 230 398,7 ns | 0,4883 | - | - | 48000B |
| PrioritéQueue | 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 | - | - | - | - |
| CouplageHeap | 1000 | 459 049,3 ns | 1706,28 ns | 1 424,82 ns | 459 364,9 ns | 0.9766 | - | - | 96000B |
| PrioritéQueue | 10000 | 3 788 802,4 ns | 11 715,81 ns | 10 958,98 ns | 3 787 811,9 ns | - | - | - | 1B |
| PrioritySet | 10000 | 5 963 100,4 ns | 26 669,04 ns | 22 269,86 ns | 5 950 915,5 ns | - | - | - | 2B |
| CouplageHeap | 10000 | 6 789 719,0 ns | 134 453,01 ns | 265 397,13 ns | 6 918 392,9 ns | 7.8125 | - | - | 960002 B |
| PrioritéQueue | 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 | - | - | - | 288B |
| CouplageHeap | 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 |

Points clés à retenir

  • ~ L'implémentation du tas d'appariement fonctionne asymptotiquement bien mieux que ses homologues basés sur un tableau. Cependant, il peut être jusqu'à 2 fois plus lent pour les petites tailles de tas (< 50 éléments), rattrape environ 1000 éléments, mais est jusqu'à 2 fois plus rapide pour les tas de taille 10^6~.
  • Comme prévu, le tas d'appariement produit une quantité importante d'allocations de tas.
  • L'implémentation "PrioritySet" est systématiquement lente ~la plus lente des trois concurrents~, nous pourrions donc ne pas vouloir poursuivre cette approche après tout.

~ À la lumière de ce qui précède, je pense toujours qu'il existe des compromis valables entre le tas de matrice de base et l'approche du tas d'appariement ~.

EDIT : mise à jour des résultats après une correction de bug dans mes benchmarks, merci @VisualMelon

@eiriktsarpalis Votre référence pour le PairingHeap est, je pense, fausse : les paramètres de Add sont dans le mauvais sens. Lorsque vous les échangez, c'est une autre histoire : https://gist.github.com/VisualMelon/00885fe50f7ab0f4ae5cd1307312109f

(J'ai fait exactement la même chose lorsque je l'ai mis en œuvre pour la première fois)

Notez que cela ne signifie pas que le tas d'appariement est plus rapide ou plus lent, il semble plutôt dépendre fortement de la distribution/de l'ordre des données fournies.

@eiriktsarpalis re: l'utilité de PrioritySet...
Nous ne devrions pas nous attendre à ce que la mise à jour soit autre chose que plus lente pour le tri par tas, car elle n'a pas de mises à jour prioritaires dans le scénario. (Aussi pour le tri par tas, il est probable que vous souhaitiez même conserver les doublons, un ensemble n'est tout simplement pas approprié.)

Le test décisif pour voir si PrioritySet est utile devrait être l'analyse comparative des algorithmes qui utilisent des mises à jour de priorité, par rapport à une implémentation sans mise à jour du même algorithme, mettant en file d'attente les valeurs en double et ignorant les doublons lors de la suppression.

Merci @VisualMelon , j'ai mis à jour mes résultats et mes commentaires après la correction suggérée.

elle semble plutôt dépendre fortement de la distribution/de l'ordre des données fournies.

Je pense qu'il aurait pu bénéficier du fait que les priorités mises en file d'attente étaient monotones.

Le test décisif pour voir si PrioritySet est utile devrait être l'analyse comparative des algorithmes qui utilisent des mises à jour de priorité, par rapport à une implémentation sans mise à jour du même algorithme, mettant en file d'attente les valeurs en double et ignorant les doublons lors de la suppression.

@TimLovellSmith, mon objectif ici était de mesurer les performances de l'application PriorityQueue la plus courante : plutôt que de mesurer les performances des mises à jour, je voulais voir l'impact pour le cas où les mises à jour ne sont pas du tout nécessaires. Il peut cependant être judicieux de produire une référence distincte qui compare le tas d'appariement avec les mises à jour "PrioritySet".

@miyu merci pour vos commentaires détaillés, c'est très apprécié !

@TimLovellSmith J'ai écrit un benchmark simple qui utilise des mises à jour :

| Méthode | Taille | moyenne | Erreur | DevStd | Médiane | Génération 0 | Génération 1 | Gen 2 | Attribué |
|------------ |-------- |---------------:|---------- -----:|---------------:|---------------:|--------:| ------:|------:|-----------:|
| PrioritySet | 10 | 1.052 us | 0,0106 us | 0,0099 nous | 1.055 us | - | - | - | - |
| CouplageHeap | 10 | 1.055 us | 0,0042 nous | 0,0035 us | 1.055 us | 0,0057 | - | - | 480B |
| PrioritySet | 50 | 7.394 us | 0,0527 us | 0,0493 us | 7.380 us | - | - | - | - |
| CouplageHeap | 50 | 8.587 us | 0,1678 us | 0,1570 nous | 8.634 us | 0,0305 | - | - | 2400B |
| PrioritySet | 150 | 27.522 us | 0,0459 us | 0,0359 us | 27.523 nous | - | - | - | - |
| CouplageHeap | 150 | 32,045 us | 0,1076 us | 0,1007 nous | 32,019 us | 0,0610 | - | - | 7200B |
| PrioritySet | 500 | 109.097 nous | 0.6548 nous | 0,6125 us | 109.162 us | - | - | - | - |
| CouplageHeap | 500 | 131,647 us | 0.5401 nous | 0,4510 nous | 131.588 nous | 0,2441 | - | - | 24000B |
| PrioritySet | 1000 | 238.184 us | 1.0282 nous | 0.9618 us | 238.457 us | - | - | - | - |
| CouplageHeap | 1000 | 293.236 nous | 0.9396 nous | 0.8789 nous | 293.257 nous | 0,4883 | - | - | 48000B |
| PrioritySet | 10000 | 3 035 982 us | 12.2952 nous | 10.8994 nous | 3 036 985 us | - | - | - | 1B |
| CouplageHeap | 10000 | 3 388.685 États-Unis | 16.0675 nous | 38.1861 nous | 3 374,565 us | - | - | - | 480002 B |
| PrioritySet | 1000000 | 841.406.888 us | 16 788,4775 us | 15 703,9522 us | 840 888.389 us | - | - | - | 288B |
| CouplageHeap | 1000000 | 989 966,501 us | 19 722,6687 us | 30 705,8191 us | 996 075,410 us | - | - | - | 4800448 B |

Sur une note distincte, y a-t-il eu des discussions / commentaires sur le manque de stabilité comme un problème (ou un non-problème) pour les cas d'utilisation des gens ?

Y a-t-il eu des discussions/des commentaires sur le manque de stabilité comme un problème (ou un non-problème) pour le cas d'utilisation des utilisateurs

Aucune des implémentations ne garantit la stabilité, mais il devrait être assez simple pour les utilisateurs d'obtenir la stabilité en augmentant l'ordinal avec l'ordre d'insertion :

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

Pour résumer certains de mes articles précédents, j'ai essayé d'identifier à quoi ressemblerait une file d'attente prioritaire .NET populaire, j'ai donc parcouru les données suivantes :

  • Modèles d'utilisation de file d'attente prioritaires courants dans le code source .NET.
  • Implémentations PriorityQueue dans les bibliothèques principales de frameworks concurrents.
  • Benchmarks de divers prototypes de file d'attente prioritaire .NET.

Ce qui a donné les enseignements suivants :

  • 90 % des cas d'utilisation de file d'attente prioritaire ne nécessitent pas de mises à jour prioritaires.
  • La prise en charge des mises à jour prioritaires entraîne un contrat d'API plus compliqué (nécessitant soit des descripteurs, soit l'unicité des éléments).
  • Dans mes benchmarks, les implémentations qui prennent en charge les mises à jour prioritaires sont 2 à 3 fois plus lentes que celles qui ne le font pas.

Prochaines étapes

À l'avenir, je propose que nous prenions les mesures suivantes pour .NET 6 :

  1. Introduisez une classe System.Collections.Generic.PriorityQueue simple, répondant à la majorité des besoins de nos utilisateurs et aussi efficace que possible. Il utilisera un tas quaternaire basé sur un tableau et ne prendra pas en charge les mises à jour prioritaires. Un prototype de la mise en œuvre peut être trouvé ici . Je vais créer un numéro séparé détaillant la proposition d'API sous peu.

  2. Nous reconnaissons le besoin de tas prenant en charge des mises à jour prioritaires efficaces, nous continuerons donc à travailler pour introduire une classe spécialisée qui réponde à cette exigence. Nous évaluons quelques prototypes [ 1 , 2 ] chacun avec leurs propres ensembles de compromis. Ma recommandation serait d'introduire ce type à un stade ultérieur, car plus de travail est nécessaire pour finaliser la conception.

En ce moment, je tiens à remercier les contributeurs de ce fil, en particulier @pgolebiowski et @TimLovellSmith. Vos commentaires ont joué un rôle énorme dans l'orientation de notre processus de conception. J'espère continuer à recevoir vos commentaires pendant que nous affinons la conception de la file d'attente prioritaire pouvant être mise à jour.

À l'avenir, je propose que nous prenions les mesures suivantes pour .NET 6 : [...]

Ça a l'air bien :)

Introduisez une classe System.Collections.Generic.PriorityQueue simple, répondant à la majorité des besoins de nos utilisateurs et aussi efficace que possible. Il utilisera un tas quaternaire basé sur un tableau et ne prendra pas en charge les mises à jour prioritaires.

Si nous avons la décision des propriétaires de la base de code que cette direction est approuvée et souhaitée, puis-je continuer à diriger la conception de l'API pour ce bit et fournir la mise en œuvre finale ?

En ce moment, je tiens à remercier les contributeurs de ce fil, en particulier @pgolebiowski et @TimLovellSmith. Vos commentaires ont joué un rôle énorme dans l'orientation de notre processus de conception. J'espère continuer à recevoir vos commentaires pendant que nous affinons la conception de la file d'attente prioritaire pouvant être mise à jour.

C'était un sacré voyage :D

L'API pour System.Collections.Generic.PriorityQueue<TElement, TPriority> vient d'être approuvée. J'ai créé un problème distinct pour poursuivre notre conversation sur une implémentation potentielle de tas prenant en charge les mises à jour prioritaires.

Je vais clore ce sujet, merci à tous pour vos contributions !

Peut-être que quelqu'un peut écrire sur ce voyage ! 6 ans entiers pour une API. :) Une chance de gagner une Guinness ?

Cette page vous a été utile?
0 / 5 - 0 notes