выход при рекурсии bst

#c# #binary-search-tree

#c# #двоичное дерево поиска

Вопрос:

Я создал шаблонный класс bst и внедрил GetEnumerator, но я действительно недоволен тем, что сделал.

итак, прежде всего, я почувствовал, что мне нужна вспомогательная функция «внутренний визит»

    private IEnumerable<Node<T>> innerVisit(Node<T> root)
    {
        if(root== null)
         yield break;

        if (root.Left != null)
        {
            var l = innerVisit(root.Left);
            foreach (var item in l)
                yield return item;
        }


        yield return root;

        if (root.Right != null)
        {
            var r = innerVisit(root.Right);
            foreach (var item in r)
                yield return item;
        }


    }
  

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

также для завершения реализации я написал

      public IEnumerator<Node<T>> GetEnumerator()
    {

       var res = innerVisit(_root);

        foreach (var item in res)
            yield return item;

    }
  

но это слишком неприятно и больше похоже на взлом, чтобы убедиться, что это будет работать в цикле foreach и т.д.

Ответ №1:

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

Мы можем заменить foreach и возвращаемый yield на Enumerable .Метод Concat, с его помощью мы можем объединить левый внутренний элемент IEnumerable, IEnumerable, который мы создадим для самого узла (массив с 1 элементом текущего узла) и правый внутренний элемент IEnumerable:

 private IEnumerable<Node<T>> InnerVisit(Node<T> node)
{
    if(node == null)
    {         
        return Enumerable.Empty<Node<T>>;
    }
    return InnerVisit(node.Left).Concat(new[] { node }).Concat(InnerVisit(node.Right));
}
  

Обратите внимание, что нет необходимости проверять, имеет ли значение Left или Right значение null перед вызовом рекурсивного метода, потому что он проверит это позже во внутреннем вызове, если оно равно null, мы вернем Enumerable.Empty<Node<T>> вместо использования yield break, как вы делали.

Мы также можем упростить GetEnumerator, вызвав GetEnumerator непосредственно по результату корневого запроса, что-то вроде этого:

 public IEnumerator<Node<T>> GetEnumerator()
{
     return InnerVisit(_root).GetEnumerator();
}
  

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

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

2. «Мне действительно не нравится повторяющийся код, но я не смог найти для него правильного решения «, этот код не повторяет все циклы возврата yield и foreach. Он не просил другого алгоритма, он просто хотел не повторять один и тот же foreach и не выдавать код повсюду.

3. Каждый вызов InnerVisit эффективно вызывает yield каждый элемент if it — он делает это столько же раз, сколько и код операционной системы. И вы вызываете InnerVisit слева и InnerVisit справа — это повторяющийся код. По сути, это тот же код, что и в OP. Вы должны попытаться объяснить, чем отличается ваше решение.

4. Что касается вашего первого вопроса, мы рекурсивно объединим все левые дочерние IEnumerables, сам текущий узел в качестве Ienumerable, и правые дочерние IEnumerables в наш результат, если узел равен null, нет данных для возврата, это условие остановки нашей рекурсии, и мы вернем «пустой» IEnumerable. Что касается вашего второго вопроса, у каждого IEnumerable есть метод GetEnumerator, поскольку возвращаемый тип InnerVisit является IEnumerable, нам не нужно изобретать велосипед, и мы можем просто использовать его метод GetEnumerator для наших целей, хотя возможно реализовать ваш собственный метод GetEnumerator.

5. @LiorA всегда пожалуйста. Я действительно рекомендую вам прочитать статью Джона Скита Итераторы, блоки итераторов и конвейеры данных для получения дополнительной информации по теме и подробностей реализации блока итераторов: автоматически генерируемые конечные автоматы , если вы действительно хотите глубоко погрузиться в эту тему.