Runtime: PriorityQueueを追加する<t>コレクションへ</t>

作成日 2015年01月30日  ·  318コメント  ·  ソース: dotnet/runtime

corefxlabリポジトリの最新の提案を参照してください。

2番目の提案オプション

https://github.com/dotnet/corefx/issues/574#issuecomment-307971397からの提案

仮定

優先キューの要素は一意です。 そうでない場合は、アイテムの「ハンドル」を導入して、更新/削除を有効にする必要があります。 または、更新/削除のセマンティクスを最初/すべてに適用する必要がありますが、これは奇妙なことです。

Queue<T>モデルにしています( MSDNリンク

API

`` `c#
パブリッククラスPriorityQueue
:IEnumerable、
IEnumerable <(TElement要素、TPriority優先度)>、
IReadOnlyCollection <(TElement要素、TPriority優先度)>
// ICollectionは意図的に含まれていません
{{
public PriorityQueue();
public PriorityQueue(IComparer比較者);

public IComparer<TPriority> Comparer { get; }
public int Count { get; }
public bool IsEmpty { get; }

public bool Contains(TElement element);

// Peek & Dequeue
public (TElement element, TPriority priority) Peek(); // Throws if empty
public (TElement element, TPriority priority) Dequeue(); // Throws if empty
public bool TryPeek(out TElement element, out TPriority priority); // Returns false if empty
public bool TryDequeue(out TElement element, out TPriority priority); // Returns false if empty

// Enqueue & Update
public void Enqueue(TElement element, TPriority priority); // Throws if it is duplicate
public void Update(TElement element, TPriority priority); // Throws if element does not exist
public void EnqueueOrUpdate(TElement element, TPriority priority);
public bool TryEnqueue(TElement element, TPriority priority); // Returns false if it is duplicate (does NOT update it)
public bool TryUpdate(TElement element, TPriority priority); // Returns false if element does not exist (does NOT add it)

public void Remove(TElement element); // Throws if element does not exist
public bool TryRemove(TElement element); // Returns false if element does not exist

public void Clear();

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

//
//セレクターパーツ
//
public PriorityQueue(FuncprioritySelector);
public PriorityQueue(FuncprioritySelector、IComparer比較者);

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

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

}
「」

未解決の質問:

  1. クラス名PriorityQueueHeap
  2. IHeapとコンストラクターのオーバーロードを導入しますか? (後で待つ必要がありますか?)
  3. IPriorityQueue紹介しますか? (後で待つ必要があります- IDictionary例)
  4. (値内に格納されている優先度の)セレ​​クターを使用するかどうか(5つのAPIの違い)
  5. タプル(TElement element, TPriority priority)KeyValuePair<TPriority, TElement>

    • PeekDequeueは、タプルではなくout引数を指定する必要がありますか?

  6. PeekDequeueスローはまったく役に立ちますか?

当初の提案

問題https://github.com/dotnet/corefx/issues/163は、コア.NETコレクションデータ構造への優先度キューの追加を要求しました。

この投稿は重複していますが、corefxAPIレビュープロセスへの正式な提出を目的としています。 問題の内容は、新しいSystem.Collections.Generic.PriorityQueueの_speclet_です。タイプ。

承認されれば、私はPRに貢献します。

理論的根拠と使用法

.NET基本クラスライブラリ(BCL)は現在、順序付けられた生産者/消費者コレクションのサポートを欠いています。 多くのソフトウェアアプリケーションに共通する要件は、時間の経過とともにアイテムのリストを生成し、受け取った順序とは異なる順序でアイテムを処理する機能です。

名前空間のSystem.Collections階層内には、並べ替えられたアイテムのコレクションをサポートする3つの一般的なデータ構造があります。 System.Collections.Generic.SortedList、System.Collections.Generic.SortedSet、およびSystem.Collections.Generic.SortedDictionary。

これらのうち、SortedSetとSortedDictionaryは、重複する値を生成する生産者/消費者パターンには適していません。 SortedListの複雑さは、AddとRemoveの両方でΘ(n)最悪の場合です。

生産者/消費者の使用パターンを持つ順序付けられたコレクションのメモリと時間効率の高いデータ構造は、優先度付きキューです。 容量のサイズ変更が必要な場合を除いて、最悪の場合の挿入(エンキュー)および最上位の削除(デキュー)のパフォーマンスはΘ(logn)であり、BCLに存在する既存のオプションよりもはるかに優れています。

優先キューは、さまざまなクラスのアプリケーションに幅広く適用できます。 優先度付きキューのウィキペディアのページには、よく理解されているさまざまなユースケースのリストがあります。 高度に専門化された実装では、カスタムの優先度キューの実装が必要になる場合がありますが、標準の実装では、幅広い使用シナリオがカバーされます。 優先キューは、複数のプロデューサーの出力をスケジュールする場合に特に役立ちます。これは、高度に並列化されたソフトウェアの重要なパターンです。

C ++標準ライブラリとJavaの両方が、基本APIの一部として優先キュー機能を提供していることは注目に値します。

提案されたAPI

`` `C#
名前空間System.Collections.Generic
{{
///


///ソートされた順序で削除されるオブジェクトのコレクションを表します。
///

///キュー内の要素のタイプを指定します。
[DebuggerDisplay( "Count = {count}")]
[DebuggerTypeProxy(typeof(System_PriorityQueueDebugView <>))]
パブリッククラスPriorityQueue:IEnumerable、ICollection、IEnumerable、IReadOnlyCollection
{{
///
///の新しいインスタンスを初期化しますクラス
///デフォルトの比較子を使用します。
///

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

}
`` `

詳細

  • 実装データ構造はバイナリヒープになります。 比較値が大きいアイテムが最初に返されます。 (降順)
  • 時間計算量:

| 操作| 複雑さ| ノート|
| --- | --- | --- |
| 構築| Θ(1)| |
| IEnumerableを使用して構築する| Θ(n)| |
| エンキュー| Θ(logn)| |
| デキュー| Θ(logn)| |
| ピーク| Θ(1)| |
| カウント| Θ(1)| |
| クリア| Θ(N)| |
| 含む| Θ(N)| |
| CopyTo | Θ(N)| Array.Copyを使用します。実際の複雑さは低くなる可能性があります|
| ToArray | Θ(N)| Array.Copyを使用します。実際の複雑さは低くなる可能性があります|
| GetEnumerator | Θ(1)| |
| Enumerator.MoveNext | Θ(1)| |

  • System.Comparisonを使用する追加のコンストラクターオーバーロード簡略化されたAPI表面積を優先して、デリゲートは意図的に省略されました。 発信者はComparerを使用できます.Createを使用して、関数またはLambda式をIComparerに変換します必要に応じてインターフェース。 これには、呼び出し元が1回限りのヒープ割り当てを行う必要があります。
  • System.Collections.Genericはまだcorefxの一部ではありませんが、当面の間、このクラスをcorefxlabに追加することをお勧めします。 System.Collections.Genericが追加されると、プライマリcorefxリポジトリに移動でき、そのステータスを実験的なAPIから公式のAPIに上げる必要があるというコンセンサスがあります。
  • Countを呼び出す追加のパフォーマンスペナルティがないため、IsEmptyプロパティは含まれていません。 コレクションデータ構造の大部分にはIsEmptyが含まれていません。
  • ICollectionのIsSynchronizedプロパティとSyncRootプロパティは、事実上廃止されているため、明示的に実装されました。 これは、他のSystem.Collection.Genericデータ構造に使用されるパターンにも従います。
  • System.Collections.Queueの確立された動作と一致するようにキューが空の場合、デキューとピークはInvalidOperationExceptionをスローします
  • IProducerConsumerCollectionドキュメントにはスレッドセーフなコレクションのみを対象としていると記載されているため、実装されませんでした。

未解決の質問

  • foreachを使用するときに、GetEnumeratorの呼び出し中に追加のヒープ割り当てを回避することは、ネストされたパブリック列挙子構造を含めるための十分に強力な根拠ですか?
  • CopyTo、ToArray、およびGetEnumeratorは、優先順位(ソート済み)の順序、またはデータ構造で使用される内部順序で結果を返す必要がありますか? 追加のパフォーマンスペナルティが発生しないため、内部注文は返送する必要があると思います。 ただし、開発者がクラスを優先キューではなく「ソートされたキュー」と見なす場合、これは潜在的なユーザビリティの問題です。
  • PriorityQueueという名前の型をSystem.Collections.Genericに追加すると、重大な変更が発生する可能性がありますか? 名前空間は頻繁に使用されるため、独自の優先キュータイプを含むプロジェクトでソースの互換性の問題が発生する可能性があります。
  • IComparerの出力に基づいて、アイテムを昇順または降順でデキューする必要があります? (私の仮定は、IComparerの通常の並べ替え規則に一致する昇順です。)。
  • コレクションは「安定」する必要がありますか? つまり、IComparisonが等しい2つのアイテムが必要です。結果は、キューに入れられたのとまったく同じ順序でデキューされますか? (私の仮定では、これは必要ありません)

    更新

  • 'Construct Using IEnumerable'の複雑さをΘ(n)に修正しました。 @svickに感謝します。

  • IComparerと比較して、優先キューを昇順または降順のどちらで並べ替えるかに関する別のオプションの質問を追加しました
  • 新しいパターンを使用する代わりに、他のSystem.Collection.Genericタイプの動作と一致するように、明示的なSyncRootプロパティからNotSupportedExceptionを削除しました。
  • パブリックGetEnumeratorメソッドがIEnumerableではなくネストされた列挙子構造体を返すようになりました、既存のSystem.Collections.Generic型と同様です。 これは、foreachループを使用するときにヒープ(GC)の割り当てを回避するための最適化です。
  • ComVisible属性を削除しました。
  • Clearの複雑さをΘ(n)に変更しました。 @mbeidlerに感謝します。
api-needs-work area-System.Collections wishlist

最も参考になるコメント

ヒープデータ構造は、leetcodeを実行するために必須です
より多くのleetcode、より多くのc#コードインタビュー、つまりより多くのc#開発者。
より多くの開発者はより良いエコシステムを意味します。
より良いエコシステムとは、明日もc#でプログラムできるかどうかを意味します。

要約すると、これは機能であるだけでなく、将来でもあります。 そのため、問題には「将来」というラベルが付けられています。

全てのコメント318件

| 操作| 複雑さ|
| --- | --- |
| IEnumerableを使用して構築する| Θ(logn)|

これはΘ(n)であるべきだと思います。 少なくとも入力を繰り返す必要があります。

+1

Rxには、本番環境でテスト済みの優先キュークラスがあります。

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

GetEnumeratorの呼び出し中およびforeachの使用時に追加のヒープ割り当てを回避するために、ネストされたパブリック列挙子構造を使用する必要がありますか? キューを介して列挙することはまれな操作であるため、私の仮定はノーです。

構造体列挙子を使用するQueue<T>と一貫性を保つために、構造体列挙子を使用することに傾倒します。 また、構造体列挙子が現在使用されていない場合、将来使用するようにPriorityQueue<T>を変更することはできません。

また、おそらくバッチ挿入の方法ですか? それが役立つ場合は、最初から開始するのではなく、常に前の挿入ポイントから並べ替えて続行できますか?:

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

ここに最初の実装のコピーを投げまし

いいね。 いくつかの初期フィードバック:

  • Queue<T>.EnumeratorIEnumerator.Reset明示的に実装します。 PriorityQueue<T>.Enumeratorも同じようにすべきですか?
  • Queue<T>.Enumeratorは、 _index == -2を使用して、列挙子が破棄されたことを示します。 PriorityQueue<T>.Enumeratorにも同じコメントがありますが、追加の_disposedフィールドがあります。 余分な_disposedフィールドを削除し、 Queue<T> _index == -2を使用して、構造体を小さくし、 Queue<T>との整合性を保つために破棄されたことを示します。
  • 静的な_emptyArrayフィールドを削除して、代わりにArray.Empty<T>()に置き換えることができると思います。

また...

  • 比較対象となる他のコレクション(例: Dictionary<TKey, TValue>HashSet<T>SortedList<TKey, TValue>SortedDictionary<TKey, TValue>SortedSet<T>など)では、nullを比較者に渡されます。この場合、 Comparer<T>.Defaultが使用されます。

また...

  • ToArrayは、新しい配列を割り当てる前に_size == 0チェックすることで最適化できます。この場合、 Array.Empty<T>()返すだけです。

@justinvp素晴らしいフィードバック、ありがとう!

  • 列挙子を実装しました。列挙子のコアで非推奨ではない機能であるため、明示的にリセットします。 公開されているかどうかは、コレクションタイプ間で一貫していないようであり、明示的なバリアントを使用しているのはごくわずかです。
  • _indexを優先して、_disposedフィールドを削除しました。ありがとうございます。 その夜の土壇場でそれを投げ入れて、明白を逃した。 古いSystem.Collections.Generic型では使用されていなくても、新しいコレクション型での正確性のためにObjectDisposedExceptionを保持することを決定しました。
  • Array.EmptyはF#機能なので、残念ながらここでは使用できません。
  • nullを受け入れるように比較パラメーターを変更しました。
  • ToArrayの最適化には注意が必要です。 _技術的に_話す配列は、長さがゼロの場合でも、C#では変更可能です。 実際には、割り当ては必要なく、最適化することができます。 私が考えていない副作用がある場合に備えて、私はより慎重な実装に傾いています。 意味的には、呼び出し元は引き続きその割り当てを期待しますが、これはマイナーな割り当てです。

@ebickle

Array.EmptyはF#機能であるため、残念ながらここでは使用できません。

もうありません: https

issue-574ブランチのebickle / corefxにコードを移行しました。

Array.Emptyを実装しました()すべてを変更して通常のビルドパイプラインに接続します。 私が紹介しなければならなかった一時的なちょっとした悩みの1つは、System.CollectionsプロジェクトをComparerとしてSystem.Collectionsnugetパッケージに依存させることでした。まだオープンソースではありません。

dotnet / corefx#966の発行が完了すると修正されます。

フィードバックを求めている重要な領域の1つは、ToArray、CopyTo、および列挙子の処理方法です。 現在、パフォーマンスが最適化されています。つまり、ターゲットアレイはヒープであり、比較対象によって並べ替えられていません。

3つのオプションがあります。
1)そのままにして、返された配列がソートされていないことを文書化します。 (これは優先キューであり、ソートされたキューではありません)
2)メソッドを変更して、返されたアイテム/配列を並べ替えます。 それらはO(n)演算ではなくなります。
3)メソッドと列挙可能なサポートを完全に削除します。 これは「純粋な」オプションですが、優先キューが不要になったときに、残りのキューに入れられたアイテムをすばやく取得する機能が削除されます。

フィードバックが必要なもう1つの点は、同じ優先度の2つのアイテムに対してキューを安定させる必要があるかどうかです(結果を0と比較してください)。 通常、優先度付きキューは、同じ優先度を持つ2つのアイテムがキューに入れられた順序でデキューされることを保証しませんが、System.Reactive.Coreで使用される内部優先度付きキューの実装では、このプロパティを確保するために追加のオーバーヘッドが必要であることに気付きました。 私の好みはこれをしないことですが、開発者の期待の観点からどちらのオプションが優れているかは完全にはわかりません。

私も.NETに優先キューを追加することに興味があったので、このPRに遭遇しました。 誰かがこの提案をする努力をしたのを見てうれしいです:)。 コードを確認した後、私は次のことに気づきました。

  • IComparer順序がEqualsと一致しない場合、このContains実装( IComparer )の動作は、一部のユーザーにとっては意外かもしれません。それは本質的に_同じ優先度のアイテムを含む_です。
  • Dequeueの配列を縮小するコードが見つかりません
  • Enqueueメソッドはnull引数を受け入れる必要がありますか?
  • Clearの複雑さはΘ(n)である必要があると思います。これは、それが使用するSystem.Array.Clearの複雑さだからです。 https://msdn.microsoft.com/en-us/library/system.array.clear%28v=vs.110%29.aspx

Dequeueの配列を縮小するコードが見つかりません

Queue<T>Stack<T>も配列を縮小しません(参照ソースに基づくと、まだCoreFXにはありません)。

@mbeidlerデキューに何らかの形式の自動配列縮小を追加することを検討しましたが、 @ svickが指摘したように、同様のデータ構造のリファレンス実装には存在しません。 .NET Core / BCLチームの誰かから、そのスタイルの実装を選択した特別な理由があるかどうか聞いてみたいと思います。

更新:リストをチェックしました、 列、Queue、およびArrayList-これらはいずれも、remove / dequeueで内部配列のサイズを縮小しません。

エンキューはnullをサポートする必要があり、nullを許可するものとして文書化されています。 バグに遭遇しましたか? そのエリアのユニットテストエリアがどれほど堅牢であるかはまだ思い出せません。

興味深いことに、 @ svickによってリンクされた参照ソースで、 Queue<T> _ShrinkThresholdという名前の未使用のプライベート定数があることに気付きました。 おそらく、その動作は以前のバージョンに存在していました。

Containsの実装でEquals IComparer代わりに次の単体テストを作成しました

@mbeidler良い点。 MSDNによると、IComparer/ IComparableゼロの値が同じソート順を持つことを保証するだけです。

ただし、他のコレクションクラスにも同じ問題が存在するように見えます。 SortedListを操作するようにコードを変更した場合、テストケースはContainsKeyで引き続き失敗します。 SortedListの実装.ContainsKeyはArray.BinarySearchを呼び出します。Array.BinarySearchはIComparerに依存して等しいかどうかをチェックします。 同じことがSortedSetにも当てはまります

おそらく、これは既存のコレクションクラスのバグでもあります。 残りのコレクションクラスを掘り下げて、IComparerを受け入れるが、同等性を個別にテストする他のデータ構造があるかどうかを確認します。 ただし、優先キューの場合、平等から完全に独立したカスタム順序付けの動作が期待されます。

修正とテストケースをフォークブランチにコミットしました。 含むの新しい実装は、リストの動作に直接基づいています。が含まれています。 リスト以降IEqualityComparerを受け入れません。動作は、機能的に同等です。

今日しばらくしてから、他の組み込みコレクションのバグレポートを提出する可能性があります。 おそらくリグレッション動作のために修正できませんが、少なくともドキュメントを更新する必要があります。

ContainsKeyIComparer<TKey>実装を使用することは理にかなっていると思います。それが、キーを指定するものだからです。 ただし、線形検索でIComparable<TValue>代わりにEqualsを使用する方が、 ContainsValue方が論理的だと思います。 ただし、この場合、型の自然順序が等しいと矛盾する可能性がはるかに低いため、スコープは大幅に縮小されます。

については、MSDNのドキュメントでそれを表示されますSortedList<TKey, TValue>のために、備考セクションContainsValue TValueのソート順序は平等の代わりに使用されていることを示しありません。

@terrajobstこれまでのAPI提案についてどう思いますか? これはCoreFXに適していると思いますか?

:+1:

これを提出していただきありがとうございます。 この提案を正式にレビューするのに十分なデータがあると思うので、「APIレビューの準備ができました」というラベルを付けました。

DequeuePeekはメソッドをスローしているため、呼び出し元は各呼び出しの前にカウントを確認する必要があります。 代わりに(またはさらに)並行コレクションのパターンに従ってTryDequeueTryPeek提供することは理にかなっていますか? 既存のジェネリックコレクションに非スローメソッドを追加することには未解決の問題があるため、これらのメソッドを持たない新しいコレクションを追加することは逆効果のようです。

@andrewgmorris関連https://github.com/dotnet/corefx/issues/4316「TryDequeueをキューに追加」

これについて基本的なレビューを行い、フレームワークにProrityQueueが必要であることに同意します。 ただし、その設計と実装を推進するのを手伝ってくれる人を雇う必要があります。 問題を把握した人は誰でも、 取り組むことができます。

では、APIにはどのような作業が残っていますか?

:これは、上記のAPIの提案から欠落しているPriorityQueue<T>実装する必要がありますIReadOnlyCollection<T>と一致するようにQueue<T>Queue<T>今すぐ実装IReadOnlyCollection<T> .NETのように4.6)。

配列ベースの優先キューが最適かどうかはわかりません。 .NETでのメモリ割り当ては非常に高速です。 古いmallocが処理したのと同じsearch-small-blocksの問題はありません。 ここから私の優先キューコードを使用することを歓迎します: https

@ebickle_speclet_の1つの小さなニット。 それは言う:

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

代わりに言うべきではありません、 /// Inserts object into the <see cref="PriorityQueue{T}"> by its priority.

@SunnyWarEnqueueメソッドのドキュメントを修正しました。ありがとうございます。

少し前に、この時点で共有することにしたスキップリストデータ構造に基づいて、優先度付きキューと同様の複雑さを持つデータ構造を作成しました: https

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

スキップリストは、Containsが平均的なケースO(log(n))あることを除いて、平均的なケースで上記の優先度キューの複雑さに一致します。 さらに、最初または最後の要素へのアクセスは一定時間の操作であり、順方向と逆方向の両方での反復は、PQの順方向の複雑さに一致します。

このような構造には明らかにメモリのコストが高くなるという欠点があり、 O(n)最悪の場合の挿入と削除に発展するため、トレードオフがあります...

これはすでにどこかに実装されていますか? 予定されているリリースはいつですか?
また、既存のアイテムの優先度を更新するのはどうですか?

@ Priya91 @ianhaysこれはレビューの準備ができているとマークする準備ができていますか?

これは上記のAPI提案にはありません:PriorityQueueIReadOnlyCollectionを実装する必要がありますキューに一致する(列IReadOnlyCollectionを実装するようになりました.NET 4.6以降)。

ここで@justinvpに同意します。

@ Priya91 @ianhaysこれはレビューの準備ができているとマークする準備ができていますか?

そうだと思います。 これはしばらくの間座っていました。 その上で動きましょう。

@justinvp @ianhaysIReadOnlyCollectionを実装するように仕様を更新しました。 ありがとうございました!

クラスの完全な実装があり、配列ベースの実装を使用するそれに関連付けられたPriorityQueueDebugViewがあります。 ユニットテストはまだ100%の範囲ではありませんが、興味があれば、少し作業をしてフォークからほこりを払うことができます。

@NKnuspererは、既存のアイテムの優先度を更新することについて良い点を述べました。 とりあえず省略しますが、スペックレビューの際に検討する必要があります。

完全なフレームワークには2つの実装があり、それらは可能な代替手段です。
https://referencesource.microsoft.com/#q = priorityqueue

参考までに、stackoverflowhttp://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueueのJavaPriorityQueueに関する質問があります 優先度が単なるint優先度ラッパーオブジェクトではなく、比較子によって処理されるのは興味深いことです。 たとえば、キュ​​ー内のアイテムの優先度をすでに簡単に変更することはできません。

APIレビュー:
CoreFXがこのタイプを使用することを期待しているため、CoreFXにこのタイプがあると便利であることに同意します。

APIシェイプの最終レビューのために、サンプルコードPriorityQueue<Thread>PriorityQueue<MyClass>ます。

  1. 優先順位をどのように維持しますか? 現在、それはTによってのみ暗示されています。
  2. エントリを追加するときに優先度を渡すことができるようにしますか? (これはかなり便利なようです)

ノート:

  • 優先度はそれ自体では変化しないと予想されます。そのためのAPIが必要になるか、キューにRemoveAddになります。
  • ここに明確な顧客コードがないことを考えると(タイプが欲しいという一般的な願いだけです)、何を最適化するかを決めるのは難しいです-パフォーマンス、使いやすさ、他の何か?

これは、CoreFXで使用するのに非常に便利なタイプです。 これをつかむことに興味がある人はいますか?

優先度付きキューをバイナリヒープに固定するという考えは好きではありません。 詳細については、私のAlgoKitwikiページをご覧ください。 簡単な考え:

  • 実装を特定のタイプのヒープに修正する場合は、それを4項ヒープにします。
  • 一部のシナリオでは、一部のタイプのヒープが他のタイプよりもパフォーマンスが高いため、ヒープの実装を修正するべきではありません。 したがって、実装を特定のヒープタイプに固定し、パフォーマンスを向上させるために別のヒープタイプが必要な場合、優先キュー全体をゼロから実装する必要があります。 それは間違っている。 ここではもっと柔軟になり、CoreFXコードの一部(少なくとも一部のインターフェイス)を再利用できるようにする必要があります。

昨年、私はいくつかのヒープタイプを実装しました。 特に、優先キューとして使用できるIHeapインターフェイスがあります。

  • ヒープと呼んでみませんか...? ヒープを使用する以外に優先キューを実装するための正しい方法を知っていますか?

私はすべて、 IHeapインターフェイスといくつかの最もパフォーマンスの高い実装(少なくともそれらの配列ベース)を紹介することに賛成です。 APIと実装は、上記でリンクしたリポジトリにあります。

したがって、_priorityqueues_はありません。 ヒープ

どう思いますか?

@karelz @safern @danmosemsft

@pgolebiowski使いやすく、非常にパフォーマンスの高いAPIを設計していることを忘れないでください。 PriorityQueue (これはコンピュータサイエンスの用語として確立されています)が必要な場合は、ドキュメント/インターネット検索で見つけることができます。
基礎となる実装がヒープ(私は個人的になぜだろうか)、または他の何かである場合、それはそれほど重要ではありません。 実装が単純な代替案よりも測定可能に優れていることを実証できると仮定します(コードの複雑さも、いくらか重要なメトリックです)。

したがって、ヒープベースの実装( IHeap APIなし)がいくつかの単純なリストベースまたは配列チャンクベースの実装よりも優れているとまだ信じている場合は、その理由を説明してください(理想的にはいくつかの文/段落で) )、実装アプローチについて合意を得ることができるようにします(そして、PRレビュー時に拒否される可能性のある複雑な実装であなたの側で時間を無駄にすることを避けます)。

ICollectionIEnumerableドロップしますか? ジェネリックバージョンを用意するだけです(ただし、ジェネリックIEnumerable<T>IEnumerableをもたらします)

@pgolebiowskiの実装方法によって、外部APIが変更されることはありません。 PriorityQueueは、動作/契約を定義します。 一方、 Heapは特定の実装です。

優先キューとヒープ

基礎となる実装がヒープ(私は個人的になぜだろうか)、または他の何かである場合、それはそれほど重要ではありません。 実装が単純な代替案よりも測定可能に優れていることを実証できると仮定します(コードの複雑さも、いくらか重要なメトリックです)。

したがって、ヒープベースの実装がいくつかの単純なリストベースまたは配列チャンクのリストベースの実装よりも優れているとまだ信じている場合は、その理由を説明してください(理想的には数文/段落で)。実装アプローチ[...]

わかった。 優先度付きキューは、何らかの方法で実装できる抽象的なデータ構造です。 もちろん、ヒープとは異なるデータ構造で実装することもできます。 しかし、これほど効率的な方法はありません。 結果として:

  • 優先度付きキューとヒープは同義語としてよく使用されます(これの最も明確な例として、以下のPythonドキュメントを参照してください)
  • 一部のライブラリで「優先度付きキュー」のサポートがある場合は常に、ヒープを使用します(以下のすべての例を参照)。

私の言葉を裏付けるために、理論的なサポートから始めましょう。 アルゴリズム入門、コルメン:

[…]優先度キューには、最大優先度キューと最小優先度キューの2つの形式があります。 ここでは、最大優先度キューを実装する方法に焦点を当てます。このキューは、最大ヒープに基づいています。

優先キューはヒープであると明確に述べました。 これはもちろんショートカットですが、あなたはその考えを理解します。 ここで、さらに重要なこととして、他の言語とその標準ライブラリが、説明している操作のサポートをどのように追加するかを見てみましょう。

  • Java: PriorityQueue<T> —ヒープで実装された優先キュー。
  • さび: BinaryHeap —APIで明示的にヒープします。 ドキュメントには、バイナリヒープで実装された優先キューであると記載されています。 しかし、APIは構造、つまりヒープについて非常に明確です。
  • Swift: CFBinaryHeap —繰り返しになりますが、データ構造が何であるかを明示的に示し、「優先度付きキュー」という抽象的な用語の使用を避けます。 クラスについて説明しているドキュメント:バイナリヒープは、優先度付きキューとして役立ちます。 私はそのアプローチが好きです。
  • C ++: priority_queue —繰り返しになりますが、配列の上部に構築されたバイナリヒープを使用して正規に実装されています。
  • Python: heapq —ヒープはAPIで明示的に公開されています。 優先度付きキューについては、ドキュメントでのみ説明されていますヒープは二分木です。 また、ヒープが優先度付きキューに等しく、ヒープが二分木に等しいという大きなショートカットにも注目してください。
  • Go: heap package —ヒープインターフェイスもあります。 明示的な優先度付きキューはありませんが、ドキュメントにのみあります。ヒープは、優先度付きキューを実装する一般的な方法です。

Rust / Swift / Python / Goの方法でヒープを明示的に公開し、優先度付きキューとして使用できることをドキュメントに明確に記載する必要があると強く信じています。 このアプローチは非常にクリーンでシンプルだと強く信じています。

  • 私たちはデータ構造について明確であり、将来的にAPIを拡張できるようにします—誰かがいくつかの面でより良い優先度キューを実装する新しい革新的な方法を思いついた場合(そして新しいタイプに対するヒープの選択はシナリオ)、APIはそのままにしておくことができます。 ライブラリを強化する新しい革新的なタイプを追加するだけで、ヒープクラスはまだそこに存在します。
  • 選択したデータ構造が安定しているかどうかをユーザーが疑問に思っていると想像してみてください。 私たちが従う解決策が優先キューである場合、これは明らかではありません。 この用語は抽象的なものであり、何でもその下に置くことができます。 したがって、ユーザーがドキュメントを検索して、内部でヒープを使用しているために安定していないことを確認するために、ある程度の時間が失われます。 これは、これがヒープであることをAPIを介して明示的に示すだけで回避できたはずです。

ヒープデータ構造を明確に公開し、ドキュメントでのみ優先度付きキューの参照を作成するというアプローチを皆さんが気に入ってくれることを願っています。

APIと実装

上記の問題に同意する必要がありますが、別のトピック、つまりこれを実装する方法から始めましょう。 私はここで2つの解決策を見ることができます:

  • ArrayHeapクラスを提供するだけです。 名前を見ると、処理しているヒープの種類がすぐにわかります(ここでも、ヒープの種類は数十あります)。 配列ベースであることがわかります。 あなたはすぐにあなたが扱っている獣を知っています。
  • 私がもっと好きなもう1つの可能性は、 IHeapインターフェイスを提供し、1つ以上の実装を提供することです。 顧客はインターフェースに依存するコードを書くことができます—これは複雑なアルゴリズムの本当に明確で読みやすい実装を提供することを可能にします。 DijkstraAlgorithmクラスを書くことを想像してみてください。 IHeapインターフェイス(1つのパラメーター化されたコンストラクター)に依存するか、 ArrayHeap (デフォルトのコンストラクター)を使用するだけです。 「優先キュー」という用語を使用しているため、明確で、単純で、明示的で、あいまいさがありません。 そして、理論的に非常に理にかなっている素晴らしいインターフェース。

上記のアプローチでは、 ArrayHeapは、配列として格納された、暗黙のヒープ順の完全なd-aryツリーを表します。 これを使用して、たとえばBinaryHeapまたはQuaternaryHeapを作成できます。

結論

さらに議論するために、このペーパーをご覧になることを強くお勧めします。 あなたはそれを知っているでしょう:

  • 4項ヒープ(4進)は、2項ヒープ(2進)よりも単純に高速です。 このホワイトペーパーでは多くのテストが行​​われています。 implicit_4implicit_2 (場合によってはimplicit_simple_4implicit_simple_2 )のパフォーマンスを比較することに興味があります。

  • 実装の最適な選択は、入力に大きく依存します。 暗黙のd-aryヒープ、ペアリングヒープ、Fibonacciヒープ、二項ヒープ、明示的なd-aryヒープ、ランクペアリングヒープ、地震ヒープ、違反ヒープ、ランク緩和された弱いヒープ、および厳密なFibonacciヒープのうち、次のタイプはさまざまなシナリオのほぼすべてのニーズに対応します。

    • 暗黙のd-aryヒープ(ArrayHeap)、<-確かにこれが必要です
    • ペアリングヒープ、<-すてきでクリーンなIHeapアプローチを採用する場合、これを追加する価値があります。多くの場合、配列ベースのソリューションよりも非常に高速です。

    どちらの場合も、プログラミングの労力は驚くほど少ないです。 私の実装を見てください。


@karelz @benaadams

これは、CoreFXで使用するのに非常に便利なタイプです。 これをつかむことに興味がある人はいますか?

@safernこれからこれを手に入れてとても嬉しいです。

OK、それは私のキーボードと私の椅子の間の問題でした- PriorityQueueもちろんHeap基づいています-私はそれが意味をなさないQueueだけを考えていましたそして、ヒープが「ソート」されていることを忘れました。ロジック、アルゴリズム、チューリングマシンなどを愛する私のような人にとって、非常に恥ずかしい思考プロセスの停止です。お詫びします。 (BTW:Javaドキュメントのリンクにあるいくつかの文を読むとすぐに、矛盾がすぐにクリックされました)

その観点から、 Heap上にAPIを構築することは理にかなっています。 ただし、そのクラスをまだ公開するべきではありません。CoreFXで必要なものである場合は、独自のAPIレビューと独自のディスカッションが必要になります。 実装によるAPIの表面クリープは望ましくありませんが、それは正しいことかもしれません。したがって、議論が必要です。
その観点から、まだIHeapを作成する必要はないと思います。 後で良い決断かもしれません。
特定のヒープ(たとえば、上記の4-ary)が一般的なランダム入力に最適であるという研究がある場合は、それを選択する必要があります。 @safern @ ianhays @ stephentoubが意見を確認/

複数の実装されたオプションを使用した基礎となるヒープのパラメーター化は、IMOがCoreFXに属していないものです(ここでも間違っている可能性があります。他の人の考えを見てみましょう)。
私の理由は、IMOが間もなく数十億の特殊なコレクションを出荷するためです。これらのコレクションは、人々(アルゴリズムのニュアンスに強いバックグラウンドを持たない平均的な開発者)が選択するのが非常に困難です。 ただし、このようなライブラリは、あなた/コミュニティが所有するこの分野の専門家にとって、優れたNuGetパッケージになります。 将来的には、 PowerCollectionsに追加することを検討する可能性があります(過去4か月間、GitHubでこのライブラリを配置する場所と、所有する必要があるかどうか、またはコミュニティに所有を促す必要があるかどうかについて活発に話し合っています-この問題についてはさまざまな意見があります、2.0以降の運命を確定する予定です)

あなたがそれに取り組みたいようにあなたに割り当てる...

@pgolebiowskiコラボレーターの招待状が送信されました

@benaadams私はICollectionを維持します(穏やかな好み)。 CoreFXの他のdsとの一貫性のため。 IMOここに奇妙な獣を1つ持つ価値はありません...少数の新しい獣を追加する場合(たとえば、PowerCollectionsを別のリポジトリに追加する場合)、一般的でないものを含めるべきではありません...考え?

OK、それは私のキーボードと私の椅子の間の問題でした。

ハハ😄心配ありません。

ヒープの上にAPIを構築することは理にかなっています。 ただし、まだそのクラスを公開するべきではありません[...]実装によるAPIサーフェスのクリープは望ましくありませんが、それは正しいことかもしれません。したがって、議論が必要です。 [...]まだIHeapを作成する必要はないと思います。 後で良い決断かもしれません。

グループの決定がPriorityQueue場合、私は設計を支援し、これを実装します。 ただし、ここでPriorityQueueを追加すると、後でAPIでHeapを追加するのが面倒になるという事実を考慮してください。どちらも、基本的に同じように動作します。 それは一種の冗長IMOでしょう。 これは私にとってデザインの匂いです。 優先キューは追加しません。 それは役に立ちません。

また、もう1つ考えました。 実際、ペアリングヒープのデータ構造はかなり頻繁に役立つ可能性があります。 配列ベースのヒープは、それらをマージするのが恐ろしいです。 この操作は基本的に線形です。 マージするヒープがたくさんある場合は、パフォーマンスが低下します。 ただし、配列ヒープの代わりにペアリングヒープを使用する場合、マージ操作は一定です(償却されます)。 これは、私が素晴らしいインターフェースと2つの実装を提供したい理由のもう1つの議論です。 1つは一般的な入力用、2つ目は特定のシナリオ用、特にヒープのマージが含まれる場合です。

IHeap + ArrayHeap + PairingHeap投票してください! 😄(Rust / Swift / Python / Goのように)

ペアリングヒープが多すぎる場合-OK。 しかし、少なくともIHeap + ArrayHeap行きましょう。 PriorityQueueクラスを使用すると、将来の可能性がロックされ、APIが不明確になると思いませんか?

しかし、私が言ったように、提案されたソリューションに対してPriorityQueueクラスに投票するなら、OKです。

共同編集者の招待状が送信されました-pingを受け入れると、それを割り当てることができます(GitHubの制限)。

@karelz ping :)

ここでPriorityQueueを追加すると、後でAPIでヒープを追加するのが面倒になるという事実を考慮してください。どちらも基本的に同じように動作するためです。 それは一種の冗長IMOでしょう。 これは私にとってデザインの匂いです。 優先キューは追加しません。 それは役に立ちません。

後で面倒になる理由について詳しく説明していただけますか? あなたの懸念は何ですか?
PriorityQueueは、人々が使用する概念です。 そのように名前が付けられたタイプがあると便利ですよね?
Heapの論理演算(少なくともそれらの名前)は異なるかもしれないと思います。 それらが同じである場合、最悪の場合、同じコードの2つの異なる実装を持つことができます(理想的ではありませんが、世界の終わりではありません)。 または、 PriorityQueue親としてHeapクラスを挿入できますか? (APIレビューの観点から許可されていると仮定します-現時点では理由はわかりませんが、APIレビューに関する長年の経験がないため、他の人が確認するのを待ちます)

投票とさらなる設計の議論がどのように進むか見てみましょう...私はIHeap + ArrayHeapのアイデアにゆっくりとウォーミングアップしていますが、まだ完全には確信していません...

少数の新しいものを追加する場合...一般的でないものを含めるべきではありません

雄牛への赤いぼろきれ。 ICollection削除できるように、他のコレクションを追加した人はいますか?

循環/リングバッファ; ジェネリックとコンカレント?

@karelz命名の問題の解決策は、DataFlowが農産物/消費者パターンに対して行うようなIPriorityQueueようなものである可能性があります。 優先キューを実装する多くの方法があり、それを気にしない場合はインターフェイスを使用します。 実装に注意するか、インスタンス使用実装クラスを作成しています。

後で面倒になる理由について詳しく説明していただけますか? あなたの懸念は何ですか?
PriorityQueueは、人々が使用する概念です。 そのように名前が付けられたタイプがあると便利ですよね? […]私はIHeap + ArrayHeapのアイデアにゆっくりとウォーミングアップしていますが、まだ完全には確信していません...

@karelz私の経験から、抽象化( IPriorityQueueまたはIHeap )を持つことが非常に重要であることがわかりました。 このようなアプローチのおかげで、開発者は分離されたコードを書くことができます。 (特定の実装ではなく)インターフェイスに対して記述されているため、柔軟性とIoCスピリットが向上します。 このようなコードの単体テストを作成するのは非常に簡単です(依存性注入を使用すると、独自のモックIPriorityQueueまたはIHeapを注入して、どのメソッドがいつ、どの引数で呼び出されるかを確認できます)。 抽象化は良いです。

「優先キュー」という用語が一般的に使用されているのは事実です。 問題は、優先キューを効果的に実装する方法が1つしかないことです。それは、ヒープを使用することです。 たくさんの種類のヒープ。 だから私たちは持つことができます:

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

また

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

私にとっては、2番目のアプローチの方が見栄えがします。 それについてどう思いますか?

PriorityQueueクラスについて—曖昧すぎて、そのようなクラスには完全に反対だと思います。 あいまいなインターフェースは良いですが、実装ではありません。 バイナリヒープの場合は、 BinaryHeapと呼んでください。 それ以外の場合は、それに応じて名前を付けます。

SortedDictionaryようなクラスの問題のため、私はすべてクラスに明確に名前を付けることにSortedListとどのように違うのかについて、開発者の間で多くの混乱があります。 単にBinarySearchTreeと呼ばれたら、生活はもっと簡単になり、家に帰って子供たちに早く会うことができます。

ヒープにヒープという名前を付けましょう。

@benaadamsそれぞれが

最も可能性の高い結果(私はソリューションが好きなので、偏った主張)は、別のリポジトリを作成し、PowerCollectionsでプライミングしてから、チームからの基本的なガイダンス/監視でコミュニティに拡張させることです。
ところで: @terrajobstは、それは価値がないと考えており(「エコシステムには、より良く、より影響力のあることを行う」)、コミュニティがそれを完全に推進し(既存のPowerCollectionsから始めることを含む)、私たちの1
//私たちが決心する前にコミュニティがこの解決策に飛びつく機会があると思います;-)。 それは議論(そして私の好み)を無言にするでしょう;-)

@pgolebiowskiあなたは、 Heapを持っている方がPriorityQueueよりも優れているとゆっくりと私に納得させています-強力なガイダンスとドキュメントが必要です "これがPriorityQueueやり方です- Heap使用してください

ただし、CoreFXに複数のヒープ実装を含めることを非常に躊躇しています。 そこにある「通常の」C#開発者の98%以上は気にしません。 彼らはどちらが最良かを考えたくもありません、彼らはただ仕事を成し遂げるために何かを必要とします。 すべてのSWのすべての部分が高性能を念頭に置いて設計されているわけではありません。 すべての1回限りのツール、UIアプリなどについて考えてください。このdsがクリティカルパス上にある高性能のスケールアウトシステムを設計しているのでない限り、気にする必要はありません。

同じように、 SortedDictionaryArrayListまたはその他のdsがどのように実装されているかは関係ありません。それらは適切に機能します。 私は(他の多くの人と同じように) 、シナリオでこれらのdsから高いパフォーマンスが必要な場合は、パフォーマンスを測定したり、実装を確認したりして、シナリオに十分かどうかを判断する必要があることを理解し

2%ではなく、98%のユースケースでユーザビリティを最適化する必要があります。 あまりにも多くのオプション(実装)を展開し、全員に決定を強いる場合、98%のユースケースで不必要な混乱を引き起こしました。 私はそれが価値があるとは思わない...
IMO .NETエコシステムは、非常に適切なパフォーマンス特性を備えた多くのAPI(コレクションだけでなく)の単一の選択肢を提供することに大きな価値があり、ほとんどのユースケースに役立ちます。 そして、それを必要とし、より深く掘り下げてより多くを学び、知識に基づいた選択とトレードオフを行うことをいとわない人々のために高性能の拡張を可能にするエコシステムを提供します。

とは言うものの、インターフェースIHeapIDictionaryIReadOnlyDictionary )を持つことは理にかなっているかもしれません-もう少し考えなければなりません/ APIレビューの専門家に聞いてくださいスペース ...

@pgolebiowskiISet<T>HashSet<T>話していることは、すでに(ある程度)わかっています。 私はそれをミラーリングすると言います。 したがって、上記のAPIはインターフェース( IPriorityQueue<T> )に変更され、独自のクラスとして公開される場合とされない場合があるヒープを内部的に使用する実装( HeapPriorityQueue<T> )があります。

それ( PriorityQueue<T> )もIList<T>実装する必要がありますか?

@karelz ICollectionに関する私の問題はSyncRootIsSynchronizedです; どちらかが実装されているため、ロックオブジェクトに追加の割り当てがあります。 または、それらを持っていることが少し無意味なときに、それらは投げます。

@benaadamsこれは誤解を招く可能性があります。 優先度付きキューの実装の99.99%は配列に基づくヒープであるため(ここでも同様に使用します)、配列の内部構造へのアクセスを公開することを意味しますか?

要素4、8、10、13、30、45のヒープがあるとします。順序を考慮すると、インデックス0、1、2、3、4、5によってアクセスされます。ただし、ヒープの内部構造は[4、8、30、10、13、45]です(バイナリでは、クォータナリでは異なります)。

  • インデックスiで内部番号を返すことは、ほとんど任意であるため、ユーザーの観点からは実際には意味がありません。
  • 番号を(優先度で)順番に返すのはコストがかかりすぎます-これは線形です。
  • これらのいずれかを返すことは、他の実装では実際には意味がありません。 多くの場合、 i要素をポップし、 i番目の要素を取得して、もう一度プッシュします。

IList<T>は通常、次のような生意気な回避策です。APIが受け入れるコレクションに柔軟に対応したいので、それらを列挙したいが、 IEnumerable<T>介して割り当てたくない。

のための一般的なインターフェースがないことに気づきました

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

だからそれについては気にしないでください😢(IReadOnlyCollectionのようなものですが)

ただし、列挙子のリセットは明示的に実装する必要があります。これは、問題があり、スローする必要があるためです。

だから私の提案された変更

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

メソッドについて話し合っているので... IHeapIPriorityQueue違いについては、まだ合意していません。メソッドの名前とロジックに少し影響します。 ただし、いずれにせよ、現在のAPI提案には次のものが欠けていることがわかります。

  • キューから特定の要素を削除する機能
  • 要素の優先度を更新する機能
  • これらの構造をマージする機能

これらの操作は非常に重要であり、特に要素を更新する可能性があります。 これがないと、多くのアルゴリズムを実装できません。 ハンドルタイプを導入する必要があります。 ここIHeapNodeです。 これは、 IHeap方法を採用するための別の議論です。そうしないと、常にヒープノードになるPriorityQueueHandleタイプを導入する必要があるためです...😜さらに、それが何を意味するのかあいまいです...一方、ヒープノード-誰もがその内容を知っており、自分が扱っているものを想像することができます。

実際、簡単に言えば、APIの提案については、このディレクトリをご覧ください。 おそらくそのサブセットだけが必要になるでしょう。 しかし、それでも、IMOに必要なものが含まれているだけなので、出発点として検討する価値があるかもしれません。

皆さん、どう思いますか?

IHeapNodeは、clrタイプのKeyValuePairと大差ありませんか?

しかし、それは優先順位とタイプを分離しているので、今ではPriorityQueue<TKey, TValue>IComparer<TKey> comparer

KeyValuePairは構造体であるだけでなく、そのプロパティは読み取り専用です。 基本的には、構造が更新されるたびに新しいオブジェクトを作成することと同じです。

キーの使用は、等しいキーでのみ機能しません。更新/削除する要素を知るには、より多くの情報が必要です。

IHeapNodeおよび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;
        }
    }

そして、 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);
        }

しかし、ヒープ内のすべての要素は追加のオブジェクトになりました。 PriorityQueueの動作が必要であるが、これらの追加の割り当てが必要ない場合はどうなりますか?

そうすると、要素を更新/削除できなくなります。 これは、 DecreaseKey操作(非常に一般的です)に依存するアルゴリズムを実装できない裸の実装になります。 たとえば、ダイクストラのアルゴリズム、プリムのアルゴリズム。 または、ある種のスケジューラーを作成していて、あるプロセス(またはその他)がその優先順位を変更した場合、それに対処することはできません。

また、他のすべてのヒープ実装では、要素は明示的に単なるノードです。 他の場合は完全に自然です。ここでは少し人工的ですが、更新/削除するために必要です。

Javaでは、優先キューには要素を更新するオプションがありません。 結果:

AlgoKitプロジェクトにヒープを実装し、これらすべての設計上の問題に遭遇したとき、これが.NETの作成者がヒープ(または優先度付きキュー)などの基本的なデータ構造を追加しないことにした理由だと思いました。 どちらのデザインも良くないからです。 それぞれに欠点があります。

簡単に言えば、優先度による要素の効率的な順序付けをサポートするデータ構造を追加する場合は、正しい方法で実行し、要素の更新/削除などの機能を追加することをお勧めします。

配列内の要素を別のオブジェクトでラップすることに不安を感じる場合は、これだけが問題ではありません。 もう1つはマージです。 配列ベースのヒープでは、これは完全に非効率的です。 ただし...ペアリングヒープデータ構造(ペアリングヒープ:自己調整ヒープの新しい形式)を使用する場合、次のようになります。

  • 要素へのハンドルは素晴らしいノードです-これらはとにかく割り当てられるべきなので、厄介なものはありません
  • マージは一定です(配列ベースのソリューションでは線形に対して)

実際、これは次の方法で解決できます。

  • すべてのメソッドをサポートするIHeapインターフェースを追加します
  • ArrayHeapPairingHeapをこれらすべてのハンドルで追加し、更新、削除、マージします
  • 追加PriorityQueue単なるラッパーであるArrayHeap APIを簡素化し、

PriorityQueueSystem.Collections.Genericあり、すべてのヒープはSystem.Collections.Specializedます。

動作しますか?

APIレビューを通じて3つの新しいデータ構造を取得する可能性はほとんどありません。 ほとんどの場合、APIの量は少ない方が良いです。 不十分になった場合はいつでも後で追加できますが、APIを削除することはできません。

これが、私がHeapNodeクラスのファンではない理由の1つです。 そのようなことは内部のみである必要があり、APIは可能であれば既存の型を公開する必要があります-この場合はおそらくKVPです。

@ianhaysこれが内部のみに保持されている場合、ユーザーはデータ構造内の要素の優先度を更新できません。 これはほとんど役に立たず、Javaのすべての問題が発生することになります。つまり、ネイティブライブラリにすでに存在するデータ構造を再実装する人々です...私には悪いように思えます。 ノードを表す単純なクラスを持つよりもはるかに悪いです。

ところで:リンクリストには、ユーザーが適切な機能を使用できるようにノードクラスがあります。 これはほとんどミラーリングです。

ユーザーは、データ構造内の要素の優先順位を更新できません。

それは必ずしも真実ではありません。 優先度は、の代わりに次のような追加のデータ構造を必要としない方法で公開される可能性があります。

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

        }

あなたは例えば

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

        }

タイプパラメータとして優先度を公開することについても、私はまだ確信していません。 大多数の人がデフォルトの優先度タイプを使用することになり、TValueは不要な特異性になります。

void Update(TKey item, TValue priority)

まず、コードのTKeyTValueは何ですか? それはどのように機能するはずですか? コンピュータサイエンスの慣習は次のとおりです。

  • キー=優先度
  • 値=要素に保存したいもの

2番:

大多数の人がデフォルトの優先度タイプを使用することになり、TValueは不要な特異性になります。

「デフォルトの優先度タイプ」を定義してください。 PriorityQueue<T>が欲しいだけだと思いますよね? その場合は、ユーザーが新しいクラス、優先度と値のラッパーを作成し、さらにIComparableようなものを実装するか、カスタムコンパレータを提供する必要があるという事実を考慮してください。 かなり貧しい。

まず、コード内のTKeyとTValueは何ですか?

アイテムとアイテムの優先度。 それらを優先度とその優先度に関連付けられたアイテムに切り替えることができますが、TKeyが必ずしも一意であるとは限らない(つまり、優先度の重複を許可する)コレクションがあります。 私はそれに反対していませんが、私にとってTKeyは通常一意性を意味します。

重要なのは、Nodeクラスを公開することは、Updateメソッドを公開するための要件ではないということです。

「デフォルトの優先度タイプ」を定義してください。 PriorityQueueが欲しいだけだと感じます、そうですか?

はい。 このスレッドをもう一度読んでも、それが事実であると完全に確信しているわけではありません。

その場合は、ユーザーが新しいクラス、優先度と値のラッパーを作成し、さらにIComparableなどを実装するか、カスタムコンパレータを提供する必要があるという事実を考慮してください。 かなり貧しい。

あなたは間違っていません。 かなりの数のユーザー、カスタムコンパレータロジックを使用してラッパータイプを作成する必要

.Netの他の部分との一貫性を保つために、タイプはHeap<T>ArrayHeap<T> 、さらにはQuaternaryHeap<T>ではなく、 PriorityQueue<T>と呼ばれるべきだと思います。

  • List<T>ではなく、 ArrayList<T>ます。
  • Dictionary<K, V>ではなく、 HashTable<K, V>ます。
  • ImmutableList<T>ではなくImmutableAVLTreeList<T>ます。
  • 我々は持っているArray.Sort() 、ないArray.IntroSort()

そのようなタイプのユーザーは通常、それらがどのように実装されているかを気にしません、それは確かにタイプについて最も目立つものであるべきではありません。

その場合は、ユーザーが新しいクラス、優先度と値のラッパーを作成し、さらにIComparableなどを実装するか、カスタムコンパレータを提供する必要があるという事実を考慮してください。

あなたは間違っていません。 かなりの数のユーザーが、カスタムコンパレータロジックを使用してラッパータイプを作成する必要があると思います。

サマリーAPIでは、比較は提供された比較者IComparer<T> comparerによって提供され、ラッパーは必要ありません。 多くの場合、優先度は型の一部になります。たとえば、タイムスケジューラは、型のプロパティとして実行時間を持ちます。

カスタムIComparer KeyValuePairを使用しても、追加の割り当ては追加されません。 ただし、アイテムではなく値を比較する必要があるため、更新のために1つの余分な間接参照が追加されます。

アイテムがrefreturnを介して取得され、更新されてrefによってupdateメソッドに戻され、refが比較されない限り、構造体アイテムの更新は問題になります。 しかし、それは少し恐ろしいです。

@svick

.Netの他の部分との一貫性を保つために、タイプはHeap<T>ArrayHeap<T> 、さらにはQuaternaryHeap<T>ではなく、 PriorityQueue<T>と呼ばれるべきだと思います。

私は人類への信頼を失っています。

  • データ構造PriorityQueue呼び出しは、.NETの他の部分と一貫性があり、 Heap呼び出しはそうではありませんか? ヒープに何か問題がありますか? その場合、同じことがスタックとキューにも当てはまります。
  • ArrayHeap -> QuaternaryHeap基本クラス。 大きな違い。
  • 議論はクールな名前を選ぶことの問題ではありません。 特定の設計パスに従った結果として、たくさんのものがやってくる。 スレッドを読み直してください。
  • ハッシュテーブルArrayList 。 それらは存在しているようです。 ところで、ユーザーの最初の考えはListはリストであるが、リストではないということなので、「リスト」はIMOに名前を付けるのにかなり悪い選択です。 😜
  • それは、楽しんで、物事がどのように実装されているかを言うことではありません。 それは、ユーザーが何を扱っているかをすぐに示す意味のある名前を付けることです。

.NETの他の部分との一貫性を保ち、このような問題が発生したいですか?

そして、そこで何がわかりますか... Jon Skeetは、実装をより厳密に反映しているため、 SortedDictionarySortedTreeと呼ばれるべきだと言っています。

@pgolebiowski

ヒープに何か問題がありますか?

はい、使用法ではなく、実装について説明しています。

その場合、同じことがスタックとキューにも当てはまります。

どちらも実際には実装を説明していません。たとえば、名前はそれらが配列ベースであることを示していません。

ArrayHeap -> QuaternaryHeap基本クラス。 大きな違い。

はい、私はあなたのデザインを理解しています。 私が言っているのは、これのどれもユーザーに公開されるべきではないということです。

特定の設計パスに従った結果として、たくさんのものがやってくる。 スレッドを読み直してください。

私はスレッドを読みました。 デザインはBCLのものではないと思います。 「四次ヒープ」とは何か、使うべきかどうか疑問に思われるようなものではなく、使いやすく理解しやすいものが含まれているべきだと思います。

デフォルトの実装が彼らにとって十分ではない場合、それは他のライブラリ(あなた自身のような)が入ってくるところです。

ハッシュテーブル、ArrayList。 それらは存在しているようです。

はい、これらは.Net Framework 1.0クラスであり、誰も使用していません。 私の知る限り、それらの名前はJavaからコピーされたものであり、.Net Framework2.0の設計者はその規則に従わないことにしました。 私の意見では、それは正しい決断でした。

ところで、ユーザーの最初の考えはListはリストであるが、リストではないということなので、「リスト」はIMOに名前を付けるのにかなり悪い選択です。

です。 リンクリストではありませんが、同じことではありません。 そして、どこにでもArrayListResizeArray (F#のList<T>の名前)を書く必要がないのが好きです。

それは、ユーザーが何を扱っているかをすぐに示す意味のある名前を付けることです。

QuaternaryHeap表示された場合、ほとんどの人は自分が何を扱っているのかわかりません。 一方、 PriorityQueueが表示されている場合は、CSのバックグラウンドがなくても、何を扱っているのかが明確になっているはずです。 彼らは実装が何であるかを知りませんが、それがドキュメントの目的です。

@ianhays

まず、コード内のTKeyとTValueは何ですか?

アイテムとアイテムの優先度。 それらを優先度とその優先度に関連付けられたアイテムに切り替えることができますが、TKeyが必ずしも一意であるとは限らない(つまり、優先度の重複を許可する)コレクションがあります。 私はそれに反対していませんが、私にとってTKeyは通常一意性を意味します。

キー-優先順位付けを行うために使用されるもの。 キーは一意である必要はありません。 そのような仮定をすることはできません。

重要なのは、Nodeクラスを公開することは、Updateメソッドを公開するための要件ではないということです。

私はそれが次のように信じています:ネイティブライブラリでのDecrease-key / Increase-keyのサポート。 このトピックについても、データ構造の処理に関係するヘルパークラスがあります。

かなりの数のユーザーが、カスタムコンパレータロジックを使用してラッパータイプを作成する必要があると思います。 優先度付きキューに入れたい同等のタイプ、またはコンパレータが定義されたタイプをすでに持っているユーザーもかなりの数いると思います。 問題は、どちらの陣営が2つのうち大きいかです。

他のことはわかりませんが、優先度付きキューを使用するたびに、カスタムコンパレータロジックを使用してラッパーを作成するのはかなり恐ろしいことです...

別の考え-ラッパーを作成したとしましょう:

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

私はこのタイプでPriorityQueue<T>を給餌します。 したがって、それに基づいて要素に優先順位を付けます。 いくつかのキーセレクターが定義されているとしましょう。 このような設計は、ヒープの内部作業をクラッシュさせる可能性があります。 要素の所有者であるため、いつでも優先度を変更できます。 ヒープを更新するための通知メカニズムはありません。 あなたはそれらすべてを処理し、それを呼び出す責任があります。 かなり危険で、私には簡単ではありません。

私が提案したハンドルの場合、タイプはユーザーの観点から不変でした。 そして、独自性に問題はありませんでした。

IMO IHeapインターフェースを持ち、次にそのインターフェースを実装するクラスAPIを持つというアイデアが好きです。 私は@ianhaysPriorityQueueに焦点を当てたいので、顧客に異なる種類のヒープを提供するために3つの新しいAPIを公開するつもりはありません。

次に、値をキューに格納するためにNodeの内部/パブリッククラスを用意する必要はないと思います。 追加の割り当ては必要ありません。IDictionaryが行うことのようなものに従うことができ、Enumerableの値を生成するときにKeyValuePairを作成できます。保存するアイテムごとに優先度を設定するオプションを選択した場合はオブジェクトになります(これが最善の方法ではないと思います。アイテムごとに優先度を保存することを完全には確信していません)。 私が望む最善のアプローチはPriorityQueue<T> (この名前はそれに1つを与えるためだけのものです)。 このアプローチでは、BCL全体がIComparer<T>で行うことに従い、その比較者と比較して優先順位を付けるコンストラクターを作成できます。 優先度をパラメーターとして渡すAPIを公開する必要はありません。

一部のAPIに優先順位を付けると、デフォルトの優先順位にしたい通常の顧客にとっては「使いにくく」なるか、より複雑になると思います。その場合、APIを与えるときに、その使用法を理解するのがより複雑になります。カスタムIComparer<T>を使用するオプションが最も合理的であり、BCL全体のガイドラインに従います。

名前は彼らが行うことであり、彼らがそれを行う方法ではありません。

それらは、実装とその達成方法ではなく、抽象的な概念とユーザーのために達成するものにちなんで名付けられています。 (これはまた、それがより良いことが証明されれば、それらの実装を改善して別の実装を使用できることを意味します)

その場合、同じことがスタックとキューにも当てはまります。

Stackは無制限のサイズ変更配列であり、 Queueは無制限のサイズ変更循環バッファーとして実装されます。 それらは抽象的な概念にちなんで名付けられています。 スタック(抽象データ型) :後入れ先出し(LIFO)、キュー(抽象データ型) :先入れ先出し(FIFO)

ハッシュテーブル、ArrayList。 それらは存在しているようです。

DictionaryEntry

ええ、でもそうではないふりをしましょう。 それらは文明化されていない時代の遺物です。 プリミティブまたは構造体を使用する場合、型安全性とボックスはありません。 したがって、すべての追加に割り当てます。

@terrajobstPlatformCompatibility Analyzerを使用すると、「しないでください」と表示されます。

リストはリストですが、リストではありません

これは実際には、シーケンスとも呼ばれるリスト(抽象データ型)です。

同様に優先度付きキューは、使用中に何を達成するかを示す抽象型です。 実装でそれがどのように行われるかではありません(多くの異なる実装が存在する可能性があるため)

たくさんの素晴らしい議論!

元の仕様は、いくつかのコア原則を念頭に置いて設計されました。

  • 汎用-CPUとメモリの消費量のバランスを取りながら、ほとんどのユースケースをカバーします。
  • System.Collections.Genericの既存のパターンと規則に可能な限り一致させます。
  • 同じ要素の複数のエントリを許可します。
  • 既存のBCL比較インターフェースとクラス(つまり、Comparer)を利用します)。*
  • 高レベルの抽象化-この仕様は、基盤となる実装テクノロジーから消費者を保護するように設計されています。

@karelz @pgolebiowski
「ヒープ」またはデータ構造の実装に合わせた別の用語に名前を変更しても、コレクションのほとんどのBCL規則と一致しません。 歴史的に、.NETコレクションクラスは、特定のデータ構造/パターンに焦点を合わせるのではなく、汎用になるように設計されてきました。 私の当初の考えは、.NETエコシステムの初期に、API設計者が意図的に「ArrayList」から「List」に移行したというものでした。"。この変更は、配列との混同が原因である可能性があります。平均的な開発者は、「ArrayList? 配列ではなく、リストが欲しいだけです。」

ヒープを使用する場合、同じことが発生する可能性があります。多くの中間スキルの開発者は、(悲しいことに)「ヒープ」を見て、「一般化されたデータ構造をヒープする」のではなく、アプリケーションのメモリヒープ(つまり、ヒープとスタック)と混同します。 System.Collections.Genericの普及により、ほぼすべての.NET開発者のインテリジェントな提案に表示され、なぜ新しいメモリヒープを割り当てることができるのか疑問に思うでしょう:)

比較すると、PriorityQueueははるかに発見しやすく、混乱しにくいです。 「キュー」と入力して、PriorityQueueのプロポーザルを取得できます。

整数の優先順位または優先順位の一般的なパラメーター(TKey、TPriorityなど)について尋ねられたいくつかの提案と質問。 明示的な優先度を追加するには、消費者が独自のロジックを作成して優先度をマッピングし、APIの複雑さを増す必要があります。 組み込みのIComparerを使用する既存のBCL機能を活用し、Comparerにオーバーロードを追加することも検討しました優先順位の比較としてアドホックラムダ式/無名関数を提供しやすくするために仕様に追加します。 悲しいことに、これはBCLで​​は一般的な慣習ではありません。

エントリが一意である必要がある場合、Enqueue()はArgumentExceptionをスローするために一意性ルックアップを必要とします。 さらに、アイテムを複数回キューに入れることを許可する有効なシナリオが存在する可能性があります。 この非一意性の設計により、どのオブジェクトが更新されているかを判断する方法がないため、Update()操作の提供が困難になります。 いくつかのコメントが示しているように、これは「ノード」参照を返すAPIに入り始め、ガベージコレクションが必要になる割り当てが(おそらく)必要になります。 これが回避されたとしても、優先キューの要素ごとのメモリ消費量が増加します。

ある時点で、仕様を投稿する前に、APIにカスタムIPriorityQueueインターフェイスがありました。 最終的に私はそれに反対することにしました-私が目指していた使用パターンは、エンキュー、デキュー、および反復でした。 すでに既存のインターフェースセットでカバーされています。 これを、内部でソートされたキューと考えてください。 アイテムがIComparerに基づいてキュー内で独自の(初期)位置を保持している限り、発信者は、優先度がどのように表されるかを気にする必要はありません。 私が行った古いリファレンス実装では、(私が正しく覚えていれば!)優先順位はまったく表されていません。 それはすべてIComparerに基づく相対的なものですまたは比較

いくつかの顧客コードの例をお借りします。私の当初の計画は、PriorityQueueの既存のBCL実装を調べて、例の基礎として使用することでした。

サマリーAPIでは、比較は提供された比較者IComparerによって提供されます比較者、ラッパーは必要ありません。 多くの場合、優先度は型の一部になります。たとえば、タイムスケジューラは、型のプロパティとして実行時間を持ちます。

提案されたOPAPIでは、クラスを使用するには、次のいずれかを満たす必要があります。

  • あなたが望むようにすでに匹敵するタイプを持っている
  • 優先度の値を含む別のクラスで型をラップし、希望する方法で比較します
  • タイプのカスタムIComparerを作成し、コンストラクターを介して渡します。 優先度を表したい値は、タイプによってすでに公開されていると想定しています。

デュアルタイプAPIは、次の2つのオプションの負担を軽減することを目的としています。 すでに優先度が含まれているタイプがある場合、新しいPriorityQueue / Heapの構築はどのように機能しますか? APIはどのように見えますか?

私はそれが次のように信じています:ネイティブライブラリでのDecrease-key / Increase-keyのサポート。

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

いずれの場合も、指定された優先度に関連付けられているアイテムが更新されます。 更新にArrayHeapNodeが必要なのはなぜですか? TKey / TValueを直接取得することでは達成できないことは何を達成しますか?

@ianhays

いずれの場合も、指定された優先度に関連付けられているアイテムが更新されます。 更新にArrayHeapNodeが必要なのはなぜですか? TKey / TValueを直接取得することでは達成できないことは何を達成しますか?

少なくともバイナリヒープ(私が最もよく知っているもの)では、ある値の優先度を更新したいが、ヒープ内でのその位置がわかっている場合は、すばやく(O(log n)時間で)実行できます。

ただし、値と優先度しかわからない場合は、最初に遅い値(O(n))を見つける必要があります。

一方、効率的に更新する必要がない場合は、ヒープ内のアイテムごとに1つの割り当てが純粋なオーバーヘッドになります。

必要なときにだけそのオーバーヘッドを支払うソリューションが欲しいのですが、それを実装することができず、結果のAPIが見栄えが悪い可能性があります。

デュアルタイプAPIは、次の2つのオプションの負担を軽減することを目的としています。 すでに優先度が含まれているタイプがある場合、新しいPriorityQueue / Heapの構築はどのように機能しますか? APIはどのように見えますか?

これは良い点です。コンシューマーが値とは別に優先値(つまり整数)を既に維持しているシナリオでは、元のAPIにギャップがあります。 裏側はそれですAPIのスタイルは、本質的に比較可能なすべてのタイプを複雑にします。 数年前、私は独自の内部ScheduledTaskクラスを持つ非常に軽量なスケジューラコンポーネントを作成しました。 各ScheduledTaskはIComparerを実装しました。 それらを優先キューに入れてください。

SortedListKey / Valueデザインを使用しているので、結果としてそれを使用しないようにしています。 キーは一意である必要があり、通常の要件は「リストで並べ替えておく必要のあるN個の値があり、追加する値が並べ替えられたままになるようにする」です。 「キー」はその方程式の一部ではなく、リストは辞書ではありません。

私にとって、同じ原則がキューにも当てはまります。 歴史的に、キューは1次元のデータ構造です。

私の最近のC ++ stdライブラリの知識は確かに少し錆びていますが、std :: priority_queueでさえ、テンプレート化された(汎用)パラメータとしてプッシュ、ポップ、および比較子を使用しているように見えます。 C ++標準ライブラリのクルーは、パフォーマンスに敏感です:)

ちょうど今、いくつかのプログラミング言語で優先度付きキューとヒープの実装を非常にすばやくスキャンしました。C++、Java、Rust、Goはすべて、単一の型で動作します(ここに掲載されている元のAPIと同様)。 NPMで最も人気のあるヒープ/優先度キューの実装をざっと見てみると、同じことがわかります。

@pgolebiowski

ただし、これは、目的のパフォーマンス目標に一致し、受け入れても構わないと思っているトレードオフがある特定のデータ構造がわかっている場合に使用します。

一般に、フレームワークコレクションは、一般的な動作が必要な用途の90%をカバーします。 次に、非常に具体的な動作や実装が必要な場合は、おそらくサードパーティのライブラリを使用します。 実装にちなんで名前が付けられているので、ニーズに合っていることがわかります。

一般的な動作タイプを特定の実装に結び付けたくないだけです。 型名は同じままである必要があり、それらが一致しないために実装が変更された場合、それは奇妙です。

議論が必要な懸念はたくさんありますが、私たちが同意しない部分に現在最も影響を与えているもの、つまり要素の更新と削除から始めましょう。

では、これらの操作をどのようにサポートしますか? それらを含める必要があります、それらは基本的です。 Javaでは、設計者はそれらを省略しているため、次のようになります。

  1. 機能が不足しているために回避策を実行する方法については、フォーラムにたくさんの質問があります。
  2. ヒープ/優先度キューのサードパーティの実装は、ほとんど役に立たないため、ファーストパーティの実装を置き換えるものがあります。

これはただ哀れです。 本当にそんなやり方を追求したい人はいますか? そのような無効化されたデータ構造を解放するのは恥ずかしいことです。

@pgolebiowskiは、ここにいる全員がプラットフォームに対して最善の意図を持っているので安心してください。 壊れたAPIを出荷したいと思う人はいません。 私たちは他人の過ちから学びたいので、そのような有用な関連情報(Javaストーリーに隣接するものなど)を持ち続けてください。

ただし、いくつか指摘したいことがあります。

  • 一夜にして変化を期待しないでください。 これは設計に関する議論です。 コンセンサスを見つける必要があります。 APIを急いでいません。 すべての意見を聞いて検討する必要がありますが、すべての意見が実行/受け入れられる保証はありません。 入力がある場合は、それを提供し、データと証拠を添えてください。 また、他の人の議論に耳を傾け、それらを認めます。 同意しない場合は、他人の意見に反する証拠を提出してください。 時々、いくつかの点で意見の相違があるという事実に同意します。 SWを含むことを覚えておいてください。 APIの設計は、白黒/正しいか間違っているかではありません。
  • 議論を非公開にしましょう。 強い言葉や言葉は使わないようにしましょう。 恵みに反対し、技術的な議論を続けましょう。 誰もが寄稿者の行動規範を参照することもできます。 私たちは.NETに対する情熱を認識し、奨励していますが、お互いを怒らせないようにしましょう。
  • デザインディスカッションのスピードや反応などについて懸念や質問がある場合は、直接私に連絡してください(私のメールは私のGHプロファイルにあります)。 必要に応じて、期待、仮定、懸念を公にまたはオフラインで明確にすることができます。

提案されたアプローチが気に入らない場合、更新/削除をどのように設計したいかを尋ねました...これは他の人の話を聞いて、コンセンサスを探していると思います。

私はあなたの善意を疑うことはありません! 時々それはあなたがどのように尋ねるかが重要です-それは人々が反対側のテキストをどのように知覚するかに影響します。 テキストには感情がないので、書き留めると物事の理解が異なります。 第二言語としての英語は物事をさらに混乱させ、私たち全員がそれを認識する必要があります。 興味があれば、オフラインで詳細についてチャットさせていただきます...ここでの議論を技術的な議論に戻しましょう...

ヒープとPriorityQueueの議論についての私の2セント:どちらのアプローチも有効であり、明らかに長所と短所があります。

そうは言っても、「PriorityQueue」は既存の.NETアプローチとはるかに一貫しているようです。 今日のコアコレクションはリストです、 辞書、スタック、 列、HashSet、SortedDictionary、およびSortedSet。 これらは、アルゴリズムではなく、機能とセマンティクスにちなんで名付けられています。 HashSetは唯一の外れ値ですが、これでさえ、(SortedSetと比較して)集合の等式セマンティクスに関連するものとして合理化できます。)。 結局のところ、ImmutableHashSetがありますこれは、ボンネットの下の木に基づいています。

1つのコレクションがここでトレンドに逆行するのは奇妙に感じるでしょう。

追加のコンストラクターを備えたPriorityQueueだと思います:PriorityQueue(IHeap)解決策になる可能性があります。 IHeapパラメーターのないコンストラクターは、デフォルトのヒープを使用できます。
その場合、PrioriryQueue(ほとんどのC#コレクションと同様に)抽象データ型を表し、IPriorityQueueを実装しますインターフェイスですが、 @ pgolebiowskiが提案するようなさまざまなヒープ実装を使用できます。

クラスArrayHeap:IHeap {}
クラスPairingHeap:IHeap {}
クラスFibonacciHeap:IHeap {}
クラスBinomialHeap:IHeap {}

わかった。 さまざまな声がたくさんあります。 私は再び議論を繰り返し、アプローチを洗練させました。 また、一般的な懸念にも対処します。 以下のテキストは、上記の投稿からの引用を利用しています。

私たちの目的

このディスカッション全体の最終的な目標は、ユーザーを満足させるための特定の機能セットを提供することです。 ユーザーが多数の要素を持っていることはかなり一般的なケースであり、それらのいくつかは他よりも優先度が高くなっています。 最終的には、次の操作を効率的に実行できるように、この一連の要素を特定の順序で保持する必要があります。

  • 優先度が最も高い要素を取得します(そして削除できるようにします)。
  • コレクションに新しい要素を追加します。
  • コレクションから要素を削除します。
  • コレクション内の要素を変更します。
  • 2つのコレクションをマージします。

その他の標準ライブラリ

他の標準ライブラリの作者もこの機能をサポートしようとしました。 このセクションでは、 PythonJavaC ++GoSwift 、およびRustでどのように解決されたかについて説明します。

それらのいずれも、コレクションにすでに挿入されている要素の変更をサポートしていStackOverflow 。 これは、インターネットを介したそのような多くの質問の1つです。 デザイナーは失敗しました。

注目に値するもう1つの点は、すべてのライブラリがバイナリヒープの実装を通じてこの部分的な機能を提供することです。 さて、それは一般的な場合(ランダム入力)で最もパフォーマンスの高い実装ではないことが示されています。 一般的なケースに最適なのは、 4次ヒープ(配列として格納された暗黙のヒープ順の完全な4元ツリー)です。 これはあまり知られていません。これが、設計者が代わりにバイナリヒープを使用した理由である可能性があります。 しかし、それでも—重大度は低くなりますが、もう1つの悪い選択です。

それから何を学びますか?

  • お客様に満足していただき、すばらしいデータ構造を無視してこの機能を自分で実装しないようにする場合は、コレクションに既に挿入されている要素を変更するためのサポートを提供する必要があります。
  • 一部の機能が何らかの標準ライブラリで何らかの方法で提供されたため、同じことを行う必要があると想定するべきではありません。

提案されたアプローチ

私は私たちが提供すべきだと強く感じています:

  • IHeap<T>インターフェース
  • Heap<T>クラス

IHeapインターフェースには、この投稿の冒頭で説明したすべての操作を実行するためのメソッドが含まれていることは明らかです。 クォータナリヒープで実装されたHeapクラスは、98%のケースで頼りになるソリューションになります。 要素の順序は、コンストラクターに渡されたIComparer<T> 、または型がすでに比較可能な場合はデフォルトの順序に基づいています。

正当化

  • 開発者は、インターフェイスに対してロジックを記述できます。 私は誰もがそれがどれほど重要であるかを知っていると思います、そして詳細には立ち入りません。 読む:依存性逆転の原則依存性注入契約による設計、制御の反転
  • 開発者は、他のヒープ実装を提供することにより、この機能を拡張してカスタムニーズを満たすことができます。 このような実装は、 PowerCollectionsなどのサードパーティライブラリに含めることができます。 このようなライブラリを参照するだけで、 IHeapを入力として受け取るロジックにカスタムヒープを挿入するだけで済みます。 特定の条件で4次ヒープよりも適切に動作する他のヒープの例としては、ペアリングヒープ、二項ヒープ、および最愛のバイナリヒープがあります。
  • 開発者が、どのタイプが最適かを考える必要なしに仕事を成し遂げるツールが必要な場合は、汎用のHeap実装を使用するだけです。 これは、ユースケースの98%に向けて最適化されています。
  • .NETエコシステムの大きな価値に加えて、非常に適切なパフォーマンス特性を備えた単一の選択肢を提供し、ほとんどのユースケースに役立ちます。同時に、それを必要とし、掘り下げたい人のために高性能の拡張機能を提供します。より深く、より多くを学ぶ/知識に基づいた選択とトレードオフを行います。
  • 提案されたアプローチは、現在の慣習を反映しています。

    • ISetおよびHashSet

    • IListおよびList

    • IDictionaryおよびDictionary

  • 一部の人々は、インスタンスの実行方法ではなく、インスタンスの実行内容に基づいてクラスに名前を付ける必要があると述べています。 これは完全に真実ではありません。 クラスの動作にちなんで名前を付ける必要があると言うのは、一般的なショートカットです。 それは確かに多くの場合に当てはまります。 ただし、これが適切なアプローチではない場合があります。 最も注目すべき例は、プリミティブ型、列挙型、データ構造などの基本的な構成要素です。 原則は、意味のある(つまり、ユーザーにとって明確な)名前を選択することです。 ここで説明している機能は、Python、Java、C ++、Go、Swift、Rustなど、常にヒープとして提供されるという事実を考慮に入れてください。 ヒープは、最も基本的なデータ構造の1つです。 Heapは確かに明確で明確です。 また、 StackQueueList 、およびArrayとも調和しています。 最新の標準ライブラリ(Go、Swift、Rust)でも、命名に関する同じアプローチが採用されました。これらは、ヒープを明示的に公開します。

@pgolebiowski Heap<T> / IHeap<T>Stack<T>Queue<T> 、および/またはList<T>ようにどのように命名されているのかわかりませんか? これらの名前はいずれも、内部でどのように実装されているかを説明していません(Tの配列が発生した場合)。

@SamuelEnglard

Heapは、内部でどのように実装されているかも示していません。 多くの人にとって、ヒープが特定の実装の直後に続く理由がわかりません。 そもそも、同じAPIを共有するヒープには多くのバリエーションがあります。

  • d-aryヒープ、
  • 2-3ヒープ、
  • 左派のヒープ、
  • ソフトヒープ、
  • 弱いヒープ、
  • Bヒープ、
  • 基数ヒープ、
  • スキューヒープ、
  • ペアリングヒープ、
  • フィボナッチヒープ、
  • 二項ヒープ、
  • ランクペアリングヒープ、
  • 地震の山、
  • 違反ヒープ。

ヒープを扱って4次ヒープを扱っていると言っても抽象的です。 T配列に基づく暗黙的なデータ構造として実装できます( Stack<T>Queue<T> 、およびList<T> )または明示的(ノードとポインターを使用)。

簡単に言えば、 Heap<T>Stack<T>Queue<T> 、およびList<T> Heap<T>と非常によく似ています。これは、基本的な抽象的な構成要素である基本的なデータ構造であるためです。多くの方法で実装できます。 さらに、たまたま、それらはすべて、下にあるT配列を使用して実装されています。 この類似性は本当に強いと思います。

それは理にかなっていますか?

ちなみに、ネーミングには無関心です。 C ++標準ライブラリの使用に慣れている人は、おそらく_priority_queue_を好むでしょう。 データ構造について教育を受けた人は、_ヒープ_を好むかもしれません。 投票する必要がある場合は、コイントスに近いですが、_ヒープ_を選択します。

@pgolebiowski私は私の質問を間違って言いました、それは私の悪いことです。 はい、 Heap<T>はそれが内部でどのように実装されているかを述べていません。

はいヒープは有効なデータ構造ですが、ヒープ!=優先度付きキューです。 どちらも異なるAPIサーフェスを公開し、さまざまなアイデアに使用されます。 Heap<T> / IHeap<T>は、(理論上の名前だけで) PriorityQueue<T> / IPriorityQueue<T>によって内部的に使用されるデータ型である必要があります。

@SamuelEnglard
コンピュータサイエンスの世界がどのように組織されているかという点では、そうです。 抽象化のレベルは次のとおりです。

  • 実装:配列に基づく暗黙的な4次ヒープ
  • 抽象化:4次ヒープ
  • 抽象化:ヒープのファミリー
  • 抽象化:優先キューのファミリー

はい、 IHeapHeap場合、 PriorityQueueの実装は基本的に次のようになります。

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

ここでデシジョンツリーを実行してみましょう。

優先度付きキューは、(理論的には)何らかの形式のヒープとは異なるデータ構造で実装することもできます。 これにより、上記のようなPriorityQueueの設計はかなり醜いものになります。これは、ヒープのファミリーのみに固定されているためです。 また、 IHeap周りの非常に薄いラッパーです。 これは疑問を投げかけます-_代わりに単にヒープのファミリーを使用しないのはなぜですか_?

IHeapインターフェイスの余地がなく、優先度キューを4次ヒープの特定の実装に修正するという1つの解決策が残されています。 私はそれがあまりにも多くのレベルの抽象化を通過していると感じており、インターフェースを持つことのすべての素晴らしい利点を殺しています。

議論の途中からデザインの選択に戻りました。 PriorityQueueIPriorityQueueます。 しかし、基本的には次のようになります。

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

醜いだけでなく、概念的にも間違っていると感じます。これらは優先度付きキューのタイプではなく、同じAPIを共有していません( @SamuelEnglardがすでに述べたように)。 私たちはヒープに固執するべきだと思います。ヒープは、それらを抽象化するのに十分な大きさの家族です。 私たちは得るでしょう:

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

そして、私たちが提供するclass Heap : IHeap {}


ところで、誰かが次のことを役立つと思うかもしれません:

| Googleクエリ| 結果|
| :--------------------------------------:| :-----:|
| 「データ構造」「優先キュー」| 172,000 |
| 「データ構造」「ヒープ」| 430,000 |
| 「データ構造」「キュー」-「優先キュー」| 496,000 |
| 「データ構造」「キュー」| 530,000 |
| 「データ構造」「スタック」| 577,000 |

@pgolebiowski私はここを行ったり来たりしていると感じているので、認めます

@karelz @safern上記についてどう思いますか? 特定のAPI提案を提示できるように、 IHeap + Heapアプローチを修正できますか?

ここでインターフェースの必要性に疑問を持っています( IHeapまたはIPriorityQueue )。 はい、このデータ構造を実装するために理論的に使用できるさまざまなアルゴリズムがあります。 ただし、フレームワークに複数のフレームワークが付属する可能性は低く、さまざまな関係者が相互互換性のあるヒープ実装を作成する際に調整できるように、ライブラリの作成者が実際に正規のインターフェイスを必要とするという考えはあまりありそうにありません。

さらに、インターフェイスが解放されると、互換性のために変更することはできません。 つまり、インターフェイスは機能の点で具体的なクラスに遅れをとる傾向があります(今日のIListとIDictionaryの問題)。 対照的に、ヒープクラスがリリースされ、IHeapインターフェイスに対する深刻な要求があった場合、インターフェイスは問題なく追加できると思います。

@madelsonフレームワークは、ヒープの複数の実装を出荷する必要がない可能性が高いことに同意します。 ただし、その実装をインターフェイスでサポートすることにより、他の実装を気にする人は、別の実装(自分で作成したものまたは外部ライブラリ内)に簡単に交換でき、インターフェイスを受け入れるコードと互換性があります。

インターフェイスへのコーディングにマイナスの点は見当たりません。 それを気にしない人はそれを無視して、具体的な実装に直接行くことができます。 世話をする人は、自分で選んだ実装を使用できます。 それは選択です。 そしてそれは私が望む選択です。

@pgolebiowskiあなたが尋ねたので、ここに私の個人的な意見があります(私がいくつかの意見をチェックしたので、他のAPIレビューア/アーキテクトがそれを共有することをある程度確信しています):
名前はPriorityQueueである必要があり、 IHeapインターフェイスを導入しないでください。 実装は1つだけである必要があります(おそらくいくつかのヒープを介して)。
IHeapインターフェイスは非常に高度なエキスパートシナリオです。PowerCollectionsライブラリ(最終的には作成される予定)またはその他のライブラリに移動することをお勧めします。
ライブラリとIHeapインターフェースが非常に人気になった場合、後で考えを変えて、(コンストラクターのオーバーロードを介して)要求に基づいてIHeapを追加できますが、それは有用/調整されていないと思いますBCLの残りの部分は、新しいインターフェイスを今すぐ追加することの複雑さを正当化するのに十分です。 単純なものから始めて、本当に必要な場合にのみ複雑なものに成長します。
...ちょうど私の2(個人)セント

意見の違いを考慮して、提案を前進させるために次のアプローチを提案します(今週初めに@ianhaysに提案したため、間接的に@safernに提案しました)。

  • 2つの代替提案を作成します。1つは上記のように単純で、もう1つは提案したヒープを使用します。それをAPIレビューに持ち込み、そこで議論し、そこで決定を下します。
  • そのグループの少なくとも1人がヒープの提案に投票しているのを見つけたら、私は自分の見解を再考したいと思います。

...私の意見(あなたが求めたもの)について完全に透明にしようとしているので、落胆したり押し返したりしないでください-API提案がどのように行われるか見てみましょう。

@karelz

名前はPriorityQueue必要があります

何か議論はありますか? ただノーと言うので

以前はかなりうまく名前を付けたと思います。_入力がある場合は、それを提供し、データと証拠を添えてください。 また、他の人の議論に耳を傾け、それらを認めます。 同意しない場合は、他人の意見に反する証拠を提出してください。_

さらに、それは名前だけではありません。 それほど浅くはありません。 私が書いたものを読んでください。 それは、抽象化のレベル間で作業し、その上にコード/ビルドを強化できるようにすることです。

ああ、この議論には理由がありました:

ヒープを使用する場合、多くの中級の開発者は(悲しいことに)「ヒープ」を見て、「一般化されたデータ構造をヒープする」のではなく、アプリケーションのメモリヒープ(つまり、ヒープとスタック)と混同します。 [...]なぜ新しいメモリヒープを割り当てることができるのか疑問に思うでしょう。

😛

IHeapインターフェースを導入すべきではありません。 IHeapインターフェイスは非常に高度なエキスパートシナリオです。PowerCollectionsライブラリに移動することをお勧めします。

@bendonoはこれについてとても素敵なコメントを書きました。 @safernは、インターフェースを使用して実装を

別の注意-インターフェイスをサードパーティのライブラリに移動することをどのように想像しているかわかりません。 開発者はどのようにしてそのインターフェースに対してコードを記述し、私たちの機能使用できるでしょうか? 彼らは私たちの非インターフェース機能に固執するか、それを完全に無視することを余儀なくされるでしょう、他のオプションはありません、それは相互に排他的です。 言い換えれば、私たちのソリューションはまったく拡張可能ではなく、どちらの場合も同じアーキテクチャコアに依存する代わりに、無効化されたソリューションまたはサードパーティのライブラリのいずれかを使用する

しかし、繰り返しになりますが、あなたはこれについてうまくコメントしました:壊れたAPIを出荷したいと思う人は誰もいません。

意見の違いを踏まえ、以下のアプローチで提案を進めていきます。 [...] 2つの代替案を作成します[...]それをAPIレビューに持ち込み、そこで議論し、そこで決定を下しましょう。 そのグループの少なくとも1人がヒープの提案に投票しているのを見つけたら、私は自分の見解を再考したいと思います。

上記のAPIのさまざまな部分について多くの議論がありました。 それは取り組みました:

  • PriorityQueue vs Heap
  • インターフェースの追加
  • コレクションからの要素の更新/削除のサポート

なぜあなたが考えている人々がこの議論に貢献できないのですか? なぜ代わりに再開する必要があるのですか?

ヒープを使用する場合、多くの中級の開発者は(悲しいことに)「ヒープ」を見て、「一般化されたデータ構造をヒープする」のではなく、アプリケーションのメモリヒープ(つまり、ヒープとスタック)と混同します。

ええ、これは私です。 私は完全にプログラミングを独学で学んでいるので、「 Heap 」が議論に入ったときに何が言われていたのかわかりません。 言うまでもなく、「物の山」というコレクションの観点からさえ、私にとっては、あらゆる方法で順序付けられていないことをより直感的に意味します。

何か議論はありますか? ただノーと言うのではなく、私が上で書いたことに対処してくれれば、少なくともいいでしょう。

私の答えを読むと、私が私の立場の重要な議論に言及していることに気付くでしょう。

  • IHeapインターフェースは非常に高度なエキスパートシナリオです
  • 今すぐ新しいインターフェースを追加することの複雑さを正当化するのに十分なほど、BCLの残りの部分と連携して有用であるとは思いません。

スレッドでIMOを数回繰り返した同じ引数。 私はそれらを要約しました

。 私が書いたものを読んでください。

私はこのスレッドを常に積極的に監視していました。 私はすべての議論とポイントを読みました。 あなたや他の人のすべてのポイントを読んだ(そして理解した)にもかかわらず、これは私の要約された意見です。
あなたはどうやらそのトピックに情熱を注いでいます。 すばらしい。 しかし、それぞれの側から同様の議論を繰り返しているだけで、それ以上先に進むことはないという立場になっていると感じています。そのため、2つの提案を推奨し、より多くの経験からそれらについてより多くのフィードバックを得ました。 BCL APIレビューア(BTW:私はまだ経験豊富なAPIレビューアとして数えていません)。

開発者はどのようにしてそのインターフェースに対してコードを記述し、私たちの機能を使用できるでしょうか?

IHeap高度なシナリオを気にする開発者は、サードパーティのライブラリからインターフェイスと実装を参照します。 私が言ったように、それが人気があることが判明した場合、後でCoreFXに移行することを検討できます。
幸いなことに、後でIHeapを追加することは完全に実行可能です。基本的に、 PriorityQueueコンストラクターのオーバーロードが1つだけ追加されます。
はい、それはあなたの観点からは理想的ではありませんが、それはあなたが将来重要であると考えるイノベーションを妨げるものではありません。 妥当な中間点だと思います。

なぜあなたが考えている人々がこの議論に貢献できないのですか?

APIレビューは、活発な議論、ブレーンストーミング、あらゆる角度からの重み付けを伴う会議です。 多くの場合、GitHubの問題を何度も繰り返すよりも生産的/効率的です。 dotnet / corefx#14354とその前身であるdotnet / corefx#8034を参照してください-あまりにも長い議論、追跡するのが難しい膨大な数の微調整を伴ういくつかの異なる意見、結論はありませんが、素晴らしい議論、またかなりの数の人々にとって重要な無駄な時間、私たちが座ってそれについて話し、合意に達するまで。
APIレビューアにすべてのAPIの問題を監視させるか、おしゃべりな問題だけでもうまく拡張できません。

なぜ代わりに再開する必要があるのですか?

二度と始めません。 どうしてそんなことを考えるのか?
最初のレベル(エリア所有者レベル)でAPIレビューを終了するには、賛否両論のある最も人気のある2つの提案を次のAPIレビューレベルに送信します。
これは、階層的な承認/レビューアプローチです。 これはビジネスレビューに似ています。意思決定権を持つVP / CEOは、社内のすべてのプロジェクトに関するすべての議論を監督するわけではありません。チーム/レポートに、最も影響力のある、または伝染性のある決定の提案を提出して、さらに議論するように依頼します。 チーム/レポートは、問題を要約し、代替ソリューションの長所/短所を提示する必要があります。

このスレッドではまだ述べられていないことがあるため、最後の2つの提案を賛否両論で提示する準備ができていないと思われる場合は、次のレビューでトップ候補が数人になるまで話し合いを続けましょう。 APIレビューレベル。
言わなければならないことはすべて言われたような気がしました。
意味がありますか?

名前はPriorityQueue必要があります

何か議論はありますか?

私の答えを読むと、私が私の立場の重要な議論に言及したことがわかります。

ねえ...私は引用したものを参照していました(明らかに)-あなたはヒープの代わりに優先度付きキューを使うことに決めました。 そして、はい、私はあなたの答えを読みました-それはこれに対する議論の正確に0%を含んでいます。

私はこのスレッドを常に積極的に監視していました。 私はすべての議論とポイントを読みました。 あなたや他の人のすべてのポイントを読んだ(そして理解した)にもかかわらず、これは私の要約された意見です。

あなたは双曲線になるのが好きです、私は前にそれに気づきました。 あなたは単にポイントの存在を認めるだけです。

開発者はどのようにしてそのインターフェースに対してコードを記述し、私たちの機能を使用できるでしょうか?

IHeapの高度なシナリオを気にする開発者は、サードパーティのライブラリからインターフェイスと実装を参照します。 私が言ったように、それが人気があることが判明した場合、後でCoreFXに移行することを検討できます。

あなたはここで私の言葉を繰り返しているだけで、上記の結果として私が提示した問題にまったく取り組んでいないことを知っていますか? 私が書いた:

彼らは私たちの非インターフェース機能に固執するか、それを完全に無視することを余儀なくされるでしょう、他のオプションはありません、それは相互に排他的です。

私はあなたのためにこれを非常に明確にします。 2つの互いに素な人々のグループがあります:

  1. 私たちの機能を使用している1つのグループ。 インターフェイスがなく、拡張できないため、サードパーティのライブラリで提供されているものに接続されていません。
  2. 私たちの機能を完全に無視し、純粋にサードパーティのソリューションを使用している2番目のグループ。

ここでの問題は、私が言ったように、それらが互いに素な人々のグループであるということです。 また共通のアーキテクチャコアがないため、コードベースは互換性がありません_。 後で元に戻すことはできません。

幸いなことに、後でIHeapを追加することは完全に実行可能です。基本的に、PriorityQueueにコンストラクターのオーバーロードが1つだけ追加されます。

私はそれがなぜダメなのかをすでに書いています

なぜあなたが考えている人々がこの議論に貢献できないのですか?

APIレビューアにすべてのAPIの問題を監視させるか、おしゃべりな問題だけでもうまく拡張できません。

はい、APIレビューアがすべてのAPIの問題を監視できない理由を尋ねていました。 正確に言えば、あなたは確かにそれに応じて応答しました。

意味がありますか?

いいえ、私はこの議論に本当にうんざりしています。 それがあなたの仕事であり、あなたがそうするように言われているという理由だけで、あなたたちの何人かは明らかにそれに参加しています。 いつも指導が必要な方もいらっしゃいますが、とても面倒です。 コンピュータサイエンスのバックグラウンドが明らかに不足しているため、優先キューを内部にヒープを使用して実装する必要がある理由を証明するように依頼されました。 ヒープが実際に何であるかさえ理解していない人もいるため、議論はさらに混乱します。

要素の更新と削除を許可しない、無効にしたPriorityQueueを使用してください。 健全なオブジェクト指向アプローチを許可しない設計を採用してください。 拡張機能を作成するときに標準ライブラリを再利用できないソリューションを使用してください。 Javaの方法で行ってください。

そしてこれは...これは驚くべきことです:

ヒープを使用する場合、多くの中級の開発者は(悲しいことに)「ヒープ」を見て、「一般化されたデータ構造をヒープする」のではなく、アプリケーションのメモリヒープ(つまり、ヒープとスタック)と混同します。

APIにアプローチを提示します。 私は興味がある。

これが本当に起こっているなんて信じられない...

ええと、excuuuuuuuuuuuuuuuuuは、 Heapがメモリヒープとは異なるある種のデータ構造であることを学ぶための適切なコンピュータサイエンス教育を受ける機会がなかったので、私を失礼します。

ただし、要点はまだ残っています。 何かのヒープは、それが何らかの形で注文されたことについてません。 処理するもののオブジェクトを格納できるコレクションが必要な場合、後で入ってくるインスタンスをもっと早く処理する必要があるかもしれませんが、 Heapと呼ばれるものを検索することはありません。 一方、 PriorityQueueは、まさにそれを実行していることを完全に伝えます。

バッキングの実装として? 確かに、実装の詳細は私の関心事ではありません。
いくつかのIHeap抽象化? APIの作成者と、それがために使われているかを知るためにCS主を持ってない人のために大きい、理由はありませんしません。
その意図をあまりよく述べておらず、その後発見可能性を制限する不可解な名前を何かに与えるのですか? 👎

ええと、ヒープがメモリヒープとは異なるある種のデータ構造であることを学ぶために、適切なコンピュータサイエンスの教育を受ける機会がなかったので、私を失礼します。

ばかげてる。 同時に、そのような機能の追加に関するディスカッションに参加したいと考えています。 トローリングのように聞こえます。

何かのヒープは、それが何らかの形で注文されたことについては何も意味しません。

あなたは間違っている。 ヒープとして注文されます。 あなたがリンクした写真のように。

バッキングの実装として? 確かに、実装の詳細は私の関心事ではありません。

私はすでにそれに対処しました。 ヒープのファミリーは巨大であり、実装の上に2つのレベルの抽象化があります。 優先キューは、3番目の抽象化レイヤーです。

処理するもののオブジェクトを格納できるコレクションが必要な場合、後で入ってくるインスタンスをより早く処理する必要がある場合は、ヒープと呼ばれるものを検索しません。 一方、PriorityQueueは、まさにそれを実行することを完全に伝達します。

そして、背景がなければ、優先キューに記事を提供するようにGoogleに依頼しますか? まあ、私たちの意見では多かれ少なかれ何が起こりそうかを議論することができます。 しかし、非常にうまく言われているように:

入力がある場合は、それを提供し、データと証拠を添えてください。 同意しない場合は、他人の意見に反する証拠を提出してください。

そして、データによると、あなたは間違っています:

クエリ| ヒット
:----:|:----:|
| 「データ構造」「優先キュー」| 172,000 |
| 「データ構造」「ヒープ」| 430,000 |

データ構造について読んでいるときにヒープに遭遇する可能性はほぼ3倍です。 さらに、Swift、Go、Rust、およびPythonの開発者は、標準ライブラリがそのようなデータ構造を提供するため、よく知っている名前です。

クエリ| ヒット
:----:|:----:|
| 「golang」「優先キュー」| 3.390 |
| 「さび」「優先キュー」| 8.630 |
| 「swift」「priorityqueue」| 18.600 |
| 「python」「優先キュー」| 72.800 |
| 「golang」「ヒープ」| 79.000 |
| 「さび」「ヒープ」| 492.000 |
| 「swift」「heap」| 551.000 |
| "python" "ヒープ" | 555.000 |

ヒープデータ構造が前世紀のある時期に導入されたため、実際にはC ++でも同様です。

その意図をあまりよく述べておらず、その後発見可能性を制限する不可解な名前を何かに与えるのですか? 👎

意見はありません。 データ。 上記を参照。 特に経歴のない人からの意見はありません。 以前にデータ構造についてある程度読んでいなければ、優先キューをグーグルで検索することもできません。 そして、ヒープは多くのデータ構造101でカバーされています。

それはコンピュータサイエンスの基礎です。 初歩です。 アルゴリズムとデータ構造の学期が複数ある場合、ヒープは最初に目にするものです。

それでも:

  • まず、上記の数字を見てください。
  • 2番目-ヒープが標準ライブラリの一部である他のすべての言語について考えてみてください。

編集: Googleトレンドを参照してください。

別の独学の開発者として、私は_heap_に問題はありません。 常に改善に努めている開発者として、私は時間をかけてデータ構造についてすべてを学び、理解してきました。 要するに、私は、命名規則が、彼らが属している分野の語彙を理解するのに時間をかけなかった人々を対象とするべきであるという含意に同意しません。

また、「名前PriorityQueueにする

APIの命名について私たちがどのように考えているかについて説明します。

  1. 私たちは、他のほとんど何よりも.NETプラットフォーム内の一貫性を優先する傾向があります。 これは、APIを見慣れた予測可能なものにするために重要です。 これは、以前に使用した用語である場合、名前が100%正しくないことを受け入れることを意味する場合があります。

  2. 私たちの目標は、コンピュータサイエンスの正式な教育を受けていない開発者も含め、さまざまな開発者が利用できるプラットフォームを設計することです。 .NETが一般的に非常に生産的で使いやすいと認識されてきた理由の一部は、その設計ポイントに部分的に起因していると考えています。

私たちは一般的に、名前や用語がどれほど有名で確立されているかをチェックするときに「検索エンジンテスト」を採用しています。 ですから、 @ pgolebiowskiが行った調査に感謝します。 私は自分で調査を行ったことがありませんが、「ヒープ」は多くの非ドメイン専門家が探している用語ではないというのが私の直感です。

したがって、私は@karelzに同意する傾向があり、 PriorityQueueより良い選択のように

しかし、私はこれを指摘したいと思います:

人々の意見を聞きたくない場合は、オープンソースにしないでください。また、それを求めないでください。

誤った二分法です。 エコシステムや貢献者からのフィードバックを望まないわけではありません(もちろんそうです)。 しかし同時に、私たちの顧客ベースは非常に多様であり、GitHubの貢献者(または私たちのチームの開発者)がすべての顧客にとって常に最良のプロキシであるとは限らないことも理解する必要があります。 使いやすさは難しく、特にコレクションのような非常に人気のある分野では、.NETに新しい概念を追加するためにいくつかの反復が必要になる可能性があります。

@pgolebiowski

私はあなたの洞察、データ、提案を高く評価します。 しかし、私はあなたの議論のスタイルに絶対に感謝しません。 あなたは私のチームのメンバーとこのスレッドのコミュニティメンバーの両方に対して個人的になりました。 あなたが私たちに同意しないからといって、それが「ただの私たちの仕事」であるという理由で私たちが専門知識を持っていない、または気にしないと非難する許可をあなたに与えません。 私たちの多くが文字通り世界中を移動し、この仕事をしたかったという理由だけで家族や友人を置き去りにしたことを考えてみてください。 あなたのようなコメントは非常に不公平であるだけでなく、デザインを進めるのにも役立ちません。

ですから、私は肌が厚いと思うのが好きですが、そのような行動にはあまり寛容ではありません。 私たちのドメインはすでに十分に複雑です。 対立的および敵対的なコミュニケーションを追加する必要はありません。

敬意を表してください。 アイデアを熱心に批判することは公正なゲームですが、人々を攻撃することはそうではありません。 ありがとうございました。

心を落ち着かせるすべての人へ、

私の厳しい態度によってあなたの幸せのレベルを下げてしまったことをお詫びします。

@karelz

技術的なミスをして、そこで自分を馬鹿にしました。 謝罪しました。 受け入れられました。 しかし、後で私に投げられました。 クールなIMOではありません。

私が書いたことがあなたを不幸にさせてすみません。 あなたがそれを説明したようにそれはそれほど悪くはありませんでし疲れ感に貢献した多くの要因の1つとして提供しました。 それほど深刻ではないと思います。 それでも、ごめんなさい。

そして、はい、誰もが間違いを犯します。 大丈夫です。 私も、例えば時々夢中になってしまいます。

私が最も得たのは、「あなたはただこれをするように言われている、あなたはそれを信じていない」ということでした-ええ、そういうわけで私は週末にもそれをします。

申し訳ありませんが、お疲れ様でした。ありがとうございました。 5/10のマイルストーンまでにあなたがどれほど特別に献身的であったかが私にはわかりました。

@terrajobst

あなたが私たちに同意しないからといって、それが「ただの私たちの仕事」であるという理由で私たちが専門知識を持っていない、または気にしないと非難する許可をあなたに与えません。

  • 専門知識がない-コンピュータサイエンスのバックグラウンドがなく、ヒープ/優先度付きキューの概念を理解していない人々を対象としています。 そのような説明が誰かに当てはまる場合-まあ、それは当てはまります、それは私のせいではありません。
  • 気にしない-技術的なポイントのいくつかを無視する傾向がある人々に向けて対処し、したがって、議論を混乱させ、他の人々がフォローするのを難しくする(その結果、入力が少なくなる)など、繰り返しの議論を強制します。

あなたのようなコメントは非常に不公平であり、デザインを進めるのにも役立ちません。

  • 私の厳しいコメントは、この議論の非効率性の結果でした。 非効率性=混沌とした議論の方法。ポイントが対処/解決されておらず、それにもかかわらず私たちはさらに進んでいます=>面倒です。
  • また、議論の中心的な推進力の一つとして、デザインを前進させるために多くのことをしたと強く感じています。 あなたがここやソーシャルメディアでやろうとしているので、私を悪魔にしないでください。

「なめて」と思って病気の方がいらっしゃいましたら、お気軽にどうぞ。


問題が発生し、対処しました。 誰もがそれから何かを学びました。 ここのみんながフレームワークの品質に関心を持っていることがわかります。それは絶対に素晴らしく、貢献する意欲を持っています。 今後ともCoreFXにご協力いただきますようお願い申し上げます。 そうは言っても、私はおそらく明日、あなたの新しい技術的インプットに取り組みます。

@pgolebiowski

いつか直接会えることを願っています。 私は正直に言って、すべてをオンラインで行うことの課題の一部は、どちらの側からも意図せずに性格が悪い方法で混ざり合う可能性があることだと信じています。

今後ともCoreFXにご協力いただきますようお願い申し上げます。 そうは言っても、私はおそらく明日、あなたの新しい技術的インプットに取り組みます。

こっちも一緒。 これは面白い空間で、一緒にできる素晴らしいことがたくさんあります:-)

@pgolebiowski最初に、返信ありがとう
私は私たちの関係で最初からやり直すことをお勧めします。 議論を技術的なものに戻しましょう。このスレッドから、将来同様の状況に対処する方法を学びましょう。相手がプラットフォームに最も関心を持っているだけだともう一度仮定しましょう。
ところで:これは過去9か月間のCoreFXリポジトリでのいくつかの難しい出会い/議論の1つであり、ご覧のとおり、私たち(特に私を含む)はまだそれらをうまく処理する方法を学んでいます-したがって、この特定のインスタンスは進行中です私たちにも利益をもたらし、それは私たちを将来より良くし、情熱的なコミュニティメンバーからのより良い異なる視点を理解するのに役立ちます。 多分それは貢献ドキュメントへの私達の更新を形作るでしょう...

私の厳しいコメントは、この議論の非効率性の結果でした。 非効率性=混沌とした議論の方法。ポイントが対処/解決されておらず、それにもかかわらず私たちはさらに進んでいます=>面倒です。

あなたの欲求不満を理解しました! 興味深いことに、同じ理由で反対側にも同様の欲求不満がありました😉...世界がどのように機能するかはほとんどおかしいです:)。
残念ながら、設計上の決定を下すとき、難しい議論は仕事の一部です。 たくさんの仕事です。 多くの人がそれを過小評価しています。 必要な重要なスキルは、すべての人に対する忍耐力と、自分の意見を超えて、うまくいかなくてもコンセンサスを推進する方法を考える能力です。 そのため、2つの提案を提案し、技術的な議論をAPIレビューアグループに「エスカレーション」することを提案しました(主に、自分が正しいかどうかわからないためですが、世界中の他のすべての開発者と同じように自分が正しいことを密かに望んでいます😉 )。

トピックについて意見を持ち、同じスレッドでコンセンサスに議論を進めることは非常に困難です。 その観点から、あなたと私はこのスレッドで最も一般的です-私たちは両方とも意見を持っていますが、私たちは両方とも議論を終結と決定に駆り立てようとしています。 それでは、緊密に協力しましょう。

私の一般的なアプローチは次のとおりです。誰かが私を攻撃していると思うときはいつでも、邪悪で、怠惰で、私を苛立たせている、または何かです。 私は最初に自分自身と特定の人に尋ねます:なぜですか? なんでそんな事を言ったの? どういう意味?
通常、それは動機の理解/コミュニケーションの欠如の兆候です。 または、行間を読みすぎて、侮辱/告発/悪意がないところを見る兆候。


技術的な質問について話し続けることを恐れていないので、以前に質問したかったことは次のとおりです。

要素の更新と削除を許可しない、無効にしたPriorityQueueを使用してください。

これは私にはわかりません。 IHeap (ここの私の/元の提案)を省略した場合、なぜそれが不可能なのですか?
IMOは、クラス機能の観点から2つの提案に違いはありません。唯一の違いは、 PriorityQueue(IHeap)コンストラクターのオーバーロードを追加するかどうかです(クラス名の争いを解決するための独立した問題として残します) 。

免責事項(誤解を避けるため):記事を読んだり調査したりする時間がありません。技術的な議論を推進したい人からのエレベーターピッチの議論を提示して、短い答えを期待しています。 注:私がトローリングしているわけではありません。 私は、この主張をしている私たちのチームの誰にでも同じ質問をします。 あなたがそれを説明する/議論を推進するエネルギーがない場合(あなたの側からの困難と時間の投資を考えると完全に理解できるでしょう)、ただそう言ってください、難しい感情はありません。 私や誰かからのプレッシャーを感じないでください(そしてそれはスレッドのすべての人に当てはまります)。

ここに不要なコメントをもう1つ追加しようとはしていませんが、このスレッドは長すぎます。 インターネット時代の一番のルールは、人間関係を気にするならテキストコミュニケーションを避けることです。 (まあ、私はそれを造った)。 他のオープンソースコミュニティは、必要性が明らかな場合、この種の議論のためにGoogleハングアウトに切り替えると思います。 他の人の顔を見ると、「侮辱的」なことは決して言わず、人はすぐにお互いに親しみます。 たぶん私たちも試すことができますか?

@karelz上記の議論の長さのため、フローが変更されていない場合、新しい人が貢献する可能性はほとんどありません。 そのため、私は今、次のアプローチを提案したいと思います。

  • 基本的な側面について、次々と投票していきます。 コミュニティからの明確な意見があります。 理想的には、APIレビューアもここに来て、まもなくコメントします。
  • 「投票投稿」には、上記のテキストの壁全体を無視できる十分な情報が含まれます。
  • この投票セッションが終了すると、APIレビュー担当者に何を期待できるかがわかり、特定のアプローチを進めることができるようになります。 基本的な側面に同意すると、この問題はクローズされ、別の問題が開かれます(これはこれを参照します)。 新刊では、結論をまとめ、これらの決定を反映したAPI提案を提供します。 そして、そこからそれを取ります。

それは理にかなっている可能性がありますか?

要素の更新と削除を許可しないPriorityQueue。

それは、これらの機能を欠いた元の提案に関するものでした:)明確にしないで申し訳ありません。

あなたがそれを説明する/議論を推進するエネルギーがない場合(あなたの側からの困難と時間の投資を考えると完全に理解できるでしょう)、ただそう言ってください、難しい感情はありません。 私や誰かからのプレッシャーを感じないでください(そしてそれはスレッドのすべての人に当てはまります)。

私はあきらめない。 痛みなしゲインなしxD

@ xied75

他のオープンソースコミュニティは、必要性が明らかな場合、この種の議論のためにGoogleハングアウトに切り替えると思います。 たぶん私たちも試すことができますか?

いいね ;)

インターフェースの提供

Heap / IHeapまたはPriorityQueue / IPriorityQueue (または他の何か)のどちらを使用する場合でも、提供しようとしている機能については...

_実装とともにインターフェースが必要ですか?_

にとって

@bendono

その実装をインターフェースでサポートすることにより、他の実装を気にする人は、(自分で作成した、または外部ライブラリーの)別の実装に簡単に交換でき、インターフェースを受け入れるコードと互換性があります。

それを気にしない人はそれを無視して、具体的な実装に直接行くことができます。 世話をする人は、自分で選んだ実装を使用できます。

に対して

@madelson

このデータ構造を実装するために理論的に使用できるさまざまなアルゴリズムがあります。 ただし、フレームワークに複数のフレームワークが付属する可能性は低く、さまざまな関係者が相互互換性のあるヒープ実装を作成する際に調整できるように、ライブラリの作成者が実際に正規のインターフェイスを必要とするという考えはあまりありそうにありません。

さらに、インターフェイスが解放されると、互換性のために変更することはできません。 つまり、インターフェイスは機能の点で具体的なクラスに遅れをとる傾向があります(今日のIListとIDictionaryの問題)。

@karelz

インターフェイスは非常に高度なエキスパートシナリオです。

ライブラリとIHeapインターフェースが非常に人気になった場合、後で考えを変えて、需要に基づいて(コンストラクターのオーバーロードを介して) IHeapを追加できますが、それは有用ではないと思います。 BCLの残りの部分は、新しいインターフェイスを今すぐ追加することの複雑さを正当化するのに十分です。 単純なものから始めて、本当に必要な場合にのみ複雑なものに成長します。

潜在的な意思決定への影響

  • インターフェースを含めることは、将来それを変更できないことを意味します。
  • インターフェイスが含まれていないということは、標準ライブラリソリューションを使用するか、サードパーティライブラリによって提供されるソリューションに対して記述されたコードを作成することを意味します(相互互換性を可能にする共通のインターフェイスはありません)。

👍と👎を使用して、これに投票します(それぞれインターフェースに賛成と反対)。 または、コメントを書いてください。 理想的には、APIレビューアが参加します。

インターフェイスの変更は難しいですが、拡張メソッド(および今後のプロパティ)を使用すると、インターフェイスの拡張や操作が簡単になります(LINQを参照)。

インターフェイスの変更は難しいですが、拡張メソッド(および今後のプロパティ)を使用すると、インターフェイスの拡張や操作が簡単になります(LINQを参照)。

それらは、インターフェース上で公に定義されたメソッドでのみ機能します。 つまり、最初に正しく理解することを意味します。

クラスが使用されて落ち着くまで、インターフェイスを少し保留することをお勧めします。その後、インターフェイスを導入します。 (インターフェースの形状に関する議論は別の問題です)

率直に言って、私が気にするのはインターフェースだけです。 しっかりした実装があればいいのですが、私(または他の誰か)はいつでも自分で作成することができます。

数年前、 HashSet<T>とまったく同じ会話をしたことを覚えています。 マイクロソフトはHashSet<T>を望んでいましたが、コミュニティはISet<T>望んでいました。 私の記憶が正しければ、最初にHashSet<T>ISet<T>を取得しました。 インターフェースがないと、パブリックAPIを変更することは(多くの場合不可能ではないにしても)困難であるため、 HashSet<T>は非常に制限されていました。

ISet<T>非BCL実装が多数あることは言うまでもなく、現在SortedSet<T>もあることに注意する必要があります。 私はパブリックAPIでISet<T>を使用しましたが、感謝しています。 私のプライベート実装では、正しいと思う具体的な実装を使用できます。 また、何も壊すことなく、実装を別の実装に簡単に交換できます。 これは、インターフェースなしでは不可能です。

いつでも独自のインターフェースを定義できると言う人は、これを検討してください。 しばらくの間、BCLのISet<T>が発生しなかったと仮定します。 これで、独自のインターフェイスIMySet<T>と確実な実装を作成できます。 しかし、ある日、BCL HashSet<T>がリリースされます。 ISet<T>実装する場合としない場合がありますが、 IMySet<T>は実装しません。 その結果、 IMySet<T>実装としてHashSet<T>をスワップインできません。

この悲劇をもう一度繰り返すのではないかと心配しています。
インターフェイスにコミットする準備ができていない場合は、具体的なクラスを導入するには時期尚早です。

意見の相違は重要だと思います。 数字を見るだけで、インターフェースを求める人が少し増えますが、それではあまりわかりません。 以前にディスカッションに参加したことがあるが、インターフェイスに関してまだ意見を表明していない他の人に聞いてみます。

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

通知された人のために: _guys、あなたはあなたの入力を提供できますか? :+ 1:、:-1:で投票するだけでも、非常に役立ちます。 この問題のコメント(上記の4つの投稿)から読み始めることができます-インターフェースを提供することが良い考えであるかどうかについて議論しています(解決されるまで、今のところこの側面のみ)。

これらの人々の中にはAPIレビュー担当者もいるかもしれませんが、先に進む前に、この基本的な決定において彼らのサポートが必要だと思います。 @ karelz@ terrajobst 、この側面を解決するのを手伝ってくれるように頼むことは可能ですか? 彼らは最終的にそれをレビューするので、彼らの意見は非常に価値があります-特定のアプローチをコミットする前に(または複数の提案を行う前に、この時点でそれを知ることは非常に役立ちます、それは面倒です彼らの決定を以前に知ることができるので、少し無意味です)。

個人的には、私はインターフェースを求めていますが、決定が異なる場合は、別の道をたどることができます。

APIレビュー担当者をディスカッションに引きずり込みたくありません-長くて面倒です。APIレビュー担当者がすべてを読み直したり、最後の重要な返信を決定したりするのは効率的ではありません(私はそれに夢中になっています) )。
2つの正式なAPI提案を作成し(そこにある「良い」例を参照)、それぞれの長所/短所を強調できるようになった時点だと思います。 その後、APIレビュー会議でそれらをレビューし、投票を考慮して推奨を行うことができます。 そこでの議論によっては(複数の意見がある場合)、戻ってきてTwitterの投票や追加のGH投票などを開始する場合があります。

ところで:APIレビューミーティングはほぼ毎週火曜日に開催されます。

それを開始するのに役立つように、提案は次のようになります。

提案例/シード

`` `c#
名前空間System.Collections.Generic
{{
パブリッククラスPriorityQueue
:IEnumerable、ICollection、IEnumerable、IReadOnlyCollection
{{
public PriorityQueue();
public PriorityQueue(intcapacity);
public PriorityQueue(IComparer比較者);
public PriorityQueue(IEnumerableコレクション);
public PriorityQueue(IEnumerableコレクション、IComparer比較者);
public PriorityQueue(intcapacity、IComparer比較者);

    public IComparer<T> Comparer { get; }
    public int Count { get; }

    public void Enqueue(T item);
    public T Dequeue();
    public T Peek();
    public void Clear();
    public bool Contains(T item);

    // Sets the capacity to the actual number of elements
    public void TrimExcess();

    public void CopyTo(T[] array, int arrayIndex);
    void ICollection.CopyTo(Array array, int index);
    public T[] ToArray();

    public Enumerator GetEnumerator();
    IEnumerator<T> IEnumerable<T>.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator();

    bool ICollection.IsSynchronized { get; }
    object ICollection.SyncRoot { get; }
    public struct Enumerator : IEnumerator<T>
    {
        public T Current { get; }
        object IEnumerator.Current { get; }
        public bool MoveNext();
        public void Reset();
        public void Dispose();
    }
}

}
`` `

TODO:

  • 使用例がありません(アイテムの優先度をどのように表現するかはわかりません)-優先度の値の入力として「int」を持つように再設計する必要がありますか? 多分それについてのいくつかの抽象化? (上で説明したと思いますが、スレッドが長すぎて読むことができないため、具体的な提案が必要であり、それ以上の議論は必要ありません)
  • UpdatePriorityシナリオがありません
  • ここで欠落している上記の他の何か?

@karelz OK、やります、お楽しみに! :笑顔:

大丈夫。 私の知る限り、インターフェースやヒープはAPIレビューに合格しません。 そのため、たとえば4次ヒープを忘れて、少し異なる解決策を提案したいと思います。 以下のデータ構造は、 PythonJavaC ++GoSwift 、およびRust (少なくともそれら)で見られるものとはいくつかの点で異なります。

主な焦点は、最適な複雑さと優れた実世界のパフォーマンスを維持しながら、正確性、機能性の観点からの完全性、および直感性にあります。

@karelz @terrajobst

提案

理論的根拠

ユーザーが多数の要素を持っていることはかなり一般的なケースであり、それらのいくつかは他よりも優先度が高くなっています。 最終的には、次の操作を効率的に実行できるように、この一連の要素を特定の順序で保持する必要があります。

  1. コレクションに新しい要素を追加します。
  2. 優先度が最も高い要素を取得します(そして削除できるようにします)。
  3. コレクションから要素を削除します。
  4. コレクション内の要素を変更します。
  5. 2つのコレクションをマージします。

使用法

用語集

  • —ユーザーデータ。
  • キー—注文の目的で使用されるオブジェクト。

さまざまな種類のユーザーデータ

まず、優先度付きキューの構築に焦点を当てます(要素の追加のみ)。 それが行われる方法は、ユーザーデータの種類によって異なります。

シナリオ1

  • TKeyTValueは別々のオブジェクトです。
  • TKeyは同等です。
var queue = new PriorityQueue<int, string>();

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

シナリオ2

  • TKeyTValueは別々のオブジェクトです。
  • TKeyは比較できません
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");

シナリオ3

  • TKeyTValue内に含まれています。
  • TKeyは比較できません
public class MyClass
{
    public MyKey Key { get; set; }
}

キーコンパレータとは別に、キーセレクタも必要です。

var selector = new Func<MyClass, MyKey>(item => item.Key);

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

queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
ノート
  • ここでは、別のEnqueueメソッドを使用します。 今回は1つの引数( TValue )のみを取ります。
  • キーセレクターが定義されている場合、メソッドEnqueue(TKey, TValue)InvalidOperationExceptionスローする必要があります。
  • キーセレクターが定義されていない場合、メソッドEnqueue(TValue)InvalidOperationExceptionスローする必要があります。

シナリオ4

  • TKeyTValue内に含まれています。
  • TKeyは同等です。
var queue = new PriorityQueue<MyKey, MyClass>(selector);

queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
ノート
  • 以下のための比較演算TKeyあると想定されるComparer<TKey>.Defaultのように、シナリオ1

シナリオ5

  • TKeyTValueは別々のオブジェクトですが、同じタイプです。
  • TKeyは同等です。
var queue = new PriorityQueue<int, int>();

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

シナリオ6

  • ユーザーデータは単一のオブジェクトであり、同等です。
  • 物理キーがないか、ユーザーがそれを使用したくない。
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 */ ));
ノート

最初は、あいまいさがあります。

  • MyClassが別個のオブジェクトであり、シナリオ5の場合のように、ユーザーが単にキーと値を別個にしたい場合があります。
  • ただし、(この場合のように)単一のオブジェクトである場合もあります。

PriorityQueueがあいまいさをどのように処理するかを次に示します。

  • キーセレクターが定義されている場合、あいまいさはありません。 Enqueue(TValue)のみが許可されます。 したがって、シナリオ6の代替ソリューションは、セレクターを定義してコンストラクターに渡すことです。
  • キーセレクターが定義されていない場合、あいまいさはEnqueueメソッドの最初の使用で解決されます。

    • Enqueue(TKey, TValue)が最初に呼び出された場合、キーと値は別々のオブジェクトと見なされます(シナリオ5 )。 それ以降、 Enqueue(TValue)メソッドはInvalidOperationExceptionスローする必要があります。

    • Enqueue(TValue)が最初に呼び出された場合、キーと値は同じオブジェクトであると見なされ、キーセレクターが推測されます(シナリオ6 )。 それ以降、 Enqueue(TKey, TValue)メソッドはInvalidOperationExceptionスローする必要があります。

その他の機能

優先キューの作成についてはすでに説明しました。 コレクションに要素を追加する方法も知っています。 次に、残りの機能に焦点を当てます。

最も優先度の高い要素

優先度が最も高い要素に対して実行できる操作は2つあります。要素を取得するか削除するかです。

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

Peek Dequeueメソッドと

要素の変更

任意の要素について、それを変更したい場合があります。 たとえば、サービスの存続期間全体で優先度キューが使用される場合、開発者が優先度を更新する可能性を望んでいる可能性が高くなります。

驚いたことに、このような機能は、 PythonJavaC ++GoSwiftRustなどの同等のデータ構造では提供されていません。 おそらく他にもいくつかありますが、私はそれらをチェックしただけです。 結果? 失望した開発者:

ここには基本的に2つのオプションがあります。

  • Javaの方法で実行し、この機能を提供しないでください。 私はそれに強く反対します。 これにより、ユーザーはコレクションから要素を削除し(これだけではうまく機能しませんが、後で説明します)、もう一度追加します。 それは非常に醜く、すべての場合に機能するとは限らず、非効率的です。
  • ハンドルの新しい概念を導入し

ハンドル

ユーザーが優先キューに要素を追加するたびに、ハンドルが与えられます。

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

Handleは、次のパブリックAPIを持つクラスです。

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

これは、優先キュー内の一意の要素への参照です。 効率が気になる場合は、 FAQをご覧

このようなアプローチにより、非常に直感的で簡単な方法で、優先度キュー内の一意の要素を簡単に変更できます。

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

上記の例では、特定のセレクターが定義されているため、ユーザーは自分でオブジェクトを更新できます。 優先キューは、それ自体を再配置するために通知する必要がありました(監視可能にしたくありません)。

ユーザーデータの種類が優先度付きキューの作成方法と要素の入力方法にどのように影響するかと同様に、更新方法も異なります。 今回はシナリオを短くします。おそらく、私が何を書くかはすでにご存知でしょう。

シナリオ

シナリオ7
  • TKeyTValueは別々のオブジェクトです。
var queue = new PriorityQueue<int, string>();

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

queue.Update(handle, 3);

ご覧のとおり、このようなアプローチは、優先度キュー内の一意の要素を参照する簡単な方法を提供します。質問はありません。 これは、キーが重複する可能性があるシナリオで特に役立ちます。 また、これは非常に効率的です。O(1)で更新する要素がわかっており、操作はO(log n)で実行できます。

または、ユーザーはO(n)の構造全体を検索する追加のメソッドを使用して、引数に一致する最初の要素を更新することもできます。 これは、Javaで要素を削除する方法です。 それは完全に正しくも効率的でもありませんが、時にはもっと単純です:

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

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

queue.Update("three", 30);

上記の方法では、値が"three"等しい最初の要素が検索されます。 そのキーは30更新されます。 条件を満たすものが複数ある場合、どれが更新されるかはわかりません。

メソッドUpdate(TKey oldKey, TValue, TKey newKey)を使用すると、少し安全になる可能性があります。 これにより、別の条件が追加されます。古いキーも一致する必要があります。 どちらのソリューションも単純ですが、100%安全ではなく、パフォーマンスも低下します(O(1 + log n)vs O(n + log n))。

シナリオ8
  • TKeyTValue内に含まれています。
var queue = new PriorityQueue<int, MyClass>(selector);

/* adding some elements */

queue.Update(handle);

このシナリオは、ハンドルセクションで例として示したものです。

上記はO(log n)で達成されます。 あるいは、ユーザーは、指定された要素と等しい最初の要素を見つけて内部再配置を行うメソッドUpdate(TValue)を使用することもできます。 もちろん、これはO(n)です。

シナリオ9
  • TKeyTValueは別々のオブジェクトですが、同じタイプです。

ハンドルを使用すると、いつものようにあいまいさはありません。 更新を可能にする他の方法の場合—もちろんあります。 これは、単純さ、パフォーマンス、および正確さ(データを複製できるかどうかによって異なります)の間のトレードオフです。

シナリオ10
  • ユーザーデータは単一のオブジェクトです。

ハンドル付きで、やはり問題ありません。 他の方法で更新する方が簡単な場合もありますが、パフォーマンスが低下し、常に正しいとは限りません(エントリが等しい)。

要素の削除

ハンドルを介して簡単かつ正確に行うことができます。 または、メソッドRemove(TValue)およびRemove(TKey, TValue)ます。 前に説明したのと同じ問題。

2つのコレクションをマージする

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

/* add some elements to both */

queue1.Merge(queue2);

ノート

  • マージが完了した後、キューは同じ内部表現を共有します。 ユーザーは2つのいずれかを使用できます。
  • タイプは一致する必要があります(静的にチェックされます)。
  • 比較対象は等しくなければなりません。そうでない場合はInvalidOperationExceptionです。
  • セレクターは等しくなければなりません。そうでない場合はInvalidOperationExceptionです。

提案された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; }
}

未解決の質問

代わりに述語

効率的で直感的で完全に正しいので、私はハンドルを使ったアプローチを強く望んでい

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

使い方はかなり甘くてパワフルでしょう。 そして本当に直感的です。 そして読みやすい(非常に表現しやすい)。 私はそのためです。

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

新しいキーの設定は、古いキーを取得して何らかの方法で変換する関数である可能性もあります。

ContainsRemoveについても同じです。

UpdateKey

キーセレクターが定義されていない場合(したがって、キーと値が別々に保持されている場合)、メソッドの名前はUpdateKeyます。 おそらくもっと表現しやすいでしょう。 ただし、キーセレクターが定義されている場合は、キーが既に更新されており、優先キュー内の一部の要素を再配置する必要があるため、 Update方が適しています。

よくある質問

非効率的な処理ではありませんか?

ハンドルの使用に関しては効率に問題はありません。 共通の懸念は、内部で配列に基づくヒープを使用しているため、追加の割り当てが必要になることです。 恐れるな。 読む。

では、どのように実装しますか?

これは、優先キューを配信するためのまったく異なるアプローチになります。 初めて、標準ライブラリは、下の配列として表されるバイナリヒープとして実装されたこの機能を提供しません。 ペアリングヒープを使用して実装されペアリングヒープは、設計上、配列を使用しません。代わりに、単にツリーを表します。

どのくらいのパフォーマンスがありますか?

  • 一般的なランダム入力の場合、4次ヒープはわずかに高速になります。
  • ただし、高速にするには、配列に基づいている必要があります。 その場合、ハンドルは単にノードに基づくことはできません。追加の割り当てが必要になります。 次に—要素を合理的に更新および削除できませんでした。
  • 現在の設計方法では、非常にパフォーマンスの高い実装でありながら、使いやすいAPIがあります。
  • ペアリングヒープを下に置くことの追加の利点は、バイナリ/クォーターヒープのようにO(n)ではなく、O(1)で2つの優先度キューをマージできることです。
  • ペアリングヒープはまだ非常に高速です。 参考文献を参照してください。 場合によっては、4次ヒープよりも高速です(マージだけでなく、入力データと実行される操作によって異なります)。

参考文献

  • Michael L. Fredman、Robert Sedgewick、Daniel D. Sleator、およびRobert E. Tarjan(1986)、ペアリングヒープ:新しい形式の自己調整ヒープAlgorithmica 1:111-129
  • Daniel H. Larkin、Siddhartha Sen、およびRobert E. Tarjan(2014)、優先度付きキューの基本に立ち返った実証的研究arXiv:1403.0252v1 [cs.DS]。

ところで、それは最初のAPIレビューの反復のためです。 _openquestions_のセクションを解決する必要があります([そして可能であればAPIレビューアの]あなたの意見が必要です)。 最初の反復が少なくとも部分的に通過した場合、残りの議論では、新しい問題を作成したいと思います(これを閉じて参照してください)。

私の知る限り、インターフェースやヒープはAPIレビューに合格しません。

@pgolebiowski :では、問題を解決してみませんか? インターフェイスがないと、このクラスはほとんど役に立たなくなります。 私がそれを使用できる唯一の場所は、プライベート実装です。 その時点で、必要に応じて独自の優先度キューを作成(または再利用)できます。 別の実装と交換する必要があるとすぐに壊れてしまうため、署名にこのタイプのパブリックAPIをコードで公開することはできません。

@bendono
さて、マイクロソフトはここで最後の言葉を持っています、スレッドにコメントしている人も少なくありません。 そして、私たちはそれを知っています:

いくつかの意見を確認したので、他のAPIレビューア/アーキテクトがそれを共有することはある程度確信しています。名前はPriorityQueueである必要があり、IHeapインターフェイスを導入するべきではありません。 実装は1つだけである必要があります(おそらくいくつかのヒープを介して)。

これは@karelz、ソフトウェアエンジニアマネージャー、および@terrajobst、プログラムマネージャとこのリポジトリ+いくつかのAPIの審査の所有者によって共有されています。

以前の投稿で明確に述べたように、私は明らかにインターフェースを使用したアプローチが好きですが、この議論ではあまり力がないことを考えると、かなり難しいことがわかります。 私達は私達の主張をしました、しかし私達はほんの一部の論評者です。 とにかく、コードは私たちのものではありません。 他に何ができますか?

では、問題を解決してみませんか? インターフェイスがないと、このクラスはほとんど役に立たなくなります。

私は十分にやりました-私の仕事を嫌う代わりに、何かをします。 実際の作業を行います。 どうして私を責めようとするの? 自分の視点で他の人を説得することに成功しなかったことを自分のせいにしてください。

そして、そのようなレトリックを私に惜しまないでください。 それは本当に幼稚です-もっとうまくやってください。

ところで、この提案は、インターフェースを導入するかどうか、またはクラスの名前がPriorityQueueHeapかに関係なく、一般的なことに焦点を当てています。 ですから、ここで本当に重要なことに焦点を当て、何かが必要な場合は行動へのバイアスを示してください。

@pgolebiowskiもちろん、それはマイクロソフトの決定です。 ただし、ニーズに合った使用したいAPIを提示することをお勧めします。 それが拒否された場合は、そうです。 提案を妥協する必要はないと思います。

あなたが私のコメントをあなたのせいにしたと解釈してくれたら、お詫びします。 それは確かに私の意図ではありませんでした。

@pgolebiowskiハンドルにKeyValuePair<TKey,TValue>を使用してみませんか?

@SamuelEnglard

ハンドルにKeyValuePair<TKey,TValue>を使ってみませんか?

  • ええと、 PriorityQueueHandleは実際には_ペアリングヒープノード_です。 TKey KeyTValue Value 2つのプロパティを公開します。 ただし、内部にははるかに多くのロジックがあり、それは単なる内部です。 これの私の実装を参照し
  • KeyValuePairは構造体であるため、毎回コピーされ、継承することはできません。
  • しかし、重要なことは、 PriorityQueueHandleはかなり複雑なクラスであり、たまたまKeyValuePairと同じパブリックAPIを公開しているということです。

@bendono

ただし、ニーズに合った使用したいAPIを提示することをお勧めします。 それが拒否された場合は、そうです。 提案を妥協する必要はないと思います。

  • それは本当です、私はそれを念頭に置いて、何が起こるかを見ます。 @karelzは、提案とともに、インターフェースに投票した以前の投稿(提案の直前にあります)の一部を渡すこともできますか? APIレビューの最初の反復の後、後でこれに戻る可能性があります。
  • それでも、どのソリューションを使用しても、機能は非常に似ているため、確認しておくと役に立ちます。 ハンドルのアイデアが拒否され、要素を適切に更新/削除できない場合(または、 TKeyTValueを別々に持つことができない場合)、このクラスは本当に役に立たないことになります- -現在Javaで使用されているため。
  • 特に、インターフェイスがない場合、私のAlgoKitライブラリ
  • そして、はい、インターフェイスを追加することがマイクロソフトによって不利であると認識されていることは私にとって本当に驚くべきことです。

それは確かに私の意図ではありませんでした。

すみません、それでは私の間違いです。

デザインに関する私の質問(免責事項:これらの質問と説明、(まだ)ハードプッシュバックはありません):

  1. 本当にPriorityQueueHandleが必要ですか? キューに一意の値が含まれていると予想した場合はどうなりますか?

    • 動機:それはかなり複雑な概念のようです。 持っているのなら、なぜ必要なのか理解したいのですが? それはどのように役立ちますか? それとも、特定の実装の詳細がAPIサーフェスに漏れているだけですか? APIの複雑さに対して支払うために、それだけのパフォーマンスを購入するのでしょうか?

  2. Mergeが必要ですか? 他のコレクションにはありますか? APIを追加するべきではありません。実装が簡単であるという理由だけで、APIには一般的なユースケースがいくつかあるはずです。

    • たぶん、 IEnumerable<KeyValuePair<TKey, TValue>>から初期化を追加するだけで十分でしょうか? + Linqに依存する

  3. comparerオーバーロードが必要ですか? 常にデフォルトにフォールバックできますか? (免責事項:この場合、知識/専門知識が不足しているので、質問してください)
  4. keySelectorオーバーロードが必要ですか? 価値の一部として優先するか、それとも別のものにするかを決める必要があると思います。 個別の優先順位は私にはもう少し自然に思えますが、私は強い意見を持っていません。 私たちは賛否両論を知っていますか?

個別/並行決定ポイント:

  1. クラス名PriorityQueueHeap
  2. IHeapとコンストラクターのオーバーロードを導入しますか?

IHeapとコンストラクターのオーバーロードを導入しますか?

物事が落ち着いて2セントを投入できるようになっているようです。個人的にはインターフェースが好きです。 私の意見では、構造を単純化し、最も使いやすくする方法で、API(およびそのAPIによって記述されるコア機能)から実装の詳細を抽象化します。

私があまり強く意見を持っていないのは、インターフェイスをPQueue / Heap / ILikeThisItemMoreThanThisItemListと同時に実行するのか、後で追加するのかということです。 APIは「流動的」である可能性があるため、フィードバックを得るまで最初にクラスとしてリリースする必要があるという議論は、確かに私が同意しない有効なものです。 問題は、インターフェイスを追加するのに十分な「安定性」があると見なされる場合になります。 上記のスレッドで、 IListIDictionaryは、かなり前に追加した正規の実装のAPIに遅れをとっていると言及されていたので、許容できる休止期間と見なされる期間はどれですか?

その期間を合理的な確実性で定義でき、それが許容できないほどブロックされていないことを確認できれば、インターフェイスなしでこの重いデータ構造のものを出荷することに問題はありません。 その後、その期間の後、使用法を調べて、インターフェースの追加を検討できます。

そして、はい、インターフェイスを追加することがマイクロソフトによって不利であると認識されていることは私にとって本当に驚くべきことです。

それは多くの点でそれ不利だからです。 インターフェイスを出荷すれば、ほぼ完了です。 そのAPIを反復する余地はあまりないので、最初は正しいことを確認し、今後数年間は正しいことを継続します。 いくつかの便利な機能が欠けていることは、すべてをより良くするこの1つの小さな変更だけを行うのが非常に良い、潜在的に不十分なインターフェイスで立ち往生するよりもはるかに良い場所です。

@karelzと@ianhaysの入力ありがとうございます!

重複を許可する

本当にPriorityQueueHandleが必要ですか? キューに一意の値が含まれていると予想した場合はどうなりますか?

動機:それはかなり複雑な概念のようです。 持っているのなら、なぜ必要なのか理解したいのですが? それはどのように役立ちますか? それとも、特定の実装の詳細がAPIサーフェスに漏れているだけですか? APIの複雑さに対して支払うために、それだけのパフォーマンスを購入するのでしょうか?

いいえ、必要ありません。 上で提案された優先キューAPIは非常に強力で、非常に柔軟性があります。 これは、要素と優先順位が重複する可能性があるという仮定に基づいています。 そのため、正しいノードを削除または更新するには、ハンドルが必要です。 ただし、要素が一意である必要があるという制限を設けると、内部クラス( PriorityQueueHandle )を公開せずに、より単純なAPIを使用して上記と同じ結果を得ることができますが、これは実際には理想的ではありません。

一意の要素のみを許可すると仮定しましょう。 これまでのすべてのシナリオを引き続きサポートし、最適なパフォーマンスを維持できます。 よりシンプルな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();
}

まもなく、基礎となるDictionary<TElement, InternalNode>ます。 優先キューに要素が含まれているかどうかのテストは、以前のアプローチよりもさらに高速に実行できます。 要素の更新と削除は、キュー内の直接要素を常に指すことができるため、大幅に簡素化されます。

おそらく複製を許可することは面倒な価値がなく、上記で十分です。 私はそれが好きだと思います。 どう思いますか?

マージ

Mergeが必要ですか? 他のコレクションにはありますか? APIを追加するべきではありません。実装が簡単であるという理由だけで、APIには一般的なユースケースがいくつかあるはずです。

同意。 必要ありません。 APIはいつでも追加できますが、削除することはできません。 このメソッドを削除し、(場合によっては)後で追加しても問題ありません(必要な場合)。

比較者

比較器の過負荷が必要ですか? 常にデフォルトにフォールバックできますか? (免責事項:この場合、知識/専門知識が不足しているので、質問してください)

これは、2つの理由から非常に重要だと思います。

  • ユーザーは、要素を昇順または降順で並べ替えたいと考えています。 最高の優先順位は、注文がどのように行われるべきかを教えてくれません。
  • 比較子をスキップすると、 IComparableを実装するクラスを常に提供するようにユーザーに強制します。

さらに、既存のAPIと一貫性があります。 SortedDictionaryをご覧ください

セレクタ

keySelectorのオーバーロードが必要ですか? 価値の一部として優先するか、それとも別のものにするかを決める必要があると思います。 個別の優先順位は私にはもう少し自然に思えますが、私は強い意見を持っていません。 私たちは賛否両論を知っていますか?

私も別の優先順位が好きです。 それとは別に、これを実装する方が簡単で、パフォーマンスとメモリ使用量が向上し、APIがより直感的になり、ユーザーの作業が少なくなります( IComparableを実装する必要はありません)。

今セレクターについて...

長所

これが、この優先キューを柔軟にする理由です。 これにより、ユーザーは次のことが可能になります。

  • 個別の要素としての要素とその優先順位
  • どこかに優先順位がある要素(複雑なクラス)
  • 特定の要素の優先度を取得する外部ロジック
  • IComparableを実装する要素

それは私が考えることができるほとんどすべての構成を可能にします。 ユーザーは「プラグアンドプレイ」するだけなので、便利だと思います。 また、かなり直感的です。

短所

  • 学ぶべきことがもっとあります。
  • その他のAPI。 2つの追加コンストラクター、1つの追加のEnqueueおよびUpdateメソッド。
  • 要素と優先度を分離または結合することにした場合、一部のユーザー(異なる形式のデータを持っている)に、このデータ構造を使用するようにコードを適合させるように強制します。

個別/並行決定ポイント

クラス名PriorityQueueHeap

IHeapとコンストラクターのオーバーロードを導入します。

  • Heapではなく、 PriorityQueueをCoreFXの一部にする必要があると感じています。
  • IHeapインターフェイスに関して—優先度付きキューはヒープとは異なるもので実装できるため、おそらくこの方法で公開したくないでしょう。 ただし、 IPriorityQueueが必要になる場合があります。

インターフェイスを出荷すれば、ほぼ完了です。 そのAPIを反復する余地はあまりないので、最初は正しいことを確認し、今後数年間は正しいことを継続します。 いくつかの便利な機能が欠けていることは、すべてをより良くするこの1つの小さな変更だけを行うのが非常に良い、潜在的に不十分なインターフェイスで立ち往生するよりもはるかに良い場所です。

完全に同意する!

同等の要素

別の仮定を追加すると、要素は比較可能でなければならないという仮定があれば、APIはさらに単純になります。 しかし、繰り返しになりますが、柔軟性は低くなります。

長所

  • IComparer必要ありません。
  • セレクターは必要ありません。
  • 1つのEnqueueおよびUpdateメソッド。
  • 2つではなく1つのジェネリック型。

短所

  • 要素と優先度を別々のオブジェクトとして持つことはできません。 ユーザーが新しいラッパークラスをその形式で持っている場合は、それを提供する必要があります。
  • ユーザーは、この優先度キューを使用する前に、常にIComparableを実装する必要があります。
  • 要素と優先順位を別々にすると、パフォーマンスとメモリ使用量が向上し、実装が簡単になります。内部ディクショナリ<TElement, InternalNode>InternalNodeTPriorityが含まれます。
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();
}

すべてがトレードオフです。 一部の機能を削除し、柔軟性を失いますが、ユーザーの95%を満足させるためにそれは必要ないかもしれません。

柔軟性が高く、使用量も多いため、インターフェースアプローチも気に入っていますが、 @ karelz@ianhaysにも同意します。新しいクラスのAPIが使用されるまで待機し、実際にインターフェースを出荷してからフィードバックを受け取る必要があります。そのバージョンがストックされており、すでに使用している顧客を壊さずに変更することはできません。

比較者についても、他のBCL APIに準拠していると思います。また、ユーザーが新しいラッパークラスを提供する必要があるという事実は気に入らないと思います。 コンストラクターのオーバーロードが比較子アプローチを受け取り、内部で実行する必要があるすべての比較の中でその比較子を使用するのが本当に好きです。比較子が提供されていないか、比較子がnullの場合は、デフォルトの比較子を使用します。

@pgolebiowskiこのような詳細で説明的なAPI提案に感謝し、このAPIが承認されてCoreFXに追加されるように積極的かつ精力的に取り組んでいます👍このディスカッションが完了し、レビューの準備ができていると考えた場合、すべての入力と最終的なAPIサーフェスをマージします1つのコメントにまとめ、上部の主要な問題のコメントを更新します。これにより、レビュー担当者の作業が楽になります。

OK、私は比較者について確信しています、それは理にかなっていて、それは一貫しています。
私はまだセレクターに引き裂かれています-IMOはそれなしで行こうとすべきです-それを2つのバリアントに分割しましょう。

`` `c#
パブリッククラスPriorityQueue
:IEnumerable、
IEnumerable <(TElement要素、TPriority優先度)>、
IReadOnlyCollection <(TElement要素、TPriority優先度)>
// ICollectionは意図的に含まれていません
{{
public PriorityQueue();
public PriorityQueue(IComparer比較者);

public IComparer<TPriority> Comparer { get; }
public int Count { get; }

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

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

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

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

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

//
//セレクターパーツ
//
public PriorityQueue(FuncprioritySelector);
public PriorityQueue(FuncprioritySelector、IComparer比較者);

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

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

}
「」

未解決の質問:

  1. クラス名PriorityQueueHeap
  2. IHeapとコンストラクターのオーバーロードを導入しますか? (後で待つ必要がありますか?)
  3. IPriorityQueue紹介しますか? (後で待つ必要があります- IDictionary例)
  4. (値内に格納されている優先度の)セレ​​クターを使用するかどうか(5つのAPIの違い)
  5. タプル(TElement element, TPriority priority)KeyValuePair<TPriority, TElement>

    • PeekDequeueは、タプルではなくout引数を指定する必要がありますか?

早めのフィードバックのために、明日APIレビューグループで実行してみます。

PeekDequeueタプルフィールド名をpriority修正しました(指摘してくれた
上記の最新の提案で更新されたトップポスト。

私は@safernのコメントを2番目にしてい@pgolebiowskiに感謝します!

未解決の質問:

ここにもう1つあると思います。

  1. 一意の要素のみに制限します(重複を許可しないでください)。

良い点は、未解決の質問ではなく、トップの投稿で「仮定」として捉えられています。 反対に、APIはかなり醜いものになります。

我々は返す必要がありますboolからRemove ? また、 out priority argとしての優先順位は? (最近、他のデータ構造に同様のオーバーロードを追加したと思います)

Contains似ています-誰かがそれから優先順位を望んでいるに違いありません。 out priorityオーバーロードを追加したい場合があります。

ヒープは実装を意味し、これをPriorityQueueと呼ぶことを正当にサポートするだろうという意見は残っています(まだ声を上げていませんが)。 (その場合、重複要素を許可し、更新などを許可しない、よりスリムなHeapクラスの提案もサポートします(元の提案に沿ったものです)が、それは期待していません。発生する)。

KeyValuePair<TPriority, TElement>は、重複する優先順位が予想されるため、絶対に使用しないでください。また、 KeyValuePair<TElement, TPriority>も混乱を招くと思うので、 KeyValuePairをまったく使用しないことをサポートします。プレーンタプル、または優先度のoutパラメーターを使用します(個人的にはoutパラメーターが好きですが、大騒ぎしていません)。

重複を許可しない場合は、異なる/変更された優先順位でそれらを再追加しようとする動作を決定する必要があります。

私はセレクターの提案を支持しません。それは冗長性を意味するという単純な理由で、そして正当に混乱を再現するからです。 要素の優先度が一緒に保存されている場合、それは2つの場所に保存され、それらが同期しなくなった場合(つまり、誰かが役に立たないように見えるUpdate(TElement)メソッドを呼び出すのを忘れた場合)、多くの苦しみがあります確実になります。 セレクターがコンピューターの場合、意図的に要素を追加し、計算元の値を変更する人を受け入れます。今、彼らがそれを再度追加しようとすると、状況によってはうまくいかない可能性のあることがたくさんあります。これが発生したときに何が起こるかの決定について。 少し高いレベルでは、要素の変更されたコピーが追加される可能性があります。これは、以前と同じではなくなったためです(これは可変キーの一般的な問題ですが、優先度と要素を分離すると、潜在的な問題を回避するのに役立ちます)。

セレクター自体が動作を変更する傾向があります。これは、ユーザーが考えずにすべてを壊すことができるもう1つの方法です。 ユーザーに優先順位を明示的に提供させる方がはるかに良いと思います。 これで私が目にする大きな問題は、要素ではなくペアを宣言しているため、重複エントリが許可されていることを伝えていることです。 ただし、賢明なインラインドキュメントとEnqueue bool戻り値は、これを簡単に修正する必要があります。 ブール値よりも優れているのは、要素の古い/新しい優先度を返すことです(たとえば、 Enqueueが新しく提供された優先度、2つのうちの最小値、または古い優先度を使用する場合)が、 Enqueueは、何かを再度追加しようとすると失敗するはずなので、成功を示すboolを返すだけです。 これにより、 EnqueueUpdate完全に分離され、明確に定義されます。

KeyValuePairをまったく使用せず、プレーンタプルを使用することもサポートします

私はタプルであなたと一緒です。

重複を許可しない場合は、異なる/変更された優先順位でそれらを再追加しようとする動作を決定する必要があります。

  • Dictionary内のインデクサーとの類似性がわかります。 そこで、 dictionary["something"] = 5を実行すると、 "something"が以前にキーであった場合に更新されます。 そこになかった場合は、追加されるだけです。
  • ただし、 Enqueueメソッドは、辞書のAddメソッドに似ています。 つまり、例外をスローする必要があります。
  • 上記の点を考慮して、優先キューにインデクサーを追加して、考えている動作をサポートすることを検討できます。
  • しかし、順番に、インデクサーはキューの概念で機能するものではないかもしれません。
  • これは、誰かが重複した要素を追加したい場合、 Enqueueメソッドは例外をスローするだけでよいという結論につながります。 同様に、存在しない要素の優先度を誰かが更新したい場合、 Updateメソッドは例外をスローする必要があります。
  • これは新しい解決策につながります-実際にbool返すTryUpdateメソッドを追加します。

冗長性を意味するという単純な理由で、セレクターの提案はサポートしていません

キーが物理的にコピーされていない(存在する場合)のではなく、セレクターは、優先順位を評価する必要があるときに呼び出される関数のままです。 冗長性はどこにありますか?

優先度と要素を分離すると、潜在的な問題を回避するのに役立つと思います

唯一の問題は、顧客が物理的な優先順位を持っていない場合です。 その時彼は多くをすることができません。

ユーザーに優先順位を明示的に提供させる方がはるかに良いと思います。 これで私が目にする大きな問題は、要素ではなくペアを宣言しているため、重複エントリが許可されていることを伝えていることです。

そのソリューションにはいくつかの問題がありますが、重複エントリが許可されていることを伝える理由は必ずしもありません。 「重複なし」ロジックはTElementのみ適用する必要があると思います。優先順位はここでは単なる値です。

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

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

@VisualMelonは意味がありますか?

@pgolebiowski

私はTryUpdateを追加することに完全に同意し、 Update投げることは私には理にかなっています。 私は( IDictionaryではなく) Enqueueに関してISet方針に沿ってもっと考えていました。 投げることも理にかなっているでしょう、私はその点を逃したに違いありません、しかし私はリターンboolがタイプの「セットネス」を伝えると思います。 おそらくTryEnqueueも( Add投げて)順番になっているでしょうか? TryRemoveもありがたいです(空の場合は失敗します)。 最後のポイントですが、そうです、それも私が考えていた行動です。 IDictionaryとのアナロジーは、リフレクションではISetよりも優れていると思いますが、それは十分に明確であるはずです。 (要約すると、私はあなたの提案に従って投げるすべてをサポートしますが、その場合はTry*を持っている必要があります。失敗条件に関するあなたの声明にも同意します)。

インデクサーに関しては、キューの概念に実際には「適合」していないのは正しいと思います。私はそれをサポートしません。 どちらかといえば、キュ​​ーまたは更新するための適切な名前のメソッドが適切です。

キーが物理的にコピーされていない(存在する場合)のではなく、セレクターは、優先順位を評価する必要があるときに呼び出される関数のままです。 冗長性はどこにありますか?

そうです、私は提案の更新を読み間違えました(それを要素に保存することについてのビット)。 セレクターがオンデマンドで呼び出されるとすると(パフォーマンスに影響を与える可能性があるため、ドキュメントで明確にする必要があります)、関数の結果はデー​​タ構造が応答せずに変更される可能性があり、 Updateが呼び出されない限り、2つの非同期。 さらに悪いことに、ユーザーが複数の要素の有効な優先度を変更し、そのうちの1つだけを更新した場合、データ構造は、「更新されていない」要素から選択されたときに「コミットされていない」変更に依存することになります(私は調べていません)提案されたDataStructureの実装の詳細ですが、これはすべてのUpdate O(<n)にとって必然的に問題になると思います)。 ユーザーに優先順位を明示的に更新するように強制すると、これが改善され、データ構造が一貫した状態から別の状態に移行する必要があります。

セレクターの提案についてこれまで述べてきたすべての不満は、APIの堅牢性に関するものであることに注意してください。セレクターを使用すると、誤った使用が簡単になると思います。 ただし、使いやすさはさておき、概念的には、キュー内の要素はそれらの優先順位を知る必要はありません。 それは潜在的に彼らとは無関係であり、ユーザーが要素をstruct Queable<T>{}か何かでラップすることになった場合、それは摩擦のないAPIを提供することに失敗したように見えます(私がこの用語を使用するのは痛いです)。

もちろん、セレクターがないと、ユーザーがセレクターを呼び出す負担があると主張することもできますが、要素が優先順位を知っていれば、(うまくいけば)きちんと公開され(つまりプロパティ)、そうでない場合はそうなると思います。 tの場合、心配するセレクターはありません(オンザフライで優先度を生成するなど)。 セレクターが必要ない場合は、セレクターをサポートするために必要な、さらに多くの無意味な配管があります。明確に定義された「セレクター」がある場合は、優先順位を渡します。 セレクターは、この情報を公開するようにユーザーを誘導しますが(おそらく役に立たない)、個別の優先順位は、設計上の決定に影響を与えるとは想像できない非常に透過的なインターフェイスを提供します。

セレクターを支持して私が本当に考えることができる唯一の議論は、それはあなたがPriorityQueue渡すことを使用できることを意味し、プログラムの他の部分は優先順位がどのように計算されるかを知らなくてもそれを使用できるということです。 これについてはもう少し考えなければなりませんが、「ニッチ」な適合性を考えると、より一般的なケースでは、かなり重いオーバーヘッドに対する報酬はほとんどありません。

_編集:もう少し詳しく説明しますが、Elementsを投げることができる自己完結型のPriorityQueueがあると非常に便利ですが、これを回避するためのコストは大きいと思いますが、導入のコストはかなり少なくなります。_

私は、オープンソースの.NETコードベースを調べて、優先度付きキューがどのように使用されているかの実際の例を確認するために少し作業を行ってきました。 トリッキーな問題は、学生のプロジェクトとトレーニングのソースコードを除外することです。

使用法1:Roslyn通知サービス
https://github.com/dotnet/roslyn/
Roslynコンパイラには、「PriorityQueue」と呼ばれるプライベート優先度キューの実装が含まれています。これは、非常に特殊な最適化が行われているようです。オブジェクトキューは、キュー内のオブジェクトを再利用して、ガベージコレクションを回避します。 Enqueue_NoLockメソッドは、current.Value.MinimumRunPointInMS <entry.Value.MinimumRunPointInMSで評価を実行して、キュー内のどこに新しいノードを配置するかを決定します。 ここで提案されている2つの主要な優先度キューの設計(比較関数/デリゲートと明示的な優先度)のいずれかが、この使用シナリオに適合します。

使用法2:Lucene.net
https://github.com/apache/lucenenet
これは間違いなく、.NETで見つけたPriorityQueueの最大の使用例です。 Apache Lucene.netは、人気のあるLucene検索エンジンライブラリの完全な.netポートです。 私の会社ではJavaバージョンを使用しており、ApacheのWebサイトによると、いくつかの有名人が.NETバージョンを使用しています。 Githubには.NETプロジェクトのフォークが多数あります。

Luceneには、HitQueue、TopOrdAndFloatQueue、PhraseQueue、SuggestWordQueueなどのいくつかの「特殊な」優先度キューによってサブクラス化された独自のPriorityQueue実装が含まれています。 同様に、プロジェクトは多くの場所でPriorityQueueを直接インスタンス化します。

上記にリンクされているLuceneのPriorityQueueの実装は、この号で投稿された元の優先度キューAPIと非常によく似ています。 「PriorityQueue」として定義されています"そしてIComparerを受け入れますコンストラクターのパラメーター。 メソッドとプロパティには、カウント、クリア、オファー(エンキュー/プッシュ)、ポーリング(デキュー/ポップ)、ピーク、削除(キューで最初に一致するアイテムを削除)、追加(オファーの同義語)が含まれます。 興味深いことに、これらはキューの列挙サポートも提供します。

LuceneのPriorityQueueの優先度は、コンストラクターに渡された比較子によって決定され、何も渡されなかった場合は、比較されたオブジェクトがIComparableを実装していると見なされます。そのインターフェースを使用して比較を行います。 ここに掲載されている元のAPI設計は、値型でも機能することを除いて、同様です。

コードベースには多数の使用例があり、 SloppyPhraseScorerもその1つです。

SloppyPhraseScorerのコンストラクターは、PriorityQueueの独自のカスタムサブクラスの1つである新しいPhraseQueue(pq)をインスタンス化します。 PhrasePositionsのコレクションが生成されます。これは、一連の投稿、位置、および一連の用語のラッパーのように見えます。 FillQueueメソッドは、フレーズの位置を列挙してキューに入れます。 PharseFreq()はAdvancePP関数を呼び出し、高レベルでは、デキューし、アイテムの優先度を更新してから、再度エンキューするように見えます。 優先度は、明示的ではなく(比較子を使用して)相対的に決定されます(優先度は、エンキュー中に2番目のパラメーターとして「渡される」ことはありません)。

PhraseQueueの実装に基づいて、コンストラクターを介して渡された比較値(整数など)がそれをカットしない可能性があることがわかります。 それらの比較関数( "LessThan")は、PhrasePositions.doc、PhrasePositions.position、およびPhrasePositions.offsetの3つの異なるフィールドを評価します。

使用法3:ゲーム開発
このスペースでの使用例の検索はまだ完了していませんが、ゲーム開発で使用されているカスタム.NETPriorityQueueの例をかなり多く見ました。 非常に一般的な意味で、これらは主なユースケース(ダイクストラ法)としてパスファインディングを中心にクラスター化される傾向がありました。 Unity 3Dにより、.NETでパスファインディングアルゴリズムを実装する方法を尋ねる人がたくさんいます。

まだこの領域を掘り下げる必要があります。 明示的な優先順位がキューに入れられているいくつかの例と、Comparer / IComparableを使用しているいくつかの例を見ました。

別の注意点として、一意の要素、明示的な要素の削除、および特定の要素が存在するかどうかの判断について、いくつかの議論がありました。

キューは、データ構造として、通常、エンキューとデキューをサポートします。 他のセット/リストのような操作を提供する道を進むと、実際にはまったく別のデータ構造を設計しているのではないかと思います。これは、タプルのソートされたリストに似ています。 発信者がエンキュー、デキュー、ピーク以外のニーズを持っている場合、おそらく優先キュー以外のものが必要ですか? キューは、定義上、キューへの挿入とキューからの順序付き削除を意味します。 他にはあまりありません。

@ebickle

他のリポジトリを調べて、優先キュー機能がどのように提供されているかを確認していただき、ありがとうございます。 ただし、IMOのこの議論は、特定の設計提案を提供することでメリットがあります。 このスレッドはすでにフォローするのが難しく、結論なしに長い話をすることはそれをさらに難しくします。

キュー[...]は通常、エンキューとデキューをサポートします。 [...]発信者がエンキュー、デキュー、ピーク以外のニーズを持っている場合、おそらく優先キュー以外のものが必要ですか? キューは、定義上、キューへの挿入とキューからの順序付き削除を意味します。 他にはあまりありません。

  • 結論は何ですか? 提案?
  • それは真実からかけ離れています。 あなたが言及したダイクストラのアルゴリズムでさえ、要素の優先順位の更新を利用しています。 また、特定の要素を更新するために必要なものは、特定の要素を削除するためにも必要です。

素晴らしい議論。 @ebickleの調査は非常に便利なIMOです!
@ebickle [2] lucene.netについて結論はありますか?最新の提案は使用法に適合していますか? (私はあなたの詳細な説明でそれを見逃していなかったと思います)

上からTry*バリアントが必要なようです+ IsEmpty + TryPeek / TryDequeue + EnqueueOrUpdate ? 考え?
`` `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

上からTry *バリアントが必要なようです

はい、正確に。

キューに入れられたものと更新されたもののブールステータスを返す必要がありますか?

どこでもステータスを返したい場合は、どこでも。 この特定の方法では、いずれかの操作( EnqueueまたはUpdate )が成功した場合に当てはまるはずです。

残りの部分については-私は単に同意します:smile:

たった1つの質問-なぜref代わりにoutか? わからない:

  • 関数に入る前に初期化する必要はありません。
  • メソッドには使用されません(消えるだけです)。

どこでもステータスを返したい場合は、どこでも。 この特定の方法では、いずれかの操作( EnqueueまたはUpdate )が成功した場合に当てはまるはずです。

それは常にtrueを返しますが、それは良い考えではありません。 このような場合、IMOはvoidを使用する必要があります。 そうしないと、人々は混乱し、戻り値をチェックして、実行されない無駄なコードを追加します。 (私が何かを逃した場合を除いて)

ref vs. outが同意し、私はそれについて自分で議論しました。 自分で決断するほどの強い意見や経験がありません。 APIレビューアに質問するか、コメントが増えるのを待つことができます。

それは常にtrueを返します

あなたは正しい、私の悪い。 ごめん。

自分で決断するほどの強い意見や経験がありません。

何かが足りないかもしれませんが、それはとても簡単だと思います。 refを使用する場合、基本的に、 PeekDequeueは、渡されたTElementTPriorityを何らかの方法で使用したいと言っています(私はそれらのフィールドを読むことを意味します)。 実際にはそうではありません-私たちのメソッドはそれらの変数にのみ値を割り当てることになっています(そして実際にはコンパイラーによってそうする必要があります)。

Try* APIで更新されたトップポスト
2つの未解決の質問を追加しました:

  • [6] PeekDequeueスローはまったく役に立ちますか?
  • [7] TryPeekおよびTryDequeue - refまたはout引数を使用する必要がありますか?

ref vs.out-その通りです。 falseを返した場合に備えて、初期化を回避するために最適化していました。 それは私から愚かで盲目的でした-時期尚早の最適化。 outに変更して、質問を削除します。

6:何かが足りないかもしれませんが、例外をスローしない場合、キューが空の場合、 PeekまたはDequeueはどうすればよいですか? これは、データ構造がnullを受け入れるべきかどうかについて疑問を投げかけ始めていると思います(私は望まないが、確固たる意見はありません)。 nullを許可しても、値型にはnullがないため( default確かにカウントされません)、 PeekDequeueは意味のない結果を伝える方法はありません。当然、例外をスローする必要があると思います(したがって、 outパラメーターに関する懸念を取り除きます!)。 既存のQueue.Dequeueの例に従わない理由はわかりません

ダイクストラ(別名、ヒューリスティックなしのヒューリスティック検索)では、優先度を変更する必要がないことを付け加えておきます。 私は何かの優先度を更新したくなかったので、ヒューリスティック検索を常に打ち消します。 (アルゴリズムのポイントは、状態を探索すると、その状態への最適なルートを探索したことがわかっていることです。そうしないと、最適ではなくなるリスクがあるため、優先順位を改善することはできず、確かに改善しません。それを減らしたい(つまり、より悪いルートを検討する)。_ヒューリスティックを意図的に選択して最適ではない場合を無視します。この場合、もちろん最適ではない結果が得られますが、それでも決して最適ではありません。優先度を更新します_)

例外をスローしない場合、キューが空の場合、PeekまたはDequeueは何をする必要がありますか?

NS。

したがって、アウトパラメータに関する懸念を取り除くことができます!

どのように? ええと、 outパラメータはTryPeekTryDequeueメソッド用です。 例外をスローするのはPeekDequeueです。

ダイクストラは優先順位の変更を必要としないはずだと付け加えておきます。

私は間違っているかもしれませんが、私の知る限り、ダイクストラのアルゴリズムはDecreaseKey演算を利用しています。 たとえば、これを参照してください。 これが効率的かどうかは別の側面です。 実際、フィボナッチヒープは、O(1)で漸近的にDecreaseKey操作を実現するように設計されています(ダイクストラを改善するため)。

しかし、それでも、私たちの議論で重要なことは、優先度付きキューの要素を更新できることは非常に役立ち、そのような機能を検索する人々がいます(StackOverflowで以前にリンクされた質問を参照してください)。 私も数回使ったことがあります。

申し訳ありませんが、はい、 outパラメータの問題が発生し、もう一度読み間違えています。 そして、ダイクストラの変種(私が信じていたよりもかなり広く適用されているように見える名前...)は、おそらくより効率的に実装できるようです(キューにすでに要素が含まれている場合は、繰り返し検索できるという利点があります)。更新可能な優先順位。 それは私が長い単語と名前を使うことで得られるものです。 Updateを削除することを提案しているわけではないことに注意してください。この機能が課す制限なしに、よりスリムなHeapまたはその他(つまり元の提案)を用意するのもよいでしょう(栄光のように)私が感謝している機能です)。

7:これらはout必要があります。 どの入力も意味を持たない可能性があるため、存在してはなりません(つまり、 refを使用するべきではありません)。 outパラメータでは、 default値を返すことを改善するつもりはありません。 これはDictionary.TryGetValueが行うことであり、それ以外の理由はありません。 _そうは言っても、 refを値またはデフォルトとして扱うことはできますが、意味のあるデフォルトがない場合は、問題を苛立たせます。_

APIレビューディスカッション:

  • すべての賛否両論を話し合うために、適切な設計会議(2時間)を行う必要があります。今日投資した30分では、コンセンサスを得る/未解決の質問を閉じるには不十分でした。

    • ほとんどの投資コミュニティメンバーを招待する可能性があります- @ pgolebiowski 、他の誰か?

重要な生のメモは次のとおりです。

  • 実験-(CoreFxLabsで)実験を行い、プレリリースのNuGetパッケージとしてリリースし、消費者にフィードバックを求める必要があります(ブログ投稿を介して)。 フィードバックループなしでAPIを釘付けにできるとは考えていません。

    • 過去にImmutableCollectionsについても同様のことを行いました(高速プレビューリリースサイクルが重要であり、APIの形成に役立ちました)。

    • この実験は、すでにCoreFxLabにあるMultiValueDictionaryとバンドルできます。 TODO:候補者がもっといるかどうかを確認してください。それぞれを個別にブログに投稿したくありません。

    • 実験的なNuGetパッケージは、実験が終了した後に削除され、ソースコードをCoreFXまたはCoreFXExtensionsに移動します(後で決定されます)。

  • アイデア: PeekDequeue TElementからPeekだけを返し、 TPriorityは返さないでください(ユーザーはTry*メソッドを使用できます)。
  • 安定性(同じ優先度の場合)-同じ優先度のアイテムは、キューに挿入された順序で返される必要があります(一般的なキューの動作)
  • 複製

    • Update削除します-ユーザーはRemoveを削除してから、異なる優先度でEnqueueアイテムを元に戻すことができます。

    • Removeは、最初に見つかったアイテムのみを削除する必要があります( List同様)。

  • アイデア: IQueueインターフェースを抽象化してQueuePriorityQueueを抽象化できますか( PeekDequeue TElementだけを返しますここ)

    • 注:不可能であることが判明する場合がありますが、APIを決定する前に調査する必要があります

私たちはセレクターについて長い間議論しました-私たちはまだ決定されていません(上記のIQueueによって要求されるかもしれませんか?)。 大多数はセレクターが気に入らなかったが、将来のAPIレビュー会議で状況が変わる可能性がある。

他の未解決の質問は議論されませんでした。

最新の提案は私にはかなり良さそうです。 私の意見/質問:

  1. PeekDequeuepriorityoutパラメータを使用した場合、優先度をまったく返さないオーバーロードが発生する可能性もあります。これは単純化すると思います。一般的な使用法。 ただし、破棄を使用して優先度を無視することもできるため、これはそれほど重要ではありません。
  2. セレクターのバージョンが異なるコンストラクターを使用して区別され、異なるメソッドのセットを有効にするのは好きではありません。 たぶん、別のPriorityQueue<T>があるはずですか? または、 PriorityQueue<T, T>機能する静的メソッドと拡張メソッドのセット?
  3. 列挙時に返される要素はどのような順序で定義されていますか? 私の推測では、それを効率的にするために、それは未定義です。
  4. 優先度を無視して、優先キュー内のアイテムだけを列挙する方法があるはずですか? または、 priorityQueue.Select(t => t.element)ようなものを使用する必要がありますか?
  5. 優先度付きキューが内部で要素タイプにDictionaryしている場合、 IEqualityComparer<TElement>を渡すオプションが必要ですか?
  6. Dictionaryを使用して要素を追跡することは、優先順位を更新する必要がない場合、不要なオーバーヘッドです。 それを無効にするオプションがあるべきですか? これは後で追加できますが、便利であることが判明した場合。

@karelz

安定性(同じ優先度の場合)-同じ優先度のアイテムは、キューに挿入された順序で返される必要があります(一般的なキューの動作)

これは、内部的には、優先度が(priority, version)ペアのようなものである必要があることを意味すると思います。ここで、 versionは追加ごとに増分されます。 特にversionが64ビットである必要があることを考えると、これは優先キューのメモリ使用量を大幅に膨らませると思います。 それが価値があるかどうかはわかりません。

重複する値を防止したくない(他のコレクションと同様):
Update削除します-ユーザーはRemoveを削除してから、異なる優先度でEnqueueアイテムを元に戻すことができます。

実装によっては、 Updateは、 Remove後にEnqueue続くよりもはるかに効率的である可能性があります。 たとえば、バイナリヒープ(クォータナリヒープは同じ時間計算量を持っていると思います)の場合、 Update (一意の値とディクショナリを使用)はO(log n )ですが、 Remove (重複した値を使用)辞書はありません)はO( n )です。

@pgolebiowski
ここでの提案に集中し続ける必要があることに完全に同意します。 オリジナルのAPI提案とオリジナルの投稿を行いました。 1月に、 @ karelzはいくつかの具体的な使用例を求めました。 これにより、PriorityQueueAPIの特定の必要性と使用法に関するより広範な質問が開かれました。 私は独自のPriorityQueue実装を持っていて、それをいくつかのプロジェクトで使用し、同様の何かがBCLで役立つと感じました。

元の投稿に欠けていたのは、既存のキューの使用状況に関する広範な調査でした。 実際の例は、設計を固定し、広く使用できるようにするのに役立ちます。

@karelz
Lucene.netには、少なくとも1つの優先度キュー比較関数が含まれています(比較と同様))複数のフィールドを評価して優先度を決定します。 Luceneの「暗黙的な」比較パターンは、現在のAPIプロポーザルの明示的なTPriorityパラメーターにうまくマッピングされません。 何らかのマッピングが必要になります。複数のフィールドを1つに結合するか、TPriorityとして渡すことができる「比較可能な」データ構造に結合します。

提案:
1)PriorityQueue上記の私の元の提案に基づくクラス(元の提案の見出しの下にリストされています)。 Update(T)、Remove(T)、およびContains(T)コンビニエンス関数を追加する可能性があります。 オープンソースコミュニティの既存の使用例の大部分に適合します。
2)PriorityQueuedotnet / corefx#1のバリアント。 Dequeue()とPeek()は、タプルではなくTElementを返します。 Try *機能はありません。Remove()は、リストに合わせてスローする代わりにブール値を返します。パターン。 「コンビニエンスタイプ」として機能するため、明示的な優先度の値を持つ開発者は、独自の同等のタイプを作成する必要はありません。

どちらのタイプも重複要素をサポートしています。 同じ優先度の要素に対してFIFOを保証するかどうかを決定する必要があります。 優先度の更新をサポートしている場合は、おそらくそうではありません。

@karelzが提案したように、CoreFxLabsで両方のバリアントを

質問:

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)

なぜ重複がないのですか?

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

これらの方法が望ましい場合は、拡張方法として使用しますか?

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

投げる必要があります。 しかし、なぜTryバリアントや拡張メソッドがないのですか?

構造体ベースの列挙子?

@benaadamsは、醜いハンドルタイプを避けるために、元々(私が)提案しました。 APIレビューからの最新の更新はそれを元に戻します。

タイプにメソッドを追加できる場合は、拡張メソッドとしてメソッドを追加しないでください。 拡張メソッドは、型に追加できない場合(たとえば、インターフェイスである場合、または.NET Frameworkに高速に配信したい場合)のバックアップです。

重複:同じエントリが複数ある場合は、1つだけ更新する必要がありますか? それから彼らは違うのですか?

投げ方:基本的にはラッパーであり、そうなるのでしょうか?

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

その制御は例外を介して流れますが?

@benaadamsはAPIレビューノートで私の返信をチェックします: https

  • 複製

    • Update削除します-ユーザーはRemoveから、異なる優先度でEnqueueアイテムを元に戻すことができます。

    • Removeは、最初に見つかったアイテムのみを削除する必要があります( List同様)。

その制御は例外を介して流れますが?

よく分からない。 BCLではかなり一般的なパターンのようです。

その制御は例外を介して流れますが?

よく分からない。

TryXメソッドがある場合

  • 結果が気になる場合; あなたはそれをチェックします
  • あなたが結果を気にしないならあなたはそれをチェックしません

例外の関与はありません。 しかし、その名前はあなたがリターンを捨てる決定をすることを奨励します。

試行しない例外スローメソッドがある場合

  • 結果が気になる場合; 事前にチェックするか、try / catchを使用して検出する必要があります
  • 結果を気にしない場合; メソッドを空にキャッチするか、予期しない例外を取得する必要があります

したがって、フロー制御に例外処理を使用するというアンチパターンになります。 それらは少し不必要に思えます。 falseの場合は、いつでもスローを追加して再作成できます。

BCLではかなり一般的なパターンのようです。

TryXメソッドは、元のBCLでは一般的なパターンではありませんでした。 並行型まで(2.0のDictionary.TryGetValueが最初の例だったと思いますか?)

たとえば、TryXメソッドはキューとスタックのコアに追加されたばかりで、まだフレームワークの一部ではありません。

安定

  • 安定性は無料ではありません。 これには、パフォーマンスとメモリのオーバーヘッドが伴います。 通常の使用では、優先キューの安定性は重要ではありません。
  • IComparerを公​​開します。 お客様が安定性を求めている場合は、簡単に自分で追加できます。 要素の「年齢」を保存し、比較中にそれを使用する通常のアプローチを使用して、実装の上にStablePriorityQueueを構築するの

IQueue

その場合、APIの競合が発生します。 ここで、既存のQueue<T>で機能するように、単純なIQueue<T>について考えてみましょう。

public interface IQueue<T> :
    IEnumerable,
    IEnumerable<T>,
    IReadOnlyCollection<T>
{
    void Enqueue(T element);

    T Peek();
    T Dequeue();

    bool TryPeek(out T element);
    bool TryDequeue(out T element);
}

優先キューには2つのオプションがあります。

  1. IQueue<TElement>実装します。
  2. IQueue<(TElement, TPriority)>実装します。

単純な数字(1)と(2)を使用して、両方のパスの意味を記述します。

エンキュー

優先度付きキューでは、要素とその優先度の両方をキューに入れる必要があります(現在のソリューション)。 通常のヒープでは、要素を1つだけ追加します。

  1. 最初のケースでは、 Enqueue(TElement)を公​​開します。これは、優先度なしで要素を挿入する場合、優先度付きキューではかなり奇妙です。 それから私達はすることを余儀なくされます...何ですか? default(TPriority)と仮定しますか? いや...
  2. 2番目のケースでは、 Enqueue((TElement, TPriority) element)を公​​開します。 基本的に、それらから作成されたタプルを受け入れることにより、2つの引数を追加します。 おそらく理想的なAPIではありません。

ピーク&デキュー

これらのメソッドがTElementのみを返すようにしたかったのです。

  1. あなたが達成したいことで動作します。
  2. 達成したいことでは機能しません— (TElement, TPriority)返します。

列挙する

  1. お客様は、要素の優先度を利用するLINQクエリを作成することはできません。 これは間違っています。
  2. できます。

PriorityQueue<T>提供することで、いくつかの問題を軽減できます。ここでは、物理的に個別の優先順位はありません。

  • ユーザーは基本的に、クラスを使用できるようにするために、コードのラッパークラスを作成する必要があります。 これは、多くの場合、 IComparableも実装したいということを意味します。 これにより、かなり定型的なコードがたくさん追加されます(そして、おそらくソースコードに新しいファイルが追加されます)。
  • 必要に応じて、明らかにこれについて再検討することができます。 また、以下に代替アプローチを提供します。

2つの優先キュー

2つの優先キューを提供すると、ソリューション全体がより強力で柔軟になります。 取るべきいくつかの注意事項があります:

  • 同じ機能を提供するために2つのクラスを公開しますが、さまざまなタイプの入力形式に対してのみです。 気分が良くない。
  • PriorityQueue<T>は、 IQueue<T>実装する可能性があります。
  • PriorityQueue<TElement, TPriority>は、 IQueueインターフェースを実装しようとすると、奇妙なAPIを公開します。

まあ…それうまくいく

更新と削除

私たちは重複を許可することを前提として考える、私たちは、ハンドルの概念を使用したくありません。

  • 要素の優先度を完全に正しく更新する機能を提供することは不可能です(特定のノードのみを更新します)。 同様に、要素の削除にも適用されます。
  • さらに、両方の操作をO(n)で実行する必要がありますが、これはO(log n)で両方を実行できるため、非常に悲しいことです。

これは基本的にJavaの機能につながります。ユーザーは、コレクション全体を毎回単純に繰り返すことなく、優先度キューの優先度を更新できるようにする場合、データ構造全体の独自の実装を提供する必要があります。

代替ハンドル

新しいハンドルタイプを追加したくないという前提で、要素の更新と削除を完全にサポートできるように(さらに効率的に)、別の方法で追加できます。 解決策は、別の方法を追加することです。

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

void Update(object handle, TPriority priority);

void Remove(object handle);

これらは、要素の更新と削除を適切に制御したい人のためのメソッドです(O(n)では、 List :stuck_out_tongue_winking_eye:のように実行しないでください)。

しかし、もっと良いのは、考えてみましょう...

代替アプローチ

または、この機能を優先キューから完全に削除して、代わりにヒープに追加することもでき

  • Heapを使用すると、より強力で効率的な操作を利用できます。
  • PriorityQueueを使用することで、シンプルなAPIと簡単な使用法を利用できます。コードを正しく機能させたい人向けです。
  • Javaの問題に遭遇することはありません。
  • PriorityQueue安定させることができます。これは、もはやトレードオフではありません。
  • この解決策は、コンピュータサイエンスのバックグラウンドが強い人は、 Heap存在を認識しているという感覚と調和しています。 また、 PriorityQueueの制限を認識しているため、代わりにHeap使用できます(たとえば、要素の更新/削除をより細かく制御したい場合や、データが必要ない場合など)速度とメモリ使用量を犠牲にして安定する構造)。
  • 優先度付きキューとヒープはどちらも、機能を損なうことなく、簡単に複製を許可できます(目的が異なるため)。
  • 共通のIQueueインターフェースを作成する方が簡単です。これは、パワーと機能がヒープフィールドに投入されるためです。 PriorityQueueのAPIは、抽象化によってQueueと互換性を持たせることに焦点を当てることができます。
  • IPriorityQueueインターフェイスを提供する必要はもうありません( PriorityQueueQueue同じように保つことに重点を置いています)。 代わりに、ヒープ領域に追加できます。基本的に、そこにIHeapがあります。 これは、標準ライブラリにあるものの上にサードパーティのライブラリを構築できるので素晴らしいです。 そして、それは正しいと感じます-繰り返しになりますが、優先キューよりも高度であると考えているため、拡張機能はその領域によって提供されます。 このような拡張機能は、個別であるため、 PriorityQueueで行う選択の影響も受けません。
  • PriorityQueueIHeapコンストラクターを考慮する必要PriorityQueueもうありません。
  • 優先キューは、CoreFXの内部で使用するのに役立つクラスです。 ただし、安定性などの機能を追加し、他の機能をいくつか削除すると、そのソリューションよりも強力なものが必要になる可能性があります。 幸いなことに、私たちのコマンドには、より強力でパフォーマンスの高いHeapがあります!

基本的に、優先キューは、パフォーマンス、パワー、および柔軟性を犠牲にして、主に使いやすさに焦点を合わせます。 ヒープは、優先度付きキューでの決定のパフォーマンスと機能への影響を認識している人向けです。 トレードオフに関する多くの問題を軽減します。

実験をするなら、今は可能だと思います。 コミュニティに聞いてみましょう。 誰もがこのスレッドを読むわけではありません。他の貴重なコメントや使用シナリオを生成する可能性があります。 どう思いますか? 個人的に、私はそのような解決策が大好きです。 みんなが幸せになると思います。

重要な注意:このようなアプローチが必要な場合は、優先キューとヒープを一緒に設計する必要があります。 それらの目的は異なり、一方のソリューションはもう一方のソリューションでは提供されないものを提供するためです。

その後、IQueueを配信します

上記のアプローチでは、優先度付きキューがIQueue<T>を実装するために(意味があるように)、セレクターのサポートをそこで削除すると仮定すると、1つの汎用タイプが必要になります。 それは、ユーザーが(user data, priority)別々に持っている場合、ラッパーを提供する必要があることを意味しますが、そのようなソリューションは依然として直感的です。 そして最も重要なことは、すべての入力形式を許可することです(そのため、セレクターをドロップした場合は、この方法で行う必要があります)。 セレクターがないと、 Enqueue(TElement, TPriority)はすでに比較可能なタイプを許可しません。 このメソッドをIQueue<T>含めることができるように、単一のジェネリック型も列挙に不可欠です。

その他

@svick

列挙時に返される要素はどのような順序で定義されていますか? 私の推測では、それを効率的にするために、それは未定義です。

列挙しながら順序を設定するには、基本的にコレクションを並べ替える必要があります。 そういうわけで、はい、それは未定義でなければなりません。顧客は自分でOrderBy実行して、ほぼ同じパフォーマンスを達成できるからです(ただし、それを必要としない人もいます)。

アイデア:優先キューでは、これは順序付けられ、ヒープでは順序付けられません。 気分が良くなります。 どういうわけか、優先キューは順番に繰り返すように感じます。 ヒープは絶対にありません。 上記のアプローチのもう1つの利点。

@pgolebiowski
それはすべて非常に正気のようです。 Delivering IQueue thenセクションで、重複要素が許可されているセレクターの代わりに、「1つのジェネリック型」にT : IComparable<T>を提案していることを明確にしていただけませんか。

私は2つの別々のタイプを持つことをサポートします。

ハンドルタイプとしてobjectを使用する理由がわかりません。これは、新しいタイプの作成を回避するためだけですか? 新しいタイプを定義すると、実装のオーバーヘッドが最小限に抑えられ、APIの誤用が難しくなり( stringRemove(object)に渡そうとするのを妨げているのは何ですか?)、使いやすくなります(私が試しているのを止めているのは何ですか?) Element自体をRemove(object)に渡すために、そして誰が私を試みたと非難することができますか?)

より表現力豊かなインターフェースのために、handleメソッドのobjectを置き換えるために、適切な名前のダミータイプを追加することを提案します。

デバッグ可能性がメモリオーバーヘッドよりも優先される場合、ハンドルタイプには、それが属するキューに関する情報を含めることもできます(Genericになる必要があります。これにより、インターフェイスのタイプの安全性が向上します)。別のキューによって」)、またはそれがすでに消費されているかどうか(「ハンドルによって参照される要素はすでに削除されています」)。

ハンドルのアイデアが進んだ場合、この情報が有用であると見なされた場合、これらの例外のサブセットも、対応するTryRemoveおよびTryUpdateメソッドによってスローされることを提案します。ただし、要素がデキューされたか、ハンドルによって削除されたため、要素は存在しなくなりました。 これには、退屈ではなく、一般的で、適切な名前のハンドルタイプが必要になります。

@VisualMelon

Delivering IQueue thenセクションで、重複要素が許可されているセレクターの代わりに、「1つのジェネリック型」にT : IComparable<T>を提案していることを明確にしていただけませんか。

これを明確にしないことをお詫びします。

  • T制約がなく、 PriorityQueue<T>を配信することを意味しました。
  • それでもIComparer<T>ます。
  • Tがすでに比較可能である場合は、単にComparer<T>.Defaultが想定されます(引数なしで優先度付きキューのデフォルトコンストラクターを呼び出すことができます)。
  • セレクターの目的は異なります。つまり、すべてのタイプのユーザーデータを使用できるようにすることです。 いくつかの構成があります。

    1. ユーザーデータ優先度(2つの物理インスタンス)
    2. ユーザーデータは優先度
    3. ユーザーデータ優先されます。
    4. まれなケース:優先順位は、他のロジック(ユーザーデータとは異なるオブジェクトに存在する)を介して取得できます。

    まれなケースはPriorityQueue<T>では不可能ですが、それはそれほど重要ではありません。 重要なのは、(1)、(2)、(3)を処理できるようになったことです。 ただし、2つのジェネリック型がある場合は、 Enqueue(TElement, TPrioriity)ようなメソッドが必要になります。 それは私たちを(1)だけに制限するでしょう。 (2)冗長性につながります。 (3)信じられないほど醜いでしょう。 これについては、上記のIQueue > Enqueueセクション(2番目のEnqueueメソッドとdefault(TPriority )にもう少しあります。

今はもっとはっきりしているといいのですが。

ところで、そのようなソリューションを想定すると、 PriorityQueue<T>IQueue<T>のAPIを設計するのは簡単です。 Queue<T>いくつかのメソッドを取得し、それらをIQueue<T>にスローして、 PriorityQueue<T>実装させます。 タダー! 😄

ハンドルタイプとしてobjectを使用する理由がわかりません。これは、新しいタイプの作成を回避するためだけですか?

  • はい、その通りです。 そのようなタイプを公開したくないという仮定を考えると、ハンドルの概念を使用する唯一の解決策です(したがって、より強力で速度があります)。 それは私たちが現在のアプローチに固執し、(私は反対です)より多くの電力&効率を持つようにしたい場合に発生しなければならないものを明確化は、むしろだった-私はかかわらず、それは理想的ではありません同意します。
  • 別のアプローチ(優先度キューとヒープを分離する)を使用することを考えると、これは簡単です。 PriorityQueue<T>System.Collections.Genericに常駐させ、ヒープ機能をSystem.Collections.Specializedます。 そこでは、そのようなタイプを導入する可能性が高くなり、最終的には素晴らしいコンパイル時エラー検出が可能になります。
  • しかし、繰り返しになりますが、このようなアプローチが必要な場合は、優先キューヒープの機能を一緒に設計することが非常に重要です。 1つのソリューションが他のソリューションが提供しないものを提供するためです。

デバッグ可能性がメモリオーバーヘッドよりも優先される場合、ハンドルタイプには、それが属するキューに関する情報を含めることもできます(Genericになる必要があります。これにより、インターフェイスのタイプの安全性が向上します)。別のキューによって」)、またはそれがすでに消費されているかどうか(「ハンドルによって参照される要素はすでに削除されています」)。

ハンドルのアイデアが進んだら、この情報が役立つと思われる場合は提案します...

間違いなくもっと可能です:ウィンク:。 特に、サードパーティのライブラリで私たちのコミュニティによってそれらすべてを拡張するために。

@karelz @safern @ianhays @terrajobst @bendono @svick @ alexey-dvortsov @SamuelEnglard @ xied75など-このようなアプローチは理にかなっていると思いますか(これこの投稿で説明されています)? それはあなたの期待とニーズのすべてを満たしますか?

2つのクラスを作成するというアイデアは非常に理にかなっており、多くの問題を解決すると思います。 私が持っているのは、 PriorityQueue<T>が内部でHeap<T>を使用し、その高度な機能を「非表示」にすることが理にかなっているということだけです。

だから基本的に...

IQueue<T>

public interface IQueue<T> :
    IEnumerable,
    IEnumerable<T>,
    IReadOnlyCollection<T>
{
    int Count { get; }

    void Clear();
    bool Contains(T element);
    bool IsEmpty();

    void Enqueue(T element);

    T Peek();
    T Dequeue();

    bool TryPeek(out T element);
    bool TryDequeue(out T element);
}

ノート

  • System.Collections.Generic
  • アイデア:ここに要素を削除するためのメソッドを追加することもできます( RemoveおよびTryRemove )。 ただし、 Queue<T>には含まれていません。 しかし、それは必要ではありません。

PriorityQueue<T>

public class PriorityQueue<T> : IQueue<T>
{
    public PriorityQueue();
    public PriorityQueue(IComparer<T> comparer);
    public PriorityQueue(IEnumerable<T> collection);
    public PriorityQueue(IEnumerable<T> collection, IComparer<T> comparer);

    public IComparer<T> Comparer { get; }
    public int Count { get; }

    public bool IsEmpty();
    public void Clear();
    public bool Contains(T element);

    public void Enqueue(T element);

    public T Peek();
    public T Dequeue();

    public bool TryPeek(out T element);
    public bool TryDequeue(out T element);

    public void Remove(T element);
    public bool TryRemove(T element);

    public IEnumerator<T> GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator();
}

ノート

  • System.Collections.Generic
  • IComparer<T>が配信されない場合、 Comparer<T>.Defaultが呼び出されます。
  • 安定しています。
  • 複製を許可します。
  • RemoveTryRemoveは、最初に発生したものだけを削除します(見つかった場合)。
  • 列挙が順序付けられます。

ヒープ

今ここにすべてを書いているわけではありませんが、:

  • System.Collections.Specialized
  • それは安定していません(そしてそれ自体、メモリの点でより速くそしてより効率的です)。
  • ハンドルのサポート、適切な更新と削除

    • O(n)ではなくO(log n)ですばやく実行

    • 正しく行われた

  • 列挙は順序付けられていません(高速)。
  • 複製を許可します。

IQueueに同意する提案。 私は今日同じことを売り込むつもりでした、それはインターフェースレベルで持つのに適切なレベルの抽象化のように感じます。 「アイテムを追加したり、注文したアイテムを削除したりできるデータ構造へのインターフェース。」

  • IQueueの仕様いいね。

  • 「intCount {get;}」をIQueueに追加することを検討してくださいIReadOnlyCollectionから継承するかどうかに関係なく、Countが必要であることは明らかです。

  • TryPeekに関するフェンスで、IQueueのTryDequeueそれらがキューにないことを考慮して、ただし、これらのヘルパーはおそらくキューとスタックにも追加する必要があります。

  • IsEmptyは外れ値のようです。 BCLの他の多くのコレクションタイプにはありません。 それをインターフェースに追加するには、キューに追加されると想定する必要があります、そしてそれをキューに追加するのは少し奇妙に思えます何もありません。 インターフェイスから、そしておそらくクラスからも削除することをお勧めします。

  • TryRemoveをドロップし、Removeを「boolRemove」に変更します。 ここでは、他のコレクションクラスとの整合性を保つことが重要になります。開発者は、「コレクション内のremove()がスローされない」という多くの筋肉の記憶を持っています。 これは多くの開発者がうまくテストできない領域であり、通常の動作が変更されると多くの驚きを引き起こします。

以前の引用から@pgolebiowski

  1. ユーザーデータは優先度(2つの物理インスタンス)とは別のものです。
  2. ユーザーデータには優先度が含まれています。
  3. ユーザーデータが優先されます。
  4. まれなケース:優先順位は、他のロジック(ユーザーデータとは異なるオブジェクトに存在する)を介して取得できます。

また、5を検討することをお勧めします。ユーザーデータには、複数のフィールドの優先度が含まれています(Lucene.netで見たように)

TryPeekに関するフェンスで、キューにないことを考慮してIQueueのTryDequeue

それらはSystem / Collections / Generic / Queue.cs#L253-L295です。

一方、キューにはありません
c# public void Remove(T element); public bool TryRemove(T element);

IReadOnlyCollectionから継承するかどうかに関係なく、Countが必要であることを明確にするために、「int Count {get;}」をIQueueに追加することを検討してください。

わかった。 それを変更します。

IsEmptyは外れ値のようです。 BCLの他の多くのコレクションタイプにはありません。

これは@karelzによって追加された

TryRemoveをドロップし、Removeを「boolRemove」に変更します。

RemoveTryRemoveは、そのような他の方法( PeekTryPeekまたはDequeueTryDequeueと一致していると思います)。

  1. ユーザーデータには、複数のフィールドの優先度が含まれています

これは有効なポイントですが、実際にはセレクターを使用して処理することもできます(結局のところ、これは任意の関数です)。ただし、これはヒープ用です。

IsEmptyは外れ値のようです。 BCLの他の多くのコレクションタイプにはありません。

FWIW、 bool IsEmpty { get; }IProducerConsumerCollection<T>に追加したいもので、それ以来何度もそこになかったことを後悔しています。 これがないと、ラッパーはCount == 0と同等の処理を行う必要があります。これは、一部のコレクションでは、特にほとんどの並行コレクションで実装するのに大幅に効率が低下します。

@pgolebiowski現在のAPIコントラクトをホストするgithubリポジトリと、設計の根拠を含む1つまたは2つの.mdファイルを作成するというアイデアについてどう思いますか。 それが安定したら、準備ができたらCoreFxLabsまでPRを行う前に、最初の実装を構築する場所として使用できますか?

@svick

PeekDequeueが優先度にoutパラメーターを使用した場合、優先度をまったく返さないオーバーロードが発生する可能性もあります。これにより、一般的な使用法が簡素化されると思います。

同意しました。 それらを追加しましょう。

優先度を無視して、優先キュー内のアイテムだけを列挙する方法があるはずですか?

良い質問。 何を提案しますか? IEnumerable<TElement> Elements { get; }

安定性-これは、内部的には、優先順位が(priority, version)ペアのようなものでなければならないことを意味すると思います

Updateを論理的なRemove + Enqueueと見なすことで、これを回避できると思います。 常に同じ優先度のアイテムの最後にアイテムを追加します(基本的に、比較結果0を-1と見なします)。 動作するはずのIMO。


@benaadams

TryXメソッドは、元のBCLでは一般的なパターンではありませんでした。 並行型まで(2.0のDictionary.TryGetValueが最初の例だったと思いますか?)
たとえば、TryXメソッドはキューとスタックのコアに追加されたばかりで、まだフレームワークの一部ではありません。

私はまだBCLに慣れていないことを認めます。 APIレビューミーティングから、そして最近私たちがたくさんのTry*メソッドを追加したという事実から、私はそれがずっと長い間一般的なパターンであるという印象を受けました😉。
いずれにせよ、それは現在一般的なパターンであり、私たちはそれを使用することを恐れてはなりません。 このパターンがまだ.NETFrameworkに含まれていないという事実は、.NET Coreでの革新を妨げるものではありません。これが主な目的であり、より迅速に革新することです。


@pgolebiowski

または、この機能を優先キューから完全に削除して、代わりにヒープに追加することもできます。 これには多くの利点があります

うーん、何かがあなたがここに議題があるかもしれないと私に言います😆
今、真剣に、それは私たちがずっと目指していた実際には良い方向です- PriorityQueueHeapをしない理由になることを決して意図していませんHeapがCoreFXに組み込まれず、PowerCollectionsと並んで高度なデータ構造としてCoreFXExtensionsリポジトリに「そのまま」留まる可能性があるという事実に問題がなければ、私はそれで問題ありません。

重要な注意:このようなアプローチが必要な場合は、優先キューとヒープを一緒に設計する必要があります。 それらの目的は異なり、一方のソリューションはもう一方のソリューションでは提供されないものを提供するためです。

なぜ一緒にやる必要があるのか​​わかりません。 IMOは、 PriorityQueue焦点を合わせ、「適切な」 Heapを並列/後で追加できます。 誰かが一緒にやってもかまいませんが、使いやすいPriorityQueue存在が、「適切で高性能な」高度なHeap設計に影響を与える理由はわかりません。

IQueue

書いてくれてありがとう。 あなたのポイントを考えると、私たちはIQueueを追加するために私たちの邪魔をするべきではないと思います。 IMOそれは持っていてうれしいです。 セレクターがあれば当然です。 ただし、セレクターがPriorityQueueによって呼び出されるタイミングを説明する際に奇妙さと複雑さをもたらすため、セレクターのアプローチは好きではありません( EnqueueUpdate

代替(オブジェクト)ハンドル

このようなオーバーロードIMOを使用することは、実際には(少し醜いですが)悪い考えではありません。 ハンドルが間違ったPriorityQueue 、つまりO(log n)からのものであることを検出できる必要があります。
APIレビューアはそれを拒否するだろうと感じていますが、IMOはそれを試してみる価値があります...

安定

安定性にはパフォーマンス/メモリのオーバーヘッドが伴うとは思いません(すでにUpdateを削除したか、 Updateを論理Remove + Enqueueとして扱うと仮定します)。したがって、基本的に要素の年齢をリセットします)。 比較結果0を-1として扱うだけで、すべてが良好です...または、何かが足りませんか?

セレクターとIQueue<T>

2つの提案をするのは良い考えかもしれません(そして私たちは潜在的に両方を取るかもしれません):

  • PriorityQueue<T,U>セレクターなし、 IQueue (面倒です)
  • PriorityQueue<T>セレクターとIQueue

かなりの数の人々が上記のことをほのめかしました。

Re: @pgolebiowskiからの最新の提案 //github.com/dotnet/corefx/issues/574#issuecomment -308427321

IQueue<T> - RemoveTryRemove追加できますが、 Queue<T>はありません。

それらをQueue<T>追加するのは理にかなっていますか? ( @ebickleは同意します)はいの場合、追加もバンドルする必要があります。
それをインターフェースに追加して、疑わしい/ Queue<T>追加も必要であることを伝えましょう。
IsEmptyについても同じです- Queue<T>Stack<T>追加が必要なものは何でも、インターフェースでそのようにマークしましょう(レビューとダイジェストが簡単になります)。
@pgolebiowski IQueue<T> 、実装されると思われるクラスのリストとともにコメントを追加していただけますか?

PriorityQueue<T>

構造体列挙子を追加しましょう(最近は一般的なBCLパターンだと思います)。 それはスレッド上で数回呼び出され、その後ドロップ/忘れられました。

ヒープ

名前空間の選択:名前空間の決定にまだ時間を無駄にしないようにしましょう。 HeapがCoreFxExtensionsになってしまう場合、そこで許可する名前空間の種類はまだわかりません。 たぶんCommunity.*かそのようなもの。 CoreFxExtensionsの目的/操作モードの議論の結果によって異なります。
注:CoreFxExtensionsリポジトリのアイデアの1つは、実績のあるコミュニティメンバーに書き込み権限を付与し、主に.NETチームがアドバイス(APIレビューの専門知識を含む)を提供し、.NETチーム/ MSが必要に応じてアービターとなるコミュニティによって推進されるようにすることです。 それが私たちが着陸する場所である場合、 System.*またはMicrosoft.*名前空間にそれを望まない可能性があります。 (免責事項:早い段階で考えてください。まだ結論にジャンプしないでください。他にも代替のガバナンスのアイデアがあります)

TryRemoveをドロップし、 Removebool Removeます。 ここでは、他のコレクションクラスとの整合性を保つことが重要になります。開発者は、「コレクション内のremove()がスローされない」という多くの筋肉の記憶を持っています。 これは多くの開発者がうまくテストできない領域であり、通常の動作が変更されると多くの驚きを引き起こします。

👍少なくとも他のコレクションとの調整を検討する必要があります。 誰かが他のコレクションをスキャンして、 Removeパターンを教えてもらえますか?

@ebickle現在のAPIコントラクトをホストするgithubリポジトリと、設計の根拠を含む1つまたは2つの.mdファイルを作成するというアイデアについてどう思いますか。

CoreFxLabsで直接ホストしましょう。 👍

@karelz

優先度を無視して、優先キュー内のアイテムだけを列挙する方法があるはずですか?

良い質問。 何を提案しますか? IEnumerable<TElement> Elements { get; }

ええ、それか、ある種のElementsCollection返すプロパティのどちらかがおそらく最良のオプションです。 特にDictionary<K, V>.Values似ているので。

常に同じ優先度のアイテムの最後にアイテムを追加します(基本的に、比較結果0を-1と見なします)。 動作するはずのIMO。

それはヒープがどのように機能するかではなく、「同じ優先順位のアイテムの終わり」はありません。 同じ優先度のアイテムは、ヒープ全体に分散できます(現在の最小値に近い場合を除く)。

または、別の言い方をすれば、安定性を維持するために、挿入時だけでなく、後でアイテムがヒープ内を移動するときも、0の比較結果を-1と見なす必要があります。 そして、私はあなたが店にのようなものがあると思うversion一緒にpriority適切にそれを行います。

IQueue--RemoveとTryRemoveを追加できますが、キューに入れますそれらを持っていません。

それらをキューに追加するのは理にかなっていますか? ( @ebickleは同意します)はいの場合、追加もバンドルする必要があります。

RemoveIQueue<T>追加するべきではないと思います; そして私はContainsが危険だとさえ提案したいと思います。 NotSupportedExceptionsもスローし始めない限り、インターフェイスの有用性と、インターフェイスを使用できるキューのタイプが制限されます。 つまり、スコープは何ですか?

それは、バニラキュー、メッセージキュー、分散キュー、ServiceBusキュー、Azure Storageキュー、ServiceFabric Reliableキューなどのためだけのものですか...(非同期メソッドを好むそれらのいくつかに光沢があります)

それはヒープがどのように機能するかではなく、「同じ優先順位のアイテムの終わり」はありません。 同じ優先度のアイテムは、ヒープ全体に分散できます(現在の最小値に近い場合を除く)。

フェアポイント。 私はそれを二分探索木として考えていました。 私が推測する私のdsの基本を磨く必要があります:)
さて、私たちはそれを異なるds(配列、バイナリ検索ツリー(または正式な名前が何であれ)で実装します- versionまたはageフィールドを維持することは、安定性を保証する価値があるとは思いません。

@ebickle現在のAPIコントラクトをホストするgithubリポジトリと、設計の根拠を含む1つまたは2つの.mdファイルを作成するというアイデアについてどう思いますか。

@karelzCoreFxLabsで直接ホストしましょう。

このドキュメントをご覧ください。

2つの提案をするのは良い考えかもしれません(そして私たちは潜在的に両方を取るかもしれません):

  • PriorityQueueセレクターなし、IQueueなし(面倒です)
  • PriorityQueueセレクターとIQueue付き

私はセレクターを完全に取り除きました。 1つの統合されたPriorityQueue<T, U>が必要な場合にのみ必要です。 2つのクラスがある場合、まあ、比較子で十分です。


追加/変更/削除する価値のあるものがあれば教えてください。

@pgolebiowski

ドキュメントはよさそうだ。 .NETコアライブラリで見られると期待する固溶体のように感じ始めています:)

  • ネストされた列挙子クラスを追加する必要があります。 状況が変わったかどうかはわかりませんが、数年前、子構造を持つことで、GetEnumerator()の結果をボックス化することによって引き起こされるガベージコレクターの割り当てを回避しました。 たとえば、 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(); } }

  • PriorityQueueネストされた列挙子構造体も必要です。

  • TryRemoveなしの「publicboolRemove(T element)」は長年のパターンであり、変更すると開発者のミスが発生する可能性が高いため、私はまだ投票しています。 API Reviewのクルーにチャイムを鳴らすことはできますが、それは私の頭の中で未解決の質問です。

  • コンストラクターで初期容量を指定したり、TrimExcess関数を使用したりすることに価値はありますか、それとも現在のマイクロ最適化です-特にIEnumerableを考慮してコンストラクターパラメーター?

@pgolebiowskiドキュメントに入れてくれてありがとう。 CoreFxLabマスターブランチに対するPRとしてドキュメントを送信していただけますか? @ianhays @safernにタグを付けると、PRをマージできるようになります。
次に、CoreFxLabの問題を使用して、特定の設計ポイントに関する詳細な議論を行うことができます。この大規模な問題よりも簡単なはずです(ここで、area-PriorityQueueを作成するために電子メールを開始しました)。

CoreFxLabプルリクエストへのリンクをここに入力してください。

  • @ebickle @ jnm2-追加
  • @ karelz @ SamuelEnglard-参照

APIが現在の形式(2つのクラス)で承認されている場合は、4次ヒープを使用して両方の実装を含むCoreFXLabへの別のPRを作成します(実装を参照)。 PriorityQueue<T>は下の1つの配列を使用するだけですが、 PriorityQueue<TElement, TPriority>は2つの配列を使用し、それらの要素を一緒に再配置します。 これにより、追加の割り当てを節約し、可能な限り効率的にすることができます。 緑色のライトが表示されたら、それを追加します。

ConcurrentQueueのようなスレッドセーフバージョンを作成する計画はありますか?

私は:heart:これの並行バージョンを見て、 IProducerConsumerCollection<T>実装しているので、 BlockingCollection<T>などで使用できます。

@aobatact @khellangまったく別のスレッドのように聞こえます:

私はそれが非常に価値があることに同意します:wink:!

public bool IsEmpty();

なぜこれが方法なのですか? IsEmpty持つフレームワーク上の他のすべてのコレクションには、プロパティとしてそれがあります。

トップポストの提案をIsEmpty { get; }更新しました。
また、corefxlabリポジトリに最新の提案ドキュメントへのリンクを追加しました。

こんにちは、みんな、
できればアップデートをサポートしたほうがいいとの声も多いと思います。 グラフ検索に便利な機能だと誰もが疑うことはなかったと思います。

しかし、あちこちでいくつかの反対意見が出されました。 だから私は私の理解を確認することができますそれは主な反対意見は次のように思われます:
-重複する要素がある場合に更新がどのように機能するかは明確ではありません。
-優先キューでサポートするために重複する要素が望ましいかどうかについては、いくつかの議論があります
-優先度を更新するためだけに、バッキングデータ構造内の要素を検索するための追加のルックアップデータ構造を用意するのは非効率的である可能性があるという懸念があります。 特に更新が実行されないシナリオの場合! および/または最悪の場合のパフォーマンス保証に影響を与えます...

私が逃したものはありますか?

OKもう1つは、意味的に「updateFirst」または「removeFirst」を意味するupdate / removeがおかしいかもしれないと主張されていると思います。 :)

PriorityQueueの使用を開始できる.NetCoreのバージョンはどれですか。

@memoryfraction .NET Core自体にはまだPriorityQueueがありません(提案はありますが、最近はそれに費やす時間がありませんでした)。 ただし、Microsoft .NETCoreディストリビューションに含める必要がある理由はありません。 コミュニティの誰もがNuGetに1つ置くことができます。 おそらく、この問題に関する誰かが提案することができます。

@memoryfraction .NET Core自体にはまだPriorityQueueがありません(提案はありますが、最近はそれに費やす時間がありませんでした)。 ただし、Microsoft .NETCoreディストリビューションに含める必要がある理由はありません。 コミュニティの誰もがNuGetに1つ置くことができます。 おそらく、この問題に関する誰かが提案することができます。

あなたの返事と提案をありがとう。
C#を使用してleetcodeを練習し、SortedSetを使用して重複する要素を持つセットを処理する必要がある場合。 この問題を解決するには、要素を一意のIDでラップする必要があります。
そのため、将来的には.NET Coreに優先度付きキューを配置する方が便利なので、優先度付きキューを使用することをお勧めします。

この問題の現状はどうなっていますか? 最近、PriorityQueueが必要になっていることに気づきました

この提案では、デフォルトでコレクションの初期化は有効になっていません。 PriorityQueue<T>Addメソッドがありません。 コレクションの初期化は、重複するメソッドの追加を保証するのに十分な大きさの言語機能だと思います。

誰かが共有したい高速のバイナリヒープを持っている場合は、それを私が書いたものと比較したいと思います。
提案どおりにAPIを完全に実装しました。 ただし、ヒープではなく、単純なソート済み配列を使用します。 最も価値の低いアイテムを最後に保持するので、通常の順序とは逆にソートされます。 アイテムの数が32未満の場合、単純な線形検索を使用して、新しい値を挿入する場所を見つけます。 その後、二分探索を使用しました。 ランダムデータを使用すると、キューを埋めてから排出するという単純なケースでは、このアプローチがバイナリヒープよりも高速であることがわかりました。
時間があれば、公開のgitリポジトリに入れて、人々がそれを批評し、このコメントを場所で更新できるようにします。

@SunnyWar現在使用しています: https
パフォーマンスが良いです。 そこでGenericPriorityQueueに対して実装をテストすることは理にかなっていると思います

私の以前の投稿は無視してください。 テストでエラーが見つかりました。 修正されると、バイナリヒープは最高のパフォーマンスを発揮します。

@karelzこの提案は現在ICollection<T>どこにありますか? コレクションが明らかに読み取り専用ではないのに、なぜそれがサポートされないのかわかりませんでしたか?

@karelzは、デキュー順序の未解決の質問です。最も低い優先度の値を優先して最初に削除されると主張します。 これは最短パスアルゴリズムに便利であり、タイマーキューの実装にも自然です。 そして、それらは私がそれが使用されると思う主な2つのシナリオです。 「高い」優先順位と「高い」数値に関連する言語学的なものでない限り、反対の議論が何であるかを私は本当に知りません。

@karelz列挙順序を再...どのように持っていることについてSort()インプレースソート順に、コレクションの内部データヒープを置くタイプの方法(N Oで(Nログ))。 そして呼び出すEnumerate()した後、 Sort() O(n)の中でコレクションを列挙します。 そして、Sort()が呼び出されない場合、O(n)でソートされていない順序で要素を返しますか?

トリアージ:これを実装する必要があるかどうかについてのコンセンサスがないように思われるため、将来に移行します。

それを聞いて非常に残念です。 私はまだこれのためにサードパーティのソリューションを使用する必要があります。

サードパーティのソリューションを使用したくない主な理由は何ですか?

ヒープデータ構造は、leetcodeを実行するために必須です
より多くのleetcode、より多くのc#コードインタビュー、つまりより多くのc#開発者。
より多くの開発者はより良いエコシステムを意味します。
より良いエコシステムとは、明日もc#でプログラムできるかどうかを意味します。

要約すると、これは機能であるだけでなく、将来でもあります。 そのため、問題には「将来」というラベルが付けられています。

サードパーティのソリューションを使用したくない主な理由は何ですか?

標準がない場合、誰もが独自の癖を発明します。

私が取り組んでいることに関連しているので、 System.Collections.Generic.PriorityQueue<T>を望んでいたことが何度もありました。 この提案は5年前からあります。 なぜそれはまだ起こらなかったのですか?

なぜそれはまだ起こらなかったのですか?

優先的な飢餓、しゃれは意図されていません。 コレクションをBCLに配置する場合、コレクションを交換する方法、実装するインターフェイス、セマンティクスなどを考慮する必要があるため、コレクションの設計は機能します。これはロケット科学ではありませんが、時間がかかります。 さらに、デザインを判断するための具体的なユーザーケースと顧客のセットが必要です。これは、コレクションがますます専門化されるにつれて難しくなります。 これまでのところ、それよりも重要であると考えられていた他の作業が常にありました。

最近の例を見てみましょう:不変のコレクション。 それらは、不変性に関するVSのユースケースのためにBCLチームで働いていない誰かによって設計されました。 私たちは彼と協力してAPIを「BCL化」しました。 そして、Roslynがオンラインになったとき、私たちはできるだけ多くの場所で彼らのコピーを私たちのものに置き換え、彼らのフィードバックに基づいて設計(および実装)を大幅に調整しました。 「ヒーロー」シナリオがなければ、これは難しいことです。

標準がない場合、誰もが独自の癖を発明します。

@masonwheelerはこれがPriorityQueue<T>見たものですか? 互換性がなく、明確に受け入れられている「ほとんどの人にとって最良のライブラリ」ではないサードパーティのオプションがいくつかあるということですか? (私は調査を行っていないので、答えはわかりません)

@eiriktsarpalis実装するかどうかについて、どのようにコンセンサスがありませんか?

@terrajobstこれがよく知られたアプリケーションでよく知られているパターンである場合、本当にヒーローシナリオが必要ですか? ここではイノベーションはほとんど必要ありません。 すでにかなり完全に開発されたインターフェース仕様さえあります。 私の意見では、このユーティリティが存在しない本当の理由は、それが難しすぎるということでも、需要もユースケースもないということでもありません。 本当の理由は、実際のソフトウェアプロジェクトでは、フレームワークライブラリに配置するための政治キャンペーンを行うよりも、自分でプロジェクトを作成する方が簡単だからです。

サードパーティのソリューションを使用したくない主な理由は何ですか?

@terrajobst
私は個人的に、サードパーティのソリューションが常に期待どおりに機能するとは限らない、または現在の言語機能を使用しないという経験をしました。 標準化されたバージョンi(ユーザーとして)を使用すると、パフォーマンスが最高のものになることを確信できます。

@danmosemsft

@masonwheelerはこれがPriorityQueue<T>見たものですか? 互換性がなく、明確に受け入れられている「ほとんどの人にとって最良のライブラリ」ではないサードパーティのオプションがいくつかあるということですか? (私は調査を行っていないので、答えはわかりません)

はい。 「C#優先キュー」をグーグルで検索してください。 最初のページはいっぱいです:

  1. Githubおよびその他のホスティングサイトでの優先キューの実装
  2. なぜ世界にCollections.Genericに公式の優先キューがないのかと尋ねる人々
  3. 独自の優先度キューの実装を構築する方法に関するチュートリアル

@terrajobstこれがよく知られたアプリケーションでよく知られているパターンである場合、本当にヒーローシナリオが必要ですか? ここではイノベーションはほとんど必要ありません。

私の経験ではそうです。 悪魔は細部にあり、APIを出荷した後は、実際に重大な変更を加えることはできません。 そして、ユーザーに見える実装の選択肢はたくさんあります。 APIをOOBとしてしばらくプレビューできますが、フィードバックを確実に収集することはできますが、ヒーローシナリオがないということは、タイブレーカーがないことを意味し、ヒーローがシナリオが到着しましたが、データ構造がその要件を満たしていません。

@masonwheelerあなたのリンクはこれにあると思いました😄今それは私の頭の中にあります。

@terrajobstが言うように、ここでの私たちの主な問題はリソース/注意です(そしてあなたが望むならそれについて私たちを責めることができます)私たちはまた、Microsoft以外のエコシステムを強化して、どんなものでも強力なライブラリを見つけることができるようにしたいと思いますシナリオ。

[わかりやすくするために編集]

@danmosemsftいや、その曲を選ぶつもりなら、シュレック2バージョンをやります。

ヒーローアプリ候補#1:TimerQueue.Portableで使用するのはどうですか?

すでに検討され、プロトタイプが作成され、破棄されています。 これにより、タイマーがすばやく作成および破棄される(タイムアウトなど)という非常に一般的なケースの効率が低下します。

@stephentoubタイマーの数が少ないシナリオでは、効率が低下すると思います。 しかし、それはどのようにスケーリングしますか?

タイマーの数が少ないシナリオでは、効率が低下することを意味していると思います。 しかし、それはどのようにスケーリングしますか?

いいえ、一般的なケースは、いつでもたくさんのタイマーがありますが、起動しているタイマーはごくわずかであるということです。 これは、タイムアウトにタイマーを使用した場合の結果です。 そして重要なことは、データ構造に追加したりデータ構造から削除したりできる速度です...それを0(1)にし、オーバーヘッドを非常に低くする必要があります。 それがO(log N)になると、問題になります。

優先キューがあると、C#は間違いなく面接に適したものになります。

優先キューがあると、C#は間違いなく面接に適したものになります。

はい、本当です。
同じ理由で探しています。

@stephentoub実際には発生しないタイムアウトについては、それは私にとって完全に理にかなっています。 しかし、突然多くのタイムアウトが発生し始めた場合、突然パケット損失や応答しないサーバーなどが発生したため、システムはどうなるのでしょうか。 また、System.Timerの定期タイマーも同じ実装を使用していますか? そこでは、タイムアウトの期限が切れるのが「ハッピーパス」になります。

しかし、突然多くのタイムアウトが発生し始めると、システムはどうなるのだろうか。

それを試してみてください。 :)

以前の実装では、実際のワークロードがかなり苦しんでいました。 私たちが調べたすべての場合において、新しいもの(優先キューを使用せず、代わりにすぐにタイマーとすぐにタイマーを分割するだけです)は問題を修正しました、そして私たちは新しいものを見ていませんそれ。

また、System.Timerの定期タイマーも同じ実装を使用していますか?

はい。 しかし、それらは一般に、実際のアプリケーションではかなり均等に分散しています。

5年後、まだPriorityQueueはありません。

Rxには、本番環境でテスト済みの優先キュークラスがあります。

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

image

5年後、まだPriorityQueueはありません。

十分に高い優先度であってはなりません...

それらはレポレイアウトを中心に変更されました。 新しい場所はhttps://github.com/dotnet/reactive/blob/master/Rx.NET/Source/src/System.Reactive/Internal/PriorityQueue.csです

@stephentoubですが、実際には@eiriktsarpalisでしょうか? 完成したAPIデザインがあれば、喜んで取り組んでいきます

これが解決されたという宣言はまだ見ていません。指定されたキラーアプリなしで最終的なAPIデザインが存在するかどうかはわかりません。 しかし...
キラーアプリのトップ候補がプログラミングコンテスト/教育/インタビューであると仮定すると、トップのエリックのデザインは十分に役立つように見えると思います...そして私はまだ反対提案を抱えています(新しく改訂されましたが、まだ撤回されていません!)

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

私がそれらの間で気づいている主な違いは、それが辞書のように話そうとするべきかどうか、そしてセレクターが物であるべきかどうかです。

なぜ辞書にしたかったのか、正確に忘れ始めています。 優先順位を更新するためのインデックス付きアサイナがあり、優先順位を使用してキーを列挙できることは、私には良いことであり、辞書のように思えました。 デバッガーで辞書として視覚化できるのもいいかもしれません。

セレクターは、実際には、期待されるクラスインターフェイスであるFWIWを大幅に変更すると思います。 セレクターは、構築時に組み込まれているものである必要があります。これがある場合は、他の誰かに優先度の値を渡す必要がなくなります。優先度を知りたい場合は、セレクターを呼び出すだけです。 したがって、セレクターのほとんどを除いて、メソッドシグネチャに優先度パラメーターを含めたくないでしょう。 したがって、その場合、それは一種のより専門的な「PrioritySet」クラスになります。 [不純なセレクターがバグの問題になる可能性があります!]

@TimLovellSmithこの動作の要望は理解していますが、これは5年間の質問であるため、セレクターを使用して配列ベースのバイナリヒープを実装し、 Queue.csと同じAPI表面積を共有するのは合理的ではないでしょうかPriorityQueueを作成する場合は、 Queue API設計をエミュレートする必要があると思います。

@Jlalondしたがって、ここでの提案ですね。 ヒープベースの配列の実装に異論はありません。 私がセレクターに対して持っていた最大の異議は、優先順位の更新を正しく行うのに十分簡単ではないということを今思い出します。 プレーンな古いキューには優先度更新操作はありませんが、要素の優先度の更新は、多くの優先度キューアルゴリズムにとって重要な操作です。
破損した優先度付きキュー構造をデバッグする必要のないAPIを使用する場合、人々は永遠に感謝するはずです。

@TimLovellSmith申し訳ありませんが、ティム、 IDictionaryからの継承を明確にする必要がありました。

優先順位が変わるユースケースはありますか?

私はあなたの実装が好きですが、IDictionaryBehaviorの複製を減らすことができると思います。 辞書のアクセスパターンは直感的ではないと思うので、 IDictionary<>から継承するのではなく、ICollectionだけを継承する必要があると思います。

ただし、Tとそれに関連する優先度を返すことは理にかなっていると思いますが、データ構造内の要素をエンキューまたはデキューするときにその優先度を知る必要があるユースケースはわかりません。

@Jlalond
優先キューがある場合は、単純かつ効率的に実行できるすべての操作をサポートする必要があります。
この種のデータ構造/抽象化に精通している人々がそこにいることを「期待」しています。

更新の優先度は、これらの両方のカテゴリに分類されるため、APIに属します。 優先度の更新は、多くのアルゴリズムで十分に重要であるため、ヒープデータ構造を使用して操作を実行する複雑さは、アルゴリズム全体の複雑さに影響し、定期的に測定されます。以下を参照してください。
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps

TBHは主に、アルゴリズムと効率に関心があり、測定されたdecrease_keyです。したがって、APIにはDecreasePriority()しかなく、実際にはUpdatePriorityがないので個人的には問題ありません。
ただし、便宜上、すべての変更が増加であるか減少であるかを心配することでAPIのユーザーを煩わせない方が

私が同意したRE辞書。 シンプルな名前で私の提案からそれを削除した方が良いです。
https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

エンキューまたはデキューするときにデータ構造内の要素の優先度を知る必要があるユースケースはわかりません。

このコメントが何なのかわかりません。 要素をデキューするために、要素の優先度を知る必要はありません。 ただし、キューに入れるときは、その優先度を知る必要があります。 要素をデキューするときに、要素の最終的な優先順位を知りたい場合があります。その値には、距離などの意味がある場合があるためです。

@TimLovellSmith

このコメントが何なのかわかりません。 要素をデキューするために、要素の優先度を知る必要はありません。 ただし、キューに入れるときは、その優先度を知る必要があります。 要素をデキューするときに、要素の最終的な優先順位を知りたい場合があります。その値には、距離などの意味がある場合があるためです。

申し訳ありませんが、キーと値のペアでTPriortyが何を意味するのか誤解しました。

さて、最後の2つの質問、なぜそれらがTPriorityでKeyValuePairであるのか、そして再優先順位付けのために私が見ることができる最良の時間はN log Nです、私はそれが価値があることに同意しますが、そのAPIはどのように見えるでしょうか?

再優先順位付けのために私が見ることができる最良の時間はNlogNです。

1つのアイテムではなく、N個のアイテムの優先順位を変更するのに最適な時期ですよね。
ドキュメントを明確にします。「UpdatePriority | O(log n)」は「UpdatePriority(single item)| O(logn)」と読み替えてください。

@TimLovellSmith理にかなっていますが、要素を削除してから再挿入する必要があるため、優先度の更新は実際にはO2 *(log n)ではありませんか? これがバイナリヒープであることに基づいて私の仮定を立てる

@TimLovellSmith理にかなっていますが、要素を削除してから再挿入する必要があるため、優先度の更新は実際にはO2 *(log n)ではありませんか? これがバイナリヒープであることに基づいて私の仮定を立てる

その2のような定数因子は、Nのサイズが大きくなるにつれてほとんど無関係になるため、複雑さの分析では一般に無視されます。

理解しましたが、ほとんどの場合、ティムに何かエキサイティングなアイデアがあるかどうかを知りたいと思っていました。
1つの操作:)

火、2020年8月18日には、23時51分masonwheeler [email protected]書きました:

@TimLovellSmith https://github.com/TimLovellSmith理にかなっていますが、
必要に応じて、優先度の更新は実際にはne O2 *(log n)ではありません。
要素を削除してから再挿入しますか? これに基づいて私の仮定
バイナリヒープであること

その2のような一定の要因は、複雑さの分析では一般的に無視されます
Nのサイズが大きくなると、ほとんど無関係になるためです。


あなたが言及されたのであなたはこれを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/dotnet/runtime/issues/14032#issuecomment-675887763
または購読を解除する
https://github.com/notifications/unsubscribe-auth/AF76XTI3YK4LRUVTOIQMVHLSBNZARANCNFSM4LTSQI6Q

@Jlalond
斬新なアイデアはありません。一定の要素を無視していましたが、減少も増加とは異なります。

優先度の更新が減少の場合は、削除して再度追加する必要はありません。バブルするだけなので、1 * O(log n)= O(log n)になります。
優先度の更新が増加である場合は、おそらくそれを削除して再度追加する必要があるため、その2 * O(log n)=まだO(log n)です。

他の誰かが、remove + readdよりも優先度を上げるためのより良いデータ構造/アルゴリズムを発明したかもしれませんが、私はそれを見つけようとしませんでした。O(log n)で十分なようです。

KeyValuePairは、辞書である間はインターフェイスに忍び込みましたが、主に_要素のコレクションを優先度_で繰り返すことができるように、辞書が削除されても存続しました。 また、要素を優先度とともにデキューできるようにします。 ただし、簡単にするために、優先順位を指定したデキューは、「高度な」バージョンのDequeueAPIでのみ行う必要があります。 (TryDequeue:D)

私はその変更を行います。

@TimLovellSmithかっこいい、私はあなたの改訂された提案を楽しみにしています。 これに取り組み始めるために、レビューのために提出できますか? さらに、マージ時間を改善するためにペアリングキューにいくらかの推進力があったことは知っていますが、それでも配列ベースのバイナリヒープが最も全体的なパフォーマンスになると思います。 考え?

@Jlalond
Peek + Dequeueapiを簡素化するためにマイナーな変更を加えました。
いくつかのICollectionKeyValuePairに関連するAPIはまだ残っています。これは、それらを置き換えるのに明らかに良いものが見当たらないためです。
同じPRリンク。 レビューのために提出する別の方法はありますか?

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

配列ベースのヒープとほぼ同じかそれ以上の速度である限り、どの実装を使用してもかまいません。

@TimLovellSmith私は実際にはAPIレビューについて何も知りませんが、 @ danmosemsftが私たちを正しい方向に思います。

私の最初の質問は何でしょうかなれ? 私はそれがint、または浮動小数点数であると思いますが、私にとって、キューの「内部」である数を宣言することは私には奇妙に思えます

そして、私は実際にはバイナリヒープを好みますが、私たち全員がその方向に進んで大丈夫であることを確認したかったのです

@eiriktsarpalis @layomiaエリアの所有者であり、APIレビューを通じてこれを管理する必要があります。 私は議論をフォローしていません、私たちはコンセンサスのポイントに達しましたか?

過去に説明したように、コアライブラリは、すべてのコレクション、特に意見が分かれているコレクションやニッチなコレクションに最適な場所ではありません。 たとえば、毎年出荷しています。図書館ほど速く移動することはできません。 そのギャップを埋めるために、コレクションパックを中心にコミュニティを構築することを目標としています。 しかし、これに対する84の賛成は、コアライブラリでこれが広く必要とされていることを示唆しています。

@Jlalond申し訳ありませんが、最初の質問が何であるかが

@TimLovellSmithうん、それが一般的かどうか知りたかっただけ

@danmosemsftありがとうダン。 私の提案[1]に対して私が目にする未解決の異議/コメントの数はほぼゼロであり、 未解決のままにされた重要な質問に対処します(これはますます類似するようになっています)。

したがって、これまでのところ健全性チェックに合格していると思います。 まだいくつかのレビューが必要です。IReadOnlyCollectionを継承することが有用かどうかについて話し合うことができます(あまり有用ではないようですが、専門家に任せる必要があります)など-それがAPIレビュープロセスの目的だと思います! @eiriktsarpalis @layomiaこれを見てください。

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

[PS-スレッドの感情に基づいて、現在提案されているキラーアプリは、コーディングコンテスト、インタビューの質問などです。ここでは、クラスまたは構造のいずれかで機能するシンプルなAPIと、その日のお気に入りの数値タイプを適切なOで使用する必要があります。 (n)実装が遅すぎないこと-そして、いくつかの問題にそれを適用するためにギニアピッグになれることを嬉しく思います。 申し訳ありませんが、これ以上エキサイティングなことはありません。]

主にグラフ検索(問題の最適化、ルート検索など)、順序付けされた抽出(チャンク(四分木に最も近い隣人など)、スケジューリング(例:上記のタイマーの説明))。 健全性チェック/テストのために実装を直接プラグインできるプロジェクトがいくつかあります。 現在の提案は私には良さそうです(理想的な適合ではないにしても、私のすべての目的に役立つでしょう)。 私はいくつかの小さな観察を持っています:

  • TElementは、 TKeyであるはずの場所に数回表示されます
  • 私自身の実装には、使いやすさ(場合によっては効率性)のためにbool EnqueueOrUpdateIfHigherPriorityが含まれていることが多く、これが(グラフ検索などで)使用される唯一のエンキュー/更新メソッドになることがよくあります。 明らかに、これは必須ではなく、APIをさらに複雑にしますが、非常に便利です。
  • Enqueueコピーが2つあります( Add意味するわけではありません):1つはbool TryEnqueue意味しますか?
  • "//コレクションを任意の順序で列挙しますが、最初に最小の要素を使用します":最後のビットは役に立たないと思います。 列挙子が比較を実行する必要がないことを望みますが、これは「無料」になると思いますので、気になりません
  • EqualityComparerの名前は少し予想外です。 KeyComparerPriorityComparerと一緒になると思っていました。
  • CopyToToArrayの複雑さに関するメモがわかりません。

@VisualMelon
レビューありがとうございます!

KeyComparerの名前の提案が好きです。 優先度を下げるAPIは非常に便利に聞こえます。 これらのAPIの一方または両方を追加するのはどうですか。 「最優先が最優先」モデルで使用しているので、減少のみを使用しますか? それともあなたも増やしたいですか?

    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

列挙すると最初に最小要素が返されると思った理由は、Peek()がコレクションの先頭にある要素を返すというメンタルモデルと一致するためです。 また、バイナリヒープの場合、実装に比較は必要ないため、景品のように見えました。 しかし、多分それは私が思うよりも自由ではありません-不当な複雑さ? そのため、取り出してよかったです。

2つのエンキューは偶発的でした。 EnqueueOrUpdateがあり、優先度は常に更新されます。 TryUpdateはその論理的な逆であり、コレクションにすでに存在するアイテムの優先度は決して更新されるべきではないと思います。 それがその論理的なギャップを埋めていることはわかりますが、それが実際に人々が望むAPIであるかどうかはわかりません。

減少するバリアントを使用することしか想像できません。ノードをエンキューするか、既存のノードを優先度の低いノードに置き換えることは、ルート検索の自然な操作です。 私はすぐに反対の使用法を提案することができませんでした。 boolean returnパラメーターは、アイテムがキューに入れられている/キューに入れられていないときに操作を実行する必要がある場合に便利です。

私はバイナリヒープを想定していませんでしたが、それが無料の場合、最初の要素が常に最小であることに問題はなく、奇妙な制約のように見えました。

@VisualMelon @TimLovellSmith 1つのAPIで問題ありませんか?
EnqueueOrUpdatePriority ? キュー内のノードを再配置する機能は、優先度が増減した場合でも、トラバーサルアルゴリズムにとって価値があると思います。

@Jlalondええ、実装によっては効率に役立つ可能性があるため、言及します。また、キューに既にあるものが改善された場合にのみ、何かをキューに入れたいというユースケースを直接エンコードします。 追加の複雑さがそのような汎用ツールに適切であるかどうかを言うとは思いませんが、それは確かに必要ではありません。

EnqueueOrIncreasePriorityを削除し、EnqueueOrUpdateとEnqueueOrDecreaseを維持するように更新しました。 HashSet.Add()のように、アイテムがコレクションに新しく追加されたときにブール値を返すようにします。

@Jlalond上記のエラーをお詫びします(この問題がいつレビューされるかを尋ねる最新のコメントの削除)。

@eiriktsarpalisが来週戻ってお伝えしたかったので、この機能の優先順位付けとAPIの確認について説明します。

これは私たちが検討していることですが、.NET6ではまだコミットされていません。

2017年にこのトピックについて数か月間(この巨大なスレッドの大部分)詳細な議論を行い、この提案をまとめて作成したにもかかわらず、このスレッドはゼロから始めてフォークすることで復活したことがわかります。上にスクロールして確認してください。 これは、 @ karelzが最初の投稿の最初の行で

corefxlabリポジトリの最新の提案を参照してください。

2017年6月16日の時点で、発生したことのないAPIレビューを待っていました。ステータスは次のとおりです。

APIが現在の形式(2つのクラス)で承認されている場合は、4次ヒープを使用して両方の実装を含むCoreFXLabへの別のPRを作成します(実装を参照)。 PriorityQueue<T>は下の1つの配列を使用するだけですが、 PriorityQueue<TElement, TPriority>は2つの配列を使用し、それらの要素を一緒に再配置します。 これにより、追加の割り当てを節約し、可能な限り効率的にすることができます。 緑色のライトが表示されたら、それを追加します。

まだ行われていない正式なレビューから始めて、2017年に私たちが行った提案を繰り返し続けることをお勧めします。 これにより、コミュニティメンバーが何百もの投稿を交換した後にまとめられた提案の裏で、フォークや反復を回避できます。また、関係者全員の努力を尊重します。 フィードバックがあれば、喜んで話し合います。

@pgolebiowskiディスカッションに戻ってくれてありがとう。 提案をさらに効果的にフォークすることをお詫びしたいと思います。これは、コラボレーションするための最も効果的な方法ではありません。 それは私の側の衝動的な動きでした。 (提案の未解決の質問が多すぎて議論が完全に行き詰まっていて、もっと意見のある提案が必要だったという印象を受けました。)

少なくとも一時的にPRのコード内容を忘れて、これを進めて、特定された「未解決の問題」についてここで議論を再開しようとしたらどうでしょうか。 それらすべてについて意見を述べたいと思います。

  1. クラス名PriorityQueueとヒープ<-すでに説明されていると思いますが、PriorityQueueの方が優れているというコンセンサスがあります。

  2. IHeapとコンストラクターのオーバーロードを導入しますか? <-私は多くの価値を予見していません。 世界の95%には、複数のヒープ実装があるという説得力のある理由(パフォーマンスデルタなど)はなく、他の5%はおそらく独自に作成する必要があると思います。

  3. IPriorityQueueを導入しますか? <-私もそれが必要だとは思いません。 他の言語やフレームワークは、標準ライブラリにこれらのインターフェイスがなくても問題なく動作すると思います。 それが正しくない場合はお知らせください。

  4. (値内に格納されている優先度の)セレ​​クターを使用するかどうか(5つのAPIの違い)<-優先度付きキューでセレクターをサポートすることを支持する強い議論は見られません。 IComparer<>は、アイテムの優先順位を比較するための非常に優れた最小限の拡張メカニズムとしてすでに機能しており、セレクターは大幅な改善を提供していないと思います。 また、優先順位が変更可能なセレクターの使用方法(または使用しない方法)について混乱する可能性があります。

  5. タプル(TElement要素、TPriority優先度)とKeyValuePairを使用する<-個人的には、アイテムはセット/辞書でキーとして扱われるため、アイテムの優先度が更新可能な優先度キューにはKeyValuePairが推奨されるオプションだと思います。

  6. PeekとDequeueは、タプルではなく引数を持たないようにする必要がありますか? <-すべての長所と短所、特にパフォーマンスについてはよくわかりません。 単純なベンチマークテストでパフォーマンスが優れているものを選択する必要がありますか?

  7. ピークとデキューのスローはまったく役に立ちますか? <-はい...これは、キューにまだアイテムが残っていると誤って想定するアルゴリズムで発生するはずです。 アルゴリズムにそれを想定させたくない場合は、PeekとDequeueのAPIをまったく提供せず、TryPeekとTryDequeueを提供するのが最善の方法です。 [特定の場合にPeekまたはDequeueを安全に呼び出すことができるアルゴリズムがあると思います。Peek/ Dequeueを使用すると、パフォーマンスと使いやすさがわずかに向上します。]

それとは別に、検討中の提案に追加することを検討するためのいくつかの提案もあります。

  1. PriorityQueue独自のハンドルである一意のアイテムでのみ機能する必要があります。 優先度の更新を行うために特定の方法で拡張不可能なオブジェクト(文字列など)を比較する場合に備えて、カスタムIEqualityComparerをサポートする必要があります。

  2. PriorityQueueグラフ検索シナリオを効率的かつ簡単に実装するために、「削除」、「小さい場合は優先度を下げる」、特に「アイテムをキューに入れるか、小さい場合は優先度を下げる」などのさまざまな操作をすべてO(log n)でサポートする必要があります。

  3. PriorityQueueがあれば、怠惰なプログラマーにとっては便利でしょう。既存のアイテムの優先度を取得/設定するためのインデックス演算子[]を提供します。 getの場合はO(1)、setの場合はO(log n)である必要があります。

  4. 最小の優先順位が最初に最適です。 優先度付きキューは多くの場合に使用されるため、「最小コスト」の最適化問題があります。

  5. 列挙は、順序の保証を提供するべきではありません。

未解決の問題-ハンドル:
PriorityQueue項目と優先順位は、重複する値を処理できるようにすることを目的としているようです。 これらの値は「不変」と見なす必要があるか、アイテムの優先度が変更されたときにコレクションに通知するメソッドが必要です。これにより特定できます。 this ??)...これがすべて役立つかどうか、そしてこれが良いアイデアかどうかについては疑問がありますが、そのようなコレクションに更新の優先順位がある場合は、その方法で発生するコストを遅らせることができれば便利かもしれません。不変のアイテムを操作しているだけで、使用したくない場合は、追加のコストはかかりません...解決方法は? ハンドルを使用するメソッドと使用しないメソッドをオーバーロードすることによる可能性はありますか? または、アイテム自体とは異なるアイテムハンドル(ルックアップのハッシュキーとして使用される)を用意しないでください。

これをより速く移動するのに役立つと考えられているので、このタイプをhttps://github.com/dotnet/runtime/discussions/42205で説明されている「コミュニティコレクション」ライブラリに移動することは理にかなってい

私は、最初に最小の優先順位が最善であることに同意します。 優先度付きキューは、ターンベースのゲームの開発にも役立ちます。優先度を、各要素が次のターンを取得するタイミングの単調に増加するカウンターにすることができると、非常に役立ちます。

これをできるだけ早くAPIレビューに持ち込むことを検討しています(ただし、.NET 6の期間内に実装を提供することはまだ約束されていません)。

ただし、これをレビューのために送信する前に、高レベルの未解決の質問のいくつかを解決する必要があります。 @TimLovellSmith記事は、その目標を達成するための良い出発点だと思います。

それらの点に関するいくつかの意見:

  • 質問1〜3についてのコンセンサスは長い間確立されており、解決済みとして扱うことができると思います。

  • _(値内に格納されている優先度の)セレ​​クターを使用するかどうか_-コメントに同意します。 この設計のもう1つの問題は、セレクターがオプションであるということです。つまり、間違ったEnqueueオーバーロードを呼び出さないように注意する必要があります(またはInvalidOperationException取得するリスクがあります)。

  • KeyValuePairよりもタプルを使用する方が好きです。 .Keyプロパティを使用してTPriority値にアクセスするの.Priorityを使用できます。

  • _PeekとDequeueは、タプルではなく引数を持たない方がよいでしょうか?_-他の場所でも同様の方法で確立された規則に従う必要があると思います。 したがって、おそらくout引数を使用します。

  • _ピークとデキューのスローはまったく役に立ちますか?_-コメントに100%同意しました。

  • _最初に最小の優先順位が最適です_-同意しました。

  • _列挙は順序保証を提供するべきではありません_-これはユーザーの期待に違反しないでしょうか? トレードオフは何ですか? 注意:APIレビューの議論のために、おそらくこのような詳細を延期することができます。

他のいくつかの未解決の質問も言い換えたいと思います。

  • PriorityQueue<T>PriorityQueue<TElement, TPriority>またはその両方を発送しますか? -個人的には、後者のみを実装する必要があると思います。後者は、よりクリーンでより汎用的なソリューションを提供するように思われるからです。 原則として、優先度はキューに入れられた要素に固有のプロパティであってはならないため、ユーザーにラッパータイプにカプセル化するように強制するべきではありません。

  • 平等まで、要素の一意性が必要ですか? -間違っている場合は訂正してください。ただし、一意性は人為的な制約であり、ハンドルアプローチに頼らずに更新シナリオをサポートする必要があるために強制されていると感じています。 また、正しい等式セマンティクスを持つ要素についても心配する必要があるため、APIサーフェスが複雑になります(要素が大きなDTOである場合の適切な等式は何ですか?)。 私はここで3つの可能なパスを見ることができます:

    1. 一意性/同等性を要求し、元の要素を渡すことによって更新シナリオをサポートします(同等性まで)。
    2. 一意性/同等性を必要とせず、ハンドルを使用した更新シナリオをサポートします。 これらは、それらを必要とするユーザーのためにオプションのEnqueueメソッドバリアントを使用して取得できます。 ハンドルの割り当てが十分に大きな懸念事項である場合、それらをValueTaskで償却できるかどうか疑問に思います。
    3. 一意性/同等性を必要とせず、優先順位の更新をサポートしません。
  • キューのマージをサポートしていますか? -マージが効率的でない実装(おそらく配列ヒープを使用)を1つだけ出荷しているため、コンセンサスはないようです。

  • どのインターフェースを実装する必要がありますか? - IQueue<T>推奨する提案をいくつか見ましたが、これはリークのある抽象化のように感じます。 個人的には、シンプルに保ち、 ICollection<T>実装することを好みます。

cc @layomia @safern

@eiriktsarpalisそれどころか、更新をサポートするための制約について人為的なことは何もありません!

優先度付きキューを使用するアルゴリズムは、多くの場合、要素の優先度を更新します。 問題は、通常のオブジェクトの優先順位を更新するために「正しく機能する」オブジェクト指向APIを提供するのか、それとも人々に強制するのかということです。
a)ハンドルモデルを理解する
b)オブジェクトのハンドルを追跡するために、追加のプロパティや外部ディクショナリなどの追加データをメモリに保持して、オブジェクトが更新できるようにします(変更できないクラスのディクショナリを使用する必要がありますか?またはオブジェクトをタプルなどにアップグレードします)
c)「メモリ管理」または「ガベージコレクト」外部構造、つまり、ディクショナリアプローチを使用する場合、キューに存在しなくなったアイテムのハンドルをクリーンアップします。
d)単一の優先度キューのコンテキストでのみ意味があるため、複数のキューを持つコンテキストで不透明なハンドルを混同しないでください

さらに、_優先度によってオブジェクトを追跡する_キューがこのように動作することを誰もが望んでいる理由全体について、この哲学的な質問があります。同じオブジェクト(等しいがtrueを返す)に2つの_異なる_優先度が必要なのはなぜですか? それらが実際に異なる優先順位を持つことになっている場合、なぜそれらは異なるオブジェクトとしてモデル化されないのですか? (または、識別子を使用してタプルにアップコンバートされましたか?)

また、ハンドルの場合、ハンドルが実際に機能するように、優先度キューにハンドルの内部テーブルが必要です。 これは、キュー内のオブジェクトを検索するための辞書を保持することと同等の作業だと思います。

PS:すべての.netオブジェクトはすでに平等/一意性の概念をサポートしているので、それを「必要とする」という大きな要求ではありません。

KeyValuePairに関しては、理想的ではないことに同意します(ただし、提案では、 Keyは要素用、 Valueは優先度用であり、さまざまなSortedと一致しています。 BCLのstructを好むでしょう。

独自性に関しては、それが根本的な懸念事項であり、それまでは他に何も決まらないと思います。 目標が単一の使いやすい汎用APIである場合は、(オプションの)コンパレーターによって定義された要素の一意性(既存の提案と提案iの両方による)を優先します。 ユニークと非ユニークは大きな亀裂であり、私は両方のタイプを異なる目的で使用します。 前者は実装が「困難」であり、誤用がより困難でありながら、最も(そしてより典型的な)ユースケース(私の経験だけ)をカバーしています。 非一意性(IMO)を別のタイプ(たとえば、単純な古いバイナリヒープ)で処理する必要があるユースケース。両方を利用できるようにしていただければ幸いです。 これは基本的に、 @ pgolebiowskiによってリンクされた元の提案が(私が理解しているように)(単純な)ラッパーを法として提供するものです。 _編集:いいえ、それは同点の優先順位をサポートしません_

それどころか、更新をサポートするための制約については、人為的なものは何もありません。

申し訳ありませんが、更新のサポートが人為的なものであることを示すつもりはありませんでした。 むしろ、一意性の要件は、更新をサポートするために人為的に導入されています。

PS:すべての.netオブジェクトはすでに平等/一意性の概念をサポートしているので、それを「必要とする」という大きな要求ではありません

もちろんですが、型に付属する等式セマンティクスが望ましいものではない場合があります(たとえば、参照の等式、本格的な構造の等式など)。 平等は難しいことを指摘しているだけで、それを設計に強制すると、まったく新しいクラスの潜在的なユーザーバグが発生します。

@eiriktsarpalisそれを明確にしてくれてありがとう。 しかし、それは本当に人工的なものですか? そうではないと思います。 そのちょうど別の自然な解決策。

APIは_well-defined_である必要があります。 ユーザーが更新したいものを正確に指定する必要がない限り、更新APIを提供することはできません。 ハンドルとオブジェクトの同等性は、明確に定義されたAPIを構築するための2つの異なるアプローチにすぎません。

ハンドルアプローチ:コレクションにオブジェクトを追加するたびに、オブジェクトに「名前」、つまり「ハンドル」を付ける必要があります。これにより、会話の後半であいまいさを感じることなく、その正確なオブジェクトを参照できます。

オブジェクトの一意性アプローチ:コレクションにオブジェクトを追加するたびに、そのオブジェクトは別のオブジェクトである必要があります。または、オブジェクトが既に存在する場合の処理​​方法を指定する必要があります。

残念ながら、オブジェクトアプローチのみが、「EnqueueIfNotExists」や「EnqueueOrDecreasePriroity(item)」などの最も便利な高レベルAPIメソッドプロパティのいくつかを実際にサポートできます。これらは、ハンドル中心の設計で提供する意味がありません。アイテムがすでにキューに存在するかどうかを知ることはできません(ハンドルを使用してそれを追跡するのはあなたの仕事だからです)。

ハンドルアプローチに対する最も明白な批判の1つ、または一意性の制約を私に落とすことは、更新可能な優先度を持つあらゆる種類のシナリオの実装がはるかに複雑になることです。

例えば

  1. PriorityQueueを使用する賛成/スコア更新されるメッセージ/感情/タグ/ユーザー名を表す文字列の場合、一意の値の優先度が変更されます
  2. PriorityQueueを使用する、double>一意のタプルを注文するには[優先度が変更されているかどうかに関係なく]-余分なハンドルをどこかに追跡する必要があります
  3. PriroityQueueを使用するグラフインデックスまたはデータベースオブジェクトIDに優先順位を付けるには、実装全体にハンドルを振りかける必要があります。

PS

もちろんですが、型に付属する等式セマンティクスが望ましいものではない場合があります

IEqualityComparerのようなエスケープハッチ、またはよりリッチなタイプへのアップコンバートが必要です。

フィードバックをありがとう🥳すべての新しい入力を考慮して、週末に提案を更新し、次のラウンドの新しい改訂を共有します。 ETA2020-09-20。

優先キューの提案(v2.0)

概要

.NET Coreコミュニティは、システムライブラリに_priority queue_機能を追加することを提案しています。これは、各要素に優先度が追加で関連付けられたデータ構造です。 具体的には、 PriorityQueue<TElement, TPriority>System.Collections.Generic名前空間に追加することを提案します。

教義

私たちの設計では、次の教義に導かれました(より良いものを知らない限り):

  • 広い範囲。 .NET Coreのお客様に、さまざまなユースケースをサポートするのに十分な汎用性を備えた貴重なデータ構造を提供したいと考えています。
  • 既知の間違いから学ぶ。 私たちは、Java、Python、C ++、Rustなどの他のフレームワークや言語に存在する顧客向けの問題から解放される優先キュー機能の提供に努めています。 お客様を不満にさせ、優先キューの有用性を低下させることが知られている設計上の選択を行うことは避けます。
  • 一方向ドアの決定に細心の注意を払ってください。 APIが導入されると、変更または削除することはできず、拡張するだけです。 お客様が永遠に立ち往生する次善のソリューションを回避するために、設計の選択を慎重に分析します。
  • 設計の麻痺を避けてください。 完璧な解決策はないかもしれないことを私たちは受け入れます。 トレードオフのバランスを取り、納品を進め、最終的にお客様が長年待ち望んでいた機能を納品します。

バックグラウンド

お客様の視点から

概念的には、優先度キューは要素のコレクションであり、各要素には関連付けられた優先度があります。 優先度付きキューの最も重要な機能は、コレクション内で最も優先度の高い要素への効率的なアクセスと、その要素を削除するオプションを提供することです。 予想される動作には、次のものも含まれます。1)すでにコレクションにある要素の優先度を変更する機能。 2)複数の優先キューをマージする機能。

コンピュータサイエンスの背景

優先度付きキューは抽象的なデータ構造です。つまり、前のセクションで説明したように、特定の動作特性を持つ概念です。 優先度付きキューの最も効率的な実装は、ヒープに基づいています。 ただし、一般的な誤解とは異なり、ヒープは抽象的なデータ構造でもあり、さまざまな方法で実現でき、それぞれに異なるメリットとデメリットがあります。

ほとんどのソフトウェアエンジニアは、配列ベースのバイナリヒープの実装にのみ精通しています。これは最も単純な実装ですが、残念ながら最も効率的な実装ではありません。 一般的なランダム入力の場合、より効率的なヒープタイプの2つの例は、 4次ヒープペアリングヒープです。 ヒープの詳細については、ウィキペディアこのペーパーを参照してください。

更新メカニズムは設計上の重要な課題です

私たちの議論は、設計で最も困難な領域であると同時にAPIに最大の影響を与えるのは、更新メカニズムであることを示しています。 具体的には、お客様に提供したい製品が、コレクションにすでに存在する要素の優先度の更新をサポートする必要があるかどうか、またどのようにサポートするかを決定することが課題です。

このような機能は、ダイクストラの最短経路アルゴリズムや、優先順位の変更を処理する必要のあるジョブスケジューラなどを実装するために必要です。 更新メカニズムがJavaにありません。これは、エンジニアにとってがっかりすることが示されています。たとえば、32k回以上表示されたこれらの3つのStackOverflowの質問(などです。 このような限られた値のAPIの導入を回避するために、提供する優先度キュー機能の基本的な要件は、コレクションにすでに存在する要素の優先度を更新する機能をサポートすることであると考えています。

更新メカニズムを提供するには、お客様が正確に何を更新したいかについて具体的にできるようにする必要があります。 これを実現する2つの方法を特定しました。a)ハンドルを介して。 b)コレクション内の要素の一意性を強制することによって。 これらにはそれぞれ、さまざまなメリットとコストが伴います。

オプション(a):ハンドル。 このアプローチでは、要素がキューに追加されるたびに、データ構造が固有のハンドルを提供します。 顧客が更新メカニズムを使用したい場合は、後で更新する要素を明確に指定できるように、そのようなハンドルを追跡する必要があります。 このソリューションの主なコストは、顧客がこれらのポインターを管理する必要があることです。 ただし、優先キュー内のハンドルをサポートするために内部割り当てが必要であることを意味するわけではありません。非配列ベースのヒープはノードに基づいており、各ノードは自動的に独自のハンドルになります。 例として、 PairingHeap.UpdateメソッドのAPIを参照してください。

オプション(b):一意性。 このアプローチでは、顧客に2つの追加の制約が課せられます。i)優先度キュー内の要素は、特定の同等性セマンティクスに準拠する必要があります。これにより、新しいクラスの潜在的なユーザーバグが発生します。 ii)2つの等しい要素を同じキューに格納することはできません。 このコストを支払うことで、ハンドルアプローチに頼ることなく更新メカニズムをサポートできるというメリットが得られます。 ただし、一意性/同等性を利用して更新する要素を決定する実装では、追加の内部マッピングが必要になるため、O(n)ではなくO(1)で実行されます。

おすすめ

ハンドルを介した更新メカニズムをサポートするPriorityQueue<TElement, TPriority>クラスをシステムライブラリに追加することをお勧めします。 基礎となる実装はペアリングヒープになります。

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

使用例

1)更新メカニズムを気にしないお客様

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)更新メカニズムを気にするお客様

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

よくある質問

優先度付きキューはどのような順序で要素を列挙しますか?

未定義の順序で、 HashSet場合と同様に、列挙がO(n)で発生する可能性があります。 効率的な実装では、要素が順番に列挙されることを保証しながら、線形時間でヒープを列挙する機能は提供されません。これには、O(n log n)が必要になります。 コレクションの注文は.OrderBy(x => x.Priority)で簡単に実行でき、すべての顧客がこの注文で列挙を気にするわけではないため、未定義の列挙注文を提供する方がよいと考えています。

付録

付録A:優先キュー機能を備えた他の言語

| 言語| タイプ| ノート|
|:-:|:-:|:-:|
| Java | PriorityQueue | 抽象クラスAbstractQueueを拡張し、インターフェースQueueを実装します。 |
| さび| BinaryHeap | |
| スイフト| CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| 行く| ヒープ| ヒープインターフェイスがあります。 |

付録B:発見可能性

データ構造について説明する場合、「ヒープ」という用語は「優先度付きキュー」の4倍の頻度で使用されることに注意してください。

  • "array" AND "data structure" —17.400.000の結果
  • "stack" AND "data structure" —12.100.000の結果
  • "queue" AND "data structure" —3.850.000の結果
  • "heap" AND "data structure" —1.830.000の結果
  • "priority queue" AND "data structure" —430.000件の結果
  • "trie" AND "data structure" —335.000件の結果

確認してください。フィードバックを受け取って、繰り返し続けてください:)私たちは収束していると感じています! 😄また、質問を喜んで受け取ります—それらをFAQに追加します!

@pgolebiowski私はハンドルベースのAPIを使用する傾向はありませんが(既存のコードを再調整する場合は、例2のようにこれをラップすることを期待します)、ほとんどの場合、見栄えがします。 私はあなたが言及した実装を試してみて、それがどのように感じられるかを確認するかもしれませんが、ここにいくつかのコメントがあります:

  • TryDequeueがノードを返すのは奇妙に思えます。なぜなら、それはその時点では実際にはハンドルではないからです(2つの別々の出力パラメーターが望ましいです)
  • 2つの要素が同じ優先度でキューに入れられた場合、それらは予測可能な順序でデキューされるという点で安定していますか? (再現性のために持っているのはいいです;そうでなければ消費者によって十分に簡単に実装することができます)
  • 以下のためのパラメータMergeなければなりませんPriorityQueue'2ませんPriorityQueueNode'2 、そしてあなたが行動を明確にすることができますか? 私はペアリングヒープに精通していませんが、おそらく2つのヒープはその後オーバーラップします
  • 私は2パラメーターメソッドのContainsという名前のファンではありません。これはTryGetスタイルのメソッドで推測される名前ではありません。
  • クラスはContains目的でカスタムIEqualityComparer<TElement>をサポートする必要がありますか?
  • ノードがまだヒープ内にあるかどうかを効率的に判断する方法はないようです(これをいつ使用するかはわかりません)。
  • Remove返品がboolであるのは奇妙です。 TryRemoveと呼ばれるか、スローされると思います(ノードがヒープ内にない場合は、 Updateがスローすると想定しています)。

@VisualMelonフィードバックをありがとう

  • TryDequeueがノードを返すのは奇妙に思えます。なぜなら、それはその時点では実際にはハンドルではないからです(2つの別々の出力パラメーターが望ましいです)
  • 以下のためのパラメータMergeなければなりませんPriorityQueue'2ませんPriorityQueueNode'2 、そしてあなたが行動を明確にすることができますか? 私はペアリングヒープに精通していませんが、おそらく2つのヒープはその後オーバーラップします
  • Remove返品がboolであるのは奇妙です。 TryRemoveと呼ばれるか、スローされると思います(ノードがヒープ内にない場合は、 Updateがスローすると想定しています)。
  • 私は2パラメーターメソッドのContainsという名前のファンではありません。これはTryGetスタイルのメソッドで推測される名前ではありません。

これら2つの説明:

  • 2つの要素が同じ優先度でキューに入れられた場合、それらは予測可能な順序でデキューされるという点で安定していますか? (再現性のために持っているのはいいです;そうでなければ消費者によって十分に簡単に実装することができます)
  • ノードがまだヒープ内にあるかどうかを効率的に判断する方法はないようです(これをいつ使用するかはわかりません)。

最初のポイントとして、目標が再現性である場合、実装は決定論的です。 目標が_同じ優先度でキューに配置された2つの要素の場合、それらはキューに配置されたのと同じ順序で出力されます_ —実装を次のように調整できるかどうかはわかりませんこの特性を達成すると、簡単な答えは「おそらくいいえ」になります。

2番目のポイントについては、はい、ヒープは要素がコレクションに存在するかどうかを確認するのに適していません。顧客は、O(1)でこれを達成するためにこれを個別に追跡するか、ハンドルに使用するマッピングを再利用する必要があります。それらは更新メカニズムを使用します。 それ以外の場合、O(n)。

  • クラスはContains目的でカスタムIEqualityComparer<TElement>をサポートする必要がありますか?

うーん...おそらくContainsをこの優先キューの責任に置くのLinqのメソッドを使用するだけで十分かもしれないと思い始めています( Containsとにかく列挙に適用する必要があります)。

@pgolebiowski説明をありがとう

最初のポイントとして、目標が再現性である場合、実装は決定論的です、はい

永久に保証されているように(たとえば、実装が変更されても、動作は変更されない)、それほど決定論的ではないので、私が求めていた答えは「いいえ」だと思います。 それは問題ありません。コンシューマーは、必要に応じてシーケンスIDを優先度に追加できますが、その時点でSortedSetがその役割を果たします。

2番目のポイントについては、はい、ヒープは要素がコレクションに存在するかどうかを確認するのに適していません。顧客は、O(1)でこれを達成するためにこれを個別に追跡するか、ハンドルに使用するマッピングを再利用する必要があります。それらは更新メカニズムを使用します。 それ以外の場合、O(n)。

Remove必要な作業のサブセットは必要ありませんか? はっきりしていなかったかもしれません。 PriorityQueueNodeして、ヒープ内にあるかどうかを確認することを意味しました( TElementはありません)。

うーん...おそらくこの優先キューの責任にContainsを入れるのは多すぎるのではないかと思い始めています

Containsがなかったとしても文句は言いません。これは、ハンドルを使用する必要があることに気付いていない人にとっても罠です。

@pgolebiowskiあなたはハンドルをかなり強く支持しているようですが、効率上の理由からそれを考えるのは正しいですか?

効率の観点から、独自性のあるシナリオとないシナリオの両方で、ハンドルが本当に最適であると思うので、フレームワークによって提供される主要なソリューションとしてそれで問題ありません。

まったく:
使いやすさの観点から、重複する要素が私が望むものになることはめったにないと思います。それでも、フレームワークが両方のモデルをサポートすることが価値があるかどうか疑問に思います。 しかし...ユニークな要素に適した「PrioritySet」クラスは、提案されたPriorityQueueラッパーとして、たとえば、よりフレンドリーなAPIに対する継続的な需要に応えて、少なくとも後で簡単に追加できます。 (需要が存在する場合。私が思うにそうかもしれません!)

現在提案されているAPIについて、いくつかの考え/質問があります。

  • TryPeek(out TElement element, out TPriority priority)オーバーロードも提供するのはどうですか?
  • 更新可能な重複キーがある場合、「dequeue」が優先キューノードを返さない場合、ハンドル追跡システムから正しい正確なノードが削除されていることをどのように確認しますか? 同じ優先度の要素のコピーを複数持つことができるためです。
  • ノードが見つからない場合、 Remove(PriorityQueueNode)スローされますか? またはfalseを返しますか?
  • Removeがスローされた場合にスローされないTryRemove()バージョンがあるべきですか?
  • Contains() apiがほとんどの場合役立つかどうかわかりませんか? 「含む」は、特に、異なる優先順位を持つ「重複する」要素、または他の異なる機能を持つシナリオの場合、見る人の目に見えるようです! その場合、エンドユーザーはおそらくとにかく独自の検索を行う必要があります。 ただし、少なくとも、重複のないシナリオには役立ちます。

@pgolebiowski新しい提案の草案作成に時間を

  • @TimLovellSmithのコメントをContains()またはTryGetNode()かが存在するかどうかはわかりません。 これらは、 TElement同等性が重要であることを意味します。これは、おそらく、ハンドルベースのアプローチが回避しようとしていたことの1つです。
  • 私はおそらくpublic void Dequeue(out TElement element);public TElement Dequeue();と言い換えるでしょう
  • TryDequeue()メソッドが優先度を返す必要があるのはなぜですか?
  • クラスはICollection<T>またはIReadOnlyCollection<T>も実装するべきではありませんか?
  • 別のPriorityQueueインスタンスから返されたPriorityQueueNodeを更新しようとするとどうなりますか?
  • 効率的なマージ操作をサポートしますか? AFAICTこれは、配列ベースの表現を使用できないことを意味します。 これは、割り当ての観点から実装にどのように影響しますか?

ほとんどのソフトウェアエンジニアは、配列ベースのバイナリヒープの実装にのみ精通しています。これは最も単純な実装ですが、残念ながら最も効率的な実装ではありません。 一般的なランダム入力の場合、より効率的なヒープタイプの2つの例は、4次ヒープとペアリングヒープです。

後者のアプローチを選択するためのトレードオフは何ですか? それらに配列ベースの実装を使用することは可能ですか?

「追加」や「削除」などの適切な署名がない場合、 ICollection<T>またはIReadOnlyCollection<T>実装できません。

精神/形がICollection<KeyValuePair<T,Priority>>はるかに近い

精神/形がICollection<KeyValuePair<T,Priority>>はるかに近い

順序付けは実質的にキーイングメカニズムであるTPriorityによって行われるため、 KeyValuePair<TPriority, TElement>ではないでしょうか。

OK、全体的にメソッドContainsTryGetを削除することに賛成しているようです。 次のリビジョンでそれらを削除し、FAQで削除の理由を説明します。

実装されたインターフェースに関しては— IEnumerable<PriorityQueueNode<TElement, TPriority>>十分ではありませんか? どのような機能が欠けていますか?

KeyValuePair.Element.Priorityタプルまたは構造体の方が望ましいという声がいくつかありました。 私はこれらに賛成だと思います。

順序付けは実質的にキーイングメカニズムであるTPriorityによって行われるため、 KeyValuePair<TPriority, TElement>ではないでしょうか。

双方にとって良い議論があります。 一方で、はい、まさにあなたが今言ったことです。 一方、KVPコレクション内のキーは通常、一意であることが期待されており、同じ優先度を持つ複数の要素を持つことは完全に有効です。

一方、KVPコレクションのキーは、通常、一意であると予想されます

私はこの声明に同意しません-キーと値のペアのコレクションはまさにそれです。 一意性の要件は、その上に階層化されます。

IEnumerable<PriorityQueueNode<TElement, TPriority>>不十分ですか? どのような機能が欠けていますか?

提供されたAPIはすでにそのインターフェースをほぼ満たしているので、個人的にはIReadOnlyCollection<PQN<TElement, TPriority>>が実装されることを期待しています。 さらに、それは他のコレクションタイプと一致します。

インターフェースについて:

`` `
public bool TryGetNode(TElement element、out PriorityQueueNodeノード); // オン)

What's the point of it if I can just do enumerate collection and do comparison of elements? And why only Try version?

public PriorityQueueNode<TElement, TPriority> Peek(); // O(1)
public void Dequeue(out TElement element); // O(log n)
I find it a bit strange to have discrepancy between Dequeue and Peek. They do pretty much same thing expect one is removing element from queue and other is not, it's looks weird for me if one returns priority and element and other just element.

public void Remove(PriorityQueueNode<TElement, TPriority> node); // O(log n)
`Queue` doesn't have `Remove` method, why `PriorityQueue` should ?


I would also like to see constructor

public PriorityQueue(IEnumerable>コレクション);
`` `

また、 PriorityQueueIReadonlyCollection<>インターフェイスを実装してもらいたいです。

パブリックAPIサーフェス以外のものにジャンプします。

このような機能は、ダイクストラの最短経路アルゴリズムや、優先順位の変更を処理する必要のあるジョブスケジューラなどを実装するために必要です。 更新メカニズムはJavaにありません。これは、エンジニアにとってがっかりすることが示されています。たとえば、32k回以上表示されたこれらの3つのStackOverflowの質問(例、例、例)などです。 このような限られた値のAPIの導入を回避するために、提供する優先度キュー機能の基本的な要件は、コレクションにすでに存在する要素の優先度を更新する機能をサポートすることであると考えています。

反対したいのですが。 前回、C ++でDijkstraを作成しました。std:: priority_queueで十分であり、優先度の更新を処理しません。 その場合のAFAIKの一般的なコンセンサスは、優先度と値が変更された偽の要素をキューに追加し、値を処理するかどうかを監視することです。 ジョブスケジューラでも同じことができます。

正直なところ、現在のキューの提案でダイクストラがどのように見えるかはわかりません。 更新の優先順位が必要なノードを追跡するにはどうすればよいですか? TryGetNode()を使用しますか? または、ノードの別のコレクションがありますか? 現在の提案のコードを見たいです。

ウィキペディアを調べると、優先度付きキューの優先度を更新するという仮定はありません。 その機能を持たず、それを回避した他のすべての言語についても同じです。 「もっと良くなるように努力する」ことは知っていますが、実際にそれに対する需要はありますか?

一般的なランダム入力の場合、より効率的なヒープタイプの2つの例は、4次ヒープとペアリングヒープです。 ヒープの詳細については、ウィキペディアとこのペーパーを参照してください。

私は紙を調べました、そしてこれはそれからの引用です:

結果は、実装の最適な選択が入力に強く依存していることを示しています。 さらに、主にL1-L2バリアで、キャッシュパフォーマンスを最適化するように注意する必要があることを示しています。 これは、複雑でキャッシュを意識しない構造は、単純なキャッシュを意識する構造と比較して、うまく機能する可能性が低いことを示唆しています。

見た目からすると、現在のキューの提案は配列ではなくツリーの実装の背後にロックされ、メモリの周りに散在するツリーノードが要素の配列ほどパフォーマンスが高くない可能性があることがわかります。

配列とペアリングヒープに基づいて単純なバイナリヒープを比較するベンチマークがあると、適切な決定を下すのに理想的だと思います。その前に、特定の実装の背後で設計をロックするのは賢明ではないと思います( Merge探しています)

別のトピックにジャンプして、私は個人的にKeyValuePairを持っていることを好みます新しいカスタムクラスより。

  • APIサーフェスが少ない
  • 私はこのようなことをすることができます: `new PriorityQueue(新しい辞書(){{1、1}、{2、2}、{3、3}、{4、4}、{5、5}}); キーの一意性によって制限されることはわかっています。 また、IDictionaryベースのコレクションを消費することは素晴らしい相乗効果をもたらすと思います。
  • これはクラスではなく構造体であるため、NullReference例外はありません
  • ある時点で、PrioirtyQueueをシリアル化/逆シリアル化する必要があり、既存のオブジェクトを使用する方が簡単だと思います。

欠点はTPriority Key見ると複雑な気持ちですが、良いドキュメントでそれを解決できると思います。
`

ダイクストラの回避策として、キューに偽の要素を追加すると機能し、処理するグラフエッジの総数は変わりません。 ただし、エッジの処理によって生成されたヒープに常駐している一時ノードの数は変化するため、メモリ使用量とエンキューおよびデキューの効率に影響を与える可能性があります。

IReadOnlyCollectionを実行できないことについては間違っていましたが、それで問題ありません。 そのインターフェイスにはAdd()とRemove()はありません。 (私が考えていたことは何でしょう...)

@ Ivanidzo4kaあなたのコメントは、2つの別々のタイプを持つことが理にかなっていることをさらに確信させます。1つは単純なバイナリヒープ(つまり、更新なし)で、もう1つは提案の1つで説明されています。

  • バイナリヒープはシンプルで実装が簡単で、APIが小さく、多くの重要なシナリオで実際にうまく機能することが知られています。
  • 本格的な優先度付きキューは、一部のシナリオ(つまり、スペースの複雑さの軽減、密に接続されたグラフでの検索の時間の複雑さの軽減)に理論上の利点を提供し、より多くのアルゴリズム/シナリオに自然なAPIを提供します。

過去には、10年前に書いたバイナリヒープと、 SortedSet / Dictionary組み合わせをほとんど使用していましたが、使用したシナリオがいくつかあります。両方のタイプが別々の役割を果たします(KSSPが思い浮かびます)。

これはクラスではなく構造体であるため、NullReference例外はありません

私はそれが逆だと主張したいと思います。誰かがデフォルト値を渡している場合、私は彼らにNREを見てもらいたいです。 ただし、これらが使用される場所を明確にする必要があると思います。ノード/ハンドルはおそらくクラスである必要がありますが、ペアを読み取り/返すだけの場合は、構造体である必要があることに同意します。


ハンドルベースの提案では、要素と優先度を更新できるようにする必要があることを提案したいと思います。 ハンドルを削除して新しいハンドルを追加するだけでも同じ効果が得られますが、これは便利な操作であり、実装によってはパフォーマンスが向上する場合があります(たとえば、ヒープによっては、比較的安価に優先度を下げることができます)。 このような変更により、多くのこと(たとえば、既存のAlgoKit ParingHeapに基づくこのやや悪夢を誘発する例)、特に状態空間の未知の領域で動作するものを実装するのがより整然となります。

優先キューの提案(v2.1)

概要

.NET Coreコミュニティは、システムライブラリに_priority queue_機能を追加することを提案しています。これは、各要素に優先度が追加で関連付けられたデータ構造です。 具体的には、 PriorityQueue<TElement, TPriority>System.Collections.Generic名前空間に追加することを提案します。

教義

私たちの設計では、次の教義に導かれました(より良いものを知らない限り):

  • 広い範囲。 .NET Coreのお客様に、さまざまなユースケースをサポートするのに十分な汎用性を備えた貴重なデータ構造を提供したいと考えています。
  • 既知の間違いから学ぶ。 私たちは、Java、Python、C ++、Rustなどの他のフレームワークや言語に存在する顧客向けの問題から解放される優先キュー機能の提供に努めています。 お客様を不満にさせ、優先キューの有用性を低下させることが知られている設計上の選択を行うことは避けます。
  • 一方向ドアの決定に細心の注意を払ってください。 APIが導入されると、変更または削除することはできず、拡張するだけです。 お客様が永遠に立ち往生する次善のソリューションを回避するために、設計の選択を慎重に分析します。
  • 設計の麻痺を避けてください。 完璧な解決策はないかもしれないことを私たちは受け入れます。 トレードオフのバランスを取り、納品を進め、最終的にお客様が長年待ち望んでいた機能を納品します。

バックグラウンド

お客様の視点から

概念的には、優先度キューは要素のコレクションであり、各要素には関連付けられた優先度があります。 優先度付きキューの最も重要な機能は、コレクション内で最も優先度の高い要素への効率的なアクセスと、その要素を削除するオプションを提供することです。 予想される動作には、次のものも含まれます。1)すでにコレクションにある要素の優先度を変更する機能。 2)複数の優先キューをマージする機能。

コンピュータサイエンスの背景

優先度付きキューは抽象的なデータ構造です。つまり、前のセクションで説明したように、特定の動作特性を持つ概念です。 優先度付きキューの最も効率的な実装は、ヒープに基づいています。 ただし、一般的な誤解とは異なり、ヒープは抽象的なデータ構造でもあり、さまざまな方法で実現でき、それぞれに異なるメリットとデメリットがあります。

ほとんどのソフトウェアエンジニアは、配列ベースのバイナリヒープの実装にのみ精通しています。これは最も単純な実装ですが、残念ながら最も効率的な実装ではありません。 一般的なランダム入力の場合、より効率的なヒープタイプの2つの例は、 4次ヒープペアリングヒープです。 ヒープの詳細については、ウィキペディアこのペーパーを参照してください。

更新メカニズムは設計上の重要な課題です

私たちの議論は、設計で最も困難な領域であると同時にAPIに最大の影響を与えるのは、更新メカニズムであることを示しています。 具体的には、お客様に提供したい製品が、コレクションにすでに存在する要素の優先度の更新をサポートする必要があるかどうか、またどのようにサポートするかを決定することが課題です。

このような機能は、ダイクストラの最短経路アルゴリズムや、優先順位の変更を処理する必要のあるジョブスケジューラなどを実装するために必要です。 更新メカニズムがJavaにありません。これは、エンジニアにとってがっかりすることが示されています。たとえば、32k回以上表示されたこれらの3つのStackOverflowの質問(などです。 このような限られた値のAPIの導入を回避するために、提供する優先度キュー機能の基本的な要件は、コレクションにすでに存在する要素の優先度を更新する機能をサポートすることであると考えています。

更新メカニズムを提供するには、お客様が正確に何を更新したいかについて具体的にできるようにする必要があります。 これを実現する2つの方法を特定しました。a)ハンドルを介して。 b)コレクション内の要素の一意性を強制することによって。 これらにはそれぞれ、さまざまなメリットとコストが伴います。

オプション(a):ハンドル。 このアプローチでは、要素がキューに追加されるたびに、データ構造が固有のハンドルを提供します。 顧客が更新メカニズムを使用したい場合は、後で更新する要素を明確に指定できるように、そのようなハンドルを追跡する必要があります。 このソリューションの主なコストは、顧客がこれらのポインターを管理する必要があることです。 ただし、優先キュー内のハンドルをサポートするために内部割り当てが必要であることを意味するわけではありません。非配列ベースのヒープはノードに基づいており、各ノードは自動的に独自のハンドルになります。 例として、 PairingHeap.UpdateメソッドのAPIを参照してください。

オプション(b):一意性。 このアプローチでは、顧客に2つの追加の制約が課せられます。i)優先度キュー内の要素は、特定の同等性セマンティクスに準拠する必要があります。これにより、新しいクラスの潜在的なユーザーバグが発生します。 ii)2つの等しい要素を同じキューに格納することはできません。 このコストを支払うことで、ハンドルアプローチに頼ることなく更新メカニズムをサポートできるというメリットが得られます。 ただし、一意性/同等性を利用して更新する要素を決定する実装では、追加の内部マッピングが必要になるため、O(n)ではなくO(1)で実行されます。

おすすめ

ハンドルを介した更新メカニズムをサポートするPriorityQueue<TElement, TPriority>クラスをシステムライブラリに追加することをお勧めします。 基礎となる実装はペアリングヒープになります。

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

使用例

1)更新メカニズムを気にしないお客様

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)更新メカニズムを気にするお客様

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

よくある質問

1.優先キューはどのような順序で要素を列挙しますか?

未定義の順序で、 HashSet場合と同様に、列挙がO(n)で発生する可能性があります。 効率的な実装では、要素が順番に列挙されることを保証しながら、線形時間でヒープを列挙する機能は提供されません。これには、O(n log n)が必要になります。 コレクションの注文は.OrderBy(x => x.Priority)で簡単に実行でき、すべての顧客がこの注文で列挙を気にするわけではないため、未定義の列挙注文を提供する方がよいと考えています。

2. ContainsまたはTryGetメソッドがないのはなぜですか?

ヒープ内の要素を見つけるにはコレクション全体の列挙が必要であるため、このようなメソッドを提供してもほとんど価値がありません。つまり、 ContainsまたはTryGetメソッドは列挙のラッパーになります。 さらに、要素がコレクションに存在するかどうかを確認するには、優先度付きキューがTElementオブジェクトの同等性チェックを実行する方法を認識している必要があります。これは、優先度付きキューの責任に該当すると思われるものではありません。

3. PriorityQueueNodeを返すDequeueおよびTryDequeueオーバーロードがあるのPriorityQueueNodeなぜですか?

これは、 UpdateまたはRemove方法を使用して、ハンドルを追跡したいお客様向けです。 優先キューから要素をデキューしている間、ハンドル追跡システムの状態を調整するために使用できるハンドルを受け取ります。

4. UpdateまたはRemoveメソッドが別のキューからノードを受信するとどうなりますか?

優先キューは例外をスローします。 LinkedListNode<T>が属するLinkedList<T>認識するのと同様に、各ノードはそれが属する優先キューを認識します。

付録

付録A:優先キュー機能を備えた他の言語

| 言語| タイプ| ノート|
|:-:|:-:|:-:|
| Java | PriorityQueue | 抽象クラスAbstractQueueを拡張し、インターフェースQueueを実装します。 |
| さび| BinaryHeap | |
| スイフト| CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| 行く| ヒープ| ヒープインターフェイスがあります。 |

付録B:発見可能性

データ構造について説明する場合、「ヒープ」という用語は「優先度付きキュー」の4倍の頻度で使用されることに注意してください。

  • "array" AND "data structure" —17.400.000の結果
  • "stack" AND "data structure" —12.100.000の結果
  • "queue" AND "data structure" —3.850.000の結果
  • "heap" AND "data structure" —1.830.000の結果
  • "priority queue" AND "data structure" —430.000件の結果
  • "trie" AND "data structure" —335.000件の結果

フィードバックありがとうございます! 提案をv2.1に更新しました。 変更ログ:

  • メソッドContainsおよびTryGet削除しました。
  • FAQ#2を追加しました:_なぜContainsまたはTryGetメソッドがないのですか?_
  • インターフェイスIReadOnlyCollection<PriorityQueueNode<TElement, TPriority>>追加しました。
  • bool TryPeek(out TElement element, out TPriority priority)オーバーロードを追加しました。
  • bool TryPeek(out TElement element)オーバーロードを追加しました。
  • void Dequeue(out TElement element, out TPriority priority)オーバーロードを追加しました。
  • void Dequeue(out TElement element)PriorityQueueNode<TElement, TPriority> Dequeue()
  • bool TryDequeue(out TElement element)オーバーロードを追加しました。
  • bool TryDequeue(out PriorityQueueNode<TElement, TPriority> node)オーバーロードを追加しました。
  • FAQ#3を追加しました:_なぜPriorityQueueNodeを返すDequeueTryDequeueオーバーロードがあるのですか?_
  • FAQ#4を追加しました:_ UpdateまたはRemoveメソッドが別のキューからノードを受信するとどうなりますか?_

変更メモをありがとう;)

明確化のための小さな要求:

  • キューにない要素にも当てはまるようにFAQ4を修飾できますか? (つまり、削除されたもの)
  • 安定性に関するFAQ、つまり同じ優先度の要素をデキューするときの保証(ある場合)を追加できますか(私の理解では、保証を行う予定はありません。これは、スケジューリングなどで知っておくことが重要です)。

@pgolebiowski提案されたMergeメソッドについて:

public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)

明らかに、そのような操作にはコピーセマンティクスがないので、マージが実行された後(たとえば、いずれかのインスタンスが失敗した後)にthisother変更を加えることに関して何か問題があるのではないかと思います。ヒーププロパティを満たすため)。

@eiriktsarpalis @VisualMelon —ありがとう! 提起されたポイント、ETA2020-10-04に対処します。

他の人がこれ以上のフィードバック/質問/懸念/考えを持っている場合—共有してください😊

優先キューの提案(v2.2)

概要

.NET Coreコミュニティは、システムライブラリに_priority queue_機能を追加することを提案しています。これは、各要素に優先度が追加で関連付けられたデータ構造です。 具体的には、 PriorityQueue<TElement, TPriority>System.Collections.Generic名前空間に追加することを提案します。

教義

私たちの設計では、次の教義に導かれました(より良いものを知らない限り):

  • 広い範囲。 .NET Coreのお客様に、さまざまなユースケースをサポートするのに十分な汎用性を備えた貴重なデータ構造を提供したいと考えています。
  • 既知の間違いから学ぶ。 私たちは、Java、Python、C ++、Rustなどの他のフレームワークや言語に存在する顧客向けの問題から解放される優先キュー機能の提供に努めています。 お客様を不満にさせ、優先キューの有用性を低下させることが知られている設計上の選択を行うことは避けます。
  • 一方向ドアの決定に細心の注意を払ってください。 APIが導入されると、変更または削除することはできず、拡張するだけです。 お客様が永遠に立ち往生する次善のソリューションを回避するために、設計の選択を慎重に分析します。
  • 設計の麻痺を避けてください。 完璧な解決策はないかもしれないことを私たちは受け入れます。 トレードオフのバランスを取り、納品を進め、最終的にお客様が長年待ち望んでいた機能を納品します。

バックグラウンド

お客様の視点から

概念的には、優先度キューは要素のコレクションであり、各要素には関連付けられた優先度があります。 優先度付きキューの最も重要な機能は、コレクション内で最も優先度の高い要素への効率的なアクセスと、その要素を削除するオプションを提供することです。 予想される動作には、次のものも含まれます。1)すでにコレクションにある要素の優先度を変更する機能。 2)複数の優先キューをマージする機能。

コンピュータサイエンスの背景

優先度付きキューは抽象的なデータ構造です。つまり、前のセクションで説明したように、特定の動作特性を持つ概念です。 優先度付きキューの最も効率的な実装は、ヒープに基づいています。 ただし、一般的な誤解とは異なり、ヒープは抽象的なデータ構造でもあり、さまざまな方法で実現でき、それぞれに異なるメリットとデメリットがあります。

ほとんどのソフトウェアエンジニアは、配列ベースのバイナリヒープの実装にのみ精通しています。これは最も単純な実装ですが、残念ながら最も効率的な実装ではありません。 一般的なランダム入力の場合、より効率的なヒープタイプの2つの例は、 4次ヒープペアリングヒープです。 ヒープの詳細については、ウィキペディアこのペーパーを参照してください。

更新メカニズムは設計上の重要な課題です

私たちの議論は、設計で最も困難な領域であると同時にAPIに最大の影響を与えるのは、更新メカニズムであることを示しています。 具体的には、お客様に提供したい製品が、コレクションにすでに存在する要素の優先度の更新をサポートする必要があるかどうか、またどのようにサポートするかを決定することが課題です。

このような機能は、ダイクストラの最短経路アルゴリズムや、優先順位の変更を処理する必要のあるジョブスケジューラなどを実装するために必要です。 更新メカニズムがJavaにありません。これは、エンジニアにとってがっかりすることが示されています。たとえば、32k回以上表示されたこれらの3つのStackOverflowの質問(などです。 このような限られた値のAPIの導入を回避するために、提供する優先度キュー機能の基本的な要件は、コレクションにすでに存在する要素の優先度を更新する機能をサポートすることであると考えています。

更新メカニズムを提供するには、お客様が正確に何を更新したいかについて具体的にできるようにする必要があります。 これを実現する2つの方法を特定しました。a)ハンドルを介して。 b)コレクション内の要素の一意性を強制することによって。 これらにはそれぞれ、さまざまなメリットとコストが伴います。

オプション(a):ハンドル。 このアプローチでは、要素がキューに追加されるたびに、データ構造が固有のハンドルを提供します。 顧客が更新メカニズムを使用したい場合は、後で更新する要素を明確に指定できるように、そのようなハンドルを追跡する必要があります。 このソリューションの主なコストは、顧客がこれらのポインターを管理する必要があることです。 ただし、優先キュー内のハンドルをサポートするために内部割り当てが必要であることを意味するわけではありません。非配列ベースのヒープはノードに基づいており、各ノードは自動的に独自のハンドルになります。 例として、 PairingHeap.UpdateメソッドのAPIを参照してください。

オプション(b):一意性。 このアプローチでは、顧客に2つの追加の制約が課せられます。i)優先度キュー内の要素は、特定の同等性セマンティクスに準拠する必要があります。これにより、新しいクラスの潜在的なユーザーバグが発生します。 ii)2つの等しい要素を同じキューに格納することはできません。 このコストを支払うことで、ハンドルアプローチに頼ることなく更新メカニズムをサポートできるというメリットが得られます。 ただし、一意性/同等性を利用して更新する要素を決定する実装では、追加の内部マッピングが必要になるため、O(n)ではなくO(1)で実行されます。

おすすめ

ハンドルを介した更新メカニズムをサポートするPriorityQueue<TElement, TPriority>クラスをシステムライブラリに追加することをお勧めします。 基礎となる実装はペアリングヒープになります。

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

使用例

1)更新メカニズムを気にしないお客様

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)更新メカニズムを気にするお客様

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

よくある質問

1.優先キューはどのような順序で要素を列挙しますか?

未定義の順序で、 HashSet場合と同様に、列挙がO(n)で発生する可能性があります。 効率的な実装では、要素が順番に列挙されることを保証しながら、線形時間でヒープを列挙する機能は提供されません。これには、O(n log n)が必要になります。 コレクションの注文は.OrderBy(x => x.Priority)で簡単に実行でき、すべての顧客がこの注文で列挙を気にするわけではないため、未定義の列挙注文を提供する方がよいと考えています。

2. ContainsまたはTryGetメソッドがないのはなぜですか?

ヒープ内の要素を見つけるにはコレクション全体の列挙が必要であるため、このようなメソッドを提供してもほとんど価値がありません。つまり、 ContainsまたはTryGetメソッドは列挙のラッパーになります。 さらに、要素がコレクションに存在するかどうかを確認するには、優先度付きキューがTElementオブジェクトの同等性チェックを実行する方法を認識している必要があります。これは、優先度付きキューの責任に該当すると思われるものではありません。

3. PriorityQueueNodeを返すDequeueおよびTryDequeueオーバーロードがあるのPriorityQueueNodeなぜですか?

これは、 UpdateまたはRemove方法を使用して、ハンドルを追跡したいお客様向けです。 優先キューから要素をデキューしている間、ハンドル追跡システムの状態を調整するために使用できるハンドルを受け取ります。

4. UpdateまたはRemoveメソッドが別のキューからノードを受信するとどうなりますか?

優先キューは例外をスローします。 LinkedListNode<T>が属するLinkedList<T>認識するのと同様に、各ノードはそれが属する優先キューを認識します。 さらに、ノードがキューから削除されている場合、そのノードでUpdateまたはRemoveを呼び出そうとすると、例外が発生します。

5. Mergeメソッドがないのはなぜですか?

2つの優先キューのマージは一定時間で実現できるため、顧客に提供するのは魅力的な機能です。 ただし、そのような機能に対する需要があることを示すデータはなく、パブリックAPIに含めることを正当化することはできません。 さらに、そのような機能の設計は重要であり、この機能が必要ない場合があることを考えると、APIの表面と実装を不必要に複雑にする可能性があります。

それでも、 Mergeメソッドを含めないことは双方向の扉です。将来、顧客がマージ機能のサポートに関心を示した場合、 PriorityQueueタイプを拡張することが可能になります。 そのため、 Mergeメソッドをまだ含めず、リリースを進めることをお勧めします。

6.コレクションは安定性を保証しますか?

コレクションは、すぐに使用できる安定性の保証を提供しません。つまり、2つの要素が同じ優先度でキューに入れられている場合、顧客はそれらが特定の順序でキューから取り出されると想定できません。 ただし、お客様がPriorityQueueを使用して安定性を実現したい場合は、それを保証するTPriorityと対応するIComparer<TPriority>を定義できます。 さらに、データ収集は決定論的です。つまり、特定の一連の操作に対して、データ収集は常に同じように動作し、再現性があります。

付録

付録A:優先キュー機能を備えた他の言語

| 言語| タイプ| ノート|
|:-:|:-:|:-:|
| Java | PriorityQueue | 抽象クラスAbstractQueueを拡張し、インターフェースQueueを実装します。 |
| さび| BinaryHeap | |
| スイフト| CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| 行く| ヒープ| ヒープインターフェイスがあります。 |

付録B:発見可能性

データ構造について説明する場合、「ヒープ」という用語は「優先度付きキュー」の4倍の頻度で使用されることに注意してください。

  • "array" AND "data structure" —17.400.000の結果
  • "stack" AND "data structure" —12.100.000の結果
  • "queue" AND "data structure" —3.850.000の結果
  • "heap" AND "data structure" —1.830.000の結果
  • "priority queue" AND "data structure" —430.000件の結果
  • "trie" AND "data structure" —335.000件の結果

変更ログ:

  • メソッドvoid Merge(PriorityQueue<TElement, TPriority> other) // O(1)削除しました。
  • FAQ#5を追加しました:なぜMergeメソッドがないのですか?
  • 優先キューから削除されたノードも保持するようにFAQ#4を変更しました。
  • FAQ#6を追加しました:コレクションは安定性の保証を提供しますか?

新しいFAQは素晴らしく見えます。 提案されたAPIに対して、ハンドル辞書を使用してダイクストラをコーディングしてみましたが、基本的には問題ないようでした。

これを行うことで私が学んだ小さなことの1つは、メソッド名/オーバーロードの現在のセットは、 out変数の暗黙的な型指定ではうまく機能しないということTryDequeue(out var node)が、残念ながら、 out変数の明示的な型をPriorityQueueNode<>指定する必要がありました。そうしないと、コンパイラーは私が優先キューノードまたは要素が必要でした。

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

私の見解では、未解決の主な設計上の質問は、実装が優先度の更新をサポートする必要があるかどうかです。 私はここで3つの可能なパスを見ることができます:

  1. 元の要素を渡すことにより、一意性/同等性を要求し、更新をサポートします。
  2. ハンドルを使用して優先度の更新をサポートします。
  3. 優先度の更新はサポートしません。

これらのアプローチは相互に排他的であり、それぞれ独自のトレードオフがあります。

  1. 平等ベースのアプローチは、一意性を強制することによってAPI契約を複雑にし、内部で追加の簿記を必要とします。 これは、ユーザーが優先度の更新を必要とするかどうかに関係なく発生します。
  2. ハンドルベースのアプローチは、キューに入れられた要素ごとに少なくとも1つの追加の割り当てを意味します。 一意性を強制するものではありませんが、更新が必要なシナリオの場合、この不変条件はほぼ確実に暗黙的です(たとえば、上記の両方のが、要素自体によってインデックス付けされた外部辞書でどのように処理されるかに注意してください)。
  3. 更新はまったくサポートされていないか、ヒープの線形トラバーサルが必要になります。 要素が重複している場合、更新があいまいになる可能性があります。

一般的なPriorityQueueアプリケーションの特定

上記のアプローチのどれがほとんどのユーザーに最高の価値を提供できるかを特定することが重要です。 そのため、最も一般的な使用パターンをよりよく理解するために、ヒープ/ PriorityQueue実装のインスタンスについて、Microsoftの内部およびパブリックの両方の.NETコードベースを調べてきました。

ほとんどのアプリケーションは、ヒープソートまたはジョブスケジューリングのバリエーションを実装しています。 トポロジカルソートやハフマンコーディングなどのアルゴリズムには、いくつかのインスタンスが使用されました。 グラフの距離の計算には、より小さな数値が使用されました。 調査した80のPriorityQueue実装のうち、何らかの形式の優先度更新を実装したのは9つだけでした。

同時に、Python、Java、C ++、Go、Swift、またはRustコアライブラリのいずれのヒープ/ PriorityQueue実装も、APIの優先度更新をサポートしていません。

推奨事項

このデータに照らして、.ΝΕΤには、重要なAPI(heapify / push / peek / pop)を公開し、特定のパフォーマンス保証を提供し(たとえば、エンキューごとに追加の割り当てがない)、強制しないベースラインPriorityQueue実装が必要であることは明らかです。一意性の制約。 これは、実装が_O(log n)優先度の更新をサポートしない_ことを意味します。

また、_O(log n)_の更新/削除をサポートし、等式ベースのアプローチを使用する別のヒープ実装のフォローアップを検討する必要があります。 これは特殊なタイプであるため、一意性の要件はそれほど問題にはなりません。

私は両方の実装のプロトタイピングに取り組んでおり、まもなくAPI提案をフォローアップします。

分析ありがとうございます、@ eiriktsarpalis! 特に、Microsoftの内部コードベースを分析して、関連するユースケースを見つけ、データとの議論をサポートする時間を割いていただき、ありがとうございます。

ハンドルベースのアプローチは、キューに入れられた要素ごとに少なくとも1つの追加の割り当てを意味します。

この仮定は誤りです。ノードベースのヒープに追加の割り当てを行う必要はありません。 ペアリングヒープは、配列ベースのバイナリヒープよりも一般的なランダム入力に対してパフォーマンスが高くなります。つまり、更新をサポートしない優先度キューに対しても、このノードベースのヒープを使用するインセンティブがあります。 先ほど参照

調査した80のPriorityQueue実装のうち、何らかの形式の優先度更新を実装したのは9つだけでした。

この小さなサンプルを与えられたとしても、それはすべての使用量の11〜12%です。 また、これは、ビデオゲームなどの特定のドメインでは過小評価されている可能性があり、この割合はもっと高くなると予想されます。

このデータに照らして、私には明らかです[...]

重要な仮定の1つが誤りであり、顧客の11〜12%が十分に重要なユースケースであるかどうかは議論の余地があることを考えると、そのような結論は明確ではないと思います。 私があなたの評価に欠けているのは、このメカニズムを気にしない顧客のための「更新をサポートするデータ構造」のコストへの影響の評価です。データ構造を使用できるため、このコストはごくわずかです。ハンドル機構の影響を受けません。

基本的:

| | 11〜12%のユースケース| 88〜89%のユースケース|
|:-:|:-:|:-:|
| 更新を気にする| はい| いいえ|
| ハンドルによって悪影響を受ける| 該当なし(必要です)| いいえ|
| ハンドルの影響を受けます| はい| いいえ|

私にとって、これは88〜89%だけでなく、100%のユースケースをサポートすることを支持するのは簡単です。

この仮定は誤りです。ノードベースのヒープに追加の割り当てを行う必要はありません。

優先度と項目の両方が値型である場合(または両方が所有していない参照型であるか、基本型を変更できない場合)、追加の割り当てが不要であることを示す実装にリンクできますか(またはそれがどのように達成されたかを説明してください)? それを見ると役に立ちます。 ありがとう。

もっと詳しく説明したり、言いたいことを言ったりできると助かります。 曖昧さを解消する必要があります。ピンポンがあり、それは長い議論になります。 または、電話を手配することもできます。

呼び出し元側であろうと実装側であろうと、割り当てを必要とするEnqueue操作を回避したいと言っています(実装で使用される配列を拡張するなど、償却された内部割り当ては問題ありません)。 ノードベースのヒープでそれがどのように可能であるかを理解しようとしています(たとえば、これらのノードオブジェクトが呼び出し元に公開されている場合、不適切な再利用/エイリアシングに関する懸念のために実装によるプーリングが禁止されます)。 私は書くことができるようになりたい:
C# pq.Enqueue(42, 84);
割り当てないでください。 あなたが参照する実装はそれをどのように達成しますか?

またはあなたが言おうとしていることを言うだけです

私はそうだと思った。

割り当てを必要とするエンキュー操作を回避したい[...]次のように記述できるようにしたい: pq.Enqueue(42, 84);そしてそれを割り当てないようにします。

この欲求はどこから来るのですか? 99.9%の顧客が満たす必要があるという要件ではなく、ソリューションの副作用があることは素晴らしいことです。 ソリューション間で設計を選択するために、この影響の少ないディメンションを選択する理由がわかりません。

0.1%の顧客が別の次元の顧客の12%に悪影響を与える場合、最適化に基づいて設計を選択することはありません。 「割り当てなしを気にする」+「2つの値型を扱う」はエッジケースです。

サポートされている動作/機能の次元は、特に幅広い対象者とさまざまなユースケース向けの汎用の多用途データ構造を設計する場合に、はるかに重要であると思います。

この欲求はどこから来るのですか?

パフォーマンスを重視するシナリオでコアコレクションタイプを使用できるようにすることから。 ノードベースのソリューションはユースケースの100%をサポートすると言いますが、 List<T>Dictionary<TKey, TValue>HashSet<T>などのように、すべてのエンキューが割り当てられる場合はそうではありません。 onは、すべてのAddに割り当てられた場合、多くの状況で使用できなくなります。

これらのメソッドの割り当てオーバーヘッドを気にするのはなぜ「0.1%」だけだと思いますか? そのデータはどこから来るのですか?

「割り当てなしを気にする」+「2つの値型を扱う」はエッジケースです

そうではない。 また、「2つの値型」だけではありません。 私が理解しているように、提案されたソリューションでは、a)関係するTに関係なく、すべてのエンキューに割り当てるか、b)要素タイプを既知の基本タイプから派生させる必要があります。余分な割り当ては避けてください。

@eiriktsarpalis
したがって、オプションを忘れないでください。リストのオプション1、2、および3に追加する実行可能なオプション4があると思いますが、これは妥協案です。

  1. 12%のユースケースをサポートする実装であり、同等の要素への更新を許可することで他の88%をほぼ最適化し、更新メソッドが最初に呼び出されたときにこれらの更新を実行するために必要なルックアップテーブルを_怠惰に_構築するだけです(サブシーケンスの更新+削除で更新します)。 したがって、この機能を使用しないアプリのコストは低くなります。

更新可能なデータ構造を必要としない、またはそもそも1つに最適化された実装から、88%または12%に利用可能な追加のパフォーマンスがあるため、オプション2および3を提供する方が良いと判断する場合があります。オプション4。しかし、別のオプションが存在することを忘れてはならないと思いました。

[または、これをより良いオプション1と見なし、1の説明を更新して、簿記は強制されないが、怠惰で、更新が使用される場合にのみ正しい同等の動作が必要であると言うことができると思います...]

@stephentoubそれはまさにあなたが言いたいことを簡単に言うことについて私が心に留めていたものです、ありがとう:)

これらのメソッドの割り当てオーバーヘッドを気にするのはなぜ0.1%だけだと思いますか? そのデータはどこから来るのですか?

直感から、つまり、「更新を実行する能力」よりも「追加の割り当てなし」を優先することがより重要であると考える同じソース。 少なくとも更新メカニズムについては、11〜12%のお客様がこの動作をサポートする必要があるというデータがあります。 リモートで近い顧客が「追加の割り当てなし」を気にすることはないと思います。

いずれの場合も、何らかの理由でメモリディメンションに固執することを選択し、他のディメンション、たとえば生の速度を忘れています。これは、優先アプローチのもう1つのトレードオフです。 「追加の割り当てなし」を提供するアレイベースの実装は、ノードベースの実装よりも遅くなります。 繰り返しますが、ここでは速度よりもメモリを優先するのは恣意的だと​​思います。

一歩下がって、顧客が何を望んでいるかに焦点を当てましょう。 私たちは、12%の顧客がデータ構造を使用できなくするかもしれないし、しないかもしれない設計上の選択を持っています。 これらをサポートしないことを選択する理由を提供することには、非常に注意する必要があると思います。

「追加の割り当てなし」を提供するアレイベースの実装は、ノードベースの実装よりも遅くなります。

その比較を実行するために使用している2つのC#実装と、その結論に到達するために使用されたベンチマークを共有してください。 理論的な論文は確かに価値がありますが、それらはパズルのほんの一部にすぎません。 より重要なことは、特定のプラットフォームと特定の実装の詳細を考慮に入れて、ゴムが道路に出会うときです。特定の実装と典型的な/予想されるデータセット/使用パターンを使用して特定のプラットフォームで検証できます。 あなたの主張が正しいことは十分にあり得ます。 また、そうではないかもしれません。 実装/データをよりよく理解するために見たいです。

その比較を実行するために使用している2つのC#実装と、その結論に達するために使用されたベンチマークを共有してください

これは有効なポイントです。私が引用する論文は、C ++での実装の比較とベンチマークのみを行っています。 さまざまなデータセットと使用パターンで複数のベンチマークを実施します。 これはC#に転送できると確信していますが、これを2倍にする必要があると思われる場合は、同僚にそのような調査を依頼するのが最善の策だと思います。

@pgolebiowski私はあなたの異議の性質をよりよく理解することに興味があります。 提案は2つの別々のタイプを提唱していますが、それはあなたの要件をカバーしていませんか?

  1. 12%のユースケースをサポートする実装であり、同等の要素への更新を許可することで他の88%をほぼ最適化し、更新メソッドが最初に呼び出されたときにそれらの更新を実行するために必要なルックアップテーブルを怠惰に構築するだけです(サブシーケンスの更新+削除で更新します)。 したがって、この機能を使用しないアプリのコストは低くなります。

私はおそらくそれをオプション1のパフォーマンス最適化として分類しますが、その特定のアプローチにはいくつかの問題があります。

  • 更新は_O(n)_になり、使用パターンによっては予測できないパフォーマンスが発生する可能性があります。
  • ルックアップテーブルは、一意性を検証するためにも必要です。 Updateを呼び出す前に同じ要素を2回キューに入れることは受け入れられ、おそらくキューを一貫性のない状態にします。

@eiriktsarpalis一度だけO(n)、その後O(1)、つまりO(1)が償却されます。 また、一意性の検証を最初の更新まで延期できます。 しかし、多分これは過度に賢いです。 2つのクラスは説明が簡単です。

私はここ数日、2つのPriorityQueue実装のプロトタイプを作成しました。更新をサポートしない基本的な実装と、要素の同等性を使用して更新をサポートする実装です。 前者をPriorityQueueと名付け、後者はより適切な名前がないため、 PrioritySetと名付けました。 私の目標は、APIの人間工学を測定し、パフォーマンスを比較することです。

実装はこのリポジトリにあります。 どちらのクラスも、配列ベースのクアッドヒープを使用して実装されます。 更新可能な実装では、要素を内部ヒープインデックスにマップするディクショナリも使用します。

基本的な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;
    }
}

タイプを使用した基本的なサンプルは次のとおりです

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

queue.Enqueue("John", 1940);
queue.Enqueue("Paul", 1942);
queue.Enqueue("George", 1943);
queue.Enqueue("Ringo", 1940);

Assert.Equal("John", queue.Dequeue());
Assert.Equal("Ringo", queue.Dequeue());
Assert.Equal("Paul", queue.Dequeue());
Assert.Equal("George", queue.Dequeue());

更新可能なPriorityQueue

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

パフォーマンスの比較

最も基本的なアプリケーションの2つの実装を比較する単純なヒープソートベンチマークを作成しました。 比較のためにLinqを使用するソートベンチマークも含めました。

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

| 方法| サイズ| 平均| エラー| StdDev | 比率| RatioSD | 第0世代| 第1世代| 第2世代| 割り当て済み|
| -------------- | ------ | -------------:| -----------: | -----------:| ------:| --------:| --------:| -------- :| --------:| ----------:|
| LinqSort | 30 | 1.439私たち| 0.0072 us | 0.0064 us | 1.00 | 0.00 | 0.0095 | -| -| 672 B |
| PriorityQueue | 30 | 1.450 us | 0.0085 us | 0.0079 us | 1.01 | 0.01 | -| -| -| -|
| PrioritySet | 30 | 2.778私たち| 0.0217 us | 0.0192 us | 1.93 | 0.02 | -| -| -| -|
| | | | | | | | | | | |
| LinqSort | 300 | 24.727私たち| 0.1032 us | 0.0915 us | 1.00 | 0.00 | 0.0305 | -| -| 3912 B |
| PriorityQueue | 300 | 29.510私たち| 0.0995 us | 0.0882 us | 1.19 | 0.01 | -| -| -| -|
| PrioritySet | 300 | 47.715私たち| 0.4455 us | 0.4168 us | 1.93 | 0.02 | -| -| -| -|
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015私たち| 1.5495 us | 1.3736私たち| 1.00 | 0.00 | 0.4883 | -| -| 36312 B |
| PriorityQueue | 3000 | 491.722 us | 4.1463私たち| 3.8785私たち| 1.19 | 0.01 | -| -| -| -|
| PrioritySet | 3000 | 677.959私たち| 3.1996私たち| 2.4981私たち| 1.64 | 0.01 | -| -| -| -|
| | | | | | | | | | | |
| LinqSort | 30000 | 5,223.560 us | 11.9077私たち| 9.9434私たち| 1.00 | 0.00 | 93.7500 | 93.7500 | 93.7500 | 360910 B |
| PriorityQueue | 30000 | 5,688.625 us | 53.0746 us | 49.6460 us | 1.09 | 0.01 | -| -| -| 2 B |
| PrioritySet | 30000 | 8,124.306 us | 39.9498私たち| 37.3691私たち| 1.55 | 0.01 | -| -| -| 4 B |

予想されるように、要素の場所を追跡するオーバーヘッドにより、パフォーマンスが大幅に低下し、ベースラインの実装と比較して約40〜50%遅くなります。

私はすべての努力に感謝します、私はそれが多くの時間とエネルギーを要したのを見ます。

  1. 2つのほぼ同一のデータ構造の理由はわかりませんが、一方が他方の劣ったバージョンです。
  2. さらに、このような2つのバージョンの優先度付きキューが必要な場合でも、「上位」バージョンが20日前の優先度付きキューの提案(v2.2)よりも優れているかどうかはわかりません。

tl; dr:

  • この提案はエキサイティングです! しかし、それはまだ私の高性能のユースケースには適合していません。
  • 計算幾何学/動作計画のパフォーマンス負荷の90%以上は、アルゴリズムの主要なN ^ m LogNであるため、PQエンキュー/デキューです。
  • 私は個別のPQ実装に賛成です。 私は通常、優先度の更新を必要とせず、2倍悪いパフォーマンスは受け入れられません。
  • PrioritySetは紛らわしい名前であり、PriorityQueueでは検出できません。
  • 優先度を2回(私の要素に1回、キューに1回)保存すると、コストがかかるように感じます。 構造体のコピーとメモリ使用量。
  • 計算の優先度が高い場合は、タプル(priority: ComputePriority(element), element)をPQに格納するだけで、優先度取得関数は単純にtuple => tuple.priorityます。
  • パフォーマンスは、操作ごとに、または実際のユースケース(グラフでの最適化されたマルチスタートマルチエンド検索など)でベンチマークする必要があります。
  • 順序付けられていない列挙動作は予期しないものです。 キューのようなDequeue()の順序セマンティクスを優先しますか?
  • クローン操作とマージ操作のサポートを検討してください。
  • 定常状態での使用では、基本的な操作は0-allocである必要があります。 これらのキューをプールします。
  • プーリングを支援するためにヒープ化を実行するEnqueueManyのサポートを検討してください。

私は、ロボット工学とゲームの両方に関連する高性能検索(モーションプランニング)と計算幾何学コード(スイープラインアルゴリズムなど)で作業しています。多くのカスタムの手動ロール優先キューを使用しています。 私が持っている他の一般的なユースケースは、更新可能な優先度が役に立たないTop-Kクエリです。

2つの実装(更新サポートがないかどうか)に関するいくつかのフィードバックが議論されています。

ネーミング:

  • PrioritySetはセットのセマンティクスを意味しますが、キューはISetを実装していません。
  • PriorityQueueを検索すると見つけやすいUpdatablePriorityQueue名を使用しています。

パフォーマンス:

  • 優先キューのパフォーマンスは、ほとんどの場合、ジオメトリ/計画アルゴリズムにおけるパフォーマンスのボトルネック(> 90%)です。
  • Funcを渡すことを検討してくださいまたは比較TPriorityをコピーするのではなくctorに(高価です!)。 計算の優先度が高い場合は、(優先度、要素)をPQに挿入し、キャッシュされた優先度を確認する比較を渡します。
  • 私のアルゴリズムのかなりの数は、PQの更新を必要としません。 更新をサポートしない組み込みのPQを使用することを検討しますが、必要のない(更新する)機能をサポートするために2倍のパフォーマンスコストがかかる場合、それは私には役に立ちません。
  • パフォーマンス分析/トレードオフについては、操作ごとのエンキュー/デキューの相対的な実時間コストを知ることが重要です@ eiriktsarpalis- 「追跡は2倍遅い」だけでは、PQが有用かどうかを評価するのに十分ではありません。
  • あなたのコンストラクターがHeapifyを実行するのを見てうれしかったです。 列挙型の割り当てを回避するために、IList、List、またはArrayを受け取るコンストラクターについて考えてみます。
  • 高パフォーマンスではコレクションをプールするのが一般的であるため、PQが最初に空の場合にHeapifyを実行するEnqueueManyを公開することを検討してください。
  • 要素に参照が含まれていない場合は、Clear notzero配列を作成することを検討してください。
  • エンキュー/デキューでの割り当ては受け入れられません。 私のアルゴリズムは、パフォーマンス上の理由からゼロ割り当てであり、スレッドローカルコレクションがプールされています。

API:

  • 優先度付きキューのクローンを作成することは、実装では簡単な操作であり、頻繁に役立ちます。

    • 関連:優先度付きキューの列挙には、キューのようなセマンティクスが必要ですか? Queueと同様に、dequeue-orderコレクションが必要です。 new List(myPriorityQueue)はpriorityQueueを変更しないが、今説明したように機能することを期待しています。

  • 上記のように、優先的に挿入するよりも、 Func<TElement, TPriority>を取り込む方が望ましいです。 計算の優先順位が高い場合は、 (priority, element)を挿入して、関数tuple => tuple.priorityを提供するだけです。
  • 2つの優先キューをマージすると便利な場合があります。
  • PeekがTItemを返すのに、Enumeration&Enqueueが(TItem、TPriority)を返すのは奇妙です。

そうは言っても、私のアルゴリズムのかなりの数について、優先度付きキューのアイテムには優先度が含まれており、それを2回(PQに1回、アイテムに1回)保存するのは非効率に聞こえます。 これは、多数のキーで注文している場合に特に当てはまります(OrderBy.ThenBy.ThenByと同様のユースケース)。 このAPIは、Insertが優先されるが、Peekがそれを返さない多くの不整合も解消します。

最後に、配列要素自体ではなく、配列のインデックスを優先度付きキューに挿入することがよくあることは注目に値します。 ただし、これはこれまでに説明したすべてのAPIでサポートされています。 例として、x軸の線で間隔の開始/終了を処理している場合、優先キューイベント(x, isStartElseEnd, intervalId)あり、xで並べ替えてからisStartElseEndで並べ替える場合があります。 これは多くの場合、インデックスから計算されたデータにマップする他のデータ構造があるためです。

@pgolebiowski 3つのアプローチすべてのインスタンスを直接比較できるように、提案されたペアリングヒープの実装をベンチマークに自由に含めることができました。 結果は次のとおりです。

| 方法| サイズ| 平均| エラー| StdDev | 中央値| 第0世代| 第1世代| 第2世代| 割り当て済み|
| -------------- | -------- | -------------------:| ---- -------------:| -----------------:| ---------------- ---:| ----------:| ------:| ------:| -----------:|
| PriorityQueue | 10 | 774.7 ns | 3.30 ns | 3.08 ns | 773.2 ns | -| -| -| -|
| PrioritySet | 10 | 1,643.0 ns | 3.89 ns | 3.45 ns | 1,642.8 ns | -| -| -| -|
| ペアリングヒープ| 10 | 1,660.2 ns | 14.11 ns | 12.51 ns | 1,657.2 ns | 0.0134 | -| -| 960 B |
| PriorityQueue | 50 | 6,413.0 ns | 14.95 ns | 13.99 ns | 6,409.5 ns | -| -| -| -|
| PrioritySet | 50 | 12,193.1 ns | 35.41 ns | 29.57 ns | 12,188.3 ns | -| -| -| -|
| ペアリングヒープ| 50 | 13,955.8 ns | 193.36 ns | 180.87 ns | 13,989.2 ns | 0.0610 | -| -| 4800 B |
| PriorityQueue | 150 | 27,402.5 ns | 76.52 ns | 71.58 ns | 27,410.2 ns | -| -| -| -|
| PrioritySet | 150 | 48,485.8 ns | 160.22 ns | 149.87 ns | 48,476.3 ns | -| -| -| -|
| ペアリングヒープ| 150 | 56,951.2 ns | 190.52 ns | 168.89 ns | 56,953.6 ns | 0.1831 | -| -| 14400 B |
| PriorityQueue | 500 | 124,933.7 ns | 429.20 ns | 380.48 ns | 124,824.4 ns | -| -| -| -|
| PrioritySet | 500 | 206,310.0 ns | 433.97 ns | 338.81 ns | 206,319.0 ns | -| -| -| -|
| ペアリングヒープ| 500 | 229,423.9 ns | 3,213.33 ns | 2,848.53 ns | 230,398.7 ns | 0.4883 | -| -| 48000 B |
| PriorityQueue | 1000 | 284,481.8 ns | 475.91 ns | 445.16 ns | 284,445.6 ns | -| -| -| -|
| PrioritySet | 1000 | 454,989.4 ns | 3,712.11 ns | 3,472.31 ns | 455,354.0 ns | -| -| -| -|
| ペアリングヒープ| 1000 | 459,049.3 ns | 1,706.28 ns | 1,424.82 ns | 459,364.9 ns | 0.9766 | -| -| 96000 B |
| PriorityQueue | 10000 | 3,788,802.4 ns | 11,715.81 ns | 10,958.98 ns | 3,787,811.9 ns | -| -| -| 1 B |
| PrioritySet | 10000 | 5,963,100.4 ns | 26,669.04 ns | 22,269.86 ns | 5,950,915.5 ns | -| -| -| 2 B |
| ペアリングヒープ| 10000 | 6,789,719.0 ns | 134,453.01 ns | 265,397.13 ns | 6,918,392.9 ns | 7.8125 | -| -| 960002 B |
| PriorityQueue | 1000000 | 595,059,170.7 ns | 4,001,349.38 ns | 3,547,092.00 ns | 595,716,610.5 ns | -| -| -| 4376 B |
| PrioritySet | 1000000 | 1,592,037,780.9 ns | 13,925,896.05 ns | 12,344,944.12 ns | 1,591,051,886.5 ns | -| -| -| 288 B |
| ペアリングヒープ| 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 |

重要なポイント

  • 〜ペアリングヒープの実装は、配列に裏打ちされた対応物よりも漸近的にはるかに優れたパフォーマンスを発揮します。 ただし、小さいヒープサイズ(<50要素)では最大2倍遅くなり、約1000要素で追いつきますが、サイズ10 ^ 6〜のヒープでは最大2倍速くなります。
  • 予想どおり、ペアリングヒープは大量のヒープ割り当てを生成します。
  • 「PrioritySet」の実装は一貫して遅い(3つの候補すべての中で最も遅い)ので、結局そのアプローチを追求したくないかもしれません。

〜上記に照らして、ベースラインアレイヒープとペアリングヒープアプローチの間にはまだ有効なトレードオフがあると思います〜。

編集:私のベンチマークのバグ修正後に結果を更新しました、@ VisualMelonに感謝します

@eiriktsarpalis PairingHeapベンチマークは、間違っていると思います。 Addへのパラメーターは間違った方法です。 それらを交換するとき、それは別の話です: https

(最初に実装したときとまったく同じことをしました)

これは、ペアリングヒープが高速または低速であることを意味するのではなく、提供されるデータの分散/順序に大きく依存しているように見えることに注意してください。

@eiriktsarpalis re:PrioritySetの有用性...
シナリオでは優先度の更新がないため、更新可能がヒープソートの速度が遅くなること以外は期待できません。 (ヒープソートの場合も、重複を保持したい可能性があります。セットは適切ではありません。)

PrioritySetが有用かどうかを確認するためのリトマス試験は、優先度の更新を使用するアルゴリズムのベンチマークである必要があります。同じアルゴリズムの非更新実装ではなく、重複する値をキューに入れ、キューから外すときに重複を無視します。

@VisualMelonに感謝します。提案された修正の後で、結果とコメントを更新しました。

むしろ、提供されたデータの分布/順序に大きく依存しているようです。

キューに入れられた優先順位が単調であったという事実から恩恵を受けたのではないかと思います。

PrioritySetが有用かどうかを確認するためのリトマス試験は、優先度の更新を使用するアルゴリズムのベンチマークである必要があります。同じアルゴリズムの非更新実装ではなく、重複する値をキューに入れ、キューから外すときに重複を無視します。

@TimLovellSmithここでの私の目標は、最も一般的なPriorityQueueアプリケーションのパフォーマンスを測定することでした。更新のパフォーマンスを測定するのではなく、更新がまったく必要ない場合の影響を確認したかったのです。 ただし、ペアリングヒープと「PrioritySet」の更新を比較する別のベンチマークを作成することは理にかなっている場合があります。

@miyu詳細なフィードバックをありがとう、それは非常にありがたいです!

@TimLovellSmith更新を使用する簡単なベンチマークを作成しました。

| 方法| サイズ| 平均| エラー| StdDev | 中央値| 第0世代| 第1世代| 第2世代| 割り当て済み|
| ------------ | -------- | ---------------:| ---------- -----:| ---------------:| ---------------:| -------:| ------:| ------:| -----------:|
| PrioritySet | 10 | 1.052 us | 0.0106 us | 0.0099 us | 1.055 us | -| -| -| -|
| ペアリングヒープ| 10 | 1.055 us | 0.0042 us | 0.0035 us | 1.055 us | 0.0057 | -| -| 480 B |
| PrioritySet | 50 | 7.394私たち| 0.0527 us | 0.0493 us | 7.380 us | -| -| -| -|
| ペアリングヒープ| 50 | 8.587私たち| 0.1678 us | 0.1570 us | 8.634私たち| 0.0305 | -| -| 2400 B |
| PrioritySet | 150 | 27.522私たち| 0.0459 us | 0.0359 us | 27.523私たち| -| -| -| -|
| ペアリングヒープ| 150 | 32.045 us | 0.1076 us | 0.1007 us | 32.019 us | 0.0610 | -| -| 7200 B |
| PrioritySet | 500 | 109.097私たち| 0.6548 us | 0.6125 us | 109.162私たち| -| -| -| -|
| ペアリングヒープ| 500 | 131.647私たち| 0.5401 us | 0.4510 us | 131.588私たち| 0.2441 | -| -| 24000 B |
| PrioritySet | 1000 | 238.184私たち| 1.0282 us | 0.9618 us | 238.457私たち| -| -| -| -|
| ペアリングヒープ| 1000 | 293.236 us | 0.9396 us | 0.8789 us | 293.257私たち| 0.4883 | -| -| 48000 B |
| PrioritySet | 10000 | 3,035.982 us | 12.2952私たち| 10.8994私たち| 3,036.985 us | -| -| -| 1 B |
| ペアリングヒープ| 10000 | 3,388.685 us | 16.0675 us | 38.1861私たち| 3,374.565 us | -| -| -| 480002 B |
| PrioritySet | 1000000 | 841,406.888 us | 16,788.4775 us | 15,703.9522 us | 840,888.389 us | -| -| -| 288 B |
| ペアリングヒープ| 1000000 | 989,966.501 us | 19,722.6687 us | 30,705.8191 us | 996,075.410 us | -| -| -| 48000448 B |

別のメモとして、安定性の欠如が人々のユースケースの問題(または非問題)であるという議論/フィードバックがありましたか?

安定性の欠如が人々のユースケースの問題(または非問題)であるという議論/フィードバックがありました

どの実装も安定性を保証しませんが、ユーザーが序数を挿入順序で拡張することによって安定性を得るのは非常に簡単なはずです。

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

以前の投稿のいくつかを要約すると、人気のある.NET優先度キューがどのように見えるかを特定しようとしていたので、次のデータを調べました。

  • .NETソースコードでの一般的な優先キューの使用パターン。
  • 競合するフレームワークのコアライブラリでのPriorityQueueの実装。
  • さまざまな.NET優先キュープロトタイプのベンチマーク。

これにより、次のポイントが得られました。

  • 優先キューのユースケースの90%は、優先度の更新を必要としません。
  • 優先度の更新をサポートすると、APIコントラクトがより複雑になります(ハンドルまたは要素の一意性が必要です)。
  • 私のベンチマークでは、優先度の更新をサポートする実装は、サポートしない実装に比べて2〜3倍遅くなります。

次のステップ

今後、.NET6に対して次のアクションを実行することを提案します。

  1. シンプルで、ユーザー要件の大部分に対応し、可能な限り効率的なSystem.Collections.Generic.PriorityQueueクラスを導入します。 配列に裏打ちされたクォータナリヒープを使用し、優先度の更新をサポートしません。 実装のプロトタイプはここにあります。 API提案の詳細を示す別の問題をまもなく作成します。

  2. 効率的な優先度の更新をサポートするヒープの必要性を認識しているため、この要件に対応する特殊なクラスの導入に向けて引き続き取り組んでいきます。 私たちは、[プロトタイプのカップルを評価している12 、トレードオフの独自のセットをそれぞれ]。 設計を完成させるにはさらに作業が必要になるため、このタイプを後の段階で導入することをお勧めします。

現時点では、このスレッドの寄稿者、特に@pgolebiowskiと@TimLovellSmithに感謝します。 あなたのフィードバックは、私たちの設計プロセスを導く上で大きな役割を果たしました。 更新可能な優先度付きキューの設計を強化するにあたり、引き続きご意見をお待ちしております。

今後、.NET6に対して次のアクションを実行することを提案します。[...]

いいですね :)

シンプルで、ユーザー要件の大部分に対応し、可能な限り効率的なSystem.Collections.Generic.PriorityQueueクラスを導入します。 配列に裏打ちされたクォータナリヒープを使用し、優先度の更新をサポートしません。

コードベースの所有者がこの方向性を承認して希望していると判断した場合、そのビットのAPI設計を引き続き主導し、最終的な実装を提供できますか?

現時点では、このスレッドの寄稿者、特に@pgolebiowskiと@TimLovellSmithに感謝します。 あなたのフィードバックは、私たちの設計プロセスを導く上で大きな役割を果たしました。 更新可能な優先度付きキューの設計を強化するにあたり、引き続きご意見をお待ちしております。

それはかなりの旅でした:D

System.Collections.Generic.PriorityQueue<TElement, TPriority>のAPIが承認されました。 優先度の更新をサポートする潜在的なヒープ実装についての会話を続けるために、別の問題を作成しました。

この号を締めくくります。ご協力ありがとうございました。

たぶん誰かがこの旅について書くことができます! 1つのAPIで全体で6年。 :)ギネスを獲得するチャンスはありますか?

このページは役に立ちましたか?
0 / 5 - 0 評価

関連する問題

matty-hall picture matty-hall  ·  3コメント

iCodeWebApps picture iCodeWebApps  ·  3コメント

bencz picture bencz  ·  3コメント

yahorsi picture yahorsi  ·  3コメント

Timovzl picture Timovzl  ·  3コメント