So sortieren Sie abhängige Objekte nach Abhängigkeit

76

Ich habe eine Sammlung:

List<VPair<Item, List<Item>> dependencyHierarchy;

Das erste Element im Paar ist ein Objekt (Element) und das zweite ist eine Sammlung von Objekten des gleichen Typs, von denen das erste abhängt. Ich möchte eine List<Item>in der Reihenfolge der Abhängigkeit erhalten, daher gibt es keine Elemente, die vom ersten Element usw. abhängen (keine zyklische Abhängigkeit!).

Eingang:

Punkt 4 hängt von Punkt 3 und Punkt 5 ab
Punkt3 hängt von Punkt1 ab
Item1 hängt von niemandem ab
Punkt 2 hängt von Punkt 4 ab 
Item5 hängt von niemandem ab 

Ergebnis:

Gegenstand 1
Item5
Item3
Item4
Item2

Vielen Dank.

LÖSUNG:

Topologische Sortierung (danke an Loïc Février für die Idee)

und

Beispiel auf C # , Beispiel auf Java (danke an xcud für großartige Beispiele)

garik
quelle
Für alle, die darauf stoßen und nach einem C # -Nuget-Paket suchen, habe ich eines erstellt: github.com/madelson/MedallionTopologicalSort
ChaseMedallion

Antworten:

89

Nachdem ich eine Weile damit zu kämpfen hatte, ist hier mein Versuch einer TSort-Erweiterungsmethode im Linq-Stil:

public static IEnumerable<T> TSort<T>( this IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle = false )
{
    var sorted = new List<T>();
    var visited = new HashSet<T>();

    foreach( var item in source )
        Visit( item, visited, sorted, dependencies, throwOnCycle );

    return sorted;
}

private static void Visit<T>( T item, HashSet<T> visited, List<T> sorted, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle )
{
    if( !visited.Contains( item ) )
    {
        visited.Add( item );

        foreach( var dep in dependencies( item ) )
            Visit( dep, visited, sorted, dependencies, throwOnCycle );

        sorted.Add( item );
    }
    else
    {
        if( throwOnCycle && !sorted.Contains( item ) )
            throw new Exception( "Cyclic dependency found" );
    }
}
Mesmo
quelle
5
+1 Viel einfacher und scheint für mich zu funktionieren. Die einzige Änderung, die ich vorgenommen habe, war die Verwendung von a Dictionary<T, object>anstelle von List<T>for visited- es sollte für große Sammlungen schneller sein.
EM0
3
Danke EM - Ich habe aktualisiert, um ein HashSet für die besuchte Sammlung zu verwenden.
Mesmo
2
+1 Ich habe mir den Pseudocode für den Algorithmus auf Wikipedia angesehen und dachte, die Implementierung wäre einfach genug, aber die eigentliche Implementierung ist noch einfacher!
ta.speot.is
17
Danke DMM! Das funktioniert bei mir mit einer Modifikation: Am Ende von habe if( !visited.Contains( item ) )ich so etwas wie (in Java) hinzugefügt else if(!sorted.contains(item)){throw new Exception("Invalid dependency cycle!");}, um den Fall zu verwalten, in dem A-> B, B-> C und C-> A sind.
Elektrotyp
3
Sehr hilfreiche Antwort, stellen Sie sich jedoch das Szenario vor, wo sources = {A, B}, wo dependencies(B) = {A}. Der Code, wie Sie ihn haben, würde dies als "zyklische Abhängigkeit" erkennen, was falsch zu sein scheint.
Supericy
20

Dafür gibt es ein Nuget .

Für diejenigen unter uns, die das Rad nicht neu erfinden möchten: Verwenden Sie nuget, um die QuickGraph .NET-Bibliothek zu installieren , die mehrere Diagrammalgorithmen einschließlich topologischer Sortierung enthält .

Um es zu verwenden, müssen Sie eine Instanz von AdjacencyGraph<,>z AdjacencyGraph<String, SEdge<String>>. Wenn Sie dann die entsprechenden Erweiterungen hinzufügen:

using QuickGraph.Algorithms;

Du kannst anrufen:

var sorted = myGraph.TopologicalSort();

Um eine Liste der sortierten Knoten zu erhalten.

sinelaw
quelle
12

Ich mochte die Antwort von DMM, aber es wird davon ausgegangen, dass die Eingabeknoten Blätter sind (was möglicherweise nicht den Erwartungen entspricht).

Ich poste eine alternative Lösung mit LINQ, die diese Annahme nicht macht. Darüber hinaus kann diese Lösung verwendet yield returnwerden, um die Blätter schnell zurückgeben zu können (z TakeWhile. B. ).

public static IEnumerable<T> TopologicalSort<T>(this IEnumerable<T> nodes, 
                                                Func<T, IEnumerable<T>> connected)
{
    var elems = nodes.ToDictionary(node => node, 
                                   node => new HashSet<T>(connected(node)));
    while (elems.Count > 0)
    {
        var elem = elems.FirstOrDefault(x => x.Value.Count == 0);
        if (elem.Key == null)
        {
            throw new ArgumentException("Cyclic connections are not allowed");
        }
        elems.Remove(elem.Key);
        foreach (var selem in elems)
        {
            selem.Value.Remove(elem.Key);
        }
        yield return elem.Key;
    }
}
Krumelur
quelle
Dies ist die kompakteste topologische Sortierimplementierung, die ich bisher gesehen habe.
Timo
1
"aber es wird davon ausgegangen, dass die Eingabeknoten Blätter sind", verstehe ich nicht. Können Sie erklären?
Osexpert
1
@osexpert, so funktioniert der Algorithmus: Wir müssen immer mit Blättern arbeiten - Knoten, die nicht von anderen Knoten abhängen. Dieser Code stellt sicher, dass : var elem = elems.FirstOrDefault(x => x.Value.Count == 0);. Stellt insbesondere x.Value.Count == 0sicher, dass für den angegebenen Knoten keine Abhängigkeiten bestehen.
Danylo Yelizarov
Auch diese Implementierung ist kompakt, aber nicht optimal. Wir suchen bei jeder Iteration nach einem Blatt, wodurch es zu O (n ^ 2) wird. Es könnte verbessert werden, indem eine Warteschlange mit allen Blättern vor einer while-Schleife erstellt und dann neue Elemente hinzugefügt werden, solange sie "unabhängig" werden.
Danylo Yelizarov
Richtig, und IIRC das Obige basierte auf etwas, das ich für den Umgang mit Abhängigkeiten in einem Compiler in der Größenordnung von Hunderten von Modulen geschrieben habe, für die, wenn es einigermaßen gut funktioniert.
Krumelur
6

Dies ist meine eigene Neuimplementierung der topologischen Sortierung. Die Idee basiert auf http://tawani.blogspot.com/2009/02/topological-sorting-and-cyclic.html (Der portierte Java-Quellcode verbraucht zu viel Speicher.) Das Überprüfen von 50.000 Objekten kostet 50.000 * 50.000 * 4 = 10 GB, was nicht akzeptabel ist. Außerdem gibt es an einigen Stellen noch Java-Codierungskonventionen.

using System.Collections.Generic;
using System.Diagnostics;

namespace Modules
{
    /// <summary>
    /// Provides fast-algorithm and low-memory usage to sort objects based on their dependencies. 
    /// </summary>
    /// <remarks>
    /// Definition: http://en.wikipedia.org/wiki/Topological_sorting
    /// Source code credited to: http://tawani.blogspot.com/2009/02/topological-sorting-and-cyclic.html    
    /// Original Java source code: http://www.java2s.com/Code/Java/Collections-Data-Structure/Topologicalsorting.htm
    /// </remarks>
    /// <author>ThangTran</author>
    /// <history>
    /// 2012.03.21 - ThangTran: rewritten based on <see cref="TopologicalSorter"/>.
    /// </history>
    public class DependencySorter<T>
    {
        //**************************************************
        //
        // Private members
        //
        //**************************************************

        #region Private members

        /// <summary>
        /// Gets the dependency matrix used by this instance.
        /// </summary>
        private readonly Dictionary<T, Dictionary<T, object>> _matrix = new Dictionary<T, Dictionary<T, object>>();

        #endregion


        //**************************************************
        //
        // Public methods
        //
        //**************************************************

        #region Public methods

        /// <summary>
        /// Adds a list of objects that will be sorted.
        /// </summary>
        public void AddObjects(params T[] objects)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(objects != null);
            Debug.Assert(objects.Length > 0);
            // --- End parameters checking code -------------------------------

            // add to matrix
            foreach (T obj in objects)
            {
                // add to dictionary
                _matrix.Add(obj, new Dictionary<T, object>());
            }
        }

        /// <summary>
        /// Sets dependencies of given object.
        /// This means <paramref name="obj"/> depends on these <paramref name="dependsOnObjects"/> to run.
        /// Please make sure objects given in the <paramref name="obj"/> and <paramref name="dependsOnObjects"/> are added first.
        /// </summary>
        public void SetDependencies(T obj, params T[] dependsOnObjects)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(dependsOnObjects != null);
            // --- End parameters checking code -------------------------------

            // set dependencies
            Dictionary<T, object> dependencies = _matrix[obj];
            dependencies.Clear();

            // for each depended objects, add to dependencies
            foreach (T dependsOnObject in dependsOnObjects)
            {
                dependencies.Add(dependsOnObject, null);
            }
        }

        /// <summary>
        /// Sorts objects based on this dependencies.
        /// Note: because of the nature of algorithm and memory usage efficiency, this method can be used only one time.
        /// </summary>
        public T[] Sort()
        {
            // prepare result
            List<T> result = new List<T>(_matrix.Count);

            // while there are still object to get
            while (_matrix.Count > 0)
            {
                // get an independent object
                T independentObject;
                if (!this.GetIndependentObject(out independentObject))
                {
                    // circular dependency found
                    throw new CircularReferenceException();
                }

                // add to result
                result.Add(independentObject);

                // delete processed object
                this.DeleteObject(independentObject);
            }

            // return result
            return result.ToArray();
        }

        #endregion


        //**************************************************
        //
        // Private methods
        //
        //**************************************************

        #region Private methods

        /// <summary>
        /// Returns independent object or returns NULL if no independent object is found.
        /// </summary>
        private bool GetIndependentObject(out T result)
        {
            // for each object
            foreach (KeyValuePair<T, Dictionary<T, object>> pair in _matrix)
            {
                // if the object contains any dependency
                if (pair.Value.Count > 0)
                {
                    // has dependency, skip it
                    continue;
                }

                // found
                result = pair.Key;
                return true;
            }

            // not found
            result = default(T);
            return false;
        }

        /// <summary>
        /// Deletes given object from the matrix.
        /// </summary>
        private void DeleteObject(T obj)
        {
            // delete object from matrix
            _matrix.Remove(obj);

            // for each object, remove the dependency reference
            foreach (KeyValuePair<T, Dictionary<T, object>> pair in _matrix)
            {
                // if current object depends on deleting object
                pair.Value.Remove(obj);
            }
        }


        #endregion
    }

    /// <summary>
    /// Represents a circular reference exception when sorting dependency objects.
    /// </summary>
    public class CircularReferenceException : Exception
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CircularReferenceException"/> class.
        /// </summary>
        public CircularReferenceException()
            : base("Circular reference found.")
        {            
        }
    }
}
Thang Tran
quelle
1

Ich würde es mir leichter machen, indem ich die Abhängigkeiten eines Elements innerhalb des Elements selbst speichere:

public class Item
{
    private List<Item> m_Dependencies = new List<Item>();

    protected AddDependency(Item _item) { m_Dependencies.Add(_item); }

    public Item()
    {
    }; // eo ctor

    public List<Item> Dependencies {get{return(m_Dependencies);};}
} // eo class Item

Vor diesem Hintergrund können Sie einen benutzerdefinierten Sortierdelegaten für List implementieren, der danach sortiert, ob das angegebene Element in der Abhängigkeitsliste des anderen enthalten ist:

int CompareItem(Item _1, Item _2)
{
    if(_2.Dependencies.Contains(_1))
        return(-1);
    else if(_1.Dependencies.Contains(_2))
        return(1);
    else
        return(0);
}
Moo-Saft
quelle
3
Die Bestellung ist nicht vollständig und funktioniert daher nicht. Sie müssten für jeden Gegenstand die Liste aller Gegenstände haben, von denen er oder einer seiner Nachkommen abhängt. (dh den vollständigen gerichteten azyklischen Graphen erstellen) Leicht zu findendes Gegenbeispiel: 1 hängt von 3 und 2 ab, 3 von 4. [3 4 1 2] wird nach Ihrem Algorithmus sortiert. Aber 3 muss nach 1 sein.
Loïc Février
ah, Danke. Daran habe ich nicht gedacht. Sehr geschätzt. Hier kommen die Abstimmungen! :)
Moo-Juice
Loic, würdest du so freundlich sein, weiter zu erklären, warum mein Vorschlag nicht funktioniert? Ich versuche nicht zu sagen, dass es richtig ist, aber nur damit ich besser lernen kann. Ich habe es gerade hier versucht und sowohl für das Beispiel des OP als auch für Ihr Beispiel war meine resultierende Liste in Ordnung. Zum Glück vielleicht? :) In Anbetracht Ihres Beispiels (1 abhängig von 3 & 2, 2 abhängig von 4) war meine resultierende Sortierung [4, 3, 2, 1]
Moo-Juice
1
Um eine Liste zu bestellen, prüft jeder Sortieralgorithmus nur, ob aufeinanderfolgende Elemente sortiert sind. In Ihrem Fall bedeutet sortiert: Der zweite hängt nicht vom ersten ab. [3 4 1 2] und [4, 3, 2, 1] sind zwei mögliche Ordnungen. Der Algorithmus nimmt Transitivität an: Wenn x <= y und y <= z, dann ist x <= z. In diesem Fall ist es nicht wahr. Sie können die Daten jedoch ändern: Wenn x von y und y von z abhängt, fügen Sie z zur Abhängigkeitsliste von x hinzu. Ihre Teilbestellung ist jetzt eine vollständige Teilbestellung und ein Sortieralgorithmus kann funktionieren. Aber die Komplexität, um es zu "vervollständigen", ist O (N ^ 2), wobei 'O (N) für die topologische Sortierung ist.
Loïc Février
Vielen Dank, dass Sie sich die Zeit genommen haben, dies weiter zu erklären. Bei weiteren Tests ist meine Idee tatsächlich gebrochen und auf den Arsch gefallen. Viel Spaß beim Programmieren!
Moo-Juice
1

Eine andere Idee für Fälle mit nur einem "Elternteil", abhängig von:

Anstelle von Deps würden Sie die Eltern speichern.
So können Sie sehr leicht feststellen, ob ein Problem von einem anderen abhängig ist.
Und dann verwenden Comparable<T>, was die Abhängigkeiten "kleiner" und die Abhängigkeit "größer" beanspruchen würde.
Und dann einfach anrufenCollections.sort( List<T>, ParentComparator<T>);

Für ein Szenario mit mehreren Elternteilen wäre eine Baumsuche erforderlich, die zu einer langsamen Ausführung führen würde. Dies könnte jedoch durch einen Cache in Form einer A * -Sortiermatrix gelöst werden.

Ondra Žižka
quelle
1

Ich habe die Idee von DMM mit dem Algorithmus für die Tiefensuche auf Wikipedia zusammengeführt. Es funktioniert perfekt für das, was ich brauchte.

public static class TopologicalSorter
{
public static List<string> LastCyclicOrder = new List<string>(); //used to see what caused the cycle

sealed class ItemTag
{
  public enum SortTag
  {
    NotMarked,
    TempMarked,
    Marked
  }

  public string Item { get; set; }
  public SortTag Tag { get; set; }

  public ItemTag(string item)
  {
    Item = item;
    Tag = SortTag.NotMarked;
  }
}

public static IEnumerable<string> TSort(this IEnumerable<string> source, Func<string, IEnumerable<string>> dependencies)
{
  TopologicalSorter.LastCyclicOrder.Clear();

  List<ItemTag> allNodes = new List<ItemTag>();
  HashSet<string> sorted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

  foreach (string item in source)
  {
    if (!allNodes.Where(n => string.Equals(n.Item, item, StringComparison.OrdinalIgnoreCase)).Any())
    {
      allNodes.Add(new ItemTag(item)); //don't insert duplicates
    }
    foreach (string dep in dependencies(item))
    {
      if (allNodes.Where(n => string.Equals(n.Item, dep, StringComparison.OrdinalIgnoreCase)).Any()) continue; //don't insert duplicates
      allNodes.Add(new ItemTag(dep));
    }
  }

  foreach (ItemTag tag in allNodes)
  {
    Visit(tag, allNodes, dependencies, sorted);
  }

  return sorted;
}

static void Visit(ItemTag tag, List<ItemTag> allNodes, Func<string, IEnumerable<string>> dependencies, HashSet<string> sorted)
{
  if (tag.Tag == ItemTag.SortTag.TempMarked)
  {
    throw new GraphIsCyclicException();
  }
  else if (tag.Tag == ItemTag.SortTag.NotMarked)
  {
    tag.Tag = ItemTag.SortTag.TempMarked;
    LastCyclicOrder.Add(tag.Item);

    foreach (ItemTag dep in dependencies(tag.Item).Select(s => allNodes.Where(t => string.Equals(s, t.Item, StringComparison.OrdinalIgnoreCase)).First())) //get item tag which falls with used string
      Visit(dep, allNodes, dependencies, sorted);

    LastCyclicOrder.Remove(tag.Item);
    tag.Tag = ItemTag.SortTag.Marked;
    sorted.Add(tag.Item);
  }
}
}
user1132959
quelle
1

Ich mag keine rekursiven Methoden, daher ist DMM nicht verfügbar. Krumelur sieht gut aus, scheint aber viel Speicher zu verbrauchen? Eine alternative stapelbasierte Methode erstellt, die zu funktionieren scheint. Verwendet dieselbe DFS-Logik wie DMM und ich habe diese Lösungen als Vergleich beim Testen verwendet.

    public static IEnumerable<T> TopogicalSequenceDFS<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> deps)
    {
        HashSet<T> yielded = new HashSet<T>();
        HashSet<T> visited = new HashSet<T>();
        Stack<Tuple<T, IEnumerator<T>>> stack = new Stack<Tuple<T, IEnumerator<T>>>();

        foreach (T t in source)
        {
            stack.Clear();
            if (visited.Add(t))
                stack.Push(new Tuple<T, IEnumerator<T>>(t, deps(t).GetEnumerator()));

            while (stack.Count > 0)
            {
                var p = stack.Peek();
                bool depPushed = false;
                while (p.Item2.MoveNext())
                {
                    var curr = p.Item2.Current;
                    if (visited.Add(curr))
                    {
                        stack.Push(new Tuple<T, IEnumerator<T>>(curr, deps(curr).GetEnumerator()));
                        depPushed = true;
                        break;
                    }
                    else if (!yielded.Contains(curr))
                        throw new Exception("cycle");
                }

                if (!depPushed)
                {
                    p = stack.Pop();
                    if (!yielded.Add(p.Item1))
                        throw new Exception("bug");
                    yield return p.Item1;
                }
            }
        }
    }

Hier ist auch eine einfachere stapelbasierte BFS-Variante. Es wird ein anderes Ergebnis als das oben genannte erzeugen, aber immer noch gültig. Ich bin mir nicht sicher, ob die Verwendung der oben genannten DFS-Variante einen Vorteil hat, aber es hat Spaß gemacht, sie zu erstellen.

    public static IEnumerable<T> TopologicalSequenceBFS<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies)
    {
        var yielded = new HashSet<T>();
        var visited = new HashSet<T>();
        var stack = new Stack<Tuple<T, bool>>(source.Select(s => new Tuple<T, bool>(s, false))); // bool signals Add to sorted

        while (stack.Count > 0)
        {
            var item = stack.Pop();
            if (!item.Item2)
            {
                if (visited.Add(item.Item1))
                {
                    stack.Push(new Tuple<T, bool>(item.Item1, true)); // To be added after processing the dependencies
                    foreach (var dep in dependencies(item.Item1))
                        stack.Push(new Tuple<T, bool>(dep, false));
                }
                else if (!yielded.Contains(item.Item1))
                    throw new Exception("cyclic");
            }
            else
            {
                if (!yielded.Add(item.Item1))
                    throw new Exception("bug");
                yield return item.Item1;
            }
        }
    }

Für .NET 4.7+ empfehle ich, Tuple durch ValueTuple zu ersetzen, um weniger Speicher zu verwenden. In älteren .NET-Versionen kann Tuple durch KeyValuePair ersetzt werden.

osexpert
quelle
0

Dies ist überarbeiteter Code aus dem Beitrag https://stackoverflow.com/a/9991916/4805491 .

// Version 1
public static class TopologicalSorter<T> where T : class {

    public struct Item {
        public readonly T Object;
        public readonly T Dependency;
        public Item(T @object, T dependency) {
            Object = @object;
            Dependency = dependency;
        }
    }


    public static T[] Sort(T[] objects, Func<T, T, bool> isDependency) {
        return Sort( objects.ToList(), isDependency ).ToArray();
    }

    public static T[] Sort(T[] objects, Item[] dependencies) {
        return Sort( objects.ToList(), dependencies.ToList() ).ToArray();
    }


    private static List<T> Sort(List<T> objects, Func<T, T, bool> isDependency) {
        return Sort( objects, GetDependencies( objects, isDependency ) );
    }

    private static List<T> Sort(List<T> objects, List<Item> dependencies) {
        var result = new List<T>( objects.Count );

        while (objects.Any()) {
            var obj = GetIndependentObject( objects, dependencies );
            RemoveObject( obj, objects, dependencies );
            result.Add( obj );
        }

        return result;
    }

    private static List<Item> GetDependencies(List<T> objects, Func<T, T, bool> isDependency) {
        var dependencies = new List<Item>();

        for (var i = 0; i < objects.Count; i++) {
            var obj1 = objects[i];
            for (var j = i + 1; j < objects.Count; j++) {
                var obj2 = objects[j];
                if (isDependency( obj1, obj2 )) dependencies.Add( new Item( obj1, obj2 ) ); // obj2 is dependency of obj1
                if (isDependency( obj2, obj1 )) dependencies.Add( new Item( obj2, obj1 ) ); // obj1 is dependency of obj2
            }
        }

        return dependencies;
    }


    private static T GetIndependentObject(List<T> objects, List<Item> dependencies) {
        foreach (var item in objects) {
            if (!GetDependencies( item, dependencies ).Any()) return item;
        }
        throw new Exception( "Circular reference found" );
    }

    private static IEnumerable<Item> GetDependencies(T obj, List<Item> dependencies) {
        return dependencies.Where( i => i.Object == obj );
    }

    private static void RemoveObject(T obj, List<T> objects, List<Item> dependencies) {
        objects.Remove( obj );
        dependencies.RemoveAll( i => i.Object == obj || i.Dependency == obj );
    }

}


// Version 2
public class TopologicalSorter {

    public static T[] Sort<T>(T[] source, Func<T, T, bool> isDependency) {
        var list = new LinkedList<T>( source );
        var result = new List<T>();

        while (list.Any()) {
            var obj = GetIndependentObject( list, isDependency );
            list.Remove( obj );
            result.Add( obj );
        }

        return result.ToArray();
    }

    private static T GetIndependentObject<T>(IEnumerable<T> list, Func<T, T, bool> isDependency) {
        return list.First( i => !GetDependencies( i, list, isDependency ).Any() );
    }

    private static IEnumerable<T> GetDependencies<T>(T obj, IEnumerable<T> list, Func<T, T, bool> isDependency) {
        return list.Where( i => isDependency( obj, i ) ); // i is dependency of obj
    }

}
Denis535
quelle