使用LINQ进行拓扑排序

18
我有一系列具有偏序关系的项目,即该列表可以被视为部分排序集。我想按照这个问题中的方式对这个列表进行排序。正如在那里正确回答的那样,这被称为拓扑排序
已经有一个相当简单的已知算法来解决这个问题。我想要一个类似于LINQ的实现。
我已经尝试使用OrderBy扩展方法,但我非常确定它无法进行拓扑排序。问题在于IComparer<TKey>接口无法表示偏序关系。这是因为Compare方法基本上只能返回3种类型的值:负数正数,分别表示相等小于大于。只有在有一种方法返回不相关的情况下,才有可能得到一个可行的解决方案。
从我的主观角度来看,我寻找的答案可能由一个IPartialOrderComparer<T>接口和一个像这样的扩展方法组成:
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IPartialOrderComparer<TKey> comparer
);

这个该怎么实现呢?IPartialOrderComparer<T> 接口应该是什么样子的呢?你会推荐不同的方法吗?我很想看看。也许有更好的方式来表示偏序关系,我不知道。

6个回答

16

我建议使用相同的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


我同意,这是表示偏序关系最合理的方式。即使我们有一种方法可以看出元素是否可比较,也不清楚在与不相关的事物之间放置某些东西的位置。相等似乎是最直接的方法。 - luke
谢谢你的回答。我还没有时间深入研究它。乍一看,我担心那里使用的 default 可能会隐藏一些错误。例如,default(int) 是零,而它几乎不是最小的 int 值。你试过负值吗?无论如何,我明天早上会尝试一下。 - jpbochi
1
好的,尽管存在“default”的情况,代码仍然可以正常工作。在第一个“foreach”中,最小变量上最初设置的任何值都将被覆盖。顺便说一句,第一个“foreach”可以很容易地被丢弃。我正在测试您的代码上的一些可能的优化。无论如何都运行得非常好。 :) - jpbochi
1
我将你的答案进行了优化并发布为我的答案,以便于文档记录。当然,我会接受你的答案。 - jpbochi
1
默认值的价值应该与正确性无关,因为第一个foreach循环。这只是为了让编译器放心,那些变量将被设置。我相信有无数种方法可以提高代码的安全性、优雅性和性能——这只是一个快速组合起来展示我最初发布的概念的例子。祝好运! - Eric Mickelsen

8
这是我优化和改进的tehMick的答案
我所做的一个更改是用逻辑列表替换了实际要产生的值的真正列表。为此,我有两个相同大小的数组。其中一个包含所有的值,另一个包含标志,告诉每个值是否已经被产生。这样,我避免了重新调整大小真正的List<Key>的成本。
另一个变化是我只在迭代开始时读取所有键一次。出于我现在无法回想起的原因(也许只是我的直觉),我不喜欢多次调用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--;
    }
}

2

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

处理拓扑排序的典型方式是使用图形,每次迭代删除所有没有入站连接的节点,并同时删除这些节点的所有出站连接。被删除的节点是迭代的输出。重复此过程,直到无法再删除任何节点为止。

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

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

换句话说,该方法可能会定义如下:

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

当它没有两个键的明确答案时,返回null
我在思考的问题是“迭代所有组合”的部分。也许有更好的处理方法,但我没看到。

1

我相信Lasse V. Karlsen的答案是正确的方向,但我不喜欢隐藏Compare方法(或者单独的接口,它不继承IComparable<T>)。

相反,我更喜欢看到这样的东西:

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

这样,您仍然拥有一个IComparer<T>的实现,可以在其他需要IComparer<T>的地方使用。

但是,它还要求您以以下方式(类似于IComparable<T>)指示T的实例之间的关系:

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

  • <0-y是可比键,但x不是。

当将此实现传递给期望IComparable<T>的任何内容时,您当然不会获得部分排序(应该注意的是,Lasse V. Karlsen的答案也无法解决此问题),因为您需要的内容无法用简单的Compare方法表示,该方法接受两个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;            
    }
}

1

定义偏序关系的接口:

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

Compare 应该返回 -1 如果 x < y0 如果 x = y1 如果 y < x,如果 xy 不可比,则返回 null

我们的目标是返回一个元素序列,该序列尊重枚举中的偏序。也就是说,我们寻求一个偏序中元素的序列 e_1, e_2, e_3, ..., e_n,使得如果 i <= j 并且 e_i 可比于 e_j,则 e_i <= 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,但一旦我们到达这个点,这很容易做到。

该算法通过对元素进行深度优先搜索来工作,如果我们已经将所有前驱元素添加到排序中(由算法中的_sorted表示),则将元素e添加到线性排序中。因此,对于每个元素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 < 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;
    }
}

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

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类。顺便说一下,用集合测试解决方案非常干净。 - jpbochi
糟糕。谢谢你发现了这个问题。我使用了深度优先搜索。 - jason
这是一个不错的方法。有一些可能的简单优化/简化。首先,我使用常规的IComparer而不是你的IPartialComparer测试了你的解决方案,它可以正确地工作。此外,TopologicalSorter类可以是静态的。无论如何,tehMick所采用的方法似乎更简单和更快。我想我必须接受他的答案。 - jpbochi

0

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

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);
     });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接