Рекурсивное функциональное объяснение в двоичном дереве

#c #c #binary-tree

#c #c #двоичное дерево

Вопрос:

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

 int countNodes( TreeNode *root )    
{   
       // Count the nodes in the binary tree to which  
       // root points, and return the answer.  
    if ( root == NULL )  
       return 0;  // The tree is empty.  It contains no nodes.  
    else
   {  
       int count = 1;   // Start by counting the root.  
       count  = countNodes(root->left);  // Add the number of nodes   
                                        //     in the left subtree.   
       count  = countNodes(root->right); // Add the number of nodes   
                                        //    in the right subtree.  
       return count;  // Return the total.  
    }  
 }   // end countNodes()
 

Теперь я сомневаюсь -> как бы это считалось, скажем, root-> left-> left of right? или root-> right-> left-> left??
Спасибо

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

1. Смотрите также Рекурсия дерева в «Структуре и интерпретации компьютерных программ»

Ответ №1:

С рекурсивными функциями вы должны думать рекурсивно! Вот как я бы подумал об этой функции:

  • Я начинаю писать сигнатуру функции, то есть
     int countNodes( TreeNode *root ) 
     
  • Итак, сначала рассмотрим случаи, которые не являются рекурсивными. Например, если данное дерево есть NULL , то узлов нет, поэтому я возвращаю 0.
  • Затем я замечаю, что количество узлов в моем дереве равно количеству узлов левого поддерева плюс количество узлов правого поддерева плюс 1 (корневой узел). Поэтому я в основном вызываю функцию для левого и правого узлов и добавляю значения, добавляя также 1.
    • Обратите внимание, что я предполагаю, что функция уже работает правильно!

Почему я это сделал? Просто, функция должна работать с любым двоичным деревом, верно? Ну, левое поддерево корневого узла на самом деле является двоичным деревом! Правое поддерево также является двоичным деревом. Таким образом, я могу с уверенностью предположить, что с помощью тех же countNodes функций я могу подсчитать узлы этих деревьев. Как только они у меня есть, я просто добавляю left right 1 и получаю свой результат.

Как на самом деле работает рекурсивная функция? Вы могли бы использовать ручку и бумагу, чтобы следовать алгоритму, но вкратце это что-то вроде этого:

Допустим, вы вызываете функцию с этим деревом:

   a
 / 
b   c
   / 
  d   e
 

Вы видите, что корень не равен нулю, поэтому вы вызываете функцию для левого поддерева:

 b
 

и позже правильное поддерево

    c
  / 
 d   e
 

Однако перед вызовом правого поддерева необходимо оценить левое поддерево.

Итак, вы находитесь в вызове функции с вводом:

 b
 

Вы видите, что корень не равен нулю, поэтому вы вызываете функцию для левого поддерева:

 NULL
 

которое возвращает 0 и правильное поддерево:

 NULL
 

который также возвращает 0. Вы вычисляете количество узлов дерева, и это 0 0 1 = 1.

Теперь вы получили 1 для левого поддерева исходного дерева, которое было

 b
 

и функция вызывается для

    c
  / 
 d   e
 

Здесь вы снова вызываете функцию для левого поддерева

 d
 

что аналогично случаю b возвращает 1, а затем правильное поддерево

 e
 

который также возвращает 1, и вы оцениваете количество узлов в дереве как 1 1 1 = 3.

Теперь вы возвращаете первый вызов функции и оцениваете количество узлов в дереве как 1 3 1 = 5.

Итак, как вы можете видеть, для каждого левого и правого, вы снова вызываете функцию, и если у них были левые или правые дочерние элементы, функция вызывается снова и снова, и каждый раз она углубляется в дерево. Поэтому root->left->left or root->right->left->left вычисляется не напрямую, а после последующих вызовов.

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

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

2. @для всех, затем в функции ниже void printTree(struct node* node) { if (node == NULL) возвращает; printTree(узел-> слева); printf(«%d», узел-> данные); printTree(узел-> справа); } в этой функции затем, когдаон столкнулся с NUll после b, он будет связан с node == NULL, следовательно, возвращает (выход из функции), поэтому приведенный ниже код, подобный printf, не будет выполняться, я прав?

3. @samprat, точно. Когда вы возвращаетесь из функции, вы просто возвращаетесь и не выполняете все, что следует после.

Ответ №2:

Это в основном то, что делает рекурсия, она добавляет 1 каждый раз, когда вызывается countNodes, когда он переходит к дочернему узлу ( int count = 1; ) и завершается, когда он пытается перейти к следующему дочернему элементу конечного узла (поскольку у листа нет дочерних элементов). Каждый узел рекурсивно вызывает countNodes для каждого из своих левых и правых дочерних элементов, и количество медленно увеличивается и поднимается вверх.

Попробуйте посмотреть на это так, где 1 добавляется для каждого узла и 0 для несуществующего узла, где рекурсия останавливается:

           1
        /   
       1     1
      /    / 
     1   0 0   0
    / 
   0   0
 

Каждый из 1 суммируется, при этом родительский узел (вызывающая функция на каждом уровне рекурсии) добавляет 1 left_size right_size и возвращает этот результат. Поэтому значения, возвращаемые на каждом этапе, будут:

           4
        /   
       2     1
      /    / 
     1   0 0   0
    / 
   0   0
 

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

Ответ №3:

Допустим, вы вызываете countNodes(myTree); . Предполагая myTree , что это не null , countNodes в конечном итоге будет выполнено count = countNodes(root->left); , где root есть myTree . Он повторно вводит вашу countNodes функцию со всем корнем дерева в root->left (который есть myTree->left ). Затем логика повторяется; если нет root->left , то функция возвращает 0. В противном случае он в конечном итоге вызовет count = countNodes(root->left); снова, но на этот раз root на самом деле будет myTree->left . Таким образом, это будет учитываться myTree->left->left . Позже он делает то же самое с нужными узлами.

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

1. Но тогда, насколько я понимаю, код в конечном итоге достигнет условия, когда root == NULL, которое, согласно вашему примеру, говорит, что это происходит myTree-> left-> left, а затем вернет 0, поэтому, когда он получит шанс вернуть count?

2. если у меня есть значения inorder 12345, то root == 4, поэтому он будет идти влево, то есть root-> left == 2 , это не null, поэтому он перейдет в root-> left-> left , w, который равен 1, и после этого нет листа, поэтому функция countnodes( root-> left-> left-> left) станет нулевым и вернет 0?

3. @samprat Важно понимать, что рекурсия выполняется одновременно в нескольких экземплярах countNodes , каждый со своим count значением. Когда root == NULL он вернет 0 вызывающей функции. Если вы не находитесь на вершине дерева, это будет еще один фрейм countNodes выполнения для родительского узла (под) дерева, представленного root , где 0 будет добавлено к текущему значению count в этом фрейме. Когда этот фрейм возвращается, возвращаемое значение будет добавлено к другому count , соответствующему узлу на два уровня выше от корня и т.д.

Ответ №4:

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

Ответ №5:

Он будет начинаться с root-> left-> (subnode-> left) и т. Д., Пока эта ветвь не вернет 0, например, если это не фактический узел (лист в дереве);

Затем самый глубокий узел проверит наличие root-> right и повторит ту же процедуру. Попробуйте визуализировать это с помощью небольшого дерева :

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

Итак, в этом случае ваша функция будет идти A-> D-> B, тогда все правильные узлы вернут 0, и вы получите последнее 1 от вашего узла C.

Ответ №6:

Реализация алгоритма, которую вы пишете, является исчерпывающей. Он посещает все дерево.

Если дерево пустое, счетчик равен нулю. Если нет, мы получаем левый узел, назовем его L и добавим 1 к нашему счету.

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

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

Теперь … это действительно работает.

Поддерево дерева — это дерево, также для пустых или одиночных узлов. Вы должны посмотреть на определение дерева.

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

Ответ №7:

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

Базовый случай — это то, как ваш рекурсивный алгоритм знает, что нужно остановиться. В этом случае это if (root == NULL) — этот узел не представляет дерево. Эта строка выполняется на каждом отдельном узле вашего двоичного дерева, даже если она вызывает каждый из root них одновременно. Это значение равно false для всех узлов дерева, но когда вы начнете вызывать рекурсивную процедуру для дочерних элементов конечных узлов — а это все NULL , — тогда оно вернется 0 как количество узлов.

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

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

Ответ №8:

Подумайте, что программа идет первой в самых глубоких ветвях. Затем он возвращается назад, возвращая номер счетчика своему предыдущему члену.

     A
   / 
  B   C
 /    
D   E   F
 

Итак, сначала программа выполняется до

 count  = countNodes(root->left);
 

Он приостанавливает то, что было сделано до сих пор (по-видимому, ничего особенного), и переходит в B. То же самое происходит там, и оно переходит в D. В D он делает то же самое. Однако здесь у нас особый случай. Программа видит в начале строки

 if ( root == NULL )
 

что несуществующий левый дочерний элемент D действительно равен NULL . Поэтому вы получаете обратно 0.
Затем мы возвращаемся к тому, где мы остановились в прошлый раз, и продолжаем так. В прошлый раз мы были на B, поэтому мы продолжаем проходить мимо строки

 count  = countNodes(root->left);
 

и запустите следующую строку

 count  = countNodes(root->right);
 

Это продолжается до тех пор, пока вы не вернетесь к A. Но в точке A мы снова остановились сразу после поиска левого конца A. Поэтому мы продолжаем с правого выхода. Как только мы закончим проходить всю эту ветку, мы вернемся к A.

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

Ответ №9:

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

Ответ №10:

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

Ответ №11:

http://www.jargon.net/jargonfile/r/recursion.html

ОБНОВЛЕНИЕ: дело в том, что и структура данных, и программа являются рекурсивными.

  • для структуры данных это означает: поддерево также является деревом.
  • для кода это означает: подсчет дерева: = подсчет поддеревьев (и добавление одного)