【问题标题】:Topological Sorting using LINQ使用 LINQ 进行拓扑排序
【发布时间】:2010-12-31 06:07:07
【问题描述】:

我有一个带有partial order relation 的项目列表,即。 e、列表可以认为是partially ordered set。我想以与question 相同的方式对该列表进行排序。正如那里正确回答的那样,这被称为topological sorting

有一个相当简单的已知算法可以解决这个问题。我想要一个类似 LINQ 的实现。

我已经尝试过使用OrderBy扩展方法,但我很确定它不能进行拓扑排序。问题是IComparer<TKey> 接口无法表示部分顺序。发生这种情况是因为Compare 方法基本上可以返回 3 种值:zeronegativepositive,意思是 are-分别等于小于大于。只有当有办法返回 are-unrelated 时,才有可能找到可行的解决方案。

从我的偏见的角度来看,我正在寻找的答案可能由IPartialOrderComparer<T> 接口和这样的扩展方法组成:

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IPartialOrderComparer<TKey> comparer
);

这将如何实施? IPartialOrderComparer&lt;T&gt; 界面会是什么样子?你会推荐一种不同的方法吗?我很想看到它。也许有更好的方式来表示偏序,我不知道。

【问题讨论】:

    标签: .net linq sorting topological-sort partial-ordering


    【解决方案1】:

    我建议使用相同的 IComparer 接口,但编写扩展方法以便将 0 解释为不相关。在偏序中,如果元素 a 和 b 相等,则它们的顺序无关紧要,同样,如果它们不相关 - 您只需根据它们已定义关系的元素对它们进行排序。

    这是一个对偶数和奇数进行部分排序的示例:

    namespace PartialOrdering
    {
        public static class Enumerable
        {
            public static IEnumerable<TSource> PartialOrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
            {
                List<TSource> list = new List<TSource>(source);
                while (list.Count > 0)
                {
                    TSource minimum = default(TSource);
                    TKey minimumKey = default(TKey);
                    foreach (TSource s in list)
                    {
                        TKey k = keySelector(s);
                        minimum = s;
                        minimumKey = k;
                        break;
                    }
                    foreach (TSource s in list)
                    {
                        TKey k = keySelector(s);
                        if (comparer.Compare(k, minimumKey) < 0)
                        {
                            minimum = s;
                            minimumKey = k;
                        }
                    }
                    yield return minimum;
                    list.Remove(minimum);
                }
                yield break;
            }
    
        }
        public class EvenOddPartialOrdering : IComparer<int>
        {
            public int Compare(int a, int b)
            {
                if (a % 2 != b % 2)
                    return 0;
                else if (a < b)
                    return -1;
                else if (a > b)
                    return 1;
                else return 0; //equal
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                IEnumerable<Int32> integers = new List<int> { 8, 4, 5, 7, 10, 3 };
                integers = integers.PartialOrderBy<Int32, Int32>(new Func<Int32, Int32>(delegate(int i) { return i; }), new EvenOddPartialOrdering());
            }
        }
    }
    

    结果:4、8、3、5、7、10

    【讨论】:

    • 我同意,这是表示偏序最合理的方式。即使我们有办法查看元素是否具有可比性,也不清楚在哪里放置与不相关的东西相关的东西。平等似乎是最直接的方法
    • 感谢您的回答。我还没有时间深入研究它。乍一看,我担心那些default 的使用可能会隐藏一些错误。例如,default(int) 为零,它几乎不是最小的 int 值。您是否尝试过负值?不管怎样,我明天早上试试。
    • 好的,尽管有default,代码仍然有效。最初放在“最小”变量上的任何值都会在第一个foreach 中被覆盖。顺便说一句,第一个 foreach 可以很容易地丢弃。我正在测试您的代码的一些可能的优化。无论如何,效果很好。 :)
    • 我发布了您的答案的优化版本,作为我自己的答案,用于文档目的。当然,我会接受你的。
    • 默认值应该与正确性无关,因为第一个 foreach。这只是为了向编译器保证将设置这些变量。我确信有无数种方法可以提高我的代码的安全性、优雅性和性能——这只是为了演示我最初发布的概念的快速组合。祝你好运!
    【解决方案2】:

    这是我对tehMick's answer的优化和翻新版本。

    我所做的一个更改是替换真正的 list 值,以便为逻辑列表生成。为此,我有两个大小相同的数组。一个具有所有值,而另一个包含指示是否已产生每个值的标志。这样,我就避免了必须调整真实 List&lt;Key&gt; 大小的成本。

    另一个变化是我在迭代开始时只读取所有键一次。由于我现在不记得的原因(也许这只是我的直觉),我不喜欢多次调用 keySelector 函数的想法。

    最后的改动是参数验证,以及使用隐式键比较器的额外重载。我希望代码足够可读。看看吧。

    public static IEnumerable<TSource> PartialOrderBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector)
    {
        return PartialOrderBy(source, keySelector, null);
    }
    
    public static IEnumerable<TSource> PartialOrderBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector,
        IComparer<TKey> comparer)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (keySelector == null) throw new ArgumentNullException("keySelector");
        if (comparer == null) comparer = (IComparer<TKey>)Comparer<TKey>.Default;
    
        return PartialOrderByIterator(source, keySelector, comparer);
    }
    
    private static IEnumerable<TSource> PartialOrderByIterator<TSource, TKey>(
        IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector,
        IComparer<TKey> comparer)
    {
        var values = source.ToArray();
        var keys = values.Select(keySelector).ToArray();
        int count = values.Length;
        var notYieldedIndexes = System.Linq.Enumerable.Range(0, count).ToArray();
        int valuesToGo = count;
    
        while (valuesToGo > 0)
        {
            //Start with first value not yielded yet
            int minIndex = notYieldedIndexes.First( i => i >= 0);
    
            //Find minimum value amongst the values not yielded yet
            for (int i=0; i<count; i++)
            if (notYieldedIndexes[i] >= 0)
            if (comparer.Compare(keys[i], keys[minIndex]) < 0) {
                minIndex = i;
            }
    
            //Yield minimum value and mark it as yielded
            yield return values[minIndex];
            notYieldedIndexes[minIndex] = -1;
            valuesToGo--;
        }
    }
    

    【讨论】:

      【解决方案3】:

      好吧,我不确定这种处理方式是不是最好的方式,但我可能是错的。

      处理拓扑排序的典型方法是使用图,在每次迭代中删除所有没有入站连接的节点,同时从这些节点中删除所有出站连接。删除的节点是迭代的输出。重复直到您无法删除更多节点。

      但是,为了首先获得这些连接,使用您的方法,您需要:

      1. 一种方法(您的比较器)可以说“在此之前”但也可以说“这两个没有信息”
      2. 迭代所有组合,为比较器返回排序的所有组合创建连接。

      换句话说,该方法可能会这样定义:

      public Int32? Compare(TKey a, TKey b) { ... }
      

      然后在没有两个键的确定答案时返回null

      我正在考虑的问题是“迭代所有组合”部分。也许有更好的方法来处理这个问题,但我没有看到它。

      【讨论】:

        【解决方案4】:

        我相信Lasse V. Karlsen's answer 是在正确的轨道上,但我不喜欢隐藏比较方法(或不从IComparable&lt;T&gt; 扩展的单独接口)。

        相反,我宁愿看到这样的东西:

        public interface IPartialOrderComparer<T> : IComparer<T>
        {
            int? InstancesAreComparable(T x, T y);
        }
        

        这样,您仍然可以在需要IComparer&lt;T&gt; 的其他地方使用IComparer&lt;T&gt; 的实现。

        但是,它也需要你用返回值来表示T的实例之间的关系,方式如下(类似于IComparable&lt;T&gt;):

        • null - 实例不能相互比较。
        • 0 - 实例可相互比较。
        • 0 - x 是可比较的键,但 y 不是。

        当然,将其实现传递给任何期望 IComparable&lt;T&gt; 的东西时,您不会得到部分排序(应该注意,Lasse V. Karlsen 的回答也不能解决这个问题)作为您需要的不能用一个简单的比较方法来表示,该方法接受两个 T 实例并返回一个 int。

        要完成解决方案,您必须提供一个自定义的 OrderBy(以及 ThenBy、OrderByDescending 和 ThenByDescending)扩展方法,该方法将接受新的实例参数(正如您已经指出的那样)。实现看起来像这样:

        public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(      
            this IEnumerable<TSource> source,      
            Func<TSource, TKey> keySelector,      
            IPartialOrderComparer<TKey> comparer)
        {
            return Enumerable.OrderBy(source, keySelector,
                new PartialOrderComparer(comparer);
        }
        
        internal class PartialOrderComparer<T> : IComparer<T>
        {
            internal PartialOrderComparer(IPartialOrderComparer<T> 
                partialOrderComparer)
            {
                this.partialOrderComparer = partialOrderComparer;
            }
        
            private readonly IPartialOrderComparer<T> partialOrderComparer;
        
            public int Compare(T x, T y)
            {
                // See if the items are comparable.
                int? comparable = partialOrderComparable.
                    InstancesAreComparable(x, y);
        
                // If they are not comparable (null), then return
                // 0, they are equal and it doesn't matter
                // what order they are returned in.
                // Remember that this only to determine the
                // values in relation to each other, so it's
                // ok to say they are equal.
                if (comparable == null) return 0;
        
                // If the value is 0, they are comparable, return
                // the result of that.
                if (comparable.Value == 0) return partialOrderComparer.Compare(x, y);
        
                // One or the other is uncomparable.
                // Return the negative of the value.
                // If comparable is negative, then y is comparable
                // and x is not.  Therefore, x should be greater than y (think
                // of it in terms of coming later in the list after
                // the ordered elements).
                return -comparable.Value;            
            }
        }
        

        【讨论】:

          【解决方案5】:

          定义偏序关系的接口:

          interface IPartialComparer<T> {
              int? Compare(T x, T y);
          }
          

          如果x &lt; y0 如果x = y1 如果y &lt; xnull 如果xy 不可比较,则Compare 应该返回null。 >

          我们的目标是以尊重枚举的偏序返回元素的排序。也就是说,我们以偏序寻找元素的序列e_1, e_2, e_3, ..., e_n,使得如果i &lt;= je_ie_j 相当,那么e_i &lt;= e_j。我将使用深度优先搜索来执行此操作。

          使用深度优先搜索实现拓扑排序的类:

          class TopologicalSorter {
              class DepthFirstSearch<TElement, TKey> {
                  readonly IEnumerable<TElement> _elements;
                  readonly Func<TElement, TKey> _selector;
                  readonly IPartialComparer<TKey> _comparer;
                  HashSet<TElement> _visited;
                  Dictionary<TElement, TKey> _keys;
                  List<TElement> _sorted;
          
                  public DepthFirstSearch(
                      IEnumerable<TElement> elements,
                      Func<TElement, TKey> selector,
                      IPartialComparer<TKey> comparer
                  ) {
                      _elements = elements;
                      _selector = selector;
                      _comparer = comparer;
                      var referenceComparer = new ReferenceEqualityComparer<TElement>();
                      _visited = new HashSet<TElement>(referenceComparer);
                      _keys = elements.ToDictionary(
                          e => e,
                          e => _selector(e), 
                          referenceComparer
                      );
                      _sorted = new List<TElement>();
                  }
          
                  public IEnumerable<TElement> VisitAll() {
                      foreach (var element in _elements) {
                          Visit(element);
                      }
                      return _sorted;
                  }
          
                  void Visit(TElement element) {
                      if (!_visited.Contains(element)) {
                          _visited.Add(element);
                          var predecessors = _elements.Where(
                              e => _comparer.Compare(_keys[e], _keys[element]) < 0
                          );
                          foreach (var e in predecessors) {
                              Visit(e);
                          }
                          _sorted.Add(element);
                      }
                  }
              }
          
              public IEnumerable<TElement> ToplogicalSort<TElement, TKey>(
                  IEnumerable<TElement> elements,
                  Func<TElement, TKey> selector, IPartialComparer<TKey> comparer
              ) {
                  var search = new DepthFirstSearch<TElement, TKey>(
                      elements,
                      selector,
                      comparer
                  );
                  return search.VisitAll();
              }
          }
          

          在进行深度优先搜索时将节点标记为已访问所需的助手类:

          class ReferenceEqualityComparer<T> : IEqualityComparer<T> {
              public bool Equals(T x, T y) {
                  return Object.ReferenceEquals(x, y);
              }
          
              public int GetHashCode(T obj) {
                  return obj.GetHashCode();
              }
          }
          

          我没有声称这是算法的最佳实现,但我相信它是正确的实现。此外,我没有按照您的要求返回IOrderedEnumerable,但是一旦我们到了这一点,这很容易做到。

          如果我们已经添加了 e 的所有前辈,则该算法通过将元素 e 添加到线性排序(在算法中由 _sorted 表示)进行深度优先搜索来工作已添加到订单中。因此,对于每个元素e,如果我们还没有访问过它,请访问它的前辈,然后添加e。因此,这是算法的核心:

          public void Visit(TElement element) {
              // if we haven't already visited the element
              if (!_visited.Contains(element)) {
                  // mark it as visited
                  _visited.Add(element);
                  var predecessors = _elements.Where(
                      e => _comparer.Compare(_keys[e], _keys[element]) < 0
                  );
                  // visit its predecessors
                  foreach (var e in predecessors) {
                      Visit(e);
                  }
                  // add it to the ordering
                  // at this point we are certain that
                  // its predecessors are already in the ordering
                  _sorted.Add(element);
              }
          }
          

          例如,考虑在{1, 2, 3} 的子集上定义的偏序,如果XY 的子集,则X &lt; Y。我按如下方式实现:

          public class SetComparer : IPartialComparer<HashSet<int>> {
              public int? Compare(HashSet<int> x, HashSet<int> y) {
                  bool xSubsety = x.All(i => y.Contains(i));
                  bool ySubsetx = y.All(i => x.Contains(i));
                  if (xSubsety) {
                      if (ySubsetx) {
                          return 0;
                      }
                      return -1;
                  }
                  if (ySubsetx) {
                      return 1;
                  }
                  return null;
              }
          }
          

          然后将sets定义为{1, 2, 3}的子集列表

          List<HashSet<int>> sets = new List<HashSet<int>>() {
              new HashSet<int>(new List<int>() {}),
              new HashSet<int>(new List<int>() { 1, 2, 3 }),
              new HashSet<int>(new List<int>() { 2 }),
              new HashSet<int>(new List<int>() { 2, 3}),
              new HashSet<int>(new List<int>() { 3 }),
              new HashSet<int>(new List<int>() { 1, 3 }),
              new HashSet<int>(new List<int>() { 1, 2 }),
              new HashSet<int>(new List<int>() { 1 })
          };
          TopologicalSorter s = new TopologicalSorter();
          var sorted = s.ToplogicalSort(sets, set => set, new SetComparer());
          

          这导致排序:

          {}, {2}, {3}, {2, 3}, {1}, {1, 3}, {1, 2}, {1, 2, 3}
          

          尊重偏序。

          这很有趣。谢谢。

          【讨论】:

          • 感谢您的回答。我很高兴这对你来说很有趣。我明天试试看。我注意到的一个细节是你说你要使用广度优先搜索,但你的代码有一个DepthFirstSearch 类。顺便说一句,用集合测试解决方案非常简洁。
          • 糟糕。谢谢你抓住那个。我使用了深度优先搜索。
          • 这是一个不错的方法。有一些可能的简单优化/简化。首先,我使用常规的IComparer 而不是IPartialComparer 测试了您的解决方案,它工作正常。此外,TopologicalSorter 类可以是静态的。无论如何,tehMick 遵循的方法似乎更简单快捷。我想我不得不接受他的回答。
          【解决方案6】:

          非常感谢大家,从 Eric Mickelsen 的回答开始,我想出了我的版本,因为我更喜欢使用空值来表示没有关系,就像 Lasse V. Karlsen 所说的那样。

          public static IEnumerable<TSource> PartialOrderBy<TSource>(
                  this IEnumerable<TSource> source,            
                  IPartialEqualityComparer<TSource> comparer)
              {
                  if (source == null) throw new ArgumentNullException("source");
                  if (comparer == null) throw new ArgumentNullException("comparer");
          
          
                  var set = new HashSet<TSource>(source);
                  while (!set.IsEmpty())
                  {
                      TSource minimum = set.First();                
          
                      foreach (TSource s in set)
                      {                    
                          var comparison = comparer.Compare(s, minimum);
                          if (!comparison.HasValue) continue;
                          if (comparison.Value <= 0)
                          {
                              minimum = s;                        
                          }
                      }
                      yield return minimum;
                      set.Remove(minimum);
                  }
              }
          
          public static IEnumerable<TSource> PartialOrderBy<TSource>(
                 this IEnumerable<TSource> source,
                 Func<TSource, TSource, int?> comparer)
              {
                  return PartialOrderBy(source, new PartialEqualityComparer<TSource>(comparer));
              }
          

          那么我有下面的比较器接口

          public interface IPartialEqualityComparer<T>
          {
              int? Compare(T x, T y);
          }
          

          还有这个辅助类

          internal class PartialEqualityComparer<TSource> : IPartialEqualityComparer<TSource>
          {
              private Func<TSource, TSource, int?> comparer;
          
              public PartialEqualityComparer(Func<TSource, TSource, int?> comparer)
              {
                  this.comparer = comparer;
              }
          
              public int? Compare(TSource x, TSource y)
              {
                  return comparer(x,y);
              }
          }
          

          这允许稍微美化使用,所以我的测试可以如下所示

           var data = new int[] { 8,7,6,5,4,3,2,1,0 };
           var partiallyOrdered = data.PartialOrderBy((x, y) =>
               {
                  if (x % 2 == 0 && y % 2 != 0) return null;
                  return x.CompareTo(y);
               });
          

          【讨论】:

            猜你喜欢
            • 2014-10-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-07-28
            • 1970-01-01
            相关资源
            最近更新 更多