Як сортувати залежні об’єкти за залежністю


77

У мене є колекція:

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

Перший елемент у парі - це якийсь об'єкт (елемент), а другий - колекція однотипних об'єктів, від яких залежить перший. Я хочу отримати List<Item>порядок залежності, тому немає елементів, які залежать від першого елемента тощо (відсутність циклічної залежності!).

Вхідні дані:

Пункт4 залежить від Пункту3 та Пункту5
Пункт 3 залежить від Пункту 1
Елемент 1 не залежить ні від одного
Пункт 2 залежить від Пункту 4 
Пункт 5 не залежить ні від одного 

Результат:

Пункт1
Пункт5
Пункт3
Пункт4
Пункт2

Дякую.

РІШЕННЯ:

Топологічне сортування (подяка Лоїку Февріє за ідею)

і

приклад на C # , приклад на Java (спасибі xcud за чудові приклади)


Для тих, хто стикається з цим і шукає пакет C # nuget, ось один, який я створив: github.com/madelson/MedallionTopologicalSort
ChaseMedallion

Відповіді:



89

Потрудившись із цим деякий час, ось моя спроба методу розширення TSort у стилі Linq:

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

5
+1 Набагато простіше і, здається, працює для мене. Єдиною зміною, яку я зробив, було використання Dictionary<T, object>замість List<T>for visited- це повинно бути швидшим для великих колекцій.
EM0,

3
Дякую EM - я оновив використання HashSet для відвіданої колекції.
Mesmo

2
+1 Я подивився псевдокод для алгоритму у Вікіпедії і думав, що його буде досить легко реалізувати, але фактична реалізація ще простіше!
ta.speot.is

17
Дякую DMM! Це працює для мене з однією модифікацією: наприкінці if( !visited.Contains( item ) )я додав щось на зразок (у Java) else if(!sorted.contains(item)){throw new Exception("Invalid dependency cycle!");}для управління випадком, коли A-> B, B-> C та C-> A.
електротип

3
Дуже корисна відповідь, проте, уявіть сценарій, де sources = {A, B}, де dependencies(B) = {A}. Код, який у вас є, виявив би це як "циклічну залежність", що здається неправильним.
Supericy

20

Для цього є самородка .

Для тих з нас , хто вважає за краще не винаходити колесо: використання NuGet для установки QuickGraph бібліотеки .NET, яка включає в себе кілька алгоритмів на графах , включаючи топологічну сортування .

Для його використання потрібно створити екземпляр AdjacencyGraph<,>такого як AdjacencyGraph<String, SEdge<String>>. Тоді, якщо ви включите відповідні розширення:

using QuickGraph.Algorithms;

Ви можете зателефонувати:

var sorted = myGraph.TopologicalSort();

Щоб отримати список відсортованих вузлів.


13

Мені сподобалася відповідь DMM, але вона передбачає, що вхідними вузлами є листи (що може бути, а може не бути очікуваним).

Я публікую альтернативне рішення, використовуючи LINQ, яке не робить цього припущення. Крім того, це рішення використовується, yield returnщоб мати можливість швидко повернути листя (використовуючи, наприклад TakeWhile).

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

Це найбільш компактна реалізація топологічного сортування, яку я бачив на даний момент.
Тимо

1
"але це передбачає, що вхідними вузлами є листя", я не розумію. Ти можеш пояснити?
osexpert

1
@osexpert, ось як працює алгоритм: нам потрібно завжди працювати з листям - вузлами, які не залежать від будь-якого іншого вузла. Цей код гарантує , що: var elem = elems.FirstOrDefault(x => x.Value.Count == 0);. Зокрема, x.Value.Count == 0гарантує відсутність залежностей для даного вузла.
Данило Єлізаров

Крім того, ця реалізація є компактною, але не оптимальною. Ми шукаємо лист на кожній ітерації, що робить його O (n ^ 2). Це можна покращити, створивши чергу з усіма листками до певного циклу, а потім додавши до неї нові елементи, доки вони стануть "незалежними".
Данило Єлізаров

Правда, і IIRC вищезасноване базувалося на чомусь, що я писав для обробки залежностей у компіляторі з порядком сотень модулів, для чого, якщо він виконувався досить добре.
Крумелур

6

Це моя власна повторна реалізація топологічного сортування, ідея базується на http://tawani.blogspot.com/2009/02/topological-sorting-and-cyclic.html (Перенесений вихідний код Java споживає занадто багато пам'яті, перевірка об'єктів 50 тис. коштує 50 тис. * 50 тис. * 4 = 10 Гб, що є неприпустимим.

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.")
        {            
        }
    }
}

1

Я полегшив би собі це, зберігаючи залежності Елемента в самому Елементі:

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

Потім, з огляду на це, ви можете реалізувати власний делегат Сортування для Списку, який сортує залежно від того, чи міститься даний Елемент у списку залежностей іншого:

int CompareItem(Item _1, Item _2)
{
    if(_2.Dependencies.Contains(_1))
        return(-1);
    else if(_1.Dependencies.Contains(_2))
        return(1);
    else
        return(0);
}

3
Замовлення не повне, тому воно не буде працювати. Вам потрібно було б мати для кожного предмета список усіх предметів, від яких він або будь-який з його нащадків залежить. (тобто побудуйте повний спрямований ациклічний графік) Легко знайти контр-приклад: 1 залежить від 3 і 2, 3 від 4. [3 4 1 2] сортується відповідно до вашого алгоритму. Але 3 повинні бути після 1.
Loïc Février

ах, дякую. Я про це не думав. Цінується. Ось голоси проти! :)
Moo-Juice

Лоїк, будь ласка, поясни, чому моя пропозиція не працює? Не намагаючись сказати, що це правильно, а лише для того, щоб я міг вчитися краще. Я просто спробував тут, і як для прикладу OP, так і для вашого прикладу, мій підсумковий список був у порядку. На щастя, можливо? :) Беручи до уваги ваш приклад (1 залежно від 3 та 2, 2 залежно від 4), результатом мого сортування було [4, 3, 2, 1]
Moo-Juice

1
Для замовлення списку кожен алгоритм сортування перевірятиме лише сортування послідовних елементів. У вашому випадку сортування означає: другий не залежить від першого. [3 4 1 2] та [4, 3, 2, 1] - два можливі замовлення. Алгоритм передбачає транзитивність: якщо x <= y та y <= z, то x <= z. У цьому випадку це неправда. Однак ви можете змінити дані: якщо x залежить від y, а y залежить від z, додайте z до списку залежностей x '. Ваше часткове замовлення тепер є повним частковим замовленням, і алгоритм сортування може працювати. Але складність "завершити" - O (N ^ 2), де як 'O (N) для топологічного сортування.
Loïc Février

Дякуємо, що знайшли час пояснити це далі. При подальшому тестуванні моя ідея справді зламалася і впала на дупу. Щасливого програмування!
Moo-Juice

1

Інша ідея для випадків, коли лише один "батько" залежить:

Замість відділів ви б зберігали батьків.
Тож ви можете дуже легко сказати, чи є проблема залежністю якоїсь іншої.
А потім використання Comparable<T>, яке вимагало б залежності "меншу" та залежність "більшу".
А потім просто зателефонуйтеCollections.sort( List<T>, ParentComparator<T>);

Для багатоповерхового сценарію потрібен пошук по дереву, що призведе до повільного виконання. Але це може бути вирішено кешем у вигляді матриці сортування A *.


1

Я об’єднав ідею DMM з алгоритмом глибокого пошуку у Вікіпедії. Це ідеально підходить для того, що мені потрібно.

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

1

Я не люблю рекурсивні методи, тому DMM вийшов. Крумелур виглядає добре, але, схоже, використовує багато пам'яті? Зроблено альтернативний метод на основі стека, який, здається, працює. Використовує ту саму логіку DFS, що і DMM, і я використовував ці рішення як порівняння під час тестування.

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

Ось також простіший варіант BFS на основі стеку. Це дасть результат, відмінний від наведеного, але все ще діючий. Я не впевнений, чи є якась перевага у використанні наведеного вище варіанту DFS, але було цікаво створювати його.

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

Для .NET 4.7+ я пропоную замінити Tuple на ValueTuple для меншого використання пам'яті. У старих версіях .NET Tuple можна замінити на KeyValuePair.


0

Це перероблений код із повідомлення 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
    }

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.