【问题标题】:Most efficient algorithm for merging sorted IEnumerable<T>合并排序的 IEnumerable<T> 的最有效算法
【发布时间】:2022-03-11 23:56:55
【问题描述】:

我有几个巨大的要合并的可枚举序列。这些列表被处理为IEnumerable,但已经排序。由于输入列表已排序,因此应该可以在一次行程中合并它们,而无需重新排序。

我想保留延迟执行的行为。

我试图编写一个简单的算法来做到这一点(见下文)。但是,它看起来很丑陋,我相信它可以优化。它可能存在更学术的算法...

IEnumerable<T> MergeOrderedLists<T, TOrder>(IEnumerable<IEnumerable<T>> orderedlists, 
                                            Func<T, TOrder> orderBy)
{
    var enumerators = orderedlists.ToDictionary(l => l.GetEnumerator(), l => default(T));
    IEnumerator<T> tag = null;

    var firstRun = true;
    while (true)
    {
        var toRemove = new List<IEnumerator<T>>();
        var toAdd = new List<KeyValuePair<IEnumerator<T>, T>>();
        foreach (var pair in enumerators.Where(pair => firstRun || tag == pair.Key))
        {
            if (pair.Key.MoveNext())
                toAdd.Add(pair);
            else
                toRemove.Add(pair.Key);
        }

        foreach (var enumerator in toRemove)
            enumerators.Remove(enumerator);

        foreach (var pair in toAdd)
            enumerators[pair.Key] = pair.Key.Current;

        if (enumerators.Count == 0)
            yield break;

        var min = enumerators.OrderBy(t => orderBy(t.Value)).FirstOrDefault();
        tag = min.Key;
        yield return min.Value;

        firstRun = false;
    }
}

方法可以这样使用:

// Person lists are already sorted by age
MergeOrderedLists(orderedList, p => p.Age);

假设以下Person 类存在于某处:

    public class Person
    {
        public int Age { get; set; }
    }

应该保留重复,我们不关心它们在新序列中的顺序。您看到我可以使用的任何明显优化吗?

【问题讨论】:

    标签: c# linq performance algorithm optimization


    【解决方案1】:

    这是我的第四次(感谢@tanascius 将其推向更多的 LINQ):

    public static IEnumerable<T> MergePreserveOrder3<T, TOrder>(
        this IEnumerable<IEnumerable<T>> aa,
        Func<T, TOrder> orderFunc)
    where TOrder : IComparable<TOrder>
    {
        var items = aa.Select(xx => xx.GetEnumerator()).Where(ee => ee.MoveNext())
            .OrderBy(ee => orderFunc(ee.Current)).ToList();
    
        while (items.Count > 0)
        {
            yield return items[0].Current;
    
            var next = items[0];
            items.RemoveAt(0);
            if (next.MoveNext())
            {
                // simple sorted linear insert
                var value = orderFunc(next.Current);
                var ii = 0;
                for ( ; ii < items.Count; ++ii)
                {
                    if (value.CompareTo(orderFunc(items[ii].Current)) <= 0)
                    {
                        items.Insert(ii, next);
                        break;
                    }
                }
    
                if (ii == items.Count) items.Add(next);
            }
            else next.Dispose(); // woops! can't forget IDisposable
        }
    }
    

    结果:

    for (int p = 0; p < people.Count; ++p)
    {
        Console.WriteLine("List {0}:", p + 1);
        Console.WriteLine("\t{0}", String.Join(", ", people[p].Select(x => x.Name)));
    }
    
    Console.WriteLine("Merged:");
    foreach (var person in people.MergePreserveOrder(pp => pp.Age))
    {
        Console.WriteLine("\t{0}", person.Name);
    }
    
    List 1:
            8yo, 22yo, 47yo, 49yo
    List 2:
            35yo, 47yo, 60yo
    List 3:
            28yo, 55yo, 64yo
    Merged:
            8yo
            22yo
            28yo
            35yo
            47yo
            47yo
            49yo
            55yo
            60yo
            64yo
    

    通过 .Net 4.0 的元组支持得到改进:

    public static IEnumerable<T> MergePreserveOrder4<T, TOrder>(
        this IEnumerable<IEnumerable<T>> aa,
        Func<T, TOrder> orderFunc) where TOrder : IComparable<TOrder>
    {
        var items = aa.Select(xx => xx.GetEnumerator())
                      .Where(ee => ee.MoveNext())
                      .Select(ee => Tuple.Create(orderFunc(ee.Current), ee))
                      .OrderBy(ee => ee.Item1).ToList();
    
        while (items.Count > 0)
        {
            yield return items[0].Item2.Current;
    
            var next = items[0];
            items.RemoveAt(0);
            if (next.Item2.MoveNext())
            {
                var value = orderFunc(next.Item2.Current);
                var ii = 0;
                for (; ii < items.Count; ++ii)
                {
                    if (value.CompareTo(items[ii].Item1) <= 0)
                    {   // NB: using a tuple to minimize calls to orderFunc
                        items.Insert(ii, Tuple.Create(value, next.Item2));
                        break;
                    }
                }
    
                if (ii == items.Count) items.Add(Tuple.Create(value, next.Item2));
            }
            else next.Item2.Dispose(); // woops! can't forget IDisposable
        }
    }
    

    【讨论】:

    • 很好,它比我糟糕的版本更干净,性能也更好。如果您考虑另一个版本,请不要犹豫更新您的答案。谢谢!
    • 最新切割 MergePreserveOrder2 是线性 w.r.t。人数或名单。我的原版和你的原版在增长方面都差得多。
    • 好的,我认为这是根据我的需要的最佳解决方案。该算法似乎只执行必要的操作,这在性能方面可能是最佳的,并且仍然易于阅读/理解。
    • 在很多情况下items.Any() 不会比items.Count 快很多吗?在内存列表中可能会慢一点,但如果任何枚举器实际上是延迟加载或使用yield,那么.Any() 应该会快很多。
    • itemsList&lt;T&gt;,所以items.Count 将是O(1)
    【解决方案2】:

    我认为可能会提高清晰度和性能的一个猜测是:

    • 根据T 上的比较函数对TIEnumerable&lt;T&gt; 对创建优先级队列
    • 对于每个要合并的 IEnumerable&lt;T&gt;,将项目添加到优先级队列中,并用引用 IEnumerable&lt;T&gt; 进行注释
    • 虽然优先队列不为空
      • 从优先队列中提取最小元素
      • 将注释中的 IEnumerable&lt;T&gt; 推进到下一个元素
      • 如果 MoveNext() 返回 true,则将下一个元素添加到优先级队列中,并使用对您刚刚推进的 IEnumerable&lt;T&gt; 的引用进行注释
      • 如果 MoveNext() 返回 false,则不要向优先级队列添加任何内容
      • 产生出队的元素

    【讨论】:

    • 顺便说一下,这也是构建并发合并排序的方式。
    • 我这里有这样一个实现:svn.vkarlsen.no:81/public/filedetails.php?repname=LVK&path=/…,使用Ctrl+F并搜索MergeSorted。
    • 请注意,这个答案中几乎所有IEnumerable 的用法实际上都应该是IEnumerator。你不需要提前一个IEnumerable,你只是得到一个IEnumerator。您推进IEnumerator。你也不需要Tuple&lt;T, IEnumerator&lt;T&gt;&gt;,你可以只拥有IEnumerator&lt;T&gt;,并在你想要序列中的当前项目时使用IEnumerator.Current
    【解决方案3】:

    您预计需要合并多少个列表?如果您有许多不同的列表要合并,您的算法似乎效率不高。这一行是问题:

    var min = enumerators.OrderBy(t => orderBy(t.Value)).FirstOrDefault();
    

    这将对所有列表中的每个元素运行一次,因此您的运行时间将为 O(n * m),其中 n 是所有列表中元素的总数,n 是列表的数量。以列表列表中一个列表的平均长度表示,运行时间为 O(a * m^2)。

    如果您需要合并很多列表,我建议您使用heap。然后每次迭代都可以从堆中移除最小值,然后从最小值所在的列表中将下一个元素添加到堆中。

    【讨论】:

    • 这是一个很好的观察。但我会说要合并的列表最多可能是 2 - 10 个。
    • +1,假设您的意思是将堆作为优先级队列实现
    【解决方案4】:

    这是一个没有排序的解决方案......只是最少的比较次数。 (为简单起见,我省略了实际的 order func 传递)。更新以构建平衡树:-

        /// <summary>
        /// Merge a pair of ordered lists
        /// </summary>
        public static IEnumerable<T> Merge<T>(IEnumerable<T> aList, IEnumerable<T> bList)
            where T:IComparable<T>
        {
            var a = aList.GetEnumerator();
            bool aOK = a.MoveNext();
    
            foreach (var b in bList)
            {
                while (aOK && a.Current.CompareTo(b) <= 0) {yield return a.Current; aOK = a.MoveNext();}
                yield return b;
            }
            // And anything left in a
            while (aOK) { yield return a.Current; aOK = a.MoveNext(); }
        }
    
        /// <summary>
        /// Merge lots of sorted lists
        /// </summary>
        public static IEnumerable<T> Merge<T>(IEnumerable<IEnumerable<T>> listOfLists)
            where T : IComparable<T>
        {
            int n = listOfLists.Count();
            if (n < 2) 
                return listOfLists.FirstOrDefault();
            else
                return Merge (Merge(listOfLists.Take(n/2)), Merge(listOfLists.Skip(n/2)));
        }
    
    
    public static void Main(string[] args)
    {
    
        var sample = Enumerable.Range(1, 5).Select((i) => Enumerable.Range(i, i+5).Select(j => string.Format("Test {0:00}", j)));
    
        Console.WriteLine("Merged:");
        foreach (var result in Merge(sample))
        {
            Console.WriteLine("\t{0}", result);
        }
    

    【讨论】:

    • listOfLists.FirstOrDefault() joelonsoftware.com/articles/fog0000000319.html
    • 这是一个非常聪明的解决方案......对于少量列表也很快。 @Craig:它值得不仅仅是一个简单的“这就是我会做的事情”。但是对于很多列表,它的表现会很差。
    • 你想让我说什么?他提出了一个与我的非常相似的解决方案。因此,我没有两次发布相同的内容。当然,在 Merge listOfLists 例程中,不断构建另一个可枚举可能会更快,但是我们在这里真正讨论了多少个列表?过早的优化可能是个问题。这是一个非常简单的解决方案,它是惰性枚举并解决了问题。
    • @tanscius - 用改进的平衡树方法再看看。
    • 它不再编译 ^^ Merge() 没有重载需要两个 IEnumerables。但我想这不会有任何帮助 - 你的方法的问题是,不同列表的元素之间的比较是一个很大的开销(见丹尼尔的回答)。但同样,这只发生在大量列表中(2-5 肯定不是问题)。
    【解决方案5】:

    这是一个具有非常好的复杂性分析的解决方案,并且比提出的其他解决方案要短得多。

    public static IEnumerable<T> Merge<T>(this IEnumerable<IEnumerable<T>> self) 
        where T : IComparable<T>
    {
        var es = self.Select(x => x.GetEnumerator()).Where(e => e.MoveNext());
        var tmp = es.ToDictionary(e => e.Current);
        var dict = new SortedDictionary<T, IEnumerator<T>>(tmp);
        while (dict.Count > 0)
        {
            var key = dict.Keys.First();
            var cur = dict[key];
            dict.Remove(key);
            yield return cur.Current;
            if (cur.MoveNext())
                dict.Add(cur.Current, cur);                    
        }
    }
    

    【讨论】:

    • 看起来它的内存使用量也很有限
    • 如果两个比较相等的元素包含在两个不同的序列中,则此实现将失败。 (在 es.ToDictionary 或 dict.Add 中)。如果有可能,您需要使用真正的优先级队列。
    • 你应该在你的枚举器上调用 Dispose()
    【解决方案6】:

    这是我的解决方案:
    该算法采用每个列表的第一个元素,并将它们放入一个小的帮助类(一个接受具有相同值的多个元素的排序列表)中。此排序列表使用二进制插入
    所以这个列表中的第一个元素就是我们接下来要返回的元素。这样做之后,我们将其从排序列表中删除并插入其原始源列表中的下一个元素(至少只要此列表包含更多元素)。同样,我们可以返回排序列表的第一个元素。当排序列表为空一次时,我们使用来自所有不同源列表的所有元素并完成。

    此解决方案在每个步骤中使用较少的 foreach 语句并且不使用 OrderBy - 这应该会改善运行时行为。只有二​​进制插入必须一次又一次地完成。

    IEnumerable<T> MergeOrderedLists<T, TOrder>( IEnumerable<IEnumerable<T>> orderedlists, Func<T, TOrder> orderBy )
    {
        // Get an enumerator for each list, create a sortedList
        var enumerators = orderedlists.Select( enumerable => enumerable.GetEnumerator() );
        var sortedEnumerators = new SortedListAllowingDoublets<TOrder, IEnumerator<T>>();
    
        // Point each enumerator onto the first element
        foreach( var enumerator in enumerators )
        {
            // Missing: assert true as the return value
            enumerator.MoveNext();
    
            //  Initially add the first value
            sortedEnumerators.AddSorted( orderBy( enumerator.Current ), enumerator );
        }
    
        // Continue as long as we have elements to return
        while( sortedEnumerators.Count != 0 )
        {
            // The first element of the sortedEnumerator list always
            // holds the next element to return
            var enumerator = sortedEnumerators[0].Value;
    
            // Return this enumerators current value
            yield return enumerator.Current;
    
            // Remove the element we just returned
            sortedEnumerators.RemoveAt( 0 );
    
            // Check if there is another element in the list of the enumerator
            if( enumerator.MoveNext() )
            {
                // Ok, so add it to the sorted list
                sortedEnumerators.AddSorted( orderBy( enumerator.Current ), enumerator );
            }
        }
    

    我的助手类(使用简单的二进制插入):

    private class SortedListAllowingDoublets<TOrder, T> : Collection<KeyValuePair<TOrder, T>> where T : IEnumerator
    {
        public void AddSorted( TOrder value, T enumerator )
        {
            Insert( GetSortedIndex( value, 0, Count - 1 ), new KeyValuePair<TOrder, T>( value, enumerator ) );
        }
    
        private int GetSortedIndex( TOrder item, int startIndex, int endIndex )
        {
            if( startIndex > endIndex )
            {
                return startIndex;
            }
            var midIndex = startIndex + ( endIndex - startIndex ) / 2;
            return Comparer<TOrder>.Default.Compare( this[midIndex].Key, item ) < 0 ? GetSortedIndex( item, midIndex + 1, endIndex ) : GetSortedIndex( item, startIndex, midIndex - 1 );
        }
    }
    

    现在没有实现的地方:检查一个空列表,这会导致问题。
    并且SortedListAllowingDoublets 类可以改进为采用比较器,而不是单独使用Comparer&lt;TOrder&gt;.Default

    【讨论】:

    • 这至少比我的更优雅。我喜欢它。
    • 你试过用链表代替集合吗?似乎可以更快。
    • @Gabe:不,我没有尝试使用链表。这个答案是我刚刚为 OP 编写的解决方案——我也没有使用分析器。但我很乐观,它是一个快速的算法(不过可以进一步改进^^)
    【解决方案7】:

    这是一个基于 Wintellect's OrderedBag 的 Linq 友好解决方案:

    public static IEnumerable<T> MergeOrderedLists<T, TOrder>(this IEnumerable<IEnumerable<T>> orderedLists, Func<T, TOrder> orderBy)
        where TOrder : IComparable<TOrder>
    {
        var enumerators = new OrderedBag<IEnumerator<T>>(orderedLists
            .Select(enumerable => enumerable.GetEnumerator())
            .Where(enumerator => enumerator.MoveNext()),
            (x, y) => orderBy(x.Current).CompareTo(orderBy(y.Current)));
        while (enumerators.Count > 0)
        {
            IEnumerator<T> minEnumerator = enumerators.RemoveFirst();
            T minValue = minEnumerator.Current;
            if (minEnumerator.MoveNext())
                enumerators.Add(minEnumerator);
            else
                minEnumerator.Dispose();
            yield return minValue;
        }
    }
    

    如果您使用任何基于 Enumerator 的解决方案,不要忘记调用 Dispose()

    这是一个简单的测试:

    [Test]
    public void ShouldMergeInOrderMultipleOrderedListWithDuplicateValues()
    {
        // given
        IEnumerable<IEnumerable<int>> orderedLists = new[]
        {
            new [] {1, 5, 7},
            new [] {1, 2, 4, 6, 7}
        };
    
        // test
        IEnumerable<int> merged = orderedLists.MergeOrderedLists(i => i);
    
        // expect
        merged.ShouldAllBeEquivalentTo(new [] { 1, 1, 2, 4, 5, 6, 7, 7 });
    }
    

    【讨论】:

    【解决方案8】:

    我的六个字母变量答案的版本。我减少了对 orderFunc 的调用次数(每个元素只通过 orderFunc 一次),并且在 tie 的情况下,跳过了排序。这针对少量源、每个源中的大量元素以及可能昂贵的 orderFunc 进行了优化。

    public static IEnumerable<T> MergePreserveOrder<T, TOrder>(
      this IEnumerable<IEnumerable<T>> sources, 
      Func<T, TOrder> orderFunc)  
      where TOrder : IComparable<TOrder> 
    {
      Dictionary<TOrder, List<IEnumerable<T>>> keyedSources =
        sources.Select(source => source.GetEnumerator())
          .Where(e => e.MoveNext())
          .GroupBy(e => orderFunc(e.Current))
          .ToDictionary(g => g.Key, g => g.ToList()); 
    
      while (keyedSources.Any())
      {
         //this is the expensive line
        KeyValuePair<TOrder, List<IEnumerable<T>>> firstPair = keyedSources
          .OrderBy(kvp => kvp.Key).First();
    
        keyedSources.Remove(firstPair.Key);
        foreach(IEnumerable<T> e in firstPair.Value)
        {
          yield return e.Current;
          if (e.MoveNext())
          {
            TOrder newKey = orderFunc(e.Current);
            if (!keyedSources.ContainsKey(newKey))
            {
              keyedSources[newKey] = new List<IEnumerable<T>>() {e};
            }
            else
            {
              keyedSources[newKey].Add(e);
            }
          }
        }
      }
    }
    

    我打赌这可以通过 SortedDictionary 进一步改进,但我没有勇气尝试使用没有编辑器的解决方案。

    【讨论】:

    • 您使用字典/列表组合对元素进行排序 - 我不确定是否为每个值创建自己的 List。 OP 说他想对大列表进行排序 - 所以这么多列表的初始化可能是一个问题。我有一个使用SortedDictionary 的解决方案,但密钥必须是唯一的 - 所以值需要再次成为一个集合。这就是为什么我决定使用能够包含多个键的单个列表(并使用快速二进制搜索)
    【解决方案9】:

    这是一个现代实现,它基于强大的新 PriorityQueue&lt;TElement, TPriority&gt; 类 (.NET 6)。它结合了user7116's solution 的低开销和tanascius's solution 的 O(log n) 复杂度(其中 N 是源的数量)。它优于这个问题中提出的大多数其他实现(我没有测量它们全部),无论是对于小 N,还是对于大 N。

    public static IEnumerable<TSource> MergeSorted<TSource, TKey>(
        this IEnumerable<IEnumerable<TSource>> sortedSources,
        Func<TSource, TKey> keySelector,
        IComparer<TKey> comparer = default)
    {
        List<IEnumerator<TSource>> enumerators = new();
        try
        {
            foreach (var source in sortedSources)
                enumerators.Add(source.GetEnumerator());
            var queue = new PriorityQueue<IEnumerator<TSource>, TKey>(comparer);
            foreach (var enumerator in enumerators)
            {
                if (enumerator.MoveNext())
                    queue.Enqueue(enumerator, keySelector(enumerator.Current));
            }
            while (queue.TryPeek(out var enumerator, out _))
            {
                yield return enumerator.Current;
                if (enumerator.MoveNext())
                    queue.EnqueueDequeue(enumerator, keySelector(enumerator.Current));
                else
                    queue.Dequeue();
            }
        }
        finally
        {
            foreach (var enumerator in enumerators) enumerator.Dispose();
        }
    }
    

    为了保持代码简单,所有枚举数都放在组合枚举的末尾。更复杂的实现会在每个枚举器完成后立即对其进行处理。

    【讨论】:

      【解决方案10】:

      这看起来是一个非常有用的功能,所以我决定尝试一下。我的方法很像 heightechrider,因为它将问题分解为将两个已排序的 IEnumerable 合并为一个,然后将其与列表中的下一个合并。您很可能可以进行一些优化,但它适用于我的简单测试用例:

            public static IEnumerable<T> mergeSortedEnumerables<T>(
                  this IEnumerable<IEnumerable<T>> listOfLists, 
                  Func<T, T, Boolean> func)
            {
                  IEnumerable<T> l1 = new List<T>{};
                  foreach (var l in listOfLists)
                  {
                       l1 = l1.mergeTwoSorted(l, func);
                  }
      
                  foreach (var t in l1)
                  {
                       yield return t;
                  }
            }
      
            public static IEnumerable<T> mergeTwoSorted<T>(
                  this IEnumerable<T> l1, 
                  IEnumerable<T> l2, 
                  Func<T, T, Boolean> func)
            {
                  using (var enumerator1 = l1.GetEnumerator())
                  using (var enumerator2 = l2.GetEnumerator())
                  {
                       bool enum1 = enumerator1.MoveNext();
                       bool enum2 = enumerator2.MoveNext();
                       while (enum1 || enum2)
                       {
                            T t1 = enumerator1.Current;
                            T t2 = enumerator2.Current;
      
                            //if they are both false
                            if (!enum1 && !enum2)
                            {
                                  break;
                            }
                            //if enum1 is false
                            else if (!enum1)
                            {
                                  enum2 = enumerator2.MoveNext();
                                  yield return t2;
      
                            }
                            //if enum2 is false
                            else if (!enum2)
                            {
                                  enum1 = enumerator1.MoveNext();
                                  yield return t1;
      
                            }
                            //they are both true
                            else
                            {
                                  //if func returns true then t1 < t2
                                  if (func(t1, t2))
                                  {
                                       enum1 = enumerator1.MoveNext();
                                       yield return t1;
      
                                  }
                                  else
                                  {
                                       enum2 = enumerator2.MoveNext();
                                       yield return t2;
      
                                  }
                            }
                       }
                  }
            }
      

      然后进行测试:

                      List<int> ws = new List<int>() { 1, 8, 9, 16, 17, 21 };
                      List<int> xs = new List<int>() { 2, 7, 10, 15, 18 };
                      List<int> ys = new List<int>() { 3, 6, 11, 14 };
                      List<int> zs = new List<int>() { 4, 5, 12, 13, 19, 20 };
                      List<IEnumerable<int>> lss = new List<IEnumerable<int>> { ws, xs, ys, zs };
      
                      foreach (var v in lss.mergeSortedEnumerables(compareInts))
                      {
                           Console.WriteLine(v);
                      }
      

      【讨论】:

        【解决方案11】:

        今天晚上我被问到这个问题作为面试问题,在分配的 20 分钟左右没有得到很好的答案。所以我强迫自己写一个算法而不做任何搜索。约束是输入已经排序。这是我的代码:

        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Text;
        
        namespace Merger
        {
          class Program
          {
            static void Main(string[] args)
            {
              int[] a = { 1, 3, 6, 102, 105, 230 };
              int[] b = { 101, 103, 112, 155, 231 };
        
              var mm = new MergeMania();
        
              foreach(var val in mm.Merge<int>(a, b))
              {
                Console.WriteLine(val);
              }
              Console.ReadLine();
            }
          }
        
          public class MergeMania
          {
            public IEnumerable<T> Merge<T>(params IEnumerable<T>[] sortedSources) 
              where T : IComparable
            {
              if (sortedSources == null || sortedSources.Length == 0) 
                throw new ArgumentNullException("sortedSources");
        
              //1. fetch enumerators for each sourc
              var enums = (from n in sortedSources 
                     select n.GetEnumerator()).ToArray();
        
              //2. fetch enumerators that have at least one value
              var enumsWithValues = (from n in enums 
                           where n.MoveNext() 
                           select n).ToArray();
              if (enumsWithValues.Length == 0) yield break; //nothing to iterate over
        
              //3. sort by current value in List<IEnumerator<T>>
              var enumsByCurrent = (from n in enumsWithValues 
                          orderby n.Current 
                          select n).ToList();
              //4. loop through
              while (true)
              {
                //yield up the lowest value
                yield return enumsByCurrent[0].Current;
        
                //move the pointer on the enumerator with that lowest value
                if (!enumsByCurrent[0].MoveNext())
                {
                  //remove the first item in the list
                  enumsByCurrent.RemoveAt(0);
        
                  //check for empty
                  if (enumsByCurrent.Count == 0) break; //we're done
                }
                enumsByCurrent = enumsByCurrent.OrderBy(x => x.Current).ToList();
              }
            }
          }
        }
        

        希望对你有帮助。

        【讨论】:

          【解决方案12】:

          尝试改进@cdiggins 的answer。 如果两个比较相等的元素包含在两个不同的序列中(即没有@ChadHenderson 提到的缺陷),则此实现可以正常工作。

          算法描述为in Wikipedia,复杂度为O(m log n),其中n是列表的数量合并,m 是列表长度的总和。

          使用来自Wintellect.PowerCollectionsOrderedBag&lt;T&gt; 代替基于堆的优先级队列,但不会改变复杂性。

          public static IEnumerable<T> Merge<T>(
             IEnumerable<IEnumerable<T>> listOfLists,
             Func<T, T, int> comparison = null)
          {
             IComparer<T> cmp = comparison != null
                ? Comparer<T>.Create(new Comparison<T>(comparison))
                : Comparer<T>.Default;
             List<IEnumerator<T>> es = listOfLists
                .Select(l => l.GetEnumerator())
                .Where(e => e.MoveNext())
                .ToList();
             var bag = new OrderedBag<IEnumerator<T>>(
                (e1, e2) => cmp.Compare(e1.Current, e2.Current));
             es.ForEach(e => bag.Add(e));
             while (bag.Count > 0)
             {
                IEnumerator<T> e = bag.RemoveFirst();
                yield return e.Current;
                if (e.MoveNext())
                {
                   bag.Add(e);
                }
             }
          }
          

          【讨论】:

          • 请不要忘记在你的枚举器上调用 Dispose()
          【解决方案13】:

          每个被合并的列表都应该已经排序。此方法将根据列表的顺序定位相等的元素。例如,如果元素 Ti == Tj,并且它们分别来自列表 i 和列表 j (i

          public static IEnumerable<T> Merge<T, TOrder>(this IEnumerable<IEnumerable<T>> TEnumerable_2, Func<T, TOrder> orderFunc, IComparer<TOrder> cmp=null)
          {
              if (cmp == null)
              {
                  cmp = Comparer<TOrder>.Default;
              }
          
              List<IEnumerator<T>> TEnumeratorLt = TEnumerable_2
                 .Select(l => l.GetEnumerator())
                 .Where(e => e.MoveNext())
                 .ToList();
          
              while (TEnumeratorLt.Count > 0)
              {
                  int intMinIndex;
                  IEnumerator<T> TSmallest = TEnumeratorLt.GetMin(TElement => orderFunc(TElement.Current), out intMinIndex, cmp);
                  yield return TSmallest.Current;
          
                  if (TSmallest.MoveNext() == false)
                  {
                      TEnumeratorLt.RemoveAt(intMinIndex);
                  }
              }
          }
          
          /// <summary>
          /// Get the first min item in an IEnumerable, and return the index of it by minIndex
          /// </summary>
          public static T GetMin<T, TOrder>(this IEnumerable<T> self, Func<T, TOrder> orderFunc, out int minIndex, IComparer<TOrder> cmp = null)
          {
              if (self == null) throw new ArgumentNullException("self");            
          
              IEnumerator<T> selfEnumerator = self.GetEnumerator();
              if (!selfEnumerator.MoveNext()) throw new ArgumentException("List is empty.", "self");
          
              if (cmp == null) cmp = Comparer<TOrder>.Default;
          
              T min = selfEnumerator.Current;
              minIndex = 0;
              int intCount = 1;
              while (selfEnumerator.MoveNext ())
              {
                  if (cmp.Compare(orderFunc(selfEnumerator.Current), orderFunc(min)) < 0)
                  {
                      min = selfEnumerator.Current;
                      minIndex = intCount;
                  }
                  intCount++;
              }
          
              return min;
          }
          

          【讨论】:

          • 从所有行中删除 4 个空格:选择所有代码并按 {} 按钮。这是你看到的切换。
          【解决方案14】:

          我采用了更实用的方法,希望读起来不错。

          这里首先是合并方法本身:

          public static IEnumerable<T> MergeSorted<T>(IEnumerable<IEnumerable<T>> xss) where T :IComparable
          {
              var stacks = xss.Select(xs => new EnumerableStack<T>(xs)).ToList();
          
              while (true)
              {
                  if (stacks.All(x => x.IsEmpty)) yield break;
          
                  yield return 
                      stacks
                          .Where(x => !x.IsEmpty)
                          .Select(x => new { peek = x.Peek(), x })
                          .MinBy(x => x.peek)
                          .x.Pop();
              }
          }
          

          我们的想法是,我们将每个 IEnumerable 转换为具有 Peek()Pop()IsEmpty 成员的 EnumerableStack

          它就像一个普通的堆栈一样工作。请注意,调用 IsEmpty 可能会枚举包装的 IEnumerable

          代码如下:

          /// <summary>
          /// Wraps IEnumerable in Stack like wrapper
          /// </summary>
          public class EnumerableStack<T>
          {
              private enum StackState
              {
                  Pending,
                  HasItem,
                  Empty
              }
          
              private readonly IEnumerator<T> _enumerator;
          
              private StackState _state = StackState.Pending;
          
              public EnumerableStack(IEnumerable<T> xs)
              {
                  _enumerator = xs.GetEnumerator();
              }
          
              public T Pop()
              {
                  var res = Peek(isEmptyMessage: "Cannot Pop from empty EnumerableStack");
                  _state = StackState.Pending;
                  return res;
              }
          
              public T Peek()
              {
                  return Peek(isEmptyMessage: "Cannot Peek from empty EnumerableStack");
              }
          
              public bool IsEmpty
              {
                  get
                  {
                      if (_state == StackState.Empty) return true;
                      if (_state == StackState.HasItem) return false;
                      ReadNext();
                      return _state == StackState.Empty;
                  }
              }
          
              private T Peek(string isEmptyMessage)
              {
                  if (_state != StackState.HasItem)
                  {
                      if (_state == StackState.Empty) throw new InvalidOperationException(isEmptyMessage);
                      ReadNext();
                      if (_state == StackState.Empty) throw new InvalidOperationException(isEmptyMessage);
                  }
                  return _enumerator.Current;
              }
          
              private void ReadNext()
              {
                  _state = _enumerator.MoveNext() ? StackState.HasItem : StackState.Empty;
              }
          }
          

          最后,如果你还没有自己写过,这里是 MinBy 扩展方法:

          public static T MinBy<T, TS>(this IEnumerable<T> xs, Func<T, TS> selector) where TS : IComparable
          {
              var en = xs.GetEnumerator();
              if (!en.MoveNext()) throw new Exception();
          
              T max = en.Current;
              TS maxVal = selector(max);
              while(en.MoveNext())
              {
                  var x = en.Current;
                  var val = selector(x);
                  if (val.CompareTo(maxVal) < 0)
                  {
                      max = x;
                      maxVal = val;
                  }
              }
          
              return max;
          }
          

          【讨论】:

            【解决方案15】:

            这是另一种解决方案:

            using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Text;
            using System.Reflection;
            using System.Data;
            using System.Text.RegularExpressions;
            
            namespace ConsoleApplication1
            {
            
                class Person
                {
                    public string Name
                    {
                        get;
                        set;
                    }
            
                    public int Age
                    {
                        get;
                        set;
                    }
                }
            
                public class Program
                {
                    public static void Main()
                    {
                        Person[] persons1 = new Person[] { new Person() { Name = "Ahmed", Age = 20 }, new Person() { Name = "Ali", Age = 40 } };
                        Person[] persons2 = new Person[] { new Person() { Name = "Zaid", Age = 21 }, new Person() { Name = "Hussain", Age = 22 } };
                        Person[] persons3 = new Person[] { new Person() { Name = "Linda", Age = 19 }, new Person() { Name = "Souad", Age = 60 } };
            
                        Person[][] personArrays = new Person[][] { persons1, persons2, persons3 };
            
                        foreach(Person person in MergeOrderedLists<Person, int>(personArrays, person => person.Age))
                        {
                            Console.WriteLine("{0} {1}", person.Name, person.Age);
                        }
            
                        Console.ReadLine();
                    }
            
                    static IEnumerable<T> MergeOrderedLists<T, TOrder>(IEnumerable<IEnumerable<T>> orderedlists, Func<T, TOrder> orderBy)
                    {
                        List<IEnumerator<T>> enumeratorsWithData = orderedlists.Select(enumerable => enumerable.GetEnumerator())
                                                                               .Where(enumerator => enumerator.MoveNext()).ToList();
            
                        while (enumeratorsWithData.Count > 0)
                        {
                            IEnumerator<T> minEnumerator = enumeratorsWithData[0];
                            for (int i = 1; i < enumeratorsWithData.Count; i++)
                                if (((IComparable<TOrder>)orderBy(minEnumerator.Current)).CompareTo(orderBy(enumeratorsWithData[i].Current)) >= 0)
                                    minEnumerator = enumeratorsWithData[i];
            
                            yield return minEnumerator.Current;
            
                            if (!minEnumerator.MoveNext())
                                enumeratorsWithData.Remove(minEnumerator);
                        }             
                    }
                }   
            }
            

            【讨论】:

              【解决方案16】:

              我怀疑 LINQ 足够聪明,可以利用现有的排序顺序:

              IEnumerable<string> BiggerSortedList =  BigListOne.Union(BigListTwo).OrderBy(s => s);
              

              【讨论】:

              • 在任何IEnumerable 上?我对此表示怀疑。
              • Union 是否保留重复项?我知道 SQL 中的 UNION 没有。
              • @mgroves,联合被推迟,不会造成欺骗。
              • 使用 Concat 而不是 Union 来保留源的所有元素。另外 - 这是一个完整的重新排序,没有利用预排序。
              • 如果 Union 保留骗子,那么 不会 为 franck 工作,对吧?
              猜你喜欢
              • 1970-01-01
              • 2013-08-04
              • 1970-01-01
              • 2010-10-10
              • 1970-01-01
              • 1970-01-01
              • 2012-04-04
              • 2013-03-27
              • 2019-10-11
              相关资源
              最近更新 更多