{"id":4591,"date":"2024-10-21T11:04:44","date_gmt":"2024-10-21T11:04:44","guid":{"rendered":"https:\/\/algocademy.com\/blog\/matrix-traversal-mastering-spiral-diagonal-and-zigzag-patterns\/"},"modified":"2026-10-15T10:34:50","modified_gmt":"2026-10-15T10:34:50","slug":"matrix-traversal-mastering-spiral-diagonal-and-zigzag-patterns","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/matrix-traversal-mastering-spiral-diagonal-and-zigzag-patterns\/","title":{"rendered":"Matrix Traversal: Spiral, Diagonal and Zigzag Patterns in Python"},"content":{"rendered":"<p>Matrix traversal problems ask you to visit every cell of a 2D grid in a specific order. Spiral order uses four shrinking boundaries, diagonal order groups cells by <code>row + col<\/code>, and zigzag order is diagonal order with every other diagonal reversed. Each runs in O(rows x cols) time, and the bugs almost always come from non-square matrices, a single row or column, and off-by-one boundaries.<\/p>\n<p>All examples are Python. They share this 3&#215;4 test matrix, and the outputs shown were produced by running the code:<\/p>\n<pre><code class=\"language-python\">m = [[1,  2,  3,  4],\n     [5,  6,  7,  8],\n     [9, 10, 11, 12]]\n<\/code><\/pre>\n<h2>Spiral order<\/h2>\n<p>Walk the outer ring clockwise, then repeat on the ring inside. Track four boundaries (<code>top<\/code>, <code>bottom<\/code>, <code>left<\/code>, <code>right<\/code>) and move each one inward after you finish the side it controls.<\/p>\n<pre><code class=\"language-python\">def spiral(matrix):\n    if not matrix or not matrix[0]:\n        return []\n    out = []\n    top, bottom = 0, len(matrix) - 1\n    left, right = 0, len(matrix[0]) - 1\n    while top &lt;= bottom and left &lt;= right:\n        for c in range(left, right + 1):          # left to right along the top\n            out.append(matrix[top][c])\n        top += 1\n        for r in range(top, bottom + 1):          # down the right side\n            out.append(matrix[r][right])\n        right -= 1\n        if top &lt;= bottom:                         # right to left along the bottom\n            for c in range(right, left - 1, -1):\n                out.append(matrix[bottom][c])\n            bottom -= 1\n        if left &lt;= right:                         # up the left side\n            for r in range(bottom, top - 1, -1):\n                out.append(matrix[r][left])\n            left += 1\n    return out\n\nprint(spiral(m))   # [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]\n<\/code><\/pre>\n<p>The two <code>if<\/code> checks are the part that matters. After the top row and right column are done, the remaining ring may have collapsed into a single row or column (for example the middle row of a 3-row matrix). Without the checks, you would walk that row a second time in the opposite direction and output duplicates. Test <code>[[1,2,3]]<\/code>, <code>[[1],[2],[3]]<\/code> and a 3&#215;4 matrix like the one above.<\/p>\n<h3>The direction-vector alternative<\/h3>\n<p>If boundary bookkeeping feels fragile, simulate a walker instead: keep going in the current direction, and turn right when the next cell is out of bounds or already visited.<\/p>\n<pre><code class=\"language-python\">def spiral_dirs(matrix):\n    if not matrix or not matrix[0]:\n        return []\n    R, C = len(matrix), len(matrix[0])\n    seen = [[False] * C for _ in range(R)]\n    dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]    # right, down, left, up\n    r = c = d = 0\n    out = []\n    for _ in range(R * C):\n        out.append(matrix[r][c])\n        seen[r][c] = True\n        nr, nc = r + dirs[d][0], c + dirs[d][1]\n        if not (0 &lt;= nr &lt; R and 0 &lt;= nc &lt; C) or seen[nr][nc]:\n            d = (d + 1) % 4\n            nr, nc = r + dirs[d][0], c + dirs[d][1]\n        r, c = nr, nc\n    return out\n<\/code><\/pre>\n<p>This is harder to get wrong but uses O(rows x cols) extra memory for <code>seen<\/code>. I checked that both versions return the same order on 500 random matrices from 1&#215;1 to 7&#215;7. It is a good fallback when you are under time pressure.<\/p>\n<h3>Writing a spiral instead of reading one<\/h3>\n<p>&#8220;Spiral Matrix II&#8221; fills an n x n grid with 1..n\u00b2 in spiral order. The boundary code is the same, but you assign instead of append:<\/p>\n<pre><code class=\"language-python\">def generate_spiral(n):\n    grid = [[0] * n for _ in range(n)]\n    top, bottom, left, right = 0, n - 1, 0, n - 1\n    k = 1\n    while top &lt;= bottom and left &lt;= right:\n        for c in range(left, right + 1):\n            grid[top][c] = k; k += 1\n        top += 1\n        for r in range(top, bottom + 1):\n            grid[r][right] = k; k += 1\n        right -= 1\n        if top &lt;= bottom:\n            for c in range(right, left - 1, -1):\n                grid[bottom][c] = k; k += 1\n            bottom -= 1\n        if left &lt;= right:\n            for r in range(bottom, top - 1, -1):\n                grid[r][left] = k; k += 1\n            left += 1\n    return grid\n\nprint(generate_spiral(3))   # [[1, 2, 3], [8, 9, 4], [7, 6, 5]]\n<\/code><\/pre>\n<h2>Diagonal order<\/h2>\n<p>All cells on the same anti-diagonal (running from top-right to bottom-left) share the same value of <code>row + col<\/code>. That gives you a clean way to enumerate them with no special cases: loop over the sum <code>s<\/code> from 0 to <code>rows + cols - 2<\/code>, and for each <code>s<\/code> collect the cells that are inside the grid.<\/p>\n<pre><code class=\"language-python\">def diagonals(matrix):\n    if not matrix or not matrix[0]:\n        return []\n    R, C = len(matrix), len(matrix[0])\n    out = []\n    for s in range(R + C - 1):\n        r = max(0, s - (C - 1))        # first row where col = s - r is still inside\n        diag = []\n        while r &lt; R and s - r &gt;= 0:\n            diag.append(matrix[r][s - r])\n            r += 1\n        out.append(diag)\n    return out\n\nprint(diagonals(m))\n# [[1], [2, 5], [3, 6, 9], [4, 7, 10], [8, 11], [12]]\n<\/code><\/pre>\n<p>For the other direction (top-left to bottom-right), cells share the same value of <code>col - row<\/code>, which ranges from <code>-(R - 1)<\/code> to <code>C - 1<\/code>:<\/p>\n<pre><code class=\"language-python\">def main_diagonals(matrix):\n    R, C = len(matrix), len(matrix[0])\n    return [[matrix[r][r + k] for r in range(R) if 0 &lt;= r + k &lt; C]\n            for k in range(-(R - 1), C)]\n\nprint(main_diagonals(m))\n# [[9], [5, 10], [1, 6, 11], [2, 7, 12], [3, 8], [4]]\n<\/code><\/pre>\n<p>Knowing &#8220;same diagonal means same <code>r + c<\/code> or same <code>c - r<\/code>&#8221; also solves problems that never mention traversal, such as placing queens in N-Queens (track used diagonals in a set) or grouping cells in &#8220;Diagonal Traverse&#8221; and &#8220;Sort the Matrix Diagonally&#8221;.<\/p>\n<h2>Zigzag order<\/h2>\n<p>Zigzag (the &#8220;Diagonal Traverse&#8221; problem) visits the same anti-diagonals but alternates direction: the first goes up-right, the next down-left, and so on. Reusing <code>diagonals<\/code>, reverse every other one. The first diagonal has one cell, so the parity does not matter for it, but the second must be read from the bottom up: <code>[5, 2]<\/code>.<\/p>\n<pre><code class=\"language-python\">def zigzag(matrix):\n    out = []\n    for s, diag in enumerate(diagonals(matrix)):\n        out.extend(diag if s % 2 else diag[::-1])\n    return out\n\nprint(zigzag(m))   # [1, 2, 5, 9, 6, 3, 4, 7, 10, 11, 8, 12]\n<\/code><\/pre>\n<p>Here <code>diagonals<\/code> lists each diagonal top-to-bottom, so odd-numbered ones are already in down-left order, and even-numbered ones are reversed to go up-right. This version uses O(rows + cols) extra memory per diagonal and is easy to reason about. If you are asked for O(1) extra space, simulate the walk instead:<\/p>\n<pre><code class=\"language-python\">def zigzag_walk(matrix):\n    if not matrix or not matrix[0]:\n        return []\n    R, C = len(matrix), len(matrix[0])\n    r = c = 0\n    up = True\n    out = []\n    for _ in range(R * C):\n        out.append(matrix[r][c])\n        if up:\n            if c == C - 1:\n                r += 1; up = False         # hit the right edge: step down, turn around\n            elif r == 0:\n                c += 1; up = False         # hit the top edge: step right, turn around\n            else:\n                r -= 1; c += 1\n        else:\n            if r == R - 1:\n                c += 1; up = True          # hit the bottom edge: step right\n            elif c == 0:\n                r += 1; up = True          # hit the left edge: step down\n            else:\n                r += 1; c -= 1\n    return out\n<\/code><\/pre>\n<p>The order of the edge checks matters. In the upward direction, check the right edge before the top edge, because in a corner such as the top-right cell you must go down, not right. The same applies in reverse for the bottom and left edges. Swapping these checks is the most common zigzag bug, and it only shows up on non-square matrices or specific corners. The two zigzag versions agree on 500 random matrices in my check.<\/p>\n<h2>Complexity<\/h2>\n<table>\n<thead>\n<tr>\n<th>Pattern<\/th>\n<th>Time<\/th>\n<th>Extra space (besides output)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Spiral (boundaries)<\/td>\n<td>O(R x C)<\/td>\n<td>O(1)<\/td>\n<\/tr>\n<tr>\n<td>Spiral (visited grid)<\/td>\n<td>O(R x C)<\/td>\n<td>O(R x C)<\/td>\n<\/tr>\n<tr>\n<td>Diagonals by r + c<\/td>\n<td>O(R x C)<\/td>\n<td>O(min(R, C)) per diagonal<\/td>\n<\/tr>\n<tr>\n<td>Zigzag walk<\/td>\n<td>O(R x C)<\/td>\n<td>O(1)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>You cannot beat O(R x C) when every cell must be visited.<\/p>\n<h2>Common mistakes<\/h2>\n<ul>\n<li><strong>Assuming a square matrix.<\/strong> Always test a 1xN, an Nx1 and a 3&#215;4. Most traversal bugs live there.<\/li>\n<li><strong>Mixing up rows and columns.<\/strong> <code>len(matrix)<\/code> is the number of rows, <code>len(matrix[0])<\/code> is the number of columns. Index as <code>matrix[row][col]<\/code>.<\/li>\n<li><strong>Forgetting the empty matrix.<\/strong> <code>[]<\/code> and <code>[[]]<\/code> are different, and <code>matrix[0]<\/code> fails on the first.<\/li>\n<li><strong>Skipping the collapse checks in spiral.<\/strong> Duplicates in the middle row or column of an odd-sized matrix are the symptom.<\/li>\n<li><strong>Counting by cells, not by structure.<\/strong> A loop that runs exactly <code>R * C<\/code> times, like the walker versions above, cannot terminate early or run forever, which makes it a safe frame for unfamiliar patterns.<\/li>\n<\/ul>\n<h2>Where these show up<\/h2>\n<p>Spiral order appears in interviews as &#8220;Spiral Matrix&#8221; and &#8220;Spiral Matrix II&#8221;, and the diagonal tricks cover &#8220;Diagonal Traverse&#8221;, &#8220;Sort the Matrix Diagonally&#8221; and &#8220;Toeplitz Matrix&#8221;. Once you can traverse in order, the next step is traversing with a purpose: finding paths through a grid. Our guides on <a href=\"https:\/\/algocademy.com\/blog\/strategies-for-solving-matrix-path-problems-a-comprehensive-guide\/\">matrix path problems<\/a> and <a href=\"https:\/\/algocademy.com\/blog\/efficient-algorithms-for-matrix-operations-mastering-the-fundamentals\/\">matrix operations<\/a> continue from here. For hands-on practice with grids, try the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=01-matrix&amp;solution=1\">01 Matrix exercise<\/a> and <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=unique-paths&amp;solution=1\">Unique Paths<\/a> on AlgoCademy.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>How to traverse a 2D matrix in spiral, diagonal and zigzag order, with tested Python code, the r+c trick, edge cases for non-square grids, and common mistakes.<\/p>\n","protected":false},"author":1,"featured_media":4590,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-4591","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4591"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=4591"}],"version-history":[{"count":1,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4591\/revisions"}],"predecessor-version":[{"id":8754,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4591\/revisions\/8754"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/4590"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=4591"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=4591"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=4591"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}