Runtime: 添加优先队列<t>收藏</t>

创建于 2015-01-30  ·  318评论  ·  资料来源: dotnet/runtime

请参阅 corefxlab 存储库中的最新提案

第二个提案选项

来自https://github.com/dotnet/corefx/issues/574#issuecomment -307971397 的提案

假设

优先级队列中的元素是唯一的。 如果不是,我们将不得不引入项目的“句柄”以启用它们的更新/删除。 或者更新/删除语义必须应用于 first/all,这很奇怪。

仿照Queue<T>MSDN 链接

应用程序接口

```c#
公共类 PriorityQueue
: IEnumerable,
IEnumerable<(TElement 元素, TPriority 优先级)>,
IReadOnlyCollection<(TElement 元素,TPriority 优先级)>
// ICollection 不是故意包含的
{
公共优先队列();
公共优先队列(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(Func优先级选择器);
公共 PriorityQueue(Func优先级选择器,IComparer比较器);

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

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

}
````

开放问题:

  1. 类名PriorityQueueHeap
  2. 引入IHeap和构造函数重载? (我们应该等一下吗?)
  3. 介绍IPriorityQueue ? (我们要不要等一下—— IDictionary例子)
  4. 使用选择器(存储在值中的优先级)或不使用(5 个 API 差异)
  5. 使用元组(TElement element, TPriority priority)KeyValuePair<TPriority, TElement>

    • PeekDequeue应该使用out参数而不是元组吗?

  6. PeekDequeue投掷有用吗?

原始提案

问题https://github.com/dotnet/corefx/issues/163请求向核心 .NET 集合数据结构添加优先级队列。

这篇文章虽然是重复的,但旨在作为向 corefx API 审查流程的正式提交。 问题内容是新 System.Collections.Generic.PriorityQueue 的 _speclet_类型。

如果获得批准,我将贡献 PR。

基本原理和用法

.NET 基类库 (BCL) 当前缺乏对有序生产者-消费者集合的支持。 许多软件应用程序的一个共同要求是能够随着时间的推移生成项目列表,并按照与接收它们的顺序不同的顺序处理它们。

命名空间的 System.Collections 层次结构中有三种通用数据结构,它们支持排序的项目集合; System.Collections.Generic.SortedList、System.Collections.Generic.SortedSet 和 System.Collections.Generic.SortedDictionary。

其中,SortedSet 和 SortedDictionary 不适用于生成重复值的生产者-消费者模式。 SortedList 的复杂度对于 Add 和 Remove 都是 Θ(n) 最坏的情况。

对于具有生产者-消费者使用模式的有序集合来说,一种更内存和时间效率更高的数据结构是优先队列。 除了需要调整容量时,更糟糕的插入(入队)和移除顶部(出队)性能是 Θ(log n) - 远好于 BCL 中存在的现有选项。

优先级队列在不同类别的应用程序中具有广泛的适用性。 优先队列的维基百科页面提供了许多不同的很好理解的用例列表。 虽然高度专业化的实现可能仍需要自定义优先级队列实现,但标准实现将涵盖广泛的使用场景。 优先队列在调度多个生产者的输出时特别有用,这是高度并行化软件中的一个重要模式。

值得注意的是,C++ 标准库和 Java 都提供优先队列功能作为其基本 API 的一部分。

提议的API

``` C#
命名空间 System.Collections.Generic
{
///


/// 表示按排序顺序移除的对象集合。
///

///指定队列中元素的类型。
[DebuggerDisplay("Count = {count}")]
[DebuggerTypeProxy(typeof(System_PriorityQueueDebugView<>))]
公共类 PriorityQueue: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
///
/// 初始化一个新的实例班级
/// 使用默认比较器。
///

公共优先队列();

    /// <summary>
    /// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class 
    /// that has the specified initial capacity.
    /// </summary>
    /// <param name="capacity">The initial number of elements that the <see cref="PriorityQueue{T}"/> can contain.</param>
    /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
    public PriorityQueue(int capacity);

    /// <summary>
    /// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class 
    /// that uses a specified comparer.
    /// </summary>
    /// <param name="comparer">The <see cref="T:System.Collections.Generic.IComparer{T}"/> to use when comparing elements.</param>
    /// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is null.</exception>
    public PriorityQueue(IComparer<T> comparer);

    /// <summary>
    /// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class 
    /// that contains elements copied from the specified collection and uses a default comparer.
    /// </summary>
    /// <param name="collection">The collection whose elements are copied to the new <see cref="PriorityQueue{T}"/>.</param>
    /// <exception cref="T:System.ArgumentNullException"><paramref name="collection"/> is null.</exception>
    public PriorityQueue(IEnumerable<T> collection);

    /// <summary>
    /// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class 
    /// that contains elements copied from the specified collection and uses a specified comparer.
    /// </summary>
    /// <param name="collection">The collection whose elements are copied to the new <see cref="PriorityQueue{T}"/>.</param>
    /// <param name="comparer">The <see cref="T:System.Collections.Generic.IComparer{T}"/> to use when comparing elements.</param>
    /// <exception cref="T:System.ArgumentNullException">
    /// <paramref name="collection"/> is null. -or-
    /// <paramref name="comparer"/> is null.
    /// </exception>
    public PriorityQueue(IEnumerable<T> collection, IComparer<T> comparer);

    /// <summary>
    /// Initializes a new instance of the <see cref="PriorityQueue{T}"/> class that is empty,
    /// has the specified initial capacity, and uses a specified comparer.
    /// </summary>
    /// <param name="capacity">The initial number of elements that the <see cref="PriorityQueue{T}"/> can contain.</param>
    /// <param name="comparer">The <see cref="T:System.Collections.Generic.IComparer{T}"/> to use when comparing elements.</param>
    /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
    /// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is null.</exception>
    public PriorityQueue(int capacity, IComparer<T> comparer);

    /// <summary>
    /// Gets the <see cref="IComparer{T}"/> for the <see cref="PriorityQueue{T}"/>. 
    /// </summary>
    /// <value>
    /// The <see cref="T:System.Collections.Generic.IComparer{T}"/> that is used when
    /// comparing elements in the <see cref="PriorityQueue{T}"/>. 
    /// </value>
    public IComparer<T> Comparer 
    { 
        get;
    }

    /// <summary>
    /// Gets the number of elements contained in the <see cref="PriorityQueue{T}"/>.
    /// </summary>
    /// <value>The number of elements contained in the <see cref="PriorityQueue{T}"/>.</value>
    public int Count 
    { 
        get;
    }

    /// <summary>
    /// Adds an object to the into the <see cref="PriorityQueue{T}"/> by its priority.
    /// </summary>
    /// <param name="item">
    /// The object to add to the <see cref="PriorityQueue{T}"/>. 
    /// The value can be null for reference types.
    /// </param>
    public void Enqueue(T item);

    /// <summary>
    /// Removes and returns the object with the lowest priority in the <see cref="PriorityQueue{T}"/>.
    /// </summary>
    /// <returns>The object with the lowest priority that is removed from the <see cref="PriorityQueue{T}"/>.</returns>
    /// <exception cref="InvalidOperationException">The <see cref="PriorityQueue{T}"/> is empty.</exception>
    public T Dequeue();

    /// <summary>
    /// Returns the object with the lowest priority in the <see cref="PriorityQueue{T}"/>.
    /// </summary>
    /// <exception cref="InvalidOperationException">The <see cref="PriorityQueue{T}"/> is empty.</exception>
    public T Peek();

    /// <summary>
    /// Removes all elements from the <see cref="PriorityQueue{T}"/>.
    /// </summary>
    public void Clear();

    /// <summary>
    /// Determines whether an element is in the <see cref="PriorityQueue{T}"/>.
    /// </summary>
    /// <param name="item">
    /// The object to add to the end of the <see cref="PriorityQueue{T}"/>. 
    /// The value can be null for reference types.
    /// </param>
    /// <returns>
    /// true if item is found in the <see cref="PriorityQueue{T}"/>;  otherwise, false.
    /// </returns>
    public bool Contains(T item);

    /// <summary>
    /// Copies the elements of the <see cref="PriorityQueue{T}"/> to an  <see cref="T:System.Array"/>, 
    /// starting at a particular <see cref="T:System.Array"/> index.
    /// </summary>
    /// <param name="array">
    /// The one-dimensional <see cref="T:System.Array">Array</see> that is the
    /// destination of the elements copied from the <see cref="PriorityQueue{T}"/>. 
    /// The <see cref="T:System.Array">Array</see> must have zero-based indexing.
    /// </param>
    /// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
    /// <exception cref="T:System.ArgumentNullException"><paramref name="array"/> is null.</exception>
    /// <exception cref="T:System.ArgumentOutOfRangeException">
    /// <paramref name="arrayIndex"/> is less than zero. -or- 
    /// <paramref name="arrayIndex"/> is equal to or greater than the length of the <paramref name="array"/>
    /// </exception>
    /// <exception cref="ArgumentException">
    /// The number of elements in the source <see cref="T:System.Collections.ICollection"/> is
    /// greater than the available space from <paramref name="index"/> to the end of the destination
    /// <paramref name="array"/>.
    /// </exception>
    public void CopyTo(T[] array, int arrayIndex);

    /// <summary>
    /// Copies the elements of the <see cref="T:System.Collections.ICollection"/> to an 
    /// <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
    /// </summary>
    /// <param name="array">
    /// The one-dimensional <see cref="T:System.Array">Array</see> that is the
    /// destination of the elements copied from the <see cref="PriorityQueue{T}"/>. 
    /// The <see cref="T:System.Array">Array</see> must have zero-based indexing.
    /// </param>
    /// <param name="index">The zero-based index in <paramref name="array"/> at which copying begins.</param>
    /// <exception cref="T:System.ArgumentNullException"><paramref name="array"/> is null.</exception>
    /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index"/> is less than zero.</exception>
    /// <exception cref="ArgumentException">
    /// <paramref name="array"/> is multidimensional. -or-
    /// <paramref name="array"/> does not have zero-based indexing. -or-
    /// <paramref name="index"/> is equal to or greater than the length of the <paramref name="array"/> -or- 
    /// The number of elements in the source <see cref="T:System.Collections.ICollection"/> is
    /// greater than the available space from <paramref name="index"/> to the end of the destination
    /// <paramref name="array"/>. -or- 
    /// The type of the source <see cref="T:System.Collections.ICollection"/> cannot be cast automatically 
    /// to the type of the destination <paramref name="array"/>.
    /// </exception>
    void ICollection.CopyTo(Array array, int index);

    /// <summary>
    /// Copies the elements stored in the <see cref="PriorityQueue{T}"/> to a new array.
    /// </summary>
    /// <returns>
    /// A new array containing a snapshot of elements copied from the <see cref="PriorityQueue{T}"/>.
    /// </returns>
    public T[] ToArray();

    /// <summary>
    /// Returns an enumerator that iterates through the <see cref="PriorityQueue{T}"/>
    /// </summary>
    /// <returns>An enumerator for the contents of the <see cref="PriorityQueue{T}"/>.</returns>
    public Enumerator GetEnumerator();

    /// <summary>
    /// Returns an enumerator that iterates through the <see cref="PriorityQueue{T}"/>
    /// </summary>
    /// <returns>An enumerator for the contents of the <see cref="PriorityQueue{T}"/>.</returns>
    IEnumerator<T> IEnumerable<T>.GetEnumerator();

    /// <summary>
    /// Returns an enumerator that iterates through the <see cref="PriorityQueue{T}"/>.
    /// </summary>
    /// <returns>An <see cref="T:System.Collections.IEnumerator"/> that can be used to iterate through the collection.</returns>
    IEnumerator IEnumerable.GetEnumerator();

    /// <summary>
    /// Sets the capacity to the actual number of elements in the <see cref="PriorityQueue{T}"/>, 
    /// if that number is less than than a threshold value.
    /// </summary>
    public void TrimExcess();

    /// <summary>
    /// Gets a value that indicates whether access to the <see cref="ICollection"/> is 
    /// synchronized with the SyncRoot.
    /// </summary>
    /// <value>true if access to the <see cref="T:System.Collections.ICollection"/> is synchronized
    /// with the SyncRoot; otherwise, false. For <see cref="PriorityQueue{T}"/>, this property always
    /// returns false.</value>
    bool ICollection.IsSynchronized
    {
        get;
    }

    /// <summary>
    /// Gets an object that can be used to synchronize access to the 
    /// <see cref="T:System.Collections.ICollection"/>.
    /// </summary>
    /// <value>
    /// An object that can be used to synchronize access to the 
    /// <see cref="T:System.Collections.ICollection"/>.
    /// </value>
    object ICollection.SyncRoot
    {
        get;
    }

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

}
``

细节

  • 实现数据结构将是一个二进制堆。 具有较大比较值的项目将首先返回。 (降序排列)
  • 时间复杂度:

| 操作 | 复杂性 | 笔记 |
| --- | --- | --- |
| 建造 | Θ(1) | |
| 使用 IEnumerable 构建 | Θ(n) | |
| 入队 | Θ(log n) | |
| 出队 | Θ(log n) | |
| 偷看 | Θ(1) | |
| 计数 | Θ(1) | |
| 清除 | Θ(N) | |
| 包含 | Θ(N) | |
| 复制到 | Θ(N) | 使用Array.Copy,实际复杂度可能更低|
| ToArray | Θ(N) | 使用Array.Copy,实际复杂度可能更低|
| 获取枚举器 | Θ(1) | |
| Enumerator.MoveNext | Θ(1) | |

  • 采用 System.Comparison 的其他构造函数重载委托被有意省略,以支持简化的 API 表面区域。 呼叫者可以使用比较器.Create 将函数或 Lambda 表达式转换为 IComparer必要时接口。 这确实需要调用者进行一次性堆分配。
  • 虽然 System.Collections.Generic 还不是 corefx 的一部分,但我建议同时将这个类添加到 corefxlab 中。 添加 System.Collections.Generic 后,可以将其移至主要 corefx 存储库,并且已达成共识,应将其状态从实验性提升为官方 API。
  • 不包括 IsEmpty 属性,因为调用 Count 没有额外的性能损失。 大多数集合数据结构不包括 IsEmpty。
  • ICollection 的 IsSynchronized 和 SyncRoot 属性已明确实现,因为它们实际上已过时。 这也遵循用于其他 System.Collection.Generic 数据结构的模式。
  • 当队列为空以匹配 System.Collections.Queue 的既定行为时,Dequeue 和 Peek 抛出 InvalidOperationException.
  • IProducerConsumerCollection未实现,因为其文档说明它仅用于线程安全集合。

开放问题

  • 当使用 foreach 时,是否在调用 GetEnumerator 期间避免额外的堆分配是包含嵌套公共枚举器结构的足够强的理由?
  • CopyTo、ToArray 和 GetEnumerator 应该按优先级(排序)顺序还是数据结构使用的内部顺序返回结果? 我的假设是应该返回内部订单,因为它不会招致任何额外的性能损失。 但是,如果开发人员将该类视为“排序队列”而不是优先级队列,则这是一个潜在的可用性问题。
  • 将名为 PriorityQueue 的类型添加到 System.Collections.Generic 是否会导致潜在的破坏性更改? 命名空间被大量使用,并且可能导致包含其自己的优先级队列类型的项目的源兼容性问题。
  • 根据 IComparer 的输出,项目应该按升序还是降序出列? (我的假设是升序,以匹配 IComparer 的正常排序约定)。
  • 收藏应该是“稳定的”吗? 换句话说,如果两个项目具有相同的 IComparison结果是否以与它们入队完全相同的顺序出队? (我的假设是不需要)

    更新

  • 将“使用 IEnumerable 构造”的复杂性固定为 Θ(n)。 谢谢@svic。

  • 添加了另一个选项问题,与 IComparer 相比,优先级队列应该按升序还是降序排序.
  • 从显式 SyncRoot 属性中删除 NotSupportedException 以匹配其他 System.Collection.Generic 类型的行为,而不是使用较新的模式。
  • 使公共 GetEnumerator 方法返回嵌套的 Enumerator 结构而不是 IEnumerable,类似于现有的 System.Collections.Generic 类型。 这是在使用 foreach 循环时避免堆 (GC) 分配的优化。
  • 删除了 ComVisible 属性。
  • 将 Clear 的复杂度更改为 Θ(n)。 谢谢@mbeidler。
api-needs-work area-System.Collections wishlist

最有用的评论

堆数据结构是处理 leetcode 的必备条件
更多的 leetcode,更多的 c# 代码面试,这意味着更多的 c# 开发人员。
更多的开发者意味着更好的生态系统。
更好的生态系统意味着如果我们明天仍然可以用 c# 编程。

总而言之:这不仅是一个功能,也是未来。 这就是问题被标记为“未来”的原因。

所有318条评论

| 操作 | 复杂性 |
| --- | --- |
| 使用 IEnumerable 构建 | Θ(log n) |

我认为这应该是 Θ(n)。 您至少需要迭代输入。

+1

在调用 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很好的反馈,谢谢!

  • 我实现了枚举器. 显式重置,因为它是枚举器的核心、非弃用功能。 它是否在集合类型中暴露似乎不一致,只有少数使用显式变体。
  • 删除了 _disposed 字段以支持 _index,谢谢! 那天晚上在最后一分钟扔掉了它,但错过了显而易见的事情。 决定保留 ObjectDisposedException 以确保新集合类型的正确性,即使旧的 System.Collections.Generic 类型不使用它。
  • 数组.空是 F# 功能,所以很遗憾不能在这里使用它!
  • 修改了比较器参数接受null,好找!
  • ToArray 优化很棘手。 从技术上讲,数组在 C# 中是可变的,即使它们的长度为零。 实际上,您是对的,不需要分配并且可以对其进行优化。 我倾向于更谨慎的实施,以防出现我没有想到的副作用。 从语义上讲,调用者仍会期望这种分配,而且这是一个次要的分配。

@ebickle

Array.Empty 是 F# 的一个特性,所以很遗憾不能在这里使用它!

不再是: https :

我已将代码迁移到 issue-574 分支中的ebickle/corefx

实现了 Array.Empty() 更改并将所有内容插入常规构建管道。 我不得不介绍的一个轻微的临时kludge是让 System.Collections 项目依赖于 System.Collections nuget 包,作为比较器还没有开源。

将在问题 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>也不会缩小它们的数组(基于Reference Source ,它们还没有在 CoreFX 中)。

@mbeidler我曾考虑在出队中添加某种形式的自动数组收缩,但正如@svick指出的那样,它在类似数据结构的参考实现中不存在。 我很想知道 .NET Core/BCL 团队中的任何人是否有任何特殊原因选择这种实现方式。

更新:我检查了列表, 队列, Queue 和 ArrayList - 它们都不会在删除/出队时缩小内部数组的大小。

Enqueue 应该支持空值,并记录为允许它们。 你遇到错误了吗? 我不记得该地区的单元测试区域有多健壮。

有趣的是,我在@svick链接的参考源中注意到Queue<T>有一个未使用的私有常量_ShrinkThreshold 。 也许这种行为存在于以前的版本中。

关于在Equals的实现中使用IComparer而不是Equals Contains ,我编写了以下单元测试,目前会失败: https://gist.github。 com/mbeidler/9e9f566ba7356302c57e

@mbeidler好点。 根据 MSDN,IComparer/ IComparable仅保证零值具有相同的排序顺序。

但是,其他集合类中似乎存在相同的问题。 如果我修改代码以对 SortedList 进行操作,测试用例在 ContainsKey 上仍然失败。 SortedList的实现.ContainsKey 调用 Array.BinarySearch,它依赖于 IComparer 来检查相等性。 这同样适用于 SortedSet.

可以说这也是现有集合类中的一个错误。 我将深入研究其余的集合类,看看是否有任何其他数据结构接受 IComparer 但单独测试相等性。 不过,您是对的,对于优先级队列,您会期望完全独立于平等的自定义排序行为。

将修复和测试用例提交到我的 fork 分支中。 contains 的新实现直接基于 List 的行为.包含。 自列表不接受 IEqualityComparer,该行为在功能上是等效的。

当我今天晚些时候有时间时,我可能会提交其他内置集合的错误报告。 由于回归行为,可能无法修复,但至少需要更新文档。

我认为ContainsKey使用IComparer<TKey>实现是有意义的,因为这是指定键的内容。 但是,我认为ContainsValue IComparable<TValue>在其线性搜索中使用Equals而不是IComparable<TValue>会更合乎逻辑; 尽管在这种情况下,范围显着减少,因为类型的自然顺序与 equals 不一致的可能性要小得多。

看来,MSDN文档中为SortedList<TKey, TValue> ,备注部分为ContainsValue不表明TValue的排序顺序以代替平等使用。

@terrajobst 到目前为止,您对 API 提案

:+1:

感谢您提交此文件。 我相信我们有足够的数据来对该提案进行正式审查,因此我将其标记为“准备进行 API 审查”

由于DequeuePeek是抛出方法,调用者需要在每次调用之前检查计数。 按照并发集合的模式代替(或另外)提供TryDequeueTryPeek是否有意义? 将非抛出方法添加到现有泛型集合中存在一些问题,因此添加没有这些方法的新集合似乎适得其反。

@andrewgmorris相关https://github.com/dotnet/corefx/issues/4316 “将 TryDequeue 添加到队列”

我们对此进行了基本审查,并同意我们希望在框架中使用 ProrityQueue。 但是,我们需要有人来帮助推动它的设计和实现。 解决问题的人可以在@terrajobst完成 API 的工作。

那么 API 还剩下什么工作呢?

上面的 API 提案中缺少这一点: PriorityQueue<T>应该实现IReadOnlyCollection<T>以匹配Queue<T>Queue<T>现在实现IReadOnlyCollection<T>作为 .NET 4.6)。

我不知道基于数组的优先级队列是最好的。 .NET 中的内存分配非常快。 我们没有旧 malloc 处理的相同搜索小块问题。 欢迎您从这里使用我的优先队列代码: https :

@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 方法的文档,谢谢!

前段时间,我创建了一个复杂性类似于基于跳过列表数据结构的优先级队列的数据结构,我现在决定分享它: https :

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

跳过列表在平均情况下与上述优先级队列的复杂性相匹配,除了包含是平均情况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

作为参考,这里是关于 stackoverflow http://stackoverflow.com/questions/683041/java-how-do-i-use-a-priorityqueue上的 Java PriorityQueue 的问题

API 审核:
我们同意在 CoreFX 中使用这种类型很有用,因为我们希望 CoreFX 使用它。

对于 API 形状的最终审查,我们希望看到示例代码: PriorityQueue<Thread>PriorityQueue<MyClass>

  1. 我们如何保持优先级? 现在它仅由T隐含。
  2. 我们是否希望在您添加条目时可以传递优先级? (这看起来很方便)

笔记:

  • 我们期望优先级不会自行改变——我们需要它的 API,或者我们期望队列中的RemoveAdd
  • 鉴于我们这里没有明确的客户代码(只是希望我们想要类型),很难决定优化什么 - 性能、可用​​性还是其他?

这将是 CoreFX 中非常有用的类型。 有人有兴趣抢这个吗?

我不喜欢将优先级队列固定到二进制堆的想法。 有关详细信息,请查看我的AlgoKit wiki 页面。 快速思考:

  • 如果我们将实现固定到某种类型的堆,请将其设为 4 进制堆。
  • 我们不应该修复堆的实现,因为在某些情况下,某些类型的堆比其他类型的堆性能更高。 因此,假设我们将实现固定为某种堆类型,而客户需要不同的堆类型以获得更高的性能,他将需要从头开始实现整个优先级队列。 那是错误的。 我们这里应该更灵活一些,让他重用一些 CoreFX 代码(至少是一些接口)。

去年我实现了一些堆类型。 特别是,有IHeap接口可以用作优先级队列。

  • 为什么不把它称为堆......? 除了使用堆之外,您知道实现优先级队列的任何明智方法吗?

我完全赞成引入IHeap接口和一些性能最高的实现(至少那些基于数组的实现)。 API 和实现位于我上面链接的存储库中。

所以没有_优先队列_。

你怎么认为?

@karelz @safern @danmosemsft

@pgolebiowski不要忘记我们正在设计易于使用且性能良好的 API。 如果您想要PriorityQueue (已建立的计算机科学术语),您应该能够在文档中/通过互联网搜索找到一个。
如果底层实现是堆(我个人想知道为什么),或者其他什么,它并不重要。 假设您可以证明该实现明显优于更简单的替代方案(代码的复杂性也是一个有些重要的指标)。

因此,如果您仍然认为基于堆的实现(没有IHeap API)比一些简单的基于列表或基于数组块列表的实现更好,请解释原因(最好用几句话/段落),以便我们就实施方法达成一致(并避免在您这边浪费时间在复杂的实施上,这些实施可能在 PR 审查时被拒绝)。

删除ICollectionIEnumerable ? 只需要通用版本(虽然通用IEnumerable<T>会带来IEnumerable

@pgolebiowski它的实现方式不会改变外部 api。 PriorityQueue定义行为/契约; 而Heap是一个特定的实现。

优先队列与堆

如果底层实现是堆(我个人想知道为什么),或者其他什么,它并不重要。 假设您可以证明该实现明显优于更简单的替代方案(代码的复杂性也是一个有些重要的指标)。

因此,如果您仍然认为基于堆的实现比一些简单的基于列表或基于数组块列表的实现更好,请解释原因(最好用几句话/段落),以便我们就实施方法 [...]

好的。 优先级队列是一种抽象的数据结构,可以通过某种方式实现。 当然,您可以使用不同于堆的数据结构来实现它。 但没有比这更有效的方法了。 其结果:

  • 优先级队列和堆通常用作同义词(请参阅下面的 Python 文档作为最清晰的示例)
  • 每当您在某个库中有“优先队列”支持时,它就会使用堆(请参阅下面的所有示例)

为了支持我的话,让我们从理论支持开始。 算法简介,Cormen:

[…] 优先级队列有两种形式:最大优先级队列和最小优先级队列。 我们将在这里重点讨论如何实现最大优先级队列,而这些队列又基于最大堆。

明确指出优先队列是堆。 这当然是一个捷径,但你明白了。 现在,更重要的是,让我们看看其他语言及其标准库如何为我们正在讨论的操作添加支持:

  • 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 明确声明这是一个堆,就可以避免这种情况。

我希望你们都喜欢这种明确公开堆数据结构的方法——并且只在文档中引用优先队列。

API & 实现

我们需要就上述问题达成一致,但让我开始另一个话题——如何实现这一点。 我可以在这里看到两个解决方案:

  • 我们只提供一个ArrayHeap类。 看到名称,您可以立即知道您正在处理哪种类型的堆(同样,有几十种堆类型)。 你看它是基于数组的。 你会立即知道你正在处理的野兽。
  • 我更喜欢的另一种可能性是提供一个IHeap接口并提供一个或多个实现。 客户可以编写依赖于接口的代码——这将允许提供复杂算法的真正清晰可读的实现。 想象一下编写一个DijkstraAlgorithm类。 它可以简单地依赖于IHeap接口(一个参数化构造函数),或者只使用ArrayHeap (默认构造函数)。 由于使用了“优先级队列”术语,因此干净、简单、明确、没有歧义。 一个美妙的界面,理论上非常有意义。

在上面的方法中, ArrayHeap表示一个隐式的堆序完全d-ary 树,存储为一个数组。 这可用于创建例如BinaryHeapQuaternaryHeap

底线

对于进一步的讨论,我强烈建议您查看这篇论文。 你会知道:

  • 四元堆(四元)比二元堆(二元)快。 论文中进行了许多测试——您可能会对比较implicit_4implicit_2 (有时是implicit_simple_4implicit_simple_2 )的性能感兴趣。

  • 实现的最佳选择强烈依赖于输入。 在隐式 d-ary 堆、配对堆、斐波那契堆、二项式堆、显式 d-ary 堆、秩配对堆、地震堆、违规堆、秩松弛弱堆和严格斐波那契堆中,以下类型似乎几乎涵盖了各种场景的所有需求:

    • 隐式 d-ary 堆 (ArrayHeap),<- 我们当然需要这个
    • 配对堆,<-如果我们采用漂亮而干净的IHeap方法,那么值得添加它,因为在许多情况下,它确实比基于数组的解决方案更快

    对于他们两人来说,编程工作量出奇地低。 看看我的实现。


@karelz @benaadams

这将是 CoreFX 中非常有用的类型。 有人有兴趣抢这个吗?

@safern从现在

好的,这是我的键盘和椅子之间的问题 - PriorityQueue当然是基于Heap - 我在想Queue没有意义忘记了堆是“排序的”——对于像我这样喜欢逻辑、算法、图灵机等的人来说,这是非常令人尴尬的思考过程中断,我很抱歉。 (顺便说一句:我在您的 Java 文档链接中读到几句话后,立即发现了差异)

从这个角度来看,在Heap之上构建 API 是有意义的。 不过,我们不应该公开该类 - 如果它是我们在 CoreFX 中需要的东西,它将需要自己的 API 审查和自己的讨论。 由于实现,我们不希望 API 表面蔓延,但这可能是正确的做法——因此需要讨论。
从这个角度来看,我认为我们还不需要创建IHeap 。 以后可能是个不错的决定。
如果有研究表明特定堆(例如上面提到的 4 进制)最适合一般随机输入,那么我们应该选择它。 让我们等待@safern @ianhays @stephentoub确认/发表意见。

具有多个实现选项的底层堆的参数化是 IMO 不属于 CoreFX 的东西(我可能在这里又错了 - 让我们看看其他人的想法)。
我的原因是 IMO 我们很快就会发布大量的专业集合,这些集合对于人们(在算法的细微差别方面没有强大背景的普通开发人员)来说是非常困难的。 然而,这样的库将为该领域的专家提供一个很棒的 NuGet 包 - 由您/社区拥有。 将来,我们可能会考虑将它添加到PowerCollections 中(过去 4 个月我们一直在积极讨论将这个库放在 GitHub 上的哪个位置,以及我们是否应该拥有它,或者我们是否应该鼓励社区拥有它——对此有不同的看法,我希望我们将在 2.0 后敲定它的命运)

根据您的意愿分配给您...

@pgolebiowski合作者邀请已发送 - 当您接受 ping 我时,我将能够将其分配给您(GitHub 限制)。

@benaadams我会保留ICollection (轻度偏好)。 为了与 CoreFX 中的其他 ds 保持一致。 IMO 在这里放置一个奇怪的野兽是不值得的......如果我们添加一些新的(例如 PowerCollections 甚至到另一个回购),我们不应该包括非通用的......想法?

好的,这是我的键盘和椅子之间的问题。

哈哈😄 不用担心。

在 Heap 之上构建 API 是有意义的。 我们不应该公开该类,尽管 [...] 我们不希望由于实现而导致 API 表面蔓延,但这可能是正确的做法——因此需要进行讨论。 [...] 我认为我们还不需要创建 IHeap。 以后可能是个不错的决定。

如果小组的决定是使用PriorityQueue ,我将帮助设计并实现它。 但是,请考虑这样一个事实,如果我们现在添加PriorityQueue稍后在 API 中添加Heap会很混乱——因为两者的行为基本相同。 这将是一种冗余 IMO。 这对我来说是一种设计气味。 我不会添加优先队列。 它没有帮助。

另外,还有一个想法。 实际上,配对堆数据结构经常会派上用场。 基于数组的堆在合并它们时很糟糕。 这个操作基本上是线性的。 当你有很多合并堆时,你正在扼杀性能。 但是,如果您使用配对堆而不是数组堆——合并操作是常数(摊销)。 这是为什么我想提供一个很好的接口和两个实现的另一个论点。 一个用于一般输入,第二个用于某些特定场景,尤其是在涉及堆合并时。

IHeap + ArrayHeap + PairingHeap投票! 😄(就像在 Rust / Swift / Python / Go 中一样)

如果配对堆太多 - 好的。 但是让我们至少使用IHeap + ArrayHeap 。 你们不觉得使用PriorityQueue类会锁定未来的可能性并使 API 变得不那么清晰吗?

但正如我所说的——如果你们都投票支持PriorityQueue类而不是提议的解决方案——好的。

已发送合作者邀请 - 当您接受 ping 我时,我将能够将其分配给您(GitHub 限制)。

@karelz平:)

请考虑这样一个事实,如果我们现在添加一个 PriorityQueue,稍后在 API 中添加堆会很混乱——因为两者的行为基本相同。 这将是一种冗余 IMO。 这对我来说是一种设计气味。 我不会添加优先队列。 它没有帮助。

你能解释更多关于为什么以后会乱七八糟的细节吗? 你有什么顾虑?
PriorityQueue是人们使用的概念。 以这种方式命名的类型很有用,对吧?
我认为Heap上的逻辑操作(至少是它们的名称)可能会有所不同。 如果它们相同,那么在最坏的情况下(不理想,但不是世界末日),我们可以有相同代码的 2 种不同实现。 或者我们可以插入Heap类作为PriorityQueue父类,对吗? (假设从 API 审查的角度来看是允许的 - 现在我看不出有什么理由不这样做,但我没有那么多年的 API 审查经验,所以将等待其他人确认)

让我们看看投票和进一步的设计讨论如何......我正在慢慢接受IHeap + ArrayHeap的想法,但还没有完全相信......

如果我们添加一些新的......我们不应该包括非通用的

对公牛的红色抹布。 有没有人要添加一些其他集合,以便我们可以删除ICollection

圆形/环形缓冲器; 通用和并发?

@karelz命名问题的解决方案可能类似于IPriorityQueue就像 DataFlow 对生产/消费者模式所做的那样。 实现优先队列的方法有很多,如果您不关心,请使用接口。 关心实现或正在创建实例使用实现类。

你能解释更多关于为什么以后会乱七八糟的细节吗? 你有什么顾虑?
PriorityQueue是人们使用的概念。 以这种方式命名的类型很有用,对吧? [...] 我正在慢慢接受IHeap + ArrayHeap的想法,但还没有完全相信......

@karelz根据我的经验,我发现抽象( IPriorityQueueIHeap )非常重要。 由于这种方法,开发人员可以编写解耦的代码。 因为它是针对一个接口(而不是一个特定的实现)编写的,所以有更多的灵活性和 IoC 精神。 为这样的代码编写单元测试非常容易(使用依赖注入可以注入自己的模拟IPriorityQueueIHeap并查看在什么时间调用什么方法以及使用什么参数)。 抽象是好的。

确实,术语“优先级队列”被普遍使用。 问题是只有一种方法可以有效地实现优先队列——使用堆。 许多类型的堆。 所以我们可以有:

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你正在慢慢地让我相信HeapPriorityQueue更好——我们只需要强有力的指导和文档“这就是你所做的PriorityQueue - 使用Heap " ... 可以的。

但是,我很犹豫要不要在 CoreFX 中包含 1 个以上的堆实现。 98% 以上的“普通”C# 开发人员不在乎。 他们甚至不想考虑哪个是最好的,他们只需要可以完成工作的东西。 并非每个软件的每一部分在设计时都考虑到了高性能,这是理所当然的。 考虑所有一次性工具、UI 应用程序等。除非您正在设计高性能横向扩展系统,并且此 ds 位于关键路径上,否则您不应该关心。

同样,我不在乎SortedDictionaryArrayList或其他 ds 是如何实现的——它们做得很好。 我(和其他许多人一样)明白,如果我的场景需要这些 ds 的高性能,我需要衡量性能和/或检查实现,并且必须决定它是否适合我的场景,或者我是否需要推出我自己的特殊实现以获得最佳性能,并根据我的需要进行调整。

我们应该为 98% 的用例优化可用性,而不是为 2%。 如果我们推出太多选项(实现)并强迫每个人做出决定,我们只会对 98% 的用例造成不必要的混淆。 我觉得不值得...
IMO .NET 生态系统在提供许多 API(不仅仅是集合)的单一选择方面具有巨大的价值,这些 API 具有非常不错的性能特征,对大多数用例都很有用。 并提供生态系统,为那些需要它并愿意深入挖掘和了解更多/做出有根据的选择和权衡的人提供高性能扩展。

也就是说,有一个接口IHeap (如IDictionaryIReadOnlyDictionary )可能是有道理的 - 我必须多考虑一下/询问 API 审查专家空间 ...

我们已经(在某种程度上)知道@pgolebiowski正在谈论ISet<T>HashSet<T> 。 我说只是镜像它。 因此,上面的 API 被更改为一个接口( IPriorityQueue<T> ),然后我们有一个实现( HeapPriorityQueue<T> ),它在内部使用一个堆,该堆可能会或可能不会公开公开,因为它是自己的类。

它( PriorityQueue<T> )也应该实现IList<T>吗?

@karelz我对ICollectionSyncRootIsSynchronized ; 要么它们被实现,这意味着锁对象有额外的分配; 或者他们扔,当它有点毫无意义的时候。

@benaadams这会产生误导。 由于优先级队列的 99.99% 的实现都是基于数组的堆(正如我所看到的,我们在这里也将使用一个),这是否意味着暴露对数组内部结构的访问?

假设我们有一个包含元素 4、8、10、13、30、45 的堆。考虑到顺序,它们将通过索引 0、1、2、3、4、5 访问。但是,堆的内部结构是 [4, 8, 30, 10, 13, 45] (在二进制中,在四进制中会有所不同)。

  • 从用户的角度来看,在索引i处返回内部数字实际上没有意义,因为它几乎是任意的。
  • 按顺序(按优先级)返回一个数字成本太高——这是线性的。
  • 返回其中任何一个在其他实现中都没有意义。 通常它只是弹出i元素,获取i -th 元素,然后再次推送它们。

IList<T>通常是一种厚脸皮的解决方法:我想要灵活处理我的 api 接受的集合,我想枚举它们但不想通过IEnumerable<T>分配

刚刚意识到没有通用接口

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

所以别介意那个😢(虽然它是一种 IReadOnlyCollection)

但是应该明确地实现枚举器上的重置,因为它很糟糕并且应该抛出。

所以我建议的改变

public bool TryDequeue(out T item);     // Add
public bool TryPeek(out T item);        // Add

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

由于您正在讨论方法......我们还没有就IHeapIPriorityQueue达成一致——它会稍微影响方法的名称和逻辑。 但是,无论哪种方式,我都发现当前的 API 提案缺少以下内容:

  • 能够从队列中删除某个元素
  • 能够更新元素的优先级
  • 能够合并这些结构

这些操作非常关键,尤其是更新元素的可能性。 没有这个,许多算法根本无法实现。 我们需要引入一个句柄类型。 在此处IHeapNode 。 这是采用IHeap方式的另一个论点,否则我们将不得不引入PriorityQueueHandle类型,它始终只是一个堆节点......而堆节点——每个人都知道这些东西并且可以想象他们正在处理的东西。

其实简而言之,对于API提案,请看这个目录。 我们可能只需要它的一个子集。 但是,尽管如此 - 它只包含我们需要 IMO 的内容,因此作为起点可能值得一看。

小伙伴们,你们有什么想法?

IHeapNode与 clr 类型KeyValuePair没有太大不同?

然而,那是将优先级和类型分开,所以现在它是一个PriorityQueue<TKey, TValue>和一个IComparer<TKey> comparer

KeyValuePair 不仅是一个结构体,而且它的属性是只读的。 每次更新结构时,它基本上等于创建一个新对象。

仅使用一个键并不能用于相同的键——需要更多信息才能知道要更新/删除哪个元素。

IHeapNodeArrayHeapNode

    /// <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操作的算法(这非常常见)。 例如 Dijkstra 算法,Prim 算法。 或者,如果您正在编写某种调度程序并且某个进程(或其他任何进程)更改了其优先级,则您无法解决这个问题。

此外,在所有其他堆实现中——元素只是节点,明确地。 在其他情况下这是完全自然的,在这里它有点人为,但对于更新/删除是必要的。

在 Java 中,优先级队列没有更新元素的选项。 结果:

当我为 AlgoKit 项目实现堆并遇到所有这些设计问题时,我认为这就是 .NET 的作者决定不添加堆(或优先级队列)这样的基本数据结构的原因。 因为这两种设计都不好。 每个都有它的缺点。

简而言之——如果我们想添加一个支持元素按优先级进行有效排序的数据结构,我们最好以正确的方式来做,并添加一个功能,比如更新/删除元素。

如果您对用另一个对象将数组中的元素包装起来感到不舒服——这不是唯一的问题。 另一个是合并。 对于基于数组的堆,这是完全低效的。 但是……如果我们使用配对堆数据结构(配对堆:一种新形式的自调整堆),则:

  • 元素的句柄是很好的节点——无论如何都应该分配这些节点,所以没有乱七八糟的东西
  • 合并是恒定的(与基于数组的解决方案中的线性相比)

其实,我们可以通过以下方式解决这个问题:

  • 添加支持所有方法的IHeap接口
  • 添加ArrayHeapPairingHeap以及所有这些句柄、更新、删除、合并
  • 添加PriorityQueue ,它只是ArrayHeap的包装器,简化了 API

PriorityQueue将在System.Collections.GenericSystem.Collections.Specialized所有堆中。

作品?

我们不太可能通过 API 审查获得三个新的数据结构。 在大多数情况下,API 量越少越好。 如果最终不足,我们可以随时添加更多,但我们无法删除 API。

这就是我不喜欢 HeapNode 类的原因之一。 Imo 这种事情应该仅限内部使用,如果可能的话,API 应该公开一个已经存在的类型 - 在这种情况下可能是 KVP。

@ianhays如果这仅保留在内部,则用户将无法更新数据结构中元素的优先级。 这几乎毫无用处,我们最终会遇到所有 Java 问题——人们重新实现本机库中已经存在的数据结构......对我来说听起来很糟糕。 比拥有一个代表节点的简单类要糟糕得多。

顺便说一句:链表有一个节点类,以便用户可以使用适当的功能。 这几乎是镜像。

用户将无法更新数据结构中元素的优先级。

这不一定是真的。 优先级可以以不需要额外数据结构的方式公开,而不是

        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)

首先——你的代码中的TKeyTValue是什么? 这应该如何运作? 计算机科学的约定是:

  • 键 = 优先级
  • value = 你想在元素中存储的任何东西

第二:

Imo 大多数人将使用默认优先级类型,而 TValue 将是不必要的特殊性。

请定义“默认优先级类型”。 我能感觉到你只是想要PriorityQueue<T> ,是吗? 如果是这种情况,请考虑这样一个事实,即用户可能必须创建一个新类、一个围绕其优先级和值的包装器,并实现诸如IComparable或提供自定义比较器。 好可怜。

首先——你的代码中的 TKey 和 TValue 是什么?

项目和项目的优先级。 您可以将它们切换为优先级以及与该优先级相关联的项目,但是您有一个集合,其中 TKey 不一定是唯一的(即允许重复的优先级)。 我并不反对,但对我来说 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。 但这有点可怕。

@svic

我认为该类型应该称为PriorityQueue<T> ,而不是Heap<T>ArrayHeap<T>甚至QuaternaryHeap<T>以与 .Net 的其余部分保持一致。

我正在失去对人性的​​信心。

  • 调用数据结构PriorityQueue与 .NET 的其余部分一致,调用它Heap不是? 堆有问题吗? 那么同样应该适用于堆栈和队列。
  • ArrayHeap -> QuaternaryHeap基类。 巨大的差异。
  • 讨论不是取一个很酷的名字的问题。 遵循某种设计路径会产生很多东西。 请重新阅读该主题。
  • 哈希表数组列表。 它们似乎存在。 顺便说一句,“列表”是命名 IMO 的一个非常糟糕的选择,因为用户的第一个想法是List是一个列表,但它不是一个列表。 😜
  • 这不是为了玩得开心,也不是说事情是如何实施的。 它是关于给出有意义的名称,立即向用户显示他们正在处理的内容。

想要与 .NET 的其余部分保持一致并遇到这样的问题吗?

我在那里看到了什么... Jon Skeet 说SortedDictionary应该被称为SortedTree因为这更接近地反映了实现。

@pgolebiowski

堆有问题吗?

是的,它描述的是实现,而不是用法。

那么同样应该适用于堆栈和队列。

两者都没有真正描述实现,例如名称并没有告诉您它们是基于数组的。

ArrayHeap -> QuaternaryHeap基类。 巨大的差异。

是的,我理解你的设计。 我的意思是这些都不应该暴露给用户。

遵循某种设计路径会产生很多东西。 请重新阅读该主题。

我确实阅读了线程。 我不认为该设计属于 BCL。 我认为它应该包含易于使用和理解的内容,而不是导致人们想知道“四元堆”是什么或是否应该使用它的内容。

如果默认实现对他们来说不够好,那就是其他库(比如你自己的)的用武之地。

哈希表,数组列表。 它们似乎存在。

是的,那些是 .Net Framework 1.0 类,没人再使用了。 据我所知,他们的名字是从 Java 复制过来的,.Net Framework 2.0 的设计者决定不遵循这个约定。 在我看来,这是正确的决定。

顺便说一句,“列表”是命名 IMO 的一个非常糟糕的选择,因为用户的第一个想法是List是一个列表,但它不是一个列表。

这是。 它不是一个链表,但这不是一回事。 而且我喜欢不必到处写ArrayListResizeArrayList<T> F# 名称)。

它是关于给出有意义的名称,立即向用户显示他们正在处理的内容。

如果看到QuaternaryHeap大多数人将不知道他们在处理什么。 另一方面,如果他们看到PriorityQueue ,即使他们没有任何 CS 背景,也应该清楚他们在处理什么。 他们不会知道实现是什么,但这就是文档的用途。

@ianhays

首先——你的代码中的 TKey 和 TValue 是什么?

项目和项目的优先级。 您可以将它们切换为优先级以及与该优先级相关联的项目,但是您有一个集合,其中 TKey 不一定是唯一的(即允许重复的优先级)。 我并不反对,但对我来说 TKey 通常意味着唯一性。

键——用于确定优先级的东西。 键不必是唯一的。 我们不能做出这样的假设。

关键是公开 Node 类并不是公开 Update 方法的必要条件。

我相信它是:本机库中的减少键/增加键支持。 同样在这个主题上,所以有涉及处理数据结构的帮助类:

我想相当多的用户将不得不使用自定义比较器逻辑创建包装器类型。 我想也有相当多的用户已经有了他们想要放入优先队列的可比较类型或具有定义比较器的类型。 问题是两个阵营中哪个阵营更大。

我不知道其他人,但我发现每次我想使用优先级队列时都使用自定义比较器逻辑创建一个包装器非常可怕......

另一个想法——假设我创建了一个包装器:

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

我用这种类型喂PriorityQueue<T> 。 所以它根据它优先考虑它的元素。 假设它定义了一些键选择器。 这样的设计可能会使堆的内部工作崩溃。 您可以随时更改优先级,因为是元素的所有者。 堆更新没有通知机制。 您将负责处理所有这些并调用它。 非常危险,对我来说并不直接。

对于我提出的句柄——从用户的角度来看,该类型是不可变的。 并且不存在唯一性问题。

IMO 我喜欢有一个IHeap接口的想法,然后是实现该接口的类 API。 我和@ianhays 在一起,我们不会公开 3 个新的 api 来为客户提供不同类型的堆,因为需要一个 API 审查过程,我们希望专注于PriorityQueue不管名字是什么。

其次,我认为不需要使用 Node 内部/公共类来存储队列中的值。 不需要额外的分配,我们可以遵循 IDictionary 所做的事情,以及在为 Enumerable 创建 KeyValuePair 时产生值如果我们选择为我们存储的每个项目设置优先级(我认为这不是最好的方法,我不完全相信为每个项目存储一个优先级)。 我想要的最好的方法是拥有PriorityQueue<T> (这个名字只是为了给它一个)。 使用这种方法,我们可以让构造函数遵循整个 BCL 对IComparer<T>所做的操作,并与该比较器进行比较以提供优先级。 无需公开将优先级作为参数传递的 API。

我认为让某些 API 获得优先级会使其“不太可用”或更复杂,对于希望成为默认优先级的有序客户,然后我们使理解它的使用变得更加复杂,当给他们具有自定义IComparer<T>将是最合理的,并且将遵循我们在整个 BCL 中的指导方针。

名字是他们做什么,而不是他们如何做。

它们以抽象概念及其为用户实现的目标命名,而不是它们的实现和实现方式。 (这也意味着如果证明更好,可以改进它们的实现以使用不同的实现)

那么同样应该适用于堆栈和队列。

Stack是一个无界调整大小的数组,而Queue被实现为一个无界调整大小的循环缓冲区。 它们以其抽象概念命名。 堆栈(抽象数据类型) :后进先出(LIFO),队列(抽象数据类型) :先进先出(FIFO)

哈希表,数组列表。 它们似乎存在。

字典条目

是的,但让我们假装他们没有; 它们是来自不那么文明的时代的人工制品。 如果您使用原语或结构,它们就没有类型安全和盒子; 所以在每个添加上分配。

您可以使用@terrajobst平台兼容性分析器,它会告诉您:“请不要”

List 是一个列表,但它不是一个列表

它确实是一个列表(抽象数据类型),也称为序列。

Equally Priority Queue是一个抽象类型,它告诉你它在使用中实现了什么; 不是它在实现中是如何做到的(因为可以有许多不同的实现)

很多很棒的讨论!

最初的规范在设计时考虑了一些核心原则:

  • 通用 - 涵盖大多数用例,并在 CPU 和内存消耗之间取得平衡。
  • 尽可能与 System.Collections.Generic 中的现有模式和约定保持一致。
  • 允许同一元素的多个条目。
  • 利用现有的 BCL 比较接口和类(即比较器).*
  • 高级抽象 - 该规范旨在保护消费者免受底层实现技术的影响。

@karelz @pgolebiowski
重命名为“堆”或与数据结构实现对齐的其他术语与大多数 BCL 集合约定不匹配。 从历史上看,.NET 集合类被设计为通用的,而不是专注于特定的数据结构/模式。 我最初的想法是,在 .NET 生态系统的早期,API 设计者有意从“ArrayList”转移到“List”。这种变化很可能是由于与 Array 混淆了——您的普通开发人员会认为“ArrayList? 我只想要一个列表,而不是一个数组”。

如果我们使用堆,可能会发生同样的情况——许多中级技术开发人员会(遗憾地)看到“堆”并将其与应用程序的内存堆(即堆和堆栈)混淆,而不是“堆泛化数据结构”。 System.Collections.Generic 的流行将导致它出现在几乎每个 .NET 开发人员的智能提案中,他们会想知道为什么他们可以分配一个新的内存堆 :)

相比之下,PriorityQueue 更容易被发现,也更不容易混淆。 您可以输入“Queue”并获取 PriorityQueue 的建议。

关于整数优先级或优先级通用参数(TKey、TPriority 等)的一些建议和问题。 添加明确的优先级将需要消费者编写自己的逻辑来映射他们的优先级并增加 API 的复杂性。 使用内置的 IComparer利用现有的 BCL 功能,我还考虑向比较器添加重载进入规范,以便更容易地提供临时 lambda 表达式/匿名函数作为优先级比较。 遗憾的是,这不是 BCL 中的常见约定。

如果要求条目是唯一的,则 Enqueue() 将需要唯一性查找以抛出 ArgumentException。 此外,可能存在允许项目多次排队的有效场景。 这种非唯一性设计使得提供 Update() 操作具有挑战性,因为无法判断哪个对象正在被更新。 正如一些评论指出的那样,这将开始进入返回“节点”引用的 API,而这些引用又(可能)需要需要被垃圾收集的分配。 即使解决了这个问题,它也会增加优先级队列的每个元素内存消耗。

在我发布规范之前,有一次我在 API 中有一个自定义的 IPriorityQueue 接口。 最终我决定反对它 - 我的目标使用模式是入队、出队和迭代。 已经被现有的接口集覆盖了。 将其视为内部排序的队列; 只要项目根据它们的 IComparer 在队列中保持自己的(初始)位置,调用者永远不需要关心优先级是如何表示的。 在我所做的旧参考实现中,(如果我没记错的话!)根本没有表示优先级。 这都是基于 IComparer 的相对或比较.

我欠你们一些客户代码示例 - 我最初的计划是通过 PriorityQueue 的现有 BCL 实现来用作示例的基础。

在摘要 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。 将它们放入优先队列中,一切顺利。

排序列表使用键/值设计,结果我发现自己避免使用它。 它要求键是唯一的,我通常的要求是“我有 N 个需要在列表中排序的值,并确保我偶尔添加的值保持排序”。 “键”不是该等式的一部分,列表不是字典。

对我来说,同样的原则适用于队列。 从历史上看,队列是一种一维数据结构。

诚然,我的现代 C++ std 库知识有点生疏,但即使是 std::priority_queue 似乎也有推送、弹出和将比较器作为模板化(通用)参数。 C++ 标准库工作人员对性能非常敏感:)

刚刚我对几种编程语言中的优先级队列和堆实现进行了非常快速的扫描——C++、Java、Rust 和 Go 都使用单一类型(类似于此处发布的原始 API)。 粗略地看一下 NPM 中最流行的堆/优先级队列实现,结果是一样的。

@pgolebiowski不要误会我的意思,应该在特定实现之后显式命名事物的特定实现。

然而,这是因为当您知道您想要的特定数据结构与您所追求的性能目标相匹配并具有您愿意接受的权衡时。

通常,框架集合涵盖了您想要一般行为的 90% 的用途。 然后,如果您想要一个非常具体的行为或实现,您可能会选择 3rd 方库; 并且希望它们以实施方式命名,以便您知道它符合您的需求。

只是不想将一般行为类型与特定实现联系起来; 如果实现更改,那么它很奇怪,因为类型名称必须保持不变并且它们将不匹配。

有很多问题需要讨论,但让我们从目前对我们不同意的部分影响最大的问题开始:更新和删除元素

那么你想如何支持这些操作呢? 我们必须包括它们,它们是基本的。 在 Java 中,设计者省略了它们,因此:

  1. 由于缺少功能,论坛上有很多关于如何解决的问题。
  2. 有堆/优先级队列的第三方实现来替换第一方实现,因为它几乎没用。

这太可悲了。 真的有人要这样追求吗? 我会为发布这样一个禁用的数据结构而感到羞耻。

@pgolebiowski 请放心,这里的每个人都对平台

不过,我想指出几点:

  • 不要指望一夜之间发生变化。 这是一个设计讨论。 我们需要找到共识。 我们不急于使用 API。 每个人的意见都应该被听取和考虑,但不能保证每个人的意见都会被执行/接受。 如果您有意见,请提供,并提供数据和证据。 也听听别人的论点,承认他们。 如果您不同意,请提供反对他人意见的证据。 有时,同意在某些点上存在分歧的事实。 请记住,SW,包括。 API 设计不是非黑即白/正确或错误的事情。
  • 让我们保持讨论文明。 我们不要使用强硬的语言和陈述。 让我们不同意恩典,让我们继续讨论技术问题。 大家也可以参考贡献者行为准则。 我们认可并鼓励人们对 .NET 充满热情,但请确保我们不会互相冒犯。
  • 如果您对设计讨论的速度、反应等有任何疑虑/问题,请随时直接与我联系(我的电子邮件在我的 GH 个人资料中)。 如果需要,我可以公开或离线帮助澄清期望、假设和担忧。

我只是问如果你们不喜欢提议的方法,你们想要如何设计更新/删除的东西......我相信这是倾听他人的意见并寻求共识。

我不怀疑你的好意! 有时,您提问的方式很重要 - 它会影响人们如何看待另一面的文本。 文字没有情感,所以写下来时可以有不同的理解。 英语作为第二语言使事情更加混乱,我们都需要意识到这一点。 如果您有兴趣,我很乐意离线讨论详细信息……让我们将讨论引回到技术讨论……

我在 Heap 与 PriorityQueue 辩论中的两分钱:这两种方法都是有效的,并且显然各有利弊。

也就是说,“PriorityQueue”似乎与现有的 .NET 方法更加一致。 今天的核心收藏是List, 字典, 堆, 队列, 哈希集, 排序字典, 和 SortedSet. 这些是以功能和语义而不是算法命名的。 哈希集是唯一的异常值,但即使这也可以合理化为与集合相等语义有关(与 SortedSet 相比))。 毕竟,我们现在有了 ImmutableHashSet这是基于引擎盖下的一棵树。

一个系列在这里逆势而上会让人感觉很奇怪。

我认为 PriorityQueue 带有额外的构造函数:PriorityQueue(堆) 可以是一个解决方案。 没有 IHeap 参数的构造函数可以使用默认堆。
在这种情况下,优先队列将表示抽象数据类型(如大多数 C# 集合)并实现 IPriorityQueue接口,但可以使用不同的堆实现,如@pgolebiowski建议:

类 ArrayHeap : IHeap {}
类 PairingHeap : IHeap {}
类 FibonacciHeap : IHeap {}
二项式堆类:IHeap {}

好的。 有很多不同的声音。 我再次进行了讨论并改进了我的方法。 我还解决了常见的问题。 下面的文字使用了上面帖子中的引述。

我们的目标

整个讨论的最终目标是提供一组特定的功能来让用户满意。 用户拥有一堆元素是一种非常常见的情况,其中一些元素的优先级高于其他元素。 最终,他们希望以某种特定顺序保留这组元素,以便能够有效地执行以下操作:

  • 检索具有最高优先级的元素(并能够将其删除)。
  • 向集合中添加一个新元素。
  • 从集合中删除一个元素。
  • 修改集合中的元素。
  • 合并两个集合。

其他标准库

其他标准库的作者也试图支持此功能。 在本节中,我将参考它是如何在PythonJavaC++GoSwiftRust 中解决的

它们都不支持修改已经插入到集合中的元素。 这是非常令人惊讶的,因为在集合的生命周期内,其元素的优先级很可能会发生变化。 特别是,如果此类集合旨在在服务的整个生命周期内使用。 还有许多算法利用了这种更新元素的功能。 因此,这样的设计完全是错误的,因为结果,我们的客户丢失了: StackOverflow 。 这是互联网上许多类似的问题之一。 设计师失败了。

另一件值得注意的事情是,每个库都通过实现二进制堆来提供部分功能。 好吧,已经证明它不是一般情况下(随机输入)性能最高的实现。 最适合一般情况的是四元堆(隐式堆序完整四元树,存储为数组)。 它鲜为人知,这可能是设计者采用二叉堆的原因。 但仍然 - 另一个糟糕的选择,尽管严重程度较低。

我们从中学到什么?

  • 如果我们希望我们的客户感到高兴,并阻止他们自己实现这个功能而忽略我们美妙的数据结构,我们应该提供对修改已插入集合中的元素的支持。
  • 我们不应该假设因为某些功能是在某个标准库中以某种方式交付的,我们应该这样做,因为现在它广为人知并且用户已经习惯了。 注意句子的最后一部分。

提议的方法

我强烈认为我们应该提供:

  • IHeap<T>接口
  • Heap<T>班级

IHeap接口显然包含实现本文开头描述的所有操作的方法。 使用四元堆实现的Heap类将成为 98% 情况下的首选解决方案。 元素的排序将基于传递给构造函数的IComparer<T>或默认排序(如果类型已经具有可比性)。

理由

  • 开发人员可以根据接口编写他们的逻辑。 我想每个人都知道这有多重要,并且不会详细介绍。 阅读:依赖倒置原理依赖注入契约式设计、控制倒置
  • 开发人员可以通过提供其他堆实现来扩展此功能以满足他们的自定义需求。 此类实现可以包含在第三方库中,例如PowerCollections 。 通过引用这样的库,您可以简单地将自定义堆注入到任何以IHeap作为输入的逻辑中。 在某些特定条件下表现优于四元堆的其他堆的一些示例是:配对堆、二项式堆和我们钟爱的二元堆。
  • 如果开发人员只需要一个工具来完成工作,而无需考虑哪种类型最好,他们可以简单地使用通用的Heap实现。 这是针对 98% 的用例进行优化。
  • 我们增加了 .NET 生态系统的巨大价值,提供具有非常不错的性能特征的单一选择,对大多数用例有用,同时为需要它并愿意挖掘的人提供高性能扩展深入了解并了解更多/做出有根据的选择和权衡。
  • 提议的方法反映了当前的惯例:

    • ISetHashSet

    • IListList

    • IDictionaryDictionary

  • 有些人说我们应该根据它们的实例做什么来命名类,而不是它们是如何做的。 这并不完全正确。 说我们应该根据类的行为来命名类,这是一种常见的捷径。 它确实适用于许多情况。 但是,在某些情况下,这不是合适的方法。 最显着的例子是基本的构建块——比如原始类型、枚举类型或数据结构。 原则是简单地选择有意义的名称(即对用户来说明确的)。 考虑到我们所讨论的功能总是以堆的形式提供——无论是 Python、Java、C++、Go、Swift 还是 Rust。 堆是最基本的数据结构之一。 Heap确实是明确而明确的。 它也与StackQueueListArray协调。 在最现代的标准库(Go、Swift、Rust)中采用了相同的命名方法——它们显式地暴露了一个堆。

@pgolebiowski我不知道Heap<T> / IHeap<T>是如何命名为Stack<T>Queue<T>和/或List<T> ? 这些名称都没有解释它们是如何在内部实现的(一个 T 数组)。

@SamuelEnglard

Heap也没有说明它是如何在内部实现的。 我不明白为什么对这么多人来说,堆紧跟在特定的实现之后。 有许多共享相同 API 的堆变体,首先是:

  • d-ary 堆,
  • 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
就计算机科学世界的组织方式而言,是的。 以下是抽象级别:

  • 实现:基于数组的隐式四元堆
  • 抽象:四元堆
  • 抽象:堆家族
  • 抽象:优先队列族

是的,有了IHeapHeapPriorityQueue基本上是:

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接口留出空间。 我觉得它经历了太多的抽象层次,扼杀了拥有界面的所有美妙好处。

我们回到讨论中间的设计选择——有PriorityQueueIPriorityQueue 。 但那时我们基本上会拥有:

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

感觉不仅丑陋,而且在概念上也是错误的——这些不是优先级队列的类型,也不共享相同的 API(正如@SamuelEnglard已经指出的那样)。 我认为我们应该坚持堆,一个足够大的家庭,可以为他们提供抽象。 我们会得到:

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

而且,由我们提供, class Heap : IHeap {}


顺便说一句,有人可能会发现以下内容有帮助:

| 谷歌查询 | 结果 |
| :----------------------------------------: | :-----: |
| “数据结构”“优先队列” | 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

有什么说法吗? 这将是至少很好,如果你讨论我写的,而不是上面只说不做

我认为你之前给它命名得很好:_如果你有意见,提供它,用数据和证据支持它。 也听听别人的论点,承认他们。 如果您不同意,请提供反对他人意见的证据。_

另外,这不仅仅是关于名称。 它没有那么浅。 请阅读我写的内容。 它是关于在抽象级别之间工作并能够在其上增强代码/构建。

哦,在这个讨论中有一个争论为什么:

如果我们使用 Heap,许多中级技术开发人员会(遗憾地)看到“Heap”并将其混淆为应用程序的内存堆(即堆和堆栈)而不是“堆泛化数据结构”。 [...] 他们会想知道为什么他们可以分配一个新的内存堆。

😛

我们不应该引入IHeap接口。 IHeap界面是非常高级的专家场景 - 我建议将它移到 PowerCollections 库中。

@bendono对此发表了非常好的评论。 @safern还想通过接口支持实现。 还有其他一些。

另一个注意事项 - 我不确定您如何想象将界面移动到第三方库中。 开发人员怎么可能针对该接口编写代码使用我们的功能? 他们将被迫要么坚持我们的非接口功能,要么完全忽略它,没有其他选择,这是相互排斥的。 换句话说——我们的解决方案根本无法扩展,导致人们使用我们禁用的解决方案或某些第三方库,而不是在两种情况下都依赖于相同的架构核心。 这就是我们在 Java 的标准库和第三方解决方案中所拥有的。

但同样,您很好地评论了这一点:没有人愿意发布损坏的 API。

鉴于意见分歧,我提出以下方法来推进该提案。 [...] 提出 2 个备选提案 [...] 让我们将其提交给 API 审查,在那里进行讨论并在那里做出决定。 如果我看到该组中至少有一个人投票支持 Heaps 提案,我会很乐意重新考虑我的观点。

上面对 API 的各个部分进行了很多讨论。 它解决了:

  • PriorityQueue vs Heap
  • 添加接口
  • 支持从集合中更新/删除元素

为什么你心目中的那些人不能为这次讨论做出贡献? 为什么我们要重新开始?

如果我们使用 Heap,许多中级技术开发人员会(遗憾地)看到“Heap”并将其混淆为应用程序的内存堆(即堆和堆栈)而不是“堆泛化数据结构”。

是的,这就是我。 我在编程方面完全是自学的,所以当“ Heap ”进入讨论时我不知道在说什么。 更不用说即使在集合方面,“一堆东西”对我来说更直观地暗示它在各种方式中都是无序的。

我不敢相信这真的发生了......

有什么说法吗? 如果您解决我上面写的内容而不是仅仅说不,那至少会很好。

如果你阅读我的回答,你会注意到我提到了我的立场的关键论点:

  • IHeap 接口非常高级的专家场景
  • 我不认为它是有用的/与 BCL 的其余部分一致,不足以证明现在添加新界面的复杂性

在 IMO 线程上多次重复相同的论点。 我只是总结了他们

. 请阅读我写的内容。

我一直在积极监视这个线程。 我阅读了所有的论点和要点。 尽管阅读(并理解)了您和其他人的所有观点,但这是我的总结意见。
你显然对这个话题充满热情。 太棒了。 然而,我有一种感觉,当我们只是从每一方重复类似的论点时,我们就陷入了困境,这不会带来更多的结果——这就是为什么我推荐了 2 个提案并从更大的经验小组中获得更多反馈BCL API 审阅者(顺便说一句:我还不认为自己是有经验的 API 审阅者)。

开发人员怎么可能针对该接口编写代码并使用我们的功能?

关心IHeap高级场景的开发人员会参考来自 3rd 方库的接口和实现。 正如我所说,如果证明它很受欢迎,我们可以考虑稍后将其移入 CoreFX。
好消息是,稍后添加IHeap是完全可行的 - 它基本上只在PriorityQueue上添加一个构造函数重载。
是的,从你的角度来看,这并不理想,但它并不妨碍你认为未来重要的创新。 我认为这是合理的中间立场。

为什么你心目中的那些人不能为这次讨论做出贡献?

API 审查是一次积极讨论、集思广益、权衡各个角度的会议。 在许多情况下,它比在 GitHub 问题上来回更高效。 参见 dotnet/corefx#14354 和它的前身 dotnet/corefx#8034 - 讨论太长,几个不同的意见以及难以追踪的无数调整,没有结论,虽然讨论很好,但对很多人来说也是非平凡的浪费时间,直到我们坐下来讨论并达成共识。
让 API 审查员监控每一个 API 问题,甚至只是喋喋不休的问题都不能很好地扩展。

为什么我们要重新开始?

我们不会重新开始。 你为什么那么想?
我们将通过将 2 个最受欢迎的具有优缺点的提案发送到下一个 API 审查级别来完成第一级(区域所有者级别)的 API 审查。
这是分层批准/审查方法。 这类似于业务审查——拥有决策权的副总裁/首席执行官不会监督公司中每个项目的每一次讨论,他们要求他们的团队/报告提出最具影响力或最具感染力的决策建议,以便进一步讨论。 团队/报告必须总结问题并提出替代解决方案的优缺点。

如果您认为我们还没有准备好展示最终的 2 个提案的优缺点,因为在这个线程上还有一些事情还没有说,让我们继续讨论,直到我们有几个顶级候选人在下一次审查API 审查级别。
我有一种感觉,该说的都已经说完了。
说得通?

名称应为PriorityQueue

有什么说法吗?

如果你阅读我的回答,你会注意到我提到了我的立场的关键论点。

天啊...我指的是我引用的内容(显然)-您决定使用优先级队列而不是堆。 是的,我已经阅读了您的答案——它恰好包含 0% 的参数。

我一直在积极监视这个线程。 我阅读了所有的论点和要点。 尽管阅读(并理解)了您和其他人的所有观点,但这是我的总结意见。

你喜欢夸张,我以前就注意到了。 你只是承认点的存在。

开发人员怎么可能针对该接口编写代码并使用我们的功能?

关心 IHeap 高级场景的开发人员会参考来自 3rd 方库的接口和实现。 正如我所说,如果证明它很受欢迎,我们可以考虑稍后将其移入 CoreFX。

您是否知道您只是在这里重复我的话,而根本没有解决我作为上述结果提出的问题? 我写:

他们将被迫要么坚持我们的非接口功能,要么完全忽略它,没有其他选择,这是相互排斥的。

我会为你非常清楚地说明这一点。 会有两个不相交的人群:

  1. 一组正在使用我们的功能。 它没有接口,不能扩展,因此它没有连接到第三方库中提供的东西。
  2. 第二组完全无视我们的功能并使用纯粹的第三方解决方案。

这里的问题是,正如我所说,那些是不相交的人群他们正在生成无法协同工作的代码,因为没有共同的架构核心。 _代码库不兼容_。 您以后无法撤消它。

好消息是稍后添加 IHeap 是完全可行的——它基本上只在 PriorityQueue 上添加一个构造函数重载。

我已经写了为什么它很糟糕:见这篇文章

为什么你心目中的那些人不能为这次讨论做出贡献?

让 API 审查员监控每一个 API 问题,甚至只是喋喋不休的问题都不能很好地扩展。

是的,我在问为什么 API 审查员不能监控每个 API 问题。 准确地说,你确实做出了相应的回应。

说得通?

不,我厌倦了这个讨论,真的。 你们中的一些人显然参与其中,只是因为这是您的工作,而且您被告知要这样做。 你们有些人一直需要指导,这很烦人。 你甚至让我证明为什么优先队列应该在内部用堆实现,显然缺乏计算机科学背景。 有些人甚至不明白什么是堆,这让讨论更加混乱。

使用不允许更新和删除元素的禁用 PriorityQueue。 使用不允许健康的面向对象方法的设计。 使用不允许在编写扩展时重用标准库的解决方案。 走 Java 之路。

这……这真是令人兴奋:

如果我们使用 Heap,许多中级技术开发人员会(遗憾地)看到“Heap”并将其混淆为应用程序的内存堆(即堆和堆栈)而不是“堆泛化数据结构”。

用你的方法展示 API。 我很好奇。

我不敢相信这真的发生了......

好吧,请原谅我没有机会接受适当的计算机科学教育来了解Heap是某种不同于内存堆的数据结构。

不过,这一点仍然成立。 一堆东​​西并不意味着它以某种方式被订购。 如果我需要一个允许我存储要处理的对象的集合,其中一些稍后进入的实例可能需要更快地处理,我不会搜索称为Heap 。 另一方面, PriorityQueue完美地传达了它正是这样做的。

作为支持实现? 当然,实现细节不应该是我关心的。
一些IHeap抽象? 伟大的API作者和有一个CS主要知道它是用来谁的人,没有理由不有。
给某些东西起一个神秘的名字,却不能很好地表达它的意图,从而限制了可发现性? 👎

好吧,原谅我没有机会接受适当的计算机科学教育来了解堆是某种不同于内存堆的数据结构。

这是荒唐的。 同时,您希望参与有关添加此类功能的讨论。 这听起来像拖钓。

一堆东​​西并不意味着它以某种方式被订购。

你错了。 它是有序的,作为一个堆。 就像你链接的图片一样。

作为支持实现? 当然,实现细节不应该是我关心的。

我已经解决了这个问题。 堆家族很大,在实现之上有两个抽象级别。 优先级队列是第三个抽象层。

如果我需要一个允许我存储要处理的对象的集合,其中一些稍后进入的实例可能需要更快地处理,我不会搜索称为堆的东西。 另一方面,PriorityQueue 完美地传达了它正是这样做的。

没有任何背景,你会要求谷歌为你提供关于优先队列的文章吗? 好吧,我们可以争论我们意见中或多或少的可能性。 但是,正如有人说得很好:

如果您有意见,请提供,并提供数据和证据。 如果您不同意,请提供反对他人意见的证据。

根据数据你错了:

查询 | 点击次数
:----: |:----:|
| “数据结构”“优先队列” | 172,000 |
| “数据结构” “堆” | 430,000 |

您在阅读数据结构时遇到堆的可能性几乎是其 3 倍。 另外,它是 Swift、Go、Rust 和 Python 开发人员熟悉的名称,因为它们的标准库提供了这样的数据结构。

查询 | 点击次数
:----: |:----:|
| "golang" "优先队列" | 3.390 |
| “锈”“优先队列” | 8.630 |
| "swift" "优先队列" | 18.600 |
| "python" "优先队列" | 72.800 |
| "golang" "堆" | 79.000 |
| “生锈” “堆” | 492.000 |
| "快速" "堆" | 551.000 |
| “蟒蛇”“堆”| 555.000 |

实际上对于 C++ 也是类似的,因为在上个世纪的某个时候在那里引入了堆数据结构。

给某些东西起一个神秘的名字,却不能很好地表达它的意图,从而限制了可发现性? 👎

没有意见。 数据。 看上面。 尤其是没有背景的人没有意见。 如果之前没有阅读过有关数据结构的一些资料,您也不会在 Google 上搜索优先队列。 许多数据结构 101都涵盖了堆。

它是计算机科学的基础。 这是基本的。 当你学习了几个学期的算法和数据结构时,你一开始就会看到堆。

但仍然:

  • 首先——看看上面的数字。
  • 其次——考虑堆是标准库的一部分的所有其他语言。

编辑:见谷歌趋势

作为另一个自学成才的开发者,我对_heap_ 没有任何问题。 作为一个一直在努力改进的开发人员,我花时间学习和理解所有关于数据结构的知识。 简而言之,我不同意命名约定应该针对那些没有花时间理解他们所属领域的词典的人的暗示。

而且我也强烈反对诸如“名称为 PriorityQueue”之类的说法。 如果你不想要人们的意见,那么就不要让它开源,也不要要求它。

让我解释一下我们如何看待 API 命名:

  1. 我们倾向于支持 .NET 平台内的一致性,而不是其他任何事情。 这对于使 API 看起来熟悉且可预测很重要。 有时这意味着我们接受一个名称不是 100% 正确的,如果这是我们以前使用过的一个术语。

  2. 我们的目标是设计一个可供各种开发人员使用的平台,其中一些开发人员没有接受过正规的计算机科学教育。 我们相信 .NET 被普遍认为非常高效且易于使用的部分原因部分是由于该设计点。

我们通常采用“搜索引擎测试”来检查名称或术语的知名程度和成熟度。 所以我非常感谢@pgolebiowski所做的研究。 我自己还没有做过这项研究,但我的直觉是“堆”不是许多非领域专家会寻找的术语。

因此,我倾向于同意@karelz 的观点,即PriorityQueue看起来是更好的选择。 它结合了现有概念(队列)并添加了表达所需功能的扭曲:基于优先级的有序检索。 但我们并非不可动摇地依附于这个名字。 我们经常根据客户的反馈更改数据结构和技术的名称。

但是,我想指出的是:

如果你不想要人们的意见,那么就不要让它开源,也不要要求它。

是错误的二分法。 并不是我们不想从我们的生态系统和贡献者那里得到反馈(我们显然是这样做的)。 但与此同时,我们也必须意识到我们的客户群非常多样化,GitHub 贡献者(或我们团队的开发人员)并不总是我们所有客户的最佳代理。 可用性很难,可能需要一些迭代才能将新概念添加到 .NET 中,尤其是在集合等高度流行的领域。

@pgolebiowski

我非常重视您的见解、数据和建议。 但我绝对不欣赏你的论证风格。 在此线程中,您对我的团队成员和社区成员都有个人看法。 仅仅因为您不同意我们的观点,您就不能指责我们没有专业知识或不关心我们,因为这“只是我们的工作”。 考虑到我们中的许多人实际上已经在全球范围内搬家,仅仅因为我们想做这份工作而离开家人和朋友。 像您这样的评论不仅非常不公平,而且也无助于推进设计。

因此,虽然我喜欢认为自己脸皮厚,但我对这种行为没有太大的容忍度。 我们的领域已经足够复杂; 我们不需要添加对抗性和对抗性的交流。

请尊重。 热情地批评想法是公平的游戏,但攻击人则不然。 谢谢你。

亲爱的所有感到沮丧的人,

我为我的严厉态度降低了你的幸福感而道歉。

@karelz

我在那里犯了技术错误,这让我自己出丑了。 我道歉了。 它被接受了。 然而后来扔给了我。 不酷的海事组织。

很抱歉我写的东西让你不开心。 虽然没有你描述的那么糟糕——我只是把它作为导致我感到疲倦的众多因素之一。 我认为它的严重性较低。 但是,我还是很抱歉。

是的,每个人都会犯错。 没关系。 我也是,例如有时会得意忘形。

最让我感动的是“你只是被告知要这样做,你不相信” - 是的,这正是我在周末也这样做的原因。

对不起,我看到你很努力,我很感激。 在 5/10 里程碑前,我看到你是多么敬业。

@terrajobst

仅仅因为您不同意我们的观点,您就不能指责我们没有专业知识或不关心我们,因为这“只是我们的工作”。

  • 没有专业知识——针对没有计算机科学背景且不了解堆/优先级队列概念的人。 如果这样的描述适用于某人——嗯,它适用,这不是我的错。
  • 不关心——针对那些倾向于忽略某些技术要点的人,从而强制重复争论,因此使讨论变得混乱,其他人更难以理解(这反过来会导致更少的投入)。

像您这样的评论非常不公平,也无助于推进设计。

  • 我的严厉评论是由于这次讨论效率低下造成的。 低效率 = 混乱的讨论方式,没有解决/解决问题,尽管如此 => 令人厌烦,我们仍继续前进。
  • 此外,作为讨论中的关键驱动因素之一,我强烈认为我已经做了很多工作来帮助推进设计。 请不要妖魔化我,就像你在这里和在社交媒体上所做的那样。

如果有生病的人想“舔我”,请随意。


我们遇到了一个问题并且已经解决了。 每个人都从中学到了一些东西。 看到这里大家都很关心框架的质量,这绝对是美妙的,让我有动力去贡献。 我期待着继续与您一起开发 CoreFX。 话虽如此,我可能会在明天解决您的新技术输入问题。

@pgolebiowski

希望我们能在某个时候亲自见面。 老实说,我认为在网上做所有事情的部分挑战是个性有时会以糟糕的方式混合,双方都无意。

我期待着继续与您一起开发 CoreFX。 话虽如此,我可能会在明天解决您的新技术输入问题。

同样在这里。 这是一个有趣的空间,我们可以一起做很多令人惊奇的事情:-)

首先@pgolebiowski ,感谢您的回复。 这表明你很关心,你的意思很好(我暗地里希望世界上的每个人/开发人员都这样做,所有的冲突都只是误解/沟通不畅)。 这让我真的很高兴——它让我继续前进和兴奋。
我建议重新开始我们的关系。 让我们把讨论转回到技术上,让我们都从这个线程中学习,以后如何处理类似的情况,让我们再次假设对方只考虑平台的最大利益。
顺便说一句:这是过去 9 个月中 CoreFX 回购中遇到的一些更困难的遭遇/讨论之一,正如你所看到的,我们(包括/特别是我)仍在学习如何很好地处理它们 - 所以这个特殊的例子正在发生甚至使我们受益,这将使我们在未来变得更好,并帮助我们更好地理解热情的社区成员的不同观点。 也许它会影响我们对贡献文档的更新......

我的严厉评论是由于这次讨论效率低下造成的。 低效率 = 混乱的讨论方式,没有解决/解决问题,尽管如此 => 令人厌烦,我们仍继续前进。

明白你的无奈! 有趣的是,出于同样的原因,另一边也有类似的挫败感😉......世界的运作方式几乎很有趣:)。
不幸的是,当您推动设计决策时,困难的讨论是工作的一部分。 这是很多工作。 许多人低估了它。 所需的关键技能是对每个人都有耐心,并且能够超越自己的意见,思考如何推动共识,即使它不符合你的意愿。 这就是为什么我建议有 2 个提案并将技术讨论“升级”到 API 审查小组(主要是因为我不确定我是对的,尽管我暗中希望我是对的,就像世界上其他所有开发人员一样😉 )。

很难对一个主题发表意见并在同一线程上推动讨论达成共识。 从这个角度来看,你和我在这个话题上最常见——我们都有意见,但我们都试图将讨论推向结束和决定。 因此,让我们密切合作。

我的一般做法是:每当我认为有人在攻击我、邪恶、懒惰、让我感到沮丧或其他什么时。 我首先问自己,也问那个特定的人:为什么? 你为什么这样说? 你什么意思?
通常这是缺乏理解/沟通动机的迹象。 或者在字里行间阅读太多的迹象,并在没有看到侮辱/指责/不良意图的地方看到。


既然我不怕继续讨论技术问题,这就是我之前想问的:

使用不允许更新和删除元素的禁用 PriorityQueue。

这是我不明白的事情。 如果我们省略IHeap (在我的/这里的原始提案中),那为什么不可能?
IMO 从类能力的角度来看,这 2 个提案之间没有区别,唯一的区别是——我们是否添加了PriorityQueue(IHeap)构造函数重载(将类名争议放在一边作为独立问题来解决) .

免责声明(为了避免误解):我没有时间阅读文章和做研究,我希望得到简短的回答,从想要推动技术讨论的人那里提出电梯论点。 注意:这不是我在拖钓。 我会向我们团队中提出此声明的任何人提出同样的问题。 如果你没有精力去解释它/推动讨论(考虑到你这边的困难和时间投入,这完全可以理解),就这么说,不要有什么难受的感觉。 请不要对我或任何人感到压力(这适用于线程中的每个人)。

不想在这里添加更多不必要的评论,这个帖子太长了。 互联网时代的第一条规则是,如果你关心人际关系,就避免文本交流。 (好吧,我创造了它)。 我相信如果有明显的需要,其他一些开源社区会转而使用 Google Hangout 进行此类讨论。 当你看别人的脸时,你永远不会说任何“侮辱”的话,人们很快就会互相熟悉。 或许我们也可以试试?

@karelz由于上述讨论的长度,如果不修改流程,任何新人都不太可能做出贡献。 因此,我现在想提出以下方法:

  • 我将一个接一个地对基本方面进行投票。 我们将从社区获得明确的意见。 理想情况下,API 审阅者也会很快来到这里并发表评论。
  • “投票帖子”将包含足够的信息,可以忽略上面的整个文本墙。
  • 在本次投票结束后,我们将知道对 API 审查者的期望,并能够继续采用某种方法。 当我们就基本方面达成一致时,这个问题将被关闭,而另一个问题将被打开(将参考这个)。 在新一期中,我将总结我们的结论并提供反映这些决定的 API 提案。 我们将从那里拿走它。

这有没有道理的机会?

PriorityQueue 不允许更新和删除元素。

这是关于原始提案,它缺乏这些功能:) 抱歉没有说清楚。

如果你没有精力去解释它/推动讨论(考虑到你这边的困难和时间投入,这完全可以理解),就这么说,不要有什么难受的感觉。 请不要对我或任何人感到压力(这适用于线程中的每个人)。

我不会放弃。 没有痛苦就没有收获xD

@xied75

我相信如果有明显的需要,其他一些开源社区会转而使用 Google Hangout 进行此类讨论。 或许我们也可以试试?

看起来挺好的 ;)

提供接口

无论我们使用Heap / IHeap还是PriorityQueue / IPriorityQueue (或其他),对于我们即将提供的功能......

_你想要一个接口以及实现吗?_

为了

@本多诺

通过接口支持该实现,我们这些关心其他实现的人可以轻松地交换另一个实现(我们自己创建的或在外部库中),并且仍然与接受该接口的代码兼容。

不在意的可以无视,直接去看具体的实现。 那些关心的人可以使用他们选择的任何实现。

反对

@马德尔森

理论上可以使用许多不同的算法来实现这种数据结构。 然而,该框架似乎不太可能提供不止一个,而且库编写者确实需要一个规范接口以便各方可以协调编写交叉兼容的堆实现的想法似乎不太可能。

此外,一旦接口被释放,它就永远不会因为兼容性而改变。 这意味着接口在功能方面往往落后于具体类(今天 IList 和 IDictionary 存在一个问题)。

@karelz

界面是非常高级的专家场景。

如果库和IHeap接口变得非常流行,我们可以稍后改变主意并根据需求添加IHeap (通过构造函数重载),但我认为它没有用/与BCL 的其余部分足以证明现在添加新界面的复杂性。 从简单开始,只有在真正需要时才变得复杂。

潜在的决策影响

  • 包括接口意味着我们将来无法更改它。
  • 不包括接口意味着人们编写的代码要么使用我们的标准库解决方案,要么针对第三方库提供的解决方案编写(没有可实现交叉兼容性的通用接口)。

使用 👍 和 👎 对此进行投票(分别支持和反对一个界面)。 或者,写一个评论。 理想情况下,API 审查员将参与。

我想补充一点,虽然更改接口很困难,但随着扩展方法(和属性的到来)接口更容易扩展和/或使用(参见 LINQ)

我想补充一点,虽然更改接口很困难,但随着扩展方法(和属性的到来)接口更容易扩展和/或使用(参见 LINQ)

它们只能使用接口上公开定义的方法; 所以这意味着第一次做对。

我建议在接口上推迟一段时间,直到类被使用并稳定下来 - 然后引入一个接口。 (关于界面形状的争论是一个单独的问题)

说白了,我唯一关心的就是界面。 一个可靠的实现会很好,但我(或其他任何人)总是可以创建我自己的。

我记得几年前我们如何与HashSet<T>完全相同的对话。 微软想要HashSet<T>而社区想要ISet<T> 。 如果我没记错的话,我们第一个拿到了HashSet<T>第二个拿到了ISet<T> 。 如果没有接口, HashSet<T>非常有限,因为更改公共 API 很困难(如果不是经常的话)。

我应该注意到现在还有SortedSet<T> ,更不用说ISet<T>许多非 BCL 实现了。 我已经在公共 API 中使用了ISet<T>并对此表示感谢。 我的私有实现可以使用我认为正确的任何具体实现。 我还可以轻松地将一个实现替换为另一个实现,而不会破坏任何东西。 如果没有接口,这是不可能的。

对于那些说我们总是可以定义自己的接口的人,请考虑这一点。 假设 BCL 中的ISet<T>从未发生过。 现在我可以创建自己的界面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 @jcouv

对于通知: _伙计们,您能否提供您的意见? 即使您只是使用 :+1:、:-1: 投票,这也会非常有帮助。 你可以开始阅读这个问题的评论(这里有上面的 4 篇文章)——我们正在讨论提供一个接口是否是一个好主意(现在只有这方面,直到它得到解决)。_

也许其中一些人是 API 审查员,但我相信在我们继续之前,我们需要他们的支持来做出这个基本决定。 @karelz@terrajobst ,您是否可以请他们帮助我们解决这方面的问题? 他们的意见非常有价值,因为他们是最终会审查它的人——在这一点上了解它会非常有帮助,然后再采用某种方法(或采用 1 个以上的提案,这会很烦人)并且有点毫无意义,因为我们可以更早地知道他们的决定)。

就我个人而言,我是为了一个界面,但如果决定不同,我很乐意走不同的道路。

我不想把 API 评论者拖进讨论中——它又长又乱,API 评论者重新阅读所有内容甚至只是决定最后一个重要回复是没有效率的(我在其中迷失了自己) )。
我认为我们现在可以创建 2 个正式的 API 提案(请参阅那里的“好”示例)并突出每个

顺便说一句:API 审查会议几乎每个星期二都会举行。

为了帮助启动它,提案应如下所示:

提案示例/种子

```c#
命名空间 System.Collections.Generic
{
公共类 PriorityQueue
: IEnumerable, ICollection, IEnumerable, IReadOnlyCollection
{
公共优先队列();
公共 PriorityQueue(整数容量);
公共优先队列(IComparer比较器);
公共优先队列(IEnumerable收藏);
公共优先队列(IEnumerable集合,IComparer比较器);
公共 PriorityQueue(整数容量,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 审查。 因此,我想提出一个稍微不同的解决方案,例如忘记四元堆。 下面的数据结构与我们在PythonJavaC++GoSwiftRust (至少是那些)中可以找到的数据结构在一些方面有所不同。

主要关注点是功能的正确性、完整性和直观性,同时保持最佳的复杂性和出色的现实世界性能。

@karelz @terrajobst

提议

基本原理

用户拥有一堆元素是一种非常常见的情况,其中一些元素的优先级高于其他元素。 最终,他们希望以某种特定顺序保留这组元素,以便能够有效地执行以下操作:

  1. 向集合中添加一个新元素。
  2. 检索具有最高优先级的元素(并能够将其删除)。
  3. 从集合中删除一个元素。
  4. 修改集合中的元素。
  5. 合并两个集合。

用法

词汇表

  • 价值——用户数据。
  • — 用于排序目的的对象。

各类用户数据

首先,我们将重点构建优先级队列(仅添加元素)。 它的完成方式取决于用户数据的类型。

场景一

  • TKeyTValue是独立的对象。
  • TKey具有可比性。
var queue = new PriorityQueue<int, string>();

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

场景二

  • TKeyTValue是独立的对象。
  • TKey没有可比性。
var comparer = Comparer<MyKey>.Create(/* custom logic */);

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

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

场景3

  • 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

场景四

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

场景5

  • TKeyTValue是不同的对象,但类型相同。
  • TKey具有可比性。
var queue = new PriorityQueue<int, int>();

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

场景 6

  • 用户数据是单个对象,具有可比性。
  • 没有物理密钥或用户不想使用它。
public class MyClass : IComparable<MyClass>
{
    public int CompareTo(MyClass other) => /* custom logic */
}
var queue = new PriorityQueue<MyClass, MyClass>();

queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
queue.Enqueue(new MyClass( /* args */ ));
笔记

一开始,有歧义。

  • MyClass是一个单独的对象,而用户只想将键和值分开,就像在场景 5 中一样
  • 但是,它也可能是单个对象(如本例中)。

以下是PriorityQueue处理歧义的方法:

  • 如果定义了键选择器,则没有歧义。 仅允许Enqueue(TValue) 。 因此,场景 6的替代解决
  • 如果未定义键选择器,则在第一次使用Enqueue方法时会解决歧义:

    • 如果Enqueue(TKey, TValue)第一次被调用,key 和 value 被认为是单独的对象(场景 5 )。 从那时起, Enqueue(TValue)方法必须抛出InvalidOperationException

    • 如果Enqueue(TValue)第一次被调用,key 和 value 被认为是同一个对象,则推断出 key 选择器(场景 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();

稍后将讨论PeekDequeue方法究竟返回什么。

修改元素

对于任意元素,我们可能想对其进行修改。 例如,如果在服务的整个生命周期中使用优先级队列,则开发人员很可能希望有更新优先级的可能性。

令人惊讶的是,这样的功能不是由等效的数据结构提供的:在PythonJavaC++GoSwiftRust 中。 可能还有其他一些,但我只检查了那些。 结果? 失望的开发者:

我们在这里基本上有两个选择:

  • 以 Java 方式进行操作,并且不提供此功能。 我强烈反对。 它强制用户从集合中删除一个元素(这本身并不能很好地工作,但我稍后会谈到),然后再次添加它。 它非常丑陋,并非在所有情况下都有效,而且效率低下。
  • 引入手柄的新概念。

把手

每次用户将元素添加到优先级队列时,他们都会获得一个句柄:

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

Handle 是一个具有以下公共 API 的类:

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

它是对优先级队列中唯一元素的引用。 如果您担心效率,请参阅常见问题解答(您将不再担心)。

这种方法允许我们以非常直观和简单的方式轻松修改优先级队列中的唯一元素:

/*
 * User wants to retrieve a server that at any given moment
 * has the lowest average response time.
 * He doesn't want to maintain a separate key object (it is
 * inside his type) and the key is already comparable.
 */

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

/* adding some elements */

var handle = queue.Enqueue(server);

/*
 * Server stats are kept along with handles (e.g. in a dictionary).
 * Whenever there is a need of updating the priority of a certain
 * server, the user simply updates the appropriate ServerStats object
 * and then simply uses the handle associated with it:
 */

queue.Update(handle);

在上面的例子中,因为定义了一个特定的选择器,用户可以自己更新对象。 只需要通知优先级队列重新排列它自己(我们不想让它成为可观察的)。

类似于用户数据的类型如何影响优先级队列的构建和填充元素的方式,它的更新方式也各不相同。 这次我将缩短场景,因为您可能已经知道我要写什么了。

场景

场景 7
  • TKeyTValue是独立的对象。
var queue = new PriorityQueue<int, string>();

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

queue.Update(handle, 3);

如您所见,这种方法提供了一种简单的方法来引用优先级队列中的唯一元素,无需提出任何问题。 它在密钥可能重复的情况下特别有用。 此外,这非常有效——我们知道要在 O(1) 中更新的元素,并且可以在 O(log n) 中执行操作。

或者,用户可以使用其他方法在 O(n) 中搜索整个结构,然后更新与参数匹配的第一个元素。 这就是在 Java 中删除元素的方式。 它既不完全正确也不高效,但有时更简单:

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

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

queue.Update("three", 30);

上面的方法找到其值等于"three"的第一个元素。 它的密钥将更新为30 。 如果有多个满足条件,我们不知道会更新哪一个。

使用方法Update(TKey oldKey, TValue, TKey newKey)可能会稍微安全一些。 这增加了另一个条件——旧密钥也必须匹配。 这两种解决方案都更简单,但不是 100% 安全且性能较低(O(1 + log n) 与 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
  • TKeyTValue是不同的对象,但类型相同。

使用句柄,没有歧义,一如既往。 在允许更新的其他方法的情况下 - 当然有。 这是简单性、性能和正确性(取决于数据是否可以复制)之间的权衡。

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

设置新密钥也可能是一个函数,它采用旧密钥并以某种方式对其进行转换。

ContainsRemove

更新密钥

如果未定义键选择器(因此键和值分开保存),则该方法可以命名为UpdateKey 。 可能表达的更清楚。 但是,当定义了键选择器时, Update更好,因为键已经更新,需要做的是重新排列优先级队列中的某些元素。

常问问题

句柄是不是效率低下?

在使用句柄方面没有效率问题。 一个常见的担忧是它需要额外的分配,因为我们在内部使用基于数组的堆。 不要害怕。 继续阅读。

那你打算如何实施呢?

这将是一种完全不同的提供优先级队列的方法。 第一次,标准库不会提供作为二进制堆实现的功能,二进制堆表示为下面的数组。 它将使用配对堆实现,它在设计上不使用数组——它只是代表一棵树。

它的性能如何?

  • 对于一般随机输入,四元堆会稍微快一些。
  • 但是,它必须基于数组才能更快。 然后,句柄不能简单地基于节点——需要额外的分配。 然后——我们无法合理地更新和删除元素。
  • 它现在的设计方式是,我们有一个易于使用的 API,同时具有非常高性能的实现。
  • 下面有一个配对堆的另一个好处是能够在 O(1) 中合并两个优先级队列,而不是二元/四元堆中的 O(n)。
  • 配对堆仍然非常快。 请参阅参考资料。 有时它比四元堆更快(取决于输入数据和执行的操作,而不仅仅是合并)。

参考

  • Michael L. Fredman、Robert Sedgewick、Daniel D. Sleator 和 Robert E. Tarjan (1986),配对堆:一种新形式的自调整堆算法 1:111-129
  • Daniel H. Larkin、Siddhartha Sen 和 Robert E. Tarjan (2014),优先队列的回归基础实证研究arXiv:1403.0252v1 [cs.DS]。

顺便说一句,这是第一次 API 审查迭代。 需要解决 _open questions_ 的部分(我需要你的意见 [和 API 审查者,如果可能的话])。 如果第一次迭代至少部分通过,在接下来的讨论中,我想创建一个新问题(关闭并参考这个问题)。

据我所知,接口或堆不会通过 API 审查。

@pgolebiowski :那为什么不直接关闭这个问题呢? 没有接口,这个类几乎没用。 我唯一可以使用它的地方是在私有实现中。 此时我可以在需要时创建(或重用)我自己的优先级队列。 我无法在我的代码中使用签名中的这种类型公开公共 API,因为一旦我需要将它换成另一个实现,它就会中断。

@本多诺
好吧,微软在这里说了算,不是几个人在线程上发表评论。 我们知道:

我有点确定其他 API 审阅者/架构师会分享它,因为我已经检查了一些人的意见:名称应该是 PriorityQueue,我们不应该引入 IHeap 接口。 应该只有一个实现(可能通过一些堆)。

这是由@karelz,软件工程师经理,和@terrajobst,项目经理和这个仓库+ API的一些评论者的拥有者。

尽管我显然喜欢使用接口的方法,正如在之前的帖子中明确指出的那样,但鉴于我们在本次讨论中没有太多权力,我可以看到它非常困难。 我们已经提出了自己的观点,但我们只是一些评论者。 无论如何,代码不属于我们。 我们还能做什么?

为什么不直接关闭问题呢? 没有接口,这个类几乎没用。

我已经做的够多了——与其讨厌我的工作,不如做点什么。 做实际工作。 你为什么要责怪我? 责怪自己没有成功地说服别人接受你的观点。

拜托,免得我像那样的花言巧语。 这真的很幼稚——做得更好。

顺便说一句,无论我们是否引入接口,或者类是否命名为PriorityQueueHeap ,该提案都侧重于常见的事情。 因此,专注于这里真正重要的事情,如果您想要某样东西,请向我们展示一些对行动的偏见。

@pgolebiowski当然,这是微软的决定。 但是最好提供一个您想要使用的、满足您需求的 API。 如果它被拒绝,那就这样吧。 我只是认为没有必要对提案进行妥协。

如果您将我的评论理解为责怪您,我深表歉意。 那当然不是我的本意。

@pgolebiowski为什么不使用KeyValuePair<TKey,TValue>作为句柄?

@SamuelEnglard

为什么不使用KeyValuePair<TKey,TValue>作为句柄?

  • 嗯, PriorityQueueHandle其实就是_配对堆节点_。 它公开了两个属性—— TKey KeyTValue Value 。 然而,它内部有更多的逻辑,这只是内部的。 请参考我对此的实现。 例如,它还包含指向树中其他节点的指针(所有内容都在 CoreFX 内部)。
  • KeyValuePair是一个结构体,所以它每次都会被复制+它不能被继承。
  • 但最重要的是PriorityQueueHandle是一个相当复杂的类,它恰好公开了与KeyValuePair相同的公共 API。

@本多诺

但是最好提供一个您想要使用的、满足您需求的 API。 如果它被拒绝,那就这样吧。 我只是认为没有必要对提案进行妥协。

  • 这是真的,我会牢记这一点,看看会发生什么。 @karelz和提案一起,你能不能也通过一些以前的帖子(它们就在提案之前),我们在那里投票支持接口? 我们可能会在 API 审查的第一次迭代之后再回来讨论这个问题。
  • 尽管如此,无论我们采用何种解决方案,其功能都将非常相似,对其进行审查会有所帮助。 因为如果句柄的想法被拒绝并且我们不能真正正确地更新/删除元素(或者我们不能有单独的TKeyTValue ),那么这个类真的几乎没用了 - - 就像现在在 Java 中一样。
  • 特别是,如果我们没有接口,我的 AlgoKit 库将无法使用 CoreFX 为堆提供公共核心,这对我来说真的很可悲。
  • 是的,让我感到惊讶的是,添加界面被微软视为劣势。

那当然不是我的本意。

对不起,那是我的错。

我对设计的问题(免责声明:这些问题和澄清,没有强硬回击(还)):

  1. 我们真的需要PriorityQueueHandle吗? 如果我们只期望队列中的唯一值怎么办?

    • 动机:这似乎是一个相当复杂的概念。 如果我们有它,我想了解为什么我们需要它? 它如何帮助? 或者它只是泄漏到 API 表面的特定实现细节? 它会为我们购买那么多性能来支付 API 中的复杂性吗?

  2. 我们需要Merge吗? 其他馆藏有吗? 我们不应该添加 API,仅仅因为它们易于实现,API 应该有一些通用的用例。

    • 也许只是从IEnumerable<KeyValuePair<TKey, TValue>>添加一些初始化就足够了? + 依靠 Linq

  3. 我们需要comparer重载吗? 我们可以总是回到默认状态吗? (免责声明:在这种情况下我缺少知识/专业知识,所以只是询问)
  4. 我们需要keySelector重载吗? 我认为我们应该决定我们是否希望将优先权作为价值的一部分,还是作为一个单独的东西。 单独的优先级对我来说似乎更自然一些,但我没有强烈的意见。 我们知道利弊吗?

独立/并行决策点:

  1. 类名PriorityQueueHeap
  2. 引入IHeap和构造函数重载?

引入 IHeap 和构造函数重载?

似乎事情已经稳定下来,足以让我投入我的 2 美分......我个人喜欢这个界面。 它以一种在我看来简化结构并实现最大可用性的方式从 API(以及该 API 描述的核心功能)中抽象出实现细节。

我没有那么强烈的意见是我们是否与 PQueue/Heap/ILikeThisItemMoreThanThisItemList 同时进行接口,或者我们是否稍后添加它。 API 可能“不断变化”的论点,因此我们应该首先将其作为一个类发布,直到我们得到反馈,这当然是一个有效的观点,我不同意。 那么问题就变成了何时认为它足够“稳定”以添加接口。 上面的线程IListIDictionary被提到落后于我们很久以前添加的规范实现的 API,那么什么时间段被认为是可接受的休息期?

如果我们能够以合理的确定性定义那个时间段,并确保它不是不可接受的阻塞,那么我认为在没有接口的情况下传输这个庞大的数据结构没有问题。 然后在那段时间之后,我们可以检查使用情况并考虑添加一个接口。

是的,让我感到惊讶的是,添加界面被微软视为劣势。

那是因为在很多方面它都是一个劣势。 如果我们发布一个接口,就差不多完成了。 迭代该 API 的余地并不大,所以我们最好确保它在第一次是正确的,并且在未来很多年都将继续是正确的。 缺少了一些不错的到了功能是一种更好的地方是不是被套牢接口可能不够哪里会哦,所以,很高兴有仅这一项变化不大,这将使这一切更好。

感谢您的投入, @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 应该有一些通用的用例。

同意。 我们不需要它。 我们总是可以添加 API 但不能删除它。 我可以删除此方法并(可能)稍后添加它(如果需要)。

比较器

我们需要比较器重载吗? 我们可以总是回到默认状态吗? (免责声明:在这种情况下我缺少知识/专业知识,所以只是询问)

我认为这很重要,原因有二:

  • 用户希望他们的元素按升序或降序排列。 最高优先级并没有告诉我们应该如何进行排序。
  • 如果我们跳过比较器,我们会强制用户始终为我们提供一个实现IComparable

另外,它与现有的 API 一致。 看看SortedDictionary

选择器

我们需要 keySelector 重载吗? 我认为我们应该决定我们是否希望将优先权作为价值的一部分,还是作为一个单独的东西。 单独的优先级对我来说似乎更自然一些,但我没有强烈的意见。 我们知道利弊吗?

我也喜欢单独的优先级。 除此之外,它更容易实现,更好的性能和内存使用,更直观的 API,用户的工作更少(不需要实现IComparable )。

现在关于选择器...

优点

这就是使这个优先级队列灵活的原因。 它允许用户拥有:

  • 元素及其作为单独元素的优先级
  • 在其中某处具有优先级的元素(复杂类)
  • 检索给定元素的优先级的外部逻辑
  • 实现IComparable元素

它几乎允许我能想到的所有配置。 我觉得它很有用,因为用户可以“即插即用”。 它也相当直观。

缺点

  • 还有更多东西要学。
  • 更多API。 两个额外的构造函数,一个额外的EnqueueUpdate方法。
  • 如果我们决定将元素和优先级分开或合并,我们会强制某些用户(他们的数据采用不同格式)调整他们的代码以使用这种数据结构。

独立/并行决策点

类名PriorityQueueHeap

引入IHeap和构造函数重载。

  • 感觉PriorityQueue应该是 CoreFX 的一部分,而不是Heap
  • 关于IHeap接口——因为优先队列可以用不同于堆的东西来实现,所以我们可能不希望以这种方式公开它。 但是,我们可能需要IPriorityQueue

如果我们发布一个接口,就差不多完成了。 迭代该 API 的余地并不大,所以我们最好确保它在第一次是正确的,并且在未来很多年都将继续是正确的。 缺少一些不错的功能是一个更好的地方,而不是被困在一个潜在的不足的界面中,因为只有这个小小的改变会让一切变得更好。

完全同意!

可比元素

如果我们添加另一个假设:元素必须具有可比性,那么 API 就更简单了。 但同样,它不太灵活。

优点

  • 不需要IComparer
  • 不需要选择器。
  • 一种EnqueueUpdate方法。
  • 一种通用类型而不是两种。

缺点

  • 我们不能将元素和优先级作为单独的对象。 如果用户拥有该格式的新包装类,则需要提供该包装类。
  • 用户总是需要实现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 被使用,我们得到反馈,直到我们真正发布接口,然后我们存储有该版本并且无法在不破坏已经使用它的客户的情况下更改它。

另外关于比较器,我认为它遵循其他 BCL api,我不喜欢用户需要提供新包装类的事实。 我真的很喜欢构造函数重载接收比较器方法并在需要在内部完成的所有比较中使用该比较器,如果没有提供比较器或比较器为空,则使用默认比较器。

@pgolebiowski感谢您提供如此详细和描述性的 API 提案,并在获得批准并将此 API 添加到 CoreFX 上如此积极和精力充沛合并为一条评论并更新顶部的主要问题评论,因为这将使审稿人的生活更轻松。

好的,我对 comparer 深信不疑,它是有道理的,而且是一致的。
我仍然对选择器感到厌烦 - IMO 我们应该尝试不使用它 - 让我们将它分成 2 个变体。

```c#
公共类 PriorityQueue
: IEnumerable,
IEnumerable<(TElement 元素, TPriority 优先级)>,
IReadOnlyCollection<(TElement 元素,TPriority 优先级)>
// ICollection 不是故意包含的
{
公共优先队列();
公共优先队列(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(Func优先级选择器);
公共 PriorityQueue(Func优先级选择器,IComparer比较器);

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

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

}
````

开放问题:

  1. 类名PriorityQueueHeap
  2. 引入IHeap和构造函数重载? (我们应该等一下吗?)
  3. 介绍IPriorityQueue ? (我们要不要等一下—— IDictionary例子)
  4. 使用选择器(存储在值中的优先级)或不使用(5 个 API 差异)
  5. 使用元组(TElement element, TPriority priority)KeyValuePair<TPriority, TElement>

    • PeekDequeue应该使用out参数而不是元组吗?

明天我将尝试由 API 审查小组运行它以获得早期反馈。

PeekDequeue元组字段名称固定为priority (感谢@pgolebiowski指出)。
顶帖更新了上面的最新提案。

我第二个@safern的评论 - 非常感谢@pgolebiowski推动它向前发展!

开放问题:

我想我们可以在这里多一个:

  1. 仅限于唯一元素(不允许重复)。

好点,在顶帖中被捕获为“假设”,而不是开放性问题。 相反会使 API 变得相当丑陋。

我们应该从Remove返回bool Remove吗? 还有out priority arg 的优先级? (我相信我们最近在其他数据结构中添加了类似的重载)

类似于Contains - 我打赌有人会想要优先考虑它。 我们可能想用out priority添加一个重载。

我仍然认为(虽然我还没有表达出来)Heap 意味着一个实现,并且会适当地支持调用这个PriorityQueue 。 (我什至支持一个更精简的Heap类的提案,该类允许重复元素,不允许更新等(更符合原始提案)但我不希望即将发生)。

KeyValuePair<TPriority, TElement>肯定不应该使用,因为预期会出现重复的优先级,而且我认为KeyValuePair<TElement, TPriority>也令人困惑,所以我支持根本不使用KeyValuePair ,或者使用普通元组或优先级参数(我个人喜欢 out params,但我并不大惊小怪)。

如果我们不允许重复,我们需要决定尝试以不同/更改的优先级重新添加它们的行为。

我不支持选择器提案,原因很简单,它意味着冗余,并且会适时地重新造成混乱。 如果一个元素的优先级和它一起存储,那么它将被存储在两个地方,如果它们变得不同步(即有人忘记调用看起来没用的Update(TElement)方法)那么痛苦将确保。 如果选择器是一台计算机,那么我们对有意添加元素的人开放,然后更改计算它们的值,现在如果他们确实尝试重新添加它,则有很多事情可能会出错,具体取决于决定发生这种情况时会发生什么。 在稍微高一点的层面上,尝试虽然可能会导致添加 Element 的更改副本,因为它不再等于以前的副本(这是可变键的普遍问题,但我认为将 Priority 和 Element 分开会有助于避免潜在问题)。

选择器本身很容易改变行为,这是用户可以不假思索地打破一切的另一种方式。 我认为,让用户明确提供优先级要好得多。 我看到的一个大问题是,它表明允许重复条目,因为我们声明的是一对,而不是元素。 但是,明智的在线文档和bool的返回值Enqueue应平凡解决这个问题。 比布尔值更好的方法可能是返回元素的旧/新优先级(例如,如果Enqueue使用新提供的优先级,或两者中的最小值,或旧优先级),但我认为Enqueue如果您尝试重新添加某些内容, bool 。 这使EnqueueUpdate完全独立且定义明确。

我会支持根本不使用 KeyValuePair,或者使用普通元组

我和你一起讨论元组。

如果我们不允许重复,我们需要决定尝试以不同/更改的优先级重新添加它们的行为。

  • 我在Dictionary看到与索引器的相似之处。 在那里,当您执行dictionary["something"] = 5 ,如果"something"是那里的键,它就会更新。 如果它不存在,它只会被添加。
  • 但是, Enqueue方法对我来说类似于字典中的Add方法。 这意味着它应该抛出异常。
  • 考虑到以上几点,我们可以考虑在优先级队列中添加一个索引器来支持您所想到的行为。
  • 但反过来,索引器可能不适用于队列概念。
  • 这导致我们得出结论,如果有人想要添加重复元素, Enqueue方法应该只抛出异常。 同样,如果有人想要更新不存在的元素的优先级,则Update方法应该抛出异常。
  • 这导致我们找到一个新的解决方案——添加TryUpdate方法,它确实返回bool

我不支持选择器提议,原因很简单,它意味着冗余

是不是键没有被物理复制(如果存在),但选择器只是一个函数,在需要评估优先级时被调用? 冗余在哪里?

我认为将 Priority 和 Element 分开将有助于避免潜在问题

唯一的问题是当客户没有物理优先权时。 那时他无能为力。

我认为,让用户明确提供优先级要好得多。 我看到的一个大问题是,它表明允许重复条目,因为我们声明的是一对,而不是元素。

我看到该解决方案存在一些问题,但不一定是为什么它表示允许重复条目。 我认为“无重复”逻辑应该只应用于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投掷对我来说很有意义。 关于Enqueue (而不是IDictionary ),我更多地考虑了ISet思路。 投掷也有意义,我一定错过了这一点,但我确实认为返回bool传达了类型的“固定性”。 也许TryEnqueue也可以(投掷Add )? TryRemove也将受到赞赏(如果为空则失败)。 关于你的最后一点,是的,这也是我想到的行为。 我认为与IDictionary的类比在反思中比ISet更好,这应该足够清楚。 (总而言之:我会支持根据您的建议抛出的所有内容,但如果是这种情况,则必须使用Try* ;我也同意您关于失败条件的陈述)。

关于索引器,我认为你是对的,它并不真正“适合”队列概念,我不支持。 如果有的话,队列或更新的命名方法将是有序的。

是不是键没有被物理复制(如果存在),但选择器只是一个函数,在需要评估优先级时被调用? 冗余在哪里?

你是对的,我误读了提案更新(关于将它存储在 Element 中的一点)。 考虑到 Selector 是按需调用的(必须在任何文档中明确说明,因为它可能会影响性能),但仍然存在这样的观点,即函数的结果可以在没有数据结构响应的情况下发生变化,从而使两个不同步,除非调用Update 。 更糟糕的是,如果用户更改多个元素的有效优先级,并且只更新其中一个,那么当它们从“未更新”元素中选择时,数据结构最终将取决于“未提交”更改(我还没有研究详细介绍了提议的 DataStructure 实现,但我认为这对于任何更新O(<n)都必然是一个问题)。 强制用户显式更新任何优先级可以解决这个问题,必须将数据结构从一种一致状态转换为另一种一致状态。

请注意,到目前为止,我对 Selector 提案的所有抱怨都是关于 API 的健壮性:我认为选择器很容易被错误地使用。 然而,除了可用性之外,从概念上讲,队列中的元素不应该知道它们的优先级。 它可能与他们无关,如果用户最终将他们的元素包装在struct Queable<T>{}或其他东西中,那么这似乎是提供无摩擦 API 的失败(尽管我使用这个术语很伤我)。

你当然可以争辩说,如果没有选择器,调用选择器的责任在于用户,但我认为如果元素知道它们的优先级,它们将(希望)整齐地暴露(即属性),如果它们不t,则无需担心选择器(即时生成优先级等)。 如果您不想要一个选择器,则需要更多无意义的管道来支持它,如果您已经有一个明确定义的“选择器”,则传递优先级。 选择器诱使用户公开这些信息(可能没有帮助),而单独的优先级提供了一个非常透明的界面,我无法想象这会影响设计决策。

我真正想到的支持选择器的唯一论点是,它意味着您可以使用传递PriorityQueue来回传递,并且程序的其他部分可以在不知道先验如何计算的情况下使用它。 我将不得不再考虑一下这个问题,但考虑到“利基”的适用性,在更一般的情况下,这对我来说是相当沉重的开销的一点奖励。

_Edit:再考虑一下,拥有独立的PriorityQueue会非常好,您可以将 Elements 放在其中,但我认为解决这个问题的成本会很高,而引入的成本会大大降低。_

我一直在做一些工作,查看开源 .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
这可以说是我在 .NET 中可以找到的最大的 PriorityQueue 使用示例。 Apache Lucene.net 是流行的 Lucene 搜索引擎库的完整 .net 端口。 我们在我的公司使用 Java 版本,根据 Apache 的网站,一些大牌使用 .NET 版本。 Github 中有大量 .NET 项目的分支。

Lucene 包括它自己的 PriorityQueue 实现,它是由许多“特殊”优先级队列子类化的:HitQueue、TopOrdAndFloatQueue、PhraseQueue 和 SuggestWordQueue。 同样,该项目在许多地方直接实例化了 PriorityQueue。

Lucene 的 PriorityQueue 实现(链接到上面)与本期发布的原始优先级队列 API 非常相似。 它被定义为“PriorityQueue" 并接受一个 IComparer其构造函数中的参数。 方法和属性包括 Count、Clear、Offer(入队/推送)、Poll(出队/弹出)、Peek、Remove(删除队列中找到的第一个匹配项)、Add(Offer 的同义词)。 有趣的是,它们还为队列提供枚举支持。

Lucene 的 PriorityQueue 中的优先级由传入构造函数的 comparer 决定,如果没有传入,则假定被比较的对象实现了 IComparable并使用该接口进行比较。 此处发布的原始 API 设计与此类似,不同之处在于它也适用于值类型。

他们的代码库中有大量使用示例, SloppyPhraseScorer就是其中之一。

SloppyPhraseScorer 的构造函数实例化了一个新的 PhraseQueue (pq),它是他们自己自定义的 PriorityQueue 子类之一。 生成 PhrasePositions 的集合,它似乎是一组帖子、一个职位和一组术语的包装器。 FillQueue 方法枚举短语位置并将它们排入队列。 PharseFreq() 调用 AdvancePP 函数,在较高级别上,它似乎出列、更新项目的优先级,然后再次入队。 优先级是相对确定的(使用比较器)而不是显式确定的(优先级在排队期间不会作为第二个参数“传入”)。

您可以看到,基于他们对PhraseQueue的实现,通过构造函数(例如整数)传入的比较值可能不会削减它。 他们的比较函数(“LessThan”)评估三个不同的字段:PhrasePositions.doc、PhrasePositions.position 和 PhrasePositions.offset。

用法 3:游戏开发
我还没有完成对这个领域的使用示例的搜索,但我看到了很多自定义 .NET PriorityQueue 用于游戏开发的示例。 在非常普遍的意义上,这些往往集中在作为主要用例(Dijkstra 的)的寻路周围。 由于Unity 3D,您可以找到很多人询问如何在 .NET 中实现寻路算法。

仍然需要挖掘这个区域; 看到了一些具有明确优先级的示例和一些使用 Comparer/IComparable 的示例。

另外,有一些关于独特元素、删除显式元素和确定特定元素是否存在的讨论。

队列作为一种数据结构,一般支持入队和出队。 如果我们继续提供其他类似集合/列表的操作,我想知道我们是否真的在设计一个完全不同的数据结构——类似于元组的排序列表。 如果调用者有除入队、出队、窥视以外的需求,也许他们需要优先级队列以外的东西? 队列,顾名思义,意味着插入队列并有序地从队列中移除; 没有别的。

@ebickle

感谢您浏览其他存储库并检查优先队列功能是如何在那里交付的。 但是,IMO 的讨论将受益于提供具体的设计建议。 这条线索已经很难理解了,而且在没有任何结论的情况下讲述一个冗长的故事会让它变得更加困难。

队列 [...] 通常支持入队和出队。 [...] 如果调用者除了 Enqueue、Dequeue、Peek 之外还有其他需求,也许他们需要的不是优先级队列? 队列,顾名思义,意味着插入队列并有序地从队列中移除; 没有别的。

  • 结论是什么? 提议?
  • 这与事实相去甚远。 甚至您提到的 Dijkstra 算法也使用了更新元素的优先级。 更新特定元素所需的内容也需要删除特定元素。

很棒的讨论。 @ebickle的研究对 IMO 非常有用!
@ebickle你对 [2] lucene.net 有结论吗?我们最新的提议是否符合这些用法? (我希望我没有在你的详细描述中遗漏它)

看起来我们需要上面的Try*变体 + IsEmpty + TryPeek / TryDequeue + EnqueueOrUpdate ? 想法?
```c#
public bool IsEmpty();

public bool TryPeek(ref TElement element, ref TPriority priority); // false if empty
public bool TryDequeue(ref TElement element, ref TPriority priority); // false if empty

public bool TryEnqueue(TElement element, TPriority priority); // false if it is duplicate (doe NOT update it)
public void EnqueueOrUpdate(TElement element, TPriority priority); // TODO: Should return bool status for enqueued vs. updated?
public bool TryUpdate(TElement element, TPriority priority); // false if element does not exist (does NOT add it)

public bool TryRemove(TElement element); // false if element does not exist

``

@karelz

看起来我们需要上面的 Try* 变体

对,就是这样。

是否应该为 enqueued 和 updated 返回 bool 状态?

如果我们想在任何地方返回状态,那么无处不在。 对于这个特定的方法,我认为如果任一操作成功( EnqueueUpdate )都应该是正确的。

其余的 - 我只是同意 :smile:

只是一个问题——为什么ref而不是out ? 我不知道:

  • 我们不需要在进入函数之前初始化它。
  • 它不用于方法(它只是熄灭)。

如果我们想在任何地方返回状态,那么无处不在。 对于这个特定的方法,我认为如果任一操作成功( EnqueueUpdate )都应该是正确的。

它总是会返回 true,这不是一个好主意。 在这种情况下,我们应该使用void国际海事组织。 否则人们会感到困惑,并会检查返回值并添加无用的永不执行代码。 (除非我错过了什么)

ref vs. out 同意,我自己辩论过。 我没有强烈的意见/足够的经验来自己做出决定。 我们可以询问 API 审核者/等待更多评论。

它总是会返回 true

你是对的,我的错。 对不起。

我没有强烈的意见/足够的经验来自己做出决定。

也许我遗漏了一些东西,但我觉得这很简单。 如果我们使用ref ,我们基本上是说PeekDequeue想要以某种方式使用传递给它的TElementTPriority (我的意思是阅读这些字段)。 事实并非如此——我们的方法应该只为这些变量赋值(实际上编译器要求它们这样做)。

使用我的Try* API 更新的热门帖子
添加了 2 个未解决的问题:

  • [6] PeekDequeue投掷真的有用吗?
  • [7] TryPeekTryDequeue - 应该使用ref还是out参数?

ref vs. out - 你是对的。 我正在优化以避免初始化,以防我们返回 false。 这对我来说是愚蠢的和盲目的 - 过早的优化。 我将其更改为 out 并删除问题。

6:我可能遗漏了一些东西,但如果不抛出异常,如果队列为空, PeekDequeue该做什么? 我想这开始回避数据结构是否应该接受null (我宁愿不接受,但没有明确的意见)。 即使我们允许null ,值类型也没有nulldefault当然不算),所以PeekDequeue有无法传达无意义的结果,我认为必须抛出异常(从而消除对out参数的任何担忧!)。 我认为没有理由不遵循现有Queue.Dequeue的例子

我只想补充一点,Dijkstra 的(又名。没有启发式的启发式搜索)不需要任何优先级更改。 我从不希望更新任何东西的优先级,而且我一直在抨击启发式搜索。 (算法的重点是,一旦你探索了一个状态,你就知道你已经探索了通往它的最佳路线,否则它就有可能不是最佳的,因此你永远无法提高优先级,你当然不会想要减少它(即考虑更糟糕的路线)。_忽略您故意选择启发式使其不是最佳的情况,在这种情况下,您当然可以获得非最佳结果,但您仍然永远不会更新优先级_)

如果不抛出异常,如果队列为空,Peek 或 Dequeue 应该怎么做?

真的。

从而消除了对输出参数的任何担忧!

如何? 好吧, out参数用于TryPeekTryDequeue方法。 抛出异常的是PeekDequeue

我只想补充一点,Dijkstra 不应该需要任何优先级更改。

我可能错了,但据我所知,Dijkstra 算法使用了DecreaseKey操作。 例如参见这个。 这是否有效是另一个方面。 事实上,斐波那契堆的设计方式是在 O(1) 中渐近地实现DecreaseKey操作(以改进 Dijkstra)。

但是,在我们的讨论中仍然很重要——能够更新优先级队列中的元素非常有帮助,并且有人会搜索这样的功能(请参阅之前有关 StackOverflow 的链接问题)。 我自己也用过几次。

抱歉,是的,我现在看到out参数的问题,又误读了。 并且似乎 Dijkstra 的变体(这个名称的应用似乎比我认为的更广泛......)可以更有效地实现(如果您的队列已经包含元素,大概有利于重复搜索)与可更新的优先级。 这就是我使用长词和名字的结果。 请注意,我不建议我们放弃了Update ,这纯粹是好的也有一个苗条的Heap或其他(即原来的提议)不限制这种能力强加(光荣我很欣赏它的能力)。

7:这些应该是out 。 任何输入都不可能有任何意义,所以不应该存在(即我们不应该使用ref )。 使用out参数,我们不会改进返回default值。 这就是Dictionary.TryGetValue所做的,我认为没有理由不这样做。 _也就是说,您可以将ref视为值或默认值,但是如果您没有有意义的默认值,那么事情就会令人沮丧。_

API审查讨论:

  • 我们需要召开一次

    • 我们可能会邀请大多数投资的社区成员 - @pgolebiowski ,还有其他人吗?

以下是关键的原始笔记:

  • 实验- 我们应该做实验(在 CoreFxLabs 中),将它作为预发布的 NuGet 包发布并询问消费者的反馈(通过博客文章)。 我们不相信我们可以在没有反馈循环的情况下确定 API。

    • 过去我们为ImmutableCollections做了类似的事情(快速预览发布周期是关键,有助于塑造 API)。

    • 我们可以将此实验与 CoreFxLab 中已有的MultiValueDictionary捆绑在一起。 TODO:检查我们是否有更多候选人,我们不想分别在博客上发布每个候选人。

    • 实验结束后,实验性 NuGet 包将被 nuked,我们会将源代码移动到 CoreFX 或 CoreFXExtensions(稍后决定)。

  • 理念:只返回TElementPeekDequeue ,不返回TPriority (用户可以使用Try*方法这一点)。
  • 稳定性(对于相同的优先级)——具有相同优先级的项目应该按照它们插入队列的顺序返回(一般队列行为)
  • 我们要启用重复项(类似于其他集合):

    • 摆脱Update - 用户可以Remove然后Enqueue以不同的优先级返回项目。

    • Remove应该只删除第一个找到的项目(就像List那样)。

  • 想法:我们可以抽象IQueue接口来抽象QueuePriorityQueuePeekDequeue只返回TElement有助于这里)

    • 注意:这可能被证明是不可能的,但我们应该在确定 API 之前探索它

我们对选择器进行了长时间的讨论 - 我们仍然没有决定(可能是上面的IQueue ?)。 大多数人不喜欢选择器,但在未来的 API 审查会议上可能会发生变化。

没有讨论其他开放性问题。

最新的提议对我来说看起来不错。 我的意见/问题:

  1. 如果PeekDequeueout参数用于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以不同的优先级返回项目。

根据实现, Update可能比Remove后跟Enqueue 。 例如,对于二元堆(我认为四元堆具有相同的时间复杂度), Update (具有唯一值和字典)是 O(log n ),而Remove (具有重复值)并且没有字典)是 O( n )。

@pgolebiowski
完全同意我们需要在这里专注于提案; 我做了原始 API 提案和原始帖子。 早在 1 月份, @karelz 就要求提供一些具体的使用示例; 这提出了一个关于 PriorityQueue API 的特定需求和使用的更广泛的问题。 我有自己的 PriorityQueue 实现并在几个项目中使用它,并且觉得在 BCL 中类似的东西会很有用。

我在原帖中缺少的是对现有队列使用情况的广泛调查; 真实世界的例子将有助于保持设计的基础并确保它可以被广泛使用。

@karelz
Lucene.net 至少包含一个优先级队列比较函数(类似于Comparison) 评估多个字段以确定优先级。 Lucene 的“隐式”比较模式不能很好地映射到当前 API 提案的显式 TPriority 参数中。 需要某种映射——将多个字段组合成一个或一个“可比较”的数据结构,可以作为 TPriority 传递。

提议:
1) 优先队列基于我上面的原始提案(列在原始提案标题下)的课程。 可能添加 Update(T)、Remove(T) 和 Contains(T) 便利函数。 适合来自开源社区的大多数现有使用示例。
2)优先队列dotnet/corefx#1 的变体。 Dequeue() 和 Peek() 返回 TElement 而不是元组。 没有 Try* 函数,Remove() 返回 bool 而不是抛出以适合 List图案。 充当“便利类型”,因此具有明确优先级值的开发人员无需创建自己的可比较类型。

这两种类型都支持重复元素。 需要确定我们是否保证相同优先级元素的FIFO; 如果我们支持优先更新,则可能不会。

按照@karelz 的建议在 CoreFxLabs 中构建这两个变体并征求反馈。

问题:

public void Enqueue(TElement element, TPriority priority); // Throws if it is duplicate`
public bool TryEnqueue(TElement element, TPriority priority); // Returns false if it is duplicate (does NOT update it)

为什么没有重复?

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

如果这些方法是可取的,将它们作为扩展方法吗?

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

应该扔; 但为什么没有 Try 变体,还有扩展方法?

基于结构的枚举器?

@benaadams throw on

当我们可以在类型上添加方法时,我们不应该添加方法作为扩展方法。 如果我们不能在类型上添加扩展方法(例如它是接口,或者我们想更快地将其交付给 .NET Framework),则扩展方法是备用的。

Dupes:如果你有多个相同的条目,你只需要更新一个? 然后他们变得不同?

投掷方法:基本上是包装器,它们将是?

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

虽然它的控制流程是通过异常?

@benaadams用 API 审查说明检查我的回复: https :

  • 我们要启用重复项(类似于其他集合):

    • 摆脱Update - 用户可以Remove然后Enqueue以不同的优先级返回项目。

    • Remove应该只删除第一个找到的项目(就像List那样)。

虽然它的控制流程是通过异常?

不明白你的意思。 这似乎是 BCL 中非常常见的模式。

虽然它的控制流程是通过异常?

不明白你的意思。

如果您只有 TryX 方法

  • 如果你关心结果; 你检查一下
  • 如果你不关心结果,你就不会检查它

无异常参与; 但这个名字鼓励你做出扔掉回报的决定。

如果你有非 Try 异常抛出方法

  • 如果你关心结果; 您要么必须预先检查,要么使用 try/catch 来检测
  • 如果你不在乎结果; 你要么必须清空 catch 方法,要么得到意外的异常

因此,您处于对 flow-control 使用异常处理的反模式。 它们似乎有点多余; 总是可以添加一个 throw if false 来重新创建。

这似乎是 BCL 中非常常见的模式。

TryX 方法不是原始 BCL 中的常见模式; 直到并发类型(尽管认为 2.0 中的Dictionary.TryGetValue可能是第一个示例?)

例如,TryX 方法刚刚被添加到队列和堆栈的核心,还不是框架的一部分。

稳定

  • 稳定性不是免费的。 这会带来性能和内存方面的开销。 对于普通用途,优先级队列的稳定性并不重要。
  • 我们公开一个IComparer 。 如果客户想要稳定性,他们可以很容易地自己添加。 在我们的实现之上构建StablePriorityQueue会很容易——使用普通方法存储元素的“年龄”并在比较期间使用它。

队列

那么就会有 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。

偷看和出队

您希望这些方法仅返回TElement

  1. 与您想要实现的目标一起工作。
  2. 不适用于您想要实现的目标 - 返回(TElement, TPriority)

枚举

  1. 客户不能编写任何利用元素优先级的 LINQ 查询。 这是错误的。
  2. 有用。

我们可以通过提供PriorityQueue<T>来缓解一些问题,其中没有物理单独的优先级。

  • 用户本质上被迫为他们的代码编写一个包装类,以便能够使用我们的类。 这意味着在许多情况下,他们还希望实现IComparable 。 它添加了许多相当样板的代码(并且很可能在其源代码中添加了一个新文件)。
  • 如果你愿意,我们显然可以重新讨论这个问题。 我还在下面提供了一种替代方法。

两个优先队列

如果我们提供两个优先级队列,那么整体解决方案将更加强大和灵活。 有几个注意事项:

  • 我们公开两个类以提供相同的功能,但仅适用于各种类型的输入格式。 感觉不是最好。
  • PriorityQueue<T>可能会实现IQueue<T>
  • PriorityQueue<TElement, TPriority>试图实现IQueue接口,它会暴露一个奇怪的 API。

嗯……它会起作用。 虽然不理想。

更新和删除

假设我们希望允许重复并且我们不想使用句柄的概念:

  • 不可能完全正确地提供更新元素优先级的功能(只更新特定节点)。 类似地,它适用于删除元素。
  • 此外,这两个操作都必须在 O(n) 中完成,这很可悲,因为可以在 O(log n) 中完成这两个操作。

这基本上导致了 Java 中的情况,如果用户希望能够更新其优先级队列中的优先级,而不必每次都天真地遍历整个集合,则他们必须提供自己对整个数据结构的实现。

替代手柄

假设我们不想添加新的句柄类型,我们可以做不同的事情,以便能够完全支持更新和删除元素(并有效地做到这一点)。 解决方案是添加替代方法:

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

void Update(object handle, TPriority priority);

void Remove(object handle);

这些方法适用于想要对更新和删除元素进行适当控制的人(不要在 O(n) 中执行此操作,就好像它是List :stuck_out_tongue_winking_eye:)。

但更好的是,让我们考虑......

替代方法

或者,我们可以从优先级队列中完全删除此功能并将其添加到堆中。 这有很多好处:

  • 使用Heap可以获得更强大和更高效的操作。
  • 使用PriorityQueue可以获得简单的 API 和直接的用法——适用于希望他们的代码正常工作的人
  • 我们不会遇到 Java 的问题。
  • 我们现在可以使PriorityQueue稳定——这不再是一种权衡。
  • 该解决方案与具有较强计算机科学背景的人会意识到Heap的存在的感觉相一致。 他们也会意识到PriorityQueue的局限性,因此可以使用Heap代替(例如,如果他们想对更新/删除元素有更多的控制或不想要数据以牺牲速度和内存使用为代价来稳定结构)。
  • 优先队列和堆都可以很容易地允许重复,同时不影响它们的功能(因为它们的目的不同)。
  • 制作一个通用的IQueue接口会更容易——因为功能和功能会被扔到堆字段中。 PriorityQueue的API 可以专注于通过抽象使其与Queue兼容。
  • 我们不再需要提供IPriorityQueue接口(我们更专注于保持PriorityQueueQueue相似)。 相反,我们可以将它添加到堆区域——并且基本上在那里有IHeap 。 这很棒,因为它允许人们在标准库的基础上构建第三方库。 感觉是对的——因为再一次,我们认为优先级队列更先进,因此该区域将提供扩展。 这样的扩展也不会受到我们在PriorityQueue所做的选择的影响,因为它是独立的。
  • 我们不需要考虑IHeap为构造PriorityQueue了。
  • 优先队列将是在 CoreFX 内部使用的有用类。 但是,如果我们添加稳定性等功能并删除一些其他功能,我们最终可能需要比该解决方案更强大的东西。 幸运的是,我们可以使用更强大、更高效的Heap

基本上,优先级队列将主要关注易用性,以牺牲性能、功率和灵活性为代价。 堆将用于那些了解我们在优先级队列中的决定的性能和功能影响的人。 我们通过权衡来缓解许多问题。

如果我们要做一个实验,我认为现在是可能的。 让我们问问社区。 不是每个人都会阅读这个帖子——我们可能会产生其他有价值的评论和使用场景。 你怎么认为? 就个人而言,我会喜欢这样的解决方案。 我想这会让每个人都开心。

重要提示:如果我们想要这样的方法,我们需要将优先级队列和堆设计在一起。 因为它们的目的会有所不同,一种解决方案会提供另一种解决方案所不具备的功能。

然后传送 IQueue

使用上面介绍的方法,为了让优先级队列实现IQueue<T> (这样它才有意义),并且假设我们在那里放弃对选择器的支持,它必须具有一种泛型类型。 尽管这意味着如果用户单独拥有(user data, priority)则需要提供包装器,但这样的解决方案仍然很直观。 最重要的是,它允许所有输入格式(这就是为什么如果我们删除选择器就必须这样做)。 如果没有选择器, Enqueue(TElement, TPriority)将不允许已经具有可比性的类型。 单一的泛型类型对于枚举也很重要——这样这个方法就可以包含在IQueue<T>

各种各样的

@svic

枚举时返回的元素是按什么顺序定义的? 我的猜测是它是未定义的,以使其高效。

要在枚举时获得顺序,我们基本上需要对集合进行排序。 这就是为什么是的,它应该是未定义的,因为客户可以简单地自己运行OrderBy并实现大致相同的性能(但有些人不需要它)。

想法:在优先队列中这可以是有序的,在堆中是无序的。 感觉好多了。 不知何故,优先队列感觉就像按顺序迭代它。 一堆肯定不是。 上述方法的另一个好处。

@pgolebiowski
这一切似乎都很理智。 您介意在Delivering IQueue then部分澄清一下,考虑到允许重复元素,您是否建议将T : IComparable<T>用作“一种通用类型”作为选择器的替代方案?

我会支持有两种不同的类型。

我不明白使用object作为句柄类型背后的基本原理:这只是为了避免创建新类型吗? 定义新类型将提供最小的实现开销,同时使 API 更难被滥用(是什么阻止我尝试将string传递给Remove(object) ?),并且更易于使用(是什么阻止我尝试将 Element 本身传递给Remove(object) ,谁能怪我尝试?)。

我建议添加一个适当命名的虚拟类型,以替换句柄方法中的object ,以实现更具表现力的界面。

如果可调试性胜过内存开销,句柄类型甚至可以包括关于它属于哪个队列的信息(必须变成通用,这将进一步提高接口的类型安全性),提供有用的异常(“提供的句柄被创建由不同的队列”),或者它是否已经被消耗(“由句柄引用的元素已经被删除”)。

如果处理的想法继续下去,我会建议如果这些信息被认为有用,那么这些异常的一个子集也将被相应的TryRemoveTryUpdate方法抛出,除了元素不再存在,要么是因为它已被出列或被句柄删除。 这将需要一个不那么无聊、通用、适当命名的句柄类型。

@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 )中有更多关于此的内容。

我希望现在更清楚了。

顺便说一句,假设有这样的解决方案,设计PriorityQueue<T>IQueue<T>的 API 将是微不足道的。 只需取出Queue<T>一些方法,将它们放入IQueue<T>并让PriorityQueue<T>实现它们。 多啊! 😄

我不明白使用object作为句柄类型背后的基本原理:这只是为了避免创建新类型吗?

  • 是的,正是。 假设我们不想公开这样的类型,这是仍然使用句柄概念的唯一解决方案(因此具有更多的权力和速度)。 我同意这并不理想——而是澄清如果我们坚持当前的方法希望拥有更多的功率和效率(我反对),将会发生什么。
  • 鉴于我们将采用替代方法(单独的优先级队列和堆),这更容易。 我们可以让PriorityQueue<T>驻留在System.Collections.Generic ,而堆功能将驻留在System.Collections.Specialized 。 在那里,我们将有更高的机会引入这样的类型,最终拥有美妙的编译时错误检测。
  • 但是再一次——如果我们想要这样一种方法,那么将优先队列堆功能设计在一起是非常重要的。 因为一种解决方案提供了另一种解决方案所没有的。

如果可调试性胜过内存开销,句柄类型甚至可以包括关于它属于哪个队列的信息(必须变成通用,这将进一步提高接口的类型安全性),提供有用的异常(“提供的句柄被创建由不同的队列”),或者它是否已经被消耗(“由句柄引用的元素已经被删除”)。

如果处理想法继续进行,我会建议如果这些信息被认为有用......

肯定有更多的可能:眨眼:。 特别是通过我们的社区在第三方库中扩展所有这些。

@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
  • 想法:我们也可以在这里添加删除元素的方法( RemoveTryRemove )。 不过,它不在Queue<T> 。 但这不是必需的。

PriorityQueue<T>

public class PriorityQueue<T> : IQueue<T>
{
    public PriorityQueue();
    public PriorityQueue(IComparer<T> comparer);
    public PriorityQueue(IEnumerable<T> collection);
    public PriorityQueue(IEnumerable<T> collection, IComparer<T> comparer);

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

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

    public void Enqueue(T element);

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

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

    public void Remove(T element);
    public bool TryRemove(T element);

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

笔记

  • System.Collections.Generic
  • 如果IComparer<T>未送达,则调用Comparer<T>.Default
  • 它是稳定的。
  • 允许重复。
  • RemoveTryRemove仅删除第一次出现(如果找到)。
  • 枚举是有序的。

现在不在这里写所有内容,但是:

  • System.Collections.Specialized
  • 它不会稳定(因此在内存方面更快更有效)。
  • 处理支持,正确更新和删除

    • 在 O(log n) 而不是 O(n) 中快速完成

    • 正确

  • 枚举不是有序的(更快)。
  • 允许重复。

同意 IQueue提议。 我今天打算推销同样的东西,感觉就像在界面级别拥有正确的抽象级别。 “数据结构的接口,可以将项目添加到其中并从中删除订购的项目。”

  • IQueue 的规范看起来挺好的。

  • 考虑将“int Count { get; }”添加到 IQueue因此无论我们是否从 IReadOnlyCollection 继承,显然都需要 Count.

  • 关于 TryPeek 的围栏,IQueue 中的 TryDequeue考虑到他们不在队列中,但这些助手可能也应该添加到队列和堆栈中。

  • IsEmpty 似乎是一个异常值; BCL 中没有很多其他集合类型有它。 要将其添加到界面中,我们必须假设它将被添加到队列中, 将它添加到 Queue 似乎有点奇怪没有别的。 建议我们从界面中删除它,也许还有类。

  • 删除 TryRemove 并将 Remove 更改为“bool Remove”。 与其他集合类保持一致在这里很重要 - 开发人员将有很多肌肉记忆说“集合中的remove() 不会抛出”。 这是许多开发人员不会很好地测试的领域,如果正常行为发生变化,将会引起很多意外。

来自你之前的引用@pgolebiowski

  1. 用户数据与优先级分开(两个物理实例)。
  2. 用户数据包含优先级。
  3. 用户数据优先。
  4. 罕见情况:优先级可通过其他一些逻辑获得(驻留在与用户数据不同的对象中)。

也建议考虑 5. 用户数据包含多个字段的优先级(正如我们在 Lucene.net 中看到的)

关于 TryPeek 的围栏,IQueue 中的 TryDequeue 考虑到它们不在队列中

它们是System/Collections/Generic/Queue.cs#L253-L295

另一方面 Queue 没有
c# public void Remove(T element); public bool TryRemove(T element);

考虑将“int Count { get; }”添加到 IQueue,这样无论我们是否从 IReadOnlyCollection 继承,显然都需要 Count。

好的。 会修改它。

IsEmpty 似乎是一个异常值; BCL 中没有很多其他集合类型有它。

这个是由@karelz添加的,我只是复制了它。 我喜欢它,可能会在 API 审查中考虑:)

删除 TryRemove 并将 Remove 更改为“bool Remove”。

我认为RemoveTryRemove与类似的其他方法( PeekTryPeekDequeueTryDequeue )。

  1. 用户数据在多个字段中包含优先级

这是一个有效的观点,但实际上这也可以用选择器来处理(毕竟它是任何函数)——但那是针对堆的。

IsEmpty 似乎是一个异常值; BCL 中没有很多其他集合类型有它。

FWIW, bool IsEmpty { get; }是我希望我们添加到IProducerConsumerCollection<T> ,我很遗憾从那以后多次没有出现。 没有它,包装器通常需要做相当于Count == 0事情,这对于某些集合来说实现效率要低得多,尤其是在大多数并发集合上。

@pgolebiowski您对创建一个 github 存储库来托管当前 API 合同和一个或两个包含设计原理的 .md 文件的想法

@svic

如果PeekDequeue使用out参数作为优先级,那么它们也可能有根本不返回优先级的重载,我认为这会简化常见用法

同意。 让我们添加它们。

是否应该有某种方法来仅枚举优先级队列中的项目,而忽略优先级?

好问题。 你会提出什么建议? IEnumerable<TElement> Elements { get; } ?

稳定性 - 我认为这意味着在内部,优先级必须像一对(priority, version)

我认为我们可以通过将Update视为合乎逻辑的Remove + Enqueue来避免这种情况。 我们总是将项目添加到相同优先级项目的末尾(基本上将比较器结果 0 视为 -1)。 应该工作的 IMO。


@贝纳亚当斯

TryX 方法不是原始 BCL 中的常见模式; 直到并发类型(尽管认为 2.0 中的Dictionary.TryGetValue可能是第一个示例?)
例如,TryX 方法刚刚被添加到队列和堆栈的核心,还不是框架的一部分。

我承认我对 BCL 还是个新手。 从 API 审查会议以及我们最近添加了一堆Try*方法的事实中,我的印象是这是一个很长一段时间的常见模式😉。
无论哪种方式,它现在都是常见的模式,我们不应该害怕使用它。 该模式尚未出现在 .NET Framework 中这一事实不应阻止我们在 .NET Core 中进行创新——这是其主要目的,即更快地创新。


@pgolebiowski

或者,我们可以从优先级队列中完全删除此功能并将其添加到堆中。 这有很多好处

嗯,有人告诉我你可能在这里有一个议程 😆
现在说真的,这实际上是我们一直瞄准的好方向 - PriorityQueue从来都不是不做Heap的理由。 如果我们都同意Heap可能不会进入 CoreFX 并且可能“只是”作为高级数据结构与 PowerCollections 一起留在 CoreFXExtensions 存储库中,我对此表示满意,

重要提示:如果我们想要这样的方法,我们需要将优先级队列和堆设计在一起。 因为它们的目的会有所不同,一种解决方案会提供另一种解决方案所不具备的功能。

我不明白为什么我们需要一起做。 IMO 我们可以专注于PriorityQueue并并行/稍后添加“适当的” Heap 。 我不介意有人一起做它们,但我看不出有任何强有力的理由说明为什么存在易于使用的PriorityQueue会影响“适当/高性能”高级Heap家庭。

IQueue

感谢您的撰写。 鉴于您的观点,我认为我们不应该特意添加IQueue 。 IMO 很高兴拥有。 如果我们有一个选择器,那就很自然了。 但是,我不喜欢选择器方法,因为它在描述选择器何时被PriorityQueue调用时带来了奇怪和复杂性(仅在EnqueueUpdate

替代(对象)句柄

这实际上并不是一个坏主意(虽然有点难看)有这样的重载 IMO。 我们必须能够检测到句柄来自错误的PriorityQueue ,即 O(log n)。
我有一种感觉,API 审查者会拒绝它,但 IMO 值得一试......

稳定

我认为稳定性不会带来任何性能/内存开销(假设我们已经删除了Update或者我们将Update视为合乎逻辑的Remove + Enqueue ,所以我们基本上重置元素的年龄)。 只需将比较器结果 0 视为 -1,一切都很好......或者我错过了什么?

选择器和IQueue<T>

有 2 个提案可能是个好主意(我们可能会同时采用这两个提案):

  • PriorityQueue<T,U>没有选择器也没有IQueue (这会很麻烦)
  • PriorityQueue<T>带选择器和IQueue

上面有不少人暗示了这一点。

回复:来自@pgolebiowski 的最新提案 - https://github.com/dotnet/corefx/issues/574#issuecomment -308427321

IQueue<T> - 我们可以添加RemoveTryRemove ,但是Queue<T>没有它们。

将它们添加到Queue<T>是否有意义? ( @ebickle同意)如果是,我们也应该捆绑添加。
让我们将它添加到界面中并指出它们是有问题的/也需要添加Queue<T>
IsEmpty ——无论需要Queue<T>Stack<T>添加,让我们在界面中标记它(这样会更容易查看和消化)。
@pgolebiowski能否请您在IQueue<T>添加评论,并附上我们认为将实施的类列表?

PriorityQueue<T>

让我们添加结构枚举器(我认为这是最近常见的 BCL 模式)。 它在线程上被调用了几次,然后被丢弃/忘记了。

命名空间选择:让我们不要在命名空间决策上浪费时间。 如果Heap最终出现在 CoreFxExtensions 中,我们还不知道我们将在那里允许什么样的命名空间。 也许Community.*或类似的东西。 这取决于 CoreFxExtensions 目的/操作模式讨论结果。
注意:CoreFxExtensions 存储库的一个想法是为经过验证的社区成员提供写入权限,并让它主要由社区驱动,.NET 团队只提供建议(包括 API 审查专业知识),.NET 团队/MS 在需要时充当仲裁者。 如果那是我们着陆的地方,我们可能不希望它在System.*Microsoft.*命名空间中。 (免责声明:早期思考,请不要急于下结论,还有其他替代治理想法正在酝酿中)

删除TryRemove并将Remove更改bool Remove 。 与其他集合类保持一致在这里很重要 - 开发人员将有很多肌肉记忆说“集合中的remove() 不会抛出”。 这是许多开发人员不会很好地测试的领域,如果正常行为发生变化,将会引起很多意外。

👍 我们绝对应该至少考虑将它与其他集合对齐。 有人可以扫描其他收藏,他们的Remove模式是什么?

@ebickle您如何看待创建一个 github 存储库来托管当前 API 合同和一个或两个包含设计原理的 .md 文件的想法。

让我们直接在CoreFxLabs 中托管它。 👍

@karelz

是否应该有某种方法来仅枚举优先级队列中的项目,而忽略优先级?

好问题。 你会提出什么建议? IEnumerable<TElement> Elements { get; } ?

是的,那个或返回某种ElementsCollection的属性可能是最好的选择。 特别是因为它类似于Dictionary<K, V>.Values

我们总是将项目添加到相同优先级项目的末尾(基本上将比较器结果 0 视为 -1)。 应该工作的 IMO。

这不是堆的工作方式,没有“相同优先级项目的结束”。 具有相同优先级的项目可以分布在整个堆中(除非它们接近当前的最小值)。

或者,换句话说,为了保持稳定性,您必须考虑 0 的比较器结果为 -1,不仅在插入时,而且在之后在堆中移动项目时也是如此。 而且我认为您必须将versionpriority一起存储才能正确执行此操作。

队列- 我们可以添加 Remove 和 TryRemove,但是 Queue没有它们。

将它们添加到队列是否有意义? ( @ebickle同意)如果是,我们也应该捆绑添加。

我不认为Remove应该添加到IQueue<T> ; 我什至建议Contains是狡猾的; 它限制了接口的有用性以及它可以用于什么类型的队列,除非您也开始抛出 NotSupportedExceptions。 即范围是什么?

是否仅用于 vanilla 队列、消息队列、分布式队列、ServiceBus 队列、Azure 存储队列、ServiceFabric Reliable Queues 等...(忽略其中一些更喜欢异步方法)

这不是堆的工作方式,没有“相同优先级项目的结束”。 具有相同优先级的项目可以分布在整个堆中(除非它们接近当前的最小值)。

有道理。 我在想它是二叉搜索树。 我想我得刷我的 ds 基础知识:)
好吧,要么我们用不同的 ds(数组、二叉搜索树(或任何正式名称)来实现它 - @stephentoub有想法/建议),要么我们满足于随机顺序。 我不认为维护versionage字段值得保证稳定性。

@ebickle您如何看待创建一个 github 存储库来托管当前 API 合同和一个或两个包含设计原理的 .md 文件的想法。

@karelz让我们直接在 CoreFxLabs 中托管它。

请看一下这个文件

有 2 个提案可能是个好主意(我们可能会同时采用这两个提案):

  • 优先队列没有选择器和 IQueue(这会很麻烦)
  • 优先队列带选择器和 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(); } }

  • 优先队列也应该有一个嵌套的 Enumerator 结构。

  • 我仍然投票支持没有 TryRemove 的“public bool Remove(T element)”,因为它是一个长期存在的模式,改变它很可能会导致开发人员出错。 我们可以让 API 审查人员插话,但在我看来这是一个悬而未决的问题。

  • 在构造函数中指定初始容量或具有 TrimExcess 函数是否有任何价值,或者现在是微优化 - 特别是考虑到 IEnumerable构造函数参数?

@pgolebiowski感谢您将其放入文档中。 您能否将您的文档作为针对 CoreFxLab 主分支的 PR 提交? 标记@ianhays @safern他们将能够合并 PR。
然后我们可以使用 CoreFxLab 问题来进一步讨论特定的设计点——它应该比这个大问题更容易(我刚刚开始发送电子邮件以在那里创建 area-PriorityQueue)。

请在此处放置 CoreFxLab 拉取请求的链接?

  • @ebickle @jnm2 -- 添加
  • @karelz @SamuelEnglard -- 引用

如果 API 以其当前形式(两个类)获得批准,我将创建另一个到 CoreFXLab 的 PR,并使用四元堆实现两者(参见实现)。 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来处理有重复元素的set。 我必须用唯一的 ID 包装元素才能解决问题。
所以,我更喜欢将来在 .NET Core 中使用 Priority Queue,因为它会更方便。

这个问题的现状如何? 我最近发现自己需要一个 PriorityQueue

该提议默认不启用集合初始化。 PriorityQueue<T>没有Add方法。 我认为集合初始化是一个足够大的语言特性,可以保证添加重复方法。

如果有人想要分享一个快速的二进制堆,我想将它与我写的进行比较。
我按照建议完全实现了 API。 但是,它使用简单的排序数组而不是堆。 我将最低价值的项目保留在最后,因此它按照通常顺序的相反顺序进行排序。 当项目数小于 32 时,我使用简单的线性搜索来查找插入新值的位置。 之后,我使用了二进制搜索。 使用随机数据,我发现在填充队列然后排空队列的简单情况下,这种方法比二进制堆更快。
如果我有时间,我会将它放在公共 git 存储库中,以便人们可以批评它并使用位置更新此评论。

@SunnyWar我目前正在使用: https :
它具有良好的性能。 我认为针对GenericPriorityQueue测试您的实现是有意义的

请无视我之前的帖子。 我在测试中发现了一个错误。 一旦修复,二叉堆的性能最好。

@karelz这个提案目前在ICollection<T>什么地方? 我不明白为什么不支持它,即使该集合显然不是只读的?

@karelz关于出队顺序的悬而未决的问题:我认为首先删除最低优先级值。 这对于最短路径算法很方便,对于定时器队列实现也是很自然的。 这些是我认为它将用于的主要两个场景。 我真的不知道相反的论点是什么,除非它是与“高”优先级和“高”数值相关的语言。

@karelz重新枚举顺序...如何在类型上使用Sort()方法,该方法会将集合的内部数据堆就地排序(在 O (n log n) 中)。 并在Sort() Enumerate()之后调用Sort()枚举 O(n) 中的集合。 如果 Sort() 没有被调用,它会以 O(n) 的非排序顺序返回元素?

分类:转向未来,因为似乎没有就我们是否应该实施这一点达成共识。

听到这真是太令人失望了。 我仍然必须为此使用第三方解决方案。

您不喜欢使用第三方解决方案的主要原因是什么?

堆数据结构是处理 leetcode 的必备条件
更多的 leetcode,更多的 c# 代码面试,这意味着更多的 c# 开发人员。
更多的开发者意味着更好的生态系统。
更好的生态系统意味着如果我们明天仍然可以用 c# 编程。

总而言之:这不仅是一个功能,也是未来。 这就是问题被标记为“未来”的原因。

您不喜欢使用第三方解决方案的主要原因是什么?

因为当没有标准时,每个人都会发明自己的标准,每个人都有自己的一套怪癖。

有很多次我希望得到System.Collections.Generic.PriorityQueue<T>因为它与我正在做的事情有关。 这个提议已经存在 5 年了。 为什么还没有发生?

为什么还没有发生?

优先饥饿,没有双关语。 设计集合是可行的,因为如果我们将它们放在 BCL 中,我们必须考虑它们如何交换、它们实现什么接口、语义是什么等等。这些都不是火箭科学,但需要一段时间。 此外,您需要一组具体的用户案例和客户来判断设计,随着集合变得越来越专业,这变得越来越困难。 到目前为止,一直有其他工作被认为比这更重要。

让我们看一个最近的例子:不可变集合。 它们是由不在 BCL 团队工作的人设计的,用于 VS 中围绕不变性的用例。 我们与他合作使 API“BCL 化”。 当 Roslyn 上线时,我们在尽可能多的地方用我们的副本替换了他们的副本,并根据他们的反馈对设计(和实施)进行了大量调整。 没有“英雄”场景,这很难。

因为当没有标准时,每个人都会发明自己的标准,每个人都有自己的一套怪癖。

@masonwheeler这就是你看到的PriorityQueue<T>吗? 有几个 3rd 方选项不可互换并且没有明确接受的“最适合大多数人的最佳库”? (我没有做过研究所以我不知道答案)

@eiriktsarpalis如何就是否实施达成共识?

@terrajobst当这是具有知名应用程序的知名模式时,您真的需要英雄场景吗? 这里应该不需要什么创新。 甚至已经有了一个相当完善的接口规范。 在我看来,这个实用程序不存在的真正原因不是它太难,也不是没有需求或用例。 真正的原因是,对于任何真正的软件项目来说,自己构建一个比尝试发起政治运动以将其放入框架库更容易。

您不喜欢使用第三方解决方案的主要原因是什么?

@terrajobst
我个人的经验是 3rdparty 解决方案并不总是按预期执行/不使用当前的语言功能。 使用标准化版本,我(作为用户)可以非常确定性能是您可以获得的最佳性能。

@danmosemsft

@masonwheeler这就是你看到的PriorityQueue<T>吗? 有几个 3rd 方选项不可互换并且没有明确接受的“最适合大多数人的最佳库”? (我没有做过研究所以我不知道答案)

是的。 只需谷歌“C# 优先级队列”; 第一页充满:

  1. Github 和其他托管站点上的优先队列实现
  2. 人们问为什么世界上在 Collections.Generic 中没有官方的优先队列
  3. 关于如何构建自己的优先级队列实现的教程

@terrajobst当这是具有知名应用程序的知名模式时,您真的需要英雄场景吗? 这里应该不需要什么创新。

根据我的经验是的。 细节决定成败,一旦我们发布了 API,我们就无法真正进行重大更改。 并且有许多用户可见的实现选择。 我们可以将 API 作为 OOB 预览一段时间,但我们也了解到,虽然我们当然可以收集反馈,但缺少英雄场景意味着您没有任何决胜局,这通常会导致英雄场景到达,数据结构不满足其要求。

@masonwheeler我以为你的链接会指向这个😄 现在它在我的脑海中。

虽然正如@terrajobst所说,我们在这里的主要问题是资源/注意力(​​如果你愿意,你可以责怪我们)我们也想加强非微软生态系统,这样你更有可能找到强大的库来做任何事情设想。

[为清晰起见编辑]

@danmosemsft 不,如果我要选择那首歌,我会选择 Shrek 2 版本。

英雄应用候选 #1:在 TimerQueue.Portable 中使用一个怎么样?

已经被考虑、设计原型并被丢弃。 它降低了快速创建和销毁(例如超时)计时器的非常常见的情况。

@stephentoub我假设您的意思是在某些计时器数量较少的情况下效率较低。 但它是如何扩展的?

我假设您的意思是在某些计时器数量很少的情况下效率较低。 但它是如何扩展的?

不,我的意思是常见的情况是您在任何时候都有很多计时器,但很少有人在触发。 这就是将计时器用于超时的结果。 重要的是您可以在数据结构中添加和删除的速度……您希望它是 0(1) 并且开销非常低。 如果这变成 O(log N),那就是一个问题。

拥有优先队列肯定会让 C# 对面试更友好。

拥有优先队列肯定会让 C# 对面试更友好。

是的,这是真的。
出于同样的原因,我正在寻找它。

@stephentoub对于从未真正发生过的超时,这对我来说非常有意义。 但是我想知道当突然发生很多超时时系统会发生什么,因为突然出现数据包丢失或服务器无响应或其他什么? 重复计时器 ala System.Timer 也使用相同的实现吗? 在那里,超时到期将是“快乐之路”。

但是我想知道当突然很多超时确实开始发生时系统会发生什么

尝试一下。 :)

我们看到了大量实际工作负载因之前的实现而受到影响。 在我们看过的每一种情况下,新的(不使用优先级队列,而是在很快和不很快的计时器之间进行简单的划分)都解决了这个问题,我们还没有看到新的它。

重复计时器 ala System.Timer 也使用相同的实现吗?

是的。 但它们通常分布在实际应用中,而且通常相当均匀。

5年后,仍然没有PriorityQueue。

5年后,仍然没有PriorityQueue。

一定不是一个足够高的优先级......

@stephentoub但也许实际上是@eiriktsarpalis我们现在真的对这个有任何吸引力吗? 如果我们有一个最终的 API 设计,我愿意为它工作

我还没有看到已经解决的声明,我不确定是否可以有没有指定杀手级应用程序的最终 api 设计。 但...
假设顶级杀手级应用程序候选人是编程竞赛/教学/面试,我认为 Eric 的顶级设计看起来足够有用......而且我仍然有我的反对意见(新修订,仍然没有撤回!)

https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

我注意到它们之间的主要区别是它是否应该像字典一样说话,以及选择器是否应该是一个东西。

我开始忘记我为什么要把它变成一本字典。 拥有一个用于更新优先级的索引分配器,并且能够枚举具有优先级的键,这对我来说似乎很好,而且非常像字典。 能够在调试器中将其可视化为字典也很不错。

我认为选择器实际上会彻底改变预期的类接口 FWIW。 选择器应该是在构建时就已经内置的东西,如果你有它,它就不需要任何人向其他人传递优先级值——如果他们想知道优先级,他们应该只调用选择器。 因此,除了选择器之外,您根本不想在任何方法签名中使用优先级参数。 所以在这种情况下,它变成了一种更专业的“PrioritySet”类。 [对于哪些不纯的选择器可能是一个错误问题!]

@TimLovellSmith我理解对这种行为的渴望,但是因为这是一个 5 年的问题,仅使用选择器实现基于数组的二进制堆并与Queue.cs共享相同的 API 表面区域是否合理? PriorityQueue我相信我们应该模仿Queue的 api 设计

@Jlalond因此这里的提议,是吧? 我不反对基于堆的数组实现。 我现在回想起我对选择器的最大反对意见,就是正确地进行优先级更新并不容易。 一个普通的旧队列没有优先级更新操作,但更新元素优先级是许多优先级队列算法的重要操作。
如果我们使用不需要调试损坏的优先级队列结构的 API,人们应该永远心存感激。

@TimLovellSmith对不起,Tim,我应该澄清IDictionary的继承。

我们是否有优先级会改变的用例?

我喜欢你的实现,但我认为我们可以减少 IDictionary Behavior 的复制。 我认为我们不应该从IDictionary<>继承,而应该只继承 ICollection,因为我不认为字典访问模式是直观的,

但是,我确实认为返回 T 及其关联的优先级是有意义的,但是我不知道在将数据结构中的元素入队或出队时需要知道该元素的优先级的用例。

@Jlalond
如果我们有一个优先级队列,那么它应该支持它简单有效地可以的所有操作,_并且_
熟悉这种数据结构/抽象的人“期望”在那里。

更新优先级属于 API,因为它属于这两个类别。 更新优先级在很多算法中都是足够重要的考虑因素,使用堆数据结构进行操作的复杂度会影响整个算法的复杂度,并且会定期测量,参见:
https://en.wikipedia.org/wiki/Priority_queue#Specialized_heaps

TBH 主要是它的decrease_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 的大小增加,它们几乎变得无关紧要。

明白了,主要是想知道 Tim 是否有任何令人兴奋的想法要做
一个操作:)

2020 年 8 月 18 日,星期二,23:51 masonwheeler [email protected]写道:

@TimLovellSmith https://github.com/TimLovellSmith有道理,但是
实际上不会有任何优先级更新 ne O2 * (log n),因为我们需要
删除元素然后重新插入它? 基于我的假设
是一个二元堆

在复杂性分析中通常会忽略像 2 这样的常数因素
因为随着 N 的大小增加,它们几乎变得无关紧要。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/dotnet/runtime/issues/14032#issuecomment-675887763
或取消订阅
https://github.com/notifications/unsubscribe-auth/AF76XTI3YK4LRUVTOIQMVHLSBNZARANCNFSM4LTSQI6Q
.

@Jlalond
没有新意,我只是忽略了一个常数因子,但减少也不同于增加

如果优先级更新是减少,您不必删除它并重新添加它,它只是冒泡,所以它的 1 * O(log n) = O(log n)。
如果优先级更新是增加,您可能必须删除它并重新添加它,所以它的 2 * O(log n) = 仍然是 O (log n)。

其他人可能发明了比 remove + readd 更好的数据结构/算法来提高优先级,但我没有尝试找到它,O(log n) 似乎已经足够了。

KeyValuePair在它还是字典时悄悄进入界面,但在字典删除后幸存下来,主要是为了可以迭代_元素的集合及其优先级_。 并且这样您就可以根据优先级将元素出列。 但也许为了简单起见,具有优先级的出队应该只在出队 API 的“高级”版本中。 (尝试出队:D)

我会做出这样的改变。

@TimLovellSmith Cool,我期待您的修订提案。 我们可以提交审查,以便我可以开始工作吗? 此外,我知道配对队列有一些动力可以改善合并时间,但我仍然认为基于数组的二进制堆将是最全面的性能。 想法?

@Jlalond
我做了一些小改动以简化 Peek + Dequeue api。
一些 ICollection与 KeyValuePair 相关的 apis 仍然存在,因为我没有看到任何明显更好的替换它们。
相同的公关链接。 还有其他方式我应该提交它以供审查吗?

https://github.com/TimLovellSmith/corefxlab/blob/priorityQueueSpecSolved/docs/specs/priority-queue.md

我不介意它使用哪种实现,只要它与基于数组的堆一样快或更好。

@TimLovellSmith我实际上对 API 审查一无所知,但我想@danmosemsft可以为我们指明正确的方向。

我的第一个问题是什么会是? 我假设它是一个 int 或浮点数,但对我来说,声明队列“内部”的数字对我来说似乎很奇怪

我实际上更喜欢二元堆,但想确保我们都可以朝那个方向前进

@eiriktsarpalis @layomia区域所有者,必须通过 API 审查来管理它。 我没有关注讨论,我们达成共识了吗?

正如我们过去所讨论的,核心图书馆并不是所有馆藏的最佳场所,尤其是那些固执己见和/或小众的馆藏。 例如,我们每年都发货——我们不能像图书馆那样快速移动。 我们的目标是围绕收藏包建立一个社区来填补这一空白。 但是 84 人对此表示赞同,这表明核心库中对此有广泛的需求。

@Jlalond抱歉,我不太明白第一个问题应该是什么。 你问为什么 TPriority 是通用的? 还是必须是数字? 我怀疑很多人会使用非数字优先级类型。 但他们可能更喜欢使用 byte、int、long、float、double 或 enum。

@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的命名有点出乎意料:我原以为KeyComparerPriorityComparer
  • 我不明白关于CopyToToArray复杂性的注释。

@VisualMelon
非常感谢您的点评!

我喜欢 KeyComparer 的命名建议。 降低优先级 API 听起来非常有用。 我们添加一个或两个 API 怎么样。 由于我们在“最低优先级优先”模型中使用,您会只使用减少吗? 或者你也希望增加?

    public void EnqueueOrIncreasePriority(TKey key, TPriority priority); // doesn't throw, only updates priority of keys already in the collection if new priority is higher
    public void EnqueueOrDeccreasePriority(TKey key, TPriority priority); // doesn't throw, only updates priority of keys already in the collection new priroity is lower

我认为枚举首先返回最少元素的原因是与 Peek() 返回集合前面元素的心理模型一致。 如果它是一个二进制堆,也不需要比较来实现它,所以它看起来像一个免费赠品。 但也许它没有我想象的那么自由——毫无根据的复杂性? 因为这个原因,我很高兴把它拿出来。

两次入队是偶然的。 我们确实有 EnqueueOrUpdate,其中优先级总是得到更新。 我猜 TryUpdate 是逻辑的反面,永远不应该更新集合中已有项目的优先级。 我可以看到它填补了逻辑上的空白,但我不确定它是否是人们在实践中想要的 API。

我只能想象使用递减变体:它是路由查找中的一种自然操作,希望将节点加入队列或将现有节点替换为具有较低优先级的节点; 我不能立即建议相反的用途。 如果您需要在项目排队/未排队时执行任何操作,则boolean返回参数会很方便。

我没有假设一个二元堆,但如果它是免费的,那么第一个元素总是最小的我没有问题,这似乎是一个奇怪的约束。

@VisualMelon @TimLovellSmith一个 API 可以吗?
EnqueueOrUpdatePriority ? 我想象在队列中重新定位节点的能力对于遍历算法是有价值的,即使它增加或减少优先级

@Jlalond是的,我提到它是因为它可能有助于提高效率,具体取决于实现,并且直接编码了仅希望对某些内容进行排队的用例,如果它改进了您在队列中已有的内容。 我不会假设额外的复杂性是否适合这种通用工具,但这当然不是必需的。

更新以移除 EnqueueOrIncreasePriority,保留 EnqueueOrUpdate 和 EnqueueOrDecrease。 让它们在新添加到集合中的项目时返回 bool,例如 HashSet.Add()。

@Jlalond我为上述错误道歉(删除您询问何时审查此问题的最新评论)。

我只想提一下@eiriktsarpalis将在下周回来,然后我们将讨论优先考虑此功能并审查 API。

这是我们正在考虑但尚未针对 .NET 6 提交的内容。

尽管我们在 2017 年对这个话题进行了几个月的深入讨论(这个巨大的线程的大部分)并共同产生了这个提案——滚动上面查看。 这也是@karelz在第一篇帖子第一行链接的提议:

请参阅 corefxlab 存储库中的最新提案

截至 2017 年 6 月 16 日,我们正在等待从未发生过的 API 审查,状态为:

如果 API 以其当前形式(两个类)获得批准,我将创建另一个到 CoreFXLab 的 PR,并使用四元堆实现两者(参见实现)。 PriorityQueue<T>将只使用下面的一个数组,而PriorityQueue<TElement, TPriority>将使用两个 -- 并将它们的元素重新排列在一起。 这可以为我们节省额外的分配,并尽可能提高效率。 一旦有绿灯,我会补充一点。

我建议继续迭代我们在 2017 年提出的提案,从尚未进行的正式审查开始。 这将使我们能够避免在社区成员交换了数百个帖子之后在提案的背后进行分叉和迭代,这也是出于对所有相关人员为此付出的努力的尊重。 如果有反馈,我很乐意讨论。

@pgolebiowski感谢您回到讨论中。 我想为有效地进一步分叉提案而道歉,这不是最有效的协作方式。 这对我来说是一个冲动的举动。 (我的印象是,由于提案中存在太多悬而未决的问题,讨论完全陷入停滞,只需要一个更有主见的提案。)

如果我们尝试向前推进,至少暂时忘记我的 PR 的代码内容,并在此处继续讨论已确定的“未解决的问题”如何? 我想就他们所有人发表我的看法。

  1. 类名 PriorityQueue 与 Heap <-- 我认为已经讨论过并且一致认为 PriorityQueue 更好。

  2. 引入 IHeap 和构造函数重载? <-- 我没有预见到很多价值。 我认为对于世界上 95% 的人来说,没有一个令人信服的理由(例如性能增量)有多个堆实现,另外 5% 的人可能不得不自己编写。

  3. 引入IPriorityQueue? <-- 我也不认为它需要。 我假设其他语言和框架在它们的标准库中没有这些接口的情况下相处得很好。 让我知道这是否不正确。

  4. 使用选择器(存储在值中的优先级)或不使用(5 个 API 差异)<-- 我没有看到支持优先级队列中的选择器的有力论据。 我觉得IComparer<>已经作为一个非常好的用于比较项目优先级的最小可扩展性机制,并且选择器没有提供任何显着的改进。 此外,人们可能会对如何使用具有可变优先级的选择器(或如何不使用它们)感到困惑。

  5. 使用元组(TElement 元素,TPriority 优先级)与 KeyValuePair<-- 我个人认为KeyValuePair是优先级队列的首选选项,其中项目具有可更新的优先级,因为这些项目被视为集合/字典中的键。

  6. Peek 和 Dequeue 应该使用 out 参数而不是 tuple 吗? <-- 我不确定所有的利弊,尤其是性能。 我们应该选择在简单的基准测试中表现更好的那个吗?

  7. Peek 和 Dequeue 投掷有用吗? <-- 是的......对于错误地假设队列中仍有项目的算法应该发生什么。 如果您不想让算法假设,最好的办法就是根本不提供 Peek 和 Dequeue api,而只提供 TryPeek 和 TryDequeue。 [我猜有一些算法可以证明在某些情况下可以安全地调用 Peek 或 Dequeue,而 Peek/Dequeue 对这些算法来说是一个轻微的性能和可用性改进。]

除此之外,我还有一些建议可以考虑添加到正在考虑的提案中:

  1. 优先队列应该只适用于独特的物品,它们是它们自己的句柄。 它应该支持自定义 IEqualityComparer,以防它想以特定方式比较不可扩展的对象(如字符串)以进行优先级更新。

  2. 优先队列应该支持各种操作,如“删除”、“如果较小则降低优先级”,尤其是“如果较小的操作则将项目入队或降低优先级”,所有这些都在 O(log n) 中 - 用于高效且简单地实现图搜索场景。

  3. 如果 PriorityQueue 对懒惰的程序员来说会很方便为获取/设置现有项目的优先级提供索引运算符 []。 对于获取,它应该是 O(1),对于集合来说应该是 O(log n)。

  4. 最小的优先级是最好的。 因为优先队列用于很多优化问题,具有“最小成本”。

  5. 枚举不应提供任何排序​​保证。

未解决的问题 - 处理:
优先队列项目和优先级似乎旨在允许使用重复值。 这些值要么需要被认为是“不可变的”,要么必须有一种方法可以在项目优先级发生变化时调用以通知集合,可能带有句柄,因此它可以具体说明哪些重复项的优先级发生了变化(什么场景需要做这??)...我怀疑所有有用的都是,这是否是一个好主意,但是如果我们在这样的集合中有更新优先级,那么如果该方法产生的成本可以懒惰地产生,那可能会很好,所以当你只是在处理不可变的项目,不想使用时,没有额外的成本......如何解决? 是否可以通过重载使用句柄的方法和不使用句柄的方法? 或者我们不应该有任何不同于项目本身的项目句柄(用作查找的哈希键)?

为了帮助加快速度,将这种类型移动到https://github.com/dotnet/runtime/discussions/42205中讨论的“社区集合”库可能是有意义的

我同意最小的优先级是最好的。 优先级队列在回合制游戏的开发中也很有用,并且能够将优先级设置为每个元素何时进入下一个回合的单调递增计数器非常有用。

我们正在考虑尽快将其纳入 API 审查(尽管我们尚未承诺在 .NET 6 时间范围内交付实施)。

但在我们将其送交审查之前,我们需要解决一些高层次的开放性问题。 我觉得@TimLovellSmith写起来是起点朝着实现这一目标的好。

关于这些点的一些评论:

  • 关于问题 1-3 的共识早已建立,我认为我们可以将这些视为已解决。

  • _使用选择器(存储在值中的优先级)与否_ -- 同意您的评论。 该设计的另一个问题是选择器是可选的,这意味着您需要小心不要调用错误的Enqueue重载(或冒着获得InvalidOperationException风险)。

  • 我喜欢在KeyValuePair使用元组。 使用.Key属性访问TPriority值对我来说感觉很奇怪。 元组允许您使用.Priority ,它具有更好的自文档属性。

  • _Peek 和 Dequeue 应该使用 out 参数而不是元组吗?_ - 我认为我们应该遵循其他地方类似方法中的既定约定。 所以可能使用out参数。

  • _Peek 和 Dequeue 投掷是否有用?_ -- 100% 同意您的意见。

  • _最小的优先级是最好的_ -- 同意。

  • _枚举不应该提供任何排序​​保证_ -- 这可能不会违反用户的期望吗? 有哪些取舍? 注意,我们可能会推迟这样的细节以进行 API 审查讨论。

我还想重新表述一些其他开放性问题:

  • 我们是否运送PriorityQueue<T>PriorityQueue<TElement, TPriority>或两者? -- 我个人认为我们应该只实施后者,因为它似乎提供了一个更清洁和更通用的解决方案。 原则上优先级不应该是排队元素的固有属性,因此我们不应该强迫用户将其封装在包装器类型中。

  • 我们是否需要元素唯一性,直到相等? -- 如果我错了,请纠正我,但我觉得唯一性是一种人为的约束,是由于需要支持更新场景而不诉诸处理方法而迫使我们这样做的。 这也使 API 表面变得复杂,因为我们现在还需要担心元素是否具有正确的相等语义(当元素是大型 DTO 时,适当的相等是什么?)。 我可以在这里看到三种可能的路径:

    1. 要求唯一性/相等性,并通过传递原始元素(直至相等性)来支持更新场景。
    2. 不需要唯一性/相等性并支持使用句柄更新场景。 这些可以使用可选的Enqueue方法变体为需要它们的用户获得。 如果句柄分配是一个足够大的问题,我想知道这些是否可以在 ValueTask 中摊销?
    3. 不要求唯一性/相等性,也不支持更新优先级。
  • 我们是否支持合并队列? -- 共识似乎是否定的,因为我们只发布了一种合并效率不高的实现(大概使用数组堆)。

  • 它应该实现哪些接口? -- 我看到一些建议推荐IQueue<T> ,但这感觉像是一个有漏洞的抽象。 我个人更喜欢保持简单,只实现ICollection<T>

抄送@layomia @safern

@eiriktsarpalis相反 - 支持更新的约束完全没有人为!

具有优先级队列的算法经常更新元素的优先级。 问题是,您是否提供了一个面向对象的 API,它“仅适用于”更新普通对象的优先级……还是您强迫人们
a) 了解手柄模型
b)在内存中保留额外的数据,例如额外的属性或外部字典,以跟踪对象的句柄,在他们这边,这样他们就可以进行更新(对于他们不能改变的类,必须使用字典?或将对象升级为元组等)
c) 使用字典方法时,“内存管理”或“垃圾收集”外部结构,即清理不再在队列中的项目的句柄
d) 不要将上下文中的不透明句柄与多个队列混淆,因为它们仅在单个优先级队列的上下文中有意义

另外,关于任何人都希望_保持按优先级跟踪对象的队列以这种方式行事的整个原因,还有一个哲学问题:为什么同一个对象(等于返回真)应该有两个_不同的_优先级? 如果它们真的应该有不同的优先级,为什么不将它们建模为不同的对象? (或向上转换为元组,带有区分符?)

同样对于句柄,优先级队列中必须有一些内部句柄表,这样句柄才能实际工作。 我认为这与保留字典以查找队列中的对象是等效的工作。

PS:每个 .net 对象都已经支持相等/唯一性的概念,因此“要求”它并不是一个很大的要求。

关于KeyValuePair ,我同意它并不理想(尽管在提案中, Key用于元素, Value用于优先级,这与各种Sorted一致BCL 中的struct而不是公共 API 上任何地方的元组,尤其是对于输入。

关于唯一性,这是一个基本问题,我认为在此之前无法决定其他任何事情。 如果目标是单个易于使用和通用的 API,我会支持由(可选)比较器定义的元素唯一性(根据现有提案和建议 i)。 唯一与非唯一是一个很大的分歧,我将这两种类型用于不同的目的。 前者“更难”实现,涵盖了最(也是更典型的)用例(只是我的经验),同时更难被误用。 那些 _require_ 非唯一性应该 (IMO) 由不同类型(例如,普通的旧二进制堆)提供服务的用例,我很感激两者都可用。 这本质上是@pgolebiowski链接的原始提案提供的(据我所知)模一个(简单的)包装器。 _编辑:不,那不支持绑定的优先级_

相反 - 支持更新的约束完全没有人为!

对不起,我并不是要表明对更新的支持是人为的; 而是人为地引入了对唯一性的要求以支持更新。

PS:每个 .net 对象都已经支持相等/唯一性的概念,所以“要求”它并不是一个很大的要求

当然,但有时类型附带的相等语义可能不是理想的(例如引用相等、完整的结构相等等)。 我只是指出平等是困难的,并且将它强加到设计中会带来一类全新的潜在用户错误。

@eiriktsarpalis感谢您澄清这一点。 但这真的是人造的吗? 我不认为是。 它只是另一种自然的解决方案。

api 必须是 _well-defined_。 如果不要求用户具体说明他们想要更新的内容,您就无法提供更新 API。 句柄和对象相等只是构建定义良好的 API 的两种不同方法。

句柄方法:每次向集合中添加对象时,都必须给它一个“名称”,即“句柄”,以便您可以在稍后的对话中准确地引用该对象,而不会产生歧义。

对象唯一性方法:每次向集合中添加对象时,它必须是不同的对象,或者必须指定如何处理对象已经存在的情况。

不幸的是,只有对象方法真正允许您支持一些最有用的高级 API 方法属性,例如 'EnqueueIfNotExists' 或 'EnqueueOrDecreasePriroity(item)' - 在以句柄为中心的设计中提供它们是没有意义的,因为您无法知道该项目是否已存在于队列中(因为您的工作是使用句柄跟踪该项目)。

对 handle 方法最有说服力的批评之一,或者将唯一性约束丢给我,是它使具有可更新优先级的各种场景的实现变得更加复杂:

例如

  1. 使用优先队列对于表示消息/情绪/标签/用户名的字符串,这些字符串得到了upvoted/scoreupdated,唯一值具有不断变化的优先级
  2. 使用优先队列, double> 对唯一元组进行排序 [无论它们是否有优先级更改] - 必须在某处跟踪额外的句柄
  3. 使用优先队列要优先考虑图形索引或数据库对象 ID,现在您必须在您的实现中添加句柄

聚苯乙烯

当然,但有时类型附带的相等语义可能不是理想的

应该有逃生舱口,如 IEqualityComparer,或向上转换为更丰富的类型。

感谢您的反馈🥳 将在周末更新提案,考虑到所有新的输入,并分享新一轮的新修订。 预计到 2020 年 9 月 20 日。

优先队列提案(v2.0)

概括

.NET Core 社区建议向系统库中添加 _priority queue_ 功能,这是一种数据结构,其中每个元素还具有与其关联的优先级。 具体来说,我们建议将PriorityQueue<TElement, TPriority>System.Collections.Generic命名空间。

信条

在我们的设计中,我们遵循以下原则(除非您知道更好的原则):

  • 覆盖面广。 我们希望为 .NET Core 客户提供一种有价值的数据结构,该结构足够灵活以支持各种用例。
  • 从已知错误中学习。 我们努力提供优先队列功能,该功能不会存在其他框架和语言(例如 Java、Python、C++、Rust)中存在的面向客户的问题。 我们将避免做出已知会让客户不满意并降低优先队列的有用性的设计选择。
  • 对单向门的决定极为谨慎。 API一旦引入,就不能修改或删除,只能扩展。 我们将仔细分析设计选择,以避免我们的客户将永远陷入困境的次优解决方案。
  • 避免设计瘫痪。 我们接受可能没有完美的解决方案。 我们将权衡取舍并继续交付,最终为我们的客户提供他们期待已久的功能。

背景

从客户的角度

从概念上讲,优先级队列是元素的集合,其中每个元素都有一个关联的优先级。 优先级队列最重要的功能是它提供对集合中具有最高优先级的元素的有效访问,以及删除该元素的选项。 预期的行为还可能包括:1) 能够修改集合中已有元素的优先级; 2) 能够合并多个优先级队列。

计算机科学背景

优先级队列是一种抽象的数据结构,即它是一个具有某些行为特征的概念,如上一节所述。 优先级队列的最有效实现是基于堆的。 然而,与一般的误解相反,堆也是一种抽象的数据结构,可以通过多种方式实现,每种方式都有不同的优点和缺点。

大多数软件工程师只熟悉基于数组的二进制堆实现——它是最简单的一种,但不幸的是不是最有效的一种。 对于一般随机输入,更有效的堆类型的两个示例是:四元堆配对堆。 有关堆的更多信息,请参阅维基百科这篇论文

更新机制是关键的设计挑战

我们的讨论表明,设计中最具挑战性的领域,同时也是对 API 影响最大的领域是更新机制。 具体而言,挑战在于确定我们希望为客户提供的产品是否以及如何支持更新集合中已有元素的优先级。

这种能力对于实现例如 Dijkstra 的最短路径算法或需要处理不断变化的优先级的作业调度程序是必要的。 Java 中缺少更新机制,这对工程师来说是令人失望的,例如在这三个 StackOverflow 问题中查看了超过 32k 次: example , example , example 。 为了避免引入价值如此有限的 API,我们认为我们提供的优先级队列功能的一个基本要求是支持更新集合中已经存在的元素的优先级的能力。

为了提供更新机制,我们必须确保客户可以明确他们想要更新的内容。 我们已经确定了两种交付方式:a) 通过句柄; b) 通过强制集合中元素的唯一性。 每一个都有不同的好处和成本。

选项 (a):手柄。 在这种方法中,每次将元素添加到队列时,数据结构都会提供其唯一的句柄。 如果客户想要使用更新机制,他们需要跟踪这些句柄,以便他们以后可以明确指定他们想要更新的元素。 该解决方案的主要成本是客户需要管理这些指针。 然而,这并不意味着需要有任何内部分配来支持优先级队列中的句柄——任何非基于数组的堆都是基于节点的,其中每个节点自动是它自己的句柄。 例如,请参阅PairingHeap.Update 方法的 API

选项(b):唯一性。 这种方法对客户提出了两个额外的约束: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:具有优先队列功能的其他语言

| 语言 | 类型 | 笔记 |
|:-:|:-:|:-:|
| 爪哇| 优先队列 | 扩展抽象类AbstractQueue并实现接口Queue 。 |
| 锈| 二进制堆| |
| 斯威夫特| CFBinaryHeap | |
| C++ | 优先队列| |
| 蟒蛇| | |
| 去 | | 有一个堆接口。 |

附录 B:可发现性

请注意,在讨论数据结构时,_heap_ 一词的使用频率是 _priority queue_ 的 4 倍。

  • "array" AND "data structure" — 17.400.000 结果
  • "stack" AND "data structure" — 12.100.000 结果
  • "queue" AND "data structure" — 3.850.000 结果
  • "heap" AND "data structure" — 1.830.000 结果
  • "priority queue" AND "data structure" — 430.000 个结果
  • "trie" AND "data structure" — 335.000 个结果

请查看,很高兴收到反馈并不断迭代:) 我觉得我们正在融合! 😄 也很高兴收到问题 - 将它们添加到常见问题解答中!

@pgolebiowski我不倾向于使用基于句柄的 API(如果我要改装任何现有代码,我希望按照示例 2 包装它),但它对我来说大多看起来不错。 我可能会尝试一下你提到的实现,看看感觉如何,但这里有一些评论:

  • TryDequeue返回一个节点似乎很奇怪,因为那时它并不是真正的句柄(我更喜欢两个单独的输出参数)
  • 如果两个元素以相同的优先级排队,它们会以可预测的顺序出列吗? (很高兴有可重复性;否则消费者可以很容易地实现)
  • Merge的参数应该是PriorityQueue'2而不是PriorityQueueNode'2 ,你能澄清一下行为吗? 我不熟悉配对堆,但大概这两个堆此后重叠
  • 我不喜欢 2 参数方法的名称Contains :对于TryGet样式方法,我猜这不是一个名称
  • 为了Contains的目的,该类是否应该支持自定义IEqualityComparer<TElement> Contains
  • 似乎没有一种方法可以有效地确定节点是否仍在堆中(不确定何时使用它,想)
  • 奇怪的是Remove返回的是bool ; 我希望它被称为TryRemove或者它被抛出(如果节点不在堆中,我假设Update会这样做)。

@VisualMelon非常感谢您的反馈! 会很快解决这些,绝对同意:

  • TryDequeue返回一个节点似乎很奇怪,因为那时它并不是真正的句柄(我更喜欢两个单独的输出参数)
  • Merge的参数应该是PriorityQueue'2而不是PriorityQueueNode'2 ,你能澄清一下行为吗? 我不熟悉配对堆,但大概这两个堆此后重叠
  • 奇怪的是Remove返回的是bool ; 我希望它被称为TryRemove或者它抛出(如果节点不在堆中,我假设Update会这样做)。
  • 我不喜欢 2 参数方法的名称Contains :对于TryGet样式方法,我猜这不是一个名称

澄清这两个:

  • 如果两个元素以相同的优先级排队,它们会以可预测的顺序出列吗? (很高兴有可重复性;否则消费者可以很容易地实现)
  • 似乎没有一种方法可以有效地确定节点是否仍在堆中(不确定何时使用它,想)

对于第一点,如果目标是可重复性,那么实施将是确定性的,是的。 如果目标是 _if 以相同的优先级放入队列的两个元素,那么它们将按照放入队列的相同顺序出现_ - 我不确定实现是否可以调整为实现此属性,快速回答将是“可能不是”。

对于第二点,是的,堆不适合检查集合中是否存在元素,客户必须单独跟踪它以在 O(1) 中实现这一点或重用他们用于句柄的映射,如果他们使用更新机制。 否则,O(n)。

  • 为了Contains的目的,该类是否应该支持自定义IEqualityComparer<TElement> Contains

嗯...我开始认为也许把Contains放在这个优先级队列的责任中可能太多了,使用Linq可能就足够了( Contains无论如何都必须应用于枚举)。

@pgolebiowski感谢您的澄清

对于第一点,如果目标是可再现性,那么实现将是确定性的,是的

没有那么多确定性,因为永远保证(例如,即使实现更改,行为也不会),所以我认为我所追求的答案是“不”。 这很好:消费者可以在需要时将序列 ID 添加到优先级,尽管此时SortedSet可以完成这项工作。

对于第二点,是的,堆不适合检查集合中是否存在元素,客户必须单独跟踪它以在 O(1) 中实现这一点或重用他们用于句柄的映射,如果他们使用更新机制。 否则,O(n)。

它不需要Remove所需工作的子集吗? 我可能不清楚:我的意思是给定一个PriorityQueueNode ,检查它是否在堆中(不是TElement )。

嗯...我开始认为,也许将 contains 放在这个优先队列的责任中可能太多了

如果Contains不在那里,我不会抱怨:对于那些没有意识到他们应该使用句柄的人来说,这也是一个陷阱。

@pgolebiowski您似乎非常支持手柄,出于效率原因,我认为它是对的吗?

从效率的角度来看,我怀疑句柄确实是最好的,对于有唯一性和没有唯一性的两种情况,所以我认为它是框架提供的主要解决方案。

总而言之:
从可用性的角度来看,我认为重复元素很少是我想要的,这仍然让我想知道框架对两种模型PriorityQueue的包装器,例如,为了响应对更友好的 API 的持续需求。 (如果需求存在。正如我认为的那样!)

对于当前提出的 API,有几个想法/问题:

  • 还提供TryPeek(out TElement element, out TPriority priority)重载怎么样?
  • 如果您有可更新的重复键,我担心如果“出队”不返回优先队列节点,那么您将如何检查是否从句柄跟踪系统中删除了正确的节点? 因为您可以拥有多个具有相同优先级的元素副本。
  • 如果找不到节点, Remove(PriorityQueueNode)抛出? 或返回假?
  • 是否应该有一个TryRemove()版本在 Remove 抛出时不会抛出?
  • 我不确定Contains() api 在大多数情况下是否有用? “包含”似乎在旁观者的眼中,尤其是对于具有不同优先级或其他不同特征的“重复”元素的场景! 在这种情况下,最终用户可能无论如何都必须自己进行搜索。 但至少它对于没有重复的场景很有用。

@pgolebiowski感谢您花时间起草新提案! 我这边的几点意见:

  • 回应@TimLovellSmith的评论,我不确定Contains()TryGetNode()是否应该以当前提议的形式存在于 API 中。 它们意味着TElement相等性很重要,这大概是基于句柄的方法试图避免的事情之一。
  • 我可能会将public void Dequeue(out TElement element);改写为public TElement Dequeue();
  • 为什么TryDequeue()方法需要返回一个优先级?
  • 该类不应该也实现ICollection<T>IReadOnlyCollection<T>吗?
  • 如果我尝试更新从不同 PriorityQueue 实例返回的 PriorityQueueNode 会发生什么?
  • 我们想要支持高效的合并操作吗? AFAICT 这意味着我们不能使用基于数组的表示。 这将如何影响分配方面的实施?

大多数软件工程师只熟悉基于数组的二进制堆实现——它是最简单的一种,但不幸的是不是最有效的一种。 对于一般随机输入,更有效的堆类型的两个示例是:四元堆和配对堆。

选择后一种方法的权衡是什么? 是否可以对这些使用基于数组的实现?

当它没有正确的“添加”和“删除”等签名时,它无法实现ICollection<T>IReadOnlyCollection<T>

它的精神/形状更接近ICollection<KeyValuePair<T,Priority>>

它的精神/形状更接近ICollection<KeyValuePair<T,Priority>>

难道不是KeyValuePair<TPriority, TElement> ,因为排序是由 TPriority 完成的,这实际上是一种键控机制?

好的,看起来我们总体上赞成删除ContainsTryGet 。 将在下一次修订中删除它们并在常见问题解答中解释删除原因。

至于实现的接口—— 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

公共优先队列(IEnumerable> 集合);
``

我还希望PriorityQueue实现IReadonlyCollection<>接口。

跳转到公共 API 表面以外的其他内容。

这种能力对于实现例如 Dijkstra 的最短路径算法或需要处理不断变化的优先级的作业调度程序是必要的。 Java 中缺少更新机制,这对工程师来说是令人失望的,例如在这三个 StackOverflow 问题中查看了超过 32k 次:example, example, example。 为了避免引入价值如此有限的 API,我们认为我们提供的优先级队列功能的一个基本要求是支持更新集合中已经存在的元素的优先级的能力。

我不同意。 上次我用 C++ std::priority_queue 编写 Dijkstra 就足够了,它不处理优先级更新。 对于这种情况,AFAIK 的共同共识是将伪造元素添加到队列中,并更改优先级和值,并监控我们是否处理值。 作业调度程序也可以这样做。

老实说,我不确定 Dijkstra 会如何使用当前的队列提案。 我应该如何跟踪需要更新优先级的节点? 使用 TryGetNode()? 或者有另一个节点集合? 希望看到当前提案的代码。

如果您查看 Wikipedia,则不会假设更新优先级队列的优先级。 对于没有该功能并侥幸逃脱的所有其他语言也是如此。 我知道“努力变得更好”,但真的有这样的需求吗?

对于一般随机输入,更有效的堆类型的两个示例是:四元堆和配对堆。 有关堆的更多信息,请参阅维基百科和这篇论文。

我查看了论文,这是引用自它:

结果表明,实现的最佳选择强烈依赖于输入。 此外,它表明必须注意优化缓存性能,主要是在 L1-L2 屏障上。 这表明与更简单的缓存感知结构相比,复杂的缓存感知结构不太可能表现良好。

从看起来当前队列提案将被锁定在树实现而不是数组后面,并且某些东西告诉我散布在内存周围的树节点有可能不如元素数组的性能。

我认为有基准来比较基于数组和配对堆的简单二元堆将是做出正确决定的理想选择,在此之前,我认为将设计锁定在特定实现之后并不明智(我在看着你Merge方法)。

跳到另一个话题,我个人更喜欢 KeyValuePair比新的自定义类。

  • 更少的 API 表面
  • 我可以这样做:`new PriorityQueue(新词典() { { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 } }); 我知道它受到密钥唯一性的限制。 此外,我认为使用基于 IDictionary 的集合会带来很好的协同作用。
  • 它是一个结构而不是类,所以没有 NullReference 异常
  • 在某些时候,PrioirtyQueue 需要序列化/反序列化,我认为对现有对象执行操作会更容易。

缺点是看着TPriority Key心情复杂,但我认为好的文档可以解决这个问题。
`

作为 Dijkstra 的一种解决方法,将假元素添加到队列中有效,并且您处理的图边总数不会改变。 但是在处理边生成的堆中驻留的临时节点的数量会发生变化,这会影响内存使用和入队和出队的效率。

我错了不能做 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 影响最大的领域是更新机制。 具体而言,挑战在于确定我们希望为客户提供的产品是否以及如何支持更新集合中已有元素的优先级。

这种能力对于实现例如 Dijkstra 的最短路径算法或需要处理不断变化的优先级的作业调度程序是必要的。 Java 中缺少更新机制,这对工程师来说是令人失望的,例如在这三个 StackOverflow 问题中查看了超过 32k 次: example , example , example 。 为了避免引入价值如此有限的 API,我们认为我们提供的优先级队列功能的一个基本要求是支持更新集合中已经存在的元素的优先级的能力。

为了提供更新机制,我们必须确保客户可以明确他们想要更新的内容。 我们已经确定了两种交付方式:a) 通过句柄; b) 通过强制集合中元素的唯一性。 每一个都有不同的好处和成本。

选项 (a):手柄。 在这种方法中,每次将元素添加到队列时,数据结构都会提供其唯一的句柄。 如果客户想要使用更新机制,他们需要跟踪这些句柄,以便他们以后可以明确指定他们想要更新的元素。 该解决方案的主要成本是客户需要管理这些指针。 然而,这并不意味着需要有任何内部分配来支持优先级队列中的句柄——任何非基于数组的堆都是基于节点的,其中每个节点自动是它自己的句柄。 例如,请参阅PairingHeap.Update 方法的 API

选项(b):唯一性。 这种方法对客户提出了两个额外的约束: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. 为什么没有ContainsTryGet方法?

提供此类方法的价值可以忽略不计,因为在堆中查找元素需要枚举整个集合,这意味着任何ContainsTryGet方法都将是枚举的包装器。 此外,为了检查集合中是否存在元素,优先级队列必须知道如何对TElement对象进行相等性检查,我们认为这不是优先级队列的职责。

3. 为什么有DequeueTryDequeue重载返回PriorityQueueNode

这适用于想要使用UpdateRemove方法并跟踪句柄的客户。 当从优先级队列中取出一个元素时,他们会收到一个句柄,他们可以用它来调整他们的句柄跟踪系统的状态。

4. 当UpdateRemove方法接收到来自不同队列的节点时会发生什么?

优先级队列会抛出异常。 每个节点都知道它所属的优先级队列,类似于LinkedListNode<T>知道它所属的LinkedList<T>

附录

附录 A:具有优先队列功能的其他语言

| 语言 | 类型 | 笔记 |
|:-:|:-:|:-:|
| 爪哇| 优先队列 | 扩展抽象类AbstractQueue并实现接口Queue 。 |
| 锈| 二进制堆| |
| 斯威夫特| CFBinaryHeap | |
| C++ | 优先队列| |
| 蟒蛇| | |
| 去 | | 有一个堆接口。 |

附录 B:可发现性

请注意,在讨论数据结构时,_heap_ 一词的使用频率是 _priority queue_ 的 4 倍。

  • "array" AND "data structure" — 17.400.000 结果
  • "stack" AND "data structure" — 12.100.000 结果
  • "queue" AND "data structure" — 3.850.000 结果
  • "heap" AND "data structure" — 1.830.000 结果
  • "priority queue" AND "data structure" — 430.000 个结果
  • "trie" AND "data structure" — 335.000 个结果

感谢大家的反馈! 我已将提案更新到 v2.1。 变更日志:

  • 删除了ContainsTryGet
  • 添加了常见问题 #2:_为什么没有ContainsTryGet方法?_
  • 添加了接口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)的重载。
  • 添加了常见问题 #3:_为什么有DequeueTryDequeue重载返回PriorityQueueNode ?_
  • 添加了常见问题 #4:_当UpdateRemove方法接收来自不同队列的节点时会发生什么?_

感谢您的更改说明;)

澄清的小要求:

  • 我们可以限定 FAQ 4 使其适用于不在队列中的元素吗? (即删除的)
  • 我们可以添加一个关于稳定性的常见问题解答,即在出列具有相同优先级的元素时的保证(如果有的话)(我的理解是没有计划做出任何保证,这对于例如调度来说很重要)。

@pgolebiowski关于提议的Merge方法:

public void Merge(PriorityQueue<TElement, TPriority> other); // O(1)

显然,这样的操作不会有复制语义,所以我想知道是否会有任何关于对thisother更改的问题来满足堆属性)。

@eiriktsarpalis @VisualMelon — 谢谢! 将解决提出的问题,预计到 2020 年 10 月 4 日。

如果其他人有更多反馈/问题/疑虑/想法-请分享😊

优先队列提案(v2.2)

概括

.NET Core 社区建议向系统库中添加 _priority queue_ 功能,这是一种数据结构,其中每个元素还具有与其关联的优先级。 具体来说,我们建议将PriorityQueue<TElement, TPriority>System.Collections.Generic命名空间。

信条

在我们的设计中,我们遵循以下原则(除非您知道更好的原则):

  • 覆盖面广。 我们希望为 .NET Core 客户提供一种有价值的数据结构,该结构足够灵活以支持各种用例。
  • 从已知错误中学习。 我们努力提供优先队列功能,该功能不会存在其他框架和语言(例如 Java、Python、C++、Rust)中存在的面向客户的问题。 我们将避免做出已知会让客户不满意并降低优先队列的有用性的设计选择。
  • 对单向门的决定极为谨慎。 API一旦引入,就不能修改或删除,只能扩展。 我们将仔细分析设计选择,以避免我们的客户将永远陷入困境的次优解决方案。
  • 避免设计瘫痪。 我们接受可能没有完美的解决方案。 我们将权衡取舍并继续交付,最终为我们的客户提供他们期待已久的功能。

背景

从客户的角度

从概念上讲,优先级队列是元素的集合,其中每个元素都有一个关联的优先级。 优先级队列最重要的功能是它提供对集合中具有最高优先级的元素的有效访问,以及删除该元素的选项。 预期的行为还可能包括:1) 能够修改集合中已有元素的优先级; 2) 能够合并多个优先级队列。

计算机科学背景

优先级队列是一种抽象的数据结构,即它是一个具有某些行为特征的概念,如上一节所述。 优先级队列的最有效实现是基于堆的。 然而,与一般的误解相反,堆也是一种抽象的数据结构,可以通过多种方式实现,每种方式都有不同的优点和缺点。

大多数软件工程师只熟悉基于数组的二进制堆实现——它是最简单的一种,但不幸的是不是最有效的一种。 对于一般随机输入,更有效的堆类型的两个示例是:四元堆配对堆。 有关堆的更多信息,请参阅维基百科这篇论文

更新机制是关键的设计挑战

我们的讨论表明,设计中最具挑战性的领域,同时也是对 API 影响最大的领域是更新机制。 具体而言,挑战在于确定我们希望为客户提供的产品是否以及如何支持更新集合中已有元素的优先级。

这种能力对于实现例如 Dijkstra 的最短路径算法或需要处理不断变化的优先级的作业调度程序是必要的。 Java 中缺少更新机制,这对工程师来说是令人失望的,例如在这三个 StackOverflow 问题中查看了超过 32k 次: example , example , example 。 为了避免引入价值如此有限的 API,我们认为我们提供的优先级队列功能的一个基本要求是支持更新集合中已经存在的元素的优先级的能力。

为了提供更新机制,我们必须确保客户可以明确他们想要更新的内容。 我们已经确定了两种交付方式:a) 通过句柄; b) 通过强制集合中元素的唯一性。 每一个都有不同的好处和成本。

选项 (a):手柄。 在这种方法中,每次将元素添加到队列时,数据结构都会提供其唯一的句柄。 如果客户想要使用更新机制,他们需要跟踪这些句柄,以便他们以后可以明确指定他们想要更新的元素。 该解决方案的主要成本是客户需要管理这些指针。 然而,这并不意味着需要有任何内部分配来支持优先级队列中的句柄——任何非基于数组的堆都是基于节点的,其中每个节点自动是它自己的句柄。 例如,请参阅PairingHeap.Update 方法的 API

选项(b):唯一性。 这种方法对客户提出了两个额外的约束: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. 为什么没有ContainsTryGet方法?

提供此类方法的价值可以忽略不计,因为在堆中查找元素需要枚举整个集合,这意味着任何ContainsTryGet方法都将是枚举的包装器。 此外,为了检查集合中是否存在元素,优先级队列必须知道如何对TElement对象进行相等性检查,我们认为这不是优先级队列的职责。

3. 为什么有DequeueTryDequeue重载返回PriorityQueueNode

这适用于想要使用UpdateRemove方法并跟踪句柄的客户。 当从优先级队列中取出一个元素时,他们会收到一个句柄,他们可以用它来调整他们的句柄跟踪系统的状态。

4. 当UpdateRemove方法接收到来自不同队列的节点时会发生什么?

优先级队列会抛出异常。 每个节点都知道它所属的优先级队列,类似于LinkedListNode<T>知道它所属的LinkedList<T> 。 此外,如果一个节点已从队列中删除,则尝试对其调用UpdateRemove也会导致异常。

5、为什么没有Merge方法?

可以在恒定时间内合并两个优先级队列,这使其成为向客户提供的诱人功能。 但是,我们没有数据证明对此类功能有需求,我们无法证明将其包含在公共 API 中是合理的。 此外,此类功能的设计非常重要,并且鉴于可能不需要此功能,它可能会不必要地使 API 表面和实现复杂化。

尽管如此,现在不包括Merge方法是一个双向的门——如果将来客户确实表示有兴趣支持合并功能,则可以扩展PriorityQueue类型。 因此,我们建议不要包含Merge方法,并继续发布。

6. 集合是否提供稳定性保证?

集合不会提供开箱即用的稳定性保证,即如果两个元素以相同的优先级排队,客户将无法假设它们将按特定顺序出列。 但是,如果客户希望使用我们的PriorityQueue实现稳定性,他们可以定义TPriority和相应的IComparer<TPriority>以确保稳定性。 此外,数据收集将是确定性的,即对于给定的操作序列,它将始终以相同的方式运行,从而实现可重复性。

附录

附录 A:具有优先队列功能的其他语言

| 语言 | 类型 | 笔记 |
|:-:|:-:|:-:|
| 爪哇| 优先队列 | 扩展抽象类AbstractQueue并实现接口Queue 。 |
| 锈| 二进制堆| |
| 斯威夫特| CFBinaryHeap | |
| C++ | 优先队列| |
| 蟒蛇| | |
| 去 | | 有一个堆接口。 |

附录 B:可发现性

请注意,在讨论数据结构时,_heap_ 一词的使用频率是 _priority queue_ 的 4 倍。

  • "array" AND "data structure" — 17.400.000 结果
  • "stack" AND "data structure" — 12.100.000 结果
  • "queue" AND "data structure" — 3.850.000 结果
  • "heap" AND "data structure" — 1.830.000 结果
  • "priority queue" AND "data structure" — 430.000 个结果
  • "trie" AND "data structure" — 335.000 个结果

变更日志:

  • 删除了方法void Merge(PriorityQueue<TElement, TPriority> other) // O(1)
  • 添加了常见问题 #5:为什么没有Merge方法?
  • 修改后的 FAQ #4 也适用于已从优先级队列中删除的节点。
  • 添加了常见问题 #6:合集是否提供稳定性保证?

新的常见问题解答看起来很棒。 我对建议的 API 使用了句柄字典对 Dijkstra 进行了编码,看起来基本上没问题。

我从这样做中学到的一个小知识是,当前的一组方法名称/重载对于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 应用程序

重要的是我们要确定上述哪种方法可以为我们的大多数用户提供最佳价值。 因此,我一直在浏览 Microsoft 内部和公共的 .NET 代码库,例如 Heap/PriorityQueue 实现的实例,以便更好地了解最常见的使用模式是什么:

大多数应用程序实现了堆排序或作业调度的变体。 一些实例用于算法,例如拓扑排序或霍夫曼编码。 较小的数字用于计算图中的距离。 在检查的 80 个 PriorityQueue 实现中,只有 9 个被发现实现了某种形式的优先级更新。

同时,Python、Java、C++、Go、Swift 或 Rust 核心库中的 Heap/PriorityQueue 实现都不支持其 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% 用例 |
|:-:|:-:|:-:|
| 关心更新| 是 | 没有|
| 受到手柄的负面影响| 不适用(它们是需要的)| 没有|
| 受到手柄的积极影响| 是 | 没有|

对我来说,这是支持 100% 用例,而不仅仅是 88-89% 的明智之举。

这个假设是错误的,您不需要在基于节点的堆中进行额外分配

如果优先级和项目都是值类型(或者如果两者都是您不拥有和/或不能更改其基本类型的引用类型),您是否可以链接到一个不需要额外分配的实现(或只是描述它是如何实现的)? 看看会很有帮助。 谢谢。

如果您可以详细说明,或者只是说出您想说的内容,那将会很有帮助。 我需要消除歧义,会有乒乓,这将变成一个冗长的讨论。 或者,我们可以安排一个电话。

我是说我们想要避免任何需要分配的 Enqueue 操作,无论是在调用者方面还是在实现方面(摊销内部分配是好的,例如扩展在实现中使用的数组)。 我试图了解基于节点的堆是如何实现的(例如,如果这些节点对象暴露给调用者,则由于担心不适当的重用/别名而禁止实现池化)。 我希望能够写:
C# pq.Enqueue(42, 84);
并没有分配。 你提到的实现是如何实现的?

或者只是说出你想说的

我以为我是。

我们想避免任何需要分配的入队操作 [...] 我希望能够写: pq.Enqueue(42, 84);并且没有分配。

这种欲望从何而来? 解决方案有副作用是件好事,而不是 99.9% 的客户需要满足的要求。 我不明白为什么你会选择这个低影响维度来在解决方案之间做出设计选择。

如果这些对 12% 的客户在另一个维度上产生负面影响,我们不会根据为 0.1% 的客户进行优化来做出设计选择。 “不关心分配”+“处理两种值类型”是一种边缘情况。

我发现支持的行为/功能的维度更为重要,尤其是在为广泛的受众和各种用例设计通用的多功能数据结构时。

这种欲望从何而来?

从希望核心集合类型在关心性能的场景中可用。 您说基于节点的解决方案将支持 100% 的用例:如果每个队列都分配,则情况并非如此,就像List<T>Dictionary<TKey, TValue>HashSet<T>等等如果它们在每个 Add 上分配,则在许多情况下将变得不可用。

为什么你认为只有“0.1%”关心这些方法的分配开销? 这些数据是从哪里来的?

“不关心分配”+“处理两种值类型”是一种边缘情况

它不是。 它也不仅仅是“两种值类型”。 据我了解,所提议的解决方案将需要 a) 在每个队列上进行分配,而不管涉及的 Ts,或者 b) 将需要元素类型从某些已知的基类型派生,这反过来又会禁止大量可能的用途避免额外分配。

@eiriktsarpalis
所以您不要忘记任何选项,我认为有一个可行的选项 4 可以添加到您的列表中的选项 1、2 和 3,这是一个折衷方案:

  1. 一个支持 12% 用例的实现,同时还通过允许更新相等的元素来几乎优化其他 88% 的用例,并且只_懒惰地构建在第一次调用更新方法时执行这些更新所需的查找表(并在子序列更新+删除时更新它)。 因此,不使用该功能的应用程序产生的成本较低。

我们可能仍会决定,因为 88% 或 12% 的人可以从不需要可更新数据结构的实现中获得额外的性能,或者首先针对一个实现进行优化,因此提供选项 2 和 3 比提供选项 2 和 3 更好选项 4。但我认为我们不应该忘记存在另一个选项。

[或者我想您可以将其视为更好的选项 1 并更新 1 的描述以说明簿记不是强制的,而是懒惰的,并且仅在使用更新时才需要正确的等效行为...]

@stephentoub这正是我想说的简单说你想说的话,谢谢:)

为什么你认为只有 0.1% 关心这些方法的分配开销? 这些数据是从哪里来的?

根据直觉,即基于相同的来源,您认为将“无额外分配”优先于“进行更新的能力”更重要。 至少对于更新机制,我们有数据表明 11-12% 的客户确实需要支持这种行为。 我认为远程关闭的客户不会关心“没有额外分配”。

在任何一种情况下,您都出于某种原因选择关注内存维度,而忘记了其他维度,例如原始速度,这是您首选方法的另一种权衡。 提供“无额外分配”的基于数组的实现将比基于节点的实现慢。 同样,我认为在这里将内存优先于速度是任意的。

让我们退后一步,专注于客户的需求。 我们有一个设计选择,可能会或可能不会使 12% 的客户无法使用数据结构。 我认为我们需要非常谨慎地提供我们选择不支持这些的理由。

提供“无额外分配”的基于数组的实现将比基于节点的实现慢。

请分享您用于执行该比较的两个 C# 实现以及用于得出该结论的基准测试。 理论论文当然很有价值,但它们只是拼图的一小部分。 更重要的是,当橡胶遇到道路时,将给定平台和给定实现的细节考虑在内,并且您能够在具有特定实现和典型/预期数据集/使用模式的特定平台上进行验证。 你的断言很可能是正确的。 它也可能不是。 我想查看实现/数据以更好地理解。

请分享您用于执行该比较的两个 C# 实现以及用于得出该结论的基准测试

这是一个有效的观点,我引用的论文仅对 C++ 中的实现进行了比较和基准测试。 它使用不同的数据集和使用模式进行多个基准测试。 我非常有信心这可以转移到 C#,但如果你认为这是我们需要加倍努力的事情,我认为最好的做法是让你的同事进行这样的研究。

@pgolebiowski我有兴趣更好地了解您的反对意见。 提案提倡两种不同的类型,这不会满足您的要求吗?

  1. 一个支持 12% 用例的实现,同时还通过允许对相等的元素进行更新来几乎优化其他 88% 的用例,并且只懒惰地构建在第一次调用更新方法时执行这些更新所需的查找表(并在子序列更新+删除时更新它)。 因此,不使用该功能的应用程序产生的成本较低。

我可能会将其归类为选项 1 的性能优化,但是我确实看到该特定方法存在一些问题:

  • 更新现在变为 _O(n)_,这可能会导致无法预测的性能取决于使用模式。
  • 还需要查找表来验证唯一性。 将相同的元素排入队列两次 _before_ 调用 Update 将被接受,并且可以说使队列进入不一致的状态。

@eiriktsarpalis它只有 O(n) 一次,然后是 O(1),这是 O(1) 摊销。 您可以将验证唯一性推迟到第一次更新。 但也许这太聪明了。 两个类更容易解释。

过去几天我一直在设计两个 PriorityQueue 实现的原型:一个没有更新支持的基本实现和一个支持使用元素相等进行更新的实现。 我将前者命名为PriorityQueue ,后者命名PrioritySet ,因为没有更好的名称。 我的目标是衡量 API 人体工程学并比较性能。

可以在这个 repo 中找到实现。 这两个类都是使用基于数组的四重堆实现的。 可更新的实现还使用了一个将元素映射到内部堆索引的字典。

基本优先队列

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

可更新的优先队列

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

性能比较

我编写了一个简单的堆排序基准测试,比较了它们最基本的应用程序中的两个实现。 我还包括一个使用 Linq 进行比较的排序基准:

BenchmarkDotNet=v0.12.1, OS=ubuntu 20.04
AMD EPYC 7452, 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=5.0.100-rc.2.20479.15
  [Host]     : .NET Core 5.0.0 (CoreCLR 5.0.20.47505, CoreFX 5.0.20.47505), X64 RyuJIT
  DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.47505, CoreFX 5.0.20.47505), X64 RyuJIT

| 方法 | 尺寸 | 意思 | 错误 | 标准偏差 | 比率 | 比率SD | 第 0 代 | 第 1 代 | 第 2 代 | 已分配 |
|-------------- |------ |-------------:|-----------: |-----------:|------:|--------:|--------:|-------- :|--------:|----------:|
| LinqSort | 30 | 1.439 美元 | 0.0072我们| 0.0064 us | 1.00 | 0.00 | 0.0095 | - | - | 第672章
| 优先队列 | 30 | 1.450 美元 | 0.0085 美元 | 0.0079 美元 | 1.01 | 0.01 | - | - | - | - |
| 优先级设置 | 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 |
| 优先队列 | 300 | 29.510 美元 | 0.0995 美元 | 0.0882我们| 1.19 | 0.01 | - | - | - | - |
| 优先级设置 | 300 | 47.715 美元 | 0.4455 美元 | 0.4168 us | 1.93 | 0.02 | - | - | - | - |
| | | | | | | | | | | |
| LinqSort | 3000 | 412.015 我们 | 1.5495 美元 | 1.3736我们| 1.00 | 0.00 | 0.4883 | - | - | 36312 B |
| 优先队列 | 3000 | 491.722我们| 4.1463我们| 3.8785 美元 | 1.19 | 0.01 | - | - | - | - |
| 优先级设置 | 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 |
| 优先队列 | 30000 | 5,688.625 美元 | 53.0746我们| 49.6460我们| 1.09 | 0.01 | - | - | - | 2 乙 |
| 优先级设置 | 30000 | 8,124.306 我们 | 39.9498我们| 37.3691我们| 1.55 | 0.01 | - | - | - | 4 乙 |

正如预期的那样,跟踪元素位置的开销增加了显着的性能损失,与基线实现相比大约慢了 40-50%。

我感谢所有的努力,我看到它花了很多时间和精力。

  1. 我真的不明白 2 个几乎相同的数据结构的原因,其中一个是另一个的劣质版本。
  2. 此外,即使您想要拥有这样 2 个版本的优先队列,我也看不出“高级”版本比 20 天前的优先队列提案 (v2.2)更好。

tl;博士:

  • 这个提议令人兴奋!! 但它还不适合我的高性能用例。
  • 我的计算几何/运动规划性能负载的 90% 以上是 PQ 入队/出队,因为这是算法中占主导地位的 N^m LogN。
  • 我赞成单独的 PQ 实现。 我通常不需要优先更新,性能差 2 倍是不可接受的。
  • PrioritySet 是一个令人困惑的名称,与 PriorityQueue 相比是不可发现的
  • 存储优先级两次(一次在我的元素中,一次在队列中)感觉很昂贵。 结构副本和内存使用情况。
  • 如果计算优先级很昂贵,我会简单地将一个元组(priority: ComputePriority(element), element)到 PQ 中,而我的优先级获取函数将只是tuple => tuple.priority
  • 性能应该在每个操作或实际用例上进行基准测试(例如优化的多开始多端搜索图)
  • 无序枚举行为是意外的。 更喜欢类似队列的 Dequeue() 顺序语义?
  • 考虑支持克隆操作和合并操作。
  • 在稳态使用中,基本操作必须是 0-alloc。 我将汇集这些队列。
  • 考虑支持执行堆化以帮助池化的 EnqueueMany。

我从事与机器人和游戏相关的高性能搜索(运动规划)和计算几何代码(例如扫描线算法),我使用了很多自定义的手动优先队列。 我拥有的另一个常见用例是 Top-K 查询,其中可更新的优先级没有用。

关于两种实现(是与否更新支持)辩论的一些反馈。

命名:

  • PrioritySet 隐含了集合语义,但队列没有实现 ISet。
  • 您正在使用 UpdatablePriorityQueue 名称,如果我搜索 PriorityQueue,它会更容易发现。

表现:

  • 优先队列性能几乎总是我的几何/规划算法中的性能瓶颈 (>90%)
  • 考虑传递一个 Func或比较ctor 而不是复制 TPriority(昂贵!)。 如果计算优先级很昂贵,我会将 (priority, element) 插入 PQ 并通过比较来查看缓存的优先级。
  • 我的很多算法不需要 PQ 更新。 我会考虑使用不支持更新的内置 PQ,但是如果某些东西的性能成本是 2 倍,以支持我不需要(更新)的功能,那么它对我来说就没用了。
  • 对于性能分析/权衡,了解每个操作@eiriktsarpalis的入队/出队的相对壁时间成本非常重要——“跟踪速度慢 2
  • 我很高兴看到您的构造函数执行 Heapify。 考虑采用 IList、List 或 Array 以避免枚举分配的构造函数。
  • 如果 PQ 最初为空,请考虑公开执行 Heapify 的 EnqueueMany,因为在高性能中,池集合很常见。
  • 如果元素不包含引用,请考虑清除非零数组。
  • 入队/出队中的分配是不可接受的。 出于性能原因,我的算法是零分配的,具有池化线程本地集合。

蜜蜂:

  • 克隆优先级队列对于您的实现来说是一项微不足道的操作,并且经常有用。

    • 相关:枚举优先队列是否应该具有类似队列的语义? 我希望有一个 dequeue-order 集合,类似于 Queue 所做的。 我希望 new List(myPriorityQueue) 不会改变 priorityQueue,而是像我刚刚描述的那样工作。

  • 如上所述,最好接受Func<TElement, TPriority>而不是优先插入。 如果计算优先级很昂贵,我可以简单地插入(priority, element)并提供一个 func tuple => tuple.priority
  • 合并两个优先级队列有时很有用。
  • 奇怪的是,Peek 返回的是 TItem,而 Enumeration & Enqueue 却有 (TItem, TPriority)。

话虽如此,对于我的大量算法,我的优先队列项包含它们的优先级,并且将其存储两次(一次在 PQ 中,一次在项目中)听起来效率很低。 如果我按多个键进行排序(类似于 OrderBy.ThenBy.ThenBy 的用例),情况尤其如此。 这个 API 还清除了插入优先但 Peek 没有返回它的许多不一致。

最后,值得注意的是,我经常将数组的索引插入优先队列,而不是数组元素本身。 不过,到目前为止讨论的所有 API 都支持这一点。 例如,如果我在 x 轴线上处理间隔的开始/结束,我可能有优先级队列事件(x, isStartElseEnd, intervalId)并按 x 排序,然后按 isStartElseEnd。 这通常是因为我有其他数据结构可以从索引映射到某些计算数据。

@pgolebiowski我冒昧地将您提议的配对堆实现包含在基准测试中,这样我们就可以直接比较所有三种方法的实例。 结果如下:

| 方法 | 尺寸 | 意思 | 错误 | 标准偏差 | 中位数 | 第 0 代 | 第 1 代 | 第 2 代 | 已分配 |
|-------------- |-------- |-----------:|---- -------------:|-----------------:|---------------- ---:|---------:|------:|------:|-----------:|
| 优先队列 | 10 | 774.7 纳秒 | 3.30 纳秒 | 3.08 纳秒 | 773.2 纳秒 | - | - | - | - |
| 优先级设置 | 10 | 1,643.0 纳秒 | 3.89 纳秒 | 3.45 纳秒 | 1,642.8 纳秒 | - | - | - | - |
| 配对堆| 10 | 1,660.2 纳秒 | 14.11 纳秒 | 12.51 纳秒 | 1,657.2 纳秒 | 0.0134 | - | - | 960 B |
| 优先队列 | 50 | 6,413.0 纳秒 | 14.95 纳秒 | 13.99 纳秒 | 6,409.5 纳秒 | - | - | - | - |
| 优先级设置 | 50 | 12,193.1 纳秒 | 35.41 纳秒 | 29.57 纳秒 | 12,188.3 纳秒 | - | - | - | - |
| 配对堆| 50 | 13,955.8 纳秒 | 193.36 纳秒 | 180.87 纳秒 | 13,989.2 纳秒 | 0.0610 | - | - | 4800 乙 |
| 优先队列 | 150 | 27,402.5 纳秒 | 76.52 纳秒 | 71.58 纳秒 | 27,410.2 纳秒 | - | - | - | - |
| 优先级设置 | 150 | 48,485.8 纳秒 | 160.22 纳秒 | 149.87 纳秒 | 48,476.3 纳秒 | - | - | - | - |
| 配对堆| 150 | 56,951.2 纳秒 | 190.52 纳秒 | 168.89 纳秒 | 56,953.6 纳秒 | 0.1831 | - | - | 14400 B |
| 优先队列 | 500 | 124,933.7 纳秒 | 429.20 纳秒 | 380.48 纳秒 | 124,824.4 纳秒 | - | - | - | - |
| 优先级设置 | 500 | 206,310.0 纳秒 | 433.97 纳秒 | 338.81 纳秒 | 206,319.0 纳秒 | - | - | - | - |
| 配对堆| 500 | 229,423.9 纳秒 | 3,213.33 纳秒 | 2,848.53 纳秒 | 230,398.7 纳秒 | 0.4883 | - | - | 48000 乙 |
| 优先队列 | 1000 | 284,481.8 纳秒 | 475.91 纳秒 | 445.16 纳秒 | 284,445.6 纳秒 | - | - | - | - |
| 优先级设置 | 1000 | 454,989.4 纳秒 | 3,712.11 纳秒 | 3,472.31 纳秒 | 455,354.0 纳秒 | - | - | - | - |
| 配对堆| 1000 | 459,049.3 纳秒 | 1,706.28 纳秒 | 1,424.82 纳秒 | 459,364.9 纳秒 | 0.9766 | - | - | 96000 乙 |
| 优先队列 | 10000 | 3,788,802.4 纳秒 | 11,715.81 纳秒 | 10,958.98 纳秒 | 3,787,811.9 纳秒 | - | - | - | 1 乙 |
| 优先级设置 | 10000 | 5,963,100.4 纳秒 | 26,669.04 纳秒 | 22,269.86 纳秒 | 5,950,915.5 纳秒 | - | - | - | 2 乙 |
| 配对堆| 10000 | 6,789,719.0 纳秒 | 134,453.01 纳秒 | 265,397.13 纳秒 | 6,918,392.9 纳秒 | 7.8125 | - | - | 960002 B |
| 优先队列 | 1000000 | 595,059,170.7 纳秒 | 4,001,349.38 纳秒 | 3,547,092.00 纳秒 | 595,716,610.5 纳秒 | - | - | - | 4376 B |
| 优先级设置 | 1000000 | 1,592,037,780.9 纳秒 | 13,925,896.05 纳秒 | 12,344,944.12 纳秒 | 1,591,051,886.5 纳秒 | - | - | - | 第288话
| 配对堆| 1000000 | 1,858,670,560.7 纳秒 | 36,405,433.20 纳秒 | 59,815,170.76 纳秒 | 1,838,721,629.0 纳秒 | 1000.0000 | - | - | 96000376 B |

关键要点

  • ~配对堆实现渐近地执行比其数组支持的对应物好得多。 然而,对于小堆大小(< 50 个元素),它最多可以慢 2 倍,赶上大约 1000 个元素,但对于大小为 10^6~ 的堆,速度最多快 2 倍。
  • 正如预期的那样,配对堆产生了大量的堆分配。
  • “PrioritySet”实现一直很慢~所有三个竞争者中最慢的~,所以我们可能不想采用这种方法。

~鉴于上述情况,我仍然相信基线数组堆和配对堆方法之间存在有效的权衡~。

编辑:在我的基准测试中修复错误后更新了结果,谢谢@VisualMelon

@eiriktsarpalis你对PairingHeap基准,我认为是错误的: Add的参数是错误的。 当你交换它们时,情况就不同了: https :

(当我第一次实施它时,我做了完全相同的事情)

请注意,这并不意味着配对堆更快或更慢,而是它似乎在很大程度上取决于所提供数据的分布/顺序。

@eiriktsarpalis 回复:PrioritySet 的用处...
我们不应该期望可更新的堆排序比更慢以外的任何东西,因为它在场景中没有优先级更新。 (对于堆排序,您甚至可能想要保留重复项,集合是不合适的。)

查看 PrioritySet 是否有用的试金石应该是使用优先级更新的基准算法,而不是相同算法的非更新实现,将重复值入队并在出队时忽略重复。

谢谢@VisualMelon ,在您提出修复建议后,我更新了我的结果和评论。

相反,它似乎在很大程度上取决于所提供数据的分布/顺序。

我相信它可能受益于排队的优先级是单调的。

查看 PrioritySet 是否有用的试金石应该是使用优先级更新的基准算法,而不是相同算法的非更新实现,将重复值入队并在出队时忽略重复。

@TimLovellSmith我的目标是衡量最常见的 PriorityQueue 应用程序的性能:我想查看根本不需要更新的情况下的影响,而不是衡量更新的性能。 然而,生成一个单独的基准测试来比较配对堆与“PrioritySet”更新可能是有意义的。

@miyu感谢您的详细反馈,非常感谢!

@TimLovellSmith我写了一个使用更新的简单基准测试

| 方法 | 尺寸 | 意思 | 错误 | 标准偏差 | 中位数 | 第 0 代 | 第 1 代 | 第 2 代 | 已分配 |
|------------ |-------- |---------------:|--------- -----:|--------------:|---------------:|-------:| ------:|------:|-----------:|
| 优先级设置 | 10 | 1.052 美元 | 0.0106 us | 0.0099 美元 | 1.055 美元 | - | - | - | - |
| 配对堆| 10 | 1.055 美元 | 0.0042我们| 0.0035 美元 | 1.055 美元 | 0.0057 | - | - | 480 B |
| 优先级设置 | 50 | 7.394 美元 | 0.0527我们| 0.0493我们| 7.380 美元 | - | - | - | - |
| 配对堆| 50 | 8.587 美元 | 0.1678 us | 0.1570 美元 | 8.634 美元 | 0.0305 | - | - | 2400 乙 |
| 优先级设置 | 150 | 27.522我们| 0.0459 美元 | 0.0359 美元 | 27.523 我们 | - | - | - | - |
| 配对堆| 150 | 32.045 美元 | 0.1076 us | 0.1007 us | 32.019 我们 | 0.0610 | - | - | 7200 B |
| 优先级设置 | 500 | 109.097 我们 | 0.6548 us | 0.6125 美元 | 109.162我们| - | - | - | - |
| 配对堆| 500 | 131.647我们| 0.5401 美元 | 0.4510 美元 | 131.588我们| 0.2441 | - | - | 24000 乙 |
| 优先级设置 | 1000 | 238.184我们| 1.0282我们| 0.9618我们| 238.457我们| - | - | - | - |
| 配对堆| 1000 | 293.236我们| 0.9396我们| 0.8789我们| 293.257我们| 0.4883 | - | - | 48000 乙 |
| 优先级设置 | 10000 | 3,035.982 美元 | 12.2952我们| 10.8994我们| 3,036.985 美元 | - | - | - | 1 乙 |
| 配对堆| 10000 | 3,388.685 美元 | 16.0675我们| 38.1861我们| 3,374.565 美元 | - | - | - | 480002 B |
| 优先级设置 | 1000000 | 841,406.888 我们 | 16,788.4775 我们 | 15,703.9522 我们 | 840,888.389 我们 | - | - | - | 第288话
| 配对堆| 1000000 | 989,966.501 我们 | 19,722.6687 我们 | 30,705.8191 美元 | 996,075.410 我们 | - | - | - | 48000448 乙 |

另外,他们是否就缺乏稳定性作为人们用例的一个问题(或非问题)进行了讨论/反馈?

他们是否一直在讨论/反馈缺乏稳定性是人们用例的一个问题(或非问题)

没有一个实现保证稳定性,但是用户通过使用插入顺序增加序数来获得稳定性应该非常简单:

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

System.Collections.Generic.PriorityQueue<TElement, TPriority>的 API 刚刚获得批准。 我创建了一个单独的问题来继续我们关于支持优先级更新的潜在堆实现的对话。

我将关闭此问题,感谢大家的贡献!

也许有人可以写下这段旅程! 一个 API 整整 6 年。 :) 有机会赢得吉尼斯吗?

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

chunseoklee picture chunseoklee  ·  3评论

omajid picture omajid  ·  3评论

omariom picture omariom  ·  3评论

bencz picture bencz  ·  3评论

aggieben picture aggieben  ·  3评论