^ Image of a heron on an old bridge support in Richmond, VA that I made during an MS paint challenge
Implementing Flood Fill
Today, Benjamin Arnavand I tackled a depth-first-search (DFS) leetcode problem called flood fill. Aging myself, I was secretly excited about this problem because it’s the algorithm behind MS paint’s classic “fill” behavior. When I was nine, I got frustrated by this mechanism, which didn’t always “squeeze through” diagonal “gaps” in the pixels like I hoped it would, but it did have a knack for still finding a way through some opening I accidentally left in my outlines. You can’t have it all.
Turns out, this is exactly the behavior leetcode asks us to create - for each pixel that needs to be changed to a new color, check all four of it’s neighbors (not all eight!) to see if they are the same as the original color, and continue changing their colors until hitting the edge or exhausting all same-color neighbors.
Even as a team, we were a bit spooked by the term “depth first search”. We discovered the correct approach during our fifteen minute pseudo-coding exercise, but then looked up answers before attempting to implement it. I believe this was a mistake, as we robbed ourselves of the full opportunity to debug our own implementation.
That said, once we verified our approach was a valid one, we were able to implement it within ten minutes. This is what we came up with in python:
Later I translated it to JavaScript:
Gotchas
- When porting to JavaScript I made the mistake of checking if
image[sr][sc] === color
, instead of original color likeimage[sr][sc] === originalColor
. Thecolor
is the color we are changing each pixel to, but only if it is the same color as the original color - Although in python this doesn’t seem necessary, the
image[rc][sc]
does seem to need to be evaluated after all of the boundaries checks, otherwise we run the risk of checking for an index on a row that doesn’t exist, throwing an error.
DFS vs. BFS
The only real difference with breadth-first-search (BFS) is that instead of following each path all the way to the end before starting the next one, we evaluate each neighbor before moving in. Because BFS can’t really be done recursively, this can be understood best by changing the DFS implementation above into an iterative one and then comparing it with iterative BFS.
Iterative DFS
Iterative BFS
Pretty similar, right? The only different is that we are applying a queue.shift()
on our array (and calling it a queue for that matter) instead of a stack.pop()
. The .shift()
is FIFO - we are making sure to execute our operations in the order they come up, and not, I dunno, starting our side quests as they come up.
.pop()
, meanwhile, gives us LIFO, or a stack, where we keep drilling down into next generations of our search as we discover them, only “back tracking” once we’ve followed a path all the way to the end.
DFS Diagram
BFS Diagram
Implementation thoughts
- Re:gotcha #2, I wonder if instead of checking for boundaries I could just do a
try/catch
and let the boundary errors happen:
Things I learned
Nested function declarations can be👌🏼
Although declaring a function inside of another function can be dangerous for scope-declaration collision concerns, it can also be really convenient for scope-declaration redundancy avoidance. I’ve been burned by this in the past during a mock interview, so initially I had some resistance when Benjamin wanted to do this, but he changed my mind. A recursive function declared inside of a parent function that can call it allows global variables to be declared once and awkwardly passed through a section time to the recursive function, even though they may not change.
Default parameter values are OP
That said, one of the coolest things I’ve learned about recursive functions recently is that because by nature each new function is nested inside a previous generation of itself, any variable declared in the first generation (possibly as a default value for a parameter), never needs to be redeclared.
Continuing on that train of thought - if default variables can be useful in this way, can they also be used to remove the recursive-function-parent-function pattern entirely? For example, something like:
Okay so in writing the above I realized no, this isn’t a good way to create global variables, because in this case the “global” variable needs to be wrapped inaccessibly inside of a conditional.
However, what are the limits to the evaluations we can run inside of the parameters?
^ I don’t see why this isn’t possible. It is a bit confusing though. Never has clear indentation practices been so important or, in this case, so inadequate.
DFS and BFS have similar forms, but distinct strengths
Another thing I learned was that, at least in this case, depth-first-search (DFS) and breadth-first-search (BFS) are very similar. In fact, the only difference in code seems to be that DFS employs pop()
in order to perform LIFO (last-in-first-out), whereas BFS uses shift
to achieve FIFO (first-in-first-out).
As far as differences in performance, here is a comparison.
DFS
- memory usage generally lower than BFS
- Good for finding a path if several exist
BFS
- Good for finding THE path if only one exists
- Guaranteed to find shortest path to something while iteration occurs, not after
- Can be slower in cases where the “goal” is deep in the structure
Conclusions
The flood fill problem is a really nice way to visualize the tree-like behavior recursion of both BFS and DFS. I would like to explore other BFS and DFS use cases to understand them better.