A common interview question is the island counter problem. You process of a matrix which values which are land or sea. Your goal is to count the number of islands. And islands are defined by land which is connected to grid positions. This can be done by square directions or also including diagonal directions.
Below is one example of a matrix which includes 3 islands.
let terrain: Terrain = [
[.land, .sea, .land],
[.sea, .sea, .sea],
[.sea, .land, .land]
]
Below is my implementation in Swift. It uses enums and a typealias to define the types and support functionality
directly relevant to the type. The typealias is named Terrain
is simply an array or arrays where the element is a
TerrainType
which is an enum. The cases include land and sea as well as marked versions of those cases. A
computed property indicates if the value is marked.
The Terrain
type has a type extension which adds useful computed properties as well as subscript support. A struct
called Position
is used with the subscript support. It makes it easy to access a value by Position
and check if the
position with the isInBounds
function and mark it with the markPosition
function.
The Position
type supports the +
operator to add a Direction
which is an enum. By adding a direction to a position
it adjusts the x
and y
values appropriately. The Direction
type includes a position
property which uses the
current case to calculate a value. This type also has a computed property for squareDirections
which excludes the
diagonal directions.
All of this support then makes the implementation much simpler. The findIslands
function will iterate over every
possible position, even if the matrix is not square. Each position is checked and when land is found it will call the
findIsland
at that position, mark it and then call consumeIsland
which will check every position in each given
direction. When creating the instance of this IslandCounter
there is an option for includeDiagonal
to use just
the square directions or all of them. That changes which directions the consume function will use. And each time
the consume function finds connected land it calls itself again recursively. It also marks the sea positions since
they can be skipped when the returning to the loops in the findIslands
function. A simple count
property is
incremented each time an unmarked land position is found. Once all positions have been checked the count is returned.
There is also an option to enable debugging when the IslandCounter
instance is created. When enabled the terrain is
printed out after each island has been consumed. It should show visually that each position has been marked including
the sea positions which were checked on the edges. Since the terrain is represented with emojies it is printed out with
fixed widths making it easy to visually understand the current state.
Tests are also included in this Swift Playground and run with a test observer which prints the results. The features
in the supporting types are tested and then various scenarios with different instances of Terrain
.
One convention used with this code matches what is apparently the internal style at Apple. Instead of immediately
returning a computed value it is set to a local variable and then returned. The benefits here are that it allows a
developer to set a breakpoint on the return to view the local variable before it is returned. And a side effect,
intentional or not, is that it actually increases code coverage. This code also declares the result
variable without
a value which is returned at the end of the function. This undefined value must be defined on all code paths or it
won't compile. This technique ensures the result is set intentionally instead of mistakenly using a default value.
And this coding style also helps with increasing code coverage. When early returns are used there can be gaps in
code coverage even when logically all lines of code are reached. Something about how code coverage is implemented with
Xcode works better with this coding style instead of using early returns.