Оборачиваю голову вокруг N родительских> детских ассоциаций - PullRequest
1 голос
/ 30 января 2010

Я постараюсь объяснить это как можно лучше. У меня довольно большие трудности, чтобы понять эту логику.

По сути, у меня есть коллекция, включающая тысячи объектов, каждый из которых состоит из свойства Parent и Child.

Итак, примерно, это:


public class MyObject{
     public string Parent { get; set; }
     public string Child { get; set; }
}

Я пытаюсь понять, как встроить это в простой элемент управления TreeView. Мне нужно строить отношения, но я не могу понять, как, потому что они могут быть смешаны. Я, вероятно, могу объяснить это лучше с тем, как должно выглядеть дерево:

Так что, если у меня есть следующие предметы в моей коллекции:


0. Parent: "A", Child: "B"
1. Parent: "B", Child: "C"
2. Parent: "B", Child: "D"

Я бы хотел, чтобы мое дерево выглядело так:


-A
--B
---C
-A
--B
---D
-B
--C
-B
--D

Как я могу сделать это в C #? Мне нужно, чтобы он поддерживал до N отношений, поскольку у нас есть несколько ветвей, которые я ожидаю достичь глубиной около 50 узлов.

Ответы [ 3 ]

5 голосов
/ 30 января 2010

UPDATE

Эта проблема на самом деле оказалась значительно более сложной, чем я первоначально предполагал, учитывая требование повторения всего дерева для каждого пути. Я просто удалил старый код, потому что больше не хочу путать.

Я хочу сохранить это в записи, что использование рекурсивной структуры данных делает это проще:

public class MyRecursiveObject
{
    public MyRecursiveObject Parent { get; set; }
    public string Name { get; set; }
    public List<MyRecursiveObject> Children { get; set; }
}

Вы очень ясно увидите , почему это проще после прочтения кода ниже:

private void PopulateTree(IEnumerable<MyObject> items)
{
    var groupedItems =
        from i in items
        group i by i.Parent into g
        select new { Name = g.Key, Children = g.Select(c => c.Child) };
    var lookup = groupedItems.ToDictionary(i => i.Name, i => i.Children);
    foreach (string parent in lookup.Keys)
    {
        if (lookup.ContainsKey(parent))
            AddToTree(lookup, Enumerable.Empty<string>(), parent);
    }
}

private void AddToTree(Dictionary<string, IEnumerable<string>> lookup,
    IEnumerable<string> path, string name)
{
    IEnumerable<string> children;
    if (lookup.TryGetValue(name, out children))
    {
        IEnumerable<string> newPath = path.Concat(new string[] { name });
        foreach (string child in children)
            AddToTree(lookup, newPath, child);
    }
    else
    {
        TreeNode parentNode = null;
        foreach (string item in path)
            parentNode = AddTreeNode(parentNode, item);
        AddTreeNode(parentNode, name);
    }
}

private TreeNode AddTreeNode(TreeNode parent, string name)
{
    TreeNode node = new TreeNode(name);
    if (parent != null)
        parent.Nodes.Add(node);
    else
        treeView1.Nodes.Add(node);
    return node;
}

Прежде всего, я понял, что словарь будет содержать ключи для промежуточных узлов, а также только корневые узлы, поэтому нам не нужны два рекурсивных вызова в рекурсивном методе AddToTree, чтобы получить «B» узлы как корни; первоначальная прогулка в методе PopulateTree уже делает это.

Что нам нужно сделать , чтобы защититься от этого, это добавить листовые узлы в начальной прогулке; используя рассматриваемую структуру данных, их можно обнаружить, проверяя, есть ли ключ в родительском словаре. С рекурсивной структурой данных это было бы намного проще: просто проверьте Parent == null. Но рекурсивная структура - это не то, что у нас есть, поэтому мы должны использовать приведенный выше код.

AddTreeNode - это в основном служебный метод, поэтому нам не нужно повторять эту логику проверки на нуль позже.

Настоящее уродство - во втором, рекурсивном AddToTree методе. Поскольку мы пытаемся создать уникальную копию каждого отдельного поддерева, мы не можем просто добавить узел дерева и затем выполнить рекурсию с этим узлом в качестве родительского. У «A» здесь только один ребенок, «B», но у «B» есть два ребенка, «C» и «D». Должно быть две копии «A», но нет никакого способа узнать об этом, когда «A» первоначально передается методу AddToTree.

Итак, что мы на самом деле должны сделать, это не создавать каких-либо узлов до финальной стадии и сохранять временный путь, для которого я выбрал IEnumerable<string>, потому что он неизменен и, следовательно, невозможно испортить , Когда нужно добавить еще детей, этот метод просто добавляет путь и рекурсоры; когда дочерних элементов больше нет, он проходит по всему сохраненному пути и добавляет узел для каждого.

Это крайне неэффективно, потому что теперь мы создаем новый перечислимый при каждом вызове AddToTree. Для большого количества узлов, это может занять много памяти. Это работает, но это было бы намного эффективнее с рекурсивной структурой данных. Используя пример структуры в верхней части, вам вообще не придется сохранять путь или создавать словарь; когда не осталось детей, просто пройдите по пути в цикле while, используя ссылку Parent.

Во всяком случае, я думаю, что это академично, потому что это не рекурсивный объект, но я подумал, что в любом случае стоило указать на то, что следует иметь в виду для будущих проектов. Код выше будет давать именно те результаты, которые вы хотите, я пошел дальше и протестировал его на реальном TreeView.


UPDATE 2 - Получается, что приведенная выше версия довольно жестока по отношению к памяти / стеку, скорее всего, в результате создания всех этих IEnumerable<string> экземпляров. Хотя это не очень хороший дизайн, мы можем устранить эту конкретную проблему, изменив переменную List<string>. Следующий фрагмент демонстрирует различия:

private void PopulateTree(IEnumerable<MyObject> items)
{
    // Snip lookup-generation code - same as before ...

    List<string> path = new List<string>();
    foreach (string parent in lookup.Keys)
    {
        if (lookup.ContainsKey(parent))
            AddToTree(lookup, path, parent);
    }
}

private void AddToTree(Dictionary<string, IEnumerable<string>> lookup,
    IEnumerable<string> path, string name)
{
    IEnumerable<string> children;
    if (lookup.TryGetValue(name, out children))
    {
        path.Add(name);
        foreach (string child in children)
            AddToTree(lookup, newPath, child);
        path.Remove(name);
    }
    // Snip "else" block - again, this part is the same as before ...
}
0 голосов
/ 30 января 2010

Если я правильно понимаю, то, что вы пытаетесь сделать, это взять одно дерево и превратить его в другое. Преобразование, по сути, берет каждый неконечный узел во входном дереве и создает узел для него (и его потомков) в выходном дереве.

Во-первых, вы будете счастливее, если разработаете структуру данных для своих узлов, которая будет действительно рекурсивной:

public class Node
{
   public Node Parent { get; private set; }
   public IEnumerable<Node> Children { get; private set; }
   public bool HasChildren { get { return Children.Count() > 0; } }

   public Node()
   {
      Children = new List<Node>();
   }
}

Ваш класс MyObject представляет отношения родитель / потомок между строковыми значениями. До тех пор, пока вы можете реализовать метод FindChildren(), который возвращает дочерние значения для данного родительского значения, использовать этот класс для рационализации родительских / дочерних отношений просто:

public string Value { get; set; }
public static Node Create(string parentKey)
{
   Node n = new Node();
   n.Value = parentKey;
   foreach (string childKey in FindChildren(parentKey))
   {
      Node child = n.Children.Add(Node.Create(childKey));
      child.Parent = n;
   } 
   return n;
}

Это просто реализовать свойство, которое возвращает потомков узла:

public IEnumerable<Node> Descendants
{
   get
   {
      foreach (Node child in Children)
      {
         yield return child;
         foreach (Node descendant in child.Descendants)
         {
            yield return descendant;
         }
      }
   }
}

Чтобы добавить Node в TreeView, вам нужно два метода. (Обратите внимание, что это не методы класса Node!) Я сделал их перегрузками, но можно привести аргумент для присвоения им разных имен:

public void AddNode(Node n, TreeView tv)
{
   TreeNode tn = tv.Nodes.Add(n.Value);
   tn.Tag = n;
   foreach (Node child in n.Children)
   {
      AddNode(child, tn);
   }
}

public void AddNode(Node n, TreeNode parent)
{
   TreeNode tn = parent.Nodes.Add(n.Value);
   parent.Tag = n;
   foreach (Node child in n.Children)
   {
      AddNode(child, tn);
   }
}

Я устанавливаю Tag на каждом TreeNode, чтобы вы могли найти путь назад к исходному Node.

Итак, чтобы инициализировать ваш TreeView из списка родительских ключей верхнего уровня, вам нужен такой метод:

public void PopulateTreeView(IEnumerable<string> parents, TreeView t)
{
   foreach (string parentKey in parents)
   {
      Node n = Node.Create(parentKey);
      AddNode(n, t);
      foreach (Node descendant in n.Descendants)
      {
         if (n.HasChildren)
         {
            AddNode(descendant, t);
         }
      }
   }
}

Edit:

Я не совсем понял, как работал ваш класс MyObject; Я думаю, что я делаю сейчас, и я отредактировал это соответственно.

0 голосов
/ 30 января 2010

как Рубенс, я пробовал оба, но немного лучше, я думаю Универсальная коллекция деревьев

эта коллекция деревьев имеет встроенную функциональность для перемещения по дереву, прочитайте всю статью

образец со ссылкой выше

Static Class Module1
{

    public static void Main()
    {
        Common.ITree<myObj> myTree = default(Common.ITree<myObj>);
        myObj a = new myObj("a");
        myObj b = new myObj("b");
        myObj c = new myObj("c");
        myObj d = new myObj("d");

        myTree = Common.NodeTree<myObj>.NewTree;
        myTree.InsertChild(a).InsertChild(b).InsertChild(c).Parent.Parent.InsertNext(a).InsertChild(b).InsertChild(d).Parent.Parent.InsertNext(b).InsertChild(c).Parent.InsertNext(b).InsertChild(d);

        Console.WriteLine(myTree.ToStringRecursive);

        Console.ReadKey();
    }
}

Class myObj
{
    public string text;

    public myObj(string value)
    {
        text = value;
    }

    public override string ToString()
    {
        return text;
    }
}

будет именно тем, что вы только что показали

-A

--- C
-

--- D
-B
* 1020-C * -B
--D

...