Get a free weekly practice problem!

Keep that axe sharp.

× No thanks

You only have free questions left (including this one).

But it doesn't have to end here! Sign up for the 7-day coding interview crash course and you'll get a free Interview Cake problem every week.

Write a function to see if a binary tree is "superbalanced" (a new tree property we just made up).

A tree is "superbalanced" if the difference between the depths of any two leaf nodes is no greater than one.

Here's a sample binary tree implementation:

typedef struct BinaryTreeNode { void *value; struct BinaryTreeNode *left; struct BinaryTreeNode *right; } BinaryTreeNode; BinaryTreeNode * binaryTreeNodeNew(const void *value, size_t valueSize) { BinaryTreeNode *node; node = malloc(sizeof(BinaryTreeNode)); assert(node != NULL); node->value = malloc(valueSize); assert(node->value != NULL); memcpy(node->value, value, valueSize); node->left = NULL; node->right = NULL; return node; } BinaryTreeNode * binaryTreeNodeInsertLeft(BinaryTreeNode *treeRoot, const void *value, size_t valueSize) { treeRoot->left = binaryTreeNodeNew(value, valueSize); return treeRoot->left; } BinaryTreeNode * binaryTreeNodeInsertRight(BinaryTreeNode *treeRoot, const void *value, size_t valueSize) { treeRoot->right = binaryTreeNodeNew(value, valueSize); return treeRoot->right; } void binaryTreeNodeFree(BinaryTreeNode *treeRoot) { if (treeRoot != NULL) { binaryTreeNodeFree(treeRoot->left); binaryTreeNodeFree(treeRoot->right); free(treeRoot->value); free(treeRoot); } }

Your first thought might be to write a recursive function, thinking, "the tree is balanced if the left subtree is balanced and the right subtree is balanced." This kind of approach works well for some other tree problems.

But this isn't quite true. Counterexample: suppose that from the root of our tree:

  • The left subtree only has leaves at depths 10 and 11.
  • The right subtree only has leaves at depths 11 and 12.

Both subtrees are balanced, but from the root we will have leaves at 3 different depths.

We could instead have our recursive function get the array of distinct leaf depths for each subtree. That could work fine. But let's come up with an iterative solution instead. It's usually better to use an iterative solution instead of a recursive one because it avoids stack overflow.

We can do this in time and space.

What about a tree with only one leaf node? Does your function handle that case properly?

Sometimes it's good to start by rephrasing or "simplifying" the problem.

The requirement of "the difference between the depths of any two leaf nodes is no greater than 1" implies that we'll have to compare the depths of all possible pairs of leaves. That'd be expensive—if there are n leaves, there are possible pairs of leaves.

But we can simplify this requirement to require less work. For example, we could equivalently say:

  • "The difference between the min leaf depth and the max leaf depth is 1 or less"
  • "There are at most two distinct leaf depths, and they are at most 1 apart"

If you're having trouble with a recursive approach, try using an iterative one.

To get to our leaves and measure their depths, we'll have to traverse the tree somehow. What methods do we know for traversing a tree?

Depth-first and breadth-first are common ways to traverse a tree. Which one should we use here?

The worst-case time and space costs of both are the same—you could make a case for either.

But one characteristic of our algorithm is that it could short-circuit and return 0 as soon as it finds two leaves with depths more than 1 apart. So maybe we should use a traversal that will hit leaves as quickly as possible...

Depth-first traversal will generally hit leaves before breadth-first, so let's go with that. How could we write a depth-first walk that also keeps track of our depth?

We do a depth-first walk through our tree, keeping track of the depth as we go. When we find a leaf, we add its depth to an array of depths if we haven't seen that depth already.

Each time we hit a leaf with a new depth, there are two ways that our tree might now be unbalanced:

  1. There are more than 2 different leaf depths
  2. There are exactly 2 leaf depths and they are more than 1 apart.

Why are we doing a depth-first walk and not a breadth-first one? You could make a case for either. We chose depth-first because it reaches leaves faster, which allows us to short-circuit earlier in some cases.

// Implements a binary tree #include "Cake/BinaryTree.h" // Implements a stack // Stack * stackNew(void); // void stackFree(Stack *stack); // int stackIsEmpty(const Stack *stack); // void stackPush(Stack *stack, const void *value, size_t valueSize); // void stackPop(Stack *stack); // void * stackPeek(const Stack *stack); #include "Cake/Stack.h" // a structure containing a node and its depth typedef struct NodeDepthPair { const BinaryTreeNode *node; size_t depth; } NodeDepthPair; size_t sizeDistance(size_t a, size_t b) { return a > b ? a - b : b - a; } int isBalanced(const BinaryTreeNode *treeRoot) { NodeDepthPair depthPair; size_t depths[3]; // we short-circuit as soon as we find more than 2 size_t depthsLength = 0; Stack *nodes = NULL; // a tree with no nodes is superbalanced, since there are no leaves! if (!treeRoot) { return 1; } nodes = stackNew(); // initialize a depthPair for the root node depthPair.node = treeRoot; depthPair.depth = 0; stackPush(nodes, &depthPair, sizeof(NodeDepthPair)); while (stackIsEmpty(nodes) == 0) { // get a node and its depth from the top of our stack and pop it NodeDepthPair *nodeDepthPair = stackPeek(nodes); const BinaryTreeNode *node = nodeDepthPair->node; size_t depth = nodeDepthPair->depth; stackPop(nodes); // case: we found a leaf if (node->left == NULL && node->right == NULL) { size_t i; int depthFound = 0; // Check if we've seen this depth already for (i = 0; i < depthsLength; i++) { if (depths[i] == depth) { depthFound = 1; break; } } // If not, record the new depth if (!depthFound) { depths[depthsLength] = depth; depthsLength++; // two ways we might now have an unbalanced tree: // 1) more than 2 different leaf depths // 2) 2 leaf depths that are more than 1 apart if (depthsLength > 2 || (depthsLength == 2 && sizeDistance(depths[0], depths[1]) > 1)) { stackFree(nodes); return 0; } } } // case: this isn't a leaf - keep stepping down else { if (node->left != NULL) { depthPair.node = node->left; depthPair.depth = depth + 1; stackPush(nodes, &depthPair, sizeof(NodeDepthPair)); } if (node->right != NULL) { depthPair.node = node->right; depthPair.depth = depth + 1; stackPush(nodes, &depthPair, sizeof(NodeDepthPair)); } } } stackFree(nodes); return 1; }

time and space.

For time, the worst case is the tree is balanced and we have to iterate over all n nodes to make sure.

For the space cost, we have two data structures to watch: depths and nodes.

depths will never hold more than three elements, so we can write that off as .

Because we’re doing a depth first search, nodes will hold at most d nodes where d is the depth of the tree (the number of levels in the tree from the root node down to the lowest node). So we could say our space cost is .

But we can also relate d to n. In a balanced tree, d is . And the more unbalanced the tree gets, the closer d gets to n.

In the worst case, the tree is a straight line of right children from the root where every node in that line also has a left child. The traversal will walk down the line of right children, adding a new left child to nodes at each step. When the traversal hits the rightmost node, nodes will hold half of the n total nodes in the tree. Half n is , so our worst case space cost is .

This is an intro to some tree basics. If this is new to you, don't worry—it can take a few questions for this stuff to come together. We have a few more coming up.

Particular things to note:

Focus on depth-first vs breadth-first traversal. You should be very comfortable with the differences between the two and the strengths and weaknesses of each.

You should also be very comfortable coding each of them up.

One tip: Remember that breadth-first uses a queue and depth-first uses a stack (could be the call stack or an actual stack object). That's not just a clue about implementation, it also helps with figuring out the differences in behavior. Those differences come from whether we visit nodes in the order we see them (first in, first out) or we visit the last-seen node first (last in, first out).

Reset editor

Powered by

. . .