Balanced Tree
When using a tree data structure, it's common for the tree to become unbalanced over time due to the insertion order of nodes, which can in turn affect the performance of our programs. Let's define a balanced tree as one where the difference in height of the left and right subtrees is at most one, for all nodes in the given tree. Write a function is_balanced(node) that determines whether a binary tree is balanced or not.
Input: The root node of a binary tree
Output: True if the tree is balanced, False otherwise.
Assume you are given the root node of a tree that conforms to the following interface:
class Node { left: Node right: Node value: any }
Examples
Example 1: Balanced
Tree: a / \ b c / \ \ d e f is_balanced(a) # => True
Example 2: Balanced
Tree: a / \ b c \ d is_balanced(a) # => True
Example 3: Not Balanced
Tree: a / \ b c \ d \ e is_balanced(a) # => False
Example 4: Not Balanced
Tree: a / \ b c / \ d e / \ f g is_balanced(a) # => False
Note that while the last tree seems symmetrical, it is not balanced because nodes b and c are not balanced.
We can calculate the depth (or height) of a tree by counting the number of nodes from the root to the leaf.
You might find it helpful to use recursion to traverse the nodes and find their height.
For every node, you should check whether the height of its subtrees meets the definition of being balanced.
How are you calculating the height of the tree? Does your function explore all paths?
Does your solution reject trees like the last example above?
Can you optimize the performance of your function? Can you find a linear time solution?
This is a classic data structures question that really tests your understanding of concepts like trees, recursion, and algorithmic runtime. The quality of being "balanced" is actually very important in real-world applications—that's why we created self-balancing variants such as b-trees and red-black trees.
The key to solving this problem is to read the prompt and examples carefully. Let's begin with the definition: "a balanced tree is one where the difference in height of the left and right subtrees is at most one, for all nodes in the given tree." Off the bat, we know we need to compare the height of the left and right subtrees. We also know we need to do this comparison for all nodes in the tree. Now the question is, how do we get the height?
Let's start off by writing a function to get the height of a tree. If you aren't certain what the "height" is, you could ask your interviewer to clarify. The height of a tree is typically defined as the maximum number of 'layers' from top to bottom.
def get_height(node):
if not node:
return 0
l_height = get_height(node.left)
r_height = get_height(node.right)
return max(l_height, r_height) + 1Now, let's use this to implement our main function, is_balanced. All we have to do is explore each node and make sure the height of its subtrees meet the condition in the prompt: the difference in height should be less than or equal to 1.
def is_balanced(node):
if not node:
return True
l_height = get_height(node.left)
r_height = get_height(node.right)
height_diff = abs(l_height - r_height)
if height_diff > 1:
return False
return is_balanced(node.left) and is_balanced(node.right)A note on recursion: Whenever you implement a recursive function, in the back of your mind you should ask whether recursion is the best way to accomplish your goal. Recursion has the downside of using the callstack, which can lead to stack overflow for deeply nested data. You're unlikely to hit this error during an interview, but it's worth mentioning as a real-life constraint and explaining how to implement a non-recursive version (e.g. by using a stack).
Time Complexity: . For each node, the get_height function is called to calculate the heights of the left and right subtrees. Since get_height is called for each of the n nodes in the worst case , this results in time complexity.
Space Complexity: . the space complexity is determined by the maximum depth of the recursion stack, which is the height of the tree (). Each recursive call of is_balanced adds to the call stack.
Optimized Approach
We can optimize the is_balanced function to reduce the time complexity to by combining the height calculation and balance checking into a single traversal. Here’s how:
def check_height_and_balance(node):
if not node:
return 0, True
l_height, l_balanced = check_height_and_balance(node.left)
r_height, r_balanced = check_height_and_balance(node.right)
if not l_balanced or not r_balanced:
return max(l_height, r_height) + 1, False
height_diff = abs(l_height - r_height)
if height_diff > 1:
return max(l_height, r_height) + 1, False
return max(l_height, r_height) + 1, True
def is_balanced(node):
_, balanced = check_height_and_balance(node)
return balancedCan you use a stack to solve this?
function visitChildren(node) { let leftSubtreeHeight = 0; let rightSubtreeHeight = 0; let isChildrenBalanced = true; if (node.left) { const { isBalanced, height } = visitChildren(node.left); isChildrenBalanced = isChildrenBalanced && isBalanced; leftSubtreeHeight += height + 1; } if (isChildrenBalanced && node.right) { const { isBalanced, height } = visitChildren(node.right); isChildrenBalanced = isChildrenBalanced && isBalanced; rightSubtreeHeight += height + 1; } return { isBalanced: isChildrenBalanced && (Math.abs(leftSubtreeHeight - rightSubtreeHeight) < 2), height: Math.max(leftSubtreeHeight, rightSubtreeHeight), }; } function isBalanced(node) { if (node) { return visitChildren(node).isBalanced; } return false; }