递归计算二叉树的高度
Previously I wrote about an algorithm for finding out the height of a binary tree using iteration. Though that method gets the job done (in 7 steps no less), the same can be accomplished in a much simpler way.
以前,我写过一篇关于使用迭代找出二叉树的高度的算法 。 尽管该方法可以完成工作(分不少于7个步骤),但可以用更简单的方法完成相同的工作。
In my opinion, one of the most powerful programming techniques is recursion. For readers new to programming — it is simply a function or method calling itself. To make the introduction simpler we have a method below that calls another method:
在我看来,递归是最强大的编程技术之一。 对于刚接触编程的读者来说,它只是一个调用自身的函数或方法。 为了使介绍更简单,我们在下面有一个调用另一个方法的方法:
def outer_method(name) (R1)
inner_method + name
end
def inner_method (R2)
"Hello "
end
print outer_method("Steve") -> #"Hello Steve"
In the above method outer_method
, which takes in a string as argument, calls inner_method
, which simply returns the string “Hello “
inside it. Recursion is similar in that, say in this case, outer_method
simply calls itself:
在上面的方法outer_method
,它以字符串作为参数,调用inner_method
,该方法只是在其中返回字符串“Hello “
。 递归相似之处在于,在这种情况下, outer_method
简单地调用自身:
def outer_method(name) (R3)
outer_method("hello ") + name
end (R3)
One caveat, though, with code R3
above — it will run until the computer complains that resources are not enough to keep processing the method. It’s like running an infinite loop except that infinite loops don’t necessarily raise exceptions. The reason for this is that code R3
doesn’t have a ‘terminal state’ or a point where it doesn’t ‘recurse’ anymore.
需要注意的是,上面的代码为R3
它会一直运行,直到计算机抱怨资源不足以继续处理该方法为止。 这就像运行一个无限循环,只是无限循环不一定会引发异常。 原因是代码R3
没有“终端状态”或不再“递归”的点。
We can solve this by including a terminal state:
我们可以通过包含终端状态来解决此问题:
def outer_method(name) (R4)
return name if name == "hello "
outer_method("hello ") + name
end
The first line inside the method definition simply states that if the argument name
is equal to ‘hello’
then simply return name
. That will then ignore any line after it. Therefore in the second line, the code outer_method(“hello “)
will simply give the string “hello “ to be added to whatever name is in the main argument. So the same print outer_method(“Steve”)
will result in the output “hello Steve”
as well.
方法定义中的第一行只是指出,如果参数name
等于'hello'
则只需返回name
。 然后,它将忽略其后的任何行。 因此,在第二行中,代码outer_method(“hello “)
将简单地将字符串“ hello”添加到主参数中的任何名称上。 因此,相同的print outer_method(“Steve”)
也会导致输出“hello Steve”
。
OK then, that may not be the best example for describing recursion (as the recursive version in this case doesn’t have that much advantage over the non-recursive one). But working on the binary tree height problem, we will see that recursion is so much simpler to understand and faster to run.
好的,那可能不是描述递归的最佳示例(因为在这种情况下,递归版本与非递归版本相比没有太多优势)。 但是在处理二叉树高度问题时,我们将看到递归非常容易理解并且运行起来更快。
For this discussion let me put again the same example as I showed in the previous article:
对于此讨论,让我再次输入与上一篇文章相同的示例:
which we can represent as the following array:
我们可以将其表示为以下数组:
tree = [1, 7, 5, 2, 6, 0, 9, 3, 7, 5, 11, 0, 0, 4, 0] (T0)
The indices of the left and right children of any sub tree can be determined as follows:
可以按以下方式确定任何子树的左子级和右子级的索引:
left child of tree[i] is at index 2*i + 1 (T1)
right child of tree[i] is at index 2*i + 2 (T2)
If you’re puzzled about how the figure above became the array following it, I’ll direct you to read the previous article on the iterative method for clarification.
如果您对上图如何变成其后的数组感到困惑,我将指导您阅读上一篇有关迭代方法的文章以进行澄清。
And again the formula for calculating the height of a binary tree, as well as the heights of any of its sub trees, is:
同样,用于计算二叉树的高度以及其任何子树的高度的公式为:
height = 1 + max of(left_child_height, right_child_height) (T3)
Now with these we can outline the steps to develop a recursive program.
现在,我们可以概述开发递归程序的步骤。
Step 0: Set default values — To make the initial method call simple, I always like setting default values for the arguments that will change during each recursive call. Since we will repetitively compute heights, our indices will always change.
步骤0:设置默认值—为了使初始方法调用变得简单,我一直喜欢为将在每次递归调用期间更改的参数设置默认值。 由于我们将重复计算高度,因此索引将始终更改。
For instance, to find the height of the root’s (tree[0]
) left child we will need to call the method on that left child (whose index is at 2*(0) + 1
). Therefore, our method definition will be:
例如,要找到根( tree[0]
)左子tree[0]
的高度,我们将需要在该左子tree[0]
(其索引为2*(0) + 1
)上调用方法。 因此,我们的方法定义将是:
def tree_height_recursive(tree_array,i=0) (S0.1)
to indicate that for the initial call we are calling it on the root element. This will simply allow us to call tree_height_recursive
by inputting only the tree_array. However, this also means, as we will see in the simulation afterwards, we can find the height of any sub tree by simply including its index as the second argument in the method call.
表示对于初始调用,我们在根元素上调用它。 这将仅允许我们通过仅输入tree_height_recursive
来调用tree_height_recursive。 但是,这也意味着,正如我们将在随后的模拟中看到的那样,我们可以通过简单地将子树的索引作为方法调用中的第二个参数包括在内来找到任何子树的高度。
Step 1: Find terminal state — At which point do we simply return a value and not do any further recursive calls? In our binary tree problem, the terminal state is at:
步骤1:查找终端状态-在什么时候我们仅返回一个值,而不进行任何进一步的递归调用? 在我们的二叉树问题中,终端状态为:
return 0 if tree[i].nil or tree[i] == 0 (S1.1)
It simply says that if the element at index i
does not exist or if its value is 0 then simply return 0. Logically, a non-existing sub tree will not have any height.
它只是说如果索引i
处的元素不存在或它的值为0,则仅返回0。从逻辑上讲,不存在的子树将不具有任何高度。
Step 2: Find the height of the left child — this is where the magic of recursion starts to benefit us. We don’t need any fancy code. No more declaring another array to hold the height of each element. No more multiple variable definitions for height indices and the heights themselves, just:
步骤2:找到左孩子的身高-这就是递归的魔力开始使我们受益的地方。 我们不需要任何花哨的代码。 无需再声明另一个数组来保存每个元素的高度。 不再需要高度索引和高度本身的多个变量定义,只需:
right_child_height = tree_height_recursive(tree_array, 2*i + 2)
We simply pass the index of the left child as second argument. Can you see why?
我们只是将左孩子的索引作为第二个参数传递。 你知道为什么吗?
We do the same for finding the right child’s height next.
接下来,我们为找到合适的孩子的身高做同样的事情。
Step 3: Find the height of right child — Likewise, we simply do a recursive call to our method but passing the index of the right child as second argument:
第3步:找到合适的孩子的身高—同样,我们仅对方法进行递归调用,但将合适的孩子的索引作为第二个参数传递:
right_child_height = tree_height_recursive(tree_array, 2*i + 2)
Now that we have the heights of the left and right children, we can now compute the total height.
现在我们有了左右孩子的高度,我们现在可以计算总高度了。
Step 4: Calculate and return total height — As code T3
states, we just add 1 and the height of whichever is taller between the left and right children.
第4步:计算并返回总高度-正如代码T3
所述,我们只加1,然后将左右两个子节点之间的较高者的高度加起来。
total_height = 1 + [left_child_height, right_child_height].max (S4.1)
Since S.4
will be the last statement in our method, then the evaluated total_height
will be returned. Remember that if the conditions in S1.1
hold true (our terminal state) then none of Steps 2–4 will run and the method will simply return 0.
由于S.4
将是我们方法中的最后一条语句, total_height
将返回评估的total_height
。 请记住,如果S1.1
的条件成立(我们的终端状态),则步骤2-4将不会运行,并且该方法将简单地返回0。
The full method below:
完整方法如下:
Comparing this to the iterative method, the recursive version took 3 fewer steps and 4 fewer variable definitions. The code also (excluding empty spaces and comments) is 7 lines fewer. On top of it, the recursive code will run 2x faster (using the benchmark
built-in Ruby module). This is a big advantage if we’re running the method on binary trees hundreds of levels tall.
与迭代方法相比 ,递归版本减少了3个步骤,减少了4个变量定义。 该代码(不包括空格和注释)也少了7行。 最重要的是,递归代码的运行速度将提高2倍(使用内置的benchmark
Ruby模块)。 如果我们在数百层高的二叉树上运行该方法,则这是一个很大的优势。
Now let’s do the same simulation as we did before. For the tree at T0
we run the recursive method:
现在,让我们进行与之前相同的模拟。 对于T0
处的树,我们运行递归方法:
tree = [1, 7, 5, 2, 6, 0, 9, 3, 7, 5, 11, 0, 0, 4, 0]
puts tree_height_recursive(tree_array)-> #should give us 4
Note that since we have a default i=0
in our method definition we don’t need to specify the index here because we are finding the height of the whole tree. To make this simulation more intuitive we shall create an imaginary array called call_stack
where push every call to tree_height_recursive
.
请注意,由于我们的方法定义中有一个默认的i=0
,因此我们不需要在此处指定索引,因为我们可以找到整棵树的高度。 为了使该模拟更加直观,我们将创建一个称为call_stack
的虚构数组,其中将每个调用推入tree_height_recursive
。
So then when we call the method the first time (the main call), we store it in a temporary variable ht_0
and push it to call_stack
:
因此,当我们第一次调用该方法(主调用)时,我们将其存储在临时变量ht_0
并将其推入call_stack
:
ht_0 = height of tree[0] = tree_height_recursive(tree_array,i=0)
call_stack = [ht_0]
We then run Step 1:
然后,我们运行步骤1:
tree[0].nil? -> #falsetree[0] == 0 -> #false, it is 2
Since this results in false
, we go ahead to Step 2:
由于结果为false
,因此我们继续执行步骤2:
since i= 0, then 2*i + 1 = 2*0 + 1 = 1:
left_child_height = tree_height_recursive(tree_array,1)
Since we cannot readily determine this height so then we push it again to call_stack
:
由于我们无法轻松确定此高度,因此我们将其再次推入call_stack
:
ht_1 = left_child_height = tree_height_recursive(tree_array,1)
call_stack = [ht_0,ht_1]
Then upon doing Step 3:
然后执行步骤3:
ht_2 = right_child_height = left_child_height = tree_height_recursive(tree_array,)
call_stack = [ht0,ht1,ht2]
We cannot proceed to Step 4 until all the items in call_stack
have been evaluated by our program and popped off from call_stack
(which should happen for every time each height has been evaluated).
在程序对call_stack
所有项目进行评估并从call_stack
弹出之前,我们无法继续执行第4步(每次评估每个高度都应发生此情况)。
So we will also do the same for each of the succeeding heights. For instance, to compute ht1
we know that we have to compute for its own left and right children’s heights too. So that means the method will be called for them too. So as not to prolong this article, the reader is invited to try this on paper.
因此,我们还将对每个后续高度执行相同的操作。 例如,要计算ht1
我们知道我们也必须计算其自己的左右孩子的身高。 因此,这也将为他们调用该方法。 为了不延长本文的篇幅,邀请读者在纸上尝试一下。
Ultimately, the method will be called recursively with i = 14
as second argument. Thus, at this point, call_stack
will be:
最终,该方法将以i = 14
作为第二个参数递归调用。 因此,此时, call_stack
将为:
call_stack = [ht0,ht1,ht2,ht3,ht4,ht5,ht6,ht7,ht8,ht9,ht10,ht11,ht12,ht13,ht14]
Now we will evaluate each. Note that from tree[7]
up to tree[14]
the elements don’t have any children. So we can simply evaluate their heights as 1 or 0 (depending on whether tree[i]
is 0 or not (where i ≥ 7
):
现在我们将评估每个。 请注意,从tree[7]
到tree[14]
元素没有任何子代。 因此,我们可以简单地将它们的高度评估为1或0(取决于tree[i]
是否为0( i ≥ 7
)):
ht14 = 0
ht13 = 1
ht12 = 0
ht11 = 0
ht10 = 1
ht9 = 1
ht8 = 1
ht7 = 1
Again, when these heights are evaluated we simply pop them off successively from call_stack.
After which, call_stack
will appear as follows:
同样,当评估这些高度时,我们只是简单地从call_stack.
连续弹出它们call_stack.
之后, call_stack
将显示如下:
call_stack = [ht0, ht1, ht2, ht3, ht4, ht5, ht6]
Now, to evaluate ht6
we must remember that it is the call to tree_height_recursive(tree_array, 6)
. Inside this call we also call on the method to compute for the heights of the left and right children of tree[6]
. These we previously already evaluated as ht13
and ht14
. So then:
现在,要评估ht6
我们必须记住它是对tree_height_recursive(tree_array, 6)
的调用。 在此调用中,我们还调用该方法来计算tree[6]
左右子级的高度。 这些我们之前已经评估为ht13
和ht14
。 因此:
ht6 = 1 + [ht13, ht14].max = 1 + [1,0] = 1 + 1 = 2
So we now evaluate ht5
, which is the height of tree[5]
. We know the heights of its children are ht11
and ht12
因此,我们现在评估ht5
,它是tree[5]
的高度。 我们知道孩子的身高分别是ht11
和ht12
ht5 = 1 + [ht11,ht12].max = 1 + [0,0].max = 1 + 0 = 1
Doing the same for ht4
to h1
(again the reader is invited to do the confirmation on paper):
对ht4
到h1
进行相同的ht4
(再次邀请读者在纸上进行确认):
ht4 = 1 + [ht9,ht10].max = 1 + [1,1].max = 1 + 1 = 2
ht3 = 1 + [ht7, ht8].max = 1 + [1, 1].max = 1 + 1 = 2
ht2 = 1 + [ht5, ht6].max = 1 + [1,2].max = 1 + 2 = 3
ht1 = 1 + [ht3, ht4].max = 1 + [2,2].max = 1 + 3 = 3
Again, we pop out each height from call_stack
as we evaluate it so after evaluating ht1
the call_stack
appears as follows:
同样,我们蹦出从每个高度call_stack
,我们评估后,评估它使ht1
的call_stack
显示如下:
call_stack = [ht0]
Now evaluating ht0
is returning to the main call to tree_height_recursive
, so this is the remaining Step 4:
现在评估ht0
返回到对tree_height_recursive
的主调用,因此这是剩余的第4步:
ht0 = 1 + [ht1, ht2].max = 1 + [3, 3].max = 1 + 3 = 4ortotal_height = 1 + [left_child_height, right_child_height].max
Which will return 4
as the result of the main method call.
作为主方法调用的结果,它将返回4
。
As I keep mentioning, doing this on paper whether during the algorithm formulation or during simulation will help a lot in understanding it. This same method can also be used to determine the height of any of the sub trees inside the tree_array
, for instance to determine only the height of the tree’s left child:
正如我一直提到的,无论是在算法制定期间还是在仿真过程中,在纸上进行此操作都将有助于您对其进行大量的理解。 同样的方法也可以用于确定tree_array
内部任何子树的tree_array
,例如,仅确定树的左子树的高度:
puts tree_height_recursive(tree_array, 1) -> #will print out 3
Or any of the lower sub trees:
或任何较低的子树:
puts tree_height_recursive(tree_array, 3) -> #will print out 2
The key takeaway in creating a recursive algorithm, in my perspective, is setting the terminal state. Again, this is the scenario wherein the main method will not have to do any recursive call to itself. Without this, the method will just keep calling itself until the computer blows up (hyperbolically speaking…). When we have the terminal state we can easily set the arguments for the recursive calls and know that our method will safely return the value we expect.
在我看来,创建递归算法的关键之处在于设置终端状态。 同样,在这种情况下,main方法将不必对其自身进行任何递归调用。 没有这种方法,该方法将一直不断调用自己,直到计算机崩溃(夸张地说……)。 当我们拥有终端状态时,我们可以轻松地为递归调用设置参数,并且知道我们的方法将安全地返回我们期望的值。
Finally, working on algorithms challenge our minds to think. As software engineers, or even engineers in general, our main task is to solve problems. We, therefore, need to develop our critical thinking skills.
最后,从事算法工作挑战了我们的思维范围。 作为软件工程师,甚至一般的工程师,我们的主要任务是解决问题。 因此,我们需要发展批判性思维能力。
If for a problem, our first option is always ‘google it’ and copy/paste other people’s code without fully understanding the problem and the copied solution, then we are defeating ourselves.
如果遇到问题,我们的第一个选择始终是“ google it”,然后在不完全了解问题和复制的解决方案的情况下复制/粘贴其他人的代码,那么我们就是在击败自己。
So my suggestion is always have pen and paper ready and not immediately type code when faced with an algorithm challenge. Simulate the problem for simple inputs then come up with the code after you determine the steps (like I outlined them above).
因此,我的建议是始终准备好笔和纸,而不要在遇到算法挑战时立即键入代码。 为简单的输入模拟问题,然后在确定步骤后提出代码(如我上面概述的步骤)。
Follow me on Twitter | Github
在Twitter上 关注我 | Github
翻译自: https://www.freecodecamp.org/news/how-to-calculate-binary-tree-height-with-the-recursive-method-aafc461f2201/
递归计算二叉树的高度