Построение объектов иерархии из плоского списка родителя/потомка

У меня есть список элементов в иерархии, и я пытаюсь разобрать этот список на реальную иерархию объектов. Я использую модифицированный обход дерева предварительного заказа для хранения/перебора через этот список, и поэтому у меня есть подмножество дерева, включая всех дочерних элементов, упорядоченных по их «левому» значению.

Например, для дерева:

  • Item A
    • Item A.1
    • Item A.2
      • Item A.2.2
  • Item B
    • Item B.1
  • Пункт С

Я получаю список:

  • Пункт A, Пункт A.1, Пункт A.2, Пункт A.2.2, Пункт B, Пункт B.1, Пункт C

(Это в порядке «левого» значения из измененной настройки дерева предварительного заказа).

Что я хочу сделать, так это разобрать это на объекты, которые содержат фактическую структуру дерева, например:

Class TreeObject {
    String Name;
    Guid ID; 
    Guid ParentID;
    List<TreeObject> Children;
}

Плоский список возвращается как список TreeObjects, и каждый TreeObject имеет свойства для ID, ParentID, Left и Right. То, что я ищу, это функция:

List<TreeObject> FlatToHeirarchy(List<TreeObject> list); 

который принимает плоский список и возвращает вложенный список.

Другими словами:

List<TreeObject> flatSet = LoadTreeObjectsFromDatabase(); 
// flatSet.count == 7; flatSet(0).Children == null
List<TreeObject> nestedSet = FlatToHeirarchy(flatSet);
// nestedSet.count == 3; nestedSet(0).Children.count == 2

Я не знаю, как это сделать - отслеживать родителей и иметь дело с большим скачком (например, пункт A.2.2 -> пункт B).


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

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


person gregmac    schedule 25.11.2008    source источник
comment
Означает ли это, что вы сохраняете parentId, а также левое и правое значения для каждого узла в базе данных?   -  person Chris Barry    schedule 06.09.2011


Ответы (6)


Вот функция, которую я написал. Я использую MPTT для хранения объектов, поэтому список находится в порядке «левого» значения, что в основном означает, что родитель всегда идет перед любым данным элементом в списке. Другими словами, элемент, на который ссылается item.ParentID, всегда уже добавлен (за исключением случаев узлов верхнего уровня или корневых узлов).

public class TreeObject
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public string Name { get; set; }
    public IList<TreeObject> Children { get; set; } = new List<TreeObject>();
}

public IEnumerable<TreeObject> FlatToHierarchy(List<TreeObject> list)
{
    // hashtable lookup that allows us to grab references to containers based on id
    var lookup = new Dictionary<int, TreeObject>();
    // actual nested collection to return
    var nested = new List<TreeObject>();

    foreach (TreeObject item in list)
    {
        if (lookup.ContainsKey(item.ParentId))
        {
            // add to the parent's child list 
            lookup[item.ParentId].Children.Add(item);
        }
        else
        {
            // no parent added yet (or this is the first time)
            nested.Add(item);
        }
        lookup.Add(item.Id, item);
    }

    return nested;
}

и простой тест (который работает в LinqPad):

void Main()
{
    var list = new List<TreeObject>() {
        new TreeObject() { Id = 1, ParentId = 0, Name = "A" },
        new TreeObject() { Id = 2, ParentId = 1, Name = "A.1" },
        new TreeObject() { Id = 3, ParentId = 1, Name = "A.2" },
        new TreeObject() { Id = 4, ParentId = 3, Name = "A.2.i" },
        new TreeObject() { Id = 5, ParentId = 3, Name = "A.2.ii" }
    };

    FlatToHierarchy(list).Dump();
}

Результаты:

введите здесь описание изображения

Поскольку я обновляю это 5 лет спустя, вот рекурсивная версия LINQ:

public IList<TreeObject> FlatToHierarchy(IEnumerable<TreeObject> list, int parentId = 0) {
    return (from i in list 
            where i.ParentId == parentId 
            select new TreeObject {
                Id = i.Id, 
                ParentId = i.ParentId,
                Name = i.Name,
                Children = FlatToHierarchy(list, i.Id)
            }).ToList();
}
person gregmac    schedule 14.01.2009
comment
Что делать с этой строкой: lookup(item.ParentID).Item(item.ParentID).Add(item); ? Это опечатка? Вы можете получить доступ к элементу только по индексу на этом уровне, не так ли? - person ScottE; 02.06.2010
comment
ScottE: Нет, поиск — это словарь, проиндексированный Guid. Это немного сбивает с толку, но это потому, что lookup(item.PranteID) содержит список PARENT для item.parentId, или, другими словами, прародитель этого элемента). поэтому .item(item.parentId) содержит список, в котором находится ЭТОТ элемент, и отдельный элемент добавляется как родственный элемент. - person gregmac; 03.11.2010
comment
Я пытаюсь реализовать это, но я получаю, что поиск является переменной, но используется как метод, чего-то не хватает, я предполагаю, что нет, и я просто делаю что-то неправильно? - person Chris Barry; 06.09.2011
comment
Это .нет? Он компилируется? Я добавил переработку вашего кода ниже, который сразу компилируется, я делаю что-то не так? - person Chris Barry; 06.09.2011
comment
Кто-нибудь может исправить этот пример? - person Daniel Little; 30.01.2013
comment
Прости; обновлен до рабочего примера. Исходный код еще тогда, когда я первоначально спрашивал, был на VB, и я думаю, что преобразование в C # сломало его - и я, очевидно, никогда не тестировал его, позор мне. Прости еще раз! - person gregmac; 07.02.2013
comment
Это просто потрясающе, Грег. - person pim; 24.03.2015
comment
Что, если у вас есть несколько деревьев внутри, и вы хотите вернуть все свои деревья, как бы вы это сделали? - person eugenekgn; 15.07.2015
comment
@eugenekgn Я не совсем уверен, о чем вы спрашиваете, но, тем не менее, вы должны задать его как новый вопрос (и включить пример кода, иллюстрирующий проблему). - person gregmac; 16.07.2015
comment
Спасибо за ваш ценный ответ LINQ. Оставь это ! - person TechnicalKalsa; 09.07.2019

Я предполагаю, что вы уже знаете родителя всех элементов.

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

Вот некоторый псевдокод:

foreach Item item in flatlist
   if item.Parent != null
      Add item to item.Parent.ChildrenList
      Remove item from flatlist
   end if
end for

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

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

person Coincoin    schedule 25.11.2008
comment
Это часть, которую я не получаю. Я предполагаю, что есть хороший способ иметь стек, который содержит всех родителей, и продолжать вставлять его, поскольку элемент находится глубже, чем его родитель, или выталкивать последний из них по мере продвижения. Чего я хочу избежать, так это много раз перебирать список, удаляя элементы. - person gregmac; 26.11.2008
comment
Как только вы узнаете родителя каждого узла, вы можете пройтись по списку один раз. Я обновил свой ответ некоторым псевдокодом. - person Coincoin; 26.11.2008
comment
Я знаю идентификатор родителя, но у меня нет ссылки на сам родительский объект (без поиска по списку, конечно). - person gregmac; 02.12.2008
comment
как выглядит ваш плоский список, это очень помогло бы понять ваш ответ ... Я не могу просто догадаться, что вы предполагаете, что это должно быть или что в нем содержится. - person PositiveGuy; 30.03.2013
comment
В некоторых языках, например в C#, это не будет работать, так как компилятор выдаст ошибку во время выполнения, потому что вы изменили перечисляемую им коллекцию. - person pim; 24.03.2015

Альтернативная версия, которая компилируется нормально, я не был уверен, есть ли проблема с приведенным выше кодом.

private List<Page> FlatToHierarchy(List<Page> list) {
      // hashtable lookup that allows us to grab references to the parent containers, based on id
        Dictionary<int, Page> lookup = new Dictionary<int, Page>();
      // actual nested collection to return
      List<Page> nested = new List<Page>();

      foreach(Page item in list) {
        if (lookup.ContainsKey(item.parentId)) {
          // add to the parent's child list 
          lookup[item.parentId].children.Add(item); //add item to parent's childs list
          lookup.Add(item.pageId, item); //add reference to page in lookup table
        } else {
          // no parent added yet (or this is the first time)
          nested.Add(item);     //add item directly to nested list                
          lookup.Add(item.pageId, item); //add reference to page in lookup table
        }
      }
    return nested;
    }
person Chris Barry    schedule 05.09.2011

вы должны использовать архитектуру хранилища базы данных, такую ​​​​как вложенный набор

[ https://en.wikipedia.org/wiki/Nested_set_model ]

person Vahid Alvandi    schedule 22.10.2019

Вот пример, надеюсь, это поможет

class Program
{
    static void Main(string[] args)
    {
        TreeObject a  = new TreeObject() { Name = "Item A" };
        a.Children.Add( new TreeObject() { Name = "Item A.1" });
        a.Children.Add( new TreeObject() { Name = "Item A.2" });

        TreeObject b = new TreeObject() { Name = "Item B" };
        b.Children.Add(new TreeObject() { Name = "Item B.1" });
        b.Children.Add(new TreeObject() { Name = "Item B.2" });

        TreeObject c = new TreeObject() { Name = "Item C" };

        List<TreeObject> nodes = new List<TreeObject>(new[] { a, b, c });

        string list = BuildList(nodes);
        Console.WriteLine(list); // Item A,Item A.1,Item A.2,Item B,Item B.1,Item B.2,Item C

        List<TreeObject> newlist = new List<TreeObject>();
        TreeObject temp = null;

        foreach (string s in list.Split(','))
        {
            if (temp == null || !s.Contains(temp.Name) || temp.Name.Length != s.Length)
            {
                temp = new TreeObject() { Name = s };
                newlist.Add(temp);
            }
            else
            {
                temp.Children.Add(new TreeObject() { Name = s });
            }                              
        }

        Console.WriteLine(BuildList(newlist)); // Item A,Item A.1,Item A.2,Item B,Item B.1,Item B.2,Item C
    }

    static string BuildList(List<TreeObject> nodes)
    {
        StringBuilder output = new StringBuilder();
        BuildList(output, nodes);
        return output.Remove(output.Length - 1, 1).ToString();
    }

    static void BuildList(StringBuilder output, List<TreeObject> nodes)
    {
        foreach (var node in nodes)
        {
            output.AppendFormat("{0},", node.Name);
            BuildList(output, node.Children);
        }
    }
}

public class TreeObject
{
    private List<TreeObject> _children = new List<TreeObject>();

    public string Name { get; set; }
    public Guid Id { get; set; }
    public List<TreeObject> Children { get { return _children; } }
}

}

person Rohan West    schedule 25.11.2008
comment
Это почти противоположно тому, что я хочу сделать. В итоге я хочу получить структуру в переменной «узлы», которую вы показываете в строке 15. Я начинаю со списка «TreeObject» со ВСЕМИ объектами непосредственно в нем, как с братьями и сестрами (например, отношения «родитель-потомок» отсутствуют). . - person gregmac; 26.11.2008

Исправление примера, приведенного gregmac

IList<TreeObject> FlatToHierarchy(IQueryable<lcc_classe> list, int? parentId)
    {
        var q = (from i in list
                where i.parent_id == parentId
                select new
                {
                    id = i.id,
                    parent_id = i.parent_id,
                    kks = i.kks,
                    nome = i.nome
                }).ToList();
        return q.Select(x => new TreeObject
            {
                children = FlatToHierarchy(list, x.id)
            }).ToList();
    }
person Paulo Vj    schedule 28.02.2014
comment
вы проверили это, мне нравится ваш подход... и я хотел бы сам попробовать на плоском столе. - person ; 21.10.2014