A Question of Depth

You find yourself perched on the precipitous brink of a black and lurid tarn. You know there are fish down there, you just can’t see them. Arnie the Angler wants you to drop a line down, and measure the pools of fishing swimming at every depth.

It’s pretty dark down there, and there’s no light. Now you understand why Weddell Seals swim under the fish and look back up, with the sky as the background and hunt by silhouette.

To make thing easier, it turns out that the fish have arranged themselves in a binary tree, root up. You first need to see how many fish are swimming at depth d. Let’s assume depth starts at 1.

Why, it’s a simple recursive search! We dive to level d, then record at that level.

package questionDepth

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func questionDepth(root *TreeNode, depth int) []*TreeNode {
	var result []*TreeNode
	if depth != 0 {
		depth--
		var traverse func(node *TreeNode, d int)
		traverse = func(node *TreeNode, d int) {
			if node != nil {
				if d > 0 {
					traverse(node.Left, d-1)
					traverse(node.Right, d-1)
				} else {
					result = append(result, node)
				}
			}
		}
		traverse(root, depth)
	}
	return result
}

This is a nice way to code it, since it’s all self-contained within the closure.

OK, that seems ok, but how much work are we doing? What is the complexity of the solution, assuming that n is the number of fish nodes?

What if this method is called frequently?

Complexity: we’re visiting every node up to depth d, so this is O(n).

Turns out Arnie wants to know about a whole bunch of different depths – how can we generate the results faster?

In addition could be a lot of fish in the sea, so it would be better to cache the results if we’re being asked this question a lot. Let’s hold off thinking about what happens if we catch a fish (delete) or throw one back (insert).

Think Dynamic Programming!

package questionDepth

var depths [][]*TreeNode

func questionDepth(root *TreeNode, depth int) []*TreeNode {
	if depth == 0 {
		return nil
	}
	depth--
	if depths == nil {
		depths = append(depths, []*TreeNode{root})
		for d := 0; len(depths[d]) != 0; d++ {
			var next []*TreeNode
			for _, n := range depths[d] {
				if n.Left != nil {
					next = append(next, n.Left)
				}
				if n.Right != nil {
					next = append(next, n.Right)
				}
			}
			depths = append(depths, next)
		}
	}
	if depth < len(depths) {
		return depths[depth]
	}
	return nil
}

Now we have a nice cache and efficient cache generation algorithm. How can we handle insert/delete?

There are several options here, depending on what you know and what you don’t. The key is, how do we delete or insert while maintaining our data structures?

Insert should be straightforward, as you will know the depth when you insert a node into the tree, and you can append the node to the array at the correct depth.

Delete could be complicated. You’d either need to lose performance by searching for the node to delete, or make the nodes themselves able to be a doubly-linked list for a quick removal.

Something to think about.

Here’s another question. We’re assuming that we’re looking at a generic sort of binary tree. What if it were a heap?

Author: Dean

Leave a Reply

Your email address will not be published. Required fields are marked *