Skip to content
Skilled.dev

Free lesson walkthrough · Kotlin

Spiral matrix traversal in Kotlin: Walk a Matrix

Traverse a rectangle in clockwise spiral order using shrinking boundaries. Follow the Kotlin implementation, its correctness argument, worked example, complexity, and boundary tests.

4 min read · Worked code, edge cases, and complexity

Return every value of a rectangular matrix in clockwise spiral order, starting at its top-left corner. For [[1, 2, 3], [4, 5, 6], [7, 8, 9]], the order is [1, 2, 3, 6, 9, 8, 7, 4, 5]. Reading values in this order does not require rotating the matrix or marking its cells.

Maintain four unvisited boundaries

Keep the first and last unvisited row and the first and last unvisited column. Initially those bounds describe the full matrix. Each pass walks the top edge left to right, the right edge top to bottom, the bottom edge right to left, and the left edge bottom to top. Move the corresponding boundary inward after walking an edge.

Moving the top boundary before walking the right edge prevents the top-right corner from being visited twice. Moving the right boundary before the bottom edge does the same for the bottom-right corner. The sequence of updates is part of the algorithm; treating all four edges as independent full-length walks repeats corners.

Trace the shrinking rectangle

In the three-by-three example, the top walk emits 1, 2, 3 and moves the top boundary to row 1. The right walk emits 6, 9 and moves the right boundary to column 1. The bottom walk emits 8, 7 and moves the bottom boundary to row 1. The left walk emits 4 and moves the left boundary to column 1.

The remaining rectangle contains only 5. The next top walk emits it, and the updated bounds cross. The outer loop then stops. The output length is nine, matching the cell count. The same boundary representation also handles a non-square rectangle without changing direction logic.

Guard the last row and column

After the top and right walks, there may be no bottom row left to visit. Check the row bounds before walking backward along it. Likewise, check the column bounds before walking upward along the left edge. A one-row input otherwise gets emitted forward and backward, and a one-column input can repeat its interior cells.

For [[1, 2, 3, 4]], the top walk finishes all useful work. For [[1], [2], [3]], the top walk emits 1 and the right walk emits 2 and 3. The remaining guards prevent a duplicate return trip. These narrow shapes expose mistakes that a square example can hide.

Prove complete, unique visitation

Before a pass, the bounds contain exactly the cells not yet emitted. Each edge walk consumes an exposed edge of that rectangle, and its boundary update removes precisely those cells. Corner handling and the final-edge guards ensure those consumed regions do not overlap. Any remaining interior cells form the next smaller rectangle.

The process ends when no rows or columns remain. Every original cell has then been consumed exactly once. Total time is O(r × c), and boundary tracking uses O(1) auxiliary space. The returned sequence needs O(r × c) storage. There is no separate visited matrix, and the input cells are not changed.

Check dimensions and adapt deliberately

The lesson assumes a rectangular matrix. Empty rows yield no values; an empty outer array must be checked before accessing its first row in languages that reject that access. Test both forms explicitly when supporting them. Ragged rows need a different contract because four rectangular bounds cannot describe their shape.

Test one cell, one row, one column, two-by-three, three-by-two, and an odd-sized square with a central cell. Use distinct cell values so repeated visits are visible. Compare output length with the cell count and use a direction-and-visited-set simulator as an independent oracle. A counterclockwise variant changes the edge order and start directions; it should preserve the same unvisited-region invariant.

Kotlin implementation

The mutable result list is separate from the matrix. The final row and column guards prevent duplicates on narrow rectangles.

The course constructs the class shown here and calls its declared methods. Nullable node references require explicit handling; a non-null assertion relies on an earlier condition that proves a value exists. Primitive arrays and mutable collections can be changed through shared references. Kotlin Int arithmetic is bounded, so use Long intermediates when a wider problem contract requires them.

class Solution {
    fun walkMatrix(matrix: Array<IntArray>): List<Int> {
        if (matrix.isEmpty()) return emptyList()
        val result = mutableListOf<Int>()

        // Initialize the upper and lower bounds indexes
        val rowCount = matrix.size
        val columnCount = matrix[0].size
        var startRow = 0
        var endRow = rowCount - 1
        var startColumn = 0
        var endColumn = columnCount - 1

        // We will keep walking as long as rows or columns remain.
        // That happens as long as the end is greater or equal to the start.
        while (endRow >= startRow && endColumn >= startColumn) {
            // Walk across the top starting row for each column from beginning to end
            // This is left-right across the top
            for (column in startColumn..endColumn) {
                // Add the item to result in order
                result.add(matrix[startRow][column])
            }

            // Increment our start row since we visited each value
            startRow++

            // Walk top-bottom for the end column
            for (row in startRow..endRow) {
                result.add(matrix[row][endColumn])
            }
            endColumn--

            // Since we increment startRow, we need to make sure we are still in bounds
            if (endRow >= startRow) {
                // Walk right-left on the bottom row
                for (column in endColumn downTo startColumn) {
                    result.add(matrix[endRow][column])
                }
            }
            endRow--

            // Since we decrement endColumn, we need to make sure we are still in bounds
            if (endColumn >= startColumn) {
                // Walk bottom-top for the start column
                for (row in endRow downTo startRow) {
                    result.add(matrix[row][startColumn])
                }
            }
            startColumn++
        }

        return result
    }
}

Practice this implementation

Open Walk a Matrix to run the implementation against the course tests, then change one boundary condition and explain the first failing case. The editor supplies the starter signature and any node types or helper APIs used above. Copy the implementation into that environment; the snippet is the solution body, without the input parser or test driver. Your saved progress belongs to the original lesson.

For more Kotlin conventions and practice problems, use the Kotlin interview guide.

Try the exercise

Write your own implementation in the course editor. This walkthrough belongs to the same lesson; your course progress stays with that exercise. Exercise access follows the course’s access settings.