Rotate a square image counterclockwise

AS
Ackshaey Singh
Difficulty Hard
Time Complexity
O(m * n)
Space Complexity
O(m * n)
Matrix
Cisco Amazon Microsoft

You're sitting in a Microsoft interview when the interviewer pulls up a grid of pixel values on the screen. "Think of this as a grayscale image," they say. "Now rotate it 90 degrees counterclockwise and return a new copy." You grab your pen and start sketching. This problem, also known as "Rotate Image Counterclockwise" on other platforms, tests your ability to decompose a geometric transformation into simpler operations. It shows up regularly at companies like Amazon, Cisco, and Microsoft, and the trick behind it is surprisingly elegant once you see it.

TL;DR

Rotate a square matrix 90 degrees counterclockwise by first transposing it (swapping rows and columns), then flipping the result on its horizontal axis (reversing the order of rows). This runs in O(m * n) time and O(m * n) space because the solution builds a new matrix rather than modifying in-place. The decomposition into two sub-problems makes the code clean and easy to explain.

Prefer a different language? Jump to solutions in other languages.

Why This Problem Matters

Matrix rotation sits at the intersection of array manipulation and geometric reasoning. If you have already solved the clockwise variant (problem 195 on Firecode), this counterclockwise version reinforces the pattern and deepens your understanding of how transpose and flip operations compose. Interviewers love it because it tests whether you can break a complex transformation into manageable pieces.

Matrices: A Quick Refresher

A matrix is a 2D grid of values arranged in rows and columns. In this problem, the matrix represents a grayscale image where each cell holds an integer between 0 and 255. Since the image is square, the number of rows equals the number of columns.

Here is a 3x3 matrix we will use as our running example:

Loading visualization...

Each cell holds a pixel value. Row 0 is [1, 2, 3], row 1 is [4, 5, 6], and row 2 is [7, 8, 9].

Key terminology:

  • Row: A horizontal line of values (row 0 = [1, 2, 3])
  • Column: A vertical line of values (column 0 = [1, 4, 7])
  • Main diagonal: The cells where the row index equals the column index (1, 5, 9)
  • Transpose: Swapping rows and columns across the main diagonal

Understanding the Problem

Given: A square 2D array of integers (pixel values between 0 and 255) Task: Return a new matrix that is the original rotated 90 degrees counterclockwise Constraint: The image is square (same width and height) and has at least 1 pixel

Let's look at the 2x2 example from the problem statement:

Loading visualization...

After rotating 90 degrees counterclockwise:

Loading visualization...

Notice how the top-right 0 moved to the top-left, and the top-left 1 moved to the bottom-left. Every element shifts to its counterclockwise destination.

For our 3x3 example, the full rotation looks like this:

Loading visualization...

The element at position (0, 2) with value 3 moved to (0, 0). The element at (2, 0) with value 7 moved to (2, 2).

Edge Cases to Consider

  1. Single pixel: A 1x1 matrix is already rotated. Return it as-is.
  2. 2x2 matrix: The simplest non-trivial case, useful for tracing.
  3. Symmetric matrix: The transpose of a symmetric matrix is itself, but the flip still changes it.

Solution Approach

Rotating a matrix counterclockwise by 90 degrees might seem like a complicated coordinate mapping at first. The direct formula maps element (i, j) to position (n-1-j, i). You could apply that formula directly, but there is a cleaner way to think about it.

The Decomposition Insight

A counterclockwise rotation can be split into two simpler operations:

  1. Transpose: Swap every element at (i, j) with the element at (j, i)
  2. Flip on horizontal axis: Reverse the order of rows (row 0 becomes the last row, and the last row becomes row 0)

Let's trace this through our 3x3 example.

Loading visualization...

Step 1: Transpose

Swap elements across the main diagonal. Elements on the diagonal stay put.

Loading visualization...

Purple cells moved during the transpose. Value 2 at (0,1) swapped with 4 at (1,0). Value 3 at (0,2) swapped with 7 at (2,0). Value 6 at (1,2) swapped with 8 at (2,1).

Step 2: Flip on horizontal axis

Reverse the order of rows. Row 0 ([1, 4, 7]) swaps with row 2 ([3, 6, 9]), and the middle row stays.

Loading visualization...

The result [[3, 6, 9], [2, 5, 8], [1, 4, 7]] matches the expected counterclockwise rotation.

Why This Works

The transpose sends (i, j) to (j, i). The horizontal flip then sends (j, i) to (n-1-j, i). Composing the two: (i, j) ends up at (n-1-j, i), which is exactly the counterclockwise rotation formula.

Compare this with clockwise rotation, which transposes and then reverses each row. The only difference is the second step: reverse rows for clockwise, reverse row order for counterclockwise.

Implementation

Here is the Java solution using two helper methods:

import java.util.stream.IntStream;

public class Solution {
  public int[][] rotateCounterClockwise(int[][] image) {
    return flipOnHorizontalAxis(transpose(image));
  }

  private int[][] transpose(int[][] image) {
    int size = image.length;
    int[][] transposed = new int[size][size];
    for (int row = 0; row < size; row++) {
      for (int col = 0; col < size; col++) {
        transposed[row][col] = image[col][row];
      }
    }
    return transposed;
  }

  private int[][] flipOnHorizontalAxis(int[][] image) {
    return IntStream
             .range(0, image.length)
             .mapToObj(i -> image[image.length - 1 - i])
             .toArray(int[][]::new);
  }
}

Let's walk through each piece.

The Main Method

The main method is a single line: return flipOnHorizontalAxis(transpose(image)). This reads left to right as "take the transpose, then flip it." The decomposition keeps the code readable and each helper independently testable.

The Transpose Helper

The transpose method creates a new matrix of the same size. For each cell (row, col), it assigns image[col][row] to transposed[row][col]. This swaps rows and columns. The nested loops visit every cell once, giving O(n^2) time.

The Flip Helper

The flipOnHorizontalAxis method reverses the order of rows. Using Java's IntStream, it maps index i to image[image.length - 1 - i], picking rows from the end first. This builds a new array with the rows in reverse order.

You could also write this with a simple loop:

private int[][] flipOnHorizontalAxis(int[][] image) {
    int size = image.length;
    int[][] flipped = new int[size][];
    for (int i = 0; i < size; i++) {
        flipped[i] = image[size - 1 - i];
    }
    return flipped;
}

Both approaches produce the same result. The stream version is more concise; the loop version is easier to follow if you are not comfortable with Java streams.

Complexity Analysis

Time Complexity: O(m * n)

The transpose step iterates through all n * n cells once. The flip step processes each of the n rows once. Total work is proportional to the number of cells in the matrix.

Space Complexity: O(m * n)

The transpose creates a new n * n matrix. The flip creates another array of n row references (though the row arrays themselves are shared from the transposed matrix). The dominant cost is the new matrix from the transpose step.

An in-place approach using four-way cyclic swaps could achieve O(1) space, but the decomposed approach is easier to implement correctly during a timed interview.

Common Pitfalls

  1. Confusing clockwise and counterclockwise: Clockwise rotation transposes then reverses each row. Counterclockwise transposes then reverses the row order. Mixing these up produces the wrong rotation direction.

  2. Modifying the input matrix: The problem asks for a copy. If you transpose in-place and then flip, you have changed the original. Always create a new matrix for the transpose unless the problem explicitly asks for in-place modification.

  3. Off-by-one in the flip: When reversing rows, index i maps to image[size - 1 - i], not image[size - i]. Forgetting the -1 causes an ArrayIndexOutOfBoundsException.

  4. Assuming non-square matrices: This problem constrains the input to square matrices. If you ever extend this to non-square matrices, the transpose produces different dimensions and the approach needs adjustment.

Interview Tips

When solving this problem in an interview:

  1. Clarify the constraints: "Is the matrix always square?" and "Should I modify in-place or return a new copy?" Both matter for your approach.

  2. Sketch the transformation: Draw a small 3x3 grid, write in values, and show where each value ends up after counterclockwise rotation. This visual makes the decomposition obvious.

  3. Name the sub-operations: Saying "I'll transpose and then flip the rows" communicates your plan clearly.

  4. Mention the clockwise variant: If you have time, mention that clockwise rotation uses the same transpose but reverses each row instead. This demonstrates depth of understanding.

  5. Discuss trade-offs: Point out that the O(m * n) space approach is clean and correct, but an O(1) space approach exists using cyclic swaps. Interviewers appreciate candidates who acknowledge trade-offs even if they choose the simpler path.

Key Takeaways

  • Counterclockwise rotation decomposes into transpose (swap rows and columns) followed by horizontal flip (reverse row order). This two-step approach is easier to implement and debug than directly computing target coordinates.
  • The transpose maps (i, j) to (j, i), and the horizontal flip maps (j, i) to (n-1-j, i), producing the exact counterclockwise rotation formula (i, j) to (n-1-j, i).
  • Creating helper methods for transpose and flip keeps each function focused and testable. The main method becomes a single readable line.
  • This approach uses O(m * n) space for the new matrix. An in-place solution with cyclic four-way swaps saves space but adds complexity. In interviews, correctness and clarity often beat micro-optimization.
  • Always trace through a small example (2x2 or 3x3) before coding. Catching an off-by-one in the flip index is much cheaper on paper than in code.

Related Problems

Once you are comfortable with counterclockwise rotation, try these related matrix problems to build on the same skills:

  • Rotate a square image clockwise (the mirror of this problem, using transpose + row reversal)
  • Return the transpose of a square image (the first half of this solution, on its own)
  • Flip an image on its horizontal axis (the second half of this solution)
  • Zero out rows and columns (another 2D array manipulation exercise)

These problems are all available on Firecode, where over 50,000 engineers have prepared for technical interviews and landed roles at top tech companies. Practicing related problems back-to-back solidifies the transpose-and-flip pattern so it becomes second nature.

Solutions in Other Languages

Python

from typing import List


class Solution:
    def rotate_counter_clockwise(self, image: List[List[int]]) -> List[List[int]]:
        return self.flip_on_horizontal_axis(self.transpose(image))

    def transpose(self, image: List[List[int]]) -> List[List[int]]:
        size = len(image)
        transposed = [[0 for _ in range(size)] for _ in range(size)]
        for row in range(size):
            for col in range(size):
                transposed[row][col] = image[col][row]
        return transposed

    def flip_on_horizontal_axis(self, image: List[List[int]]) -> List[List[int]]:
        return [image[len(image) - 1 - index] for (index, _) in enumerate(image)]

JavaScript

class Solution {
  rotateCounterClockwise(image) {
    return this.flipOnHorizontalAxis(this.transpose(image));
  }

  transpose(image) {
    const size = image.length;
    const transposed = [...Array(size)].map(_ => Array(size));
    for (let row = 0; row < size; row++) {
      for (let col = 0; col < size; col++) {
        transposed[row][col] = image[col][row];
      }
    }
    return transposed;
  }

  flipOnHorizontalAxis(image) {
    return image.map((row, i) => image[image.length - 1 - i]);
  }
}

TypeScript

class Solution {
  rotateCounterClockwise(image: number[][]): number[][] {
    return this.flipOnHorizontalAxis(this.transpose(image));
  }

  transpose(image: number[][]): number[][] {
    const size = image.length;
    const transposed: number[][] = Array.from({ length: size }, () => new Array(size));
    for (let row = 0; row < size; row++) {
      for (let col = 0; col < size; col++) {
        transposed[row][col] = image[col][row];
      }
    }
    return transposed;
  }

  flipOnHorizontalAxis(image: number[][]): number[][] {
    return image.map((_, i) => image[image.length - 1 - i]);
  }
}

C++

#include <vector>

class Solution {
public:
  std::vector<std::vector<int>> rotateCounterClockwise(
      const std::vector<std::vector<int>> &image) {
    return flipOnHorizontalAxis(transpose(image));
  }

private:
  std::vector<std::vector<int>> transpose(
      const std::vector<std::vector<int>> &image) {
    int size = image.size();
    std::vector<std::vector<int>> transposed(size, std::vector<int>(size));
    for (int row = 0; row < size; row++) {
      for (int col = 0; col < size; col++) {
        transposed[row][col] = image[col][row];
      }
    }
    return transposed;
  }

  std::vector<std::vector<int>> flipOnHorizontalAxis(
      const std::vector<std::vector<int>> &image) {
    std::vector<std::vector<int>> flipped(image.rbegin(), image.rend());
    return flipped;
  }
};

Scala

class Solution {
  def rotateCounterClockwise(image: Array[Array[Int]]): Array[Array[Int]] = {
    flipOnHorizontalAxis(transpose(image))
  }

  private def transpose(image: Array[Array[Int]]): Array[Array[Int]] = {
    val size = image.length
    val transposed = Array.ofDim[Int](size, size)
    for (row <- 0 until size) {
      for (col <- 0 until size) {
        transposed(row)(col) = image(col)(row)
      }
    }
    transposed
  }

  private def flipOnHorizontalAxis(image: Array[Array[Int]]): Array[Array[Int]] = {
    image.indices.map(i => image(image.length - 1 - i)).toArray
  }
}

Kotlin

class Solution {
    fun rotateCounterClockwise(image: Array<IntArray>): Array<IntArray> {
        return flipOnHorizontalAxis(transpose(image))
    }

    private fun transpose(image: Array<IntArray>): Array<IntArray> {
        val size = image.size
        val transposed = Array(size) { IntArray(size) }
        for (row in 0 until size) {
            for (col in 0 until size) {
                transposed[row][col] = image[col][row]
            }
        }
        return transposed
    }

    private fun flipOnHorizontalAxis(image: Array<IntArray>): Array<IntArray> {
        return Array(image.size) { i -> image[image.size - 1 - i] }
    }
}

Rust

impl Solution {
    pub fn rotate_counter_clockwise(&self, image: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
        Self::flip_on_horizontal_axis(Self::transpose(&image))
    }

    fn transpose(image: &Vec<Vec<i32>>) -> Vec<Vec<i32>> {
        let size = image.len();
        let mut transposed = vec![vec![0; size]; size];
        for row in 0..size {
            for col in 0..size {
                transposed[row][col] = image[col][row];
            }
        }
        transposed
    }

    fn flip_on_horizontal_axis(image: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
        image.into_iter().rev().collect()
    }
}

Swift

class Solution {
    func rotateCounterClockwise(_ image: [[Int]]) -> [[Int]] {
        return flipOnHorizontalAxis(transpose(image))
    }

    private func transpose(_ image: [[Int]]) -> [[Int]] {
        let size = image.count
        var transposed = Array(repeating: Array(repeating: 0, count: size), count: size)
        for row in 0..<size {
            for col in 0..<size {
                transposed[row][col] = image[col][row]
            }
        }
        return transposed
    }

    private func flipOnHorizontalAxis(_ image: [[Int]]) -> [[Int]] {
        return (0..<image.count).map { image[image.count - 1 - $0] }
    }
}

Ruby

class Solution
  def rotate_counter_clockwise(image)
    flip_on_horizontal_axis(transpose(image))
  end

  def transpose(image)
    size = image.length
    transposed = Array.new(size) { Array.new(size, 0) }
    size.times do |row|
      size.times do |col|
        transposed[row][col] = image[col][row]
      end
    end
    transposed
  end

  def flip_on_horizontal_axis(image)
    image.reverse
  end
end

Dart

class Solution {
  List<List<int>> rotateCounterClockwise(List<List<int>> image) {
    return flipOnHorizontalAxis(transpose(image));
  }

  List<List<int>> transpose(List<List<int>> image) {
    int size = image.length;
    List<List<int>> transposed = List.generate(size, (_) => List.filled(size, 0));
    for (int row = 0; row < size; row++) {
      for (int col = 0; col < size; col++) {
        transposed[row][col] = image[col][row];
      }
    }
    return transposed;
  }

  List<List<int>> flipOnHorizontalAxis(List<List<int>> image) {
    return List.generate(image.length, (i) => image[image.length - 1 - i]);
  }
}

C#

using System.Linq;

public class Solution {
    public int[][] RotateCounterClockwise(int[][] image) {
        return FlipOnHorizontalAxis(Transpose(image));
    }

    private int[][] Transpose(int[][] image) {
        int size = image.Length;
        var transposed = new int[size][];
        for (int row = 0; row < size; row++) {
            transposed[row] = new int[size];
            for (int col = 0; col < size; col++) {
                transposed[row][col] = image[col][row];
            }
        }
        return transposed;
    }

    private int[][] FlipOnHorizontalAxis(int[][] image) {
        return Enumerable.Range(0, image.Length)
            .Select(i => image[image.Length - 1 - i])
            .ToArray();
    }
}

PHP

<?php

class Solution {
    public function rotateCounterClockwise(array $image): array {
        return $this->flipOnHorizontalAxis($this->transpose($image));
    }

    private function transpose(array $image): array {
        $size = count($image);
        $transposed = array_fill(0, $size, array_fill(0, $size, 0));
        for ($row = 0; $row < $size; $row++) {
            for ($col = 0; $col < $size; $col++) {
                $transposed[$row][$col] = $image[$col][$row];
            }
        }
        return $transposed;
    }

    private function flipOnHorizontalAxis(array $image): array {
        return array_reverse($image);
    }
}

Frequently Asked Questions

What is the transpose-then-flip approach for counterclockwise rotation?
First, transpose the matrix by swapping every element at position (i, j) with the element at (j, i). Then flip the matrix on its horizontal axis by reversing the order of rows. The combination of these two operations produces a 90-degree counterclockwise rotation. This approach creates a new matrix rather than modifying in-place.
How does counterclockwise rotation differ from clockwise rotation?
Clockwise rotation transposes the matrix and then reverses each row. Counterclockwise rotation transposes the matrix and then reverses the row order (flips vertically). The transpose step is the same, but the second step differs: row reversal for clockwise versus row-order reversal for counterclockwise.
What is the time complexity of rotating a matrix counterclockwise?
O(m * n) where m and n are the dimensions of the square matrix. The transpose step visits all m * n elements once, and the horizontal flip step also processes all m rows. Since the matrix is square (m = n), this simplifies to O(n^2). Every element must be placed in its new position.
What is the space complexity of this approach?
O(m * n) because the solution creates a new matrix of the same dimensions for the transposed result. The flip step can reuse the transposed matrix or create another copy depending on implementation. An in-place approach using four-way swaps could achieve O(1) space, but the decomposed approach is cleaner for interviews.
Why decompose the rotation into transpose and flip?
Breaking a complex geometric transformation into two simpler, well-understood operations makes the code easier to write, debug, and explain during an interview. Each sub-operation has a clear mathematical definition, and reusing helper methods for transpose and flip demonstrates good software engineering practices.
Can you rotate a non-square matrix 90 degrees?
Yes, but the output dimensions change. A 90-degree rotation of an m x n matrix produces an n x m matrix. The transpose-then-flip approach still works conceptually, but the transpose of a non-square matrix produces different dimensions, so you cannot do it in-place. This problem specifically constrains the input to square matrices.
What is the relationship between transpose and rotation?
A transpose reflects the matrix across its main diagonal, turning rows into columns. Combining a transpose with a row or column reversal produces a rotation. Transpose plus reverse-rows gives clockwise rotation. Transpose plus reverse-row-order gives counterclockwise rotation. These are fundamental operations in linear algebra.
How do you verify the rotation is correct?
For a counterclockwise rotation, element (i, j) should map to position (n-1-j, i) in the output. Pick several elements and verify they land in the correct position. For example, in a 3x3 matrix, element (0, 2) with value 3 should move to (0, 0). Tracing a few elements catches most index errors.
How often does matrix rotation appear in coding interviews?
Matrix rotation is a common interview question at companies like Amazon, Microsoft, and Cisco. It frequently appears as a warm-up or medium-difficulty problem. Interviewers use it to test candidates' ability to manipulate 2D arrays and think through coordinate transformations systematically.
What are real-world applications of matrix rotation?
Image rotation in photo editing software, screen orientation changes on mobile devices, game board transformations in puzzle games, and satellite imagery processing all rely on matrix rotation. Graphics engines use rotation matrices for 2D and 3D transformations, and machine learning pipelines use rotation as a data augmentation technique.