【问题标题】:c# Adding a Remove(int index) method to the .NET Queue classc# 为.NET Queue 类添加 Remove(int index) 方法
【发布时间】:2010-10-06 13:58:02
【问题描述】:

我想使用 .NET 框架 (3.5) 中描述的通用队列类 但我需要一个 Remove(int index) 方法来从队列中删除项目。我可以使用扩展方法实现此功能吗?有人愿意为我指明正确的方向吗?

【问题讨论】:

    标签: c# queue


    【解决方案1】:

    有人可能会开发出更好的解决方案,但据我所知,您需要在 Remove 方法中返回一个新的 Queue 对象。您需要检查索引是否超出范围,并且我可能将添加的项目的顺序弄错了,但这里有一个快速而肮脏的示例,可以很容易地变成扩展。

    public class MyQueue<T> : Queue<T> {
        public MyQueue() 
            : base() {
            // Default constructor
        }
    
        public MyQueue(Int32 capacity)
            : base(capacity) {
            // Default constructor
        }
    
        /// <summary>
        /// Removes the item at the specified index and returns a new Queue
        /// </summary>
        public MyQueue<T> RemoveAt(Int32 index) {
            MyQueue<T> retVal = new MyQueue<T>(Count - 1);
    
            for (Int32 i = 0; i < this.Count - 1; i++) {
                if (i != index) {
                    retVal.Enqueue(this.ElementAt(i));
                }
            }
    
            return retVal;
        }
    }
    

    【讨论】:

    • 谢谢大卫,这看起来很有趣,但每次创建这样一个新队列肯定会有相当多的开销?也许我最好解决逆问题:只使用一个列表并制作我自己的 Queue、Peek 和 Dequeue 方法?
    • 几分钟前我写了一篇关于这个的博客,应该回答关于我发布的代码的任何问题。 renevo.com/blogs/developer/archive/2009/02/10/…
    • 您的 for 循环似乎不正确。我没有看到不遍历所有队列项目的理由。正确的是 (Int32 i = 0; i
    • 不得在 RemoveAt 内部调用 ElementAt。您已将复制队列的 O(n) 操作 - 已经很糟糕 - 更改为 O(n^2) 操作,因为 ElementAt 遍历所有元素直到传递的索引以找到它的元素 - 在 every循环的单次迭代。使用 foreach 并在 O(n) 时间内通过循环计数器跳过有问题的元素要好得多。
    • 我的实现 here 是 O(n) 并且修改了现有的 Queue&lt;T&gt; 而不是返回一个新的。
    【解决方案2】:

    你想要的是一个List&lt;T&gt;,当你想从Queue 获取项目时,你总是调用RemoveAt(0)。其他一切都一样,真的(调用Add 会在Queue 的末尾添加一个项目)。

    【讨论】:

    • 这样做的缺点是使Dequeues O(n) 而不是 O(1)
    • @CodeInChaos:当然,如果您使用队列,则假设您首先不需要随机访问。看起来是错误的数据结构。
    • 如果 removeat 只是偶尔发生,那么保留 Queue 结构是有意义的,但重建它减去一项。我已经添加了详细的答案。
    • @CodesInChaos 我意识到似乎会发生性能下降,但在发现Queues, Stacks, and Listss are all backed by an array 之后,我不再认为实现是理所当然的。您是否有基准建议或确认您的主张?正如this answer says,队列“会更明确地暴露你的意图”,但是大的O损失并不像看起来那么明显[对我来说]。 (我希望队列更快;但是是吗?)
    • @ruffin List&lt;T&gt; 在移除元素时移动所有后面的元素,这意味着在移除第一个元素时移动所有元素。 Queue&lt;T&gt; 使用支持数组作为环形缓冲区,可以实现快速出队。
    【解决方案3】:

    事实上,这违背了 Queue 的全部目的,并且您最终会提出的类将完全违反 FIFO 语义。

    【讨论】:

    • 是的,这适用于任何专业收藏。 (堆栈,队列,其他)。如果它不符合您的要求,那么它就不是正确的集合。
    【解决方案4】:

    David Anderson's solution 可能是最好的,但有一些开销。 您是否在队列中使用自定义对象?如果是这样,添加一个类似取消的布尔值

    如果设置了该布尔值,请与处理队列的工作人员核实,然后跳过它。

    【讨论】:

    • 不要在回答中提问。 :-)
    【解决方案5】:

    队列类很难理解。请改用通用列表。

    【讨论】:

    • 相反,很容易理解。与通用列表相比,您只需多花几分钟时间来了解它并打开一个全新的简单世界。
    【解决方案6】:

    将 casperOne 和 David Anderson 的建议结合到一个新的水平。下面的类继承自 List 并隐藏了对 FIFO 概念有害的方法,同时添加了三个 Queue 方法(Equeue、Dequeu、Peek)。

    public class ListQueue<T> : List<T>
    {
        new public void Add(T item) { throw new NotSupportedException(); }
        new public void AddRange(IEnumerable<T> collection) { throw new NotSupportedException(); }
        new public void Insert(int index, T item) { throw new NotSupportedException(); }
        new public void InsertRange(int index, IEnumerable<T> collection) { throw new NotSupportedException(); }
        new public void Reverse() { throw new NotSupportedException(); }
        new public void Reverse(int index, int count) { throw new NotSupportedException(); }
        new public void Sort() { throw new NotSupportedException(); }
        new public void Sort(Comparison<T> comparison) { throw new NotSupportedException(); }
        new public void Sort(IComparer<T> comparer) { throw new NotSupportedException(); }
        new public void Sort(int index, int count, IComparer<T> comparer) { throw new NotSupportedException(); }
    
        public void Enqueue(T item)
        {
            base.Add(item);
        }
    
        public T Dequeue()
        {
            var t = base[0]; 
            base.RemoveAt(0);
            return t;
        }
    
        public T Peek()
        {
            return base[0];
        }
    }
    

    测试代码:

    class Program
    {
        static void Main(string[] args)
        {
            ListQueue<string> queue = new ListQueue<string>();
    
            Console.WriteLine("Item count in ListQueue: {0}", queue.Count);
            Console.WriteLine();
    
            for (int i = 1; i <= 10; i++)
            {
                var text = String.Format("Test{0}", i);
                queue.Enqueue(text);
                Console.WriteLine("Just enqueued: {0}", text);
            }
    
            Console.WriteLine();
            Console.WriteLine("Item count in ListQueue: {0}", queue.Count);
            Console.WriteLine();
    
            var peekText = queue.Peek();
            Console.WriteLine("Just peeked at: {0}", peekText);
            Console.WriteLine();
    
            var textToRemove = "Test5";
            queue.Remove(textToRemove);
            Console.WriteLine("Just removed: {0}", textToRemove);
            Console.WriteLine();
    
            var queueCount = queue.Count;
            for (int i = 0; i < queueCount; i++)
            {
                var text = queue.Dequeue();
                Console.WriteLine("Just dequeued: {0}", text);
            }
    
            Console.WriteLine();
            Console.WriteLine("Item count in ListQueue: {0}", queue.Count);
    
            Console.WriteLine();
            Console.WriteLine("Now try to ADD an item...should cause an exception.");
            queue.Add("shouldFail");
    
        }
    }
    

    【讨论】:

      【解决方案7】:

      如果队列用于保留集合中项目的顺序,并且您不会有重复的项目,那么SortedSet 可能就是您要查找的内容。 SortedSet 的行为很像 List&lt;T&gt;,但保持有序。非常适合下拉选择。

      【讨论】:

      • SortedSet 仅在 .NET 4 或更高版本中可用。
      【解决方案8】:

      我不认为我们应该使用List&lt;T&gt; 来模拟队列,对于队列,入队和出队操作应该非常高效,而使用List&lt;T&gt; 时则不会。但是,对于 RemoveAt 方法,可以接受非性能,因为它不是 Queue&lt;T&gt; 的主要目的。

      我在实现RemoveAt 的方法是 O(n),但队列仍然维持在很大程度上 O(1) 入队(有时内部数组需要重新分配,这使得操作 O(n))并且总是 O(1) 出队.

      这是我为Queue&lt;T&gt; 实现的RemoveAt(int) 扩展方法:

      public static void RemoveAt<T>(this Queue<T> queue, int index)
      {
          Contract.Requires(queue != null);
          Contract.Requires(index >= 0);
          Contract.Requires(index < queue.Count);
      
          var i = 0;
      
          // Move all the items before the one to remove to the back
          for (; i < index; ++i)
          {
              queue.MoveHeadToTail();
          }
      
          // Remove the item at the index
          queue.Dequeue();
      
          // Move all subsequent items to the tail end of the queue.
          var queueCount = queue.Count;
          for (; i < queueCount; ++i)
          {
              queue.MoveHeadToTail();
          }
      }
      

      其中MoveHeadToTail定义如下:

      private static void MoveHeadToTail<T>(this Queue<T> queue)
      {
          Contract.Requires(queue != null);
      
          var dequed = queue.Dequeue();
          queue.Enqueue(dequed);
      }
      

      这个实现还修改了实际的Queue&lt;T&gt;,而不是返回一个新的Queue&lt;T&gt;(我认为这与其他RemoveAt 实现更一致)。

      【讨论】:

        【解决方案9】:

        这是一个很晚的答案,但我写它是为了未来的读者

        List&lt;T&gt; 正是您所需要的,但与Queue&lt;T&gt; 相比,它有一个很大的缺点:它是用数组实现的,然后Dequeue() 相当广泛(就时间而言),因为所有项目都必须移动一步回来Array.Copy。甚至Queue&lt;T&gt; 也使用了一个数组,但还有两个索引(用于头部和尾部)。

        在您的情况下,您还需要 Remove/RemoveAt 并且它的性能不会很好(出于同样的原因:如果您不从列表尾部删除,则必须分配另一个数组并复制项目)。

        具有快速Dequeue/Remove 时间的更好的数据结构是链表(您将牺牲-一点-Enqueue 的性能,但假设您的队列有相同数量的Enqueue/ Dequeue 操作,您将获得巨大的性能提升,尤其是当它的大小增加时)。

        让我们看一个简单的实现框架(我将跳过IEnumerable&lt;T&gt;IList&lt;T&gt; 和其他辅助方法的实现)。

        class LinkedQueue<T>
        {
            public int Count
            {
                get { return _items.Count; }
            }
        
            public void Enqueue(T item)
            {
                _items.AddLast(item);
            }
        
            public T Dequeue()
            {
                if (_items.First == null)
                   throw new InvalidOperationException("...");
        
                var item = _items.First.Value;
                _items.RemoveFirst();
        
                return item;
            }
        
            public void Remove(T item)
            {
                _items.Remove(item);
            }
        
            public void RemoveAt(int index)
            {
                Remove(_items.Skip(index).First());
            }
        
            private LinkedList<T> _items = new LinkedList<T>();
        }
        

        快速比较:

        队列列表 LinkedList 入队 O(1)/O(n)* O(1)/O(n)* O(1) 出队 O(1) O(n) O(1) 删除 n/a O(n) O(n)

        * O(1) 是 典型 的情况,但 有时 它会是 O(n)(当内部数组需要调整大小时)。

        当然,您会为获得的收益付出一些代价:内存使用量更大(尤其是对于较小的T 开销会很大)。正确的实现(List&lt;T&gt; vs LinkedList&lt;T&gt;)必须根据您的使用场景谨慎选择,您也可以将该代码转换为使用单链表以减少 50% 的内存开销。

        【讨论】:

          【解决方案10】:

          以下是使用一行 Linq 从队列中删除特定项的方法(它正在重新创建队列,但由于缺乏更好的方法...)

          //replace "<string>" with your actual underlying type
          myqueue = new Queue<string>(myqueue.Where(s => s != itemToBeRemoved));
          

          我知道它不是 by index 删除,但仍然有人会觉得这很有用(这个问题在 Google 中排名为“从 ac# 队列中删除特定项目”,所以我决定添加这个答案,对不起)

          【讨论】:

          • 这也有助于过滤多个项目,如果您有一个包含许多您希望删除的项目的队列,myqueue= new Queue&lt;object&gt;(myqueue.Where(x =&gt; x.Param != "foo"))
          【解决方案11】:

          请注意,如果您不实际删除项目而只是将其“标记”为“已删除”,则使用列表可以使“删除”过程更有效。是的,你必须添加一些代码来处理你是如何做到的,但回报是效率。

          举个例子——假设你有一个List&lt;string&gt;。然后,例如,您可以将该特定项目设置为 null 并完成它。

          【讨论】:

            【解决方案12】:

            虽然没有内置方式,但不应该使用 List 结构或其他结构,IFF RemoveAt 不是常用操作。

            如果您通常在入队和出队但只是偶尔移除,那么您应该能够在移除时重新构建队列。

            public static void Remove<T>(this Queue<T> queue, T itemToRemove) where T : class
            {
                var list = queue.ToList(); //Needs to be copy, so we can clear the queue
                queue.Clear();
                foreach (var item in list)
                {
                    if (item == itemToRemove)
                        continue;
            
                    queue.Enqueue(item);
                }
            }
            
            public static void RemoveAt<T>(this Queue<T> queue, int itemIndex) 
            {
                var list = queue.ToList(); //Needs to be copy, so we can clear the queue
                queue.Clear();
            
                for (int i = 0; i < list.Count; i++)
                {
                    if (i == itemIndex)
                        continue;
            
                    queue.Enqueue(list[i]);
                }
            }
            

            以下方法可能更高效,使用更少的内存,从而减少 GC:

            public static void RemoveAt<T>(this Queue<T> queue, int itemIndex)
            {
                var cycleAmount = queue.Count;
            
                for (int i = 0; i < cycleAmount; i++)
                {
                    T item = queue.Dequeue();
                    if (i == itemIndex)
                        continue;
            
                    queue.Enqueue(item);
                }
            }
            

            【讨论】:

            • 当 Enqueue/Dequeue 操作比 remove 方法高得多时,我更喜欢这个。谢谢托德
            • 这在偶尔移除队列中的元素时非常棒。频繁删除元素时可能需要其他实现或选择其他数据结构。无论如何,这个答案很棒,谢谢!
            猜你喜欢
            • 2020-06-05
            • 2023-04-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-06-15
            • 2020-06-02
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多