2009-12-30 14 views
17

Ich habe eine Liste von Elementen, die eine partial order relation haben, ich. e, die Liste kann als partially ordered set betrachtet werden. Ich möchte diese Liste auf die gleiche Weise wie in dieser question sortieren. Wie dort richtig beantwortet, ist dies als topological sorting bekannt.Topologische Sortierung mit LINQ

Es gibt einen einigermaßen einfachen bekannten Algorithmus, um das Problem zu lösen. Ich möchte eine LINQ-ähnliche Implementierung davon.

Ich habe bereits versucht, OrderBy Extension-Methode zu verwenden, aber ich bin mir ziemlich sicher, es ist nicht in der Lage, topologische Sortierung zu machen. Das Problem ist, dass die Schnittstelle IComparer<TKey> keine Teilauftrag darstellen kann. Dies geschieht, weil die Compare Methode grundsätzlich drei Arten von Werten zurückkehren können: Null, negativ und positive, was bedeutet, sind-gleich, ist-weniger-als und ist-mehr-dann , beziehungsweise. Eine funktionierende Lösung wäre nur möglich, wenn es einen Rückweg geben würde sind nicht verwandt.

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

Wie dies umgesetzt werden würde:

Aus meiner voreingenommen Sicht die Antwort, die ich bin auf der Suche könnte durch eine IPartialOrderComparer<T> Schnittstelle und eine Erweiterung Methode wie folgt zusammengesetzt sein? Wie sieht die Schnittstelle IPartialOrderComparer<T> aus? Würden Sie einen anderen Ansatz empfehlen? Ich bin gespannt darauf, es zu sehen. Vielleicht gibt es eine schönere Möglichkeit, die partielle Ordnung zu repräsentieren, ich weiß es nicht.

Antwort

15

Ich würde vorschlagen, die gleiche IComparer-Schnittstelle, aber das Schreiben die Erweiterung Methode verwendet, um 0 als nicht im Zusammenhang zu interpretieren.Wenn die Elemente a und b in einer partiellen Ordnung gleich sind, spielt ihre Reihenfolge keine Rolle, ebenso wenig, wenn sie nicht verwandt sind - Sie müssen sie nur in Bezug auf Elemente ordnen, mit denen sie Beziehungen definiert haben.

Hier ist ein Beispiel, das eine partielle Ordnung von geraden und ungeraden Zahlen tut:

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

Ergebnis: 4, 8, 3, 5, 7, 10

+0

Ich würde ag ree, das ist der vernünftigste Weg, die Teilaufordnung zu repräsentieren. Selbst wenn wir einen Weg hätten, also zu sehen, ob Elemente vergleichbar sind oder nicht, wäre es nicht klar, wo etwas in Beziehung zu etwas steht, mit dem es nicht verwandt ist. Gleichheit scheint der einfachste Weg zu sein, dies zu tun – luke

+0

Danke für die Antwort. Ich habe noch keine Zeit, es noch einmal gründlich zu untersuchen. Auf den ersten Blick fürchte ich, dass die Verwendung dieser 'Standard's einige Bugs verbergen könnte. Zum Beispiel ist 'default (int)' gleich null und es ist kaum der minimale int-Wert. Hast du Negativwerte probiert? Jedenfalls werde ich es morgen früh versuchen. – jpbochi

+0

Ok, der Code funktioniert trotz der 'default's. Jeder anfänglich auf die 'Minimum'-Variablen gesetzte Wert wird in der ersten' foreach 'überschrieben. BTW, die erste 'foreach' kann ziemlich leicht verworfen werden. Ich teste einige mögliche Optimierungen für Ihren Code. Funktioniert sowieso sehr gut. :) – jpbochi

2

Nun, ich bin nicht sicher, dass diese Art und Weise es von der Handhabung der beste Weg ist, aber ich könnte falsch sein.

Die typische Vorgehensweise bei der topologischen Sortierung besteht darin, ein Diagramm zu verwenden und für jede Iteration alle Knoten zu entfernen, die keine eingehende Verbindung haben, und gleichzeitig alle ausgehenden Verbindungen von diesen Knoten zu entfernen. Die entfernten Knoten sind die Ausgabe der Iteration. Wiederholen Sie dies, bis Sie keine weiteren Knoten entfernen können.

Um jedoch diese Verbindungen in erster Linie mit der Methode zu erhalten, müßten Sie:

  1. Ein Verfahren (Ihr Vergleich), die „diese vorher“, sondern auch „keine Informationen sagen könnten für diese zwei "
  2. Iterieren Sie alle Kombinationen und erstellen Sie Verbindungen für alle Kombinationen, für die der Vergleich eine Bestellung zurückgibt.

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

    und dann null zurück, wenn es eine schlüssige Antwort für zwei Schlüssel nicht hat:

Mit anderen Worten, würde das Verfahren wahrscheinlich wie folgt definiert.

Das Problem, über das ich nachdenke, ist der Teil "iterate all combinations". Vielleicht gibt es einen besseren Weg, damit umzugehen, aber ich sehe es nicht.

1

Ich glaube, dass Lasse V. Karlsen's answer auf der rechten Seite verfolgen, aber ich mag es nicht, die Compare-Methode zu verstecken (oder eine separate Schnittstelle, die sich nicht von IComparable<T> erstreckt).

Stattdessen würde ich eher so etwas wie dies sehen:

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

diese Weise werden Sie immer noch eine Implementierung von IComparer<T> haben, die in anderen Orten eingesetzt werden können, die IComparer<T> erfordern.

aber es erfordert, dass Sie auch die Beziehung der Instanzen von T zueinander mit dem Rückgabewert in der folgenden Art und Weise (ähnlich IComparable<T>), um anzuzeigen:

  • null - Instanzen jeweils nicht vergleichbar sind andere.
  • 0 - die Instanzen sind miteinander vergleichbar.
  • 0 - x ist ein vergleichbarer Schlüssel, aber y ist nicht.

  • < 0 - y ist ein vergleichbarer Schlüssel, aber x ist nicht.

Natürlich, werden Sie nicht teilweise Ordnung erhalten, wenn eine Implementierung dies alles vorbei, die ein IComparable<T> erwartet (und es sollte beachtet werden, dass Lasse V. Karlsen Antwort löst nicht das entweder) als das, was Sie need kann nicht in einer einfachen Compare-Methode dargestellt werden, die zwei Instanzen von T benötigt und einen int zurückgibt.

Um die Lösung zu beenden, müssen Sie eine benutzerdefinierte OrderBy-Methode (sowie ThenBy, OrderByDescending und ThenByDescending) angeben, die den neuen Instanzparameter akzeptiert (wie Sie bereits angegeben haben). Die Umsetzung würde wie folgt aussehen:

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

Schnittstelle partielle Ordnung Beziehung zu definieren:

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

Compare-1 wenn x < y zurückkehren sollte, 0 wenn x = y, 1 wenn y < x und null wenn x und y sind nicht vergleichbar.

Unser Ziel ist es, eine Reihenfolge der Elemente in der Teilaufordnung zurückzugeben, die die Aufzählung berücksichtigt. Das heißt, wir suchen eine Sequenz e_1, e_2, e_3, ..., e_n der Elemente in der Teilaufordnung, so dass, wenn i <= j und e_i vergleichbar ist mit e_j dann e_i <= e_j. Ich mache das mit einer Tiefensuche.

Klasse, die topologische Sortierung mittels Tiefensuche implementiert:

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

Helper-Klasse, die für Knoten Markierung als besucht, während der Tiefensuche zu tun:

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

Ich mache keinen Anspruch, dass diese ist die beste Implementierung des Algorithmus, aber ich glaube, dass es eine korrekte Implementierung ist. Außerdem habe ich keine IOrderedEnumerable wie gewünscht zurückgeben, aber das ist einfach zu tun, sobald wir an diesem Punkt sind.

Der Algorithmus arbeitet durch eine Tiefensuche durch die Elemente tun ein Element e auf die lineare Ordnung Zugabe (von _sorted im Algorithmus dargestellt), wenn wir schon alle Vorgänger von e hinzugefügt haben bereits auf die Bestellung hinzugefügt . Daher sollten Sie für jedes Element e, wenn wir es nicht bereits besucht haben, seine Vorgänger aufrufen und dann e hinzufügen. Somit ist dies der Kern des Algorithmus:

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

Als Beispiel betrachte man die Teilordnungs definiert auf Teilmengen von {1, 2, 3} wo X < Y wenn eine Teilmenge von XY ist. Ich dies wie folgt implementieren:

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

dann mit sets als Liste definiert von Teilmengen von {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()); 

Daraus ergibt sich die Reihenfolge:

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

, die die partielle Ordnung respektiert.

Das war eine Menge Spaß. Vielen Dank.

+0

Danke für die Antwort. Ich bin froh, dass es dir Spaß gemacht hat. Ich werde es morgen versuchen. Ein Detail, das mir aufgefallen ist, ist, dass Sie sagen, dass Sie eine Breitensuche verwenden werden, aber Ihr Code hat eine 'DepthFirstSearch'-Klasse. BTW, Testen der Lösung mit Sets war sehr ordentlich. – jpbochi

+0

Hoppla. Danke, dass du das verstanden hast. Ich habe eine Tiefensuche verwendet. – jason

+0

Dies ist ein netter Ansatz. Es gibt einige mögliche einfache Optimierungen/Vereinfachungen. Für den Anfang habe ich deine Lösung mit dem normalen 'IComparer' anstelle deines' IPartialComparer' getestet und es hat richtig funktioniert. Außerdem kann die 'TopologicalSorter'-Klasse statisch sein. Wie auch immer, der Ansatz, dem ** tehMick ** folgte, scheint einfacher und schneller zu sein. Ich denke, ich muss seine Antwort akzeptieren. – jpbochi

8

Dies ist meine optimierte und überholte Version von tehMick's answer.

Eine Änderung, die ich vorgenommen habe, war das Ersetzen der echten Liste von Werten, die für eine logische Liste übrig bleiben. Dafür habe ich zwei Arrays gleicher Größe. Einer hat alle Werte, und die anderen enthalten Flags, die anzeigen, ob jeder Wert ausgegeben wurde. Auf diese Weise vermeide ich die Kosten für die Größe eines echten List<Key>.

Eine andere Änderung ist, dass ich alle Schlüssel nur einmal zu Beginn der Iteration lesen. Aus Gründen, an die ich mich jetzt nicht erinnern kann (vielleicht war es nur mein Instinkt) mag ich nicht die Idee, die keySelector Funktion mehrmals aufzurufen.

Die letzten Berührungen waren die Parameterüberprüfung und eine zusätzliche Überladung, die einen impliziten Schlüsselvergleich verwendet. Ich hoffe, der Code ist ausreichend lesbar. Hör zu.

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

vielen Dank an alle, von Antwort Eric Mickelsen Start Ich habe kam mit meiner Version, wie ich es vorziehen, Nullwerte zu verwenden, keine Beziehung, um anzuzeigen, wie Lasse V. sagte 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)); 
    } 

dann habe ich die folgende Schnittstelle für den Vergleich

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

und diese Hilfsklasse

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

, dass die Verwendung ein wenig zu verschönern ermöglicht so meine Tests können wie folgt aussieht

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