web-dev-qa-db-fra.com

Un moyen agréable et universel de convertir une liste d'éléments en arbre

J'ai une liste de catégories:

╔════╦═════════════╦═════════════╗
║ Id ║ Name        ║ Parent_id   ║
╠════╬═════════════╬═════════════╣
║ 1  ║ Sports      ║ 0           ║
║ 2  ║ Balls       ║ 1           ║
║ 3  ║ Shoes       ║ 1           ║
║ 4  ║ Electronics ║ 0           ║
║ 5  ║ Cameras     ║ 4           ║
║ 6  ║ Lenses      ║ 5           ║
║ 7  ║ Tripod      ║ 5           ║
║ 8  ║ Computers   ║ 4           ║
║ 9  ║ Laptops     ║ 8           ║
║ 10 ║ Empty       ║ 0           ║
║ -1 ║ Broken      ║ 999         ║
╚════╩═════════════╩═════════════╝ 

Chaque catégorie a un parent. Quand parent est 0 - cela signifie que c'est la catégorie racine.

Quel est le moyen le plus gentil de le convertir en arborescence comme ci-dessous?

enter image description here

En d'autres termes, comment importer des données de cette structure:

class category
{
    public int Id;
    public int ParentId;
    public string Name;
}

Dans celui-ci:

class category
{
    public int Id;
    public int ParentId;
    public string Name;

    public List<Category> Subcategories;
}

de manière universelle? // Universel signifie non seulement pour la classe mentionnée.

Avez-vous des idées intelligentes? ;)


Les données:

var categories = new List<category>() {
    new category(1, "Sport", 0),
    new category(2, "Balls", 1),
    new category(3, "Shoes", 1),
    new category(4, "Electronics", 0),
    new category(5, "Cameras", 4),
    new category(6, "Lenses", 5),  
    new category(7, "Tripod", 5), 
    new category(8, "Computers", 4),
    new category(9, "Laptops", 8),
    new category(10, "Empty", 0),
    new category(-1, "Broken", 999),
};
31
user2930009

Si vous voulez avoir la méthode universal vous aurez besoin d'une classe supplémentaire:

public class TreeItem<T>
{
    public T Item { get; set; }
    public IEnumerable<TreeItem<T>> Children { get; set; }
}

Puis utilisez-le avec cette aide:

internal static class GenericHelpers
{
    /// <summary>
    /// Generates tree of items from item list
    /// </summary>
    /// 
    /// <typeparam name="T">Type of item in collection</typeparam>
    /// <typeparam name="K">Type of parent_id</typeparam>
    /// 
    /// <param name="collection">Collection of items</param>
    /// <param name="id_selector">Function extracting item's id</param>
    /// <param name="parent_id_selector">Function extracting item's parent_id</param>
    /// <param name="root_id">Root element id</param>
    /// 
    /// <returns>Tree of items</returns>
    public static IEnumerable<TreeItem<T>> GenerateTree<T, K>(
        this IEnumerable<T> collection,
        Func<T, K> id_selector,
        Func<T, K> parent_id_selector,
        K root_id = default(K))
    {
        foreach (var c in collection.Where(c => parent_id_selector(c).Equals(root_id)))
        {
            yield return new TreeItem<T>
            {
                Item = c,
                Children = collection.GenerateTree(id_selector, parent_id_selector, id_selector(c))
            };
        }
    }
}

Usage:

var root = categories.GenerateTree(c => c.Id, c => c.ParentId);

Essai:

static void Test(IEnumerable<TreeItem<category>> categories, int deep = 0)
{
    foreach (var c in categories)
    {
        Console.WriteLine(new String('\t', deep) + c.Item.Name);
        Test(c.Children, deep + 1);
    }
}
// ...
Test(root);

Sortie

Sport
    Balls
    Shoes
Electronics
    Cameras
        Lenses  
        Tripod
    Computers
        Laptops
Empty
33
Damian Drygiel
foreach (var cat in categories)
{
    cat.Subcategories = categories.Where(child => child.ParentId == cat.Id)
                                  .ToList();
}

Vous aurez la complexité O(n*n).


Une manière plus optimisée consiste à utiliser des tables de recherche:

var childsHash = categories.ToLookup(cat => cat.ParentId);

foreach (var cat in categories)
{
    cat.Subcategories = childsHash[cat.Id].ToList();
}

Ce qui vous donne O(2*n)O(n)

En conséquence, vous aurez la structure suivante (montrée par LinqPad):

enter image description here

18
Ilya Ivanov

Vous pouvez utiliser la requête de base de données ci-dessous pour obtenir la liste des catégories avec des relations parent-enfant:

WITH tree (categoryId, parentId, level, categoryName, rn) as 
(
   SELECT categoryId, parentid, 0 as level, categoryName,

       convert(varchar(max),right(row_number() over (order by categoryId),10)) rn
   FROM Categories
   WHERE parentid = 0

   UNION ALL

   SELECT c2.categoryId, c2.parentid, tree.level + 1, c2.categoryName,

       rn + '/' + convert(varchar(max),right(row_number() over 
       (order by tree.categoryId),10))
   FROM Categories c2 

     INNER JOIN tree ON tree.categoryId = c2.parentid
)

SELECT *
FROM tree
order by RN

J'espère que cela vous aidera.

3
Girish Vadhel

en utilisant Ilya Ivanov algorithm (voir ci-dessus), j’ai rendu la méthode plus générique.

public static IEnumerable<TJ> GenerateTree<T, TK, TJ>(this IEnumerable<T> items,
                                                      Func<T, TK> idSelector,
                                                      Func<T, TK> parentSelector,
                                                      Func<T, IEnumerable<T>, TJ> outSelector)
{
       IList<T> mlist = items.ToList();

       ILookup<TK, T> mcl = mlist.ToLookup(parentSelector);

       return mlist.Select(cat => outSelector(cat, mcl[idSelector(cat)]));
}

utilisation:

IEnumerable<Category> mlc = GenerateTree(categories,
                                         c => c.Id, 
                                         c => c.ParentId,
                                         (c, ci) => new Category
                                         {
                                              Id = c.Id,
                                              Name = c.Name,
                                              ParentId = c.ParentId ,
                                              Subcategories = ci
                                         });
1
Andrew

Voici un petit exemple que j'ai fouetté. C'est joli "générique".

On pourrait aussi faire une approche générique en définissant une interface (ce qui permettrait alors de simplifier les arguments de la fonction) - mais j’ai choisi de ne pas le faire. Dans tous les cas, les fonctions de "mappeur" et de sélecteur permettent de travailler avec différents types.

Notez également que ceci est pas une implémentation très efficace (car elle garde tous les enfants possibles pour tous les sous-arbres et les répète à plusieurs reprises), mais peut convenir à la tâche donnée. Dans le passé, j'ai également utilisé une approche Dictionary<key,collection>, qui a de meilleures limites, mais je n'avais pas envie de l'écrire de cette façon :)

Cela fonctionne comme un "programme LINQPad C #". Prendre plaisir!

// F - flat type
// H - hiearchial type
IEnumerable<H> MakeHierarchy<F,H>(
    // Remaining items to process
    IEnumerable<F> flat,
    // Current "parent" to look for
    object parentKey,
    // Find key for given F-type
    Func<F,object> key,
    // Convert between types
    Func<F,IEnumerable<H>,H> mapper,
    // Should this be added as immediate child?
    Func<F,object,bool> isImmediateChild) {

    var remainder = flat.Where(f => !isImmediateChild(f, parentKey))
        .ToList();

    return flat
        .Where(f => isImmediateChild(f, parentKey))
        .Select(f => {
            var children = MakeHierarchy(remainder, key(f), key, mapper, isImmediateChild);
            return mapper(f, children);
        });
}

class category1
{
    public int Id;
    public int ParentId;
    public string Name;

    public category1(int id, string name, int parentId) {
        Id = id;
        Name = name;
        ParentId = parentId;
    }
};

class category2
{
    public int Id;
    public int ParentId;
    public string Name;

    public IEnumerable<category2> Subcategories;
};

List<category1> categories = new List<category1>() {
    new category1(1, "Sport", 0),
    new category1(2, "Balls", 1),
    new category1(3, "Shoes", 1),
    new category1(4, "Electronics", 0),
    new category1(5, "Cameras", 4),
    new category1(6, "Lenses", 5),  
    new category1(7, "Tripod", 5), 
    new category1(8, "Computers", 4),
    new category1(9, "Laptops", 8),
    new category1(10, "Empty", 0),
    new category1(-1, "Broken", 999),
};

object KeyForCategory (category1 c1) {
    return c1.Id;
}

category2 MapCategories (category1 c1, IEnumerable<category2> subs) {
    return new category2 {
        Id = c1.Id,
        Name = c1.Name,
        ParentId = c1.ParentId,
        Subcategories = subs,
    };
}

bool IsImmediateChild (category1 c1, object id) {
    return c1.ParentId.Equals(id);
}

void Main()
{
    var h = MakeHierarchy<category1,category2>(categories, 0,
        // These make it "Generic". You can use lambdas or whatever;
        // here I am using method groups.
        KeyForCategory, MapCategories, IsImmediateChild);
    h.Dump();
}
1
user2864740

À l’aide de Ilya Ivanov et Damian Drygiel solutions, j’ai écrit du code qui crée un arbre avec n’importe quelle collection et n’importe quel niveau d’enfants, même si vous ne savez pas exactement quels nœuds seront des racines .

Entrée de nœud d'arbre

public sealed class TreeNode<T, TKey>
{
    public T Item { get; set; }
    public TKey ParentId { get; set; }

    public IEnumerable<TreeNode<T, TKey>> Children { get; set; }
}

Méthodes d'extension

public static class EnumerableExtensions
{
    public static IEnumerable<TreeNode<T, TKey>> ToTree<T, TKey>(
        this IList<T> collection,
        Func<T, TKey> itemIdSelector,
        Func<T, TKey> parentIdSelector)
    {
        var rootNodes = new List<TreeNode<T, TKey>>();
        var collectionHash = collection.ToLookup(parentIdSelector);

        //find root nodes
        var parentIds = collection.Select(parentIdSelector);
        var itemIds = collection.Select(itemIdSelector);
        var rootIds = parentIds.Except(itemIds);

        foreach (var rootId in rootIds)
        {
            rootNodes.AddRange(
                GetTreeNodes(
                    itemIdSelector,
                    collectionHash,
                    rootId)
                );
        }

        return rootNodes;
    }

    private static IEnumerable<TreeNode<T, TKey>> GetTreeNodes<T, TKey>(
        Func<T, TKey> itemIdSelector,
        ILookup<TKey, T> collectionHash,
        TKey parentId)
    {
        return collectionHash[parentId].Select(collectionItem => new TreeNode<T, TKey>
        {
            ParentId = parentId,
            Item = collectionItem,
            Children = GetTreeNodes(
                itemIdSelector,
                collectionHash,
                itemIdSelector(collectionItem))
        });
    }
}

Exemple:

 var collection = new List<TestTreeItem>
        {
            new TestTreeItem {Id = 1, Name = "1", ParentId = 14},
            new TestTreeItem {Id = 2, Name = "2", ParentId = 0},
            new TestTreeItem {Id = 3, Name = "3", ParentId = 1},
            new TestTreeItem {Id = 4, Name = "4", ParentId = 1},
            new TestTreeItem {Id = 5, Name = "5", ParentId = 2},
            new TestTreeItem {Id = 6, Name = "6", ParentId = 2},
            new TestTreeItem {Id = 7, Name = "7", ParentId = 3},
            new TestTreeItem {Id = 8, Name = "8", ParentId = 3},
            new TestTreeItem {Id = 9, Name = "9", ParentId = 5},
            new TestTreeItem {Id = 10, Name = "10", ParentId = 7}
        };

        var tree = collection.ToTree(item => item.Id, item => item.ParentId);

J'espère que ça aide quelqu'un. Prendre plaisir