【问题标题】:Efficient .NET pair processing高效的 .NET 对处理
【发布时间】:2011-05-29 04:06:39
【问题描述】:

以下代码允许您处理所有可能的对象对(其中 DoSomething(a,b) 等效于 DoSomething(b,a) 并且您不想同时执行这两个操作,并且您永远不需要 DoSomething(a ,a)):

        void MyMethod (MyThing[] myArray)
        {
        for (int j = 0; j < (myArray.Length-1); ++j)
            {
            for (int k = j+1; k < myArray.Length; ++k)
                {
                DoSomething(myArray[j], myArray[k]);
                }
            }
        }

我知道这大约是 n*n/2 次操作。

如果使用List&lt;MyThing&gt; 而不是数组,是否有同样有效的方法来处理所有可能的对? (特别是一个解决方案,我不必在内部循环中旋转已经完成的元素)。也许以某种方式使用枚举器?

数组不好,因为我事先不知道我需要多少 MyThings(可能是 0,可能永远不会超过 1000)。对于这种特殊用途,有没有比List&lt;&gt; 更好的集合?我不需要排序,我只需要创建新集合、清除现有集合、添加到集合、枚举集合或处理上述配对。无论我使用什么集合,我都可能需要 100 到 10000 个,因此创建/保存它们的成本不会太高。

假设 DoSomething() 正在对移动对象执行碰撞检测和响应。

【问题讨论】:

    标签: c# arrays list collections generics


    【解决方案1】:

    有没有同样有效的方法 处理所有可能的对,如果 使用列表而不是 数组?

    List&lt;T&gt; 只是 T[] 的精美包装,当超出列表容量时,它会偶尔调整大小。

    你的算法已经是最优的了。如果您想将它与List&lt;T&gt; 一起使用,那么只需将声明从void MyMethod (MyThing[] myArray) 更改为void MyMethod (List&lt;MyThing&gt; myArray)

    有没有更好的收藏 比 List 更特殊的用法?我不 需要排序,我只新建 收藏,清除现有 收藏,添加到收藏, 枚举一个集合或进程 配对如上。随便收藏 我使用,我可能需要 100 到 10000 个 他们,所以他们不会太昂贵 创建/保留。

    首先让我们试着了解一些关于数据结构的东西:

    在内部,List&lt;T&gt; 保存一个大小为 N 的数组;当您将项目添加到数组中时,如果超出内部数组的大小,则列表将允许大小为 N*2 的新数组,复制元素,然后添加新元素。调整大小的最坏情况为 O(n);但是,每次调整大小时将数组加倍意味着您必须添加两倍于以前的元素才能触发最坏情况的行为。列表有一个属性,可以为它们提供amortized O(1) 插入,这意味着您可以在 O(n) 时间内执行 n 次操作。

    通常,LinkedList 的插入速度非常快。据我所知,它不使用底层数组,而是有一个节点集合,其中包含指向集合中相邻项目的 Next 和 Previous 指针。从好的方面来说,最坏情况下的插入是 O(1),但由于引用的局部性较差(即列表中的相邻项在内存中不相邻),有时链表在理论上可能低于最佳性能。

    我个人从未见过迭代数组比链表慢的场景。在您过多考虑引用的局部性之前,我肯定会先考虑一个乏味的链表。

    话虽如此,如果您真的想要一个具有良好引用局部性的动态大小的集合,而且还支持快速插入,那么请尝试VList。它具有您正在寻找的两个属性,并且非常容易编写:

    public class VList<T> : IEnumerable<T>
    {
        VListNode<T> RootNode;
        public int Count { get; private set; }
    
        public VList() : this(4) { }
    
        public VList(int size)
        {
            RootNode = new VListNode<T>(4, null);
        }
    
        public void Add(T element)
        {
            if (RootNode.Count == RootNode.MaxSize)
                RootNode = new VListNode<T>(RootNode.MaxSize * 2, RootNode);
            RootNode.Add(element);
            Count++;
        }
    
        public void Clear()
        {
            RootNode = new VListNode<T>(4, null);
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            VListNode<T> node = RootNode;
            while (node != null)
            {
                foreach (T t in node)
                    yield return t;
                node = node.Next;
            }
        }
    
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    }
    
    public class VListNode<T> : IEnumerable<T>
    {
        readonly T[] Elements;
        public VListNode<T> Next { get; private set; }
        public int Count { get; private set; }
        public int MaxSize { get; private set; }
    
        public VListNode(int size, VListNode<T> next)
        {
            MaxSize = size;
            Elements = new T[size];
            Next = next;
        }
    
        public void Add(T element)
        {
            Elements[Count] = element;
            Count++;
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            // iterate in reverse to return elements in LIFO order.
            for (int i = Count - 1; i >= 0; i--)
                yield return Elements[i];
        }
    
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
    }
    

    上面的简单实现应该支持 Add in O(1),同时保持良好的引用局部性。

    【讨论】:

      【解决方案2】:

      在这种情况下,总是有机会记住算法,但如果不了解DoSomething 的更多细节,就不可能说出可以做什么。

      例如,假设DoSomething 看起来像这样:

      void DoSomething(MyThing arg1, MyThing arg2)
      {
          // Let's assume MyThing.Value is an int
          // and you want to print the product of both values
          Console.WriteLine(arg1.Value * arg2.Value);
      }
      

      在这种情况下,我们可以跟踪内存中传递了哪些参数,并且只为我们还没有看到的参数组合调用方法。当然,这仅适用于需要大量执行时间来保证记忆开销的 DoSomething 实现。

      【讨论】:

      • 每个 MyThing 都在不断变化,因此必须定期重新评估配对。新的 MyThings 可能随时创建。记忆化似乎仍然合理吗?
      • @Stomp - 可以,但听起来不太可能。老实说,DoSomething 的实施应该是推动决策的因素。
      • 假设 DoSomething() 正在对移动对象执行碰撞检测和响应。 (我将此添加到问题中)
      • 好的,有什么方法可以发布实现吗?如果不能,你能发一份合理的传真吗?
      • 基本上我只是在质疑我对 List 的使用(它很容易使用)并希望有更好的数据结构支持 nn/2(而不是 n n) 用于配对处理的循环...
      【解决方案3】:

      一次取 2 个 N 项的唯一组合数为 N!/(2!(N-2)!) = N(N-1)/2 ~= O(n^2)。要对列表中两个项目的每一个组合执行一个操作是不可能的。

      就要使用的集合而言,List 将直接放入您拥有 Array 的位置,只需进行一次更改; List 使用 Count 属性来确定基数,而不是 Length。

      至于将 As 和 Bs 放在一起的更优雅的方式,Linq 确实有一些优势:

      var combinations = 
          from a in myThings.Reverse()
          from b in myThings.TakeWhile(x=>x!=a)
          select new {a,b};
      
      foreach(var combo in combinations)
         DoSomething(combo.a, combo.b);
      

      这仍然会比你原来的算法慢,但我认为它会比 cdhowie 快一点,因为它只会遍历 N 个额外的项目(创建 Reverse() 可枚举),而不是跳过N(N-1) 作为 cdhowie 的内部 foreach 最终做的。

      【讨论】:

      • Keith,我试图为列表实现 nn/2,即试图避免 nn。我喜欢 Linq,感谢您的贡献!
      • 大值平方的一半仍然是大值。但是,就像我说的,你不能再简单了。该算法的复杂度约为 N*(N-1)/2 + N,可以通过使用非 Linq 解决方案来改进(for 循环遍历索引)。
      【解决方案4】:

      List&lt;T&gt; 可以正常工作。您可以将方法签名中的MyThing[] 替换为List&lt;MyThing&gt;

      如果您正在寻找一种适用于可枚举的解决方案,那简直就是地狱:

      void MyMethod<T>(IEnumerable<T> myThings, Action<T, T> action)
      {
          int index = 0;
          foreach (var a in myThings)
              foreach (var b in myThings.Skip(++index))
                  action(a, b);
      }
      

      请注意,这会慢一些,因为Skip 实际上会遍历跳过的元素。当您接近列表末尾时,这可能会抵消您从内存缓存中获得的任何好处,当然它会浪费时间丢弃前 N 个元素。

      【讨论】:

      • 专门试图避免跳过大约 n*n/2 次跳过是低效的假设。
      • -1:外循环为 O(n)。内循环是 O(n)。 enumerable.skip(n) 是 O(n),而不是 O(1),这使得整个事情 O(n^3)。这比 OP 的解决方案效率低很多。
      • @Juliet:OP 特别提到他对可枚举感兴趣,所以我想我会提供该代码的示例。我明确指出,它的性能将低于基于列表的方法。
      猜你喜欢
      • 2013-09-07
      • 2023-02-09
      • 2014-03-16
      • 2017-02-24
      • 2016-05-26
      • 1970-01-01
      • 2012-11-04
      • 2010-10-29
      • 1970-01-01
      相关资源
      最近更新 更多