diff --git a/.templates/leetcode/json/container_with_most_water.json b/.templates/leetcode/json/container_with_most_water.json new file mode 100644 index 0000000..b974e9c --- /dev/null +++ b/.templates/leetcode/json/container_with_most_water.json @@ -0,0 +1,42 @@ +{ + "problem_name": "container_with_most_water", + "class_name": "ContainerWithMostWater", + "method_name": "max_area", + "problem_number": "11", + "problem_title": "Container With Most Water", + "difficulty": "Medium", + "topics": "Array, Two Pointers, Greedy", + "tags": ["grind-75"], + "problem_description": "You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).\n\nFind two lines that together with the x-axis form a container, such that the container contains the most water.\n\nReturn the maximum amount of water a container can store.\n\nNotice that you may not slant the container.", + "examples": [ + { + "input": "height = [1,8,6,2,5,4,8,3,7]", + "output": "49", + "explanation": "The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49." + }, + { "input": "height = [1,1]", "output": "1" } + ], + "constraints": "- n == height.length\n- 2 <= n <= 10^5\n- 0 <= height[i] <= 10^4", + "parameters": "height: list[int]", + "return_type": "int", + "dummy_return": "0", + "imports": "", + "test_cases": [ + { "args": [[1, 8, 6, 2, 5, 4, 8, 3, 7]], "expected": 49 }, + { "args": [[1, 1]], "expected": 1 }, + { "args": [[1, 2, 1]], "expected": 2 }, + { "args": [[2, 3, 4, 5, 18, 17, 6]], "expected": 17 }, + { "args": [[1, 2, 4, 3]], "expected": 4 } + ], + "param_names": "height, expected", + "param_names_with_types": "height: list[int], expected: int", + "input_description": "height={height}", + "input_params": "height", + "expected_param": "expected", + "method_args": "height", + "test_setup": "", + "test_logging": "", + "assertion_code": "assert result == expected", + "test_input_setup": "# Example test case\nheight = [1, 8, 6, 2, 5, 4, 8, 3, 7]", + "expected_output_setup": "expected = 49" +} diff --git a/.templates/leetcode/json/spiral_matrix.json b/.templates/leetcode/json/spiral_matrix.json new file mode 100644 index 0000000..d4b7952 --- /dev/null +++ b/.templates/leetcode/json/spiral_matrix.json @@ -0,0 +1,66 @@ +{ + "problem_name": "spiral_matrix", + "class_name": "SpiralMatrix", + "method_name": "spiral_order", + "problem_number": "54", + "problem_title": "Spiral Matrix", + "difficulty": "Medium", + "topics": "Array, Matrix, Simulation", + "tags": ["grind-75"], + "problem_description": "Given an m x n matrix, return all elements of the matrix in spiral order.", + "examples": [ + { "input": "matrix = [[1,2,3],[4,5,6],[7,8,9]]", "output": "[1,2,3,6,9,8,7,4,5]" }, + { + "input": "matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]", + "output": "[1,2,3,4,8,12,11,10,9,5,6,7]" + } + ], + "constraints": "- m == matrix.length\n- n == matrix[i].length\n- 1 <= m, n <= 10\n- -100 <= matrix[i][j] <= 100", + "parameters": "matrix: list[list[int]]", + "return_type": "list[int]", + "dummy_return": "[]", + "imports": "", + "test_cases": [ + { + "args": [ + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ] + ], + "expected": [1, 2, 3, 6, 9, 8, 7, 4, 5] + }, + { + "args": [ + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12] + ] + ], + "expected": [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7] + }, + { "args": [[[1]]], "expected": [1] }, + { + "args": [ + [ + [1, 2], + [3, 4] + ] + ], + "expected": [1, 2, 4, 3] + } + ], + "param_names": "matrix, expected", + "param_names_with_types": "matrix: list[list[int]], expected: list[int]", + "input_description": "matrix={matrix}", + "input_params": "matrix", + "expected_param": "expected", + "method_args": "matrix", + "test_setup": "", + "test_logging": "", + "assertion_code": "assert result == expected", + "test_input_setup": "# Example test case\nmatrix = [[1,2,3],[4,5,6],[7,8,9]]", + "expected_output_setup": "expected = [1,2,3,6,9,8,7,4,5]" +} diff --git a/Makefile b/Makefile index a3a1721..3bc7bbb 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= insert_interval +PROBLEM ?= spiral_matrix FORCE ?= 0 sync_submodules: diff --git a/README.md b/README.md index 00249b4..ecacdf2 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,8 @@ Premium LeetCode practice repository with Python solutions, algorithm templates, ## 📋 Prerequisites -- Python 3.9+ -- make -- git -- Optional: Graphviz for tree visualizations +- Python 3.13+ +- make, git, Graphviz, poetry ## 🛠️ Installation diff --git a/leetcode/container_with_most_water/README.md b/leetcode/container_with_most_water/README.md new file mode 100644 index 0000000..be54e14 --- /dev/null +++ b/leetcode/container_with_most_water/README.md @@ -0,0 +1,40 @@ +# 11. Container With Most Water + +**Difficulty:** Medium +**Topics:** Array, Two Pointers, Greedy +**Tags:** grind-75 +**LeetCode:** [Problem 11](https://leetcode.com/problems/container-with-most-water/description/) + +## Problem Description + +You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]). + +Find two lines that together with the x-axis form a container, such that the container contains the most water. + +Return the maximum amount of water a container can store. + +Notice that you may not slant the container. + +## Examples + +### Example 1: + +![Example1](example1.png) + +``` +Input: height = [1,8,6,2,5,4,8,3,7] +Output: 49 +``` + +### Example 2: + +``` +Input: height = [1,1] +Output: 1 +``` + +## Constraints + +- n == height.length +- 2 <= n <= 10^5 +- 0 <= height[i] <= 10^4 diff --git a/leetcode/container_with_most_water/__init__.py b/leetcode/container_with_most_water/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/container_with_most_water/example1.png b/leetcode/container_with_most_water/example1.png new file mode 100644 index 0000000..d565b18 Binary files /dev/null and b/leetcode/container_with_most_water/example1.png differ diff --git a/leetcode/container_with_most_water/playground.ipynb b/leetcode/container_with_most_water/playground.ipynb new file mode 100644 index 0000000..1f25e22 --- /dev/null +++ b/leetcode/container_with_most_water/playground.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "height = [1, 8, 6, 2, 5, 4, 8, 3, 7]\n", + "expected = 49" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "49" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().max_area(height)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result == expected" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/container_with_most_water/solution.py b/leetcode/container_with_most_water/solution.py new file mode 100644 index 0000000..34a1025 --- /dev/null +++ b/leetcode/container_with_most_water/solution.py @@ -0,0 +1,17 @@ +class Solution: + # Time: O(n) + # Space: O(1) + def max_area(self, height: list[int]) -> int: + left = 0 + right = len(height) - 1 + max_area_so_far = 0 + + while left < right: + area = min(height[left], height[right]) * (right - left) + max_area_so_far = max(area, max_area_so_far) + if height[right] > height[left]: + left += 1 + else: + right -= 1 + + return max_area_so_far diff --git a/leetcode/container_with_most_water/tests.py b/leetcode/container_with_most_water/tests.py new file mode 100644 index 0000000..751ac27 --- /dev/null +++ b/leetcode/container_with_most_water/tests.py @@ -0,0 +1,28 @@ +import pytest +from loguru import logger + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestContainerWithMostWater: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "height, expected", + [ + ([1, 8, 6, 2, 5, 4, 8, 3, 7], 49), + ([1, 1], 1), + ([1, 2, 1], 2), + ([2, 3, 4, 5, 18, 17, 6], 17), + ([1, 2, 4, 3], 4), + ], + ) + @logged_test + def test_max_area(self, height: list[int], expected: int): + logger.info(f"Testing with height={height}") + result = self.solution.max_area(height) + logger.success(f"Got result: {result}") + assert result == expected diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb index 3ba0c7d..62707c1 100644 --- a/leetcode/invert_binary_tree/playground.ipynb +++ b/leetcode/invert_binary_tree/playground.ipynb @@ -17,11 +17,115 @@ "execution_count": 2, "id": "setup", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "0->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "4->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "4->6\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([4, 2, 7, 1, 3, 6, 9])" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Example test case\n", "root = TreeNode.from_list([4, 2, 7, 1, 3, 6, 9])\n", - "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])" + "expected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])\n", + "root" ] }, { @@ -163,7 +267,7 @@ "file_extension": ".py", "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python3", + "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.7" } diff --git a/leetcode/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py index 5df0b83..3a6481e 100644 --- a/leetcode/invert_binary_tree/solution.py +++ b/leetcode/invert_binary_tree/solution.py @@ -1,12 +1,59 @@ +from collections import deque + from leetcode_py.tree_node import TreeNode +# Note: "Fringe" is the general CS term for the data structure holding nodes to be explored. +# Stack (LIFO) → DFS, Queue (FIFO) → BFS, Priority Queue → A*/Best-first search + class Solution: + # DFS recursive # Time: O(n) - # Space: O(h) + # Space: O(h) where h is height of tree def invert_tree(self, root: TreeNode | None) -> TreeNode | None: if not root: return None root.left, root.right = self.invert_tree(root.right), self.invert_tree(root.left) return root + + +class SolutionDFS: + # DFS iterative + # Time: O(n) + # Space: O(h) where h is height of tree + def invert_tree(self, root: TreeNode | None) -> TreeNode | None: + if not root: + return None + + stack: list[TreeNode | None] = [root] + while stack: + node = stack.pop() + if node is None: + continue + node.left, node.right = node.right, node.left + + stack.append(node.left) + stack.append(node.right) + + return root + + +class SolutionBFS: + # Time: O(n) + # Space: O(w) where w is maximum width of tree + def invert_tree(self, root: TreeNode | None) -> TreeNode | None: + if not root: + return None + + queue: deque[TreeNode | None] = deque([root]) + while queue: + node = queue.popleft() + if node is None: + continue + node.left, node.right = node.right, node.left + + queue.append(node.left) + queue.append(node.right) + + return root diff --git a/leetcode/invert_binary_tree/tests.py b/leetcode/invert_binary_tree/tests.py index 4d2ba0a..71c84d6 100644 --- a/leetcode/invert_binary_tree/tests.py +++ b/leetcode/invert_binary_tree/tests.py @@ -4,26 +4,34 @@ from leetcode_py.test_utils import logged_test from leetcode_py.tree_node import TreeNode -from .solution import Solution +from .solution import Solution, SolutionBFS, SolutionDFS +test_cases = [ + ([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), + ([2, 1, 3], [2, 3, 1]), + ([], []), + ([1], [1]), + ([1, 2], [1, None, 2]), + ([1, None, 2], [1, 2]), + ([1, 2, 3, 4, 5], [1, 3, 2, None, None, 5, 4]), + ([1, 2, 3, None, None, 4, 5], [1, 3, 2, 5, 4]), +] -class TestInvertBinaryTree: - def setup_method(self): - self.solution = Solution() - @pytest.mark.parametrize( - "root_list, expected_list", - [ - ([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), - ([2, 1, 3], [2, 3, 1]), - ([], []), - ], - ) +class TestInvertBinaryTree: + @pytest.mark.parametrize("solution_class", [Solution, SolutionDFS, SolutionBFS]) + @pytest.mark.parametrize("root_list, expected_list", test_cases) @logged_test - def test_invert_tree(self, root_list: list[int | None], expected_list: list[int | None]): - logger.info(f"Testing with root_list={root_list}") + def test_invert_tree( + self, + solution_class: type[Solution | SolutionDFS | SolutionBFS], + root_list: list[int | None], + expected_list: list[int | None], + ): + solution = solution_class() + logger.info(f"Testing {solution_class.__name__} with root_list={root_list}") root = TreeNode.from_list(root_list) expected = TreeNode.from_list(expected_list) - result = self.solution.invert_tree(root) + result = solution.invert_tree(root) logger.success(f"Got result: {result.to_list() if result else []}") assert result == expected diff --git a/leetcode/reverse_linked_list_ii/solution.py b/leetcode/reverse_linked_list_ii/solution.py index 2b7e88b..0830bd0 100644 --- a/leetcode/reverse_linked_list_ii/solution.py +++ b/leetcode/reverse_linked_list_ii/solution.py @@ -17,14 +17,36 @@ def reverse_between(self, head: ListNode | None, left: int, right: int) -> ListN assert prev.next prev = prev.next - # Reverse from left to right + # Reverse from left to right using iterative approach + # Example: [1,2,3,4,5] left=2, right=4 -> [1,4,3,2,5] + # + # Initial: prev curr + # ↓ ↓ + # 1 -> 2 -> 3 -> 4 -> 5 + # assert prev.next - curr = prev.next + curr = prev.next # First node to be reversed (will become last after reversal) + + # Reverse by moving nodes one by one to the front of the section for _ in range(right - left): assert curr.next - next_node = curr.next + next_node = curr.next # Node to move to front + # + # prev curr next_node + # ↓ ↓ ↓ + # 1 -> 2 -> 3 -> 4 -> 5 + # curr.next = next_node.next + # 1 -> 2 -----> 4 -> 5 + # 3 ↗ + # next_node.next = prev.next + # 1 -> 2 -----> 4 -> 5 + # 3 ↗ + # prev.next = next_node + # 1 -> 3 -> 2 -> 4 -> 5 + # prev ↑ curr + # next_node return dummy.next diff --git a/leetcode/reverse_linked_list_ii/tests.py b/leetcode/reverse_linked_list_ii/tests.py index 23eb3b7..31c2e8b 100644 --- a/leetcode/reverse_linked_list_ii/tests.py +++ b/leetcode/reverse_linked_list_ii/tests.py @@ -17,6 +17,12 @@ def setup_method(self): ([1, 2, 3, 4, 5], 2, 4, [1, 4, 3, 2, 5]), ([5], 1, 1, [5]), ([1, 2, 3], 1, 3, [3, 2, 1]), + ([1, 2], 1, 2, [2, 1]), + ([7, 3, 9, 2, 8], 1, 5, [8, 2, 9, 3, 7]), + ([4, 6, 1, 9, 3], 3, 3, [4, 6, 1, 9, 3]), + ([2, 8, 5, 1, 7, 4], 2, 5, [2, 7, 1, 5, 8, 4]), + ([9, 5, 2, 6], 1, 1, [9, 5, 2, 6]), + ([3, 7, 1, 8], 4, 4, [3, 7, 1, 8]), ], ) @logged_test diff --git a/leetcode/spiral_matrix/README.md b/leetcode/spiral_matrix/README.md new file mode 100644 index 0000000..65ecc16 --- /dev/null +++ b/leetcode/spiral_matrix/README.md @@ -0,0 +1,45 @@ +# 54. Spiral Matrix + +**Difficulty:** Medium +**Topics:** Array, Matrix, Simulation +**Tags:** grind-75 +**LeetCode:** [Problem 54](https://leetcode.com/problems/spiral-matrix/description/) + +## Problem Description + +Given an m x n matrix, return all elements of the matrix in spiral order. + +## Examples + +### Example 1: + +``` +1 → 2 → 3 + ↓ +4 → 5 6 +↑ ↓ +7 ← 8 ← 9 + +Input: matrix = [[1,2,3],[4,5,6],[7,8,9]] +Output: [1,2,3,6,9,8,7,4,5] +``` + +### Example 2: + +``` +1 → 2 → 3 → 4 + ↓ +5 → 6 → 7 8 +↑ ↓ +9 ← 10← 11← 12 + +Input: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] +Output: [1,2,3,4,8,12,11,10,9,5,6,7] +``` + +## Constraints + +- m == matrix.length +- n == matrix[i].length +- 1 <= m, n <= 10 +- -100 <= matrix[i][j] <= 100 diff --git a/leetcode/spiral_matrix/__init__.py b/leetcode/spiral_matrix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/spiral_matrix/playground.ipynb b/leetcode/spiral_matrix/playground.ipynb new file mode 100644 index 0000000..bbf61a8 --- /dev/null +++ b/leetcode/spiral_matrix/playground.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]\n", + "expected = [1, 2, 3, 6, 9, 8, 7, 4, 5]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 6, 9, 8, 7, 4, 5]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().spiral_order(matrix)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result == expected" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "leetcode-py-py3.13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/spiral_matrix/solution.py b/leetcode/spiral_matrix/solution.py new file mode 100644 index 0000000..65e1113 --- /dev/null +++ b/leetcode/spiral_matrix/solution.py @@ -0,0 +1,41 @@ +class Solution: + # Time: O(m*n) + # Space: O(1) + def spiral_order(self, matrix: list[list[int]]) -> list[int]: + if not matrix or not matrix[0]: + return [] + + # Check if all rows have same length + cols = len(matrix[0]) + for row in matrix: + if len(row) != cols: + raise ValueError("Invalid matrix: all rows must have same length") + + result = [] + top, bottom = 0, len(matrix) - 1 + left, right = 0, cols - 1 + + while top <= bottom and left <= right: + # Right + for c in range(left, right + 1): + result.append(matrix[top][c]) + top += 1 + + # Down + for r in range(top, bottom + 1): + result.append(matrix[r][right]) + right -= 1 + + # Left (if still valid row) + if top <= bottom: + for c in range(right, left - 1, -1): + result.append(matrix[bottom][c]) + bottom -= 1 + + # Up (if still valid column) + if left <= right: + for r in range(bottom, top - 1, -1): + result.append(matrix[r][left]) + left += 1 + + return result diff --git a/leetcode/spiral_matrix/tests.py b/leetcode/spiral_matrix/tests.py new file mode 100644 index 0000000..72f6660 --- /dev/null +++ b/leetcode/spiral_matrix/tests.py @@ -0,0 +1,53 @@ +import pytest +from loguru import logger + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestSpiralMatrix: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "matrix, expected", + [ + ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], [1, 2, 3, 6, 9, 8, 7, 4, 5]), + ([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]), + ([[1]], [1]), + ([[1, 2], [3, 4]], [1, 2, 4, 3]), + ([[1, 2, 3]], [1, 2, 3]), + ([[1], [2], [3]], [1, 2, 3]), + ([[1, 2], [3, 4], [5, 6]], [1, 2, 4, 6, 5, 3]), + ([[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5]), + ([[1], [2], [3], [4], [5]], [1, 2, 3, 4, 5]), + ([[1, 2, 3, 4], [5, 6, 7, 8]], [1, 2, 3, 4, 8, 7, 6, 5]), + ([], []), + ([[]], []), + ], + ) + @logged_test + def test_spiral_order(self, matrix: list[list[int]], expected: list[int]): + logger.info(f"Testing with matrix={matrix}") + result = self.solution.spiral_order(matrix) + logger.success(f"Got result: {result}") + assert result == expected + + +class TestSpiralMatrixInvalid: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "matrix", + [ + [[1, 2, 3], [4, 5], [6, 7, 8]], + [[1], [2, 3], [4, 5, 6]], + [[1, 2], [3, 4, 5]], + [[1, 2, 3, 4], [5, 6]], + ], + ) + def test_invalid_matrix(self, matrix: list[list[int]]): + with pytest.raises(ValueError, match="Invalid matrix: all rows must have same length"): + self.solution.spiral_order(matrix)