Runtime: PriorityQueue hinzufügen<t>zu Sammlungen</t>

Erstellt am 30. Jan. 2015  ·  318Kommentare  ·  Quelle: dotnet/runtime

Siehe AKTUELLES Angebot im corefxlab Repository.

Optionen für den zweiten Vorschlag

Vorschlag von https://github.com/dotnet/corefx/issues/574#issuecomment -307971397

Annahmen

Elemente in der Prioritätswarteschlange sind eindeutig. Wenn dies nicht der Fall ist, müssten wir "Handles" von Elementen einführen, um deren Aktualisierung/Entfernung zu ermöglichen. Oder die Aktualisierungs-/Entfernungssemantik müsste zuerst/alles gelten, was seltsam ist.

Modelliert nach Queue<T> ( MSDN-Link )

API

```c#
öffentliche Klasse PriorityQueue
: IEzählbar,
IEnumerable<(TElement-Element, TPriority-Priorität)>,
IReadOnlyCollection<(TElement-Element, TPriority-Priorität)>
// ICollection absichtlich nicht enthalten
{
public PriorityQueue();
public PriorityQueue(IComparerVergleich);

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

//
// Selektorteil
//
public PriorityQueue(FuncPrioritätSelektor);
public PriorityQueue(FuncPrioritySelector, IComparerVergleich);

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

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

}
````

Offene Fragen:

  1. Klassenname PriorityQueue vs. Heap
  2. Einführung von IHeap und Konstruktorüberladung? (Sollen wir auf später warten?)
  3. Stellen Sie IPriorityQueue ? (Sollen wir auf später warten - IDictionary Beispiel)
  4. Selektor verwenden (der im Wert gespeicherten Priorität) oder nicht (5 APIs Unterschied)
  5. Tupel verwenden (TElement element, TPriority priority) vs. KeyValuePair<TPriority, TElement>

    • Sollten Peek und Dequeue eher ein out Argument anstelle eines Tupels haben?

  6. Ist das Werfen von Peek und Dequeue sinnvoll?

Ursprünglicher Vorschlag

Problem https://github.com/dotnet/corefx/issues/163 forderte das Hinzufügen einer Prioritätswarteschlange zu den Kerndatenstrukturen der .NET-Sammlung.

Dieser Beitrag ist zwar ein Duplikat, soll aber die formelle Einreichung beim corefx API Review Process darstellen. Der Inhalt der Ausgabe ist das _Speclet_ für eine neue System.Collections.Generic.PriorityQueueTyp.

Ich werde die PR beitragen, wenn genehmigt.

Begründung und Verwendung

Die .NET-Basisklassenbibliotheken (BCL) bieten derzeit keine Unterstützung für geordnete Producer-Consumer-Sammlungen. Eine häufige Anforderung vieler Softwareanwendungen ist die Möglichkeit, im Laufe der Zeit eine Liste von Artikeln zu erstellen und sie in einer anderen Reihenfolge als der Reihenfolge zu verarbeiten, in der sie eingegangen sind.

Es gibt drei generische Datenstrukturen innerhalb der System.Collections-Hierarchie von Namespaces, die eine sortierte Auflistung von Elementen unterstützten. System.Collections.Generic.SortedList, System.Collections.Generic.SortedSet und System.Collections.Generic.SortedDictionary.

Von diesen sind SortedSet und SortedDictionary nicht für Erzeuger-Verbraucher-Muster geeignet, die doppelte Werte erzeugen. Die Komplexität von SortedList ist Θ(n) Worst Case für Add und Remove.

Eine viel speicher- und zeiteffizientere Datenstruktur für geordnete Sammlungen mit Nutzungsmustern zwischen Herstellern und Verbrauchern ist eine Prioritätswarteschlange. Außer wenn eine Kapazitätsanpassung erforderlich ist, beträgt die Leistung beim Einfügen (Enqueue) und Entfernen der obersten (Dequeue) im schlimmsten Fall Θ(log n) – weit besser als die vorhandenen Optionen, die in der BCL vorhanden sind.

Prioritätswarteschlangen haben ein breites Maß an Anwendbarkeit auf verschiedene Anwendungsklassen. Die Wikipedia-Seite zu Prioritätswarteschlangen bietet eine Liste mit vielen verschiedenen, gut verständlichen Anwendungsfällen. Während hochspezialisierte Implementierungen möglicherweise noch benutzerdefinierte Prioritätswarteschlangenimplementierungen erfordern, würde eine Standardimplementierung ein breites Spektrum von Nutzungsszenarien abdecken. Prioritätswarteschlangen sind besonders nützlich bei der Planung der Ausgabe mehrerer Produzenten, was ein wichtiges Muster in hochgradig parallelisierter Software ist.

Es ist erwähnenswert, dass sowohl die C++-Standardbibliothek als auch Java als Teil ihrer grundlegenden APIs eine Prioritätswarteschlangenfunktionalität bieten.

Vorgeschlagene API

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


/// Stellt eine Sammlung von Objekten dar, die in sortierter Reihenfolge entfernt werden.
///

///Gibt den Typ der Elemente in der Warteschlange an.
[DebuggerDisplay("Count = {count}")]
[DebuggerTypeProxy(typeof(System_PriorityQueueDebugView<>))]
öffentliche Klasse PriorityQueue: IEzählbar, ICollection, IEnumerable, IReadOnlyCollection
{
///
/// Initialisiert eine neue Instanz des Klasse
/// die einen Standardvergleich verwendet.
///

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

}
```

Einzelheiten

  • Die Implementierungsdatenstruktur ist ein binärer Heap. Artikel mit einem größeren Vergleichswert werden zuerst zurückgegeben. (absteigende Reihenfolge)
  • Zeitliche Komplexität:

| Bedienung | Komplexität | Anmerkungen |
| --- | --- | --- |
| Konstrukt | (1) | |
| Konstrukt mit IEnumerable | (n) | |
| Einreihen | (log n) | |
| Aus der Warteschlange | (log n) | |
| Blick | (1) | |
| Zählen | (1) | |
| Löschen | (N) | |
| Enthält | (N) | |
| Kopieren nach | (N) | Verwendet Array.Copy, tatsächliche Komplexität kann geringer sein |
| ToArray | (N) | Verwendet Array.Copy, tatsächliche Komplexität kann geringer sein |
| GetEnumerator | (1) | |
| Enumerator.MoveNext | (1) | |

  • Zusätzliche Konstruktorüberladungen, die die System.ComparisonDelegierte wurden absichtlich zugunsten einer vereinfachten API-Oberfläche weggelassen. Anrufer können Comparer verwenden.Erstellen, um eine Funktion oder einen Lambda-Ausdruck in einen IComparer zu konvertierenSchnittstelle ggf. Dies erfordert, dass der Aufrufer eine einmalige Heap-Zuordnung vornimmt.
  • Obwohl System.Collections.Generic noch nicht Teil von corefx ist, schlage ich vor, diese Klasse in der Zwischenzeit zu corefxlab hinzuzufügen. Es kann in das primäre corefx-Repository verschoben werden, sobald System.Collections.Generic hinzugefügt wird und es besteht Konsens, dass sein Status von experimentell auf eine offizielle API erhöht werden sollte.
  • Eine IsEmpty-Eigenschaft wurde nicht eingeschlossen, da es beim Aufrufen von Count keine zusätzlichen Leistungseinbußen gibt. Die Mehrheit der Sammlungsdatenstrukturen enthält kein IsEmpty.
  • Die Eigenschaften IsSynchronized und SyncRoot von ICollection wurden explizit implementiert, da sie effektiv veraltet sind. Dies folgt auch dem Muster, das für die anderen System.Collection.Generic-Datenstrukturen verwendet wird.
  • Dequeue und Peek lösen eine InvalidOperationException aus, wenn die Warteschlange leer ist, um dem etablierten Verhalten von System.Collections.Queue zu entsprechen.
  • IProduzentConsumerCollectionwurde nicht implementiert, da in der Dokumentation angegeben ist, dass es nur für threadsichere Sammlungen gedacht ist.

Offene Fragen

  • Ist das Vermeiden einer zusätzlichen Heap-Zuordnung während Aufrufen von GetEnumerator bei Verwendung von foreach ein ausreichend starkes Argument für das Einschließen der geschachtelten öffentlichen Enumeratorstruktur?
  • Sollen CopyTo, ToArray und GetEnumerator Ergebnisse in priorisierter (sortierter) Reihenfolge oder in der von der Datenstruktur verwendeten internen Reihenfolge zurückgeben? Ich gehe davon aus, dass die Innenbestellung zurückgegeben werden sollte, da sie keine zusätzlichen Leistungseinbußen nach sich zieht. Dies ist jedoch ein potenzielles Usability-Problem, wenn ein Entwickler die Klasse als "sortierte Warteschlange" und nicht als Prioritätswarteschlange betrachtet.
  • Führt das Hinzufügen eines Typs namens PriorityQueue zu System.Collections.Generic zu einer potenziell bahnbrechenden Änderung? Der Namespace wird häufig verwendet und kann bei Projekten, die ihren eigenen Prioritätswarteschlangentyp enthalten, ein Problem mit der Quellkompatibilität verursachen.
  • Sollen Elemente in aufsteigender oder absteigender Reihenfolge aus der Warteschlange entfernt werden, basierend auf der Ausgabe von IComparer? (meine Annahme ist aufsteigend, um der normalen Sortierkonvention von IComparer zu entsprechen).
  • Soll die Sammlung „stabil“ sein? Mit anderen Worten, sollten zwei Elemente mit gleichem IComparisonErgebnisse in der gleichen Reihenfolge aus der Warteschlange entfernt werden, in der sie in die Warteschlange gestellt werden? (meine vermutung ist das ist nicht nötig)

    Aktualisierung

  • Die Komplexität von 'Construct Using IEnumerable' wurde zu Θ(n) korrigiert. Danke @svick.

  • Es wurde eine weitere Optionsfrage hinzugefügt, ob die Prioritätswarteschlange im Vergleich zum IComparer aufsteigend oder absteigend sortiert werden soll.
  • NotSupportedException aus der expliziten SyncRoot-Eigenschaft entfernt, um dem Verhalten anderer System.Collection.Generic-Typen zu entsprechen, anstatt das neuere Muster zu verwenden.
  • Die öffentliche GetEnumerator-Methode hat eine verschachtelte Enumerator-Struktur anstelle von IEnumerable zurückgegeben, ähnlich den vorhandenen System.Collections.Generic-Typen. Dies ist eine Optimierung, um eine Heap-(GC)-Zuweisung zu vermeiden, wenn eine foreach-Schleife verwendet wird.
  • ComVisible-Attribut entfernt.
  • Komplexität von Clear zu Θ(n) geändert. Danke @mbeidler.
api-needs-work area-System.Collections wishlist

Hilfreichster Kommentar

Heap-Datenstruktur ist ein MUSS für die Erstellung von Leetcode
mehr leetcode, mehr c#-Code-Interview, was mehr c#-Entwickler bedeutet.
Mehr Entwickler bedeuten ein besseres Ökosystem.
besseres Ökosystem bedeutet, dass wir morgen noch in c# programmieren können.

Fazit: Das ist nicht nur ein Feature, sondern auch die Zukunft. Aus diesem Grund wird dieses Thema als "Zukunft" bezeichnet.

Alle 318 Kommentare

| Bedienung | Komplexität |
| --- | --- |
| Konstrukt mit IEnumerable | (log n) |

Ich denke, das sollte Θ(n) sein. Sie müssen zumindest die Eingabe wiederholen.

+1

Rx verfügt über eine hochgradig produktionserprobte Prioritätswarteschlangenklasse:

https://github.com/Reactive-Extensions/Rx.NET/blob/master/Rx.NET/Source/System.Reactive.Core/Reactive/Internal/PriorityQueue.cs

Sollte eine verschachtelte öffentliche Enumeratorstruktur verwendet werden, um eine zusätzliche Heap-Zuordnung bei Aufrufen von GetEnumerator und bei der Verwendung von foreach zu vermeiden? Meine Annahme ist nein, da das Aufzählen über eine Warteschlange ein ungewöhnlicher Vorgang ist.

Ich würde dazu neigen, den Struktur-Enumerator zu verwenden, um mit Queue<T> konsistent zu sein, der einen Struktur-Enumerator verwendet. Wenn jetzt kein Struct-Enumerator verwendet wird, können wir PriorityQueue<T> nicht mehr ändern, um in Zukunft einen zu verwenden.

Vielleicht auch eine Methode für Stapeleinfügungen? Kann man dann immer sortieren und vom vorherigen Einfügepunkt aus fortfahren, anstatt am Anfang zu beginnen, wenn das helfen würde?:

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

Ich habe eine Kopie der ersten Implementierung geworfen hier . Die Testabdeckung ist noch lange nicht vollständig, aber wenn jemand neugierig ist, schaut bitte vorbei und lasst mich wissen, was ihr denkt. Ich habe versucht, die bestehenden Codierungskonventionen aus den System.Collections-Klassen so weit wie möglich zu befolgen.

Cool. Einige erste Rückmeldungen:

  • Queue<T>.Enumerator implementiert IEnumerator.Reset explizit. Sollte PriorityQueue<T>.Enumerator dasselbe tun?
  • Queue<T>.Enumerator verwendet _index == -2 um anzuzeigen, dass der Enumerator verworfen wurde. PriorityQueue<T>.Enumerator hat denselben Kommentar, aber ein zusätzliches Feld _disposed . Ziehen Sie in Erwägung, das zusätzliche Feld _disposed loszuwerden und verwenden Sie _index == -2 um anzuzeigen, dass es verworfen wurde, um die Struktur zu verkleinern und mit Queue<T> konsistent zu sein
  • Ich denke, das statische Feld _emptyArray kann entfernt und die Verwendung stattdessen durch Array.Empty<T>() werden.

Ebenfalls...

  • Andere Sammlungen, die einen Vergleich benötigen (z. B. Dictionary<TKey, TValue> , HashSet<T> , SortedList<TKey, TValue> , SortedDictionary<TKey, TValue> , SortedSet<T> usw.) lassen null sein für den Vergleicher übergeben, in diesem Fall wird Comparer<T>.Default verwendet.

Ebenfalls...

  • ToArray kann optimiert werden, indem auf _size == 0 geprüft wird, bevor das neue Array zugewiesen wird. In diesem Fall geben Sie einfach Array.Empty<T>() .

@justinvp Tolles Feedback, danke!

  • Ich habe Enumerator implementiert.Reset explizit zurück, da es sich um die nicht veraltete Kernfunktionalität eines Enumerators handelt. Ob es offengelegt ist oder nicht, scheint bei den Sammlungstypen inkonsistent zu sein, und nur wenige verwenden die explizite Variante.
  • Das Feld _disposed wurde zugunsten von _index entfernt, danke! Habe das an diesem Abend in letzter Minute hineingeworfen und das Offensichtliche übersehen. Beschlossen, die ObjectDisposedException aus Gründen der Korrektheit mit den neueren Auflistungstypen beizubehalten, obwohl die alten System.Collections.Generic-Typen sie nicht verwenden.
  • Array.Leerist eine F#-Funktion, kann sie also hier leider nicht verwenden!
  • Die Vergleichsparameter wurden so geändert, dass sie null akzeptieren, gute Suche!
  • Die ToArray-Optimierung ist knifflig. _Technisch_ gesprochen sind Arrays in C# veränderbar, selbst wenn sie eine Länge von Null haben. In Wirklichkeit haben Sie Recht, die Zuordnung wird nicht benötigt und kann optimiert werden. Ich tendiere zu einer vorsichtigeren Implementierung, falls es Nebenwirkungen gibt, an die ich nicht denke. Semantisch wird der Anrufer immer noch diese Zuordnung erwarten, und es ist eine geringfügige.

@ebickle

Array.Empty ist eine F#-Funktion, kann sie also hier leider nicht verwenden!

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

Ich habe den Code nach ebickle/corefx im Issue-574-Zweig migriert.

Implementiert das Array.Empty() ändern und alles in die reguläre Build-Pipeline stecken. Ein kleiner vorübergehender Fehler, den ich einführen musste, war, dass das System.Collections-Projekt als Vergleicher vom System.Collections-Nuget-Paket abhängtist noch nicht Open Source.

Wird behoben, sobald das Problem dotnet/corefx#966 abgeschlossen ist.

Ein wichtiger Bereich, in dem ich nach Feedback suche, ist, wie ToArray, CopyTo und der Enumerator behandelt werden sollten. Derzeit sind sie auf Leistung optimiert, was bedeutet, dass das Zielarray ein Heap ist und nicht vom Vergleicher sortiert wird.

Es gibt drei Möglichkeiten:
1) Lassen Sie es unverändert und dokumentieren Sie, dass das zurückgegebene Array nicht sortiert ist. (es ist eine Prioritätswarteschlange, keine sortierte Warteschlange)
2) Ändern Sie die Methoden, um die zurückgegebenen Elemente/Arrays zu sortieren. Sie werden keine O(n)-Operationen mehr sein.
3) Entfernen Sie die Methoden und die aufzählbare Unterstützung insgesamt. Dies ist die "puristische" Option, entfernt jedoch die Möglichkeit, die verbleibenden Elemente in der Warteschlange schnell zu greifen, wenn die Prioritätswarteschlange nicht mehr benötigt wird.

Eine andere Sache, zu der ich gerne Feedback hätte, ist, ob die Warteschlange für zwei Elemente mit derselben Priorität stabil sein soll (vergleiche Ergebnis 0). Im Allgemeinen bieten Prioritätswarteschlangen keine Garantie dafür, dass zwei Elemente mit derselben Priorität in der Reihenfolge, in der sie in die Warteschlange eingereiht wurden, aus der Warteschlange entfernt werden, aber ich habe festgestellt, dass die in System.Reactive.Core verwendete interne Prioritätswarteschlangenimplementierung zusätzlichen Aufwand erforderte, um diese Eigenschaft sicherzustellen. Ich würde es vorziehen, dies nicht zu tun, aber ich bin mir nicht ganz sicher, welche Option im Hinblick auf die Erwartungen der Entwickler besser ist.

Ich bin auf diese PR gestoßen, weil ich auch daran interessiert war, .NET eine Prioritätswarteschlange hinzuzufügen. Schön zu sehen, dass sich jemand die Mühe gemacht hat, diesen Vorschlag zu machen :). Nach der Überprüfung des Codes ist mir folgendes aufgefallen:

  • Wenn die IComparer Reihenfolge nicht mit Equals übereinstimmt, kann das Verhalten dieser Contains Implementierung (die IComparer ) für einige Benutzer überraschend sein, da es ist im Wesentlichen _enthält ein Element mit gleicher Priorität_.
  • Ich habe keinen Code zum Verkleinern des Arrays in Dequeue . Das Schrumpfen des Heap-Arrays um die Hälfte, wenn ein Viertel voll ist, ist typisch.
  • Sollte die Methode Enqueue null Argumente akzeptieren?
  • Ich denke, die Komplexität von Clear sollte Θ(n) sein, da dies die Komplexität von System.Array.Clear , die es verwendet. https://msdn.microsoft.com/en-us/library/system.array.clear%28v=vs.110%29.aspx

Ich habe keinen Code zum Verkleinern des Arrays in Dequeue . Das Schrumpfen des Heap-Arrays um die Hälfte, wenn ein Viertel voll ist, ist typisch.

Queue<T> und Stack<T> verkleinern ihre Arrays auch nicht (basierend auf Reference Source , sie sind noch nicht in CoreFX).

@mbeidler Ich hatte erwogen, eine Form der automatischen Array-Verkleinerung in Dequeue hinzuzufügen, aber wie @svick darauf hingewiesen hat, existiert sie in den Referenzimplementierungen ähnlicher Datenstrukturen nicht. Es würde mich interessieren, von jedem im .NET Core/BCL-Team zu hören, ob es einen bestimmten Grund gibt, warum sie sich für diesen Implementierungsstil entschieden haben.

Update: Ich habe die Liste überprüft, Warteschlange, Queue und ArrayList - keiner von ihnen verkleinert die Größe des internen Arrays beim Entfernen/Entfernen aus der Warteschlange.

Enqueue sollte Nullen unterstützen und ist so dokumentiert, dass sie diese zulässt. Sind Sie auf einen Fehler gestoßen? Ich kann mich nicht erinnern, wie robust der Unit-Test-Bereich in der Gegend noch ist.

Interessanterweise habe ich in der von @svick verlinkten Referenzquelle Queue<T> eine ungenutzte private Konstante namens _ShrinkThreshold . Vielleicht existierte dieses Verhalten in einer früheren Version.

Bezüglich der Verwendung von IComparer anstelle von Equals in der Implementierung von Contains ich den folgenden Unit-Test geschrieben, der derzeit fehlschlagen würde: https://gist.github. com/mbeidler/9e9f566ba7356302c57e

@mbeidler Guter Punkt. Laut MSDN, IComparer/ IVergleichbargarantiert nur, dass ein Wert von Null dieselbe Sortierreihenfolge hat.

Es sieht jedoch so aus, als ob das gleiche Problem in den anderen Auflistungsklassen existiert. Wenn ich den Code ändere, um auf SortedList zu arbeiten, schlägt der Testfall bei ContainsKey immer noch fehl. Die Implementierung von SortedList.ContainsKey ruft Array.BinarySearch auf, das auf IComparer angewiesen ist, um auf Gleichheit zu prüfen. Das gleiche gilt für SortedSet.

Es ist wohl auch ein Fehler in den bestehenden Sammlungsklassen. Ich werde den Rest der Sammlungsklassen durchforsten und sehen, ob es andere Datenstrukturen gibt, die einen IComparer akzeptieren, aber separat auf Gleichheit testen. Sie haben jedoch Recht, für eine Prioritätswarteschlange würden Sie ein benutzerdefiniertes Bestellverhalten erwarten, das völlig unabhängig von der Gleichheit ist.

Ich habe einen Fix und den Testfall in meinen Fork-Zweig übertragen. Die neue Implementierung von Enthält basiert direkt auf dem Verhalten von List.Enthält. Seit Listeakzeptiert keinen IEqualityComparer, das Verhalten ist funktional äquivalent.

Wenn ich heute etwas später dazu komme, werde ich wahrscheinlich Fehlerberichte für die anderen integrierten Sammlungen einreichen. Das kann aufgrund von Regressionsverhalten wahrscheinlich nicht behoben werden, aber zumindest muss die Dokumentation aktualisiert werden.

Ich denke, es ist sinnvoll, dass ContainsKey die Implementierung von IComparer<TKey> , da dies den Schlüssel angibt. Ich denke jedoch, dass es für ContainsValue logischer wäre, Equals anstelle von IComparable<TValue> in seiner linearen Suche zu verwenden; obwohl in diesem Fall der Geltungsbereich erheblich reduziert wird, da die natürliche Ordnung eines Typs viel weniger wahrscheinlich mit Gleichen übereinstimmt.

Es scheint, dass in der MSDN-Dokumentation für SortedList<TKey, TValue> der Anmerkungsabschnitt für ContainsValue anzeigt, dass die Sortierreihenfolge von TValue anstelle von Gleichheit verwendet wird.

@terrajobst Wie

:+1:

Danke, dass du das eingereicht hast. Ich glaube, wir haben genügend Daten, um diesen Vorschlag offiziell zu überprüfen, daher habe ich ihn als "bereit für die API-Überprüfung" gekennzeichnet.

Da Dequeue und Peek werfende Methoden sind, muss der Aufrufer die Anzahl vor jedem Aufruf überprüfen. Wäre es sinnvoll, stattdessen (oder zusätzlich) TryDequeue und TryPeek nach dem Muster der gleichzeitigen Sammlungen bereitzustellen? Es gibt Probleme beim Hinzufügen von nicht werfenden Methoden zu bestehenden generischen Sammlungen, so dass das Hinzufügen einer neuen Sammlung ohne diese Methoden kontraproduktiv erscheint.

@andrewgmorris related https://github.com/dotnet/corefx/issues/4316 "TryDequeue zur Warteschlange hinzufügen"

Wir hatten eine grundlegende Überprüfung und sind uns einig, dass wir eine ProrityQueue im Rahmen haben wollen. Wir müssen jedoch jemanden finden, der das Design und die Implementierung vorantreibt. Wer das Problem packt, kann @terrajobst an der Finalisierung der API arbeiten.

Welche Arbeit bleibt also an der API übrig?

Dies fehlt im obigen API-Vorschlag: PriorityQueue<T> sollte IReadOnlyCollection<T> implementieren, um Queue<T> ( Queue<T> implementiert jetzt IReadOnlyCollection<T> ab .NET 4.6).

Ich weiß nicht, dass Array-basierte Prioritätswarteschlangen am besten sind. Die Speicherzuweisung in .NET ist wirklich schnell. Wir haben nicht das gleiche Problem mit der Suche nach kleinen Blöcken, mit dem der alte Malloc umgegangen ist. Sie können gerne meinen Prioritäts-Warteschlangencode von hier aus verwenden: https://github.com/BrannonKing/Kts.Astar/tree/master/Kts.AStar

@ebickle Eine kleine Nisse auf dem _Speclet_. Es sagt:

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

Sollte es nicht stattdessen sagen, /// Inserts object into the <see cref="PriorityQueue{T}"> by its priority.

@SunnyWar Die Dokumentation der Enqueue-Methode wurde korrigiert, danke!

Vor einiger Zeit habe ich eine Datenstruktur mit einer ähnlichen Komplexität wie eine Prioritätswarteschlange basierend auf einer Skip List-Datenstruktur erstellt, die ich an dieser Stelle freigeben möchte :

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

Die Skip-Liste stimmt in durchschnittlichen Fällen mit der Komplexität einer obigen Prioritätswarteschlange überein, außer dass Enthält ein durchschnittlicher Fall ist O(log(n)) . Außerdem sind der Zugriff entweder auf das erste oder das letzte Element Operationen mit konstanter Zeit, und die Iteration sowohl in der Vorwärts- als auch in der Rückwärtsreihenfolge entspricht der Komplexität einer PQ in der Vorwärtsreihenfolge.

Eine solche Struktur hat offensichtlich Nachteile in Form höherer Speicherkosten, und sie geht auf O(n) Worst-Case-Einfügungen und -Entfernungen über, also hat sie ihre Nachteile ...

Ist das schon irgendwo implementiert? Wann ist die voraussichtliche Veröffentlichung?
Und was ist mit der Aktualisierung der Priorität eines vorhandenen Elements?

@Priya91 @ianhays ist dies bereit für die Überprüfung als bereit markiert zu werden?

Dies fehlt im obigen API-Vorschlag: PriorityQueuesollte IReadOnlyCollection implementierenzur Warteschlange passen(Warteschlangeimplementiert jetzt IReadOnlyCollectionab .NET 4.6).

Ich stimme @justinvp hier zu.

@Priya91 @ianhays ist dies bereit für die Überprüfung als bereit markiert zu werden?

Ich würde sagen. Das sitzt schon eine Weile; lassen Sie uns bewegen.

@justinvp @ianhays Ich habe die Spezifikation aktualisiert, um IReadOnlyCollection zu implementieren. Dankeschön!

Ich habe eine vollständige Implementierung der Klasse und der zugehörigen PriorityQueueDebugView, die eine Array-basierte Implementierung verwendet. Unit-Tests sind noch nicht zu 100 % abgedeckt, aber wenn Interesse besteht, kann ich ein bisschen arbeiten und meine Gabel abstauben.

@NKnusperer machte einen guten Punkt, die Priorität eines vorhandenen Elements zu aktualisieren. Ich lasse es vorerst weg, aber es ist etwas, das bei der Spezifikationsüberprüfung berücksichtigt werden muss.

Es gibt 2 Implementierungen im vollständigen Framework, die mögliche Alternativen sind.
https://referencesource.microsoft.com/#q =priorityqueue

Als Referenz hier eine Frage zur Java PriorityQueue auf Stackoverflow http://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueue. Es ist interessant, dass die Priorität von einem Vergleicher gehandhabt wird und nicht nur von einem einfachen Wrapper-Objekt mit int-Priorität. Es ist beispielsweise nicht einfach, die Priorität eines Elements in der Warteschlange bereits zu ändern.

API-Überprüfung:
Wir stimmen zu, dass es nützlich ist, diesen Typ in CoreFX zu haben, da wir erwarten, dass CoreFX ihn verwendet.

Zur abschließenden Überprüfung der API-Form möchten wir uns Beispielcode ansehen: PriorityQueue<Thread> und PriorityQueue<MyClass> .

  1. Wie halten wir die Priorität aufrecht? Im Moment wird es nur durch T impliziert.
  2. Soll es möglich sein, die Priorität beim Hinzufügen eines Eintrags zu übergeben? (was ziemlich praktisch erscheint)

Anmerkungen:

  • Wir gehen davon aus, dass sich die Priorität nicht von selbst ändert - wir benötigen entweder eine API dafür oder wir erwarten Remove und Add in der Warteschlange.
  • Da wir hier keinen klaren Kundencode haben (nur allgemeiner Wunsch, dass wir den Typ haben wollen), ist es schwer zu entscheiden, wofür optimiert werden soll - Leistung vs. Benutzerfreundlichkeit vs. etwas anderes?

Dies wäre ein wirklich nützlicher Typ in CoreFX. Hat jemand Interesse, sich diesen zu schnappen?

Ich mag die Idee nicht, die Prioritätswarteschlange auf einen binären Heap festzulegen. Weitere Informationen finden Sie auf meiner

  • Wenn wir die Implementierung auf einen bestimmten Heap-Typ festlegen, machen Sie daraus einen 4-ary-Heap.
  • Wir sollten die Implementierung des Heaps nicht korrigieren, da in einigen Szenarien einige Arten von Heaps leistungsfähiger sind als andere. Wenn wir also die Implementierung auf einen bestimmten Heap-Typ festlegen und der Kunde einen anderen für mehr Leistung benötigt, müsste er die gesamte Prioritätswarteschlange von Grund auf implementieren. Das ist falsch. Wir sollten hier flexibler sein und ihn einen Teil des CoreFX-Codes (zumindest einige Schnittstellen) wiederverwenden lassen.

Letztes Jahr habe ich einige Heap-Typen implementiert. Insbesondere gibt es eine IHeap Schnittstelle, die als Prioritätswarteschlange verwendet werden könnte.

  • Warum nennst du es nicht einfach einen Haufen...? Kennen Sie eine andere vernünftige Möglichkeit, eine Prioritätswarteschlange zu implementieren, als einen Heap zu verwenden?

Ich bin dafür, eine IHeap Schnittstelle und einige performanteste Implementierungen (zumindest die Array-basierten) einzuführen. Die API und Implementierungen befinden sich in dem oben verlinkten Repository.

Also keine _Prioritätswarteschlangen_. Haufen .

Was denken Sie?

@karelz @safern @danmosemsft

@pgolebiowski Vergiss nicht, dass wir eine PriorityQueue (ein etablierter Begriff in der Informatik) möchten, sollten Sie einen in docs / über die Internetsuche finden.
Wenn die zugrunde liegende Implementierung Heap ist (was ich persönlich frage, warum) oder etwas anderes, spielt es keine allzu große Rolle. Vorausgesetzt, Sie können demonstrieren, dass die Implementierung messbar besser ist als einfachere Alternativen (die Komplexität des Codes ist ebenfalls eine Metrik, die eine gewisse Bedeutung hat).

Wenn Sie also immer noch der Meinung sind, dass eine heap-basierte Implementierung (ohne IHeap API) besser ist als eine einfache list-basierte oder list-of-array-chunks-basierte Implementierung, erklären Sie bitte warum (idealerweise in ein paar Sätzen/Absätzen). ), damit wir uns über den Implementierungsansatz einigen können (und vermeiden, dass Sie Zeit mit komplexer Implementierung verschwenden, die bei der PR-Überprüfung möglicherweise abgelehnt wird).

ICollection , IEnumerable ? Habe nur die generischen Versionen (obwohl generische IEnumerable<T> IEnumerable einbringen wird)

@pgolebiowski, wie es implementiert ist, ändert die externe API nicht. PriorityQueue definiert das Verhalten/den Vertrag; wohingegen ein Heap eine spezifische Implementierung ist.

Prioritätswarteschlangen vs. Haufen

Wenn die zugrunde liegende Implementierung Heap ist (was ich persönlich frage, warum) oder etwas anderes, spielt es keine allzu große Rolle. Vorausgesetzt, Sie können demonstrieren, dass die Implementierung messbar besser ist als einfachere Alternativen (die Komplexität des Codes ist ebenfalls eine Metrik, die eine gewisse Bedeutung hat).

Wenn Sie also immer noch der Meinung sind, dass eine Heap-basierte Implementierung besser ist als eine einfache list-basierte oder list-of-array-chunks-basierte Implementierung, erklären Sie bitte warum (idealerweise in wenigen Sätzen/Absätzen), damit wir eine Einigung darüber erzielen können Implementierungsansatz [...]

OK. Eine Prioritätswarteschlange ist eine abstrakte Datenstruktur, die dann auf irgendeine Weise implementiert werden kann. Natürlich können Sie es mit einer anderen Datenstruktur als einem Heap implementieren. Aber kein Weg ist effizienter. Als Ergebnis:

  • Prioritätswarteschlange und Heap werden oft als Synonyme verwendet (siehe Python-Dokument unten als deutlichstes Beispiel dafür)
  • Immer wenn Sie in einer Bibliothek eine "Prioritätswarteschlange"-Unterstützung haben, verwendet sie einen Heap (siehe alle Beispiele unten).

Um meine Worte zu untermauern, beginnen wir mit der theoretischen Unterstützung. Einführung in Algorithmen , Cormen:

[…] Warteschlangen mit Priorität gibt es in zwei Formen: Warteschlangen mit maximaler Priorität und Warteschlangen mit minimaler Priorität. Wir konzentrieren uns hier auf die Implementierung von Max-Priority-Warteschlangen, die wiederum auf Max-Heaps basieren.

Deutlich gesagt, dass Prioritätswarteschlangen Haufen sind. Dies ist natürlich eine Abkürzung, aber Sie bekommen die Idee. Sehen wir uns nun, was noch wichtiger ist, an, wie andere Sprachen und ihre Standardbibliotheken die von uns besprochenen Operationen unterstützen:

  • Java: PriorityQueue<T> — Prioritätswarteschlange mit einem Heap implementiert.
  • Rust: BinaryHeap — Heap explizit in der API. In den Dokumenten steht, dass es sich um eine Prioritätswarteschlange handelt, die mit einem binären Heap implementiert ist. Aber die API ist sehr klar in der Struktur – ein Haufen.
  • Swift: CFBinaryHeap — sagt wiederum explizit, was die Datenstruktur ist, und vermeidet die Verwendung des abstrakten Begriffs "Prioritätswarteschlange". Die Dokumente, die die Klasse beschreiben: Binäre Heaps können als Prioritätswarteschlangen nützlich sein. Der Ansatz gefällt mir.
  • C++: priority_queue — noch einmal kanonisch mit einem binären Heap implementiert, der oben auf einem Array aufgebaut ist.
  • Python: heapq — Heap wird explizit in der API bereitgestellt. Die Priority Queue wird nur in der Dokumentation erwähnt: Dieses Modul bietet eine Implementierung des Heap Queue Algorithmus, auch bekannt als Priority Queue Algorithmus.
  • Gehen Sie: heap package — es gibt sogar eine Heap-Schnittstelle. Keine explizite Prioritätswarteschlange, aber wiederum nur in Dokumenten: Ein Heap ist eine gängige Möglichkeit, eine Prioritätswarteschlange zu implementieren.

Ich bin fest davon überzeugt, dass wir den Weg Rust / Swift / Python / Go gehen und einen Heap explizit freigeben sollten, während wir in den Dokumenten klar angeben, dass er als Prioritätswarteschlange verwendet werden kann. Ich bin fest davon überzeugt, dass dieser Ansatz sehr sauber und einfach ist.

  • Wir sind uns über die Datenstruktur im Klaren und lassen die API in Zukunft verbessern – wenn jemand einen neuen, revolutionären Weg zur Implementierung einer Prioritätswarteschlange findet, die in einigen Aspekten besser ist (und die Auswahl Heap gegenüber dem neuen Typ würde davon abhängen .) das Szenario), kann unsere API noch intakt sein. Wir könnten einfach den neuen revolutionären Typ hinzufügen, der die Bibliothek erweitert – und die Heap-Klasse wäre dort immer noch vorhanden.
  • Stellen Sie sich vor, ein Benutzer fragt sich, ob die von uns gewählte Datenstruktur stabil ist. Wenn die von uns verfolgte Lösung eine Prioritätswarteschlange ist , ist dies nicht offensichtlich. Der Begriff ist abstrakt und alles kann darunter sein. Dadurch verliert der Benutzer einige Zeit, um die Dokumente zu durchsuchen und herauszufinden, dass er intern einen Heap verwendet und als solcher nicht stabil ist. Dies hätte vermieden werden können, indem Sie einfach explizit über die API angeben, dass dies ein Heap ist.

Ich hoffe, Sie alle mögen den Ansatz, eine Heap-Datenstruktur einfach offenzulegen – und nur in den Dokumenten eine Prioritätswarteschlangenreferenz zu machen.

API & Implementierung

Wir müssen uns auf die obige Sache einigen, aber lassen Sie mich einfach ein anderes Thema beginnen – wie man dies implementiert . Ich sehe hier zwei Lösungen:

  • Wir bieten nur eine Klasse ArrayHeap an. Wenn Sie den Namen sehen, können Sie sofort erkennen, mit welcher Art von Heap Sie es zu tun haben (wiederum gibt es Dutzende von Heap-Typen). Sie sehen, es ist Array-basiert. Sie wissen sofort, mit welcher Bestie Sie es zu tun haben.
  • Eine andere Möglichkeit, die mir viel besser gefällt, besteht darin, eine IHeap Schnittstelle bereitzustellen und eine oder mehrere Implementierungen bereitzustellen. Der Kunde könnte Code schreiben, der von Schnittstellen abhängt – dies würde es ermöglichen, wirklich klare und lesbare Implementierungen komplexer Algorithmen bereitzustellen. Stellen Sie sich vor, Sie schreiben einen DijkstraAlgorithm Kurs. Es könnte einfach von der IHeap Schnittstelle abhängen (ein parametrisierter Konstruktor) oder einfach ArrayHeap (Standardkonstruktor) verwenden. Sauber, einfach, explizit, keine Mehrdeutigkeit aufgrund der Verwendung eines "Prioritätswarteschlangen"-Begriffs. Und eine wunderbare Schnittstelle, die theoretisch sehr viel Sinn macht.

Im obigen Ansatz repräsentiert ArrayHeap einen impliziten Heap-geordneten vollständigen d-ary-Baum, der als Array gespeichert wird. Dies kann zum Beispiel verwendet werden, um ein BinaryHeap oder ein QuaternaryHeap zu erstellen.

Endeffekt

Zur weiteren Diskussion empfehle ich Ihnen dringend, sich dieses Papier anzusehen. Sie werden das wissen:

  • 4-äre Heaps (quaternär) sind einfach schneller als 2-äre Heaps (binär). Es gibt viele Tests, die in der Arbeit durchgeführt wurden – Sie würden daran interessiert sein, die Leistung von implicit_4 und implicit_2 (manchmal implicit_simple_4 und implicit_simple_2 ) zu vergleichen.

  • Die optimale Wahl der Implementierung ist stark eingabeabhängig. Von impliziten D-Ary-Heaps, Pairing Heaps, Fibonacci Heaps, Binomial Heaps, Explicit D-Ary Heaps, Rang-Paaring Heaps, Beben Heaps, Verstoß Heaps, Rang-relaxierten Schwachen Heaps und strikten Fibonacci Heaps scheinen die folgenden Typen decken fast alle Bedürfnisse für verschiedene Szenarien ab:

    • Implizite d-ary Heaps (ArrayHeap), <- das brauchen wir sicher
    • Pairing Heaps, <- Wenn wir den netten und sauberen Ansatz von IHeap wählen, lohnt es sich, dies hinzuzufügen, da es in vielen Fällen wirklich schnell und schneller ist als die Array-basierte Lösung .

    Bei beiden ist der Programmieraufwand überraschend gering. Schauen Sie sich meine Implementierungen an.


@karelz @benaadams

Dies wäre ein wirklich nützlicher Typ in CoreFX. Hat jemand Interesse, sich diesen zu schnappen?

@safen Das würde ich mir ab sofort sehr gerne schnappen.

OK, es war ein Problem zwischen meiner Tastatur und meinem Stuhl - PriorityQueue basiert natürlich auf Heap -- ich dachte nur an Queue wo es keinen Sinn macht und vergaß, dass der Haufen 'sortiert' ist - sehr peinlicher Denkprozess-Ausfall für jemanden wie mich, der Logik, Algorithmen, Turing-Maschinen usw. liebt, Entschuldigung. (Übrigens: Sobald ich ein paar Sätze in Ihrem Java-Docs-Link gelesen habe, hat die Diskrepanz sofort geklickt)

Aus dieser Perspektive ist es sinnvoll, die API auf Heap aufzubauen. Wir sollten diese Klasse jedoch noch nicht veröffentlichen - sie erfordert eine eigene API-Überprüfung und eine eigene Diskussion, wenn wir sie in CoreFX benötigen. Wir möchten kein API-Oberflächenkriechen aufgrund der Implementierung, aber es kann das Richtige sein – daher die erforderliche Diskussion.
Aus dieser Perspektive denke ich, dass wir noch nicht IHeap erstellen müssen. Es kann später eine gute Entscheidung sein.
Wenn es Untersuchungen gibt, dass ein bestimmter Heap (z. B. 4-ary, wie Sie oben erwähnt haben) am besten für allgemeine zufällige Eingaben geeignet ist , sollten wir diesen auswählen. Warten wir, bis @safern @ianhays @stephentoub die Meinung bestätigt /

Die Parametrisierung des zugrunde liegenden Heaps mit mehreren implementierten Optionen ist etwas, das IMO nicht in CoreFX gehört (ich kann hier wieder falsch liegen - mal sehen, was andere denken).
Mein Grund ist, dass wir IMO bald Millionen von spezialisierten Sammlungen ausliefern würden, die für die Leute (durchschnittliche Entwickler ohne fundierten Hintergrund in Nuancen von Algorithmen) sehr schwer zu wählen wären. Eine solche Bibliothek wäre jedoch ein großartiges NuGet-Paket für Experten auf diesem Gebiet - im Besitz von Ihnen/der Community. In Zukunft könnten wir erwägen, sie zu PowerCollections hinzuzufügen (wir diskutieren seit 4 Monaten aktiv, wo auf GitHub diese Bibliothek platziert werden soll und ob wir sie besitzen oder ob wir die Community ermutigen sollten, sie zu besitzen - es gibt verschiedene Meinungen zu diesem Thema , ich gehe davon aus, dass wir sein Schicksal nach 2.0 abschließen werden)

Ihnen zuordnen, wie Sie daran arbeiten möchten ...

@pgolebiowski- Mitarbeitereinladung gesendet - Wenn Sie Ping akzeptieren, kann ich sie Ihnen dann zuweisen (GitHub-Einschränkungen).

@benaadams Ich würde ICollection behalten (milde Präferenz). Für Konsistenz mit anderen DS in CoreFX. IMO lohnt es sich nicht, hier ein seltsames Biest zu haben ... wenn wir eine Handvoll neue hinzufügen würden (zB PowerCollections sogar zu einem anderen Repository), sollten wir die nicht-generischen nicht aufnehmen ... Gedanken?

OK, es war ein Problem zwischen meiner Tastatur und meinem Stuhl.

Haha Keine Sorge.

Es ist sinnvoll, die API auf Heap aufzubauen. Wir sollten diese Klasse aber noch nicht öffentlich machen [...] Wir wollen kein API-Oberflächenkriechen aufgrund der Implementierung, aber es könnte das Richtige sein - daher die notwendige Diskussion. [...] Ich glaube nicht, dass wir IHeap jetzt noch erstellen müssen. Es kann später eine gute Entscheidung sein.

Wenn sich die Gruppe für PriorityQueue , helfe ich nur beim Design und implementiere dies. Bitte bedenken Sie jedoch, dass, wenn wir jetzt ein PriorityQueue hinzufügen, es später in der API unübersichtlich wird, Heap hinzuzufügen - da sich beide im Grunde genommen gleich verhalten. Es wäre eine Art Redundanz IMO. Das wäre ein Design-Geruch für mich. Ich würde die Prioritätswarteschlange nicht hinzufügen. Es hilft nicht.

Außerdem noch ein Gedanke. Tatsächlich könnte sich die Pairing-Heap-Datenstruktur ziemlich oft als nützlich erweisen. Array-basierte Heaps sind beim Zusammenführen schrecklich. Dieser Vorgang ist grundsätzlich linear . Wenn Sie viele zusammengeführte Haufen haben, zerstören Sie die Leistung. Wenn Sie jedoch einen Paarungsheap anstelle eines Array-Heaps verwenden, ist der Zusammenführungsvorgang konstant (amortisiert). Dies ist ein weiteres Argument, warum ich eine schöne Schnittstelle und zwei Implementierungen bereitstellen möchte. Eine für allgemeine Eingaben, die zweite für einige spezifische Szenarien, insbesondere wenn Heaps zusammengeführt werden.

Abstimmung für IHeap + ArrayHeap + PairingHeap ! 😄 (wie in Rust / Swift / Python / Go)

Wenn der Pairing-Heap zu groß ist – OK. Aber lassen Sie uns wenigstens mit IHeap + ArrayHeap . Glauben Sie nicht, dass die Verwendung einer PriorityQueue Klasse die Möglichkeiten in der Zukunft versperrt und die API weniger klar macht?

Aber wie gesagt - wenn Sie alle für eine PriorityQueue Klasse gegenüber der vorgeschlagenen Lösung stimmen - OK.

Mitarbeitereinladung gesendet - Wenn Sie Ping akzeptieren, kann ich sie Ihnen dann zuweisen (GitHub-Einschränkungen).

@karelz ping :)

Bitte berücksichtigen Sie die Tatsache, dass, wenn wir jetzt eine PriorityQueue hinzufügen, es später in der API unübersichtlich wird, Heap hinzuzufügen - da sich beide im Grunde genommen gleich verhalten. Es wäre eine Art Redundanz IMO. Das wäre ein Design-Geruch für mich. Ich würde die Prioritätswarteschlange nicht hinzufügen. Es hilft nicht.

Können Sie genauer erklären, warum es später chaotisch sein wird? Was sind Ihre Bedenken?
PriorityQueue ist ein Konzept, das die Leute verwenden. Es ist sinnvoll, einen so benannten Typ zu haben, oder?
Ich denke, die logischen Operationen (zumindest ihre Namen) auf Heap könnten unterschiedlich sein. Wenn sie gleich sind, können wir im schlimmsten Fall 2 verschiedene Implementierungen desselben Codes haben (nicht ideal, aber nicht das Ende der Welt). Oder wir können die Klasse Heap als Eltern von PriorityQueue einfügen, richtig? (vorausgesetzt, es ist aus der Perspektive der API-Überprüfung erlaubt - im Moment sehe ich keinen Grund dafür, aber ich habe nicht so viele Jahre Erfahrung mit API-Überprüfungen, also warte ich auf die Bestätigung durch andere)

Mal sehen, wie das Voting & die weitere Designdiskussion abläuft ... Ich wärme mich langsam auf die Idee von IHeap + ArrayHeap , bin aber noch nicht ganz überzeugt ...

wenn wir eine Handvoll neue hinzufügen würden ... sollten wir die nicht generischen nicht einschließen

Roter Lappen zu einem Stier. Hat jemand noch andere Sammlungen hinzuzufügen, damit wir ICollection ?

Kreis-/Ringpuffer; Generisch und gleichzeitig?

@karelz Eine Lösung für das IPriorityQueue wie es DataFlow für Produktions-/Verbrauchermuster tut. Es gibt viele Möglichkeiten, eine Prioritätswarteschlange zu implementieren, und wenn Sie sich nicht darum kümmern, verwenden Sie die Schnittstelle. Kümmern Sie sich um die Implementierung oder erstellen Sie eine Instanz, verwenden Sie die implementierende Klasse.

Können Sie genauer erklären, warum es später chaotisch sein wird? Was sind Ihre Bedenken?
PriorityQueue ist ein Konzept, das die Leute verwenden. Es ist sinnvoll, einen so benannten Typ zu haben, oder? […] Ich werde langsam warm mit der Idee von IHeap + ArrayHeap , aber noch nicht ganz überzeugt ...

@karelz Aus meiner Erfahrung finde ich es wirklich wichtig, eine Abstraktion zu haben ( IPriorityQueue oder IHeap ). Dank eines solchen Ansatzes kann ein Entwickler entkoppelten Code schreiben. Da es für eine Schnittstelle geschrieben wird (und nicht für eine bestimmte Implementierung), gibt es mehr Flexibilität und IoC-Geist. Es ist sehr einfach, Unit-Tests für einen solchen Code zu schreiben (mit Dependency Injection kann man seine eigenen verspotteten IPriorityQueue oder IHeap injizieren und sehen, welche Methoden zu welcher Zeit und mit welchen Argumenten aufgerufen werden). Abstraktionen sind gut.

Es stimmt, dass der Begriff "Prioritätswarteschlange" allgemein verwendet wird. Das Problem ist, dass es nur eine Möglichkeit gibt, eine Prioritätswarteschlange effektiv zu implementieren – mit einem Heap. Viele Arten von Haufen. Wir könnten also haben:

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

oder

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

Für mich sieht der zweite Ansatz besser aus. Wie stehen Sie dazu?

Was die PriorityQueue -Klasse betrifft – ich denke, das wäre zu vage und bin absolut gegen eine solche Klasse. Eine vage Schnittstelle ist gut, aber keine Implementierung. Wenn es sich um einen binären Heap handelt, nennen Sie ihn einfach BinaryHeap . Wenn es etwas anderes ist, benennen Sie es entsprechend.

Ich bin ganz für die Benennung von Klassen wegen der Probleme mit Klassen wie SortedDictionary . Unter Entwicklern herrscht große Verwirrung darüber, wofür es steht und wie es sich von SortedList . Wenn es nur BinarySearchTree heißen würde, wäre das Leben einfacher und wir könnten früher nach Hause gehen und unsere Kinder sehen.

Nennen wir einen Haufen einen Haufen.

@benaadams jeder von ihnen muss es in CoreFX schaffen (dh er muss für den CoreFX-Code selbst wertvoll sein oder so einfach und weit verbreitet sein wie die, die wir bereits haben) -- wir haben diese Dinge in letzter Zeit ziemlich oft diskutiert . Immer noch kein 100%iger Konsens, aber niemand möchte CoreFX einfach weitere Sammlungen hinzufügen.

Das wahrscheinlichste Ergebnis (voreingenommene Behauptung, weil mir die Lösung gefällt) ist, dass wir ein weiteres Repo erstellen und es mit PowerCollections vorbereiten und es dann der Community mit einigen grundlegenden Anleitungen/Aufsichten von unserem Team erweitern lassen.
Übrigens :
// Ich denke, es gibt Gelegenheit für die Community, auf diese Lösung zu springen, bevor wir uns entscheiden ;-). Das würde die Diskussion (und meine Präferenz) stumm machen ;-)

@pgolebiowski du überzeugst mich langsam davon, dass Heap besser ist als PriorityQueue - wir brauchen nur eine starke Anleitung und Dokumente "So machst du PriorityQueue - benutze Heap " ... das könnte funktionieren.

Ich zögere jedoch sehr, mehr als eine Heap-Implementierung in CoreFX aufzunehmen. 98%+ der 'normalen' C#-Entwickler da draußen ist es egal. Sie wollen nicht einmal darüber nachdenken, welche die beste ist, sie brauchen nur etwas, das ihre Arbeit erledigt. Nicht jedes Teil jeder SW ist zu Recht auf hohe Leistung ausgelegt. Denken Sie an all die einmaligen Tools, UI-Apps usw. Wenn Sie kein Hochleistungs-Scaling-Out-System entwerfen, bei dem sich dieser ds auf dem kritischen Pfad befindet, sollte es Ihnen egal sein.

Genauso ist es mir egal, wie SortedDictionary oder ArrayList oder andere ds implementiert werden – sie machen ihre Arbeit anständig. Ich (wie viele andere) verstehe, dass ich, wenn ich für meine Szenarien eine hohe Leistung dieser DS brauche, die Leistung messen und/oder die Implementierung überprüfen und entscheiden muss, ob es für meine Szenarien gut genug ist oder ob ich es muss meine eigene spezielle Implementierung ausrollen, um die beste Leistung zu erzielen, die auf meine Bedürfnisse abgestimmt ist.

Wir sollten die Benutzerfreundlichkeit für 98% der Anwendungsfälle optimieren, nicht für die 2%. Wenn wir zu viele Optionen (Implementierungen) einführen und alle zwingen, sich zu entscheiden, haben wir bei 98% der Anwendungsfälle nur unnötige Verwirrung verursacht. ich finde es lohnt sich nicht...
Das IMO .NET-Ökosystem hat einen großen Wert darin, eine einzige Auswahl vieler APIs (nicht nur Sammlungen) mit sehr anständigen Leistungsmerkmalen anzubieten, die für die meisten Anwendungsfälle nützlich sind. Und bietet ein Ökosystem, das leistungsstarke Erweiterungen für diejenigen ermöglicht, die sie benötigen und bereit sind, tiefer zu graben und mehr zu lernen / fundierte Entscheidungen und Kompromisse zu treffen.

Allerdings kann es sinnvoll sein, eine Schnittstelle IHeap (wie IDictionary und IReadOnlyDictionary ) zu haben - ich muss noch ein bisschen darüber nachdenken / fragen Sie die API-Review-Experten in der Platz ...

Wir haben (bis zu einem gewissen Grad) bereits das, worüber @pgolebiowski mit ISet<T> und HashSet<T> spricht. Ich sage nur spiegeln. Die obige API wird also in eine Schnittstelle ( IPriorityQueue<T> ) geändert und dann haben wir eine Implementierung ( HeapPriorityQueue<T> ), die intern einen Heap verwendet, der als eigene Klasse öffentlich verfügbar gemacht werden kann oder nicht.

Sollte es ( PriorityQueue<T> ) auch IList<T> implementieren?

@karelz mein Problem mit ICollection ist SyncRoot und IsSynchronized ; entweder sind sie implementiert, was bedeutet, dass es eine zusätzliche Zuordnung für das Sperrobjekt gibt; oder sie werfen, wenn es ein bisschen sinnlos ist, sie zu haben.

@benaadams Das wäre irreführend. Da 99,99% der Implementierungen von Prioritätswarteschlangen Heaps sind, die auf Arrays basieren (und wie ich sehe, gehen wir auch hier mit einem vor), würde dies bedeuten, den Zugriff auf die interne Struktur des Arrays freizugeben?

Nehmen wir an, wir haben einen Heap mit den Elementen 4, 8, 10, 13, 30, 45. In Anbetracht der Reihenfolge würde auf sie über die Indizes 0, 1, 2, 3, 4, 5 zugegriffen. Die interne Struktur des Heaps ist [4, 8, 30, 10, 13, 45] (binär, quaternär wäre es anders).

  • Die Rückgabe der internen Nummer am Index i ist aus Sicht des Benutzers nicht wirklich sinnvoll, da es fast willkürlich ist.
  • Das Zurückgeben einer Zahl in der Reihenfolge (nach Priorität) ist zu kostspielig - dies ist linear.
  • In anderen Implementierungen ist es nicht wirklich sinnvoll, eine dieser Optionen zurückzugeben. Oftmals werden i Elemente einfach gepoppt, das i -te Element abgerufen und dann erneut verschoben.

IList<T> ist normalerweise der freche Workaround für: Ich möchte flexibel mit Sammlungen sein, die meine API akzeptiert, und ich möchte sie aufzählen, aber nicht über IEnumerable<T> zuweisen

Habe gerade festgestellt, dass es keine generische Schnittstelle für . gibt

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

Also egal (Obwohl es eine Art IReadOnlyCollection ist)

Aber Reset on Enumerator sollte explizit implementiert werden, da es schlecht ist und nur ausgelöst werden sollte.

Also meine Änderungsvorschläge

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

Da Sie über die Methoden sprechen... Wir haben uns noch nicht auf die Sache IHeap vs. IPriorityQueue geeinigt -- es beeinflusst die Namen der Methoden und die Logik ein wenig. Wie auch immer, ich finde, dass im aktuellen API-Vorschlag Folgendes fehlt:

  • Möglichkeit, ein bestimmtes Element aus der Warteschlange zu entfernen
  • Möglichkeit, die Priorität eines Elements zu aktualisieren
  • Fähigkeit, diese Strukturen zusammenzuführen

Diese Operationen sind ziemlich wichtig, insbesondere die Möglichkeit, ein Element zu aktualisieren. Ohne dies können viele Algorithmen einfach nicht implementiert werden. Wir müssen einen Handle-Typ einführen. In der API hier ist das Handle IHeapNode . Was ein weiteres Argument dafür ist, den IHeap Weg zu gehen, denn sonst müssten wir den PriorityQueueHandle Typ einführen, der immer nur ein Heap-Knoten wäre... 😜 Außerdem ist es nur vage, was er bedeutet... Bei einem Heap-Knoten hingegen kennt sich jeder aus und kann sich vorstellen, mit was er es zu tun hat.

Kurz gesagt, für den API-Vorschlag schauen Sie bitte in dieses Verzeichnis . Wir würden wahrscheinlich nur eine Teilmenge davon brauchen. Aber nichtsdestotrotz - es enthält nur das, was wir IMO brauchen, also könnte es sich lohnen, es als Ausgangspunkt zu betrachten.

Was sind eure Gedanken, Jungs?

IHeapNode ist nicht viel anders als der clr Typ KeyValuePair ?

Das trennt dann jedoch die Priorität und die Eingabe, also ist es jetzt ein PriorityQueue<TKey, TValue> mit einem IComparer<TKey> comparer ?

KeyValuePair ist nicht nur eine Struktur, sondern seine Eigenschaften sind schreibgeschützt. Es wäre im Grunde gleich, jedes Mal, wenn die Struktur aktualisiert wird, ein neues Objekt zu erstellen.

Nur einen Schlüssel zu verwenden funktioniert nicht mit gleichen Schlüsseln - es sind mehr Informationen erforderlich, um zu wissen, welches Element aktualisiert / entfernt werden soll.

Von IHeapNode und 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;
        }
    }

Und die Update-Methode innerhalb eines 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);
        }

Aber jetzt ist jedes Element im Heap ein zusätzliches Objekt; Was ist, wenn ich ein PriorityQueue-Verhalten möchte, diese zusätzlichen Zuweisungen jedoch nicht?

Dann könnten Sie keine Elemente aktualisieren / entfernen. Es wäre eine bloße Implementierung, die es Ihnen nicht erlaubt, einen Algorithmus zu implementieren, der von der DecreaseKey Operation abhängig ist (was sehr häufig vorkommt). Zum Beispiel der Algorithmus von Dijkstra, der Algorithmus von Prim. Oder wenn Sie eine Art Scheduler schreiben und ein Prozess (oder was auch immer) seine Priorität geändert hat, können Sie das nicht angehen.

Außerdem sind in jeder anderen Heap-Implementierung -- Elemente explizit nur Knoten. In anderen Fällen ist es ganz natürlich, hier ist es etwas künstlich, aber notwendig zum Aktualisieren / Entfernen.

In Java hat die Prioritätswarteschlange keine Option zum Aktualisieren von Elementen. Ergebnis:

Als ich Heaps für das AlgoKit-Projekt implementierte und auf all diese Designprobleme stieß, dachte ich, dass dies der Grund ist, warum die Autoren von .NET entschieden haben, keine so grundlegende Datenstruktur wie einen Heap (oder eine Prioritätswarteschlange) hinzuzufügen. Denn weder Design ist gut. Jeder hat seine Mängel.

Kurz gesagt -- wenn wir eine Datenstruktur hinzufügen möchten, die eine effiziente Sortierung von Elementen nach ihrer Priorität unterstützt, machen wir es besser richtig und fügen eine Funktionalität wie das Aktualisieren / Entfernen von Elementen hinzu.

Wenn Sie ein schlechtes Gewissen haben, Elemente in ein Array mit einem anderen Objekt zu packen, ist dies nicht das einzige Problem. Ein anderer fusioniert. Bei Array-basierten Heaps ist dies völlig ineffizient. Wenn wir jedoch die Pairing-Heap-Datenstruktur verwenden ( The pairing heap: A new form of self-adjusting heap ), dann:

  • Handles für die Elemente sind wundervolle Knoten -- diese sollten sowieso zugewiesen werden, also kein unordentliches Zeug
  • Das Zusammenführen ist konstant (im Vergleich zu linear in einer Array-basierten Lösung)

Eigentlich könnten wir das folgendermaßen lösen:

  • IHeap Schnittstelle hinzufügen, die alle Methoden unterstützt
  • Fügen Sie ArrayHeap und PairingHeap mit all diesen Handles hinzu, aktualisieren, entfernen, zusammenführen
  • Fügen Sie PriorityQueue was nur ein Wrapper um ein ArrayHeap , um die API zu vereinfachen

PriorityQueue wäre in System.Collections.Generic und alle Haufen in System.Collections.Specialized .

Funktioniert?

Es ist ziemlich unwahrscheinlich, dass wir durch die API-Überprüfung drei neue Datenstrukturen erhalten. In den meisten Fällen ist eine kleinere Menge API besser. Wir können später immer noch mehr hinzufügen, wenn es am Ende nicht ausreicht, aber wir können die API nicht entfernen.

Das ist einer der Gründe, warum ich kein Fan der HeapNode-Klasse bin. Imo sollte so etwas nur intern sein und die API sollte wenn möglich einen bereits existierenden Typ verfügbar machen - in diesem Fall wahrscheinlich KVP.

@ianhays Wenn dies nur intern beibehalten wird,

Übrigens: Eine verknüpfte Liste hat eine Knotenklasse, damit Benutzer die richtige Funktionalität verwenden können. Das ist ziemlich spiegelbildlich.

Benutzer könnten die Prioritäten von Elementen in der Datenstruktur nicht aktualisieren.

Das stimmt nicht unbedingt. Die Priorität könnte so offengelegt werden, dass keine zusätzliche Datenstruktur erforderlich ist, sodass anstelle von

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

        }

du hättest zB

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

        }

Ich bin auch noch nicht davon überzeugt, die Priorität als Typparameter freizugeben. Imo wird die Mehrheit der Leute den Standardprioritätstyp verwenden und der TValue wird eine unnötige Spezifität sein.

void Update(TKey item, TValue priority)

Erstens – was ist TKey und TValue in Ihrem Code? Wie soll das funktionieren? Die Konvention in der Informatik lautet:

  • Schlüssel = Priorität
  • Wert = was auch immer Sie in einem Element speichern möchten

Sekunde:

Imo wird die Mehrheit der Leute den Standardprioritätstyp verwenden und der TValue wird eine unnötige Spezifität sein.

Bitte definieren Sie "den Standardprioritätstyp". Ich kann fühlen, dass du nur PriorityQueue<T> haben willst, ist das richtig? Berücksichtigen Sie in diesem Fall die Tatsache, dass ein Benutzer wahrscheinlich eine neue Klasse erstellen muss, einen Wrapper um seine Priorität und seinen Wert, plus etwas wie IComparable implementieren oder einen benutzerdefinierten Komparator bereitstellen muss. Ziemlich arm.

Erstens – was sind TKey und TValue in Ihrem Code?

Das Element und die Priorität des Elements. Sie können sie auf die Priorität und das mit dieser Priorität verknüpfte Element umschalten, aber dann haben Sie eine Sammlung, in der die TKeys nicht unbedingt eindeutig sind (dh doppelte Prioritäten zulassen). Ich bin nicht dagegen, aber für mich bedeutet TKey normalerweise Einzigartigkeit.

Der Punkt ist, dass die Bereitstellung einer Node-Klasse keine Voraussetzung für die Bereitstellung einer Update-Methode ist.

Bitte definieren Sie "den Standardprioritätstyp". Ich kann fühlen, dass du nur PriorityQueue haben willst, ist das richtig?

Jawohl. Obwohl ich diesen Thread noch einmal durchgelesen habe, bin ich nicht ganz davon überzeugt, dass dies der Fall ist.

Berücksichtigen Sie in diesem Fall die Tatsache, dass ein Benutzer wahrscheinlich eine neue Klasse erstellen muss, einen Wrapper um seine Priorität und seinen Wert, plus etwas wie IComparable implementieren oder einen benutzerdefinierten Komparator bereitstellen muss. Ziemlich arm.

Du liegst nicht falsch. Ich stelle mir eine ganze Menge Nutzer müssen einen Wrapper - Typ mit Logik benutzerdefinierten Komparator erstellen. Ich kann mir auch vorstellen, dass es eine ganze Reihe von Benutzern gibt, die bereits einen vergleichbaren Typ haben, den sie in die Prioritätswarteschlange stellen möchten, oder einen Typ mit einem definierten Vergleicher. Die Frage ist, welches Lager das größere von beiden ist.

Ich denke, der Typ sollte PriorityQueue<T> heißen, nicht Heap<T> , ArrayHeap<T> oder sogar QuaternaryHeap<T> , um mit dem Rest von .Net konsistent zu bleiben:

  • Wir haben List<T> , nicht ArrayList<T> .
  • Wir haben Dictionary<K, V> , nicht HashTable<K, V> .
  • Wir haben ImmutableList<T> , nicht ImmutableAVLTreeList<T> .
  • Wir haben Array.Sort() , nicht Array.IntroSort() .

Benutzern solcher Typen ist es normalerweise egal, wie sie implementiert werden, es sollte sicherlich nicht das Auffälligste an dem Typ sein.

Berücksichtigen Sie in diesem Fall die Tatsache, dass ein Benutzer wahrscheinlich eine neue Klasse erstellen muss, einen Wrapper um seine Priorität und seinen Wert, plus etwas wie IComparable implementieren oder einen benutzerdefinierten Komparator bereitstellen muss.

Du liegst nicht falsch. Ich kann mir vorstellen, dass eine ganze Reihe von Benutzern einen Wrapper-Typ mit benutzerdefinierter Komparatorlogik erstellen müssen.

In der Übersichts-API wird der Vergleich durch den bereitgestellten Vergleicher IComparer<T> comparer bereitgestellt, es ist kein Wrapper erforderlich. Oft ist die Priorität Teil des Typs, zB hat ein Zeitplaner die Ausführungszeit als Eigenschaft des Typs.

Die Verwendung eines KeyValuePair mit einem benutzerdefinierten IComparer fügt keine zusätzlichen Zuweisungen hinzu; Es fügt jedoch eine zusätzliche Indirektion für eine Aktualisierung hinzu, da Werte statt des Elements verglichen werden müssen.

Aktualisierungen wären für struct-Elemente problematisch, es sei denn, das Element wurde über eine ref-Rückgabe abgerufen und dann aktualisiert und per ref an eine Aktualisierungsmethode zurückgegeben und die Refs wurden verglichen. Aber das ist ein bisschen schrecklich.

@svick

Ich denke, der Typ sollte PriorityQueue<T> , nicht Heap<T> , ArrayHeap<T> oder sogar QuaternaryHeap<T> heißen, um mit dem Rest von .Net konsistent zu bleiben.

Ich verliere meinen Glauben an die Menschheit.

  • Das Aufrufen einer Datenstruktur PriorityQueue ist mit dem Rest von .NET konsistent und das Aufrufen von Heap nicht? Stimmt was nicht mit Haufen? Dasselbe sollte dann für Stacks und Queues gelten.
  • ArrayHeap -> Basisklasse für QuaternaryHeap . Riesiger Unterschied.
  • In der Diskussion geht es nicht darum, einen coolen Namen zu finden. Es gibt eine Menge Dinge, die sich ergeben, wenn man einem bestimmten Designpfad folgt. Lies den Thread bitte nochmal.
  • Hashtable , ArrayList . Sie scheinen zu existieren. Übrigens, eine "Liste" ist eine ziemlich schlechte Wahl für die Benennung von IMO, da der erste Gedanke eines Benutzers ist, dass List eine Liste ist, aber keine Liste. 😜
  • Es geht nicht darum, Spaß zu haben und zu sagen, wie eine Sache umgesetzt wird. Es geht darum, aussagekräftige Namen zu vergeben, die den Nutzern sofort zeigen, womit sie es zu tun haben.

Möchten Sie mit dem Rest von .NET konsistent bleiben und auf Probleme wie dieses stoßen?

Und was sehe ich dort ... Jon Skeet sagt, dass SortedDictionary SortedTree heißen sollte, da dies die Implementierung genauer widerspiegelt.

@pgolebiowski

Stimmt was nicht mit Haufen?

Ja, es beschreibt die Implementierung, nicht die Verwendung.

Dasselbe sollte dann für Stacks und Queues gelten.

Beides beschreibt die Implementierung nicht wirklich, zum Beispiel sagt der Name nicht, dass sie Array-basiert sind.

ArrayHeap -> Basisklasse für QuaternaryHeap . Riesiger Unterschied.

Ja, ich verstehe dein Design. Was ich sage, ist, dass nichts davon dem Benutzer ausgesetzt werden sollte.

Es gibt eine Menge Dinge, die sich ergeben, wenn man einem bestimmten Designpfad folgt. Lies den Thread bitte nochmal.

Ich habe den Thread gelesen. Ich glaube nicht, dass Design zum BCL gehört. Ich denke, es sollte etwas enthalten, das einfach zu verwenden und zu verstehen ist, und nicht etwas, das dazu führt, dass sich die Leute fragen, was "quaternärer Heap" ist oder ob sie ihn verwenden sollten.

Wenn die Standardimplementierung für sie nicht gut genug ist, kommen andere Bibliotheken (wie Ihre eigene) ins Spiel.

Hashtabelle, ArrayList. Sie scheinen zu existieren.

Ja, das sind .Net Framework 1.0-Klassen, die niemand mehr verwendet. Soweit ich das beurteilen kann, wurden ihre Namen von Java kopiert und die .Net Framework 2.0-Designer haben sich entschieden, dieser Konvention nicht zu folgen. Das war meiner Meinung nach die richtige Entscheidung.

Übrigens, eine "Liste" ist eine ziemlich schlechte Wahl für die Benennung von IMO, da der erste Gedanke eines Benutzers ist, dass List eine Liste ist, aber keine Liste.

Es ist. Es ist keine verlinkte Liste, aber das ist nicht dasselbe. Und ich mag es, nicht überall ArrayList oder ResizeArray (F#s Name für List<T> ) schreiben zu müssen.

Es geht darum, aussagekräftige Namen zu vergeben, die den Nutzern sofort zeigen, womit sie es zu tun haben.

Die meisten Leute haben keine Ahnung, womit sie es zu tun haben, wenn sie QuaternaryHeap . Auf der anderen Seite, wenn sie PriorityQueue , sollte klar sein, womit sie es zu tun haben, auch wenn sie keinen CS-Hintergrund haben. Sie werden nicht wissen, was die Implementierung ist, aber dafür ist die Dokumentation da.

@ianhays

Erstens – was sind TKey und TValue in Ihrem Code?

Das Element und die Priorität des Elements. Sie können sie auf die Priorität und das mit dieser Priorität verknüpfte Element umschalten, aber dann haben Sie eine Sammlung, in der die TKeys nicht unbedingt eindeutig sind (dh doppelte Prioritäten zulassen). Ich bin nicht dagegen, aber für mich bedeutet TKey normalerweise Einzigartigkeit.

Schlüssel – Dinge, die verwendet werden, um die Priorisierung durchzuführen. Schlüssel müssen nicht eindeutig sein. Wir können eine solche Annahme nicht treffen.

Der Punkt ist, dass die Bereitstellung einer Node-Klasse keine Voraussetzung für die Bereitstellung einer Update-Methode ist.

Ich glaube, es ist: Unterstützung für Schlüssel verringern/Schlüssel erhöhen in nativen Bibliotheken . Auch zu diesem Thema gibt es also Hilfsklassen für den Umgang mit Datenstrukturen:

Ich kann mir vorstellen, dass eine ganze Reihe von Benutzern einen Wrapper-Typ mit benutzerdefinierter Komparatorlogik erstellen müssen. Ich kann mir auch vorstellen, dass es eine ganze Reihe von Benutzern gibt, die bereits einen vergleichbaren Typ haben, den sie in die Prioritätswarteschlange stellen möchten, oder einen Typ mit einem definierten Vergleicher. Die Frage ist, welches Lager das größere von beiden ist.

Ich weiß nichts über die anderen, aber ich finde es ziemlich schrecklich, jedes Mal, wenn ich eine Prioritätswarteschlange verwenden möchte, einen Wrapper mit benutzerdefinierter Komparatorlogik zu erstellen ...

Noch ein Gedanke – nehmen wir an, ich habe einen Wrapper erstellt:

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

Ich füttere PriorityQueue<T> mit diesem Typ. Auf dieser Grundlage priorisiert es seine Elemente. Nehmen wir an, es hat einen Schlüsselselektor definiert. Ein solches Design kann die interne Arbeit eines Heaps zum Absturz bringen. Sie können die Prioritäten jederzeit ändern, da Sie der Eigentümer des Elements sind. Es gibt keinen Benachrichtigungsmechanismus für die Aktualisierung des Heaps. Sie wären dafür verantwortlich, all das zu handhaben und anzurufen. Ziemlich gefährlich und für mich nicht direkt.

Im Fall des von mir vorgeschlagenen Handles war der Typ aus der Sicht des Benutzers unveränderlich. Und es gab keine Probleme mit der Einzigartigkeit.

IMO Ich mag die Idee, eine IHeap Schnittstelle zu haben und dann die Klassen-API, die diese Schnittstelle implementiert. Ich stimme mit PriorityQueue konzentrieren möchten.

Zweitens denke ich, dass es nicht erforderlich ist, eine interne/öffentliche Klasse Node zu haben, um die Werte in der Warteschlange zu speichern. Es sind keine zusätzlichen Zuweisungen erforderlich, wir könnten einer Art folgen, was IDictionary tut und wenn die Werte für das Enumerable-Erzeugungs-SchlüsselWert-Paar zurückgegeben werdenObjekte, wenn wir uns für die Option entscheiden, für jeden Artikel, den wir speichern, eine Priorität zu haben (was meiner Meinung nach nicht der beste Weg ist, ich bin nicht ganz davon überzeugt, eine Priorität pro Artikel zu speichern). Der beste Ansatz, den ich gerne hätte, wäre PriorityQueue<T> (dieser Name ist nur um ihm einen zu geben). Mit diesem Ansatz könnten wir einen Konstruktor haben, der dem folgt, was die gesamte BCL mit einem IComparer<T> und den Vergleich mit diesem Comprarer durchführt, um die Priorität zu geben. Es ist nicht erforderlich, APIs bereitzustellen, die eine Priorität als Parameter übergeben.

Ich denke, wenn einige der APIs eine Priorität erhalten, würde dies für ordinale Kunden, die die Standardpriorität sein möchten, "weniger nutzbar" oder komplizierter werden Option, ein benutzerdefiniertes IComparer<T> zu haben, ist die vernünftigste und wird den Richtlinien folgen, die wir bei BCL haben.

Die Namen sind, was sie tun, nicht wie sie es tun.

Sie sind nach den abstrakten Konzepten und dem, was sie für den Benutzer erreichen, benannt, nicht nach ihrer Implementierung und wie sie es erreichen. (Was auch bedeutet, dass ihre Implementierung verbessert werden kann, um eine andere Implementierung zu verwenden, wenn sich dies als besser erweist)

Dasselbe sollte dann für Stacks und Queues gelten.

Stack ist ein unbegrenztes Größenänderungs-Array und Queue ist als unbegrenzter Größenänderungs-Ringpuffer implementiert. Sie sind nach ihren abstrakten Begriffen benannt. Stack (abstrakter Datentyp) : Last-In-First-Out (LIFO), Queue (abstrakter Datentyp) : First-In-First-Out (FIFO)

Hashtabelle, ArrayList. Sie scheinen zu existieren.

Eintrag im Wörterbuch

Ja, aber tun wir so, als ob sie es nicht tun; sie sind Artefakte aus einer weniger zivilisierten Zeit. Sie haben keine Typsicherheit und keine Box, wenn Sie Primitive oder Structs verwenden; also bei jedem Hinzufügen zuordnen.

Sie können den Platform Compatibility Analyzer von @terrajobst verwenden und er wird Ihnen sagen: "Bitte nicht"

Liste ist eine Liste, aber keine Liste

Es ist wirklich eine Liste (abstrakter Datentyp), die auch als Sequenz bekannt ist.

Warteschlange mit gleicher

Viele tolle Diskussionen!

Die ursprüngliche Spezifikation wurde unter Berücksichtigung einiger Kernprinzipien entwickelt:

  • Allgemeiner Zweck – Decken Sie die meisten Anwendungsfälle mit einem ausgewogenen Verhältnis zwischen CPU- und Arbeitsspeicherverbrauch ab.
  • Richten Sie sich so weit wie möglich an den vorhandenen Mustern und Konventionen in System.Collections.Generic aus.
  • Erlaube mehrere Einträge desselben Elements.
  • Nutzen Sie die vorhandenen BCL-Vergleichsschnittstellen und -klassen (z. B. Comparer).*
  • Hohe Abstraktion – Die Spezifikation soll Verbraucher von der zugrunde liegenden Implementierungstechnologie abschirmen.

@karelz @pgolebiowski
Die Umbenennung in "Heap" oder einen anderen Begriff, der auf eine Datenstrukturimplementierung ausgerichtet ist, würde den meisten BCL-Konventionen für Sammlungen nicht entsprechen. Historisch gesehen wurden .NET-Sammlungsklassen für allgemeine Zwecke konzipiert, anstatt sich auf die spezifische Datenstruktur/das spezifische Datenmuster zu konzentrieren. Meine ursprüngliche Überlegung war, dass die API-Designer zu Beginn des .NET-Ökosystems absichtlich von "ArrayList" zu "List". Die Änderung war wahrscheinlich auf eine Verwechslung mit einem Array zurückzuführen - Ihr durchschnittlicher Entwickler hätte gedacht "ArrayList? Ich möchte nur eine Liste, kein Array".

Wenn wir Heap verwenden, könnte das gleiche passieren - viele Entwickler mit mittlerer Erfahrung sehen (leider) "Heap" und verwechseln es mit dem Speicherheap der Anwendung (dh Heap und Stack) anstelle von "Heap die verallgemeinerte Datenstruktur". Die Verbreitung von System.Collections.Generic wird dazu führen, dass es in fast jedem intelligenten Vorschlag jedes .NET-Entwicklers auftaucht, und sie werden sich fragen, warum sie einen neuen Speicherheap zuweisen können :)

PriorityQueue ist im Vergleich dazu weit auffindbarer und weniger anfällig für Verwechslungen. Sie können "Queue" eingeben und Vorschläge für PriorityQueue erhalten.

Einige Vorschläge und Fragen zu ganzzahligen Prioritäten oder einem generischen Parameter für die Priorität (TKey, TPriority usw.). Das Hinzufügen einer expliziten Priorität würde erfordern, dass Verbraucher ihre eigene Logik schreiben, um ihre Prioritäten abzubilden und die Komplexität der API zu erhöhen. Verwenden des integrierten IComparernutzt die vorhandene BCL-Funktionalität, und ich habe auch überlegt, Überladungen zu Comparer hinzuzufügenin die Spezifikation ein, um die Bereitstellung von Ad-hoc-Lambda-Ausdrücken/anonymen Funktionen als Prioritätsvergleiche zu erleichtern. Das ist leider keine übliche Konvention in der BCL.

Wenn Einträge eindeutig sein müssen, würde Enqueue() eine Eindeutigkeitssuche erfordern, um eine ArgumentException auszulösen. Darüber hinaus gibt es wahrscheinlich gültige Szenarien, in denen ein Element mehr als einmal in die Warteschlange gestellt werden kann. Dieses nicht eindeutige Design macht die Bereitstellung einer Update()-Operation zu einer Herausforderung, da es keine Möglichkeit gibt, festzustellen, welches Objekt aktualisiert wird. Wie einige Kommentare andeuteten, würde dies anfangen, in APIs zu gelangen, die "Knoten"-Referenzen zurückgeben, die wiederum (wahrscheinlich) Zuweisungen erfordern würden, die einer Garbage Collection unterzogen werden müssten. Selbst wenn dies umgangen würde, würde dies den Speicherverbrauch pro Element der Prioritätswarteschlange erhöhen.

Irgendwann hatte ich eine benutzerdefinierte IPriorityQueue-Schnittstelle in der API, bevor ich die Spezifikation veröffentlichte. Letztendlich habe ich mich dagegen entschieden - das angestrebte Nutzungsmuster war Enqueue, Dequeue und Iterate. Bereits durch das vorhandene Schnittstellenset abgedeckt. Betrachten Sie dies als eine intern sortierte Warteschlange; solange Elemente ihre eigene (anfängliche) Position in der Warteschlange basierend auf ihrem IComparer haben, muss sich der Anrufer nie darum kümmern, wie die Priorität dargestellt wird. In der alten Referenzimplementierung, die ich gemacht habe, wird (wenn ich mich recht erinnere!) die Priorität überhaupt nicht dargestellt. Es ist alles relativ basierend auf ICompareroder Vergleich.

Ich schulde Ihnen allen einige Kundencodebeispiele - mein ursprünglicher Plan war es, die vorhandenen BCL-Implementierungen von PriorityQueue als Grundlage für die Beispiele zu verwenden.

In der Übersichts-API wird der Vergleich vom bereitgestellten Vergleicher IComparer bereitgestelltvergleichen, ist kein Wrapper erforderlich. Oft ist die Priorität Teil des Typs, zB hat ein Zeitplaner die Ausführungszeit als Eigenschaft des Typs.

Unter der vorgeschlagenen OP-API muss eine der folgenden Bedingungen erfüllt sein, um die Klasse zu verwenden:

  • Haben Sie einen Typ, der bereits so vergleichbar ist, wie Sie ihn haben möchten
  • Umschließen Sie einen Typ mit einer anderen Klasse, die den Prioritätswert enthält und wie gewünscht vergleicht
  • Erstellen Sie einen benutzerdefinierten IComparer für Ihren Typ und übergeben Sie ihn über den Konstruktor. Geht davon aus, dass der Wert, den Sie darstellen möchten, bereits von Ihrem Typ öffentlich verfügbar gemacht wird.

Die Dual-Type-API zielt darauf ab, die Last der zweiten beiden Optionen zu verringern, ja? Wie funktioniert der Aufbau einer neuen PriorityQueue/Heap, wenn Sie einen Typ haben, der bereits die Priorität enthält? Wie sieht die API aus?

Ich glaube, es ist: Unterstützung für Schlüssel verringern/Schlüssel erhöhen in nativen Bibliotheken.

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

In jedem Fall wird das der gegebenen Priorität zugeordnete Element aktualisiert. Warum ist der ArrayHeapNode für die Aktualisierung erforderlich? Was wird erreicht, was nicht erreicht werden kann, indem der TKey/TValue direkt genommen wird?

@ianhays

In jedem Fall wird das der gegebenen Priorität zugeordnete Element aktualisiert. Warum ist der ArrayHeapNode für die Aktualisierung erforderlich? Was wird erreicht, was nicht erreicht werden kann, indem der TKey/TValue direkt genommen wird?

Zumindest mit dem binären Heap (dem mir am besten vertraut ist), wenn Sie die Priorität eines Wertes aktualisieren möchten und seine Position im Heap kennen, können Sie dies schnell (in O (log n) Zeit) tun.

Wenn Sie jedoch nur den Wert und die Priorität kennen, müssen Sie zuerst den Wert finden, der langsam ist (O(n)).

Wenn Sie andererseits nicht effizient aktualisieren müssen, ist diese eine Zuordnung pro Element im Heap ein reiner Overhead.

Ich würde gerne eine Lösung sehen, bei der Sie diesen Overhead nur dann bezahlen, wenn Sie ihn benötigen, aber das ist möglicherweise nicht implementierbar und die resultierende API sieht möglicherweise nicht gut aus.

Die Dual-Type-API zielt darauf ab, die Last der zweiten beiden Optionen zu verringern, ja? Wie funktioniert der Aufbau einer neuen PriorityQueue/Heap, wenn Sie einen Typ haben, der bereits die Priorität enthält? Wie sieht die API aus?

Das ist ein guter Punkt, es gibt eine Lücke in der ursprünglichen API für Szenarien, in denen der Verbraucher bereits einen Prioritätswert (dh eine Ganzzahl) hat, der getrennt vom Wert verwaltet wird. Die Kehrseite ist, dass aAPI-Stil würde alle intrinsisch vergleichbaren Typen verkomplizieren. Vor Jahren habe ich eine sehr leichte Scheduler-Komponente geschrieben, die eine eigene interne ScheduledTask-Klasse hatte. Jede ScheduledTask implementierte IComparer. Werfen Sie sie in eine Prioritätswarteschlange, gut zu gehen.

Sortierte Listeverwendet ein Schlüssel/Wert-Design und ich habe es deshalb vermieden. Es erfordert, dass Schlüssel eindeutig sind, und meine übliche Anforderung ist "Ich habe N Werte, die ich in einer Liste sortieren muss, und stellen Sie sicher, dass gelegentliche Werte, die ich hinzufüge, sortiert bleiben". Ein "Schlüssel" ist kein Teil dieser Gleichung, eine Liste ist kein Wörterbuch.

Für mich gilt das gleiche Prinzip für Warteschlangen. Historisch gesehen ist eine Warteschlange eine eindimensionale Datenstruktur.

Mein Wissen über moderne C++-Std-Bibliotheken ist zugegebenermaßen etwas eingerostet, aber selbst std::priority_queue scheint Push, Pop und einen Vergleich als vorlagenbasierten (generischen) Parameter zu haben. Die Crew der C++-Standardbibliothek ist so leistungssensitiv wie möglich :)

Ich habe gerade einen sehr schnellen Scan der Prioritätswarteschlangen- und Heap-Implementierungen in einigen Programmiersprachen durchgeführt - C++, Java, Rust und Go funktionieren alle mit einem einzigen Typ (ähnlich der hier veröffentlichten ursprünglichen API). Ein flüchtiger Blick auf die beliebtesten Heap-/Prioritätswarteschlangenimplementierungen in NPM zeigt dasselbe.

@pgolebiowski versteh mich nicht falsch, es sollte spezifische Implementierungen von Dingen geben, die explizit nach ihrer spezifischen Implementierung benannt sind.

Dies ist jedoch der Fall, wenn Sie wissen, welche spezifische Datenstruktur Sie wünschen, die den von Ihnen angestrebten Leistungszielen entspricht und die Kompromisse aufweist, die Sie akzeptieren möchten.

Im Allgemeinen decken die Framework-Sammlungen die 90% der Anwendungen ab, bei denen Sie ein allgemeines Verhalten wünschen. Wenn Sie dann ein sehr spezifisches Verhalten oder eine sehr spezifische Implementierung wünschen, würden Sie sich wahrscheinlich für eine Bibliothek von Drittanbietern entscheiden. und sie werden hoffentlich nach der Implementierung benannt, damit Sie wissen, dass sie Ihren Anforderungen entspricht.

Ich möchte die allgemeinen Verhaltenstypen nur nicht an eine bestimmte Implementierung binden; da es dann seltsam ist, wenn sich die Implementierung ändert, weil der Typname gleich bleiben muss und sie nicht übereinstimmen.

Es gibt viele Bedenken, die diskutiert werden müssen, aber beginnen wir mit dem, das derzeit den größten Einfluss auf die Teile hat, über die wir uns nicht einig sind: das Aktualisieren und Entfernen von Elementen .

Wie wollen Sie diese Operationen dann unterstützen? Wir müssen sie einbeziehen, sie sind grundlegend. In Java haben die Designer sie weggelassen, und als Ergebnis:

  1. In Foren gibt es viele Fragen zur Problemumgehung aufgrund fehlender Funktionen.
  2. Es gibt Drittanbieter-Implementierungen einer Heap-/Prioritätswarteschlange, um die Erstanbieter-Implementierung zu ersetzen, da sie ziemlich nutzlos ist.

Das ist einfach erbärmlich. Gibt es jemanden, der wirklich einen solchen Weg verfolgen möchte? Ich würde mich schämen, eine so deaktivierte Datenstruktur freizugeben.

@pgolebiowski sei versichert, dass jeder hier die besten Absichten für die Plattform hat. Niemand möchte defekte APIs versenden. Wir möchten aus den Fehlern anderer lernen, also bringen Sie bitte weiterhin solche nützlichen relevanten Informationen (wie die Geschichte über Java) mit.

Ich möchte jedoch auf einige Dinge hinweisen:

  • Erwarten Sie keine Veränderungen über Nacht. Dies ist eine Designdiskussion. Wir müssen einen Konsens finden. Wir überstürzen APIs nicht. Jede Meinung sollte gehört und berücksichtigt werden, aber es gibt keine Garantie dafür, dass die Meinung aller umgesetzt/akzeptiert wird. Wenn Sie Eingaben gemacht haben, stellen Sie diese zur Verfügung, untermauern Sie sie mit Daten und Beweisen. Hören Sie sich auch die Argumente anderer an, bestätigen Sie sie. Legen Sie Beweise gegen die Meinung anderer vor, wenn Sie anderer Meinung sind. Manchmal stimmen Sie der Tatsache zu, dass es in einigen Punkten Meinungsverschiedenheiten gibt. Beachten Sie, dass SW, inkl. API-Design ist nicht schwarz oder weiß / richtig oder falsch.
  • Lassen Sie uns die Diskussion zivil halten. Lassen Sie uns keine starken Worte und Aussagen verwenden. Lassen Sie uns mit Anmut nicht zustimmen und die Diskussion technisch halten. Jeder kann sich auch auf den Verhaltenskodex für
  • Wenn Sie Bedenken/Fragen bezüglich der Geschwindigkeit der Designdiskussion, Reaktionen usw. haben, können Sie sich gerne direkt an mich wenden (meine E-Mail befindet sich in meinem GH-Profil). Ich kann bei Bedarf helfen, Erwartungen, Annahmen und Bedenken öffentlich oder offline zu klären.

Ich habe nur gefragt, wie ihr die Aktualisierung/Entfernung gestalten wollt, wenn euch der vorgeschlagene Ansatz nicht gefällt... Das heißt, anderen zuzuhören und nach einem Konsens zu suchen, glaube ich.

Ich zweifle nicht an deinen guten Absichten! Manchmal ist es wichtig, wie Sie fragen - es beeinflusst, wie die Leute den Text auf der anderen Seite wahrnehmen. Text ist frei von Emotionen, so dass Dinge beim Aufschreiben anders verstanden werden können. Englisch als Zweitsprache trübt die Dinge noch mehr und wir alle müssen uns dessen bewusst sein. Bei Interesse spreche ich gerne offline über Details ... lenken wir die Diskussion hier wieder zurück zur technischen Diskussion ...

Meine zwei Cent zur Heap vs. PriorityQueue-Debatte: Beide Ansätze sind gültig und haben eindeutig Vor- und Nachteile.

Allerdings scheint "PriorityQueue" mit dem bestehenden .NET-Ansatz weitaus konsistenter zu sein. Heute sind die Kernkollektionen List, Wörterbuch, Stapel, Warteschlange, HashSet, Sortiertes Wörterbuch, und SortedSet. Diese sind nach der Funktionalität und Semantik benannt, nicht nach dem Algorithmus. HashSetist der einzige Ausreißer, aber selbst dies kann mit Bezug auf die Semantik der Mengengleichheit (im Vergleich zu SortedSet). Immerhin haben wir jetzt ImmutableHashSetdie auf einem Baum unter der Haube basiert.

Es würde sich komisch anfühlen, wenn sich eine Kollektion hier dem Trend widersetzt.

Ich denke PriorityQueue mit zusätzlichem Konstruktor: PriorityQueue(IHeap) kann eine Lösung sein. Konstruktoren ohne IHeap-Parameter können den Standardheap verwenden.
In diesem Fall PrioriryQueuestellt einen abstrakten Datentyp dar (wie die meisten C#-Sammlungen) und implementiert IPriorityQueueSchnittstelle, kann aber verschiedene Heap-Implementierungen wie von @pgolebiowski vorgeschlagen verwenden:

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

OK. Es gibt viele verschiedene Stimmen. Ich ging die Diskussion noch einmal durch und verfeinerte meinen Ansatz. Ich gehe auch auf allgemeine Bedenken ein. Der folgende Text verwendet Zitate aus den obigen Beiträgen.

Unser Ziel

Das ultimative Ziel dieser gesamten Diskussion ist es, einen bestimmten Satz von Funktionalitäten bereitzustellen, um den Benutzer glücklich zu machen. Es ist ein ziemlich häufiger Fall, dass ein Benutzer eine Reihe von Elementen hat, von denen einige eine höhere Priorität haben als andere. Schließlich möchten sie diese Reihe von Elementen in einer bestimmten Reihenfolge halten, um die folgenden Operationen effizient ausführen zu können:

  • Rufen Sie das Element mit der höchsten Priorität ab (und können Sie es entfernen).
  • Fügen Sie der Sammlung ein neues Element hinzu.
  • Entfernen Sie ein Element aus der Auflistung.
  • Ändern Sie ein Element in der Auflistung.
  • Zwei Sammlungen zusammenführen.

Andere Standardbibliotheken

Auch die Autoren anderer Standardbibliotheken haben versucht, diese Funktionalität zu unterstützen. In diesem Abschnitt werde ich mich darauf beziehen, wie es in Python , Java , C++ , Go , Swift und Rust gelöst wurde.

Keines von ihnen unterstützt das Ändern von Elementen, die bereits in die Sammlung eingefügt wurden. Es ist äußerst überraschend, denn es ist sehr wahrscheinlich, dass sich während der Lebensdauer einer Sammlung die Prioritäten ihrer Elemente ändern. Insbesondere, wenn eine solche Sammlung während der gesamten Lebensdauer eines Dienstes verwendet werden soll. Es gibt auch eine Reihe von Algorithmen, die genau diese Funktionalität der Aktualisierung von Elementen nutzen. Ein solches Design ist also schlichtweg falsch, denn dadurch gehen unsere Kunden verloren: StackOverflow . Dies ist eine von vielen, vielen Fragen dieser Art über das Internet. Die Designer haben versagt.

Bemerkenswert ist auch, dass jede einzelne Bibliothek diese Teilfunktionalität durch die Implementierung eines binären Heaps bereitstellt. Nun, es hat sich gezeigt, dass es im allgemeinen Fall (zufällige Eingabe) nicht die performanteste Implementierung ist. Die beste Anpassung für den allgemeinen Fall ist ein quaternärer Heap (ein impliziter Heap-geordneter vollständiger 4-ärer Baum, der als Array gespeichert wird). Es ist deutlich weniger bekannt und dies ist wahrscheinlich der Grund, warum sich die Designer stattdessen für einen binären Heap entschieden haben. Aber trotzdem – eine weitere schlechte Wahl, wenn auch von geringerer Schwere.

Was lernen wir daraus?

  • Wenn wir möchten, dass unsere Kunden zufrieden sind und sie davon abhalten, diese Funktionalität selbst zu implementieren, während wir unsere wunderbare Datenstruktur ignorieren, sollten wir eine Unterstützung bieten, um bereits in die Sammlung eingefügte Elemente zu ändern.
  • Wir sollten nicht davon ausgehen, dass wir das Gleiche tun sollten,

Vorgeschlagene Herangehensweise

Ich bin der festen Überzeugung, dass wir Folgendes bereitstellen sollten:

  • IHeap<T> Schnittstelle
  • Heap<T> Klasse

Die IHeap Schnittstelle würde offensichtlich Methoden enthalten, um alle am Anfang dieses Beitrags beschriebenen Operationen auszuführen. Die Klasse Heap , implementiert mit einem quaternären Heap, wäre in 98% der Fälle die Lösung der Wahl. Die Reihenfolge der Elemente basiert auf IComparer<T> an den Konstruktor übergeben werden, oder der Standardreihenfolge, wenn ein Typ bereits vergleichbar ist.

Rechtfertigung

  • Entwickler können ihre Logik gegen eine Schnittstelle schreiben. Ich gehe davon aus, dass jeder weiß, wie wichtig das ist, und werde nicht ins Detail gehen. Lesen Sie: Prinzip der Abhängigkeitsinversion , Design by Contract , Inversion der Steuerung .
  • Entwickler können diese Funktionalität erweitern, um ihre benutzerdefinierten Anforderungen zu erfüllen, indem sie andere Heap-Implementierungen bereitstellen. Solche Implementierungen könnten in Bibliotheken von Drittanbietern wie PowerCollections enthalten sein . Indem Sie einfach auf eine solche Bibliothek verweisen, können Sie Ihren benutzerdefinierten Heap einfach in jede Logik einfügen, die IHeap als Eingabe benötigt. Einige Beispiele für andere Heaps, die sich unter bestimmten Bedingungen besser verhalten als der quaternäre Heap, sind: Pairing Heap , Binomial Heap und unser geliebter Binary Heap.
  • Wenn ein Entwickler nur ein Tool benötigt, das seine Arbeit erledigt, ohne dass er sich überlegen muss, welcher Typ der beste ist, kann er einfach die Allzweckimplementierung Heap . Dies optimiert in Richtung 98% der Anwendungsfälle.
  • Wir tragen zum großen Wert des .NET-Ökosystems in Bezug auf das Angebot einer einzigen Auswahl mit sehr anständigen Leistungsmerkmalen bei, die für die meisten Anwendungsfälle nützlich sind und gleichzeitig leistungsstarke Erweiterungen für diejenigen ermöglichen, die sie benötigen und bereit sind zu graben vertiefen und mehr erfahren / fundierte Entscheidungen und Kompromisse treffen.
  • Der vorgeschlagene Ansatz spiegelt aktuelle Konventionen wider:

    • ISet und HashSet

    • IList und List

    • IDictionary und Dictionary

  • Einige Leute haben gesagt, dass wir Klassen basierend auf dem, was ihre Instanzen tun, benennen sollten, und nicht darauf, wie sie es tun. Dies ist nicht ganz richtig. Es ist eine gängige Abkürzung zu sagen, dass wir Klassen nach ihrem Verhalten benennen sollten. Es trifft in der Tat in zahlreichen Fällen zu. Es gibt jedoch Fälle, in denen dies nicht der richtige Ansatz ist. Die bemerkenswertesten Beispiele sind grundlegende Bausteine ​​– wie primitive Typen, Aufzählungstypen oder Datenstrukturen. Das Prinzip besteht darin, einfach sinnvolle (dh für den Benutzer eindeutige) Namen zu wählen. Berücksichtigen Sie, dass die besprochene Funktionalität immer als Heap bereitgestellt wird – sei es Python, Java, C++, Go, Swift oder Rust. Heap ist eine der elementarsten Datenstrukturen. Heap ist in der Tat eindeutig und klar. Es steht auch im Einklang mit Stack , Queue , List und Array . Der gleiche Ansatz bei der Namensgebung wurde in den modernsten Standardbibliotheken (Go, Swift, Rust) verfolgt – sie legen explizit einen Heap offen.

@pgolebiowski Ich Heap<T> / IHeap<T> wie Stack<T> , Queue<T> und/oder List<T> ? Keiner dieser Namen erklärt, wie sie intern implementiert werden (ein Array von T, wie es passiert).

@SamuelEnglard

Heap sagt auch nicht, wie es intern implementiert ist. Ich verstehe nicht, warum für so viele Leute sofort ein Haufen auf eine bestimmte Implementierung folgt. Zunächst gibt es viele Varianten von Heaps, die dieselbe API verwenden:

  • d-ary Haufen,
  • 2-3 Haufen,
  • linke haufen,
  • weiche Haufen,
  • schwache Haufen,
  • B-Haufen,
  • Radix Haufen,
  • Haufen verzerren,
  • paar Haufen,
  • Fibonacci-Haufen,
  • Binomialhaufen,
  • Rang-Paarungs-Haufen,
  • Beben Haufen,
  • Verletzung haufenweise.

Zu sagen, dass wir es mit einem Haufen zu tun haben, ist noch sehr abstrakt. Tatsächlich ist es abstrakt, selbst zu sagen, dass wir es mit einem quaternären Heap zu tun haben – es könnte entweder als implizite Datenstruktur basierend auf einem Array von T implementiert werden (wie unsere Stack<T> , Queue<T> und List<T> ) oder explizit (unter Verwendung von Knoten und Zeigern).

Kurz gesagt, Heap<T> ist Stack<T> , Queue<T> und List<T> sehr ähnlich, weil es eine elementare Datenstruktur ist, ein grundlegender abstrakter Baustein, der kann auf viele Arten umgesetzt werden. Außerdem werden sie alle mit einem darunter liegenden Array von T implementiert. Ich finde diese Ähnlichkeit sehr stark.

Ist das sinnvoll?

Fürs Protokoll, mir ist die Namensgebung gleichgültig. Leute, die es gewohnt sind, die C++-Standardbibliothek zu verwenden, werden vielleicht _priority_queue_ bevorzugen. Personen, die über Datenstrukturen informiert sind, bevorzugen möglicherweise _Heap_. Wenn ich wählen müsste, würde ich _heap_ wählen, obwohl es für mich fast wie ein Münzwurf ist.

@pgolebiowski Ich habe meine Frage falsch formuliert, das ist mein Heap<T> sagt nicht, wie es intern implementiert wird.

Ja Heap ist eine gültige Datenstruktur, aber Heap != Priority Queue. Beide legen unterschiedliche API-Oberflächen offen und werden für unterschiedliche Ideen verwendet. Heap<T> / IHeap<T> sollten Datentypen sein, die intern von (nur theoretischen Namen) PriorityQueue<T> / IPriorityQueue<T> .

@SamuelEnglard
Im Hinblick darauf, wie die Welt der Informatik organisiert ist, ja. Hier sind die Abstraktionsebenen:

  • Implementierung : impliziter quaternärer Heap basierend auf einem Array
  • Abstraktion : quartärer Haufen
  • Abstraktion : Haufenfamilie
  • Abstraktion : Familie von Prioritätswarteschlangen

Und ja, mit IHeap und Heap die Implementierung von PriorityQueue im Grunde:

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

Lassen Sie uns hier einen Entscheidungsbaum erstellen.

Eine Prioritätswarteschlange könnte auch mit einer anderen Datenstruktur als irgendeiner Form eines Haufens (theoretisch) implementiert werden. Das macht das Design eines PriorityQueue wie das obige ziemlich hässlich, da es nur auf die Familie der Haufen ausgerichtet ist. Es ist auch eine sehr dünne Hülle um ein IHeap . Dies wirft eine Frage auf -- _warum nicht einfach stattdessen die Familie der Haufen verwenden_?

Uns bleibt nur eine Lösung – die Festlegung der Prioritätswarteschlange auf eine bestimmte Implementierung eines quaternären Heaps ohne Platz für die IHeap Schnittstelle. Ich habe das Gefühl, dass es zu viele Abstraktionsebenen durchläuft und all die wunderbaren Vorteile einer Schnittstelle zunichte macht.

Wir sind zurück mit der Designwahl aus der Mitte der Diskussion – haben PriorityQueue und IPriorityQueue . Aber dann hätten wir im Grunde:

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

Fühlt sich nicht nur hässlich an, sondern auch konzeptionell falsch - dies sind keine Arten von Prioritätswarteschlangen und teilen nicht dieselbe API (wie @SamuelEnglard bereits bemerkt hat). Ich denke, wir sollten bei Haufen bleiben, einer Familie, die groß genug ist, um eine Abstraktion für sie zu haben. Wir würden bekommen:

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

Und, von uns zur Verfügung gestellt, class Heap : IHeap {} .


Übrigens, vielleicht findet jemand folgendes hilfreich:

| Google-Abfrage | Ergebnisse |
| :-------------------------------------------------: | :-----: |
| "Datenstruktur" "Prioritätswarteschlange" | 172.000 |
| "Datenstruktur" "Heap" | 430.000 |
| "Datenstruktur" "Warteschlange" -"Prioritätswarteschlange" | 496.000 |
| "Datenstruktur" "Warteschlange" | 530.000 |
| "Datenstruktur" "Stack" | 577.000 |

@pgolebiowski Ich fühle mich hier hin und her gehen, also gebe ich zu

@karelz @safen Wie Gesagte ? Können wir uns auf den Ansatz von IHeap + Heap festlegen, damit ich einen spezifischen API-Vorschlag präsentieren kann?

Ich bezweifle hier die Notwendigkeit einer Schnittstelle (sei es IHeap oder IPriorityQueue ). Ja, es gibt viele verschiedene Algorithmen, die theoretisch verwendet werden können, um diese Datenstruktur zu implementieren. Es erscheint jedoch unwahrscheinlich, dass das Framework mit mehr als einem ausgeliefert wird, und die Idee, dass Bibliotheksautoren wirklich eine kanonische Schnittstelle benötigen, damit sich verschiedene Parteien beim Schreiben kreuzkompatibler Heap-Implementierungen koordinieren können, scheint nicht so wahrscheinlich.

Außerdem kann eine einmal freigegebene Schnittstelle aus Kompatibilitätsgründen nicht mehr geändert werden. Das bedeutet, dass die Schnittstelle in Bezug auf die Funktionalität tendenziell hinter den konkreten Klassen zurückbleibt (ein Problem mit IList und IDictionary heute). Im Gegensatz dazu, wenn eine Heap-Klasse veröffentlicht würde und es ernsthafte Nachfrage nach einer IHeap-Schnittstelle gäbe, könnte die Schnittstelle meiner Meinung nach ohne Probleme hinzugefügt werden.

@madelson Ich stimme zu, dass das Framework wahrscheinlich nicht mehr als eine einzelne Implementierung eines Heaps

Ich sehe keine negativen Punkte bei der Codierung einer Schnittstelle. Wer sich nicht darum kümmert, kann es ignorieren und direkt an die konkrete Umsetzung gehen. Diejenigen, die sich darum kümmern, können jede Implementierung ihrer Wahl verwenden. Es ist eine Wahl. Und es ist eine Wahl, die ich will.

@pgolebiowski Nun, da Sie gefragt haben, hier ist meine persönliche Meinung (ich bin mir ziemlich sicher, dass andere API-Rezensenten / Architekten sie teilen werden, da ich mit einigen nach Meinungen
Der Name sollte PriorityQueue , und wir sollten keine IHeap Schnittstelle einführen. Es sollte genau eine Implementierung geben (wahrscheinlich über einen Heap).
IHeap Schnittstelle ist ein sehr fortgeschrittenes Expertenszenario - ich würde vorschlagen, sie in die PowerCollections-Bibliothek (eventuell zu erstellen) oder eine andere Bibliothek zu verschieben.
Wenn die Bibliothek und die IHeap Schnittstelle wirklich beliebt werden, können wir unsere Meinung später ändern und IHeap nach Bedarf
... nur meine 2 (persönlichen) Cent

Angesichts der unterschiedlichen Meinungen schlage ich folgenden Ansatz vor, um den Vorschlag voranzubringen (den ich Anfang dieser Woche

  • Machen Sie 2 alternative Vorschläge - einen einfachen wie oben beschrieben und einen mit den Heaps, wie Sie ihn vorgeschlagen haben. Lassen Sie uns ihn zur API-Überprüfung bringen, dort diskutieren und dort eine Entscheidung treffen.
  • Wenn ich sehe, dass mindestens eine Person in dieser Gruppe für den Heaps-Vorschlag stimmt, überdenke ich gerne meinen Standpunkt.

... ich versuche, meine Meinung (um die Sie gebeten haben) vollständig transparent zu machen. Bitte nehmen Sie es nicht als Entmutigung oder Zurückweisung - mal sehen, wie der API-Vorschlag läuft.

@karelz

Der Name sollte PriorityQueue lauten

Irgendein Argument? Es wäre zumindest schön, wenn Sie das, was ich oben geschrieben habe, ansprechen würden, anstatt nur nein zu sagen.

Ich denke, Sie haben es zuvor ziemlich gut benannt: _Wenn Sie Eingaben haben, geben Sie sie an, untermauern Sie sie mit Daten und Beweisen. Hören Sie sich auch die Argumente anderer an, bestätigen Sie sie. Legen Sie Beweise gegen die Meinung anderer vor, wenn Sie anderer Meinung sind._

Außerdem geht es nicht nur um den Namen . Es ist nicht so flach. Bitte lesen Sie, was ich geschrieben habe. Es geht darum, zwischen Abstraktionsebenen zu arbeiten und in der Lage zu sein, Code zu verbessern / darauf aufzubauen.

Oh, in dieser Diskussion gab es ein Argument, warum:

Wenn wir uns für Heap entscheiden, sehen viele Entwickler mit mittlerer Erfahrung (leider) "Heap" und verwechseln es mit dem Speicherheap der Anwendung (dh Heap und Stack) anstelle von "Heap der verallgemeinerten Datenstruktur". [...] werden sie sich fragen, warum sie einen neuen Speicherhaufen zuweisen können.

😛

wir sollten keine IHeap Schnittstelle einführen. IHeap Schnittstelle ist ein sehr fortgeschrittenes Expertenszenario - ich würde vorschlagen, sie in die PowerCollections-Bibliothek zu verschieben.

@bendono hat dazu einen sehr netten Kommentar geschrieben. @safen wollte die Implementierung auch mit einer Schnittstelle

Noch ein Hinweis - ich bin mir nicht sicher, wie Sie sich das Verschieben einer Schnittstelle in eine Bibliothek eines Drittanbieters vorstellen. Wie könnten Entwickler möglicherweise ihren Code gegen diese Schnittstelle schreiben und unsere Funktionalität nutzen? Sie wären gezwungen, entweder an unserer Nicht-Schnittstellen-Funktionalität festzuhalten oder sie vollständig zu ignorieren, es gibt keine andere Option, sie schließt sich gegenseitig aus. Mit anderen Worten – unsere Lösung wäre überhaupt nicht erweiterbar, was dazu führen würde, dass Benutzer entweder unsere deaktivierte Lösung oder eine Bibliothek von Drittanbietern verwenden, anstatt sich in beiden Fällen auf denselben Architekturkern zu verlassen . Dies ist, was wir in der Standardbibliothek von Java und in Lösungen von Drittanbietern haben.

Aber auch hier hast du schön kommentiert: Niemand möchte kaputte APIs versenden.

Angesichts der unterschiedlichen Meinungen schlage ich folgenden Ansatz vor, um den Vorschlag voranzubringen. [...] Machen Sie 2 alternative Vorschläge [...] bringen wir es zur API-Überprüfung, diskutieren Sie es dort und treffen Sie dort eine Entscheidung. Wenn ich sehe, dass mindestens eine Person in dieser Gruppe für den Heaps-Vorschlag stimmt, überdenke ich gerne meinen Standpunkt.

Es gab viele Diskussionen über verschiedene Teile der oben genannten API. Es angegangen:

  • PriorityQueue vs Heap
  • Hinzufügen einer Schnittstelle
  • Unterstützung beim Aktualisieren / Entfernen von Elementen aus der Sammlung

Warum können die Leute, die Sie im Sinn haben, nicht einfach zu dieser Diskussion beitragen? Warum sollten wir es stattdessen neu starten?

Wenn wir uns für Heap entscheiden, sehen viele Entwickler mit mittlerer Erfahrung (leider) "Heap" und verwechseln es mit dem Speicherheap der Anwendung (dh Heap und Stack) anstelle von "Heap der verallgemeinerten Datenstruktur".

Ja, das bin ich. Ich bin ein Autodidakt in der Programmierung, also habe ich keine Ahnung, was gesagt wurde, als " Heap " in die Diskussion eintrat. Ganz zu schweigen davon, dass selbst in Bezug auf eine Sammlung, "ein Haufen Dinge", für mich intuitiver bedeuten würde, dass sie in jeder Hinsicht ungeordnet ist.

Ich kann nicht glauben, dass das wirklich passiert ...

Irgendein Argument? Es wäre zumindest schön, wenn Sie das, was ich oben geschrieben habe, ansprechen würden, anstatt nur nein zu sagen.

Wenn Sie meine Antwort lesen, können Sie feststellen, dass ich wichtige Argumente für meine Position erwähnt habe:

  • Die IHeap-Schnittstelle ist ein sehr fortgeschrittenes Expertenszenario
  • Ich denke nicht, dass es nützlich / auf den Rest von BCL ausgerichtet ist, um die Komplikation des Hinzufügens einer neuen Schnittstelle jetzt zu rechtfertigen

Dieselben Argumente, die IMO mehrmals im Thread wiederholt wurden. Ich habe sie nur zusammengefasst

. Bitte lesen Sie, was ich geschrieben habe.

Ich habe diesen Thread die ganze Zeit aktiv beobachtet. Ich habe alle Argumente und Punkte gelesen. Dies ist meine zusammengefasste Meinung, obwohl ich alle Ihre und die anderer Punkte gelesen (und verstanden) habe.
Das Thema liegt Ihnen offenbar am Herzen. Das ist großartig. Ich habe jedoch das Gefühl, dass wir in eine Position geraten sind, wenn wir nur ähnliche Argumente von beiden Seiten wiederholen, was nicht viel weiterführen wird - deshalb habe ich 2 Vorschläge empfohlen und mehr Feedback dazu aus einem größeren Erfahrungskreis erhalten BCL-API-Rezensenten (Übrigens: Ich zähle mich noch nicht zu den erfahrenen API-Rezensenten).

Wie könnten Entwickler möglicherweise ihren Code gegen diese Schnittstelle schreiben und unsere Funktionalität nutzen?

Die Entwickler, die sich für das erweiterte Szenario IHeap interessieren, würden auf die Schnittstelle und die Implementierung aus der Bibliothek von Drittanbietern verweisen. Wie gesagt, wenn es sich als beliebt erweist, könnten wir es später in CoreFX verschieben.
Die gute Nachricht ist, dass das spätere Hinzufügen von IHeap völlig machbar ist - es fügt im Grunde nur eine Konstruktorüberladung für PriorityQueue .
Ja, das ist aus Ihrer Sicht nicht ideal, verhindert aber nicht die Innovation, die Sie für die Zukunft für wichtig erachten. Ich denke, es ist ein vernünftiger Mittelweg.

Warum können die Leute, die Sie im Sinn haben, nicht einfach zu dieser Diskussion beitragen?

API-Review ist ein Treffen mit aktiver Diskussion, Brainstorming und Abwägen aller Blickwinkel. In vielen Fällen ist es produktiver/effizienter als hin und her bei GitHub-Problemen. Siehe dotnet/corefx#14354 und seinen Vorgänger dotnet/corefx#8034 - zu lange Diskussion, mehrere unterschiedliche Meinungen mit Unmengen von Tweaks, die schwer zu verfolgen sind, kein Fazit, während tolle Diskussion, auch nicht triviale Zeitverschwendung für einige Leute, bis wir uns hingesetzt und darüber geredet haben und zu einem Konsens gekommen sind.
Wenn API-Reviewer jedes einzelne API-Problem oder auch nur die gesprächigen Probleme überwachen, ist die Skalierung nicht gut.

Warum sollten wir es stattdessen neu starten?

Wir werden nicht wieder anfangen. Warum denkst du das?
Wir werden die API-Überprüfung auf der ersten Ebene (der Gebietsbesitzerebene) abschließen, indem wir die 2 beliebtesten Vorschläge mit Vor- und Nachteilen an die nächste API-Überprüfungsebene senden.
Es ist ein hierarchischer Genehmigungs-/Überprüfungsansatz. Es ist vergleichbar mit Unternehmensbewertungen – VPs/CEOs mit Entscheidungsbefugnis beaufsichtigen nicht jede Diskussion über jedes Projekt in ihrem Unternehmen, sie bitten ihre Teams/Berichte, Vorschläge für die wirkungsvollsten oder ansteckendsten Entscheidungen zu unterbreiten, um eine weitere Diskussion darüber zu führen. Teams/Berichte müssen das Problem zusammenfassen und die Vor- und Nachteile alternativer Lösungen darstellen.

Wenn Sie der Meinung sind, dass wir noch nicht bereit sind, die letzten 2 Vorschläge mit Vor- und Nachteilen vorzulegen, weil es in diesem Thread noch Dinge gibt, die noch nicht gesagt wurden, diskutieren wir weiter, bis wir beim nächsten nur ein paar Top-Kandidaten überprüfen müssen API-Review-Level.
Ich hatte das Gefühl, dass alles gesagt wurde, was gesagt werden musste.
Macht Sinn?

Der Name sollte PriorityQueue lauten

Irgendein Argument?

Wenn Sie meine Antwort lesen, können Sie feststellen, dass ich wichtige Argumente für meine Position erwähnt habe.

Meine Güte ... Ich bezog mich auf das, was ich ( offensichtlich ) zitiert habe - Sie entscheiden sich für eine Prioritätswarteschlange anstelle eines Haufens. Und ja, ich habe Ihre Antwort gelesen – sie enthält genau 0% der Argumente dafür.

Ich habe diesen Thread die ganze Zeit aktiv beobachtet. Ich habe alle Argumente und Punkte gelesen. Dies ist meine zusammengefasste Meinung, obwohl ich alle Ihre und die anderer Punkte gelesen (und verstanden) habe.

Du bist gerne hyperbolisch, das ist mir schon mal aufgefallen. Sie erkennen lediglich die Existenz von Punkten an.

Wie könnten Entwickler möglicherweise ihren Code gegen diese Schnittstelle schreiben und unsere Funktionalität nutzen?

Die Entwickler, die sich für das erweiterte IHeap-Szenario interessieren, verweisen auf die Schnittstelle und die Implementierung aus der Drittanbieterbibliothek. Wie gesagt, wenn es sich als beliebt erweist, könnten wir es später in CoreFX verschieben.

Ist Ihnen bewusst, dass Sie hier nur meine Worte wiederholen und das Problem, das ich als Konsequenz aus dem Vorstehenden vorgestellt habe, überhaupt nicht ansprechen? Ich schrieb:

Sie wären gezwungen, entweder an unserer Nicht-Schnittstellen-Funktionalität festzuhalten oder sie vollständig zu ignorieren, es gibt keine andere Option, sie schließt sich gegenseitig aus.

Ich werde dies für Sie sehr deutlich machen. Es gäbe zwei disjunkte Personengruppen:

  1. Eine Gruppe, die unsere Funktionalität nutzt. Es hat keine Schnittstelle und ist nicht erweiterbar, daher ist es nicht mit dem verbunden, was in Bibliotheken von Drittanbietern bereitgestellt wird.
  2. Zweite Gruppe, die unsere Funktionalität komplett ignoriert und ausschließlich Lösungen von Drittanbietern verwendet.

Das Problem dabei ist, wie gesagt, dass es sich um unzusammenhängende Gruppen von Menschen handelt . Und sie produzieren Code, der nicht zusammenarbeitet , weil es keinen gemeinsamen Architekturkern gibt. _ CODEBASE IST NICHT KOMPATIBEL _. Sie können es später nicht mehr rückgängig machen.

Die gute Nachricht ist, dass das spätere Hinzufügen von IHeap absolut machbar ist - es fügt im Grunde nur eine Konstruktorüberladung in PriorityQueue hinzu.

Ich habe bereits geschrieben, warum es scheiße ist: siehe diesen Beitrag .

Warum können die Leute, die Sie im Sinn haben, nicht einfach zu dieser Diskussion beitragen?

Wenn API-Reviewer jedes einzelne API-Problem oder auch nur die gesprächigen Probleme überwachen, ist die Skalierung nicht gut.

Ja, ich habe gefragt, warum API-Reviewer nicht jedes einzelne API-Problem überwachen können. Genau, Sie haben tatsächlich entsprechend reagiert.

Macht Sinn?

Nein. Ich habe diese Diskussion wirklich satt. Einige von euch nehmen eindeutig daran teil, einfach weil es ihr Job ist und es euch gesagt wird. Einige von euch brauchen ständig Anleitung, was sehr ermüdend ist. Sie haben mich sogar gebeten zu beweisen, warum eine Priority Queue intern mit einem Heap implementiert werden sollte, da es eindeutig an Informatik-Hintergrund fehlt. Einige von Ihnen verstehen nicht einmal, was ein Haufen wirklich ist, was die Diskussion noch chaotischer macht.

Gehen Sie mit Ihrer deaktivierten PriorityQueue, die keine Aktualisierung und Entfernung von Elementen zulässt. Gehen Sie mit Ihrem Design, das keinen gesunden OO-Ansatz zulässt. Gehen Sie mit Ihrer Lösung, die die Wiederverwendung der Standardbibliothek beim Schreiben einer Erweiterung nicht zulässt. Gehen Sie den Java-Weg.

Und das... das ist einfach umwerfend:

Wenn wir uns für Heap entscheiden, sehen viele Entwickler mit mittlerer Erfahrung (leider) "Heap" und verwechseln es mit dem Speicherheap der Anwendung (dh Heap und Stack) anstelle von "Heap der verallgemeinerten Datenstruktur".

Präsentieren Sie die API mit Ihrem Ansatz. Ich bin neugierig.

Ich kann nicht glauben, dass das wirklich passiert...

Nun, entschuldige mich, dass ich nicht die Möglichkeit hatte, eine richtige Informatikausbildung zu machen, um zu lernen, dass Heap eine Art Datenstruktur ist, die sich vom Speicherhaufen unterscheidet.

Der Punkt steht trotzdem. Ein Haufen von etwas impliziert nichts davon, dass es in irgendeiner Weise geordnet ist. Wenn ich eine Sammlung benötige, die es mir ermöglicht, Objekte von zu verarbeitenden Dingen zu speichern, wobei einige Instanzen, die später eingehen, möglicherweise früher verarbeitet werden müssen, würde ich nicht nach etwas namens Heap suchen. PriorityQueue hingegen kommuniziert perfekt , dass es genau das tut.

Als unterstützende Implementierung? Sicher, Implementierungsdetails sollten mich nicht interessieren.
Etwas IHeap Abstraktion? Groß für API Autoren und Menschen , die einen CS - Dur zu wissen , haben Sie , was es für verwendet wird , muß kein Grund , nicht.
Etwas einen kryptischen Namen geben, der seine Absicht nicht so gut ausdrückt und die Auffindbarkeit einschränkt? 👎

Nun, entschuldige mich, dass ich nicht die Möglichkeit hatte, eine richtige Informatikausbildung zu absolvieren, um zu lernen, dass Heap eine Art Datenstruktur ist, die sich vom Speicherheap unterscheidet.

Das ist lächerlich. Gleichzeitig möchten Sie an einer Diskussion über das Hinzufügen einer solchen Funktionalität teilnehmen. Es klingt nach Trolling.

Ein Haufen von etwas impliziert nichts davon, dass es in irgendeiner Weise geordnet ist.

Sie liegen falsch. Es ist als Haufen bestellt. Wie auf den von dir verlinkten Bildern.

Als unterstützende Implementierung? Sicher, Implementierungsdetails sollten mich nicht interessieren.

Das habe ich schon angesprochen. Die Familie der Heaps ist riesig und es gibt zwei Abstraktionsebenen über einer Implementierung. Die Prioritätswarteschlange ist die dritte Abstraktionsschicht.

Wenn ich eine Sammlung benötige, die es mir ermöglicht, Objekte von zu verarbeitenden Dingen zu speichern, wobei einige Instanzen, die später eingehen, möglicherweise früher verarbeitet werden müssen, würde ich nicht nach einem sogenannten Heap suchen. PriorityQueue hingegen kommuniziert perfekt, dass es genau das tut.

Und ohne Hintergrundinformationen würden Sie Google bitten, Ihnen Artikel zu Prioritätswarteschlangen bereitzustellen? Nun, wir können argumentieren, was unserer Meinung nach mehr oder weniger wahrscheinlich ist. Aber wie schon sehr schön gesagt:

Wenn Sie Eingaben gemacht haben, stellen Sie diese zur Verfügung, untermauern Sie sie mit Daten und Beweisen. Legen Sie Beweise gegen die Meinung anderer vor, wenn Sie anderer Meinung sind.

Und nach den Daten liegst du falsch:

Abfrage | Treffer
:----: |:----:|
| "Datenstruktur" "Prioritätswarteschlange" | 172.000 |
| "Datenstruktur" "Heap" | 430.000 |

Es ist fast dreimal wahrscheinlicher, dass Sie beim Lesen über Datenstrukturen auf einen Haufen stoßen. Außerdem ist es ein Name, mit dem Swift-, Go-, Rust- und Python-Entwickler vertraut sind, da ihre Standardbibliotheken eine solche Datenstruktur bereitstellen.

Abfrage | Treffer
:----: |:----:|
| "golang" "Prioritätswarteschlange" | 3.390 |
| "rost" "prioritätswarteschlange" | 8.630 |
| "schnell" "Prioritätswarteschlange" | 18.600 |
| "Python" "Prioritätswarteschlange" | 72.800 |
| "golang" "Haufen" | 79.000 |
| "Rost" "Haufen" | 492.000 |
| "schnell" "Haufen" | 551.000 |
| "python" "haufen" | 555.000 |

Eigentlich ist es auch für C++ ähnlich, weil dort irgendwann im vorigen Jahrhundert eine Heap-Datenstruktur eingeführt wurde.

Etwas einen kryptischen Namen geben, der seine Absicht nicht so gut ausdrückt und die Auffindbarkeit einschränkt? 👎

Keine Meinungen. Daten. Siehe oben. Vor allem keine Meinungen von jemandem, der keinen Hintergrund hat. Sie würden auch keine Prioritätswarteschlange googeln, ohne vorher etwas über Datenstrukturen gelesen zu haben. Und ein Heap wird in vielen Datenstrukturen 101 abgedeckt.

Es ist die Grundlage der Informatik. Es ist elementar. Wenn Sie über mehrere Semester Algorithmen und Datenstrukturen verfügen, sehen Sie am Anfang einen Haufen.

Aber dennoch:

  • Sehen Sie sich zuerst die Zahlen oben an.
  • Zweitens – denken Sie an all die anderen Sprachen, in denen ein Heap Teil der Standardbibliothek ist.

BEARBEITEN: Siehe Google Trends .

Als weiterer Autodidakt habe ich kein Problem mit _heap_. Als Entwickler, der immer nach Verbesserung strebt, habe ich mir die Zeit genommen, alles über Datenstrukturen zu lernen und zu verstehen. Kurz gesagt, ich stimme der Implikation nicht zu, dass eine Namenskonvention auf diejenigen abzielen sollte, die sich nicht die Zeit genommen haben, das Lexikon des Gebiets zu verstehen, zu dem sie gehören.

Und auch Aussagen wie "Der Name sollte PriorityQueue lauten" widerspreche ich vehement. Wenn Sie den Input der Leute nicht wollen, dann machen Sie ihn nicht Open Source und fragen Sie nicht danach.

Lassen Sie mich erklären, wie wir über die API-Benennung denken:

  1. Wir neigen dazu, Konsistenz innerhalb der .NET-Plattform vor fast allem anderen zu bevorzugen. Das ist wichtig, damit APIs vertraut und vorhersehbar aussehen. Manchmal bedeutet dies, dass wir akzeptieren, dass ein Name nicht 100% korrekt ist, wenn dies ein Begriff ist, den wir zuvor verwendet haben.

  2. Unser Ziel ist es, eine Plattform zu entwickeln, die für eine Vielzahl von Entwicklern zugänglich ist, von denen einige keine formale Ausbildung in Informatik haben. Wir glauben, dass ein Teil des Grundes, warum .NET im Allgemeinen als sehr produktiv und einfach zu verwenden angesehen wird, teilweise auf diesen Designpunkt zurückzuführen ist.

Generell setzen wir den „Suchmaschinentest“ ein, wenn es darum geht, den Bekanntheitsgrad und die Etablierung eines Namens oder einer Terminologie zu überprüfen. Daher schätze ich die Forschung von @pgolebiowski sehr. Ich habe die Forschung nicht selbst durchgeführt, aber mein Bauchgefühl ist, dass "Haufen" kein Begriff ist, nach dem viele Nicht-Domain-Experten suchen würden.

Daher neige ich dazu, @karelz zuzustimmen, dass PriorityQueue wie die bessere Wahl aussieht . Es kombiniert ein bestehendes Konzept (Warteschlange) und fügt den Twist hinzu, der die gewünschte Fähigkeit ausdrückt: geordnete Abfrage basierend auf einer Priorität. Aber wir sind nicht unbeweglich mit diesem Namen verbunden. Wir haben oft die Namen von Datenstrukturen und Technologien aufgrund von Kundenfeedback geändert.

Ich möchte jedoch darauf hinweisen, dass:

Wenn Sie den Input der Leute nicht wollen, dann machen Sie ihn nicht Open Source und fragen Sie nicht danach.

ist eine falsche Dichotomie. Es ist nicht so, dass wir kein Feedback von unserem Ökosystem und unseren Mitwirkenden wollen (das tun wir natürlich). Gleichzeitig müssen wir aber auch anerkennen, dass unser Kundenstamm sehr vielfältig ist und GitHub-Mitwirkende (oder Entwickler in unserem Team) nicht immer der beste Proxy für alle unsere Kunden sind. Die Benutzerfreundlichkeit ist schwierig und es wird wahrscheinlich einige Iterationen erfordern, um neue Konzepte in .NET hinzuzufügen, insbesondere in sehr beliebten Bereichen wie Sammlungen.

@pgolebiowski :

Ich schätze Ihre Erkenntnisse, Daten und Vorschläge sehr. Aber ich schätze deinen Argumentationsstil absolut nicht. Du bist in diesem Thread sowohl gegenüber Mitgliedern meines Teams als auch gegenüber Community-Mitgliedern persönlich geworden. Nur weil Sie mit uns nicht einverstanden sind, können Sie uns nicht vorwerfen, dass wir keine Expertise haben oder uns nicht interessieren, weil es "nur unser Job" ist. Bedenken Sie, dass viele von uns buchstäblich um den Globus gezogen sind, Familien und Freunde zurückgelassen haben, nur weil wir diesen Job machen wollten. Kommentare wie Ihre sind nicht nur sehr unfair, sie tragen auch nicht dazu bei, das Design voranzutreiben.

Obwohl ich gerne denke, dass ich eine dicke Haut habe, habe ich keine große Toleranz für diese Art von Verhalten. Unsere Domäne ist bereits komplex genug; wir müssen keine konfrontative und gegnerische Kommunikation hinzufügen.

Bitte seien Sie respektvoll. Ideen leidenschaftlich zu kritisieren ist ein faires Spiel, Menschen jedoch nicht anzugreifen. Dankeschön.

Liebe alle, die sich niedergeschlagen fühlen,

Ich entschuldige mich dafür, dass ich Ihr Glücksniveau durch meine harte Einstellung verringert habe.

@karelz

Da habe ich mich durch technische Fehler lächerlich gemacht. Ich habe mich entschuldigt. Es wurde akzeptiert. Aber später auf mich geworfen. Nicht cool IMO.

Es tut mir leid, dass das, was ich geschrieben habe, Sie unglücklich gemacht hat. Obwohl es nicht so schlimm war, wie Sie es beschrieben haben – ich habe dies nur als einen von vielen Faktoren angegeben, die zu meinem Müdigkeitsgefühl beigetragen haben. Es ist von geringerer Schwere, denke ich. Aber es tut mir trotzdem leid.

Und ja, jeder macht Fehler. Es ist okay. Ich auch, zum Beispiel indem ich mich manchmal hinreißen lasse.

Was mich am meisten überzeugt hat, war "das wird dir nur gesagt, du glaubst es nicht" - ja, genau deshalb mache ich es AUCH am Wochenende.

Es tut mir leid, ich sehe, dass Sie hart arbeiten, und ich weiß das sehr zu schätzen. Es war für mich sichtbar, wie sehr Sie am 5/10-Meilenstein besonders engagiert waren.

@terrajobst

Nur weil Sie mit uns nicht einverstanden sind, können Sie uns nicht vorwerfen, dass wir keine Expertise haben oder uns nicht interessieren, weil es "nur unser Job" ist.

  • Kein Fachwissen – richtet sich an Personen ohne Informatikhintergrund, die das Konzept von Heaps / Prioritätswarteschlangen nicht verstehen. Wenn eine solche Beschreibung auf jemanden zutrifft – nun, es trifft zu, es ist nicht meine Schuld.
  • Gleichgültig – richtet sich an diejenigen, die dazu neigen, einige der technischen Punkte zu ignorieren, wodurch wiederholte Argumente erzwungen werden, was die Diskussion chaotisch und für andere schwerer zu verfolgen macht (was wiederum zu weniger Input führt).

Kommentare wie Ihre sind sehr unfair und tragen auch nicht dazu bei, das Design voranzutreiben.

  • Meine scharfen Kommentare waren das Ergebnis der Ineffizienz in dieser Diskussion. Ineffizienz = chaotischer Diskussionsstil, bei dem Punkte nicht angesprochen / gelöst werden und wir trotzdem weiterkommen => ermüdend.
  • Als einer der Haupttreiber in der Diskussion habe ich außerdem das Gefühl, viel getan zu haben, um das Design voranzutreiben. Bitte dämonisiere mich nicht, wie du es hier und in den sozialen Medien versuchst.

Wenn jemand krank ist, der mich "lecken" möchte, bitte zögern Sie nicht.


Wir sind auf ein Problem gestoßen und haben uns darum gekümmert. Jeder hat etwas daraus gelernt. Ich sehe, dass sich hier alle Sorgen um die Qualität des Rahmens machen, was absolut wunderbar ist und mich motiviert, einen Beitrag zu leisten. Ich freue mich darauf, mit Ihnen weiter an CoreFX zu arbeiten. Davon abgesehen werde ich wahrscheinlich morgen auf Ihren neuen technischen Input eingehen.

@pgolebiowski

Hoffentlich können wir uns irgendwann einmal persönlich treffen. Ich glaube ehrlich, dass ein Teil der Herausforderung, alles online zu machen, darin besteht, dass sich Persönlichkeiten manchmal ohne Absicht von beiden Seiten auf schlechte Weise vermischen können.

Ich freue mich darauf, mit Ihnen weiter an CoreFX zu arbeiten. Davon abgesehen werde ich wahrscheinlich morgen auf Ihren neuen technischen Input eingehen.

Hier gilt das gleiche. Dies ist ein interessanter Ort und es gibt viele tolle Dinge, die wir zusammen tun können :-)

@pgolebiowski zuerst, danke für deine Antwort. Es zeigt, dass Sie sich interessieren und es gut meinen (was ich insgeheim hoffe, dass jede Person / jeder Entwickler auf der Welt das tut, alle Konflikte sind nur Missverständnisse / Missverständnisse). Und das macht mich wirklich glücklich - es hält mich am Laufen und begeistert.
Ich würde vorschlagen, in unserer Beziehung von vorne anzufangen. Wenden wir die Diskussion wieder auf die Technik zurück, lernen wir alle aus diesem Thread, wie man in Zukunft mit ähnlichen Situationen umgeht, nehmen wir alle wieder an, dass der andere nur das Beste für die Plattform im Sinn hat.
Übrigens: Dies ist eine der schwierigeren Begegnungen/Diskussionen über das CoreFX-Repo in den letzten 9 Monaten, und wie Sie sehen, lernen wir (einschließlich/insbesondere ich) immer noch, wie man gut damit umgeht - also diese spezielle Instanz läuft sogar uns zugute kommen, und es wird uns in Zukunft besser machen und uns helfen, die unterschiedlichen Standpunkte leidenschaftlicher Community-Mitglieder besser zu verstehen. Vielleicht wird es unsere Aktualisierungen zu Beitragsdokumenten prägen ...

Meine scharfen Kommentare waren das Ergebnis der Ineffizienz in dieser Diskussion. Ineffizienz = chaotischer Diskussionsstil, bei dem Punkte nicht angesprochen / gelöst werden und wir trotzdem weiterkommen => ermüdend.

Verstehe deinen Frust! Interessanterweise war ähnlicher Frust auch auf der anderen Seite aus dem gleichen Grund 😉 ... es ist fast schon lustig, wie die Welt funktioniert :).
Leider gehört die schwierige Diskussion zum Job, wenn Sie eine Designentscheidung treffen. Es ist VIEL Arbeit. Viele Leute unterschätzen es. Die erforderliche Schlüsselkompetenz ist Geduld mit allen und die Fähigkeit, über Ihre eigene Meinung hinauszugehen und darüber nachzudenken, wie Sie einen Konsens erzielen können, auch wenn es nicht in Ihre Richtung geht. Deshalb habe ich vorgeschlagen, 2 Vorschläge zu machen und die technische Diskussion an die API-Rezensentengruppe zu "eskalieren" (hauptsächlich, weil ich nicht sicher bin, ob ich recht habe, obwohl ich insgeheim hoffe, dass ich Recht habe, wie jeder andere Entwickler auf der Welt es tun würde ).

Es ist SEHR schwer, eine Meinung zu einem Thema zu haben UND die Diskussion zum KONSENSUS im selben Thread zu führen. Aus dieser Perspektive haben Sie und ich die häufigsten in diesem Thread - wir haben beide Meinungen, aber wir versuchen beide, die Diskussion zum Abschluss und zur Entscheidung zu bringen. Lassen Sie uns also eng zusammenarbeiten.

Meine generelle Herangehensweise ist: Immer wenn ich denke, dass mich jemand angreift, böse ist, faul ist, mich frustriert oder so. Ich frage zuerst mich und auch die jeweilige Person: Warum? Warum hast du das gesagt? Was hast du gemeint?
In der Regel ist es das Zeichen von mangelndem Verständnis/Kommunikation von Motiven. Oder ein Zeichen dafür, dass Sie zu viel zwischen den Zeilen lesen und Beleidigungen/Anschuldigungen/schlechte Absichten sehen, wo sie nicht sind.


Jetzt, da ich keine Angst habe, weiterhin technische Fragen zu diskutieren, wollte ich vorhin Folgendes fragen:

Gehen Sie mit Ihrer deaktivierten PriorityQueue, die keine Aktualisierung und Entfernung von Elementen zulässt.

Das ist etwas, was ich nicht verstehe. Wenn wir IHeap (in meinem / dem ursprünglichen Vorschlag hier) weglassen, warum sollte das nicht möglich sein?
IMO gibt es keinen Unterschied zwischen den beiden Vorschlägen aus der Sicht der Klassenfähigkeiten, der einzige Unterschied ist - fügen wir die PriorityQueue(IHeap) Konstruktorüberladung hinzu oder nicht (lassen den Streit um den Klassennamen als unabhängiges Problem zur Lösung beiseite) .

Haftungsausschluss (um Missverständnisse zu vermeiden): Ich habe keine Zeit, Artikel zu lesen und zu recherchieren, ich erwarte kurze Antworten und Argumente für den Aufzug von jedem, der die Fachdiskussion vorantreiben möchte. Hinweis: Es ist nicht mein Trolling. Die gleiche Frage würde ich jedem in unserem Team stellen, der diese Behauptung aufstellt. Wenn Sie keine Energie haben, es zu erklären / die Diskussion voranzutreiben (was angesichts der Schwierigkeiten und des Zeitaufwands von Ihrer Seite völlig verständlich wäre), sagen Sie es einfach, keine harten Gefühle. Bitte lasst euch weder von mir noch von irgendjemandem unter Druck setzen (und das gilt für alle im Thread).

Ich versuche hier nicht noch einen unnötigen Kommentar hinzuzufügen, dieser Thread war VIEL ZU LANG. Die Regel Nr. 1 des Internetzeitalters lautet, Textkommunikation zu vermeiden, wenn Ihnen die Beziehung zwischen Menschen wichtig ist. (Nun, ich habe es geprägt). Ich glaube, dass eine andere Open-Source-Community für diese Art von Diskussion zu einem Google Hangout wechseln würde, wenn die Notwendigkeit offensichtlich ist. Wenn man sich die Gesichter anderer Leute ansieht, würde man nie etwas "Beleidigendes" sagen und die Leute lernen sich sehr schnell kennen. Vielleicht können wir es auch versuchen?

@karelz Aufgrund der Länge der obigen Diskussion ist es sehr unwahrscheinlich, dass jemand neu dazu beiträgt, wenn der Flow nicht geändert wird. Als solches möchte ich jetzt folgenden Ansatz vorschlagen:

  • Ich werde nacheinander über grundlegende Aspekte abstimmen. Wir werden klaren Input von der Community erhalten. Im Idealfall kommen auch API-Rezensenten hierher und kommentieren in Kürze.
  • "Abstimmungsbeiträge" enthalten genügend Informationen, um die gesamte Textwand oben ignorieren zu können.
  • Nach Abschluss dieser Abstimmungssitzung wissen wir, was wir von API-Reviewern erwarten können und können mit einem bestimmten Ansatz fortfahren. Wenn wir uns über die grundlegenden Aspekte einig sind, wird dieses Thema geschlossen und ein anderes eröffnet (das sich auf dieses bezieht). In der neuen Ausgabe werde ich unsere Schlussfolgerungen zusammenfassen und einen API-Vorschlag unterbreiten, der diese Entscheidungen widerspiegelt. Und wir werden es von dort aus übernehmen.

Hat das eine Chance, Sinn zu machen?

PriorityQueue, die das Aktualisieren und Entfernen von Elementen nicht zulässt.

Es bezog sich auf den ursprünglichen Vorschlag, dem diese Fähigkeiten fehlten :) Sorry, dass ich es nicht klar gemacht habe.

Wenn Sie keine Energie haben, es zu erklären / die Diskussion voranzutreiben (was angesichts der Schwierigkeiten und des Zeitaufwands von Ihrer Seite völlig verständlich wäre), sagen Sie es einfach, keine harten Gefühle. Bitte lasst euch weder von mir noch von irgendjemandem unter Druck setzen (und das gilt für alle im Thread).

Ich werde nicht aufgeben. Kein Schmerz kein Gewinn xD

@xied75

Ich glaube, dass eine andere Open-Source-Community für diese Art von Diskussion zu einem Google Hangout wechseln würde, wenn die Notwendigkeit offensichtlich ist. Vielleicht können wir es auch versuchen?

Sieht gut aus ;)

Bereitstellung einer Schnittstelle

Egal, ob wir uns für Heap / IHeap oder PriorityQueue / IPriorityQueue (oder etwas anderes) entscheiden, für die Funktionalität, die wir bereitstellen werden...

_möchten Sie neben der Implementierung auch eine Schnittstelle haben?_

Zum

@bendono

Indem wir diese Implementierung durch eine Schnittstelle unterstützen, können diejenigen von uns, die sich für andere Implementierungen interessieren, leicht in eine andere Implementierung (von uns selbst oder in einer externen Bibliothek) wechseln und trotzdem mit Code kompatibel sein, der die Schnittstelle akzeptiert.

Wer sich nicht darum kümmert, kann es ignorieren und direkt an die konkrete Umsetzung gehen. Diejenigen, die sich darum kümmern, können jede Implementierung ihrer Wahl verwenden.

Gegen

@madelson

Es gibt viele verschiedene Algorithmen, die theoretisch verwendet werden können, um diese Datenstruktur zu implementieren. Es erscheint jedoch unwahrscheinlich, dass das Framework mit mehr als einem ausgeliefert wird, und die Idee, dass Bibliotheksautoren wirklich eine kanonische Schnittstelle benötigen, damit sich verschiedene Parteien beim Schreiben kreuzkompatibler Heap-Implementierungen koordinieren können, scheint nicht so wahrscheinlich.

Außerdem kann eine einmal freigegebene Schnittstelle aus Kompatibilitätsgründen nicht mehr geändert werden. Das bedeutet, dass die Schnittstelle in Bezug auf die Funktionalität tendenziell hinter den konkreten Klassen zurückbleibt (ein Problem mit IList und IDictionary heute).

@karelz

Schnittstelle ist ein sehr fortgeschrittenes Expertenszenario.

Wenn die Bibliothek und die IHeap Schnittstelle wirklich beliebt werden, können wir unsere Meinung später ändern und IHeap nach Bedarf

Potenzielle Entscheidungsauswirkungen

  • Die Einbeziehung der Schnittstelle bedeutet, dass wir sie in Zukunft nicht mehr ändern können.
  • Das Nichteinbeziehen der Schnittstelle bedeutet, dass die Leute Code schreiben, der entweder unsere Standardbibliothekslösung verwendet oder gegen eine Lösung geschrieben wird, die von einer Drittanbieterbibliothek bereitgestellt wird (es gibt keine gemeinsame Schnittstelle, die Kreuzkompatibilität ermöglichen würde).

Verwenden Sie 👍 und 👎, um über diese abzustimmen (für bzw. gegen eine Schnittstelle). Alternativ schreiben Sie einen Kommentar. Im Idealfall nehmen API-Rezensenten teil.

Ich möchte hinzufügen, dass das Ändern von Schnittstellen zwar schwierig ist, aber mit Erweiterungsmethoden (und kommenden Eigenschaften) Schnittstellen einfacher zu erweitern und/oder zu bearbeiten sind (siehe LINQ).

Ich möchte hinzufügen, dass das Ändern von Schnittstellen zwar schwierig ist, aber mit Erweiterungsmethoden (und kommenden Eigenschaften) Schnittstellen einfacher zu erweitern und/oder zu bearbeiten sind (siehe LINQ).

Sie können nur mit den öffentlich definierten Methoden der Schnittstelle arbeiten; es bedeutet also, es beim ersten Mal richtig zu machen.

Ich würde vorschlagen, mit der Schnittstelle etwas zu warten, bis die Klasse verwendet wird und sich eingerichtet hat - dann führen Sie eine Schnittstelle ein. (wobei die Debatte über die Form der Schnittstelle ein separates Thema ist)

Um ehrlich zu sein, das einzige, was mich interessiert, ist die Schnittstelle. Eine solide Implementierung wäre schön, aber ich (oder jeder andere) könnte immer meine eigene erstellen.

Ich erinnere mich, wie wir vor ein paar Jahren genau das gleiche Gespräch mit HashSet<T> . Microsoft wollte HashSet<T> während die Community ISet<T> . Wenn ich mich richtig erinnere, haben wir zuerst HashSet<T> und dann ISet<T> . Ohne eine Schnittstelle war die Verwendung von HashSet<T> ziemlich eingeschränkt, da es schwierig (wenn nicht oft unmöglich) ist, eine öffentliche API zu ändern.

Ich sollte beachten, dass es jetzt auch SortedSet<T> , ganz zu schweigen von zahlreichen Nicht-BCL-Implementierungen von ISet<T> . Ich habe ISet<T> in öffentlichen APIs verwendet und bin dankbar dafür. Meine private Implementierung kann jede konkrete Implementierung verwenden, die ich für richtig halte. Ich kann auch leicht eine Implementierung gegen eine andere austauschen, ohne etwas zu beschädigen. Dies wäre ohne die Schnittstelle nicht möglich.

Für diejenigen, die sagen, dass wir immer unsere eigenen Schnittstellen definieren können, bedenken Sie dies. Nehmen Sie für einen Moment an, dass ISet<T> in der BCL nie passiert ist. Jetzt kann ich meine eigene Schnittstelle IMySet<T> sowie solide Implementierungen erstellen. Doch eines Tages wird die BCL HashSet<T> veröffentlicht. Es kann ISet<T> implementieren oder nicht, aber es implementiert nicht IMySet<T> . Daher kann ich HashSet<T> als Implementierung meiner IMySet<T> .

Ich fürchte, wir werden diese Travestie noch einmal wiederholen.
Wenn Sie noch nicht bereit sind, sich auf eine Schnittstelle festzulegen, ist es zu früh, eine konkrete Klasse einzuführen.

Die Meinungsverschiedenheiten finde ich gravierend. Wenn man sich nur die Zahlen ansieht, sind etwas mehr Leute für ein Interface, aber das sagt uns nicht viel. Ich werde versuchen, einige andere Leute zu fragen, die bereits an der Diskussion teilgenommen haben, aber noch keine Meinung zu einer Schnittstelle geäußert haben:

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

Für die benachrichtigten: _Typen, könnten Sie bitte Ihren Beitrag liefern? Es wäre sehr hilfreich, auch wenn Sie nur mit :+1:, :-1: abstimmen. Sie können mit dem Lesen dieses Problemkommentars beginnen (4 Beiträge oben hier) -- wir diskutieren, ob die Bereitstellung einer Schnittstelle eine gute Idee ist (nur dieser Aspekt vorerst, bis er gelöst ist)._

Vielleicht sind einige dieser Leute API-Rezensenten, aber ich glaube, wir brauchen ihre Unterstützung bei dieser grundlegenden Entscheidung, bevor wir fortfahren. @karelz , @terrajobst , wäre es möglich, dass Sie sie bitten, uns bei der Lösung dieses Aspekts zu helfen? Ihr Beitrag wäre sehr wertvoll, da sie diejenigen sind, die ihn schließlich überprüfen werden – es wäre sehr hilfreich, ihn zu diesem Zeitpunkt zu wissen, bevor Sie sich auf einen bestimmten Ansatz festlegen (oder mehr als einen Vorschlag annehmen, was ermüdend wäre). und ein bisschen sinnlos, da wir ihre Entscheidung früher kennen können).

Ich persönlich bin für eine Schnittstelle, aber wenn die Entscheidung anders ausfällt, gehe ich gerne einen anderen Weg.

Ich möchte keine API-Rezensenten in die Diskussion hineinziehen – es ist lang und chaotisch, es wäre nicht effizient für API-Rezensenten, alles noch einmal zu lesen oder auch nur zu entscheiden, was die letzte wichtige Antwort ist (ich verliere mich darin ).
Ich denke, wir sind an dem Punkt angelangt, an dem wir zwei formale API-Vorschläge erstellen können (siehe das "gute" Beispiel dort) und die Vor- und Nachteile jedes einzelnen hervorheben können. Wir können sie dann in unserem API-Review-Meeting überprüfen und unter Berücksichtigung der Stimmen Empfehlungen aussprechen. Abhängig von der Diskussion dort (wenn es mehrere Meinungen gibt) kommen wir möglicherweise zurück und starten eine Twitter-Umfrage / zusätzliches GH-Voting usw.

Übrigens: API-Review-Meetings finden fast jeden Dienstag statt.

Um den Start zu erleichtern, sollte ein Vorschlag wie folgt aussehen:

Vorschlagsbeispiel / Saatgut

```c#
Namespace System.Collections.Generic
{
öffentliche Klasse PriorityQueue
: IEzählbar, ICollection, IEnumerable, IReadOnlyCollection
{
public PriorityQueue();
öffentliche PriorityQueue (int-Kapazität);
public PriorityQueue(IComparerVergleich);
public PriorityQueue(IEnumerableSammlung);
public PriorityQueue(IEnumerableSammlung, IComparerVergleich);
public PriorityQueue (int-Kapazität, IComparerVergleich);

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

}
```

MACHEN:

  • Fehlendes Verwendungsbeispiel (es ist mir unklar, wie ich die Priorität von Elementen ausdrücke) - vielleicht sollten wir es so umgestalten, dass 'int' als Prioritätswerteingabe verwendet wird? Vielleicht eine Abstraktion dazu? (Ich glaube, es wurde oben diskutiert, aber der Thread ist viel zu lang zum Lesen, deshalb brauchen wir konkrete Vorschläge und keine weitere Diskussion)
  • Szenario UpdatePriority fehlt
  • Fehlt hier noch etwas, das oben besprochen wurde?

@karelz OK, werde es tun, bleib dran! :Lächeln:

In Ordung. Soweit ich das beurteilen kann, wird eine Schnittstelle oder ein Heap die API-Überprüfung nicht bestehen. Daher möchte ich eine etwas andere Lösung vorschlagen und beispielsweise den quaternären Heap vergessen. Die folgende Datenstruktur unterscheidet sich in einigen Punkten von der, die wir in Python , Java , C++ , Go , Swift und Rust (zumindest diese) finden können.

Das Hauptaugenmerk liegt auf Korrektheit, Vollständigkeit in Bezug auf Funktionalität und Intuitivität unter Beibehaltung optimaler Komplexität und großer realer Leistung.

@karelz @terrajobst

Vorschlag

Begründung

Es ist ein ziemlich häufiger Fall, dass ein Benutzer eine Reihe von Elementen hat, von denen einige eine höhere Priorität haben als andere. Schließlich möchten sie diese Reihe von Elementen in einer bestimmten Reihenfolge halten, um die folgenden Operationen effizient ausführen zu können:

  1. Fügen Sie der Sammlung ein neues Element hinzu.
  2. Rufen Sie das Element mit der höchsten Priorität ab (und können Sie es entfernen).
  3. Entfernen Sie ein Element aus der Auflistung.
  4. Ändern Sie ein Element in der Auflistung.
  5. Zwei Sammlungen zusammenführen.

Verwendungszweck

Glossar

  • Wert — Benutzerdaten.
  • Schlüssel — ein Objekt, das für Bestellzwecke verwendet wird.

Verschiedene Arten von Benutzerdaten

Zuerst werden wir uns auf den Aufbau der Prioritätswarteschlange konzentrieren (nur Elemente hinzufügen). Die Vorgehensweise hängt von der Art der Benutzerdaten ab.

Szenario 1

  • TKey und TValue sind separate Objekte.
  • TKey ist vergleichbar.
var queue = new PriorityQueue<int, string>();

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

Szenario 2

  • TKey und TValue sind separate Objekte.
  • TKey ist nicht vergleichbar.
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");

Szenario 3

  • TKey ist in TValue .
  • TKey ist nicht vergleichbar.
public class MyClass
{
    public MyKey Key { get; set; }
}

Neben einem Schlüsselkomparator benötigen wir auch einen Schlüsselselektor:

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 */ ));
Anmerkungen
  • Wir verwenden hier eine andere Enqueue Methode. Diesmal braucht es nur ein Argument ( TValue ).
  • Wenn der Schlüsselselektor definiert ist, muss die Methode Enqueue(TKey, TValue) InvalidOperationException .
  • Wenn der Schlüsselselektor nicht definiert ist, muss die Methode Enqueue(TValue) InvalidOperationException .

Szenario 4

  • TKey ist in TValue .
  • TKey ist vergleichbar.
var queue = new PriorityQueue<MyKey, MyClass>(selector);

queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
Anmerkungen
  • Der Vergleich für TKey wird wie in Szenario 1 als Comparer<TKey>.Default angenommen.

Szenario 5

  • TKey und TValue sind separate Objekte, aber vom gleichen Typ.
  • TKey ist vergleichbar.
var queue = new PriorityQueue<int, int>();

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

Szenario 6

  • Benutzerdaten sind ein einziges Objekt, das vergleichbar ist.
  • Es gibt keinen physischen Schlüssel oder ein Benutzer möchte ihn nicht verwenden.
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 */ ));
Anmerkungen

Am Anfang steht eine Unklarheit.

  • Es ist möglich, dass MyClass ein separates Objekt ist und der Benutzer einfach Schlüssel und Wert getrennt haben möchte, wie es in Szenario 5 der Fall war .
  • Es kann sich aber auch um ein einzelnes Objekt handeln (wie in diesem Fall).

So geht PriorityQueue mit der Mehrdeutigkeit um:

  • Wenn der Schlüsselselektor definiert ist, gibt es keine Mehrdeutigkeit. Es sind nur Enqueue(TValue) erlaubt. Daher besteht eine alternative Lösung zu Szenario 6 darin, einfach einen Selektor zu definieren und ihn an den Konstruktor zu übergeben.
  • Wenn der Schlüsselselektor nicht definiert ist, wird die Mehrdeutigkeit bei der ersten Verwendung einer Enqueue Methode aufgelöst:

    • Beim ersten Aufruf von Enqueue(TKey, TValue) Schlüssel und Wert als separate Objekte betrachtet ( Szenario 5 ). Von da an muss die Methode Enqueue(TValue) InvalidOperationException werfen.

    • Wenn Enqueue(TValue) zum ersten Mal aufgerufen wird, werden Schlüssel und Wert als dasselbe Objekt betrachtet, der Schlüsselselektor wird abgeleitet ( Szenario 6 ). Von da an muss die Methode Enqueue(TKey, TValue) InvalidOperationException werfen.

Andere Funktionen

Wir haben bereits die Erstellung einer Prioritätswarteschlange behandelt. Wir wissen auch, wie man Elemente zur Sammlung hinzufügt. Jetzt konzentrieren wir uns auf die verbleibende Funktionalität.

Element mit höchster Priorität

Es gibt zwei Operationen, die wir für das Element mit der höchsten Priorität ausführen können – es abrufen oder entfernen.

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

Was genau von den Methoden Peek und Dequeue , wird später besprochen.

Ändern eines Elements

Für ein beliebiges Element möchten wir es vielleicht ändern. Wenn beispielsweise eine Prioritätswarteschlange während der gesamten Lebensdauer eines Dienstes verwendet wird, besteht eine hohe Wahrscheinlichkeit, dass der Entwickler die Möglichkeit haben möchte, Prioritäten zu aktualisieren.

Überraschenderweise wird eine solche Funktionalität nicht durch äquivalente Datenstrukturen bereitgestellt: in Python , Java , C++ , Go , Swift und Rust . Möglicherweise auch einige andere, aber ich habe nur diese überprüft. Das Ergebnis? Enttäuschte Entwickler:

Wir haben hier grundsätzlich zwei Möglichkeiten:

  • Machen Sie es auf die Java-Art und stellen Sie diese Funktionalität nicht bereit. Ich bin entschieden dagegen. Es zwingt den Benutzer, ein Element aus der Sammlung zu entfernen (was alleine nicht gut funktioniert, aber dazu komme ich später) und es dann wieder hinzuzufügen. Es ist sehr hässlich, funktioniert nicht in allen Fällen und ist ineffizient.
  • Einführung eines neuen Griffkonzepts .

Griffe

Jedes Mal, wenn ein Benutzer ein Element zur Prioritätswarteschlange hinzufügt, wird ihm ein Handle gegeben:

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

Handle ist eine Klasse mit der folgenden öffentlichen API:

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

Es ist ein Verweis auf ein eindeutiges Element in der Prioritätswarteschlange. Wenn Sie sich Sorgen um die Effizienz machen, lesen Sie bitte die FAQ (und Sie werden sich keine Sorgen mehr machen).

Ein solcher Ansatz ermöglicht es uns, ein einzigartiges Element in der Prioritätswarteschlange auf sehr intuitive und einfache Weise zu ändern:

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

Im obigen Beispiel konnte ein Benutzer das Objekt selbst aktualisieren, da ein bestimmter Selektor definiert wurde. Die Prioritätswarteschlange musste lediglich benachrichtigt werden, um sich neu anzuordnen (wir wollen sie nicht beobachtbar machen).

Ähnlich wie die Art der Benutzerdaten die Art und Weise beeinflusst, wie eine Prioritätswarteschlange erstellt und mit Elementen gefüllt wird, variiert auch die Art und Weise, wie sie aktualisiert wird. Ich werde die Szenarien diesmal kürzer machen, da Sie wahrscheinlich schon wissen, was ich schreiben werde.

Szenarien

Szenario 7
  • TKey und TValue sind separate Objekte.
var queue = new PriorityQueue<int, string>();

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

queue.Update(handle, 3);

Wie Sie sehen, bietet ein solcher Ansatz eine einfache Möglichkeit, auf ein eindeutiges Element in der Prioritätswarteschlange zu verweisen, ohne dass Fragen gestellt werden. Es ist besonders hilfreich in Szenarien, in denen Schlüssel dupliziert werden können. Dies ist auch sehr effizient – ​​wir kennen das zu aktualisierende Element in O(1), und die Operation kann in O(log n) ausgeführt werden.

Alternativ könnte der Benutzer zusätzliche Methoden verwenden, die die gesamte Struktur in O(n) durchsuchen und dann das erste Element aktualisieren, das mit den Argumenten übereinstimmt. So wird ein Element in Java entfernt. Es ist weder ganz richtig noch effizient, aber manchmal einfacher:

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

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

queue.Update("three", 30);

Die obige Methode findet das erste Element mit seinem Wert gleich "three" . Sein Schlüssel wird auf 30 aktualisiert. Wir wissen jedoch nicht, welche aktualisiert wird, wenn mehr als eine die Bedingung erfüllt.

Es könnte mit einer Methode Update(TKey oldKey, TValue, TKey newKey) etwas sicherer sein. Dies fügt eine weitere Bedingung hinzu – der alte Schlüssel muss ebenfalls übereinstimmen. Beide Lösungen sind einfacher, aber nicht 100% sicher und weniger leistungsfähig (O(1 + log n) vs O(n + log n)).

Szenario 8
  • TKey ist in TValue .
var queue = new PriorityQueue<int, MyClass>(selector);

/* adding some elements */

queue.Update(handle);

Dieses Szenario wurde als Beispiel im Abschnitt Handles angegeben .

Das Obige wird in O(log n) erreicht. Alternativ könnte der Benutzer eine Methode Update(TValue) , die das erste Element gleich dem angegebenen findet und eine interne Neuanordnung durchführt. Dies ist natürlich O(n).

Szenario 9
  • TKey und TValue sind separate Objekte, aber vom gleichen Typ.

Mit einem Griff gibt es wie immer keine Mehrdeutigkeit. Im Falle anderer Methoden, die eine Aktualisierung ermöglichen, gibt es dies natürlich. Dies ist ein Kompromiss zwischen Einfachheit, Leistung und Korrektheit (der davon abhängt, ob Daten dupliziert werden können oder nicht).

Szenario 10
  • Benutzerdaten sind ein einzelnes Objekt.

Mit einem Griff gibt es wieder kein Problem. Das Aktualisieren über andere Methoden kann einfacher sein – aber auch weniger performant und nicht immer korrekt (gleiche Einträge).

Entfernen eines Elements

Kann einfach und richtig über einen Griff erfolgen. Alternativ über die Methoden Remove(TValue) und Remove(TKey, TValue) . Gleiche Probleme wie zuvor beschrieben.

Zusammenführung zweier Kollektionen

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

/* add some elements to both */

queue1.Merge(queue2);

Anmerkungen

  • Nach dem Zusammenführen teilen sich Warteschlangen dieselbe interne Darstellung. Der Benutzer kann eine der beiden verwenden.
  • Die Typen müssen übereinstimmen (statisch geprüft).
  • Vergleicher müssen gleich sein, sonst InvalidOperationException .
  • Selektoren müssen gleich sein, sonst InvalidOperationException .

Vorgeschlagene API

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

Offene Fragen

Prädikate stattdessen

Ich bin stark für die Herangehensweise mit Griffen, weil sie effizient, intuitiv und völlig korrekt ist . Die Frage ist, wie man mit einfacheren, aber möglicherweise nicht so sicheren Methoden umgeht. Eine Sache, die wir tun könnten, ist, diese einfacheren Methoden durch so etwas zu ersetzen:

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

Die Verwendung wäre ziemlich süß und mächtig. Und wirklich intuitiv. Und lesbar (sehr ausdrückbar). Ich bin dafür.

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

Das Setzen des neuen Schlüssels könnte möglicherweise auch eine Funktion sein, die den alten Schlüssel nimmt und ihn irgendwie umwandelt.

Gleiches gilt für Contains und Remove .

UpdateKey

Wenn der Schlüsselselektor nicht definiert ist (und daher Schlüssel und Wert getrennt aufbewahrt werden), könnte die Methode UpdateKey heißen. Es ist wahrscheinlich ausdrucksvoller. Wenn der Schlüsselselektor jedoch definiert ist, ist Update besser, da der Schlüssel bereits aktualisiert ist und was getan werden muss, ist die Neuordnung einiger Elemente in der Prioritätswarteschlange.

FAQ

Sind Griffe nicht ineffizient?

Es gibt kein Problem mit der Effizienz in Bezug auf die Verwendung von Griffen. Eine häufige Befürchtung ist, dass zusätzliche Zuweisungen erforderlich sind, da wir intern einen Heap verwenden, der auf einem Array basiert. Keine Angst. Weiter lesen.

Wie wollen Sie es dann umsetzen?

Es wäre ein völlig anderer Ansatz zur Bereitstellung einer Prioritätswarteschlange. Zum ersten Mal bietet eine Standardbibliothek diese Funktionalität nicht implementiert als binärer Heap, der als Array darunter dargestellt wird. Es wird mit einem Pairing-Heap implementiert, das vom Design her kein Array verwendet – es stellt stattdessen einfach einen Baum dar.

Wie performant wäre es?

  • Für allgemeine zufällige Eingaben wäre ein quaternärer Heap etwas schneller.
  • Es müsste jedoch auf einem Array basieren, um schneller zu sein. Dann könnten Handles nicht einfach auf Knoten basieren – zusätzliche Zuweisungen wären erforderlich. Dann konnten wir Elemente nicht vernünftig aktualisieren und entfernen.
  • So wie es derzeit konzipiert ist, haben wir eine einfach zu bedienende API bei einer ziemlich performanten Implementierung.
  • Ein zusätzlicher Vorteil eines darunter liegenden Pairing-Heaps ist die Möglichkeit, zwei Prioritätswarteschlangen in O(1) zusammenzuführen, anstelle von O(n) wie in binären/quaternären Heaps.
  • Pairing Heap ist immer noch blitzschnell. Siehe Referenzen. Manchmal ist es schneller als quaternäre Heaps (hängt von den Eingabedaten und den durchgeführten Operationen ab, nicht nur vom Zusammenführen).

Verweise

  • Michael L. Fredman, Robert Sedgewick, Daniel D. Sleator und Robert E. Tarjan (1986), The pairing heap: A new form of self-adjusting heap , Algorithmica 1:111-129 .
  • Daniel H. Larkin, Siddhartha Sen und Robert E. Tarjan (2014), A back-to-basics empirische Untersuchung von Prioritätswarteschlangen , arXiv:1403.0252v1 [cs.DS].

Übrigens, es ist für die 1. API-Review-Iteration. Der Abschnitt mit _offenen Fragen_ muss gelöst werden (ich brauche Ihre Meinung [und wenn möglich von API-Reviewern]). Wenn die 1. Iteration zumindest teilweise bestanden hat, möchte ich für den Rest der Diskussion ein neues Problem erstellen (schließen und verweisen).

Soweit ich das beurteilen kann, wird eine Schnittstelle oder ein Heap die API-Überprüfung nicht bestehen.

@pgolebiowski : Warum schließt man das Thema dann nicht einfach? Ohne eine Schnittstelle ist diese Klasse nahezu nutzlos. Der einzige Ort, an dem ich es verwenden könnte, sind private Implementierungen. An diesem Punkt kann ich bei Bedarf einfach meine eigene Prioritätswarteschlange erstellen (oder wiederverwenden). Ich kann eine öffentliche API in meinem Code mit diesem Typ in der Signatur nicht verfügbar machen, da sie kaputt geht, sobald ich sie für eine andere Implementierung austauschen muss.

@bendono
Nun, Microsoft hat hier das letzte Wort, nicht wenige Leute kommentieren den Thread. Und das wissen wir:

Ich bin mir ziemlich sicher, dass andere API-Rezensenten/Architekten es teilen werden, da ich einige Meinungen geprüft habe: Der Name sollte PriorityQueue sein, und wir sollten keine IHeap-Schnittstelle einführen. Es sollte genau eine Implementierung geben (wahrscheinlich über einen Heap).

Dies wird von @karelz , Software Engineer Manager, und @terrajobst , Program Manager und dem Besitzer dieses Repositorys + einigen API-Reviewern geteilt.

Obwohl mir der Ansatz mit Schnittstellen offensichtlich gefällt, wie in den vorherigen Posts deutlich gemacht wurde, kann ich sehen, dass er ziemlich schwierig ist, da wir in dieser Diskussion nicht viel Macht haben. Wir haben unsere Punkte dargelegt, aber wir sind nur einige Kommentatoren. Der Code gehört uns sowieso nicht. Was können wir sonst noch tun?

Warum schließt man das Thema dann nicht einfach? Ohne eine Schnittstelle ist diese Klasse nahezu nutzlos.

Ich habe genug getan – anstatt meine Arbeit zu hassen, tue etwas. Machen Sie die eigentliche Arbeit. Warum versuchst du mich für irgendetwas verantwortlich zu machen? Machen Sie sich selbst die Schuld, dass Sie es nicht geschafft haben, andere von Ihrem Standpunkt zu überzeugen.

Und bitte ersparen Sie mir solche Rhetorik. Es ist wirklich kindisch – mach es besser.

Übrigens, der Vorschlag konzentriert sich auf Dinge, die üblich sind, egal ob wir eine Schnittstelle einführen oder nicht, oder ob die Klasse PriorityQueue oder Heap . Konzentrieren Sie sich also auf das, was hier wirklich wichtig ist, und zeigen Sie uns etwas Voreingenommenheit, wenn Sie etwas wollen.

@pgolebiowski Natürlich ist es die Entscheidung von Microsoft. Aber es ist besser, eine API vorzustellen, die Sie verwenden möchten, die Ihren Anforderungen entspricht. Wenn es abgelehnt wird, dann soll es so sein. Ich sehe einfach keine Notwendigkeit, den Vorschlag zu kompromittieren.

Ich entschuldige mich, wenn Sie meine Kommentare als Vorwürfe interpretiert haben. Das war sicher nicht meine Absicht.

@pgolebiowski warum nicht KeyValuePair<TKey,TValue> für das Handle verwenden?

@SamuelEnglard

Warum nicht KeyValuePair<TKey,TValue> für das Handle verwenden?

  • Nun, der PriorityQueueHandle ist tatsächlich _ Paarungs-Heap-Knoten _. Es stellt zwei Eigenschaften bereit – TKey Key und TValue Value . Es hat jedoch viel mehr Logik im Inneren, die nur intern ist. Bitte beachten Sie meine Umsetzung dazu . Es enthält zum Beispiel auch Zeiger auf andere Knoten im Baum (alles wäre intern in CoreFX).
  • KeyValuePair ist eine Struktur, also wird sie jedes Mal kopiert + sie kann nicht vererbt werden.
  • Aber die Hauptsache ist, dass PriorityQueueHandle eine ziemlich komplizierte Klasse ist, die zufällig dieselbe öffentliche API wie KeyValuePair bereitstellt.

@bendono

Aber es ist besser, eine API vorzustellen, die Sie verwenden möchten, die Ihren Anforderungen entspricht. Wenn es abgelehnt wird, dann soll es so sein. Ich sehe einfach keine Notwendigkeit, den Vorschlag zu kompromittieren.

  • Es ist wahr, ich werde das im Hinterkopf behalten und sehen, was passiert. @karelz , könnten Sie zusammen mit dem Vorschlag auch einen Teil des vorherigen Beitrags (sie sind direkt vor dem Vorschlag) übergeben, in dem wir für die Schnittstellen gestimmt haben? Wir werden vielleicht später, nach der ersten Iteration der API-Überprüfung, darauf zurückkommen.
  • Unabhängig davon, welche Lösung wir wählen, wird die Funktionalität jedoch sehr ähnlich sein und es wäre hilfreich, sie überprüfen zu lassen. Denn wenn die Idee für Handles abgelehnt wird und wir Elemente nicht wirklich richtig aktualisieren / entfernen können (oder wir keine separaten TKey und TValue ), ist diese Klasse dann wirklich fast nutzlos - - wie es jetzt in Java ist.
  • Insbesondere wenn wir keine Schnittstelle haben, kann meine AlgoKit-Bibliothek keinen gemeinsamen Kern für Haufen mit CoreFX haben, was für mich wirklich traurig wäre.
  • Und ja, es überrascht mich wirklich, dass das Hinzufügen einer Schnittstelle von Microsoft als Nachteil empfunden wird.

Das war sicher nicht meine Absicht.

Sorry, dann mein Fehler.

Meine Fragen zum Design (Disclaimer: diese Fragen & Klarstellungen, (noch) keine harten Pushbacks):

  1. Brauchen wir wirklich PriorityQueueHandle ? Was ist, wenn wir nur eindeutige Werte in der Warteschlange erwarten?

    • Motivation: Es scheint ein ziemlich kompliziertes Konzept zu sein. Wenn wir es haben, würde ich gerne verstehen, warum wir es brauchen? Wie hilft es? Oder sind es nur spezifische Implementierungsdetails, die in die API-Oberfläche eindringen? Wird es uns so viel Leistung bringen, die Komplikation in der API zu bezahlen?

  2. Brauchen wir Merge ? Gibt es das in anderen Sammlungen? Wir sollten keine APIs hinzufügen, nur weil sie einfach zu implementieren sind, sollte es einen allgemeinen Anwendungsfall für die APIs geben.

    • Vielleicht würde es ausreichen, nur eine Initialisierung von IEnumerable<KeyValuePair<TKey, TValue>> hinzuzufügen? + verlassen Sie sich auf Linq

  3. Brauchen wir comparer Überladung? Können wir einfach immer auf Standard zurückgreifen? (Haftungsausschluss: Mir fehlen in diesem Fall Kenntnisse/Fachkenntnisse, also einfach nachfragen)
  4. Brauchen wir keySelector Überladungen? Ich denke, wir sollten entscheiden, ob wir Priorität als Teil des Wertes oder als separate Sache haben wollen. Separate Priorität scheint mir etwas natürlicher, aber ich habe keine starke Meinung. Kennen wir die Vor- und Nachteile?

Separate/parallele Entscheidungspunkte:

  1. Klassenname PriorityQueue vs. Heap
  2. Einführung von IHeap und Konstruktorüberladung?

IHeap- und Konstruktorüberladung einführen?

Scheint so, als ob sich die Dinge so weit beruhigt haben, dass ich meine 2 Cent einwerfen kann ... Ich mag die Benutzeroberfläche persönlich. Es abstrahiert die Implementierungsdetails von der API (und der von dieser API beschriebenen Kernfunktionalität) auf eine Weise, die meiner Meinung nach die Struktur vereinfacht und die meiste Benutzerfreundlichkeit ermöglicht.

Worüber ich nicht so überzeugt bin, ist, ob wir die Schnittstelle gleichzeitig mit PQueue/Heap/ILikeThisItemMoreThanThisItemList erstellen oder später hinzufügen. Das Argument, dass die API möglicherweise "in Bewegung" ist und wir sie daher zuerst als Klasse veröffentlichen sollten, bis wir Feedback erhalten, ist sicherlich ein berechtigtes Argument, dem ich nicht widerspreche. Es stellt sich dann die Frage, wann es als "stabil" genug angesehen wird, um eine Schnittstelle hinzuzufügen. Weit oben im Thread wurde erwähnt, dass IList und IDictionary hinter den APIs ihrer kanonischen Implementierungen zurückbleiben, die wir vor langer Zeit hinzugefügt haben. Welche Zeitspanne wird also als akzeptable Ruhezeit angesehen?

Wenn wir diesen Zeitraum mit hinreichender Sicherheit definieren können und sichergehen können, dass er nicht inakzeptabel blockiert, sehe ich kein Problem darin, dieses umfangreiche Datenstruktur-Ding ohne Schnittstelle zu versenden. Nach diesem Zeitraum können wir die Nutzung untersuchen und erwägen, eine Schnittstelle hinzuzufügen.

Und ja, es überrascht mich wirklich, dass das Hinzufügen einer Schnittstelle von Microsoft als Nachteil empfunden wird.

Das ist , weil in vielerlei Hinsicht es ein Nachteil ist. Wenn wir ein Interface ausliefern, ist es so ziemlich fertig. Es gibt nicht viel Spielraum für die Iteration dieser API, daher sollten wir sicher sein, dass sie beim ersten Mal richtig ist und auch in den kommenden Jahren richtig bleiben wird. Einige nette Funktionen zu vermissen ist ein viel besserer Ort, als an einer möglicherweise unzureichenden Benutzeroberfläche festzusitzen, wo es ach so schön wäre, nur diese eine kleine Änderung zu haben, die alles besser machen würde.

Danke für den Input, @karelz und @ianhays!

Duplikate zulassen

Brauchen wir wirklich PriorityQueueHandle ? Was ist, wenn wir nur eindeutige Werte in der Warteschlange erwarten?

Motivation: Es scheint ein ziemlich kompliziertes Konzept zu sein. Wenn wir es haben, würde ich gerne verstehen, warum wir es brauchen? Wie hilft es? Oder sind es nur spezifische Implementierungsdetails, die in die API-Oberfläche eindringen? Wird es uns so viel Leistung bringen, die Komplikation in der API zu bezahlen?

Nein, das brauchen wir nicht. Die oben vorgeschlagene API für die Prioritätswarteschlange ist ziemlich leistungsstark und sehr flexibel. Es basiert auf der Annahme, dass Elemente und Prioritäten dupliziert werden könnten . Aufgrund dieser Annahme ist ein Handle erforderlich, um den richtigen Knoten entfernen oder aktualisieren zu können. Wenn wir jedoch einschränken, dass Elemente eindeutig sein müssen, könnten wir mit einer einfacheren API und ohne Offenlegung einer internen Klasse ( PriorityQueueHandle ) das gleiche Ergebnis wie oben erzielen, was in der Tat nicht ideal ist.

Nehmen wir an, wir erlauben nur eindeutige Elemente. Wir konnten immer noch alle vorherigen Szenarien unterstützen und die optimale Leistung beibehalten. Einfachere API:

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

In Kürze würde es einen Basiswert von Dictionary<TElement, InternalNode> . Das Testen, ob die Prioritätswarteschlange ein Element enthält, kann noch schneller als beim vorherigen Ansatz durchgeführt werden. Das Aktualisieren und Entfernen von Elementen wird erheblich vereinfacht, da wir immer auf ein direktes Element in der Warteschlange zeigen können.

Wahrscheinlich lohnt es sich nicht, Duplikate zuzulassen, und das oben Genannte reicht aus. Ich denke, ich mag das. Was denken Sie?

Verschmelzen

Brauchen wir Merge ? Gibt es das in anderen Sammlungen? Wir sollten keine APIs hinzufügen, nur weil sie einfach zu implementieren sind, sollte es einen allgemeinen Anwendungsfall für die APIs geben.

Zustimmen. Wir brauchen es nicht. Wir können jederzeit API hinzufügen, aber nicht entfernen. Ich bin damit einverstanden, diese Methode zu entfernen und (möglicherweise) später hinzuzufügen (falls erforderlich).

Vergleich

Brauchen wir eine Vergleicherüberlastung? Können wir einfach immer auf Standard zurückgreifen? (Haftungsausschluss: Mir fehlen in diesem Fall Kenntnisse/Fachkenntnisse, also einfach nachfragen)

Das halte ich aus zwei Gründen für ziemlich wichtig:

  • Ein Benutzer möchte, dass seine Elemente in aufsteigender oder absteigender Reihenfolge sortiert werden. Die höchste Priorität sagt uns nicht, wie die Bestellung erfolgen soll.
  • Wenn wir einen Vergleich überspringen, zwingen wir den Benutzer, uns immer eine Klasse zu liefern, die IComparable implementiert.

Außerdem ist es konsistent mit der bestehenden API. Sehen Sie sich SortedDictionary an .

Wähler

Brauchen wir keySelector-Überladungen? Ich denke, wir sollten entscheiden, ob wir Priorität als Teil des Wertes oder als separate Sache haben wollen. Separate Priorität scheint mir etwas natürlicher, aber ich habe keine starke Meinung. Kennen wir die Vor- und Nachteile?

Ich mag auch getrennte Prioritäten. Abgesehen davon ist es einfacher zu implementieren, bessere Leistung und Speichernutzung, intuitivere API, weniger Arbeit für den Benutzer (keine Notwendigkeit, IComparable zu implementieren).

Nun zum Selektor...

Vorteile

Dies macht diese Prioritätswarteschlange flexibel. Es ermöglicht Benutzern:

  • Elemente und ihre Prioritäten als separate Elemente
  • Elemente (komplexe Klassen), die irgendwo Prioritäten haben
  • eine externe Logik, die die Priorität für ein bestimmtes Element abruft
  • Elemente, die IComparable implementieren

Es erlaubt so ziemlich jede Konfiguration, die ich mir vorstellen kann. Ich finde es nützlich, da die Benutzer einfach "plug n play" könnten. Es ist auch ziemlich intuitiv.

Nachteile

  • Es gibt noch mehr zu lernen.
  • Mehr API. Zwei zusätzliche Konstruktoren, eine zusätzliche Methode Enqueue und Update .
  • Wenn wir uns entscheiden, das Element und die Priorität getrennt oder zusammenzuführen, zwingen wir einige Benutzer (die ihre Daten in einem anderen Format haben), ihren Code an diese Datenstruktur anzupassen.

Separate/parallele Entscheidungspunkte

Klassenname PriorityQueue vs. Heap

Führen Sie IHeap und die Konstruktorüberladung ein.

  • Es fühlt sich an, dass PriorityQueue ein Teil von CoreFX sein sollte und nicht Heap .
  • Was die IHeap -Schnittstelle betrifft – da eine Prioritätswarteschlange mit etwas anderem als einem Heap implementiert werden kann, möchten wir sie wahrscheinlich nicht auf diese Weise offenlegen. Möglicherweise benötigen wir jedoch IPriorityQueue .

Wenn wir ein Interface ausliefern, ist es so ziemlich fertig. Es gibt nicht viel Spielraum für die Iteration dieser API, daher sollten wir sicher sein, dass sie beim ersten Mal richtig ist und auch in den kommenden Jahren richtig bleiben wird. Einige nette Funktionen zu vermissen ist ein viel besserer Ort, als an einer möglicherweise unzureichenden Benutzeroberfläche festzusitzen, wo es ach so schön wäre, nur diese eine kleine Änderung zu haben, die alles besser machen würde.

Stimme voll und ganz zu!

Vergleichbare Elemente

Wenn wir noch eine weitere Annahme hinzufügen: dass Elemente vergleichbar sein müssen, dann ist die API noch einfacher. Aber auch hier ist es weniger flexibel.

Vorteile

  • Keine Notwendigkeit für IComparer .
  • Keine Notwendigkeit für den Selektor.
  • Eine Methode Enqueue und Update .
  • Ein generischer Typ statt zwei.

Nachteile

  • Wir können Element und Priorität nicht als separate Objekte haben. Benutzer müssen eine neue Wrapper-Klasse bereitstellen, wenn sie in diesem Format vorliegt.
  • Benutzer müssen immer IComparable implementieren, bevor sie diese Prioritätswarteschlange verwenden können.
  • Wenn wir Elemente und Prioritäten getrennt haben, könnten wir es einfacher implementieren, mit besserer Leistung und Speichernutzung - internes Wörterbuch <TElement, InternalNode> und InternalNode enthalten 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();
}

Alles ist ein Kompromiss. Wir entfernen einige Funktionen, verlieren etwas Flexibilität, aber vielleicht brauchen wir das nicht, um 95 % unserer Nutzer zufrieden zu stellen.

Ich mag den Schnittstellenansatz auch, weil er flexibler ist und mehr Verwendungsmöglichkeiten bietet, aber ich stimme auch @karelz und @ianhays zu, dass wir warten sollten, bis die neue Klassen-API verwendet wird und wir Feedback erhalten, bis wir die Schnittstelle tatsächlich

Auch in Bezug auf Vergleicher denke ich, dass es den anderen BCL-APIs folgt, und ich mag es nicht, dass Benutzer eine neue Wrapper-Klasse bereitstellen müssen. Ich mag es wirklich, dass die Konstruktorüberladung einen Vergleicheransatz erhält und diesen Vergleicher in allen Vergleichen verwendet, die intern durchgeführt werden müssen. Wenn kein Vergleicher bereitgestellt wird oder Vergleicher null ist, verwenden Sie den Standardvergleich.

@pgolebiowski vielen Dank für diesen detaillierten und beschreibenden API-Vorschlag und für die proaktive und tatkräftige Unterstützung, diese API genehmigt und zu CoreFX hinzuzufügen in einen Kommentar und aktualisieren Sie den Hauptkommentar oben, da dies den Rezensenten das Leben erleichtern würde.

OK, ich bin überzeugt von Comparer, es macht Sinn und es ist stimmig.
Ich bin immer noch am Selektor hin- und hergerissen - IMO sollten wir versuchen, darauf zu verzichten - teilen wir es in 2 Varianten auf.

```c#
öffentliche Klasse PriorityQueue
: IEzählbar,
IEnumerable<(TElement-Element, TPriority-Priorität)>,
IReadOnlyCollection<(TElement-Element, TPriority-Priorität)>
// ICollection absichtlich nicht enthalten
{
public PriorityQueue();
public PriorityQueue(IComparerVergleich);

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

//
// Selektorteil
//
public PriorityQueue(FuncPrioritätSelektor);
public PriorityQueue(FuncPrioritySelector, IComparerVergleich);

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

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

}
````

Offene Fragen:

  1. Klassenname PriorityQueue vs. Heap
  2. Einführung von IHeap und Konstruktorüberladung? (Sollen wir auf später warten?)
  3. Stellen Sie IPriorityQueue ? (Sollen wir auf später warten - IDictionary Beispiel)
  4. Selektor verwenden (der im Wert gespeicherten Priorität) oder nicht (5 APIs Unterschied)
  5. Tupel verwenden (TElement element, TPriority priority) vs. KeyValuePair<TPriority, TElement>

    • Sollten Peek und Dequeue eher ein out Argument anstelle eines Tupels haben?

Ich werde versuchen, es morgen von der API-Überprüfungsgruppe auszuführen, um ein frühes Feedback zu erhalten.

Peek und Dequeue Tupelfeldname auf priority korrigiert (danke
Top-Post aktualisiert mit dem neuesten Vorschlag oben.

Ich schließe mich dem Kommentar von @pgolebiowski für das Vorantreiben!

Offene Fragen:

Ich denke, wir könnten hier noch einen haben:

  1. Beschränken Sie sich nur auf einzigartige Elemente (erlauben Sie keine Duplikate).

Guter Punkt, festgehalten als 'Annahmen' im Top-Post, und nicht als offene Frage. Das Gegenteil würde die API ziemlich hässlich machen.

Sollen wir bool von Remove ? Und auch die Priorität als out priority arg? (Ich glaube, wir haben kürzlich ähnliche Überladungen in andere Datenstrukturen eingefügt)

Ähnlich wie bei Contains - ich wette, jemand wird die Priorität daraus ziehen wollen. Vielleicht möchten wir eine Überladung mit out priority hinzufügen.

Ich bleibe der Meinung (obwohl ich es noch nicht äußern muss), dass Heap eine Implementierung implizieren würde, und würde den Aufruf von PriorityQueue gebührend unterstützen. (Ich würde dann sogar einen Vorschlag für eine schlankere Heap Klasse unterstützen, die doppelte Elemente zulässt und keine Aktualisierung usw. zulässt (mehr im Einklang mit dem ursprünglichen Vorschlag), aber das erwarte ich nicht passieren).

KeyValuePair<TPriority, TElement> sollte auf keinen Fall verwendet werden, da doppelte Prioritäten erwartet werden, und ich denke, dass KeyValuePair<TElement, TPriority> auch verwirrend ist, also würde ich es unterstützen, KeyValuePair überhaupt nicht zu verwenden, und entweder Verwenden von einfachen Tupeln oder out-Parametern für die Prioritäten (persönlich mag ich out-Parameter, aber ich bin nicht aufgeregt).

Wenn wir Duplikate nicht zulassen, müssen wir entscheiden, wie wir versuchen, sie mit anderen/geänderten Prioritäten erneut hinzuzufügen.

Ich unterstütze den Auswahlvorschlag aus dem einfachen Grund nicht, weil er Redundanz impliziert und ordnungsgemäß Verwirrung stiften wird. Wenn die Priorität eines Elements mit ihm gespeichert wird, dann wird es an zwei Stellen gespeichert, und wenn sie nicht mehr synchron sind (dh jemand vergisst, die nutzlos aussehende Methode Update(TElement) aufzurufen), dann viel Leid werde sicherstellen. Wenn der Selektor ein Computer ist, sind wir offen dafür, dass Leute absichtlich ein Element hinzufügen, dann die Werte ändern, aus denen sie berechnet werden, und wenn sie jetzt versuchen, es erneut hinzuzufügen, gibt es eine Fülle von Dingen, die je nach Bedarf schief gehen können über die Entscheidung, was passiert, wenn dies geschieht. Auf einer etwas höheren Ebene könnte ein Versuch jedoch dazu führen, dass eine geänderte Kopie des Elements hinzugefügt wird, da es nicht mehr dem entspricht, was es einmal war (dies ist ein allgemeines Problem mit veränderlichen Schlüsseln, aber ich denke, dass die Trennung von Priorität und Element dies tun wird.) helfen, potenzielle Probleme zu vermeiden).

Der Selektor selbst neigt dazu, sein Verhalten zu ändern, was eine weitere Möglichkeit ist, mit der Benutzer alles ohne Nachdenken zerstören können. Viel besser finde ich es, wenn die Benutzer die Prioritäten explizit angeben. Das große Problem, das ich dabei sehe, ist, dass es vermittelt, dass doppelte Einträge zulässig sind, da wir ein Paar und kein Element deklarieren. Eine sinnvolle Inline-Dokumentation und ein bool Rückgabewert auf Enqueue sollten hier jedoch trivial Abhilfe schaffen. Besser als ein Boolean wäre vielleicht, die alte/neue Priorität des Elements zurückzugeben (zB wenn Enqueue die neu bereitgestellte Priorität oder das Minimum der beiden oder die alte Priorität verwendet), aber ich denke, dass Enqueue sollte einfach fehlschlagen, wenn Sie versuchen, etwas erneut hinzuzufügen, und sollte daher nur ein bool , das den Erfolg anzeigt. Dadurch bleiben Enqueue und Update vollständig getrennt und klar definiert.

Ich würde es unterstützen, KeyValuePair überhaupt nicht zu verwenden und entweder einfache Tupel zu verwenden

Bei Tupeln bin ich bei dir.

Wenn wir Duplikate nicht zulassen, müssen wir entscheiden, wie wir versuchen, sie mit anderen/geänderten Prioritäten erneut hinzuzufügen.

  • Ich sehe eine Ähnlichkeit mit dem Indexer innerhalb eines Dictionary . Wenn Sie dort dictionary["something"] = 5 ausführen, wird es aktualisiert, wenn "something" dort ein Schlüssel war. Wenn es nicht da war, wird es einfach hinzugefügt.
  • Die Methode Enqueue ist für mich jedoch analog zur Methode Add im Wörterbuch. Das bedeutet, dass eine Ausnahme ausgelöst werden sollte.
  • Unter Berücksichtigung der obigen Punkte könnten wir erwägen, der Prioritätswarteschlange einen Indexer hinzuzufügen, um das von Ihnen gewünschte Verhalten zu unterstützen.
  • Aber ein Indexer wiederum ist möglicherweise nicht etwas, das mit dem Konzept von Warteschlangen funktioniert.
  • Was uns zu dem Schluss führt, dass die Methode Enqueue nur eine Ausnahme auslösen sollte, wenn jemand ein dupliziertes Element hinzufügen möchte. Ebenso sollte die Methode Update eine Ausnahme auslösen, wenn jemand die Priorität eines nicht vorhandenen Elements aktualisieren möchte.
  • Was uns zu einer neuen Lösung führt – fügen Sie die Methode TryUpdate , die tatsächlich bool zurückgibt.

Ich unterstütze den Selektorvorschlag aus dem einfachen Grund nicht, weil er Redundanz impliziert

Ist es nicht so, dass der Schlüssel nicht physisch kopiert wird (falls vorhanden), sondern der Selektor nur eine Funktion bleibt, die aufgerufen wird, wenn Prioritäten ausgewertet werden müssen? Wo ist die Redundanz?

Ich denke, dass die Trennung von Priorität und Element dazu beitragen wird, potenzielle Probleme zu vermeiden.

Das einzige Problem ist der Fall, wenn der Kunde nicht die physische Priorität hat. Dann kann er nicht viel machen.

Viel besser finde ich es, wenn die Benutzer die Prioritäten explizit angeben. Das große Problem, das ich dabei sehe, ist, dass es vermittelt, dass doppelte Einträge zulässig sind, da wir ein Paar und kein Element deklarieren.

Ich sehe einige Probleme mit dieser Lösung, aber nicht unbedingt, warum sie vermittelt, dass doppelte Einträge zulässig sind. Ich denke, die Logik "keine Duplikate" sollte nur auf TElement angewendet werden - die Priorität ist hier nur ein Wert.

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

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

@VisualMelon macht das Sinn?

@pgolebiowski

Ich stimme dem Hinzufügen eines TryUpdate voll und ganz zu, und Update Werfen macht für mich Sinn. Ich dachte eher an ISet in Bezug auf Enqueue (statt IDictionary ). Werfen würde auch Sinn machen, ich muss diesen Punkt übersehen haben, aber ich denke, dass die Rückgabe bool die "Festigkeit" des Typs vermittelt. Vielleicht wäre auch ein TryEnqueue in Ordnung (mit Add Werfen)? Ein TryRemove würde auch geschätzt werden (fehlt, wenn leer). Zu Ihrem letzten Punkt, ja, das ist das Verhalten, das ich auch im Sinn hatte. Ich nehme an, eine Analogie mit IDictionary ist besser als ISet , wenn man es überlegt, und das sollte klar genug sein. (Zusammenfassend: Ich würde alles Werfen gemäß Ihrem Vorschlag unterstützen, aber Try* ist ein Muss, wenn dies der Fall ist; ich stimme auch Ihren Aussagen zu den Fehlerbedingungen zu).

Was den Indexer betrifft, haben Sie Recht, dass er nicht wirklich zum Warteschlangenkonzept passt, das würde ich nicht unterstützen. Wenn überhaupt, wäre eine gut benannte Methode zum Queue oder Update in Ordnung.

Ist es nicht so, dass der Schlüssel nicht physisch kopiert wird (falls vorhanden), sondern der Selektor nur eine Funktion bleibt, die aufgerufen wird, wenn Prioritäten ausgewertet werden müssen? Wo ist die Redundanz?

Sie haben Recht, ich habe die Vorschlagsaktualisierung falsch gelesen (das bisschen über das Speichern im Element). Wenn man davon ausgeht, dass der Selektor bei Bedarf aufgerufen wird (was in jeder Dokumentation klargestellt werden müsste, da dies Auswirkungen auf die Leistung haben könnte), stehen immer noch die Punkte fest, dass sich das Ergebnis der Funktion ändern kann, ohne dass die Datenstruktur reagiert und die zwei nicht synchronisiert, es sei denn, Update wird aufgerufen. Schlimmer noch, wenn ein Benutzer die effektive Priorität mehrerer Elemente ändert und nur eines von ihnen aktualisiert, hängt die Datenstruktur von "nicht festgeschriebenen" Änderungen ab, wenn sie aus "nicht aktualisierten" Elementen ausgewählt werden (ich habe nicht nachgesehen die vorgeschlagene DataStructure-Implementierung im Detail, aber ich denke, dies ist unbedingt ein Problem für jedes Update O(<n) ). Hierdurch wird der Benutzer gezwungen, alle Prioritäten explizit zu aktualisieren, wodurch die Datenstruktur notwendigerweise von einem konsistenten Zustand in einen anderen überführt wird.

Beachten Sie, dass sich alle meine bisherigen Kritikpunkte mit dem Selector-Vorschlag um die Robustheit der API drehen: Ich denke, der Selektor macht es leicht, ihn falsch zu verwenden. Abgesehen von der Benutzerfreundlichkeit sollten die Elemente in der Warteschlange jedoch konzeptionell nicht ihre Priorität kennen müssen. Es ist für sie potenziell irrelevant, und wenn der Benutzer seine Elemente in ein struct Queable<T>{} oder so einhüllt, dann scheint dies ein Fehler bei der Bereitstellung einer reibungslosen API zu sein (so sehr es mir schadet, den Begriff zu verwenden).

Sie könnten natürlich argumentieren, dass der Benutzer ohne den Selektor die Last trägt, den Selektor aufzurufen, aber ich denke, wenn die Elemente ihre Priorität kennen, werden sie (hoffentlich) ordentlich (dh eine Eigenschaft) freigelegt, und wenn sie es nicht tun t, dann gibt es keinen Selektor, um den Sie sich kümmern müssen (Prioritäten im laufenden Betrieb generieren usw.). Es ist viel mehr bedeutungslose Installation erforderlich, um einen Selektor zu unterstützen, wenn Sie ihn nicht möchten, als um die Prioritäten weiterzugeben, wenn Sie bereits einen gut definierten 'Selektor' haben. Der Selektor verleitet den Benutzer dazu, diese Informationen offenzulegen (vielleicht nicht hilfreich), während separate Prioritäten eine extrem transparente Oberfläche bieten, von der ich mir nicht vorstellen kann, dass sie Designentscheidungen beeinflusst.

Das einzige Argument, das mir wirklich für den Selektor einfällt, ist, dass Sie PriorityQueue , und andere Teile des Programms können es verwenden, ohne zu wissen, wie die Prioritäten berechnet werden. Ich muss etwas länger darüber nachdenken, aber angesichts der Nischentauglichkeit erscheint mir dies als wenig Belohnung für den relativ hohen Aufwand in allgemeineren Fällen.

_Edit: Nachdem ich noch etwas darüber nachgedacht habe, wäre es eher sehr schön, in sich geschlossene PriorityQueue s zu haben, auf die man einfach Elemente werfen kann, aber ich behaupte, dass die Kosten für die Umgehung dieses Problems groß wären, während die Einführungskosten wären erheblich geringer._

Ich habe mich ein wenig damit beschäftigt, Open-Source-.NET-Codebasen zu durchsuchen, um reale Beispiele für die Verwendung von Prioritätswarteschlangen zu sehen. Das knifflige Problem besteht darin, alle Schülerprojekte und den Quellcode der Schulung herauszufiltern.

Verwendung 1: Roslyn-Benachrichtigungsdienst
https://github.com/dotnet/roslyn/
Der Roslyn-Compiler enthält eine private Prioritätswarteschlangenimplementierung namens "PriorityQueue", die eine sehr spezifische Optimierung zu enthalten scheint - eine Objektwarteschlange, die Objekte in der Warteschlange wiederverwendet, um zu vermeiden, dass sie durch Datenmüll gesammelt werden. Die Methode Enqueue_NoLock führt eine Auswertung für current.Value.MinimumRunPointInMS < entry.Value.MinimumRunPointInMS durch, um zu bestimmen, wo in der Warteschlange der neue Knoten platziert werden soll. Jedes der beiden hier vorgeschlagenen Hauptprioritätswarteschlangendesigns (Vergleichsfunktion/Delegierter vs. explizite Priorität) würde zu diesem Nutzungsszenario passen.

Verwendung 2: Lucene.net
https://github.com/apache/lucenenet
Dies ist wohl das größte Verwendungsbeispiel für eine PriorityQueue, das ich in .NET finden konnte. Apache Lucene.net ist eine vollständige .net-Portierung der beliebten Lucene-Suchmaschinenbibliothek. In meinem Unternehmen verwenden wir die Java-Version, und laut der Apache-Website verwenden einige große Namen die .NET-Version. Es gibt eine große Anzahl von Forks des .NET-Projekts in Github.

Lucene enthält eine eigene PriorityQueue-Implementierung, die in eine Reihe von "spezialisierten" Prioritätswarteschlangen unterteilt ist: HitQueue, TopOrdAndFloatQueue, PhraseQueue und SuggestWordQueue. Außerdem instanziiert das Projekt an mehreren Stellen direkt PriorityQueue.

Die oben verlinkte PriorityQueue-Implementierung von Lucene ist der ursprünglichen Priority-Queue-API, die in dieser Ausgabe veröffentlicht wurde, sehr ähnlich. Es ist definiert als "PriorityQueue" und akzeptiert einen IComparerParameter in seinem Konstruktor. Zu den Methoden und Eigenschaften gehören Count, Clear, Offer (Enqueue/Push), Poll (Dequeue/Pop), Peek, Remove (entfernt das erste übereinstimmende Element, das in der Warteschlange gefunden wurde), Add (Synonym für Offer). Interessanterweise bieten sie auch Enumerationsunterstützung für die Warteschlange.

Die Priorität in der PriorityQueue von Lucene wird durch den Vergleicher bestimmt, der an den Konstruktor übergeben wurde, und wenn keine übergeben wurde, wird davon ausgegangen, dass die verglichenen Objekte IComparable implementierenund verwendet diese Schnittstelle, um den Vergleich durchzuführen. Das hier veröffentlichte ursprüngliche API-Design ist ähnlich, außer dass es auch mit Werttypen funktioniert.

Es gibt eine große Anzahl von Anwendungsbeispielen durch ihre Codebasis, SloppyPhraseScorer ist eines davon.

Der Konstruktor von SloppyPhraseScorer instanziiert eine neue PhraseQueue (pq), die eine ihrer eigenen benutzerdefinierten Unterklassen von PriorityQueue ist. Es wird eine Sammlung von PhrasePositions generiert, die wie ein Wrapper für eine Menge von Postings, eine Position und eine Menge von Begriffen aussieht. Die Methode FillQueue zählt die Phrasenpositionen auf und reiht sie ein. PharseFreq() ruft eine AdvancePP-Funktion auf und scheint auf einer hohen Ebene die Warteschlange zu entfernen, die Priorität eines Elements zu aktualisieren und dann erneut in die Warteschlange einzureihen. Die Priorität wird relativ (mithilfe eines Vergleichers) und nicht explizit bestimmt (Priorität wird nicht als zweiter Parameter beim Enqueue "übergeben").

Sie sehen, dass ein über den Konstruktor übergebener Vergleichswert (zB Integer) aufgrund ihrer Implementierung von ausreicht . Ihre Vergleichsfunktion ("LessThan") wertet drei verschiedene Felder aus: PhrasePositions.doc, PhrasePositions.position und PhrasePositions.offset.

Verwendung 3: Spieleentwicklung
Ich habe die Suche nach Verwendungsbeispielen in diesem Bereich noch nicht abgeschlossen, aber ich habe einige Beispiele für benutzerdefinierte .NET PriorityQueue gesehen, die in der Spieleentwicklung verwendet werden. Ganz allgemein gruppierten sich diese eher um die Pfadfindung als Hauptanwendungsfall (Dijkstras). Es gibt viele Leute, die fragen, wie Pfadfindungsalgorithmen in .NET aufgrund von Unity 3D implementiert werden können.

Muss noch durch diesen Bereich graben; sah einige Beispiele mit expliziter Priorität, die in die Warteschlange eingereiht wurden, und einige Beispiele, die Comparer/IComparable verwenden.

In einer separaten Anmerkung gab es einige Diskussionen über eindeutige Elemente, das Entfernen expliziter Elemente und die Feststellung, ob ein bestimmtes Element vorhanden ist.

Warteschlangen als Datenstruktur unterstützen im Allgemeinen das Einreihen und Entfernen aus der Warteschlange. Wenn wir uns auf den Weg machen, andere mengen-/listenähnliche Operationen bereitzustellen, frage ich mich, ob wir tatsächlich eine ganz andere Datenstruktur entwerfen - so etwas wie eine sortierte Liste von Tupeln. Wenn ein Anrufer einen anderen Bedarf als Enqueue, Dequeue, Peek hat, braucht er vielleicht etwas anderes als eine Prioritätswarteschlange? Warteschlange impliziert per Definition das Einfügen in eine Warteschlange und das geordnete Entfernen aus der Warteschlange; sonst nicht viel.

@ebickle

Ich schätze Ihre Bemühungen, andere Repositorys durchzugehen und zu überprüfen, wie die Prioritätswarteschlangenfunktionalität dort bereitgestellt wurde. IMO würde diese Diskussion jedoch von der Bereitstellung spezifischer Designvorschläge profitieren. Dieser Thread ist bereits schwer zu verfolgen und eine lange Geschichte ohne Schlussfolgerungen zu erzählen, macht es noch schwieriger.

Queues [...] unterstützen generell das Enqueuing und Dequeuing. [...] Wenn ein Anrufer einen anderen Bedarf als Enqueue, Dequeue, Peek hat, braucht er vielleicht etwas anderes als eine Prioritätswarteschlange? Warteschlange impliziert per Definition das Einfügen in eine Warteschlange und das geordnete Entfernen aus der Warteschlange; sonst nicht viel.

  • Was ist die Schlussfolgerung? Vorschlag?
  • Es ist sehr weit von der Wahrheit entfernt. Sogar der von Ihnen erwähnte Algorithmus von Dijkstra verwendet die Aktualisierung der Prioritäten von Elementen. Und was zum Aktualisieren bestimmter Elemente benötigt wird, wird auch zum Entfernen bestimmter Elemente benötigt.

Tolle Diskussion. Die Recherche von @ebickle ist IMO äußerst nützlich!
@ebicle hast du Schlussfolgerungen zu [2] lucene.net - passt unser neuestes Angebot zu den Verwendungszwecken oder nicht? (Ich hoffe, ich habe es in deiner ausführlichen Beschreibung nicht übersehen)

Es sieht so aus, als ob wir Try* Varianten von oben + IsEmpty + TryPeek / TryDequeue + EnqueueOrUpdate brauchen? Die Gedanken?
```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

Es sieht so aus, als ob wir Try*-Varianten von oben brauchen

Ja genau.

Soll der bool-Status für in die Warteschlange eingereiht vs. aktualisiert zurückgegeben werden?

Wenn wir überall Status zurückgeben wollen, dann überall. Für diese spezielle Methode sollte dies meiner Meinung nach zutreffen, wenn eine der beiden Operationen erfolgreich war ( Enqueue oder Update ).

Im übrigen stimme ich einfach zu :smile:

Nur eine Frage - warum ref statt out ? Ich bin mir nicht sicher:

  • Wir müssen es nicht initialisieren, bevor wir die Funktion eingeben.
  • Es wird nicht für die Methode verwendet (es erlischt einfach).

Wenn wir überall Status zurückgeben wollen, dann überall. Für diese spezielle Methode sollte dies meiner Meinung nach zutreffen, wenn eine der beiden Operationen erfolgreich war ( Enqueue oder Update ).

Es würde immer true zurückgeben, das ist keine gute Idee. Wir sollten in diesem Fall IMO void . Andernfalls werden die Leute verwirrt sein und den Rückgabewert überprüfen und nutzlosen, niemals auszuführenden Code hinzufügen. (Es sei denn, ich habe etwas verpasst)

ref vs. out vereinbart, ich habe es selbst debattiert. Ich habe keine starke Meinung / genug Erfahrung, um selbst eine Entscheidung zu treffen. Wir können API-Rezensenten fragen / auf weitere Kommentare warten.

Es würde immer wahr zurückgeben

Du hast recht, mein Böser. Es tut uns leid.

Ich habe keine starke Meinung / genug Erfahrung, um selbst eine Entscheidung zu treffen.

Vielleicht übersehe ich etwas, aber ich finde es ziemlich einfach. Wenn wir ref , sagen wir im Grunde, dass Peek und Dequeue irgendwie die an sie übergebenen TElement und TPriority wollen ( Ich meine, lesen Sie diese Felder). Was nicht wirklich der Fall ist -- unsere Methoden sollen nur diesen Variablen Werte zuweisen (und sie werden vom Compiler tatsächlich dazu aufgefordert).

Top-Beitrag aktualisiert mit meinen Try* APIs
2 offene Fragen hinzugefügt:

  • [6] Ist Peek und Dequeue werfen überhaupt sinnvoll?
  • [7] TryPeek und TryDequeue - sollten ref oder out Argumente verwendet werden?

ref vs. out - Sie haben Recht. Ich habe optimiert, um die Initialisierung zu vermeiden, falls wir false zurückgeben. Das war dumm & blindlings von mir - verfrühte Optimierung. Ich werde es in out ändern und die Frage entfernen.

6: Ich könnte etwas übersehen, aber was soll Peek oder Dequeue tun, wenn die Warteschlange leer ist, wenn keine Ausnahme ausgelöst wird? Ich nehme an, das wirft die Frage auf, ob die Datenstruktur null akzeptieren sollte (ich würde es vorziehen, aber keine feste Meinung). Selbst wenn wir null zulassen, haben Werttypen kein null ( default sicherlich nicht), also haben Peek und Dequeue keine Möglichkeit, ein bedeutungsloses Ergebnis zu übermitteln, und ich denke, ordnungsgemäß muss eine Ausnahme ausgelöst werden (wodurch alle Bedenken bezüglich out Parametern beseitigt werden!). Ich sehe keinen Grund, dem Beispiel der bestehenden Queue.Dequeue nicht zu folgen

Ich füge nur hinzu, dass Dijkstras (alias heuristische Suche ohne Heuristik) keine Prioritätsänderungen erfordern sollte. Ich wollte nie die Priorität von irgendetwas aktualisieren und schlage die ganze Zeit heuristische Suchen durch. (Der Punkt des Algorithmus ist, dass Sie, sobald Sie einen Zustand erkundet haben, wissen, dass Sie den besten Weg dorthin erkundet haben verringern möchten (dh einen schlechteren Weg dorthin in Betracht ziehen). eine Priorität aktualisieren_)

Wenn keine Ausnahme ausgelöst wird, was sollte Peek oder Dequeue tun, wenn die Warteschlange leer ist?

Wahr.

So beseitigen Sie alle Bedenken bezüglich unserer Parameter!

Wie? Nun, die Parameter out sind für die Methoden TryPeek und TryDequeue . Diejenigen, die eine Ausnahme auslösen, sind Peek und Dequeue .

Ich füge nur hinzu, dass Dijkstras keine Prioritätsänderungen erfordern sollten.

Ich könnte mich irren, aber meines Wissens verwendet der Algorithmus von Dijkstra die Operation DecreaseKey . Siehe zum Beispiel dies . Ob dies effizient ist oder nicht, ist ein anderer Aspekt. Tatsächlich wurde Fibonacci-Heap so entworfen, dass die Operation DecreaseKey in O(1) asymptotisch ausgeführt wird (um Dijkstra zu verbessern).

Aber was in unserer Diskussion wichtig ist – die Möglichkeit, ein Element in einer Prioritätswarteschlange zu aktualisieren, ist sehr hilfreich und es gibt Leute, die nach einer solchen Funktionalität suchen (siehe zuvor verlinkte Fragen zu StackOverflow). Ich habe es selbst auch schon ein paar Mal benutzt.

Entschuldigung, ja, ich sehe das Problem mit out Parametern jetzt, die Dinge wieder falsch zu lesen. Und es scheint, dass eine Variante von Dijkstras (ein Name, der etwas breiter angelegt zu sein scheint, als ich dachte ...) vielleicht effizienter implementiert werden kann (wenn Ihre Warteschlange bereits die Elemente enthält, gibt es vermutlich Vorteile für wiederholbare Suchen) mit aktualisierbare Prioritäten. Das bekomme ich, wenn ich lange Wörter und Namen verwende. Beachten Sie, dass ich vorschlage wir nicht fallen Update , es wäre schön , gerade auch eine schlankere zu haben Heap oder andere (dh der ursprüngliche Vorschlag) ohne die Einschränkungen dieser Fähigkeit auferlegt (wie herrlich eine Fähigkeit, wie ich sie schätze).

7: Diese sollten out . Jede Eingabe könnte niemals eine Bedeutung haben, sollte also nicht existieren (dh wir sollten nicht mit ref ). Mit out Parametern werden wir die Rückgabe von default Werten nicht verbessern. Dies ist, was Dictionary.TryGetValue tut, und ich sehe keinen Grund, etwas anderes zu tun. _Das heißt, Sie könnten ref als Wert oder Standardwert behandeln, aber wenn Sie keinen sinnvollen Standardwert haben, ist das frustrierend._

Diskussion zur API-Überprüfung:

  • Wir werden ein richtiges Design-Meeting (2h) brauchen, um alle Vor- und Nachteile zu besprechen. Die 30 Minuten, die wir heute investiert haben, reichten nicht aus, um einen Konsens / Abschluss bei offenen Fragen zu erzielen.

    • Wir werden wahrscheinlich die am meisten investierten Community-Mitglieder einladen - @pgolebiowski , noch jemand?

Hier sind die wichtigsten Rohnotizen:

  • Experimentieren - wir sollten experimentieren (in CoreFxLabs), es als Vorab-NuGet-Paket veröffentlichen und die Verbraucher um Feedback bitten (über Blog-Post). Wir glauben nicht, dass wir die API ohne Feedback-Schleife auf den Punkt bringen können.

    • Ähnliches haben wir in der Vergangenheit für ImmutableCollections (ein schneller Vorschau-Release-Zyklus war der Schlüssel und hilfreich bei der Gestaltung der APIs).

    • Wir können dieses Experiment zusammen mit MultiValueDictionary bündeln, das bereits in CoreFxLab vorhanden ist. TODO: Überprüfen Sie, ob wir mehr Kandidaten haben, wir möchten nicht jeden von ihnen separat bloggen.

    • Das experimentelle NuGet-Paket wird nach Ende des Experiments nuklearisiert und wir werden den Quellcode in CoreFX oder CoreFXExtensions verschieben (wird später entschieden).

  • Idee: Geben Sie nur TElement von Peek & Dequeue , geben Sie nicht TPriority (Benutzer können dafür Try* Methoden verwenden).
  • Stabilität (für gleiche Prioritäten) - Elemente mit gleichen Prioritäten sollten in der Reihenfolge zurückgegeben werden, in der sie in die Warteschlange eingefügt wurden (allgemeines Verhalten der Warteschlange)
  • Wir möchten Duplikate aktivieren (ähnlich wie bei anderen Sammlungen):

    • Update loswerden - der Benutzer kann Remove und dann Enqueue Artikel mit unterschiedlicher Priorität zurückgeben.

    • Remove sollte nur das erste gefundene Element entfernen (wie es List tut).

  • Idee: Können wir die Schnittstelle IQueue zu abstrakten Queue und PriorityQueue abstrahieren ( Peek und Dequeue nur TElement ? Hier)

    • Hinweis: Es kann sich als unmöglich erweisen, aber wir sollten es untersuchen, bevor wir uns auf die API festlegen

Wir hatten lange Diskussionen über den Selektor - wir sind immer noch nicht entschieden (kann von IQueue oben verlangt werden?). Die Mehrheit mochte den Selektor nicht, aber die Dinge können sich in einem zukünftigen API-Review-Meeting ändern.

Andere offene Fragen wurden nicht diskutiert.

Der neueste Vorschlag sieht für mich ziemlich gut aus. Meine Meinung/Fragen:

  1. Wenn Peek und Dequeue out Parameter für priority , dann könnten sie auch Überladungen haben, die überhaupt keine Priorität zurückgeben, was meiner Meinung nach vereinfachen würde gemeinsame Nutzung. Die Priorität kann jedoch auch mit einem Verwerfen ignoriert werden, was dies weniger wichtig macht.
  2. Ich mag es nicht, dass die Selektorversion durch die Verwendung eines anderen Konstruktors unterschieden wird und einen anderen Satz von Methoden aktiviert. Vielleicht sollte es ein separates PriorityQueue<T> ? Oder eine Reihe von statischen und Erweiterungsmethoden, die mit PriorityQueue<T, T> ?
  3. Ist definiert, in welcher Reihenfolge die Elemente beim Aufzählen zurückgegeben werden? Ich vermute, dass es nicht definiert ist, um es effizient zu machen.
  4. Sollte es eine Möglichkeit geben, nur die Elemente in der Prioritätswarteschlange aufzuzählen und Prioritäten zu ignorieren? Oder ist es akzeptabel, etwas wie priorityQueue.Select(t => t.element) ?
  5. Wenn die Prioritätswarteschlange intern ein Dictionary für den Elementtyp verwendet, sollte es dann eine Option geben, ein IEqualityComparer<TElement> ?
  6. Das Verfolgen der Elemente mit einem Dictionary ist unnötiger Overhead, wenn ich nie Prioritäten aktualisieren muss. Sollte es eine Option zum Deaktivieren geben? Dies kann jedoch später hinzugefügt werden, wenn sich herausstellt, dass es nützlich ist.

@karelz

Stabilität (für gleiche Prioritäten) - Elemente mit gleichen Prioritäten sollten in der Reihenfolge zurückgegeben werden, in der sie in die Warteschlange eingefügt wurden (allgemeines Verhalten der Warteschlange)

Ich denke, das würde bedeuten, dass intern die Priorität so etwas wie ein Paar von (priority, version) müsste, wobei version für jede Hinzufügung erhöht wird. Ich denke, dies würde die Speichernutzung der Prioritätswarteschlange erheblich aufblähen, insbesondere wenn man bedenkt, dass die version wahrscheinlich 64-Bit sein müssten. Ich bin mir nicht sicher, ob sich das lohnen würde.

Wir möchten doppelte Werte nicht verhindern (ähnlich wie bei anderen Sammlungen):
Update loswerden - der Benutzer kann Remove und dann Enqueue Artikel mit unterschiedlicher Priorität zurückgeben.

Abhängig von der Implementierung ist Update wahrscheinlich viel effizienter als Remove gefolgt von Enqueue . Bei binärem Heap (ich denke, quaternärer Heap hat die gleiche Zeitkomplexität) ist beispielsweise Update (mit eindeutigen Werten und einem Wörterbuch) O(log n ), während Remove (mit doppelten Werten) und kein Wörterbuch) ist O( n ).

@pgolebiowski
Stimme voll und ganz zu, dass wir uns hier auf Vorschläge konzentrieren müssen; Ich habe den ursprünglichen API-Vorschlag und den ursprünglichen Beitrag gemacht. Bereits im Januar fragte @karelz nach einigen konkreten Anwendungsbeispielen; Dies hat eine viel umfassendere Frage bezüglich des spezifischen Bedarfs und der Verwendung einer PriorityQueue-API aufgeworfen. Ich hatte meine eigene PriorityQueue-Implementierung und habe sie in einigen Projekten verwendet und war der Meinung, dass etwas Ähnliches in der BCL nützlich wäre.

Was mir im ursprünglichen Beitrag gefehlt hat, war eine umfassende Übersicht über die bestehende Warteschlangennutzung; Beispiele aus der Praxis helfen dabei, das Design auf dem Boden zu halten und sicherzustellen, dass es breit verwendet werden kann.

@karelz
Lucene.net enthält mindestens eine Prioritätswarteschlangen-Vergleichsfunktion (ähnlich wie Vergleich), das mehrere Felder auswertet, um die Priorität zu bestimmen. Das "implizite" Vergleichsmuster von Lucene passt nicht gut in den expliziten TPriority-Parameter des aktuellen API-Vorschlags. Eine Art von Zuordnung wäre erforderlich - das Kombinieren der mehreren Felder in einem oder in einer "vergleichbaren" Datenstruktur, die als TPriority übergeben werden kann.

Vorschlag:
1) PrioritätswarteschlangeKlasse basierend auf meinem ursprünglichen Vorschlag oben (aufgeführt unter der Überschrift Ursprünglicher Vorschlag). Fügen Sie möglicherweise die Komfortfunktionen Aktualisieren (T), Entfernen (T) und Enthält (T) hinzu. Passt zu den meisten bestehenden Anwendungsbeispielen aus der Open-Source-Community.
2) PrioritätswarteschlangeVariante von dotnet/corefx#1. Dequeue() und Peek() geben TElement anstelle von Tupel zurück. Keine Try*-Funktionen, Remove() gibt bool zurück, anstatt zu werfen, um die Liste zu passenMuster. Fungiert als „Annehmlichkeitstyp“, sodass Entwickler, die einen expliziten Prioritätswert haben, keinen eigenen vergleichbaren Typ erstellen müssen.

Beide Typen unterstützen doppelte Elemente. Es muss festgestellt werden, ob wir FIFO für Elemente derselben Priorität garantieren oder nicht; wahrscheinlich nicht, wenn wir Priority-Updates unterstützen.

Erstellen Sie beide Varianten in den CoreFxLabs wie von @karelz vorgeschlagen und holen

Fragen:

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)

Warum keine Duplikate?

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

Wenn diese Methoden wünschenswert sind, haben Sie sie als Erweiterungsmethoden?

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

Sollte werfen; aber warum keine Variante ausprobieren, auch Erweiterungsmethoden?

Strukturbasierter Enumerator?

@benaadams Wurf auf Dupes wurde ursprünglich (von mir) vorgeschlagen, um den hässlichen

Wir sollten keine Methoden als Erweiterungsmethoden hinzufügen, wenn wir sie dem Typ hinzufügen können. Erweiterungsmethoden sind Backup, wenn wir sie nicht zum Typ hinzufügen können (z. B. ist es eine Schnittstelle oder wir möchten sie schneller an .NET Framework liefern).

Dupes: Wenn Sie mehrere identische Einträge haben, müssen Sie nur einen aktualisieren? Dann werden sie anders?

Wurfmethoden: sind im Grunde Wrapper und werden sie es sein?

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

Obwohl der Kontrollfluss über Ausnahmen erfolgt?

@benaadams überprüfe meine Antwort mit API-Überprüfungsnotizen: https://github.com/dotnet/corefx/issues/574#issuecomment -308206064

  • Wir möchten Duplikate aktivieren (ähnlich wie bei anderen Sammlungen):

    • Update loswerden - der Benutzer kann Remove und dann Enqueue Artikel mit unterschiedlicher Priorität zurückgeben.

    • Remove sollte nur das erste gefundene Element entfernen (wie es List tut).

Obwohl der Kontrollfluss über Ausnahmen erfolgt?

Nicht sicher was du meinst. Es scheint ein ziemlich häufiges Muster in BCL zu sein.

Obwohl der Kontrollfluss über Ausnahmen erfolgt?

Nicht sicher was du meinst.

Wenn Sie nur TryX-Methoden haben

  • wenn Ihnen das Ergebnis wichtig ist; du überprüfst es
  • Wenn dir das Ergebnis egal ist, überprüfe es nicht

Keine Ausnahmebeteiligung; aber der Name ermutigt Sie, die Rücksendung wegzuwerfen.

Wenn Sie Methoden zum Auslösen von Ausnahmen verwenden, die keine Try-Auslöser sind

  • wenn Ihnen das Ergebnis wichtig ist; Sie müssen entweder eine Vorabprüfung durchführen oder ein Try/Catch verwenden, um es zu erkennen
  • wenn Ihnen das Ergebnis egal ist; Sie müssen entweder die Methode catch leeren oder unerwartete Ausnahmen erhalten

Sie befinden sich also im Anti-Muster , die Ausnahmebehandlung für die Flusssteuerung zu verwenden . Sie wirken nur ein bisschen überflüssig; kann immer nur einen Wurf hinzufügen, wenn false, um neu zu erstellen.

Es scheint ein ziemlich häufiges Muster in BCL zu sein.

Die TryX-Methoden waren in der ursprünglichen BCL kein übliches Muster; bis zu den Concurrent-Typen (obwohl Dictionary.TryGetValue in 2.0 vielleicht das erste Beispiel war?)

zB wurden die TryX-Methoden gerade erst zu Core für Queue und Stack hinzugefügt und sind noch nicht Teil des Frameworks.

Stabilität

  • Stabilität gibt es nicht umsonst. Dies ist mit einem Mehraufwand an Leistung und Speicher verbunden. Für eine normale Verwendung ist die Stabilität in Prioritätswarteschlangen nicht wichtig.
  • Wir legen ein IComparer . Wenn der Kunde Stabilität haben möchte, kann er diese einfach selbst hinzufügen. Es wäre einfach, StablePriorityQueue auf unserer Implementierung aufzubauen – mit dem üblichen Ansatz, das "Alter" eines Elements zu speichern und es bei Vergleichen zu verwenden.

IQueue

Dann gibt es API-Konflikte. Betrachten wir nun ein einfaches IQueue<T> damit es mit dem vorhandenen Queue<T> funktioniert:

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

Wir haben zwei Optionen für die Prioritätswarteschlange:

  1. Implementieren Sie IQueue<TElement> .
  2. Implementieren Sie IQueue<(TElement, TPriority)> .

Ich werde Implikationen beider Pfade schreiben, indem ich einfach die Zahlen (1) und (2) verwende.

Einreihen

In die Prioritätswarteschlange müssen wir sowohl ein Element als auch seine Priorität (aktuelle Lösung) einreihen. Im normalen Heap fügen wir nur ein Element hinzu.

  1. Im ersten Fall machen wir Enqueue(TElement) , was für eine Prioritätswarteschlange ziemlich seltsam ist, wenn wir ein Element ohne Priorität einfügen. Wir sind dann gezwungen zu tun... was? Angenommen default(TPriority) ? Nö...
  2. Im zweiten Fall machen wir Enqueue((TElement, TPriority) element) . Wir fügen im Wesentlichen zwei Argumente hinzu, indem wir ein daraus erstelltes Tupel akzeptieren. Wahrscheinlich keine ideale API.

Einsehen und aus der Warteschlange

Sie wollten, dass diese Methoden nur TElement .

  1. Funktioniert mit dem, was Sie erreichen möchten.
  2. Funktioniert nicht mit dem, was Sie erreichen möchten — gibt (TElement, TPriority) .

Aufzählen

  1. Der Kunde kann keine LINQ-Abfrage schreiben, die die Prioritäten der Elemente nutzt. Das ist falsch.
  2. Es klappt.

Wir könnten einige der Probleme mildern, indem wir PriorityQueue<T> bereitstellen,

  • Der Benutzer ist im Wesentlichen gezwungen, eine Wrapper-Klasse für seinen Code zu schreiben, um unsere Klasse verwenden zu können. Dies bedeutet, dass sie in vielen Fällen auch IComparable implementieren möchten. Das fügt viel ziemlich Boilerplate-Code hinzu (und höchstwahrscheinlich eine neue Datei in ihrem Quellcode).
  • Wir können dies natürlich noch einmal besprechen, wenn Sie möchten. Im Folgenden biete ich auch einen alternativen Ansatz an.

Zwei Prioritätswarteschlangen

Wenn wir zwei Prioritätswarteschlangen bereitstellen, wäre die Gesamtlösung leistungsfähiger und flexibler. Es sind einige Hinweise zu beachten:

  • Wir stellen zwei Klassen zur Verfügung, um die gleiche Funktionalität bereitzustellen, jedoch nur für verschiedene Arten von Eingabeformaten. Fühlt sich nicht gut an.
  • PriorityQueue<T> könnte möglicherweise IQueue<T> implementieren.
  • PriorityQueue<TElement, TPriority> würde eine seltsame API verfügbar machen, wenn sie versucht, die IQueue Schnittstelle zu implementieren.

Nun... es würde funktionieren . Allerdings nicht optimal.

Aktualisieren und entfernen

Unter der Annahme, dass wir Duplikate zulassen und das Konzept eines Handles nicht verwenden möchten:

  • Es ist unmöglich, die Funktionalität zum Aktualisieren der Prioritäten von Elementen vollständig korrekt bereitzustellen (nur den spezifischen Knoten aktualisieren). Analog gilt es für das Entfernen von Elementen.
  • Außerdem müssen beide Operationen in O(n) ausgeführt werden, was ziemlich traurig ist, da beides in O(log n) möglich ist.

Dies führt im Grunde zu dem, was in Java ist, wo Benutzer eine eigene Implementierung der gesamten Datenstruktur bereitstellen müssen, wenn sie Prioritäten in ihren Prioritätswarteschlangen aktualisieren möchten, ohne jedes Mal naiv durch die gesamte Sammlung zu iterieren.

Alternativer Griff

Unter der Annahme, dass wir keinen neuen Handle-Typ hinzufügen möchten, können wir es anders machen, um die volle Unterstützung für das Aktualisieren und Entfernen von Elementen zu haben (und dies effizient zu tun). Die Lösung besteht darin, alternative Methoden hinzuzufügen:

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

void Update(object handle, TPriority priority);

void Remove(object handle);

Dies wären Methoden für diejenigen, die eine angemessene Kontrolle über das Aktualisieren und Entfernen von Elementen haben möchten (und dies nicht in O(n) tun, als wäre es ein List :stuck_out_tongue_winking_eye:).

Aber besser, überlegen wir...

Alternativer Ansatz

Alternativ können wir diese Funktionalität vollständig aus der Prioritätswarteschlange entfernen und stattdessen im Heap hinzufügen. Dies hat zahlreiche Vorteile:

  • Die leistungsfähigeren und effizienteren Operationen sind mit einem Heap verfügbar.
  • Eine einfache API und unkomplizierte Verwendung ist mit einem PriorityQueue verfügbar - für Leute, die möchten, dass ihr Code einfach funktioniert .
  • Wir stoßen nicht auf die Probleme von Java.
  • Wir könnten die PriorityQueue jetzt stabilisieren – das ist kein Kompromiss mehr.
  • Die Lösung steht im Einklang mit dem Gefühl, dass Menschen mit einem stärkeren Informatikhintergrund die Existenz von Heap . Sie wären sich auch der Einschränkungen von PriorityQueue bewusst und könnten daher stattdessen die Heap verwenden (z. B. wenn sie mehr Kontrolle über das Aktualisieren/Entfernen von Elementen haben möchten oder die Daten nicht möchten Stabilität auf Kosten von Geschwindigkeit und Speicherverbrauch).
  • Sowohl die Prioritätswarteschlange als auch der Heap können leicht Duplikate zulassen, ohne ihre Funktionalität zu beeinträchtigen (weil ihre Zwecke unterschiedlich sind).
  • Es wäre einfacher, eine gemeinsame IQueue -Schnittstelle zu erstellen - da die Leistung und Funktionalität in das Heap-Feld geworfen würde. Die API von PriorityQueue könnte darauf ausgerichtet sein, sie durch eine Abstraktion mit Queue kompatibel zu machen.
  • Wir müssen die IPriorityQueue Schnittstelle nicht mehr bereitstellen (wir konzentrieren uns vielmehr darauf, die Funktionalität von PriorityQueue und Queue ähnlich zu halten). Stattdessen können wir es im Heap-Bereich hinzufügen – und haben dort im Wesentlichen IHeap . Das ist großartig, weil es den Leuten ermöglicht, Bibliotheken von Drittanbietern auf den Standardbibliotheken aufzubauen. Und es fühlt sich richtig an – denn auch hier halten wir Heaps für fortschrittlicher als Priority-Warteschlangen , daher würden Erweiterungen von diesem Bereich bereitgestellt. Solche Erweiterungen würden auch nicht unter den Optionen leiden, die wir in PriorityQueue treffen würden, da sie separat wären.
  • Wir müssen den IHeap Konstruktor für PriorityQueue nicht mehr berücksichtigen.
  • Die Prioritätswarteschlange wäre eine hilfreiche Klasse für die interne Verwendung in CoreFX. Wenn wir jedoch Funktionen wie Stabilität hinzufügen und einige andere Funktionalitäten weglassen, werden wir wahrscheinlich etwas Mächtigeres als diese Lösung benötigen. Glücklicherweise gibt es die leistungsstärkeren und leistungsfähigeren Heap uns zur Verfügung stehen!

Grundsätzlich würde sich die Prioritätswarteschlange hauptsächlich auf Benutzerfreundlichkeit konzentrieren, zu Lasten von: Leistung, Leistung und Flexibilität. Der Heap wäre für diejenigen gedacht, die sich der Leistung und funktionalen Auswirkungen unserer Entscheidungen in der Prioritätswarteschlange bewusst sind. Wir entschärfen viele Probleme mit Kompromissen.

Wenn wir ein Experiment machen wollen, denke ich, dass es jetzt möglich ist. Fragen wir einfach die Community. Nicht jeder liest diesen Thread – wir könnten andere wertvolle Kommentare und Nutzungsszenarien generieren. Was denken Sie? Ich persönlich würde mich über eine solche Lösung freuen. Ich denke, es würde alle glücklich machen.

Wichtiger Hinweis: Wenn wir einen solchen Ansatz wollen, müssen wir Priority Queue und Heap zusammen entwerfen. Denn ihre Zwecke wären unterschiedlich und eine Lösung würde das bieten, was die andere nicht bieten würde.

IQueue dann ausliefern

Mit dem oben vorgestellten Ansatz, damit die Prioritätswarteschlange IQueue<T> implementiert (so dass es Sinn macht) und angenommen, dass wir die Unterstützung für den Selektor dort entfernen, müsste sie einen generischen Typ haben. Obwohl dies bedeuten würde, dass Benutzer einen Wrapper bereitstellen müssen, wenn sie (user data, priority) separat haben, ist eine solche Lösung immer noch intuitiv. Und vor allem erlaubt es alle Eingabeformate (deshalb muss es so gemacht werden, wenn wir den Selektor weglassen). Ohne den Selektor würde Enqueue(TElement, TPriority) keine Typen zulassen, die bereits vergleichbar sind. Ein einziger generischer Typ ist auch für die Aufzählung entscheidend – damit diese Methode in IQueue<T> .

Sonstig

@svick

Ist definiert, in welcher Reihenfolge die Elemente beim Aufzählen zurückgegeben werden? Ich vermute, dass es nicht definiert ist, um es effizient zu machen.

Um die Reihenfolge beim Aufzählen zu haben, müssen wir im Wesentlichen die Sammlung sortieren. Deshalb ja, es sollte undefiniert sein, da der Kunde OrderBy einfach selbst ausführen und ungefähr die gleiche Leistung erzielen kann (aber manche Leute brauchen es nicht).

Idee: In der Priority Queue könnte dies geordnet sein, im Heap ungeordnet. Es fühlt sich besser an. Irgendwie fühlt sich eine Prioritätswarteschlange so an, als würde sie der Reihe nach durchlaufen. Ein Haufen definitiv nicht. Ein weiterer Vorteil des obigen Ansatzes.

@pgolebiowski
Das scheint alles sehr vernünftig zu sein. Würde es Ihnen etwas ausmachen, im Abschnitt Delivering IQueue then klären, schlagen Sie T : IComparable<T> für den "einen generischen Typ" als Alternative zum Selektor vor, da doppelte Elemente zulässig sind?

Ich würde es unterstützen, die beiden getrennten Typen zu haben.

Ich verstehe den Grund für die Verwendung von object als Handle-Typ nicht: Ist dies nur, um zu vermeiden, dass ein neuer Typ erstellt wird? Das Definieren eines neuen Typs würde einen minimalen Implementierungsaufwand bedeuten und gleichzeitig den Missbrauch der API erschweren (was hindert mich daran, string an Remove(object) weiterzugeben?) und einfacher zu verwenden (was hindert mich daran, es zu versuchen?) um das Element selbst an Remove(object) , und wer könnte es mir verübeln, dass ich es versucht habe?).

Ich schlage vor, einen passend benannten Dummy-Typ hinzuzufügen, um object in den Handle-Methoden zu ersetzen, im Interesse einer ausdrucksvolleren Schnittstelle.

Wenn die Fehlersuchbarkeit den Speicher-Overhead übertrumpft, könnte der Handle-Typ sogar Informationen darüber enthalten, zu welcher Warteschlange er gehört (muss generisch werden, was die Typsicherheit der Schnittstelle fördern würde) und nützliche Ausnahmen wie ("Handle bereitgestellt wurde erstellt durch eine andere Warteschlange") oder ob es bereits verbraucht wurde ("Element, auf das von Handle verwiesen wird, wurde bereits entfernt").

Wenn die Handle-Idee weitergeht, würde ich vorschlagen, dass, wenn diese Informationen als nützlich erachtet werden, eine Teilmenge dieser Ausnahmen auch von den entsprechenden Methoden TryRemove und TryUpdate ausgelöst wird, außer dass, wo die -Element ist nicht mehr vorhanden, weil es entweder aus der Warteschlange entfernt oder per Handle entfernt wurde. Dies würde einen weniger langweiligen, generischen Grifftyp mit geeignetem Namen zur Folge haben.

@VisualMelon

Würde es Ihnen etwas ausmachen, im Abschnitt Delivering IQueue then klären, schlagen Sie T : IComparable<T> für den "einen generischen Typ" als Alternative zum Selektor vor, da doppelte Elemente zulässig sind?

Entschuldigung, dass ich das nicht deutlich gemacht habe.

  • Ich meinte, PriorityQueue<T> liefern, ohne Beschränkung auf die T .
  • Es würde immer noch ein IComparer<T> .
  • Wenn T bereits vergleichbar ist, wird einfach Comparer<T>.Default angenommen (und Sie können den Standardkonstruktor der Prioritätswarteschlange ohne Argumente aufrufen).
  • Der Selektor hatte einen anderen Zweck – alle Arten von Benutzerdaten verarbeiten zu können. Es gibt mehrere Konfigurationen:

    1. Benutzerdaten sind von der Priorität getrennt (zwei physische Instanzen).
    2. Benutzerdaten enthalten die Priorität.
    3. Benutzerdaten ist die Priorität.
    4. Seltener Fall: Priorität ist über eine andere Logik erreichbar (liegt in einem anderen Objekt als den Benutzerdaten).

    Der seltene Fall wäre in PriorityQueue<T> nicht möglich, aber das macht nicht viel aus. Wichtig ist, dass wir jetzt mit (1), (2) und (3) umgehen können. Wenn wir jedoch zwei generische Typen hätten, müssten wir eine Methode wie Enqueue(TElement, TPrioriity) . Das würde uns nur auf (1) beschränken. (2) würde zu Redundanz führen. (3) wäre unglaublich hässlich. Im Abschnitt IQueue > Enqueue oben (zweite Enqueue Methode und default(TPriority ).

Ich hoffe es ist jetzt klarer.

Übrigens, unter der Annahme einer solchen Lösung wäre das Entwerfen der API von PriorityQueue<T> und IQueue<T> trivial. Nehmen Sie einfach einige der Methoden in Queue<T> , werfen Sie sie in IQueue<T> und lassen Sie PriorityQueue<T> implementieren. Tadaa! 😄

Ich verstehe den Grund für die Verwendung von object als Handle-Typ nicht: Ist dies nur, um zu vermeiden, dass ein neuer Typ erstellt wird?

  • Ja, genau. Unter der Annahme, dass wir einen solchen Typ nicht offenlegen möchten, ist dies die einzige Lösung, das Konzept eines Handles noch zu verwenden (und als solches mehr Leistung und Geschwindigkeit zu haben). Ich bin damit einverstanden , es ist nicht ideal , obwohl - es war vielmehr eine Präzisierung , was würde passieren , wenn wir auf den aktuellen Ansatz bleiben und wollen mehr Leistung und Effizienz haben (was ich bin gegen).
  • Da wir den alternativen Ansatz wählen würden (separate Prioritätswarteschlange und Haufen), ist dies einfacher. Wir könnten PriorityQueue<T> in System.Collections.Generic , während sich die Heap-Funktionalität in System.Collections.Specialized . Dort hätten wir eine höhere Chance, einen solchen Typ einzuführen und schließlich die wunderbare Fehlererkennung während der Kompilierung zu haben.
  • Aber noch einmal -- es ist sehr wichtig, Prioritätswarteschlangen- und Heap-Funktionalität zusammen zu entwerfen, wenn wir einen solchen Ansatz haben möchten. Denn eine Lösung bietet, was die andere nicht bietet.

Wenn die Fehlersuchbarkeit den Speicher-Overhead übertrumpft, könnte der Handle-Typ sogar Informationen darüber enthalten, zu welcher Warteschlange er gehört (muss generisch werden, was die Typsicherheit der Schnittstelle fördern würde) und nützliche Ausnahmen wie ("Handle bereitgestellt wurde erstellt durch eine andere Warteschlange") oder ob es bereits verbraucht wurde ("Element, auf das von Handle verwiesen wird, wurde bereits entfernt").

Wenn die Idee mit dem Griff weitergeht, würde ich vorschlagen, dass, wenn diese Informationen als nützlich erachtet werden ...

Dann ist definitiv mehr möglich : zwinker:. Vor allem, um all das durch unsere Community in Bibliotheken von Drittanbietern zu erweitern.

@karelz @safern @ianhays @terrajobst @bendono @svick @alexey-dvortsov @SamuelEnglard @xied75 und andere -- denken Sie, dass ein solcher Ansatz sinnvoll wäre (wie in diesem und diesem Beitrag beschrieben)? Würde es alle Ihre Erwartungen und Bedürfnisse erfüllen?

Ich denke, die Idee, zwei Klassen zu erstellen, ist sehr sinnvoll und löst viele Probleme. Ich habe nur, dass es für PriorityQueue<T> sinnvoll sein könnte, das Heap<T> intern zu verwenden und seine erweiterten Funktionen einfach zu "verstecken".

Also im Grunde genommen...

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

Anmerkungen

  • In System.Collections.Generic .
  • Idee: Wir könnten hier auch Methoden zum Entfernen von Elementen hinzufügen ( Remove und TryRemove ). Es ist jedoch nicht in Queue<T> . Aber es ist nicht notwendig.

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

Anmerkungen

  • In System.Collections.Generic .
  • Wenn das IComparer<T> nicht geliefert wird, wird Comparer<T>.Default gerufen.
  • Es ist stabil.
  • Erlaubt Duplikate.
  • Remove und TryRemove entfernen nur das erste Vorkommen (wenn sie es finden).
  • Aufzählung ist geordnet.

Haufen

Jetzt nicht alles hier schreiben, aber:

  • In System.Collections.Specialized .
  • Es wäre nicht stabil (und als solches schneller und effizienter in Bezug auf den Speicher).
  • Handhabung des Supports, ordnungsgemäße Aktualisierung und Entfernung

    • schnell erledigt in O(log n) statt O(n)

    • richtig gemacht

  • Aufzählung ist nicht geordnet (schneller).
  • Erlaubt Duplikate.

Stimme der IQueue zuVorschlag. Ich wollte heute dasselbe vorschlagen, es fühlt sich an wie die richtige Abstraktionsebene auf Schnittstellenebene. "Eine Schnittstelle zu einer Datenstruktur, der Artikel hinzugefügt und bestellte Artikel daraus entfernt werden können."

  • Die Spezifikation für IQueuesieht gut aus.

  • Erwägen Sie das Hinzufügen von "int Count { get; }" zu IQueuedamit klar ist, dass Count gewünscht ist, unabhängig davon, ob wir von IReadOnlyCollection erben oder nicht.

  • Auf dem Zaun bezüglich TryPeek, TryDequeue in IQueuewenn man bedenkt, dass sie nicht in der Warteschlange sind, aber diese Helfer sollten wahrscheinlich auch zu Queue und Stack hinzugefügt werden.

  • IsEmpty scheint ein Ausreißer zu sein; nicht viele andere Sammlungstypen in der BCL haben es. Um es der Schnittstelle hinzuzufügen, müssen wir davon ausgehen, dass es der Warteschlange hinzugefügt wird, und es scheint ein bisschen seltsam, es zur Warteschlange hinzuzufügenund sonst nichts. Wir empfehlen, es aus der Schnittstelle zu entfernen und vielleicht auch aus der Klasse.

  • Lassen Sie TryRemove fallen und ändern Sie Remove in "bool Remove". Hier ist es wichtig, mit den anderen Sammlungsklassen in Einklang zu bleiben - Entwickler werden viel Muskelgedächtnis haben, das sagt, dass "remove() in einer Sammlung nicht ausgelöst wird". Dies ist ein Bereich, den viele Entwickler nicht gut testen werden und viele Überraschungen bereiten wird, wenn das normale Verhalten geändert wird.

Aus deinem früheren Zitat @pgolebiowski

  1. Benutzerdaten sind von der Priorität getrennt (zwei physische Instanzen).
  2. Benutzerdaten enthalten die Priorität.
  3. Benutzerdaten haben Priorität.
  4. Seltener Fall: Priorität ist über eine andere Logik erreichbar (liegt in einem anderen Objekt als den Benutzerdaten).

Empfehlen Sie auch 5. Benutzerdaten enthalten die Priorität in mehreren Feldern (wie wir in Lucene.net gesehen haben).

Auf dem Zaun bezüglich TryPeek, TryDequeue in IQueue, da sie nicht in der Warteschlange sind

Sie sind System/Collections/Generic/Queue.cs#L253-L295

Auf der anderen Seite hat Queue nicht
c# public void Remove(T element); public bool TryRemove(T element);

Ziehen Sie in Betracht, "int Count { get; }" zu IQueue hinzuzufügen, damit klar ist, dass Count gewünscht wird, unabhängig davon, ob wir von IReadOnlyCollection erben oder nicht.

OK. Werde es modifizieren.

IsEmpty scheint ein Ausreißer zu sein; nicht viele andere Sammlungstypen in der BCL haben es.

Dieser wurde von @karelz hinzugefügt, ich habe ihn gerade kopiert. Ich mag es aber, könnte im API-Review berücksichtigt werden :)

Lassen Sie TryRemove fallen und ändern Sie Remove in "bool Remove".

Ich denke, dass Remove und TryRemove mit anderen Methoden übereinstimmen ( Peek und TryPeek oder Dequeue und TryDequeue ).

  1. Benutzerdaten enthalten die Priorität in mehreren Feldern

Dies ist ein gültiger Punkt, aber dies kann tatsächlich auch mit einem Selektor behandelt werden (es ist schließlich eine beliebige Funktion) - aber das ist für Haufen.

IsEmpty scheint ein Ausreißer zu sein; nicht viele andere Sammlungstypen in der BCL haben es.

FWIW, bool IsEmpty { get; } ist etwas, von dem ich wünschte, wir hätten es zu IProducerConsumerCollection<T> hinzugefügt, und ich habe es seitdem mehrmals bereut, dass es nicht dort war. Ohne sie müssen Wrapper oft das Äquivalent von Count == 0 , was für einige Sammlungen deutlich weniger effizient zu implementieren ist, insbesondere für die meisten gleichzeitigen Sammlungen.

@pgolebiowski Was halten Sie von der Idee, ein Github-Repository zu erstellen, um die aktuellen API-Verträge und ein oder zwei .md-Dateien mit den Designgründen zu hosten? Sobald sich dies stabilisiert hat, kann es als Ort verwendet werden, um die anfängliche Implementierung aufzubauen, bevor eine PR bis zu CoreFxLabs durchgeführt wird, wenn sie fertig sind?

@svick

Wenn Peek und Dequeue out Parameter für die Priorität verwenden, dann könnten sie auch Überladungen haben, die überhaupt keine Priorität zurückgeben, was meiner Meinung nach die allgemeine Verwendung vereinfachen würde

Einverstanden. Fügen wir sie hinzu.

Sollte es eine Möglichkeit geben, nur die Elemente in der Prioritätswarteschlange aufzuzählen und Prioritäten zu ignorieren?

Gute Frage. Was würden Sie vorschlagen? IEnumerable<TElement> Elements { get; } ?

Stabilität - Ich denke, das würde bedeuten, dass intern Priorität wie ein Paar von (priority, version) müsste

Ich denke, wir können das vermeiden, indem wir Update als logisches Remove + Enqueue . Wir würden Items immer am Ende von Items gleicher Priorität hinzufügen (betrachte grundsätzlich das Vergleichsergebnis 0 als -1). IMO das sollte funktionieren.


@benaadams

Die TryX-Methoden waren in der ursprünglichen BCL kein übliches Muster; bis zu den Concurrent-Typen (obwohl Dictionary.TryGetValue in 2.0 vielleicht das erste Beispiel war?)
zB wurden die TryX-Methoden gerade erst zu Core für Queue und Stack hinzugefügt und sind noch nicht Teil des Frameworks.

Ich gebe zu, dass ich noch neu bei BCL bin. Von API-Review-Meetings und der Tatsache, dass wir kürzlich eine Reihe von Try* Methoden hinzugefügt haben, hatte ich den Eindruck, dass dies noch viel länger ein gängiges Muster ist 😉.
So oder so, es ist jetzt ein gängiges Muster und wir sollten keine Angst davor haben, es zu verwenden. Die Tatsache, dass das Muster noch nicht in .NET Framework vorhanden ist, sollte uns nicht daran hindern, in .NET Core zu innovieren – das ist sein Hauptzweck, schnellere Innovationen.


@pgolebiowski

Alternativ können wir diese Funktionalität vollständig aus der Prioritätswarteschlange entfernen und stattdessen dem Heap hinzufügen. Das hat viele Vorteile

Hmm, irgendwas sagt mir, dass du hier vielleicht eine Agenda hast 😆
Nun im Ernst, es ist eigentlich eine gute Richtung, die wir die ganze Zeit anstrebten - PriorityQueue sollte nie ein Grund sein, Heap NICHT zu tun. Wenn wir alle damit einverstanden sind, dass Heap möglicherweise nicht in CoreFX schafft und "nur" im CoreFXExtensions-Repository als erweiterte Datenstruktur neben PowerCollections bleibt, bin ich damit einverstanden.

Wichtiger Hinweis: Wenn wir einen solchen Ansatz wünschen, müssen wir Prioritätswarteschlange und Heap zusammen entwerfen. Denn ihre Zwecke wären unterschiedlich und eine Lösung würde das bieten, was die andere nicht bieten würde.

Ich verstehe nicht, warum wir das zusammen machen müssen. IMO können wir uns auf PriorityQueue und "richtige" Heap parallel/später hinzufügen. Es macht mir nichts aus, wenn jemand sie zusammen macht, aber ich sehe keinen triftigen Grund, warum die Existenz von einfach zu bedienenden PriorityQueue das Design von "richtigen/hochperfanten" erweiterten Heap Familie.

IQueue

Danke für das Aufschreiben. Angesichts Ihrer Punkte denke ich, dass wir nicht alles tun sollten, um IQueue hinzuzufügen. IMO ist es nice-to-have. Wenn wir einen Selektor hätten, wäre es natürlich. Ich mag den Selektor-Ansatz jedoch nicht, da er bei der Beschreibung, wann der Selektor von PriorityQueue aufgerufen wird (nur bei Enqueue und Update .

Alternativer (Objekt-)Handle

Das ist eigentlich keine schlechte Idee (wenn auch etwas hässlich), solche Überlastungen IMO zu haben. Wir müssten in der Lage sein zu erkennen, dass das Handle vom falschen PriorityQueue , also O(log n).
Ich habe das Gefühl, dass API-Rezensenten es ablehnen werden, aber IMO ist es einen Versuch wert, damit zu experimentieren ...

Stabilität

Ich glaube nicht, dass Stabilität mit Leistungs-/Speicher-Overhead einhergeht (vorausgesetzt, wir haben bereits Update oder wir behandeln Update als logische Remove + Enqueue , also setzen wir grundsätzlich das Alter des Elements zurück). Behandeln Sie das Vergleichsergebnis 0 einfach als -1 und alles ist gut ... Oder übersehe ich etwas?

Selektor und IQueue<T>

Es könnte eine gute Idee sein, 2 Vorschläge zu haben (und wir können möglicherweise beide annehmen):

  • PriorityQueue<T,U> ohne Selektor und ohne IQueue (was mühsam wäre)
  • PriorityQueue<T> mit Selektor und IQueue

Das haben nicht wenige Leute oben angedeutet.

Betreff : Neuester Vorschlag von https://github.com/dotnet/corefx/issues/574#issuecomment -308427321

IQueue<T> - wir könnten Remove und TryRemove hinzufügen, aber Queue<T> hat sie nicht.

Wäre es sinnvoll, sie zu Queue<T> hinzuzufügen? ( @ebckle stimmt zu) Wenn ja, sollten wir den Zusatz auch bündeln.
Fügen wir es in der Benutzeroberfläche hinzu und nennen sie fragwürdig / müssen auch Queue<T> hinzugefügt werden.
Dasselbe gilt für IsEmpty -- was auch immer Queue<T> und Stack<T> , markieren wir es so in der Benutzeroberfläche (es ist einfacher zu überprüfen und zu verdauen).
@pgolebiowski können Sie bitte einen Kommentar zu IQueue<T> mit einer Liste der Klassen hinzufügen, von denen wir glauben, dass sie implementiert werden?

PriorityQueue<T>

Lassen Sie uns den Struct-Enumerator hinzufügen (ich denke, es ist in letzter Zeit ein übliches BCL-Muster). Es wurde ein paar Mal im Thread aufgerufen und dann fallen gelassen/vergessen.

Haufen

Namespace-Wahl: Lassen Sie uns noch keine Zeit mit der Namespace-Entscheidung verschwenden. Wenn Heap in CoreFxExtensions landet, wissen wir noch nicht, welche Art von Namespaces wir dort zulassen. Vielleicht Community.* oder so ähnlich. Es hängt vom Ergebnis der Diskussionen über den Zweck/Betriebsmodus von CoreFxExtensions ab.
Hinweis: Eine Idee für das CoreFxExtensions-Repository besteht darin, bewährten Community-Mitgliedern Schreibberechtigungen zu erteilen und es hauptsächlich von der Community steuern zu lassen, wobei das .NET-Team nur Ratschläge gibt (einschließlich API-Review-Expertise) und das .NET-Team/MS bei Bedarf der Schiedsrichter ist. Wenn wir dort landen, möchten wir es wahrscheinlich nicht im Namensraum System.* oder Microsoft.* . (Haftungsausschluss: frühes Denken, bitte noch keine voreiligen Schlüsse ziehen, es gibt andere alternative Governance-Ideen im Flug)

Lassen Sie TryRemove und ändern Sie Remove in bool Remove . Hier ist es wichtig, mit den anderen Sammlungsklassen in Einklang zu bleiben - Entwickler werden viel Muskelgedächtnis haben, das sagt, dass "remove() in einer Sammlung nicht ausgelöst wird". Dies ist ein Bereich, den viele Entwickler nicht gut testen werden und viele Überraschungen bereiten wird, wenn das normale Verhalten geändert wird.

👍 Wir sollten auf jeden Fall zumindest überlegen, es mit anderen Kollektionen abzugleichen. Kann jemand andere Sammlungen scannen, was deren Remove Muster ist?

@ebickle Was halten Sie von der Idee, ein Github-Repository zu erstellen, um die aktuellen API-Verträge und ein oder zwei .md-Dateien mit den Designgründen zu hosten?

Lassen Sie es uns direkt in CoreFxLabs hosten . 👍

@karelz

Sollte es eine Möglichkeit geben, nur die Elemente in der Prioritätswarteschlange aufzuzählen und Prioritäten zu ignorieren?

Gute Frage. Was würden Sie vorschlagen? IEnumerable<TElement> Elements { get; } ?

Ja, entweder das oder eine Eigenschaft, die eine Art von ElementsCollection ist wahrscheinlich die beste Option. Zumal es ähnlich wie Dictionary<K, V>.Values .

Wir würden Items immer am Ende von Items gleicher Priorität hinzufügen (betrachte grundsätzlich das Vergleichsergebnis 0 als -1). IMO das sollte funktionieren.

So funktionieren Heaps nicht, es gibt kein "Ende der Elemente gleicher Priorität". Elemente mit derselben Priorität können über den gesamten Heap verteilt werden (es sei denn, sie entsprechen dem aktuellen Minimum).

Oder anders ausgedrückt, um die Stabilität zu gewährleisten, müssen Sie das Vergleichsergebnis von 0 nicht nur beim Einfügen als -1 betrachten, sondern auch, wenn Elemente danach im Heap verschoben werden. Und ich denke, Sie müssen etwas wie version zusammen mit den priority speichern, um das richtig zu machen.

IQueue- wir könnten Remove und TryRemove hinzufügen, aber Queuehat sie nicht.

Wäre es sinnvoll, sie zur Warteschlange hinzuzufügen?? ( @ebckle stimmt zu) Wenn ja, sollten wir den Zusatz auch bündeln.

Ich denke nicht, dass Remove zu IQueue<T> hinzugefügt werden sollte; und ich würde sogar vorschlagen, dass Contains zwielichtig ist; es schränkt die Nützlichkeit der Schnittstelle und die Arten von Warteschlangen ein, für die sie verwendet werden kann, es sei denn, Sie beginnen auch, NotSupportedExceptions auszulösen. dh was ist der umfang?

Ist es nur für Vanilla Queues, Message Queues, Distributed Queues, ServiceBus Queues, Azure Storage Queues, ServiceFabric Reliable​Queues etc...

So funktionieren Heaps nicht, es gibt kein "Ende der Elemente gleicher Priorität". Elemente mit derselben Priorität können über den gesamten Heap verteilt werden (es sei denn, sie entsprechen dem aktuellen Minimum).

Gutes Argument. Ich dachte daran als binären Suchbaum. Ich muss meine DS-Grundlagen putzen, denke ich :)
Nun, entweder implementieren wir es mit verschiedenen ds (Array, binärer Suchbaum (oder wie auch immer der offizielle Name ist) - @stephentoub hat Ideen/Vorschläge) oder wir geben uns mit einer zufälligen Reihenfolge version oder age aufrechtzuerhalten.

@ebickle Was halten Sie von der Idee, ein Github-Repository zu erstellen, um die aktuellen API-Verträge und ein oder zwei .md-Dateien mit den Designgründen zu hosten?

@karelz Lassen Sie es uns direkt in CoreFxLabs hosten.

Bitte schauen Sie sich dieses Dokument an .

Es könnte eine gute Idee sein, 2 Vorschläge zu haben (und wir können möglicherweise beide annehmen):

  • Prioritätswarteschlangeohne Selektor und ohne IQueue (was mühsam wäre)
  • Prioritätswarteschlangemit Selektor und IQueue

Den Selektor habe ich komplett abgeschafft. Es ist nur notwendig, wenn wir ein einheitliches PriorityQueue<T, U> . Wenn wir zwei Klassen haben, reicht ein Vergleich.


Bitte lassen Sie es mich wissen, wenn Sie etwas finden, das es wert ist, hinzugefügt / geändert / entfernt zu werden.

@pgolebiowski

Dokument sieht gut aus. Es fühlt sich an wie eine solide Lösung, die ich in den .NET-Kernbibliotheken erwarten würde :)

  • Sollte eine verschachtelte Enumerator-Klasse hinzufügen. Ich bin mir nicht sicher, ob sich die Situation geändert hat, aber vor Jahren verhinderte die untergeordnete Struktur eine Garbage Collector-Zuordnung, die durch das Boxen des Ergebnisses von GetEnumerator() verursacht wurde. Siehe zum Beispiel https://github.com/dotnet/coreclr/issues/1579 .
    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(); } }

  • Prioritätswarteschlangesollte auch eine verschachtelte Enumerator-Struktur haben.

  • Ich stimme immer noch für "public bool Remove(T element)" ohne TryRemove, da es sich um ein seit langem bestehendes Muster handelt und dessen Änderung sehr wahrscheinlich zu Entwicklerfehlern führt. Wir können die API-Review-Crew einstimmen lassen, aber das ist in meinen Augen eine offene Frage.

  • Gibt es einen Wert darin, die Anfangskapazität im Konstruktor anzugeben oder eine TrimExcess-Funktion zu verwenden, oder ist dies derzeit eine Mikrooptimierung - insbesondere angesichts der IEnumerableKonstruktor-Parameter?

@pgolebiowski danke für das @ianhays @safen können sie die PRs zusammenführen.
Wir können CoreFxLab-Probleme dann für weitere Diskussionen über bestimmte Designpunkte verwenden - es sollte einfacher sein als dieses Mega-Problem (ich habe gerade eine E-Mail gestartet, um dort eine area-PriorityQueue zu erstellen).

Bitte hier einen Link zum CoreFxLab-Pull-Request setzen?

  • @ebickle @jnm2 -- hinzugefügt
  • @karelz @SamuelEnglard -- referenziert

Wenn die API in ihrer aktuellen Form (zwei Klassen) genehmigt wird, würde ich einen weiteren PR für CoreFXLab mit einer Implementierung für beide erstellen, die einen quaternären Heap verwendet ( siehe Implementierung ). PriorityQueue<T> würde nur ein Array darunter verwenden, während PriorityQueue<TElement, TPriority> zwei verwenden würde -- und ihre Elemente zusammen neu anordnen würden. Dies könnte uns zusätzliche Zuweisungen ersparen und so effizient wie möglich sein. Ich füge das hinzu, sobald es grünes Licht gibt.

Gibt es einen Plan, eine threadsichere Version wie ConcurrentQueue zu erstellen?

Ich würde :heart: eine gleichzeitige Version davon sehen, die IProducerConsumerCollection<T> implementiert, damit sie mit BlockingCollection<T> usw. verwendet werden könnte.

@aobatact @khellang Es klingt wie ein ganz anderer Thread:

Ich stimme zu, dass es sehr wertvoll ist :wink:!

public bool IsEmpty();

Warum ist das eine Methode? Jede andere Sammlung im Framework, die IsEmpty hat, hat sie als Eigenschaft.

Ich habe den Vorschlag im Top-Post mit IsEmpty { get; } aktualisiert.
Ich habe auch einen Link zum neuesten Vorschlagsdokument im corefxlab Repo hinzugefügt.

Hallo zusammen,
Ich denke, es gibt viele Stimmen, die argumentieren, dass es gut wäre, Updates nach Möglichkeit zu unterstützen. Ich denke, niemand hat daran gezweifelt, dass es sich um eine nützliche Funktion für die Graphsuche handelt.

Aber es gab hier und da Einwände. Damit ich mein Verständnis überprüfen kann, scheinen die Haupteinwände zu sein:
-Es ist nicht klar, wie das Update funktionieren soll, wenn doppelte Elemente vorhanden sind.
-Es gibt einige Diskussionen darüber, ob doppelte Elemente überhaupt in einer Prioritätswarteschlange unterstützt werden sollen
- Es gibt einige Bedenken, dass es ineffizient sein könnte, eine zusätzliche Nachschlagedatenstruktur zum Auffinden von Elementen in der unterstützenden Datenstruktur zu haben, nur um ihre Priorität zu aktualisieren. Insbesondere für Szenarien, in denen niemals ein Update durchgeführt wird! Und/oder die Leistungsgarantien im schlimmsten Fall beeinträchtigen...

Habe ich etwas verpasst?

OK, ich denke, noch eins war - es wurde behauptet, dass update/remove semantisch "updateFirst" oder "removeFirst" bedeutet, könnte seltsam sein. :)

In welcher Version von .Net Core können wir PriorityQueue verwenden?

@memoryfraction .NET Core selbst hat noch keine PriorityQueue (es gibt Vorschläge - aber wir hatten in letzter Zeit keine Zeit dafür). Es gibt jedoch keinen Grund, warum es in der Microsoft .NET Core-Distribution enthalten sein muss. Jeder in der Community kann einen auf NuGet stellen. Vielleicht kann jemand zu diesem Thema einen vorschlagen.

@memoryfraction .NET Core selbst hat noch keine PriorityQueue (es gibt Vorschläge - aber wir hatten in letzter Zeit keine Zeit dafür). Es gibt jedoch keinen Grund, warum es in der Microsoft .NET Core-Distribution enthalten sein muss. Jeder in der Community kann einen auf NuGet stellen. Vielleicht kann jemand zu diesem Thema einen vorschlagen.

Danke für deine Antwort und deinen Vorschlag.
Wenn ich C# verwende, um Leetcode zu üben, und SortedSet verwenden muss, um Sets mit doppelten Elementen zu behandeln. Ich muss das Element mit einer eindeutigen ID umschließen, um das Problem zu lösen.
Daher bevorzuge ich es, die Prioritätswarteschlange in Zukunft in .NET Core zu verwenden, da dies bequemer ist.

Wie ist der aktuelle Stand dieses Problems? Ich habe gerade festgestellt, dass ich vor kurzem eine PriorityQueue benötige

Dieser Vorschlag aktiviert nicht standardmäßig die Sammlungsinitialisierung. PriorityQueue<T> hat keine Methode Add . Ich denke, die Sammlungsinitialisierung ist ein ausreichend großes Sprachfeature, um das Hinzufügen einer doppelten Methode zu rechtfertigen.

Wenn jemand einen schnellen binären Heap hat, den er teilen möchte, würde ich ihn gerne mit dem vergleichen, was ich geschrieben habe.
Ich habe die API wie vorgeschlagen vollständig implementiert. Anstelle eines Heaps wird jedoch ein einfaches sortiertes Array verwendet. Ich behalte das Element mit dem niedrigsten Wert am Ende, damit es in umgekehrter Reihenfolge sortiert wird. Wenn die Anzahl der Elemente weniger als 32 beträgt, verwende ich eine einfache lineare Suche, um herauszufinden, wo neue Werte eingefügt werden müssen. Danach habe ich eine binäre Suche verwendet. Unter Verwendung von Zufallsdaten fand ich, dass dieser Ansatz schneller ist als ein binärer Heap, wenn die Warteschlange einfach gefüllt und dann geleert wird.
Wenn ich Zeit habe, stelle ich es in ein öffentliches Git-Repository, damit die Leute es kritisieren und diesen Kommentar mit dem Ort aktualisieren können.

@SunnyWar Ich verwende derzeit: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
was eine gute Leistung hat. Ich denke, es wäre sinnvoll, Ihre Implementierung dort gegen die GenericPriorityQueue zu testen

Bitte ignorieren Sie meinen früheren Beitrag. Ich habe im Test einen Fehler gefunden. Sobald der binäre Heap behoben ist, funktioniert er am besten.

@karelz Wo befindet sich dieser Vorschlag derzeit auf ICollection<T> ? Ich habe nicht verstanden, warum das nicht unterstützt werden sollte, obwohl die Sammlung offensichtlich nicht zum Lesen gedacht ist?

@karelz zur offenen Frage der Dequeue-Reihenfolge: Ich plädiere dafür, dass der niedrigste Prioritätswert zuerst entfernt wird. Es ist praktisch für Shortest-Path-Algorithmen und auch für Timer-Warteschlangen-Implementierungen natürlich. Und das sind die beiden Hauptszenarien, für die es meiner Meinung nach verwendet werden wird. Ich bin mir nicht wirklich bewusst, was das Gegenargument wäre, es sei denn, es handelt sich um ein linguistisches Argument, das sich auf "hohe" Prioritäten und "hohe" numerische Werte bezieht.

@karelz erneut die Aufzählung um ... wie etwa eine mit Sort() Methode von der Art , die die Sammlung der interne Datum Halde in sortierter Reihenfolge an Ort und Enumerate() nach Sort() zählt die Sammlung in O(n) auf. Und wenn Sort() NICHT aufgerufen wird, gibt es Elemente in nicht sortierter Reihenfolge in O(n) zurück?

Triage: Wechsel in die Zukunft, da anscheinend kein Konsens darüber besteht, ob wir dies implementieren sollten.

Das ist sehr enttäuschend zu hören. Ich muss dafür noch Lösungen von Drittanbietern verwenden.

Was sind die Hauptgründe, warum Sie eine Drittanbieterlösung nicht verwenden möchten?

Heap-Datenstruktur ist ein MUSS für die Erstellung von Leetcode
mehr leetcode, mehr c#-Code-Interview, was mehr c#-Entwickler bedeutet.
Mehr Entwickler bedeuten ein besseres Ökosystem.
besseres Ökosystem bedeutet, dass wir morgen noch in c# programmieren können.

Fazit: Das ist nicht nur ein Feature, sondern auch die Zukunft. Aus diesem Grund wird dieses Thema als "Zukunft" bezeichnet.

Was sind die Hauptgründe, warum Sie eine Drittanbieterlösung nicht verwenden möchten?

Denn wenn es keinen Standard gibt, erfindet jeder seinen eigenen, jeder mit seinen eigenen Macken.

Es gab viele Male, in denen ich mir ein System.Collections.Generic.PriorityQueue<T> gewünscht habe, weil es für etwas relevant ist, an dem ich arbeite. Dieses Angebot gibt es schon seit 5 Jahren. Warum ist es immer noch nicht passiert?

Warum ist es immer noch nicht passiert?

Vorrangiges Verhungern, kein Wortspiel beabsichtigt. Das Entwerfen von Sammlungen ist Arbeit, denn wenn wir sie in die BCL aufnehmen, müssen wir darüber nachdenken, wie sie ausgetauscht werden, welche Schnittstellen sie implementieren, was die Semantik ist usw. Nichts davon ist Raketenwissenschaft, aber es dauert eine Weile. Darüber hinaus benötigen Sie eine Reihe konkreter Anwendungsfälle und Kunden, um das Design zu beurteilen, was mit zunehmender Spezialisierung der Kollektionen schwieriger wird. Bisher gab es immer andere Arbeiten, die als wichtiger angesehen wurden.

Schauen wir uns ein aktuelles Beispiel an: unveränderliche Sammlungen. Sie wurden von jemandem, der nicht im BCL-Team arbeitet, für einen Anwendungsfall in VS entwickelt, bei dem es um Unveränderlichkeit ging. Wir haben mit ihm zusammengearbeitet, um die APIs "BCL-ified" zu bekommen. Und als Roslyn online ging, haben wir ihre Kopien an so vielen Stellen wie möglich durch unsere ersetzt und das Design (und die Implementierung) basierend auf ihrem Feedback stark optimiert. Ohne ein "Helden"-Szenario ist dies schwer.

Denn wenn es keinen Standard gibt, erfindet jeder seinen eigenen, jeder mit seinen eigenen Macken.

@masonwheeler ist das, was Sie für PriorityQueue<T> ? Dass es mehrere 3rd-Party-Optionen gibt, die nicht austauschbar sind und keine eindeutig akzeptierte "beste Bibliothek für die meisten"? (Ich habe nicht recherchiert, daher kenne ich die Antwort nicht)

@eiriktsarpalis Wie gibt es keinen Konsens über die Implementierung?

@terrajobst Brauchen Sie wirklich ein

Was sind die Hauptgründe, warum Sie eine Drittanbieterlösung nicht verwenden möchten?

@terrajobst
Ich habe persönlich die Erfahrung gemacht, dass Lösungen von Drittanbietern nicht immer wie erwartet funktionieren / aktuelle Sprachfunktionen nicht verwenden. Mit einer standardisierten Version könnte ich (als Benutzer) ziemlich sicher sein, dass die Leistung die beste ist, die Sie bekommen können.

@danmosemsft

@masonwheeler ist das, was Sie für PriorityQueue<T> ? Dass es mehrere 3rd-Party-Optionen gibt, die nicht austauschbar sind und keine eindeutig akzeptierte "beste Bibliothek für die meisten"? (Ich habe nicht recherchiert, daher kenne ich die Antwort nicht)

Jawohl. Google einfach "C#-Prioritätswarteschlange"; Die erste Seite ist voll von:

  1. Prioritätswarteschlangenimplementierungen auf Github und anderen Hosting-Sites
  2. Leute fragen, warum es in Collections.Generic keine offizielle Prioritätswarteschlange gibt
  3. Tutorials zum Erstellen Ihrer eigenen Prioritätswarteschlangenimplementierung

@terrajobst Brauchen Sie wirklich ein

Nach meiner Erfahrung ja. Der Teufel steckt im Detail und sobald wir eine API ausgeliefert haben, können wir keine wirklichen Änderungen vornehmen. Und es gibt viele Implementierungsoptionen, die für den Benutzer sichtbar sind. Wir können die API eine Weile als OOB in der Vorschau anzeigen, aber wir haben auch gelernt, dass wir zwar sicherlich Feedback sammeln können, aber das Fehlen eines Heldenszenarios bedeutet, dass Sie keine Unentschieden haben, was oft dazu führt, dass der Held Szenario eintrifft, entspricht die Datenstruktur nicht ihren Anforderungen.

Anscheinend brauchen wir einen Helden. 😛

@masonwheeler nahm ich an Ihrem Link wäre dies 😄 Jetzt ist es in meinem Kopf.

Obwohl, wie @terrajobst sagt, unser

[aus Gründen der Übersichtlichkeit bearbeitet]

@danmosemsft Nein , wenn ich diesen Song auswählen würde, würde ich die Shrek 2-Version machen.

Hero-App-Kandidat Nr. 1: Wie wäre es, eine in TimerQueue.Portable zu verwenden?

Wurde bereits in Betracht gezogen, als Prototyp erstellt und verworfen. Dadurch wird der sehr häufige Fall, dass ein Timer schnell erstellt und zerstört wird (zB für eine Zeitüberschreitung), weniger effizient.

@stephentoub Ich nehme an, Sie meinen, dass es für einige Szenarien weniger effizient ist, in denen eine kleine Anzahl von Timern vorhanden ist. Aber wie skaliert es?

Ich gehe davon aus, dass Sie meinen, dass es für einige Szenarien weniger effizient ist, in denen es eine kleine Anzahl von Timern gibt. Aber wie skaliert es?

Nein, ich meinte, der übliche Fall ist, dass Sie zu jedem Zeitpunkt viele Timer haben, aber nur sehr wenige feuern. Dies ergibt sich, wenn Timer für Timeouts verwendet werden. Und das Wichtigste dabei ist die Geschwindigkeit, mit der Sie die Datenstruktur hinzufügen und daraus entfernen können ... Sie möchten, dass 0(1) und mit sehr geringem Overhead vorhanden sind. Wenn das O(log N) wird, ist das ein Problem.

Eine Prioritätswarteschlange wird C# definitiv interviewfreundlicher machen.

Eine Prioritätswarteschlange wird C# definitiv interviewfreundlicher machen.

Ja das stimmt.
Ich suche es aus dem gleichen Grund.

@stephentoub Für die Auszeiten, die eigentlich nie passieren, macht das für mich

Aber ich frage mich, was mit dem System passiert, wenn plötzlich viele Timeouts passieren

Versuch es. :)

Wir haben gesehen, dass bei der vorherigen Implementierung eine Menge realer Workloads gelitten haben. In jedem Fall, den wir uns angesehen haben, hat der neue (der keine Prioritätswarteschlange verwendet, sondern nur eine einfache Aufteilung zwischen bald und nicht bald Timer hat) das Problem behoben, und wir haben keine neuen mit gesehen es.

Verwenden auch wiederkehrende Timer ala System.Timer dieselbe Implementierung?

Jawohl. In realen Anwendungen sind sie jedoch im Allgemeinen, oft ziemlich gleichmäßig, verteilt.

5 Jahre später gibt es immer noch keine PriorityQueue.

Rx verfügt über eine hochgradig produktionserprobte Prioritätswarteschlangenklasse:

https://github.com/Reactive-Extensions/Rx.NET/blob/master/Rx.NET/Source/System.Reactive.Core/Reactive/Internal/PriorityQueue.cs

image

5 Jahre später gibt es immer noch keine PriorityQueue.

Muss nicht hoch genug sein...

Sie haben sich um das Repo-Layout herum geändert. Neuer Speicherort ist https://github.com/dotnet/reactive/blob/master/Rx.NET/Source/src/System.Reactive/Internal/PriorityQueue.cs

@stephentoub, aber vielleicht tatsächlich @eiriktsarpalis haben wir jetzt tatsächlich eine Traktion? Wenn wir ein fertiges API-Design haben, wäre ich bereit, daran zu arbeiten

Ich habe noch keine Erklärung gesehen, dass dies noch erledigt ist, und ich bin mir nicht sicher, ob es ein endgültiges API-Design ohne eine ausgewiesene Killer-App geben kann. Aber...
Angenommen, der Top-Killer-App-Kandidat programmiert Wettbewerbe / Lehren / Interviews, denke ich, dass Erics Design an der Spitze brauchbar genug aussieht ... und ich habe immer noch meinen Gegenvorschlag herumsitzen (neu überarbeitet, immer noch nicht zurückgezogen!)

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

Die Hauptunterschiede, die ich zwischen ihnen bemerke, sind, ob es versuchen sollte, wie ein Wörterbuch zu sprechen, und ob Selektoren eine Sache sein sollten.

Ich beginne zu vergessen, warum ich wollte, dass es ein Wörterbuch ist. Einen indizierten Zuweiser zum Aktualisieren von Prioritäten zu haben und in der Lage zu sein, Schlüssel mit ihren Prioritäten aufzuzählen, schien mir gut und sehr wörterbuchähnlich. In der Lage zu sein, es als Wörterbuch im Debugger zu visualisieren, könnte auch süß sein.

Ich denke, Selektoren ändern die erwartete Klassenschnittstelle, FWIW, tatsächlich drastisch. Der Selektor sollte zur Konstruktionszeit etwas eingebrannt sein, und wenn Sie ihn haben, muss niemand jemals einem anderen einen Prioritätswert übergeben – er sollte den Selektor einfach anrufen, wenn er die Priorität wissen möchte. Sie möchten also in keiner Methodensignatur Prioritätsparameter haben, mit Ausnahme der Selektoren. In diesem Fall wird sie also zu einer Art spezialisierterer 'PrioritySet'-Klasse. [Für welche unreinen Selektoren ein mögliches Fehlerproblem ist!]

@TimLovellSmith Ich verstehe den Wunsch nach diesem Verhalten, aber da dies eine 5-Jahres-Frage war, wäre es nicht vernünftig, einfach einen Array-basierten Binärheap mit einem Selektor zu implementieren und die gleiche API-Oberfläche wie Queue.cs teilen PriorityQueue erstellen, sollten wir einfach das API-Design von Queue emulieren.

@Jlalond Daher der Vorschlag hier, oder? Ich habe keine Einwände gegen eine Heap-basierte Array-Implementierung. Ich erinnere mich jetzt an den größten Einwand, den ich gegen Selektoren hatte, dass es nicht einfach genug ist, Prioritätsaktualisierungen richtig durchzuführen. Eine einfache alte Warteschlange hat keine Prioritätsaktualisierungsoperation, aber die Aktualisierung der Elementpriorität ist eine wichtige Operation für viele Prioritätswarteschlangen-Algorithmen.
Die Leute sollten für immer dankbar sein, wenn wir eine API verwenden, die es nicht erfordert, beschädigte Prioritätswarteschlangenstrukturen zu debuggen.

@TimLovellSmith Sorry Tim, ich hätte die Vererbung von IDictionary klären sollen.

Haben wir einen Anwendungsfall, bei dem sich die Priorität ändern würde?

Ich mag Ihre Implementierung, aber ich denke, wir können die Replikation von IDictionary Behavior reduzieren. Ich denke, wir sollten nicht von IDictionary<> erben, sondern nur von ICollection, da ich nicht glaube, dass die Wörterbuchzugriffsmuster intuitiv sind.

Ich denke jedoch wirklich, dass die Rückgabe von T und der damit verbundenen Priorität sinnvoll wäre, aber ich kenne keinen Anwendungsfall, bei dem ich die Priorität eines Elements innerhalb der Datenstruktur kennen müsste, wenn ich es in die Warteschlange einreihe oder aus der Warteschlange entferne.

@Jlalond
Wenn wir eine Prioritätswarteschlange haben, sollte sie alle Operationen unterstützen, die sie einfach und effizient kann, _und_ das sind
'erwartet' von Leuten, die mit dieser Art von Datenstruktur / Abstraktion von einem vertraut sind.

Die Aktualisierungspriorität gehört in die API, da sie in beide Kategorien fällt. Die Aktualisierungspriorität ist bei vielen Algorithmen so wichtig, dass die Komplexität der Operation mit Heap-Datenstrukturen die Komplexität des Gesamtalgorithmus beeinflusst und regelmäßig gemessen wird, siehe:
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps

TBH ist hauptsächlich der Reduce_key, der für Algorithmen und Effizienz interessant ist und gemessen wird, daher finde ich es persönlich gut, dass die API nur DecreasePriority () und nicht tatsächlich UpdatePriority hat.
Der Einfachheit halber erscheint es jedoch besser, die Benutzer der API nicht damit zu belästigen, sich Gedanken darüber zu machen, ob jede Änderung eine Zunahme oder eine Abnahme darstellt. Damit Joe Developer sofort "Update" hat, ohne sich fragen zu müssen, warum es nur Verringern und nicht Erhöhen unterstützt (und welches wann verwendet werden soll), denke ich, dass es am besten ist, eine allgemeine API mit Update-Priorität zu haben, aber dokumentieren Sie das wenn es zum Verringern verwendet wird, kostet es X, wenn es zum Erhöhen verwendet wird, kostet es Y, weil es genauso implementiert ist wie Entfernen+Hinzufügen.

RE-Wörterbuch Ich habe zugestimmt. Im Namen von einfacher ist es aus meinem Vorschlag entfernt.
https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

Ich kenne keinen Anwendungsfall, bei dem ich die Priorität eines Elements innerhalb der Datenstruktur kennen müsste, wenn ich es in die Warteschlange einreihe oder aus der Warteschlange entferne.

Ich bin mir nicht sicher, worum es in diesem Kommentar geht. Sie müssen die Priorität eines Elements nicht kennen, um es aus der Warteschlange zu entfernen. Sie müssen jedoch die Priorität kennen, wenn Sie sie in die Warteschlange stellen. Vielleicht möchten Sie die endgültige Priorität eines Elements erfahren, wenn Sie es aus der Warteschlange entfernen, da dieser Wert eine Bedeutung haben kann, z. B. Abstand.

@TimLovellSmith

Ich bin mir nicht sicher, worum es in diesem Kommentar geht. Sie müssen die Priorität eines Elements nicht kennen, um es aus der Warteschlange zu entfernen. Sie müssen jedoch die Priorität kennen, wenn Sie sie in die Warteschlange stellen. Vielleicht möchten Sie die endgültige Priorität eines Elements erfahren, wenn Sie es aus der Warteschlange entfernen, da dieser Wert eine Bedeutung haben kann, z. B. Abstand.

Tut mir leid, ich habe die Bedeutung von TPriorty in Ihrem Schlüsselwertpaar falsch verstanden.

Okay, die letzten beiden Fragen, warum sie KeyValuePair mit TPriority sind, und die beste Zeit, die ich für eine Neupriorisierung sehen kann, ist N log N, ich stimme zu, dass es wertvoll ist, aber auch, wie würde diese API aussehen?

die beste Zeit, die ich für eine Neupriorisierung sehen kann, ist N log N,

Das ist die beste Zeit für eine Neupriorisierung von N Elementen im Gegensatz zu einem Element, oder?
Ich werde das Dokument klarstellen: "UpdatePriority | O(log n)" sollte "UpdatePriority (single item) | O(log n)" lauten.

@TimLovellSmith Macht Sinn, aber würde nicht jede Aktualisierung der Priorität eigentlich ne O2 * (log n) sein, da wir das Element entfernen und dann wieder

@TimLovellSmith Macht Sinn, aber würde nicht jede Aktualisierung der Priorität eigentlich ne O2 * (log n) sein, da wir das Element entfernen und dann wieder

Konstante Faktoren wie 2 werden in der Komplexitätsanalyse im Allgemeinen ignoriert, da sie mit zunehmender Größe von N meist irrelevant werden.

Verstanden, wollte hauptsächlich wissen, ob Tim irgendwelche aufregenden Ideen hatte, die er nur machen konnte
eine Operation :)

Am Di, 18. August 2020, 23:51 Uhr schrieb masonwheeler [email protected] :

@TimLovellSmith https://github.com/TimLovellSmith Macht Sinn, aber
bräuchte kein update von priorität eigentlich ne O2* (log n), da müssten wir
das Element entfernen und dann wieder einfügen? Basierend auf dieser Annahme
ein binärer Haufen sein

Konstante Faktoren wie 2 werden in der Komplexitätsanalyse im Allgemeinen ignoriert
weil sie mit zunehmender Größe von N meist irrelevant werden.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/dotnet/runtime/issues/14032#issuecomment-675887763 ,
oder abmelden
https://github.com/notifications/unsubscribe-auth/AF76XTI3YK4LRUVTOIQMVHLSBNZARANCNFSM4LTSQI6Q
.

@Jlalond
Keine neuen Ideen, ich habe nur einen konstanten Faktor ignoriert, aber Abnahmen unterscheiden sich auch von Zunahmen

Wenn die Prioritätsaktualisierung eine Verringerung ist, müssen Sie sie nicht entfernen und erneut hinzufügen, sie sprudelt nur hoch, also ist 1 * O(log n) = O(log n).
Wenn die Prioritätsaktualisierung eine Erhöhung ist, müssen Sie sie wahrscheinlich entfernen und erneut hinzufügen, also ist es 2 * O(log n) = immer noch O (log n).

Jemand anderes könnte eine bessere Datenstruktur / einen besseren Algorithmus zur Erhöhung der Priorität erfunden haben als remove + readd, aber ich habe nicht versucht, ihn zu finden, O (log n) scheint gut genug zu sein.

KeyValuePair sich in die Benutzeroberfläche eingeschlichen, als es ein Wörterbuch war, überlebte aber die Entfernung des Wörterbuchs, hauptsächlich damit es möglich ist, die Sammlung von _Elementen mit ihren Prioritäten_ zu iterieren. Und auch, damit Sie Elemente mit ihren Prioritäten aus der Warteschlange entfernen können. Aber vielleicht noch einmal der Einfachheit halber sollte Dequeue mit Prioritäten nur in der 'erweiterten' Version der Dequeue API sein. (Versuchen Sie, die Warteschlange aufzulösen :D)

Ich werde diese Änderung vornehmen.

@TimLovellSmith Cool, ich freue mich auf Ihren überarbeiteten Vorschlag. Können wir es zur Überprüfung einreichen, damit ich mit der Arbeit beginnen kann? Darüber hinaus weiß ich, dass es einige Impulse für eine Paarungswarteschlange gab, um die Zusammenführungszeit zu verbessern, aber ich denke immer noch, dass ein Array-basierter Binärheap die beste Gesamtleistung wäre. Die Gedanken?

@Jlalond
Ich habe die kleinen Änderungen vorgenommen, um die Peek + Dequeue-API zu vereinfachen.
Einige ICollectionAPIs im Zusammenhang mit KeyValuePair bleiben weiterhin bestehen, da ich nichts offensichtlich besseres sehe, um sie zu ersetzen.
Gleicher PR-Link. Gibt es eine andere Möglichkeit, es zur Überprüfung einzureichen?

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

Es macht mir nichts aus, welche Implementierung es verwendet, solange es ungefähr so ​​​​schnell wie ein Array-basierter Heap oder besser ist.

@TimLovellSmith Ich weiß eigentlich nichts über API-Review, aber ich kann mir vorstellen, dass @danmosemsft uns in die richtige Richtung

Meine erste Frage ist, was würde?Sein? Ich nehme an, es wäre eine Int- oder Gleitkommazahl, aber für mich scheint es seltsam, die Zahl zu deklarieren, die "intern" für die Warteschlange ist

Und ich bevorzuge eigentlich binäre Haufen, wollte aber sicherstellen, dass es uns allen gut geht, wenn wir in diese Richtung gehen

@eiriktsarpalis @layomia sind die Gebietsbesitzer und müssten dies durch die API-Überprüfung hüten . Ich habe die Diskussion nicht verfolgt, haben wir einen Konsens erreicht?

Wie wir in der Vergangenheit besprochen haben, sind die Kernbibliotheken nicht der beste Ort für alle Sammlungen, insbesondere solche, die eigensinnig und/oder nischenorientiert sind. ZB versenden wir jährlich - wir können nicht so schnell umziehen wie eine Bibliothek. Wir haben das Ziel, eine Community rund um ein Sammlungspaket aufzubauen, um diese Lücke zu schließen. Aber 84 Daumen hoch für diesen Punkt legen nahe, dass dies in den Kernbibliotheken weit verbreitet ist.

@Jlalond Entschuldigung, ich habe nicht ganz verstanden, was die erste Frage sein soll. Haben Sie sich gefragt, warum TPriority generisch ist? Oder muss es eine Zahl sein? Ich bezweifle, dass viele Leute nicht-numerische Prioritätstypen verwenden werden. Möglicherweise bevorzugen sie jedoch die Verwendung von byte, int, long, float, double oder enum.

@TimLovellSmith Ja , wollte nur wissen, ob es generisch ist

@danmosemsft Danke Dan. Die Anzahl der nicht adressierten Einwände/Kommentare, die ich zu meinem Vorschlag[1] sehe, ist ungefähr null, und es geht auf alle wichtigen Fragen ein, die von @ebickles Vorschlag oben offen

Also behaupte ich, dass es die Gesundheitsprüfungen bisher besteht. Es muss noch eine Überprüfung erforderlich sein, wir können darüber sprechen, ob es sinnvoll ist, IReadOnlyCollection zu erben (klingt nicht sehr nützlich, aber ich sollte mich den Experten überlassen) und so weiter - ich denke, dafür ist der API-Überprüfungsprozess da! @eiriktsarpalis @layomia darf ich dich bitten, dir das mal anzuschauen?

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

[PS – basierend auf dem Thread-Sentiment besteht die derzeit vorgeschlagene Killer-App aus Codierungswettbewerben, Interviewfragen usw (n) und eine nicht zu langsame Umsetzung - und ich bin gerne Versuchskaninchen, um es auf ein paar Probleme anzuwenden. Tut mir leid, es ist nichts Aufregenderes.]

Ich spreche nur, weil ich ständig Prioritätswarteschlangen für "echte" Projekte verwende, hauptsächlich für die Graphsuche (z. B. Problemoptimierung, Routenfindung) und geordnete Extraktion (nach Blöcken (z die Timer-Diskussion oben)). Ich habe ein paar Projekte, in die ich eine Implementierung direkt zum Prüfen/Testen der Integrität einbinden könnte, wenn es hilft. Der aktuelle Vorschlag sieht für mich gut aus (würde für alle meine Zwecke funktionieren, wenn er nicht ideal passt). Ich habe ein paar kleine Beobachtungen:

  • TElement erscheint ein paar Mal, wo es sein sollte TKey
  • Meine eigenen Implementierungen enthalten oft ein bool EnqueueOrUpdateIfHigherPriority aus Gründen der Benutzerfreundlichkeit (und in einigen Fällen der Effizienz), das oft die einzige Methode zum Einreihen/Aktualisieren ist, die verwendet wird (zB bei der Graphensuche). Offensichtlich ist es nicht unbedingt erforderlich und würde die API zusätzlich komplex machen, aber es ist eine sehr schöne Sache.
  • Es gibt zwei Kopien von Enqueue (ich meine nicht Add ): Soll eine bool TryEnqueue ?
  • "// zählt die Sammlung in beliebiger Reihenfolge auf, aber mit dem kleinsten Element zuerst": Ich glaube nicht, dass das letzte Bit nützlich ist; Ich würde es vorziehen, dass der Enumerator keine Vergleiche durchführen müsste, aber ich gehe davon aus, dass dies "kostenlos" ist, damit es mich nicht stört
  • Die Benennung von EqualityComparer ist etwas unerwartet: Ich hätte erwartet, dass KeyComparer mit PriorityComparer .
  • Ich verstehe die Anmerkungen zur Komplexität von CopyTo und ToArray .

@VisualMelon
Vielen Dank für die Bewertung!

Ich mag den Namensvorschlag von KeyComparer. Die API zum Verringern der Priorität klingt sehr nützlich. Wie wäre es, wenn wir eine oder beide dieser APIs hinzufügen. Da wir das Modell "geringste Priorität kommt zuerst" verwenden, würden Sie nur die Verringerung verwenden? Oder wünschen Sie sich auch eine Erhöhung?

    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

Der Grund, warum ich dachte, dass die Aufzählung das kleinste Element zuerst zurückgibt, war die Übereinstimmung mit dem mentalen Modell, dass Peek() das Element am Anfang der Auflistung zurückgibt. Und es sind auch keine Vergleiche erforderlich, um es zu implementieren, wenn es sich um einen binären Heap handelt, also schien es wie ein Werbegeschenk zu sein. Aber vielleicht ist es weniger kostenlos als ich denke - ungerechtfertigte Komplexität? Aus diesem Grund nehme ich es gerne heraus.

Zwei Enqueues waren zufällig. Wir haben EnqueueOrUpdate, bei dem die Priorität immer aktualisiert wird. Ich denke, TryUpdate ist die logische Umkehrung davon, wo die Priorität für Elemente, die sich bereits in der Sammlung befinden, niemals aktualisiert werden sollte. Ich kann sehen, dass es diese logische Lücke füllt, aber ich bin mir nicht sicher, ob es eine API ist, die die Leute in der Praxis wollen.

Ich kann mir nur vorstellen, die abnehmende Variante zu verwenden: Es ist eine natürliche Operation bei der Routenfindung, einen Knoten in die Warteschlange einzureihen oder einen vorhandenen Knoten durch einen mit niedrigerer Priorität zu ersetzen; Ich konnte nicht sofort eine Verwendung für das Gegenteil vorschlagen. Ein Rückgabeparameter boolean kann praktisch sein, wenn Sie eine Operation ausführen müssen, wenn das Element in der Warteschlange ist/nicht in der Warteschlange ist.

Ich habe nicht von einem binären Heap ausgegangen, aber wenn es kostenlos ist, habe ich kein Problem damit, dass das erste Element immer das Minimum ist, es schien nur eine seltsame Einschränkung zu sein.

@VisualMelon @TimLovellSmith Wäre eine API in Ordnung?
EnqueueOrUpdatePriority ? Ich kann mir vorstellen, dass allein die Möglichkeit, einen Knoten innerhalb der Warteschlange neu zu positionieren, für Traversalalgorithmen wertvoll ist, selbst wenn die Priorität erhöht oder verringert wird

@Jlalond ja, ich erwähne es, weil es je nach Implementierung für die Effizienz hilfreich sein kann und den Anwendungsfall direkt codiert, nur etwas in die Warteschlange zu stellen, wenn es das verbessert, was Sie bereits in der Warteschlange haben. Ob die zusätzliche Komplexität für ein solches Allzweckwerkzeug angemessen ist, wage ich nicht zu sagen, ist aber sicherlich nicht notwendig.

Aktualisiert, um EnqueueOrIncreasePriority zu entfernen, wobei EnqueueOrUpdate und EnqueueOrDecrease beibehalten werden. Sie geben bool zurück, wenn ein Element der Sammlung neu hinzugefügt wird, wie HashSet.Add().

@Jlalond Ich entschuldige mich für den obigen Fehler (Löschung Ihres letzten Kommentars, in dem Sie gefragt werden, wann dieses Problem überprüft wird).

Ich wollte nur erwähnen, dass @eiriktsarpalis nächste Woche wieder da ist und wir dann die Priorisierung dieser Funktion und die Überprüfung der APIs besprechen.

Dies ist etwas, das wir in Betracht ziehen, aber noch nicht für .NET 6 festgelegt sind.

Ich sehe, dass dieser Thread wiederbelebt wurde, indem wir von vorne anfingen und sich aufgaben, obwohl wir 2017 mehrere Monate lang eingehende Diskussionen zu diesem Thema geführt haben (den Großteil dieses riesigen Threads) und gemeinsam diesen Vorschlag erstellt haben – scrollen Sie nach oben, um zu sehen. Dies ist auch der Vorschlag, den @karelz in der ersten Zeile des ersten Beitrags zur Sichtbarkeit verlinkt hat:

Siehe AKTUELLES Angebot im corefxlab Repository.

Am 16.06.2017 warteten wir auf eine API-Überprüfung, die noch nie stattgefunden hat und der Status lautete:

Wenn die API in ihrer aktuellen Form (zwei Klassen) genehmigt wird, würde ich einen weiteren PR für CoreFXLab mit einer Implementierung für beide erstellen, die einen quaternären Heap verwendet ( siehe Implementierung ). PriorityQueue<T> würde nur ein Array darunter verwenden, während PriorityQueue<TElement, TPriority> zwei verwenden würde -- und ihre Elemente zusammen neu anordnen würden. Dies könnte uns zusätzliche Zuweisungen ersparen und so effizient wie möglich sein. Ich füge das hinzu, sobald es grünes Licht gibt.

Ich empfehle, den Vorschlag, den wir 2017 gemacht haben, weiter zu wiederholen, ausgehend von einer formalen Überprüfung, die noch nicht stattgefunden hat. Dies wird es uns ermöglichen, Forking zu vermeiden und auf der Grundlage eines Vorschlags zu wiederholen, der nach Hunderten von Beiträgen von Community-Mitgliedern erstellt wurde, auch aus Respekt vor der Anstrengung aller Beteiligten. Ich bespreche es gerne, wenn es Feedback gibt.

@pgolebiowski Vielen Dank, dass Sie auf die Diskussion zurückgekommen sind. Ich möchte mich dafür entschuldigen, dass ich die Vorschläge effektiv noch weiter gespalten habe, was nicht die effektivste Art der Zusammenarbeit ist. Es war ein impulsiver Schritt meinerseits. (Ich hatte den Eindruck, dass die Diskussion aufgrund zu vieler offener Fragen in den Vorschlägen zu weit ins Stocken geraten war und brauchte nur einen eigenwilligeren Vorschlag.)

Wie wäre es, wenn wir versuchen, dies voranzutreiben, indem wir zumindest vorübergehend den Code-Inhalt meiner PR vergessen und hier die identifizierten "offenen Probleme" weiter diskutieren? Zu allen möchte ich meine Meinung sagen.

  1. Klassenname PriorityQueue vs. Heap <-- Ich denke, es wurde bereits diskutiert und Konsens ist, dass PriorityQueue besser ist.

  2. IHeap- und Konstruktorüberladung einführen? <-- Ich sehe nicht viel Wert. Ich denke, für 95% der Welt gibt es keinen zwingenden Grund (z. B. in Bezug auf das Performance-Delta), mehrere Heap-Implementierungen zu haben, und die anderen 5% werden wahrscheinlich ihre eigenen schreiben müssen.

  3. IPriorityQueue einführen? <-- Ich glaube auch nicht, dass es nötig ist. Ich gehe davon aus, dass andere Sprachen und Frameworks ohne diese Schnittstellen in ihrer Standardbibliothek gut auskommen. Lass mich wissen, wenn das falsch ist.

  4. Selektor verwenden (der im Wert gespeicherten Priorität) oder nicht (5 APIs Unterschied) <-- Ich sehe kein starkes Argument für die Unterstützung von Selektoren in der Prioritätswarteschlange. Ich glaube, dass IComparer<> bereits als wirklich guter Mechanismus zur minimalen Erweiterbarkeit zum Vergleichen von Gegenstandsprioritäten dient, und Selektoren bieten keine wesentliche Verbesserung. Außerdem sind die Leute wahrscheinlich verwirrt darüber, wie Selektoren mit veränderlichen Prioritäten verwendet werden (oder wie sie NICHT verwendet werden sollen).

  5. Tupel verwenden (TElement-Element, TPriority-Priorität) vs. KeyValuePair<-- Persönlich denke ich, dass KeyValuePair die bevorzugte Option für eine Prioritätswarteschlange ist, in der Elemente aktualisierbare Prioritäten haben, da die Elemente als Schlüssel in einem Satz/Wörterbuch behandelt werden.

  6. Sollten Peek und Dequeue eher ein Argument als ein Tupel haben? <-- Ich kenne nicht alle Vor- und Nachteile, insbesondere die Leistung. Sollten wir einfach diejenige auswählen, die in einem einfachen Benchmark-Test besser abschneidet?

  7. Ist das Werfen von Peek und Dequeue überhaupt sinnvoll? <-- Ja... Dies sollte bei Algorithmen passieren, die fälschlicherweise davon

Abgesehen davon habe ich auch einige Vorschläge, die ich in Erwägung ziehen sollte, um den in Betracht gezogenen Vorschlag zu ergänzen:

  1. Prioritätswarteschlangesollte nur mit Unikaten funktionieren, die ihren eigenen Griff haben. Es sollte benutzerdefinierten IEqualityComparer unterstützen, falls es nicht erweiterbare Objekte (wie Strings) auf bestimmte Weise vergleichen möchte, um Prioritätsaktualisierungen durchzuführen.

  2. Prioritätswarteschlangesollte eine Vielzahl von Operationen wie 'Entfernen', 'Priorität verringern, wenn kleiner' und insbesondere 'Element in die Warteschlange einreihen oder Priorität verringern, wenn kleinere Operation', alle in O(log n) unterstützen - für eine effiziente und einfache Implementierung von Graphsuchszenarien.

  3. Für faule Programmierer wäre es praktisch, wenn PriorityQueuebietet Indexoperator[] zum Abrufen/Setzen der Priorität vorhandener Elemente. Es sollte O(1) für get, O(log n) für set sein.

  4. Die kleinste Priorität zuerst ist am besten. Da Prioritätswarteschlangen für viele verwendet werden, sind Optimierungsprobleme mit 'minimalen Kosten'.

  5. Die Aufzählung sollte keine Bestellgarantien bieten.

Offenes Problem - behandelt:
PrioritätswarteschlangeElemente und Prioritäten scheinen dazu gedacht zu sein, mit doppelten Werten zu arbeiten. Diese Werte müssen entweder als "unveränderlich" betrachtet werden, oder es muss eine Methode zum Aufrufen der Sammlung vorhanden sein, wenn sich die Elementprioritäten geändert haben, möglicherweise mit Handles, damit die Priorität der geänderten Duplikate spezifisch angegeben werden kann (was für ein Szenario erforderlich ist) das??)... Ich habe Zweifel, ob dies sinnvoll ist und ob dies eine gute Idee ist, aber wenn wir Aktualisierungsprioritäten in einer solchen Sammlung haben, könnte es schön sein, wenn die durch diese Methode anfallenden Kosten träge anfallen können, also dass, wenn Sie nur unveränderliche Elemente bearbeiten und diese nicht verwenden möchten, keine zusätzlichen Kosten entstehen ... wie kann man das lösen? Kann es an überladenen Methoden liegen, die Handles verwenden und solche, die dies nicht tun? Oder sollten wir einfach keine Element-Handles haben, die sich von den Elementen selbst unterscheiden (als Hash-Schlüssel zum Nachschlagen verwendet)?

Um dies zu beschleunigen, kann es sinnvoll sein, diesen Typ in die Bibliothek "Community Collections" zu verschieben, die in https://github.com/dotnet/runtime/discussions/42205 diskutiert wird

Ich stimme zu, dass die kleinste Priorität zuerst das Beste ist. Eine Prioritätswarteschlange ist auch bei der Entwicklung rundenbasierter Spiele nützlich, und es ist sehr hilfreich, die Priorität ein monoton ansteigender Zähler dafür zu haben, wann jedes Element seinen nächsten Zug bekommt.

Wir versuchen, dies so schnell wie möglich zur API-Überprüfung zu bringen (obwohl wir uns noch nicht verpflichtet haben, eine Implementierung innerhalb des .NET 6-Zeitrahmens bereitzustellen).

Aber bevor wir dies zur Überprüfung senden, müssen wir einige der offenen Fragen auf hoher Ebene klären. Ich denke , @TimLovellSmith ‚s bis zu

Ein paar Anmerkungen zu diesen Punkten:

  • Der Konsens zu den Fragen 1-3 ist längst etabliert, ich denke, wir könnten diese als gelöst behandeln.

  • _Selektor verwenden (der im Wert gespeicherten Priorität) oder nicht_ -- Stimmt mit Ihren Bemerkungen überein. Ein weiteres Problem bei diesem Design ist, dass Selektoren optional sind, was bedeutet, dass Sie aufpassen müssen, dass Sie nicht die falsche Enqueue Überladung aufrufen (oder riskieren, ein InvalidOperationException ) zu erhalten.

  • Ich bevorzuge die Verwendung eines Tupels über KeyValuePair . Die Verwendung einer .Key Eigenschaft für den Zugriff auf einen TPriority Wert fühlt sich für mich seltsam an. Mit einem Tupel können Sie .Priority das bessere Selbstdokumentationseigenschaften hat.

  • _Sollten Peek und Dequeue lieber out-Argumente statt Tupel verwenden?_ -- Ich denke, wir sollten einfach die etablierten Konventionen bei ähnlichen Methoden an anderer Stelle befolgen. Verwenden Sie also wahrscheinlich ein out Argument.

  • _Ist das Werfen von Peek und Dequeue überhaupt sinnvoll?_ -- Stimme deinen Kommentaren zu 100 % zu.

  • _Kleinste Priorität zuerst ist am besten_ -- Einverstanden.

  • _Aufzählung sollte keine Bestellgarantien bieten_ -- Könnte dies nicht den Erwartungen der Benutzer widersprechen? Was sind die Kompromisse? Hinweis: Details wie diese können wir wahrscheinlich für die Diskussion über die API-Überprüfung zurückstellen.

Ich möchte noch ein paar andere offene Fragen umformulieren:

  • Versenden wir PriorityQueue<T> , PriorityQueue<TElement, TPriority> oder beides? -- Ich persönlich denke, wir sollten nur letzteres implementieren, da es eine sauberere und universellere Lösung zu bieten scheint. Im Prinzip sollte die Priorität keine dem Element in der Warteschlange innewohnende Eigenschaft sein, daher sollten wir Benutzer nicht zwingen, es in Wrapper-Typen zu kapseln.

  • Benötigen wir Element-Einzigartigkeit bis hin zur Gleichheit? -- Korrigieren Sie mich, wenn ich falsch liege, aber ich glaube, Einzigartigkeit ist eine künstliche Einschränkung, die uns durch die Anforderung aufgezwungen wird, Aktualisierungsszenarien zu unterstützen, ohne auf einen Handle-Ansatz zurückzugreifen. Es verkompliziert auch die API-Oberfläche, da wir uns jetzt auch um Elemente mit der richtigen Gleichheitssemantik kümmern müssen (was ist die angemessene Gleichheit, wenn Elemente große DTOs sind?). Ich sehe hier drei mögliche Pfade:

    1. Erfordern Sie Eindeutigkeit/Gleichheit und unterstützen Sie Aktualisierungsszenarien, indem Sie das ursprüngliche Element (bis zur Gleichheit) übergeben.
    2. Erfordern Sie keine Eindeutigkeit/Gleichheit und unterstützen Sie Aktualisierungsszenarien mit Handles. Diese können mit optionalen Enqueue Methodenvarianten für Benutzer abgerufen werden, die sie benötigen. Wenn Handle-Zuordnungen ein großes Problem darstellen, frage ich mich, ob diese à la ValueTask amortisiert werden könnten?
    3. Erfordern keine Eindeutigkeit/Gleichheit und unterstützen keine Aktualisierungsprioritäten.
  • Unterstützen wir das Zusammenführen von Warteschlangen? -- Konsens scheint nein zu sein, da wir nur eine Implementierung (vermutlich unter Verwendung von Array-Heaps) ausliefern, bei der das Zusammenführen nicht effizient ist.

  • Welche Schnittstellen soll es implementieren? -- Ich habe einige Vorschläge gesehen, die IQueue<T> empfehlen, aber das fühlt sich an wie eine undichte Abstraktion. Ich persönlich würde es vorziehen, es einfach zu halten und einfach ICollection<T> implementieren.

cc @layomia @safen

@eiriktsarpalis Im Gegenteil - es ist nichts Künstliches an der Einschränkung, Updates zu unterstützen!

Algorithmen mit Prioritätswarteschlangen aktualisieren häufig die Prioritäten von Elementen. Die Frage ist, stellen Sie eine objektorientierte API bereit, die "einfach funktioniert", um die Prioritäten gewöhnlicher Objekte zu aktualisieren ... oder zwingen Sie die Leute dazu?
a) das Griffmodell verstehen
b) zusätzliche Daten im Speicher halten, wie zusätzliche Eigenschaften oder ein externes Wörterbuch, um die Handles von Objekten auf ihrer Seite zu verfolgen, nur damit sie Aktualisierungen durchführen können (muss mit dem Wörterbuch für Klassen gehen, die sie nicht ändern können? oder aktualisiere die Objekte in Tupel usw.)
c) „Memory Manage“ oder „Garbage Collect“ externe Strukturen, dh Bereinigung des Handles für Elemente, die sich nicht mehr in der Warteschlange befinden, wenn der Wörterbuchansatz verwendet wird
d) undurchsichtige Handles in Kontexten mit mehreren Warteschlangen nicht verwechseln, da sie nur im Kontext einer einzigen Prioritätswarteschlange von Bedeutung sind

Außerdem gibt es diese philosophische Frage um den ganzen Grund, warum jemand möchte, dass sich eine Warteschlange so verhält, die _Objekte nach Priorität verfolgt_: Warum sollte das gleiche Objekt (gleich gibt wahr zurück) zwei _unterschiedliche_ Prioritäten haben? Wenn sie wirklich unterschiedliche Prioritäten haben sollen, warum werden sie dann nicht als unterschiedliche Objekte modelliert? (oder in Tupel hochkonvertiert, mit Unterscheidungsmerkmalen?)

Auch für Handles muss es eine interne Tabelle von Handles in der Prioritätswarteschlange geben, damit die Handles tatsächlich funktionieren. Ich denke, dies entspricht der Arbeit, ein Wörterbuch zum Nachschlagen von Objekten in der Warteschlange zu führen.

PS: Jedes .net-Objekt unterstützt bereits Konzepte der Gleichheit/Eindeutigkeit, daher ist es keine große Frage, es zu "erfordern".

In Bezug auf KeyValuePair würde ich zustimmen, dass dies nicht ideal ist (obwohl im Vorschlag Key für das Element, Value für die Priorität steht, was mit den verschiedenen Sorted -Datentypen in der BCL) und ihre Angemessenheit hängt von einer Entscheidung bezüglich der Eindeutigkeit ab. Persönlich würde ich ein dediziertes gut benanntes struct einem Tupel irgendwo in der öffentlichen API _sehr vorziehen, insbesondere für die Eingabe.

In Bezug auf Einzigartigkeit ist das ein grundlegendes Anliegen, und ich denke, bis dahin kann nichts anderes entschieden werden. Ich würde die Einzigartigkeit der Elemente bevorzugen, wie sie durch den (optionalen) Vergleicher definiert wird (sowohl nach bestehenden Vorschlägen als auch nach Vorschlag i), wenn das Ziel eine einzige benutzerfreundliche und universelle API ist. Einzigartig vs. nicht eindeutig ist ein großer Riss, und ich verwende beide Typen für unterschiedliche Zwecke. Ersteres ist "schwerer" zu implementieren und deckt die meisten (und typischeren) Anwendungsfälle ab (nur meine Erfahrung), während es schwieriger zu missbrauchen ist. Diese Anwendungsfälle, die Nichteindeutigkeit _erfordern_, sollten (IMO) von einem anderen Typ bedient werden (zB einem einfachen alten binären Heap), und ich würde es begrüßen, wenn beide verfügbar wären. Dies ist im Wesentlichen das, was der von @pgolebiowski verlinkte ursprüngliche Vorschlag (so wie ich ihn verstehe) modulo einen (einfachen) Wrapper bietet. _Edit: Nein, das würde gebundene Prioritäten nicht unterstützen_

Im Gegenteil - es ist nichts Künstliches an der Einschränkung, Updates zu unterstützen!

Es tut mir leid, ich wollte nicht andeuten, dass die Unterstützung für Updates künstlich ist; vielmehr wird die Forderung nach Eindeutigkeit künstlich eingeführt, um Updates zu unterstützen.

PS: Jedes .net-Objekt unterstützt bereits Konzepte der Gleichheit/Einzigartigkeit, daher ist es keine große Aufforderung, es zu "erfordern".

Sicher, aber gelegentlich kann die mit dem Typ gelieferte Gleichheitssemantik nicht die wünschenswerte sein (zB Referenzgleichheit, vollständige strukturelle Gleichheit usw.). Ich weise nur darauf hin, dass Gleichheit schwer ist und sie in das Design zu zwingen, bringt eine ganz neue Klasse potenzieller Benutzerfehler mit sich.

@eiriktsarpalis Danke für die Klarstellung. Aber ist das wirklich künstlich? Ich glaube nicht. Es ist nur eine weitere natürliche Lösung.

Die API muss _wohldefiniert_ sein. Sie können keine Update-API bereitstellen, ohne dass der Benutzer genau angeben muss, was er aktualisieren möchte . Handles und Objektgleichheit sind nur zwei verschiedene Ansätze zum Erstellen der wohldefinierten API.

Handle-Ansatz: Jedes Mal, wenn Sie ein Objekt zur Sammlung hinzufügen, müssen Sie ihm einen 'Namen', dh ein 'Handle' geben, damit Sie später in der Konversation ohne Mehrdeutigkeit auf genau dieses Objekt verweisen können.

Ansatz der Eindeutigkeit von Objekten: Jedes Mal, wenn Sie der Sammlung ein Objekt hinzufügen, muss es sich um ein anderes Objekt handeln, oder Sie müssen angeben, wie mit dem Fall umgegangen werden soll, dass das Objekt bereits vorhanden ist.

Leider können Sie nur mit dem Objektansatz einige der nützlichsten High-Level-API-Methodeneigenschaften unterstützen, wie z kann nicht wissen, ob das Element bereits in der Warteschlange vorhanden ist (da es Ihre Aufgabe ist, dies mit Handles zu verfolgen).

Einer der aufschlussreichsten Kritikpunkte an dem Handle-Ansatz oder dem Weglassen der Eindeutigkeitsbeschränkung für mich ist, dass die Implementierung aller Arten von Szenarien mit aktualisierbarer Priorität viel komplizierter wird:

z.B

  1. Verwenden Sie eine PriorityQueuefür Strings, die Nachrichten/Sentiments/Tags/Benutzernamen darstellen, die hochgestimmt/scoreupdated werden, haben eindeutige Werte eine wechselnde Priorität
  2. PriorityQueue verwenden, double> um eindeutige Tupel zu bestellen [ob sie Prioritätsänderungen haben oder nicht] - muss irgendwo zusätzliche Handles verfolgen
  3. PriroityQueue verwendenUm Graph-Indizes oder Datenbankobjekt-IDs zu priorisieren, müssen Sie jetzt Handles durch Ihre Implementierung streuen

PS

Sicher, aber gelegentlich ist die Gleichheitssemantik, die mit dem Typ geliefert wird, möglicherweise nicht die wünschenswerte

Es soll Fluchtschraffuren geben, wie IEqualityComparer, oder die Aufwärtskonvertierung in einen reichhaltigeren Typ.

Danke für das Feedback 🥳 Werde den Vorschlag am Wochenende unter Berücksichtigung aller neuen Eingaben aktualisieren und eine neue Überarbeitung für eine weitere Runde teilen. ETA 2020-09-20.

Vorschlag für Prioritätswarteschlange (v2.0)

Zusammenfassung

Die .NET Core-Community schlägt vor, der Systembibliothek die _priority queue_-Funktionalität hinzuzufügen, eine Datenstruktur, in der jedem Element zusätzlich eine Priorität zugeordnet ist. Insbesondere schlagen wir vor, PriorityQueue<TElement, TPriority> zum Namensraum System.Collections.Generic hinzuzufügen.

Grundsätze

Bei unserem Design haben wir uns von den folgenden Grundsätzen leiten lassen (es sei denn, Sie kennen bessere):

  • Breite Abdeckung. Wir möchten .NET Core-Kunden eine wertvolle Datenstruktur bieten, die vielseitig genug ist, um eine Vielzahl von Anwendungsfällen zu unterstützen.
  • Lernen Sie aus bekannten Fehlern. Wir bemühen uns, Prioritätswarteschlangenfunktionalität bereitzustellen, die frei von den kundenseitigen Problemen ist, die in anderen Frameworks und Sprachen vorhanden sind, z. B. Java, Python, C++, Rust. Wir werden vermeiden, Designentscheidungen zu treffen, von denen bekannt ist, dass sie Kunden unzufrieden machen und die Nützlichkeit von Prioritätswarteschlangen verringern.
  • Äußerste Sorgfalt bei 1-Weg-Türentscheidungen. Sobald eine API eingeführt wurde, kann sie nicht mehr geändert oder gelöscht, sondern nur erweitert werden. Wir werden die Designentscheidungen sorgfältig analysieren, um suboptimale Lösungen zu vermeiden, an denen unsere Kunden für immer festhalten werden.
  • Vermeiden Sie Designlähmungen. Wir akzeptieren, dass es möglicherweise keine perfekte Lösung gibt. Wir werden Kompromisse abwägen und die Lieferung vorantreiben, um unseren Kunden endlich die Funktionalität zu liefern, auf die sie seit Jahren warten.

Hintergrund

Aus Sicht eines Kunden

Konzeptionell ist eine Prioritätswarteschlange eine Sammlung von Elementen, wobei jedem Element eine zugeordnete Priorität zugeordnet ist. Die wichtigste Funktionalität einer Prioritätswarteschlange besteht darin, dass sie einen effizienten Zugriff auf das Element mit der höchsten Priorität in der Sammlung und eine Option zum Entfernen dieses Elements bietet. Das erwartete Verhalten kann auch umfassen: 1) Fähigkeit, die Priorität eines Elements zu ändern, das sich bereits in der Sammlung befindet; 2) Fähigkeit, mehrere Prioritätswarteschlangen zusammenzuführen.

Informatik Hintergrund

Eine Prioritätswarteschlange ist eine abstrakte Datenstruktur, dh ein Konzept mit bestimmten Verhaltensmerkmalen, wie im vorherigen Abschnitt beschrieben. Die effizientesten Implementierungen einer Prioritätswarteschlange basieren auf Heaps. Ein Heap ist jedoch entgegen dem allgemeinen Missverständnis auch eine abstrakte Datenstruktur und kann auf verschiedene Weise realisiert werden, die jeweils unterschiedliche Vor- und Nachteile bietet.

Die meisten Software-Ingenieure sind nur mit der Array-basierten Binär-Heap-Implementierung vertraut – sie ist die einfachste, aber leider nicht die effizienteste. Für allgemeine zufällige Eingaben sind zwei Beispiele für effizientere Heap-Typen: quaternärer Heap und Paarungsheap . Weitere Informationen zu Heaps finden Sie in Wikipedia und in diesem Papier .

Update-Mechanismus ist die größte Herausforderung beim Design

Unsere Diskussionen haben gezeigt, dass der Aktualisierungsmechanismus der schwierigste Bereich im Design und gleichzeitig mit den größten Auswirkungen auf die API ist. Die Herausforderung besteht insbesondere darin, festzustellen, ob und wie das Produkt, das wir den Kunden anbieten möchten, die Aktualisierung der Prioritäten von bereits in der Kollektion vorhandenen Elementen unterstützen soll.

Eine solche Fähigkeit ist notwendig, um zB den kürzesten Pfad-Algorithmus von Dijkstra oder einen Job-Scheduler zu implementieren, der sich ändernde Prioritäten handhaben muss. Der Update-Mechanismus fehlt in Java, was sich für Ingenieure als enttäuschend erwiesen hat, zB in diesen drei StackOverflow-Fragen, die über 32.000 Mal aufgerufen wurden: example , example , example . Um die Einführung von APIs mit einem derart begrenzten Wert zu vermeiden, besteht unserer Ansicht nach eine grundlegende Anforderung für die von uns bereitgestellte Prioritätswarteschlangenfunktion darin, die Möglichkeit zu unterstützen, Prioritäten für bereits in der Sammlung vorhandene Elemente zu aktualisieren.

Um den Aktualisierungsmechanismus bereitzustellen, müssen wir sicherstellen, dass der Kunde genau angeben kann, was er genau aktualisieren möchte. Wir haben zwei Möglichkeiten identifiziert, dies zu liefern: a) über Handles; und b) durch Erzwingen der Einzigartigkeit von Elementen in der Sammlung. Jeder von ihnen hat unterschiedliche Vorteile und Kosten.

Option (a): Griffe. Bei diesem Ansatz stellt die Datenstruktur jedes Mal, wenn ein Element zur Warteschlange hinzugefügt wird, ihr eindeutiges Handle bereit. Wenn der Kunde den Update-Mechanismus nutzen möchte, muss er solche Handles im Auge behalten, um später eindeutig angeben zu können, welches Element er aktualisieren möchte. Die Hauptkosten dieser Lösung bestehen darin, dass Kunden diese Zeiger verwalten müssen. Dies bedeutet jedoch nicht, dass es interne Zuweisungen zur Unterstützung von Handles innerhalb der Prioritätswarteschlange geben muss – jeder nicht Array-basierte Heap basiert auf Nodes, wobei jeder Node automatisch sein eigener Handle ist. Sehen Sie sich als Beispiel die API der PairingHeap.Update-Methode an .

Option (b): Einzigartigkeit. Dieser Ansatz erlegt dem Kunden zwei zusätzliche Beschränkungen auf: i) Elemente innerhalb der Prioritätswarteschlange müssen einer bestimmten Gleichheitssemantik entsprechen, was eine neue Klasse potenzieller Benutzerfehler mit sich bringt; ii) zwei gleiche Elemente können nicht in derselben Warteschlange gespeichert werden. Durch die Zahlung dieser Kosten profitieren wir von der Unterstützung des Aktualisierungsmechanismus, ohne auf den Handle-Ansatz zurückgreifen zu müssen. Jede Implementierung, die Eindeutigkeit/Gleichheit nutzt, um das zu aktualisierende Element zu bestimmen, erfordert jedoch eine zusätzliche interne Zuordnung, sodass dies in O(1) und nicht in O(n) erfolgt.

Empfehlung

Wir empfehlen, der Systembibliothek eine Klasse PriorityQueue<TElement, TPriority> hinzuzufügen, die den Aktualisierungsmechanismus durch Handles unterstützt. Die zugrunde liegende Implementierung wäre ein Paarungsheap.

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

Anwendungsbeispiel

1) Kunde, dem der Update-Mechanismus egal ist

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) Kunde, der sich um den Update-Mechanismus kümmert

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

In welcher Reihenfolge zählt die Prioritätswarteschlange die Elemente auf?

In undefinierter Reihenfolge, damit die Aufzählung in O(n) erfolgen kann, ähnlich wie bei einem HashSet . Keine effiziente Implementierung würde die Möglichkeit bieten, einen Heap in linearer Zeit aufzuzählen und gleichzeitig sicherzustellen, dass die Elemente der Reihe nach aufgezählt werden – dies würde O(n log n) erfordern. Da die Bestellung einer Kollektion trivialerweise mit .OrderBy(x => x.Priority) und nicht jeder Kunde sich bei dieser Bestellung um die Aufzählung kümmert, halten wir es für besser, eine undefinierte Aufzählungsreihenfolge bereitzustellen.

Anhänge

Anhang A: Andere Sprachen mit Prioritätswarteschlangenfunktion

| Sprache | Typ | Anmerkungen |
|:-:|:-:|:-:|
| Java | Prioritätswarteschlange | Erweitert die abstrakte Klasse AbstractQueue und implementiert das Interface Queue . |
| Rost | BinaryHeap | |
| Schnell | CFBinaryHeap | |
| C++ | prioritätswarteschlange | |
| Python | Haufenq | |
| Los | Haufen | Es gibt eine Heap-Schnittstelle. |

Anhang B: Auffindbarkeit

Beachten Sie, dass bei der Diskussion von Datenstrukturen der Begriff _Heap_ 4-mal häufiger verwendet wird als _Priority Queue_.

  • "array" AND "data structure" — 17.400.000 Ergebnisse
  • "stack" AND "data structure" — 12.100.000 Ergebnisse
  • "queue" AND "data structure" — 3.850.000 Ergebnisse
  • "heap" AND "data structure" — 1.830.000 Ergebnisse
  • "priority queue" AND "data structure" — 430.000 Ergebnisse
  • "trie" AND "data structure" — 335.000 Ergebnisse

Bitte überprüfen Sie, freue mich über Feedback und iteriere weiter :) Ich habe das Gefühl, dass wir uns nähern! 😄 Freue mich auch über Fragen — werde sie in die FAQ aufnehmen!

@pgolebiowski Ich neige dazu, keine Handle-basierten APIs zu verwenden (ich würde erwarten, dies wie in Beispiel 2 zu

  • Es scheint seltsam, dass TryDequeue einen Knoten zurückgibt, da es zu diesem Zeitpunkt nicht wirklich ein Handle ist (ich würde zwei separate Out-Parameter bevorzugen)
  • Wird es insofern stabil sein, als dass, wenn zwei Elemente mit derselben Priorität in die Warteschlange gestellt werden, sie in einer vorhersagbaren Reihenfolge aus der Warteschlange entfernt werden? (nice to have aus Gründen der Reproduzierbarkeit; ansonsten leicht vom Verbraucher umsetzbar)
  • Der Parameter für Merge sollte PriorityQueue'2 nicht PriorityQueueNode'2 , und können Sie das Verhalten klären? Ich kenne den Pairing Heap nicht, aber vermutlich überlappen sich die beiden Heaps danach
  • Ich bin kein Fan des Namens Contains für die 2-Parameter-Methode: das ist kein Name, den ich für eine TryGet Methode vermuten würde
  • Sollte die Klasse ein benutzerdefiniertes IEqualityComparer<TElement> für Contains ?
  • Es scheint keine Möglichkeit zu geben, effizient zu bestimmen, ob sich ein Knoten noch im Heap befindet (nicht sicher, wann ich das verwenden würde, dachte)
  • Es ist seltsam, dass Remove ein bool zurückgibt; Ich würde erwarten, dass es TryRemove oder dass es warf (was ich annehme, dass Update tut, wenn sich der Knoten nicht im Heap befindet).

@VisualMelon vielen Dank für das Feedback! Werde diese schnell lösen, stimme definitiv zu:

  • Es scheint seltsam, dass TryDequeue einen Knoten zurückgibt, da es zu diesem Zeitpunkt nicht wirklich ein Handle ist (ich würde zwei separate Out-Parameter bevorzugen)
  • Der Parameter für Merge sollte PriorityQueue'2 nicht PriorityQueueNode'2 , und können Sie das Verhalten klären? Ich kenne den Pairing Heap nicht, aber vermutlich überlappen sich die beiden Heaps danach
  • Es ist seltsam, dass Remove ein bool zurückgibt; Ich würde erwarten, dass es TryRemove oder dass es warf (was ich annehme, dass Update tut, wenn sich der Knoten nicht im Heap befindet).
  • Ich bin kein Fan des Namens Contains für die 2-Parameter-Methode: das ist kein Name, den ich für eine Methode im Stil von TryGet vermuten würde

Klarstellung für diese beiden:

  • Wird es insofern stabil sein, als dass, wenn zwei Elemente mit derselben Priorität in die Warteschlange gestellt werden, sie in einer vorhersagbaren Reihenfolge aus der Warteschlange entfernt werden? (nice to have aus Gründen der Reproduzierbarkeit; ansonsten leicht vom Verbraucher umsetzbar)
  • Es scheint keine Möglichkeit zu geben, effizient zu bestimmen, ob sich ein Knoten noch im Heap befindet (nicht sicher, wann ich das verwenden würde, dachte)

Für den ersten Punkt, wenn das Ziel Reproduzierbarkeit ist, dann wird die Implementierung deterministisch sein, ja. Wenn das Ziel ist, _wenn zwei Elemente mit der gleichen Priorität in die Warteschlange gestellt werden, folgt daraus, dass sie in der gleichen Reihenfolge herauskommen, in der sie in die Warteschlange gestellt wurden_ — ich bin mir nicht sicher, ob die Implementierung angepasst werden kann diese Eigenschaft erreichen, wäre eine schnelle Antwort "wahrscheinlich nein".

Für den zweiten Punkt, ja, Heaps sind nicht gut, um zu überprüfen, ob ein Element in der Sammlung vorhanden ist, ein Kunde müsste dies separat verfolgen, um dies in O(1) zu erreichen, oder das Mapping, das sie für die Handles verwenden, wiederverwenden, wenn sie verwenden den Update-Mechanismus. Andernfalls O(n).

  • Sollte die Klasse ein benutzerdefiniertes IEqualityComparer<TElement> für Contains ?

Hmm ... Ich fange an zu denken, dass es vielleicht zu viel ist, Contains in die Verantwortung dieser Prioritätswarteschlange zu stellen, und die Verwendung der Methode von Linq könnte ausreichen ( Contains muss ohnehin bei der Aufzählung angewendet werden).

@pgolebiowski danke für die Klarstellungen

Für den ersten Punkt, wenn das Ziel Reproduzierbarkeit ist, wird die Implementierung deterministisch sein, ja

Nicht so sehr deterministisch, sondern für immer garantiert (zB ändert sich auch die Implementierung, das Verhalten würde sich nicht ändern), also denke ich, dass die Antwort, nach der ich gesucht habe, "Nein" lautet. Das ist in Ordnung: Der Konsument kann der Priorität eine Sequenz-ID hinzufügen, wenn er dies benötigt, obwohl zu diesem Zeitpunkt ein SortedSet die Arbeit erledigen würde.

Für den zweiten Punkt, ja, Heaps sind nicht gut, um zu überprüfen, ob ein Element in der Sammlung vorhanden ist, ein Kunde müsste dies separat verfolgen, um dies in O(1) zu erreichen, oder das Mapping, das sie für die Handles verwenden, wiederverwenden, wenn sie verwenden den Update-Mechanismus. Andernfalls O(n).

Benötigt es nicht eine Teilmenge der Arbeit, die für Remove erforderlich ist? Ich war vielleicht nicht klar: Ich meinte ein PriorityQueueNode , um zu prüfen, ob es sich im Heap befindet (nicht ein TElement ).

Hmm... Ich fange an zu denken, dass es vielleicht zu viel ist, Enthält die Verantwortung für diese Prioritätswarteschlange zu übernehmen

Ich würde mich nicht beschweren, wenn Contains nicht da wäre: Es ist auch eine Falle für Leute, die nicht wissen, dass sie Handles verwenden sollten.

@pgolebiowski Sie scheinen ziemlich stark für Griffe zu sein, denke ich richtig, dass es aus Effizienzgründen ist?

Aus Effizienzgesichtspunkten vermute ich, dass Handles wirklich die besten sind, für beide Szenarien mit Einzigartigkeit und ohne, also bin ich damit einverstanden, dass dies die primäre Lösung ist, die das Framework bietet.

Überhaupt:
Aus der Sicht der Benutzerfreundlichkeit denke ich, dass doppelte Elemente selten das sind, was ich will, was mich immer noch fragen lässt, ob es für das Framework sinnvoll ist, beide Modelle zu unterstützen. Aber ... eine für einzigartige Elemente geeignete "PrioritySet" -Klasse wäre zumindest später leicht als Wrapper für das vorgeschlagene PriorityQueue hinzuzufügen, zB als Reaktion auf die anhaltende Nachfrage nach einer benutzerfreundlicheren API. (wenn die Nachfrage besteht. Wie ich denke, könnte es sein!)

Für die aktuelle API, die sich selbst vorgeschlagen hat, ein paar Gedanken / Fragen:

  • wie wäre es auch mit einer Überladung von TryPeek(out TElement element, out TPriority priority) ?
  • Wenn Sie aktualisierbare doppelte Schlüssel haben, habe ich eine Sorge, dass, wenn "dequeue" den Prioritätswarteschlangenknoten nicht zurückgibt, wie würden Sie dann überprüfen, ob Sie den richtigen genauen Knoten aus Ihrem Handle-Tracking-System entfernen? Da Sie mehrere Kopien von Elementen mit derselben Priorität haben könnten.
  • Wird Remove(PriorityQueueNode) ausgelöst, wenn der Knoten nicht gefunden wird? oder false zurückgeben?
  • Sollte es eine TryRemove() Version geben, die nicht ausgelöst wird, wenn Remove wirft?
  • Ich bin mir nicht sicher, ob die API von Contains() die meiste Zeit nützlich ist? „Enthält“ scheint im Auge des Betrachters zu liegen, insbesondere bei Szenarien mit „doppelten“ Elementen mit unterschiedlichen Prioritäten oder anderen unterschiedlichen Merkmalen! In diesem Fall muss der Endbenutzer wahrscheinlich sowieso seine eigene Suche durchführen. Aber zumindest kann es für das Szenario ohne Duplikate nützlich sein.

@pgolebiowski Vielen Dank, dass Sie sich die Zeit genommen haben, den neuen Vorschlag zu entwerfen! Ein paar Anmerkungen von meiner Seite:

  • In Anlehnung an den Kommentar von Contains() oder TryGetNode() in der derzeit vorgeschlagenen Form überhaupt in der API vorhanden sein sollten. Sie implizieren, dass die Gleichheit für TElement Bedeutung ist, was vermutlich eines der Dinge ist, die ein Handle-basierter Ansatz vermeiden wollte.
  • Ich würde wahrscheinlich umformulieren public void Dequeue(out TElement element); als public TElement Dequeue();
  • Warum muss die Methode TryDequeue() eine Priorität zurückgeben?
  • Sollte die Klasse nicht auch ICollection<T> oder IReadOnlyCollection<T> implementieren?
  • Was sollte passieren, wenn ich versuche, einen PriorityQueueNode zu aktualisieren, der von einer anderen PriorityQueue-Instanz zurückgegeben wurde?
  • Wollen wir einen effizienten Zusammenführungsvorgang unterstützen? AFAICT dies impliziert, dass wir keine Array-basierte Darstellung verwenden können. Wie würde sich dies auf die Umsetzung in Bezug auf die Zuweisungen auswirken?

Die meisten Software-Ingenieure sind nur mit der Array-basierten Binär-Heap-Implementierung vertraut – sie ist die einfachste, aber leider nicht die effizienteste. Für allgemeine zufällige Eingaben sind zwei Beispiele für effizientere Heap-Typen: quaternärer Heap und Paarungsheap.

Was sind die Kompromisse bei der Wahl der letztgenannten Ansätze? Ist es möglich, eine Array-basierte Implementierung für diese zu verwenden?

Es kann ICollection<T> oder IReadOnlyCollection<T> nicht implementieren, wenn es nicht die richtigen Signaturen für 'Hinzufügen' und 'Entfernen' usw. hat.

Es ist in Geist / Form viel näher an ICollection<KeyValuePair<T,Priority>>

Es ist in Geist / Form viel näher an ICollection<KeyValuePair<T,Priority>>

Wäre es nicht KeyValuePair<TPriority, TElement> , da die Bestellung durch TPriority erfolgt, was effektiv ein Schlüsselmechanismus ist?

OK, es scheint, dass wir insgesamt dafür sind, die Methoden Contains und TryGet . Werde sie in der nächsten Überarbeitung entfernen und den Entfernungsgrund in den FAQ erläutern.

Was die implementierten Schnittstellen betrifft – ist IEnumerable<PriorityQueueNode<TElement, TPriority>> ausreichend? Welche Funktionalität fehlt?

Auf den KeyValuePair - es gab ein paar Stimmen, die ein Tupel oder eine Struktur mit .Element und .Priority wünschenswerter sind. Ich glaube, ich bin dafür.

Wäre es nicht KeyValuePair<TPriority, TElement> , da die Bestellung durch TPriority erfolgt, was effektiv ein Schlüsselmechanismus ist?

Es gibt gute Argumente für beide Seiten. Einerseits ja, genau das, was Sie gerade gesagt haben. Andererseits wird von Schlüsseln in einer KVP-Sammlung im Allgemeinen erwartet, dass sie eindeutig sind, und es ist absolut gültig, mehrere Elemente mit derselben Priorität zu haben.

Andererseits wird von Schlüsseln in einer KVP-Sammlung im Allgemeinen erwartet, dass sie einzigartig sind

Ich würde dieser Aussage nicht zustimmen - eine Sammlung von Schlüssel-Wert-Paaren ist genau das; alle Eindeutigkeitsanforderungen werden darüber geschichtet.

reicht IEnumerable<PriorityQueueNode<TElement, TPriority>> aus? Welche Funktionalität fehlt?

Ich persönlich würde erwarten, dass IReadOnlyCollection<PQN<TElement, TPriority>> implementiert wird, da die bereitgestellte API diese Schnittstelle bereits größtenteils erfüllt. Außerdem würde dies mit anderen Sammlungstypen übereinstimmen.

Zur Schnittstelle:

```
public bool TryGetNode(TElement element, out PriorityQueueNodeKnoten); // Auf)

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

Ich möchte auch, dass PriorityQueue die Schnittstelle IReadonlyCollection<> implementiert.

Springen zu anderen Dingen als der öffentlichen API-Oberfläche.

Eine solche Fähigkeit ist notwendig, um zB den kürzesten Pfad-Algorithmus von Dijkstra oder einen Job-Scheduler zu implementieren, der sich ändernde Prioritäten handhaben muss. Der Update-Mechanismus fehlt in Java, was sich für Ingenieure als enttäuschend erwiesen hat, zB in diesen drei StackOverflow-Fragen, die über 32.000 Mal aufgerufen wurden: Beispiel, Beispiel, Beispiel. Um die Einführung von APIs mit einem derart begrenzten Wert zu vermeiden, besteht unserer Ansicht nach eine grundlegende Anforderung für die von uns bereitgestellte Prioritätswarteschlangenfunktion darin, die Möglichkeit zu unterstützen, Prioritäten für bereits in der Sammlung vorhandene Elemente zu aktualisieren.

Ich möchte widersprechen. Das letzte Mal, als ich Dijkstra in C++ geschrieben habe, war std::priority_queue ausreichend und behandelt keine Prioritätsaktualisierung. Der gemeinsame AFAIK-

Um ehrlich zu sein, bin ich mir nicht sicher, wie Dijkstra mit dem aktuellen Warteschlangenvorschlag aussehen würde. Wie soll ich Knoten verfolgen, für die ich die Aktualisierungspriorität benötige? Mit TryGetNode()? Oder haben Sie eine andere Sammlung von Knoten? Würde gerne Code für den aktuellen Vorschlag sehen.

Wenn Sie in Wikipedia nachsehen, wird nicht davon ausgegangen, dass Prioritäten für die Prioritätswarteschlange aktualisiert werden. Gleiches gilt für alle anderen Sprachen, die diese Funktionalität nicht haben und damit durchgekommen sind. Ich weiß "strebe, besser zu sein", aber gibt es dafür tatsächlich eine Nachfrage?

Für allgemeine zufällige Eingaben sind zwei Beispiele für effizientere Heap-Typen: quaternärer Heap und Paarungsheap. Weitere Informationen zu Heaps finden Sie in Wikipedia und in diesem Papier.

Ich habe in Papier geschaut und dies ist ein Zitat daraus:

Die Ergebnisse zeigen, dass die optimale Wahl der Implementierung stark inputabhängig ist. Darüber hinaus zeigt es, dass darauf geachtet werden muss, die Cache-Leistung zu optimieren, hauptsächlich an der L1-L2-Barriere. Dies deutet darauf hin, dass komplizierte, Cache-unbewusste Strukturen im Vergleich zu einfacheren, Cache-bewussten Strukturen wahrscheinlich nicht gut funktionieren.

Wie es aussieht, wäre der aktuelle Warteschlangenvorschlag eher hinter einer Baumimplementierung als einem Array eingeschlossen, und etwas sagt mir, welche Chance besteht, dass Baumknoten, die im Speicher verstreut sind, nicht so leistungsfähig sind wie ein Array von Elementen.

Ich denke, Benchmarks zum Vergleich von einfachen binären Heaps basierend auf Array und Pairing Heap wären ideal, um die richtige Entscheidung zu treffen. Vorher halte ich es nicht für klug, das Design hinter einer bestimmten Implementierung zu sperren (ich schaue auf dich Merge Methode).

Um zu einem anderen Thema zu springen, würde ich persönlich lieber KeyValuePair habenals neue benutzerdefinierte Klasse.

  • Weniger API-Oberfläche
  • Ich kann so etwas tun: `new PriorityQueue(neues Wörterbuch() {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}); Ich weiß, dass es durch die Eindeutigkeit des Schlüssels begrenzt ist. Ich denke auch, dass es schöne Synergien bringen würde, IDictionary-basierte Sammlungen zu konsumieren.
  • Es ist eher eine Struktur als eine Klasse, also keine NullReference-Ausnahmen
  • Irgendwann müsste PrioirtyQueue serialisiert/deserialisiert werden und ich denke, es wäre einfacher, was mit bereits vorhandenen Objekten zu tun.

Der Nachteil sind gemischte Gefühle bei der Betrachtung von TPriority Key aber ich denke, eine gute Dokumentation kann das lösen.
`

Als Workaround für Dijkstra funktioniert das Hinzufügen von gefälschten Elementen zur Warteschlange, und die Gesamtzahl der von Ihnen verarbeiteten Graphkanten ändert sich nicht. Aber die Anzahl der temporären Knoten, die in dem durch die Verarbeitung von Kanten erzeugten Heap resident bleiben, ändert sich, und das kann sich auf die Speichernutzung und die Effizienz von Enqueue und Dequeue auswirken.

Ich habe mich geirrt, weil ich IReadOnlyCollection nicht ausführen konnte, das sollte in Ordnung sein. Kein Add() und Remove() in dieser Schnittstelle, hah! (Was habe ich mir gedacht...)

@Ivanidzo4ka Ihre Kommentare überzeugen mich noch mehr davon, dass es sinnvoll wäre, zwei separate Typen zu haben: einen einfachen binären Heap (dh ohne Update) und einen zweiten, wie in einem der Vorschläge beschrieben:

  • Binary Heaps sind einfach, leicht zu implementieren, haben eine kleine API und sind in der Praxis in vielen wichtigen Szenarien dafür bekannt, dass sie gut funktionieren.
  • Eine vollwertige Prioritätswarteschlange bietet für einige Szenarien theoretische Vorteile (dh reduzierte Raumkomplexität und reduzierte Zeitkomplexität für die Suche in dicht verbundenen Graphen) und stellt eine natürliche API für mehr Algorithmen/Szenarien bereit.

In der Vergangenheit bin ich meistens mit einem binären Haufen davongekommen, den ich vor einem Jahrzehnt geschrieben habe, und einer Kombination von SortedSet / Dictionary , und es gibt Szenarien, in denen ich verwendet habe beide Typen in getrennten Rollen (da fällt mir spontan KSSP ein).

Es ist eher eine Struktur als eine Klasse, also keine NullReference-Ausnahmen

Ich würde argumentieren, dass es umgekehrt ist: Wenn jemand Standardwerte weitergibt, würde ich es begrüßen, wenn er eine NRE sieht; Ich denke jedoch, wir müssen uns klar machen, wo wir diese Dinge erwarten: Knoten/Handles sollten wahrscheinlich Klassen sein, aber wenn wir nur ein Paar lesen/zurückgeben, dann stimme ich zu, dass es eine Struktur sein sollte.


Ich bin versucht vorzuschlagen, dass es möglich sein sollte, sowohl das Element als auch die Priorität im auf Handles basierenden Vorschlag zu aktualisieren. Sie können den gleichen Effekt erzielen, indem Sie einfach ein Handle entfernen und ein neues hinzufügen, aber es ist eine nützliche Operation und kann je nach Implementierung Leistungsvorteile haben (zB können einige Heaps die Priorität von etwas relativ billig reduzieren). Eine solche Änderung würde die Implementierung vieler Dinge aufgeräumter machen (zB dieses etwas Albtraum verursachende Beispiel basierend auf dem bestehenden AlgoKit ParingHeap), insbesondere diejenigen, die in einer unbekannten Region des Zustandsraums operieren.

Vorschlag für Prioritätswarteschlange (v2.1)

Zusammenfassung

Die .NET Core-Community schlägt vor, der Systembibliothek die _priority queue_-Funktionalität hinzuzufügen, eine Datenstruktur, in der jedem Element zusätzlich eine Priorität zugeordnet ist. Insbesondere schlagen wir vor, PriorityQueue<TElement, TPriority> zum Namensraum System.Collections.Generic hinzuzufügen.

Grundsätze

Bei unserem Design haben wir uns von den folgenden Grundsätzen leiten lassen (es sei denn, Sie kennen bessere):

  • Breite Abdeckung. Wir möchten .NET Core-Kunden eine wertvolle Datenstruktur bieten, die vielseitig genug ist, um eine Vielzahl von Anwendungsfällen zu unterstützen.
  • Lernen Sie aus bekannten Fehlern. Wir bemühen uns, Prioritätswarteschlangenfunktionalität bereitzustellen, die frei von den kundenseitigen Problemen ist, die in anderen Frameworks und Sprachen vorhanden sind, z. B. Java, Python, C++, Rust. Wir werden vermeiden, Designentscheidungen zu treffen, von denen bekannt ist, dass sie Kunden unzufrieden machen und die Nützlichkeit von Prioritätswarteschlangen verringern.
  • Äußerste Sorgfalt bei 1-Weg-Türentscheidungen. Sobald eine API eingeführt wurde, kann sie nicht mehr geändert oder gelöscht, sondern nur erweitert werden. Wir werden die Designentscheidungen sorgfältig analysieren, um suboptimale Lösungen zu vermeiden, an denen unsere Kunden für immer festhalten werden.
  • Vermeiden Sie Designlähmungen. Wir akzeptieren, dass es möglicherweise keine perfekte Lösung gibt. Wir werden Kompromisse abwägen und die Lieferung vorantreiben, um unseren Kunden endlich die Funktionalität zu liefern, auf die sie seit Jahren warten.

Hintergrund

Aus Sicht eines Kunden

Konzeptionell ist eine Prioritätswarteschlange eine Sammlung von Elementen, wobei jedem Element eine zugeordnete Priorität zugeordnet ist. Die wichtigste Funktionalität einer Prioritätswarteschlange besteht darin, dass sie einen effizienten Zugriff auf das Element mit der höchsten Priorität in der Sammlung und eine Option zum Entfernen dieses Elements bietet. Das erwartete Verhalten kann auch umfassen: 1) Fähigkeit, die Priorität eines Elements zu ändern, das sich bereits in der Sammlung befindet; 2) Fähigkeit, mehrere Prioritätswarteschlangen zusammenzuführen.

Informatik Hintergrund

Eine Prioritätswarteschlange ist eine abstrakte Datenstruktur, dh ein Konzept mit bestimmten Verhaltensmerkmalen, wie im vorherigen Abschnitt beschrieben. Die effizientesten Implementierungen einer Prioritätswarteschlange basieren auf Heaps. Ein Heap ist jedoch entgegen dem allgemeinen Missverständnis auch eine abstrakte Datenstruktur und kann auf verschiedene Weise realisiert werden, die jeweils unterschiedliche Vor- und Nachteile bietet.

Die meisten Software-Ingenieure sind nur mit der Array-basierten Binär-Heap-Implementierung vertraut – sie ist die einfachste, aber leider nicht die effizienteste. Für allgemeine zufällige Eingaben sind zwei Beispiele für effizientere Heap-Typen: quaternärer Heap und Paarungsheap . Weitere Informationen zu Heaps finden Sie in Wikipedia und in diesem Papier .

Update-Mechanismus ist die größte Herausforderung beim Design

Unsere Diskussionen haben gezeigt, dass der Aktualisierungsmechanismus der schwierigste Bereich im Design und gleichzeitig mit den größten Auswirkungen auf die API ist. Die Herausforderung besteht insbesondere darin, festzustellen, ob und wie das Produkt, das wir den Kunden anbieten möchten, die Aktualisierung der Prioritäten von bereits in der Kollektion vorhandenen Elementen unterstützen soll.

Eine solche Fähigkeit ist notwendig, um zB den kürzesten Pfad-Algorithmus von Dijkstra oder einen Job-Scheduler zu implementieren, der sich ändernde Prioritäten handhaben muss. Der Update-Mechanismus fehlt in Java, was sich für Ingenieure als enttäuschend erwiesen hat, zB in diesen drei StackOverflow-Fragen, die über 32.000 Mal aufgerufen wurden: example , example , example . Um die Einführung von APIs mit einem derart begrenzten Wert zu vermeiden, besteht unserer Ansicht nach eine grundlegende Anforderung für die von uns bereitgestellte Prioritätswarteschlangenfunktion darin, die Möglichkeit zu unterstützen, Prioritäten für bereits in der Sammlung vorhandene Elemente zu aktualisieren.

Um den Aktualisierungsmechanismus bereitzustellen, müssen wir sicherstellen, dass der Kunde genau angeben kann, was er genau aktualisieren möchte. Wir haben zwei Möglichkeiten identifiziert, dies zu liefern: a) über Handles; und b) durch Erzwingen der Einzigartigkeit von Elementen in der Sammlung. Jeder von ihnen hat unterschiedliche Vorteile und Kosten.

Option (a): Griffe. Bei diesem Ansatz stellt die Datenstruktur jedes Mal, wenn ein Element zur Warteschlange hinzugefügt wird, ihr eindeutiges Handle bereit. Wenn der Kunde den Update-Mechanismus nutzen möchte, muss er solche Handles im Auge behalten, um später eindeutig angeben zu können, welches Element er aktualisieren möchte. Die Hauptkosten dieser Lösung bestehen darin, dass Kunden diese Zeiger verwalten müssen. Dies bedeutet jedoch nicht, dass es interne Zuweisungen zur Unterstützung von Handles innerhalb der Prioritätswarteschlange geben muss – jeder nicht Array-basierte Heap basiert auf Nodes, wobei jeder Node automatisch sein eigener Handle ist. Sehen Sie sich als Beispiel die API der PairingHeap.Update-Methode an .

Option (b): Einzigartigkeit. Dieser Ansatz erlegt dem Kunden zwei zusätzliche Beschränkungen auf: i) Elemente innerhalb der Prioritätswarteschlange müssen einer bestimmten Gleichheitssemantik entsprechen, was eine neue Klasse potenzieller Benutzerfehler mit sich bringt; ii) zwei gleiche Elemente können nicht in derselben Warteschlange gespeichert werden. Durch die Zahlung dieser Kosten profitieren wir von der Unterstützung des Aktualisierungsmechanismus, ohne auf den Handle-Ansatz zurückgreifen zu müssen. Jede Implementierung, die Eindeutigkeit/Gleichheit nutzt, um das zu aktualisierende Element zu bestimmen, erfordert jedoch eine zusätzliche interne Zuordnung, sodass dies in O(1) und nicht in O(n) erfolgt.

Empfehlung

Wir empfehlen, der Systembibliothek eine Klasse PriorityQueue<TElement, TPriority> hinzuzufügen, die den Aktualisierungsmechanismus durch Handles unterstützt. Die zugrunde liegende Implementierung wäre ein Paarungsheap.

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

Anwendungsbeispiel

1) Kunde, dem der Update-Mechanismus egal ist

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) Kunde, der sich um den Update-Mechanismus kümmert

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. In welcher Reihenfolge zählt die Prioritätswarteschlange die Elemente auf?

In undefinierter Reihenfolge, damit die Aufzählung in O(n) erfolgen kann, ähnlich wie bei einem HashSet . Keine effiziente Implementierung würde die Möglichkeit bieten, einen Heap in linearer Zeit aufzuzählen und gleichzeitig sicherzustellen, dass die Elemente der Reihe nach aufgezählt werden – dies würde O(n log n) erfordern. Da die Bestellung einer Kollektion trivialerweise mit .OrderBy(x => x.Priority) und nicht jeder Kunde sich bei dieser Bestellung um die Aufzählung kümmert, halten wir es für besser, eine undefinierte Aufzählungsreihenfolge bereitzustellen.

2. Warum gibt es keine Methode Contains oder TryGet ?

Die Bereitstellung solcher Methoden hat einen vernachlässigbaren Wert, da das Auffinden eines Elements in einem Heap die Aufzählung der gesamten Sammlung erfordert, was bedeutet, dass jede Contains oder TryGet Methode ein Wrapper um die Aufzählung wäre. Um zu überprüfen, ob ein Element in der Sammlung vorhanden ist, müsste die Prioritätswarteschlange außerdem wissen, wie Gleichheitsprüfungen von TElement Objekten durchgeführt werden, was unserer Meinung nach nicht in die Verantwortung einer Prioritätswarteschlange fallen sollte.

3. Warum gibt es Dequeue und TryDequeue Überladungen, die PriorityQueueNode ?

Dies ist für Kunden gedacht, die die Methode Update oder Remove verwenden und die Handles im Auge behalten möchten. Beim Entfernen eines Elements aus der Prioritätswarteschlange erhalten sie ein Handle, mit dem sie den Status ihres Handle-Tracking-Systems anpassen können.

4. Was passiert, wenn die Methode Update oder Remove einen Knoten aus einer anderen Warteschlange empfängt?

Die Prioritätswarteschlange löst eine Ausnahme aus. Jeder Knoten kennt die Prioritätswarteschlange, zu der er gehört, ähnlich wie ein LinkedListNode<T> die LinkedList<T> kennt, zu der er gehört.

Anhänge

Anhang A: Andere Sprachen mit Prioritätswarteschlangenfunktion

| Sprache | Typ | Anmerkungen |
|:-:|:-:|:-:|
| Java | Prioritätswarteschlange | Erweitert die abstrakte Klasse AbstractQueue und implementiert das Interface Queue . |
| Rost | BinaryHeap | |
| Schnell | CFBinaryHeap | |
| C++ | prioritätswarteschlange | |
| Python | Haufenq | |
| Los | Haufen | Es gibt eine Heap-Schnittstelle. |

Anhang B: Auffindbarkeit

Beachten Sie, dass bei der Diskussion von Datenstrukturen der Begriff _Heap_ 4-mal häufiger verwendet wird als _Priority Queue_.

  • "array" AND "data structure" — 17.400.000 Ergebnisse
  • "stack" AND "data structure" — 12.100.000 Ergebnisse
  • "queue" AND "data structure" — 3.850.000 Ergebnisse
  • "heap" AND "data structure" — 1.830.000 Ergebnisse
  • "priority queue" AND "data structure" — 430.000 Ergebnisse
  • "trie" AND "data structure" — 335.000 Ergebnisse

Vielen Dank für die Rückmeldungen! Ich habe den Vorschlag auf v2.1 aktualisiert. Änderungsprotokoll:

  • Die Methoden Contains und TryGet .
  • FAQ #2 hinzugefügt: _Warum gibt es keine Methode Contains oder TryGet ?_
  • Schnittstelle IReadOnlyCollection<PriorityQueueNode<TElement, TPriority>> hinzugefügt.
  • Eine Überladung von bool TryPeek(out TElement element, out TPriority priority) hinzugefügt.
  • Eine Überladung von bool TryPeek(out TElement element) hinzugefügt.
  • Eine Überladung von void Dequeue(out TElement element, out TPriority priority) hinzugefügt.
  • void Dequeue(out TElement element) in PriorityQueueNode<TElement, TPriority> Dequeue() geändert.
  • Eine Überladung von bool TryDequeue(out TElement element) hinzugefügt.
  • Eine Überladung von bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node) hinzugefügt.
  • FAQ #3 hinzugefügt: _Warum gibt es Dequeue und TryDequeue Überladungen die PriorityQueueNode ?_
  • FAQ #4 hinzugefügt: _Was passiert, wenn die Methode Update oder Remove einen Knoten aus einer anderen Warteschlange erhält?_

Danke für die Änderungshinweise ;)

Kleine Bitte um Klarstellung:

  • Können wir FAQ 4 so qualifizieren, dass es für Elemente gilt, die sich nicht in der Warteschlange befinden? (dh diejenigen entfernt)
  • Können wir eine FAQ zur Stabilität hinzufügen, dh zu den Garantien (falls vorhanden) beim Entfernen von Elementen mit derselben Priorität (mein Verständnis ist nicht geplant, Garantien zu geben, was z. B. für die Planung wichtig ist).

@pgolebiowski Bezüglich der vorgeschlagenen Methode Merge :

public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)

Offensichtlich hätte eine solche Operation keine Kopiersemantik, daher frage ich mich, ob es irgendwelche Fallstricke gibt, wenn Änderungen an this und other _nach_ eine Zusammenführung durchgeführt wurde (z um die Heap-Eigenschaft zu erfüllen).

@eiriktsarpalis @VisualMelon — Danke! Wird auf angesprochene Punkte eingehen, ETA 2020-10-04.

Wenn andere noch mehr Feedback/Fragen/Anliegen/Gedanken haben – bitte teilen 😊

Vorschlag für Prioritätswarteschlange (v2.2)

Zusammenfassung

Die .NET Core-Community schlägt vor, der Systembibliothek die _priority queue_-Funktionalität hinzuzufügen, eine Datenstruktur, in der jedem Element zusätzlich eine Priorität zugeordnet ist. Insbesondere schlagen wir vor, PriorityQueue<TElement, TPriority> zum Namensraum System.Collections.Generic hinzuzufügen.

Grundsätze

Bei unserem Design haben wir uns von den folgenden Grundsätzen leiten lassen (es sei denn, Sie kennen bessere):

  • Breite Abdeckung. Wir möchten .NET Core-Kunden eine wertvolle Datenstruktur bieten, die vielseitig genug ist, um eine Vielzahl von Anwendungsfällen zu unterstützen.
  • Lernen Sie aus bekannten Fehlern. Wir bemühen uns, Prioritätswarteschlangenfunktionalität bereitzustellen, die frei von den kundenseitigen Problemen ist, die in anderen Frameworks und Sprachen vorhanden sind, z. B. Java, Python, C++, Rust. Wir werden vermeiden, Designentscheidungen zu treffen, von denen bekannt ist, dass sie Kunden unzufrieden machen und die Nützlichkeit von Prioritätswarteschlangen verringern.
  • Äußerste Sorgfalt bei 1-Weg-Türentscheidungen. Sobald eine API eingeführt wurde, kann sie nicht mehr geändert oder gelöscht, sondern nur erweitert werden. Wir werden die Designentscheidungen sorgfältig analysieren, um suboptimale Lösungen zu vermeiden, an denen unsere Kunden für immer festhalten werden.
  • Vermeiden Sie Designlähmungen. Wir akzeptieren, dass es möglicherweise keine perfekte Lösung gibt. Wir werden Kompromisse abwägen und die Lieferung vorantreiben, um unseren Kunden endlich die Funktionalität zu liefern, auf die sie seit Jahren warten.

Hintergrund

Aus Sicht eines Kunden

Konzeptionell ist eine Prioritätswarteschlange eine Sammlung von Elementen, wobei jedem Element eine zugeordnete Priorität zugeordnet ist. Die wichtigste Funktionalität einer Prioritätswarteschlange besteht darin, dass sie einen effizienten Zugriff auf das Element mit der höchsten Priorität in der Sammlung und eine Option zum Entfernen dieses Elements bietet. Das erwartete Verhalten kann auch umfassen: 1) Fähigkeit, die Priorität eines Elements zu ändern, das sich bereits in der Sammlung befindet; 2) Fähigkeit, mehrere Prioritätswarteschlangen zusammenzuführen.

Informatik Hintergrund

Eine Prioritätswarteschlange ist eine abstrakte Datenstruktur, dh ein Konzept mit bestimmten Verhaltensmerkmalen, wie im vorherigen Abschnitt beschrieben. Die effizientesten Implementierungen einer Prioritätswarteschlange basieren auf Heaps. Ein Heap ist jedoch entgegen dem allgemeinen Missverständnis auch eine abstrakte Datenstruktur und kann auf verschiedene Weise realisiert werden, die jeweils unterschiedliche Vor- und Nachteile bietet.

Die meisten Software-Ingenieure sind nur mit der Array-basierten Binär-Heap-Implementierung vertraut – sie ist die einfachste, aber leider nicht die effizienteste. Für allgemeine zufällige Eingaben sind zwei Beispiele für effizientere Heap-Typen: quaternärer Heap und Paarungsheap . Weitere Informationen zu Heaps finden Sie in Wikipedia und in diesem Papier .

Update-Mechanismus ist die größte Herausforderung beim Design

Unsere Diskussionen haben gezeigt, dass der Aktualisierungsmechanismus der schwierigste Bereich im Design und gleichzeitig mit den größten Auswirkungen auf die API ist. Die Herausforderung besteht insbesondere darin, festzustellen, ob und wie das Produkt, das wir den Kunden anbieten möchten, die Aktualisierung der Prioritäten von bereits in der Kollektion vorhandenen Elementen unterstützen soll.

Eine solche Fähigkeit ist notwendig, um zB den kürzesten Pfad-Algorithmus von Dijkstra oder einen Job-Scheduler zu implementieren, der sich ändernde Prioritäten handhaben muss. Der Update-Mechanismus fehlt in Java, was sich für Ingenieure als enttäuschend erwiesen hat, zB in diesen drei StackOverflow-Fragen, die über 32.000 Mal aufgerufen wurden: example , example , example . Um die Einführung von APIs mit einem derart begrenzten Wert zu vermeiden, besteht unserer Ansicht nach eine grundlegende Anforderung für die von uns bereitgestellte Prioritätswarteschlangenfunktion darin, die Möglichkeit zu unterstützen, Prioritäten für bereits in der Sammlung vorhandene Elemente zu aktualisieren.

Um den Aktualisierungsmechanismus bereitzustellen, müssen wir sicherstellen, dass der Kunde genau angeben kann, was er genau aktualisieren möchte. Wir haben zwei Möglichkeiten identifiziert, dies zu liefern: a) über Handles; und b) durch Erzwingen der Einzigartigkeit von Elementen in der Sammlung. Jeder von ihnen hat unterschiedliche Vorteile und Kosten.

Option (a): Griffe. Bei diesem Ansatz stellt die Datenstruktur jedes Mal, wenn ein Element zur Warteschlange hinzugefügt wird, ihr eindeutiges Handle bereit. Wenn der Kunde den Update-Mechanismus nutzen möchte, muss er solche Handles im Auge behalten, um später eindeutig angeben zu können, welches Element er aktualisieren möchte. Die Hauptkosten dieser Lösung bestehen darin, dass Kunden diese Zeiger verwalten müssen. Dies bedeutet jedoch nicht, dass es interne Zuweisungen zur Unterstützung von Handles innerhalb der Prioritätswarteschlange geben muss – jeder nicht Array-basierte Heap basiert auf Nodes, wobei jeder Node automatisch sein eigener Handle ist. Sehen Sie sich als Beispiel die API der PairingHeap.Update-Methode an .

Option (b): Einzigartigkeit. Dieser Ansatz erlegt dem Kunden zwei zusätzliche Beschränkungen auf: i) Elemente innerhalb der Prioritätswarteschlange müssen einer bestimmten Gleichheitssemantik entsprechen, was eine neue Klasse potenzieller Benutzerfehler mit sich bringt; ii) zwei gleiche Elemente können nicht in derselben Warteschlange gespeichert werden. Durch die Zahlung dieser Kosten profitieren wir von der Unterstützung des Aktualisierungsmechanismus, ohne auf den Handle-Ansatz zurückgreifen zu müssen. Jede Implementierung, die Eindeutigkeit/Gleichheit nutzt, um das zu aktualisierende Element zu bestimmen, erfordert jedoch eine zusätzliche interne Zuordnung, sodass dies in O(1) und nicht in O(n) erfolgt.

Empfehlung

Wir empfehlen, der Systembibliothek eine Klasse PriorityQueue<TElement, TPriority> hinzuzufügen, die den Aktualisierungsmechanismus durch Handles unterstützt. Die zugrunde liegende Implementierung wäre ein Paarungsheap.

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

Anwendungsbeispiel

1) Kunde, dem der Update-Mechanismus egal ist

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) Kunde, der sich um den Update-Mechanismus kümmert

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. In welcher Reihenfolge zählt die Prioritätswarteschlange die Elemente auf?

In undefinierter Reihenfolge, damit die Aufzählung in O(n) erfolgen kann, ähnlich wie bei einem HashSet . Keine effiziente Implementierung würde die Möglichkeit bieten, einen Heap in linearer Zeit aufzuzählen und gleichzeitig sicherzustellen, dass die Elemente der Reihe nach aufgezählt werden – dies würde O(n log n) erfordern. Da die Bestellung einer Kollektion trivialerweise mit .OrderBy(x => x.Priority) und nicht jeder Kunde sich bei dieser Bestellung um die Aufzählung kümmert, halten wir es für besser, eine undefinierte Aufzählungsreihenfolge bereitzustellen.

2. Warum gibt es keine Methode Contains oder TryGet ?

Die Bereitstellung solcher Methoden hat einen vernachlässigbaren Wert, da das Auffinden eines Elements in einem Heap die Aufzählung der gesamten Sammlung erfordert, was bedeutet, dass jede Contains oder TryGet Methode ein Wrapper um die Aufzählung wäre. Um zu überprüfen, ob ein Element in der Sammlung vorhanden ist, müsste die Prioritätswarteschlange außerdem wissen, wie Gleichheitsprüfungen von TElement Objekten durchgeführt werden, was unserer Meinung nach nicht in die Verantwortung einer Prioritätswarteschlange fallen sollte.

3. Warum gibt es Dequeue und TryDequeue Überladungen, die PriorityQueueNode ?

Dies ist für Kunden gedacht, die die Methode Update oder Remove verwenden und die Handles im Auge behalten möchten. Beim Entfernen eines Elements aus der Prioritätswarteschlange erhalten sie ein Handle, mit dem sie den Status ihres Handle-Tracking-Systems anpassen können.

4. Was passiert, wenn die Methode Update oder Remove einen Knoten aus einer anderen Warteschlange empfängt?

Die Prioritätswarteschlange löst eine Ausnahme aus. Jeder Knoten kennt die Prioritätswarteschlange, zu der er gehört, ähnlich wie ein LinkedListNode<T> die LinkedList<T> kennt, zu der er gehört. Wenn ein Knoten aus einer Warteschlange entfernt wurde, führt auch der Versuch, Update oder Remove auf ihm aufzurufen, ebenfalls zu einer Ausnahme.

5. Warum gibt es keine Methode Merge ?

Das Zusammenführen von zwei Prioritätswarteschlangen kann in konstanter Zeit erreicht werden, was es zu einer verlockenden Funktion macht, Kunden anzubieten. Wir haben jedoch keine Daten, die belegen, dass eine solche Funktionalität erforderlich ist, und wir können die Aufnahme in die öffentliche API nicht rechtfertigen. Darüber hinaus ist das Design einer solchen Funktionalität nicht trivial, und da dieses Feature möglicherweise nicht benötigt wird, könnte es die API-Oberfläche und -Implementierung unnötig komplizieren.

Die Methode Merge jetzt nicht einzubeziehen ist jedoch eine Zweiwegetür – wenn Kunden in Zukunft Interesse an der Unterstützung der Merge-Funktionalität bekunden, wird es möglich sein, den Typ PriorityQueue . Daher empfehlen wir, die Methode Merge noch nicht einzubeziehen und mit der Einführung fortzufahren.

6. Bietet die Kollektion eine Stabilitätsgarantie?

Die Sammlung bietet keine Stabilitätsgarantie out of the box, dh wenn zwei Elemente mit der gleichen Priorität eingereiht werden, kann der Kunde nicht davon ausgehen, dass sie in einer bestimmten Reihenfolge ausgebucht werden. Wenn ein Kunde jedoch mit unserem PriorityQueue Stabilität erreichen möchte, könnte er ein TPriority und das entsprechende IComparer<TPriority> , das dies gewährleistet. Außerdem wird die Datenerhebung deterministisch sein, dh für eine gegebene Abfolge von Operationen wird sie sich immer gleich verhalten, was eine Reproduzierbarkeit ermöglicht.

Anhänge

Anhang A: Andere Sprachen mit Prioritätswarteschlangenfunktion

| Sprache | Typ | Anmerkungen |
|:-:|:-:|:-:|
| Java | Prioritätswarteschlange | Erweitert die abstrakte Klasse AbstractQueue und implementiert das Interface Queue . |
| Rost | BinaryHeap | |
| Schnell | CFBinaryHeap | |
| C++ | prioritätswarteschlange | |
| Python | Haufenq | |
| Los | Haufen | Es gibt eine Heap-Schnittstelle. |

Anhang B: Auffindbarkeit

Beachten Sie, dass bei der Diskussion von Datenstrukturen der Begriff _Heap_ 4-mal häufiger verwendet wird als _Priority Queue_.

  • "array" AND "data structure" — 17.400.000 Ergebnisse
  • "stack" AND "data structure" — 12.100.000 Ergebnisse
  • "queue" AND "data structure" — 3.850.000 Ergebnisse
  • "heap" AND "data structure" — 1.830.000 Ergebnisse
  • "priority queue" AND "data structure" — 430.000 Ergebnisse
  • "trie" AND "data structure" — 335.000 Ergebnisse

Änderungsprotokoll:

  • Die Methode void Merge(PriorityQueue<TElement, TPriority> other) // O(1) .
  • FAQ #5 hinzugefügt: Warum gibt es keine Methode Merge ?
  • FAQ Nr. 4 wurde geändert, um auch für Knoten zu gelten, die aus der Prioritätswarteschlange entfernt wurden.
  • FAQ #6 hinzugefügt: Bietet die Sammlung eine Stabilitätsgarantie?

Die neue FAQ sieht toll aus. Ich habe mit der Codierung von Dijkstra gegen die vorgeschlagene API herumgespielt, mit Handle Dictionary, und es schien im Grunde gut zu sein.

Die einzige kleine Erkenntnis, die ich dabei hatte, war, dass der aktuelle Satz von Methodennamen/Überladungen für die implizite Eingabe von out Variablen nicht so gut funktioniert. Was ich mit dem C#-Code machen wollte, war TryDequeue(out var node) - aber leider musste ich den expliziten Typ der out Variablen als PriorityQueueNode<> angeben, sonst wusste der Compiler nicht, ob ich wollte einen Prioritätswarteschlangenknoten oder ein Element.

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

Die wichtigste ungelöste Designfrage ist aus meiner Sicht, ob die Implementierung vorrangige Updates unterstützen sollte oder nicht. Ich sehe hier drei mögliche Pfade:

  1. Erfordern Sie Einzigartigkeit/Gleichheit und unterstützen Sie Aktualisierungen, indem Sie das ursprüngliche Element übergeben.
  2. Unterstützen Sie Prioritätsupdates mithilfe von Handles.
  3. Unterstützen Sie keine Prioritätsupdates.

Diese Ansätze schließen sich gegenseitig aus und haben jeweils ihre eigenen Kompromisse:

  1. Der gleichstellungsbasierte Ansatz verkompliziert den API-Vertrag, indem er Einzigartigkeit erzwingt und erfordert zusätzliche Buchhaltung unter der Haube. Dies geschieht unabhängig davon, ob der Benutzer Prioritätsaktualisierungen benötigt oder nicht.
  2. Der handhabungsbasierte Ansatz würde mindestens eine zusätzliche Zuweisung pro in die Warteschlange eingereihtes Element implizieren. Obwohl es keine Eindeutigkeit erzwingt, ist mein Eindruck, dass diese Invariante für Szenarien, die Aktualisierungen benötigen, mit ziemlicher Sicherheit implizit ist (beachten Sie beispielsweise, wie beide oben aufgeführten Beispiele Handles in externen Wörterbüchern speichern, die von den Elementen selbst indiziert sind).
  3. Aktualisierungen werden entweder überhaupt nicht unterstützt oder erfordern eine lineare Durchquerung des Heaps. Bei doppelten Elementen können Aktualisierungen mehrdeutig sein.

Identifizieren allgemeiner PriorityQueue-Anwendungen

Es ist wichtig, dass wir herausfinden, welcher der oben genannten Ansätze für die meisten unserer Benutzer den besten Wert bietet. Also habe ich .NET-Codebasen durchgesehen, sowohl Microsoft-intern als auch öffentlich, für Instanzen von Heap/PriorityQueue-Implementierungen, um besser zu verstehen, was die gängigsten Nutzungsmuster sind:

Die meisten Anwendungen implementieren Varianten von Heap-Sortierung oder Job-Scheduling. Einige Beispiele wurden für Algorithmen wie die topologische Sortierung oder die Huffman-Codierung verwendet. Eine kleinere Zahl wurde für die Berechnung von Entfernungen in Diagrammen verwendet. Von den 80 untersuchten PriorityQueue-Implementierungen wurde festgestellt, dass nur 9 irgendeine Form von Prioritätsaktualisierungen implementiert haben.

Gleichzeitig unterstützt keine der Heap/PriorityQueue-Implementierungen in Python-, Java-, C++-, Go-, Swift- oder Rust-Kernbibliotheken Prioritätsaktualisierungen in ihren APIs.

Empfehlungen

Angesichts dieser Daten ist mir klar, dass .ΝΕΤ eine grundlegende PriorityQueue-Implementierung benötigt, die die wesentliche API (heapify/push/peek/pop) verfügbar macht, bestimmte Leistungsgarantien bietet (z. B. keine zusätzlichen Zuweisungen pro Enqueue) und nicht erzwingt Einzigartigkeitsbeschränkungen. Dies impliziert, dass die Implementierung _O(log n) Prioritätsaktualisierungen nicht unterstützen würde_.

Wir sollten auch eine separate Heap-Implementierung in Betracht ziehen, die _O(log n)_ Aktualisierungen/Entfernungen unterstützt und einen gleichheitsbasierten Ansatz verwendet. Da dies ein spezialisierter Typ wäre, sollte die Eindeutigkeitsanforderung kein großes Problem darstellen.

Ich habe am Prototyping beider Implementierungen gearbeitet und werde in Kürze einen API-Vorschlag erstellen.

Vielen Dank für die Analyse, @eiriktsarpalis! Ich schätze insbesondere die Zeit, die interne Microsoft-Codebasis zu analysieren, um relevante Anwendungsfälle zu finden und unsere Diskussion mit Daten zu unterstützen.

Der handhabungsbasierte Ansatz würde mindestens eine zusätzliche Zuweisung pro in die Warteschlange eingereihtes Element implizieren.

Diese Annahme ist falsch, Sie benötigen keine zusätzlichen Zuweisungen in knotenbasierten Heaps. Der Paarungsheap ist für eine allgemeine Zufallseingabe leistungsfähiger als der Array-basierte Binärheap, was bedeutet, dass Sie den Anreiz hätten, diesen knotenbasierten Heap selbst für eine Prioritätswarteschlange zu verwenden, die keine Aktualisierungen unterstützt. Sie können Benchmarks in dem Papier sehen, auf

Von den 80 untersuchten PriorityQueue-Implementierungen wurde festgestellt, dass nur 9 irgendeine Form von Prioritätsaktualisierungen implementiert haben.

Selbst bei dieser kleinen Stichprobe sind das 11-12% aller Verwendungen. Außerdem kann dies in bestimmten Bereichen wie zB Videospielen unterrepräsentiert sein, wo ich einen höheren Prozentsatz erwarte.

Angesichts dieser Daten ist mir klar, dass [...]

Ich glaube nicht, dass eine solche Schlussfolgerung klar ist, da eine der wichtigsten Annahmen falsch ist und es fraglich ist, ob 11-12% der Kunden ein ausreichend wichtiger Anwendungsfall sind oder nicht. Was mir in Ihrer Einschätzung fehlt, ist die Bewertung der Kostenauswirkungen der "Datenstruktur unterstützenden Aktualisierungen" für Kunden, die sich nicht für diesen Mechanismus interessieren - was für mich vernachlässigbar ist, da sie die Datenstruktur verwenden könnten ohne durch die Griffmechanik beeinträchtigt zu werden.

Grundsätzlich:

| | 11-12 % Anwendungsfälle | 88-89% Anwendungsfälle |
|:-:|:-:|:-:|
| kümmert sich um Updates | ja | nein |
| wird durch die Griffe negativ beeinflusst | N/A (sie sind erwünscht) | nein |
| wird durch die Griffe positiv beeinflusst | ja | nein |

Für mich ist dies ein Kinderspiel für die Unterstützung von 100% Anwendungsfällen, nicht nur 88-89%.

Diese Annahme ist falsch, Sie benötigen keine zusätzlichen Zuweisungen in knotenbasierten Heaps

Wenn sowohl die Priorität als auch das Element Werttypen sind (oder wenn beide Referenztypen sind, deren Eigentümer Sie nicht sind und/oder deren Basistyp Sie nicht ändern können), können Sie eine Verknüpfung zu einer Implementierung herstellen, die zeigt, dass keine zusätzlichen Zuordnungen erforderlich sind (oder beschreiben Sie einfach, wie es erreicht wird)? Das wäre hilfreich zu sehen. Vielen Dank.

Es wäre hilfreich, wenn Sie mehr erläutern oder einfach sagen könnten, was Sie sagen möchten. Ich müsste eindeutig sagen, es würde Ping-Pong geben, und das würde zu einer langen Diskussion werden. Alternativ könnten wir einen Anruf vereinbaren.

Ich sage, dass wir jede Enqueue-Operation vermeiden wollen, die eine Zuweisung erfordert, sei es seitens des Aufrufers oder seitens der Implementierung (amortisierte interne Zuweisung ist in Ordnung, zB um ein in der Implementierung verwendetes Array zu erweitern). Ich versuche zu verstehen, wie dies mit einem knotenbasierten Heap möglich ist (z. B. wenn diese Knotenobjekte dem Aufrufer ausgesetzt sind, der das Pooling durch die Implementierung aufgrund von Bedenken hinsichtlich unangemessener Wiederverwendung / Aliasing verbietet). Ich möchte schreiben können:
C# pq.Enqueue(42, 84);
und habe das nicht zuordnen. Wie erreichen die Implementierungen, auf die Sie sich beziehen, dies?

oder sag einfach was du sagen willst

Ich dachte ich war.

wir wollen jede Enqueue-Operation vermeiden, die eine Zuweisung erfordert [...] Ich möchte schreiben können: pq.Enqueue(42, 84); und diese nicht zuordnen lassen.

Woher kommt dieser Wunsch? Es ist ein schöner Nebeneffekt einer Lösung, nicht eine Anforderung, die 99,9 % der Kunden erfüllen müssen. Ich verstehe nicht, warum Sie diese Dimension mit geringer Auswirkung wählen sollten, um Designentscheidungen zwischen Lösungen zu treffen.

Wir treffen keine Designentscheidungen basierend auf Optimierungen für die 0,1 % der Kunden, wenn sich diese in einer anderen Dimension negativ auf 12 % der Kunden auswirken. "Kümmern um keine Zuweisungen" + "Umgang mit zwei Werttypen" ist ein Randfall.

Ich finde die Dimension des unterstützten Verhaltens/der Funktionalität viel wichtiger, insbesondere beim Entwerfen einer universellen, vielseitigen Datenstruktur für ein breites Publikum und eine Vielzahl von Anwendungsfällen.

Woher kommt dieser Wunsch?

Von dem Wunsch, dass die Kernsammlungstypen in Szenarien verwendet werden können, bei denen es auf die Leistung ankommt. Sie sagen, die knotenbasierte Lösung würde 100 % der Anwendungsfälle unterstützen: Das ist nicht der Fall, wenn jedes Enqueue zuweist, genauso wie List<T> , Dictionary<TKey, TValue> , HashSet<T> usw. on würde in vielen Situationen unbrauchbar werden, wenn sie bei jedem Add zugewiesen würden.

Warum glauben Sie, dass nur "0,1 %" sich um die Allokations-Overheads dieser Methoden kümmern? Woher kommen diese Daten?

"Kümmern um keine Zuweisungen" + "Umgang mit zwei Werttypen" ist ein Randfall

Es ist nicht. Es sind auch nicht nur "zwei Werttypen". Nach meinem Verständnis würde die vorgeschlagene Lösung entweder a) eine Zuweisung für jedes Enqueue erfordern, unabhängig von den beteiligten Ts, oder b) würde erfordern, dass der Elementtyp von einem bekannten Basistyp abgeleitet wird, was wiederum eine große Anzahl möglicher Verwendungen für vermeiden Sie zusätzliche Zuweisungen.

@eiriktsarpalis
Damit Sie keine Optionen vergessen, denke ich, dass es eine praktikable Option 4 gibt, die Sie zu den Optionen 1, 2 und 3 in Ihrer Liste hinzufügen können, was ein Kompromiss ist:

  1. eine Implementierung, die den Anwendungsfall von 12% unterstützt, während sie auch für die anderen 88% nahezu optimiert ist, indem sie Aktualisierungen von Elementen zulässt, die gleichberechtigt sind, und nur _träge_ die Nachschlagetabelle erstellt, die für diese Aktualisierungen erforderlich ist, wenn eine Aktualisierungsmethode zum ersten Mal aufgerufen wird ( und Aktualisieren bei nachfolgenden Aktualisierungen + Entfernungen). Dadurch fallen weniger Kosten für Apps an, die die Funktionalität nicht nutzen.

Wir könnten immer noch entscheiden, dass es besser ist, die Optionen 2 und 3 bereitzustellen, als 88% oder 12% einer Implementierung, die keine aktualisierbare Datenstruktur benötigt oder für eine solche optimiert ist, zusätzliche Leistung zur Verfügung stellen Option 4. Aber ich dachte, wir sollten nicht vergessen, dass es noch eine andere Option gibt.

[Oder ich nehme an, Sie könnten dies einfach als bessere Option 1 ansehen und die Beschreibung von 1 aktualisieren, um zu sagen, dass die Buchhaltung nicht erzwungen, sondern faul ist und ein korrektes, gleichberechtigtes Verhalten nur erforderlich ist, wenn Updates verwendet werden ...]

@stephentoub Genau das hatte ich mir vorgestellt, einfach zu sagen, was man sagen möchte, danke :)

Warum sind Ihrer Meinung nach nur 0,1 % der Allokations-Overheads dieser Methoden wichtig? Woher kommen diese Daten?

Aus Intuition, dh aus derselben Quelle, von der Sie glauben, dass es wichtiger ist, "keine zusätzlichen Zuweisungen" vor "Fähigkeit zur Durchführung von Updates" zu priorisieren. Zumindest für den Aktualisierungsmechanismus haben wir die Daten, die 11-12% der Kunden benötigen, um dieses Verhalten zu unterstützen. Ich glaube nicht, dass Kunden aus der Ferne sich um "keine zusätzlichen Zuweisungen" kümmern würden.

In jedem Fall entscheiden Sie sich aus irgendeinem Grund dafür, sich auf die Speicherdimension zu fixieren und andere Dimensionen zu vergessen, zB die rohe Geschwindigkeit, was ein weiterer Kompromiss für Ihren bevorzugten Ansatz ist. Eine Array-basierte Implementierung, die "keine zusätzlichen Zuweisungen" bereitstellt, wäre langsamer als eine knotenbasierte Implementierung. Auch hier denke ich, dass es hier willkürlich ist, Speicher über Geschwindigkeit zu priorisieren.

Gehen wir einen Schritt zurück und konzentrieren uns auf das, was die Kunden wollen. Wir haben eine Designwahl, die die Datenstruktur für 12% der Kunden unbrauchbar machen kann oder nicht. Ich denke, wir müssen sehr vorsichtig sein, wenn wir Gründe dafür angeben, warum wir uns dafür entscheiden, diese nicht zu unterstützen.

Eine Array-basierte Implementierung, die "keine zusätzlichen Zuweisungen" bereitstellt, wäre langsamer als eine knotenbasierte Implementierung.

Bitte teilen Sie die beiden C#-Implementierungen, die Sie für diesen Vergleich verwenden, und die Benchmarks, die zu diesem Schluss gekommen sind. Theoretische Arbeiten sind sicherlich wertvoll, aber sie sind nur ein kleiner Teil des Puzzles. Das Wichtigste ist, wenn der Gummi auf die Straße trifft, die Details der gegebenen Plattform und der gegebenen Implementierungen berücksichtigt werden und Sie in der Lage sind, auf der spezifischen Plattform mit der spezifischen Implementierung und den typischen/erwarteten Datensätzen / Nutzungsmustern zu validieren. Es kann durchaus sein, dass Ihre Aussage richtig ist. Es kann auch nicht sein. Ich würde gerne die Implementierungen / Daten sehen, um sie besser zu verstehen.

Bitte teilen Sie die beiden C#-Implementierungen, die Sie für diesen Vergleich verwenden, und die Benchmarks mit, die zu diesem Schluss gekommen sind

Dies ist ein gültiger Punkt, das von mir zitierte Papier vergleicht und bewertet nur Implementierungen in C++. Es führt mehrere Benchmarks mit unterschiedlichen Datensätzen und Nutzungsmustern durch. Ich bin mir ziemlich sicher, dass dies auf C# übertragbar wäre, aber wenn Sie glauben, dass dies etwas ist, das wir verdoppeln müssen, wäre es meiner Meinung nach am besten, wenn Sie einen Kollegen bitten, eine solche Studie durchzuführen.

@pgolebiowski Ich wäre daran interessiert, die Art Ihres

  1. eine Implementierung, die den Anwendungsfall von 12 % unterstützt und gleichzeitig für die anderen 88 % nahezu optimiert, indem sie Aktualisierungen von Elementen zulässt, die gleichberechtigt sind, und die Nachschlagetabelle, die für diese Aktualisierungen erforderlich ist, nur langsam erstellt, wenn eine Aktualisierungsmethode zum ersten Mal aufgerufen wird ( und Aktualisieren bei nachfolgenden Aktualisierungen + Entfernungen). Dadurch fallen weniger Kosten für Apps an, die die Funktionalität nicht nutzen.

Ich würde dies wahrscheinlich als Leistungsoptimierung für Option 1 einstufen, jedoch sehe ich bei diesem speziellen Ansatz ein paar Probleme:

  • Update wird jetzt zu _O(n)_, was je nach Nutzungsmuster zu unvorhersehbarer Leistung führen kann.
  • Die Nachschlagetabelle wird auch zum Überprüfen der Eindeutigkeit benötigt. Das zweimalige Einreihen desselben Elements in die Warteschlange _vor_ dem Aufrufen von Update würde akzeptiert werden und die Warteschlange wohl in einen inkonsistenten Zustand bringen.

@eiriktsarpalis Es ist nur O(n) einmal und O(1) danach, was O(1) amortisiert ist. Und Sie können die Validierung der Eindeutigkeit bis zum ersten Update verschieben. Aber vielleicht ist das zu clever. Zwei Klassen sind einfacher zu erklären.

Ich habe die letzten Tage damit verbracht, Prototypen von zwei PriorityQueue-Implementierungen zu erstellen: eine Basisimplementierung ohne Update-Unterstützung und eine Implementierung, die Updates mit Elementgleichheit unterstützt. Ich habe das erstere PriorityQueue und das letztere, mangels eines besseren Namens, PrioritySet . Mein Ziel ist es, die API-Ergonomie zu messen und die Leistung zu vergleichen.

Die Implementierungen finden Sie in diesem Repository . Beide Klassen werden mit Array-basierten Quad-Heaps implementiert. Die aktualisierbare Implementierung verwendet außerdem ein Wörterbuch, das Elemente internen Heap-Indizes zuordnet.

Basic PriorityQueue

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

Hier ist ein grundlegendes Beispiel mit dem Typ

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

Aktualisierbare PriorityQueue

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

Leistungsvergleich

Ich habe einen einfachen Heapsort-Benchmark geschrieben , der die beiden Implementierungen in ihrer grundlegendsten Anwendung vergleicht. Ich habe auch einen Sortierbenchmark hinzugefügt, der Linq zum Vergleich verwendet:

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

| Methode | Größe | Mittelwert | Fehler | StdAbw | Verhältnis | VerhältnisSD | Gen 0 | Gen 1 | Gen 2 | Zugewiesen |
|---------------- |-------- |------------:|------------: |------------:|--------:|--------:|--------:|-------- :|--------:|----------:|
| LinqSort | 30 | 1.439 us | 0,0072 uns | 0,0064 uns | 1,00 | 0,00 | 0,0095 | - | - | 672 B |
| Prioritätswarteschlange | 30 | 1.450 us | 0,0085 uns | 0,0079 uns | 1,01 | 0,01 | - | - | - | - |
| PrioritySet | 30 | 2.778 us | 0,0217 us | 0,0192 us | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 300 | 24.727 uns | 0.1032 us | 0,0915 us | 1,00 | 0,00 | 0,0305 | - | - | 3912 B |
| Prioritätswarteschlange | 300 | 29.510 us | 0,0995 us | 0,0882 us | 1,19 | 0,01 | - | - | - | - |
| PrioritySet | 300 | 47.715 uns | 0.4455 uns | 0,4168 us | 1,93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015 uns | 1.5495 us | 1.3736 us | 1,00 | 0,00 | 0,4883 | - | - | 36312 B |
| Prioritätswarteschlange | 3000 | 491.722 uns | 4.1463 uns | 3.8785 us | 1,19 | 0,01 | - | - | - | - |
| PrioritySet | 3000 | 677.959 uns | 3.1996 uns | 2.4981 uns | 1,64 | 0,01 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 30000 | 5,223,560 us | 11.9077 uns | 9,9434 uns | 1,00 | 0,00 | 93.7500 | 93.7500 | 93.7500 | 360910 B |
| Prioritätswarteschlange | 30000 | 5.688.625 us | 53.0746 us | 49.6460 uns | 1,09 | 0,01 | - | - | - | 2 B |
| PrioritySet | 30000 | 8.124.306 uns | 39.9498 us | 37.3691 uns | 1,55 | 0,01 | - | - | - | 4 B |

Wie zu erwarten ist, führt der Overhead der Positionsverfolgungselemente zu einer erheblichen Leistungseinbuße, die im Vergleich zur Basisimplementierung etwa 40-50 % langsamer ist.

Ich weiß all die Mühe zu schätzen, ich sehe, es hat viel Zeit und Energie gekostet.

  1. Ich sehe jedoch nicht den Grund für 2 fast identische Datenstrukturen, bei denen eine eine minderwertige Version einer anderen ist.
  2. Selbst wenn Sie solche 2 Versionen einer Prioritätswarteschlange haben möchten, sehe ich nicht, wie die "überlegene" Version besser ist als der Vorschlag für die Prioritätswarteschlange (v2.2) von vor 20 Tagen.

tl;dr:

  • Dieser Vorschlag ist spannend!! Aber es passt noch nicht zu meinen High-Perf-Anwendungsfällen.
  • > 90 % meiner Rechengeometrie-/Bewegungsplanungsleistungslast ist PQ-Einreihen/Ausreihen, weil dies das dominante N^m LogN in einem Algorithmus ist.
  • Ich bin für separate PQ-Implementierungen. Normalerweise brauche ich keine Prioritätsaktualisierungen, und eine 2-mal schlechtere Leistung ist inakzeptabel.
  • PrioritySet ist ein verwirrender Name und ist im Vergleich zu PriorityQueue nicht auffindbar
  • Das zweimalige Speichern der Priorität (einmal in meinem Element, einmal in der Warteschlange) fühlt sich teuer an. Strukturierte Kopien und Speichernutzung.
  • Wenn die Berechnungspriorität teuer wäre, würde ich einfach ein Tupel (priority: ComputePriority(element), element) in einer PQ speichern, und meine Prioritäts-Getter-Funktion wäre einfach tuple => tuple.priority .
  • Die Leistung sollte pro Vorgang oder alternativ anhand von realen Anwendungsfällen bewertet werden (z. B. optimierte Multi-Start-Multi-End-Suche in einem Diagramm)
  • Ungeordnetes Aufzählungsverhalten ist unerwartet. Bevorzugen Sie eine Warteschlangen-ähnliche Dequeue()-Reihenfolge-Semantik?
  • Ziehen Sie in Erwägung, den Klon- und Zusammenführungsvorgang zu unterstützen.
  • Grundoperationen müssen im stationären Betrieb 0-alloc sein. Ich werde diese Warteschlangen bündeln.
  • Ziehen Sie in Betracht, EnqueueMany zu unterstützen, das Heapify durchführt, um das Pooling zu unterstützen.

Ich arbeite mit High-Performance-Suche (Bewegungsplanung) und Computational-Geometrie-Code (zB Sweepline-Algorithmen), die sowohl für Robotik als auch für Spiele relevant sind. Ich verwende viele benutzerdefinierte handgerollte Prioritätswarteschlangen. Der andere häufige Anwendungsfall, den ich habe, ist eine Top-K-Abfrage, bei der eine aktualisierbare Priorität nicht nützlich ist.

Einige Rückmeldungen zur Debatte über zwei Implementierungen (ja vs. keine Update-Unterstützung).

Benennung:

  • PrioritySet impliziert Set-Semantik, aber die Warteschlange implementiert ISet nicht.
  • Sie verwenden den Namen UpdatablePriorityQueue, der leichter zu finden ist, wenn ich PriorityQueue durchsuche.

Leistung:

  • Die Leistung der Prioritätswarteschlange ist fast immer mein Leistungsengpass (>90%) in meinen Geometrie- / Planungsalgorithmen
  • Ziehen Sie in Betracht, eine Func zu bestehenoder Vergleichzu ctor, anstatt TPriority herumzukopieren (teuer!). Wenn die Rechenpriorität teuer ist, füge ich (Priorität, Element) in eine PQ ein und überlasse einen Vergleich, der meine zwischengespeicherte Priorität betrachtet.
  • Eine beträchtliche Anzahl meiner Algorithmen benötigt keine PQ-Updates. Ich würde in Betracht ziehen, eine integrierte PQ zu verwenden, die keine Updates unterstützt, aber wenn etwas doppelte Perf-Kosten hat, um eine Funktion zu unterstützen, die ich nicht brauche (aktualisieren), dann ist es für mich nutzlos.
  • Für Leistungsanalysen / Kompromisse wäre es wichtig, die relativen Wandzeitkosten von Enqueue/Dequeue pro Operation @eiriktsarpalis zu kennen -- "Tracking ist 2x langsamer" reicht nicht aus, um zu beurteilen, ob eine PQ nützlich ist.
  • Ich habe mich gefreut, dass Ihre Konstrukteure Heapify ausführen. Stellen Sie sich einen Konstruktor vor, der eine IList, eine Liste oder ein Array verwendet, um Aufzählungszuweisungen zu vermeiden.
  • Ziehen Sie in Betracht, ein EnqueueMany verfügbar zu machen, das Heapify ausführt, wenn die PQ anfänglich leer ist, da es bei High-Perf-Auflistungen üblich ist, Sammlungen zu bündeln.
  • Ziehen Sie in Betracht, das Array Clear nicht null zu machen, wenn Elemente keine Referenzen enthalten.
  • Zuweisungen im Enqueue/Dequeue sind nicht akzeptabel. Meine Algorithmen sind aus Leistungsgründen null alloc, mit gepoolten Thread-lokalen Sammlungen.

APIs:

  • Das Klonen einer Prioritätswarteschlange ist bei Ihren Implementierungen eine triviale Operation und häufig nützlich.

    • Verwandte: Sollte das Aufzählen einer Prioritätswarteschlange eine warteschlangenähnliche Semantik haben? Ich würde eine Dequeue-Order-Sammlung erwarten, ähnlich wie bei Queue. Ich erwarte, dass new List(myPriorityQueue) die PriorityQueue nicht mutiert, sondern so funktioniert, wie ich es gerade beschrieben habe.

  • Wie oben erwähnt, ist es vorzuziehen, ein Func<TElement, TPriority> anstatt mit Priorität einzufügen. Wenn die Rechenpriorität teuer ist, kann ich einfach (priority, element) einfügen und eine Funktion bereitstellen tuple => tuple.priority
  • Das Zusammenführen von zwei Prioritätswarteschlangen ist manchmal nützlich.
  • Es ist seltsam, dass Peek ein TItem zurückgibt, Enumeration & Enqueue jedoch (TItem, TPriority) haben.

Davon abgesehen enthalten meine Prioritätswarteschlangenelemente für eine beträchtliche Anzahl meiner Algorithmen ihre Prioritäten, und das zweimalige Speichern (einmal in der PQ, einmal in den Elementen) klingt ineffizient. Dies ist insbesondere dann der Fall, wenn ich nach mehreren Schlüsseln bestelle (ähnlicher Anwendungsfall wie OrderBy.ThenBy.ThenBy). Diese API bereinigt auch viele Inkonsistenzen, bei denen Einfügen Priorität hat, Peek sie jedoch nicht zurückgibt.

Schließlich ist es erwähnenswert, dass ich oft Indizes eines Arrays in eine Prioritätswarteschlange einfüge, anstatt Arrayelemente selbst . Dies wird jedoch von allen bisher besprochenen APIs unterstützt. Wenn ich beispielsweise den Anfang/das Ende von Intervallen auf einer x-Achsenlinie verarbeite, habe ich möglicherweise Prioritätswarteschlangenereignisse (x, isStartElseEnd, intervalId) und sortiere nach x und dann nach isStartElseEnd. Dies liegt oft daran, dass ich andere Datenstrukturen habe, die von einem Index auf einige berechnete Daten abbilden.

@pgolebiowski Ich Pairing-Heap-Implementierung in die Benchmarks aufzunehmen, damit wir Instanzen aller drei Ansätze direkt vergleichen können. Hier sind die Ergebnisse:

| Methode | Größe | Mittelwert | Fehler | StdAbw | Mittelwert | Gen 0 | Gen 1 | Gen 2 | Zugewiesen |
|------------ |-------- |------------------:|---- --------------:|-----------------:|---------------- ---:|----------:|------:|-----:|-----------:|
| Prioritätswarteschlange | 10 | 774,7 ns | 3,30 ns | 3,08 ns | 773,2 ns | - | - | - | - |
| PrioritySet | 10 | 1.643,0 ns | 3,89 ns | 3,45 ns | 1.642,8 ns | - | - | - | - |
| PairingHeap | 10 | 1.660,2 ns | 14,11 ns | 12,51 ns | 1.657,2 ns | 0,0134 | - | - | 960 B |
| Prioritätswarteschlange | 50 | 6.413,0 ns | 14,95 ns | 13,99 ns | 6.409,5 ns | - | - | - | - |
| PrioritySet | 50 | 12.193.1 ns | 35,41 ns | 29,57 ns | 12.188,3 ns | - | - | - | - |
| PairingHeap | 50 | 13.955,8 ns | 193,36 ns | 180,87 ns | 13.989.2 ns | 0,0610 | - | - | 4800 B |
| Prioritätswarteschlange | 150 | 27.402,5 ns | 76,52 ns | 71,58 ns | 27.410,2 ns | - | - | - | - |
| PrioritySet | 150 | 48.485,8 ns | 160,22 ns | 149,87 ns | 48.476,3 ns | - | - | - | - |
| PairingHeap | 150 | 56.951,2 ns | 190,52 ns | 168,89 ns | 56.953,6 ns | 0,1831 | - | - | 14400 B |
| Prioritätswarteschlange | 500 | 124.933,7 ns | 429,20 ns | 380,48 ns | 124.824,4 ns | - | - | - | - |
| PrioritySet | 500 | 206,310,0 ns | 433,97 ns | 338,81 ns | 206.319,0 ns | - | - | - | - |
| PairingHeap | 500 | 229.423,9 ns | 3.213,33 ns | 2.848,53 ns | 230.398,7 ns | 0,4883 | - | - | 48000 B |
| Prioritätswarteschlange | 1000 | 284.481,8 ns | 475.91 ns | 445,16 ns | 284,445,6 ns | - | - | - | - |
| PrioritySet | 1000 | 454.989,4 ns | 3.712,11 ns | 3.472,31 ns | 455.354,0 ns | - | - | - | - |
| PairingHeap | 1000 | 459.049,3 ns | 1.706,28 ns | 1.424,82 ns | 459.364,9 ns | 0,9766 | - | - | 96000 B |
| Prioritätswarteschlange | 10000 | 3.788.802,4 ns | 11.715,81 ns | 10.958,98 ns | 3.787.811,9 ns | - | - | - | 1 B |
| PrioritySet | 10000 | 5.963.100,4 ns | 26.669,04 ns | 22.269,86 ns | 5.950.915,5 ns | - | - | - | 2 B |
| PairingHeap | 10000 | 6.789.719,0 ns | 134.453,01 ns | 265.397,13 ns | 6.918.392,9 ns | 7.8125 | - | - | 960002 B |
| Prioritätswarteschlange | 1000000 | 595.059.170,7 ns | 4.001.349,38 ns | 3.547.092,00 ns | 595.716.610,5 ns | - | - | - | 4376 B |
| PrioritySet | 1000000 | 1.592.037.780,9 ns | 13.925.896,05 ns | 12.344.944,12 ns | 1.591.051.886,5 ns | - | - | - | 288 B |
| PairingHeap | 1000000 | 1.858.670.560,7 ns | 36.405.433,20 ns | 59.815.170,76 ns | 1.838.721.629,0 ns | 1000.0000 | - | - | 96000376 B |

Die zentralen Thesen

  • ~Die Pairing-Heap-Implementierung funktioniert asymptotisch viel besser als ihre Array-gestützten Gegenstücke. Es kann jedoch bei kleinen Heap-Größen (< 50 Elemente) bis zu 2x langsamer sein, holt bei etwa 1000 Elementen auf, ist aber bei Heaps der Größe 10^6~ bis zu 2x schneller.
  • Wie erwartet erzeugt der Paarungsheap eine beträchtliche Menge an Heap-Zuordnungen.
  • Die Implementierung von "PrioritySet" ist durchweg langsam ~der langsamste aller drei Konkurrenten~, daher möchten wir diesen Ansatz vielleicht doch nicht weiterverfolgen.

~In Anbetracht der obigen Ausführungen glaube ich immer noch, dass es gültige Kompromisse zwischen dem Baseline-Array-Heap und dem Pairing-Heap-Ansatz gibt~.

EDIT: Die Ergebnisse nach einem Bugfix in meinen Benchmarks aktualisiert, danke @VisualMelon

@eiriktsarpalis Dein Benchmark für PairingHeap ist meiner Meinung nach falsch: Die Parameter für Add sind falsch herum. Wenn Sie sie austauschen, ist es eine andere Geschichte: https://gist.github.com/VisualMelon/00885fe50f7ab0f4ae5cd1307312109f

(Ich habe genau das gleiche gemacht, als ich es zum ersten Mal implementiert habe)

Beachten Sie, dass dies nicht bedeutet, dass der Pairing-Heap schneller oder langsamer ist, sondern es scheint stark von der Verteilung / Reihenfolge der gelieferten Daten abzuhängen.

@eiriktsarpalis re: die Nützlichkeit von PrioritySet...
Wir sollten nicht erwarten, dass aktualisierbar etwas anderes als langsamer für Heapsort ist, da es im Szenario keine Prioritätsaktualisierungen hat. (Auch für Heapsort ist es wahrscheinlich, dass Sie Duplikate behalten möchten, ein Set ist einfach nicht geeignet.)

Der Lackmustest, um festzustellen, ob PrioritySet nützlich ist, sollten Benchmarking-Algorithmen sein, die Prioritätsaktualisierungen verwenden, im Vergleich zu einer nicht aktualisierenden Implementierung desselben Algorithmus, die die doppelten Werte in die Warteschlange einreiht und Duplikate beim Entfernen aus der Warteschlange ignoriert.

Danke @VisualMelon , ich habe meine Ergebnisse und Kommentare nach Ihrem vorgeschlagenen Fix aktualisiert.

vielmehr scheint es stark von der Verteilung/Reihenfolge der gelieferten Daten abzuhängen.

Ich glaube, es hätte von der Tatsache profitiert, dass die in die Warteschlange gestellten Prioritäten monoton waren.

Der Lackmustest, um festzustellen, ob PrioritySet nützlich ist, sollten Benchmarking-Algorithmen sein, die Prioritätsaktualisierungen verwenden, im Vergleich zu einer nicht aktualisierenden Implementierung desselben Algorithmus, die die doppelten Werte in die Warteschlange einreiht und Duplikate beim Entfernen aus der Warteschlange ignoriert.

@TimLovellSmith Mein Ziel hier war es, die Leistung für die gängigste PriorityQueue-Anwendung zu messen: Anstatt die Leistung von Updates zu messen, wollte ich die Auswirkungen für den Fall sehen, in dem Updates überhaupt nicht benötigt werden. Es kann jedoch sinnvoll sein, einen separaten Benchmark zu erstellen, der Pairing Heap mit "PrioritySet"-Updates vergleicht.

@miyu danke für dein ausführliches Feedback, es wird sehr geschätzt!

@TimLovellSmith Ich habe einen einfachen Benchmark geschrieben , der Updates verwendet:

| Methode | Größe | Mittelwert | Fehler | StdAbw | Mittelwert | Gen 0 | Gen 1 | Gen 2 | Zugewiesen |
|------------ |-------- |--------------:|---------- -----:|--------------:|---------------:|-------:| ------:|------:|-----------:|
| PrioritySet | 10 | 1.052 us | 0,0106 us | 0,0099 uns | 1.055 us | - | - | - | - |
| PairingHeap | 10 | 1.055 us | 0,0042 us | 0,0035 uns | 1.055 us | 0,0057 | - | - | 480 B |
| PrioritySet | 50 | 7.394 us | 0.0527 us | 0.0493 us | 7.380 us | - | - | - | - |
| PairingHeap | 50 | 8.587 us | 0,1678 us | 0.1570 us | 8.634 uns | 0,0305 | - | - | 2400 B |
| PrioritySet | 150 | 27.522 uns | 0,0459 us | 0,0359 us | 27.523 uns | - | - | - | - |
| PairingHeap | 150 | 32.045 us | 0.1076 us | 0.1007 us | 32.019 uns | 0,0610 | - | - | 7200 B |
| PrioritySet | 500 | 109.097 uns | 0.6548 us | 0,6125 us | 109.162 uns | - | - | - | - |
| PairingHeap | 500 | 131.647 us | 0,5401 us | 0,4510 us | 131.588 uns | 0,2441 | - | - | 24000 B |
| PrioritySet | 1000 | 238,184 us | 1.0282 uns | 0,9618 us | 238.457 us | - | - | - | - |
| PairingHeap | 1000 | 293.236 uns | 0,9396 us | 0.8789 us | 293.257 us | 0,4883 | - | - | 48000 B |
| PrioritySet | 10000 | 3.035.982 us | 12.2952 uns | 10.8994 uns | 3.036,985 us | - | - | - | 1 B |
| PairingHeap | 10000 | 3.388.685 us | 16.0675 uns | 38.1861 uns | 3.374.565 us | - | - | - | 480002 B |
| PrioritySet | 1000000 | 841.406.888 uns | 16.788.4775 us | 15.703,9522 us | 840,888,389 uns | - | - | - | 288 B |
| PairingHeap | 1000000 | 989.966,501 us | 19.722,6687 us | 30.705.8191 us | 996.075.410 us | - | - | - | 48000448 B |

Ist die Diskussion/das Feedback dazu, dass mangelnde Stabilität ein Problem (oder kein Problem) für die Anwendungsfälle der Menschen ist, in einer separaten Anmerkung?

Gab es Diskussionen/Feedbacks darüber, dass mangelnde Stabilität ein Problem (oder kein Problem) für den Anwendungsfall der Leute ist?

Keine der Implementierungen garantiert Stabilität, aber es sollte für Benutzer ziemlich einfach sein, Stabilität zu erreichen, indem sie die Ordinalzahl mit der Einfügereihenfolge erhöhen:

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

Um einige meiner früheren Beiträge zusammenzufassen, habe ich versucht herauszufinden, wie eine beliebte .NET-Prioritätswarteschlange aussehen würde, also bin ich die folgenden Daten durchgegangen:

  • Häufige Verwendungsmuster für Prioritätswarteschlangen im .NET-Quellcode.
  • PriorityQueue-Implementierungen in Kernbibliotheken konkurrierender Frameworks.
  • Benchmarks verschiedener .NET Priority Queue-Prototypen.

Was zu folgenden Erkenntnissen führte:

  • 90 % der Anwendungsfälle mit Prioritätswarteschlangen erfordern keine Prioritätsaktualisierungen.
  • Die Unterstützung von Prioritätsupdates führt zu einem komplizierteren API-Vertrag (der entweder Handles oder Eindeutigkeit von Elementen erfordert).
  • In meinen Benchmarks sind Implementierungen, die Prioritätsupdates unterstützen, 2-3x langsamer als solche, die dies nicht tun.

Nächste Schritte

In Zukunft schlage ich vor, dass wir die folgenden Maßnahmen für .NET 6 ergreifen:

  1. Führen Sie eine System.Collections.Generic.PriorityQueue Klasse ein, die einfach ist, die meisten unserer Benutzeranforderungen erfüllt und so effizient wie möglich ist. Es verwendet einen Array-gestützten quaternären Heap und unterstützt keine Prioritätsaktualisierungen. Einen Prototyp der Implementierung finden Sie hier . Ich werde in Kürze eine separate Ausgabe erstellen, in der der API-Vorschlag detailliert beschrieben wird.

  2. Wir erkennen den Bedarf an Heaps, die effiziente Prioritätsupdates unterstützen, und werden daher weiter daran arbeiten, eine spezialisierte Klasse einzuführen, die diese Anforderung erfüllt. Wir evaluieren derzeit einige Prototypen [ 1 , 2 ] mit jeweils eigenen Kompromissen. Meine Empfehlung wäre, diesen Typ zu einem späteren Zeitpunkt einzuführen, da noch mehr Arbeit erforderlich ist, um das Design fertigzustellen.

An dieser Stelle möchte ich den Mitwirkenden dieses Threads danken, insbesondere @pgolebiowski und @TimLovellSmith. Ihr Feedback hat bei unserem Designprozess eine große Rolle gespielt. Ich hoffe, weiterhin Ihren Input zu erhalten, während wir das Design für die aktualisierbare Prioritätswarteschlange ausbügeln.

Für die Zukunft schlage ich vor, dass wir die folgenden Maßnahmen für .NET 6 ergreifen: [...]

Hört sich gut an :)

Führen Sie eine System.Collections.Generic.PriorityQueue Klasse ein, die einfach ist, die meisten unserer Benutzeranforderungen erfüllt und so effizient wie möglich ist. Es verwendet einen Array-gestützten quaternären Heap und unterstützt keine Prioritätsaktualisierungen.

Wenn wir die Entscheidung der Codebase-Besitzer haben, dass diese Richtung genehmigt und gewünscht wird, kann ich dann weiterhin das API-Design für dieses Bit leiten und die endgültige Implementierung bereitstellen?

An dieser Stelle möchte ich den Mitwirkenden dieses Threads danken, insbesondere @pgolebiowski und @TimLovellSmith. Ihr Feedback hat bei unserem Designprozess eine große Rolle gespielt. Ich hoffe, weiterhin Ihren Input zu erhalten, während wir das Design für die aktualisierbare Prioritätswarteschlange ausbügeln.

Es war eine ziemliche Reise :D

Die API für System.Collections.Generic.PriorityQueue<TElement, TPriority> wurde gerade genehmigt. Ich habe ein separates Problem erstellt , um unser Gespräch über eine potenzielle Heap-Implementierung fortzusetzen, die Prioritätsupdates unterstützt.

Ich schließe diese Ausgabe, vielen Dank für Ihre Beiträge!

Vielleicht kann jemand diese Reise aufschreiben! Ganze 6 Jahre für eine API. :) Gibt es eine Chance ein Guinness zu gewinnen?

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen