Runtime: Добавить PriorityQueue<t>в Коллекции</t>

Созданный на 30 янв. 2015  ·  318Комментарии  ·  Источник: dotnet/runtime

Смотрите ПОСЛЕДНИЕ предложения в репозитории corefxlab.

Варианты второго предложения

Предложение от https://github.com/dotnet/corefx/issues/574#issuecomment -307971397

Предположения

Элементы в очереди с приоритетом уникальны. Если это не так, нам придется ввести «дескрипторы» элементов, чтобы разрешить их обновление / удаление. Или семантика обновления / удаления должна применяться к первому / всем, что странно.

Создан по образцу Queue<T> ( ссылка MSDN )

API

`` С #
открытый класс PriorityQueue
: IEnumerable,
IEnumerable <(элемент TElement, приоритет TPriority)>,
IReadOnlyCollection <(элемент TElement, приоритет TPriority)>
// ICollection не включен намеренно
{
общедоступный PriorityQueue ();
общедоступный 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();

//
// Селекторная часть
//
общедоступный PriorityQueue (FuncprioritySelector);
общедоступный PriorityQueue (FuncprioritySelector, IComparerкомпаратор);

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

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

}
`` ''

Открытые вопросы:

  1. Имя класса PriorityQueue vs. Heap
  2. Ввести IHeap и перегрузку конструктора? (Стоит ли нам подождать попозже?)
  3. Ввести IPriorityQueue ? (Стоит ли ждать позже - пример IDictionary )
  4. Использовать селектор (приоритета хранится внутри значения) или нет (разница в 5 API)
  5. Используйте кортежи (TElement element, TPriority priority) vs. KeyValuePair<TPriority, TElement>

    • Должны ли Peek и Dequeue иметь аргумент out вместо кортежа?

  6. Полезны ли вообще метания Peek и Dequeue ?

Первоначальное предложение

Проблема https://github.com/dotnet/corefx/issues/163 требовала добавления очереди приоритетов к основным структурам данных коллекции .NET.

Этот пост, хотя и является дубликатом, предназначен для формального представления в процессе проверки API corefx. Содержимое проблемы - это _speclet_ для нового System.Collections.Generic.PriorityQueueтип.

Я буду способствовать PR, если будет одобрено.

Обоснование и использование

Библиотеки базовых классов .NET (BCL) в настоящее время не поддерживают упорядоченные коллекции производитель-потребитель. Общим требованием для многих программных приложений является возможность с течением времени создавать список элементов и обрабатывать их в порядке, отличном от порядка их получения.

В иерархии пространств имен System.Collections есть три общие структуры данных, которые поддерживают отсортированный набор элементов; System.Collections.Generic.SortedList, System.Collections.Generic.SortedSet и System.Collections.Generic.SortedDictionary.

Из них SortedSet и SortedDictionary не подходят для шаблонов производитель-потребитель, которые генерируют повторяющиеся значения. Сложность SortedList составляет Θ (n) наихудший случай как для добавления, так и для удаления.

Для упорядоченных коллекций с шаблонами использования производитель-потребитель гораздо более эффективная по памяти и времени структура данных - это очередь с приоритетом. За исключением случаев, когда требуется изменение размера емкости, производительность вставки (постановки в очередь) и удаления вершины (удаления из очереди) в худшем случае составляет Θ (log n) - намного лучше, чем существующие параметры, существующие в BCL.

Очереди приоритетов широко применяются в различных классах приложений. Страница Википедии о приоритетных очередях предлагает список множества различных хорошо понятных сценариев использования. Хотя узкоспециализированные реализации могут по-прежнему требовать реализации настраиваемой очереди приоритетов, стандартная реализация может охватывать широкий спектр сценариев использования. Очереди приоритетов особенно полезны при планировании вывода нескольких производителей, что является важным шаблоном в программном обеспечении с высокой степенью параллелизма.

Стоит отметить, что и стандартная библиотека C ++, и Java предлагают функции очереди приоритетов как часть своих базовых API.

Предлагаемый API

`` С #
пространство имен System.Collections.Generic
{
///


/// Представляет коллекцию объектов, которые удаляются в отсортированном порядке.
///

///Задает тип элементов в очереди.
[DebuggerDisplay ("Count = {count}")]
[DebuggerTypeProxy (typeof (System_PriorityQueueDebugView <>))]
открытый класс PriorityQueue: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
///
/// Инициализирует новый экземпляр класс
/// который использует компаратор по умолчанию.
///

общедоступный 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 | Θ (п) | |
| Enqueue | Θ (журнал п) | |
| Dequeue | Θ (журнал п) | |
| Peek | Θ (1) | |
| Граф | Θ (1) | |
| Очистить | Θ (N) | |
| Содержит | Θ (N) | |
| CopyTo | Θ (N) | Использует Array.Copy, фактическая сложность может быть ниже |
| ToArray | Θ (N) | Использует Array.Copy, фактическая сложность может быть ниже |
| GetEnumerator | Θ (1) | |
| Enumerator.MoveNext | Θ (1) | |

  • Дополнительные перегрузки конструктора, которые принимают System.Comparisonделегат были намеренно опущены в пользу упрощенной области API. Абоненты могут использовать Comparer.Create для преобразования функции или лямбда-выражения в IComparerинтерфейс при необходимости. Это действительно требует, чтобы вызывающая сторона произвела единовременное выделение кучи.
  • Хотя System.Collections.Generic еще не является частью corefx, я предлагаю пока добавить этот класс в corefxlab. Его можно переместить в основной репозиторий corefx после добавления System.Collections.Generic и достижения консенсуса в отношении того, что его статус должен быть повышен с экспериментального до официального API.
  • Свойство IsEmpty не было включено, так как при вызове Count отсутствует дополнительная потеря производительности. Большинство структур данных коллекции не включают IsEmpty.
  • Свойства IsSynchronized и SyncRoot ICollection были реализованы явно, поскольку они фактически устарели. Это также следует шаблону, используемому для других структур данных System.Collection.Generic.
  • Dequeue и Peek вызывают исключение InvalidOperationException, когда очередь пуста, чтобы соответствовать установленному поведению System.Collections.Queue.
  • IProducerConsumerCollectionне был реализован, поскольку в его документации указано, что он предназначен только для поточно-ориентированных коллекций.

Открытые вопросы

  • Является ли избежание выделения дополнительной кучи во время вызовов GetEnumerator при использовании foreach достаточно веским основанием для включения вложенной структуры общедоступного перечислителя?
  • Должны ли CopyTo, ToArray и GetEnumerator возвращать результаты в приоритетном (отсортированном) порядке или во внутреннем порядке, используемом структурой данных? Я предполагаю, что внутренний заказ должен быть возвращен, поскольку он не влечет за собой дополнительных штрафов за производительность. Однако это потенциальная проблема удобства использования, если разработчик думает о классе как о «сортированной очереди», а не как о очереди с приоритетами.
  • Вызывает ли добавление типа PriorityQueue в System.Collections.Generic потенциально критического изменения? Пространство имен активно используется и может вызвать проблему совместимости исходного кода для проектов, которые включают собственный тип очереди приоритетов.
  • Должны ли элементы удаляться из очереди в порядке возрастания или убывания, в зависимости от вывода IComparer? (мое предположение - это возрастающий порядок, чтобы соответствовать нормальному соглашению о сортировке IComparer).
  • Должна ли коллекция быть «стабильной»? Другими словами, если два элемента с одинаковым значением IComparisonрезультаты будут исключены из очереди в том же порядке, в котором они были поставлены в очередь? (я предполагаю, что это не нужно)

    Обновления

  • Фиксированная сложность «Construct Using IEnumerable» до Θ (n). Спасибо @svick.

  • Добавлен еще один вариант вопроса о том, следует ли упорядочивать приоритетную очередь в порядке возрастания или убывания по сравнению с IComparer..
  • Удалено NotSupportedException из явного свойства SyncRoot для соответствия поведению других типов System.Collection.Generic вместо использования более нового шаблона.
  • Теперь общедоступный метод GetEnumerator возвращал вложенную структуру Enumerator вместо IEnumerable., аналогично существующим типам System.Collections.Generic. Это оптимизация, позволяющая избежать выделения кучи (GC) при использовании цикла foreach.
  • Удален атрибут ComVisible.
  • Сложность Clear изменена на Θ (n). Спасибо @mbeidler.
api-needs-work area-System.Collections wishlist

Самый полезный комментарий

структура данных кучи ОБЯЗАТЕЛЬНА для выполнения leetcode
больше leetcode, больше интервью по коду C #, что означает больше разработчиков C #.
больше разработчиков означает лучшую экосистему.
лучшая экосистема означает, что если мы еще сможем программировать на C # завтра.

В общем: это не только особенность, но и будущее. именно поэтому вопрос помечен как «будущее».

Все 318 Комментарий

| Операция | Сложность |
| --- | --- |
| Построить с использованием IEnumerable | Θ (журнал п) |

Я думаю, это должно быть (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 ...
    }

Я бросил сюда копию первоначальной реализации. Покрытие тестов далеко не полное, но если кому-то интересно, взгляните и дайте мне знать, что вы думаете. Я старался максимально следовать существующим соглашениям о кодировании из классов System.Collections.

Прохладный. Некоторые первоначальные отзывы:

  • Queue<T>.Enumerator IEnumerator.Reset явно реализует PriorityQueue<T>.Enumerator сделать то же самое?
  • Queue<T>.Enumerator использует _index == -2 чтобы указать, что перечислитель был удален. PriorityQueue<T>.Enumerator имеет тот же комментарий, но имеет дополнительное поле _disposed . Подумайте об избавлении от лишнего поля _disposed и используйте _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 Отличный отзыв, спасибо!

  • Я реализовал Enumerator.Сбросить явно, так как это основная, нерекомендуемая функция перечислителя. Независимо от того, выставлен он или нет, кажется непоследовательным для разных типов коллекций, и только некоторые используют явный вариант.
  • Убрано поле _disposed в пользу _index, спасибо! Бросил это в последнюю минуту той ночью и упустил очевидное. Решено сохранить исключение ObjectDisposedException для корректности с новыми типами коллекций, хотя старые типы System.Collections.Generic его не используют.
  • Array.Emptyэто функция F #, поэтому, к сожалению, здесь ее нельзя использовать!
  • Изменены параметры компаратора, чтобы они принимали значение null, хороший результат!
  • Оптимизация ToArray - непростая задача. _Технически_ говоря, массивы изменяемы в C #, даже если они имеют нулевую длину. На самом деле вы правы, распределение не нужно и его можно оптимизировать. Я склоняюсь к более осторожной реализации, на случай побочных эффектов, о которых я не думаю. Семантически вызывающий по-прежнему будет ожидать этого выделения, и это второстепенное.

@ebickle

Array.Empty - это функция F #, поэтому, к сожалению, здесь ее нельзя использовать!

Больше нет: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Array.cs#L1060 -L1069

Я перенес код на ветке issue-574.

Реализован Array.Empty() изменил и включил все в обычный конвейер сборки. Одна небольшая временная путаница, которую мне пришлось ввести, заключалась в том, что проект System.Collections зависел от пакета Nuget System.Collections, как Comparerеще не с открытым исходным кодом.

Будет исправлено, как только проблема с dotnet / corefx # 966 будет устранена.

Одна из ключевых областей, которую я ищу, - это то, как следует обрабатывать ToArray, CopyTo и перечислитель. В настоящее время они оптимизированы для повышения производительности, что означает, что целевой массив представляет собой кучу и не сортируется компаратором.

Есть три варианта:
1) Оставьте это как есть и задокументируйте, что возвращаемый массив не отсортирован. (это очередь с приоритетом, а не сортированная очередь)
2) Измените методы для сортировки возвращаемых элементов / массивов. Они больше не будут операциями O (n).
3) Полностью удалить методы и перечисляемую поддержку. Это вариант «пуриста», но он исключает возможность быстрого захвата оставшихся в очереди элементов, когда очередь с приоритетом больше не нужна.

Еще одна вещь, по которой я хотел бы получить обратную связь, - должна ли очередь быть стабильной для двух элементов с одинаковым приоритетом (сравните результат 0). Обычно очереди с приоритетом не гарантируют, что два элемента с одинаковым приоритетом будут исключены из очереди в том порядке, в котором они были поставлены в очередь, но я заметил, что реализация внутренней очереди приоритетов, используемая в System.Reactive.Core, потребовала дополнительных накладных расходов для обеспечения этого свойства. Я бы предпочел не делать этого, но я не совсем уверен, какой вариант лучше с точки зрения ожиданий разработчиков.

Я наткнулся на этот PR, потому что я тоже был заинтересован в добавлении очереди приоритетов в .NET. Рад видеть, что кто-то приложил усилия, чтобы сделать это предложение :). Изучив код, я заметил следующее:

  • Когда порядок 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 - ни один из них не уменьшает размер внутреннего массива при удалении / удалении из очереди.

Enqueue должен поддерживать нули и задокументирован как разрешающий их. Вы столкнулись с ошибкой? Я пока не могу вспомнить, насколько надежна область модульных тестов в этой области.

Интересно, что в справочном источнике, на который ссылается @svick , я заметил, что Queue<T> имеет неиспользуемую частную константу с именем _ShrinkThreshold . Возможно, такое поведение существовало и в предыдущей версии.

Что касается использования IComparer вместо Equals в реализации Contains , я написал следующий модульный тест, который в настоящее время не работает: https: //gist.github. com / mbeidler / 9e9f566ba7356302c57e

@mbeidler Хорошее замечание. Согласно MSDN, IComparer/ IComparableтолько гарантирует, что нулевое значение имеет тот же порядок сортировки.

Однако похоже, что такая же проблема существует и в других классах коллекций. Если я изменю код для работы с SortedList, тестовый пример по-прежнему не работает на ContainsKey. Реализация SortedList.ContainsKey вызывает Array.BinarySearch, который использует IComparer для проверки равенства. То же самое и для SortedSet..

Возможно, это ошибка и в существующих классах коллекций. Я покопаюсь в остальных классах коллекций и посмотрю, есть ли какие-либо другие структуры данных, которые принимают IComparer, но проверяют равенство отдельно. Однако вы правы, для очереди с приоритетом можно ожидать настраиваемого поведения упорядочивания, которое полностью не зависит от равенства.

Зафиксировал исправление и тестовый пример в моей ветке вилки. Новая реализация Contains основана непосредственно на поведении List..Содержит. Поскольку списокне принимает IEqualityComparer, поведение функционально эквивалентно.

Когда у меня будет время позже, я, скорее всего, отправлю отчеты об ошибках для других встроенных коллекций. Вероятно, это не может быть исправлено из-за регрессионного поведения, но, по крайней мере, необходимо обновить документацию.

Я думаю, что для ContainsKey имеет смысл использовать реализацию IComparer<TKey> , поскольку это то, что определяет ключ. Однако я думаю, что для ContainsValue было бы более логично использовать Equals вместо IComparable<TValue> в своем линейном поиске; хотя в этом случае область видимости значительно сокращается, поскольку естественный порядок типов с меньшей вероятностью будет несовместим с equals.

Похоже, что в документации MSDN для SortedList<TKey, TValue> раздел примечаний для ContainsValue действительно указывает, что порядок сортировки TValue используется вместо равенства.

@terrajobst Как вы относитесь к предложению API на данный момент? Считаете ли вы, что это хорошо подходит для CoreFX?

: +1:

Спасибо, что заполнили это. Я считаю, что у нас достаточно данных, чтобы провести формальный анализ этого предложения, поэтому я пометил его как «готово к проверке API».

Поскольку Dequeue и Peek бросают методы, вызывающий должен проверять счетчик перед каждым вызовом. Имеет ли смысл вместо (или в дополнение) предоставить TryDequeue и TryPeek по образцу параллельных коллекций? Существуют проблемы, связанные с добавлением методов без метания в существующие общие коллекции, поэтому добавление новой коллекции, в которой нет этих методов, кажется контрпродуктивным.

@andrewgmorris related 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. Вы можете использовать мой код очереди приоритетов отсюда: https://github.com/BrannonKing/Kts.Astar/tree/master/Kts.AStar

@ebickle Одна маленькая гнида на _speclet_. Он говорит:

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

@SunnyWar Исправлена ​​документация метода Enqueue, спасибо!

Некоторое время назад я создал структуру данных со сложностями, подобными очереди с приоритетами, на основе структуры данных Skip List, которой я решил сейчас поделиться: https://gist.github.com/bbarry/5e0f3cc1ac7f7521fe6ea25947f48ace

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

Список пропусков соответствует вышеописанной сложности приоритетной очереди в средних случаях, за исключением того, что Contains - это средний случай O(log(n)) . Кроме того, доступ к первому или последнему элементу является операцией с постоянным временем, а итерация как в прямом, так и в обратном порядке соответствует сложности прямого порядка PQ.

Очевидно, что у такой структуры есть недостатки в виде более высоких затрат на память, и она переходит в наихудший случай вставки и удаления O(n) , поэтому у нее есть свои компромиссы ...

Это где-то уже реализовано? Когда ожидается релиз?
Также как насчет обновления приоритета существующего элемента?

@ Priya91 @ianhays это готово для проверки?

Это отсутствует в предложении API выше: PriorityQueueдолжен реализовать IReadOnlyCollectionсоответствовать очереди(Очередьтеперь реализует IReadOnlyCollectionс .NET 4.6).

Я согласен с @justinvp здесь.

@ Priya91 @ianhays это готово для проверки?

Я бы так сказал. Это сидело какое-то время; давайте делать ходы по нему.

@justinvp @ianhays Я обновил спецификацию, чтобы реализовать IReadOnlyCollection. Спасибо!

У меня есть полная реализация класса и связанный с ним PriorityQueueDebugView, который использует реализацию на основе массива. Модульные тесты еще не охватывают 100%, но если есть какой-то интерес, я могу немного поработать и отряхнуть свою вилку.

@NKnusperer сделал хорошее замечание об обновлении приоритета существующего элемента. Я оставлю это на данный момент, но это то, что нужно учитывать во время обзора спецификаций.

В полной структуре есть 2 реализации, которые являются возможными альтернативами.
https://referencesource.microsoft.com/#q = priorityqueue

Для справки - это вопрос о Java PriorityQueue в stackoverflow http://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueue. Интересно, что приоритет обрабатывается компаратором, а не просто объектом-оболочкой с приоритетом int. Например, изменить приоритет элемента в очереди нелегко.

Обзор API:
Мы согласны с тем, что иметь этот тип в CoreFX полезно, потому что мы ожидаем, что CoreFX будет его использовать.

Для окончательного обзора формы API мы хотели бы увидеть пример кода: PriorityQueue<Thread> и PriorityQueue<MyClass> .

  1. Как сохранить приоритет? Прямо сейчас это подразумевается только T .
  2. Хотим ли мы, чтобы при добавлении записи можно было передавать приоритет? (что кажется довольно удобным)

Примечания:

  • Мы ожидаем, что приоритет не изменяется сам по себе - для этого нам понадобится либо API, либо мы ожидаем в очереди Remove и Add .
  • Учитывая, что у нас здесь нет четкого кода клиента (просто общее желание, чтобы нам нужен тип), трудно решить, для чего оптимизировать - производительность, удобство использования или что-то еще?

Это было бы действительно полезно в CoreFX. Кто-нибудь заинтересован в том, чтобы взять это?

Мне не нравится идея фиксировать приоритетную очередь в двоичной куче. Подробности читайте на моей

  • Если мы фиксируем реализацию к определенному типу кучи, сделайте ее четырехзначной.
  • Мы не должны исправлять реализацию кучи, так как в некоторых сценариях некоторые типы кучи более производительны, чем другие. Таким образом, учитывая, что мы фиксируем реализацию для определенного типа кучи, а клиенту требуется другой для большей производительности, ему потребуется реализовать всю приоритетную очередь с нуля. Это неверно. Здесь мы должны быть более гибкими и позволить ему повторно использовать часть кода CoreFX (по крайней мере, некоторые интерфейсы).

В прошлом году я реализовал несколько типов кучи . В частности, есть интерфейс IHeap который можно использовать как приоритетную очередь.

  • Почему бы просто не назвать это кучей ...? Вы знаете какой-нибудь разумный способ реализовать приоритетную очередь, кроме использования кучи?

Я полностью за представление интерфейса IHeap и некоторых наиболее производительных реализаций (по крайней мере, на основе массивов). API и реализации находятся в репозитории, который я указал выше.

Так что никаких _приоритетных очередей_. Куча .

Что вы думаете?

@karelz @safern @danmosemsft

@pgolebiowski Не забывайте, что мы разрабатываем простой в использовании, но при этом довольно производительный API. Если вам нужен PriorityQueue (устоявшийся термин в области компьютерных наук), вы сможете найти его в документации / через поиск в Интернете.
Если основная реализация - это куча (что мне лично интересно, почему) или что-то еще, это не имеет большого значения. Предполагая, что вы можете продемонстрировать, что реализация значительно лучше, чем более простые альтернативы (сложность кода также является показателем, который в некоторой степени имеет значение).

Поэтому, если вы все еще считаете, что реализация на основе кучи (без IHeap API) лучше, чем какая-то простая реализация на основе списков или списков массивов, объясните, почему (в идеале, в нескольких предложениях / абзацах ), чтобы мы могли согласовать подход к реализации (и не тратить время с вашей стороны на сложную реализацию, которая может быть отклонена во время проверки PR).

Сбросьте ICollection , IEnumerable ? Просто имейте общие версии (хотя общий IEnumerable<T> принесет IEnumerable )

@pgolebiowski, как это реализовано, не меняет внешний api. PriorityQueue определяет поведение / контракт; тогда как Heap - это конкретная реализация.

Приоритетные очереди и кучи

Если основная реализация - это куча (что мне лично интересно, почему) или что-то еще, это не имеет большого значения. Предполагая, что вы можете продемонстрировать, что реализация значительно лучше, чем более простые альтернативы (сложность кода также является показателем, который в некоторой степени имеет значение).

Поэтому, если вы все еще считаете, что реализация на основе кучи лучше, чем какая-то простая реализация на основе списков или списков массивов, объясните, почему (в идеале, в нескольких предложениях / абзацах), чтобы мы могли прийти к соглашению по поводу подход к реализации [...]

OK. Очередь с приоритетом - это абстрактная структура данных, которую затем можно каким-либо образом реализовать. Конечно, вы можете реализовать это со структурой данных, отличной от кучи. Но эффективнее нет способа. Как результат:

  • очередь приоритетов и куча часто используются как синонимы (см. документ Python ниже, как наиболее ясный пример этого)
  • всякий раз, когда у вас есть поддержка «очереди приоритетов» в какой-либо библиотеке, она использует кучу (см. все примеры ниже)

В подтверждение своих слов начнем с теоретического обоснования. Введение в алгоритмы , Кормен:

[…] Очереди с приоритетом бывают двух видов: очереди с максимальным приоритетом и очереди с минимальным приоритетом. Здесь мы сосредоточимся на том, как реализовать очереди с максимальным приоритетом, которые, в свою очередь, основаны на максимальных кучах.

Четко сказано, что очереди с приоритетом - это кучи. Это, конечно, ярлык, но идею вы поняли. Теперь, что более важно, давайте посмотрим, как другие языки и их стандартные библиотеки добавляют поддержку для операций, которые мы обсуждаем:

  • Java: PriorityQueue<T> - приоритетная очередь, реализованная с помощью кучи.
  • Rust: BinaryHeap - куча явно в API. В документации говорится, что это приоритетная очередь, реализованная с помощью двоичной кучи. Но у API очень четкая структура - куча.
  • Swift: CFBinaryHeap - опять же, явно указывает, что такое структура данных, избегая использования абстрактного термина «приоритетная очередь». Документы, описывающие класс: Двоичные кучи могут быть полезны в качестве очередей с приоритетом. Мне нравится подход.
  • C ++: priority_queue - опять же, канонически реализовано с помощью двоичной кучи, построенной на вершине массива.
  • Python: heapq - куча явно отображается в API. Очередь с приоритетом упоминается только в документации: этот модуль обеспечивает реализацию алгоритма очереди кучи, также известного как алгоритм очереди с приоритетом.
  • Go: heap package - есть даже куча интерфейса. Нет явной очереди с приоритетом, но опять же только в документации: куча - это распространенный способ реализации очереди с приоритетом.

Я твердо верю, что мы должны пойти по пути Rust / Swift / Python / Go и явно раскрыть кучу , четко указав в документации, что ее можно использовать в качестве очереди с приоритетом. Я твердо верю, что этот подход очень простой и понятный.

  • У нас есть четкое представление о структуре данных, и мы дадим возможность усовершенствовать API в будущем - если кто-то предложит новый революционный способ реализации очереди с приоритетами, который лучше в некоторых аспектах (и выбор кучи по сравнению с новым типом будет зависеть от сценарий), наш API все еще может быть нетронутым. Мы могли бы просто добавить новый революционный тип, улучшающий библиотеку - и класс кучи по-прежнему существовал бы там.
  • Представьте, что пользователь задается вопросом, стабильна ли выбранная нами структура данных. Когда решение, которому мы следуем, представляет собой очередь с

Я надеюсь, что вам всем понравится подход, когда просто ясно раскрывается структура данных кучи и делается ссылка на приоритетную очередь только в документации.

API и реализация

Нам нужно договориться по вышеупомянутому вопросу, но позвольте мне начать другую тему - как это реализовать . Здесь я вижу два решения:

  • Мы просто предоставляем класс ArrayHeap . По названию сразу видно, с какой кучей вы имеете дело (опять же, есть десятки типов кучи). Вы видите, что это основано на массивах. Вы сразу знаете, с каким чудовищем имеете дело.
  • Другая возможность, которая мне гораздо больше нравится, - это предоставление интерфейса IHeap и предоставление одной или нескольких реализаций. Заказчик мог написать код, зависящий от интерфейсов - это позволило бы обеспечить действительно понятные и читаемые реализации сложных алгоритмов. Представьте, что вы пишете класс DijkstraAlgorithm . Это может просто зависеть от интерфейса IHeap (один параметризованный конструктор) или просто использовать ArrayHeap (конструктор по умолчанию). Чистый, простой, явный, без двусмысленности из-за использования термина «очередь приоритетов». И замечательный интерфейс, который теоретически имеет очень большой смысл.

В вышеприведенном подходе ArrayHeap представляет неявное упорядоченное в куче полное d-арное дерево, хранящееся в виде массива. Это можно использовать, например, для создания BinaryHeap или QuaternaryHeap .

Нижняя линия

Для дальнейшего обсуждения я настоятельно рекомендую вам ознакомиться с этой статьей . Вы будете знать, что:

  • 4-х мерные кучи (четвертичные) просто быстрее, чем двухкомпонентные кучи (бинарные). В статье проводится много тестов - вам будет интересно сравнить производительность implicit_4 и implicit_2 (иногда implicit_simple_4 и implicit_simple_2 ).

  • Оптимальный выбор реализации сильно зависит от ввода. Из неявных д-арных куч, парных куч, куч Фибоначчи, биномиальных куч, явных д-арных куч, куч с парными рангами, куч землетрясений, куч нарушений, слабых куч с ослабленным рангом и куч строгих Фибоначчи, по-видимому, следующие типы покрывают практически все потребности для различных сценариев:

    • Неявные d-арные кучи (ArrayHeap), <- конечно, нам это нужно
    • Объединение куч, <- если мы пойдем на хороший и чистый подход IHeap , стоит добавить это, поскольку во многих случаях это действительно быстро и быстрее, чем решение на основе массивов .

    Для них обоих на удивление мало усилий по программированию. Взгляните на мои реализации.


@karelz @benaadams

Это было бы действительно полезно в CoreFX. Кто-нибудь заинтересован в том, чтобы взять это?

@safern С этого

Хорошо, это была проблема между моей клавиатурой и стулом - PriorityQueue конечно, основано на Heap - я думал только о Queue где это не имеет смысла и забыл, что куча «отсортирована» - очень неловкое отключение процесса мышления для кого-то вроде меня, который любит логику, алгоритмы, машины Тьюринга и т. д., мои извинения. (Кстати: как только я прочитал пару предложений в вашей ссылке на документы Java, сразу же появилось несоответствие)

С этой точки зрения имеет смысл построить API поверх Heap . Однако мы пока не должны делать этот класс общедоступным - для него потребуется отдельная проверка API и его собственное обсуждение, если это то, что нам нужно в CoreFX. Мы не хотим «сползания» поверхности API из-за реализации, но это может быть правильным решением - следовательно, необходимо обсуждение.
С этой точки зрения я не думаю, что нам сейчас нужно создавать IHeap . Это может быть хорошее решение позже.
Если есть исследования, что конкретная куча (например, четырехзначная, как вы упомянули выше) лучше всего подходит для общего случайного ввода , то мы должны выбрать ее. Подождем, пока @safern @ianhays @stephentoub подтвердит /

Параметризация базовой кучи с несколькими реализованными параметрами - это то, что IMO не принадлежит CoreFX (я могу ошибаться здесь, снова - давайте посмотрим, что думают другие).
Моя причина в том, что, IMO, мы скоро отправим миллионы специализированных коллекций, из которых людям (среднему разработчику без сильного опыта в нюансах алгоритмов) будет очень трудно выбирать. Однако такая библиотека могла бы стать отличным пакетом NuGet для экспертов в данной области, принадлежащим вам / сообществу. В будущем мы можем рассмотреть возможность добавления ее в PowerCollections (последние 4 месяца мы активно обсуждаем, где на GitHub разместить эту библиотеку и должны ли мы владеть ею, или мы должны поощрять сообщество к тому, чтобы владеть ею - есть разные мнения по этому поводу. , Надеюсь мы доработаем его судьбу пост 2.0)

Поручая вам, как вы хотите над этим работать ...

Приглашение соавтора команду ping, я смогу назначить его вам (ограничения GitHub).

@benaadams Я бы ICollection (умеренное предпочтение). Для согласованности с другими ds в CoreFX. ИМО, здесь не стоит иметь одного странного зверя ... если бы мы добавляли несколько новых (например, PowerCollections даже в другое репо), мы не должны включать неуниверсальные ... мысли?

Хорошо, это была проблема между моей клавиатурой и стулом.

Ха-ха 😄 Не беспокойся.

имеет смысл построить API поверх Heap. Мы не должны пока делать этот класс общедоступным, хотя [...] Мы не хотим, чтобы поверхность API расползалась из-за реализации, но это может быть правильным решением - следовательно, необходимо обсуждение. [...] Я не думаю, что нам сейчас нужно создавать IHeap. Это может быть хорошее решение позже.

Если группа решит пойти с PriorityQueue , я просто помогу с дизайном и выполню это. Однако, пожалуйста, примите во внимание тот факт, что если мы добавим PriorityQueue сейчас, позже добавление Heap в API будет беспорядочным, поскольку оба ведут себя в основном одинаково. Это было бы своего рода избыточностью ИМО. Для меня это был бы дизайнерский запах. Я бы не стал добавлять приоритетную очередь. Это не помогает.

И еще одна мысль. На самом деле, структура данных кучи сопряжения может пригодиться довольно часто. Кучи на основе массивов ужасны при их объединении. Эта операция в основном линейная . Когда у вас много куч слияния, вы убиваете производительность. Однако, если вы используете кучу сопряжения вместо кучи массива - операция слияния будет постоянной (амортизированной). Это еще один аргумент, почему я хотел бы предоставить хороший интерфейс и две реализации. Один для общего ввода, второй для некоторых конкретных сценариев, особенно когда задействовано слияние куч.

Голосование за IHeap + ArrayHeap + PairingHeap ! 😄 (как в Rust / Swift / Python / Go)

Если куча сопряжения слишком велика - ОК. Но давайте хотя бы возьмем IHeap + ArrayHeap . Разве вам не кажется, что переход с класса PriorityQueue блокирует возможности в будущем и делает API менее понятным?

Но, как я уже сказал, если вы все проголосуете за класс PriorityQueue вместо предложенного решения - ОК.

Приглашение соавтора отправлено - когда вы примете пинг, я смогу назначить его вам (ограничения GitHub).

@karelz ping :)

пожалуйста, примите во внимание тот факт, что если мы добавим PriorityQueue сейчас, позже добавление кучи в API будет беспорядочным, поскольку оба ведут себя в основном одинаково. Это было бы своего рода избыточностью ИМО. Для меня это был бы дизайнерский запах. Я бы не стал добавлять приоритетную очередь. Это не помогает.

Не могли бы вы позже объяснить более подробно, почему это будет беспорядочно? Что вас беспокоит?
PriorityQueue - это концепция, которую используют люди. Иметь такой тип имени полезно, не так ли?
Я думаю, что логические операции (по крайней мере, их названия) над Heap могут быть разными. Если они одинаковы, мы можем иметь 2 разные реализации одного и того же кода в худшем случае (не идеально, но и не конец света). Или мы можем вставить Heap class как родительский для PriorityQueue , верно? (при условии, что это разрешено с точки зрения проверки API - сейчас я не вижу причин для этого, но у меня нет такого многолетнего опыта работы с обзорами API, поэтому буду ждать подтверждения от других)

Давайте посмотрим, как пройдут голосование и дальнейшее обсуждение дизайна ... Я медленно нагреваюсь до идеи IHeap + ArrayHeap , но еще не полностью убежден ...

если бы мы добавляли несколько новых ... мы не должны включать неуниверсальные

Красная тряпка быку. У кого-нибудь есть другие коллекции, которые нужно добавить, чтобы мы могли отбросить ICollection ?

Круговой / кольцевой буфер; Общий и параллельный?

@karelz Решением проблемы именования может быть что-то вроде IPriorityQueue как это делает DataFlow для шаблонов производства / потребителей. Есть много способов реализовать приоритетную очередь, и если вам это не важно, используйте интерфейс. Позаботьтесь о реализации или создайте экземпляр класса реализации.

Не могли бы вы позже объяснить более подробно, почему это будет беспорядочно? Что вас беспокоит?
PriorityQueue - это концепция, которую используют люди. Иметь такой тип имени полезно, не так ли? […] Я постепенно приучаюсь к идее IHeap + ArrayHeap , но еще не полностью убежден ...

@karelz По моему опыту, я считаю очень важным иметь абстракцию ( IPriorityQueue или IHeap ). Благодаря такому подходу разработчик может писать несвязанный код. Поскольку он написан для интерфейса (а не для конкретной реализации), существует большая гибкость и дух IoC. Для такого кода очень легко написать модульные тесты (имея Dependency Injection, можно внедрить свои собственные макеты IPriorityQueue или IHeap и посмотреть, какие методы вызываются, в какое время и с какими аргументами). Абстракции хороши.

Это правда, что термин «приоритетная очередь» используется повсеместно. Проблема в том, что есть только один способ эффективно реализовать приоритетную очередь - с помощью кучи. Множество видов куч. Итак, у нас могло быть:

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

или

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

На мой взгляд, лучше выглядит второй подход. Как вы к этому относитесь?

Что касается класса PriorityQueue - я думаю, что это было бы слишком расплывчато и категорически против такого класса. Расплывчатый интерфейс - это хорошо, но не реализация. Если это двоичная куча, просто назовите ее BinaryHeap . Если это что-то другое, назовите его соответствующим образом.

Я полностью за четкое именование классов из-за проблем с такими классами, как SortedDictionary . Разработчики не понимают, что это означает и чем отличается от SortedList . Если бы он назывался просто BinarySearchTree , жизнь была бы проще, и мы могли бы пойти домой и увидеть своих детей раньше.

Назовем кучу кучей.

@benaadams, каждый из них должен превратиться в CoreFX (то есть он должен быть ценным для самого кода CoreFX, или он должен быть таким же базовым и широко используемым, как те, которые у нас уже есть) - в последнее время мы довольно много обсуждали эти вещи . Все еще не 100% консенсус, но никто не хочет просто добавлять больше коллекций в CoreFX.

Наиболее вероятный результат (предвзятое утверждение, потому что мне нравится решение) состоит в том, что мы создадим еще одно репо и заполним его PowerCollections, а затем позволим сообществу расширить его с некоторыми базовыми рекомендациями / надзором со стороны нашей команды.
Кстати: @terrajobst считает, что оно того не стоит («у нас есть лучшие и более эффективные вещи в экосистеме, которые нужно делать»), и мы должны поощрять сообщество полностью управлять им (в том числе начинать с существующих PowerCollections), а не делать это одним из наших РЕПО - некоторые обсуждения и решения перед нами.
// Думаю, у сообщества есть возможность воспользоваться этим решением, прежде чем мы примем решение ;-). Это сделало бы обсуждение (и мои предпочтения) немым ;-)

@pgolebiowski, вы постепенно убеждаете меня, что иметь Heap лучше, чем PriorityQueue - нам просто нужны строгие рекомендации и документы "Вот как вы делаете PriorityQueue - используйте Heap "... это может сработать.

Однако я очень не решаюсь включать в CoreFX более одной реализации кучи. 98% + «нормальных» разработчиков C # это не волнует. Они даже не хотят думать, какой из них лучше, им просто нужно что-то, что выполняет свою работу. Не каждая часть каждого программного обеспечения разработана с учетом высокой производительности, и это справедливо. Подумайте обо всех одноразовых инструментах, приложениях пользовательского интерфейса и т.д.

Точно так же меня не волнует, как реализованы SortedDictionary или ArrayList или другие ds - они делают свою работу достойно. Я (как и многие другие) понимаю, что если мне нужна высокая производительность этих ds для моих сценариев , мне нужно измерить производительность и / или проверить реализацию и решить, достаточно ли это для моих сценариев , или мне нужно развернуть мою собственную специальную реализацию, чтобы получить лучшую производительность, настроенную для моих нужд.

Мы должны оптимизировать юзабилити для 98% случаев использования, а не для 2%. Если мы развернем слишком много вариантов (реализаций) и заставим всех принять решение, мы просто вызовем ненужную путаницу для 98% случаев использования. Не думаю, что оно того стоит ...
Экосистема IMO .NET имеет большое значение, предлагая единый выбор множества API (не только коллекций) с очень приличными характеристиками производительности, полезными для большинства случаев использования. И предлагает экосистему, которая позволяет создавать высокопроизводительные расширения для тех, кто в них нуждается и желает копать глубже и узнавать больше / делать осознанный выбор и идти на компромиссы.

Тем не менее, наличие интерфейса IHeap (например, IDictionary и IReadOnlyDictionary ) может иметь смысл - мне нужно подумать об этом немного больше / спросить экспертов по обзору API в Космос ...

У нас уже (до некоторой степени) есть то, о чем говорит @pgolebiowski , с ISet<T> и HashSet<T> . Я говорю, просто отразите это. Таким образом, приведенный выше API заменяется интерфейсом ( IPriorityQueue<T> ), а затем у нас есть реализация ( HeapPriorityQueue<T> ), которая внутренне использует кучу, которая может или не может быть публично представлена ​​как собственный класс.

Следует ли ( PriorityQueue<T> ) также реализовать IList<T> ?

@karelz моя проблема с ICollection - SyncRoot и IsSynchronized ; либо они реализованы, что означает дополнительное выделение для объекта блокировки; или они бросают, когда иметь их немного бессмысленно.

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

Но Reset on Enumerator должен быть реализован явно, потому что это плохо, и его нужно просто выбросить.

Итак, мои предложенные изменения

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

Поскольку вы обсуждаете методы ... Мы еще не пришли к соглашению относительно IHeap vs IPriorityQueue thingy - это немного влияет на имена методов и логику. Однако в любом случае я считаю, что в текущем предложении API отсутствует следующее:

  • Возможность удалить определенный элемент из очереди
  • Возможность обновить приоритет элемента
  • Возможность объединения этих структур

Эти операции очень важны, особенно возможность обновления элемента. Без этого многие алгоритмы просто не могут быть реализованы. Нам нужно ввести тип ручки. Здесь в API используется дескриптор IHeapNode . Это еще один аргумент в пользу того, чтобы пойти по пути IHeap , потому что в противном случае нам пришлось бы ввести тип PriorityQueueHandle , который всегда был бы просто узлом кучи ... 😜 Плюс неясно, что это значит ... В то время как узел кучи - все разбираются в вещах и могут представить себе, с чем имеют дело.

На самом деле, если коротко, то для предложения API загляните в этот каталог . Вероятно, нам понадобится только его часть. Но тем не менее - он просто содержит то, что нам нужно ИМО, поэтому, возможно, стоит взглянуть на него как на отправную точку.

Что вы думаете, ребята?

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 решили не добавлять такую ​​базовую структуру данных, как куча (или очередь с приоритетами). Потому что ни один дизайн не хорош. У каждого есть свои недостатки.

Короче говоря, если мы хотим добавить структуру данных, поддерживающую эффективное упорядочение элементов по их приоритету, нам лучше сделать это правильно и добавить такую ​​функциональность, как обновление / удаление элементов из нее.

Если вам не нравится оборачивать элементы в массиве другим объектом - это не единственная проблема. Другой - слияние. С кучей на основе массивов это совершенно неэффективно. Однако ... если мы используем структуру данных кучи сопряжения (куча сопряжения: новая форма самонастраивающейся кучи ), то:

  • дескрипторы элементов - прекрасные узлы - они все равно должны быть выделены, чтобы не было беспорядка
  • слияние является постоянным (по сравнению с линейным в решении на основе массива)

Собственно, это можно было решить следующим образом:

  • Добавить интерфейс IHeap , поддерживающий все методы
  • Добавьте ArrayHeap и PairingHeap со всеми этими дескрипторами, обновление, удаление, слияние
  • Добавьте PriorityQueue который является просто оболочкой вокруг ArrayHeap , упрощая API

PriorityQueue будет в System.Collections.Generic а все кучи - в System.Collections.Specialized .

Работает?

Маловероятно, что мы получим три новые структуры данных через анализ API. В большинстве случаев лучше использовать меньшее количество API. Мы всегда можем добавить больше позже, если этого окажется недостаточно, но мы не можем удалить API.

Это одна из причин, по которой я не являюсь поклонником класса HeapNode. Imo такого рода вещи должны быть только для внутреннего использования, и API должен предоставлять уже существующий тип, если это вообще возможно - в этом случае, вероятно, KVP.

@ianhays Если это будет

Кстати: связанный список имеет класс узла, чтобы пользователи могли использовать правильную функциональность. Это в значительной степени зеркальное отображение.

пользователи не смогут обновлять приоритеты элементов в структуре данных.

Это не обязательно правда. Приоритет может быть представлен таким образом, который не требует дополнительной структуры данных, например, вместо

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

        }

у вас, например,

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

        }

Я еще не уверен в том, чтобы выставлять приоритет как параметр типа. Imo, большинство людей будут использовать тип приоритета по умолчанию, а TValue будет ненужной специфичностью.

void Update(TKey item, TValue priority)

Во-первых, что означает TKey и TValue в вашем коде? Как это должно работать? В компьютерных науках принято следующее:

  • ключ = приоритет
  • значение = все, что вы хотите сохранить в элементе

Второй:

Imo, большинство людей будут использовать тип приоритета по умолчанию, а TValue будет ненужной специфичностью.

Пожалуйста, определите «тип приоритета по умолчанию». Я чувствую, что ты просто хочешь PriorityQueue<T> , верно? Если это так, примите во внимание тот факт, что пользователю, вероятно, придется создать новый класс, оболочку для своего приоритета и значения, а также реализовать что-то вроде IComparable или предоставить собственный компаратор. Довольно бедно.

Во-первых, что такое TKey и TValue в вашем коде?

Элемент и приоритет элемента. Вы можете переключить их на приоритет и элемент, связанный с этим приоритетом, но тогда у вас есть коллекция, в которой TKeys не обязательно уникальны (т. Е. Допускают дублирование приоритетов). Я не против, но для меня TKey обычно подразумевает уникальность.

Дело в том, что предоставление класса Node не является обязательным требованием для раскрытия метода Update.

Пожалуйста, определите «тип приоритета по умолчанию». Я чувствую, что вы просто хотите иметь PriorityQueue, это правильно?

да. Хотя я еще раз перечитываю эту ветку, я не совсем уверен, что это так.

Если это так, учтите тот факт, что пользователю, вероятно, придется создать новый класс, оболочку для своего приоритета и значения, а также реализовать что-то вроде IComparable или предоставить собственный компаратор. Довольно бедно.

Вы не ошиблись. Я представляю себе изрядное количество пользователей будет создать тип обертки с пользовательским компаратором логикой. Я полагаю, что есть также изрядное количество пользователей, у которых уже есть сопоставимый тип, который они хотят поместить в очередь приоритетов, или тип с определенным компаратором. Вопрос в том, какой лагерь больше из двух.

Я думаю, что тип должен называться PriorityQueue<T> , а не Heap<T> , ArrayHeap<T> или даже QuaternaryHeap<T> чтобы оставаться согласованным с остальной частью .Net:

  • У нас List<T> , а не ArrayList<T> .
  • У нас Dictionary<K, V> , а не HashTable<K, V> .
  • У нас ImmutableList<T> , а не ImmutableAVLTreeList<T> .
  • У нас Array.Sort() , а не Array.IntroSort() .

Пользователи таких типов обычно не заботятся о том, как они реализованы, это, конечно, не должно быть самым важным в типе.

Если это так, учтите тот факт, что пользователю, вероятно, придется создать новый класс, оболочку для своего приоритета и значения, а также реализовать что-то вроде IComparable или предоставить собственный компаратор.

Вы не ошиблись. Я предполагаю, что значительному количеству пользователей придется создать тип оболочки с настраиваемой логикой компаратора.

В сводном API сравнение обеспечивается предоставленным компаратором IComparer<T> comparer , оболочка не требуется. Часто приоритет будет частью типа, например, планировщик времени будет иметь время выполнения как свойство типа.

Использование KeyValuePair с пользовательским IComparer добавляет дополнительных выделений; хотя он добавляет одно дополнительное косвенное указание для обновления, поскольку необходимо сравнивать значения, а не элемент.

Обновления были бы проблематичными для элементов структуры, если только элемент не был получен посредством возврата ref, а затем обновлен и передан обратно в метод обновления путем сравнения ref и refs. Но это немного ужасно.

@svick

Я думаю, что тип должен называться PriorityQueue<T> , а не Heap<T> , ArrayHeap<T> или даже QuaternaryHeap<T> чтобы оставаться согласованным с остальной частью .Net.

Я теряю веру в человечество.

  • Вызов структуры данных PriorityQueue согласуется с остальной частью .NET, а вызов Heap - нет? Что-то не так с кучей? То же самое должно применяться к стопкам и очередям.
  • ArrayHeap -> базовый класс для QuaternaryHeap . Огромная разница.
  • Речь идет не о подборе крутого названия. В результате следования определенному пути проектирования возникает множество вещей. Пожалуйста, перечитайте ветку.
  • Хеш-таблица , ArrayList . Кажется, они существуют. Кстати, «список» - это довольно плохой выбор для наименования IMO, поскольку первая мысль пользователя заключается в том, что List - это список, но это не список. 😜
  • Дело не в том, чтобы повеселиться и рассказать, как что-то реализовано. Речь идет о присвоении значимых имен, которые сразу показывают пользователям, с чем они имеют дело.

Хотите оставаться в соответствии с остальной частью .NET и столкнуться с проблемами, подобными этой?

И что я там вижу ... Джон Скит говорит, что SortedDictionary следует называть SortedTree поскольку это более точно отражает реализацию.

@pgolebiowski

Что-то не так с кучей?

Да, здесь описывается реализация, а не использование.

То же самое должно применяться к стопкам и очередям.

Ни один из них на самом деле не описывает реализацию, например, название не говорит вам, что они основаны на массивах.

ArrayHeap -> базовый класс для QuaternaryHeap . Огромная разница.

Да, я понимаю ваш замысел. Я говорю, что ничто из этого не должно открываться пользователю.

В результате следования определенному пути проектирования возникает множество вещей. Пожалуйста, перечитайте ветку.

Я прочитал ветку. Я не думаю, что дизайн принадлежит BCL. Я думаю, что он должен содержать что-то простое в использовании и понимании, а не то, что заставляет людей задаваться вопросом, что такое «четвертичная куча» и следует ли им ее использовать.

Если реализация по умолчанию для них недостаточно хороша, тогда на помощь приходят другие библиотеки (например, ваша собственная).

Хеш-таблица, ArrayList. Кажется, они существуют.

Да, это классы .Net Framework 1.0, которые больше никто не использует. Насколько я могу судить, их имена были скопированы с Java и .Net Framework 2.0, дизайнеры решили не следовать этому соглашению. На мой взгляд, это было правильное решение.

Кстати, «список» - довольно плохой выбор для наименования IMO, поскольку первая мысль пользователя заключается в том, что List - это список, но это не список.

Это. Это не связанный список, но это не одно и то же. И мне нравится не писать везде ArrayList или ResizeArray (имя F # для List<T> ).

Речь идет о присвоении значимых имен, которые сразу показывают пользователям, с чем они имеют дело.

Большинство людей не поймут, с чем имеют дело, если увидят QuaternaryHeap . С другой стороны, если они видят PriorityQueue , должно быть ясно, с чем они имеют дело, даже если у них нет опыта работы с CS. Они не будут знать, что это за реализация, но для этого нужна документация.

@ianhays

Во-первых, что такое TKey и TValue в вашем коде?

Элемент и приоритет элемента. Вы можете переключить их на приоритет и элемент, связанный с этим приоритетом, но тогда у вас есть коллекция, в которой TKeys не обязательно уникальны (т. Е. Допускают дублирование приоритетов). Я не против, но для меня TKey обычно подразумевает уникальность.

Ключи - штучки, используемые для расстановки приоритетов. Ключи не обязательно должны быть уникальными. Мы не можем сделать такое предположение.

Дело в том, что предоставление класса Node не является обязательным требованием для раскрытия метода Update.

Я считаю, что это: поддержка клавиши уменьшения / увеличения в собственных библиотеках . Также по этой теме, поэтому для работы со структурами данных задействованы вспомогательные классы:

Я предполагаю, что значительному количеству пользователей придется создать тип оболочки с настраиваемой логикой компаратора. Я полагаю, что есть также изрядное количество пользователей, у которых уже есть сопоставимый тип, который они хотят поместить в очередь приоритетов, или тип с определенным компаратором. Вопрос в том, какой лагерь больше из двух.

Я не знаю насчет других, но я считаю, что создание оболочки с настраиваемой логикой компаратора каждый раз, когда я хочу использовать очередь с приоритетами, довольно ужасно ...

Еще одна мысль - допустим, я создал обертку:

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

Я кормлю этим типом PriorityQueue<T> . Таким образом, он расставляет приоритеты в своих элементах на основе этого. Допустим, он определил какой-то ключевой селектор. Такая конструкция может нарушить внутреннюю работу кучи. Вы можете изменять приоритеты в любое время , как вы являетесь владельцем элемента. Механизма уведомления об обновлении кучи нет. Вы будете нести ответственность за все это и вызовы. Довольно опасно и не прямолинейно для меня.

В случае предложенного мною дескриптора тип был неизменным с точки зрения пользователя. И проблем с уникальностью не было.

IMO Мне нравится идея иметь интерфейс IHeap а затем API класса, реализующего этот интерфейс. Я согласен с проверки API, и мы хотели бы сосредоточиться на PriorityQueue неважно как зовут.

Во-вторых, я думаю, что нет необходимости иметь внутренний / общедоступный класс Node для хранения значений в очереди. Нет необходимости в дополнительных выделениях, мы могли бы проследить что-то вроде того, что делает IDictionary, и при получении значений для Enumerable, создавая KeyValuePairобъектов, если мы выберем возможность иметь приоритет для каждого элемента, который мы храним (что я не думаю, что это лучший способ, я не полностью уверен в сохранении приоритета для каждого элемента). Лучшим подходом, который я хотел бы, было бы иметь PriorityQueue<T> (это имя просто для того, чтобы дать ему один). При таком подходе у нас может быть конструктор, который следит за тем, что делает весь BCL с IComparer<T> и сравнивает его с этим компаратором, чтобы отдать приоритет. Нет необходимости раскрывать API, которые передают приоритет в качестве параметра.

Я думаю, что получение приоритета для некоторых API сделало бы его «менее удобным» или более сложным для обычных клиентов, которые хотели бы быть приоритетом по умолчанию, тогда мы усложняем понимание его использования, давая им вариант иметь собственный IComparer<T> будет наиболее разумным и будет соответствовать рекомендациям, которые мы имеем для BCL.

Имена - это то, что они делают, а не то, как они это делают.

Они названы в честь абстрактных концепций и того, что они достигают для пользователя, а не их реализации и того, как они этого достигают. (Это также означает, что их реализация может быть улучшена для использования другой реализации, если она окажется лучше)

То же самое должно применяться к стопкам и очередям.

Stack - это массив неограниченного изменения размера, а Queue реализован как кольцевой буфер неограниченного изменения размера. Они названы в честь их абстрактных концепций. Стек (абстрактный тип данных) : последний вошел - первым ушел (LIFO), очередь (абстрактный тип данных) : первым пришел - первым ушел (FIFO)

Хеш-таблица, ArrayList. Кажется, они существуют.

DictionaryEntry

Да, но давайте притворимся, что они этого не делают; это артефакты менее цивилизованных времен. У них нет безопасности типов и ящика, если вы используете примитивы или структуры; поэтому выделяйте на каждое добавление.

Вы можете использовать анализатор совместимости платформ @terrajobst, и он скажет вам: «Пожалуйста, не делайте этого».

Список - это список, но это не список

Это действительно список (абстрактный тип данных), также известный как последовательность.

Equally Priority Queue - это абстрактный тип, который сообщает вам, чего он добивается при использовании; не так, как это происходит в реализации (поскольку может быть много разных реализаций)

Большое обсуждение!

Первоначальная спецификация была разработана с учетом нескольких основных принципов:

  • Общее назначение - охват большинства случаев использования с балансом между потреблением ресурсов ЦП и памяти.
  • По возможности согласовывайте с существующими шаблонами и соглашениями в 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, возвращающих ссылки на «узлы», которые, в свою очередь, (вероятно) потребуют выделения, которые необходимо будет собирать сборщиком мусора. Даже если бы это было решено, это увеличило бы потребление памяти для каждого элемента очереди с приоритетами.

В какой-то момент у меня был собственный интерфейс IPriorityQueue в API, прежде чем я опубликовал спецификацию. В конечном итоге я отказался от этого - шаблон использования, к которому я стремился, был Enqueue, Dequeue и Iterate. Уже охвачено существующим набором интерфейсов. Думая об этом как о внутренней сортированной очереди; пока элементы занимают свою (начальную) позицию в очереди на основе их IComparer, вызывающему абоненту никогда не нужно беспокоиться о том, как представлен приоритет. В старой эталонной реализации, которую я сделал (если я правильно помню!), Приоритет вообще не представлен. Все относительно на основе IComparerили сравнение.

Я в долгу перед вами несколькими примерами клиентского кода - мой первоначальный план состоял в том, чтобы пройти через существующие BCL-реализации PriorityQueue, чтобы использовать их в качестве основы для примеров.

В сводном API сравнение обеспечивается предоставленным компаратором IComparerКомпаратор, обертка не требуется. Часто приоритет будет частью типа, например, планировщик времени будет иметь время выполнения как свойство типа.

Согласно предложенному OP API, для использования класса необходимо выполнить одно из следующих требований:

  • Иметь тип, который уже сопоставим так, как вы хотите
  • Оберните тип другим классом, который содержит значение приоритета и сравнивает его так, как вы хотите
  • Создайте собственный IComparer для своего типа и передайте его через конструктор. Предполагает, что значение, которое вы хотите представить приоритетом, уже открыто для вашего типа.

API двойного типа призван облегчить использование вторых двух вариантов, да? Как работает построение нового PriorityQueue / Heap, если у вас есть тип, который уже содержит приоритет ? Как выглядит API?

Я считаю, что это: поддержка клавиши уменьшения / увеличения в собственных библиотеках.

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

С другой стороны, если вам не нужно эффективно обновлять, одно выделение для каждого элемента в куче является чистыми накладными расходами.

Я хотел бы увидеть решение, в котором вы оплачиваете эти накладные расходы только тогда, когда они вам нужны, но это может быть невозможно реализовать, и полученный API может выглядеть не очень хорошо.

API двойного типа призван облегчить использование вторых двух вариантов, да? Как работает построение нового PriorityQueue / Heap, если у вас есть тип, который уже содержит приоритет? Как выглядит API?

Это хороший момент, в исходном API есть пробел для сценариев, в которых у потребителя уже есть значение приоритета (т.е. целое число), поддерживаемое отдельно от значения. Обратной стороной является то, чтостиль API усложнил бы все внутренне сопоставимые типы. Несколько лет назад я написал очень легкий компонент планировщика, у которого был собственный внутренний класс ScheduledTask. В каждой ScheduledTask реализован IComparer. Бросьте их в приоритетную очередь, готово.

SortedListиспользует дизайн «ключ / значение», и в результате я обнаружил, что избегаю его использования. Он требует, чтобы ключи были уникальными, и мое обычное требование: «У меня есть N значений, которые мне нужно отсортировать в списке, и гарантировать, что случайные значения, которые я добавляю, остаются отсортированными». «Ключ» не является частью этого уравнения, список - это не словарь.

На мой взгляд, тот же принцип применим к очередям. Очередь исторически представляет собой одномерную структуру данных.

Мои современные знания библиотеки std C ++, по общему признанию, немного ржавые, но даже std :: priority_queue, похоже, имеет push, pop и take a comparer в качестве шаблонного (общего) параметра. Команда стандартной библиотеки C ++ чувствительна к производительности настолько, насколько это возможно :)

Я только что провел очень быстрое сканирование реализаций очереди приоритетов и кучи на нескольких языках программирования - C ++, Java, Rust и Go работают с одним типом (аналогично оригинальному API, размещенному здесь). Беглый взгляд на наиболее популярные реализации кучи / приоритетной очереди в NPM показывает то же самое.

@pgolebiowski не поймите меня неправильно, должны быть конкретные реализации вещей, названных явно после их конкретной реализации.

Однако это когда вы знаете, какая конкретная структура данных вам нужна, которая соответствует целям производительности, которые вы преследуете, и имеет компромиссы, которые вы готовы принять.

Как правило, коллекции фреймворков охватывают 90% случаев использования, в которых требуется общее поведение. Затем, если вам нужно очень конкретное поведение или реализация, вы, вероятно, выберете стороннюю библиотеку; и они, надеюсь, будут названы в честь реализации, чтобы вы знали, что она соответствует вашим потребностям.

Просто не хочу привязывать общие типы поведения к конкретной реализации; как тогда странно, если реализация изменится, потому что имя типа должно оставаться прежним, и они не будут совпадать.

Есть много проблем, которые требуют обсуждения, но давайте начнем с той, которая в настоящее время оказывает наибольшее влияние на части, по которым мы не согласны: обновление и удаление элементов .

Как тогда вы хотите поддерживать эти операции? Мы должны их включить, они базовые. В Java дизайнеры их опустили, и в результате:

  1. На форумах есть множество вопросов о том, как найти обходные пути из-за отсутствующих функций.
  2. Существуют сторонние реализации очереди кучи / приоритета для замены собственной реализации, потому что это в значительной степени бесполезно.

Это просто жалко. Есть ли кто-нибудь, кто действительно хочет пойти по этому пути? Мне было бы стыдно выпустить такую ​​отключенную структуру данных.

@pgolebiowski будьте уверены, что все здесь имеют самые лучшие намерения в отношении платформы. Никто не хочет поставлять сломанные API. Мы хотим учиться на чужих ошибках, поэтому, пожалуйста, продолжайте приносить такую ​​полезную релевантную информацию (например, историю о Java).

Однако я хочу отметить несколько моментов:

  • Не ждите изменений в одночасье. Это обсуждение дизайна. Нам нужно найти консенсус. Мы не торопимся с API. Каждое мнение должно быть услышано и учтено, но нет гарантии, что мнение каждого будет реализовано / принято. Если у вас есть вклад, предоставьте его, подкрепите его данными и доказательствами. Также прислушивайтесь к аргументам других, признавайте их. Если вы не согласны, представьте доказательства против мнения других. Иногда можно согласиться с тем, что по некоторым пунктам существуют разногласия. Учтите, что ПО, в т.ч. Дизайн API - это не что-то черно-белое, правильное или неправильное.
  • Давайте вести дискуссию вежливо. Не будем употреблять сильных слов и утверждений. Давайте не будем соглашаться с изяществом и продолжим обсуждение технических вопросов. Каждый может также сослаться на кодекс поведения участников . Мы признаем и поощряем страсть к .NET, но давайте позаботимся о том, чтобы не обижать друг друга.
  • Если у вас есть какие-либо проблемы / вопросы относительно скорости обсуждения дизайна, реакции и т. Д., Не стесняйтесь обращаться ко мне напрямую (моя электронная почта находится в моем профиле GH). Я могу помочь прояснить ожидания, предположения и опасения публично или офлайн, если это необходимо.

Я просто спросил, как вы, ребята, хотите разработать средство обновления / удаления, если вам не нравится предлагаемый подход ... Я считаю, что это прислушивается к другим и ищет консенсуса.

Я не сомневаюсь в ваших добрых намерениях! Иногда имеет значение, как вы спрашиваете - это влияет на то, как люди воспринимают текст с другой стороны. Текст лишен эмоций, поэтому его можно понять по-разному. Английский как второй язык еще больше запутывает ситуацию, и мы все должны это осознавать. Я буду рад обсудить детали в автономном режиме, если вам интересно ... давайте вернем обсуждение сюда, обратно к техническому обсуждению ...

Мои два цента в дебатах кучи и PriorityQueue: оба подхода действительны и явно имеют свои преимущества и недостатки.

Тем не менее, «PriorityQueue» кажется гораздо более совместимым с существующим подходом .NET. Сегодня основными коллекциями являются Список, Словарь, Куча, Очередь, HashSet, SortedDictionaryи SortedSet. Они названы в честь функциональности и семантики, а не алгоритма. HashSetявляется единственным выбросом, но даже это можно рационализировать как относящееся к семантике равенства множеств (по сравнению с SortedSet). В конце концов, у нас есть ImmutableHashSetкоторый основан на дереве под капотом.

Было бы странно, если бы одна коллекция изменила тенденцию здесь.

Думаю, PriorityQueue с дополнительным конструктором: PriorityQueue(IHeap) может быть решением. Конструкторы без параметра IHeap могут использовать кучу по умолчанию.
В этом случае PrioriryQueueбудет представлять абстрактный тип данных (как и большинство коллекций C #) и реализовывать IPriorityQueueинтерфейс, но может использовать разные реализации кучи, такие как @pgolebiowski :

класс ArrayHeap: IHeap {}
class PairingHeap: IHeap {}
class FibonacciHeap: IHeap {}
class BinomialHeap: IHeap {}

OK. Есть много разных голосов. Я снова прошел через обсуждение и уточнил свой подход. Я также обращаю внимание на общие проблемы. В приведенном ниже тексте используются цитаты из сообщений выше.

Наша цель

Конечная цель всего этого обсуждения - предоставить определенный набор функций, чтобы сделать пользователя счастливым. Довольно распространенный случай, когда у пользователя есть набор элементов, некоторые из которых имеют более высокий приоритет, чем другие. В конце концов, они хотят сохранить эту группу элементов в определенном порядке, чтобы иметь возможность эффективно выполнять следующие операции:

  • Получить элемент с наивысшим приоритетом (и иметь возможность удалить его).
  • Добавьте новый элемент в коллекцию.
  • Удалить элемент из коллекции.
  • Измените элемент в коллекции.
  • Объедините две коллекции.

Другие стандартные библиотеки

Авторы других стандартных библиотек также попытались поддержать эту функциональность. В этом разделе я расскажу, как это было решено в Python , Java , C ++ , Go , Swift и Rust .

Ни один из них не поддерживает изменение элементов, уже вставленных в коллекцию. Это чрезвычайно удивительно, потому что очень вероятно, что в течение жизни коллекции приоритеты ее элементов изменятся. Особенно, если такая коллекция предназначена для использования в течение всего срока службы службы. Также существует ряд алгоритмов, которые используют именно эту функцию обновления элементов. Таким образом, такой дизайн просто неправильный, потому что в результате теряются наши клиенты: StackOverflow . Это один из множества подобных вопросов в Интернете. Конструкторы потерпели неудачу.

Также стоит отметить, что каждая библиотека предоставляет эту частичную функциональность за счет реализации двоичной кучи . Что ж, было показано, что это не самая производительная реализация в общем случае (случайный ввод). Лучше всего для общего случая подходит четверная куча (неявное упорядоченное в куче полное 4-арное дерево, хранящееся в виде массива). Это значительно менее известно, и это, вероятно, причина, по которой дизайнеры вместо этого использовали двоичную кучу. Но все же - еще один неудачный выбор, хотя и меньшей степени серьезности.

Что мы узнаем из этого?

  • Если мы хотим, чтобы наши клиенты были довольны и не позволяли им самостоятельно реализовывать эту функцию, игнорируя нашу замечательную структуру данных, мы должны предоставить поддержку для изменения элементов, уже вставленных в коллекцию.
  • Мы не должны предполагать, что из-за того,

Предложил подход

Я твердо убежден, что мы должны предоставить:

  • IHeap<T> интерфейс
  • Heap<T> класс

Интерфейс IHeap , очевидно, будет включать методы для выполнения всех операций, описанных в начале этого сообщения. Класс Heap , реализованный с использованием четвертичной кучи, будет подходящим решением в 98% случаев. Порядок элементов будет основан на IComparer<T> переданном в конструктор, или на порядке по умолчанию, если тип уже сопоставим.

Обоснование

  • Разработчики могут написать свою логику для интерфейса. Я полагаю, что все знают, насколько это важно, и не буду вдаваться в подробности. Читайте: принцип инверсии зависимостей, внедрение зависимостей , проектирование по контракту , инверсия управления .
  • Разработчики могут расширить эту функциональность в соответствии со своими потребностями, предоставив другие реализации кучи. Такие реализации могут быть включены в сторонние библиотеки, такие как PowerCollections . Просто сославшись на такую ​​библиотеку, вы можете просто вставить свою настраиваемую кучу в любую логику, которая принимает IHeap в качестве входных данных. Вот некоторые примеры других куч, которые ведут себя лучше, чем четвертичная куча в некоторых конкретных условиях: сопряженная куча , биномиальная куча и наша любимая двоичная куча.
  • Если разработчику просто нужен инструмент, который выполняет свою работу, не думая о том, какой тип лучше всего, они могут просто использовать универсальную реализацию Heap . Это оптимизирует до 98% случаев использования.
  • Мы добавляем к огромной ценности экосистемы .NET, предлагая одноразовый выбор с очень приличными характеристиками производительности, полезными для большинства случаев использования, в то же время позволяя высокопроизводительные расширения для тех, кто в этом нуждается и готов копать глубже и узнать больше / сделать осознанный выбор и идти на компромиссы.
  • Предлагаемый подход отражает действующие соглашения:

    • ISet и HashSet

    • IList и List

    • IDictionary и Dictionary

  • Некоторые люди заявили, что мы должны называть классы на основе того, что делают их экземпляры, а не того, как они это делают. Это не совсем правда. Это распространенный ярлык, чтобы сказать, что мы должны называть классы в соответствии с их поведением. Это действительно применимо во многих случаях. Однако есть случаи, когда это неправильный подход. Наиболее заметными примерами являются базовые строительные блоки, такие как примитивные типы, типы перечислений или структуры данных. Принцип состоит в том, чтобы просто выбрать значимые имена (т.е. однозначные для пользователя). Примите во внимание тот факт, что обсуждаемая функциональность всегда предоставляется в виде кучи - будь то Python, Java, C ++, Go, Swift или Rust. Куча - одна из самых элементарных структур данных. Heap действительно недвусмысленно и ясно. Он также гармонирует с Stack , Queue , List и Array . Такой же подход к именованию был использован в самых современных стандартных библиотеках (Go, Swift, Rust) - они явно открывают кучу.

@pgolebiowski Я не вижу, как Heap<T> / IHeap<T> называется как Stack<T> , Queue<T> и / или List<T> ? Ни одно из этих имен не объясняет, как они реализованы внутри (как это бывает, массив T).

@SamuelEnglard

Heap тоже не говорит, как это реализовано внутри. Я не понимаю, почему для такого количества людей куча сразу же следует за конкретной реализацией. Для начала существует множество вариантов куч, которые используют один и тот же API:

  • д-рые кучи,
  • 2-3 кучи,
  • левые кучи,
  • мягкие кучи,
  • слабые груды,
  • B-кучи,
  • основание системы счисления,
  • косые кучи,
  • спаривание кучи,
  • Кучи Фибоначчи,
  • биномиальные кучи,
  • кучи ранговых пар,
  • землетрясения
  • нарушения кучи.

Сказать, что мы имеем дело с кучей , все еще очень абстрактно. На самом деле, даже утверждение, что мы имеем дело с четверной кучей, является абстрактным - это может быть реализовано либо как неявная структура данных на основе массива T (например, наши Stack<T> , Queue<T> и List<T> ) или явным (с использованием узлов и указателей).

Короче говоря, Heap<T> очень похож на Stack<T> , Queue<T> и List<T> , потому что это элементарная структура данных, базовый абстрактный строительный блок, который можно реализовать разными способами. Кроме того, как это часто бывает, все они реализованы с использованием массива T внизу. Я считаю это сходство очень сильным.

Имеет ли это смысл?

Кстати, я равнодушен к именованию. Люди, которые привыкли использовать стандартную библиотеку C ++, возможно, предпочтут _priority_queue_. Люди, которые знают о структурах данных, могут предпочесть _Heap_. Если бы мне пришлось голосовать, я бы выбрал _heap_, хотя для меня это почти подбрасывание монеты.

@pgolebiowski Я неправильно сформулировал свой вопрос, это моя Heap<T> не говорит, как это реализовано внутри.

Да Heap - допустимая структура данных, но Heap! = Priority Queue. Оба они открывают разные поверхности API и используются для разных идей. Heap<T> / IHeap<T> должны быть типами данных, которые используются внутри (только теоретические имена) PriorityQueue<T> / IPriorityQueue<T> .

@SamuelEnglard
Что касается организации мира компьютерных наук, то да. Вот уровни абстракции:

  • реализация : неявная четвертичная куча на основе массива
  • абстракция : четвертичная куча
  • абстракция : семейство куч
  • абстракция : семейство приоритетных очередей

И да, имея IHeap и Heap , реализация 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 . Я чувствую, что он проходит через слишком много уровней абстракции и убивает все замечательные преимущества интерфейса.

Мы вернулись к выбору дизайна из середины обсуждения - есть PriorityQueue и IPriorityQueue . Но тогда у нас в основном будет:

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 Как вы относитесь к вышесказанному? Можем ли мы закрепить себя на подходе IHeap + Heap чтобы я мог представить конкретное предложение API?

Я сомневаюсь в необходимости интерфейса (будь то IHeap или IPriorityQueue ). Да, существует множество различных алгоритмов, которые теоретически можно использовать для реализации этой структуры данных. Однако маловероятно, что фреймворк будет поставляться с более чем одним, и идея о том, что разработчикам библиотек действительно нужен канонический интерфейс, чтобы различные стороны могли координировать свои действия при написании кросс-совместимых реализаций кучи, не кажется такой вероятной.

Более того, как только интерфейс выпущен, его уже нельзя будет изменить из-за совместимости. Это означает, что интерфейс имеет тенденцию отставать от конкретных классов с точки зрения функциональности (проблема с IList и IDictionary сегодня). Напротив, если был выпущен класс Heap и возникли серьезные требования к интерфейсу IHeap, я думаю, что этот интерфейс можно было бы добавить в дальнейшем без проблем.

@madelson Я согласен с тем, что фреймворк, скорее всего, не должен поставлять более одной реализации кучи. Однако, поддерживая эту реализацию интерфейсом, те из нас, кто действительно заботится о других реализациях, могут легко заменить другую реализацию (созданную нами самостоятельно или во внешней библиотеке) и при этом быть совместимыми с кодом, принимающим интерфейс.

Я действительно не вижу никаких отрицательных моментов в кодировании интерфейса. Те, кого это не волнует, могут проигнорировать это и сразу перейти к конкретной реализации. Те, кому не все равно, могут использовать любую реализацию по своему выбору. Это выбор. И я хочу этого выбора.

@pgolebiowski Теперь, когда вы спросили, вот мое личное мнение (я несколько уверен, что другие рецензенты / архитекторы API поделятся им, поскольку я проверял мнение некоторых):
Имя должно быть PriorityQueue , и мы не должны вводить интерфейс IHeap . Должна быть только одна реализация (вероятно, через какую-то кучу).
IHeap интерфейс - это очень продвинутый экспертный сценарий - я бы предложил переместить его в библиотеку PowerCollections (которая в конечном итоге будет создана) или любую другую библиотеку.
Если библиотека и интерфейс IHeap станут действительно популярными, мы можем позже передумать и добавить IHeap по запросу (через перегрузку вашего конструктора), но я не думаю, что это полезно / согласовано с остальной частью BCL достаточно, чтобы оправдать сложность добавления нового интерфейса сейчас. Начните с простого, переходите к сложному, только если это действительно необходимо.
... только мои 2 (личных) цента

Учитывая разницу во мнениях, я предлагаю следующий подход для продвижения предложения (который я предложил ранее на этой неделе @ianhays и, следовательно, косвенно @safern):

  • Сделайте 2 альтернативных предложения - одно простое, как я описал выше, и одно с кучей, как вы предлагали, давайте перенесем его на рассмотрение API, обсудим его там и примем решение там.
  • Если я увижу, что хотя бы один человек в этой группе голосует за предложение Heaps, я буду счастлив пересмотреть свою точку зрения.

... пытаясь быть полностью прозрачным в своем мнении (которое вы просили), пожалуйста, не воспринимайте это как разочарование или возражение - давайте посмотрим, как пойдет предложение API.

@karelz

Имя должно быть PriorityQueue

Любой аргумент? Было бы, по крайней мере, хорошо, если бы вы обратились к тому, что я написал выше, вместо того, чтобы просто сказать « нет» .

Думаю, раньше вы довольно хорошо назвали это: _Если у вас есть вклад, предоставьте его, подкрепите его данными и доказательствами. Также прислушивайтесь к аргументам других, признавайте их. Если вы не согласны, предоставьте доказательства против мнения других.

К тому же дело не только в названии . Это не так уж и плохо. Прочтите, пожалуйста, что я написал. Речь идет о работе между уровнями абстракции и возможности улучшать код / ​​строить поверх него.

О, в этой дискуссии был аргумент, почему:

Если мы пойдем с кучей, многие разработчики со средним уровнем квалификации (к сожалению) увидят «кучу» и перепутают ее с кучей памяти приложения (то есть кучей и стеком) вместо «кучи обобщенной структуры данных». [...] они будут удивляться, почему они могут выделить новую кучу памяти.

😛

мы не должны вводить интерфейс IHeap . IHeap интерфейс - очень продвинутый экспертный сценарий - я бы предложил перенести его в библиотеку PowerCollections.

@bendono написал очень хороший комментарий по этому поводу. @safern также хотел поддержать реализацию интерфейсом. И еще несколько.

Еще одно замечание - я не уверен, как вы представляете перенос интерфейса в стороннюю библиотеку. Как разработчики могли написать свой код для этого интерфейса и использовать наши функции? Они были бы вынуждены либо придерживаться нашей неинтерфейсной функциональности, либо полностью игнорировать ее, другого варианта нет, это взаимоисключающий. Другими словами, наше решение вообще не будет расширяемым, в результате люди будут использовать либо наше отключенное решение, либо какую-то стороннюю библиотеку вместо того, чтобы полагаться на одно и то же архитектурное ядро ​​в обоих случаях . Это то, что есть в стандартной библиотеке Java и сторонних решениях.

Но опять же, вы хорошо прокомментировали это: никто не хочет поставлять сломанные API.

Учитывая разницу во мнениях, я предлагаю следующий подход для продвижения предложения. [...] Сделайте 2 альтернативных предложения [...] давайте вынесем это на рассмотрение API, обсудим там и примем решение там. Если я увижу, что хотя бы один человек в этой группе голосует за предложение Heaps, я буду счастлив пересмотреть свою точку зрения.

Было много дискуссий по различным частям API выше. Рассмотрены:

  • PriorityQueue против Heap
  • Добавление интерфейса
  • Поддержка обновления / удаления элементов из коллекции

Почему те люди, которых вы имеете в виду, не могут просто внести свой вклад в это обсуждение? Почему мы должны начать заново?

Если мы пойдем с кучей, многие разработчики со средним уровнем квалификации (к сожалению) увидят «кучу» и перепутают ее с кучей памяти приложения (то есть кучей и стеком) вместо «кучи обобщенной структуры данных».

Ага, это я. Я полностью самоучка в программировании, поэтому я понятия не имею, что было сказано, когда в обсуждение вошло " Heap ". Не говоря уже о том, что даже с точки зрения коллекции, «кучи вещей», для меня более интуитивно это означало бы, что она неупорядочена во всех смыслах.

Не могу поверить, что это действительно происходит ...

Любой аргумент? Было бы, по крайней мере, хорошо, если бы вы обратились к тому, что я написал выше, вместо того, чтобы просто сказать «нет».

Если вы прочитаете мой ответ, то заметите, что я привел ключевые аргументы в пользу своей позиции:

  • Интерфейс IHeap - очень продвинутый экспертный сценарий
  • Я не думаю, что это полезно / согласовано с остальной частью BCL, чтобы оправдать сложность добавления нового интерфейса сейчас.

Те же аргументы, которые повторялись в ветке несколько раз, ИМО. Я только что их резюмировал

. Прочтите, пожалуйста, что я написал.

Я все время активно отслеживал эту ветку. Я прочитал все аргументы и пункты. Это мое обобщенное мнение, несмотря на то, что я прочитал (и понял) все ваши и чужие мнения.
Вы явно увлечены этой темой. Это великолепно. Тем не менее, у меня такое ощущение, что мы оказались в ситуации, когда мы просто повторяем похожие аргументы с каждой стороны, что не приведет к дальнейшему продвижению - поэтому я рекомендовал 2 предложения и получил больше отзывов по ним от более широкой группы опыта. Рецензенты BCL API (Кстати: я еще не считаю себя опытным рецензентом API).

Как разработчики могли написать свой код для этого интерфейса и использовать наши функции?

Разработчики, которым важен расширенный сценарий IHeap будут ссылаться на интерфейс и реализацию из сторонней библиотеки. Как я уже сказал, если он окажется популярным, мы могли бы рассмотреть возможность переноса его в CoreFX позже.
Хорошая новость заключается в том, что добавление IHeap позже вполне выполнимо - оно в основном добавляет только одну перегрузку конструктора для PriorityQueue .
Да, это не идеально с вашей точки зрения, но это не мешает инновациям, которые вы считаете важными в будущем. Я думаю, что это разумная золотая середина.

Почему те люди, которых вы имеете в виду, не могут просто внести свой вклад в это обсуждение?

Обзор API - это встреча с активным обсуждением, мозговым штурмом, взвешиванием со всех сторон. Во многих случаях это более продуктивно / эффективно, чем постоянное обсуждение проблем с GitHub. См. Dotnet / corefx # 14354 и его предшественник. пока мы не сели и не обсудили это и не пришли к консенсусу.
Если рецензенты API отслеживают каждую проблему API, или даже просто болтливую, это плохо масштабируется.

Почему мы должны начать заново?

Мы не будем начинать заново. Почему ты так думаешь?
Мы завершим рассмотрение API на первом уровне (уровне владельца области), отправив 2 самых популярных предложения с плюсами и минусами на следующий уровень обзора API.
Это иерархический подход к утверждениям / проверкам. Это похоже на бизнес-обзоры - вице-президенты / генеральные директора с полномочиями принимать решения не наблюдают за каждым обсуждением каждого проекта в своей компании, они просят свои команды / отчеты вносить предложения по наиболее действенным или заразительным решениям для дальнейшего обсуждения их. Команды / отчеты должны обобщить проблему и представить плюсы / минусы альтернативных решений.

Если вы считаете, что мы не готовы представить последние 2 предложения с плюсами и минусами, потому что есть вещи, о которых еще не говорилось в этой ветке, давайте продолжим обсуждение, пока у нас не будет всего несколько лучших кандидатов для рассмотрения на следующем этапе. Уровень проверки API.
У меня такое ощущение, что все, что нужно было сказать, уже сказано.
Имеет смысл?

Имя должно быть PriorityQueue

Любой аргумент?

Если вы прочитаете мой ответ, то заметите, что я привел ключевые аргументы в пользу своей позиции.

Боже ... Я имел в виду то, что я процитировал ( очевидно ) - вы решили использовать очередь с приоритетом вместо кучи. И да, я прочитал ваш ответ - он содержит ровно 0% аргументов в пользу этого.

Я все время активно отслеживал эту ветку. Я прочитал все аргументы и пункты. Это мое обобщенное мнение, несмотря на то, что я прочитал (и понял) все ваши и чужие мнения.

Вы любите быть гиперболичным, я заметил это раньше. Вы просто подтверждаете наличие баллов.

Как разработчики могли написать свой код для этого интерфейса и использовать наши функции?

Разработчики, которым небезразличен расширенный сценарий IHeap, будут ссылаться на интерфейс и реализацию из сторонней библиотеки. Как я уже сказал, если он окажется популярным, мы могли бы рассмотреть возможность переноса его в CoreFX позже.

Знаете ли вы, что вы просто повторяете здесь мои слова и совсем не затрагиваете проблему, которую я представил как следствие вышеизложенного? Я написал:

Они были бы вынуждены либо придерживаться нашей неинтерфейсной функциональности, либо полностью игнорировать ее, другого варианта нет, это взаимоисключающий.

Я объясню вам это очень ясно. Были бы две непересекающиеся группы людей:

  1. Одна группа, использующая наши функции. Он не имеет интерфейса и не может быть расширен, поэтому он не связан с тем, что предоставляется в сторонних библиотеках.
  2. Вторая группа, которая полностью игнорирует нашу функциональность и использует чисто сторонние решения.

Как я уже сказал, проблема здесь в том, что это несвязные группы людей . И они создают код, который не работает вместе , потому что нет общего архитектурного ядра. _ КОДОВАЯ БАЗА НЕ СОВМЕСТИМА _. Вы не сможете отменить это позже.

Хорошей новостью является то, что добавление IHeap позже вполне выполнимо - в основном это добавляет только одну перегрузку конструктора в PriorityQueue.

Я уже писал, почему это отстой: см. Этот пост .

Почему те люди, которых вы имеете в виду, не могут просто внести свой вклад в это обсуждение?

Если рецензенты API отслеживают каждую проблему API, или даже просто болтливую, это плохо масштабируется.

Да, я спрашивал, почему рецензенты API не могут отслеживать каждую проблему API. Точнее, вы действительно ответили соответствующим образом.

Имеет смысл?

Нет, я действительно устал от этой дискуссии. Некоторые из вас, ребята, явно участвуют в этом просто потому, что это ваша работа, и вам велят это сделать. Некоторым из вас все время нужно руководство, что очень утомительно. Вы даже попросили меня доказать, почему приоритетная очередь должна быть реализована с использованием кучи внутри, явно не имея опыта в области компьютерных наук. Некоторые из вас даже не понимают, что такое куча на самом деле, что делает обсуждение еще более хаотичным.

Используйте отключенный PriorityQueue, который не позволяет обновлять и удалять элементы. Используйте свой дизайн, который не допускает здорового объектно-ориентированного подхода. Используйте свое решение, которое не позволяет повторно использовать стандартную библиотеку при написании расширения. Идите по пути Java.

А это ... это просто сногсшибательно:

Если мы пойдем с кучей, многие разработчики со средним уровнем квалификации (к сожалению) увидят «кучу» и перепутают ее с кучей памяти приложения (то есть кучей и стеком) вместо «кучи обобщенной структуры данных».

Представьте API своим подходом. Мне любопытно.

Не могу поверить, что это действительно происходит ...

Что ж, извините меня за то, что у меня не было возможности получить надлежащее образование в области компьютерных наук, чтобы узнать, что Heap - это какая-то структура данных, отличная от кучи памяти.

Однако точка зрения все еще остается в силе. Куча чего-то не означает, что это было заказано каким-то образом. Если бы мне понадобилась коллекция, которая позволяет мне хранить объекты вещей для обработки, где некоторые экземпляры, которые появятся позже, возможно, потребуется обработать раньше, я бы не стал искать что-то, называемое Heap . PriorityQueue другой стороны, прекрасно сообщает, что он делает именно это.

В качестве вспомогательной реализации? Конечно, детали реализации меня не должны волновать.
Какая-то IHeap абстракция? Отлично подходит для авторов API и людей, у которых есть CS Major, чтобы знать, для чего он используется, без причины не иметь.
Присваивать чему-то загадочное имя, которое не очень хорошо выражает его цель и, следовательно, ограничивает возможность обнаружения? 👎

Что ж, извините меня за то, что у меня не было возможности получить надлежащее образование в области компьютерных наук, чтобы узнать, что куча - это некая структура данных, отличная от кучи памяти.

Это смешно. В то же время вы хотите участвовать в обсуждении добавления подобной функциональности. Похоже на троллинг.

Куча чего-то не означает, что это было заказано каким-то образом.

Ты неправ. Заказывается, в куче. Как на фотографиях, которые вы связали.

В качестве вспомогательной реализации? Конечно, детали реализации меня не должны волновать.

Я уже говорил об этом. Семейство куч огромно, и над реализацией есть два уровня абстракции. Очередь приоритетов - это третий уровень абстракции.

Если бы мне понадобилась коллекция, которая позволяет мне хранить объекты вещей для обработки, где некоторые экземпляры, которые появятся позже, возможно, потребуется обработать раньше, я бы не стал искать что-то, называемое кучей. PriorityQueue, с другой стороны, прекрасно сообщает, что делает именно это.

А без какой-либо предыстории вы бы попросили Google предоставить вам статьи о приоритетных очередях? Что ж, мы можем спорить о том, что более или менее вероятно в нашем мнении. Но, как было прекрасно сказано:

Если у вас есть вклад, предоставьте его, подкрепите его данными и доказательствами. Если вы не согласны, представьте доказательства против мнения других.

И по данным вы ошибаетесь:

Запрос | Хиты
: ----: |: ----: |
| «структура данных» «очередь приоритетов» | 172,000 |
| "структура данных" "куча" | 430,000 |

Почти в 3 раза выше вероятность того, что вы натолкнетесь на кучу, читая о структурах данных. Кроме того, это имя знакомо разработчикам Swift, Go, Rust и Python, потому что их стандартные библиотеки предоставляют такую ​​структуру данных.

Запрос | Хиты
: ----: |: ----: |
| "голанг" "приоритетная очередь" | 3.390 |
| "ржавчина" "приоритетная очередь" | 8.630 |
| «быстрая» «приоритетная очередь» | 18.600 |
| "python" "приоритетная очередь" | 72.800 |
| "голанг" "куча" | 79.000 |
| "ржавчина" "куча" | 492,000 |
| «стремительная» «куча» | 551.000 |
| "питон" "куча" | 555.000 |

На самом деле это также похоже на C ++, потому что структура данных в виде кучи была введена там где-то в прошлом веке.

Присваивать чему-то загадочное имя, которое не очень хорошо выражает его цель и, следовательно, ограничивает возможность обнаружения? 👎

Нет мнений. Данные. См. Выше. Особенно никаких мнений от тех, у кого нет предыстории. Вы также не стали бы Google приоритетной очереди, не прочитав ранее о структурах данных. И куча описана во многих структурах данных 101 .

Это фундамент компьютерных наук. Это элементарно. Когда у вас есть несколько семестров алгоритмов и структур данных, вы видите кучу в начале.

Но все равно:

  • Во-первых, посмотрите на цифры выше.
  • Во-вторых, подумайте обо всех других языках, где куча является частью стандартной библиотеки.

РЕДАКТИРОВАТЬ: см. Google Trends .

Как другой разработчик-самоучка, у меня нет проблем с _heap_. Как разработчик, который всегда стремится к совершенствованию, я нашел время, чтобы изучить и понять все о структурах данных. Короче говоря, я не согласен с тем, что соглашение об именах должно быть нацелено на тех, кто не нашел времени, чтобы понять лексику области, частью которой они являются.

И я также категорически не согласен с такими утверждениями, как «Имя должно быть PriorityQueue». Если вы не хотите, чтобы люди внесли свой вклад, не делайте его открытым и не просите об этом.

Позвольте мне объяснить, как мы относимся к именованию API:

  1. Мы склонны отдавать предпочтение согласованности внутри платформы .NET выше всего остального. Это важно для того, чтобы API выглядели знакомыми и предсказуемыми. Иногда это означает, что мы принимаем, что имя не на 100% правильное, если это термин, который мы использовали раньше.

  2. Наша цель - разработать платформу, доступную широкому кругу разработчиков, некоторые из которых не имели формального образования в области информатики. Мы считаем, что одна из причин, по которой .NET обычно воспринимается как очень производительный и простой в использовании, частично связана с этой особенностью проектирования.

Обычно мы применяем «тест поисковой системы», когда проверяем, насколько хорошо известно и признано имя или терминология. Поэтому я высоко ценю исследование, проведенное @pgolebiowski . Я сам не проводил исследования, но чувствую, что «куча» - это не тот термин, который искали бы многие эксперты, не относящиеся к предметной области.

Следовательно, я склонен согласиться с @karelz, что PriorityQueue выглядит лучшим выбором. Он объединяет существующую концепцию (очередь) и добавляет изюминку, которая выражает желаемую возможность: упорядоченное извлечение на основе приоритета. Но мы не безоговорочно привязаны к этому имени. Мы часто меняли названия структур данных и технологий на основе отзывов клиентов.

Однако хочу отметить, что это:

Если вы не хотите, чтобы люди внесли свой вклад, не делайте его открытым и не просите об этом.

это ложная дихотомия. Дело не в том, что нам не нужна обратная связь от нашей экосистемы и участников (очевидно, что мы хотим). Но в то же время мы также должны понимать, что наша клиентская база весьма разнообразна и что участники GitHub (или разработчики в нашей команде) не всегда являются лучшим прокси для всех наших клиентов. Удобство использования - это сложно, и, вероятно, потребуется несколько итераций для добавления новых концепций в .NET, особенно в таких очень популярных областях, как коллекции.

@pgolebiowski :

Я высоко ценю вашу проницательность, данные и предложения. Но мне совершенно не нравится ваш стиль аргументации. Вы лично поговорили как с членами моей команды, так и с членами сообщества в этой ветке. Тот факт, что вы не согласны с нами, не дает вам права обвинять нас в том, что мы не обладаем опытом или безразличием, потому что это «просто наша работа». Учтите, что многие из нас буквально переместились по миру, оставили семьи и друзей только потому, что хотели заниматься этой работой. Комментарии, подобные вашему, не только очень несправедливы, но и не способствуют продвижению дизайна.

Так что, хотя мне нравится думать, что у меня толстая кожа, я не терплю такого поведения. Наш домен уже достаточно сложен; нам не нужно добавлять конфронтационное и враждебное общение.

Пожалуйста, будьте вежливы. Страстно критиковать идеи - это честная игра, а нападать на людей - нет. Спасибо.

Дорогие все, кто расстроен,

Прошу прощения за то, что мое резкое отношение снизило ваш уровень счастья.

@karelz

Я выставил себя дураком, допустив техническую ошибку. Я извинился. Это было принято. Но бросили на меня позже. Не круто, ИМО.

Мне очень жаль, что то, что я написал, сделало вас несчастным. Хотя это было не так уж плохо, как вы это описали - я просто указал это как один из многих факторов, которые способствовали моему чувству усталости . Думаю, он менее серьезен. Но все же мне очень жаль.

И да, все делают ошибки. Все хорошо. Я тоже, например, иногда увлекаюсь.

Что меня больше всего поразило, так это «вам просто говорят сделать это, вы не верите» - да, именно поэтому я делаю это ТАКЖЕ по выходным.

Мне очень жаль, я вижу, что вы много работаете, и я это очень ценю. Мне было видно, насколько вы были особенно преданы делу на этапе 5/10.

@terrajobst

Тот факт, что вы не согласны с нами, не дает вам права обвинять нас в том, что мы не обладаем опытом или безразличием, потому что это «просто наша работа».

  • Отсутствие опыта - адресовано людям без образования в области компьютерных наук и не понимающим концепции кучи / приоритетных очередей. Если такое описание относится к кому-то - ну, применимо, это не моя вина.
  • Безразличие - адресовано тем, кто склонен игнорировать некоторые технические моменты, тем самым заставляя повторять аргументы, как таковые, делая обсуждение хаотичным и усложняющим отслеживание другим людям (что, в свою очередь, приводит к меньшему вкладу).

Комментарии, подобные вашему, очень несправедливы и не способствуют продвижению дизайна.

  • Мои резкие комментарии были результатом неэффективности этого обсуждения. Неэффективность = хаотичный способ обсуждения, когда вопросы не решаются / не решаются, и мы идем дальше, несмотря на это => утомительно.
  • Кроме того, как один из ключевых факторов в обсуждении, я твердо уверен, что сделал много для продвижения дизайна. Пожалуйста, не демонизируйте меня, как вы пытаетесь делать здесь и в социальных сетях.

Если есть больной кто-нибудь, кто захочет «лизать меня», пожалуйста, не стесняйтесь.


Мы столкнулись с проблемой и решили ее. Каждый чему-то научился. Я вижу, что всех здесь беспокоит качество структуры, что абсолютно замечательно и побуждает меня вносить свой вклад. Я с нетерпением жду продолжения совместной с вами работы над CoreFX. При этом, возможно, завтра я обращусь к вашим новым техническим предложениям.

@pgolebiowski

Надеюсь, когда-нибудь мы сможем встретиться лично. Я искренне верю, что часть проблемы, связанной с тем, чтобы делать все в Интернете, заключается в том, что личности иногда могут смешиваться плохим образом без каких-либо намерений с обеих сторон.

Я с нетерпением жду продолжения совместной с вами работы над CoreFX. При этом, возможно, завтра я обращусь к вашим новым техническим предложениям.

То же самое. Это интересное место, и мы можем сделать много удивительных вещей вместе :-)

@pgolebiowski во- первых, спасибо за ответ. Это показывает вашу заботу и хорошие намерения (я втайне надеюсь, что это делает каждый человек / разработчик в мире, все конфликты - это просто недопонимание / недопонимание). И это делает меня по-настоящему счастливым - это поддерживает меня и воодушевляет.
Я предлагаю начать наши отношения заново. Давайте вернем обсуждение к техническим вопросам, давайте все узнаем из этой ветки, как справляться с подобными ситуациями в будущем, давайте все снова предположим, что другая сторона имеет в виду только наилучшие интересы для платформы.
Кстати: это одна из немногих более сложных встреч / дискуссий по репо CoreFX за последние 9 месяцев, и, как вы видите, мы (включая меня) все еще учимся, как с ними справляться, так что этот конкретный случай будет чтобы принести пользу даже нам, и это сделает нас лучше в будущем и поможет нам лучше понять различные точки зрения от страстных членов сообщества. Может быть, это повлияет на наши обновления документации ...

Мои резкие комментарии были результатом неэффективности этого обсуждения. Неэффективность = хаотичный способ обсуждения, когда вопросы не решаются / не решаются, и мы идем дальше, несмотря на это => утомительно.

Понял ваше разочарование! Интересно, что подобное разочарование было и с другой стороны по той же причине 😉 ... почти забавно, как устроен мир :).
К сожалению, трудное обсуждение является частью работы, когда вы принимаете дизайнерское решение. Это ОЧЕНЬ много работы. Многие недооценивают это. Ключевой требуемый навык - это терпение по отношению ко всем и способность превзойти собственное мнение и подумать, как достичь консенсуса, даже если это не идет вам на пользу. Вот почему я предложил сделать 2 предложения и «передать» техническое обсуждение группе рецензентов API (в основном потому, что я не знаю наверняка, что я прав, хотя втайне надеюсь, что я прав, как и любой другой разработчик в мире 😉 ).

ОЧЕНЬ сложно иметь мнение по теме и вести обсуждение в КОНСЕНСУС в той же ветке. С этой точки зрения у нас с вами есть наиболее общие точки зрения на эту тему - у нас обоих есть мнения, но мы оба пытаемся довести обсуждение до завершения и принятия решения. Так что давайте работать вместе.

Мой общий подход: всякий раз, когда я думаю, что кто-то нападает на меня, злой, ленивый, расстраивает меня или что-то в этом роде. Я сначала спрашиваю себя, а также конкретного человека: почему? Почему ты это сказал? Что ты имел в виду?
Обычно это признак непонимания / передачи мотивов. Или признак того, что вы слишком много читаете между строк и видите оскорбления / обвинения / злые намерения там, где их нет.


Теперь, когда я не боюсь обсуждать технические вопросы, вот что я хотел задать ранее:

Используйте отключенный PriorityQueue, который не позволяет обновлять и удалять элементы.

Этого я не понимаю. Если мы опустим IHeap (в моем / исходном предложении здесь), почему это будет невозможно?
IMO нет никакой разницы между двумя предложениями с точки зрения возможностей класса, единственная разница в том, добавляем ли мы перегрузку конструктора PriorityQueue(IHeap) или нет (оставляя спор имени класса в стороне как независимую проблему для решения) .

Заявление об ограничении ответственности (во избежание недопонимания): у меня нет времени читать статьи и проводить исследования, я ожидаю краткого ответа, представляющего аргументы в пользу лифта от тех, кто хочет вести техническую дискуссию. Примечание: это не я троллинг. Я бы задал тот же вопрос любому члену нашей команды, делающему это заявление. Если у вас нет сил объяснять это / вести дискуссию (что было бы совершенно понятно, учитывая трудности и затраты времени с вашей стороны), просто скажите об этом, без обид. Пожалуйста, не испытывайте давления со стороны меня или кого-либо (и это относится ко всем участникам обсуждения).

Не пытаясь добавить сюда еще один ненужный комментарий, эта ветка была СЛИШКОМ ДЛИННОЙ. Правило № 1 эпохи Интернета: избегайте текстового общения, если вам важны отношения между людьми. (Ну, это я придумал). Я полагаю, что какое-то другое сообщество разработчиков ПО с открытым исходным кодом переключится на Google Hangout для такого рода обсуждений, если в этом возникнет необходимость. Когда вы смотрите на лица других людей, вы никогда не скажете ничего «оскорбительного», и люди очень быстро знакомятся друг с другом. Может, мы тоже попробуем?

@karelz Из-за продолжительности обсуждения выше маловероятно, что кто-то из

  • Я буду проводить голосование по принципиальным моментам, одно за другим. У нас будет четкий вклад сообщества. В идеале, рецензенты API тоже придут сюда и в ближайшее время прокомментируют.
  • «Сообщения для голосования» будут содержать достаточно информации, чтобы можно было игнорировать всю стену текста выше.
  • После завершения этого сеанса голосования мы будем знать, чего ожидать от рецензентов API, и сможем перейти к определенному подходу. Когда мы договоримся о фундаментальных аспектах, этот вопрос будет закрыт, а другой откроется (который будет ссылаться на этот). В новом выпуске я обобщу наши выводы и предоставлю предложение API, которое отражает эти решения. И мы возьмем это оттуда.

Есть ли в этом смысл?

PriorityQueue, не позволяющий обновлять и удалять элементы.

Речь шла об исходном предложении, в котором не было этих возможностей :) Извините за то, что не пояснил.

Если у вас нет сил объяснять это / вести дискуссию (что было бы совершенно понятно, учитывая трудности и затраты времени с вашей стороны), просто скажите об этом, без обид. Пожалуйста, не испытывайте давления со стороны меня или кого-либо (и это относится ко всем участникам обсуждения).

Я не сдамся. Без боли нет выигрыша xD

@ xied75

Я полагаю, что какое-то другое сообщество разработчиков ПО с открытым исходным кодом переключится на Google Hangout для такого рода обсуждений, если в этом возникнет необходимость. Может, мы тоже попробуем?

Выглядит неплохо ;)

Предоставление интерфейса

Неважно, выберем ли мы Heap / IHeap или PriorityQueue / IPriorityQueue (или что-то еще), для функциональности, которую мы собираемся предоставить ...

_хотели бы вы иметь интерфейс вместе с реализацией? _

Для

@bendono

Поддерживая эту реализацию интерфейсом, те из нас, кто действительно заботится о других реализациях, могут легко заменить другую реализацию (созданную нами самостоятельно или во внешней библиотеке) и при этом быть совместимыми с кодом, принимающим интерфейс.

Те, кого это не волнует, могут проигнорировать это и сразу перейти к конкретной реализации. Те, кому не все равно, могут использовать любую реализацию по своему выбору.

Против

@madelson

Существует множество различных алгоритмов, которые теоретически можно использовать для реализации этой структуры данных. Однако маловероятно, что фреймворк будет поставляться с более чем одним, и идея о том, что разработчикам библиотек действительно нужен канонический интерфейс, чтобы различные стороны могли координировать свои действия при написании кросс-совместимых реализаций кучи, не кажется такой вероятной.

Более того, как только интерфейс выпущен, его уже нельзя будет изменить из-за совместимости. Это означает, что интерфейс имеет тенденцию отставать от конкретных классов с точки зрения функциональности (проблема с IList и IDictionary сегодня).

@karelz

Интерфейс очень продвинутый экспертный сценарий.

Если библиотека и интерфейс IHeap станут действительно популярными, мы можем позже передумать и добавить IHeap по запросу (через перегрузку конструктора), но я не думаю, что это полезно / согласовано с остальной части BCL достаточно, чтобы оправдать сложность добавления нового интерфейса сейчас. Начните с простого, переходите к сложному, только если это действительно необходимо.

Возможное влияние на решение

  • Включение интерфейса означает, что мы не сможем его изменить в будущем.
  • Исключение интерфейса означает, что люди пишут код, который либо использует наше стандартное библиотечное решение, либо написан на основе решения, поставляемого сторонней библиотекой (нет общего интерфейса, который бы обеспечивал кросс-совместимость).

Используйте 👍 и 👎, чтобы проголосовать за этот (за и против интерфейса, соответственно). Или напишите комментарий. В идеале будут участвовать рецензенты API.

Я хотел бы добавить, что, хотя изменение интерфейсов сложно, с помощью методов расширения (и свойств) интерфейсы легче расширять и / или работать с ними (см. LINQ)

Я хотел бы добавить, что, хотя изменение интерфейсов сложно, с помощью методов расширения (и свойств) интерфейсы легче расширять и / или работать с ними (см. LINQ)

Они могут работать только с общедоступными методами интерфейса; так что это значит сделать все правильно с первого раза.

Я бы посоветовал немного подождать с интерфейсом, пока класс не будет использоваться и не освоится, а затем ввести интерфейс. (обсуждение формы интерфейса - отдельная тема)

Откровенно говоря, меня волнует только интерфейс. Хорошая реализация была бы хороша, но я (или кто-либо другой) всегда мог бы создать свою собственную.

Я помню, как несколько лет назад у нас был такой же разговор с HashSet<T> . Microsoft требовала HashSet<T> то время как сообщество хотело ISet<T> . Если я правильно помню, сначала мы получили HashSet<T> и ISet<T> вторую. Без интерфейса использование HashSet<T> было довольно ограниченным, поскольку было сложно (если не часто невозможно) изменить общедоступный API.

Я должен отметить, что сейчас есть также SortedSet<T> , не говоря уже о многочисленных реализациях ISet<T> без использования BCL. Я использовал ISet<T> в общедоступных API и благодарен за это. Моя частная реализация может использовать любую конкретную реализацию, которую я считаю правильной. Я также могу легко заменить одну реализацию на другую, ничего не сломав. Это было бы невозможно без интерфейса.

Тем, кто говорит, что мы всегда можем определить наши собственные интерфейсы, учтите это. Предположим на мгновение, что ISet<T> в BCL никогда не происходило. Теперь я могу создать свой собственный интерфейс IMySet<T> а также надежные реализации. Однако однажды выпускается BCL HashSet<T> . Он может реализовывать или не реализовывать ISet<T> , но не реализует IMySet<T> . В результате я не могу поменять местами HashSet<T> как реализацию моего IMySet<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 проходят почти каждый вторник.

Вот как должно выглядеть предложение:

Пример предложения / исходное значение

`` С #
пространство имен System.Collections.Generic
{
открытый класс PriorityQueue
: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
общедоступный PriorityQueue ();
общедоступный PriorityQueue (int capacity);
общедоступный PriorityQueue (IComparerкомпаратор);
общедоступный PriorityQueue (IEnumerableколлекция);
общедоступный PriorityQueue (IEnumerableколлекция, IComparerкомпаратор);
public PriorityQueue (int capacity, 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();
    }
}

}
`` ''

ДЕЛАТЬ:

  • Отсутствует пример использования (мне неясно, как я выражаю приоритет элементов) - может быть, нам стоит переделать его, чтобы он имел 'int' в качестве входного значения приоритета? Может какая-то абстракция на нем? (Я думаю, что это обсуждалось выше, но тема слишком длинная, чтобы ее читать, поэтому нам нужны конкретные предложения, а не дальнейшее обсуждение)
  • Отсутствует сценарий UpdatePriority
  • Что-нибудь еще, о чем говорилось выше, отсутствует здесь?

@karelz ОК, сделаем это, следите за обновлениями! :улыбка:

Хорошо. Насколько я могу судить, интерфейс или куча не пройдут проверку API. Поэтому я хотел бы предложить немного другое решение, забыв, например, о четвертичной куче. Приведенная ниже структура данных несколько отличается от той, которую мы можем найти в Python , Java , C ++ , Go , Swift и Rust (по крайней мере, в этих).

Основное внимание уделяется правильности, полноте с точки зрения функциональности и интуитивности, сохраняя при этом оптимальную сложность и отличную реальную производительность.

@karelz @terrajobst

Предложение

Обоснование

Довольно распространенный случай, когда у пользователя есть набор элементов, некоторые из которых имеют более высокий приоритет, чем другие. В конце концов, они хотят сохранить эту группу элементов в определенном порядке, чтобы иметь возможность эффективно выполнять следующие операции:

  1. Добавьте новый элемент в коллекцию.
  2. Получить элемент с наивысшим приоритетом (и иметь возможность удалить его).
  3. Удалить элемент из коллекции.
  4. Измените элемент в коллекции.
  5. Объедините две коллекции.

использование

Глоссарий

  • Значение - данные пользователя.
  • Ключ - объект, который используется для целей заказа.

Различные типы пользовательских данных

Сначала мы сфокусируемся на построении очереди приоритетов (только добавление элементов). То, как это делается, зависит от типа пользовательских данных.

Сценарий 1

  • TKey и TValue - это отдельные объекты.
  • TKey сопоставимо.
var queue = new PriorityQueue<int, string>();

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

Сценарий 2

  • TKey и TValue - это отдельные объекты.
  • 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

  • TKey содержится в TValue .
  • 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 . На этот раз требуется только один аргумент ( TValue ).
  • Если ключевой селектор определен, метод Enqueue(TKey, TValue) должен выдать InvalidOperationException .
  • Если селектор ключа не определен, метод Enqueue(TValue) должен выбросить InvalidOperationException .

Сценарий 4

  • TKey содержится в TValue .
  • 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

  • TKey и TValue - это отдельные объекты, но одного типа.
  • 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 .

Другой функционал

Мы уже рассмотрели создание очереди с приоритетом. Мы также знаем, как добавлять элементы в коллекцию. Теперь остановимся на остальном функционале.

Элемент с наивысшим приоритетом

Есть две операции, которые мы можем выполнить с элементом с наивысшим приоритетом - получить его или удалить.

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 будет обсуждаться позже.

Изменение элемента

Для произвольного элемента мы можем изменить его. Например, если очередь с приоритетами используется в течение всего срока службы службы, высока вероятность того, что разработчик захочет иметь возможность обновлять приоритеты.

Удивительно, но такая функциональность не обеспечивается эквивалентными структурами данных: в Python , Java , C ++ , Go , Swift и Rust . Возможно, и другие, но я только их проверил. Результат? Разочарованные разработчики:

В основном у нас есть два варианта:

  • Делайте это по-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
  • TKey и TValue - это отдельные объекты.
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) против O (n + log n)).

Сценарий 8
  • TKey содержится в TValue .
var queue = new PriorityQueue<int, MyClass>(selector);

/* adding some elements */

queue.Update(handle);

Этот сценарий был приведен в качестве примера в разделе « Ручки ».

Вышеупомянутое достигается за O (log n). В качестве альтернативы пользователь может использовать метод Update(TValue) который находит первый элемент, равный указанному, и выполняет внутреннюю перестановку. Конечно, это O (n).

Сценарий 9
  • TKey и TValue - это отдельные объекты, но одного типа.

Используя ручку, как всегда, нет двусмысленности. Если есть другие способы, позволяющие обновлять - конечно есть. Это компромисс между простотой, производительностью и правильностью (что зависит от того, можно ли дублировать данные или нет).

Сценарий 10
  • Пользовательские данные - это единый объект.

С ручкой опять нет проблем. Обновление с помощью других методов может быть проще, но, опять же, менее производительно и не всегда корректно (одинаковые записи).

Удаление элемента

Делается просто и правильно с помощью ручки. Как вариант, методами Remove(TValue) и Remove(TKey, TValue) . Проблемы, описанные ранее.

Объединение двух коллекций

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

/* add some elements to both */

queue1.Merge(queue2);

Примечания

  • После слияния очереди используют одно и то же внутреннее представление. Пользователь может использовать любой из двух.
  • Типы должны совпадать (проверяться статически).
  • Компоненты должны быть равны, иначе 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);

Установка нового ключа потенциально также может быть функцией, которая берет старый ключ и каким-то образом его преобразует.

То же самое для Contains и Remove .

UpdateKey

Если селектор ключа не определен (и, следовательно, ключ и значение хранятся отдельно), метод может быть назван UpdateKey . Наверное, это более выразительно. Однако, когда ключевой селектор определен, лучше использовать Update , потому что ключ уже обновлен, и что необходимо сделать, это переупорядочить некоторые элементы в очереди с приоритетами.

часто задаваемые вопросы

Разве ручки не неэффективны?

Нет проблем с эффективностью использования ручек. Распространенное опасение состоит в том, что для этого потребуются дополнительные выделения, потому что мы используем кучу, основанную на внутреннем массиве. Не бойся. Читать дальше.

Как тогда вы собираетесь это реализовать?

Это был бы совершенно другой подход к доставке очереди с приоритетом. Впервые стандартная библиотека не будет предоставлять эту функциональность, реализованную в виде двоичной кучи, которая представлена ​​в виде массива внизу. Он будет реализован с помощью кучи сопряжения , которая по замыслу не использует массив - вместо этого она просто представляет собой дерево.

Насколько это было бы производительно?

  • Для обычного случайного ввода четвертичная куча будет немного быстрее.
  • Однако, чтобы быть быстрее, он должен быть основан на массиве. Тогда дескрипторы не могут быть просто основаны на узлах - потребуются дополнительные распределения. Потом - мы не могли разумно обновлять и удалять элементы.
  • В том виде, в каком он разработан прямо сейчас, у нас есть простой в использовании API, но при этом довольно производительная реализация.
  • Дополнительным преимуществом наличия кучи сопряжения ниже является возможность объединить две очереди с приоритетом в O (1) вместо O (n), как в двоичных / четвертичных кучах.
  • Кучу сопряжения по-прежнему невероятно быстро. См. Ссылки. Иногда это быстрее, чем четвертичные кучи (зависит от входных данных и выполняемых операций, а не только слияния).

использованная литература

  • Майкл Л. Фредман, Роберт Седжвик, Дэниел Д. Слейтор и Роберт Э. Тарьян (1986), Куча сопряжения: новая форма самонастраивающейся кучи , Algorithmica 1: 111-129 .
  • Дэниел Х. Ларкин, Сиддхарта Сен и Роберт Э. Тарджан (2014), Эмпирическое исследование приоритетных очередей с возвратом к основам , arXiv: 1403.0252v1 [cs.DS].

Кстати, это для 1-й итерации обзора API. Раздел с _открытыми вопросами_ требует разрешения (мне нужно ваше мнение [и, если возможно, рецензентов API]). Если 1-я итерация пройдет хотя бы частично, для остальной части обсуждения я хотел бы создать новую задачу (закройте и укажите эту).

Насколько я могу судить, интерфейс или куча не пройдут проверку API.

@pgolebiowski : Почему бы тогда просто не закрыть вопрос? Без интерфейса этот класс почти бесполезен. Единственное место, где я мог бы его использовать, - это частные реализации. В этот момент я могу просто создать (или повторно использовать) свою собственную очередь приоритетов, когда это необходимо. Я не могу предоставить общедоступный API в моем коде с этим типом в подписи, так как он сломается, как только мне понадобится заменить его на другую реализацию.

@bendono
Что ж, последнее слово здесь за Microsoft, а не за несколькими людьми, комментирующими эту ветку. И мы знаем, что:

Я в некоторой степени уверен, что другие рецензенты / архитекторы API поделятся им, поскольку я проверил мнение некоторых: имя должно быть PriorityQueue, и мы не должны вводить интерфейс IHeap. Должна быть только одна реализация (вероятно, через какую-то кучу).

Его разделяют @terrajobst , менеджер программ и владелец этого репозитория, а также некоторые рецензенты API.

Хотя мне, очевидно, нравится подход с интерфейсами, как ясно сказано в предыдущих сообщениях, я вижу, что это довольно сложно, учитывая, что у нас не так много сил в этом обсуждении. Мы высказали свое мнение, но мы всего лишь комментаторы. В любом случае код нам не принадлежит. Что еще мы можем сделать?

Почему бы тогда просто не закрыть вопрос? Без интерфейса этот класс почти бесполезен.

Я сделал достаточно - вместо того, чтобы ненавидеть свою работу, сделай что-нибудь. Сделайте реальную работу. Почему вы пытаетесь обвинить меня в чем-либо? Вините себя в том, что не удалось убедить других принять свою точку зрения.

И, пожалуйста, избавь меня от такой риторики. Это действительно по-детски - делай лучше.

Кстати, предложение фокусируется на общих вещах, независимо от того, вводим ли мы интерфейс или нет, или называется ли класс PriorityQueue или Heap . Так что сосредоточьтесь на том, что здесь действительно важно, и покажите нам некоторую предвзятость к действию, если вы чего-то хотите.

@pgolebiowski Конечно, это решение Microsoft. Но лучше представить API, который вы хотите использовать, который соответствует вашим потребностям. Если его отвергают, значит, так тому и быть. Я просто не вижу необходимости идти на компромисс с предложением.

Прошу прощения, если вы истолковали мои комментарии как обвиняющие вас. Это, конечно, не входило в мои намерения.

@pgolebiowski, почему бы не использовать KeyValuePair<TKey,TValue> для ручки?

@SamuelEnglard

Почему бы не использовать KeyValuePair<TKey,TValue> для ручки?

  • Ну, PriorityQueueHandle на самом деле _ узел кучи сопряжения _. Он предоставляет два свойства - TKey Key и TValue Value . Однако в нем гораздо больше внутренней логики. Пожалуйста, обратитесь к моей реализации этого . Он содержит, например, также указатели на другие узлы в дереве (в CoreFX все будет внутренним).
  • KeyValuePair - это структура, поэтому она копируется каждый раз + не может быть унаследована.
  • Но главное, что PriorityQueueHandle - довольно сложный класс, который просто случайно предоставляет тот же публичный API, что и KeyValuePair .

@bendono

Но лучше представить API, который вы хотите использовать, который соответствует вашим потребностям. Если его отвергают, значит, так тому и быть. Я просто не вижу необходимости идти на компромисс с предложением.

  • Это правда, я буду иметь это в виду и посмотреть, что произойдет. @karelz , вместе с предложением, не могли бы вы также передать часть предыдущего поста (они прямо перед предложением), где мы голосовали за интерфейсы? Мы можем вернуться к этому позже, после первой итерации обзора API.
  • Тем не менее, независимо от того, какое решение мы выберем, функциональность будет очень похожей, и было бы полезно проверить ее. Потому что, если идея дескрипторов отклоняется, и мы не можем действительно обновить / удалить элементы должным образом (или у нас не может быть отдельных TKey и TValue ), этот класс действительно близок к тому, чтобы быть бесполезным, тогда - - как сейчас в Java.
  • В частности, если у нас не будет интерфейса, моя библиотека AlgoKit не сможет иметь общее ядро ​​для кучи с CoreFX, что было бы для меня по-настоящему грустно.
  • И да, для меня действительно удивительно, что добавление интерфейса воспринимается Microsoft как недостаток.

Это, конечно, не входило в мои намерения.

Извини, тогда моя ошибка.

Мои вопросы по дизайну (отказ от ответственности: эти вопросы и пояснения, никаких жестких ответов (пока)):

  1. Неужели нам действительно нужны PriorityQueueHandle ? Что, если мы просто ожидаем уникальных значений в очереди?

    • Мотивация: Представляется довольно запутанной концепцией. Если он у нас есть, хотелось бы понять, зачем он нам нужен? Как это помогает? Или это просто конкретная деталь реализации, просачивающаяся на поверхность API? Собирается ли он купить нам столько производительности, чтобы заплатить за усложнение API?

  2. Нужен ли нам Merge ? Есть ли это в других коллекциях? Мы не должны добавлять API, потому что их легко реализовать, для них должен быть какой-то общий вариант использования.

    • Может быть, достаточно просто добавить инициализацию из IEnumerable<KeyValuePair<TKey, TValue>> ? + полагаться на Linq

  3. Нужна ли нам перегрузка comparer ? Можем ли мы просто всегда вернуться к дефолту? (отказ от ответственности: в этом случае мне не хватает знаний / опыта, поэтому просто спрашиваю)
  4. Нужны ли нам keySelector перегрузки? Я думаю, мы должны решить, хотим ли мы иметь приоритет как часть ценности или как отдельную вещь. Раздельный приоритет кажется мне более естественным, но у меня нет твердого мнения. Знаем ли мы плюсы и минусы?

Отдельные / параллельные точки принятия решения:

  1. Имя класса PriorityQueue vs. Heap
  2. Ввести IHeap и перегрузку конструктора?

Представить IHeap и перегрузку конструктора?

Похоже, все наладилось, и я могу бросить свои 2 цента .... Мне лично нравится интерфейс. Он абстрагирует детали реализации от API (и основных функций, описываемых этим API) таким образом, что, на мой взгляд, упрощает структуру и обеспечивает максимальное удобство использования.

У меня нет столь твердого мнения о том, делаем ли мы интерфейс одновременно с PQueue / Heap / ILikeThisItemMoreThanThisItemList или добавим его позже. Аргумент о том, что API может быть «в движении», и поэтому мы должны сначала выпустить его как класс, пока мы не получим обратную связь, безусловно, действительный, с которым я не согласен. Тогда возникает вопрос, когда он считается «достаточно стабильным», чтобы добавить интерфейс. Выше в ветке IList и IDictionary были упомянуты как отстающие от API канонических реализаций, которые мы добавили очень давно, так какой период времени считается приемлемым периодом отдыха?

Если мы сможем определить этот период с разумной уверенностью и убедиться, что он не является недопустимым блокированием, то я не вижу проблем с доставкой этой громоздкой структуры данных без интерфейса. Затем, по прошествии этого периода времени, мы можем изучить использование и рассмотреть возможность добавления интерфейса.

И да, для меня действительно удивительно, что добавление интерфейса воспринимается Microsoft как недостаток.

Это потому , что во многих отношениях это является недостатком. Если мы отправим интерфейс, все будет готово. Там не так уж много места для итерации этого API, поэтому нам лучше убедиться, что он верен с первого раза и будет оставаться правильным в течение многих лет. Отсутствие какой-то полезной функциональности - это куда лучше, чем застревание с потенциально неадекватным интерфейсом, где было бы о-так приятно иметь только одно небольшое изменение, которое сделало бы все лучше.

Спасибо за ваш вклад, @karelz и @ianhays!

Разрешение дубликатов

Неужели нам действительно нужны PriorityQueueHandle ? Что, если мы просто ожидаем уникальных значений в очереди?

Мотивация: Представляется довольно запутанной концепцией. Если он у нас есть, хотелось бы понять, зачем он нам нужен? Как это помогает? Или это просто конкретная деталь реализации, просачивающаяся на поверхность API? Собирается ли он купить нам столько производительности, чтобы заплатить за усложнение API?

Нет, нам это не нужно. Предложенный выше API очереди приоритетов достаточно мощный и очень гибкий. Он основан на предположении, что элементы и приоритеты могут дублироваться . Из-за этого предположения наличие дескриптора необходимо, чтобы иметь возможность удалить или обновить правильный узел. Однако, если мы установим ограничение, что элементы должны быть уникальными, мы могли бы достичь того же результата, что и выше, с более простым API и без раскрытия внутреннего класса ( PriorityQueueHandle ), что действительно не идеально.

Предположим, что мы разрешаем только уникальные элементы. Мы по-прежнему можем поддерживать все предыдущие сценарии и поддерживать оптимальную производительность. Более простой 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, но не удалить его. Я согласен удалить этот метод и (потенциально) добавить его позже (при необходимости).

Сравнить

Нужна ли перегрузка компаратора? Можем ли мы просто всегда вернуться к дефолту? (отказ от ответственности: в этом случае мне не хватает знаний / опыта, поэтому просто спрашиваю)

Я считаю, что это очень важно по двум причинам:

  • Пользователь хотел бы, чтобы их элементы были упорядочены в порядке возрастания или убывания. Высший приоритет не говорит нам, как следует делать заказ.
  • Если мы пропускаем средство сравнения, мы заставляем пользователя всегда предоставлять нам класс, реализующий IComparable .

Кроме того, это соответствует существующему API. Взгляните на SortedDictionary .

Селектор

Нужны ли нам перегрузки keySelector? Я думаю, мы должны решить, хотим ли мы иметь приоритет как часть ценности или как отдельную вещь. Раздельный приоритет кажется мне более естественным, но у меня нет твердого мнения. Знаем ли мы плюсы и минусы?

Еще мне нравится отдельный приоритет. Кроме того, это проще реализовать, лучше производительность и использование памяти, более интуитивно понятный API, меньше работы для пользователя (нет необходимости реализовывать IComparable ).

Что касается селектора сейчас ...

Плюсы

Это то, что делает эту очередь приоритетов гибкой. Это позволяет пользователям иметь:

  • элементы и их приоритеты как отдельные элементы
  • элементы (сложные классы), которые имеют приоритеты где-то внутри
  • внешняя логика, которая извлекает приоритет для данного элемента
  • элементы, реализующие IComparable

Он позволяет практически любую конфигурацию, о которой я могу думать. Я считаю это полезным, поскольку пользователи могут просто «подключать и играть». Это также довольно интуитивно понятно.

Минусы

  • Есть еще чему поучиться.
  • Больше API. Два дополнительных конструктора, один дополнительный Enqueue и метод Update .
  • Если мы решаем разделить или объединить элемент и приоритет, мы заставляем некоторых пользователей (у которых есть данные в другом формате) адаптировать свой код для использования этой структуры данных.

Отдельные / параллельные точки принятия решения

Имя класса PriorityQueue vs. Heap

Представьте IHeap и перегрузку конструктора.

  • Мне кажется, что PriorityQueue должен быть частью CoreFX, а не Heap .
  • Что касается интерфейса IHeap - поскольку очередь с приоритетом может быть реализована с помощью чего-то другого, кроме кучи, вероятно, мы не хотели бы раскрывать ее таким образом. Однако нам может понадобиться IPriorityQueue .

Если мы отправим интерфейс, все будет готово. Там не так уж много места для итерации этого API, поэтому нам лучше убедиться, что он верен с первого раза и будет оставаться правильным в течение многих лет. Отсутствие некоторых полезных функций - лучшее место для жизни, чем застревание с потенциально неадекватным интерфейсом, где было бы о-так приятно иметь только одно небольшое изменение, которое сделало бы все лучше.

Полностью согласен!

Сопоставимые элементы

Если мы добавим еще одно предположение: элементы должны быть сопоставимы, тогда API станет еще проще. Но опять же, он менее гибкий.

Плюсы

  • Нет необходимости в IComparer .
  • Селектор не нужен.
  • Один метод Enqueue и Update .
  • Один универсальный тип вместо двух.

Минусы

  • У нас не может быть элемента и приоритета как отдельных объектов. Пользователям необходимо предоставить новый класс-оболочку, если он у них есть в этом формате.
  • Пользователям всегда необходимо реализовать IComparable прежде чем они смогут использовать эту приоритетную очередь.
  • Если бы у нас были отдельные элементы и приоритеты, мы могли бы реализовать это проще, с лучшей производительностью и использованием памяти - внутренний словарь <TElement, InternalNode> и InternalNode содержат TPriority .
public class PriorityQueue<T> : IEnumerable, IEnumerable<T>, IReadOnlyCollection<T>
    where T : IComparable<T>
    // ICollection not included on purpose
{
    public PriorityQueue();
    // some other constructors like building it from a collection, or initial capacity if we have an array beneath

    public int Count { get; }

    public void Clear();
    public bool Contains(T element);

    public T Peek();
    public T Dequeue();

    public void Enqueue(T element);
    public void Update(T element);
    public void Remove(T element);

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

Все - компромисс. Мы удаляем некоторые функции, теряем некоторую гибкость, но, возможно, нам это не нужно, чтобы удовлетворить 95% наших пользователей.

Мне также нравится подход к интерфейсу, потому что он более гибкий и дает больше возможностей, но я также согласен с @karelz и @ianhays, что мы должны подождать, пока новый API класса не будет использован, и мы получим обратную связь, пока мы действительно не отправим интерфейс, а затем мы поставляются с этой версией и не могут изменить ее, не нарушив работу клиентов, которые ее уже использовали.

Также о компараторах я думаю, что он следует за другими API BCL, и мне не нравится тот факт, что пользователям необходимо предоставить новый класс-оболочку. Мне очень нравится, что перегрузка конструктора получает подход компаратора и использует этот компаратор во всех сравнениях, которые должны выполняться внутри, если компаратор не предоставлен или компаратор имеет значение null, тогда используйте компаратор по умолчанию.

@pgolebiowski благодарит за такое подробное и

Хорошо, я убежден в компараторе, он имеет смысл и согласован.
Я все еще разрываюсь на селекторе - ИМО, надо попробовать обойтись без него - давайте разделим его на 2 варианта.

`` С #
открытый класс PriorityQueue
: IEnumerable,
IEnumerable <(элемент TElement, приоритет TPriority)>,
IReadOnlyCollection <(элемент TElement, приоритет TPriority)>
// ICollection не включен намеренно
{
общедоступный PriorityQueue ();
общедоступный 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();

//
// Селекторная часть
//
общедоступный PriorityQueue (FuncprioritySelector);
общедоступный 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. Имя класса PriorityQueue vs. Heap
  2. Ввести IHeap и перегрузку конструктора? (Стоит ли нам подождать попозже?)
  3. Ввести IPriorityQueue ? (Если нам подождать позже - пример IDictionary )
  4. Использовать селектор (приоритета хранится внутри значения) или нет (разница в 5 API)
  5. Используйте кортежи (TElement element, TPriority priority) vs. KeyValuePair<TPriority, TElement>

    • Должны ли Peek и Dequeue иметь аргумент out вместо кортежа?

Я постараюсь запустить его завтра группой проверки API, чтобы получить раннюю обратную связь.

Исправлено имя поля кортежа Peek и Dequeue на priority (спасибо @pgolebiowski за указание на это).
Верхний пост обновлен последним предложением выше.

Второй комментарий @safern - Большое спасибо @pgolebiowski за то, что продвинули его вперед!

Открытые вопросы:

Думаю, здесь может быть еще один:

  1. Ограничьтесь только уникальными элементами (не допускайте дублирования).

Хороший момент, обозначенный как «Предположения» в верхнем посте, а не открытый вопрос. Напротив, API будет немного уродливым.

Должны ли мы возвращать bool из Remove ? А также приоритет как out priority arg? (Я считаю, что недавно мы добавили аналогичные перегрузки в другие структуры данных)

Подобно Contains - держу пари, кто-то захочет получить от него приоритет. Мы могли бы захотеть добавить перегрузку с out priority .

Я по-прежнему придерживаюсь мнения (хотя я еще не озвучил его), что Heap будет подразумевать реализацию и должным образом поддержит вызов этого PriorityQueue . (Я бы даже поддержал предложение о более тонком классе Heap который допускает дублирование элементов и не позволяет обновлять и т. Д. (Больше в соответствии с исходным предложением), но я не ожидаю, что произойдет).

KeyValuePair<TPriority, TElement> безусловно, не следует использовать, поскольку ожидается дублирование приоритетов, и я думаю, что KeyValuePair<TElement, TPriority> также сбивает с толку, поэтому я бы поддержал отказ от использования KeyValuePair вообще, и либо используя простые кортежи или параметры out для приоритетов (лично мне нравятся параметры out, но я не волнуюсь).

Если мы запрещаем дублирование, нам нужно решить, как будет пытаться повторно добавить их с другими / измененными приоритетами.

Я не поддерживаю предложение селектора по той простой причине, что оно подразумевает избыточность и должным образом воссоздает путаницу. Если приоритет элемента хранится вместе с ним, то он будет храниться в двух местах, и если они станут несинхронизированными (то есть кто-то забудет вызвать бесполезно выглядящий метод Update(TElement) ), тогда будет много страданий. обеспечит. Если селектором является компьютер, то мы открыты для людей, намеренно добавляющих элемент, а затем изменяющих значения, из которых они вычисляются, и теперь, если они все же попытаются повторно добавить его, есть множество вещей, которые могут пойти не так, в зависимости от на решение о том, что произойдет, когда это произойдет. На немного более высоком уровне попытка может привести к добавлению измененной копии элемента, поскольку она больше не равна тому, что было раньше (это общая проблема с изменяемыми ключами, но я думаю, что разделение приоритета и элемента приведет к помочь избежать потенциальной проблемы).

Сам селектор склонен к изменению поведения, что является еще одним способом, с помощью которого пользователи могут сломать все, не задумываясь. Я думаю, что гораздо лучше, если пользователи явно укажут приоритеты. Большая проблема, которую я вижу в этом, заключается в том, что это означает, что разрешены повторяющиеся записи, поскольку мы объявляем пару, а не элемент. Однако разумная встроенная документация и возвращаемое значение bool для Enqueue должны тривиально исправить это. Лучше, чем логическое значение, возможно, было бы вернуть старый / новый приоритет элемента (например, если Enqueue использует вновь предоставленный приоритет, или минимум из двух, или старый приоритет), но я думаю, что Enqueue должен просто потерпеть неудачу, если вы попытаетесь что-то повторно добавить, и поэтому должен просто вернуть bool указывающий на успех. При этом Enqueue и Update полностью разделены и четко определены.

Я бы поддержал не использовать KeyValuePair вообще или использовать простые кортежи

Я с тобой по кортежам.

Если мы запрещаем дублирование, нам нужно решить, как будет пытаться повторно добавить их с другими / измененными приоритетами.

  • Я вижу сходство с индексатором в Dictionary . Там, когда вы выполняете dictionary["something"] = 5 он обновляется, если "something" был там ранее ключом. Если его там не было, он просто добавляется.
  • Однако метод Enqueue для меня аналогичен методу Add в словаре. Это означает, что он должен вызвать исключение.
  • Принимая во внимание вышеизложенное, мы могли бы рассмотреть возможность добавления индексатора в очередь приоритетов для поддержки заданного вами поведения.
  • Но, в свою очередь, индексатор может не работать с концепцией очередей.
  • Это приводит нас к выводу, что метод Enqueue должен просто генерировать исключение, если кто-то хочет добавить дублированный элемент. Точно так же метод Update должен генерировать исключение, если кто-то хочет обновить приоритет элемента, которого нет.
  • Это приводит нас к новому решению - добавить метод TryUpdate который действительно возвращает bool .

Я не поддерживаю предложение селектора по той простой причине, что оно подразумевает избыточность.

Разве ключ не копируется физически (если он существует), а селектор остается функцией, которая вызывается, когда необходимо оценить приоритеты? Где избыточность?

Я думаю, что разделение приоритета и элемента поможет избежать потенциальных проблем.

Проблема только в том, что у заказчика нет физического приоритета. Тогда он ничего не может сделать.

Я думаю, что гораздо лучше, если пользователи явно укажут приоритеты. Большая проблема, которую я вижу в этом, заключается в том, что это означает, что разрешены повторяющиеся записи, поскольку мы объявляем пару, а не элемент.

Я вижу некоторые проблемы с этим решением, но не обязательно, почему оно сообщает, что дублирующиеся записи разрешены. Я думаю, что логика «без дубликатов» должна применяться только к 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 имеет для меня смысл. Я больше думал о ISet отношении Enqueue (а не IDictionary ). Бросок тоже имел бы смысл, я, должно быть, пропустил этот момент, но я действительно думаю, что return bool передает «установленность» типа. Может быть, тоже подойдет TryEnqueue (с метанием Add )? Также приветствуется TryRemove (не работает, если пусто). Что касается вашего последнего замечания, да, я тоже имел в виду такое поведение. Я полагаю, что аналогия с IDictionary лучше, чем ISet при размышлении, и это должно быть достаточно ясным. (Подводя итог: я бы поддержал все, что вы бросали в соответствии с вашим предложением, но наличие Try* является обязательным, если это так; я также согласен с вашими утверждениями относительно условий отказа).

Что касается индексатора, я думаю, вы правы, что он действительно не «вписывается» в концепцию очереди, я бы этого не поддержал. Во всяком случае, хорошо подойдет метод Queue или Update.

Разве ключ не копируется физически (если он существует), а селектор остается функцией, которая вызывается, когда необходимо оценить приоритеты? Где избыточность?

Вы правы, я неправильно прочитал обновление предложения (бит о его хранении в элементе). Принимая во внимание, что селектор вызывается по запросу (что должно быть четко указано в любой документации, поскольку это может иметь последствия для производительности), все еще остается точка зрения, что результат функции может измениться без ответа структуры данных, оставляя два не синхронизированы, если не вызывается Update . Хуже того, если пользователь изменяет эффективный приоритет нескольких элементов и обновляет только один из них, тогда структура данных будет зависеть от «незавершенных» изменений, когда они выбраны из «не обновленных» элементов (я не изучал предлагаемую реализацию DataStructure подробно, но я думаю, что это обязательно проблема для любого обновления O(<n) ). Это исправляет принуждение пользователя к явному обновлению любых приоритетов, обязательно переводя структуру данных из одного согласованного состояния в другое.

Обратите внимание, что все мои заявленные претензии к предложению Selector касаются надежности API: я думаю, что селектор упрощает неправильное использование. Однако концептуально, помимо удобства использования, элементы в очереди не должны знать свой приоритет. Это потенциально не имеет отношения к ним, и если пользователь заканчивает тем, что оборачивает свои элементы в struct Queable<T>{} или что-то в этом роде, это похоже на отказ в предоставлении API без трения (насколько мне больно использовать этот термин).

Вы, конечно, можете возразить, что без селектора бремя вызова селектора лежит на пользователе, но я думаю, что если элементы знают свой приоритет, они (надеюсь) будут аккуратно выставлены (то есть свойство), а если они этого не сделают. t, то не нужно беспокоиться о селекторе (генерация приоритетов на лету и т. д.). Для поддержки селектора, если он вам не нужен, требуется гораздо больше бессмысленной сантехники, а затем для передачи приоритетов, если у вас уже есть четко определенный «селектор». Селектор побуждает пользователя раскрыть эту информацию (возможно, бесполезно), в то время как отдельные приоритеты обеспечивают чрезвычайно прозрачный интерфейс, который, я не могу себе представить, повлияет на дизайнерские решения.

Единственный аргумент, который я действительно могу придумать в пользу селектора, заключается в том, что он означает, что вы можете использовать передачу PriorityQueue , а другие части программы могут использовать его, не зная, как вычисляются априорные значения. Мне придется подумать над этим немного дольше, но, учитывая «нишевую» пригодность, это кажется мне небольшой наградой за достаточно большие накладные расходы в более общих случаях.

_Edit: Подумав об этом еще немного, было бы неплохо иметь автономные PriorityQueue s, в которые вы можете просто бросать элементы, но я утверждаю, что стоимость работы по этому поводу была бы большой, в то время как стоимость внедрения будет значительно меньше.

Я немного поработал, просматривая базы кода .NET с открытым исходным кодом, чтобы увидеть реальные примеры использования очередей приоритетов. Сложная проблема - отфильтровать любые студенческие проекты и исходный код обучения.

Использование 1: Служба уведомлений Roslyn
https://github.com/dotnet/roslyn/
Компилятор Roslyn включает реализацию частной приоритетной очереди под названием «PriorityQueue», которая, по-видимому, имеет очень специфическую оптимизацию - очередь объектов, которая повторно использует объекты в очереди, чтобы избежать их сбора мусора. Метод Enqueue_NoLock выполняет оценку current.Value.MinimumRunPointInMS <entry.Value.MinimumRunPointInMS, чтобы определить, где в очереди разместить новый узел. Любой из двух предложенных здесь основных проектов очереди приоритетов (функция / делегат сравнения против явного приоритета) подходит для этого сценария использования.

Использование 2: Lucene.net
https://github.com/apache/lucenenet
Это, пожалуй, самый крупный пример использования PriorityQueue, который я смог найти в .NET. Apache Lucene.net - это полный порт .net популярной библиотеки поисковых систем Lucene. В моей компании мы используем версию Java, и, согласно веб-сайту Apache, несколько громких имен используют версию .NET. В Github есть большое количество форков проекта .NET.

Lucene включает в себя собственную реализацию PriorityQueue, которая разделена на несколько «специализированных» приоритетных очередей: HitQueue, TopOrdAndFloatQueue, PhraseQueue и SuggestWordQueue. Кроме того, проект напрямую создает экземпляр PriorityQueue в нескольких местах.

Реализация PriorityQueue в Lucene, ссылка на которую приведена выше, очень похожа на исходный API очереди приоритетов, опубликованный в этом выпуске. Он определяется как «PriorityQueue"и принимает IComparerпараметр в его конструкторе. Методы и свойства включают Count, Clear, Offer (enqueue / push), Poll (dequeue / pop), Peek, Remove (удаляет первый соответствующий элемент, найденный в очереди), Add (синоним предложения). Интересно, что они также обеспечивают поддержку перечисления для очереди.

Приоритет в PriorityQueue Lucene определяется компаратором, переданным в конструктор, и если ничего не было передано, предполагается, что сравниваемые объекты реализуют IComparableи использует этот интерфейс для сравнения. Оригинальный дизайн API, размещенный здесь, аналогичен, за исключением того, что он также работает с типами значений.

Их кодовая база содержит большое количество примеров использования, и SloppyPhraseScorer является одним из них.

Конструктор SloppyPhraseScorer создает экземпляр нового PhraseQueue (pq), который является одним из их собственных подклассов PriorityQueue. Создается коллекция PhrasePositions, которая выглядит как оболочка для набора сообщений, позиции и набора терминов. Метод FillQueue перечисляет позиции фраз и ставит их в очередь. PharseFreq () вызывает функцию AdvancePP и на высоком уровне, кажется, удаляет из очереди, обновляет приоритет элемента, а затем снова ставит в очередь. Приоритет определяется относительно (с использованием компаратора), а не явно (приоритет не «передается» в качестве второго параметра во время постановки в очередь).

Вы можете видеть, что на основе их реализации PhraseQueue значение сравнения, переданное через конструктор (например, целое число), может не сократить его. Их функция сравнения («LessThan») оценивает три разных поля: PhrasePositions.doc, PhrasePositions.position и PhrasePositions.offset.

Использование 3: Разработка игр
Я не закончил поиск примеров использования в этой области, но я видел довольно много примеров использования пользовательского .NET PriorityQueue при разработке игр. В очень общем смысле они, как правило, группировались вокруг поиска пути как основного варианта использования (Дейкстры). Вы можете встретить множество людей, которые спрашивают, как реализовать алгоритмы поиска пути в .NET с помощью Unity 3D .

Еще нужно покопаться в этой области; видел несколько примеров с явным постановлением приоритета в очередь и несколько примеров с использованием Comparer / IComparable.

Отдельно стоит упомянуть об уникальных элементах, удалении явных элементов и определении того, существует ли конкретный элемент.

Очереди как структура данных обычно поддерживают постановку в очередь и исключение из очереди. Если мы пойдем по пути предоставления других операций, подобных множеству / списку, мне интересно, действительно ли мы вообще проектируем другую структуру данных - что-то вроде отсортированного списка кортежей. Если у вызывающего абонента есть потребность, отличная от Enqueue, Dequeue, Peek, возможно, ему нужно что-то другое, кроме очереди с приоритетом? Очередь, по определению, подразумевает вставку в очередь и упорядоченное удаление из очереди; не более того.

@ebickle

Я ценю ваши усилия по просмотру других репозиториев и проверке того, как там были реализованы функции очереди приоритетов. Тем не менее, ИМО в этом обсуждении было бы полезно предоставить конкретные проектные предложения . За этой цепочкой уже трудно следить, а рассказывать длинную историю без каких-либо выводов еще больше.

Очереди [...] обычно поддерживают постановку в очередь и исключение из очереди. [...] Если у вызывающего абонента есть потребность, отличная от Enqueue, Dequeue, Peek, возможно, ему нужно что-то кроме очереди с приоритетом? Очередь, по определению, подразумевает вставку в очередь и упорядоченное удаление из очереди; не более того.

  • Какой вывод? Предложение?
  • Это очень далеко от истины. Даже упомянутый вами алгоритм Дейкстры использует обновление приоритетов элементов. И то, что необходимо для обновления определенных элементов, также необходимо для удаления определенных элементов.

Отличное обсуждение. ИМО , исследование
@ebickle у вас есть заключение по [2] lucene.net - соответствует ли наше последнее предложение обычаям или нет? (Надеюсь, я не пропустил это в вашем подробном описании)

Похоже, нам нужны Try* варианты, указанные выше + IsEmpty + TryPeek / TryDequeue + EnqueueOrUpdate ? Мысли?
`` С #
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:

Только один вопрос - почему ref вместо out ? Я не уверен:

  • Нам не нужно инициализировать его перед входом в функцию.
  • Для метода не используется (просто тухнет).

Если мы хотим вернуть статусы везде, то везде. Для этого конкретного метода, я думаю, это должно быть правдой, если какая-либо операция завершилась успешно ( Enqueue или Update ).

Он всегда будет возвращать истину, это плохая идея. Мы должны использовать void в этом случае IMO. В противном случае люди будут сбиты с толку и будут проверять возвращаемое значение и добавлять бесполезный, никогда не выполняемый код. (Если я что-то не пропустил)

ref vs. out договорились, я сам это обсудил. У меня нет твердого мнения / достаточно опыта, чтобы самому принимать решения. Мы можем спросить у обозревателей API / дождаться дополнительных комментариев.

Он всегда будет возвращать истину

Вы правы, моя беда. Извините.

У меня нет твердого мнения / достаточно опыта, чтобы самому принимать решения.

Возможно, я чего-то упускаю, но мне это кажется довольно простым. Если мы используем ref , мы в основном говорим, что Peek и Dequeue хотят каким-то образом использовать переданные ему TElement и TPriority ( Я имею в виду прочитать эти поля). На самом деле это не так - наши методы должны присваивать значения только этим переменным (и на самом деле они должны делать это компилятором).

Верхний пост обновлен с моими Try* API
Добавлено 2 открытых вопроса:

  • [6] Полезны ли вообще метания Peek и Dequeue ?
  • [7] TryPeek и TryDequeue - следует использовать ref или out args?

ref vs. out - Вы правы. Я оптимизировал, чтобы избежать инициализации в случае, если мы вернем false. Это было глупо и ослеплено с моей стороны - преждевременная оптимизация. Я поменяю его на out и сниму вопрос.

6: Возможно, я что-то упускаю, но если не генерировать исключение, что делать Peek или Dequeue , если очередь пуста? Я полагаю, это вызывает вопрос о том, должна ли структура данных принимать null (я бы предпочел этого не делать, но без твердого мнения). Даже если мы допустим null , типы значений не имеют null ( default конечно, не учитываются), поэтому Peek и Dequeue имеют нет способа передать бессмысленный результат, и я думаю, что должным образом необходимо выбросить исключение (таким образом снимая любые опасения по поводу параметров out !). Я не вижу причин не следовать примеру существующего Queue.Dequeue

Я просто добавлю, что метод Дейкстры (он же эвристический поиск без эвристики) не требует изменения приоритета. Мне никогда не хотелось обновлять приоритет чего-либо, и я постоянно выполняю эвристический поиск. (суть алгоритма в том, что, исследуя состояние, вы знаете, что исследовали лучший путь к нему, в противном случае он рискует оказаться неоптимальным, следовательно, вы никогда не сможете улучшить приоритет, и вы, конечно же, не хотите уменьшить его (т. е. рассмотреть худший путь к нему). _ Игнорирование случаев, когда вы намеренно выбираете эвристику так, чтобы она не была оптимальной, и в этом случае вы, конечно, можете получить неоптимальный результат, но вы все равно никогда не обновить приоритет_)

если исключение не генерируется, что делать Peek или Dequeue, если очередь пуста?

Правда.

таким образом снимая любые опасения по поводу параметров!

Как? Ну, параметры out предназначены для методов TryPeek и TryDequeue . Исключение вызывают Peek и Dequeue .

Я просто добавлю, что Dijkstra не требует изменения приоритета.

Возможно, я ошибаюсь, но, насколько мне известно, алгоритм Дейкстры использует операцию DecreaseKey . См. Например это . Эффективно это или нет - другой аспект. Фактически, куча Фибоначчи была спроектирована таким образом, чтобы асимптотически выполнять операцию DecreaseKey за O (1) (для улучшения Дейкстры).

Но все же в нашем обсуждении важно то, что возможность обновить элемент в очереди с приоритетом очень полезна, и есть люди, которые ищут такую ​​функциональность (см. Ранее связанные вопросы на StackOverflow). Я тоже использовал его несколько раз.

Извините, да, теперь я вижу проблему с out params, снова неправильно читая вещи. И похоже, что вариант Дейкстры (имя, которое, кажется, применяется более широко, чем я думал ...) может быть реализован, возможно, более эффективно (если ваша очередь уже содержит элементы, по-видимому, есть преимущество для повторяющихся поисков) с обновляемые приоритеты. Вот что я получаю, используя длинные слова и имена. Обратите внимание, что я не предлагаю отказаться от Update , было бы неплохо также иметь более тонкий Heap или другое (то есть исходное предложение) без ограничений, налагаемых этой возможностью (как замечательно возможность, как я ее ценю).

7: Это должно быть out . Любой ввод никогда не может иметь никакого значения, поэтому не должен существовать (т.е. мы не должны использовать ref ). С out params мы не собираемся улучшать возврат значений default . Это то, что делает Dictionary.TryGetValue , и я не вижу причин поступать иначе. _Тем не менее, вы можете рассматривать ref как значение или значение по умолчанию, но если у вас нет значимого значения по умолчанию, это расстраивает ситуацию. _

Обсуждение обзора API:

  • Нам нужно будет провести соответствующее совещание по дизайну (2 часа), чтобы обсудить все за / против, 30 минут, которые мы потратили сегодня, было недостаточно для достижения консенсуса / закрытия по открытым вопросам.

    • Скорее всего, мы пригласим наиболее активных участников сообщества -

Вот основные необработанные заметки:

  • Эксперимент - мы должны провести эксперимент (в CoreFxLabs), выпустить его как предварительный пакет NuGet и попросить потребителей оставить отзыв (через сообщение в блоге). Мы не верим, что сможем использовать API без обратной связи.

    • Мы делали подобное для ImmutableCollections в прошлом (быстрый цикл выпуска предварительного просмотра был ключевым и полезным при формировании API).

    • Мы можем связать этот эксперимент вместе с MultiValueDictionary который уже есть в CoreFxLab. ЗАДАЧИ: Проверьте, есть ли у нас больше кандидатов, мы не хотим публиковать в блоге каждого из них отдельно.

    • Экспериментальный пакет NuGet будет уничтожен после завершения эксперимента, и мы переместим исходный код в CoreFX или CoreFXExtensions (решение будет принято позже).

  • Идея: вернуть только TElement из Peek & Dequeue , не возвращать TPriority (для этого пользователи могут использовать методы Try* ).
  • Стабильность (для одинаковых приоритетов) - элементы с одинаковыми приоритетами должны возвращаться в том порядке, в котором они были вставлены в очередь (общее поведение очереди)
  • Мы хотим включить дубликаты (аналогично другим коллекциям):

    • Избавьтесь от Update - пользователь может вернуть Remove а затем Enqueue item с другим приоритетом.

    • Remove должен удалить только 1-й найденный элемент (как это делает List ).

  • Идея: можем ли мы абстрагировать интерфейс IQueue чтобы абстрагировать Queue и PriorityQueue ( Peek и Dequeue возвращающие только TElement помогают здесь)

    • Примечание: это может оказаться невозможным, но мы должны изучить это, прежде чем останавливаться на API.

Мы долго обсуждали селектор - мы все еще не определились (может потребоваться IQueue выше?). Большинству не понравился селектор, но все может измениться на будущей встрече по рассмотрению API.

Остальные открытые вопросы не обсуждались.

Последнее предложение мне очень нравится. Мои мнения / вопросы:

  1. Если Peek и Dequeue использовали параметры out для priority , тогда у них также могли быть перегрузки, которые вообще не возвращают приоритет, что, я думаю, упростит обычное использование. Хотя приоритет также можно игнорировать, используя сброс, что делает его менее важным.
  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 item с другим приоритетом.

В зависимости от реализации, Update , вероятно, будет намного эффективнее, чем Remove за которым следует Enqueue . Например, с двоичной кучей (я думаю, что четвертичная куча имеет те же временные сложности), Update (с уникальными значениями и словарем) - это O (log n ), а Remove (с повторяющимися значениями и нет словаря) O ( n ).

@pgolebiowski
Полностью согласен, что здесь нам нужно сосредоточиться на предложениях; Я сделал исходное предложение API и исходный пост. Еще в январе @karelz попросил несколько конкретных примеров использования; это открыло гораздо более широкий вопрос, касающийся конкретных потребностей и использования PriorityQueue API. У меня была собственная реализация PriorityQueue, я использовал ее в нескольких проектах и ​​чувствовал, что нечто подобное будет полезно в BCL.

Чего мне не хватало в исходном посте, так это широкого обзора существующего использования очередей; Примеры из реальной жизни помогут сохранить обоснованность дизайна и обеспечат его широкое использование.

@karelz
Lucene.net содержит по крайней мере одну функцию сравнения очередей приоритетов (аналогично Comparison), который оценивает несколько полей для определения приоритета. «Неявный» шаблон сравнения Lucene плохо согласуется с явным параметром TPriority текущего предложения API. Потребуется какое-то сопоставление - объединение нескольких полей в одно или в «сопоставимую» структуру данных, которую можно передать как TPriority.

Предложение:
1) PriorityQueueкласс, основанный на моем первоначальном предложении выше (перечисленном под заголовком Исходного предложения). Возможно добавление вспомогательных функций Обновить (T), Удалить (T) и Содержит (T). Подходит для большинства существующих примеров использования из сообщества открытого исходного кода.
2) PriorityQueueвариант dotnet / corefx # 1. Dequeue () и Peek () возвращают TElement вместо кортежа. Нет функций Try *, Remove () возвращает bool вместо того, чтобы отбрасывать, чтобы соответствовать спискушаблон. Выступает в качестве «удобного типа», поэтому разработчикам, имеющим явное значение приоритета, не нужно создавать свой собственный сопоставимый тип.

Оба типа поддерживают повторяющиеся элементы. Необходимо определить, гарантируем ли мы FIFO для элементов с одинаковым приоритетом; скорее всего, нет, если мы поддерживаем приоритетные обновления.

Создайте оба варианта в CoreFxLabs, как предложил @karelz, и запросите отзывы.

Вопросов:

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 throw on

Мы не должны добавлять методы в качестве методов расширения, если мы можем добавить их к типу. Методы расширения являются резервными, если мы не можем добавить их к типу (например, это интерфейс, или мы хотим быстрее доставить его в .NET Framework).

Дупли: Если у вас несколько одинаковых записей, вам нужно обновить только одну? Потом они стали другими?

Методы закидывания: в основном обертки и будут?

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

Хотя его поток управления через исключения?

@benaadams, проверьте мой ответ примечаниями к обзору API: https://github.com/dotnet/corefx/issues/574#issuecomment -308206064

  • Мы хотим включить дубликаты (аналогично другим коллекциям):

    • Избавьтесь от Update - пользователь может вернуть Remove а затем Enqueue item с другим приоритетом.

    • Remove должен удалить только 1-й найденный элемент (как это делает List ).

Хотя его поток управления через исключения?

Не уверен, что вы имеете в виду. Похоже, это довольно распространенный паттерн в BCL.

Хотя его поток управления через исключения?

Не уверен, что вы имеете в виду.

Если у вас есть только методы TryX

  • если вам небезразличен результат; ты проверяешь это
  • если вас не волнует результат, не проверяйте его

Без исключения участие; но название побуждает вас принять решение выбросить возврат.

Если у вас есть методы генерации исключений, отличные от Try

  • если вам небезразличен результат; вам нужно либо предварительно проверить, либо использовать try / catch для обнаружения
  • если вас не волнует результат; вам нужно либо очистить метод catch, либо получить неожиданные исключения

Итак, вы находитесь в анти-паттерне использования обработки исключений для управления потоком . Они просто кажутся немного лишними; всегда можно просто добавить бросок, если false, чтобы воссоздать.

Похоже, это довольно распространенный паттерн в BCL.

Методы TryX не были обычным шаблоном в исходном BCL; до одновременных типов (хотя думаете, что Dictionary.TryGetValue в 2.0 может быть первым примером?)

например, методы TryX только что были добавлены в Core для Queue и Stack и еще не являются частью Framework.

Стабильность

  • Стабильность не дается бесплатно. Это связано с накладными расходами на производительность и память. Для обычного использования стабильность очередей приоритетов не важна.
  • Выставляем IComparer . Если заказчик хочет стабильности, он может легко добавить его самостоятельно. Было бы легко построить StablePriorityQueue поверх нашей реализации - используя обычный подход с сохранением «возраста» элемента и использованием его во время сравнений.

IQueue

Значит, есть конфликты API. Давайте теперь рассмотрим простой IQueue<T> чтобы он работал с существующим Queue<T> :

public interface IQueue<T> :
    IEnumerable,
    IEnumerable<T>,
    IReadOnlyCollection<T>
{
    void Enqueue(T element);

    T Peek();
    T Dequeue();

    bool TryPeek(out T element);
    bool TryDequeue(out T element);
}

У нас есть два варианта приоритетной очереди:

  1. Реализуйте IQueue<TElement> .
  2. Реализуйте IQueue<(TElement, TPriority)> .

Я напишу значение обоих путей, используя простые числа (1) и (2).

Поставить в очередь

В очереди приоритетов нам нужно поставить в очередь и элемент, и его приоритет (текущее решение). В обычную кучу мы добавляем всего один элемент.

  1. В первом случае мы открываем Enqueue(TElement) , что довольно странно для очереди с приоритетом, если мы вставляем элемент без приоритета. Мы тогда вынуждены делать ... что? Допустим, default(TPriority) ? Неа...
  2. Во втором случае мы выставляем Enqueue((TElement, TPriority) element) . По сути, мы добавляем два аргумента, принимая созданный из них кортеж. Не идеальный API наверное.

Peek & Dequeue

Вы хотели, чтобы эти методы возвращали только TElement .

  1. Работает с тем, чего вы хотите достичь.
  2. Не работает с тем, чего вы хотите достичь - возвращает (TElement, TPriority) .

Перечислить

  1. Заказчик не может написать ни один запрос LINQ, который использует приоритеты элементов. Это не правильно.
  2. Оно работает.

Мы могли бы смягчить некоторые проблемы, указав PriorityQueue<T> , где нет отдельного физического приоритета.

  • Пользователь, по сути, вынужден написать класс-оболочку для своего кода, чтобы иметь возможность использовать наш класс. Это означает, что во многих случаях они также захотят реализовать IComparable . Что добавляет много довольно шаблонного кода (и, скорее всего, новый файл в их исходный код).
  • Очевидно, мы можем обсудить это повторно, если хотите. Я также предлагаю альтернативный подход ниже.

Две приоритетные очереди

Если мы поставим две очереди с приоритетом, общее решение будет более мощным и гибким. Следует сделать несколько заметок:

  • Мы предоставляем два класса для обеспечения одинаковой функциональности, но только для различных типов входного формата. Мне не лучше.
  • PriorityQueue<T> потенциально может реализовать IQueue<T> .
  • PriorityQueue<TElement, TPriority> представит странный API, если попытается реализовать интерфейс IQueue .

Что ж… это сработает . Хотя не идеально.

Обновление и удаление

Учитывая предположение, что мы хотим разрешить дубликаты и не хотим использовать концепцию дескриптора:

  • Невозможно полностью корректно обеспечить функционал обновления приоритетов элементов (обновлять только конкретный узел). Аналогично это относится к удалению элементов.
  • Кроме того, обе операции должны выполняться за 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 .
  • Простой API и простое использование доступны с использованием PriorityQueue - для людей, которые хотят, чтобы их код просто работал .
  • Мы не сталкиваемся с проблемами Java.
  • Мы могли бы сделать PriorityQueue стабильным сейчас - это больше не компромисс.
  • Решение находится в гармонии с чувством, что люди с более сильным образованием в области компьютерных наук будут знать о существовании Heap . Они также будут знать об ограничениях PriorityQueue и, таким образом, могут использовать вместо них Heap (например, если они хотят иметь больший контроль над обновлением / удалением элементов или не хотят, чтобы данные структура должна быть стабильной за счет скорости и использования памяти).
  • И очередь с приоритетом, и куча могут легко разрешить дублирование, не ставя под угрозу их функциональность (потому что их цели разные).
  • Было бы проще создать общий интерфейс IQueue потому что мощность и функциональность были бы перенесены в поле кучи. API PriorityQueue можно было бы сфокусировать на том, чтобы сделать его совместимым с Queue через абстракцию.
  • Нам больше не нужно предоставлять интерфейс IPriorityQueue (мы скорее сосредоточимся на сохранении функциональности PriorityQueue и Queue аналогичными). Вместо этого мы можем добавить его в область кучи и получить там IHeap . Это здорово, потому что позволяет людям создавать сторонние библиотеки поверх того, что есть в стандартной библиотеке. И это кажется правильным - потому что, опять же, мы считаем, что кучи более продвинуты, чем очереди с приоритетом , поэтому в этой области будут предоставляться расширения. Такие расширения также не пострадали бы от выбора, который мы сделали бы в PriorityQueue , потому что он был бы отдельным.
  • Нам больше не нужно рассматривать конструктор IHeap для PriorityQueue .
  • Приоритетная очередь была бы полезным классом для внутреннего использования в CoreFX. Однако, если мы добавим такие функции, как стабильность, и откажемся от некоторых других функций, нам, скорее всего, понадобится что-то более мощное, чем это решение. К счастью, в нашем распоряжении был бы более мощный и производительный Heap !

По сути, приоритетная очередь будет сосредоточена в первую очередь на простоте использования за счет: производительности, мощности и гибкости. Куча предназначена для тех, кто осведомлен о производительности и функциональных последствиях наших решений в очереди с приоритетами. Мы смягчаем многие проблемы с помощью компромиссов.

Если мы за эксперимент, я думаю, что сейчас это возможно. Давайте просто спросим у сообщества. Не все читают эту ветку - мы можем генерировать другие ценные комментарии и сценарии использования. Что вы думаете? Лично мне бы очень понравилось такое решение. Думаю, это сделает всех счастливыми.

Важное примечание: если нам нужен такой подход, нам нужно спроектировать приоритетную очередь и кучу вместе . Потому что их цели были бы разными, и одно решение обеспечивало бы то, чего не могло бы дать другое.

Доставка IQueue тогда

С подходом, представленным выше, для того, чтобы приоритетная очередь реализовывала IQueue<T> (чтобы это имело смысл), и предполагая, что мы откажемся от поддержки селектора там, он должен иметь один общий тип. Хотя это означало бы, что пользователям необходимо предоставить оболочку, если у них есть (user data, priority) отдельно, такое решение по-прежнему интуитивно понятно. И, что наиболее важно, он позволяет использовать все форматы ввода (вот почему это должно быть сделано таким образом, если мы отбросим селектор). Без селектора Enqueue(TElement, TPriority) не допускал бы уже сопоставимых типов. Один универсальный тип также важен для перечисления - так что этот метод может быть включен в IQueue<T> .

Разное

@svick

Определено ли, в каком порядке возвращаются элементы при перечислении? Я предполагаю, что он не определен, чтобы сделать его эффективным.

Чтобы иметь порядок при перечислении, нам необходимо отсортировать коллекцию. Вот почему да, он должен быть неопределенным, поскольку клиент может просто запустить OrderBy самостоятельно и достичь примерно такой же производительности (но некоторым людям это не нужно).

Идея: в приоритетной очереди это можно было бы заказать, в куче - неупорядочить. Он чувствует себя лучше. Почему-то очередь с приоритетом кажется повторением по порядку. Куча точно нет. Еще одно преимущество описанного выше подхода.

@pgolebiowski
Все это кажется очень разумным. Не могли бы вы пояснить, в разделе Delivering IQueue then вы предлагаете T : IComparable<T> для «одного универсального типа» в качестве альтернативы селектору с учетом допуска повторяющихся элементов?

Я бы поддержал наличие двух разных типов.

Я не понимаю причин использования object в качестве типа дескриптора: это просто для того, чтобы избежать создания нового типа? Определение нового типа обеспечит минимальные накладные расходы на реализацию, в то же время затрудняя неправильное использование API (что мешает мне попытаться передать string в Remove(object) ?) И более простое в использовании (что мешает мне пытаться передать сам элемент Remove(object) , и кто может винить меня за попытку?).

Я предлагаю дополнительный фиктивный тип с подходящим названием для замены object в методах дескриптора в интересах более выразительного интерфейса.

Если возможность отладки превосходит накладные расходы памяти, тип дескриптора может даже включать информацию о том, к какой очереди он принадлежит (должен стать универсальным, что повысит безопасность типа интерфейса), обеспечивая полезные исключения в строках ("Предоставленный дескриптор был создан другой Очередью "), или был ли он уже использован (" Элемент, на который ссылается Дескриптор, уже удален ").

Если идея дескриптора будет реализована, я бы предположил, что, если эта информация будет сочтена полезной, то подмножество этих исключений также будет выдано соответствующими методами TryRemove и TryUpdate , за исключением того, где элемента больше нет, потому что он был исключен из очереди или удален дескриптором. Это повлечет за собой менее скучный, общий тип ручки с подходящим названием.

@VisualMelon

Не могли бы вы пояснить, в разделе Delivering IQueue then вы предлагаете T : IComparable<T> для «одного универсального типа» в качестве альтернативы селектору с учетом допуска повторяющихся элементов?

Приносим извинения за то, что не прояснили это.

  • Я имел в виду доставку PriorityQueue<T> без ограничения T .
  • Он все равно будет использовать IComparer<T> .
  • Если случится так, что T уже сопоставимо, тогда будет принято просто Comparer<T>.Default (и вы можете вызвать конструктор по умолчанию очереди приоритетов без аргументов).
  • Селектор имел другое назначение - иметь возможность потреблять все типы пользовательских данных. Есть несколько конфигураций:

    1. Пользовательские данные отделены от приоритетных (два физических экземпляра).
    2. Пользовательские данные содержат приоритет.
    3. Пользовательские данные - приоритет.
    4. Редкий случай: приоритет можно получить с помощью другой логики (находится в объекте, отличном от данных пользователя).

    Редкий случай невозможен в PriorityQueue<T> , но это не имеет большого значения. Важно то, что теперь мы можем обрабатывать (1), (2) и (3). Однако, если бы у нас было два универсальных типа, нам бы потребовался такой метод, как Enqueue(TElement, TPrioriity) . Это ограничило бы нас только (1). (2) приведет к избыточности. (3) было бы невероятно уродливо. Подробнее об этом в разделе IQueue > Enqueue выше (второй метод Enqueue и default(TPriority ).

Надеюсь, теперь стало понятнее.

Кстати, при таком решении разработка API PriorityQueue<T> и IQueue<T> была бы тривиальной. Просто возьмите некоторые методы из Queue<T> , добавьте их в IQueue<T> и заставьте PriorityQueue<T> реализовать их. Тадаа! 😄

Я не понимаю смысла использования object в качестве типа дескриптора: это просто для того, чтобы избежать создания нового типа?

  • Да, именно так. Учитывая предположение, что мы не хотим раскрывать такой тип, это единственное решение, чтобы по-прежнему использовать концепцию дескриптора (и, следовательно, иметь большую мощность и скорость). Я согласен, что это не идеально - это было скорее разъяснение того, что должно произойти, если мы будем придерживаться текущего подхода и хотим иметь больше мощности и эффективности (что я против).
  • Учитывая, что мы будем использовать альтернативный подход (отдельная очередь приоритетов и кучи), это проще. Мы могли бы позволить PriorityQueue<T> находиться в System.Collections.Generic , а функциональность кучи была бы в System.Collections.Specialized . Там у нас будет больше шансов ввести такой тип, и в конечном итоге получить замечательное обнаружение ошибок во время компиляции.
  • Но еще раз - очень важно разработать функциональность очереди приоритетов и кучи вместе, если мы хотим иметь такой подход. Потому что одно решение обеспечивает то, чего нет в другом.

Если возможность отладки превосходит накладные расходы памяти, тип дескриптора может даже включать информацию о том, к какой очереди он принадлежит (должен стать универсальным, что повысит безопасность типа интерфейса), обеспечивая полезные исключения в строках ("Предоставленный дескриптор был создан другой Очередью "), или был ли он уже использован (" Элемент, на который ссылается Дескриптор, уже удален ").

Если идея ручки будет реализована, я бы предложил, если эта информация будет сочтена полезной ...

Тогда определенно возможно большее: wink :. Особенно, чтобы расширить все это нашим сообществом в сторонних библиотеках.

@karelz @safern @ianhays @terrajobst @bendono @svick @ alexey-dvortsov @SamuelEnglard @ xied75 и другие - как вы думаете, такой подход будет иметь смысл (как описано в этом и этом посте)? Удовлетворит ли это все ваши ожидания и потребности?

Я думаю, что идея создания двух классов имеет большой смысл и решает множество проблем. Хотя у меня есть только то, что для 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 .
  • Это стабильно.
  • Допускает дубликаты.
  • Remove и TryRemove удаляют только первое вхождение (если они его находят).
  • Перечисление заказное.

Кучи

Не пишу здесь сейчас все, но:

  • В System.Collections.Specialized .
  • Это было бы нестабильно (и поэтому быстрее и эффективнее с точки зрения памяти).
  • Обработка поддержки, правильное обновление и удаление

    • выполняется быстро за O (log n) вместо O (n)

    • сделано правильно

  • Перечисление не упорядочено (быстрее).
  • Допускает дубликаты.

Согласен с IQueueпредложение. Сегодня я собирался представить то же самое, мне кажется, что на уровне интерфейса должен быть правильный уровень абстракции. «Интерфейс к структуре данных, в которую можно добавлять элементы и удалять из нее упорядоченные элементы».

  • Спецификация для IQueueвыглядит неплохо.

  • Подумайте о добавлении int Count {get;} в IQueueтак что ясно, что Count желателен независимо от того, наследуем ли мы от IReadOnlyCollection.

  • На заборе относительно TryPeek, TryDequeue в IQueueучитывая, что они не в очереди, но эти помощники, вероятно, также должны быть добавлены в очередь и стек.

  • IsEmpty кажется выбросом; не многие другие типы коллекций в BCL имеют его. Чтобы добавить его в интерфейс, мы должны предположить, что он будет добавлен в очередь., и кажется немного странным добавление его в очередьи ничего больше. Рекомендуя удалить его из интерфейса и, возможно, из класса.

  • Отбросьте TryRemove и измените Remove на «bool Remove». Здесь будет важно поддерживать согласованность с другими классами коллекций - у разработчиков будет много мышечной памяти, которая говорит, что «remove () в коллекции не бросает». Это область, которую многие разработчики не будут хорошо тестировать, и она вызовет много сюрпризов, если изменить нормальное поведение.

Из вашей предыдущей цитаты @pgolebiowski

  1. Пользовательские данные отделены от приоритетных (два физических экземпляра).
  2. Пользовательские данные содержат приоритет.
  3. Пользовательские данные - приоритет.
  4. Редкий случай: приоритет можно получить с помощью другой логики (находится в объекте, отличном от данных пользователя).

Также рекомендую рассмотреть 5. Пользовательские данные содержат приоритет в нескольких полях (как мы видели в Lucene.net)

На заборе относительно TryPeek, TryDequeue в IQueue, учитывая, что они не в очереди

Это System / Collections / Generic / Queue.cs # L253-L295.

С другой стороны, в очереди нет
c# public void Remove(T element); public bool TryRemove(T element);

Подумайте о добавлении int Count {get;} в IQueue, чтобы было ясно, что Count желателен независимо от того, наследуем ли мы от IReadOnlyCollection или нет.

OK. Буду дорабатывать его.

IsEmpty кажется выбросом; не многие другие типы коллекций в BCL имеют его.

Этот был добавлен @karelz , я его просто скопировал. Хотя мне нравится, может быть рассмотрен в обзоре API :)

Отбросьте TryRemove и измените Remove на «bool Remove».

Я думаю, что Remove и TryRemove совместимы с другими подобными методами ( Peek и TryPeek или Dequeue и TryDequeue ).

  1. Пользовательские данные содержат приоритет в нескольких полях

Это верный момент, но на самом деле с этим также можно справиться с помощью селектора (в конце концов, это любая функция) - но это для кучи.

IsEmpty кажется выбросом; не многие другие типы коллекций в BCL имеют его.

FWIW, bool IsEmpty { get; } - это то, что я хотел бы добавить в IProducerConsumerCollection<T> , и с тех пор я сожалел, что этого не было несколько раз. Без него оболочкам часто необходимо выполнять эквивалент Count == 0 , который для некоторых коллекций значительно менее эффективен для реализации, в частности, для большинства параллельных коллекций.

@pgolebiowski Как вы относитесь к идее создания репозитория github для размещения текущих контрактов API и одного или двух файлов .md, содержащих обоснование дизайна. Как только это стабилизируется, его можно будет использовать как место для создания начальной реализации, прежде чем делать PR до CoreFxLabs, когда он будет готов?

@svick

Если Peek и Dequeue использовали параметры out для приоритета, тогда у них также могли быть перегрузки, которые вообще не возвращают приоритет, что, я думаю, упростило бы обычное использование.

Согласовано. Добавим их.

Должен ли быть способ перечислить только элементы в очереди с приоритетами, игнорируя приоритеты?

Хороший вопрос. Что бы вы предложили? IEnumerable<TElement> Elements { get; } ?

Стабильность - я думаю, это будет означать, что внутренне приоритет должен быть чем-то вроде пары (priority, version)

Я думаю, этого можно избежать, если рассматривать Update как логическое Remove + Enqueue . Мы бы всегда добавляли элементы в конец элементов с одинаковым приоритетом (в основном считайте результат компаратора 0 как -1). ИМО, это должно работать.


@benaadams

Методы TryX не были обычным шаблоном в исходном BCL; до одновременных типов (хотя думаете, что Dictionary.TryGetValue в 2.0 может быть первым примером?)
например, методы TryX только что были добавлены в Core для Queue и Stack и еще не являются частью Framework.

Признаюсь, я все еще новичок в BCL. Из собраний по обзору API и того факта, что мы недавно добавили кучу методов Try* меня сложилось впечатление, что это распространенный шаблон гораздо дольше 😉.
В любом случае, сейчас это обычный шаблон, и мы не должны бояться его использовать. Тот факт, что шаблон еще не реализован в .NET Framework, не должен останавливать нас при внедрении инноваций в .NET Core - это его основная цель - быстрее внедрять инновации.


@pgolebiowski

В качестве альтернативы мы можем полностью удалить эту функцию из очереди приоритетов и вместо этого добавить ее в кучу. Это имеет множество преимуществ

Хм, что-то мне подсказывает, что у вас тут может быть повестка дня 😆
А если серьезно, это действительно хорошее направление, к которому мы стремились все время - PriorityQueue никогда не должно было быть причиной НЕ делать Heap . Если нас всех устраивает тот факт, что Heap может не попасть в CoreFX и может остаться «просто» в репозитории CoreFXExtensions в качестве расширенной структуры данных наряду с PowerCollections, меня это устраивает,

Важное примечание: если нам нужен такой подход, нам нужно спроектировать приоритетную очередь и кучу вместе. Потому что их цели были бы разными, и одно решение обеспечивало бы то, чего не могло бы дать другое.

Я не понимаю, зачем нам делать это вместе. ИМО, мы можем сосредоточиться на PriorityQueue и добавить «правильные» Heap параллельно / позже. Я не возражаю, если кто-то будет делать их вместе, но я не вижу веских причин, по которым наличие простых в использовании PriorityQueue должно повлиять на дизайн "правильного / высокопроизводительного" продвинутого Heap семья.

IQueue

Спасибо, что написали. Учитывая ваши баллы, я не думаю, что мы должны изо всех сил добавлять IQueue . ИМО это приятно иметь. Если бы у нас был селектор, то это было бы естественно. Однако мне не нравится подход селектора, поскольку он привносит странность и сложность в описание того, когда селектор вызывается PriorityQueue (только для Enqueue и Update .

Альтернативный (объектный) дескриптор

На самом деле это неплохая идея (хотя и немного уродливая) иметь такие перегрузки 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 - https://github.com/dotnet/corefx/issues/574#issuecomment -308427321

IQueue<T> - мы могли бы добавить Remove и TryRemove , но в Queue<T> их нет.

Имеет ли смысл добавлять их в Queue<T> ? ( @ebickle соглашается) Если да, мы также должны связать это дополнение.
Давайте добавим его в интерфейс и обозначим, что они сомнительны / нуждаются в добавлении Queue<T> .
То же самое для IsEmpty - все, что требует добавления Queue<T> и Stack<T> , отметьте это в интерфейсе (так будет легче просматривать и переваривать).
@pgolebiowski, не могли бы вы добавить комментарий к IQueue<T> со списком классов, которые, как мы думаем, будут реализовывать?

PriorityQueue<T>

Давайте добавим перечислитель структур (я думаю, что в последнее время это обычный шаблон BCL). Он был вызван пару раз в ветке, затем отброшен / забыт.

Кучи

Выбор пространства имен: давайте пока не будем тратить время на решение о пространстве имен. Если Heap попадает в CoreFxExtensions, мы еще не знаем, какие пространства имен мы там разрешим. Может, Community.* или что-то в этом роде. Это зависит от результата обсуждения назначения / режима работы CoreFxExtensions.
Примечание. Одна из идей для репозитория CoreFxExtensions - предоставить проверенным членам сообщества права на запись и позволить им управлять в первую очередь сообществом с командой .NET, предоставляющей только советы (включая экспертизу API), а команда .NET / MS выступает в роли арбитра, когда это необходимо. Если мы приземлимся именно здесь, мы, скорее всего, не захотим, чтобы это было в пространстве имен System.* или Microsoft.* . (Отказ от ответственности: раннее размышление, пожалуйста, пока не спешите с выводами, есть и другие альтернативные идеи управления в стадии разработки)

Отбросьте TryRemove и измените Remove на bool Remove . Здесь будет важно поддерживать согласованность с другими классами коллекций - у разработчиков будет много мышечной памяти, которая говорит, что «remove () в коллекции не бросает». Это область, которую многие разработчики не будут хорошо тестировать, и она вызовет много сюрпризов, если изменить нормальное поведение.

👍 Мы обязательно должны хотя бы рассмотреть возможность согласования его с другими коллекциями. Может ли кто-нибудь сканировать другие коллекции, каков их шаблон Remove ?

@ebickle Как вы относитесь к идее создания репозитория github для размещения текущих контрактов API и одного или двух файлов .md, содержащих обоснование дизайна.

Разместим его прямо в

@karelz

Должен ли быть способ перечислить только элементы в очереди с приоритетами, игнорируя приоритеты?

Хороший вопрос. Что бы вы предложили? IEnumerable<TElement> Elements { get; } ?

Да, либо это, либо свойство, возвращающее какую-то сумму ElementsCollection , вероятно, лучший вариант. Тем более что это похоже на Dictionary<K, V>.Values .

Мы бы всегда добавляли элементы в конец элементов с одинаковым приоритетом (в основном считайте результат компаратора 0 как -1). ИМО, это должно работать.

Кучи работают не так, здесь нет «конца элементов с одинаковым приоритетом». Элементы с одинаковым приоритетом могут быть распределены по всей куче (если они не близки к текущему минимуму).

Или, другими словами, для поддержания стабильности вы должны рассматривать результат компаратора, равный 0, как -1 не только при вставке, но и при последующем перемещении элементов в куче. И я думаю, вам нужно сохранить что-то вроде version вместе с priority чтобы сделать это правильно.

IQueue- мы могли бы добавить Remove и TryRemove, но Queueих нет.

Будет ли им смысл добавлять в очередь?? ( @ebickle соглашается) Если да, мы также должны связать это дополнение.

Я не думаю, что Remove следует добавлять к IQueue<T> ; и я бы даже посоветовал Contains хитроумно; он ограничивает полезность интерфейса и типы очередей, для которых он может использоваться, если только вы не начнете также генерировать NotSupportedExceptions. т.е. каков объем?

Это только для обычных очередей, очередей сообщений, распределенных очередей, очередей ServiceBus, очередей хранилища Azure, надежных очередей ServiceFabric и т. Д. (Замалчивая некоторые из них, предпочитая асинхронные методы)

Кучи работают не так, здесь нет «конца элементов с одинаковым приоритетом». Элементы с одинаковым приоритетом могут быть распределены по всей куче (если они не близки к текущему минимуму).

Честная оценка. Я думал об этом как о двоичном дереве поиска. Я думаю, нужно почистить мои основы ds :)
Что ж, либо мы реализуем его с разными ds (массив, двоичное дерево поиска (или другое официальное название) - у соглашаемся на случайный порядок. Я не думаю, что поддержание полей version или age стоит гарантии стабильности.

@ebickle Как вы относитесь к идее создания репозитория github для размещения текущих контрактов API и одного или двух файлов .md, содержащих обоснование дизайна.

@karelz Разместим его прямо в CoreFxLabs.

Пожалуйста, ознакомьтесь с этим документом .

Было бы неплохо иметь 2 предложения (и мы потенциально можем принять оба):

  • PriorityQueueбез селектора и без IQueue (что было бы громоздко)
  • PriorityQueueс селектором и IQueue

Я вообще избавился от селектора. Это необходимо только в том случае, если мы хотим иметь один единый PriorityQueue<T, U> . Если у нас два класса - ну и компаратора достаточно.


Пожалуйста, дайте мне знать, если вы найдете что-нибудь, что стоит добавить / изменить / удалить.

@pgolebiowski

Документ выглядит хорошо. Я начинаю чувствовать себя надежным решением, которое я ожидал увидеть в основных библиотеках .NET :)

  • Следует добавить вложенный класс Enumerator. Не уверен, изменилась ли ситуация, но много лет назад дочерняя структура позволила избежать выделения сборщиком мусора, вызванного упаковкой результата 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также должна иметь вложенную структуру перечислителя.

  • Я все еще голосую за «public bool Remove (T element)» без TryRemove, поскольку это давний шаблон, и его изменение делает ошибки разработчика весьма вероятными. Мы можем позволить команде проверки API вмешаться, но, на мой взгляд, это открытый вопрос.

  • Есть ли какое-то значение в указании начальной емкости в конструкторе или наличии функции TrimExcess, или это микрооптимизация прямо сейчас - особенно с учетом IEnumerableпараметры конструктора?

@pgolebiowski благодарит за то, что Отметьте @ianhays @safern, они смогут объединить PR.
Затем мы можем использовать проблемы CoreFxLab для дальнейшего обсуждения конкретных моментов проектирования - это должно быть проще, чем эта мега-проблема (я только что запустил электронное письмо, чтобы создать там область PriorityQueue).

Пожалуйста, разместите здесь ссылку на пулреквест CoreFxLab?

  • @ebickle @ jnm2 - добавлено
  • @karelz @SamuelEnglard - упоминается

Если API будет одобрен в его текущей форме (два класса), я бы создал еще один PR для CoreFXLab с реализацией для обоих с использованием четвертичной кучи ( см. Реализацию ). PriorityQueue<T> будет использовать только один массив внизу, а PriorityQueue<TElement, TPriority> будет использовать два - и переставить их элементы вместе. Это могло бы избавить нас от дополнительных ассигнований и быть максимально эффективным. Добавлю, что когда загорится зеленый свет.

Есть ли план создания поточно-ориентированной версии, такой как ConcurrentQueue?

Я бы: heart: увидеть параллельную версию этого, реализующую IProducerConsumerCollection<T> , чтобы ее можно было использовать с BlockingCollection<T> и т. Д.

@aobatact @khellang Звучит как совершенно другая тема:

Я согласен, это очень ценно: wink :!

public bool IsEmpty();

Почему это метод? Каждая другая коллекция в платформе, которая имеет IsEmpty имеет это как свойство.

Я обновил предложение в верхнем посте, добавив IsEmpty { get; } .
Я также добавил ссылку на последний документ с предложением в репозиторий corefxlab.

Всем привет,
Я думаю, что многие люди утверждают, что было бы хорошо поддержать обновление, если это возможно. Думаю, никто не усомнился в том, что это полезная функция для поиска по графам.

Но кое-где высказывались возражения. Итак, я могу проверить свое понимание, похоже, что основные возражения таковы:
- непонятно, как должно работать обновление при наличии повторяющихся элементов.
- есть некоторые аргументы относительно того, желательно ли поддерживать повторяющиеся элементы в очереди с приоритетом
- есть некоторые опасения, что наличие дополнительной структуры данных поиска для поиска элементов в резервной структуре данных может быть неэффективным только для обновления их приоритета. Специально для сценариев, когда обновление никогда не выполняется! И / или повлиять на гарантии производительности в худшем случае ...

Я что-то пропустил?

Хорошо, я предполагаю, что еще один был - утверждалось, что обновление / удаление семантически означающее «updateFirst» или «removeFirst» может быть странным. :)

В какой версии .Net Core мы можем начать использовать PriorityQueue?

@memoryfraction В самом .NET Core пока нет PriorityQueue (есть предложения, но в последнее время у нас не было времени на это потратить). Однако нет причин, по которым он должен быть в дистрибутиве Microsoft .NET Core. Любой участник сообщества может разместить его на NuGet. Возможно, кто-то по этому вопросу может предложить такое.

@memoryfraction В самом .NET Core пока нет PriorityQueue (есть предложения, но в последнее время у нас не было времени на это потратить). Однако нет причин, по которым он должен быть в дистрибутиве Microsoft .NET Core. Любой участник сообщества может разместить его на NuGet. Возможно, кто-то по этому вопросу может предложить такое.

Спасибо за ваш ответ и предложение.
Когда я использую C # для практики leetcode и мне нужно использовать SortedSet для обработки набора с повторяющимися элементами. Мне нужно обернуть элемент уникальным идентификатором, чтобы решить проблему.
Поэтому в будущем я предпочитаю иметь очередь приоритетов в .NET Core, потому что так будет удобнее.

Каково текущее состояние этой проблемы? Я недавно обнаружил, что мне требуется PriorityQueue

Это предложение не включает инициализацию коллекции по умолчанию. PriorityQueue<T> не имеет метода Add . Я думаю, что инициализация коллекции - это достаточно большая языковая функция, чтобы гарантировать добавление дублирующего метода.

Если у кого-то есть быстрая двоичная куча, которой он хотел бы поделиться, я хотел бы сравнить ее с тем, что я написал.
Я полностью реализовал API, как было предложено. Однако вместо кучи он использует простой отсортированный массив. Я оставляю элемент с наименьшей стоимостью в конце, поэтому он отсортирован в обратном порядке. Когда количество элементов меньше 32, я использую простой линейный поиск, чтобы найти, где вставить новые значения. После этого я использовал бинарный поиск. Используя случайные данные, я обнаружил, что этот подход быстрее, чем двоичная куча в простом случае заполнения очереди и последующего ее осушения.
Если у меня будет время, я помещу его в публичный репозиторий git, чтобы люди могли его критиковать и обновить этот комментарий, указав местоположение.

@SunnyWar Я сейчас использую: https://github.com/BlueRaja/High-Speed-Priority-Queue-for-C-Sharp
который имеет хорошую производительность. Я думаю, имеет смысл протестировать вашу реализацию против GenericPriorityQueue там

Пожалуйста, не обращайте внимания на мой предыдущий пост. В тесте обнаружил ошибку. После исправления двоичная куча работает лучше всего.

@karelz Где сейчас это предложение на ICollection<T> ? Я не понял, почему это не будет поддерживаться, хотя коллекция явно не предназначена только для чтения?

@karelz re открытый вопрос о порядке удаления из очереди: я утверждаю, что сначала удаляется значение с наименьшим приоритетом. Это удобно для алгоритмов кратчайшего пути и естественно для реализаций очереди с таймером. И это два основных сценария, для которых, я думаю, он будет использоваться. Я действительно не знаю, каков будет противоположный аргумент, если только он не лингвистический, связанный с «высокими» приоритетами и «высокими» числовыми значениями.

@karelz re порядок перечисления ... как насчет наличия метода Sort() для типа, который поместит внутреннюю кучу данных коллекции в отсортированный порядок на месте (в O (n log n)). И вызов Enumerate() после Sort() перечисляет коллекцию за O (n). И если Sort () НЕ вызывается, он возвращает элементы в несортированном порядке за O (n)?

Triage: переход в будущее, поскольку, похоже, нет единого мнения о том, следует ли нам это реализовывать.

Это очень обидно слышать. Мне все еще приходится использовать для этого сторонние решения.

Каковы основные причины, по которым вам не нравится использовать сторонние решения?

структура данных кучи ОБЯЗАТЕЛЬНА для выполнения leetcode
больше leetcode, больше интервью по коду C #, что означает больше разработчиков C #.
больше разработчиков означает лучшую экосистему.
лучшая экосистема означает, что если мы еще сможем программировать на C # завтра.

В общем: это не только особенность, но и будущее. именно поэтому вопрос помечен как «будущее».

Каковы основные причины, по которым вам не нравится использовать сторонние решения?

Потому что, когда нет стандарта, каждый придумывает свой, каждый со своим набором причуд.

Было много раз, когда я хотел получить System.Collections.Generic.PriorityQueue<T> потому что он имеет отношение к тому, над чем я работаю. Это предложение существует уже 5 лет. Почему этого до сих пор не произошло?

Почему этого до сих пор не произошло?

Приоритетное голодание, никакой каламбур. Создание коллекций - это работа, потому что, если мы помещаем их в BCL, мы должны думать о том, как они обмениваются, какие интерфейсы они реализуют, какова семантика и т. Д. Все это не ракетостроение, но это требует времени. Кроме того, вам нужен набор конкретных пользовательских случаев и клиентов, по которым можно судить о дизайне, что становится все сложнее, поскольку коллекции становятся все более и более специализированными. До сих пор всегда были другие работы, которые считались более важными, чем это.

Давайте посмотрим на недавний пример: неизменяемые коллекции. Они были разработаны кем-то, кто не работает в команде BCL, для случая использования в VS, связанного с неизменяемостью. Мы работали с ним, чтобы получить "BCL-ified" API. И когда Roslyn появился в сети, мы заменили их копии на наши в максимально возможном количестве мест и сильно изменили дизайн (и реализацию) на основе их отзывов. Без «героического» сценария это сложно.

Потому что, когда нет стандарта, каждый придумывает свой, каждый со своим набором причуд.

@masonwheeler это то, что вы видели за PriorityQueue<T> ? Что есть несколько вариантов сторонних производителей, которые не являются взаимозаменяемыми, и что нет однозначно признанной "лучшей библиотеки для большинства"? (Я не проводил исследования, поэтому не знаю ответа)

@eiriktsarpalis Почему нет единого мнения о том, следует ли внедрять?

@terrajobst Вам действительно нужен сценарий героя, когда это хорошо известный шаблон с хорошо известными приложениями? Здесь не должно потребоваться очень мало нововведений. Уже есть довольно развитая спецификация интерфейса. На мой взгляд, настоящая причина, по которой эта утилита не существует, заключается не в том, что она слишком сложна, не требует и не использует ее. Настоящая причина в том, что для любого реального программного проекта проще просто создать его самостоятельно, чем пытаться вести политическую кампанию за его включение в библиотеки фреймворка.

Каковы основные причины, по которым вам не нравится использовать сторонние решения?

@terrajobst
Я лично убедился, что сторонние решения не всегда работают должным образом / не используют текущие языковые функции. Со стандартизированной версией я (как пользователь) мог быть уверен, что производительность будет наилучшей из возможных.

@danmosemsft

@masonwheeler это то, что вы видели за PriorityQueue<T> ? Что есть несколько вариантов сторонних производителей, которые не являются взаимозаменяемыми, и что нет однозначно признанной "лучшей библиотеки для большинства"? (Я не проводил исследования, поэтому не знаю ответа)

да. Просто погуглите "очередь приоритетов C #"; первая страница заполнена:

  1. Реализации приоритетной очереди на Github и других хостинговых сайтах
  2. Люди спрашивают, почему в мире нет официальной очереди приоритетов в Коллекциях.
  3. Учебники по созданию собственной реализации очереди приоритетов

@terrajobst Вам действительно нужен сценарий героя, когда это хорошо известный шаблон с хорошо известными приложениями? Здесь не должно потребоваться очень мало нововведений.

По моему опыту да. Дьявол кроется в деталях, и после того, как мы выпустили API, мы не можем вносить критические изменения. И есть много вариантов реализации, которые видны пользователю. Мы можем предварительно просмотреть API как OOB какое-то время, но мы также узнали, что, хотя мы, безусловно, можем собирать отзывы, отсутствие сценария героя означает, что у вас нет никаких тай-брейков, что часто приводит к ситуации, когда герой сценарий прибывает, структура данных не соответствует его требованиям.

Видимо нам нужен герой. 😛

@masonwheeler Я предположил, что ваша ссылка будет на это 😄 Теперь это у меня в голове.

Хотя, как говорит @terrajobst , нашей основной проблемой здесь были ресурсы / внимание (и вы можете обвинить нас в этом, если хотите), мы также хотели бы усилить экосистему сторонних разработчиков, чтобы с большей вероятностью вы могли найти надежные библиотеки для чего угодно. сценарий.

[отредактировано для ясности]

@danmosemsft Нет, если бы я собирался выбрать эту песню, я бы сделал версию Shrek 2.

Кандидат в приложение-герой №1: как насчет того, чтобы использовать его в TimerQueue.Portable?

https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs

Кандидат в приложение-герой №1: как насчет того, чтобы использовать его в TimerQueue.Portable?

Уже рассмотрено, прототипировано и выброшено. Это делает очень распространенный случай быстрого создания и уничтожения таймера (например, из-за тайм-аута) менее эффективным.

@stephentoub Я полагаю, вы имеете в виду, что он менее эффективен для некоторых сценариев, где есть небольшое количество таймеров. Но как это масштабировать?

Я предполагаю, вы имеете в виду, что он менее эффективен для некоторых сценариев, где есть небольшое количество таймеров. Но как это масштабировать?

Нет, я имел в виду, что в большинстве случаев у вас есть много таймеров в любой момент, но очень немногие срабатывают. Вот что получается, когда таймеры используются для тайм-аутов. И что важно, это скорость, с которой вы можете добавлять и удалять структуру данных ... вы хотите, чтобы она была равна 0 (1) и с очень низкими накладными расходами. Если это становится O (log N), это проблема.

Наличие очереди с приоритетом определенно сделает C # более удобным для собеседований.

Наличие очереди с приоритетом определенно сделает C # более удобным для собеседований.

Да, это правда.
Ищу по той же причине.

@stephentoub Для тайм-аутов, которых на самом деле никогда не бывает, для меня это имеет смысл. Но мне интересно, что происходит с системой, когда внезапно начинает происходить много тайм-аутов, потому что внезапно происходит потеря пакетов, или сервер не отвечает, или что-то еще? Также используют ли повторяющиеся таймеры ala System.Timer ту же реализацию? Там истечение тайм-аута будет «счастливым путем».

Но мне интересно, что происходит с системой, когда внезапно начинает происходить много тайм-аутов.

Попытайся. :)

Мы видели, как в предыдущей реализации страдает много реальных рабочих нагрузок. В каждом случае, который мы рассмотрели, новый (который не использует очередь приоритетов, а вместо этого просто имеет простое разделение между таймерами «скоро» и «не скоро») устраняет проблему, и мы не видели новых с Это.

Также используют ли повторяющиеся таймеры ala 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 Я понимаю стремление к такому поведению, но, поскольку это был пятилетний запрос, не было бы разумным просто реализовать двоичную кучу на основе массива с селектором и использовать ту же площадь поверхности API, что и Queue.cs ? Я думаю, что это, безусловно, то, что нужно большинству пользователей. На мой взгляд, эта структура данных не была реализована, потому что мы не можем договориться о дизайне API, но если мы делаем PriorityQueue я считаю, что мы должны просто подражать дизайну api Queue .

@Jlalond Отсюда и предложение, а? Я не возражаю против реализации массива на основе кучи. Теперь я помню , самое большое возражение я имел против селекторов, является то , что его не просто достаточно , чтобы сделать важные обновления правильно. Обычная старая очередь не имеет операции обновления приоритета, но обновление приоритета элемента является важной операцией для многих алгоритмов очереди приоритетов.
Люди должны быть всегда благодарны, если мы используем API, который не требует от них отладки поврежденных структур очереди приоритетов.

@TimLovellSmith Извини, Тим, мне следовало уточнить наследование от IDictionary .

Есть ли у нас вариант использования, в котором изменился бы приоритет?

Мне нравится ваша реализация, но я думаю, что мы можем уменьшить репликацию IDictionary Behavior. Я думаю, что нам следует наследовать не от IDictionary<> а только от ICollection, потому что я не думаю, что шаблоны доступа к словарю интуитивно понятны,

Однако я действительно думаю, что возвращение T и связанного с ним приоритета имело бы смысл, но я не знаю варианта использования, когда мне нужно было бы знать приоритет элемента в структуре данных при его постановке или исключении из очереди.

@Jlalond
Если у нас есть приоритетная очередь, она должна поддерживать все операции, которые она может просто и эффективно выполнять, _и_ которые
"ожидается" там людьми, знакомыми с такой структурой данных / абстракцией одного.

Приоритет обновления принадлежит API, потому что он попадает в обе эти категории. Обновление приоритета является достаточно важным соображением во многих алгоритмах, поскольку сложность выполнения операции со структурами данных кучи влияет на сложность всего алгоритма, и она регулярно измеряется, см.:
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps

TBH, в основном, его reduce_key, который интересен алгоритмами и эффективностью и измеряется, поэтому я лично в порядке, потому что API имеет только DecreasePriority () и на самом деле не имеет UpdatePriority.
Но для удобства кажется лучше не беспокоить пользователей API, беспокоясь о том, является ли каждое изменение увеличением или уменьшением. Так что у разработчика Joe есть `` обновление '' прямо из коробки, не задаваясь вопросом, почему он поддерживает только уменьшение, а не увеличение (и что использовать, когда), я думаю, что лучше всего иметь общий API приоритета обновления, но задокументировать это при использовании для уменьшения он имеет стоимость X, при использовании для увеличения он имеет стоимость Y, потому что он реализован так же, как Remove + Add.

RE словарь Я согласен. Убрал его из моего предложения во имя того, что проще - лучше.
https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

Я не знаю случая использования, когда мне нужно было бы знать приоритет элемента в структуре данных при его постановке или исключении из очереди.

Я не уверен, о чем этот комментарий. Вам не нужно знать приоритет элемента, чтобы исключить его из очереди. Но вам нужно знать его приоритет, когда вы ставите его в очередь. Возможно, вы захотите узнать окончательный приоритет элемента при его удалении из очереди, поскольку это значение может иметь значение, например, расстояние.

@TimLovellSmith

Я не уверен, о чем этот комментарий. Вам не нужно знать приоритет элемента, чтобы исключить его из очереди. Но вам нужно знать его приоритет, когда вы ставите его в очередь. Возможно, вы захотите узнать окончательный приоритет элемента при его удалении из очереди, поскольку это значение может иметь значение, например, расстояние.

Извините, я неправильно понял, что имел в виду TPriorty в вашей паре "ключ-значение".

Хорошо, последние два вопроса, почему они KeyValuePair с TPriority, и лучшее время, которое я вижу для изменения приоритета, - это N log N, я согласен, это ценно, но как будет выглядеть этот API?

лучшее время, которое я вижу для изменения приоритетов, - N log N,

Это лучшее время для изменения приоритета N элементов, а не одного элемента, верно?
Я поясню документ: «UpdatePriority | O (log n)» должен читать «UpdatePriority (single item) | O (log n)».

@TimLovellSmith Имеет смысл, но разве не будет какое-либо обновление приоритета на самом деле ne O2 * (log n), поскольку нам нужно будет удалить элемент, а затем снова вставить его? Основывая свое предположение на том, что это двоичная куча

@TimLovellSmith Имеет смысл, но разве не будет какое-либо обновление приоритета на самом деле ne O2 * (log n), поскольку нам нужно будет удалить элемент, а затем снова вставить его? Основывая свое предположение на том, что это двоичная куча

Постоянные факторы, подобные этому 2, обычно игнорируются при анализе сложности, потому что они теряют значение по мере роста размера N.

Понятно, в основном хотел знать, есть ли у Тима какие-то захватывающие идеи, которыми можно только заняться.
одна операция :)

18 августа 2020 г., 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 проник в интерфейс, пока это был словарь, но пережил удаление словаря, главным образом для того, чтобы можно было перебирать коллекцию _элементов с их приоритетами_. А также, чтобы вы могли удалять элементы с их приоритетами. Но, возможно, снова для простоты удаление из очереди с приоритетами должно быть только в «расширенной» версии Dequeue API. (TryDequeue: D)

Я внесу это изменение.

@TimLovellSmith Круто, я с нетерпением жду вашего пересмотренного предложения. Можем ли мы отправить его на рассмотрение, чтобы я мог начать работу над этим? Кроме того, я знаю, что для очереди сопряжения был некоторый стимул для улучшения времени слияния, но я все же думаю, что двоичная куча на основе массива будет наиболее общей производительностью. Мысли?

@Jlalond
Я внес небольшие изменения, чтобы упростить этот API Peek + Dequeue.
Некоторая коллекцияapis, связанные с KeyValuePair, все еще остаются, потому что я не вижу ничего явно лучшего для их замены.
Та же 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], примерно равно нулю, и оно касается любых важных вопросов, которые остались открытыми из-за предложения @ebickle вверху (которое становится все более похожим на).

Итак, я утверждаю, что пока он проходит проверку на вменяемость. Должен быть еще какой-то обзор, мы можем поговорить о том, полезно ли наследовать 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 (я не имею в виду Add ): должна ли одна быть bool TryEnqueue ?
  • «// перечисляет коллекцию в произвольном порядке, но с наименьшим количеством элементов первым»: я не думаю, что последний бит полезен; Я бы предпочел, чтобы перечислитель не проводил никаких сравнений, но предполагаю, что это будет «бесплатно», поэтому меня это не беспокоит.
  • Именование EqualityComparer несколько неожиданно: я ожидал, что KeyComparer пойдет с PriorityComparer .
  • Я не понимаю примечаний о сложности CopyTo и ToArray .

@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 () возвращает элемент в начале коллекции. А также не нужно никаких сравнений для его реализации, если это двоичная куча, так что это казалось халявой. Но, может быть, он менее бесплатный, чем я думаю - неоправданная сложность? Я счастлив вынуть его по этой причине.

Две очереди были случайными. У нас есть EnqueueOrUpdate, где всегда обновляется приоритет. Я предполагаю, что TryUpdate - это логическая противоположность этого, где приоритет никогда не должен обновляться для элементов, уже находящихся в коллекции. Я вижу, что это заполняет этот логический пробел, но я не уверен, понадобится ли это API людям на практике.

Я могу только представить себе вариант с уменьшением: естественная операция при поиске маршрута - поставить узел в очередь или заменить существующий узел на узел с более низким приоритетом; Я не мог сразу предложить применение противоположному. Параметр boolean return может быть удобен, если вам нужно выполнить какую-либо операцию, когда элемент находится в очереди / не в очереди.

Я не предполагал двоичную кучу, но если она бесплатна, то у меня нет проблем с тем, что первый элемент всегда является минимальным, это просто казалось странным ограничением.

@VisualMelon @TimLovellSmith Подойдет ли один API?
EnqueueOrUpdatePriority ? Я полагаю, что для алгоритмов обхода ценна только возможность перемещать узел в очереди, даже если он увеличивает или уменьшает приоритет.

@Jlalond да, я упоминаю об этом, потому что это может быть полезно для эффективности в зависимости от реализации и напрямую кодирует вариант использования, когда вы хотите поставить что-то в очередь только в том случае, если это улучшает то, что у вас уже есть в очереди. Я не берусь утверждать, подходит ли дополнительная сложность для такого универсального инструмента, но в этом нет необходимости.

Обновлено: удаление EnqueueOrIncreasePriority с сохранением EnqueueOrUpdate и EnqueueOrDecrease. Позволяя им возвращать bool, когда элемент вновь добавляется в коллекцию, например HashSet.Add ().

@Jlalond Прошу прощения за вышеуказанную ошибку (удаление вашего последнего комментария с вопросом, когда эта проблема будет рассмотрена).

Я просто хотел упомянуть, что @eiriktsarpalis вернется на следующей неделе, и тогда мы обсудим приоритезацию этой функции и обзор API.

Это то, что мы рассматриваем, но еще не делаем для .NET 6.

Я вижу, что эта ветка была возрождена, начав с нуля и разветвившись, хотя мы подробно обсуждали эту тему в 2017 году в течение нескольких месяцев (большая часть этой огромной ветки) и коллективно подготовили это предложение - прокрутите выше, чтобы увидеть. Это также предложение, которое @karelz связал в первой строке первого сообщения для наглядности:

Смотрите ПОСЛЕДНИЕ предложения в репозитории corefxlab.

По состоянию на 16.06.2017 мы ждали проверки API, которой никогда не было, и статус был:

Если API будет одобрен в его текущей форме (два класса), я бы создал еще один PR для CoreFXLab с реализацией для обоих с использованием четвертичной кучи ( см. Реализацию ). PriorityQueue<T> будет использовать только один массив внизу, а PriorityQueue<TElement, TPriority> будет использовать два - и переставить их элементы вместе. Это могло бы избавить нас от дополнительных ассигнований и быть максимально эффективным. Добавлю, что когда загорится зеленый свет.

Я рекомендую продолжить итерацию предложения, которое мы сделали в 2017 году, начиная с официального обзора, который еще не состоялся. Это позволит нам избежать разветвления и повторения предложения, составленного после сотен сообщений, которыми обмениваются члены сообщества, также из уважения к усилиям, приложенным к этому всеми участниками. Буду рад обсудить это, если будут отзывы.

@pgolebiowski Спасибо, что вернулись к обсуждению. Я хотел бы извиниться за эффективное дальнейшее развитие предложений, что не является самым эффективным способом сотрудничества. С моей стороны это был импульсивный шаг. (У меня создалось впечатление, что обсуждение полностью зашло в тупик из-за того, что в предложениях слишком много открытых вопросов, и мне просто нужно было более категоричное предложение.)

Как насчет того, если мы попытаемся продвинуться вперед, хотя бы временно забыв кодовое содержание моего PR, и возобновим обсуждение здесь выявленных «открытых проблем»? Я хотел бы высказать свое мнение обо всех из них.

  1. Имя класса PriorityQueue vs. Heap <- я думаю, что это уже обсуждалось, и все согласны с тем, что PriorityQueue лучше.

  2. Представить IHeap и перегрузку конструктора? <- Я не предвижу большой ценности. Я думаю, что для 95% мира нет веской причины (например, дельты производительности) иметь несколько реализаций кучи, а остальным 5%, вероятно, придется писать свои собственные.

  3. Представить IPriorityQueue? <- Я тоже не думаю, что это нужно. Я предполагаю, что другие языки и фреймворки прекрасно обходятся без этих интерфейсов в их стандартной библиотеке. Сообщите мне, если это неверно.

  4. Использовать селектор (приоритета хранится внутри значения) или нет (разница в 5 API) <- я не вижу веских аргументов в пользу поддержки селекторов в очереди приоритетов. Я чувствую, что IComparer<> уже служит действительно хорошим механизмом минимальной расширяемости для сравнения приоритетов элементов, а селекторы не предлагают каких-либо значительных улучшений. Также люди могут быть сбиты с толку относительно того, как использовать селекторы с изменяемыми приоритетами (или как их НЕ использовать).

  5. Используйте кортежи (элемент TElement, приоритет TPriority) по сравнению с KeyValuePair<- Лично я считаю, что KeyValuePair является предпочтительным вариантом для очереди с приоритетами, в которой элементы имеют обновляемые приоритеты, поскольку элементы обрабатываются как ключи в наборе / словаре.

  6. Должны ли Peek и Dequeue иметь аргумент out вместо кортежа? <- Я не уверен во всех плюсах и минусах, особенно в производительности. Должны ли мы просто выбрать тот, который лучше работает в простом тесте производительности?

  7. Полезны ли броски Peek и Dequeue? <- Да ... Это то, что должно происходить с алгоритмами, которые ошибочно предполагают, что в очереди все еще есть элементы . Если вы не хотите, чтобы алгоритмы когда-либо предполагали это, лучше всего не предоставлять API Peek и Dequeue, а просто предоставить TryPeek и TryDequeue. [Я бы предположил, что есть алгоритмы, которые могут безопасно вызывать Peek или Dequeue в определенных случаях, и наличие Peek / Dequeue - небольшое улучшение производительности + удобства использования для них.]

Помимо этого, у меня также есть несколько предложений, которые я могу добавить к рассматриваемому предложению:

  1. PriorityQueueследует работать только с уникальными предметами, которые имеют собственную ручку. Он должен поддерживать настраиваемый IEqualityComparer, если он хочет сравнить нерасширяемые объекты (например, строки) определенными способами для выполнения приоритетных обновлений.

  2. PriorityQueueдолжен поддерживать различные операции, такие как «удалить», «уменьшить приоритет, если меньше», и особенно «поставить элемент в очередь или уменьшить приоритет, если меньшая операция», все в O (log n) - для эффективной и простой реализации сценариев поиска по графу.

  3. Ленивым программистам было бы удобно, если бы PriorityQueueпредоставляет оператор индекса [] для получения / установки приоритета существующих элементов. Это должно быть O (1) для получения, O (log n) для набора.

  4. Наименьший приоритет сначала лучше. Поскольку очереди с приоритетом используются в большом количестве, возникают проблемы оптимизации с «минимальными затратами».

  5. Перечисление не должно давать никаких гарантий заказа.

Открытый выпуск - обрабатывает:
PriorityQueueэлементы и приоритеты, похоже, предназначены для работы с повторяющимися значениями. Эти значения либо должны рассматриваться как `` неизменяемые '', либо должен существовать метод для вызова, чтобы уведомить коллекцию об изменении приоритетов элементов, возможно, с помощью дескрипторов, чтобы можно было точно указать приоритет того, какие дубликаты изменились (какой сценарий должен делать это ??) ... Я сомневаюсь, насколько это полезно, и насколько это хорошая идея, но если у нас есть приоритеты обновления в такой коллекции, было бы неплохо, если бы затраты, понесенные этим методом, можно было бы понести лениво, поэтому что, когда вы просто работаете с неизменяемыми элементами и не хотите использовать, никаких дополнительных затрат ... как решить? Может ли это быть из-за наличия перегруженных методов, использующих дескрипторы, и методов, которые не используют? Или у нас просто не должно быть никаких дескрипторов элементов, отличных от самих элементов (используемых в качестве хеш-ключа для поиска)?

Мысль о том, чтобы помочь ускорить этот процесс, может иметь смысл переместить этот тип в библиотеку «Коллекции сообщества», обсуждаемую в https://github.com/dotnet/runtime/discussions/42205

Я согласен с тем, что самый маленький приоритет сначала лучше. Очередь приоритетов также полезна при разработке пошаговых игр, и очень полезно иметь возможность монотонно увеличивать приоритет, когда каждый элемент получает свой следующий ход.

Мы планируем как можно скорее представить это для проверки API (хотя мы еще не взяли на себя обязательства по доставке реализации в сроки .NET 6).

Но прежде чем мы отправим это на рассмотрение, нам нужно будет сгладить некоторые из открытых вопросов высокого уровня. Я думаю , что @TimLovellSmith «s подправить является хорошей отправной точкой на пути к достижению этой цели.

Несколько замечаний по этим вопросам:

  • Консенсус по вопросам 1-3 существует давно, я думаю, мы можем рассматривать их как решенные.

  • _Использовать селектор (приоритета хранится внутри значения) или нет_ - Согласен с вашими замечаниями. Другая проблема с этим дизайном заключается в том, что селекторы являются необязательными, что означает, что вам нужно быть осторожным, чтобы не вызвать неправильную перегрузку Enqueue (или рискнуть получить InvalidOperationException ).

  • Я предпочитаю использовать кортеж, а не KeyValuePair . Мне кажется странным использование свойства .Key для доступа к значению TPriority . Кортеж позволяет вам использовать .Priority который имеет лучшие свойства самодокументирования.

  • _Должны ли Peek и Dequeue иметь аргумент out вместо кортежа? _ - Я думаю, мы должны просто следовать установленному соглашению в аналогичных методах в других местах. Так что, вероятно, используйте аргумент out .

  • _Полезны ли вообще функции Peek и Dequeue? _ - Согласен на 100% с вашими комментариями.

  • _Низкий приоритет сначала лучше_ - Согласен.

  • _Перечисление не должно предоставлять никаких гарантий заказа_. Может ли это не противоречить ожиданиям пользователей? Какие компромиссы? NB, мы, вероятно, можем отложить подобные детали для обсуждения обзора API.

Я также хотел бы перефразировать несколько других открытых вопросов:

  • Мы отправляем PriorityQueue<T> , PriorityQueue<TElement, TPriority> или оба? - Лично я считаю, что нам следует реализовывать только последнее, поскольку оно, кажется, обеспечивает более чистое и универсальное решение. В принципе приоритет не должен быть свойством, присущим элементу в очереди, поэтому мы не должны заставлять пользователей инкапсулировать его в типы оболочки.

  • Требуется ли уникальность элементов, вплоть до равенства? - Поправьте меня, если я ошибаюсь, но я считаю, что уникальность - это искусственное ограничение, навязанное нам требованием поддерживать сценарии обновления, не прибегая к методу дескриптора. Это также усложняет поверхность API, поскольку теперь нам также нужно беспокоиться об элементах, имеющих правильную семантику равенства (какое равенство подходит, когда элементы являются большими DTO?). Здесь я вижу три возможных пути:

    1. Требовать уникальности / равенства и поддерживать сценарии обновления путем передачи исходного элемента (до равенства).
    2. Не требуют уникальности / равенства и поддерживают сценарии обновления с использованием дескрипторов. Их можно получить с помощью дополнительных вариантов метода Enqueue для пользователей, которым они нужны. Если распределение дескрипторов является достаточно серьезной проблемой, мне интересно, можно ли их амортизировать в соответствии с ValueTask?
    3. Не требуют уникальности / равенства и не поддерживают приоритеты обновления.
  • Поддерживаем ли мы объединение очередей? - Похоже, что консенсуса нет, поскольку мы поставляем только одну реализацию (предположительно с использованием кучи массивов), в которой слияние неэффективно.

  • Какие интерфейсы следует реализовать? - Я видел несколько предложений, рекомендующих IQueue<T> , но это похоже на дырявую абстракцию. Лично я предпочел бы сделать это простым и просто реализовать ICollection<T> .

Копия @layomia @safern

@eiriktsarpalis Напротив - в ограничении поддержки обновлений нет ничего искусственного!

Алгоритмы с очередями приоритета часто обновляют приоритеты элементов. Вопрос в том, предоставляете ли вы объектно-ориентированный API, который «просто работает» для обновления приоритетов обычных объектов ... или вы заставляете людей
а) понять модель ручки
б) хранить в памяти дополнительные данные, такие как дополнительные свойства или внешний словарь, чтобы отслеживать дескрипторы объектов на их стороне, чтобы они могли выполнять обновления (должен идти со словарем для классов, которые они не могут изменить? или обновите объекты в кортежи и т. д.)
c) внешние структуры «управление памятью» или «сборщик мусора», т.е. дескриптор очистки для элементов, которых больше нет в очереди, при использовании словарного подхода
г) не путать непрозрачные дескрипторы в контекстах с несколькими очередями, поскольку они имеют смысл только в контексте очереди с одним приоритетом

К тому же существует философский вопрос, почему кто-то захочет, чтобы очередь, отслеживающая объекты по приоритету, вела себя таким образом: почему один и тот же объект (равно возвращает истину) должен иметь два разных приоритета? Если у них действительно должны быть разные приоритеты, почему они не моделируются как разные объекты? (или преобразованы в кортежи с отличителями?)

Также для дескрипторов должна быть некоторая внутренняя таблица дескрипторов в очереди приоритетов, чтобы дескрипторы действительно работали. Я думаю, что это эквивалентно ведению словаря для поиска объектов в очереди.

PS: каждый объект .net уже поддерживает концепции равенства / уникальности, поэтому не сложно «требовать» его.

Что касается KeyValuePair , я бы согласился, что это не идеально (хотя в предложении Key - для элемента, Value для приоритета, что согласуется с различными Sorted data-types в BCL), и его уместность будет зависеть от решения относительно уникальности. Лично я _much_ предпочел бы специальный хорошо названный struct кортежу в любом месте общедоступного API, особенно для ввода.

Что касается уникальности, это фундаментальная проблема, и я не думаю, что еще что-то можно решить, пока это не произойдет. Я бы предпочел уникальность элемента, определенную (необязательным) компаратором (как в существующих предложениях, так и в предложении i), если целью является единый простой в использовании API общего назначения. Уникальный и неуникальный - это большой разрыв, и я использую оба типа для разных целей. Первый вариант «сложнее» реализовать, и он охватывает наиболее (и более типичные) варианты использования (только мой опыт), но при этом его труднее использовать не по назначению. Те варианты использования, которые _require_ неуникальность, должны (IMO) обслуживаться другим типом (например, простой старой двоичной кучей), и я был бы признателен за наличие обоих. По сути, это то, что предоставляет исходное предложение, связанное с @pgolebiowski (насколько я понимаю) по модулю (простой) оболочки. _Edit: Нет, это не поддерживает привязанные приоритеты_

Напротив - в ограничении поддержки обновлений нет ничего искусственного!

Извините, я не хотел указать, что поддержка обновлений является искусственной; скорее, требование уникальности вводится искусственно для поддержки обновлений.

PS: каждый объект .net уже поддерживает концепции равенства / уникальности, поэтому его не сложно `` потребовать ''

Конечно, но иногда семантика равенства, которая идет с типом, может быть нежелательной (например, ссылочное равенство, полномасштабное структурное равенство и т. Д.). Я просто указываю на то, что равенство - это непросто, и его использование в дизайне приводит к появлению целого класса потенциальных пользовательских ошибок.

@eiriktsarpalis Спасибо, что разъяснили это. Но действительно ли это искусственно? Я так не думаю. Это просто еще одно естественное решение.

API должен быть четко определен. Вы не можете предоставить API обновления, не требуя, чтобы пользователь указывал , что именно он хочет обновить . Дескрипторы и равенство объектов - это всего лишь два разных подхода к созданию четко определенного API.

Подход с ручным управлением: каждый раз, когда вы добавляете объект в коллекцию, вы должны давать ему «имя», то есть «дескриптор», чтобы вы могли ссылаться на этот точный объект без двусмысленности позже в разговоре.

Подход уникальности объекта: каждый раз, когда вы добавляете объект в коллекцию, это должен быть другой объект, или вы должны указать, как поступать в случае, если объект уже существует.

К сожалению, только объектный подход действительно позволяет поддерживать некоторые из наиболее полезных свойств высокоуровневых методов API, например EnqueueIfNotExists или EnqueueOrDecreasePriroity (item) - их нет смысла предоставлять в дизайне, ориентированном на дескрипторы, потому что вы не могу не знать, существует ли элемент уже в очереди (поскольку ваша задача - отслеживать это с помощью дескрипторов).

Одно из самых ярких критических замечаний в отношении подхода с описанием или отказа от ограничения уникальности заключается в том, что он значительно усложняет реализацию всевозможных сценариев с обновляемым приоритетом:

например

  1. использовать PriorityQueueдля строк, представляющих сообщения / настроения / теги / имя пользователя, за которые проголосовали / обновились, уникальные значения имеют изменяющийся приоритет
  2. использовать PriorityQueue, double>, чтобы упорядочить уникальные кортежи [независимо от того, имеют ли они изменения приоритета или нет] - необходимо где-то отслеживать дополнительные дескрипторы
  3. использовать PriroityQueueчтобы определить приоритеты индексов графа или идентификаторов объектов базы данных, теперь вам нужно добавить дескрипторы в вашу реализацию.

PS

Конечно, но иногда семантика равенства, связанная с типом, может оказаться нежелательной.

Должны быть escape-штриховки, такие как IEqualityComparer, или преобразование с повышением частоты в более богатый тип.

Спасибо за отзыв 🥳 Обновим предложение на выходных с учетом всех новых отзывов и поделимся новой редакцией для следующего раунда. ETA 2020-09-20.

Предложение приоритетной очереди (v2.0)

Резюме

Сообщество .NET Core предлагает добавить в системную библиотеку функциональность _priority queue_, структуру данных, в которой каждый элемент дополнительно имеет связанный с ним приоритет. В частности, мы предлагаем добавить PriorityQueue<TElement, TPriority> в пространство имен System.Collections.Generic .

Постулаты

В своем дизайне мы руководствовались следующими принципами (если вы не знаете, что лучше):

  • Широкий охват. Мы хотим предложить клиентам .NET Core ценную структуру данных, достаточно универсальную, чтобы поддерживать широкий спектр сценариев использования.
  • Учитесь на известных ошибках. Мы стремимся предоставить функции приоритетной очереди, которые были бы свободны от проблем, с которыми сталкиваются клиенты, которые присутствуют в других фреймворках и языках, например, Java, Python, C ++, Rust. Мы будем избегать выбора дизайна, который, как известно, расстраивает клиентов, и снижает полезность очередей с приоритетом.
  • Исключительная осторожность с односторонними дверными решениями. После того, как API введен, его нельзя изменить или удалить, а только расширить. Мы тщательно проанализируем выбор дизайна, чтобы избежать неоптимальных решений, с которыми наши клиенты будут застрять навсегда.
  • Избегайте паралича дизайна. Мы согласны с тем, что идеального решения не существует. Мы найдем компромиссы и продвинемся вперед с поставкой, чтобы наконец предоставить нашим клиентам функциональность, которую они ждали годами.

Фон

С точки зрения клиента

Концептуально очередь приоритетов - это набор элементов, каждый из которых имеет связанный приоритет. Наиболее важная функциональность очереди с приоритетом заключается в том, что она обеспечивает эффективный доступ к элементу с наивысшим приоритетом в коллекции и возможность удаления этого элемента. Ожидаемое поведение может также включать: 1) возможность изменять приоритет элемента, который уже находится в коллекции; 2) возможность объединения нескольких очередей приоритета.

Фон информатики

Очередь с приоритетом - это абстрактная структура данных, т. Е. Это концепция с определенными поведенческими характеристиками, как описано в предыдущем разделе. Наиболее эффективные реализации очереди с приоритетами основаны на кучах. Однако, вопреки распространенному заблуждению, куча также является абстрактной структурой данных и может быть реализована различными способами, каждый из которых имеет свои преимущества и недостатки.

Большинство инженеров-программистов знакомы только с реализацией двоичной кучи на основе массивов - она ​​самая простая, но, к сожалению, не самая эффективная. Для обычного случайного ввода есть два примера более эффективных типов кучи: четвертичная куча и куча сопряжения . Дополнительные сведения о кучах см. В Википедии и в этом документе .

Механизм обновления - ключевая задача дизайна

Наши обсуждения показали, что самой сложной областью в дизайне, и в то же время оказывающей наибольшее влияние на API, является механизм обновления. В частности, задача состоит в том, чтобы определить, должен ли продукт, который мы хотим предложить покупателям, поддерживать обновление приоритетов элементов, уже присутствующих в коллекции, и каким образом.

Такая возможность необходима для реализации, например, алгоритма кратчайшего пути Дейкстры или планировщика заданий, который должен обрабатывать изменение приоритетов. Механизм обновления отсутствует в Java, что разочаровало инженеров, например, в этих трех вопросах StackOverflow, просмотренных более 32 тысяч раз: пример , пример , пример . Чтобы избежать внедрения API с таким ограниченным значением, мы считаем, что фундаментальным требованием для предоставляемых нами функций очереди приоритетов будет поддержка возможности обновления приоритетов для элементов, уже присутствующих в коллекции.

Чтобы предоставить механизм обновления, мы должны убедиться, что заказчик может указать, что именно он хочет обновить. Мы определили два способа доставки этого: а) через дескрипторы; и б) путем обеспечения уникальности элементов в коллекции. Каждый из них имеет свои преимущества и затраты.

Вариант (а): Ручки. В этом подходе каждый раз, когда элемент добавляется в очередь, структура данных предоставляет его уникальный дескриптор. Если клиент хочет использовать механизм обновления, ему необходимо отслеживать такие дескрипторы, чтобы впоследствии они могли однозначно указать, какой элемент он хочет обновить. Основная стоимость этого решения заключается в том, что клиентам необходимо управлять этими указателями. Однако это не означает, что должны быть какие-либо внутренние выделения для поддержки дескрипторов в очереди приоритетов - любая куча, не основанная на массиве, основана на узлах, где каждый узел автоматически является собственным дескриптором. В качестве примера см. API метода PairingHeap.Update .

Вариант (б): Уникальность. Этот подход накладывает на клиента два дополнительных ограничения: i) элементы в очереди с приоритетами должны соответствовать определенной семантике равенства, что создает новый класс потенциальных ошибок пользователя; ii) два одинаковых элемента не могут храниться в одной очереди. Оплачивая эту стоимость, мы получаем преимущество поддержки механизма обновления, не прибегая к методу дескриптора. Однако любая реализация, которая использует уникальность / равенство для определения обновляемого элемента, потребует дополнительного внутреннего сопоставления, чтобы оно выполнялось в O (1), а не в O (n).

Рекомендация

Мы рекомендуем добавить в системную библиотеку класс 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);

часто задаваемые вопросы

В каком порядке очередь приоритетов перечисляет элементы?

В неопределенном порядке, чтобы перечисление могло происходить за O (n), аналогично HashSet . Никакая эффективная реализация не предоставит возможности для перечисления кучи за линейное время, обеспечивая при этом перечисление элементов по порядку - для этого потребуется O (n log n). Поскольку упорядочивание коллекции может быть тривиально выполнено с помощью .OrderBy(x => x.Priority) и не каждый покупатель заботится о перечислении с помощью этого порядка, мы считаем, что лучше обеспечить неопределенный порядок перечисления.

Приложения

Приложение A. Другие языки с функцией очереди приоритетов

| Язык | Тип | Примечания |
|: -: |: -: |: -: |
| Java | PriorityQueue | Расширяет абстрактный класс AbstractQueue и реализует интерфейс Queue . |
| Ржавчина | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| Вперед | куча | Есть куча интерфейса. |

Приложение Б. Обнаруживаемость

Обратите внимание, что при обсуждении структур данных термин _heap_ используется в 4 раза чаще, чем _priority queue_.

  • "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 возвращает узел, потому что в этот момент это не дескриптор (я бы предпочел два отдельных выходных параметра)
  • Будет ли он стабильным в том смысле, что если два элемента поставлены в очередь с одинаковым приоритетом, они удаляются из очереди в предсказуемом порядке? (приятно иметь для воспроизводимости; в противном случае может быть достаточно легко реализован потребителем)
  • Параметр для Merge должен быть PriorityQueue'2 не PriorityQueueNode'2 , и вы можете уточнить поведение? Я не знаком с парной кучей, но, по-видимому, две кучи после этого перекрываются
  • Я не поклонник имени Contains для метода с двумя параметрами: это не то имя, которое я бы предположил для метода стиля TryGet
  • Должен ли класс поддерживать пользовательский IEqualityComparer<TElement> для цели Contains ?
  • Кажется, не существует способа эффективно определить, находится ли узел все еще в куче (не уверен, когда я буду использовать это, подумал)
  • Странно, что Remove возвращает bool ; Я ожидал, что он назывался TryRemove или что он выбросил (что, я предполагаю, Update делает, если узел не находится в куче).

@VisualMelon большое спасибо за отзыв! Быстро разрешу эти, однозначно согласен:

  • Кажется странным, что TryDequeue возвращает узел, потому что в этот момент это не дескриптор (я бы предпочел два отдельных выходных параметра)
  • Параметр для Merge должен быть PriorityQueue'2 не PriorityQueueNode'2 , и вы можете уточнить поведение? Я не знаком с парной кучей, но, по-видимому, две кучи после этого перекрываются
  • Странно, что Remove возвращает bool ; Я ожидал, что он назывался TryRemove или что он выбросил (что, я предполагаю, Update делает, если узел не находится в куче).
  • Я не поклонник имени Contains для метода с двумя параметрами: это не то имя, которое я бы предположил для метода стиля TryGet

Разъяснение для этих двоих:

  • Будет ли он стабильным в том смысле, что если два элемента поставлены в очередь с одинаковым приоритетом, они удаляются из очереди в предсказуемом порядке? (приятно иметь для воспроизводимости; в противном случае может быть достаточно легко реализован потребителем)
  • Кажется, не существует способа эффективно определить, находится ли узел все еще в куче (не уверен, когда я буду использовать это, подумал)

Для первого пункта, если целью является воспроизводимость, то реализация будет детерминированной, да. Если цель - _если два элемента помещены в очередь с одинаковым приоритетом, из этого следует, что они появятся в том же порядке, в котором они были помещены в очередь_ - я не уверен, можно ли настроить реализацию на Чтобы достичь этого свойства, быстрый ответ будет «вероятно, нет».

Что касается второго пункта, да, кучи не подходят для проверки наличия элемента в коллекции, клиенту придется отслеживать это отдельно, чтобы достичь этого в O (1), или повторно использовать сопоставление, которое они используют для дескрипторов, если они используют механизм обновления. В противном случае O (n).

  • Должен ли класс поддерживать пользовательский IEqualityComparer<TElement> для цели Contains ?

Хм ... Я начинаю думать, что, возможно, возложить ответственность за эту приоритетную очередь на Contains может быть слишком много, и использование метода из Linq может быть достаточным ( Contains в любом случае должен применяться при перечислении).

@pgolebiowski спасибо за разъяснения

Для первого пункта, если цель - воспроизводимость, тогда реализация будет детерминированной, да

Не столько детерминированный, сколько гарантированный навсегда (например, даже реализация не изменится, поведение не изменится), поэтому я думаю, что я получил ответ «нет». Это нормально: потребитель может добавить идентификатор последовательности к приоритету, если ему это нужно, хотя в этот момент SortedSet выполнит свою работу.

Что касается второго пункта, да, кучи не подходят для проверки наличия элемента в коллекции, клиенту придется отслеживать это отдельно, чтобы достичь этого в 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?
  • Должна ли быть версия TryRemove() , которая не срабатывает при срабатывании Remove?
  • Я не уверен, что в большинстве случаев Contains() api будет полезным? «Содержит», кажется, в глазах смотрящего, особенно для сценариев с «повторяющимися» элементами с разными приоритетами или другими отличными функциями! В этом случае конечный пользователь, вероятно, в любом случае должен будет выполнить свой собственный поиск. Но, по крайней мере, это может быть полезно для сценария без дубликатов.

@pgolebiowski Спасибо, что

  • Повторяя комментарий @TimLovellSmith , я не уверен, что Contains() или TryGetNode() должны вообще существовать в API в его предлагаемой в настоящее время форме. Они подразумевают, что равенство для TElement является значительным, что, по-видимому, является одной из вещей, которых пытался избежать подход на основе дескрипторов.
  • Я бы, наверное, перефразировал public void Dequeue(out TElement element); как public TElement Dequeue();
  • Почему метод TryDequeue() должен возвращать приоритет?
  • Разве класс не должен также реализовать ICollection<T> или IReadOnlyCollection<T> ?
  • Что должно произойти, если я попытаюсь обновить PriorityQueueNode, возвращенный из другого экземпляра PriorityQueue?
  • Хотим ли мы поддерживать эффективную операцию слияния? AFAICT это означает, что мы не можем использовать представление на основе массива. Как это повлияет на реализацию с точки зрения распределения?

Большинство инженеров-программистов знакомы только с реализацией двоичной кучи на основе массивов - она ​​самая простая, но, к сожалению, не самая эффективная. Для обычного случайного ввода есть два примера более эффективных типов кучи: четвертичная куча и куча сопряжения.

Каковы компромиссы при выборе последнего подхода? Можно ли использовать для этого реализацию на основе массива?

Он не может реализовать ICollection<T> или IReadOnlyCollection<T> если у него нет правильных подписей для «Добавить», «Удалить» и т. Д.

По духу / форме он намного ближе к ICollection<KeyValuePair<T,Priority>>

По духу / форме он намного ближе к ICollection<KeyValuePair<T,Priority>>

Разве это не будет KeyValuePair<TPriority, TElement> , поскольку упорядочивание выполняется с помощью TPriority, который, по сути, является механизмом ввода ключей?

Хорошо, похоже, что мы в целом за отказ от методов Contains и TryGet . Удалим их в следующей ревизии и объясним причину удаления в FAQ.

Что касается реализованных интерфейсов - разве IEnumerable<PriorityQueueNode<TElement, TPriority>> достаточно? Какая функциональность отсутствует?

На KeyValuePair - было несколько голосов, что кортеж или структура с .Element и .Priority более желательны. Думаю, я за это.

Разве это не будет KeyValuePair<TPriority, TElement> , поскольку упорядочивание выполняется с помощью TPriority, который, по сути, является механизмом ввода ключей?

У обеих сторон есть веские аргументы. С одной стороны, да, именно то, что вы только что сказали. С другой стороны, ключи в коллекции KVP обычно должны быть уникальными, и вполне допустимо иметь несколько элементов с одинаковым приоритетом.

С другой стороны, обычно ожидается, что ключи в коллекции KVP будут уникальными.

Я не согласен с этим утверждением - это всего лишь набор пар ключ-значение; любые требования к уникальности накладываются поверх этого.

недостаточно IEnumerable<PriorityQueueNode<TElement, TPriority>> ? Какая функциональность отсутствует?

Я лично ожидал бы реализации IReadOnlyCollection<PQN<TElement, TPriority>> , поскольку предоставленный API уже в основном удовлетворяет этому интерфейсу. Кроме того, это согласуется с другими типами коллекций.

По поводу интерфейса:

`` ''
public bool TryGetNode (элемент TElement, вне 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

общедоступный PriorityQueue (IEnumerable> сборник);
`` ''

Я также хотел бы, чтобы PriorityQueue реализовал интерфейс IReadonlyCollection<> .

Переход к другим вещам, кроме общедоступной поверхности API.

Такая возможность необходима для реализации, например, алгоритма кратчайшего пути Дейкстры или планировщика заданий, который должен обрабатывать изменение приоритетов. Механизм обновления отсутствует в Java, что разочаровало инженеров, например, в этих трех вопросах StackOverflow, просмотренных более 32 тысяч раз: пример, пример, пример. Чтобы избежать внедрения API с таким ограниченным значением, мы считаем, что фундаментальным требованием для предоставляемых нами функций очереди приоритетов будет поддержка возможности обновления приоритетов для элементов, уже присутствующих в коллекции.

Я бы не согласился. В прошлый раз, когда я написал Дейкстру на C ++, std :: priority_queue было достаточно, и он не обрабатывает обновление приоритета. Общий консенсус AFAIK для этого случая заключается в том, чтобы добавить поддельный элемент в очередь с измененным приоритетом и значением и контролировать, обрабатываем ли мы значение или нет. То же самое можно сделать с планировщиком заданий.

Честно говоря, я не уверен, как будет выглядеть Дейкстра с текущим предложением об очереди. Как я могу отслеживать узлы, которым мне нужен приоритет обновления? С помощью TryGetNode ()? Или есть другой набор узлов? Хотелось бы увидеть код для текущего предложения.

Если вы посмотрите в Википедию, там нет предположения об обновлении приоритетов для очереди приоритетов. То же самое для всех других языков, у которых нет такой функциональности, и они остались безнаказанными. Я знаю «стремиться быть лучше», но действительно ли на это есть спрос?

Для обычного случайного ввода есть два примера более эффективных типов кучи: четвертичная куча и куча сопряжения. Дополнительные сведения о кучах см. В Википедии и в этом документе.

Я заглянул в бумагу, и это цитата из нее:

Результаты показывают, что оптимальный выбор реализации сильно зависит от ввода. Более того, это показывает, что необходимо позаботиться об оптимизации производительности кеша, в первую очередь на барьере 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 Ваши комментарии еще больше убеждают меня в том, что имеет смысл иметь два отдельных типа: один - простая двоичная куча (т.е. без обновления), а второй - как описано в одном из предложений:

  • Двоичные кучи просты, легко реализуемы, имеют небольшой API и, как известно, хорошо работают на практике во многих важных сценариях.
  • Полноценная приоритетная очередь обеспечивает теоретические преимущества для некоторых сценариев (т. Е. Меньшая пространственная сложность и меньшая временная сложность для поиска на плотно связанных графах) и предоставляет естественный API для большего количества алгоритмов / сценариев.

Раньше мне в основном удавалось обходиться двоичной кучей, которую я написал лучшую часть десятилетия назад, и комбинацией 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) возможность объединения нескольких очередей приоритета.

Фон информатики

Очередь с приоритетом - это абстрактная структура данных, т. Е. Это концепция с определенными поведенческими характеристиками, как описано в предыдущем разделе. Наиболее эффективные реализации очереди с приоритетами основаны на кучах. Однако, вопреки распространенному заблуждению, куча также является абстрактной структурой данных и может быть реализована различными способами, каждый из которых имеет свои преимущества и недостатки.

Большинство инженеров-программистов знакомы только с реализацией двоичной кучи на основе массивов - она ​​самая простая, но, к сожалению, не самая эффективная. Для обычного случайного ввода есть два примера более эффективных типов кучи: четвертичная куча и куча сопряжения . Дополнительные сведения о кучах см. В Википедии и в этом документе .

Механизм обновления - ключевая задача дизайна

Наши обсуждения показали, что самой сложной областью в дизайне, и в то же время оказывающей наибольшее влияние на API, является механизм обновления. В частности, задача состоит в том, чтобы определить, должен ли продукт, который мы хотим предложить покупателям, поддерживать обновление приоритетов элементов, уже присутствующих в коллекции, и каким образом.

Такая возможность необходима для реализации, например, алгоритма кратчайшего пути Дейкстры или планировщика заданий, который должен обрабатывать изменение приоритетов. Механизм обновления отсутствует в Java, что разочаровало инженеров, например, в этих трех вопросах StackOverflow, просмотренных более 32 тысяч раз: пример , пример , пример . Чтобы избежать внедрения API с таким ограниченным значением, мы считаем, что фундаментальным требованием для предоставляемых нами функций очереди приоритетов будет поддержка возможности обновления приоритетов для элементов, уже присутствующих в коллекции.

Чтобы предоставить механизм обновления, мы должны убедиться, что заказчик может указать, что именно он хочет обновить. Мы определили два способа доставки этого: а) через дескрипторы; и б) путем обеспечения уникальности элементов в коллекции. Каждый из них имеет свои преимущества и затраты.

Вариант (а): Ручки. В этом подходе каждый раз, когда элемент добавляется в очередь, структура данных предоставляет его уникальный дескриптор. Если клиент хочет использовать механизм обновления, ему необходимо отслеживать такие дескрипторы, чтобы впоследствии они могли однозначно указать, какой элемент он хочет обновить. Основная стоимость этого решения заключается в том, что клиентам необходимо управлять этими указателями. Однако это не означает, что должны быть какие-либо внутренние выделения для поддержки дескрипторов в очереди приоритетов - любая куча, не основанная на массиве, основана на узлах, где каждый узел автоматически является собственным дескриптором. В качестве примера см. API метода PairingHeap.Update .

Вариант (б): Уникальность. Этот подход накладывает на клиента два дополнительных ограничения: i) элементы в очереди с приоритетами должны соответствовать определенной семантике равенства, что создает новый класс потенциальных ошибок пользователя; ii) два одинаковых элемента не могут храниться в одной очереди. Оплачивая эту стоимость, мы получаем преимущество поддержки механизма обновления, не прибегая к методу дескриптора. Однако любая реализация, которая использует уникальность / равенство для определения обновляемого элемента, потребует дополнительного внутреннего сопоставления, чтобы оно выполнялось в O (1), а не в O (n).

Рекомендация

Мы рекомендуем добавить в системную библиотеку класс 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. В каком порядке очередь с приоритетом перечисляет элементы?

В неопределенном порядке, чтобы перечисление могло происходить за O (n), аналогично HashSet . Никакая эффективная реализация не предоставит возможности для перечисления кучи за линейное время, обеспечивая при этом перечисление элементов по порядку - для этого потребуется O (n log n). Поскольку упорядочивание коллекции может быть тривиально выполнено с помощью .OrderBy(x => x.Priority) и не каждый покупатель заботится о перечислении с помощью этого порядка, мы считаем, что лучше обеспечить неопределенный порядок перечисления.

2. Почему нет методов Contains или TryGet ?

Предоставление таких методов имеет незначительную ценность, потому что для поиска элемента в куче требуется перечисление всей коллекции, а это означает, что любой метод Contains или TryGet будет оберткой вокруг перечисления. Кроме того, чтобы проверить, существует ли элемент в коллекции, приоритетная очередь должна знать, как проводить проверки равенства объектов TElement , что, по нашему мнению, не должно попадать в ответственность очереди приоритетов.

3. Почему есть перегрузки Dequeue и TryDequeue которые возвращают PriorityQueueNode ?

Это для клиентов, которые хотят использовать метод Update или Remove и отслеживать дескрипторы. При извлечении элемента из очереди приоритетов они получали дескриптор, который они могли использовать для настройки состояния своей системы отслеживания дескрипторов.

4. Что происходит, когда метод Update или Remove получает узел из другой очереди?

Очередь с приоритетом вызовет исключение. Каждому узлу известна очередь приоритетов, к которой он принадлежит, аналогично тому, как LinkedListNode<T> знает LinkedList<T> к

Приложения

Приложение A. Другие языки с функцией очереди приоритетов

| Язык | Тип | Примечания |
|: -: |: -: |: -: |
| Java | PriorityQueue | Расширяет абстрактный класс AbstractQueue и реализует интерфейс Queue . |
| Ржавчина | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| Вперед | куча | Есть куча интерфейса. |

Приложение Б. Обнаруживаемость

Обратите внимание, что при обсуждении структур данных термин _heap_ используется в 4 раза чаще, чем _priority queue_.

  • "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 результатов

Спасибо всем за отзывы! Я обновил предложение до версии 2.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: _Почему существуют перегрузки Dequeue и TryDequeue которые возвращают PriorityQueueNode ? _
  • Добавлен FAQ # 4: _Что происходит, когда метод Update или Remove получает узел из другой очереди? _

Спасибо за заметки об изменениях;)

Небольшие просьбы о разъяснении:

  • Можем ли мы квалифицировать FAQ 4 так, чтобы он действовал для элементов не в очереди? (т.е. удаленные)
  • Можем ли мы добавить FAQ о стабильности, то есть о гарантиях (если есть) при исключении из очереди элементов с одинаковым приоритетом (я понимаю, что нет плана давать какие-либо гарантии, что важно знать, например, для планирования).

@pgolebiowski Относительно предлагаемого метода Merge :

public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)

Ясно, что такая операция не будет иметь семантики копирования, поэтому мне интересно, будут ли какие-то подводные камни при внесении изменений в this и other _after_ было выполнено слияние (например, сбой любого экземпляра чтобы удовлетворить свойству кучи).

@eiriktsarpalis @VisualMelon - Спасибо! Рассмотрим поднятые вопросы, ETA 2020-10-04.

Если у других есть еще отзывы / вопросы / проблемы / мысли - поделитесь 😊

Предложение приоритетной очереди (v2.2)

Резюме

Сообщество .NET Core предлагает добавить в системную библиотеку функциональность _priority queue_, структуру данных, в которой каждый элемент дополнительно имеет связанный с ним приоритет. В частности, мы предлагаем добавить PriorityQueue<TElement, TPriority> в пространство имен System.Collections.Generic .

Постулаты

В своем дизайне мы руководствовались следующими принципами (если вы не знаете, что лучше):

  • Широкий охват. Мы хотим предложить клиентам .NET Core ценную структуру данных, достаточно универсальную, чтобы поддерживать широкий спектр сценариев использования.
  • Учитесь на известных ошибках. Мы стремимся предоставить функции приоритетной очереди, которые были бы свободны от проблем, с которыми сталкиваются клиенты, которые присутствуют в других фреймворках и языках, например, Java, Python, C ++, Rust. Мы будем избегать выбора дизайна, который, как известно, расстраивает клиентов, и снижает полезность очередей с приоритетом.
  • Исключительная осторожность с односторонними дверными решениями. После того, как API введен, его нельзя изменить или удалить, а только расширить. Мы тщательно проанализируем выбор дизайна, чтобы избежать неоптимальных решений, с которыми наши клиенты будут застрять навсегда.
  • Избегайте паралича дизайна. Мы согласны с тем, что идеального решения не существует. Мы найдем компромиссы и продвинемся вперед с поставкой, чтобы наконец предоставить нашим клиентам функциональность, которую они ждали годами.

Фон

С точки зрения клиента

Концептуально очередь приоритетов - это набор элементов, каждый из которых имеет связанный приоритет. Наиболее важная функциональность очереди с приоритетом заключается в том, что она обеспечивает эффективный доступ к элементу с наивысшим приоритетом в коллекции и возможность удаления этого элемента. Ожидаемое поведение может также включать: 1) возможность изменять приоритет элемента, который уже находится в коллекции; 2) возможность объединения нескольких очередей приоритета.

Фон информатики

Очередь с приоритетом - это абстрактная структура данных, т. Е. Это концепция с определенными поведенческими характеристиками, как описано в предыдущем разделе. Наиболее эффективные реализации очереди с приоритетами основаны на кучах. Однако, вопреки распространенному заблуждению, куча также является абстрактной структурой данных и может быть реализована различными способами, каждый из которых имеет свои преимущества и недостатки.

Большинство инженеров-программистов знакомы только с реализацией двоичной кучи на основе массивов - она ​​самая простая, но, к сожалению, не самая эффективная. Для обычного случайного ввода есть два примера более эффективных типов кучи: четвертичная куча и куча сопряжения . Дополнительные сведения о кучах см. В Википедии и в этом документе .

Механизм обновления - ключевая задача дизайна

Наши обсуждения показали, что самой сложной областью в дизайне, и в то же время оказывающей наибольшее влияние на API, является механизм обновления. В частности, задача состоит в том, чтобы определить, должен ли продукт, который мы хотим предложить покупателям, поддерживать обновление приоритетов элементов, уже присутствующих в коллекции, и каким образом.

Такая возможность необходима для реализации, например, алгоритма кратчайшего пути Дейкстры или планировщика заданий, который должен обрабатывать изменение приоритетов. Механизм обновления отсутствует в Java, что разочаровало инженеров, например, в этих трех вопросах StackOverflow, просмотренных более 32 тысяч раз: пример , пример , пример . Чтобы избежать внедрения API с таким ограниченным значением, мы считаем, что фундаментальным требованием для предоставляемых нами функций очереди приоритетов будет поддержка возможности обновления приоритетов для элементов, уже присутствующих в коллекции.

Чтобы предоставить механизм обновления, мы должны убедиться, что заказчик может указать, что именно он хочет обновить. Мы определили два способа доставки этого: а) через дескрипторы; и б) путем обеспечения уникальности элементов в коллекции. Каждый из них имеет свои преимущества и затраты.

Вариант (а): Ручки. В этом подходе каждый раз, когда элемент добавляется в очередь, структура данных предоставляет его уникальный дескриптор. Если клиент хочет использовать механизм обновления, ему необходимо отслеживать такие дескрипторы, чтобы впоследствии они могли однозначно указать, какой элемент он хочет обновить. Основная стоимость этого решения заключается в том, что клиентам необходимо управлять этими указателями. Однако это не означает, что должны быть какие-либо внутренние выделения для поддержки дескрипторов в очереди приоритетов - любая куча, не основанная на массиве, основана на узлах, где каждый узел автоматически является собственным дескриптором. В качестве примера см. API метода PairingHeap.Update .

Вариант (б): Уникальность. Этот подход накладывает на клиента два дополнительных ограничения: i) элементы в очереди с приоритетами должны соответствовать определенной семантике равенства, что создает новый класс потенциальных ошибок пользователя; ii) два одинаковых элемента не могут храниться в одной очереди. Оплачивая эту стоимость, мы получаем преимущество поддержки механизма обновления, не прибегая к методу дескриптора. Однако любая реализация, которая использует уникальность / равенство для определения обновляемого элемента, потребует дополнительного внутреннего сопоставления, чтобы оно выполнялось в O (1), а не в O (n).

Рекомендация

Мы рекомендуем добавить в системную библиотеку класс 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. В каком порядке очередь с приоритетом перечисляет элементы?

В неопределенном порядке, чтобы перечисление могло происходить за O (n), аналогично HashSet . Никакая эффективная реализация не предоставит возможности для перечисления кучи за линейное время, обеспечивая при этом перечисление элементов по порядку - для этого потребуется O (n log n). Поскольку упорядочивание коллекции может быть тривиально выполнено с помощью .OrderBy(x => x.Priority) и не каждый покупатель заботится о перечислении с помощью этого порядка, мы считаем, что лучше обеспечить неопределенный порядок перечисления.

2. Почему нет методов Contains или TryGet ?

Предоставление таких методов имеет незначительную ценность, потому что для поиска элемента в куче требуется перечисление всей коллекции, а это означает, что любой метод Contains или TryGet будет оберткой вокруг перечисления. Кроме того, чтобы проверить, существует ли элемент в коллекции, приоритетная очередь должна знать, как проводить проверки равенства объектов TElement , что, по нашему мнению, не должно попадать в ответственность очереди приоритетов.

3. Почему есть перегрузки Dequeue и TryDequeue которые возвращают PriorityQueueNode ?

Это для клиентов, которые хотят использовать метод Update или Remove и отслеживать дескрипторы. При извлечении элемента из очереди приоритетов они получали дескриптор, который они могли использовать для настройки состояния своей системы отслеживания дескрипторов.

4. Что происходит, когда метод Update или Remove получает узел из другой очереди?

Очередь с приоритетом вызовет исключение. Каждому узлу известна очередь приоритетов, к которой он принадлежит, аналогично тому, как LinkedListNode<T> знает LinkedList<T> к Update или Remove также приведет к исключению.

5. Почему нет метода Merge ?

Слияние двух очередей с приоритетом может быть достигнуто за постоянное время, что делает эту функцию заманчивой для предложения клиентов. Однако у нас нет данных, подтверждающих, что такая функциональность востребована, и мы не можем оправдать включение ее в общедоступный API. Кроме того, разработка такой функции нетривиальна, и, учитывая, что эта функция может не понадобиться, она может излишне усложнить поверхность API и реализацию.

Тем не менее, исключение метода Merge сейчас является двусторонней дверью - если в будущем клиенты выразят заинтересованность в поддержке функции слияния, можно будет расширить тип PriorityQueue . Поэтому мы рекомендуем пока не включать метод Merge и продолжить запуск.

6. Предоставляет ли коллекция гарантию стабильности?

Коллекция не обеспечивает гарантии стабильности из коробки, т.е. если два элемента поставлены в очередь с одинаковым приоритетом, заказчик не сможет предположить, что они будут исключены из очереди в определенном порядке. Однако, если клиент хочет достичь стабильности с помощью нашего PriorityQueue , он может определить TPriority и соответствующий IComparer<TPriority> , который обеспечит это. Кроме того, сбор данных будет детерминированным, то есть для данной последовательности операций он всегда будет вести себя одинаково, обеспечивая воспроизводимость.

Приложения

Приложение A. Другие языки с функцией очереди приоритетов

| Язык | Тип | Примечания |
|: -: |: -: |: -: |
| Java | PriorityQueue | Расширяет абстрактный класс AbstractQueue и реализует интерфейс Queue . |
| Ржавчина | BinaryHeap | |
| Swift | CFBinaryHeap | |
| C ++ | priority_queue | |
| Python | heapq | |
| Вперед | куча | Есть куча интерфейса. |

Приложение Б. Обнаруживаемость

Обратите внимание, что при обсуждении структур данных термин _heap_ используется в 4 раза чаще, чем _priority queue_.

  • "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, со словарем дескрипторов, и это казалось в основном нормальным.

Из этого я понял, что текущий набор имен / перегрузок методов не работает так хорошо для неявной типизации переменных out . То, что я хотел сделать с кодом C #, было 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);
            }
        }
    }

На мой взгляд, главный нерешенный вопрос дизайна заключается в том, должна ли реализация поддерживать приоритетные обновления. Здесь я вижу три возможных пути:

  1. Требовать уникальности / равенства и поддерживать обновления, передавая исходный элемент.
  2. Поддержка приоритетных обновлений с помощью дескрипторов.
  3. Не поддерживают приоритетные обновления.

Эти подходы являются взаимоисключающими и, соответственно, имеют свои собственные компромиссы:

  1. Подход, основанный на равенстве, усложняет контракт API, вынуждая его уникальность, и требует дополнительной внутренней бухгалтерии. Это происходит независимо от того, нужны ли пользователю приоритетные обновления.
  2. Подход, основанный на описателях, предполагает как минимум одно дополнительное выделение для каждого элемента в очереди. Хотя он не обеспечивает уникальность, у меня сложилось впечатление, что для сценариев, которые действительно нуждаются в обновлении, этот инвариант почти наверняка неявный (например, обратите внимание, как оба приведенных выше примера хранятся во внешних словарях, индексируемых самими элементами).
  3. Обновления либо вообще не поддерживаются, либо потребуют линейного обхода кучи. В случае дублирования элементов обновления могут быть неоднозначными.

Определение общих приложений PriorityQueue

Важно определить, какой из вышеперечисленных подходов может принести наибольшую пользу большинству наших пользователей. Итак, я просмотрел кодовые базы .NET, как внутренние, так и общедоступные Microsoft, для экземпляров реализаций Heap / PriorityQueue, чтобы лучше понять, каковы наиболее распространенные шаблоны использования:

Большинство приложений реализуют варианты сортировки кучи или планирования заданий. Несколько экземпляров использовались для таких алгоритмов, как топологическая сортировка или кодирование Хаффмана. Меньшее число использовалось для расчета расстояний на графиках. Было обнаружено, что из 80 изученных реализаций PriorityQueue только 9 реализовали ту или иную форму приоритетных обновлений.

В то же время ни одна из реализаций Heap / PriorityQueue в основных библиотеках Python, Java, C ++, Go, Swift или Rust не поддерживает обновления приоритетов в своих API.

Рекомендации

В свете этих данных мне ясно, что .ΝΕΤ нуждается в базовой реализации PriorityQueue, которая предоставляет необходимый API (heapify / push / peek / pop), предлагает определенные гарантии производительности (например, никаких дополнительных выделений на каждую очередь) и не обеспечивает ограничения уникальности. Это означает, что реализация _ не будет поддерживать обновления приоритета O (log n) _.

Нам также следует подумать о последующей реализации отдельной реализации кучи, которая поддерживает _O (log n) _ обновления / удаления и использует подход, основанный на равенстве. Поскольку это будет специализированный тип, требование уникальности не должно быть большой проблемой.

Я работаю над прототипами обеих реализаций и вскоре внесу предложение по API.

Большое спасибо за анализ, @eiriktsarpalis! Я особенно ценю время для анализа внутренней кодовой базы Microsoft, чтобы найти подходящие варианты использования и поддержать наше обсуждение данными.

Подход, основанный на описателях, предполагает как минимум одно дополнительное выделение для каждого элемента в очереди.

Это предположение неверно, вам не нужны дополнительные выделения в кучах на основе узлов. Куча сопряжения более эффективна для обычного случайного ввода, чем двоичная куча на основе массива, а это означает, что у вас будет стимул использовать эту кучу на основе узлов даже для очереди с приоритетом, которая не поддерживает обновления. Вы можете увидеть тесты в статье, на которую я ссылался ранее .

Было обнаружено, что из 80 изученных реализаций PriorityQueue только 9 реализовали ту или иную форму приоритетных обновлений.

Даже с учетом этой небольшой выборки это 11–12% всех использований. Кроме того, это может быть недостаточно представлено в определенных областях, например, в видеоиграх, где я ожидал бы, что этот процент будет выше.

В свете этих данных мне ясно, что [...]

Я не думаю, что такой вывод однозначен, учитывая, что одно из ключевых предположений неверно, и спорно, являются ли 11–12% клиентов достаточно важным вариантом использования или нет. Чего мне не хватает в вашей оценке, так это оценки влияния на затраты «поддержки обновлений структуры данных» для клиентов, которые не заботятся об этом механизме - что для меня эти затраты незначительны, поскольку они могут использовать структуру данных. не будучи затронутым механизмом ручки.

По сути:

| | 11-12% вариантов использования | 88-89% вариантов использования |
|: -: |: -: |: -: |
| заботится об обновлениях | да | нет |
| отрицательно сказываются на ручках | N / A (они желательны) | нет |
| положительно влияет на ручки | да | нет |

Для меня это очевидная вещь в пользу поддержки 100% вариантов использования, а не только 88-89%.

Это предположение неверно, вам не нужны дополнительные выделения в кучах на основе узлов

Если и приоритет, и элемент являются типами значений (или если оба являются ссылочными типами, которыми вы не владеете и / или не можете изменить базовый тип), можете ли вы ссылаться на реализацию, которая демонстрирует, что дополнительных выделений не требуется (или просто опишите как это достигается)? Было бы полезно увидеть. Спасибо.

Было бы полезно, если бы вы могли подробнее рассказать или просто сказать то, что пытаетесь сказать. Я должен устранить двусмысленность, будет пинг-понг, и это превратится в долгую дискуссию. В качестве альтернативы мы могли бы договориться о звонке.

Я говорю, что мы хотим избежать любой операции постановки в очередь, требующей выделения, будь то со стороны вызывающей стороны или со стороны реализации (амортизированное внутреннее выделение - это нормально, например, для расширения массива, используемого в реализации). Я пытаюсь понять, как это возможно с кучей на основе узлов (например, если эти объекты узлов открыты для вызывающей стороны, что запрещает объединение в пул из-за опасений по поводу несоответствующего повторного использования / псевдонимов). Я хочу уметь писать:
C# pq.Enqueue(42, 84);
и уже этого не выделить. Каким образом реализации, о которых вы говорите, достигают этого?

или просто скажи то, что ты пытаешься сказать

Я думал, что был.

мы хотим избежать любой операции постановки в очередь, требующей выделения [...] Я хочу иметь возможность писать: pq.Enqueue(42, 84); а это не выделять.

Откуда это желание? Приятно иметь побочный эффект решения, а не требование, которое должны удовлетворять 99,9% клиентов. Я не понимаю, почему вы выбрали этот вариант с низким уровнем воздействия, чтобы делать выбор дизайна между решениями.

Мы не делаем выбор дизайна на основе оптимизации для 0,1% клиентов, если это отрицательно влияет на 12% клиентов в другом измерении. «забота об отсутствии выделения» + «работа с двумя типами значений» - крайний случай.

Я считаю гораздо более важным аспект поддерживаемого поведения / функциональности, особенно при разработке универсальной универсальной структуры данных для широкой аудитории и различных вариантов использования.

Откуда это желание?

От желания использовать основные типы коллекций в сценариях, которые заботятся о производительности. Вы говорите, что решение на основе узлов будет поддерживать 100% вариантов использования: это не тот случай, если каждая очередь выделяет, точно так же, как List<T> , Dictionary<TKey, TValue> , HashSet<T> и т. Д. on стало бы непригодным для использования во многих ситуациях, если бы они выделялись для каждого Add.

Как вы думаете, почему только «0,1%» заботятся о накладных расходах на распределение этих методов? Откуда берутся эти данные?

"забота об отсутствии выделения" + "работа с двумя типами значений" - крайний случай

Нет. Это также не просто «два типа значений». Насколько я понимаю, предлагаемое решение потребует либо а) выделения для каждой очереди независимо от задействованных Ts, либо б) потребует, чтобы тип элемента был производным от некоторого известного базового типа, что, в свою очередь, запрещает большое количество возможных применений для Избегайте лишнего выделения.

@eiriktsarpalis
Так что вы не забываете ни о каких вариантах, я думаю, что есть возможный вариант 4, который можно добавить к вариантам 1, 2 и 3 в вашем списке, что является компромиссом:

  1. реализация, которая поддерживает вариант использования 12%, а также почти оптимизирует для остальных 88%, позволяя обновлять элементы, которые являются эквивалентными, и только _lazily_ строит таблицу поиска, необходимую для выполнения этих обновлений при первом вызове метода обновления ( и обновляя его при обновлении подпоследовательности + удалении). Следовательно, меньше затрат на приложения, которые не используют эту функциональность.

Мы все же можем решить, что, поскольку существует дополнительная производительность, доступная для 88% или 12% от реализации, которая не нуждается в обновляемой структуре данных или оптимизирована для нее в первую очередь, лучше предоставить варианты 2 и 3, чем вариант 4. Но я подумал, что не следует забывать, что существует еще один вариант.

[Или я полагаю, вы могли бы просто рассматривать это как лучший вариант 1 и обновить описание 1, чтобы сказать, что бухгалтерский учет не является принудительным, а ленивым, и правильное равноправное поведение требуется только при использовании обновлений ...]

@stephentoub Это именно то, что я имел в виду, говоря просто то, что вы хотите сказать, спасибо :)

Как вы думаете, почему только 0,1% заботятся о накладных расходах на распределение этих методов? Откуда берутся эти данные?

Из интуиции, т. Е. Из того же источника, на основе которого, по вашему мнению, важнее установить приоритет «отсутствие дополнительных распределений» над «возможностью проводить обновления». По крайней мере, для механизма обновления у нас есть данные о том, что 11–12% клиентов действительно нуждаются в поддержке такого поведения. Я не думаю, что удаленно близкие клиенты будут заботиться о том, чтобы «не выделялись дополнительные средства».

В любом случае вы по какой-то причине решили сосредоточиться на измерении памяти, забывая о других измерениях, например, исходной скорости, что является еще одним компромиссом для вашего предпочтительного подхода. Реализация на основе массива, обеспечивающая «без дополнительных выделений», будет медленнее, чем реализация на основе узлов. Опять же, я считаю произвольным отдавать приоритет памяти над скоростью.

Давайте сделаем шаг назад и сосредоточимся на том, чего хотят клиенты. У нас есть выбор дизайна, который может сделать или не сделать структуру данных непригодной для 12% клиентов. Я думаю, что нам нужно быть очень осторожными с объяснением причин, по которым мы решили бы не поддерживать их.

Реализация на основе массива, обеспечивающая «без дополнительных выделений», будет медленнее, чем реализация на основе узлов.

Пожалуйста, поделитесь двумя реализациями C #, которые вы используете для выполнения этого сравнения, и тестами, которые использовались, чтобы прийти к такому выводу. Теоретические статьи, безусловно, ценны, но они - лишь небольшой кусочек головоломки. Более важно, когда резина встречается с дорогой, учитывая детали данной платформы и данных реализаций, и вы можете выполнить проверку на конкретной платформе с конкретной реализацией и типичными / ожидаемыми наборами данных / шаблонами использования. Вполне возможно, что ваше утверждение верно. Этого тоже может не быть. Я хотел бы увидеть реализации / данные, чтобы лучше понять.

Пожалуйста, поделитесь двумя реализациями C #, которые вы используете для выполнения этого сравнения, и тестами, которые использовались, чтобы прийти к такому выводу.

Это верный момент, статья, которую я цитирую, только сравнивает и тестирует реализации на C ++. Он проводит несколько тестов с разными наборами данных и шаблонами использования. Я совершенно уверен, что это можно будет перенести на C #, но если вы считаете, что нам нужно удвоить усилия, я думаю, что лучше всего было бы попросить коллегу провести такое исследование.

@pgolebiowski Мне было бы интересно лучше понять суть вашего возражения. Предложение поддерживает два разных типа, разве это не покрывает ваши требования?

  1. реализация, которая поддерживает вариант использования 12%, а также почти оптимизирует для остальных 88%, позволяя обновлять элементы, которые являются эквивалентными, и только лениво строит таблицу поиска, необходимую для выполнения этих обновлений при первом вызове метода обновления ( и обновляя его при обновлении подпоследовательности + удалении). Следовательно, меньше затрат на приложения, которые не используют эту функциональность.

Я бы, вероятно, классифицировал это как оптимизацию производительности для варианта 1, однако я вижу пару проблем с этим конкретным подходом:

  • Обновление теперь становится _O (n) _, что может привести к непредсказуемой производительности в зависимости от шаблонов использования.
  • Таблица поиска также необходима для проверки уникальности. Двойная постановка одного и того же элемента в очередь _ перед_ вызовом Update будет принята и, возможно, приведет очередь к несогласованному состоянию.

@eiriktsarpalis Его только O (n) один раз, а затем O (1), который амортизируется за O (1). И вы можете отложить проверку уникальности до первого обновления. Но, может быть, это слишком умно. Два класса легче объяснить.

Последние несколько дней я потратил на создание прототипов двух реализаций 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;
    }
}

Сравнение производительности

Я написал простой тест производительности heapsort, который сравнивает две реализации в их самом

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 | Gen 0 | Gen 1 | Gen 2 | Выделено |
| -------------- | ------ | -------------: | -----------: | -----------: | ------: | --------: | --------: | -------- : | --------: | ----------: |
| LinqSort | 30 | 1,439 нас | 0,0072 мкс | 0,0064 мкс | 1.00 | 0.00 | 0,0095 | - | - | 672 B |
| PriorityQueue | 30 | 1.450 мкс | 0,0085 мкс | 0,0079 мкс | 1.01 | 0,01 | - | - | - | - |
| PrioritySet | 30 | 2,778 нас | 0,0217 мкс | 0,0192 мкс | 1.93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 300 | 24,727 нас | 0,1032 нас | 0,0915 мкс | 1.00 | 0.00 | 0,0305 | - | - | 3912 B |
| PriorityQueue | 300 | 29.510 нас | 0,0995 мкс | 0,0882 мкс | 1.19 | 0,01 | - | - | - | - |
| PrioritySet | 300 | 47,715 нас | 0,4455 мкс | 0,4168 мкс | 1.93 | 0,02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015 нас | 1.5495 нас | 1.3736 нас | 1.00 | 0.00 | 0,4883 | - | - | 36312 B |
| PriorityQueue | 3000 | 491.722 нас | 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 нас | 11.9077 нас | 9.9434 нас | 1.00 | 0.00 | 93.7500 | 93.7500 | 93.7500 | 360910 B |
| PriorityQueue | 30000 | 5,688,625 нас | 53.0746 нас | 49.6460 нас | 1.09 | 0,01 | - | - | - | 2 B |
| PrioritySet | 30000 | 8,124.306 нас | 39.9498 нас | 37.3691 нас | 1,55 | 0,01 | - | - | - | 4 B |

Как и следовало ожидать, накладные расходы на расположение элементов отслеживания значительно снижают производительность, примерно на 40-50% медленнее по сравнению с базовой реализацией.

Я ценю все старания, вижу, что на это ушло много времени и сил.

  1. Я действительно не вижу причин для двух почти идентичных структур данных, одна из которых является худшей версией другой.
  2. Кроме того, даже если вы захотите иметь такие 2 версии очереди с приоритетом, я не понимаю, чем «улучшенная» версия лучше, чем предложение о очереди с приоритетом (v2.2) от 20 дней назад.

tl; dr:

  • Это захватывающее предложение !! Но это еще не подходит для моих сценариев использования с высокой производительностью.
  • > 90% моей вычислительной нагрузки / производительности планирования движения приходится на постановку / удаление PQ, потому что это доминирующий N ^ m LogN в алгоритме.
  • Я за отдельные реализации PQ. Обычно мне не нужны приоритетные обновления, и вдвое худшая производительность недопустима.
  • PrioritySet - это запутанное имя, и его нельзя обнаружить по сравнению с PriorityQueue.
  • Сохранять приоритет дважды (один раз в моем элементе, один раз в очереди) кажется дорогостоящим. Структурированные копии и использование памяти.
  • Если бы приоритет вычислений был дорогостоящим, я бы просто сохранил кортеж (priority: ComputePriority(element), element) в PQ, а моя функция получения приоритета была бы просто tuple => tuple.priority .
  • Производительность следует оценивать для каждой операции или, в качестве альтернативы, на реальных сценариях использования (например, оптимизированный многостартный многосторонний поиск на графике)
  • Неупорядоченное перечисление является неожиданным. Предпочитаете Queue-like Dequeue () - семантику порядка?
  • Рассмотрите возможность поддержки операции клонирования и операции слияния.
  • Базовые операции должны иметь нулевое выделение в установившемся режиме. Я объединю эти очереди.
  • Подумайте о поддержке EnqueueMany, который выполняет heapify для облегчения объединения.

Я работаю над высокопроизводительным поиском (планирование движения) и кодом вычислительной геометрии (например, алгоритмами Sweepline), который актуален как для робототехники, так и для игр, я использую множество настраиваемых вручную приоритетных очередей. Другой распространенный вариант использования, который у меня есть, - это запрос Top-K, в котором обновляемый приоритет бесполезен.

Некоторые отзывы о дебатах о двух реализациях (да и без поддержки обновлений).

Именование:

  • PrioritySet подразумевает семантику набора, но очередь не реализует ISet.
  • Вы используете имя UpdatablePriorityQueue, которое легче узнать, если я буду искать PriorityQueue.

Представление:

  • Производительность очереди приоритетов почти всегда является узким местом (> 90%) в моих алгоритмах геометрии / планирования.
  • Подумайте о передаче Funcили сравнениев ctor, а не копировать TPriority (дорого!). Если приоритет вычислений обходится дорого, я вставляю (приоритет, элемент) в PQ и передаю сравнение, которое смотрит на мой кэшированный приоритет.
  • Значительное количество моих алгоритмов не нуждаются в обновлениях PQ. Я бы подумал об использовании встроенного PQ, который не поддерживает обновления, но если что-то имеет удвоенную стоимость производительности для поддержки функции, которая мне не нужна (обновление), тогда это бесполезно для меня.
  • Для анализа производительности / компромиссов было бы важно знать относительные временные затраты на постановку / удаление из очереди на операцию @eiriktsarpalis - «отслеживание в 2 раза медленнее» недостаточно для оценки полезности PQ.
  • Я был рад видеть, как ваши конструкторы выполняют Heapify. Рассмотрим конструктор, который принимает IList, List или Array, чтобы избежать выделения при перечислении.
  • Рассмотрите возможность предоставления EnqueueMany, который выполняет Heapify, если PQ изначально пуст, поскольку при высокой производительности это обычное дело для коллекций пула.
  • Рассмотрите возможность создания массива Clear not zero, если элементы не содержат ссылок.
  • Выделения в очереди / вне очереди недопустимы. Мои алгоритмы имеют нулевое выделение из соображений производительности с объединенными локальными коллекциями потока.

API:

  • Клонирование очереди с приоритетом - тривиальная операция с вашими реализациями и часто бывает полезной.

    • Связанный: Должно ли перечисление очереди приоритетов иметь семантику, подобную очереди? Я ожидал бы коллекцию порядка удаления из очереди, аналогичную тому, что делает Queue. Я ожидаю, что новый список (myPriorityQueue) не изменит priorityQueue, а будет работать так, как я только что описал.

  • Как упоминалось выше, предпочтительно использовать Func<TElement, TPriority> а не вставлять с приоритетом. Если приоритет вычислений стоит дорого, я могу просто вставить (priority, element) и предоставить функцию tuple => tuple.priority
  • Иногда полезно объединение двух приоритетных очередей.
  • Странно, что Peek возвращает TItem, но Enumeration и Enqueue имеют (TItem, TPriority).

При этом для значительного числа моих алгоритмов элементы моей очереди приоритетов содержат свои приоритеты, и их сохранение дважды (один раз в PQ, один раз в элементах) звучит неэффективно. Это особенно актуально, если я заказываю по множеству ключей (аналогичный вариант использования OrderBy.ThenBy.ThenBy). Этот API также устраняет множество несоответствий, когда Insert имеет приоритет, а Peek не возвращает его.

Наконец, стоит отметить, что я часто вставляю в Priority Queue индексы массива, а не сами элементы массива . Тем не менее, это поддерживается всеми API-интерфейсами, которые обсуждались до сих пор. В качестве примера, если я обрабатываю начало / конец интервалов на линии оси x, у меня могут быть события очереди приоритетов (x, isStartElseEnd, intervalId) и порядок по x, а затем по isStartElseEnd. Часто это происходит потому, что у меня есть другие структуры данных, которые сопоставляют индекс с некоторыми вычисленными данными.

@pgolebiowski Я взял на себя смелость включить предложенную вами реализацию кучи сопряжения в тесты, чтобы мы могли напрямую сравнить экземпляры всех трех подходов. Вот результаты:

| Метод | Размер | Среднее | Ошибка | StdDev | Медиана | Gen 0 | Gen 1 | Gen 2 | Выделено |
| -------------- | -------- | -------------------: | ---- -------------: | -----------------: | ---------------- ---: | ----------: | ------: | ------: | -----------: |
| PriorityQueue | 10 | 774,7 нс | 3,30 нс | 3,08 нс | 773,2 нс | - | - | - | - |
| PrioritySet | 10 | 1,643.0 нс | 3.89 нс | 3,45 нс | 1,642,8 нс | - | - | - | - |
| PairingHeap | 10 | 1,660,2 нс | 14.11 нс | 12,51 нс | 1,657,2 нс | 0,0134 | - | - | 960 B |
| PriorityQueue | 50 | 6,413.0 нс | 14.95 нс | 13,99 нс | 6409,5 нс | - | - | - | - |
| PrioritySet | 50 | 12,193,1 нс | 35,41 нс | 29,57 нс | 12 188,3 нс | - | - | - | - |
| PairingHeap | 50 | 13 955,8 нс | 193,36 нс | 180,87 нс | 13 989,2 нс | 0,0610 | - | - | 4800 B |
| PriorityQueue | 150 | 27 402,5 нс | 76,52 нс | 71,58 нс | 27 410,2 нс | - | - | - | - |
| PrioritySet | 150 | 48 485,8 нс | 160,22 нс | 149,87 нс | 48 476,3 нс | - | - | - | - |
| PairingHeap | 150 | 56,951,2 нс | 190.52 нс | 168,89 нс | 56 953,6 нс | 0,1831 | - | - | 14400 B |
| PriorityQueue | 500 | 124 933,7 нс | 429.20 нс | 380,48 нс | 124 824,4 нс | - | - | - | - |
| PrioritySet | 500 | 206,310.0 нс | 433.97 нс | 338,81 нс | 206 319.0 нс | - | - | - | - |
| PairingHeap | 500 | 229 423,9 нс | 3 213,33 нс | 2 848,53 нс | 230 398,7 нс | 0,4883 | - | - | 48000 B |
| PriorityQueue | 1000 | 284 481,8 нс | 475.91 нс | 445,16 нс | 284 445,6 нс | - | - | - | - |
| PrioritySet | 1000 | 454 989,4 нс | 3,712,11 нс | 3,472,31 нс | 455,354.0 нс | - | - | - | - |
| PairingHeap | 1000 | 459 049,3 нс | 1,706,28 нс | 1,424,82 нс | 459 364,9 нс | 0,9766 | - | - | 96000 B |
| PriorityQueue | 10000 | 3,788,802,4 нс | 11,715,81 нс | 10,958.98 нс | 3,787,811,9 нс | - | - | - | 1 B |
| PrioritySet | 10000 | 5 963 100,4 нс | 26,669,04 нс | 22 269,86 нс | 5 950 915,5 нс | - | - | - | 2 B |
| PairingHeap | 10000 | 6,789,719.0 нс | 134 453.01 нс | 265,397,13 нс | 6 918 392,9 нс | 7.8125 | - | - | 960002 B |
| PriorityQueue | 1000000 | 595 059 170,7 нс | 4 001 349,38 нс | 3,547,092.00 нс | 595 716 610,5 нс | - | - | - | 4376 B |
| PrioritySet | 1000000 | 1,592,037,780,9 нс | 13,925,896,05 нс | 12 344 944,12 нс | 1 591 051 886,5 нс | - | - | - | 288 B |
| PairingHeap | 1000000 | 1,858,670,560,7 нс | 36,405,433.20 нс | 59,815,170,76 нс | 1,838,721,629.0 нс | 1000.0000 | - | - | 96000376 B |

Ключевые выводы

  • ~ Реализация кучи сопряжения асимптотически работает намного лучше, чем ее аналоги на основе массива. Однако он может быть в 2 раза медленнее для небольших размеров кучи (<50 элементов), догоняет около 1000 элементов, но до 2 раз быстрее для куч размером 10 ^ 6 ~.
  • Как и ожидалось, куча сопряжения производит значительное количество распределений кучи.
  • Реализация «PrioritySet» неизменно медленная ~ самая медленная из всех трех соперников ~, так что мы, возможно, не захотим использовать этот подход в конце концов.

~ В свете вышесказанного я по-прежнему считаю, что есть допустимые компромиссы между кучей базовых массивов и подходом к объединению кучи ~.

РЕДАКТИРОВАТЬ: обновил результаты после исправления ошибки в моих тестах, спасибо @VisualMelon

@eiriktsarpalis Я думаю, что ваш тест для PairingHeap неверен: параметры Add неверны. Когда вы меняете их местами, это совсем другая история: https://gist.github.com/VisualMelon/00885fe50f7ab0f4ae5cd1307312109f

(Я сделал то же самое, когда впервые реализовал это)

Обратите внимание, что это не означает, что куча сопряжения работает быстрее или медленнее, скорее, похоже, что она сильно зависит от распределения / порядка предоставленных данных.

@eiriktsarpalis re: полезность PrioritySet ...
Не следует ожидать, что обновляемая функция будет чем-то иным, кроме более медленной для heapsort, поскольку она не имеет приоритетных обновлений в сценарии. (Также для heapsort, вероятно, вы даже захотите сохранить дубликаты, набор просто не подходит.)

Лакмусовой бумажкой для определения полезности PrioritySet должны быть алгоритмы сравнительного анализа, которые используют обновления приоритета, а не необновляющую реализацию того же алгоритма, добавляя повторяющиеся значения в очередь и игнорируя дубликаты при исключении из очереди.

Спасибо @VisualMelon , я обновил свои результаты и комментарии после вашего предложенного исправления.

скорее, это, похоже, сильно зависит от распределения / порядка предоставленных данных.

Я считаю, что этому могло бы помочь то обстоятельство, что поставленные в очередь приоритеты были монотонными.

Лакмусовой бумажкой для определения полезности PrioritySet должны быть алгоритмы сравнительного анализа, которые используют обновления приоритета, а не необновляющую реализацию того же алгоритма, добавляя повторяющиеся значения в очередь и игнорируя дубликаты при исключении из очереди.

@TimLovellSmith моей целью было измерить производительность для наиболее распространенного приложения PriorityQueue: вместо того, чтобы измерять производительность обновлений, я хотел увидеть влияние на случай, когда обновления вообще не нужны. Однако имеет смысл создать отдельный тест, который сравнивает кучу сопряжения с обновлениями «PrioritySet».

@miyu, спасибо за подробный отзыв, мы очень ценим его!

@TimLovellSmith Я написал простой тест, в котором используются обновления:

| Метод | Размер | Среднее | Ошибка | StdDev | Медиана | Gen 0 | Gen 1 | Gen 2 | Выделено |
| ------------ | -------- | ---------------: | ---------- -----: | ---------------: | ---------------: | -------: | ------: | ------: | -----------: |
| PrioritySet | 10 | 1.052 мкс | 0,0106 мкс | 0,0099 мкс | 1.055 мкс | - | - | - | - |
| PairingHeap | 10 | 1.055 мкс | 0,0042 мкс | 0,0035 мкс | 1.055 мкс | 0,0057 | - | - | 480 B |
| PrioritySet | 50 | 7.394 нас | 0,0527 мкс | 0,0493 мкс | 7.380 мкс | - | - | - | - |
| PairingHeap | 50 | 8.587 нас | 0,1678 мкс | 0,1570 мкс | 8,634 нас | 0,0305 | - | - | 2400 B |
| PrioritySet | 150 | 27,522 нас | 0,0459 мкс | 0,0359 мкс | 27,523 нас | - | - | - | - |
| PairingHeap | 150 | 32.045 мкс | 0,1076 нас | 0.1007 мкс | 32.019 нас | 0,0610 | - | - | 7200 B |
| PrioritySet | 500 | 109.097 нас | 0,6548 мкс | 0,6125 мкс | 109,162 нас | - | - | - | - |
| PairingHeap | 500 | 131.647 нас | 0,5401 мкс | 0,4510 мкс | 131,588 нас | 0,2441 | - | - | 24000 B |
| PrioritySet | 1000 | 238.184 нас | 1.0282 нас | 0,9618 нас | 238.457 нас | - | - | - | - |
| PairingHeap | 1000 | 293,236 нас | 0,9396 мкс | 0,8789 нас | 293,257 нас | 0,4883 | - | - | 48000 B |
| PrioritySet | 10000 | 3,035.982 нас | 12.2952 нас | 10.8994 нас | 3036.985 нас | - | - | - | 1 B |
| PairingHeap | 10000 | 3,388,685 нас | 16.0675 нас | 38.1861 нас | 3,374,565 нас | - | - | - | 480002 B |
| PrioritySet | 1000000 | 841,406,888 нас | 16,788,4775 нас | 15,703.9522 нас | 840,888,389 нас | - | - | - | 288 B |
| PairingHeap | 1000000 | 989,966.501 нас | 19,722.6687 нас | 30,705,8191 нас | 996 075.410 нас | - | - | - | 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 раза медленнее, чем те, которые этого не делают.

Следующие шаги

В дальнейшем я предлагаю предпринять следующие действия для .NET 6:

  1. Представьте класс System.Collections.Generic.PriorityQueue который прост, удовлетворяет большинство требований наших пользователей и является максимально эффективным. Он будет использовать четвертичную кучу с поддержкой массива и не будет поддерживать приоритетные обновления. Прототип реализации можно найти здесь . Вскоре я создам отдельный выпуск с подробным описанием предложения API.

  2. Мы осознаем потребность в кучах, поддерживающих эффективные обновления приоритетов, поэтому продолжим работать над внедрением специализированного класса, отвечающего этому требованию. Мы оцениваем пару прототипов [ 1 , 2 ], каждый со своими наборами компромиссов. Я бы порекомендовал внедрить этот тип на более позднем этапе, поскольку для завершения дизайна потребуется дополнительная работа.

Сейчас я хотел бы поблагодарить участников этой @pgolebiowski и @TimLovellSmith. Ваши отзывы сыграли огромную роль в нашем процессе проектирования. Я надеюсь и дальше получать ваши отзывы по мере того, как мы дорабатываем дизайн обновляемой очереди приоритетов.

Забегая вперед, я предлагаю предпринять следующие действия для .NET 6: [...]

Звучит отлично :)

Представьте класс System.Collections.Generic.PriorityQueue который прост, удовлетворяет большинство требований наших пользователей и является максимально эффективным. Он будет использовать четвертичную кучу с поддержкой массива и не будет поддерживать приоритетные обновления.

Если у нас есть решение владельцев кодовой базы, что это направление одобрено и желательно, могу ли я продолжить разработку API для этого бита и предоставить окончательную реализацию?

Сейчас я хотел бы поблагодарить участников этой @pgolebiowski и @TimLovellSmith. Ваши отзывы сыграли огромную роль в нашем процессе проектирования. Я надеюсь и дальше получать ваши отзывы по мере того, как мы дорабатываем дизайн обновляемой очереди приоритетов.

Это было настоящее путешествие: D

API для System.Collections.Generic.PriorityQueue<TElement, TPriority> только что одобрен. Я создал отдельную задачу, чтобы продолжить наш разговор о возможной реализации кучи, которая поддерживает приоритетные обновления.

Я собираюсь закрыть этот выпуск, спасибо всем за ваш вклад!

Может быть, кто-нибудь сможет написать об этом путешествии! Целых 6 лет на один API. :) Есть ли шанс выиграть Гиннесс?

Была ли эта страница полезной?
0 / 5 - 0 рейтинги