Modifying state in a recursive function is a bad idea if you can avoid it. If a function does not modify state, it becomes a pure function; such functions are easier to reason about and test. To produce output, the best way is usually to return a value.
But first, a word on the terminology you chose. In computer science, the depth of a tree node refers to how far it is from the root node; in other words, how far you can walk up. The value you are computing, is how far you can walk down from a node, which is commonly called the height of a node.
Your original function, renamed to height
, is:
def height(node, currentHeight, currentMaxContainer):
currentHeight += 1
if currentHeight > currentMaxContainer[0]:
currentMaxContainer[0] = currentHeight
for child in node.children:
if child is not None:
height(child, currentHeight, currentMaxContainer)
Let's modify it so it doesn't modify currentMaxContainer
, but returns a value instead:
def height(node, currentHeight):
for child in node.children:
if child is not None:
child_height = height(child, currentHeight)
if child_height > currentHeight:
currentHeight = child_height
return currentHeight + 1
Now it finds the maximum height of its children, adds 1 (for the current node), and returns that. Usage is now print(height(startNode, startHeight))
, or print(height(rootNode, 0))
.
We can make the fact that it finds the maximum height of its children more explicit:
def height(node, currentHeight):
max_height = 0
for child in node.children:
if child is not None:
child_height = height(child, 0)
if child_height > max_height:
max_height = child_height
return currentHeight + max_height + 1
This brings into question: why is currentHeight
passed as an argument? It's only used once, and added to the total result. Really, the function doesn't care what some height (depth?) above the current node is, it should only concern itself with the height below the current node. It's a bit of outside state that is irrelevant. After all, you can just add any currentHeight
value to the return value of height()
!
def height(node):
max_height = 0
for child in node.children:
if child is not None:
child_height = height(child)
if child_height > max_height:
max_height = child_height
return max_height + 1
This finds the height of the subtree referred to by node
(a definition now completely consistent with common CS terminology). Usage is now print(height(startNode) + startHeight)
, or print(height(rootNode) + 0)
, which obviously becomes print(height(rootNode))
.
Having removed unnecessary state from the function, it becomes easier to simplify. First, let's consider that None
case; a child that is None
should not increase max_height
, that's what the if
is for. But if we specify that height(None) = 0
, then we don't need that test in the loop:
def height(node):
if node is None:
return 0
max_height = 0
for child in node.children:
child_height = height(child)
if child_height > max_height:
max_height = child_height
return max_height + 1
The remaining loop is just finding the maximum of height(child)
for all children, which can be expressed a lot shorter and more Pythonic:
def height(node):
if node is None:
return 0
return max(map(height, node.children), default=0) + 1
Alternatively, we could filter out the None
children like we did before with the explicit test:
def height(node):
return max(map(height, filter(None, node.children)), default=0) + 1
But I like the previous version better, because it is more explicit.
Those None
children are a bit of a recurring headache though, and that will be the case for any code that has to iterate over a node's children. I recommend simply disallowing that case. In other words, node.children
should only contain (0 or more) valid children, never any None
s. This should simplify a lot of code that interacts with your trees. The function then is simply:
def height(node):
return max(map(height, node.children), default=0) + 1
And finally, this function should really be a method on the class:
class Node:
...
def height(self):
return max(c.height() for c in self.children), default=0) + 1
Usage then finally becomes print(startNode.height() + startHeight)
, or print(rootNode.height())
.
Note that tree structures are very common, so there are libraries implementing them along with common tree operations. You may want to use one of those, or perhaps look at their APIs and/or source for inspiration. One such library for Python is anytree (no affiliation or endorsement).
lower_case_with_underscores
style. \$\endgroup\$return
. \$\endgroup\$