Как эффективно строить дерево из плоской структуры?
У меня есть куча объектов в плоской структуре. Эти объекты имеют свойство ID
и ParentID
, чтобы они могли быть расположены в деревьях. Они не имеют особого порядка.
Каждое свойство ParentID
не обязательно совпадает с ID
в структуре. Поэтому их могут быть несколько деревьев, выходящих из этих объектов.
Как бы вы обрабатывали эти объекты для создания результирующих деревьев?
Я не так далек от решения, но я уверен, что он далеко не оптимален...
Мне нужно создать эти деревья, чтобы затем вставить данные в базу данных в правильном порядке.
Нет круглых ссылок. A Node является RootNode, когда ParentID == null или когда ParentID не может быть найден в других объектах
Ответы
Ответ 1
Сохранять идентификаторы объектов в таблице хеш-таблицы для конкретного объекта. Перечислите все объекты и найдите их родительский объект, если он существует, и соответствующим образом обновите его родительский указатель.
class MyObject
{ // The actual object
public int ParentID { get; set; }
public int ID { get; set; }
}
class Node
{
public List<Node> Children = new List<Node>();
public Node Parent { get; set; }
public MyObject AssociatedObject { get; set; }
}
IEnumerable<Node> BuildTreeAndGetRoots(List<MyObject> actualObjects)
{
Dictionary<int, Node> lookup = new Dictionary<int, Node>();
actualObjects.ForEach(x => lookup.Add(x.ID, new Node { AssociatedObject = x }));
foreach (var item in lookup.Values) {
Node proposedParent;
if (lookup.TryGetValue(item.AssociatedObject.ParentID, out proposedParent)) {
item.Parent = proposedParent;
proposedParent.Children.Add(item);
}
}
return lookup.Values.Where(x => x.Parent == null);
}
Ответ 2
Основываясь на ответе Мехрдада Афшари и комментария Эндрю Хэнлона на ускорение, вот мой прием.
Важное отличие от исходной задачи: у root node есть ID == parentID.
class MyObject
{ // The actual object
public int ParentID { get; set; }
public int ID { get; set; }
}
class Node
{
public List<Node> Children = new List<Node>();
public Node Parent { get; set; }
public MyObject Source { get; set; }
}
List<Node> BuildTreeAndGetRoots(List<MyObject> actualObjects)
{
var lookup = new Dictionary<int, Node>();
var rootNodes = new List<Node>();
foreach (var item in actualObjects)
{
// add us to lookup
Node ourNode;
if (lookup.TryGetValue(item.ID, out ourNode))
{ // was already found as a parent - register the actual object
ourNode.Source = item;
}
else
{
ourNode = new Node() { Source = item };
lookup.Add(item.ID, ourNode);
}
// hook into parent
if (item.ParentID == item.ID)
{ // is a root node
rootNodes.Add(ourNode);
}
else
{ // is a child row - so we have a parent
Node parentNode;
if (!lookup.TryGetValue(item.ParentID, out parentNode))
{ // unknown parent, construct preliminary parent
parentNode = new Node();
lookup.Add(item.ParentID, parentNode);
}
parentNode.Children.Add(ourNode);
ourNode.Parent = parentNode;
}
}
return rootNodes;
}
Ответ 3
Вот простой алгоритм JavaScript для синтаксического анализа плоской таблицы в родительской/дочерней древовидной структуре, которая выполняется в N раз:
var table = [
{parent_id: 0, id: 1, children: []},
{parent_id: 0, id: 2, children: []},
{parent_id: 0, id: 3, children: []},
{parent_id: 1, id: 4, children: []},
{parent_id: 1, id: 5, children: []},
{parent_id: 1, id: 6, children: []},
{parent_id: 2, id: 7, children: []},
{parent_id: 7, id: 8, children: []},
{parent_id: 8, id: 9, children: []},
{parent_id: 3, id: 10, children: []}
];
var root = {id:0, parent_id: null, children: []};
var node_list = { 0 : root};
for (var i = 0; i < table.length; i++) {
node_list[table[i].id] = table[i];
node_list[table[i].parent_id].children.push(node_list[table[i].id]);
}
console.log(root);
Ответ 4
Решение Python
def subtree(node, relationships):
return {
v: subtree(v, relationships)
for v in [x[0] for x in relationships if x[1] == node]
}
Например:
# (child, parent) pairs where -1 means no parent
flat_tree = [
(1, -1),
(4, 1),
(10, 4),
(11, 4),
(16, 11),
(17, 11),
(24, 17),
(25, 17),
(5, 1),
(8, 5),
(9, 5),
(7, 9),
(12, 9),
(22, 12),
(23, 12),
(2, 23),
(26, 23),
(27, 23),
(20, 9),
(21, 9)
]
subtree(-1, flat_tree)
Выдает:
{
"1": {
"4": {
"10": {},
"11": {
"16": {},
"17": {
"24": {},
"25": {}
}
}
},
"5": {
"8": {},
"9": {
"20": {},
"12": {
"22": {},
"23": {
"2": {},
"27": {},
"26": {}
}
},
"21": {},
"7": {}
}
}
}
}
Ответ 5
JS-версия, которая возвращает один корень или массив корней, каждый из которых будет иметь свойство массива "Дети", содержащее связанные дочерние элементы. Не зависит от упорядоченного ввода, прилично компактно и не использует рекурсию. Наслаждайтесь!
// creates a tree from a flat set of hierarchically related data
var MiracleGrow = function(treeData, key, parentKey)
{
var keys = [];
treeData.map(function(x){
x.Children = [];
keys.push(x[key]);
});
var roots = treeData.filter(function(x){return keys.indexOf(x[parentKey])==-1});
var nodes = [];
roots.map(function(x){nodes.push(x)});
while(nodes.length > 0)
{
var node = nodes.pop();
var children = treeData.filter(function(x){return x[parentKey] == node[key]});
children.map(function(x){
node.Children.push(x);
nodes.push(x)
});
}
if (roots.length==1) return roots[0];
return roots;
}
// demo/test data
var treeData = [
{id:9, name:'Led Zep', parent:null},
{id:10, name:'Jimmy', parent:9},
{id:11, name:'Robert', parent:9},
{id:12, name:'John', parent:9},
{id:8, name:'Elec Gtr Strings', parent:5},
{id:1, name:'Rush', parent:null},
{id:2, name:'Alex', parent:1},
{id:3, name:'Geddy', parent:1},
{id:4, name:'Neil', parent:1},
{id:5, name:'Gibson Les Paul', parent:2},
{id:6, name:'Pearl Kit', parent:4},
{id:7, name:'Rickenbacker', parent:3},
{id:100, name:'Santa', parent:99},
{id:101, name:'Elf', parent:100},
];
var root = MiracleGrow(treeData, "id", "parent")
console.log(root)
Ответ 6
Нашел отличную версию JavaScript здесь: http://oskarhane.com/create-a-nested-array-recursively-in-javascript/
Допустим, у вас есть такой массив:
const models = [
{id: 1, title: 'hello', parent: 0},
{id: 2, title: 'hello', parent: 0},
{id: 3, title: 'hello', parent: 1},
{id: 4, title: 'hello', parent: 3},
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4},
{id: 7, title: 'hello', parent: 3},
{id: 8, title: 'hello', parent: 2}
];
И вы хотите, чтобы объекты были вложены так:
const nestedStructure = [
{
id: 1, title: 'hello', parent: 0, children: [
{
id: 3, title: 'hello', parent: 1, children: [
{
id: 4, title: 'hello', parent: 3, children: [
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4}
]
},
{id: 7, title: 'hello', parent: 3}
]
}
]
},
{
id: 2, title: 'hello', parent: 0, children: [
{id: 8, title: 'hello', parent: 2}
]
}
];
Вот рекурсивная функция, которая делает это возможным.
function getNestedChildren(models, parentId) {
const nestedTreeStructure = [];
const length = models.length;
for (let i = 0; i < length; i++) { // for-loop for perf reasons, huge difference in ie11
const model = models[i];
if (model.parent == parentId) {
const children = getNestedChildren(models, model.id);
if (children.length > 0) {
model.children = children;
}
nestedTreeStructure.push(model);
}
}
return nestedTreeStructure;
}
Usuage:
const models = [
{id: 1, title: 'hello', parent: 0},
{id: 2, title: 'hello', parent: 0},
{id: 3, title: 'hello', parent: 1},
{id: 4, title: 'hello', parent: 3},
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4},
{id: 7, title: 'hello', parent: 3},
{id: 8, title: 'hello', parent: 2}
];
const nestedStructure = getNestedChildren(models, 0);
Ответ 7
Смутно, как кажется мне вопрос, я, вероятно, создаю карту из идентификатора для фактического объекта. В псевдо-java (я не проверял, работает ли он/компилируется), это может быть что-то вроде:
Map<ID, FlatObject> flatObjectMap = new HashMap<ID, FlatObject>();
for (FlatObject object: flatStructure) {
flatObjectMap.put(object.ID, object);
}
И для поиска каждого родителя:
private FlatObject getParent(FlatObject object) {
getRealObject(object.ParentID);
}
private FlatObject getRealObject(ID objectID) {
flatObjectMap.get(objectID);
}
Повторно используя getRealObject(ID)
и делая карту из объекта в коллекцию объектов (или их идентификаторы), вы также получаете карту parent- > children.
Ответ 8
Я написал общее решение в С# свободно на основе ответа @Mehrdad Afshari:
void Example(List<MyObject> actualObjects)
{
List<TreeNode<MyObject>> treeRoots = actualObjects.BuildTree(obj => obj.ID, obj => obj.ParentID, -1);
}
public class TreeNode<T>
{
public TreeNode(T value)
{
Value = value;
Children = new List<TreeNode<T>>();
}
public T Value { get; private set; }
public List<TreeNode<T>> Children { get; private set; }
}
public static class TreeExtensions
{
public static List<TreeNode<TValue>> BuildTree<TKey, TValue>(this IEnumerable<TValue> objects, Func<TValue, TKey> keySelector, Func<TValue, TKey> parentKeySelector, TKey defaultKey = default(TKey))
{
var roots = new List<TreeNode<TValue>>();
var allNodes = objects.Select(overrideValue => new TreeNode<TValue>(overrideValue)).ToArray();
var nodesByRowId = allNodes.ToDictionary(node => keySelector(node.Value));
foreach (var currentNode in allNodes)
{
TKey parentKey = parentKeySelector(currentNode.Value);
if (Equals(parentKey, defaultKey))
{
roots.Add(currentNode);
}
else
{
nodesByRowId[parentKey].Children.Add(currentNode);
}
}
return roots;
}
}
Ответ 9
Для всех, кто интересуется версией решения Eugene на С#, обратите внимание, что access_list получает доступ к карте, поэтому используйте словарь.
Имейте в виду, что это решение работает только в том случае, если таблица сортируется по parent_id.
var table = new[]
{
new Node { parent_id = 0, id = 1 },
new Node { parent_id = 0, id = 2 },
new Node { parent_id = 0, id = 3 },
new Node { parent_id = 1, id = 4 },
new Node { parent_id = 1, id = 5 },
new Node { parent_id = 1, id = 6 },
new Node { parent_id = 2, id = 7 },
new Node { parent_id = 7, id = 9 },
new Node { parent_id = 8, id = 9 },
new Node { parent_id = 3, id = 10 },
};
var root = new Node { id = 0 };
var node_list = new Dictionary<int, Node>{
{ 0, root }
};
foreach (var item in table)
{
node_list.Add(item.id, item);
node_list[item.parent_id].children.Add(node_list[item.id]);
}
Node определяется следующим образом.
class Node
{
public int id { get; set; }
public int parent_id { get; set; }
public List<Node> children = new List<Node>();
}
Ответ 10
Вот Java-решение ответа Мехрдада Афшари.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class Tree {
Iterator<Node> buildTreeAndGetRoots(List<MyObject> actualObjects) {
Map<Integer, Node> lookup = new HashMap<>();
actualObjects.forEach(x -> lookup.put(x.id, new Node(x)));
//foreach (var item in lookup.Values)
lookup.values().forEach(item ->
{
Node proposedParent;
if (lookup.containsKey(item.associatedObject.parentId)) {
proposedParent = lookup.get(item.associatedObject.parentId);
item.parent = proposedParent;
proposedParent.children.add(item);
}
}
);
//return lookup.values.Where(x =>x.Parent ==null);
return lookup.values().stream().filter(x -> x.parent == null).iterator();
}
}
class MyObject { // The actual object
public int parentId;
public int id;
}
class Node {
public List<Node> children = new ArrayList<Node>();
public Node parent;
public MyObject associatedObject;
public Node(MyObject associatedObject) {
this.associatedObject = associatedObject;
}
}
Ответ 11
Вы застряли, используя только те атрибуты? В противном случае было бы неплохо создать массив дочерних узлов, где один раз вы можете циклически перебирать все эти объекты для создания таких атрибутов. Оттуда выберите node с детьми, но без родителей и итеративно создайте свое дерево сверху вниз.
Ответ 12
Я могу сделать это в 4 строках кода и времени O (n log n), считая, что словарь - это что-то вроде TreeMap.
dict := Dictionary new.
ary do: [:each | dict at: each id put: each].
ary do: [:each | (dict at: each parent) addChild: each].
root := dict at: nil.
EDIT:
Хорошо, и теперь я читаю, что некоторые parentID являются поддельными, поэтому забудьте выше, и сделайте следующее:
dict := Dictionary new.
dict at: nil put: OrderedCollection new.
ary do: [:each | dict at: each id put: each].
ary do: [:each |
(dict at: each parent ifAbsent: [dict at: nil])
add: each].
roots := dict at: nil.
Ответ 13
Большинство ответов предполагают, что вы хотите сделать это за пределами базы данных. Если ваши деревья относительно статичны по своей природе, и вам просто нужно как-то сопоставить деревья в базе данных, вы можете рассмотреть возможность использования вложенных представлений множества на стороне базы данных. Просмотрите книги Джо Селко (или здесь
для обзора Celko).
Если все равно привязаны к Oracle dbs, проверьте их CONNECT BY для прямых подходов SQL.
При любом подходе вы можете полностью пропустить сопоставление деревьев, прежде чем загружать данные в базу данных. Просто подумал, что я предлагаю это в качестве альтернативы, это может быть совершенно неуместно для ваших конкретных потребностей. Вся часть "правильного порядка" первоначального вопроса несколько подразумевает, что вам нужно, чтобы заказ был "правильным" в db по какой-то причине? Это может подтолкнуть меня к работе с деревьями там.
Ответ 14
Это не совсем то же самое, что искал искатель, но мне трудно было обернуть голову вокруг двусмысленных ответов, приведенных здесь, и я до сих пор считаю, что этот ответ соответствует названию.
Мой ответ предназначен для сопоставления плоской структуры с деревом непосредственно на объекте, где все, что у вас есть, - это ParentID
для каждого объекта. ParentID
- null
или 0
, если он является корнем. Напротив меня, я предполагаю, что все действительные ParentID
указывают на что-то еще в списке:
var rootNodes = new List<DTIntranetMenuItem>();
var dictIntranetMenuItems = new Dictionary<long, DTIntranetMenuItem>();
//Convert the flat database items to the DTO's,
//that has a list of children instead of a ParentID.
foreach (var efIntranetMenuItem in flatIntranetMenuItems) //List<tblIntranetMenuItem>
{
//Automapper (nuget)
DTIntranetMenuItem intranetMenuItem =
Mapper.Map<DTIntranetMenuItem>(efIntranetMenuItem);
intranetMenuItem.Children = new List<DTIntranetMenuItem>();
dictIntranetMenuItems.Add(efIntranetMenuItem.ID, intranetMenuItem);
}
foreach (var efIntranetMenuItem in flatIntranetMenuItems)
{
//Getting the equivalent object of the converted ones
DTIntranetMenuItem intranetMenuItem = dictIntranetMenuItems[efIntranetMenuItem.ID];
if (efIntranetMenuItem.ParentID == null || efIntranetMenuItem.ParentID <= 0)
{
rootNodes.Add(intranetMenuItem);
}
else
{
var parent = dictIntranetMenuItems[efIntranetMenuItem.ParentID.Value];
parent.Children.Add(intranetMenuItem);
//intranetMenuItem.Parent = parent;
}
}
return rootNodes;
Ответ 15
вот реализация ruby:
Он будет каталогизирован по имени атрибута или результату вызова метода.
CatalogGenerator = ->(depth) do
if depth != 0
->(hash, key) do
hash[key] = Hash.new(&CatalogGenerator[depth - 1])
end
else
->(hash, key) do
hash[key] = []
end
end
end
def catalog(collection, root_name: :root, by:)
method_names = [*by]
log = Hash.new(&CatalogGenerator[method_names.length])
tree = collection.each_with_object(log) do |item, catalog|
path = method_names.map { |method_name| item.public_send(method_name)}.unshift(root_name.to_sym)
catalog.dig(*path) << item
end
tree.with_indifferent_access
end
students = [#<Student:0x007f891d0b4818 id: 33999, status: "on_hold", tenant_id: 95>,
#<Student:0x007f891d0b4570 id: 7635, status: "on_hold", tenant_id: 6>,
#<Student:0x007f891d0b42c8 id: 37220, status: "on_hold", tenant_id: 6>,
#<Student:0x007f891d0b4020 id: 3444, status: "ready_for_match", tenant_id: 15>,
#<Student:0x007f8931d5ab58 id: 25166, status: "in_partnership", tenant_id: 10>]
catalog students, by: [:tenant_id, :status]
# this would out put the following
{"root"=>
{95=>
{"on_hold"=>
[#<Student:0x007f891d0b4818
id: 33999,
status: "on_hold",
tenant_id: 95>]},
6=>
{"on_hold"=>
[#<Student:0x007f891d0b4570 id: 7635, status: "on_hold", tenant_id: 6>,
#<Student:0x007f891d0b42c8
id: 37220,
status: "on_hold",
tenant_id: 6>]},
15=>
{"ready_for_match"=>
[#<Student:0x007f891d0b4020
id: 3444,
status: "ready_for_match",
tenant_id: 15>]},
10=>
{"in_partnership"=>
[#<Student:0x007f8931d5ab58
id: 25166,
status: "in_partnership",
tenant_id: 10>]}}}
Ответ 16
Принятый ответ выглядит слишком сложным для меня, поэтому я добавляю его версии для Ruby и NodeJS
Предположим, что список плоских узлов имеет следующую структуру:
nodes = [
{ id: 7, parent_id: 1 },
...
] # ruby
nodes = [
{ id: 7, parentId: 1 },
...
] # nodeJS
Функции, которые превратят структуру плоского списка выше в дерево, выглядят следующим образом
для Ruby:
def to_tree(nodes)
nodes.each do |node|
parent = nodes.find { |another| another[:id] == node[:parent_id] }
next unless parent
node[:parent] = parent
parent[:children] ||= []
parent[:children] << node
end
nodes.select { |node| node[:parent].nil? }
end
для NodeJS:
const toTree = (nodes) => {
nodes.forEach((node) => {
const parent = nodes.find((another) => another.id == node.parentId)
if(parent == null) return;
node.parent = parent;
parent.children = parent.children || [];
parent.children = parent.children.concat(node);
});
return nodes.filter((node) => node.parent == null)
};
Ответ 17
Один из элегантных способов сделать это - представить элементы в списке в виде строки, содержащей список родителей, разделенных точками, и, наконец, значение:
server.port=90
server.hostname=localhost
client.serverport=90
client.database.port=1234
client.database.host=localhost
При сборке дерева вы получите что-то вроде:
server:
port: 90
hostname: localhost
client:
serverport=1234
database:
port: 1234
host: localhost
У меня есть библиотека конфигурации, которая реализует эту конфигурацию переопределения (дерево) из аргументов командной строки (список). Алгоритм добавления одного элемента в список в дереве находится здесь.
Ответ 18
Вот та же самая реализация, которую дал Мехрдад Афшари, но на этот раз в java:
public class Node {
private SomeObject someObject;
private List<Node> children;
private Node parent;
public Node(SomeObject so) {
super();
this.someObject = so;
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public SomeObject getSomeObject() {
return someObject;
}
public void setSomeObject(SomeObject so) {
this.someObject = so;
}
public List<Node> getChildren() {
return children;
}
public void setChildren(List<Node> children) {
this.children = children;
}
public void addChild(Node node) {
if (children == null) {
children = new ArrayList<Node>();
}
children.add(node);
}
}
for (SomeObject someObject: list) {
lookup.put(someObject.getId(), new Node(someObject));
}
Set<Long> keySet = lookup.keySet();
for (Long id : keySet) {
Node value = lookup.get(id);
long parentId = value.getSomeObject().getParentId();
Node parentNode = lookup.get(parentId);
if (parentNode != null) {
value.setParent(parentNode);
parentNode.addChild(value);
}
}