Rotate a square image counterclockwise
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
- Single pixel: A 1x1 matrix is already rotated. Return it as-is.
- 2x2 matrix: The simplest non-trivial case, useful for tracing.
- 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:
- Transpose: Swap every element at
(i, j)with the element at(j, i) - 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
-
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.
-
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.
-
Off-by-one in the flip: When reversing rows, index
imaps toimage[size - 1 - i], notimage[size - i]. Forgetting the-1causes anArrayIndexOutOfBoundsException. -
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:
-
Clarify the constraints: "Is the matrix always square?" and "Should I modify in-place or return a new copy?" Both matter for your approach.
-
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.
-
Name the sub-operations: Saying "I'll transpose and then flip the rows" communicates your plan clearly.
-
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.
-
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);
}
}