【问题标题】:C# Priority QueueC# 优先队列
【发布时间】:2010-12-28 14:30:27
【问题描述】:

我正在寻找具有如下界面的优先级队列:

class PriorityQueue<T>
{
    public void Enqueue(T item, int priority)
    {
    }

    public T Dequeue()
    {
    }
}

我见过的所有实现都假设itemIComparable,但我不喜欢这种方法;我想在将其推入队列时指定优先级。

如果不存在现成的实现,那么我自己执行此操作的最佳方法是什么?我应该使用什么底层数据结构?某种自平衡树,还是什么?一个标准的 C#.net 结构会很好。

【问题讨论】:

  • 你打算从多个线程调用它吗?
  • @sohum: 不...我的程序线程的,但只有一个线程需要访问它。
  • T 支持 IComparable 立即浮现在脑海的原因是,如果您将两个项目以优先级 2 推入队列,您仍然需要比较项目并决定处理这两个优先级的顺序两个项目。 . .所以最终你需要 T 具有可比性。那么如何使用您的界面来做到这一点......好吧,您在下面有一些很好的建议。
  • @Jason:这不会违背优先级队列的目的吗?这是一个队列,因为第一个进来的就是第一个出去的。就是这样决定的。否则它只是一个排序列表。
  • @Jason D:如果所有键都相同,那么优先级队列应该退化为 FIFO 队列。如果正在使用项目的顺序,那么这将被违反。因此,任何依赖于项目顺序的优先级队列的实现都是可疑的。

标签: c# data-structures queue


【解决方案1】:

这正是我的highly optimized C# priority-queue使用的接口。

它是专门为寻路应用程序(A* 等)开发的,但也可以完美地适用于任何其他应用程序。

public class User
{
    public string Name { get; private set; }
    public User(string name)
    {
        Name = name;
    }
}

...

var priorityQueue = new SimplePriorityQueue<User>();
priorityQueue.Enqueue(new User("Jason"), 1);
priorityQueue.Enqueue(new User("Joseph"), 10);

//Because it's a min-priority queue, the following line will return "Jason"
User user = priorityQueue.Dequeue();

【讨论】:

    【解决方案2】:

    有点晚了,我会在这里添加以供参考

    https://github.com/ERufian/Algs4-CSharp

    键值对优先级队列在 Algs4/IndexMaxPQ.cs、Algs4/IndexMinPQ.cs 和 Algs4/IndexPQDictionary.cs 中实现

    注意事项:

    1. 如果优先级不是 IComparable 的,则可以在构造函数中指定 IComparer
    2. 不是将对象及其优先级加入队列,而是加入一个索引及其优先级(对于原始问题,需要单独的 List 或 T[] 将该索引转换为预期结果)李>

    【讨论】:

      【解决方案3】:

      我知道您的问题特别要求基于非 IComparable 的实现,但我想指出 Visual Studio Magazine 最近的一篇文章。

      http://visualstudiomagazine.com/articles/2012/11/01/priority-queues-with-c.aspx

      这篇有@itowlson的文章可以给出完整的答案。

      【讨论】:

        【解决方案4】:

        这样的事情有什么可怕的?

        class PriorityQueue<TItem, TPriority> where TPriority : IComparable
        {
            private SortedList<TPriority, Queue<TItem>> pq = new SortedList<TPriority, Queue<TItem>>();
            public int Count { get; private set; }
        
            public void Enqueue(TItem item, TPriority priority)
            {
                ++Count;
                if (!pq.ContainsKey(priority)) pq[priority] = new Queue<TItem>();
                pq[priority].Enqueue(item);
            }
        
            public TItem Dequeue()
            {
                --Count;
                var queue = pq.ElementAt(0).Value;
                if (queue.Count == 1) pq.RemoveAt(0);
                return queue.Dequeue();
            }
        }
        
        class PriorityQueue<TItem> : PriorityQueue<TItem, int> { }
        

        【讨论】:

        • 看起来很整洁。 Unity——出于某种原因——不提供ElementAt()。所以我在那里使用了Values[0]
        【解决方案5】:

        这是一个非常简单的轻量级实现,对于推送和弹出都具有 O(log(n)) 的性能。它使用建立在 List 之上的heap data structure

        /// <summary>Implements a priority queue of T, where T has an ordering.</summary>
        /// Elements may be added to the queue in any order, but when we pull
        /// elements out of the queue, they will be returned in 'ascending' order.
        /// Adding new elements into the queue may be done at any time, so this is
        /// useful to implement a dynamically growing and shrinking queue. Both adding
        /// an element and removing the first element are log(N) operations. 
        /// 
        /// The queue is implemented using a priority-heap data structure. For more 
        /// details on this elegant and simple data structure see "Programming Pearls"
        /// in our library. The tree is implemented atop a list, where 2N and 2N+1 are
        /// the child nodes of node N. The tree is balanced and left-aligned so there
        /// are no 'holes' in this list. 
        /// <typeparam name="T">Type T, should implement IComparable[T];</typeparam>
        public class PriorityQueue<T> where T : IComparable<T> {
           /// <summary>Clear all the elements from the priority queue</summary>
           public void Clear () {
              mA.Clear ();
           }
        
           /// <summary>Add an element to the priority queue - O(log(n)) time operation.</summary>
           /// <param name="item">The item to be added to the queue</param>
           public void Add (T item) {
              // We add the item to the end of the list (at the bottom of the
              // tree). Then, the heap-property could be violated between this element
              // and it's parent. If this is the case, we swap this element with the 
              // parent (a safe operation to do since the element is known to be less
              // than it's parent). Now the element move one level up the tree. We repeat
              // this test with the element and it's new parent. The element, if lesser
              // than everybody else in the tree will eventually bubble all the way up
              // to the root of the tree (or the head of the list). It is easy to see 
              // this will take log(N) time, since we are working with a balanced binary
              // tree.
              int n = mA.Count; mA.Add (item);
              while (n != 0) {
                 int p = n / 2;    // This is the 'parent' of this item
                 if (mA[n].CompareTo (mA[p]) >= 0) break;  // Item >= parent
                 T tmp = mA[n]; mA[n] = mA[p]; mA[p] = tmp; // Swap item and parent
                 n = p;            // And continue
              }
           }
        
           /// <summary>Returns the number of elements in the queue.</summary>
           public int Count {
              get { return mA.Count; }
           }
        
           /// <summary>Returns true if the queue is empty.</summary>
           /// Trying to call Peek() or Next() on an empty queue will throw an exception.
           /// Check using Empty first before calling these methods.
           public bool Empty {
              get { return mA.Count == 0; }
           }
        
           /// <summary>Allows you to look at the first element waiting in the queue, without removing it.</summary>
           /// This element will be the one that will be returned if you subsequently call Next().
           public T Peek () {
              return mA[0];
           }
        
           /// <summary>Removes and returns the first element from the queue (least element)</summary>
           /// <returns>The first element in the queue, in ascending order.</returns>
           public T Next () {
              // The element to return is of course the first element in the array, 
              // or the root of the tree. However, this will leave a 'hole' there. We
              // fill up this hole with the last element from the array. This will 
              // break the heap property. So we bubble the element downwards by swapping
              // it with it's lower child until it reaches it's correct level. The lower
              // child (one of the orignal elements with index 1 or 2) will now be at the
              // head of the queue (root of the tree).
              T val = mA[0];
              int nMax = mA.Count - 1;
              mA[0] = mA[nMax]; mA.RemoveAt (nMax);  // Move the last element to the top
        
              int p = 0;
              while (true) {
                 // c is the child we want to swap with. If there
                 // is no child at all, then the heap is balanced
                 int c = p * 2; if (c >= nMax) break;
        
                 // If the second child is smaller than the first, that's the one
                 // we want to swap with this parent.
                 if (c + 1 < nMax && mA[c + 1].CompareTo (mA[c]) < 0) c++;
                 // If the parent is already smaller than this smaller child, then
                 // we are done
                 if (mA[p].CompareTo (mA[c]) <= 0) break;
        
                 // Othewise, swap parent and child, and follow down the parent
                 T tmp = mA[p]; mA[p] = mA[c]; mA[c] = tmp;
                 p = c;
              }
              return val;
           }
        
           /// <summary>The List we use for implementation.</summary>
           List<T> mA = new List<T> ();
        }
        

        【讨论】:

        • 他说他不想T必须是IComparable
        • 他写道“即使是那些使用 IComparable 的看起来也不是很好”,我对此做出了回应。关键是优先级队列不需要完全排序的列表;我们只需要知道在任何时候哪个是列表中的第一个元素。我使用的堆数据结构没有完全排序,但保持了足够的排序以有效地实现 log(n) 插入和删除。显然,这比成熟的二叉树要轻得多,并且总体上具有相同的性能。
        • 澄清一下:这个 PriorityQueue 使用的空间不比普通 List 多,并且具有 log(n) 插入和删除性能。
        • Tarydon 说得对,我可以将它与其他关于“生成” IComparable 的建议一起使用。然而,一个好的 PriorityQueue 应该删除 O(1),而不是 O(log(n))。
        • O(1) 删除意味着您始终维护一个排序列表。这将导致 n*log(n) 时间用于在幼稚的情况下插入(重新排序)。如果您要使用一个简单的线性列表并进行二进制搜索然后插入,那仍然需要 O(n) 时间来插入(移动元素以腾出空间)。而且,任何基于树的算法(如 SortedList)对于插入和删除都是 O(log(n))(因为重新平衡树需要 O(log(n)))。所以,我不确定我们是否可以通过 O(log(n)) 插入来实现 O(1) 删除。
        【解决方案6】:

        您可以添加安全检查等等,但这里有一个使用SortedList 的非常简单的实现:

        class PriorityQueue<T> {
            SortedList<Pair<int>, T> _list;
            int count;
        
            public PriorityQueue() {
                _list = new SortedList<Pair<int>, T>(new PairComparer<int>());
            }
        
            public void Enqueue(T item, int priority) {
                _list.Add(new Pair<int>(priority, count), item);
                count++;
            }
        
            public T Dequeue() {
                T item = _list[_list.Keys[0]];
                _list.RemoveAt(0);
                return item;
            }
        }
        

        我假设priority 的较小值对应于较高优先级的项目(这很容易修改)。

        如果多个线程将访问队列,您还需要添加锁定机制。这很简单,但如果您需要这里的指导,请告诉我。

        SortedList 在内部实现为二叉树。

        上述实现需要以下帮助类。此地址是 Lasse V. Karlsen 的评论,即无法使用使用 SortedList 的幼稚实现添加具有相同优先级的项目。

        class Pair<T> {
            public T First { get; private set; }
            public T Second { get; private set; }
        
            public Pair(T first, T second) {
                First = first;
                Second = second;
            }
        
            public override int GetHashCode() {
                return First.GetHashCode() ^ Second.GetHashCode();
            }
        
            public override bool Equals(object other) {
                Pair<T> pair = other as Pair<T>;
                if (pair == null) {
                    return false;
                }
                return (this.First.Equals(pair.First) && this.Second.Equals(pair.Second));
            }
        }
        
        class PairComparer<T> : IComparer<Pair<T>> where T : IComparable {
            public int Compare(Pair<T> x, Pair<T> y) {
                if (x.First.CompareTo(y.First) < 0) {
                    return -1;
                }
                else if (x.First.CompareTo(y.First) > 0) {
                    return 1;
                }
                else {
                    return x.Second.CompareTo(y.Second);
                }
            }
        }
        

        【讨论】:

        • SortedList 的问题是它不允许重复的优先级,所以它迫使你确保唯一的优先级。
        • 对我来说更有意义的是,更高的数字对应于更高的优先级。
        • 很容易修复,如果 SortedList 足够好,只需否定优先级值。
        • @Mark:就像我说的,这很容易解决。使用较低的数字对应较高的优先级只会使实现更简单,从而使想法更清晰。
        【解决方案7】:

        如果您有一个基于 IComparable 的现有优先级队列实现,您可以轻松地使用它来构建您需要的结构:

        public class CustomPriorityQueue<T>  // where T need NOT be IComparable
        {
          private class PriorityQueueItem : IComparable<PriorityQueueItem>
          {
            private readonly T _item;
            private readonly int _priority:
        
            // obvious constructor, CompareTo implementation and Item accessor
          }
        
          // the existing PQ implementation where the item *does* need to be IComparable
          private readonly PriorityQueue<PriorityQueueItem> _inner = new PriorityQueue<PriorityQueueItem>();
        
          public void Enqueue(T item, int priority)
          {
            _inner.Enqueue(new PriorityQueueItem(item, priority));
          }
        
          public T Dequeue()
          {
            return _inner.Dequeue().Item;
          }
        }
        

        【讨论】:

          【解决方案8】:

          您可以围绕现有实现之一编写一个包装器,将接口修改为您的偏好:

          using System;
          
          class PriorityQueueThatYouDontLike<T> where T: IComparable<T>
          {
              public void Enqueue(T item) { throw new NotImplementedException(); }
              public T Dequeue() { throw new NotImplementedException(); }
          }
          
          class PriorityQueue<T>
          {
              class ItemWithPriority : IComparable<ItemWithPriority>
              {
                  public ItemWithPriority(T t, int priority)
                  {
                      Item = t;
                      Priority = priority;
                  }
          
                  public T Item {get; private set;}
                  public int Priority {get; private set;}
          
                  public int CompareTo(ItemWithPriority other)
                  {
                      return Priority.CompareTo(other.Priority);
                  }
              }
          
              PriorityQueueThatYouDontLike<ItemWithPriority> q = new PriorityQueueThatYouDontLike<ItemWithPriority>();
          
              public void Enqueue(T item, int priority)
              {
                  q.Enqueue(new ItemWithPriority(item, priority));
              }
          
              public T Dequeue()
              {
                  return q.Dequeue().Item;
              }
          }
          

          这与 itowlson 的建议相同。我只是花了更长的时间来写我的,因为我填写了更多的方法。 :-s

          【讨论】:

          • 仍在尝试找到一个很好的 PriorityQueueThatYouDontLike 实现。即使是那些使用 IComparables 的看起来也不是很好。
          • 我这里有一个通用堆:vkarlsen.serveftp.com:81/websvn/…,用户名和密码都是“guest”(不带引号)。堆仍然成为您提到需要 IComparable 的“问题”的牺牲品,但您可以使用 Mark 发布的 ItemWithPriority 类。
          【解决方案9】:

          似乎您可以使用一系列队列来创建自己的队列,每个优先级一个。字典,只需将其添加到适当的字典中即可。

          【讨论】:

          • 这对 pop 的性能很差,因为你必须找到第一个非空队列。
          • @Yuliy:我一开始也是这么想的,但是如果我们在队列变空时弹出队列,这不是问题吗?
          • 您将在某处支付费用以管理优先级。如果你有一个链表,你将不得不在插入时遍历它以找到当前优先级的“结束”。管理优先级不是免费的。
          • 我猜你可以有一个平衡的队列二叉搜索树,为标准操作提供 log n 性能,但在这一点上,你还不如实现一个适当的堆。
          猜你喜欢
          • 2012-03-28
          • 1970-01-01
          • 1970-01-01
          • 2016-04-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-04-11
          • 2017-06-13
          相关资源
          最近更新 更多