Обход вложенной иерархии любой глубины снизу вверх

#c# #.net #json #recursion

#c# #.net #json #рекурсия

Вопрос:

Возьмите вложенный рекурсивный фрагмент JSON, подобный этому, который может продолжаться на любую глубину:

 {
   "Id": null,
   "Foos": [
      {
         "FooId": 1,
         "FooName": "ABC",
         "Foos": [
            {
               "FooId": 2,
               "FooName": "DEF",
               "Foos": null
            },
            {
               "FooId": 3,
               "FooName": "GHI",
               "Foos": [
                  {
                     "FooId": 4,
                     "FooName": "JKL",
                     "Foos": null
                  },
                  {
                     "FooId": 5,
                     "FooName": "MNO",
                     "Foos": [
                        {
                           "FooId": 6,
                           "FooName": "PQR",
                           "Foos": null
                        },
                        {
                           "FooId": 7,
                           "FooName": "STU",
                           "Foos": null
                        }
                     ]
                  }
               ]
            }
         ]
      }
   ]
}
  

Использование JSON.NET Я могу отобразить это в структуру, подобную этой:

 public class Root {
    public string Id { get; set; }
    public List<Foo> Foos { get; set; }
}

public class Foo {
    public int FooId { get; set; }
    public string FooName { get; set; }
    public List<Foo> Foos { get; set; }
}
  

пока все хорошо … но теперь мне нужно работать снизу вверх по иерархии (начиная с дочерних элементов с fooId = 5), а затем возвращаться к корню. Как мне эффективно справиться с этим?

Комментарии:

1. Было бы плохо, если бы вы повторили дерево один раз после десериализации?

2. Чтобы уточнить, вы хотите выполнить итерацию объектов в порядке 6, 7, 5, 4, 3, 2, 1?

3. Что, если у нас есть дерево A [ B [ C, D ] , E [ F, G ] ] ? Вы хотите C, D, B, F, G, E, A? Или вы хотите C, D, F, G, B, E, A? Первый — это обход после заказа, второй — обход уровня, и они очень разные. Из вашего вопроса неясно, что вам нужно.

4. Пожалуйста, прочитайте en.wikipedia.org/wiki/Tree_traversal и более четко скажите, какой обход вы хотите.

5. Или вы действительно просто хотите перейти к родителям FooId": 5 узла, аналогично тому, как XElement.AncestorsAndSelf или JToken.AncestorsAndSelf() перечислить родителей данного узла? Например, предки будут 5, 3, 1, null

Ответ №1:

Из вашего вопроса неясно, хотите ли вы обход postorder (сначала в глубину) или обратный обход уровня (сначала в ширину, обратный). Предполагая, что вы хотите postorder, алгоритм прост:

 public static IEnumerable<T> Postorder<T>(
  this IEnumerable<T> nodes,
  Func<T, IEnumerable<T>> children)
{
  foreach(T node in nodes)
  {
    foreach(T descendant in children(node).Postorder(children))
      yield return descendant;
    yield return node;
  }
}
  

Каждый узел выдается только после всех его потомков, так что это обход по порядку.

Это достаточно эффективно, если дерево мелкое, но вы говорите, что хотите решить проблему для дерева «любой глубины». Этот подход будет эффективно работать только для деревьев глубиной до нескольких десятков уровней, потому что это O (nd), где n — общее количество узлов, а d — средняя глубина; средняя глубина зависит от коэффициента ветвления и поэтому может быть как 1, так и n, что делает этот алгоритм потенциально квадратичным.

Более того, поскольку он использует пространство стека O (dmax), где dmax — максимальная глубина, мы можем взорвать стек вызовов.

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

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

Но вы сказали, что вам нужны деревья любой глубины. Что, если в дереве миллиарды или триллионы узлов глубиной в миллиарды или триллионы? В этом случае вам нужно будет использовать решение с внешней памятью, и я бы рекомендовал создать пользовательскую систему хранения, предназначенную для этой проблемы; проведите некоторое исследование масштабируемых графических баз данных, которые могут решить такого рода проблемы.

В любом случае, теперь, когда у вас есть общее решение, ваше конкретное решение простое:

 var ids = root.Foos
              .Postorder(f => f.Foos)
              .Select(f => f.FooId)
              .ToList();
  

или что угодно.

Комментарии:

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