From 4d8140094fd62014836f81283fb398718a1eaba3 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 11:53:05 +0700 Subject: [PATCH 01/26] feat: add lowest_common_ancestor_of_a_binary_tree --- ...mmon_ancestor_of_a_binary_search_tree.json | 11 +- ...west_common_ancestor_of_a_binary_tree.json | 48 ++++ Makefile | 2 +- .../playground.ipynb | 166 ++++++++++++- .../tests.py | 14 +- .../README.md | 50 ++++ .../__init__.py | 0 .../playground.ipynb | 222 ++++++++++++++++++ .../solution.py | 21 ++ .../tests.py | 32 +++ leetcode_py/data_structures/tree_node.py | 12 + poetry.lock | 52 ++-- 12 files changed, 575 insertions(+), 55 deletions(-) create mode 100644 .templates/leetcode/json/lowest_common_ancestor_of_a_binary_tree.json create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_tree/README.md create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_tree/__init__.py create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_tree/playground.ipynb create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_tree/solution.py create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py diff --git a/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json b/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json index eb71d4b..f697b50 100644 --- a/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json +++ b/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json @@ -30,12 +30,7 @@ "test_imports": "import pytest\nfrom leetcode_py import TreeNode\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", "test_class_name": "LowestCommonAncestorOfABinarySearchTree", "test_helper_methods": [ - { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }, - { - "name": "_find_node", - "parameters": "root: TreeNode[int], val: int", - "body": "if not root:\n return None\nif root.val == val:\n return root\nleft = self._find_node(root.left, val)\nif left:\n return left\nreturn self._find_node(root.right, val)" - } + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } ], "test_methods": [ { @@ -43,11 +38,11 @@ "parametrize": "root_list, p_val, q_val, expected_val", "parametrize_typed": "root_list: list[int | None], p_val: int, q_val: int, expected_val: int", "test_cases": "[([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5], 2, 8, 6), ([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5], 2, 4, 2), ([2, 1], 2, 1, 2), ([2, 1], 1, 2, 2), ([6, 2, 8, 0, 4, 7, 9], 0, 4, 2), ([6, 2, 8, 0, 4, 7, 9], 7, 9, 8)]", - "body": "root = TreeNode[int].from_list(root_list)\nassert root is not None\np = self._find_node(root, p_val)\nq = self._find_node(root, q_val)\nassert p is not None and q is not None\nresult = self.solution.lowest_common_ancestor(root, p, q)\nassert result is not None\nassert result.val == expected_val" + "body": "root = TreeNode[int].from_list(root_list)\nassert root is not None\np = root.find_node(p_val)\nq = root.find_node(q_val)\nassert p is not None and q is not None\nresult = self.solution.lowest_common_ancestor(root, p, q)\nassert result is not None\nassert result.val == expected_val" } ], "playground_imports": "from leetcode_py import TreeNode\nfrom solution import Solution", "playground_test_case": "# Example test case\nroot_list = [6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]\np_val = 2\nq_val = 8\nexpected_val = 6", - "playground_execution": "root = TreeNode[int].from_list(root_list)\np = find_node(root, p_val)\nq = find_node(root, q_val)\nresult = Solution().lowest_common_ancestor(root, p, q)\nresult.val if result else None", + "playground_execution": "root = TreeNode[int].from_list(root_list)\nassert root is not None\np = root.find_node(p_val)\nq = root.find_node(q_val)\nassert p is not None and q is not None\nresult = Solution().lowest_common_ancestor(root, p, q)\nresult.val if result else None", "playground_assertion": "assert result and result.val == expected_val" } diff --git a/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_tree.json b/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_tree.json new file mode 100644 index 0000000..2e19705 --- /dev/null +++ b/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_tree.json @@ -0,0 +1,48 @@ +{ + "problem_name": "lowest_common_ancestor_of_a_binary_tree", + "solution_class_name": "Solution", + "problem_number": "236", + "problem_title": "Lowest Common Ancestor of a Binary Tree", + "difficulty": "Medium", + "topics": "Tree, Depth-First Search, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.\n\nAccording to the definition of LCA on Wikipedia: \"The lowest common ancestor is defined between two nodes `p` and `q` as the lowest node in `T` that has both `p` and `q` as descendants (where we allow **a node to be a descendant of itself**).\"", + "readme_examples": [ + { + "content": "\"\"\n\n```\nInput: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1\nOutput: 3\nExplanation: The LCA of nodes 5 and 1 is 3.\n```" + }, + { + "content": "\"\"\n\n```\nInput: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4\nOutput: 5\nExplanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.\n```" + }, + { "content": "```\nInput: root = [1,2], p = 1, q = 2\nOutput: 1\n```" } + ], + "readme_constraints": "- The number of nodes in the tree is in the range [2, 10^5].\n- -10^9 <= Node.val <= 10^9\n- All Node.val are unique.\n- p != q\n- p and q will exist in the tree.", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "lowest_common_ancestor", + "parameters": "root: TreeNode, p: TreeNode, q: TreeNode", + "return_type": "TreeNode", + "dummy_return": "root" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Solution", + "test_class_name": "LowestCommonAncestorOfABinaryTree", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_lowest_common_ancestor", + "parametrize": "root_list, p_val, q_val, expected_val", + "parametrize_typed": "root_list: list[int | None], p_val: int, q_val: int, expected_val: int", + "test_cases": "[([3,5,1,6,2,0,8,None,None,7,4], 5, 1, 3), ([3,5,1,6,2,0,8,None,None,7,4], 5, 4, 5), ([1,2], 1, 2, 1)]", + "body": "root = TreeNode.from_list(root_list)\nassert root is not None\np = root.find_node(p_val)\nq = root.find_node(q_val)\nassert p is not None and q is not None\nresult = self.solution.lowest_common_ancestor(root, p, q)\nassert result is not None\nassert result.val == expected_val" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode", + "playground_test_case": "# Example test case\nroot_list = [3,5,1,6,2,0,8,None,None,7,4]\nroot = TreeNode.from_list(root_list)\nassert root is not None\np = root.find_node(5)\nq = root.find_node(1)\nexpected_val = 3", + "playground_execution": "result = Solution().lowest_common_ancestor(root, p, q)\nresult.val", + "playground_assertion": "assert result.val == expected_val" +} diff --git a/Makefile b/Makefile index 1bd60aa..770e889 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= product_of_array_except_self +PROBLEM ?= lowest_common_ancestor_of_a_binary_search_tree FORCE ?= 0 COMMA := , diff --git a/leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb index 763d0ea..89e233e 100644 --- a/leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb +++ b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "setup", "metadata": {}, "outputs": [], @@ -28,21 +28,171 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "execute", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "root = TreeNode[int].from_list(root_list)\n", - "p = find_node(root, p_val)\n", - "q = find_node(root, q_val)\n", + "assert root is not None\n", + "p = root.find_node(p_val)\n", + "q = root.find_node(q_val)\n", + "assert p is not None and q is not None\n", "result = Solution().lowest_common_ancestor(root, p, q)\n", "result.val if result else None" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, + "id": "d84494ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "8\n", + "\n", + "\n", + "\n", + "0->6\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "0\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "3->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "3->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "6->7\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "6->8\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root" + ] + }, + { + "cell_type": "code", + "execution_count": 4, "id": "test", "metadata": {}, "outputs": [], @@ -65,7 +215,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/lowest_common_ancestor_of_a_binary_search_tree/tests.py b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/tests.py index 33767fe..b28db6c 100644 --- a/leetcode/lowest_common_ancestor_of_a_binary_search_tree/tests.py +++ b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/tests.py @@ -10,16 +10,6 @@ class TestLowestCommonAncestorOfABinarySearchTree: def setup_method(self): self.solution = Solution() - def _find_node(self, root: TreeNode[int] | None, val: int): - if not root: - return None - if root.val == val: - return root - left = self._find_node(root.left, val) - if left: - return left - return self._find_node(root.right, val) - @pytest.mark.parametrize( "root_list, p_val, q_val, expected_val", [ @@ -37,8 +27,8 @@ def test_lowest_common_ancestor( ): root = TreeNode[int].from_list(root_list) assert root is not None - p = self._find_node(root, p_val) - q = self._find_node(root, q_val) + p = root.find_node(p_val) + q = root.find_node(q_val) assert p is not None and q is not None result = self.solution.lowest_common_ancestor(root, p, q) assert result is not None diff --git a/leetcode/lowest_common_ancestor_of_a_binary_tree/README.md b/leetcode/lowest_common_ancestor_of_a_binary_tree/README.md new file mode 100644 index 0000000..1c562a6 --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_tree/README.md @@ -0,0 +1,50 @@ +# Lowest Common Ancestor of a Binary Tree + +**Difficulty:** Medium +**Topics:** Tree, Depth-First Search, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 236](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) + +## Problem Description + +Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. + +According to the definition of LCA on Wikipedia: "The lowest common ancestor is defined between two nodes `p` and `q` as the lowest node in `T` that has both `p` and `q` as descendants (where we allow **a node to be a descendant of itself**)." + +## Examples + +### Example 1: + + + +``` +Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 +Output: 3 +Explanation: The LCA of nodes 5 and 1 is 3. +``` + +### Example 2: + + + +``` +Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 +Output: 5 +Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition. +``` + +### Example 3: + +``` +Input: root = [1,2], p = 1, q = 2 +Output: 1 +``` + +## Constraints + +- The number of nodes in the tree is in the range [2, 10^5]. +- -10^9 <= Node.val <= 10^9 +- All Node.val are unique. +- p != q +- p and q will exist in the tree. diff --git a/leetcode/lowest_common_ancestor_of_a_binary_tree/__init__.py b/leetcode/lowest_common_ancestor_of_a_binary_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/lowest_common_ancestor_of_a_binary_tree/playground.ipynb b/leetcode/lowest_common_ancestor_of_a_binary_tree/playground.ipynb new file mode 100644 index 0000000..6594efb --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_tree/playground.ipynb @@ -0,0 +1,222 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root_list = [3, 5, 1, 6, 2, 0, 8, None, None, 7, 4]\n", + "root = TreeNode.from_list(root_list)\n", + "assert root is not None\n", + "p = root.find_node(5)\n", + "q = root.find_node(1)\n", + "expected_val = 3" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6ad16444", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "0->6\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "6\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "3->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "3->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "0\n", + "\n", + "\n", + "\n", + "6->7\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "8\n", + "\n", + "\n", + "\n", + "6->8\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().lowest_common_ancestor(root, p, q)\n", + "result.val" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result.val == expected_val" + ] + } + ], + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/lowest_common_ancestor_of_a_binary_tree/solution.py b/leetcode/lowest_common_ancestor_of_a_binary_tree/solution.py new file mode 100644 index 0000000..33f138a --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_tree/solution.py @@ -0,0 +1,21 @@ +from leetcode_py import TreeNode + + +class Solution: + # Time: O(n) + # Space: O(h) + def lowest_common_ancestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: + result = self._lca(root, p, q) + assert result is not None + return result + + def _lca(self, root: TreeNode | None, p: TreeNode, q: TreeNode) -> TreeNode | None: + if not root or root == p or root == q: + return root + + left = self._lca(root.left, p, q) + right = self._lca(root.right, p, q) + + if left and right: + return root + return left or right diff --git a/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py b/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py new file mode 100644 index 0000000..c30c332 --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py @@ -0,0 +1,32 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestLowestCommonAncestorOfABinaryTree: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "root_list, p_val, q_val, expected_val", + [ + ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 5, 1, 3), + ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 5, 4, 5), + ([1, 2], 1, 2, 1), + ], + ) + @logged_test + def test_lowest_common_ancestor( + self, root_list: list[int | None], p_val: int, q_val: int, expected_val: int + ): + root = TreeNode.from_list(root_list) + assert root is not None + p = root.find_node(p_val) + q = root.find_node(q_val) + assert p is not None and q is not None + result = self.solution.lowest_common_ancestor(root, p, q) + assert result is not None + assert result.val == expected_val diff --git a/leetcode_py/data_structures/tree_node.py b/leetcode_py/data_structures/tree_node.py index ca96921..a716e9b 100644 --- a/leetcode_py/data_structures/tree_node.py +++ b/leetcode_py/data_structures/tree_node.py @@ -109,5 +109,17 @@ def __eq__(self, other: object) -> bool: return False return self.to_list() == other.to_list() + def find_node(self, val: T) -> "TreeNode[T] | None": + """Find node with given value.""" + if self.val == val: + return self + if self.left: + left_result = self.left.find_node(val) + if left_result: + return left_result + if self.right: + return self.right.find_node(val) + return None + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.to_list()})" diff --git a/poetry.lock b/poetry.lock index 2ccaad1..3cc5f73 100644 --- a/poetry.lock +++ b/poetry.lock @@ -570,14 +570,14 @@ files = [ [[package]] name = "executing" -version = "2.2.0" +version = "2.2.1" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, - {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, + {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, + {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, ] [package.extras] @@ -1308,14 +1308,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] @@ -1623,31 +1623,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.12.11" +version = "0.12.12" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065"}, - {file = "ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93"}, - {file = "ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8"}, - {file = "ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f"}, - {file = "ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000"}, - {file = "ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2"}, - {file = "ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39"}, - {file = "ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9"}, - {file = "ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3"}, - {file = "ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd"}, - {file = "ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea"}, - {file = "ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d"}, + {file = "ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc"}, + {file = "ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727"}, + {file = "ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45"}, + {file = "ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489"}, + {file = "ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee"}, + {file = "ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1"}, + {file = "ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d"}, + {file = "ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093"}, + {file = "ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6"}, ] [[package]] From 32e2f2c2ea1f5344a703204767067ae06c3fe261 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 12:30:07 +0700 Subject: [PATCH 02/26] feat: update dict tree --- .../leetcode/json/implement_trie_prefix_tree.json | 9 +++++++-- leetcode/implement_trie_prefix_tree/playground.ipynb | 6 +++--- leetcode/implement_trie_prefix_tree/solution.py | 8 +++----- .../lowest_common_ancestor_of_a_binary_tree/tests.py | 8 ++++++++ leetcode_py/data_structures/__init__.py | 4 ++-- leetcode_py/data_structures/dict_tree.py | 11 +++++++---- 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.templates/leetcode/json/implement_trie_prefix_tree.json b/.templates/leetcode/json/implement_trie_prefix_tree.json index f6cf7eb..90ed349 100644 --- a/.templates/leetcode/json/implement_trie_prefix_tree.json +++ b/.templates/leetcode/json/implement_trie_prefix_tree.json @@ -14,9 +14,14 @@ ], "readme_constraints": "- `1 <= word.length, prefix.length <= 2000`\n- `word` and `prefix` consist only of lowercase English letters.\n- At most `3 * 10^4` calls **in total** will be made to `insert`, `search`, and `starts_with`.", "readme_additional": "", - "solution_imports": "from leetcode_py.data_structures import DictTree", + "solution_imports": "from leetcode_py.data_structures import DictTree, RecursiveDict", "solution_methods": [ - { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" }, + { + "name": "__init__", + "parameters": "", + "return_type": "None", + "dummy_return": "super().__init__()\n self.root: RecursiveDict[str] = {}" + }, { "name": "insert", "parameters": "word: str", "return_type": "None", "dummy_return": "" }, { "name": "search", "parameters": "word: str", "return_type": "bool", "dummy_return": "False" }, { diff --git a/leetcode/implement_trie_prefix_tree/playground.ipynb b/leetcode/implement_trie_prefix_tree/playground.ipynb index 627c0cd..75d2107 100644 --- a/leetcode/implement_trie_prefix_tree/playground.ipynb +++ b/leetcode/implement_trie_prefix_tree/playground.ipynb @@ -60,7 +60,7 @@ { "cell_type": "code", "execution_count": 5, - "id": "f3bac41e", + "id": "c8308208", "metadata": {}, "outputs": [ { @@ -170,7 +170,7 @@ "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -207,7 +207,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/implement_trie_prefix_tree/solution.py b/leetcode/implement_trie_prefix_tree/solution.py index 19ecc1e..35ea50b 100644 --- a/leetcode/implement_trie_prefix_tree/solution.py +++ b/leetcode/implement_trie_prefix_tree/solution.py @@ -1,6 +1,4 @@ -from typing import Any - -from leetcode_py.data_structures import DictTree +from leetcode_py.data_structures import DictTree, RecursiveDict class Trie(DictTree[str]): @@ -9,7 +7,7 @@ class Trie(DictTree[str]): # Time: O(1) # Space: O(1) def __init__(self) -> None: - self.root: dict[str, Any] = {} + self.root: RecursiveDict[str] = {} # Time: O(m) where m is word length # Space: O(m) @@ -19,7 +17,7 @@ def insert(self, word: str) -> None: if char not in node: node[char] = {} node = node[char] - node[self.END_OF_WORD] = True # End of word marker + node[self.END_OF_WORD] = True # Time: O(m) where m is word length # Space: O(1) diff --git a/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py b/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py index c30c332..d629f35 100644 --- a/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py +++ b/leetcode/lowest_common_ancestor_of_a_binary_tree/tests.py @@ -16,6 +16,14 @@ def setup_method(self): ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 5, 1, 3), ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 5, 4, 5), ([1, 2], 1, 2, 1), + ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 6, 7, 5), + ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 7, 4, 2), + ([3, 5, 1, 6, 2, 0, 8, None, None, 7, 4], 0, 8, 1), + ([1], 1, 1, 1), + ([2, 1, 3], 1, 3, 2), + ([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5], 2, 8, 6), + ([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5], 3, 5, 4), + ([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5], 0, 3, 2), ], ) @logged_test diff --git a/leetcode_py/data_structures/__init__.py b/leetcode_py/data_structures/__init__.py index 34a8d61..8d61bf4 100644 --- a/leetcode_py/data_structures/__init__.py +++ b/leetcode_py/data_structures/__init__.py @@ -1,6 +1,6 @@ -from .dict_tree import DictTree +from .dict_tree import DictTree, RecursiveDict from .graph_node import GraphNode from .list_node import ListNode from .tree_node import TreeNode -__all__ = ["DictTree", "GraphNode", "ListNode", "TreeNode"] +__all__ = ["DictTree", "GraphNode", "ListNode", "RecursiveDict", "TreeNode"] diff --git a/leetcode_py/data_structures/dict_tree.py b/leetcode_py/data_structures/dict_tree.py index 5b8e614..1f324aa 100644 --- a/leetcode_py/data_structures/dict_tree.py +++ b/leetcode_py/data_structures/dict_tree.py @@ -1,19 +1,20 @@ -from typing import Any, Generic, Hashable, TypeVar +from typing import Any, Generic, Hashable, TypeAlias, TypeVar K = TypeVar("K", bound=Hashable) +RecursiveDict: TypeAlias = dict[K, "RecursiveDict[K] | Any"] class DictTree(Generic[K]): def __init__(self) -> None: - self.root: dict[K, Any] = {} + self.root: RecursiveDict[K] = {} def __str__(self) -> str: if not hasattr(self, "root") or not self.root: return "Empty" return self._render_dict_tree(self.root) - def _render_dict_tree(self, node: dict[K, Any], prefix: str = "", depth: int = 0) -> str: + def _render_dict_tree(self, node: RecursiveDict[K], prefix: str = "", depth: int = 0) -> str: if not node: return "" @@ -49,7 +50,9 @@ def _repr_html_(self) -> str: self._add_dict_nodes(dot, self.root, "root") return dot.pipe(format="svg", encoding="utf-8") - def _add_dict_nodes(self, dot: Any, node: dict[K, Any], node_id: str, char: K | str = "") -> None: + def _add_dict_nodes( + self, dot: Any, node: RecursiveDict[K], node_id: str, char: K | str = "" + ) -> None: if not node: return From 93834e5c51d69cd0e20256d80b56c31e943e0b75 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 13:06:00 +0700 Subject: [PATCH 03/26] feat: add word ladder --- .templates/leetcode/json/word_ladder.json | 47 +++++++++++++ Makefile | 2 +- leetcode/word_ladder/README.md | 47 +++++++++++++ leetcode/word_ladder/__init__.py | 0 leetcode/word_ladder/playground.ipynb | 81 +++++++++++++++++++++++ leetcode/word_ladder/solution.py | 33 +++++++++ leetcode/word_ladder/tests.py | 42 ++++++++++++ 7 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/word_ladder.json create mode 100644 leetcode/word_ladder/README.md create mode 100644 leetcode/word_ladder/__init__.py create mode 100644 leetcode/word_ladder/playground.ipynb create mode 100644 leetcode/word_ladder/solution.py create mode 100644 leetcode/word_ladder/tests.py diff --git a/.templates/leetcode/json/word_ladder.json b/.templates/leetcode/json/word_ladder.json new file mode 100644 index 0000000..0e417b3 --- /dev/null +++ b/.templates/leetcode/json/word_ladder.json @@ -0,0 +1,47 @@ +{ + "problem_name": "word_ladder", + "solution_class_name": "Solution", + "problem_number": "127", + "problem_title": "Word Ladder", + "difficulty": "Hard", + "topics": "Hash Table, String, Breadth-First Search", + "tags": ["grind-75"], + "readme_description": "A **transformation sequence** from word `beginWord` to word `endWord` using a dictionary `wordList` is a sequence of words `beginWord -> s1 -> s2 -> ... -> sk` such that:\n\n- Every adjacent pair of words differs by a single letter.\n- Every `si` for `1 <= i <= k` is in `wordList`. Note that `beginWord` does not need to be in `wordList`.\n- `sk == endWord`\n\nGiven two words, `beginWord` and `endWord`, and a dictionary `wordList`, return the **number of words** in the **shortest transformation sequence** from `beginWord` to `endWord`, or `0` if no such sequence exists.", + "readme_examples": [ + { + "content": "```\nInput: beginWord = \"hit\", endWord = \"cog\", wordList = [\"hot\",\"dot\",\"dog\",\"lot\",\"log\",\"cog\"]\nOutput: 5\n```\n**Explanation:** One shortest transformation sequence is \"hit\" -> \"hot\" -> \"dot\" -> \"dog\" -> \"cog\", which is 5 words long." + }, + { + "content": "```\nInput: beginWord = \"hit\", endWord = \"cog\", wordList = [\"hot\",\"dot\",\"dog\",\"lot\",\"log\"]\nOutput: 0\n```\n**Explanation:** The endWord \"cog\" is not in wordList, therefore there is no valid transformation sequence." + } + ], + "readme_constraints": "- 1 <= beginWord.length <= 10\n- endWord.length == beginWord.length\n- 1 <= wordList.length <= 5000\n- wordList[i].length == beginWord.length\n- beginWord, endWord, and wordList[i] consist of lowercase English letters.\n- beginWord != endWord\n- All the words in wordList are unique.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "ladder_length", + "parameters": "begin_word: str, end_word: str, word_list: list[str]", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "WordLadder", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_ladder_length", + "parametrize": "begin_word, end_word, word_list, expected", + "parametrize_typed": "begin_word: str, end_word: str, word_list: list[str], expected: int", + "test_cases": "[('hit', 'cog', ['hot', 'dot', 'dog', 'lot', 'log', 'cog'], 5), ('hit', 'cog', ['hot', 'dot', 'dog', 'lot', 'log'], 0), ('a', 'c', ['a', 'b', 'c'], 2)]", + "body": "result = self.solution.ladder_length(begin_word, end_word, word_list)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nbegin_word = 'hit'\nend_word = 'cog'\nword_list = ['hot', 'dot', 'dog', 'lot', 'log', 'cog']\nexpected = 5", + "playground_execution": "result = Solution().ladder_length(begin_word, end_word, word_list)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 770e889..f7e7efe 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= lowest_common_ancestor_of_a_binary_search_tree +PROBLEM ?= word_ladder FORCE ?= 0 COMMA := , diff --git a/leetcode/word_ladder/README.md b/leetcode/word_ladder/README.md new file mode 100644 index 0000000..32820d3 --- /dev/null +++ b/leetcode/word_ladder/README.md @@ -0,0 +1,47 @@ +# Word Ladder + +**Difficulty:** Hard +**Topics:** Hash Table, String, Breadth-First Search +**Tags:** grind-75 + +**LeetCode:** [Problem 127](https://leetcode.com/problems/word-ladder/description/) + +## Problem Description + +A **transformation sequence** from word `beginWord` to word `endWord` using a dictionary `wordList` is a sequence of words `beginWord -> s1 -> s2 -> ... -> sk` such that: + +- Every adjacent pair of words differs by a single letter. +- Every `si` for `1 <= i <= k` is in `wordList`. Note that `beginWord` does not need to be in `wordList`. +- `sk == endWord` + +Given two words, `beginWord` and `endWord`, and a dictionary `wordList`, return the **number of words** in the **shortest transformation sequence** from `beginWord` to `endWord`, or `0` if no such sequence exists. + +## Examples + +### Example 1: + +``` +Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] +Output: 5 +``` + +**Explanation:** One shortest transformation sequence is "hit" -> "hot" -> "dot" -> "dog" -> "cog", which is 5 words long. + +### Example 2: + +``` +Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] +Output: 0 +``` + +**Explanation:** The endWord "cog" is not in wordList, therefore there is no valid transformation sequence. + +## Constraints + +- 1 <= beginWord.length <= 10 +- endWord.length == beginWord.length +- 1 <= wordList.length <= 5000 +- wordList[i].length == beginWord.length +- beginWord, endWord, and wordList[i] consist of lowercase English letters. +- beginWord != endWord +- All the words in wordList are unique. diff --git a/leetcode/word_ladder/__init__.py b/leetcode/word_ladder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/word_ladder/playground.ipynb b/leetcode/word_ladder/playground.ipynb new file mode 100644 index 0000000..da0260d --- /dev/null +++ b/leetcode/word_ladder/playground.ipynb @@ -0,0 +1,81 @@ +{ + "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", + "begin_word = \"hit\"\n", + "end_word = \"cog\"\n", + "word_list = [\"hot\", \"dot\", \"dog\", \"lot\", \"log\", \"cog\"]\n", + "expected = 5" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().ladder_length(begin_word, end_word, word_list)\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/word_ladder/solution.py b/leetcode/word_ladder/solution.py new file mode 100644 index 0000000..93b746c --- /dev/null +++ b/leetcode/word_ladder/solution.py @@ -0,0 +1,33 @@ +class Solution: + # Time: O(M^2 * N) where M is length of each word, N is total number of words + # Space: O(M * N) for the visited sets + def ladder_length(self, begin_word: str, end_word: str, word_list: list[str]) -> int: + if end_word not in word_list: + return 0 + + word_set = set(word_list) + begin_set = {begin_word} + end_set = {end_word} + length = 1 + + while begin_set and end_set: + if len(begin_set) > len(end_set): + begin_set, end_set = end_set, begin_set + + next_set = set() + for word in begin_set: + for i in range(len(word)): + for c in "abcdefghijklmnopqrstuvwxyz": + new_word = word[:i] + c + word[i + 1 :] + + if new_word in end_set: + return length + 1 + + if new_word in word_set: + next_set.add(new_word) + word_set.remove(new_word) + + begin_set = next_set + length += 1 + + return 0 diff --git a/leetcode/word_ladder/tests.py b/leetcode/word_ladder/tests.py new file mode 100644 index 0000000..0b7682f --- /dev/null +++ b/leetcode/word_ladder/tests.py @@ -0,0 +1,42 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestWordLadder: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "begin_word, end_word, word_list, expected", + [ + # Basic cases + ("hit", "cog", ["hot", "dot", "dog", "lot", "log", "cog"], 5), + ("hit", "cog", ["hot", "dot", "dog", "lot", "log"], 0), + ("a", "c", ["a", "b", "c"], 2), + # Edge cases + ("hot", "dog", ["hot", "dog"], 0), # No intermediate words + ("hot", "hot", ["hot"], 1), # Same word + ("cat", "dog", [], 0), # Empty word list + ("cat", "dog", ["cat"], 0), # End word not in list + # Single character changes + ("a", "b", ["a", "b"], 2), + ("ab", "cd", ["ab", "ad", "cd"], 3), + # Longer paths + ("red", "tax", ["ted", "tex", "red", "tax", "tad", "den", "rex", "pee"], 4), + # Multiple possible paths (should return shortest) + ("cat", "dog", ["cat", "bat", "bag", "dag", "dog", "cag", "cog"], 4), + # No path exists + ("abc", "def", ["abc", "def", "ghi"], 0), + # Direct transformation + ("cat", "bat", ["cat", "bat"], 2), + # Longer word length + ("word", "form", ["word", "worm", "form", "foam", "flam", "flab"], 3), + ], + ) + @logged_test + def test_ladder_length(self, begin_word: str, end_word: str, word_list: list[str], expected: int): + result = self.solution.ladder_length(begin_word, end_word, word_list) + assert result == expected From d40fce4cd8288ca182e7e08032f3f3be6a92b63e Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 13:28:30 +0700 Subject: [PATCH 04/26] feat: add Serialize and Deserialize Binary Tree --- ...serialize_and_deserialize_binary_tree.json | 52 ++++++ .../{{cookiecutter.problem_name}}/solution.py | 2 +- Makefile | 2 +- .../README.md | 38 ++++ .../__init__.py | 0 .../playground.ipynb | 167 ++++++++++++++++++ .../solution.py | 81 +++++++++ .../tests.py | 100 +++++++++++ 8 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 .templates/leetcode/json/serialize_and_deserialize_binary_tree.json create mode 100644 leetcode/serialize_and_deserialize_binary_tree/README.md create mode 100644 leetcode/serialize_and_deserialize_binary_tree/__init__.py create mode 100644 leetcode/serialize_and_deserialize_binary_tree/playground.ipynb create mode 100644 leetcode/serialize_and_deserialize_binary_tree/solution.py create mode 100644 leetcode/serialize_and_deserialize_binary_tree/tests.py diff --git a/.templates/leetcode/json/serialize_and_deserialize_binary_tree.json b/.templates/leetcode/json/serialize_and_deserialize_binary_tree.json new file mode 100644 index 0000000..36ae36f --- /dev/null +++ b/.templates/leetcode/json/serialize_and_deserialize_binary_tree.json @@ -0,0 +1,52 @@ +{ + "problem_name": "serialize_and_deserialize_binary_tree", + "solution_class_name": "Codec", + "problem_number": "297", + "problem_title": "Serialize and Deserialize Binary Tree", + "difficulty": "Hard", + "topics": "String, Tree, Depth-First Search, Breadth-First Search, Design, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.\n\nDesign an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.\n\n**Clarification:** The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2020/09/15/serdeser.jpg)\n\n```\nInput: root = [1,2,3,null,null,4,5]\nOutput: [1,2,3,null,null,4,5]\n```" + }, + { "content": "```\nInput: root = []\nOutput: []\n```" } + ], + "readme_constraints": "- The number of nodes in the tree is in the range [0, 10^4].\n- -1000 <= Node.val <= 1000", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { "name": "__init__", "parameters": "", "return_type": "", "dummy_return": "" }, + { + "name": "serialize", + "parameters": "root: TreeNode | None", + "return_type": "str", + "dummy_return": "''" + }, + { + "name": "deserialize", + "parameters": "data: str", + "return_type": "TreeNode | None", + "dummy_return": "None" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Codec", + "test_class_name": "SerializeAndDeserializeBinaryTree", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.codec = Codec()" } + ], + "test_methods": [ + { + "name": "test_serialize_deserialize", + "parametrize": "root_list", + "parametrize_typed": "root_list: list[int | None]", + "test_cases": "[([1, 2, 3, None, None, 4, 5]), ([]), ([1]), ([1, 2]), ([1, None, 2]), ([1, 2, 3, 4, 5, 6, 7]), ([5, 2, 3, None, None, 2, 4, 3, 1])]", + "body": "root = TreeNode.from_list(root_list) if root_list else None\nserialized = self.codec.serialize(root)\ndeserialized = self.codec.deserialize(serialized)\nif root is None:\n assert deserialized is None\nelse:\n assert deserialized is not None\n assert deserialized.to_list() == root.to_list()" + } + ], + "playground_imports": "from solution import Codec\nfrom leetcode_py import TreeNode", + "playground_test_case": "# Example test case\nroot_list = [1, 2, 3, None, None, 4, 5]\nroot = TreeNode.from_list(root_list) if root_list else None", + "playground_execution": "codec = Codec()\nserialized = codec.serialize(root)\ndeserialized = codec.deserialize(serialized)\nprint(f'Original: {root.to_list() if root else None}')\nprint(f'Serialized: {serialized}')\nprint(f'Deserialized: {deserialized.to_list() if deserialized else None}')\ndeserialized", + "playground_assertion": "if root is None:\n assert deserialized is None\nelse:\n assert deserialized is not None\n assert deserialized.to_list() == root.to_list()" +} diff --git a/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py index a48e5d1..467ba85 100644 --- a/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py +++ b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py @@ -4,7 +4,7 @@ class {{cookiecutter.solution_class_name}}: {%- for _, methods in cookiecutter._solution_methods | dictsort %} {%- for method in methods %} # Time: O(?) - # Space: O(?){# TODO: add decorator // optional self. #} + # Space: O(?){# TODO: add decorator // optional self. // optional return type // optional return #} def {{method.name}}(self, {{method.parameters}}) -> {{method.return_type}}: # TODO: Implement {{method.name}}{# TODO: add body #} return {{method.dummy_return}} diff --git a/Makefile b/Makefile index f7e7efe..afa62da 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= word_ladder +PROBLEM ?= serialize_and_deserialize_binary_tree FORCE ?= 0 COMMA := , diff --git a/leetcode/serialize_and_deserialize_binary_tree/README.md b/leetcode/serialize_and_deserialize_binary_tree/README.md new file mode 100644 index 0000000..8226e6c --- /dev/null +++ b/leetcode/serialize_and_deserialize_binary_tree/README.md @@ -0,0 +1,38 @@ +# Serialize and Deserialize Binary Tree + +**Difficulty:** Hard +**Topics:** String, Tree, Depth-First Search, Breadth-First Search, Design, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 297](https://leetcode.com/problems/serialize-and-deserialize-binary-tree/description/) + +## Problem Description + +Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment. + +Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure. + +**Clarification:** The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2020/09/15/serdeser.jpg) + +``` +Input: root = [1,2,3,null,null,4,5] +Output: [1,2,3,null,null,4,5] +``` + +### Example 2: + +``` +Input: root = [] +Output: [] +``` + +## Constraints + +- The number of nodes in the tree is in the range [0, 10^4]. +- -1000 <= Node.val <= 1000 diff --git a/leetcode/serialize_and_deserialize_binary_tree/__init__.py b/leetcode/serialize_and_deserialize_binary_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/serialize_and_deserialize_binary_tree/playground.ipynb b/leetcode/serialize_and_deserialize_binary_tree/playground.ipynb new file mode 100644 index 0000000..36ca758 --- /dev/null +++ b/leetcode/serialize_and_deserialize_binary_tree/playground.ipynb @@ -0,0 +1,167 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Codec\n", + "\n", + "from leetcode_py import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root_list = [1, 2, 3, None, None, 4, 5]\n", + "root = TreeNode.from_list(root_list) if root_list else None" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original: [1, 2, 3, None, None, 4, 5]\n", + "Serialized: 1,2,#,#,3,4,#,#,5,#,#\n", + "Deserialized: [1, 2, 3, None, None, 4, 5]\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "0->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "2->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "2->4\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([1, 2, 3, None, None, 4, 5])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "codec = Codec()\n", + "serialized = codec.serialize(root)\n", + "deserialized = codec.deserialize(serialized)\n", + "print(f\"Original: {root.to_list() if root else None}\")\n", + "print(f\"Serialized: {serialized}\")\n", + "print(f\"Deserialized: {deserialized.to_list() if deserialized else None}\")\n", + "deserialized" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "if root is None:\n", + " assert deserialized is None\n", + "else:\n", + " assert deserialized is not None\n", + " assert deserialized.to_list() == root.to_list()" + ] + } + ], + "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/serialize_and_deserialize_binary_tree/solution.py b/leetcode/serialize_and_deserialize_binary_tree/solution.py new file mode 100644 index 0000000..aeb3177 --- /dev/null +++ b/leetcode/serialize_and_deserialize_binary_tree/solution.py @@ -0,0 +1,81 @@ +from leetcode_py import TreeNode + + +class Codec: + # Preorder with Null Markers + # Time: O(n) + # Space: O(n) + def serialize(self, root: TreeNode | None) -> str: + vals = [] + + def dfs(node: TreeNode | None): + if not node: + vals.append("#") + return + vals.append(str(node.val)) + dfs(node.left) + dfs(node.right) + + dfs(root) + return ",".join(vals) + + # Time: O(n) + # Space: O(n) + def deserialize(self, data: str) -> TreeNode | None: + vals = iter(data.split(",")) + + def dfs(): + val = next(vals) + if val == "#": + return None + node = TreeNode(int(val)) + node.left = dfs() + node.right = dfs() + return node + + return dfs() + + +# Binary Tree Serialization Techniques + +# Example Tree: +# 1 +# / \ +# 2 3 +# / \ +# 4 5 + +# 1. Preorder with Null Markers (This Implementation) +# Visit: root → left → right, mark nulls with '#' +# Result: "1,2,#,#,3,4,#,#,5,#,#" +# Pros: Self-contained, unambiguous, O(n) reconstruction +# Cons: Longer string due to null markers + +# 2. Level-order (BFS) with Null Markers +# Visit level by level, mark nulls with '#' +# Result: "1,2,3,#,#,4,5" +# Pros: Simple format like preorder, level-by-level intuitive +# Cons: Still requires queue processing + +# 3. Postorder with Null Markers +# Visit: left → right → root +# Result: "#,#,2,#,#,4,#,#,5,3,1" +# Pros: Bottom-up reconstruction +# Cons: Less intuitive than preorder + +# 4. Inorder + Preorder (Two Arrays) +# Inorder: [2,1,4,3,5], Preorder: [1,2,3,4,5] +# Pros: Works for any binary tree structure +# Cons: Requires two arrays, only works with unique values + +# 5. Parenthetical Preorder +# Same traversal as #1 but with parentheses format: value(left)(right) +# Result: "1(2()())(3(4()())(5()()))" +# Pros: Human readable structure, shows nesting clearly +# Cons: Complex parsing, verbose + +# 6. Parenthetical Postorder +# Same traversal as #3 but with parentheses format: (left)(right)value +# Result: "(()()2)((()()4)(()()5)3)1" +# Pros: Bottom-up readable structure +# Cons: Even more complex parsing diff --git a/leetcode/serialize_and_deserialize_binary_tree/tests.py b/leetcode/serialize_and_deserialize_binary_tree/tests.py new file mode 100644 index 0000000..f1fb387 --- /dev/null +++ b/leetcode/serialize_and_deserialize_binary_tree/tests.py @@ -0,0 +1,100 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Codec + + +class TestSerializeAndDeserializeBinaryTree: + def setup_method(self): + self.codec = Codec() + + @pytest.mark.parametrize( + "root_list", + [ + # Original test cases + ([1, 2, 3, None, None, 4, 5]), + ([]), + ([1]), + ([1, 2]), + ([1, None, 2]), + ([1, 2, 3, 4, 5, 6, 7]), + ([5, 2, 3, None, None, 2, 4, 3, 1]), + # Edge cases + ([0]), # Single node with value 0 + ([-1]), # Single node with negative value + ([1000]), # Single node with max value + ([-1000]), # Single node with min value + # Skewed trees + ([1, 2, None, 3, None, 4, None]), # Left skewed + ([1, None, 2, None, 3, None, 4]), # Right skewed + # Trees with negative values + ([-5, -3, -8, -2, -1, -7, -9]), + ([0, -1, 1, -2, None, None, 2]), + # Larger trees + ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]), + # Trees with mixed null patterns + ([1, None, 2, None, 3, None, 4, None, 5]), + ([1, 2, None, 3, None, 4, None, 5]), + ([5, 4, 7, 3, None, 2, None, -1, None, 9]), + # Duplicate values + ([1, 1, 1, 1, 1, 1, 1]), + ([2, 2, None, 2, None]), + # Complex asymmetric trees + ([10, 5, 15, None, 6, 12, 20, None, None, None, 13, 18, 25]), + ([50, 30, 70, 20, 40, 60, 80, 10, 25, 35, 45]), + ], + ) + @logged_test + def test_serialize_deserialize(self, root_list: list[int | None]): + root = TreeNode.from_list(root_list) if root_list else None + serialized = self.codec.serialize(root) + deserialized = self.codec.deserialize(serialized) + if root is None: + assert deserialized is None + else: + assert deserialized is not None + assert deserialized.to_list() == root.to_list() + + @logged_test + def test_multiple_serialize_deserialize_cycles(self): + """Test that multiple serialize/deserialize cycles preserve the tree""" + root_list = [1, 2, 3, None, None, 4, 5] + root = TreeNode.from_list(root_list) + + # Perform multiple cycles + current = root + for _ in range(3): + serialized = self.codec.serialize(current) + current = self.codec.deserialize(serialized) + + assert current is not None + assert current.to_list() == root_list + + @logged_test + def test_serialization_format(self): + """Test that serialization produces expected string format""" + # Simple tree: [1, 2, 3] + root = TreeNode.from_list([1, 2, 3]) + serialized = self.codec.serialize(root) + + # Should be preorder: root, left, right with # for null + assert serialized == "1,2,#,#,3,#,#" + + # Empty tree + serialized_empty = self.codec.serialize(None) + assert serialized_empty == "#" + + @logged_test + def test_deserialization_edge_cases(self): + """Test deserialization with various string inputs""" + # Single null + assert self.codec.deserialize("#") is None + + # Single node + single = self.codec.deserialize("42,#,#") + assert single is not None + assert single.val == 42 + assert single.left is None + assert single.right is None From 19374dcebf412ba8fbdb3ad28ddccc65daeda4fa Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 13:55:27 +0700 Subject: [PATCH 05/26] feat: add find_median_from_data_stream --- .../json/find_median_from_data_stream.json | 39 ++++++ Makefile | 2 +- .../find_median_from_data_stream/README.md | 54 ++++++++ .../find_median_from_data_stream/__init__.py | 0 .../playground.ipynb | 79 ++++++++++++ .../find_median_from_data_stream/solution.py | 115 ++++++++++++++++++ .../find_median_from_data_stream/tests.py | 71 +++++++++++ leetcode/word_ladder/solution.py | 3 + 8 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/find_median_from_data_stream.json create mode 100644 leetcode/find_median_from_data_stream/README.md create mode 100644 leetcode/find_median_from_data_stream/__init__.py create mode 100644 leetcode/find_median_from_data_stream/playground.ipynb create mode 100644 leetcode/find_median_from_data_stream/solution.py create mode 100644 leetcode/find_median_from_data_stream/tests.py diff --git a/.templates/leetcode/json/find_median_from_data_stream.json b/.templates/leetcode/json/find_median_from_data_stream.json new file mode 100644 index 0000000..7cee350 --- /dev/null +++ b/.templates/leetcode/json/find_median_from_data_stream.json @@ -0,0 +1,39 @@ +{ + "problem_name": "find_median_from_data_stream", + "solution_class_name": "MedianFinder", + "problem_number": "295", + "problem_title": "Find Median from Data Stream", + "difficulty": "Hard", + "topics": "Two Pointers, Design, Sorting, Heap (Priority Queue), Data Stream", + "tags": ["grind-75"], + "readme_description": "The **median** is the middle value in an ordered integer list. If the size of the list is even, there is no middle value, and the median is the mean of the two middle values.\n\n- For example, for `arr = [2,3,4]`, the median is `3`.\n- For example, for `arr = [2,3]`, the median is `(2 + 3) / 2 = 2.5`.\n\nImplement the MedianFinder class:\n\n- `MedianFinder()` initializes the `MedianFinder` object.\n- `void addNum(int num)` adds the integer `num` from the data stream to the data structure.\n- `double findMedian()` returns the median of all elements so far. Answers within `10^-5` of the actual answer will be accepted.", + "readme_examples": [ + { + "content": "```\nInput\n[\"MedianFinder\", \"addNum\", \"addNum\", \"findMedian\", \"addNum\", \"findMedian\"]\n[[], [1], [2], [], [3], []]\nOutput\n[null, null, null, 1.5, null, 2.0]\n```\n\n**Explanation:**\n```\nMedianFinder medianFinder = new MedianFinder();\nmedianFinder.addNum(1); // arr = [1]\nmedianFinder.addNum(2); // arr = [1, 2]\nmedianFinder.findMedian(); // return 1.5 (i.e., (1 + 2) / 2)\nmedianFinder.addNum(3); // arr = [1, 2, 3]\nmedianFinder.findMedian(); // return 2.0\n```" + } + ], + "readme_constraints": "- `-10^5 <= num <= 10^5`\n- There will be at least one element in the data structure before calling `findMedian`.\n- At most `5 * 10^4` calls will be made to `addNum` and `findMedian`.", + "readme_additional": "**Follow up:**\n\n- If all integer numbers from the stream are in the range `[0, 100]`, how would you optimize your solution?\n- If `99%` of all integer numbers from the stream are in the range `[0, 100]`, how would you optimize your solution?", + "solution_imports": "", + "solution_methods": [ + { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" }, + { "name": "add_num", "parameters": "num: int", "return_type": "None", "dummy_return": "" }, + { "name": "find_median", "parameters": "", "return_type": "float", "dummy_return": "0.0" } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import MedianFinder", + "test_class_name": "FindMedianFromDataStream", + "test_helper_methods": [], + "test_methods": [ + { + "name": "test_median_finder", + "parametrize": "operations, inputs, expected", + "parametrize_typed": "operations: list[str], inputs: list[list[int]], expected: list[float | None]", + "test_cases": "[([\"MedianFinder\", \"addNum\", \"addNum\", \"findMedian\", \"addNum\", \"findMedian\"], [[], [1], [2], [], [3], []], [None, None, None, 1.5, None, 2.0])]", + "body": "mf: MedianFinder | None = None\nresults: list[float | None] = []\nfor i, op in enumerate(operations):\n if op == \"MedianFinder\":\n mf = MedianFinder()\n results.append(None)\n elif op == \"addNum\" and mf is not None:\n mf.add_num(inputs[i][0])\n results.append(None)\n elif op == \"findMedian\" and mf is not None:\n results.append(mf.find_median())\nassert results == expected" + } + ], + "playground_imports": "from solution import MedianFinder", + "playground_test_case": "# Example test case\noperations = ['MedianFinder', 'addNum', 'addNum', 'findMedian', 'addNum', 'findMedian']\ninputs = [[], [1], [2], [], [3], []]\nexpected = [None, None, None, 1.5, None, 2.0]", + "playground_execution": "mf = None\nresults: list[float | None] = []\nfor i, op in enumerate(operations):\n if op == 'MedianFinder':\n mf = MedianFinder()\n results.append(None)\n elif op == 'addNum' and mf is not None:\n mf.add_num(inputs[i][0])\n results.append(None)\n elif op == 'findMedian' and mf is not None:\n results.append(mf.find_median())\nresults", + "playground_assertion": "assert results == expected" +} diff --git a/Makefile b/Makefile index afa62da..6a52a4a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= serialize_and_deserialize_binary_tree +PROBLEM ?= find_median_from_data_stream FORCE ?= 0 COMMA := , diff --git a/leetcode/find_median_from_data_stream/README.md b/leetcode/find_median_from_data_stream/README.md new file mode 100644 index 0000000..90ace29 --- /dev/null +++ b/leetcode/find_median_from_data_stream/README.md @@ -0,0 +1,54 @@ +# Find Median from Data Stream + +**Difficulty:** Hard +**Topics:** Two Pointers, Design, Sorting, Heap (Priority Queue), Data Stream +**Tags:** grind-75 + +**LeetCode:** [Problem 295](https://leetcode.com/problems/find-median-from-data-stream/description/) + +## Problem Description + +The **median** is the middle value in an ordered integer list. If the size of the list is even, there is no middle value, and the median is the mean of the two middle values. + +- For example, for `arr = [2,3,4]`, the median is `3`. +- For example, for `arr = [2,3]`, the median is `(2 + 3) / 2 = 2.5`. + +Implement the MedianFinder class: + +- `MedianFinder()` initializes the `MedianFinder` object. +- `void addNum(int num)` adds the integer `num` from the data stream to the data structure. +- `double findMedian()` returns the median of all elements so far. Answers within `10^-5` of the actual answer will be accepted. + +## Examples + +### Example 1: + +``` +Input +["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] +[[], [1], [2], [], [3], []] +Output +[null, null, null, 1.5, null, 2.0] +``` + +**Explanation:** + +``` +MedianFinder medianFinder = new MedianFinder(); +medianFinder.addNum(1); // arr = [1] +medianFinder.addNum(2); // arr = [1, 2] +medianFinder.findMedian(); // return 1.5 (i.e., (1 + 2) / 2) +medianFinder.addNum(3); // arr = [1, 2, 3] +medianFinder.findMedian(); // return 2.0 +``` + +## Constraints + +- `-10^5 <= num <= 10^5` +- There will be at least one element in the data structure before calling `findMedian`. +- At most `5 * 10^4` calls will be made to `addNum` and `findMedian`. + +**Follow up:** + +- If all integer numbers from the stream are in the range `[0, 100]`, how would you optimize your solution? +- If `99%` of all integer numbers from the stream are in the range `[0, 100]`, how would you optimize your solution? diff --git a/leetcode/find_median_from_data_stream/__init__.py b/leetcode/find_median_from_data_stream/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/find_median_from_data_stream/playground.ipynb b/leetcode/find_median_from_data_stream/playground.ipynb new file mode 100644 index 0000000..3562233 --- /dev/null +++ b/leetcode/find_median_from_data_stream/playground.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import MedianFinder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "operations = [\"MedianFinder\", \"addNum\", \"addNum\", \"findMedian\", \"addNum\", \"findMedian\"]\n", + "inputs = [[], [1], [2], [], [3], []]\n", + "expected = [None, None, None, 1.5, None, 2.0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "mf = None\n", + "results: list[float | None] = []\n", + "for i, op in enumerate(operations):\n", + " if op == \"MedianFinder\":\n", + " mf = MedianFinder()\n", + " results.append(None)\n", + " elif op == \"addNum\" and mf is not None:\n", + " mf.add_num(inputs[i][0])\n", + " results.append(None)\n", + " elif op == \"findMedian\" and mf is not None:\n", + " results.append(mf.find_median())\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert results == 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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/find_median_from_data_stream/solution.py b/leetcode/find_median_from_data_stream/solution.py new file mode 100644 index 0000000..af1924e --- /dev/null +++ b/leetcode/find_median_from_data_stream/solution.py @@ -0,0 +1,115 @@ +import heapq + + +class MedianFinder: + # Two balanced heaps approach for general streaming median + # Time: O(1) init + # Space: O(n) + def __init__(self) -> None: + self.small: list[int] = [] # max heap (negated) + self.large: list[int] = [] # min heap + + # Time: O(log n) + # Space: O(1) + def add_num(self, num: int) -> None: + heapq.heappush(self.small, -num) + + if self.small and self.large and (-self.small[0] > self.large[0]): + heapq.heappush(self.large, -heapq.heappop(self.small)) + + if len(self.small) > len(self.large) + 1: + heapq.heappush(self.large, -heapq.heappop(self.small)) + if len(self.large) > len(self.small) + 1: + heapq.heappush(self.small, -heapq.heappop(self.large)) + + # Time: O(1) + # Space: O(1) + def find_median(self) -> float: + if len(self.small) > len(self.large): + return -self.small[0] + if len(self.large) > len(self.small): + return self.large[0] + return (-self.small[0] + self.large[0]) / 2.0 + + +class MedianFinderHybrid: + # Hybrid counting array + heaps for bounded ranges with outliers + # Time: O(1) init + # Space: O(R + k) where R = range_size, k = outliers + def __init__(self, min_val: int = 0, max_val: int = 100) -> None: + self.min_val = min_val + self.max_val = max_val + self.counts = [0] * (max_val - min_val + 1) + self.outliers_small: list[int] = [] # max heap for < min_val + self.outliers_large: list[int] = [] # min heap for > max_val + self.total = 0 + + # Time: O(1) for range, O(log k) for outliers + # Space: O(1) + def add_num(self, num: int) -> None: + if self.min_val <= num <= self.max_val: + self.counts[num - self.min_val] += 1 + elif num < self.min_val: + heapq.heappush(self.outliers_small, -num) + else: + heapq.heappush(self.outliers_large, num) + self.total += 1 + + # Time: O(R + k log k) worst case, O(R) typical, O(1) if R constant + # Space: O(k) for sorting outliers + def find_median(self) -> float: + target = self.total // 2 + count = 0 + + # Count outliers < 0 + outliers_small_count = len(self.outliers_small) + if count + outliers_small_count > target: + sorted_small = sorted([-x for x in self.outliers_small]) + if self.total % 2 == 1: + return sorted_small[target - count] + else: + if target - count == 0: + return (sorted_small[0] + self._get_next_value(0)) / 2.0 + return (sorted_small[target - count - 1] + sorted_small[target - count]) / 2.0 + count += outliers_small_count + + # Count [min_val, max_val] range + for i in range(len(self.counts)): + if count + self.counts[i] > target: + val = i + self.min_val + if self.total % 2 == 1: + return val + else: + if target == count: + return (self._get_prev_value(count - 1) + val) / 2.0 + return val + count += self.counts[i] + + # Must be in outliers > 100 + sorted_large = sorted(self.outliers_large) + idx = target - count + if self.total % 2 == 1: + return sorted_large[idx] + else: + if idx == 0: + return (self._get_prev_value(count - 1) + sorted_large[0]) / 2.0 + return (sorted_large[idx - 1] + sorted_large[idx]) / 2.0 + + def _get_prev_value(self, pos: int) -> int: + count = 0 + # Check outliers < 0 + if pos < len(self.outliers_small): + return sorted([-x for x in self.outliers_small])[pos] + count += len(self.outliers_small) + + # Check [min_val, max_val] range + for i in range(len(self.counts)): + if count + self.counts[i] > pos: + return i + self.min_val + count += self.counts[i] + + # Must be in outliers > 100 + return sorted(self.outliers_large)[pos - count] + + def _get_next_value(self, pos: int) -> int: + return self._get_prev_value(pos + 1) diff --git a/leetcode/find_median_from_data_stream/tests.py b/leetcode/find_median_from_data_stream/tests.py new file mode 100644 index 0000000..0a07039 --- /dev/null +++ b/leetcode/find_median_from_data_stream/tests.py @@ -0,0 +1,71 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import MedianFinder, MedianFinderHybrid + + +class TestFindMedianFromDataStream: + @pytest.mark.parametrize( + "finder_class, operations, inputs, expected", + [ + ( + MedianFinder, + ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"], + [[], [1], [2], [], [3], []], + [None, None, None, 1.5, None, 2.0], + ), + ( + MedianFinderHybrid, + ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"], + [[], [1], [2], [], [3], []], + [None, None, None, 1.5, None, 2.0], + ), + ( + MedianFinder, + ["MedianFinder", "addNum", "findMedian"], + [[], [5], []], + [None, None, 5.0], + ), + ( + MedianFinderHybrid, + ["MedianFinder", "addNum", "findMedian"], + [[], [5], []], + [None, None, 5.0], + ), + ( + MedianFinder, + ["MedianFinder", "addNum", "addNum", "addNum", "addNum", "findMedian"], + [[], [1], [3], [2], [4], []], + [None, None, None, None, None, 2.5], + ), + ( + MedianFinderHybrid, + ["MedianFinder", "addNum", "addNum", "addNum", "addNum", "findMedian"], + [[], [1], [3], [2], [4], []], + [None, None, None, None, None, 2.5], + ), + ( + MedianFinderHybrid, + ["MedianFinder", "addNum", "addNum", "addNum", "findMedian"], + [[], [-1], [50], [101], []], + [None, None, None, None, 50.0], + ), + ], + ) + @logged_test + def test_median_finder( + self, finder_class, operations: list[str], inputs: list[list[int]], expected: list[float | None] + ): + mf = None + results: list[float | None] = [] + for i, op in enumerate(operations): + if op == "MedianFinder": + mf = finder_class() + results.append(None) + elif op == "addNum" and mf is not None: + mf.add_num(inputs[i][0]) + results.append(None) + elif op == "findMedian" and mf is not None: + results.append(mf.find_median()) + assert results == expected diff --git a/leetcode/word_ladder/solution.py b/leetcode/word_ladder/solution.py index 93b746c..7b74aa4 100644 --- a/leetcode/word_ladder/solution.py +++ b/leetcode/word_ladder/solution.py @@ -5,6 +5,9 @@ def ladder_length(self, begin_word: str, end_word: str, word_list: list[str]) -> if end_word not in word_list: return 0 + if begin_word == end_word: + return 1 + word_set = set(word_list) begin_set = {begin_word} end_set = {end_word} From 7eff2324c76c923b581c295129fc3e6e65eb32c0 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 14:07:57 +0700 Subject: [PATCH 06/26] feat: add Basic Calculator --- .../leetcode/json/basic_calculator.json | 39 +++++++++++ Makefile | 2 +- leetcode/basic_calculator/README.md | 46 +++++++++++++ leetcode/basic_calculator/__init__.py | 0 leetcode/basic_calculator/playground.ipynb | 67 ++++++++++++++++++ leetcode/basic_calculator/solution.py | 61 +++++++++++++++++ leetcode/basic_calculator/tests.py | 68 +++++++++++++++++++ 7 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/basic_calculator.json create mode 100644 leetcode/basic_calculator/README.md create mode 100644 leetcode/basic_calculator/__init__.py create mode 100644 leetcode/basic_calculator/playground.ipynb create mode 100644 leetcode/basic_calculator/solution.py create mode 100644 leetcode/basic_calculator/tests.py diff --git a/.templates/leetcode/json/basic_calculator.json b/.templates/leetcode/json/basic_calculator.json new file mode 100644 index 0000000..d8c0cce --- /dev/null +++ b/.templates/leetcode/json/basic_calculator.json @@ -0,0 +1,39 @@ +{ + "problem_name": "basic_calculator", + "solution_class_name": "Solution", + "problem_number": "224", + "problem_title": "Basic Calculator", + "difficulty": "Hard", + "topics": "Math, String, Stack, Recursion", + "tags": ["grind-75"], + "readme_description": "Given a string `s` representing a valid expression, implement a basic calculator to evaluate it, and return the result of the evaluation.\n\n**Note:** You are **not** allowed to use any built-in function which evaluates strings as mathematical expressions, such as `eval()`.", + "readme_examples": [ + { "content": "```\nInput: s = \"1 + 1\"\nOutput: 2\n```" }, + { "content": "```\nInput: s = \" 2-1 + 2 \"\nOutput: 3\n```" }, + { "content": "```\nInput: s = \"(1+(4+5+2)-3)+(6+8)\"\nOutput: 23\n```" } + ], + "readme_constraints": "- `1 <= s.length <= 3 * 10^5`\n- `s` consists of digits, `'+'`, `'-'`, `'('`, `')'`, and `' '`.\n- `s` represents a valid expression.\n- `'+'` is **not** used as a unary operation (i.e., `\"+1\"` and `\"+(2 + 3)\"` is invalid).\n- `'-'` could be used as a unary operation (i.e., `\"-1\"` and `\"-(2 + 3)\"` is valid).\n- There will be no two consecutive operators in the input.\n- Every number and running calculation will fit in a signed 32-bit integer.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { "name": "calculate", "parameters": "s: str", "return_type": "int", "dummy_return": "0" } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "BasicCalculator", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_calculate", + "parametrize": "s, expected", + "parametrize_typed": "s: str, expected: int", + "test_cases": "[(\"1 + 1\", 2), (\" 2-1 + 2 \", 3), (\"(1+(4+5+2)-3)+(6+8)\", 23), (\"1\", 1), (\"-1\", -1), (\"-(1+2)\", -3), (\"2147483647\", 2147483647), (\"1-1+1\", 1)]", + "body": "result = self.solution.calculate(s)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = '(1+(4+5+2)-3)+(6+8)'\nexpected = 23", + "playground_execution": "result = Solution().calculate(s)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 6a52a4a..d5052e5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= find_median_from_data_stream +PROBLEM ?= basic_calculator FORCE ?= 0 COMMA := , diff --git a/leetcode/basic_calculator/README.md b/leetcode/basic_calculator/README.md new file mode 100644 index 0000000..885a6c8 --- /dev/null +++ b/leetcode/basic_calculator/README.md @@ -0,0 +1,46 @@ +# Basic Calculator + +**Difficulty:** Hard +**Topics:** Math, String, Stack, Recursion +**Tags:** grind-75 + +**LeetCode:** [Problem 224](https://leetcode.com/problems/basic-calculator/description/) + +## Problem Description + +Given a string `s` representing a valid expression, implement a basic calculator to evaluate it, and return the result of the evaluation. + +**Note:** You are **not** allowed to use any built-in function which evaluates strings as mathematical expressions, such as `eval()`. + +## Examples + +### Example 1: + +``` +Input: s = "1 + 1" +Output: 2 +``` + +### Example 2: + +``` +Input: s = " 2-1 + 2 " +Output: 3 +``` + +### Example 3: + +``` +Input: s = "(1+(4+5+2)-3)+(6+8)" +Output: 23 +``` + +## Constraints + +- `1 <= s.length <= 3 * 10^5` +- `s` consists of digits, `'+'`, `'-'`, `'('`, `')'`, and `' '`. +- `s` represents a valid expression. +- `'+'` is **not** used as a unary operation (i.e., `"+1"` and `"+(2 + 3)"` is invalid). +- `'-'` could be used as a unary operation (i.e., `"-1"` and `"-(2 + 3)"` is valid). +- There will be no two consecutive operators in the input. +- Every number and running calculation will fit in a signed 32-bit integer. diff --git a/leetcode/basic_calculator/__init__.py b/leetcode/basic_calculator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/basic_calculator/playground.ipynb b/leetcode/basic_calculator/playground.ipynb new file mode 100644 index 0000000..d6fe346 --- /dev/null +++ b/leetcode/basic_calculator/playground.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "s = \"(1+(4+5+2)-3)+(6+8)\"\n", + "expected = 23" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().calculate(s)\nresult" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/basic_calculator/solution.py b/leetcode/basic_calculator/solution.py new file mode 100644 index 0000000..e441460 --- /dev/null +++ b/leetcode/basic_calculator/solution.py @@ -0,0 +1,61 @@ +class Solution: + # Time: O(n) + # Space: O(n) + def calculate(self, s: str) -> int: + stack = [] + num = 0 + sign = 1 + result = 0 + + for char in s: + if char.isdigit(): + num = num * 10 + int(char) + elif char in "+-": + result += sign * num + num = 0 + sign = 1 if char == "+" else -1 + elif char == "(": + stack.append(result) + stack.append(sign) + result = 0 + sign = 1 + elif char == ")": + if len(stack) < 2: + raise ValueError("Mismatched parentheses") + result += sign * num + num = 0 + result *= stack.pop() + result += stack.pop() + elif char != " ": + raise ValueError(f"Invalid character: '{char}'") + + if stack: + raise ValueError("Mismatched parentheses") + + return result + sign * num + + +# Example walkthrough: "(1+(4+5+2)-3)+(6+8)" = 23 +# +# char | num | sign | result | stack | action +# -----|-----|------|--------|------------|------------------ +# '(' | 0 | 1 | 0 | [0, 1] | push result=0, sign=1 +# '1' | 1 | 1 | 0 | [0, 1] | build num=1 +# '+' | 0 | 1 | 1 | [0, 1] | result += 1*1 = 1 +# '(' | 0 | 1 | 0 | [0,1,1,1] | push result=1, sign=1 +# '4' | 4 | 1 | 0 | [0,1,1,1] | build num=4 +# '+' | 0 | 1 | 4 | [0,1,1,1] | result += 1*4 = 4 +# '5' | 5 | 1 | 4 | [0,1,1,1] | build num=5 +# '+' | 0 | 1 | 9 | [0,1,1,1] | result += 1*5 = 9 +# '2' | 2 | 1 | 9 | [0,1,1,1] | build num=2 +# ')' | 0 | 1 | 11 | [0, 1] | result=11*1+1 = 12 +# '-' | 0 | -1 | 12 | [0, 1] | sign = -1 +# '3' | 3 | -1 | 12 | [0, 1] | build num=3 +# ')' | 0 | 1 | 9 | [] | result=9*1+0 = 9 +# '+' | 0 | 1 | 9 | [] | sign = 1 +# '(' | 0 | 1 | 0 | [9, 1] | push result=9, sign=1 +# '6' | 6 | 1 | 0 | [9, 1] | build num=6 +# '+' | 0 | 1 | 6 | [9, 1] | result += 1*6 = 6 +# '8' | 8 | 1 | 6 | [9, 1] | build num=8 +# ')' | 0 | 1 | 14 | [] | result=14*1+9 = 23 +# end | 0 | 1 | 14 | [] | return 14+1*0 = 23 diff --git a/leetcode/basic_calculator/tests.py b/leetcode/basic_calculator/tests.py new file mode 100644 index 0000000..08c96e9 --- /dev/null +++ b/leetcode/basic_calculator/tests.py @@ -0,0 +1,68 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestBasicCalculator: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, expected", + [ + ("1 + 1", 2), + (" 2-1 + 2 ", 3), + ("(1+(4+5+2)-3)+(6+8)", 23), + ("1", 1), + ("-1", -1), + ("-(1+2)", -3), + ("2147483647", 2147483647), + ("1-1+1", 1), + # Additional edge cases + ("0", 0), + ("-0", 0), + ("()", 0), + ("((()))", 0), + ("1+(2+3)", 6), + ("(1+2)+3", 6), + ("1-(2+3)", -4), + ("(1-2)+3", 2), + ("-(-1)", 1), + ("-(-(-1))", -1), + ("1000000-999999", 1), + ("10+20-30+40", 40), + ("((1+2)+(3+4))", 10), + ("1+(2-(3+4))", -4), + ("-(1+(2+3))", -6), + (" 1 + 2 ", 3), + ("123+456", 579), + ("-2147483648", -2147483648), + ], + ) + @logged_test + def test_calculate(self, s: str, expected: int): + result = self.solution.calculate(s) + assert result == expected + + @pytest.mark.parametrize( + "s, error_msg", + [ + ("(1+2", "Mismatched parentheses"), + ("1+2)", "Mismatched parentheses"), + ("((1+2)", "Mismatched parentheses"), + ("1+2))", "Mismatched parentheses"), + ("1*2", r"Invalid character: '\*'"), + ("1/2", "Invalid character: '/'"), + ("1%2", "Invalid character: '%'"), + ("1^2", r"Invalid character: '\^'"), + ("1&2", "Invalid character: '&'"), + ("a+b", "Invalid character: 'a'"), + ("1+2.5", r"Invalid character: '\.'"), + ], + ) + @logged_test + def test_calculate_invalid_input(self, s: str, error_msg: str): + with pytest.raises(ValueError, match=error_msg): + self.solution.calculate(s) From ad6325cf657e7c1b413f74768b1850a91ce44a3c Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 14:33:04 +0700 Subject: [PATCH 07/26] feat: add Merge k Sorted Lists --- .../leetcode/json/merge_k_sorted_lists.json | 46 ++++++++++ Makefile | 2 +- leetcode/basic_calculator/playground.ipynb | 26 ++++-- leetcode/merge_k_sorted_lists/README.md | 61 ++++++++++++++ leetcode/merge_k_sorted_lists/__init__.py | 0 .../merge_k_sorted_lists/playground.ipynb | 83 +++++++++++++++++++ leetcode/merge_k_sorted_lists/solution.py | 35 ++++++++ leetcode/merge_k_sorted_lists/tests.py | 45 ++++++++++ 8 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 .templates/leetcode/json/merge_k_sorted_lists.json create mode 100644 leetcode/merge_k_sorted_lists/README.md create mode 100644 leetcode/merge_k_sorted_lists/__init__.py create mode 100644 leetcode/merge_k_sorted_lists/playground.ipynb create mode 100644 leetcode/merge_k_sorted_lists/solution.py create mode 100644 leetcode/merge_k_sorted_lists/tests.py diff --git a/.templates/leetcode/json/merge_k_sorted_lists.json b/.templates/leetcode/json/merge_k_sorted_lists.json new file mode 100644 index 0000000..f5a1e19 --- /dev/null +++ b/.templates/leetcode/json/merge_k_sorted_lists.json @@ -0,0 +1,46 @@ +{ + "problem_name": "merge_k_sorted_lists", + "solution_class_name": "Solution", + "problem_number": "23", + "problem_title": "Merge k Sorted Lists", + "difficulty": "Hard", + "topics": "Linked List, Divide and Conquer, Heap (Priority Queue), Merge Sort", + "tags": ["grind-75"], + "readme_description": "You are given an array of `k` linked-lists `lists`, each linked-list is sorted in ascending order.\n\n*Merge all the linked-lists into one sorted linked-list and return it.*", + "readme_examples": [ + { + "content": "```\nInput: lists = [[1,4,5],[1,3,4],[2,6]]\nOutput: [1,1,2,3,4,4,5,6]\n```\n**Explanation:** The linked-lists are:\n```\n[\n 1->4->5,\n 1->3->4,\n 2->6\n]\n```\nmerging them into one sorted linked list:\n```\n1->1->2->3->4->4->5->6\n```" + }, + { "content": "```\nInput: lists = []\nOutput: []\n```" }, + { "content": "```\nInput: lists = [[]]\nOutput: []\n```" } + ], + "readme_constraints": "- `k == lists.length`\n- `0 <= k <= 10^4`\n- `0 <= lists[i].length <= 500`\n- `-10^4 <= lists[i][j] <= 10^4`\n- `lists[i]` is sorted in ascending order.\n- The sum of `lists[i].length` will not exceed `10^4`.", + "readme_additional": "", + "solution_imports": "from leetcode_py import ListNode", + "solution_methods": [ + { + "name": "merge_k_lists", + "parameters": "lists: list[ListNode | None]", + "return_type": "ListNode | None", + "dummy_return": "None" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import ListNode\nfrom .solution import Solution", + "test_class_name": "MergeKSortedLists", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_merge_k_lists", + "parametrize": "lists_data, expected_data", + "parametrize_typed": "lists_data: list[list[int]], expected_data: list[int]", + "test_cases": "[([[1, 4, 5], [1, 3, 4], [2, 6]], [1, 1, 2, 3, 4, 4, 5, 6]), ([], []), ([[]], []), ([[1]], [1]), ([[1, 2], [3, 4]], [1, 2, 3, 4]), ([[5], [1, 3], [2, 4, 6]], [1, 2, 3, 4, 5, 6]), ([[-1, 0, 1], [-2, 2]], [-2, -1, 0, 1, 2]), ([[1, 1, 1], [2, 2, 2]], [1, 1, 1, 2, 2, 2]), ([[]], []), ([[], [1], []], [1])]", + "body": "lists = [ListNode.from_list(lst) for lst in lists_data]\nresult = self.solution.merge_k_lists(lists)\nexpected = ListNode.from_list(expected_data)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import ListNode", + "playground_test_case": "# Example test case\nlists_data = [[1, 4, 5], [1, 3, 4], [2, 6]]\nlists = [ListNode.from_list(lst) for lst in lists_data]\nexpected_data = [1, 1, 2, 3, 4, 4, 5, 6]", + "playground_execution": "result = Solution().merge_k_lists(lists)\nListNode.to_list(result) if result else []", + "playground_assertion": "expected = ListNode.from_list(expected_data)\nassert result == expected" +} diff --git a/Makefile b/Makefile index d5052e5..6bffaed 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= basic_calculator +PROBLEM ?= merge_k_sorted_lists FORCE ?= 0 COMMA := , diff --git a/leetcode/basic_calculator/playground.ipynb b/leetcode/basic_calculator/playground.ipynb index d6fe346..1724458 100644 --- a/leetcode/basic_calculator/playground.ipynb +++ b/leetcode/basic_calculator/playground.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "setup", "metadata": {}, "outputs": [], @@ -24,17 +24,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "execute", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "23" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "result = Solution().calculate(s)\nresult" + "result = Solution().calculate(s)\n", + "result" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "test", "metadata": {}, "outputs": [], @@ -57,7 +69,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/merge_k_sorted_lists/README.md b/leetcode/merge_k_sorted_lists/README.md new file mode 100644 index 0000000..7912c38 --- /dev/null +++ b/leetcode/merge_k_sorted_lists/README.md @@ -0,0 +1,61 @@ +# Merge k Sorted Lists + +**Difficulty:** Hard +**Topics:** Linked List, Divide and Conquer, Heap (Priority Queue), Merge Sort +**Tags:** grind-75 + +**LeetCode:** [Problem 23](https://leetcode.com/problems/merge-k-sorted-lists/description/) + +## Problem Description + +You are given an array of `k` linked-lists `lists`, each linked-list is sorted in ascending order. + +_Merge all the linked-lists into one sorted linked-list and return it._ + +## Examples + +### Example 1: + +``` +Input: lists = [[1,4,5],[1,3,4],[2,6]] +Output: [1,1,2,3,4,4,5,6] +``` + +**Explanation:** The linked-lists are: + +``` +[ + 1->4->5, + 1->3->4, + 2->6 +] +``` + +merging them into one sorted linked list: + +``` +1->1->2->3->4->4->5->6 +``` + +### Example 2: + +``` +Input: lists = [] +Output: [] +``` + +### Example 3: + +``` +Input: lists = [[]] +Output: [] +``` + +## Constraints + +- `k == lists.length` +- `0 <= k <= 10^4` +- `0 <= lists[i].length <= 500` +- `-10^4 <= lists[i][j] <= 10^4` +- `lists[i]` is sorted in ascending order. +- The sum of `lists[i].length` will not exceed `10^4`. diff --git a/leetcode/merge_k_sorted_lists/__init__.py b/leetcode/merge_k_sorted_lists/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/merge_k_sorted_lists/playground.ipynb b/leetcode/merge_k_sorted_lists/playground.ipynb new file mode 100644 index 0000000..f374726 --- /dev/null +++ b/leetcode/merge_k_sorted_lists/playground.ipynb @@ -0,0 +1,83 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import ListNode" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "lists_data = [[1, 4, 5], [1, 3, 4], [2, 6]]\n", + "lists = [ListNode.from_list(lst) for lst in lists_data]\n", + "expected_data = [1, 1, 2, 3, 4, 4, 5, 6]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 1, 2, 3, 4, 4, 5, 6]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().merge_k_lists(lists)\n", + "ListNode.to_list(result) if result else []" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "expected = ListNode.from_list(expected_data)\n", + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/merge_k_sorted_lists/solution.py b/leetcode/merge_k_sorted_lists/solution.py new file mode 100644 index 0000000..d6ed124 --- /dev/null +++ b/leetcode/merge_k_sorted_lists/solution.py @@ -0,0 +1,35 @@ +from leetcode_py import ListNode + + +class Solution: + # Time: O(n log k) where n is total nodes, k is number of lists + # Space: O(log k) for recursion stack + def merge_k_lists(self, lists: list[ListNode | None]) -> ListNode | None: + if not lists: + return None + return self._divide_conquer(lists, 0, len(lists) - 1) + + def _divide_conquer(self, lists: list[ListNode | None], left: int, right: int) -> ListNode | None: + if left == right: + return lists[left] + + mid = (left + right) // 2 + l1 = self._divide_conquer(lists, left, mid) + l2 = self._divide_conquer(lists, mid + 1, right) + return self._merge_two(l1, l2) + + def _merge_two(self, l1: ListNode | None, l2: ListNode | None) -> ListNode | None: + dummy = ListNode(0) + curr = dummy + + while l1 and l2: + if l1.val <= l2.val: + curr.next = l1 + l1 = l1.next + else: + curr.next = l2 + l2 = l2.next + curr = curr.next + + curr.next = l1 or l2 + return dummy.next diff --git a/leetcode/merge_k_sorted_lists/tests.py b/leetcode/merge_k_sorted_lists/tests.py new file mode 100644 index 0000000..7249fc0 --- /dev/null +++ b/leetcode/merge_k_sorted_lists/tests.py @@ -0,0 +1,45 @@ +import pytest + +from leetcode_py import ListNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMergeKSortedLists: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "lists_data, expected_data", + [ + ([[1, 4, 5], [1, 3, 4], [2, 6]], [1, 1, 2, 3, 4, 4, 5, 6]), + ([], []), + ([[]], []), + ([[1]], [1]), + ([[1, 2], [3, 4]], [1, 2, 3, 4]), + ([[5], [1, 3], [2, 4, 6]], [1, 2, 3, 4, 5, 6]), + ([[-1, 0, 1], [-2, 2]], [-2, -1, 0, 1, 2]), + ([[1, 1, 1], [2, 2, 2]], [1, 1, 1, 2, 2, 2]), + ([[]], []), + ([[], [1], []], [1]), + ([[0]], [0]), + ([[-10, -5, -1], [-8, -3], [-7, -2, 0]], [-10, -8, -7, -5, -3, -2, -1, 0]), + ([[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5]), + ([[10], [9], [8], [7]], [7, 8, 9, 10]), + ([[1, 100], [2, 99], [3, 98]], [1, 2, 3, 98, 99, 100]), + ([[], [], []], []), + ([[0, 0, 0], [0, 0]], [0, 0, 0, 0, 0]), + ([[1, 3, 5, 7], [2, 4, 6, 8]], [1, 2, 3, 4, 5, 6, 7, 8]), + ( + [[100, 200, 300], [50, 150, 250], [75, 125, 175]], + [50, 75, 100, 125, 150, 175, 200, 250, 300], + ), + ], + ) + @logged_test + def test_merge_k_lists(self, lists_data: list[list[int]], expected_data: list[int]): + lists = [ListNode.from_list(lst) for lst in lists_data] + result = self.solution.merge_k_lists(lists) + expected = ListNode.from_list(expected_data) + assert result == expected From e586905968ae0555f42a95182a00af6a9d7e0219 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 14:48:31 +0700 Subject: [PATCH 08/26] feat: add DoublyListNode --- leetcode/lru_cache/solution.py | 75 ++++++++++ leetcode/lru_cache/tests.py | 27 +++- .../data_structures/doubly_list_node.py | 128 ++++++++++++++++++ 3 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 leetcode_py/data_structures/doubly_list_node.py diff --git a/leetcode/lru_cache/solution.py b/leetcode/lru_cache/solution.py index 33cdbb6..c0bcd7a 100644 --- a/leetcode/lru_cache/solution.py +++ b/leetcode/lru_cache/solution.py @@ -1,5 +1,7 @@ from collections import OrderedDict +from leetcode_py.data_structures.doubly_list_node import DoublyListNode + class LRUCache: # Space: O(capacity) @@ -31,3 +33,76 @@ def put(self, key: int, value: int) -> None: self.cache.popitem(last=False) self.cache[key] = value + + +class CacheNode(DoublyListNode[int]): + def __init__(self, key: int = 0, val: int = 0) -> None: + super().__init__(val) + self.key = key + + +class LRUCacheWithDoublyList: + def __init__(self, capacity: int) -> None: + self.capacity = capacity + self.cache: dict[int, CacheNode] = {} + + # Dummy head and tail nodes + self.head = CacheNode() + self.tail = CacheNode() + self.head.next = self.tail + self.tail.prev = self.head + + def _add_node(self, node: CacheNode) -> None: + """Add node right after head""" + node.prev = self.head + node.next = self.head.next + if self.head.next: + self.head.next.prev = node + self.head.next = node + + def _remove_node(self, node: CacheNode) -> None: + """Remove node from list""" + if node.prev: + node.prev.next = node.next + if node.next: + node.next.prev = node.prev + + def _move_to_head(self, node: CacheNode) -> None: + """Move node to head (most recent)""" + self._remove_node(node) + self._add_node(node) + + def _pop_tail(self) -> CacheNode: + """Remove last node before tail""" + last_node = self.tail.prev + assert isinstance(last_node, CacheNode), "Expected CacheNode" + self._remove_node(last_node) + return last_node + + def get(self, key: int) -> int: + node = self.cache.get(key) + if not node: + return -1 + + # Move to head (most recent) + self._move_to_head(node) + return node.val + + def put(self, key: int, value: int) -> None: + node = self.cache.get(key) + + if node: + # Update existing + node.val = value + self._move_to_head(node) + else: + # Add new + new_node = CacheNode(key, value) + + if len(self.cache) >= self.capacity: + # Remove LRU + tail = self._pop_tail() + del self.cache[tail.key] + + self.cache[key] = new_node + self._add_node(new_node) diff --git a/leetcode/lru_cache/tests.py b/leetcode/lru_cache/tests.py index ad8b5e4..8d3e6e8 100644 --- a/leetcode/lru_cache/tests.py +++ b/leetcode/lru_cache/tests.py @@ -2,10 +2,11 @@ from leetcode_py.test_utils import logged_test -from .solution import LRUCache +from .solution import LRUCache, LRUCacheWithDoublyList class TestLRUCache: + @pytest.mark.parametrize("lru_class", [LRUCache, LRUCacheWithDoublyList]) @pytest.mark.parametrize( "operations, inputs, expected", [ @@ -13,16 +14,32 @@ class TestLRUCache: ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"], [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]], [None, None, None, 1, None, -1, None, -1, 3, 4], - ) + ), + ( + ["LRUCache", "get", "put", "get", "put", "put", "get", "get"], + [[2], [2], [2, 6], [1], [1, 5], [1, 2], [1], [2]], + [None, -1, None, -1, None, None, 2, 6], + ), + ( + ["LRUCache", "put", "get", "put", "get", "get"], + [[1], [2, 1], [2], [3, 2], [2], [3]], + [None, None, 1, None, -1, 2], + ), ], ) @logged_test - def test_lru_cache(self, operations: list[str], inputs: list[list[int]], expected: list[int | None]): - cache: LRUCache | None = None + def test_lru_cache( + self, + lru_class: type[LRUCache | LRUCacheWithDoublyList], + operations: list[str], + inputs: list[list[int]], + expected: list[int | None], + ): + cache = None results: list[int | None] = [] for i, op in enumerate(operations): if op == "LRUCache": - cache = LRUCache(inputs[i][0]) + cache = lru_class(inputs[i][0]) results.append(None) elif op == "get" and cache is not None: results.append(cache.get(inputs[i][0])) diff --git a/leetcode_py/data_structures/doubly_list_node.py b/leetcode_py/data_structures/doubly_list_node.py new file mode 100644 index 0000000..74996fa --- /dev/null +++ b/leetcode_py/data_structures/doubly_list_node.py @@ -0,0 +1,128 @@ +from typing import Generic, TypeVar + +# TODO: Remove TypeVar when minimum Python version is 3.12+ (use class DoublyListNode[T]: syntax) +T = TypeVar("T") + + +class DoublyListNode(Generic[T]): + def __init__( + self, val: T, prev: "DoublyListNode[T] | None" = None, next: "DoublyListNode[T] | None" = None + ): + self.val = val + self.prev = prev + self.next = next + + @classmethod + def from_list(cls, arr: list[T]) -> "DoublyListNode[T] | None": + if not arr: + return None + head = cls(arr[0]) + current = head + for val in arr[1:]: + new_node = cls(val, prev=current) + current.next = new_node + current = new_node + return head + + def _has_cycle(self) -> bool: + """Detect cycle using Floyd's algorithm in forward direction.""" + slow = fast = self + while fast and fast.next and fast.next.next: + assert slow.next is not None + slow = slow.next + fast = fast.next.next + if slow is fast: + return True + return False + + def to_list(self, max_length: int = 1000) -> list[T]: + result: list[T] = [] + current: "DoublyListNode[T] | None" = self + visited: set[int] = set() + + while current and len(result) < max_length: + if id(current) in visited: + break + visited.add(id(current)) + result.append(current.val) + current = current.next + return result + + def __str__(self) -> str: + if self._has_cycle(): + result: list[str] = [] + current: "DoublyListNode[T] | None" = self + visited: dict[int, int] = {} + position = 0 + + while current: + if id(current) in visited: + cycle_pos = visited[id(current)] + cycle_val = result[cycle_pos] + result_str = " <-> ".join(result) + return f"{result_str} <-> ... (cycle back to {cycle_val})" + + visited[id(current)] = position + result.append(str(current.val)) + current = current.next + position += 1 + + values = self.to_list() + result_str = " <-> ".join(str(val) for val in values) + if len(values) >= 1000: + result_str += " <-> ... (long list)" + return result_str + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.to_list()})" + + def _repr_html_(self) -> str: + """Generate HTML representation with bidirectional arrows.""" + try: + import graphviz + except ImportError: + return f"
{self.__str__()}
" + + dot = graphviz.Digraph(comment="DoublyLinkedList") + dot.attr(rankdir="LR") + dot.attr("node", shape="box", style="rounded,filled", fillcolor="lightgreen") + dot.attr("edge", color="black") + + current: "DoublyListNode[T] | None" = self + visited: dict[int, int] = {} + node_id = 0 + + while current: + if id(current) in visited: + cycle_target = visited[id(current)] + dot.edge( + f"node_{node_id - 1}", + f"node_{cycle_target}", + color="red", + style="dashed", + label="cycle", + ) + break + + visited[id(current)] = node_id + dot.node(f"node_{node_id}", str(current.val)) + + if current.next and id(current.next) not in visited: + # Forward edge + dot.edge(f"node_{node_id}", f"node_{node_id + 1}", label="next") + # Backward edge + dot.edge(f"node_{node_id + 1}", f"node_{node_id}", label="prev", color="blue") + + current = current.next + node_id += 1 + + return dot.pipe(format="svg", encoding="utf-8") + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DoublyListNode): + return False + + if self._has_cycle() or other._has_cycle(): + return False + + return self.to_list() == other.to_list() From 5b27fa9207a7722641aaff5450115f789d3f8013 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 15:22:37 +0700 Subject: [PATCH 09/26] feat: add Ransom Note --- .templates/leetcode/json/ransom_note.json | 44 ++++++++++ .../{{cookiecutter.problem_name}}/solution.py | 2 +- Makefile | 2 +- leetcode/ransom_note/README.md | 41 ++++++++++ leetcode/ransom_note/__init__.py | 0 leetcode/ransom_note/playground.ipynb | 80 +++++++++++++++++++ leetcode/ransom_note/solution.py | 18 +++++ leetcode/ransom_note/tests.py | 38 +++++++++ 8 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 .templates/leetcode/json/ransom_note.json create mode 100644 leetcode/ransom_note/README.md create mode 100644 leetcode/ransom_note/__init__.py create mode 100644 leetcode/ransom_note/playground.ipynb create mode 100644 leetcode/ransom_note/solution.py create mode 100644 leetcode/ransom_note/tests.py diff --git a/.templates/leetcode/json/ransom_note.json b/.templates/leetcode/json/ransom_note.json new file mode 100644 index 0000000..013a6da --- /dev/null +++ b/.templates/leetcode/json/ransom_note.json @@ -0,0 +1,44 @@ +{ + "problem_name": "ransom_note", + "solution_class_name": "Solution", + "problem_number": "383", + "problem_title": "Ransom Note", + "difficulty": "Easy", + "topics": "Hash Table, String, Counting", + "tags": ["grind-75"], + "readme_description": "Given two strings `ransomNote` and `magazine`, return `true` if `ransomNote` can be constructed by using the letters from `magazine` and `false` otherwise.\n\nEach letter in `magazine` can only be used once in `ransomNote`.", + "readme_examples": [ + { "content": "```\nInput: ransomNote = \"a\", magazine = \"b\"\nOutput: false\n```" }, + { "content": "```\nInput: ransomNote = \"aa\", magazine = \"ab\"\nOutput: false\n```" }, + { "content": "```\nInput: ransomNote = \"aa\", magazine = \"aab\"\nOutput: true\n```" } + ], + "readme_constraints": "- 1 <= ransomNote.length, magazine.length <= 10^5\n- ransomNote and magazine consist of lowercase English letters.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "can_construct", + "parameters": "ransom_note: str, magazine: str", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "RansomNote", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_can_construct", + "parametrize": "ransom_note, magazine, expected", + "parametrize_typed": "ransom_note: str, magazine: str, expected: bool", + "test_cases": "[('a', 'b', False), ('aa', 'ab', False), ('aa', 'aab', True), ('aab', 'baa', True)]", + "body": "result = self.solution.can_construct(ransom_note, magazine)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nransom_note = 'aa'\nmagazine = 'aab'\nexpected = True", + "playground_execution": "result = Solution().can_construct(ransom_note, magazine)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py index 467ba85..a2014d3 100644 --- a/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py +++ b/.templates/leetcode/{{cookiecutter.problem_name}}/solution.py @@ -1,5 +1,5 @@ {{cookiecutter.solution_imports}} - +{# TODO: add helper class like class Node: .... #} class {{cookiecutter.solution_class_name}}: {%- for _, methods in cookiecutter._solution_methods | dictsort %} {%- for method in methods %} diff --git a/Makefile b/Makefile index 6bffaed..08c9ba0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= merge_k_sorted_lists +PROBLEM ?= ransom_note FORCE ?= 0 COMMA := , diff --git a/leetcode/ransom_note/README.md b/leetcode/ransom_note/README.md new file mode 100644 index 0000000..59c1c12 --- /dev/null +++ b/leetcode/ransom_note/README.md @@ -0,0 +1,41 @@ +# Ransom Note + +**Difficulty:** Easy +**Topics:** Hash Table, String, Counting +**Tags:** grind-75 + +**LeetCode:** [Problem 383](https://leetcode.com/problems/ransom-note/description/) + +## Problem Description + +Given two strings `ransomNote` and `magazine`, return `true` if `ransomNote` can be constructed by using the letters from `magazine` and `false` otherwise. + +Each letter in `magazine` can only be used once in `ransomNote`. + +## Examples + +### Example 1: + +``` +Input: ransomNote = "a", magazine = "b" +Output: false +``` + +### Example 2: + +``` +Input: ransomNote = "aa", magazine = "ab" +Output: false +``` + +### Example 3: + +``` +Input: ransomNote = "aa", magazine = "aab" +Output: true +``` + +## Constraints + +- 1 <= ransomNote.length, magazine.length <= 10^5 +- ransomNote and magazine consist of lowercase English letters. diff --git a/leetcode/ransom_note/__init__.py b/leetcode/ransom_note/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/ransom_note/playground.ipynb b/leetcode/ransom_note/playground.ipynb new file mode 100644 index 0000000..864b296 --- /dev/null +++ b/leetcode/ransom_note/playground.ipynb @@ -0,0 +1,80 @@ +{ + "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", + "ransom_note = \"aa\"\n", + "magazine = \"aab\"\n", + "expected = True" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().can_construct(ransom_note, magazine)\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/ransom_note/solution.py b/leetcode/ransom_note/solution.py new file mode 100644 index 0000000..f99deef --- /dev/null +++ b/leetcode/ransom_note/solution.py @@ -0,0 +1,18 @@ +from collections import Counter + + +class Solution: + # Time: O(m + n) where m = magazine length, n = ransom_note length + # Space: O(1) - at most 26 lowercase letters + def can_construct(self, ransom_note: str, magazine: str) -> bool: + if len(ransom_note) > len(magazine): + return False + + magazine_count = Counter(magazine) + + for char in ransom_note: + if magazine_count[char] == 0: + return False + magazine_count[char] -= 1 + + return True diff --git a/leetcode/ransom_note/tests.py b/leetcode/ransom_note/tests.py new file mode 100644 index 0000000..90881af --- /dev/null +++ b/leetcode/ransom_note/tests.py @@ -0,0 +1,38 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestRansomNote: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "ransom_note, magazine, expected", + [ + # Original test cases + ("a", "b", False), + ("aa", "ab", False), + ("aa", "aab", True), + ("aab", "baa", True), + # Edge cases + ("", "", True), # Both empty + ("", "abc", True), # Empty ransom note + ("a", "", False), # Empty magazine + ("a", "a", True), # Single char match + ("ab", "a", False), # Ransom longer than magazine + # More complex cases + ("abc", "aabbcc", True), # Multiple of each char + ("aab", "baa", True), # Same chars, different order + ("aaa", "aa", False), # Not enough of same char + ("abcd", "dcba", True), # All different chars + ("hello", "helloworld", True), # Ransom is substring + ("world", "hello", False), # Missing chars + ], + ) + @logged_test + def test_can_construct(self, ransom_note: str, magazine: str, expected: bool): + result = self.solution.can_construct(ransom_note, magazine) + assert result == expected From f0a2a39968d0d0b9102d869b68cf7ef5c312d73f Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 19:51:33 +0700 Subject: [PATCH 10/26] feat: add First Bad Version --- .../leetcode/json/first_bad_version.json | 56 +++++++++++++ Makefile | 2 +- leetcode/first_bad_version/README.md | 47 +++++++++++ leetcode/first_bad_version/__init__.py | 0 leetcode/first_bad_version/playground.ipynb | 81 +++++++++++++++++++ leetcode/first_bad_version/solution.py | 45 +++++++++++ leetcode/first_bad_version/tests.py | 17 ++++ 7 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/first_bad_version.json create mode 100644 leetcode/first_bad_version/README.md create mode 100644 leetcode/first_bad_version/__init__.py create mode 100644 leetcode/first_bad_version/playground.ipynb create mode 100644 leetcode/first_bad_version/solution.py create mode 100644 leetcode/first_bad_version/tests.py diff --git a/.templates/leetcode/json/first_bad_version.json b/.templates/leetcode/json/first_bad_version.json new file mode 100644 index 0000000..cdaf3bf --- /dev/null +++ b/.templates/leetcode/json/first_bad_version.json @@ -0,0 +1,56 @@ +{ + "problem_name": "first_bad_version", + "solution_class_name": "Solution", + "problem_number": "278", + "problem_title": "First Bad Version", + "difficulty": "Easy", + "topics": "Binary Search, Interactive", + "tags": ["grind-75"], + "readme_description": "You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad.\n\nSuppose you have `n` versions `[1, 2, ..., n]` and you want to find out the first bad one, which causes all the following ones to be bad.\n\nYou are given an API `bool isBadVersion(version)` which returns whether `version` is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API.", + "readme_examples": [ + { + "content": "```\nInput: n = 5, bad = 4\nOutput: 4\n```\n**Explanation:**\n```\ncall isBadVersion(3) -> false\ncall isBadVersion(5) -> true\ncall isBadVersion(4) -> true\n```\nThen 4 is the first bad version." + }, + { "content": "```\nInput: n = 1, bad = 1\nOutput: 1\n```" } + ], + "readme_constraints": "- 1 <= bad <= n <= 2^31 - 1", + "readme_additional": "**Note:** The `isBadVersion` API is already defined for you.", + "solution_imports": "", + "solution_methods": [ + { + "name": "is_bad_version", + "parameters": "version: int", + "return_type": "bool", + "dummy_return": "False" + }, + { + "name": "first_bad_version", + "parameters": "n: int", + "return_type": "int", + "dummy_return": "1" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "FirstBadVersion", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }, + { + "name": "is_bad_version", + "parameters": "version: int, bad: int", + "body": "return version >= bad" + } + ], + "test_methods": [ + { + "name": "test_first_bad_version", + "parametrize": "n, bad, expected", + "parametrize_typed": "n: int, bad: int, expected: int", + "test_cases": "[(5, 4, 4), (1, 1, 1), (3, 1, 1), (10, 7, 7), (5, -1, 1)]", + "body": "solution = Solution()\n# Mock is_bad_version for this test\nsolution.is_bad_version = lambda version: self.is_bad_version(version, bad)\nresult = solution.first_bad_version(n)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nn = 5\nbad = 4\nexpected = 4", + "playground_execution": "solution = Solution()\nsolution.is_bad_version = lambda version: version >= bad\nresult = solution.first_bad_version(n)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 08c9ba0..d4b1fa0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= ransom_note +PROBLEM ?= first_bad_version FORCE ?= 0 COMMA := , diff --git a/leetcode/first_bad_version/README.md b/leetcode/first_bad_version/README.md new file mode 100644 index 0000000..a710eb0 --- /dev/null +++ b/leetcode/first_bad_version/README.md @@ -0,0 +1,47 @@ +# First Bad Version + +**Difficulty:** Easy +**Topics:** Binary Search, Interactive +**Tags:** grind-75 + +**LeetCode:** [Problem 278](https://leetcode.com/problems/first-bad-version/description/) + +## Problem Description + +You are a product manager and currently leading a team to develop a new product. Unfortunately, the latest version of your product fails the quality check. Since each version is developed based on the previous version, all the versions after a bad version are also bad. + +Suppose you have `n` versions `[1, 2, ..., n]` and you want to find out the first bad one, which causes all the following ones to be bad. + +You are given an API `bool isBadVersion(version)` which returns whether `version` is bad. Implement a function to find the first bad version. You should minimize the number of calls to the API. + +## Examples + +### Example 1: + +``` +Input: n = 5, bad = 4 +Output: 4 +``` + +**Explanation:** + +``` +call isBadVersion(3) -> false +call isBadVersion(5) -> true +call isBadVersion(4) -> true +``` + +Then 4 is the first bad version. + +### Example 2: + +``` +Input: n = 1, bad = 1 +Output: 1 +``` + +## Constraints + +- 1 <= bad <= n <= 2^31 - 1 + +**Note:** The `isBadVersion` API is already defined for you. diff --git a/leetcode/first_bad_version/__init__.py b/leetcode/first_bad_version/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/first_bad_version/playground.ipynb b/leetcode/first_bad_version/playground.ipynb new file mode 100644 index 0000000..c0d9e87 --- /dev/null +++ b/leetcode/first_bad_version/playground.ipynb @@ -0,0 +1,81 @@ +{ + "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", + "n = 5\n", + "bad = 4\n", + "expected = 4" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solution = Solution(first_bad=bad)\n", + "result = solution.first_bad_version(n)\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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/first_bad_version/solution.py b/leetcode/first_bad_version/solution.py new file mode 100644 index 0000000..7fa3f49 --- /dev/null +++ b/leetcode/first_bad_version/solution.py @@ -0,0 +1,45 @@ +class Solution: + # TODO: template constraint + def __init__(self, first_bad): + self.is_bad_version = lambda version: version >= first_bad + + # Time: O(log n) + # Space: O(1) + def first_bad_version(self, n: int) -> int: + left = 1 + right = n + + while left < right: + mid = (left + right) // 2 + if self.is_bad_version(mid): + right = mid + else: + left = mid + 1 + + return right + + +# BISECT PATTERNS - General Binary Search +# Given: arr = [10,20,30,30,30,40,50], target = 30 +# 0 1 2 3 4 5 6 +# +# bisect_left: Find FIRST occurrence (leftmost insertion point) +# while left < right: +# if arr[mid] >= target: # >= keeps moving left +# right = mid +# Returns: 2 (index of first 30, value=30) +# [10,20,30,30,30,40,50] +# 0 1 2 3 4 5 6 +# ↑ index 2 +# +# bisect_right: Find position AFTER last occurrence +# while left < right: +# if arr[mid] > target: # > allows equal values +# right = mid +# Returns: 5 (index after last 30, value=40) +# [10,20,30,30,30,40,50] +# 0 1 2 3 4 5 6 +# ↑ index 5 +# +# Key difference: >= vs > in the condition +# This problem uses bisect_left pattern to find first bad version diff --git a/leetcode/first_bad_version/tests.py b/leetcode/first_bad_version/tests.py new file mode 100644 index 0000000..570373b --- /dev/null +++ b/leetcode/first_bad_version/tests.py @@ -0,0 +1,17 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestFirstBadVersion: + + @pytest.mark.parametrize( + "n, bad, expected", [(5, 4, 4), (1, 1, 1), (3, 1, 1), (10, 7, 7), (5, -1, 1)] + ) + @logged_test + def test_first_bad_version(self, n: int, bad: int, expected: int): + solution = Solution(first_bad=bad) + result = solution.first_bad_version(n) + assert result == expected From 49412c331f2c41eedc2ca09280f6eb4a288ee561 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 20:02:12 +0700 Subject: [PATCH 11/26] feat: Climbing Stairs --- .templates/leetcode/json/climbing_stairs.json | 42 ++++++++++ Makefile | 2 +- leetcode/climbing_stairs/README.md | 44 +++++++++++ leetcode/climbing_stairs/__init__.py | 0 leetcode/climbing_stairs/playground.ipynb | 79 +++++++++++++++++++ leetcode/climbing_stairs/solution.py | 16 ++++ leetcode/climbing_stairs/tests.py | 19 +++++ 7 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/climbing_stairs.json create mode 100644 leetcode/climbing_stairs/README.md create mode 100644 leetcode/climbing_stairs/__init__.py create mode 100644 leetcode/climbing_stairs/playground.ipynb create mode 100644 leetcode/climbing_stairs/solution.py create mode 100644 leetcode/climbing_stairs/tests.py diff --git a/.templates/leetcode/json/climbing_stairs.json b/.templates/leetcode/json/climbing_stairs.json new file mode 100644 index 0000000..37a0d7f --- /dev/null +++ b/.templates/leetcode/json/climbing_stairs.json @@ -0,0 +1,42 @@ +{ + "problem_name": "climbing_stairs", + "solution_class_name": "Solution", + "problem_number": "70", + "problem_title": "Climbing Stairs", + "difficulty": "Easy", + "topics": "Math, Dynamic Programming, Memoization", + "tags": ["grind-75"], + "readme_description": "You are climbing a staircase. It takes `n` steps to reach the top.\n\nEach time you can either climb `1` or `2` steps. In how many distinct ways can you climb to the top?", + "readme_examples": [ + { + "content": "```\nInput: n = 2\nOutput: 2\n```\n**Explanation:** There are two ways to climb to the top.\n1. 1 step + 1 step\n2. 2 steps" + }, + { + "content": "```\nInput: n = 3\nOutput: 3\n```\n**Explanation:** There are three ways to climb to the top.\n1. 1 step + 1 step + 1 step\n2. 1 step + 2 steps\n3. 2 steps + 1 step" + } + ], + "readme_constraints": "- 1 <= n <= 45", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { "name": "climb_stairs", "parameters": "n: int", "return_type": "int", "dummy_return": "1" } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "ClimbingStairs", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_climb_stairs", + "parametrize": "n, expected", + "parametrize_typed": "n: int, expected: int", + "test_cases": "[(1, 1), (2, 2), (3, 3), (4, 5), (5, 8), (6, 13), (10, 89), (20, 10946), (45, 1836311903)]", + "body": "result = self.solution.climb_stairs(n)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nn = 3\nexpected = 3", + "playground_execution": "result = Solution().climb_stairs(n)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index d4b1fa0..016303e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= first_bad_version +PROBLEM ?= climbing_stairs FORCE ?= 0 COMMA := , diff --git a/leetcode/climbing_stairs/README.md b/leetcode/climbing_stairs/README.md new file mode 100644 index 0000000..18ea888 --- /dev/null +++ b/leetcode/climbing_stairs/README.md @@ -0,0 +1,44 @@ +# Climbing Stairs + +**Difficulty:** Easy +**Topics:** Math, Dynamic Programming, Memoization +**Tags:** grind-75 + +**LeetCode:** [Problem 70](https://leetcode.com/problems/climbing-stairs/description/) + +## Problem Description + +You are climbing a staircase. It takes `n` steps to reach the top. + +Each time you can either climb `1` or `2` steps. In how many distinct ways can you climb to the top? + +## Examples + +### Example 1: + +``` +Input: n = 2 +Output: 2 +``` + +**Explanation:** There are two ways to climb to the top. + +1. 1 step + 1 step +2. 2 steps + +### Example 2: + +``` +Input: n = 3 +Output: 3 +``` + +**Explanation:** There are three ways to climb to the top. + +1. 1 step + 1 step + 1 step +2. 1 step + 2 steps +3. 2 steps + 1 step + +## Constraints + +- 1 <= n <= 45 diff --git a/leetcode/climbing_stairs/__init__.py b/leetcode/climbing_stairs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/climbing_stairs/playground.ipynb b/leetcode/climbing_stairs/playground.ipynb new file mode 100644 index 0000000..2edd00e --- /dev/null +++ b/leetcode/climbing_stairs/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", + "n = 3\n", + "expected = 3" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().climb_stairs(n)\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/climbing_stairs/solution.py b/leetcode/climbing_stairs/solution.py new file mode 100644 index 0000000..0a56db6 --- /dev/null +++ b/leetcode/climbing_stairs/solution.py @@ -0,0 +1,16 @@ +class Solution: + # Time: O(n) + # Space: O(1) + + # This follows Fibonacci pattern + # Standard Fib: F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5... + def climb_stairs(self, n: int) -> int: + if n <= 2: + return n + + prev2, prev1 = 1, 2 + for _ in range(3, n + 1): + current = prev1 + prev2 + prev2, prev1 = prev1, current + + return prev1 diff --git a/leetcode/climbing_stairs/tests.py b/leetcode/climbing_stairs/tests.py new file mode 100644 index 0000000..f3c338f --- /dev/null +++ b/leetcode/climbing_stairs/tests.py @@ -0,0 +1,19 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestClimbingStairs: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "n, expected", + [(1, 1), (2, 2), (3, 3), (4, 5), (5, 8), (6, 13), (10, 89), (20, 10946), (45, 1836311903)], + ) + @logged_test + def test_climb_stairs(self, n: int, expected: int): + result = self.solution.climb_stairs(n) + assert result == expected From a93e7614fee408db3367742dce6819ab330acf99 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 20:11:38 +0700 Subject: [PATCH 12/26] feat: add Majority Element --- .../leetcode/json/majority_element.json | 43 ++++++++++ Makefile | 2 +- leetcode/majority_element/README.md | 37 +++++++++ leetcode/majority_element/__init__.py | 0 leetcode/majority_element/playground.ipynb | 79 +++++++++++++++++++ leetcode/majority_element/solution.py | 14 ++++ leetcode/majority_element/tests.py | 19 +++++ 7 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/majority_element.json create mode 100644 leetcode/majority_element/README.md create mode 100644 leetcode/majority_element/__init__.py create mode 100644 leetcode/majority_element/playground.ipynb create mode 100644 leetcode/majority_element/solution.py create mode 100644 leetcode/majority_element/tests.py diff --git a/.templates/leetcode/json/majority_element.json b/.templates/leetcode/json/majority_element.json new file mode 100644 index 0000000..135a1cf --- /dev/null +++ b/.templates/leetcode/json/majority_element.json @@ -0,0 +1,43 @@ +{ + "problem_name": "majority_element", + "solution_class_name": "Solution", + "problem_number": "169", + "problem_title": "Majority Element", + "difficulty": "Easy", + "topics": "Array, Hash Table, Divide and Conquer, Sorting, Counting", + "tags": ["grind-75"], + "readme_description": "Given an array `nums` of size `n`, return the majority element.\n\nThe majority element is the element that appears more than `\u230an / 2\u230b` times. You may assume that the majority element always exists in the array.", + "readme_examples": [ + { "content": "```\nInput: nums = [3,2,3]\nOutput: 3\n```" }, + { "content": "```\nInput: nums = [2,2,1,1,1,2,2]\nOutput: 2\n```" } + ], + "readme_constraints": "- n == nums.length\n- 1 <= n <= 5 * 10^4\n- -10^9 <= nums[i] <= 10^9", + "readme_additional": "**Follow-up:** Could you solve the problem in linear time and in O(1) space?", + "solution_imports": "", + "solution_methods": [ + { + "name": "majority_element", + "parameters": "nums: list[int]", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "MajorityElement", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_majority_element", + "parametrize": "nums, expected", + "parametrize_typed": "nums: list[int], expected: int", + "test_cases": "[([3,2,3], 3), ([2,2,1,1,1,2,2], 2), ([1], 1), ([1,1,2], 1), ([2,2,2,1,1], 2)]", + "body": "result = self.solution.majority_element(nums)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [3,2,3]\nexpected = 3", + "playground_execution": "result = Solution().majority_element(nums)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 016303e..7e4dbaa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= climbing_stairs +PROBLEM ?= majority_element FORCE ?= 0 COMMA := , diff --git a/leetcode/majority_element/README.md b/leetcode/majority_element/README.md new file mode 100644 index 0000000..3822272 --- /dev/null +++ b/leetcode/majority_element/README.md @@ -0,0 +1,37 @@ +# Majority Element + +**Difficulty:** Easy +**Topics:** Array, Hash Table, Divide and Conquer, Sorting, Counting +**Tags:** grind-75 + +**LeetCode:** [Problem 169](https://leetcode.com/problems/majority-element/description/) + +## Problem Description + +Given an array `nums` of size `n`, return the majority element. + +The majority element is the element that appears more than `⌊n / 2⌋` times. You may assume that the majority element always exists in the array. + +## Examples + +### Example 1: + +``` +Input: nums = [3,2,3] +Output: 3 +``` + +### Example 2: + +``` +Input: nums = [2,2,1,1,1,2,2] +Output: 2 +``` + +## Constraints + +- n == nums.length +- 1 <= n <= 5 \* 10^4 +- -10^9 <= nums[i] <= 10^9 + +**Follow-up:** Could you solve the problem in linear time and in O(1) space? diff --git a/leetcode/majority_element/__init__.py b/leetcode/majority_element/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/majority_element/playground.ipynb b/leetcode/majority_element/playground.ipynb new file mode 100644 index 0000000..de16ac0 --- /dev/null +++ b/leetcode/majority_element/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", + "nums = [3, 2, 3]\n", + "expected = 3" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().majority_element(nums)\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/majority_element/solution.py b/leetcode/majority_element/solution.py new file mode 100644 index 0000000..1499c32 --- /dev/null +++ b/leetcode/majority_element/solution.py @@ -0,0 +1,14 @@ +class Solution: + # Time: O(n) + # Space: O(1) + def majority_element(self, nums: list[int]) -> int: + # Boyer-Moore Voting Algorithm + candidate = 0 + count = 0 + + for num in nums: + if count == 0: + candidate = num + count += 1 if num == candidate else -1 + + return candidate diff --git a/leetcode/majority_element/tests.py b/leetcode/majority_element/tests.py new file mode 100644 index 0000000..5414f0f --- /dev/null +++ b/leetcode/majority_element/tests.py @@ -0,0 +1,19 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMajorityElement: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, expected", + [([3, 2, 3], 3), ([2, 2, 1, 1, 1, 2, 2], 2), ([1], 1), ([1, 1, 2], 1), ([2, 2, 2, 1, 1], 2)], + ) + @logged_test + def test_majority_element(self, nums: list[int], expected: int): + result = self.solution.majority_element(nums) + assert result == expected From 1e319751a87eb9d2e2b9bb74f227cea2260eac06 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 20:20:52 +0700 Subject: [PATCH 13/26] feat: add Diameter of Binary Tree --- .../json/diameter_of_binary_tree.json | 45 +++++ Makefile | 2 +- leetcode/diameter_of_binary_tree/README.md | 40 ++++ leetcode/diameter_of_binary_tree/__init__.py | 0 .../diameter_of_binary_tree/playground.ipynb | 171 ++++++++++++++++++ leetcode/diameter_of_binary_tree/solution.py | 22 +++ leetcode/diameter_of_binary_tree/tests.py | 31 ++++ 7 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/diameter_of_binary_tree.json create mode 100644 leetcode/diameter_of_binary_tree/README.md create mode 100644 leetcode/diameter_of_binary_tree/__init__.py create mode 100644 leetcode/diameter_of_binary_tree/playground.ipynb create mode 100644 leetcode/diameter_of_binary_tree/solution.py create mode 100644 leetcode/diameter_of_binary_tree/tests.py diff --git a/.templates/leetcode/json/diameter_of_binary_tree.json b/.templates/leetcode/json/diameter_of_binary_tree.json new file mode 100644 index 0000000..bf3d6bb --- /dev/null +++ b/.templates/leetcode/json/diameter_of_binary_tree.json @@ -0,0 +1,45 @@ +{ + "problem_name": "diameter_of_binary_tree", + "solution_class_name": "Solution", + "problem_number": "543", + "problem_title": "Diameter of Binary Tree", + "difficulty": "Easy", + "topics": "Tree, Depth-First Search, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given the `root` of a binary tree, return the length of the **diameter** of the tree.\n\nThe **diameter** of a binary tree is the **length** of the longest path between any two nodes in a tree. This path may or may not pass through the `root`.\n\nThe **length** of a path between two nodes is represented by the number of edges between them.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/03/06/diamtree.jpg)\n\n```\nInput: root = [1,2,3,4,5]\nOutput: 3\n```\n**Explanation:** 3 is the length of the path [4,2,1,3] or [5,2,1,3]." + }, + { "content": "```\nInput: root = [1,2]\nOutput: 1\n```" } + ], + "readme_constraints": "- The number of nodes in the tree is in the range [1, 10^4].\n- -100 <= Node.val <= 100", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "diameter_of_binary_tree", + "parameters": "root: TreeNode | None", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Solution", + "test_class_name": "DiameterOfBinaryTree", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_diameter_of_binary_tree", + "parametrize": "root_list, expected", + "parametrize_typed": "root_list: list[int | None], expected: int", + "test_cases": "[([1, 2, 3, 4, 5], 3), ([1, 2], 1)]", + "body": "root = TreeNode.from_list(root_list)\nresult = self.solution.diameter_of_binary_tree(root)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode", + "playground_test_case": "# Example test case\nroot_list: list[int | None] = [1, 2, 3, 4, 5]\nexpected = 3", + "playground_execution": "root = TreeNode.from_list(root_list)\nresult = Solution().diameter_of_binary_tree(root)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 7e4dbaa..0420dfd 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= majority_element +PROBLEM ?= diameter_of_binary_tree FORCE ?= 0 COMMA := , diff --git a/leetcode/diameter_of_binary_tree/README.md b/leetcode/diameter_of_binary_tree/README.md new file mode 100644 index 0000000..4b1580b --- /dev/null +++ b/leetcode/diameter_of_binary_tree/README.md @@ -0,0 +1,40 @@ +# Diameter of Binary Tree + +**Difficulty:** Easy +**Topics:** Tree, Depth-First Search, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 543](https://leetcode.com/problems/diameter-of-binary-tree/description/) + +## Problem Description + +Given the `root` of a binary tree, return the length of the **diameter** of the tree. + +The **diameter** of a binary tree is the **length** of the longest path between any two nodes in a tree. This path may or may not pass through the `root`. + +The **length** of a path between two nodes is represented by the number of edges between them. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/03/06/diamtree.jpg) + +``` +Input: root = [1,2,3,4,5] +Output: 3 +``` + +**Explanation:** 3 is the length of the path [4,2,1,3] or [5,2,1,3]. + +### Example 2: + +``` +Input: root = [1,2] +Output: 1 +``` + +## Constraints + +- The number of nodes in the tree is in the range [1, 10^4]. +- -100 <= Node.val <= 100 diff --git a/leetcode/diameter_of_binary_tree/__init__.py b/leetcode/diameter_of_binary_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/diameter_of_binary_tree/playground.ipynb b/leetcode/diameter_of_binary_tree/playground.ipynb new file mode 100644 index 0000000..4821880 --- /dev/null +++ b/leetcode/diameter_of_binary_tree/playground.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root_list: list[int | None] = [1, 2, 3, 4, 5]\n", + "expected = 3" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root = TreeNode.from_list(root_list)\n", + "result = Solution().diameter_of_binary_tree(root)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "248f3295", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "0->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "1->3\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([1, 2, 3, 4, 5])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root" + ] + }, + { + "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/diameter_of_binary_tree/solution.py b/leetcode/diameter_of_binary_tree/solution.py new file mode 100644 index 0000000..bd0cbd2 --- /dev/null +++ b/leetcode/diameter_of_binary_tree/solution.py @@ -0,0 +1,22 @@ +from leetcode_py import TreeNode + + +class Solution: + # Time: O(n) + # Space: O(h) + def diameter_of_binary_tree(self, root: TreeNode | None) -> int: + self.max_diameter = 0 + + def dfs(node: TreeNode | None) -> int: + if not node: + return 0 + + left = dfs(node.left) + right = dfs(node.right) + + self.max_diameter = max(self.max_diameter, left + right) + + return max(left, right) + 1 + + dfs(root) + return self.max_diameter diff --git a/leetcode/diameter_of_binary_tree/tests.py b/leetcode/diameter_of_binary_tree/tests.py new file mode 100644 index 0000000..914bab5 --- /dev/null +++ b/leetcode/diameter_of_binary_tree/tests.py @@ -0,0 +1,31 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestDiameterOfBinaryTree: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "root_list, expected", + [ + ([1, 2, 3, 4, 5], 3), + ([1, 2], 1), + ([1], 0), + ([], 0), + ([1, 2, 3, 4, 5, None, None, 6, 7], 4), + ([1, None, 2, None, 3, None, 4], 3), + ([1, 2, None, 3, None, 4], 3), + ([1, 2, 3], 2), + ([1, 2, 3, 4, None, None, 5], 4), + ], + ) + @logged_test + def test_diameter_of_binary_tree(self, root_list: list[int | None], expected: int): + root = TreeNode.from_list(root_list) + result = self.solution.diameter_of_binary_tree(root) + assert result == expected From 63dce8921b88af128b4b1dcada0df5be6a4a04f4 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 20:30:40 +0700 Subject: [PATCH 14/26] feat: add Middle of the Linked List --- .../json/middle_of_the_linked_list.json | 47 +++++++ Makefile | 2 +- leetcode/middle_of_the_linked_list/README.md | 42 ++++++ .../middle_of_the_linked_list/__init__.py | 0 .../playground.ipynb | 127 ++++++++++++++++++ .../middle_of_the_linked_list/solution.py | 13 ++ leetcode/middle_of_the_linked_list/tests.py | 31 +++++ 7 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/middle_of_the_linked_list.json create mode 100644 leetcode/middle_of_the_linked_list/README.md create mode 100644 leetcode/middle_of_the_linked_list/__init__.py create mode 100644 leetcode/middle_of_the_linked_list/playground.ipynb create mode 100644 leetcode/middle_of_the_linked_list/solution.py create mode 100644 leetcode/middle_of_the_linked_list/tests.py diff --git a/.templates/leetcode/json/middle_of_the_linked_list.json b/.templates/leetcode/json/middle_of_the_linked_list.json new file mode 100644 index 0000000..ac2deb4 --- /dev/null +++ b/.templates/leetcode/json/middle_of_the_linked_list.json @@ -0,0 +1,47 @@ +{ + "problem_name": "middle_of_the_linked_list", + "solution_class_name": "Solution", + "problem_number": "876", + "problem_title": "Middle of the Linked List", + "difficulty": "Easy", + "topics": "Linked List, Two Pointers", + "tags": ["grind-75"], + "readme_description": "Given the `head` of a singly linked list, return *the middle node of the linked list*.\n\nIf there are two middle nodes, return **the second middle** node.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/07/23/lc-midlist1.jpg)\n\n```\nInput: head = [1,2,3,4,5]\nOutput: [3,4,5]\n```\n**Explanation:** The middle node of the list is node 3." + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2021/07/23/lc-midlist2.jpg)\n\n```\nInput: head = [1,2,3,4,5,6]\nOutput: [4,5,6]\n```\n**Explanation:** Since the list has two middle nodes with values 3 and 4, we return the second one." + } + ], + "readme_constraints": "- The number of nodes in the list is in the range `[1, 100]`.\n- `1 <= Node.val <= 100`", + "readme_additional": "", + "solution_imports": "from leetcode_py import ListNode", + "solution_methods": [ + { + "name": "middle_node", + "parameters": "head: ListNode | None", + "return_type": "ListNode | None", + "dummy_return": "None" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import ListNode\nfrom .solution import Solution", + "test_class_name": "MiddleOfTheLinkedList", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_middle_node", + "parametrize": "head_list, expected_list", + "parametrize_typed": "head_list: list[int], expected_list: list[int]", + "test_cases": "[([1, 2, 3, 4, 5], [3, 4, 5]), ([1, 2, 3, 4, 5, 6], [4, 5, 6]), ([1], [1]), ([1, 2], [2])]", + "body": "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)\nresult = self.solution.middle_node(head)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import ListNode", + "playground_test_case": "# Example test case\nhead_list = [1, 2, 3, 4, 5]\nexpected_list = [3, 4, 5]", + "playground_execution": "head = ListNode.from_list(head_list)\nresult = Solution().middle_node(head)\nresult", + "playground_assertion": "expected = ListNode.from_list(expected_list)\nassert result == expected" +} diff --git a/Makefile b/Makefile index 0420dfd..7bfe3a4 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= diameter_of_binary_tree +PROBLEM ?= middle_of_the_linked_list FORCE ?= 0 COMMA := , diff --git a/leetcode/middle_of_the_linked_list/README.md b/leetcode/middle_of_the_linked_list/README.md new file mode 100644 index 0000000..1f36bf2 --- /dev/null +++ b/leetcode/middle_of_the_linked_list/README.md @@ -0,0 +1,42 @@ +# Middle of the Linked List + +**Difficulty:** Easy +**Topics:** Linked List, Two Pointers +**Tags:** grind-75 + +**LeetCode:** [Problem 876](https://leetcode.com/problems/middle-of-the-linked-list/description/) + +## Problem Description + +Given the `head` of a singly linked list, return _the middle node of the linked list_. + +If there are two middle nodes, return **the second middle** node. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/07/23/lc-midlist1.jpg) + +``` +Input: head = [1,2,3,4,5] +Output: [3,4,5] +``` + +**Explanation:** The middle node of the list is node 3. + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2021/07/23/lc-midlist2.jpg) + +``` +Input: head = [1,2,3,4,5,6] +Output: [4,5,6] +``` + +**Explanation:** Since the list has two middle nodes with values 3 and 4, we return the second one. + +## Constraints + +- The number of nodes in the list is in the range `[1, 100]`. +- `1 <= Node.val <= 100` diff --git a/leetcode/middle_of_the_linked_list/__init__.py b/leetcode/middle_of_the_linked_list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/middle_of_the_linked_list/playground.ipynb b/leetcode/middle_of_the_linked_list/playground.ipynb new file mode 100644 index 0000000..921ff0f --- /dev/null +++ b/leetcode/middle_of_the_linked_list/playground.ipynb @@ -0,0 +1,127 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import ListNode" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "head_list = [1, 2, 3, 4, 5]\n", + "expected_list = [3, 4, 5]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "node_0\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "node_1\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "node_0->node_1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "node_2\n", + "\n", + "5\n", + "\n", + "\n", + "\n", + "node_1->node_2\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "ListNode([3, 4, 5])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "head = ListNode.from_list(head_list)\n", + "result = Solution().middle_node(head)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "expected = ListNode.from_list(expected_list)\n", + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/middle_of_the_linked_list/solution.py b/leetcode/middle_of_the_linked_list/solution.py new file mode 100644 index 0000000..bec661f --- /dev/null +++ b/leetcode/middle_of_the_linked_list/solution.py @@ -0,0 +1,13 @@ +from leetcode_py import ListNode + + +class Solution: + # Time: O(n) + # Space: O(1) + def middle_node(self, head: ListNode | None) -> ListNode | None: + slow = fast = head + while fast and fast.next: + assert slow is not None + slow = slow.next + fast = fast.next.next + return slow diff --git a/leetcode/middle_of_the_linked_list/tests.py b/leetcode/middle_of_the_linked_list/tests.py new file mode 100644 index 0000000..7454186 --- /dev/null +++ b/leetcode/middle_of_the_linked_list/tests.py @@ -0,0 +1,31 @@ +import pytest + +from leetcode_py import ListNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMiddleOfTheLinkedList: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "head_list, expected_list", + [ + ([1, 2, 3, 4, 5], [3, 4, 5]), # odd length + ([1, 2, 3, 4, 5, 6], [4, 5, 6]), # even length + ([1], [1]), # single node + ([1, 2], [2]), # two nodes + ([1, 2, 3], [2, 3]), # three nodes + ([1, 2, 3, 4], [3, 4]), # four nodes + ([10, 20, 30, 40, 50, 60, 70], [40, 50, 60, 70]), # larger odd + ([5, 15, 25, 35], [25, 35]), # larger even + ], + ) + @logged_test + def test_middle_node(self, head_list: list[int], expected_list: list[int]): + head = ListNode.from_list(head_list) + expected = ListNode.from_list(expected_list) + result = self.solution.middle_node(head) + assert result == expected From a7ad8ba36a182c7cdf2f4daa1f37b8780a747ed3 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 20:43:30 +0700 Subject: [PATCH 15/26] feat: add Contains Duplicate --- .../leetcode/json/contains_duplicate.json | 48 ++++++++++++++++ Makefile | 2 +- leetcode/contains_duplicate/README.md | 43 ++++++++++++++ leetcode/contains_duplicate/__init__.py | 0 leetcode/contains_duplicate/playground.ipynb | 57 +++++++++++++++++++ leetcode/contains_duplicate/solution.py | 10 ++++ leetcode/contains_duplicate/tests.py | 32 +++++++++++ 7 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/contains_duplicate.json create mode 100644 leetcode/contains_duplicate/README.md create mode 100644 leetcode/contains_duplicate/__init__.py create mode 100644 leetcode/contains_duplicate/playground.ipynb create mode 100644 leetcode/contains_duplicate/solution.py create mode 100644 leetcode/contains_duplicate/tests.py diff --git a/.templates/leetcode/json/contains_duplicate.json b/.templates/leetcode/json/contains_duplicate.json new file mode 100644 index 0000000..23ce24b --- /dev/null +++ b/.templates/leetcode/json/contains_duplicate.json @@ -0,0 +1,48 @@ +{ + "problem_name": "contains_duplicate", + "solution_class_name": "Solution", + "problem_number": "217", + "problem_title": "Contains Duplicate", + "difficulty": "Easy", + "topics": "Array, Hash Table, Sorting", + "tags": ["grind-75"], + "readme_description": "Given an integer array `nums`, return `true` if any value appears **at least twice** in the array, and return `false` if every element is distinct.", + "readme_examples": [ + { + "content": "```\nInput: nums = [1,2,3,1]\nOutput: true\n```\n**Explanation:** The element 1 occurs at the indices 0 and 3." + }, + { + "content": "```\nInput: nums = [1,2,3,4]\nOutput: false\n```\n**Explanation:** All elements are distinct." + }, + { "content": "```\nInput: nums = [1,1,1,3,3,4,3,2,4,2]\nOutput: true\n```" } + ], + "readme_constraints": "- 1 <= nums.length <= 10^5\n- -10^9 <= nums[i] <= 10^9", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "contains_duplicate", + "parameters": "nums: list[int]", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "ContainsDuplicate", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_contains_duplicate", + "parametrize": "nums, expected", + "parametrize_typed": "nums: list[int], expected: bool", + "test_cases": "[([1, 2, 3, 1], True), ([1, 2, 3, 4], False), ([1, 1, 1, 3, 3, 4, 3, 2, 4, 2], True)]", + "body": "result = self.solution.contains_duplicate(nums)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [1, 2, 3, 1]\nexpected = True", + "playground_execution": "result = Solution().contains_duplicate(nums)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 7bfe3a4..ca1e121 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= middle_of_the_linked_list +PROBLEM ?= contains_duplicate FORCE ?= 0 COMMA := , diff --git a/leetcode/contains_duplicate/README.md b/leetcode/contains_duplicate/README.md new file mode 100644 index 0000000..c09c400 --- /dev/null +++ b/leetcode/contains_duplicate/README.md @@ -0,0 +1,43 @@ +# Contains Duplicate + +**Difficulty:** Easy +**Topics:** Array, Hash Table, Sorting +**Tags:** grind-75 + +**LeetCode:** [Problem 217](https://leetcode.com/problems/contains-duplicate/description/) + +## Problem Description + +Given an integer array `nums`, return `true` if any value appears **at least twice** in the array, and return `false` if every element is distinct. + +## Examples + +### Example 1: + +``` +Input: nums = [1,2,3,1] +Output: true +``` + +**Explanation:** The element 1 occurs at the indices 0 and 3. + +### Example 2: + +``` +Input: nums = [1,2,3,4] +Output: false +``` + +**Explanation:** All elements are distinct. + +### Example 3: + +``` +Input: nums = [1,1,1,3,3,4,3,2,4,2] +Output: true +``` + +## Constraints + +- 1 <= nums.length <= 10^5 +- -10^9 <= nums[i] <= 10^9 diff --git a/leetcode/contains_duplicate/__init__.py b/leetcode/contains_duplicate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/contains_duplicate/playground.ipynb b/leetcode/contains_duplicate/playground.ipynb new file mode 100644 index 0000000..3a84993 --- /dev/null +++ b/leetcode/contains_duplicate/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\nnums = [1, 2, 3, 1]\nexpected = True"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().contains_duplicate(nums)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/contains_duplicate/solution.py b/leetcode/contains_duplicate/solution.py new file mode 100644 index 0000000..b9bd400 --- /dev/null +++ b/leetcode/contains_duplicate/solution.py @@ -0,0 +1,10 @@ +class Solution: + # Time: O(n) + # Space: O(n) + def contains_duplicate(self, nums: list[int]) -> bool: + seen = set() + for num in nums: + if num in seen: + return True + seen.add(num) + return False diff --git a/leetcode/contains_duplicate/tests.py b/leetcode/contains_duplicate/tests.py new file mode 100644 index 0000000..3a1d6cc --- /dev/null +++ b/leetcode/contains_duplicate/tests.py @@ -0,0 +1,32 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestContainsDuplicate: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, expected", + [ + ([1, 2, 3, 1], True), + ([1, 2, 3, 4], False), + ([1, 1, 1, 3, 3, 4, 3, 2, 4, 2], True), + ([], False), + ([1], False), + ([1, 1], True), + ([-1, -2, -3, -1], True), + ([-1, -2, -3, -4], False), + ([0, 0], True), + ([1000000, 999999, 1000000], True), + (list(range(1000)), False), + ([1] * 1000, True), + ], + ) + @logged_test + def test_contains_duplicate(self, nums: list[int], expected: bool): + result = self.solution.contains_duplicate(nums) + assert result == expected From 6d4d393a5a85477b9b43562c9cbe2406172bd197 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 21:21:01 +0700 Subject: [PATCH 16/26] feat: add Flood Fill --- .templates/leetcode/json/flood_fill.json | 47 +++++++++++++++++++ Makefile | 2 +- leetcode/flood_fill/README.md | 51 +++++++++++++++++++++ leetcode/flood_fill/__init__.py | 0 leetcode/flood_fill/playground.ipynb | 57 ++++++++++++++++++++++++ leetcode/flood_fill/solution.py | 17 +++++++ leetcode/flood_fill/tests.py | 26 +++++++++++ 7 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/flood_fill.json create mode 100644 leetcode/flood_fill/README.md create mode 100644 leetcode/flood_fill/__init__.py create mode 100644 leetcode/flood_fill/playground.ipynb create mode 100644 leetcode/flood_fill/solution.py create mode 100644 leetcode/flood_fill/tests.py diff --git a/.templates/leetcode/json/flood_fill.json b/.templates/leetcode/json/flood_fill.json new file mode 100644 index 0000000..5cab119 --- /dev/null +++ b/.templates/leetcode/json/flood_fill.json @@ -0,0 +1,47 @@ +{ + "problem_name": "flood_fill", + "solution_class_name": "Solution", + "problem_number": "733", + "problem_title": "Flood Fill", + "difficulty": "Easy", + "topics": "Array, Depth-First Search, Breadth-First Search, Matrix", + "tags": ["grind-75"], + "readme_description": "You are given an image represented by an `m x n` grid of integers `image`, where `image[i][j]` represents the pixel value of the image. You are also given three integers `sr`, `sc`, and `color`. Your task is to perform a **flood fill** on the image starting from the pixel `image[sr][sc]`.\n\nTo perform a **flood fill**:\n\n1. Begin with the starting pixel and change its color to `color`.\n2. Perform the same process for each pixel that is **directly adjacent** (pixels that share a side with the original pixel, either horizontally or vertically) and shares the **same color** as the starting pixel.\n3. Keep **repeating** this process by checking neighboring pixels of the *updated* pixels and modifying their color if it matches the original color of the starting pixel.\n4. The process **stops** when there are **no more** adjacent pixels of the original color to update.\n\nReturn the **modified** image after performing the flood fill.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/06/01/flood1-grid.jpg)\n\n```\nInput: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2\nOutput: [[2,2,2],[2,2,0],[2,0,1]]\n```\n**Explanation:** From the center of the image with position `(sr, sc) = (1, 1)` (i.e., the red pixel), all pixels connected by a path of the same color as the starting pixel (i.e., the blue pixels) are colored with the new color. Note the bottom corner is not colored 2, because it is not horizontally or vertically connected to the starting pixel." + }, + { + "content": "```\nInput: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, color = 0\nOutput: [[0,0,0],[0,0,0]]\n```\n**Explanation:** The starting pixel is already colored with 0, which is the same as the target color. Therefore, no changes are made to the image." + } + ], + "readme_constraints": "- `m == image.length`\n- `n == image[i].length`\n- `1 <= m, n <= 50`\n- `0 <= image[i][j], color < 2^16`\n- `0 <= sr < m`\n- `0 <= sc < n`", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "flood_fill", + "parameters": "image: list[list[int]], sr: int, sc: int, color: int", + "return_type": "list[list[int]]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "FloodFill", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_flood_fill", + "parametrize": "image, sr, sc, color, expected", + "parametrize_typed": "image: list[list[int]], sr: int, sc: int, color: int, expected: list[list[int]]", + "test_cases": "[([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 2, [[2, 2, 2], [2, 2, 0], [2, 0, 1]]), ([[0, 0, 0], [0, 0, 0]], 0, 0, 0, [[0, 0, 0], [0, 0, 0]]), ([[0, 0, 0], [0, 1, 1]], 1, 1, 1, [[0, 0, 0], [0, 1, 1]]), ([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 1, [[1, 1, 1], [1, 1, 0], [1, 0, 1]])]", + "body": "result = self.solution.flood_fill(image, sr, sc, color)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nimage = [[1, 1, 1], [1, 1, 0], [1, 0, 1]]\nsr = 1\nsc = 1\ncolor = 2\nexpected = [[2, 2, 2], [2, 2, 0], [2, 0, 1]]", + "playground_execution": "result = Solution().flood_fill(image, sr, sc, color)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index ca1e121..cc42507 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= contains_duplicate +PROBLEM ?= flood_fill FORCE ?= 0 COMMA := , diff --git a/leetcode/flood_fill/README.md b/leetcode/flood_fill/README.md new file mode 100644 index 0000000..24f808f --- /dev/null +++ b/leetcode/flood_fill/README.md @@ -0,0 +1,51 @@ +# Flood Fill + +**Difficulty:** Easy +**Topics:** Array, Depth-First Search, Breadth-First Search, Matrix +**Tags:** grind-75 + +**LeetCode:** [Problem 733](https://leetcode.com/problems/flood-fill/description/) + +## Problem Description + +You are given an image represented by an `m x n` grid of integers `image`, where `image[i][j]` represents the pixel value of the image. You are also given three integers `sr`, `sc`, and `color`. Your task is to perform a **flood fill** on the image starting from the pixel `image[sr][sc]`. + +To perform a **flood fill**: + +1. Begin with the starting pixel and change its color to `color`. +2. Perform the same process for each pixel that is **directly adjacent** (pixels that share a side with the original pixel, either horizontally or vertically) and shares the **same color** as the starting pixel. +3. Keep **repeating** this process by checking neighboring pixels of the _updated_ pixels and modifying their color if it matches the original color of the starting pixel. +4. The process **stops** when there are **no more** adjacent pixels of the original color to update. + +Return the **modified** image after performing the flood fill. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/06/01/flood1-grid.jpg) + +``` +Input: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2 +Output: [[2,2,2],[2,2,0],[2,0,1]] +``` + +**Explanation:** From the center of the image with position `(sr, sc) = (1, 1)` (i.e., the red pixel), all pixels connected by a path of the same color as the starting pixel (i.e., the blue pixels) are colored with the new color. Note the bottom corner is not colored 2, because it is not horizontally or vertically connected to the starting pixel. + +### Example 2: + +``` +Input: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, color = 0 +Output: [[0,0,0],[0,0,0]] +``` + +**Explanation:** The starting pixel is already colored with 0, which is the same as the target color. Therefore, no changes are made to the image. + +## Constraints + +- `m == image.length` +- `n == image[i].length` +- `1 <= m, n <= 50` +- `0 <= image[i][j], color < 2^16` +- `0 <= sr < m` +- `0 <= sc < n` diff --git a/leetcode/flood_fill/__init__.py b/leetcode/flood_fill/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/flood_fill/playground.ipynb b/leetcode/flood_fill/playground.ipynb new file mode 100644 index 0000000..53f0ea6 --- /dev/null +++ b/leetcode/flood_fill/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\nimage = [[1, 1, 1], [1, 1, 0], [1, 0, 1]]\nsr = 1\nsc = 1\ncolor = 2\nexpected = [[2, 2, 2], [2, 2, 0], [2, 0, 1]]"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().flood_fill(image, sr, sc, color)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/flood_fill/solution.py b/leetcode/flood_fill/solution.py new file mode 100644 index 0000000..897ac71 --- /dev/null +++ b/leetcode/flood_fill/solution.py @@ -0,0 +1,17 @@ +class Solution: + # Time: O(m*n) + # Space: O(m*n) + def flood_fill(self, image: list[list[int]], sr: int, sc: int, color: int) -> list[list[int]]: + original = image[sr][sc] + if original == color: + return image + + def dfs(r: int, c: int) -> None: + if r < 0 or r >= len(image) or c < 0 or c >= len(image[0]) or image[r][c] != original: + return + image[r][c] = color + for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + dfs(r + dr, c + dc) + + dfs(sr, sc) + return image diff --git a/leetcode/flood_fill/tests.py b/leetcode/flood_fill/tests.py new file mode 100644 index 0000000..fdb3209 --- /dev/null +++ b/leetcode/flood_fill/tests.py @@ -0,0 +1,26 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestFloodFill: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "image, sr, sc, color, expected", + [ + ([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 2, [[2, 2, 2], [2, 2, 0], [2, 0, 1]]), + ([[0, 0, 0], [0, 0, 0]], 0, 0, 0, [[0, 0, 0], [0, 0, 0]]), + ([[0, 0, 0], [0, 1, 1]], 1, 1, 1, [[0, 0, 0], [0, 1, 1]]), + ([[1, 1, 1], [1, 1, 0], [1, 0, 1]], 1, 1, 1, [[1, 1, 1], [1, 1, 0], [1, 0, 1]]), + ], + ) + @logged_test + def test_flood_fill( + self, image: list[list[int]], sr: int, sc: int, color: int, expected: list[list[int]] + ): + result = self.solution.flood_fill(image, sr, sc, color) + assert result == expected From bd6d987b8a65152da6713baa2898a77844a509db Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 21:26:49 +0700 Subject: [PATCH 17/26] feat: add Valid Parentheses --- .../leetcode/json/valid_parentheses.json | 41 ++++++++++++ Makefile | 2 +- leetcode/valid_parentheses/README.md | 59 ++++++++++++++++ leetcode/valid_parentheses/__init__.py | 0 leetcode/valid_parentheses/playground.ipynb | 67 +++++++++++++++++++ leetcode/valid_parentheses/solution.py | 14 ++++ leetcode/valid_parentheses/tests.py | 46 +++++++++++++ 7 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/valid_parentheses.json create mode 100644 leetcode/valid_parentheses/README.md create mode 100644 leetcode/valid_parentheses/__init__.py create mode 100644 leetcode/valid_parentheses/playground.ipynb create mode 100644 leetcode/valid_parentheses/solution.py create mode 100644 leetcode/valid_parentheses/tests.py diff --git a/.templates/leetcode/json/valid_parentheses.json b/.templates/leetcode/json/valid_parentheses.json new file mode 100644 index 0000000..5029661 --- /dev/null +++ b/.templates/leetcode/json/valid_parentheses.json @@ -0,0 +1,41 @@ +{ + "problem_name": "valid_parentheses", + "solution_class_name": "Solution", + "problem_number": "20", + "problem_title": "Valid Parentheses", + "difficulty": "Easy", + "topics": "String, Stack", + "tags": ["grind-75"], + "readme_description": "Given a string `s` containing just the characters `'('`, `')'`, `'{'`, `'}'`, `'['` and `']'`, determine if the input string is valid.\n\nAn input string is valid if:\n\n1. Open brackets must be closed by the same type of brackets.\n2. Open brackets must be closed in the correct order.\n3. Every close bracket has a corresponding open bracket of the same type.", + "readme_examples": [ + { "content": "```\nInput: s = \"()\"\nOutput: true\n```" }, + { "content": "```\nInput: s = \"()[]{}\"\nOutput: true\n```" }, + { "content": "```\nInput: s = \"(]\"\nOutput: false\n```" }, + { "content": "```\nInput: s = \"([])\"\nOutput: true\n```" }, + { "content": "```\nInput: s = \"([)]\"\nOutput: false\n```" } + ], + "readme_constraints": "- `1 <= s.length <= 10^4`\n- `s` consists of parentheses only `'()[]{}'`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { "name": "is_valid", "parameters": "s: str", "return_type": "bool", "dummy_return": "False" } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "ValidParentheses", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_is_valid", + "parametrize": "s, expected", + "parametrize_typed": "s: str, expected: bool", + "test_cases": "[('()', True), ('()[]{}', True), ('(]', False), ('([])', True), ('([)]', False), ('', True), ('(', False), (')', False), ('{[()]}', True), ('{[(])}', False)]", + "body": "result = self.solution.is_valid(s)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = '()'\nexpected = True", + "playground_execution": "result = Solution().is_valid(s)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index cc42507..f4e60fa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= flood_fill +PROBLEM ?= valid_parentheses FORCE ?= 0 COMMA := , diff --git a/leetcode/valid_parentheses/README.md b/leetcode/valid_parentheses/README.md new file mode 100644 index 0000000..05d24f0 --- /dev/null +++ b/leetcode/valid_parentheses/README.md @@ -0,0 +1,59 @@ +# Valid Parentheses + +**Difficulty:** Easy +**Topics:** String, Stack +**Tags:** grind-75 + +**LeetCode:** [Problem 20](https://leetcode.com/problems/valid-parentheses/description/) + +## Problem Description + +Given a string `s` containing just the characters `'('`, `')'`, `'{'`, `'}'`, `'['` and `']'`, determine if the input string is valid. + +An input string is valid if: + +1. Open brackets must be closed by the same type of brackets. +2. Open brackets must be closed in the correct order. +3. Every close bracket has a corresponding open bracket of the same type. + +## Examples + +### Example 1: + +``` +Input: s = "()" +Output: true +``` + +### Example 2: + +``` +Input: s = "()[]{}" +Output: true +``` + +### Example 3: + +``` +Input: s = "(]" +Output: false +``` + +### Example 4: + +``` +Input: s = "([])" +Output: true +``` + +### Example 5: + +``` +Input: s = "([)]" +Output: false +``` + +## Constraints + +- `1 <= s.length <= 10^4` +- `s` consists of parentheses only `'()[]{}'`. diff --git a/leetcode/valid_parentheses/__init__.py b/leetcode/valid_parentheses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/valid_parentheses/playground.ipynb b/leetcode/valid_parentheses/playground.ipynb new file mode 100644 index 0000000..b5b4f34 --- /dev/null +++ b/leetcode/valid_parentheses/playground.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "s = \"()\"\n", + "expected = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().is_valid(s)\nresult" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/valid_parentheses/solution.py b/leetcode/valid_parentheses/solution.py new file mode 100644 index 0000000..779f734 --- /dev/null +++ b/leetcode/valid_parentheses/solution.py @@ -0,0 +1,14 @@ +class Solution: + # Time: O(n) + # Space: O(n) + def is_valid(self, s: str) -> bool: + stack = [] + pairs = {"(": ")", "[": "]", "{": "}"} + + for char in s: + if char in pairs: + stack.append(char) + elif not stack or pairs[stack.pop()] != char: + return False + + return not stack diff --git a/leetcode/valid_parentheses/tests.py b/leetcode/valid_parentheses/tests.py new file mode 100644 index 0000000..528dacb --- /dev/null +++ b/leetcode/valid_parentheses/tests.py @@ -0,0 +1,46 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestValidParentheses: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, expected", + [ + ("()", True), + ("()[]{}", True), + ("(]", False), + ("([])", True), + ("([)]", False), + ("", True), + ("(", False), + (")", False), + ("{[()]}", True), + ("{[(])}", False), + ("(((", False), + (")))", False), + ("()()()", True), + ("({[]})", True), + ("({[}])", False), + ("[[[[[]]]]]", True), + ("[[[[[]]]]", False), + ("{{{{{}}}}}", True), + ("((((((((((", False), + ("))))))))))", False), + ("(){}[]", True), + ("([{}])", True), + ("([{]})", False), + ("(())", True), + ("(()", False), + ("())", False), + ], + ) + @logged_test + def test_is_valid(self, s: str, expected: bool): + result = self.solution.is_valid(s) + assert result == expected From 8f83609008bebd3e4150a4ad8ba4fd038af1e47e Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 21:37:05 +0700 Subject: [PATCH 18/26] feat: add Balanced Binary Tree --- .../leetcode/json/balanced_binary_tree.json | 48 +++++ Makefile | 2 +- leetcode/balanced_binary_tree/README.md | 45 +++++ leetcode/balanced_binary_tree/__init__.py | 0 .../balanced_binary_tree/playground.ipynb | 171 ++++++++++++++++++ leetcode/balanced_binary_tree/solution.py | 20 ++ leetcode/balanced_binary_tree/tests.py | 33 ++++ 7 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/balanced_binary_tree.json create mode 100644 leetcode/balanced_binary_tree/README.md create mode 100644 leetcode/balanced_binary_tree/__init__.py create mode 100644 leetcode/balanced_binary_tree/playground.ipynb create mode 100644 leetcode/balanced_binary_tree/solution.py create mode 100644 leetcode/balanced_binary_tree/tests.py diff --git a/.templates/leetcode/json/balanced_binary_tree.json b/.templates/leetcode/json/balanced_binary_tree.json new file mode 100644 index 0000000..b9f3446 --- /dev/null +++ b/.templates/leetcode/json/balanced_binary_tree.json @@ -0,0 +1,48 @@ +{ + "problem_name": "balanced_binary_tree", + "solution_class_name": "Solution", + "problem_number": "110", + "problem_title": "Balanced Binary Tree", + "difficulty": "Easy", + "topics": "Tree, Depth-First Search, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given a binary tree, determine if it is **height-balanced**.\n\nA height-balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2020/10/06/balance_1.jpg)\n\n```\nInput: root = [3,9,20,null,null,15,7]\nOutput: true\n```" + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2020/10/06/balance_2.jpg)\n\n```\nInput: root = [1,2,2,3,3,null,null,4,4]\nOutput: false\n```" + }, + { "content": "```\nInput: root = []\nOutput: true\n```" } + ], + "readme_constraints": "- The number of nodes in the tree is in the range `[0, 5000]`.\n- `-10^4 <= Node.val <= 10^4`", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "is_balanced", + "parameters": "root: TreeNode | None", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Solution", + "test_class_name": "BalancedBinaryTree", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_is_balanced", + "parametrize": "root_list, expected", + "parametrize_typed": "root_list: list[int | None], expected: bool", + "test_cases": "[([3, 9, 20, None, None, 15, 7], True), ([1, 2, 2, 3, 3, None, None, 4, 4], False), ([], True)]", + "body": "root = TreeNode.from_list(root_list)\nresult = self.solution.is_balanced(root)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode", + "playground_test_case": "# Example test case\nroot_list = [3, 9, 20, None, None, 15, 7]\nroot = TreeNode.from_list(root_list)\nexpected = True", + "playground_execution": "result = Solution().is_balanced(root)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index f4e60fa..4f1ec78 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= valid_parentheses +PROBLEM ?= balanced_binary_tree FORCE ?= 0 COMMA := , diff --git a/leetcode/balanced_binary_tree/README.md b/leetcode/balanced_binary_tree/README.md new file mode 100644 index 0000000..776d6aa --- /dev/null +++ b/leetcode/balanced_binary_tree/README.md @@ -0,0 +1,45 @@ +# Balanced Binary Tree + +**Difficulty:** Easy +**Topics:** Tree, Depth-First Search, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 110](https://leetcode.com/problems/balanced-binary-tree/description/) + +## Problem Description + +Given a binary tree, determine if it is **height-balanced**. + +A height-balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2020/10/06/balance_1.jpg) + +``` +Input: root = [3,9,20,null,null,15,7] +Output: true +``` + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2020/10/06/balance_2.jpg) + +``` +Input: root = [1,2,2,3,3,null,null,4,4] +Output: false +``` + +### Example 3: + +``` +Input: root = [] +Output: true +``` + +## Constraints + +- The number of nodes in the tree is in the range `[0, 5000]`. +- `-10^4 <= Node.val <= 10^4` diff --git a/leetcode/balanced_binary_tree/__init__.py b/leetcode/balanced_binary_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/balanced_binary_tree/playground.ipynb b/leetcode/balanced_binary_tree/playground.ipynb new file mode 100644 index 0000000..9e4e1e6 --- /dev/null +++ b/leetcode/balanced_binary_tree/playground.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root_list = [3, 9, 20, None, None, 15, 7]\n", + "root = TreeNode.from_list(root_list)\n", + "expected = True" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f76011d0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "20\n", + "\n", + "\n", + "\n", + "0->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "15\n", + "\n", + "\n", + "\n", + "2->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "7\n", + "\n", + "\n", + "\n", + "2->4\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([3, 9, 20, None, None, 15, 7])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().is_balanced(root)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "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/balanced_binary_tree/solution.py b/leetcode/balanced_binary_tree/solution.py new file mode 100644 index 0000000..81a74aa --- /dev/null +++ b/leetcode/balanced_binary_tree/solution.py @@ -0,0 +1,20 @@ +from leetcode_py import TreeNode + + +class Solution: + # Time: O(n) + # Space: O(h) + def is_balanced(self, root: TreeNode | None) -> bool: + def height(node: TreeNode | None) -> int: + if not node: + return 0 + + left = height(node.left) + right = height(node.right) + + if left == -1 or right == -1 or abs(left - right) > 1: + return -1 + + return max(left, right) + 1 + + return height(root) != -1 diff --git a/leetcode/balanced_binary_tree/tests.py b/leetcode/balanced_binary_tree/tests.py new file mode 100644 index 0000000..cbc49a0 --- /dev/null +++ b/leetcode/balanced_binary_tree/tests.py @@ -0,0 +1,33 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestBalancedBinaryTree: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "root_list, expected", + [ + ([3, 9, 20, None, None, 15, 7], True), + ([1, 2, 2, 3, 3, None, None, 4, 4], False), + ([], True), + ([1], True), + ([1, 2], True), + ([1, None, 2], True), + ([1, 2, 3], True), + ([1, 2, None, 3], False), + ([1, None, 2, None, 3], False), + ([1, 2, 3, 4, 5, 6, 7], True), + ([1, 2, 3, 4, None, None, 7, 8], False), + ], + ) + @logged_test + def test_is_balanced(self, root_list: list[int | None], expected: bool): + root = TreeNode.from_list(root_list) + result = self.solution.is_balanced(root) + assert result == expected From 0ed0402b858059e39ee8e620d35f10eb84a68f1b Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 21:47:49 +0700 Subject: [PATCH 19/26] feat: add Merge Two Sorted Lists --- .../leetcode/json/merge_two_sorted_lists.json | 46 ++++++++++++ Makefile | 2 +- leetcode/merge_two_sorted_lists/README.md | 46 ++++++++++++ leetcode/merge_two_sorted_lists/__init__.py | 0 .../merge_two_sorted_lists/playground.ipynb | 74 +++++++++++++++++++ leetcode/merge_two_sorted_lists/solution.py | 22 ++++++ leetcode/merge_two_sorted_lists/tests.py | 37 ++++++++++ 7 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/merge_two_sorted_lists.json create mode 100644 leetcode/merge_two_sorted_lists/README.md create mode 100644 leetcode/merge_two_sorted_lists/__init__.py create mode 100644 leetcode/merge_two_sorted_lists/playground.ipynb create mode 100644 leetcode/merge_two_sorted_lists/solution.py create mode 100644 leetcode/merge_two_sorted_lists/tests.py diff --git a/.templates/leetcode/json/merge_two_sorted_lists.json b/.templates/leetcode/json/merge_two_sorted_lists.json new file mode 100644 index 0000000..f00c2a1 --- /dev/null +++ b/.templates/leetcode/json/merge_two_sorted_lists.json @@ -0,0 +1,46 @@ +{ + "problem_name": "merge_two_sorted_lists", + "solution_class_name": "Solution", + "problem_number": "21", + "problem_title": "Merge Two Sorted Lists", + "difficulty": "Easy", + "topics": "Linked List, Recursion", + "tags": ["grind-75"], + "readme_description": "You are given the heads of two sorted linked lists `list1` and `list2`.\n\nMerge the two lists into one **sorted** list. The list should be made by splicing together the nodes of the first two lists.\n\nReturn *the head of the merged linked list*.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2020/10/03/merge_ex1.jpg)\n\n```\nInput: list1 = [1,2,4], list2 = [1,3,4]\nOutput: [1,1,2,3,4,4]\n```" + }, + { "content": "```\nInput: list1 = [], list2 = []\nOutput: []\n```" }, + { "content": "```\nInput: list1 = [], list2 = [0]\nOutput: [0]\n```" } + ], + "readme_constraints": "- The number of nodes in both lists is in the range `[0, 50]`.\n- `-100 <= Node.val <= 100`\n- Both `list1` and `list2` are sorted in **non-decreasing** order.", + "readme_additional": "", + "solution_imports": "from leetcode_py import ListNode", + "solution_methods": [ + { + "name": "merge_two_lists", + "parameters": "list1: ListNode | None, list2: ListNode | None", + "return_type": "ListNode | None", + "dummy_return": "None" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import ListNode\nfrom .solution import Solution", + "test_class_name": "MergeTwoSortedLists", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_merge_two_lists", + "parametrize": "list1_vals, list2_vals, expected_vals", + "parametrize_typed": "list1_vals: list[int], list2_vals: list[int], expected_vals: list[int]", + "test_cases": "[([1, 2, 4], [1, 3, 4], [1, 1, 2, 3, 4, 4]), ([], [], []), ([], [0], [0]), ([1], [2], [1, 2]), ([2], [1], [1, 2])]", + "body": "list1 = ListNode.from_list(list1_vals)\nlist2 = ListNode.from_list(list2_vals)\nexpected = ListNode.from_list(expected_vals)\nresult = self.solution.merge_two_lists(list1, list2)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import ListNode", + "playground_test_case": "# Example test case\nlist1_vals = [1, 2, 4]\nlist2_vals = [1, 3, 4]\nexpected_vals = [1, 1, 2, 3, 4, 4]", + "playground_execution": "list1 = ListNode.from_list(list1_vals)\nlist2 = ListNode.from_list(list2_vals)\nresult = Solution().merge_two_lists(list1, list2)\nresult", + "playground_assertion": "expected = ListNode.from_list(expected_vals)\nassert result == expected" +} diff --git a/Makefile b/Makefile index 4f1ec78..bfb022b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= balanced_binary_tree +PROBLEM ?= merge_two_sorted_lists FORCE ?= 0 COMMA := , diff --git a/leetcode/merge_two_sorted_lists/README.md b/leetcode/merge_two_sorted_lists/README.md new file mode 100644 index 0000000..a9721a5 --- /dev/null +++ b/leetcode/merge_two_sorted_lists/README.md @@ -0,0 +1,46 @@ +# Merge Two Sorted Lists + +**Difficulty:** Easy +**Topics:** Linked List, Recursion +**Tags:** grind-75 + +**LeetCode:** [Problem 21](https://leetcode.com/problems/merge-two-sorted-lists/description/) + +## Problem Description + +You are given the heads of two sorted linked lists `list1` and `list2`. + +Merge the two lists into one **sorted** list. The list should be made by splicing together the nodes of the first two lists. + +Return _the head of the merged linked list_. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2020/10/03/merge_ex1.jpg) + +``` +Input: list1 = [1,2,4], list2 = [1,3,4] +Output: [1,1,2,3,4,4] +``` + +### Example 2: + +``` +Input: list1 = [], list2 = [] +Output: [] +``` + +### Example 3: + +``` +Input: list1 = [], list2 = [0] +Output: [0] +``` + +## Constraints + +- The number of nodes in both lists is in the range `[0, 50]`. +- `-100 <= Node.val <= 100` +- Both `list1` and `list2` are sorted in **non-decreasing** order. diff --git a/leetcode/merge_two_sorted_lists/__init__.py b/leetcode/merge_two_sorted_lists/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/merge_two_sorted_lists/playground.ipynb b/leetcode/merge_two_sorted_lists/playground.ipynb new file mode 100644 index 0000000..492ef37 --- /dev/null +++ b/leetcode/merge_two_sorted_lists/playground.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import ListNode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "list1_vals = [1, 2, 4]\n", + "list2_vals = [1, 3, 4]\n", + "expected_vals = [1, 1, 2, 3, 4, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "list1 = ListNode.from_list(list1_vals)\n", + "list2 = ListNode.from_list(list2_vals)\n", + "result = Solution().merge_two_lists(list1, list2)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "expected = ListNode.from_list(expected_vals)\n", + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/merge_two_sorted_lists/solution.py b/leetcode/merge_two_sorted_lists/solution.py new file mode 100644 index 0000000..07ebb03 --- /dev/null +++ b/leetcode/merge_two_sorted_lists/solution.py @@ -0,0 +1,22 @@ +from leetcode_py import ListNode + + +class Solution: + # Time: O(m + n) + # Space: O(1) + def merge_two_lists(self, list1: ListNode | None, list2: ListNode | None) -> ListNode | None: + + dummy = ListNode(0) + current = dummy + + while list1 and list2: + if list1.val <= list2.val: + current.next = list1 + list1 = list1.next + else: + current.next = list2 + list2 = list2.next + current = current.next + + current.next = list1 or list2 + return dummy.next diff --git a/leetcode/merge_two_sorted_lists/tests.py b/leetcode/merge_two_sorted_lists/tests.py new file mode 100644 index 0000000..5cb715d --- /dev/null +++ b/leetcode/merge_two_sorted_lists/tests.py @@ -0,0 +1,37 @@ +import pytest + +from leetcode_py import ListNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMergeTwoSortedLists: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "list1_vals, list2_vals, expected_vals", + [ + ([1, 2, 4], [1, 3, 4], [1, 1, 2, 3, 4, 4]), + ([], [], []), + ([], [0], [0]), + ([0], [], [0]), + ([1], [2], [1, 2]), + ([2], [1], [1, 2]), + ([1, 1, 1], [2, 2, 2], [1, 1, 1, 2, 2, 2]), + ([1, 3, 5], [2, 4, 6], [1, 2, 3, 4, 5, 6]), + ([-10, -5, 0], [-8, -3, 1], [-10, -8, -5, -3, 0, 1]), + ([5], [1, 2, 3, 4], [1, 2, 3, 4, 5]), + ([1, 2, 3, 4], [5], [1, 2, 3, 4, 5]), + ], + ) + @logged_test + def test_merge_two_lists( + self, list1_vals: list[int], list2_vals: list[int], expected_vals: list[int] + ): + list1 = ListNode.from_list(list1_vals) + list2 = ListNode.from_list(list2_vals) + expected = ListNode.from_list(expected_vals) + result = self.solution.merge_two_lists(list1, list2) + assert result == expected From 76581dcc347cbfa96d907e1df1a9bb98fc64ad46 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 21:53:52 +0700 Subject: [PATCH 20/26] feat: add Best Time to Buy and Sell Stock --- .../json/best_time_to_buy_and_sell_stock.json | 47 +++++++++++ Makefile | 2 +- .../best_time_to_buy_and_sell_stock/README.md | 41 ++++++++++ .../__init__.py | 0 .../playground.ipynb | 79 +++++++++++++++++++ .../solution.py | 12 +++ .../best_time_to_buy_and_sell_stock/tests.py | 28 +++++++ 7 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/best_time_to_buy_and_sell_stock.json create mode 100644 leetcode/best_time_to_buy_and_sell_stock/README.md create mode 100644 leetcode/best_time_to_buy_and_sell_stock/__init__.py create mode 100644 leetcode/best_time_to_buy_and_sell_stock/playground.ipynb create mode 100644 leetcode/best_time_to_buy_and_sell_stock/solution.py create mode 100644 leetcode/best_time_to_buy_and_sell_stock/tests.py diff --git a/.templates/leetcode/json/best_time_to_buy_and_sell_stock.json b/.templates/leetcode/json/best_time_to_buy_and_sell_stock.json new file mode 100644 index 0000000..9822f0a --- /dev/null +++ b/.templates/leetcode/json/best_time_to_buy_and_sell_stock.json @@ -0,0 +1,47 @@ +{ + "problem_name": "best_time_to_buy_and_sell_stock", + "solution_class_name": "Solution", + "problem_number": "121", + "problem_title": "Best Time to Buy and Sell Stock", + "difficulty": "Easy", + "topics": "Array, Dynamic Programming", + "tags": ["grind-75"], + "readme_description": "You are given an array `prices` where `prices[i]` is the price of a given stock on the ith day.\n\nYou want to maximize your profit by choosing a **single day** to buy one stock and choosing a **different day in the future** to sell that stock.\n\nReturn *the maximum profit you can achieve from this transaction*. If you cannot achieve any profit, return `0`.", + "readme_examples": [ + { + "content": "```\nInput: prices = [7,1,5,3,6,4]\nOutput: 5\n```\n**Explanation:** Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.\nNote that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell." + }, + { + "content": "```\nInput: prices = [7,6,4,3,1]\nOutput: 0\n```\n**Explanation:** In this case, no transactions are done and the max profit = 0." + } + ], + "readme_constraints": "- 1 <= prices.length <= 10^5\n- 0 <= prices[i] <= 10^4", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "max_profit", + "parameters": "prices: list[int]", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "BestTimeToBuyAndSellStock", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_max_profit", + "parametrize": "prices, expected", + "parametrize_typed": "prices: list[int], expected: int", + "test_cases": "[([7, 1, 5, 3, 6, 4], 5), ([7, 6, 4, 3, 1], 0), ([1, 2, 3, 4, 5], 4), ([5, 4, 3, 2, 1], 0), ([1], 0), ([2, 1], 0), ([1, 2], 1), ([3, 2, 6, 5, 0, 3], 4)]", + "body": "result = self.solution.max_profit(prices)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nprices = [7, 1, 5, 3, 6, 4]\nexpected = 5", + "playground_execution": "result = Solution().max_profit(prices)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index bfb022b..b780c38 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= merge_two_sorted_lists +PROBLEM ?= best_time_to_buy_and_sell_stock FORCE ?= 0 COMMA := , diff --git a/leetcode/best_time_to_buy_and_sell_stock/README.md b/leetcode/best_time_to_buy_and_sell_stock/README.md new file mode 100644 index 0000000..a18f45b --- /dev/null +++ b/leetcode/best_time_to_buy_and_sell_stock/README.md @@ -0,0 +1,41 @@ +# Best Time to Buy and Sell Stock + +**Difficulty:** Easy +**Topics:** Array, Dynamic Programming +**Tags:** grind-75 + +**LeetCode:** [Problem 121](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) + +## Problem Description + +You are given an array `prices` where `prices[i]` is the price of a given stock on the ith day. + +You want to maximize your profit by choosing a **single day** to buy one stock and choosing a **different day in the future** to sell that stock. + +Return _the maximum profit you can achieve from this transaction_. If you cannot achieve any profit, return `0`. + +## Examples + +### Example 1: + +``` +Input: prices = [7,1,5,3,6,4] +Output: 5 +``` + +**Explanation:** Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5. +Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell. + +### Example 2: + +``` +Input: prices = [7,6,4,3,1] +Output: 0 +``` + +**Explanation:** In this case, no transactions are done and the max profit = 0. + +## Constraints + +- 1 <= prices.length <= 10^5 +- 0 <= prices[i] <= 10^4 diff --git a/leetcode/best_time_to_buy_and_sell_stock/__init__.py b/leetcode/best_time_to_buy_and_sell_stock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/best_time_to_buy_and_sell_stock/playground.ipynb b/leetcode/best_time_to_buy_and_sell_stock/playground.ipynb new file mode 100644 index 0000000..cc80126 --- /dev/null +++ b/leetcode/best_time_to_buy_and_sell_stock/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", + "prices = [7, 1, 5, 3, 6, 4]\n", + "expected = 5" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().max_profit(prices)\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/best_time_to_buy_and_sell_stock/solution.py b/leetcode/best_time_to_buy_and_sell_stock/solution.py new file mode 100644 index 0000000..4f877ec --- /dev/null +++ b/leetcode/best_time_to_buy_and_sell_stock/solution.py @@ -0,0 +1,12 @@ +class Solution: + # Time: O(n) + # Space: O(1) + def max_profit(self, prices: list[int]) -> int: + min_price = prices[0] + max_profit = 0 + + for price in prices[1:]: + max_profit = max(max_profit, price - min_price) + min_price = min(min_price, price) + + return max_profit diff --git a/leetcode/best_time_to_buy_and_sell_stock/tests.py b/leetcode/best_time_to_buy_and_sell_stock/tests.py new file mode 100644 index 0000000..9b66879 --- /dev/null +++ b/leetcode/best_time_to_buy_and_sell_stock/tests.py @@ -0,0 +1,28 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestBestTimeToBuyAndSellStock: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "prices, expected", + [ + ([7, 1, 5, 3, 6, 4], 5), + ([7, 6, 4, 3, 1], 0), + ([1, 2, 3, 4, 5], 4), + ([5, 4, 3, 2, 1], 0), + ([1], 0), + ([2, 1], 0), + ([1, 2], 1), + ([3, 2, 6, 5, 0, 3], 4), + ], + ) + @logged_test + def test_max_profit(self, prices: list[int], expected: int): + result = self.solution.max_profit(prices) + assert result == expected From a99f4c416ccd89af31e6e64f6dc3fcad1c71e787 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 22:13:24 +0700 Subject: [PATCH 21/26] feat: add Implement Queue using Stacks --- .../json/implement_queue_using_stacks.json | 41 ++++++++++ .templates/leetcode/json/valid_anagram.json | 43 ++++++++++ Makefile | 2 +- .../implement_queue_using_stacks/README.md | 54 +++++++++++++ .../implement_queue_using_stacks/__init__.py | 0 .../playground.ipynb | 81 +++++++++++++++++++ .../implement_queue_using_stacks/solution.py | 51 ++++++++++++ .../implement_queue_using_stacks/tests.py | 48 +++++++++++ leetcode/valid_anagram/README.md | 34 ++++++++ leetcode/valid_anagram/__init__.py | 0 leetcode/valid_anagram/playground.ipynb | 80 ++++++++++++++++++ leetcode/valid_anagram/solution.py | 8 ++ leetcode/valid_anagram/tests.py | 38 +++++++++ 13 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/implement_queue_using_stacks.json create mode 100644 .templates/leetcode/json/valid_anagram.json create mode 100644 leetcode/implement_queue_using_stacks/README.md create mode 100644 leetcode/implement_queue_using_stacks/__init__.py create mode 100644 leetcode/implement_queue_using_stacks/playground.ipynb create mode 100644 leetcode/implement_queue_using_stacks/solution.py create mode 100644 leetcode/implement_queue_using_stacks/tests.py create mode 100644 leetcode/valid_anagram/README.md create mode 100644 leetcode/valid_anagram/__init__.py create mode 100644 leetcode/valid_anagram/playground.ipynb create mode 100644 leetcode/valid_anagram/solution.py create mode 100644 leetcode/valid_anagram/tests.py diff --git a/.templates/leetcode/json/implement_queue_using_stacks.json b/.templates/leetcode/json/implement_queue_using_stacks.json new file mode 100644 index 0000000..cc38ff1 --- /dev/null +++ b/.templates/leetcode/json/implement_queue_using_stacks.json @@ -0,0 +1,41 @@ +{ + "problem_name": "implement_queue_using_stacks", + "solution_class_name": "MyQueue", + "problem_number": "232", + "problem_title": "Implement Queue using Stacks", + "difficulty": "Easy", + "topics": "Stack, Design, Queue", + "tags": ["grind-75"], + "readme_description": "Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (`push`, `peek`, `pop`, and `empty`).\n\nImplement the `MyQueue` class:\n\n- `void push(int x)` Pushes element x to the back of the queue.\n- `int pop()` Removes the element from the front of the queue and returns it.\n- `int peek()` Returns the element at the front of the queue.\n- `boolean empty()` Returns `true` if the queue is empty, `false` otherwise.", + "readme_examples": [ + { + "content": "```\nInput\n[\"MyQueue\", \"push\", \"push\", \"peek\", \"pop\", \"empty\"]\n[[], [1], [2], [], [], []]\nOutput\n[null, null, null, 1, 1, false]\n```\n**Explanation:**\n```\nMyQueue myQueue = new MyQueue();\nmyQueue.push(1); // queue is: [1]\nmyQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)\nmyQueue.peek(); // return 1\nmyQueue.pop(); // return 1, queue is [2]\nmyQueue.empty(); // return false\n```" + } + ], + "readme_constraints": "- 1 <= x <= 9\n- At most 100 calls will be made to push, pop, peek, and empty.\n- All the calls to pop and peek are valid.", + "readme_additional": "**Notes:**\n- You must use **only** standard operations of a stack, which means only `push to top`, `peek/pop from top`, `size`, and `is empty` operations are valid.\n- Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack's standard operations.\n\n**Follow-up:** Can you implement the queue such that each operation is amortized `O(1)` time complexity? In other words, performing `n` operations will take overall `O(n)` time even if one of those operations may take longer.", + "solution_imports": "", + "solution_methods": [ + { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" }, + { "name": "push", "parameters": "x: int", "return_type": "None", "dummy_return": "" }, + { "name": "pop", "parameters": "", "return_type": "int", "dummy_return": "0" }, + { "name": "peek", "parameters": "", "return_type": "int", "dummy_return": "0" }, + { "name": "empty", "parameters": "", "return_type": "bool", "dummy_return": "True" } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import MyQueue", + "test_class_name": "ImplementQueueUsingStacks", + "test_helper_methods": [], + "test_methods": [ + { + "name": "test_queue_operations", + "parametrize": "operations, inputs, expected", + "parametrize_typed": "operations: list[str], inputs: list[list[int]], expected: list[int | None | bool]", + "test_cases": "[(['MyQueue', 'push', 'push', 'peek', 'pop', 'empty'], [[], [1], [2], [], [], []], [None, None, None, 1, 1, False]), (['MyQueue', 'empty', 'push', 'peek', 'pop', 'empty'], [[], [], [1], [], [], []], [None, True, None, 1, 1, True]), (['MyQueue', 'push', 'push', 'push', 'pop', 'pop', 'peek', 'pop', 'empty'], [[], [1], [2], [3], [], [], [], [], []], [None, None, None, None, 1, 2, 3, 3, True])]", + "body": "queue = None\nresults: list[int | None | bool] = []\nfor i, op in enumerate(operations):\n if op == 'MyQueue':\n queue = MyQueue()\n results.append(None)\n elif op == 'push' and queue is not None:\n queue.push(inputs[i][0])\n results.append(None)\n elif op == 'pop' and queue is not None:\n results.append(queue.pop())\n elif op == 'peek' and queue is not None:\n results.append(queue.peek())\n elif op == 'empty' and queue is not None:\n results.append(queue.empty())\nassert results == expected" + } + ], + "playground_imports": "from solution import MyQueue", + "playground_test_case": "# Example test case\nqueue = MyQueue()\nqueue.push(1)\nqueue.push(2)", + "playground_execution": "result_peek = queue.peek()\nresult_pop = queue.pop()\nresult_empty = queue.empty()\nprint(f'peek: {result_peek}, pop: {result_pop}, empty: {result_empty}')", + "playground_assertion": "assert result_peek == 1\nassert result_pop == 1\nassert result_empty == False" +} diff --git a/.templates/leetcode/json/valid_anagram.json b/.templates/leetcode/json/valid_anagram.json new file mode 100644 index 0000000..32a2ab1 --- /dev/null +++ b/.templates/leetcode/json/valid_anagram.json @@ -0,0 +1,43 @@ +{ + "problem_name": "valid_anagram", + "solution_class_name": "Solution", + "problem_number": "242", + "problem_title": "Valid Anagram", + "difficulty": "Easy", + "topics": "Hash Table, String, Sorting", + "tags": ["grind-75"], + "readme_description": "Given two strings `s` and `t`, return `true` if `t` is an anagram of `s`, and `false` otherwise.", + "readme_examples": [ + { "content": "```\nInput: s = \"anagram\", t = \"nagaram\"\nOutput: true\n```" }, + { "content": "```\nInput: s = \"rat\", t = \"car\"\nOutput: false\n```" } + ], + "readme_constraints": "- 1 <= s.length, t.length <= 5 * 10^4\n- s and t consist of lowercase English letters.", + "readme_additional": "**Follow up:** What if the inputs contain Unicode characters? How would you adapt your solution to such a case?", + "solution_imports": "", + "solution_methods": [ + { + "name": "is_anagram", + "parameters": "s: str, t: str", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "ValidAnagram", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_is_anagram", + "parametrize": "s, t, expected", + "parametrize_typed": "s: str, t: str, expected: bool", + "test_cases": "[('anagram', 'nagaram', True), ('rat', 'car', False), ('listen', 'silent', True), ('hello', 'bello', False), ('', '', True), ('a', 'a', True), ('a', 'b', False), ('ab', 'ba', True)]", + "body": "result = self.solution.is_anagram(s, t)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = 'anagram'\nt = 'nagaram'\nexpected = True", + "playground_execution": "result = Solution().is_anagram(s, t)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index b780c38..902385b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= best_time_to_buy_and_sell_stock +PROBLEM ?= implement_queue_using_stacks FORCE ?= 0 COMMA := , diff --git a/leetcode/implement_queue_using_stacks/README.md b/leetcode/implement_queue_using_stacks/README.md new file mode 100644 index 0000000..a1e659f --- /dev/null +++ b/leetcode/implement_queue_using_stacks/README.md @@ -0,0 +1,54 @@ +# Implement Queue using Stacks + +**Difficulty:** Easy +**Topics:** Stack, Design, Queue +**Tags:** grind-75 + +**LeetCode:** [Problem 232](https://leetcode.com/problems/implement-queue-using-stacks/description/) + +## Problem Description + +Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (`push`, `peek`, `pop`, and `empty`). + +Implement the `MyQueue` class: + +- `void push(int x)` Pushes element x to the back of the queue. +- `int pop()` Removes the element from the front of the queue and returns it. +- `int peek()` Returns the element at the front of the queue. +- `boolean empty()` Returns `true` if the queue is empty, `false` otherwise. + +## Examples + +### Example 1: + +``` +Input +["MyQueue", "push", "push", "peek", "pop", "empty"] +[[], [1], [2], [], [], []] +Output +[null, null, null, 1, 1, false] +``` + +**Explanation:** + +``` +MyQueue myQueue = new MyQueue(); +myQueue.push(1); // queue is: [1] +myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue) +myQueue.peek(); // return 1 +myQueue.pop(); // return 1, queue is [2] +myQueue.empty(); // return false +``` + +## Constraints + +- 1 <= x <= 9 +- At most 100 calls will be made to push, pop, peek, and empty. +- All the calls to pop and peek are valid. + +**Notes:** + +- You must use **only** standard operations of a stack, which means only `push to top`, `peek/pop from top`, `size`, and `is empty` operations are valid. +- Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack's standard operations. + +**Follow-up:** Can you implement the queue such that each operation is amortized `O(1)` time complexity? In other words, performing `n` operations will take overall `O(n)` time even if one of those operations may take longer. diff --git a/leetcode/implement_queue_using_stacks/__init__.py b/leetcode/implement_queue_using_stacks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/implement_queue_using_stacks/playground.ipynb b/leetcode/implement_queue_using_stacks/playground.ipynb new file mode 100644 index 0000000..5430378 --- /dev/null +++ b/leetcode/implement_queue_using_stacks/playground.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import MyQueue" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "queue = MyQueue()\n", + "queue.push(1)\n", + "queue.push(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "peek: 1, pop: 1, empty: False\n" + ] + } + ], + "source": [ + "result_peek = queue.peek()\n", + "result_pop = queue.pop()\n", + "result_empty = queue.empty()\n", + "print(f\"peek: {result_peek}, pop: {result_pop}, empty: {result_empty}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result_peek == 1\n", + "assert result_pop == 1\n", + "assert not result_empty" + ] + } + ], + "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/implement_queue_using_stacks/solution.py b/leetcode/implement_queue_using_stacks/solution.py new file mode 100644 index 0000000..71581ee --- /dev/null +++ b/leetcode/implement_queue_using_stacks/solution.py @@ -0,0 +1,51 @@ +class MyQueue: + # Time: O(1) + # Space: O(n) + def __init__(self) -> None: + self.input_stack: list[int] = [] + self.output_stack: list[int] = [] + + # Time: O(1) + # Space: O(1) + def push(self, x: int) -> None: + self.input_stack.append(x) + + # Time: O(1) amortized + # Space: O(1) + def pop(self) -> int: + self._move_to_output() + return self.output_stack.pop() + + # Time: O(1) amortized + # Space: O(1) + def peek(self) -> int: + self._move_to_output() + return self.output_stack[-1] + + # Time: O(1) + # Space: O(1) + def empty(self) -> bool: + return not self.input_stack and not self.output_stack + + def _move_to_output(self) -> None: + if not self.output_stack: + while self.input_stack: + self.output_stack.append(self.input_stack.pop()) + + +# Amortized O(1) Explanation: +# Example with 4 push + 4 pop operations: +# +# push(1) # input: [1], output: [] - O(1) +# push(2) # input: [1,2], output: [] - O(1) +# push(3) # input: [1,2,3], output: [] - O(1) +# push(4) # input: [1,2,3,4], output: [] - O(1) +# +# pop() # Move all 4 to output: input: [], output: [4,3,2,1] then pop 1 - O(4) +# pop() # output: [4,3,2], just pop 2 - O(1) +# pop() # output: [4,3], just pop 3 - O(1) +# pop() # output: [4], just pop 4 - O(1) +# +# Total cost: 4 + 4 + 1 + 1 + 1 = 11 operations for 8 calls = 1.4 per operation +# Key: Each element moves exactly once from input to output, so expensive O(n) +# transfer is "spread out" over multiple cheap O(1) operations = amortized O(1) diff --git a/leetcode/implement_queue_using_stacks/tests.py b/leetcode/implement_queue_using_stacks/tests.py new file mode 100644 index 0000000..519121f --- /dev/null +++ b/leetcode/implement_queue_using_stacks/tests.py @@ -0,0 +1,48 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import MyQueue + + +class TestImplementQueueUsingStacks: + @pytest.mark.parametrize( + "operations, inputs, expected", + [ + ( + ["MyQueue", "push", "push", "peek", "pop", "empty"], + [[], [1], [2], [], [], []], + [None, None, None, 1, 1, False], + ), + ( + ["MyQueue", "empty", "push", "peek", "pop", "empty"], + [[], [], [1], [], [], []], + [None, True, None, 1, 1, True], + ), + ( + ["MyQueue", "push", "push", "push", "pop", "pop", "peek", "pop", "empty"], + [[], [1], [2], [3], [], [], [], [], []], + [None, None, None, None, 1, 2, 3, 3, True], + ), + ], + ) + @logged_test + def test_queue_operations( + self, operations: list[str], inputs: list[list[int]], expected: list[int | None | bool] + ): + queue = None + results: list[int | None | bool] = [] + for i, op in enumerate(operations): + if op == "MyQueue": + queue = MyQueue() + results.append(None) + elif op == "push" and queue is not None: + queue.push(inputs[i][0]) + results.append(None) + elif op == "pop" and queue is not None: + results.append(queue.pop()) + elif op == "peek" and queue is not None: + results.append(queue.peek()) + elif op == "empty" and queue is not None: + results.append(queue.empty()) + assert results == expected diff --git a/leetcode/valid_anagram/README.md b/leetcode/valid_anagram/README.md new file mode 100644 index 0000000..1d0cc68 --- /dev/null +++ b/leetcode/valid_anagram/README.md @@ -0,0 +1,34 @@ +# Valid Anagram + +**Difficulty:** Easy +**Topics:** Hash Table, String, Sorting +**Tags:** grind-75 + +**LeetCode:** [Problem 242](https://leetcode.com/problems/valid-anagram/description/) + +## Problem Description + +Given two strings `s` and `t`, return `true` if `t` is an anagram of `s`, and `false` otherwise. + +## Examples + +### Example 1: + +``` +Input: s = "anagram", t = "nagaram" +Output: true +``` + +### Example 2: + +``` +Input: s = "rat", t = "car" +Output: false +``` + +## Constraints + +- 1 <= s.length, t.length <= 5 \* 10^4 +- s and t consist of lowercase English letters. + +**Follow up:** What if the inputs contain Unicode characters? How would you adapt your solution to such a case? diff --git a/leetcode/valid_anagram/__init__.py b/leetcode/valid_anagram/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/valid_anagram/playground.ipynb b/leetcode/valid_anagram/playground.ipynb new file mode 100644 index 0000000..d312663 --- /dev/null +++ b/leetcode/valid_anagram/playground.ipynb @@ -0,0 +1,80 @@ +{ + "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", + "s = \"anagram\"\n", + "t = \"nagaram\"\n", + "expected = True" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().is_anagram(s, t)\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/valid_anagram/solution.py b/leetcode/valid_anagram/solution.py new file mode 100644 index 0000000..ce6f797 --- /dev/null +++ b/leetcode/valid_anagram/solution.py @@ -0,0 +1,8 @@ +from collections import Counter + + +class Solution: + # Time: O(n) + # Space: O(1) - at most 26 unique characters + def is_anagram(self, s: str, t: str) -> bool: + return Counter(s) == Counter(t) diff --git a/leetcode/valid_anagram/tests.py b/leetcode/valid_anagram/tests.py new file mode 100644 index 0000000..aea8280 --- /dev/null +++ b/leetcode/valid_anagram/tests.py @@ -0,0 +1,38 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestValidAnagram: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, t, expected", + [ + ("anagram", "nagaram", True), + ("rat", "car", False), + ("listen", "silent", True), + ("hello", "bello", False), + ("", "", True), + ("a", "a", True), + ("a", "b", False), + ("ab", "ba", True), + ("abc", "bca", True), + ("abc", "def", False), + ("aab", "abb", False), + ("aabbcc", "abcabc", True), + ("abcd", "abcde", False), + ("race", "care", True), + ("elbow", "below", True), + ("study", "dusty", True), + ("night", "thing", True), + ("stressed", "desserts", True), + ], + ) + @logged_test + def test_is_anagram(self, s: str, t: str, expected: bool): + result = self.solution.is_anagram(s, t) + assert result == expected From 197c64b5a60943bbb6cd8123c7d3c1d93e36088b Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sat, 6 Sep 2025 22:31:39 +0700 Subject: [PATCH 22/26] feat: add 01 Matrix --- .templates/leetcode/json/zero_one_matrix.json | 47 +++++++++++++++ Makefile | 2 +- leetcode/zero_one_matrix/README.md | 44 ++++++++++++++ leetcode/zero_one_matrix/__init__.py | 0 leetcode/zero_one_matrix/playground.ipynb | 57 +++++++++++++++++++ leetcode/zero_one_matrix/solution.py | 30 ++++++++++ leetcode/zero_one_matrix/tests.py | 29 ++++++++++ 7 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/zero_one_matrix.json create mode 100644 leetcode/zero_one_matrix/README.md create mode 100644 leetcode/zero_one_matrix/__init__.py create mode 100644 leetcode/zero_one_matrix/playground.ipynb create mode 100644 leetcode/zero_one_matrix/solution.py create mode 100644 leetcode/zero_one_matrix/tests.py diff --git a/.templates/leetcode/json/zero_one_matrix.json b/.templates/leetcode/json/zero_one_matrix.json new file mode 100644 index 0000000..fd537af --- /dev/null +++ b/.templates/leetcode/json/zero_one_matrix.json @@ -0,0 +1,47 @@ +{ + "problem_name": "zero_one_matrix", + "solution_class_name": "Solution", + "problem_number": "542", + "problem_title": "01 Matrix", + "difficulty": "Medium", + "topics": "Array, Dynamic Programming, Breadth-First Search, Matrix", + "tags": ["grind-75"], + "readme_description": "Given an `m x n` binary matrix `mat`, return the distance of the nearest `0` for each cell.\n\nThe distance between two cells sharing a common edge is `1`.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/04/24/01-1-grid.jpg)\n\n```\nInput: mat = [[0,0,0],[0,1,0],[0,0,0]]\nOutput: [[0,0,0],[0,1,0],[0,0,0]]\n```" + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2021/04/24/01-2-grid.jpg)\n\n```\nInput: mat = [[0,0,0],[0,1,0],[1,1,1]]\nOutput: [[0,0,0],[0,1,0],[1,2,1]]\n```" + } + ], + "readme_constraints": "- `m == mat.length`\n- `n == mat[i].length`\n- `1 <= m, n <= 10^4`\n- `1 <= m * n <= 10^4`\n- `mat[i][j]` is either `0` or `1`\n- There is at least one `0` in `mat`", + "readme_additional": "**Note:** This question is the same as 1765: [Map of Highest Peak](https://leetcode.com/problems/map-of-highest-peak/)", + "solution_imports": "", + "solution_methods": [ + { + "name": "update_matrix", + "parameters": "mat: list[list[int]]", + "return_type": "list[list[int]]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "ZeroOneMatrix", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_update_matrix", + "parametrize": "mat, expected", + "parametrize_typed": "mat: list[list[int]], expected: list[list[int]]", + "test_cases": "[([[0, 0, 0], [0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 1, 0], [0, 0, 0]]), ([[0, 0, 0], [0, 1, 0], [1, 1, 1]], [[0, 0, 0], [0, 1, 0], [1, 2, 1]])]", + "body": "result = self.solution.update_matrix(mat)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nmat = [[0, 0, 0], [0, 1, 0], [1, 1, 1]]\nexpected = [[0, 0, 0], [0, 1, 0], [1, 2, 1]]", + "playground_execution": "result = Solution().update_matrix(mat)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 902385b..23fa74a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= implement_queue_using_stacks +PROBLEM ?= zero_one_matrix FORCE ?= 0 COMMA := , diff --git a/leetcode/zero_one_matrix/README.md b/leetcode/zero_one_matrix/README.md new file mode 100644 index 0000000..29b3127 --- /dev/null +++ b/leetcode/zero_one_matrix/README.md @@ -0,0 +1,44 @@ +# 01 Matrix + +**Difficulty:** Medium +**Topics:** Array, Dynamic Programming, Breadth-First Search, Matrix +**Tags:** grind-75 + +**LeetCode:** [Problem 542](https://leetcode.com/problems/zero-one-matrix/description/) + +## Problem Description + +Given an `m x n` binary matrix `mat`, return the distance of the nearest `0` for each cell. + +The distance between two cells sharing a common edge is `1`. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/04/24/01-1-grid.jpg) + +``` +Input: mat = [[0,0,0],[0,1,0],[0,0,0]] +Output: [[0,0,0],[0,1,0],[0,0,0]] +``` + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2021/04/24/01-2-grid.jpg) + +``` +Input: mat = [[0,0,0],[0,1,0],[1,1,1]] +Output: [[0,0,0],[0,1,0],[1,2,1]] +``` + +## Constraints + +- `m == mat.length` +- `n == mat[i].length` +- `1 <= m, n <= 10^4` +- `1 <= m * n <= 10^4` +- `mat[i][j]` is either `0` or `1` +- There is at least one `0` in `mat` + +**Note:** This question is the same as 1765: [Map of Highest Peak](https://leetcode.com/problems/map-of-highest-peak/) diff --git a/leetcode/zero_one_matrix/__init__.py b/leetcode/zero_one_matrix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/zero_one_matrix/playground.ipynb b/leetcode/zero_one_matrix/playground.ipynb new file mode 100644 index 0000000..659bd5e --- /dev/null +++ b/leetcode/zero_one_matrix/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\nmat = [[0, 0, 0], [0, 1, 0], [1, 1, 1]]\nexpected = [[0, 0, 0], [0, 1, 0], [1, 2, 1]]"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().update_matrix(mat)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/zero_one_matrix/solution.py b/leetcode/zero_one_matrix/solution.py new file mode 100644 index 0000000..b70c86c --- /dev/null +++ b/leetcode/zero_one_matrix/solution.py @@ -0,0 +1,30 @@ +from collections import deque + + +class Solution: + # Time: O(m * n) + # Space: O(m * n) + def update_matrix(self, mat: list[list[int]]) -> list[list[int]]: + UNSEEN = -1 + m, n = len(mat), len(mat[0]) + queue: deque[tuple[int, int]] = deque() + + # Mark 1s as UNSEEN and add all 0s to queue + for i in range(m): + for j in range(n): + if mat[i][j] == 0: + queue.append((i, j)) + else: + mat[i][j] = UNSEEN + + # BFS from all 0s simultaneously + directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] + while queue: + row, col = queue.popleft() + for dr, dc in directions: + r, c = row + dr, col + dc + if 0 <= r < m and 0 <= c < n and mat[r][c] == UNSEEN: + mat[r][c] = mat[row][col] + 1 + queue.append((r, c)) + + return mat diff --git a/leetcode/zero_one_matrix/tests.py b/leetcode/zero_one_matrix/tests.py new file mode 100644 index 0000000..6b5a4e4 --- /dev/null +++ b/leetcode/zero_one_matrix/tests.py @@ -0,0 +1,29 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestZeroOneMatrix: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "mat, expected", + [ + ([[0, 0, 0], [0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 1, 0], [0, 0, 0]]), + ([[0, 0, 0], [0, 1, 0], [1, 1, 1]], [[0, 0, 0], [0, 1, 0], [1, 2, 1]]), + ([[0]], [[0]]), + ([[1, 1, 1], [1, 1, 1], [1, 1, 0]], [[4, 3, 2], [3, 2, 1], [2, 1, 0]]), + ([[0, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]), + ( + [[1, 0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 1, 0, 1, 0, 1, 1]], + [[1, 0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 1, 0, 1, 0, 1, 2]], + ), + ], + ) + @logged_test + def test_update_matrix(self, mat: list[list[int]], expected: list[list[int]]): + result = self.solution.update_matrix(mat) + assert result == expected From d73819bc829d0a8067fadb19810de92bbd15142f Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sun, 7 Sep 2025 08:46:44 +0700 Subject: [PATCH 23/26] feat: add Course Schedule --- .../binary_tree_level_order_traversal.json | 46 +++++++++++ .templates/leetcode/json/course_schedule.json | 47 +++++++++++ ...ubstring_without_repeating_characters.json | 50 ++++++++++++ .templates/leetcode/json/three_sum.json | 50 ++++++++++++ Makefile | 2 +- .../README.md | 41 ++++++++++ .../__init__.py | 0 .../playground.ipynb | 71 +++++++++++++++++ .../solution.py | 31 ++++++++ .../tests.py | 39 +++++++++ leetcode/course_schedule/README.md | 43 ++++++++++ leetcode/course_schedule/__init__.py | 0 leetcode/course_schedule/playground.ipynb | 57 +++++++++++++ leetcode/course_schedule/solution.py | 29 +++++++ leetcode/course_schedule/tests.py | 40 ++++++++++ .../README.md | 46 +++++++++++ .../__init__.py | 0 .../playground.ipynb | 79 +++++++++++++++++++ .../solution.py | 15 ++++ .../tests.py | 35 ++++++++ leetcode/three_sum/README.md | 52 ++++++++++++ leetcode/three_sum/__init__.py | 0 leetcode/three_sum/playground.ipynb | 57 +++++++++++++ leetcode/three_sum/solution.py | 23 ++++++ leetcode/three_sum/tests.py | 43 ++++++++++ 25 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/binary_tree_level_order_traversal.json create mode 100644 .templates/leetcode/json/course_schedule.json create mode 100644 .templates/leetcode/json/longest_substring_without_repeating_characters.json create mode 100644 .templates/leetcode/json/three_sum.json create mode 100644 leetcode/binary_tree_level_order_traversal/README.md create mode 100644 leetcode/binary_tree_level_order_traversal/__init__.py create mode 100644 leetcode/binary_tree_level_order_traversal/playground.ipynb create mode 100644 leetcode/binary_tree_level_order_traversal/solution.py create mode 100644 leetcode/binary_tree_level_order_traversal/tests.py create mode 100644 leetcode/course_schedule/README.md create mode 100644 leetcode/course_schedule/__init__.py create mode 100644 leetcode/course_schedule/playground.ipynb create mode 100644 leetcode/course_schedule/solution.py create mode 100644 leetcode/course_schedule/tests.py create mode 100644 leetcode/longest_substring_without_repeating_characters/README.md create mode 100644 leetcode/longest_substring_without_repeating_characters/__init__.py create mode 100644 leetcode/longest_substring_without_repeating_characters/playground.ipynb create mode 100644 leetcode/longest_substring_without_repeating_characters/solution.py create mode 100644 leetcode/longest_substring_without_repeating_characters/tests.py create mode 100644 leetcode/three_sum/README.md create mode 100644 leetcode/three_sum/__init__.py create mode 100644 leetcode/three_sum/playground.ipynb create mode 100644 leetcode/three_sum/solution.py create mode 100644 leetcode/three_sum/tests.py diff --git a/.templates/leetcode/json/binary_tree_level_order_traversal.json b/.templates/leetcode/json/binary_tree_level_order_traversal.json new file mode 100644 index 0000000..b9f6380 --- /dev/null +++ b/.templates/leetcode/json/binary_tree_level_order_traversal.json @@ -0,0 +1,46 @@ +{ + "problem_name": "binary_tree_level_order_traversal", + "solution_class_name": "Solution", + "problem_number": "102", + "problem_title": "Binary Tree Level Order Traversal", + "difficulty": "Medium", + "topics": "Tree, Breadth-First Search, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given the `root` of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level).", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/02/19/tree1.jpg)\n\n```\nInput: root = [3,9,20,null,null,15,7]\nOutput: [[3],[9,20],[15,7]]\n```" + }, + { "content": "```\nInput: root = [1]\nOutput: [[1]]\n```" }, + { "content": "```\nInput: root = []\nOutput: []\n```" } + ], + "readme_constraints": "- The number of nodes in the tree is in the range [0, 2000]\n- -1000 <= Node.val <= 1000", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "level_order", + "parameters": "root: TreeNode | None", + "return_type": "list[list[int]]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Solution", + "test_class_name": "BinaryTreeLevelOrderTraversal", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_level_order", + "parametrize": "root_list, expected", + "parametrize_typed": "root_list: list[int | None], expected: list[list[int]]", + "test_cases": "[([3, 9, 20, None, None, 15, 7], [[3], [9, 20], [15, 7]]), ([1], [[1]]), ([], []), ([1, 2, 3, 4, 5, 6, 7], [[1], [2, 3], [4, 5, 6, 7]]), ([1, 2, None, 3, None, 4, None, 5], [[1], [2], [3], [4], [5]])]", + "body": "root = TreeNode.from_list(root_list) if root_list else None\nresult = self.solution.level_order(root)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode", + "playground_test_case": "# Example test case\nroot_list = [3, 9, 20, None, None, 15, 7]\nroot = TreeNode.from_list(root_list)\nexpected = [[3], [9, 20], [15, 7]]", + "playground_execution": "result = Solution().level_order(root)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/course_schedule.json b/.templates/leetcode/json/course_schedule.json new file mode 100644 index 0000000..9ac990d --- /dev/null +++ b/.templates/leetcode/json/course_schedule.json @@ -0,0 +1,47 @@ +{ + "problem_name": "course_schedule", + "solution_class_name": "Solution", + "problem_number": "207", + "problem_title": "Course Schedule", + "difficulty": "Medium", + "topics": "Depth-First Search, Breadth-First Search, Graph, Topological Sort", + "tags": ["grind-75"], + "readme_description": "There are a total of `numCourses` courses you have to take, labeled from `0` to `numCourses - 1`. You are given an array `prerequisites` where `prerequisites[i] = [ai, bi]` indicates that you **must** take course `bi` first if you want to take course `ai`.\n\n- For example, the pair `[0, 1]`, indicates that to take course `0` you have to first take course `1`.\n\nReturn `true` if you can finish all courses. Otherwise, return `false`.", + "readme_examples": [ + { + "content": "```\nInput: numCourses = 2, prerequisites = [[1,0]]\nOutput: true\n```\n**Explanation:** There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible." + }, + { + "content": "```\nInput: numCourses = 2, prerequisites = [[1,0],[0,1]]\nOutput: false\n```\n**Explanation:** There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible." + } + ], + "readme_constraints": "- `1 <= numCourses <= 2000`\n- `0 <= prerequisites.length <= 5000`\n- `prerequisites[i].length == 2`\n- `0 <= ai, bi < numCourses`\n- All the pairs prerequisites[i] are **unique**.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "can_finish", + "parameters": "num_courses: int, prerequisites: list[list[int]]", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "CourseSchedule", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_can_finish", + "parametrize": "num_courses, prerequisites, expected", + "parametrize_typed": "num_courses: int, prerequisites: list[list[int]], expected: bool", + "test_cases": "[(2, [[1, 0]], True), (2, [[1, 0], [0, 1]], False), (1, [], True), (3, [[1, 0], [2, 1]], True), (4, [[1, 0], [2, 1], [3, 2], [1, 3]], False)]", + "body": "result = self.solution.can_finish(num_courses, prerequisites)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnum_courses = 2\nprerequisites = [[1, 0]]\nexpected = True", + "playground_execution": "result = Solution().can_finish(num_courses, prerequisites)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/longest_substring_without_repeating_characters.json b/.templates/leetcode/json/longest_substring_without_repeating_characters.json new file mode 100644 index 0000000..c1c9b9c --- /dev/null +++ b/.templates/leetcode/json/longest_substring_without_repeating_characters.json @@ -0,0 +1,50 @@ +{ + "problem_name": "longest_substring_without_repeating_characters", + "solution_class_name": "Solution", + "problem_number": "3", + "problem_title": "Longest Substring Without Repeating Characters", + "difficulty": "Medium", + "topics": "Hash Table, String, Sliding Window", + "tags": ["grind-75"], + "readme_description": "Given a string `s`, find the length of the **longest** **substring** without duplicate characters.", + "readme_examples": [ + { + "content": "```\nInput: s = \"abcabcbb\"\nOutput: 3\n```\n**Explanation:** The answer is \"abc\", with the length of 3." + }, + { + "content": "```\nInput: s = \"bbbbb\"\nOutput: 1\n```\n**Explanation:** The answer is \"b\", with the length of 1." + }, + { + "content": "```\nInput: s = \"pwwkew\"\nOutput: 3\n```\n**Explanation:** The answer is \"wke\", with the length of 3.\nNotice that the answer must be a substring, \"pwke\" is a subsequence and not a substring." + } + ], + "readme_constraints": "- 0 <= s.length <= 5 * 10^4\n- s consists of English letters, digits, symbols and spaces.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "length_of_longest_substring", + "parameters": "s: str", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "LongestSubstringWithoutRepeatingCharacters", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_length_of_longest_substring", + "parametrize": "s, expected", + "parametrize_typed": "s: str, expected: int", + "test_cases": "[('abcabcbb', 3), ('bbbbb', 1), ('pwwkew', 3), ('', 0), ('a', 1), ('au', 2), ('dvdf', 3)]", + "body": "result = self.solution.length_of_longest_substring(s)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = 'abcabcbb'\nexpected = 3", + "playground_execution": "result = Solution().length_of_longest_substring(s)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/three_sum.json b/.templates/leetcode/json/three_sum.json new file mode 100644 index 0000000..3c161a0 --- /dev/null +++ b/.templates/leetcode/json/three_sum.json @@ -0,0 +1,50 @@ +{ + "problem_name": "three_sum", + "solution_class_name": "Solution", + "problem_number": "15", + "problem_title": "3Sum", + "difficulty": "Medium", + "topics": "Array, Two Pointers, Sorting", + "tags": ["grind-75"], + "readme_description": "Given an integer array `nums`, return all the triplets `[nums[i], nums[j], nums[k]]` such that `i != j`, `i != k`, and `j != k`, and `nums[i] + nums[j] + nums[k] == 0`.\n\nNotice that the solution set must not contain duplicate triplets.", + "readme_examples": [ + { + "content": "```\nInput: nums = [-1,0,1,2,-1,-4]\nOutput: [[-1,-1,2],[-1,0,1]]\n```\n**Explanation:** \nnums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.\nnums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.\nnums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.\nThe distinct triplets are [-1,0,1] and [-1,-1,2].\nNotice that the order of the output and the order of the triplets does not matter." + }, + { + "content": "```\nInput: nums = [0,1,1]\nOutput: []\n```\n**Explanation:** The only possible triplet does not sum up to 0." + }, + { + "content": "```\nInput: nums = [0,0,0]\nOutput: [[0,0,0]]\n```\n**Explanation:** The only possible triplet sums up to 0." + } + ], + "readme_constraints": "- 3 <= nums.length <= 3000\n- -10^5 <= nums[i] <= 10^5", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "three_sum", + "parameters": "nums: list[int]", + "return_type": "list[list[int]]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "ThreeSum", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_three_sum", + "parametrize": "nums, expected", + "parametrize_typed": "nums: list[int], expected: list[list[int]]", + "test_cases": "[([-1, 0, 1, 2, -1, -4], [[-1, -1, 2], [-1, 0, 1]]), ([0, 1, 1], []), ([0, 0, 0], [[0, 0, 0]]), ([-1, 0, 1], [[-1, 0, 1]]), ([1, 2, -2, -1], []), ([-2, 0, 1, 1, 2], [[-2, 0, 2], [-2, 1, 1]])]", + "body": "result = self.solution.three_sum(nums)\n# Sort both result and expected for comparison since order doesn't matter\nresult_sorted = [sorted(triplet) for triplet in result]\nexpected_sorted = [sorted(triplet) for triplet in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [-1, 0, 1, 2, -1, -4]\nexpected = [[-1, -1, 2], [-1, 0, 1]]", + "playground_execution": "result = Solution().three_sum(nums)\nresult", + "playground_assertion": "# Sort for comparison since order doesn't matter\nresult_sorted = [sorted(triplet) for triplet in result]\nexpected_sorted = [sorted(triplet) for triplet in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted" +} diff --git a/Makefile b/Makefile index 23fa74a..1d3cc9e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= zero_one_matrix +PROBLEM ?= course_schedule FORCE ?= 0 COMMA := , diff --git a/leetcode/binary_tree_level_order_traversal/README.md b/leetcode/binary_tree_level_order_traversal/README.md new file mode 100644 index 0000000..346f623 --- /dev/null +++ b/leetcode/binary_tree_level_order_traversal/README.md @@ -0,0 +1,41 @@ +# Binary Tree Level Order Traversal + +**Difficulty:** Medium +**Topics:** Tree, Breadth-First Search, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 102](https://leetcode.com/problems/binary-tree-level-order-traversal/description/) + +## Problem Description + +Given the `root` of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level). + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/02/19/tree1.jpg) + +``` +Input: root = [3,9,20,null,null,15,7] +Output: [[3],[9,20],[15,7]] +``` + +### Example 2: + +``` +Input: root = [1] +Output: [[1]] +``` + +### Example 3: + +``` +Input: root = [] +Output: [] +``` + +## Constraints + +- The number of nodes in the tree is in the range [0, 2000] +- -1000 <= Node.val <= 1000 diff --git a/leetcode/binary_tree_level_order_traversal/__init__.py b/leetcode/binary_tree_level_order_traversal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/binary_tree_level_order_traversal/playground.ipynb b/leetcode/binary_tree_level_order_traversal/playground.ipynb new file mode 100644 index 0000000..c0a13d9 --- /dev/null +++ b/leetcode/binary_tree_level_order_traversal/playground.ipynb @@ -0,0 +1,71 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import TreeNode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "root_list = [3, 9, 20, None, None, 15, 7]\n", + "root = TreeNode.from_list(root_list)\n", + "expected = [[3], [9, 20], [15, 7]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().level_order(root)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/binary_tree_level_order_traversal/solution.py b/leetcode/binary_tree_level_order_traversal/solution.py new file mode 100644 index 0000000..702c596 --- /dev/null +++ b/leetcode/binary_tree_level_order_traversal/solution.py @@ -0,0 +1,31 @@ +from collections import deque + +from leetcode_py import TreeNode + + +class Solution: + # Time: O(n) + # Space: O(w) where w is max width of tree + def level_order(self, root: TreeNode | None) -> list[list[int]]: + if not root: + return [] + + result = [] + queue = deque([root]) + + while queue: + level_size = len(queue) + level = [] + + for _ in range(level_size): + node = queue.popleft() + level.append(node.val) + + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + result.append(level) + + return result diff --git a/leetcode/binary_tree_level_order_traversal/tests.py b/leetcode/binary_tree_level_order_traversal/tests.py new file mode 100644 index 0000000..daddfb4 --- /dev/null +++ b/leetcode/binary_tree_level_order_traversal/tests.py @@ -0,0 +1,39 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestBinaryTreeLevelOrderTraversal: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "root_list, expected", + [ + ([3, 9, 20, None, None, 15, 7], [[3], [9, 20], [15, 7]]), + ([1], [[1]]), + ([], []), + ([1, 2, 3, 4, 5, 6, 7], [[1], [2, 3], [4, 5, 6, 7]]), + ([1, 2, None, 3, None, 4, None, 5], [[1], [2], [3], [4], [5]]), + # Edge cases + ([1, None, 2, None, 3], [[1], [2], [3]]), # Right skewed + ([1, 2, None, 3, None], [[1], [2], [3]]), # Left skewed + ([0], [[0]]), # Single zero node + ([-1, -2, -3], [[-1], [-2, -3]]), # Negative values + ([1, 2, 3, None, None, None, 4], [[1], [2, 3], [4]]), # Sparse tree + ( + [5, 4, 8, 11, None, 13, 4, 7, 2, None, None, None, 1], + [[5], [4, 8], [11, 13, 4], [7, 2, 1]], + ), # Complex tree + ([1, 2, 2, 3, 3, 3, 3], [[1], [2, 2], [3, 3, 3, 3]]), # Duplicate values + ([1, None, None], [[1]]), # Single node with null children + ], + ) + @logged_test + def test_level_order(self, root_list: list[int | None], expected: list[list[int]]): + root = TreeNode.from_list(root_list) if root_list else None + result = self.solution.level_order(root) + assert result == expected diff --git a/leetcode/course_schedule/README.md b/leetcode/course_schedule/README.md new file mode 100644 index 0000000..bdc1cdd --- /dev/null +++ b/leetcode/course_schedule/README.md @@ -0,0 +1,43 @@ +# Course Schedule + +**Difficulty:** Medium +**Topics:** Depth-First Search, Breadth-First Search, Graph, Topological Sort +**Tags:** grind-75 + +**LeetCode:** [Problem 207](https://leetcode.com/problems/course-schedule/description/) + +## Problem Description + +There are a total of `numCourses` courses you have to take, labeled from `0` to `numCourses - 1`. You are given an array `prerequisites` where `prerequisites[i] = [ai, bi]` indicates that you **must** take course `bi` first if you want to take course `ai`. + +- For example, the pair `[0, 1]`, indicates that to take course `0` you have to first take course `1`. + +Return `true` if you can finish all courses. Otherwise, return `false`. + +## Examples + +### Example 1: + +``` +Input: numCourses = 2, prerequisites = [[1,0]] +Output: true +``` + +**Explanation:** There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible. + +### Example 2: + +``` +Input: numCourses = 2, prerequisites = [[1,0],[0,1]] +Output: false +``` + +**Explanation:** There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible. + +## Constraints + +- `1 <= numCourses <= 2000` +- `0 <= prerequisites.length <= 5000` +- `prerequisites[i].length == 2` +- `0 <= ai, bi < numCourses` +- All the pairs prerequisites[i] are **unique**. diff --git a/leetcode/course_schedule/__init__.py b/leetcode/course_schedule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/course_schedule/playground.ipynb b/leetcode/course_schedule/playground.ipynb new file mode 100644 index 0000000..a3fbcc4 --- /dev/null +++ b/leetcode/course_schedule/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\nnum_courses = 2\nprerequisites = [[1, 0]]\nexpected = True"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().can_finish(num_courses, prerequisites)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/course_schedule/solution.py b/leetcode/course_schedule/solution.py new file mode 100644 index 0000000..54cf72e --- /dev/null +++ b/leetcode/course_schedule/solution.py @@ -0,0 +1,29 @@ +class Solution: + # Time: O(V + E) where V = num_courses, E = prerequisites + # Space: O(V + E) for adjacency list and recursion stack + def can_finish(self, num_courses: int, prerequisites: list[list[int]]) -> bool: + UNVISITED, VISITING, VISITED = 0, 1, 2 + + graph: list[list[int]] = [[] for _ in range(num_courses)] + for course, prereq in prerequisites: + graph[course].append(prereq) + + state = [UNVISITED] * num_courses + + def has_cycle(course: int) -> bool: + if state[course] == VISITING: # Currently visiting - cycle detected + return True + if state[course] == VISITED: + return False + + state[course] = VISITING + for prereq in graph[course]: + if has_cycle(prereq): + return True + state[course] = VISITED + return False + + for course in range(num_courses): + if state[course] == UNVISITED and has_cycle(course): + return False + return True diff --git a/leetcode/course_schedule/tests.py b/leetcode/course_schedule/tests.py new file mode 100644 index 0000000..5eb8d60 --- /dev/null +++ b/leetcode/course_schedule/tests.py @@ -0,0 +1,40 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestCourseSchedule: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "num_courses, prerequisites, expected", + [ + # Basic cases + (2, [[1, 0]], True), + (2, [[1, 0], [0, 1]], False), + (1, [], True), + (3, [[1, 0], [2, 1]], True), + (4, [[1, 0], [2, 1], [3, 2], [1, 3]], False), + # Edge cases + (0, [], True), + (5, [], True), + (3, [[0, 1], [0, 2], [1, 2]], True), + # Self-loop + (2, [[0, 0]], False), + (3, [[1, 1]], False), + # Complex valid cases + (6, [[1, 0], [2, 0], [3, 1], [3, 2], [4, 3], [5, 4]], True), + (4, [[0, 1], [0, 2], [1, 3], [2, 3]], True), + # Complex cycles + (3, [[0, 1], [1, 2], [2, 0]], False), + (5, [[1, 0], [2, 1], [3, 2], [4, 3], [0, 4]], False), + (4, [[1, 0], [2, 0], [0, 3], [3, 1]], False), + ], + ) + @logged_test + def test_can_finish(self, num_courses: int, prerequisites: list[list[int]], expected: bool): + result = self.solution.can_finish(num_courses, prerequisites) + assert result == expected diff --git a/leetcode/longest_substring_without_repeating_characters/README.md b/leetcode/longest_substring_without_repeating_characters/README.md new file mode 100644 index 0000000..53c884d --- /dev/null +++ b/leetcode/longest_substring_without_repeating_characters/README.md @@ -0,0 +1,46 @@ +# Longest Substring Without Repeating Characters + +**Difficulty:** Medium +**Topics:** Hash Table, String, Sliding Window +**Tags:** grind-75 + +**LeetCode:** [Problem 3](https://leetcode.com/problems/longest-substring-without-repeating-characters/description/) + +## Problem Description + +Given a string `s`, find the length of the **longest** **substring** without duplicate characters. + +## Examples + +### Example 1: + +``` +Input: s = "abcabcbb" +Output: 3 +``` + +**Explanation:** The answer is "abc", with the length of 3. + +### Example 2: + +``` +Input: s = "bbbbb" +Output: 1 +``` + +**Explanation:** The answer is "b", with the length of 1. + +### Example 3: + +``` +Input: s = "pwwkew" +Output: 3 +``` + +**Explanation:** The answer is "wke", with the length of 3. +Notice that the answer must be a substring, "pwke" is a subsequence and not a substring. + +## Constraints + +- 0 <= s.length <= 5 \* 10^4 +- s consists of English letters, digits, symbols and spaces. diff --git a/leetcode/longest_substring_without_repeating_characters/__init__.py b/leetcode/longest_substring_without_repeating_characters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/longest_substring_without_repeating_characters/playground.ipynb b/leetcode/longest_substring_without_repeating_characters/playground.ipynb new file mode 100644 index 0000000..c437eba --- /dev/null +++ b/leetcode/longest_substring_without_repeating_characters/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": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "s = \"abcabcbb\"\n", + "expected = 3" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().length_of_longest_substring(s)\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/longest_substring_without_repeating_characters/solution.py b/leetcode/longest_substring_without_repeating_characters/solution.py new file mode 100644 index 0000000..034f4a8 --- /dev/null +++ b/leetcode/longest_substring_without_repeating_characters/solution.py @@ -0,0 +1,15 @@ +class Solution: + # Time: O(n) + # Space: O(min(m, n)) where m is charset size + def length_of_longest_substring(self, s: str) -> int: + seen: set[str] = set() + left = max_len = 0 + + for right in range(len(s)): + while s[right] in seen: + seen.remove(s[left]) + left += 1 + seen.add(s[right]) + max_len = max(max_len, right - left + 1) + + return max_len diff --git a/leetcode/longest_substring_without_repeating_characters/tests.py b/leetcode/longest_substring_without_repeating_characters/tests.py new file mode 100644 index 0000000..272cdd4 --- /dev/null +++ b/leetcode/longest_substring_without_repeating_characters/tests.py @@ -0,0 +1,35 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestLongestSubstringWithoutRepeatingCharacters: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, expected", + [ + ("abcabcbb", 3), # "abc" + ("bbbbb", 1), # "b" + ("pwwkew", 3), # "wke" + ("", 0), # empty + ("a", 1), # single char + ("au", 2), # "au" + ("dvdf", 3), # "vdf" + ("abcdef", 6), # no repeats + ("aab", 2), # "ab" + ("cdd", 2), # "cd" + ("abba", 2), # "ab" or "ba" + ("tmmzuxt", 5), # "mzuxt" + (" ", 1), # space char + ("!@#$%", 5), # symbols + ("abcabcabcabc", 3), # repeating pattern + ], + ) + @logged_test + def test_length_of_longest_substring(self, s: str, expected: int): + result = self.solution.length_of_longest_substring(s) + assert result == expected diff --git a/leetcode/three_sum/README.md b/leetcode/three_sum/README.md new file mode 100644 index 0000000..c031c0e --- /dev/null +++ b/leetcode/three_sum/README.md @@ -0,0 +1,52 @@ +# 3Sum + +**Difficulty:** Medium +**Topics:** Array, Two Pointers, Sorting +**Tags:** grind-75 + +**LeetCode:** [Problem 15](https://leetcode.com/problems/three-sum/description/) + +## Problem Description + +Given an integer array `nums`, return all the triplets `[nums[i], nums[j], nums[k]]` such that `i != j`, `i != k`, and `j != k`, and `nums[i] + nums[j] + nums[k] == 0`. + +Notice that the solution set must not contain duplicate triplets. + +## Examples + +### Example 1: + +``` +Input: nums = [-1,0,1,2,-1,-4] +Output: [[-1,-1,2],[-1,0,1]] +``` + +**Explanation:** +nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0. +nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0. +nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0. +The distinct triplets are [-1,0,1] and [-1,-1,2]. +Notice that the order of the output and the order of the triplets does not matter. + +### Example 2: + +``` +Input: nums = [0,1,1] +Output: [] +``` + +**Explanation:** The only possible triplet does not sum up to 0. + +### Example 3: + +``` +Input: nums = [0,0,0] +Output: [[0,0,0]] +``` + +**Explanation:** The only possible triplet sums up to 0. + +## Constraints + +- 3 <= nums.length <= 3000 +- -10^5 <= nums[i] <= 10^5 diff --git a/leetcode/three_sum/__init__.py b/leetcode/three_sum/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/three_sum/playground.ipynb b/leetcode/three_sum/playground.ipynb new file mode 100644 index 0000000..007270c --- /dev/null +++ b/leetcode/three_sum/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\nnums = [-1, 0, 1, 2, -1, -4]\nexpected = [[-1, -1, 2], [-1, 0, 1]]"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().three_sum(nums)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": ["# Sort for comparison since order doesn't matter\nresult_sorted = [sorted(triplet) for triplet in result]\nexpected_sorted = [sorted(triplet) for triplet in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted"] + } + ], + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/three_sum/solution.py b/leetcode/three_sum/solution.py new file mode 100644 index 0000000..6846e00 --- /dev/null +++ b/leetcode/three_sum/solution.py @@ -0,0 +1,23 @@ +class Solution: + # Time: O(n^2) + # Space: O(k) where k is number of unique triplets + def three_sum(self, nums: list[int]) -> list[list[int]]: + nums.sort() + result = set() + + for i in range(len(nums) - 2): + left, right = i + 1, len(nums) - 1 + + while left < right: + total = nums[i] + nums[left] + nums[right] + + if total < 0: + left += 1 + elif total > 0: + right -= 1 + else: + result.add((nums[i], nums[left], nums[right])) + left += 1 + right -= 1 + + return [list(triplet) for triplet in result] diff --git a/leetcode/three_sum/tests.py b/leetcode/three_sum/tests.py new file mode 100644 index 0000000..9448085 --- /dev/null +++ b/leetcode/three_sum/tests.py @@ -0,0 +1,43 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestThreeSum: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, expected", + [ + ([-1, 0, 1, 2, -1, -4], [[-1, -1, 2], [-1, 0, 1]]), + ([0, 1, 1], []), + ([0, 0, 0], [[0, 0, 0]]), + ([-1, 0, 1], [[-1, 0, 1]]), + ([1, 2, -2, -1], []), + ([-2, 0, 1, 1, 2], [[-2, 0, 2], [-2, 1, 1]]), + # Edge cases + ([1, 2, 3], []), # All positive + ([-3, -2, -1], []), # All negative + ([0, 0, 0, 0], [[0, 0, 0]]), # Multiple zeros + ([-1, -1, 2, 2], [[-1, -1, 2]]), # Duplicate pairs + ([3, 0, -2, -1, 1, 2], [[-2, -1, 3], [-2, 0, 2], [-1, 0, 1]]), # Multiple solutions + ( + [-4, -2, -2, -2, 0, 1, 2, 2, 2, 3, 3, 4, 4, 6, 6], + [[-4, -2, 6], [-4, 0, 4], [-4, 1, 3], [-4, 2, 2], [-2, -2, 4], [-2, 0, 2]], + ), # Many duplicates + ([1, -1, 0], [[-1, 0, 1]]), # Simple case + ([2, -1, -1], [[-1, -1, 2]]), # Solution with duplicates + ], + ) + @logged_test + def test_three_sum(self, nums: list[int], expected: list[list[int]]): + result = self.solution.three_sum(nums) + # Sort both result and expected for comparison since order doesn't matter + result_sorted = [sorted(triplet) for triplet in result] + expected_sorted = [sorted(triplet) for triplet in expected] + result_sorted.sort() + expected_sorted.sort() + assert result_sorted == expected_sorted From b422bafed04c083a4ae67ebf86f4ab3d675c5626 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sun, 7 Sep 2025 12:59:23 +0700 Subject: [PATCH 24/26] feat: add Rotting Oranges --- .templates/leetcode/json/min_stack.json | 41 ++++++++ .../leetcode/json/number_of_islands.json | 47 +++++++++ .templates/leetcode/json/rotting_oranges.json | 50 ++++++++++ Makefile | 2 +- leetcode/min_stack/README.md | 53 +++++++++++ leetcode/min_stack/__init__.py | 0 leetcode/min_stack/playground.ipynb | 95 +++++++++++++++++++ leetcode/min_stack/solution.py | 43 +++++++++ leetcode/min_stack/tests.py | 52 ++++++++++ leetcode/number_of_islands/README.md | 46 +++++++++ leetcode/number_of_islands/__init__.py | 0 leetcode/number_of_islands/playground.ipynb | 72 ++++++++++++++ leetcode/number_of_islands/solution.py | 27 ++++++ leetcode/number_of_islands/tests.py | 67 +++++++++++++ leetcode/rotting_oranges/README.md | 55 +++++++++++ leetcode/rotting_oranges/__init__.py | 0 leetcode/rotting_oranges/playground.ipynb | 57 +++++++++++ leetcode/rotting_oranges/solution.py | 43 +++++++++ leetcode/rotting_oranges/tests.py | 60 ++++++++++++ 19 files changed, 809 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/min_stack.json create mode 100644 .templates/leetcode/json/number_of_islands.json create mode 100644 .templates/leetcode/json/rotting_oranges.json create mode 100644 leetcode/min_stack/README.md create mode 100644 leetcode/min_stack/__init__.py create mode 100644 leetcode/min_stack/playground.ipynb create mode 100644 leetcode/min_stack/solution.py create mode 100644 leetcode/min_stack/tests.py create mode 100644 leetcode/number_of_islands/README.md create mode 100644 leetcode/number_of_islands/__init__.py create mode 100644 leetcode/number_of_islands/playground.ipynb create mode 100644 leetcode/number_of_islands/solution.py create mode 100644 leetcode/number_of_islands/tests.py create mode 100644 leetcode/rotting_oranges/README.md create mode 100644 leetcode/rotting_oranges/__init__.py create mode 100644 leetcode/rotting_oranges/playground.ipynb create mode 100644 leetcode/rotting_oranges/solution.py create mode 100644 leetcode/rotting_oranges/tests.py diff --git a/.templates/leetcode/json/min_stack.json b/.templates/leetcode/json/min_stack.json new file mode 100644 index 0000000..1ed8390 --- /dev/null +++ b/.templates/leetcode/json/min_stack.json @@ -0,0 +1,41 @@ +{ + "problem_name": "min_stack", + "solution_class_name": "MinStack", + "problem_number": "155", + "problem_title": "Min Stack", + "difficulty": "Medium", + "topics": "Stack, Design", + "tags": ["grind-75"], + "readme_description": "Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.\n\nImplement the `MinStack` class:\n\n- `MinStack()` initializes the stack object.\n- `void push(int val)` pushes the element `val` onto the stack.\n- `void pop()` removes the element on the top of the stack.\n- `int top()` gets the top element of the stack.\n- `int getMin()` retrieves the minimum element in the stack.\n\nYou must implement a solution with `O(1)` time complexity for each function.", + "readme_examples": [ + { + "content": "```\nInput\n[\"MinStack\",\"push\",\"push\",\"push\",\"getMin\",\"pop\",\"top\",\"getMin\"]\n[[],[-2],[0],[-3],[],[],[],[]]\n\nOutput\n[null,null,null,null,-3,null,0,-2]\n```\n**Explanation:**\n```\nMinStack minStack = new MinStack();\nminStack.push(-2);\nminStack.push(0);\nminStack.push(-3);\nminStack.getMin(); // return -3\nminStack.pop();\nminStack.top(); // return 0\nminStack.getMin(); // return -2\n```" + } + ], + "readme_constraints": "- `-2^31 <= val <= 2^31 - 1`\n- Methods `pop`, `top` and `getMin` operations will always be called on **non-empty** stacks.\n- At most `3 * 10^4` calls will be made to `push`, `pop`, `top`, and `getMin`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" }, + { "name": "push", "parameters": "val: int", "return_type": "None", "dummy_return": "" }, + { "name": "pop", "parameters": "", "return_type": "None", "dummy_return": "" }, + { "name": "top", "parameters": "", "return_type": "int", "dummy_return": "0" }, + { "name": "get_min", "parameters": "", "return_type": "int", "dummy_return": "0" } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import MinStack", + "test_class_name": "MinStack", + "test_helper_methods": [], + "test_methods": [ + { + "name": "test_min_stack", + "parametrize": "operations, inputs, expected", + "parametrize_typed": "operations: list[str], inputs: list[list[int]], expected: list[int | None]", + "test_cases": "[([\"MinStack\", \"push\", \"push\", \"push\", \"getMin\", \"pop\", \"top\", \"getMin\"], [[], [-2], [0], [-3], [], [], [], []], [None, None, None, None, -3, None, 0, -2]), ([\"MinStack\", \"push\", \"top\", \"getMin\", \"pop\"], [[], [5], [], [], []], [None, None, 5, 5, None]), ([\"MinStack\", \"push\", \"push\", \"push\", \"getMin\", \"pop\", \"getMin\", \"pop\", \"getMin\"], [[], [1], [1], [2], [], [], [], [], []], [None, None, None, None, 1, None, 1, None, 1]), ([\"MinStack\", \"push\", \"push\", \"getMin\", \"push\", \"getMin\", \"pop\", \"getMin\"], [[], [3], [1], [], [0], [], [], []], [None, None, None, 1, None, 0, None, 1])]", + "body": "stack: MinStack | None = None\nresults: list[int | None] = []\nfor i, op in enumerate(operations):\n if op == \"MinStack\":\n stack = MinStack()\n results.append(None)\n elif op == \"push\" and stack is not None:\n stack.push(inputs[i][0])\n results.append(None)\n elif op == \"pop\" and stack is not None:\n stack.pop()\n results.append(None)\n elif op == \"top\" and stack is not None:\n results.append(stack.top())\n elif op == \"getMin\" and stack is not None:\n results.append(stack.get_min())\nassert results == expected" + } + ], + "playground_imports": "from solution import MinStack", + "playground_test_case": "# Example test case\noperations = ['MinStack', 'push', 'push', 'push', 'getMin', 'pop', 'top', 'getMin']\ninputs = [[], [-2], [0], [-3], [], [], [], []]", + "playground_execution": "stack = None\nresults: list[int | None] = []\nfor i, op in enumerate(operations):\n if op == 'MinStack':\n stack = MinStack()\n results.append(None)\n elif op == 'push' and stack is not None:\n stack.push(inputs[i][0])\n results.append(None)\n elif op == 'pop' and stack is not None:\n stack.pop()\n results.append(None)\n elif op == 'top' and stack is not None:\n results.append(stack.top())\n elif op == 'getMin' and stack is not None:\n results.append(stack.get_min())\nresults", + "playground_assertion": "expected = [None, None, None, None, -3, None, 0, -2]\nassert results == expected" +} diff --git a/.templates/leetcode/json/number_of_islands.json b/.templates/leetcode/json/number_of_islands.json new file mode 100644 index 0000000..9a08c09 --- /dev/null +++ b/.templates/leetcode/json/number_of_islands.json @@ -0,0 +1,47 @@ +{ + "problem_name": "number_of_islands", + "solution_class_name": "Solution", + "problem_number": "200", + "problem_title": "Number of Islands", + "difficulty": "Medium", + "topics": "Array, Depth-First Search, Breadth-First Search, Union Find, Matrix", + "tags": ["grind-75"], + "readme_description": "Given an `m x n` 2D binary grid `grid` which represents a map of `'1'`s (land) and `'0'`s (water), return *the number of islands*.\n\nAn **island** is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.", + "readme_examples": [ + { + "content": "```\nInput: grid = [\n [\"1\",\"1\",\"1\",\"1\",\"0\"],\n [\"1\",\"1\",\"0\",\"1\",\"0\"],\n [\"1\",\"1\",\"0\",\"0\",\"0\"],\n [\"0\",\"0\",\"0\",\"0\",\"0\"]\n]\nOutput: 1\n```" + }, + { + "content": "```\nInput: grid = [\n [\"1\",\"1\",\"0\",\"0\",\"0\"],\n [\"1\",\"1\",\"0\",\"0\",\"0\"],\n [\"0\",\"0\",\"1\",\"0\",\"0\"],\n [\"0\",\"0\",\"0\",\"1\",\"1\"]\n]\nOutput: 3\n```" + } + ], + "readme_constraints": "- `m == grid.length`\n- `n == grid[i].length`\n- `1 <= m, n <= 300`\n- `grid[i][j]` is `'0'` or `'1'`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "num_islands", + "parameters": "grid: list[list[str]]", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "NumberOfIslands", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_num_islands", + "parametrize": "grid, expected", + "parametrize_typed": "grid: list[list[str]], expected: int", + "test_cases": "[([['1','1','1','1','0'],['1','1','0','1','0'],['1','1','0','0','0'],['0','0','0','0','0']], 1), ([['1','1','0','0','0'],['1','1','0','0','0'],['0','0','1','0','0'],['0','0','0','1','1']], 3), ([['1']], 1), ([['0']], 0), ([['1','0','1'],['0','1','0'],['1','0','1']], 5)]", + "body": "result = self.solution.num_islands(grid)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ngrid = [\n ['1','1','1','1','0'],\n ['1','1','0','1','0'],\n ['1','1','0','0','0'],\n ['0','0','0','0','0']\n]\nexpected = 1", + "playground_execution": "result = Solution().num_islands(grid)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/rotting_oranges.json b/.templates/leetcode/json/rotting_oranges.json new file mode 100644 index 0000000..2467648 --- /dev/null +++ b/.templates/leetcode/json/rotting_oranges.json @@ -0,0 +1,50 @@ +{ + "problem_name": "rotting_oranges", + "solution_class_name": "Solution", + "problem_number": "994", + "problem_title": "Rotting Oranges", + "difficulty": "Medium", + "topics": "Array, Breadth-First Search, Matrix", + "tags": ["grind-75"], + "readme_description": "You are given an `m x n` `grid` where each cell can have one of three values:\n\n- `0` representing an empty cell,\n- `1` representing a fresh orange, or\n- `2` representing a rotten orange.\n\nEvery minute, any fresh orange that is **4-directionally adjacent** to a rotten orange becomes rotten.\n\nReturn *the minimum number of minutes that must elapse until no cell has a fresh orange*. If *this is impossible, return* `-1`.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2019/02/16/oranges.png)\n\n```\nInput: grid = [[2,1,1],[1,1,0],[0,1,1]]\nOutput: 4\n```" + }, + { + "content": "```\nInput: grid = [[2,1,1],[0,1,1],[1,0,1]]\nOutput: -1\n```\n**Explanation:** The orange in the bottom left corner (row 2, column 0) is never rotten, because rotting only happens 4-directionally." + }, + { + "content": "```\nInput: grid = [[0,2]]\nOutput: 0\n```\n**Explanation:** Since there are already no fresh oranges at minute 0, the answer is just 0." + } + ], + "readme_constraints": "- `m == grid.length`\n- `n == grid[i].length`\n- `1 <= m, n <= 10`\n- `grid[i][j]` is `0`, `1`, or `2`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "oranges_rotting", + "parameters": "grid: list[list[int]]", + "return_type": "int", + "dummy_return": "0" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "TestRottingOranges", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_oranges_rotting", + "parametrize": "grid, expected", + "parametrize_typed": "grid: list[list[int]], expected: int", + "test_cases": "[([[2, 1, 1], [1, 1, 0], [0, 1, 1]], 4), ([[2, 1, 1], [0, 1, 1], [1, 0, 1]], -1), ([[0, 2]], 0), ([[0]], 0), ([[1]], -1), ([[2]], 0), ([[1, 2]], 1), ([[2, 1]], 1), ([[0, 1, 2]], 1), ([[2, 2], [1, 1], [0, 0]], 1), ([[2, 1, 1], [1, 1, 1], [0, 1, 2]], 2)]", + "body": "result = self.solution.oranges_rotting(grid)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ngrid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]]\nexpected = 4", + "playground_execution": "result = Solution().oranges_rotting(grid)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 1d3cc9e..0580c5a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= course_schedule +PROBLEM ?= rotting_oranges FORCE ?= 0 COMMA := , diff --git a/leetcode/min_stack/README.md b/leetcode/min_stack/README.md new file mode 100644 index 0000000..46edefb --- /dev/null +++ b/leetcode/min_stack/README.md @@ -0,0 +1,53 @@ +# Min Stack + +**Difficulty:** Medium +**Topics:** Stack, Design +**Tags:** grind-75 + +**LeetCode:** [Problem 155](https://leetcode.com/problems/min-stack/description/) + +## Problem Description + +Design a stack that supports push, pop, top, and retrieving the minimum element in constant time. + +Implement the `MinStack` class: + +- `MinStack()` initializes the stack object. +- `void push(int val)` pushes the element `val` onto the stack. +- `void pop()` removes the element on the top of the stack. +- `int top()` gets the top element of the stack. +- `int getMin()` retrieves the minimum element in the stack. + +You must implement a solution with `O(1)` time complexity for each function. + +## Examples + +### Example 1: + +``` +Input +["MinStack","push","push","push","getMin","pop","top","getMin"] +[[],[-2],[0],[-3],[],[],[],[]] + +Output +[null,null,null,null,-3,null,0,-2] +``` + +**Explanation:** + +``` +MinStack minStack = new MinStack(); +minStack.push(-2); +minStack.push(0); +minStack.push(-3); +minStack.getMin(); // return -3 +minStack.pop(); +minStack.top(); // return 0 +minStack.getMin(); // return -2 +``` + +## Constraints + +- `-2^31 <= val <= 2^31 - 1` +- Methods `pop`, `top` and `getMin` operations will always be called on **non-empty** stacks. +- At most `3 * 10^4` calls will be made to `push`, `pop`, `top`, and `getMin`. diff --git a/leetcode/min_stack/__init__.py b/leetcode/min_stack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/min_stack/playground.ipynb b/leetcode/min_stack/playground.ipynb new file mode 100644 index 0000000..72b888c --- /dev/null +++ b/leetcode/min_stack/playground.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import MinStack" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "operations = [\"MinStack\", \"push\", \"push\", \"push\", \"getMin\", \"pop\", \"top\", \"getMin\"]\n", + "inputs = [[], [-2], [0], [-3], [], [], [], []]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[None, None, None, None, -3, None, 0, -2]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stack = None\n", + "results: list[int | None] = []\n", + "for i, op in enumerate(operations):\n", + " if op == \"MinStack\":\n", + " stack = MinStack()\n", + " results.append(None)\n", + " elif op == \"push\" and stack is not None:\n", + " stack.push(inputs[i][0])\n", + " results.append(None)\n", + " elif op == \"pop\" and stack is not None:\n", + " stack.pop()\n", + " results.append(None)\n", + " elif op == \"top\" and stack is not None:\n", + " results.append(stack.top())\n", + " elif op == \"getMin\" and stack is not None:\n", + " results.append(stack.get_min())\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "expected = [None, None, None, None, -3, None, 0, -2]\n", + "assert results == 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/min_stack/solution.py b/leetcode/min_stack/solution.py new file mode 100644 index 0000000..fc59403 --- /dev/null +++ b/leetcode/min_stack/solution.py @@ -0,0 +1,43 @@ +class MinStack: + # Time: O(1) for all operations + # Space: O(n) where n is number of elements + def __init__(self) -> None: + self.stack: list[int] = [] + self.min_stack: list[int] = [] + + # Time: O(1) + # Space: O(1) + def push(self, val: int) -> None: + self.stack.append(val) + if not self.min_stack or val <= self.min_stack[-1]: + self.min_stack.append(val) + + # Time: O(1) + # Space: O(1) + def pop(self) -> None: + if self.stack[-1] == self.min_stack[-1]: + self.min_stack.pop() + self.stack.pop() + + # Time: O(1) + # Space: O(1) + def top(self) -> int: + return self.stack[-1] + + # Time: O(1) + # Space: O(1) + def get_min(self) -> int: + return self.min_stack[-1] + + +# Example walkthrough: push(-2), push(0), push(-3), getMin(), pop(), top(), getMin() +# +# Initial: stack=[], min_stack=[] +# +# push(-2): stack=[-2], min_stack=[-2] (first element, add to both) +# push(0): stack=[-2,0], min_stack=[-2] (0 > -2, don't add to min_stack) +# push(-3): stack=[-2,0,-3], min_stack=[-2,-3] (-3 <= -2, add to min_stack) +# getMin(): return -3 (top of min_stack) +# pop(): stack=[-2,0], min_stack=[-2] (-3 was min, remove from both stacks) +# top(): return 0 (top of main stack) +# getMin(): return -2 (top of min_stack after pop) diff --git a/leetcode/min_stack/tests.py b/leetcode/min_stack/tests.py new file mode 100644 index 0000000..af73df4 --- /dev/null +++ b/leetcode/min_stack/tests.py @@ -0,0 +1,52 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import MinStack + + +class TestMinStack: + @pytest.mark.parametrize( + "operations, inputs, expected", + [ + ( + ["MinStack", "push", "push", "push", "getMin", "pop", "top", "getMin"], + [[], [-2], [0], [-3], [], [], [], []], + [None, None, None, None, -3, None, 0, -2], + ), + ( + ["MinStack", "push", "top", "getMin", "pop"], + [[], [5], [], [], []], + [None, None, 5, 5, None], + ), + ( + ["MinStack", "push", "push", "push", "getMin", "pop", "getMin", "pop", "getMin"], + [[], [1], [1], [2], [], [], [], [], []], + [None, None, None, None, 1, None, 1, None, 1], + ), + ( + ["MinStack", "push", "push", "getMin", "push", "getMin", "pop", "getMin"], + [[], [3], [1], [], [0], [], [], []], + [None, None, None, 1, None, 0, None, 1], + ), + ], + ) + @logged_test + def test_min_stack(self, operations: list[str], inputs: list[list[int]], expected: list[int | None]): + stack: MinStack | None = None + results: list[int | None] = [] + for i, op in enumerate(operations): + if op == "MinStack": + stack = MinStack() + results.append(None) + elif op == "push" and stack is not None: + stack.push(inputs[i][0]) + results.append(None) + elif op == "pop" and stack is not None: + stack.pop() + results.append(None) + elif op == "top" and stack is not None: + results.append(stack.top()) + elif op == "getMin" and stack is not None: + results.append(stack.get_min()) + assert results == expected diff --git a/leetcode/number_of_islands/README.md b/leetcode/number_of_islands/README.md new file mode 100644 index 0000000..ada9064 --- /dev/null +++ b/leetcode/number_of_islands/README.md @@ -0,0 +1,46 @@ +# Number of Islands + +**Difficulty:** Medium +**Topics:** Array, Depth-First Search, Breadth-First Search, Union Find, Matrix +**Tags:** grind-75 + +**LeetCode:** [Problem 200](https://leetcode.com/problems/number-of-islands/description/) + +## Problem Description + +Given an `m x n` 2D binary grid `grid` which represents a map of `'1'`s (land) and `'0'`s (water), return _the number of islands_. + +An **island** is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water. + +## Examples + +### Example 1: + +``` +Input: grid = [ + ["1","1","1","1","0"], + ["1","1","0","1","0"], + ["1","1","0","0","0"], + ["0","0","0","0","0"] +] +Output: 1 +``` + +### Example 2: + +``` +Input: grid = [ + ["1","1","0","0","0"], + ["1","1","0","0","0"], + ["0","0","1","0","0"], + ["0","0","0","1","1"] +] +Output: 3 +``` + +## Constraints + +- `m == grid.length` +- `n == grid[i].length` +- `1 <= m, n <= 300` +- `grid[i][j]` is `'0'` or `'1'`. diff --git a/leetcode/number_of_islands/__init__.py b/leetcode/number_of_islands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/number_of_islands/playground.ipynb b/leetcode/number_of_islands/playground.ipynb new file mode 100644 index 0000000..6b0ed5f --- /dev/null +++ b/leetcode/number_of_islands/playground.ipynb @@ -0,0 +1,72 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "grid = [\n", + " [\"1\", \"1\", \"1\", \"1\", \"0\"],\n", + " [\"1\", \"1\", \"0\", \"1\", \"0\"],\n", + " [\"1\", \"1\", \"0\", \"0\", \"0\"],\n", + " [\"0\", \"0\", \"0\", \"0\", \"0\"],\n", + "]\n", + "expected = 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().num_islands(grid)\nresult" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/number_of_islands/solution.py b/leetcode/number_of_islands/solution.py new file mode 100644 index 0000000..62368c8 --- /dev/null +++ b/leetcode/number_of_islands/solution.py @@ -0,0 +1,27 @@ +class Solution: + # Time: O(m * n) where m = rows, n = cols + # Space: O(m * n) for recursion stack in worst case + def num_islands(self, grid: list[list[str]]) -> int: + if not grid or not grid[0]: + return 0 + VISITED = "0" + UNVISITED_ISLAND = "1" + + rows, cols = len(grid), len(grid[0]) + islands = 0 + + def dfs(r: int, c: int) -> None: + if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != UNVISITED_ISLAND: + return + + grid[r][c] = VISITED + for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + dfs(r + dr, c + dc) + + for r in range(rows): + for c in range(cols): + if grid[r][c] == UNVISITED_ISLAND: + islands += 1 + dfs(r, c) + + return islands diff --git a/leetcode/number_of_islands/tests.py b/leetcode/number_of_islands/tests.py new file mode 100644 index 0000000..c3eb4e3 --- /dev/null +++ b/leetcode/number_of_islands/tests.py @@ -0,0 +1,67 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestNumberOfIslands: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "grid, expected", + [ + # Basic examples + ( + [ + ["1", "1", "1", "1", "0"], + ["1", "1", "0", "1", "0"], + ["1", "1", "0", "0", "0"], + ["0", "0", "0", "0", "0"], + ], + 1, + ), + ( + [ + ["1", "1", "0", "0", "0"], + ["1", "1", "0", "0", "0"], + ["0", "0", "1", "0", "0"], + ["0", "0", "0", "1", "1"], + ], + 3, + ), + # Edge cases + ([["1"]], 1), + ([["0"]], 0), + ([["1", "0", "1"], ["0", "1", "0"], ["1", "0", "1"]], 5), + # All water + ([["0", "0", "0"], ["0", "0", "0"]], 0), + # All land + ([["1", "1", "1"], ["1", "1", "1"]], 1), + # Single row + ([["1", "0", "1", "0", "1"]], 3), + # Single column + ([["1"], ["0"], ["1"], ["0"], ["1"]], 3), + # L-shaped island + ([["1", "1", "0"], ["1", "0", "0"], ["1", "1", "1"]], 1), + # Diagonal pattern (no connections) + ([["1", "0", "0"], ["0", "1", "0"], ["0", "0", "1"]], 3), + # Large grid with multiple islands + ( + [ + ["1", "0", "0", "1", "1", "0"], + ["0", "0", "0", "0", "1", "0"], + ["0", "0", "1", "0", "0", "0"], + ["1", "1", "0", "0", "0", "1"], + ], + 5, + ), + # Snake-like island + ([["1", "1", "0"], ["0", "1", "0"], ["0", "1", "1"]], 1), + ], + ) + @logged_test + def test_num_islands(self, grid: list[list[str]], expected: int): + result = self.solution.num_islands(grid) + assert result == expected diff --git a/leetcode/rotting_oranges/README.md b/leetcode/rotting_oranges/README.md new file mode 100644 index 0000000..36d2c82 --- /dev/null +++ b/leetcode/rotting_oranges/README.md @@ -0,0 +1,55 @@ +# Rotting Oranges + +**Difficulty:** Medium +**Topics:** Array, Breadth-First Search, Matrix +**Tags:** grind-75 + +**LeetCode:** [Problem 994](https://leetcode.com/problems/rotting-oranges/description/) + +## Problem Description + +You are given an `m x n` `grid` where each cell can have one of three values: + +- `0` representing an empty cell, +- `1` representing a fresh orange, or +- `2` representing a rotten orange. + +Every minute, any fresh orange that is **4-directionally adjacent** to a rotten orange becomes rotten. + +Return _the minimum number of minutes that must elapse until no cell has a fresh orange_. If _this is impossible, return_ `-1`. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2019/02/16/oranges.png) + +``` +Input: grid = [[2,1,1],[1,1,0],[0,1,1]] +Output: 4 +``` + +### Example 2: + +``` +Input: grid = [[2,1,1],[0,1,1],[1,0,1]] +Output: -1 +``` + +**Explanation:** The orange in the bottom left corner (row 2, column 0) is never rotten, because rotting only happens 4-directionally. + +### Example 3: + +``` +Input: grid = [[0,2]] +Output: 0 +``` + +**Explanation:** Since there are already no fresh oranges at minute 0, the answer is just 0. + +## Constraints + +- `m == grid.length` +- `n == grid[i].length` +- `1 <= m, n <= 10` +- `grid[i][j]` is `0`, `1`, or `2`. diff --git a/leetcode/rotting_oranges/__init__.py b/leetcode/rotting_oranges/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/rotting_oranges/playground.ipynb b/leetcode/rotting_oranges/playground.ipynb new file mode 100644 index 0000000..077b70a --- /dev/null +++ b/leetcode/rotting_oranges/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\ngrid = [[2, 1, 1], [1, 1, 0], [0, 1, 1]]\nexpected = 4"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().oranges_rotting(grid)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/rotting_oranges/solution.py b/leetcode/rotting_oranges/solution.py new file mode 100644 index 0000000..747d2c9 --- /dev/null +++ b/leetcode/rotting_oranges/solution.py @@ -0,0 +1,43 @@ +from collections import deque + + +class Solution: + # Time: O(m*n) + # Space: O(m*n) + def oranges_rotting(self, grid: list[list[int]]) -> int: + EMPTY, FRESH, ROTTEN = 0, 1, 2 + _ = EMPTY + + m, n = len(grid), len(grid[0]) + queue: deque[tuple[int, int]] = deque() + fresh = 0 + + # Find all rotten oranges and count fresh ones + for i in range(m): + for j in range(n): + if grid[i][j] == ROTTEN: + queue.append((i, j)) + elif grid[i][j] == FRESH: + fresh += 1 + + if fresh == 0: + return 0 + + minutes = 0 + directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] + + while queue: + size = len(queue) + for _ in range(size): + x, y = queue.popleft() + for dx, dy in directions: + nx, ny = x + dx, y + dy + if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == FRESH: + grid[nx][ny] = ROTTEN + fresh -= 1 + queue.append((nx, ny)) + + if queue: + minutes += 1 + + return minutes if fresh == 0 else -1 diff --git a/leetcode/rotting_oranges/tests.py b/leetcode/rotting_oranges/tests.py new file mode 100644 index 0000000..cbf8011 --- /dev/null +++ b/leetcode/rotting_oranges/tests.py @@ -0,0 +1,60 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestTestRottingOranges: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "grid, expected", + [ + # Original examples + ([[2, 1, 1], [1, 1, 0], [0, 1, 1]], 4), + ([[2, 1, 1], [0, 1, 1], [1, 0, 1]], -1), + ([[0, 2]], 0), + # Single cell cases + ([[0]], 0), + ([[1]], -1), + ([[2]], 0), + # Two cell cases + ([[1, 2]], 1), + ([[2, 1]], 1), + ([[0, 1, 2]], 1), + # Multiple rotten sources + ([[2, 2], [1, 1], [0, 0]], 1), + ([[2, 1, 1], [1, 1, 1], [0, 1, 2]], 2), + # All empty grid + ([[0, 0, 0], [0, 0, 0]], 0), + # All rotten grid + ([[2, 2, 2], [2, 2, 2]], 0), + # All fresh grid (impossible) + ([[1, 1, 1], [1, 1, 1]], -1), + # Large grid with barriers + ([[2, 1, 0, 0, 1], [1, 0, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 0, 2]], 3), + # Cross pattern + ([[0, 1, 0], [1, 2, 1], [0, 1, 0]], 1), + # Diagonal (no spread) + ([[2, 0, 1], [0, 0, 0], [1, 0, 1]], -1), + # Ring pattern + ([[1, 1, 1], [1, 0, 1], [1, 1, 1]], -1), + # Multiple disconnected fresh groups + ([[2, 1, 0, 1], [0, 0, 0, 0], [1, 0, 0, 2]], -1), + # Linear spread + ([[2, 1, 1, 1, 1]], 4), + # Vertical spread + ([[2], [1], [1], [1]], 3), + # Corner cases + ([[2, 0, 0], [0, 0, 0], [0, 0, 1]], -1), + ([[1, 0, 0], [0, 0, 0], [0, 0, 2]], -1), + # Complex maze-like + ([[2, 1, 1, 0, 1], [1, 0, 1, 0, 1], [1, 1, 1, 0, 2]], 4), + ], + ) + @logged_test + def test_oranges_rotting(self, grid: list[list[int]], expected: int): + result = self.solution.oranges_rotting(grid) + assert result == expected From 61ef1f666ab085446c5a985ad2eff804b49f7e7b Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sun, 7 Sep 2025 13:26:54 +0700 Subject: [PATCH 25/26] feat: add Permutations --- .templates/leetcode/json/combination_sum.json | 48 ++++++++++++++++ .templates/leetcode/json/permutations.json | 46 +++++++++++++++ Makefile | 2 +- leetcode/combination_sum/README.md | 47 +++++++++++++++ leetcode/combination_sum/__init__.py | 0 leetcode/combination_sum/playground.ipynb | 57 +++++++++++++++++++ leetcode/combination_sum/solution.py | 19 +++++++ leetcode/combination_sum/tests.py | 43 ++++++++++++++ leetcode/permutations/README.md | 40 +++++++++++++ leetcode/permutations/__init__.py | 0 leetcode/permutations/playground.ipynb | 57 +++++++++++++++++++ leetcode/permutations/solution.py | 18 ++++++ leetcode/permutations/tests.py | 43 ++++++++++++++ 13 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/combination_sum.json create mode 100644 .templates/leetcode/json/permutations.json create mode 100644 leetcode/combination_sum/README.md create mode 100644 leetcode/combination_sum/__init__.py create mode 100644 leetcode/combination_sum/playground.ipynb create mode 100644 leetcode/combination_sum/solution.py create mode 100644 leetcode/combination_sum/tests.py create mode 100644 leetcode/permutations/README.md create mode 100644 leetcode/permutations/__init__.py create mode 100644 leetcode/permutations/playground.ipynb create mode 100644 leetcode/permutations/solution.py create mode 100644 leetcode/permutations/tests.py diff --git a/.templates/leetcode/json/combination_sum.json b/.templates/leetcode/json/combination_sum.json new file mode 100644 index 0000000..de4cbfb --- /dev/null +++ b/.templates/leetcode/json/combination_sum.json @@ -0,0 +1,48 @@ +{ + "problem_name": "combination_sum", + "solution_class_name": "Solution", + "problem_number": "39", + "problem_title": "Combination Sum", + "difficulty": "Medium", + "topics": "Array, Backtracking", + "tags": ["grind-75"], + "readme_description": "Given an array of **distinct** integers `candidates` and a target integer `target`, return *a list of all **unique combinations** of* `candidates` *where the chosen numbers sum to* `target`. You may return the combinations in **any order**.\n\nThe **same** number may be chosen from `candidates` an **unlimited number of times**. Two combinations are unique if the frequency of at least one of the chosen numbers is different.\n\nThe test cases are generated such that the number of unique combinations that sum up to `target` is less than `150` combinations for the given input.", + "readme_examples": [ + { + "content": "```\nInput: candidates = [2,3,6,7], target = 7\nOutput: [[2,2,3],[7]]\n```\n**Explanation:** 2 and 3 are candidates, and 2 + 2 + 3 = 7. Note that 2 can be used multiple times. 7 is a candidate, and 7 = 7. These are the only two combinations." + }, + { + "content": "```\nInput: candidates = [2,3,5], target = 8\nOutput: [[2,2,2,2],[2,3,3],[3,5]]\n```" + }, + { "content": "```\nInput: candidates = [2], target = 1\nOutput: []\n```" } + ], + "readme_constraints": "- 1 <= candidates.length <= 30\n- 2 <= candidates[i] <= 40\n- All elements of candidates are distinct.\n- 1 <= target <= 40", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "combination_sum", + "parameters": "candidates: list[int], target: int", + "return_type": "list[list[int]]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "CombinationSum", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_combination_sum", + "parametrize": "candidates, target, expected", + "parametrize_typed": "candidates: list[int], target: int, expected: list[list[int]]", + "test_cases": "[([2, 3, 6, 7], 7, [[2, 2, 3], [7]]), ([2, 3, 5], 8, [[2, 2, 2, 2], [2, 3, 3], [3, 5]]), ([2], 1, [])]", + "body": "result = self.solution.combination_sum(candidates, target)\n# Sort both result and expected for comparison\nresult_sorted = [sorted(combo) for combo in result]\nexpected_sorted = [sorted(combo) for combo in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ncandidates = [2, 3, 6, 7]\ntarget = 7\nexpected = [[2, 2, 3], [7]]", + "playground_execution": "result = Solution().combination_sum(candidates, target)\nresult", + "playground_assertion": "# Sort for comparison\nresult_sorted = [sorted(combo) for combo in result]\nexpected_sorted = [sorted(combo) for combo in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted" +} diff --git a/.templates/leetcode/json/permutations.json b/.templates/leetcode/json/permutations.json new file mode 100644 index 0000000..cc4f436 --- /dev/null +++ b/.templates/leetcode/json/permutations.json @@ -0,0 +1,46 @@ +{ + "problem_name": "permutations", + "solution_class_name": "Solution", + "problem_number": "46", + "problem_title": "Permutations", + "difficulty": "Medium", + "topics": "Array, Backtracking", + "tags": ["grind-75"], + "readme_description": "Given an array `nums` of distinct integers, return all the possible permutations. You can return the answer in any order.", + "readme_examples": [ + { + "content": "```\nInput: nums = [1,2,3]\nOutput: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]\n```" + }, + { "content": "```\nInput: nums = [0,1]\nOutput: [[0,1],[1,0]]\n```" }, + { "content": "```\nInput: nums = [1]\nOutput: [[1]]\n```" } + ], + "readme_constraints": "- 1 <= nums.length <= 6\n- -10 <= nums[i] <= 10\n- All the integers of nums are unique.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "permute", + "parameters": "nums: list[int]", + "return_type": "list[list[int]]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "TestPermutations", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_permute", + "parametrize": "nums, expected", + "parametrize_typed": "nums: list[int], expected: list[list[int]]", + "test_cases": "[([1, 2, 3], [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]), ([0, 1], [[0, 1], [1, 0]]), ([1], [[1]])]", + "body": "result = self.solution.permute(nums)\n # Sort both result and expected for comparison since order doesn't matter\n result_sorted = [sorted(perm) for perm in result]\n expected_sorted = [sorted(perm) for perm in expected]\n result_sorted.sort()\n expected_sorted.sort()\n assert len(result) == len(expected)\n assert result_sorted == expected_sorted" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [1, 2, 3]\nexpected = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]", + "playground_execution": "result = Solution().permute(nums)\nresult", + "playground_assertion": "# Check that we have the right number of permutations\nassert len(result) == len(expected)\n# Sort for comparison since order doesn't matter\nresult_sorted = [sorted(perm) for perm in result]\nexpected_sorted = [sorted(perm) for perm in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted" +} diff --git a/Makefile b/Makefile index 0580c5a..0e64917 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= rotting_oranges +PROBLEM ?= permutations FORCE ?= 0 COMMA := , diff --git a/leetcode/combination_sum/README.md b/leetcode/combination_sum/README.md new file mode 100644 index 0000000..6d59d3e --- /dev/null +++ b/leetcode/combination_sum/README.md @@ -0,0 +1,47 @@ +# Combination Sum + +**Difficulty:** Medium +**Topics:** Array, Backtracking +**Tags:** grind-75 + +**LeetCode:** [Problem 39](https://leetcode.com/problems/combination-sum/description/) + +## Problem Description + +Given an array of **distinct** integers `candidates` and a target integer `target`, return _a list of all **unique combinations** of_ `candidates` _where the chosen numbers sum to_ `target`. You may return the combinations in **any order**. + +The **same** number may be chosen from `candidates` an **unlimited number of times**. Two combinations are unique if the frequency of at least one of the chosen numbers is different. + +The test cases are generated such that the number of unique combinations that sum up to `target` is less than `150` combinations for the given input. + +## Examples + +### Example 1: + +``` +Input: candidates = [2,3,6,7], target = 7 +Output: [[2,2,3],[7]] +``` + +**Explanation:** 2 and 3 are candidates, and 2 + 2 + 3 = 7. Note that 2 can be used multiple times. 7 is a candidate, and 7 = 7. These are the only two combinations. + +### Example 2: + +``` +Input: candidates = [2,3,5], target = 8 +Output: [[2,2,2,2],[2,3,3],[3,5]] +``` + +### Example 3: + +``` +Input: candidates = [2], target = 1 +Output: [] +``` + +## Constraints + +- 1 <= candidates.length <= 30 +- 2 <= candidates[i] <= 40 +- All elements of candidates are distinct. +- 1 <= target <= 40 diff --git a/leetcode/combination_sum/__init__.py b/leetcode/combination_sum/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/combination_sum/playground.ipynb b/leetcode/combination_sum/playground.ipynb new file mode 100644 index 0000000..2bd1081 --- /dev/null +++ b/leetcode/combination_sum/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\ncandidates = [2, 3, 6, 7]\ntarget = 7\nexpected = [[2, 2, 3], [7]]"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().combination_sum(candidates, target)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": ["# Sort for comparison\nresult_sorted = [sorted(combo) for combo in result]\nexpected_sorted = [sorted(combo) for combo in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted"] + } + ], + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/combination_sum/solution.py b/leetcode/combination_sum/solution.py new file mode 100644 index 0000000..e0508a3 --- /dev/null +++ b/leetcode/combination_sum/solution.py @@ -0,0 +1,19 @@ +class Solution: + # Time: O(N^(T/M)) where N=len(candidates), T=target, M=min(candidates) + # Space: O(T/M) recursion + O(K * T/M) output, where K = number of solutions + def combination_sum(self, candidates: list[int], target: int) -> list[list[int]]: + result = [] + + def backtrack(start: int, path: list[int], remaining: int) -> None: + if remaining == 0: + result.append(path[:]) + return + + for i in range(start, len(candidates)): + if candidates[i] <= remaining: + path.append(candidates[i]) + backtrack(i, path, remaining - candidates[i]) + path.pop() + + backtrack(0, [], target) + return result diff --git a/leetcode/combination_sum/tests.py b/leetcode/combination_sum/tests.py new file mode 100644 index 0000000..6456f2a --- /dev/null +++ b/leetcode/combination_sum/tests.py @@ -0,0 +1,43 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestCombinationSum: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "candidates, target, expected", + [ + ([2, 3, 6, 7], 7, [[2, 2, 3], [7]]), + ([2, 3, 5], 8, [[2, 2, 2, 2], [2, 3, 3], [3, 5]]), + ([2], 1, []), + ([1], 1, [[1]]), + ([1], 2, [[1, 1]]), + ([2, 3, 4], 6, [[2, 2, 2], [2, 4], [3, 3]]), + ( + [7, 3, 2], + 18, + [ + [2, 2, 2, 2, 2, 2, 2, 2, 2], + [2, 2, 2, 2, 2, 2, 3, 3], + [2, 2, 2, 3, 3, 3, 3], + [2, 2, 7, 7], + [2, 3, 3, 3, 7], + [3, 3, 3, 3, 3, 3], + ], + ), + ], + ) + @logged_test + def test_combination_sum(self, candidates: list[int], target: int, expected: list[list[int]]): + result = self.solution.combination_sum(candidates, target) + # Sort both result and expected for comparison + result_sorted = [sorted(combo) for combo in result] + expected_sorted = [sorted(combo) for combo in expected] + result_sorted.sort() + expected_sorted.sort() + assert result_sorted == expected_sorted diff --git a/leetcode/permutations/README.md b/leetcode/permutations/README.md new file mode 100644 index 0000000..69459b5 --- /dev/null +++ b/leetcode/permutations/README.md @@ -0,0 +1,40 @@ +# Permutations + +**Difficulty:** Medium +**Topics:** Array, Backtracking +**Tags:** grind-75 + +**LeetCode:** [Problem 46](https://leetcode.com/problems/permutations/description/) + +## Problem Description + +Given an array `nums` of distinct integers, return all the possible permutations. You can return the answer in any order. + +## Examples + +### Example 1: + +``` +Input: nums = [1,2,3] +Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] +``` + +### Example 2: + +``` +Input: nums = [0,1] +Output: [[0,1],[1,0]] +``` + +### Example 3: + +``` +Input: nums = [1] +Output: [[1]] +``` + +## Constraints + +- 1 <= nums.length <= 6 +- -10 <= nums[i] <= 10 +- All the integers of nums are unique. diff --git a/leetcode/permutations/__init__.py b/leetcode/permutations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/permutations/playground.ipynb b/leetcode/permutations/playground.ipynb new file mode 100644 index 0000000..a90984e --- /dev/null +++ b/leetcode/permutations/playground.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": ["from solution import Solution"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": ["# Example test case\nnums = [1, 2, 3]\nexpected = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().permute(nums)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": ["# Check that we have the right number of permutations\nassert len(result) == len(expected)\n# Sort for comparison since order doesn't matter\nresult_sorted = [sorted(perm) for perm in result]\nexpected_sorted = [sorted(perm) for perm in expected]\nresult_sorted.sort()\nexpected_sorted.sort()\nassert result_sorted == expected_sorted"] + } + ], + "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": "python3", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/leetcode/permutations/solution.py b/leetcode/permutations/solution.py new file mode 100644 index 0000000..1bdf59f --- /dev/null +++ b/leetcode/permutations/solution.py @@ -0,0 +1,18 @@ +class Solution: + # Time: O(n! * n) + # Space: O(n! * n) output + O(n) recursion + def permute(self, nums: list[int]) -> list[list[int]]: + result = [] + + def backtrack(start: int) -> None: + if start == len(nums): + result.append(nums[:]) + return + + for i in range(start, len(nums)): + nums[start], nums[i] = nums[i], nums[start] + backtrack(start + 1) + nums[start], nums[i] = nums[i], nums[start] + + backtrack(0) + return result diff --git a/leetcode/permutations/tests.py b/leetcode/permutations/tests.py new file mode 100644 index 0000000..d455d72 --- /dev/null +++ b/leetcode/permutations/tests.py @@ -0,0 +1,43 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestPermutations: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, expected", + [ + ([], [[]]), + ([1, 2, 3], [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]), + ([0, 1], [[0, 1], [1, 0]]), + ([1], [[1]]), + ([-1, 0, 1], [[-1, 0, 1], [-1, 1, 0], [0, -1, 1], [0, 1, -1], [1, -1, 0], [1, 0, -1]]), + ([4, 5], [[4, 5], [5, 4]]), + ([1, 2, 3, 4], 24), # Test count only for larger input + ([10], [[10]]), + ([-5, -3], [[-5, -3], [-3, -5]]), + ], + ) + @logged_test + def test_permute(self, nums: list[int], expected): + result = self.solution.permute(nums) + + # If expected is int, just check count (for larger inputs) + if isinstance(expected, int): + assert len(result) == expected + # Verify all permutations are unique + assert len(set(tuple(perm) for perm in result)) == expected + return + + # Sort both result and expected for comparison since order doesn't matter + result_sorted = [sorted(perm) for perm in result] + expected_sorted = [sorted(perm) for perm in expected] + result_sorted.sort() + expected_sorted.sort() + assert len(result) == len(expected) + assert result_sorted == expected_sorted From 96908f2e0c4af6627e7b4aad571b112c5aba297d Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Sun, 7 Sep 2025 16:39:59 +0700 Subject: [PATCH 26/26] feat: add Word Break --- .templates/leetcode/json/sort_colors.json | 43 ++++ .../json/time_based_key_value_store.json | 49 ++++ .templates/leetcode/json/word_break.json | 50 ++++ Makefile | 2 +- leetcode/combination_sum/tests.py | 1 + leetcode/sort_colors/README.md | 39 +++ leetcode/sort_colors/__init__.py | 0 leetcode/sort_colors/playground.ipynb | 80 ++++++ leetcode/sort_colors/solution.py | 19 ++ leetcode/sort_colors/tests.py | 27 ++ leetcode/time_based_key_value_store/README.md | 49 ++++ .../time_based_key_value_store/__init__.py | 0 .../playground.ipynb | 89 +++++++ .../time_based_key_value_store/solution.py | 34 +++ leetcode/time_based_key_value_store/tests.py | 40 +++ leetcode/word_break/README.md | 49 ++++ leetcode/word_break/__init__.py | 0 leetcode/word_break/playground.ipynb | 80 ++++++ leetcode/word_break/solution.py | 15 ++ leetcode/word_break/tests.py | 27 ++ .../data_structures/test_doubly_list_node.py | 237 ++++++++++++++++++ 21 files changed, 929 insertions(+), 1 deletion(-) create mode 100644 .templates/leetcode/json/sort_colors.json create mode 100644 .templates/leetcode/json/time_based_key_value_store.json create mode 100644 .templates/leetcode/json/word_break.json create mode 100644 leetcode/sort_colors/README.md create mode 100644 leetcode/sort_colors/__init__.py create mode 100644 leetcode/sort_colors/playground.ipynb create mode 100644 leetcode/sort_colors/solution.py create mode 100644 leetcode/sort_colors/tests.py create mode 100644 leetcode/time_based_key_value_store/README.md create mode 100644 leetcode/time_based_key_value_store/__init__.py create mode 100644 leetcode/time_based_key_value_store/playground.ipynb create mode 100644 leetcode/time_based_key_value_store/solution.py create mode 100644 leetcode/time_based_key_value_store/tests.py create mode 100644 leetcode/word_break/README.md create mode 100644 leetcode/word_break/__init__.py create mode 100644 leetcode/word_break/playground.ipynb create mode 100644 leetcode/word_break/solution.py create mode 100644 leetcode/word_break/tests.py create mode 100644 tests/data_structures/test_doubly_list_node.py diff --git a/.templates/leetcode/json/sort_colors.json b/.templates/leetcode/json/sort_colors.json new file mode 100644 index 0000000..0cca3e8 --- /dev/null +++ b/.templates/leetcode/json/sort_colors.json @@ -0,0 +1,43 @@ +{ + "problem_name": "sort_colors", + "solution_class_name": "Solution", + "problem_number": "75", + "problem_title": "Sort Colors", + "difficulty": "Medium", + "topics": "Array, Two Pointers, Sorting", + "tags": ["grind-75"], + "readme_description": "Given an array `nums` with `n` objects colored red, white, or blue, sort them **in-place** so that objects of the same color are adjacent, with the colors in the order red, white, and blue.\n\nWe will use the integers `0`, `1`, and `2` to represent the color red, white, and blue, respectively.\n\nYou must solve this problem without using the library's sort function.", + "readme_examples": [ + { "content": "```\nInput: nums = [2,0,2,1,1,0]\nOutput: [0,0,1,1,2,2]\n```" }, + { "content": "```\nInput: nums = [2,0,1]\nOutput: [0,1,2]\n```" } + ], + "readme_constraints": "- `n == nums.length`\n- `1 <= n <= 300`\n- `nums[i]` is either `0`, `1`, or `2`.", + "readme_additional": "**Follow up:** Could you come up with a one-pass algorithm using only constant extra space?", + "solution_imports": "", + "solution_methods": [ + { + "name": "sort_colors", + "parameters": "nums: list[int]", + "return_type": "None", + "dummy_return": "" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "SortColors", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_sort_colors", + "parametrize": "nums, expected", + "parametrize_typed": "nums: list[int], expected: list[int]", + "test_cases": "[([2, 0, 2, 1, 1, 0], [0, 0, 1, 1, 2, 2]), ([2, 0, 1], [0, 1, 2]), ([0], [0]), ([1], [1]), ([2], [2]), ([0, 1, 2], [0, 1, 2])]", + "body": "nums_copy = nums.copy()\nself.solution.sort_colors(nums_copy)\nassert nums_copy == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [2, 0, 2, 1, 1, 0]\nexpected = [0, 0, 1, 1, 2, 2]", + "playground_execution": "nums_copy = nums.copy()\nSolution().sort_colors(nums_copy)\nnums_copy", + "playground_assertion": "assert nums_copy == expected" +} diff --git a/.templates/leetcode/json/time_based_key_value_store.json b/.templates/leetcode/json/time_based_key_value_store.json new file mode 100644 index 0000000..11ace03 --- /dev/null +++ b/.templates/leetcode/json/time_based_key_value_store.json @@ -0,0 +1,49 @@ +{ + "problem_name": "time_based_key_value_store", + "solution_class_name": "TimeMap", + "problem_number": "981", + "problem_title": "Time Based Key-Value Store", + "difficulty": "Medium", + "topics": "Hash Table, String, Binary Search, Design", + "tags": ["grind-75"], + "readme_description": "Design a time-based key-value data structure that can store multiple values for the same key at different time stamps and retrieve the key's value at a certain timestamp.\n\nImplement the `TimeMap` class:\n\n- `TimeMap()` Initializes the object of the data structure.\n- `void set(String key, String value, int timestamp)` Stores the key `key` with the value `value` at the given time `timestamp`.\n- `String get(String key, int timestamp)` Returns a value such that `set` was called previously, with `timestamp_prev <= timestamp`. If there are multiple such values, it returns the value associated with the largest `timestamp_prev`. If there are no values, it returns `\"\"`.", + "readme_examples": [ + { + "content": "```\nInput\n[\"TimeMap\", \"set\", \"get\", \"get\", \"set\", \"get\", \"get\"]\n[[], [\"foo\", \"bar\", 1], [\"foo\", 1], [\"foo\", 3], [\"foo\", \"bar2\", 4], [\"foo\", 4], [\"foo\", 5]]\nOutput\n[null, null, \"bar\", \"bar\", null, \"bar2\", \"bar2\"]\n```\n\n**Explanation:**\n```\nTimeMap timeMap = new TimeMap();\ntimeMap.set(\"foo\", \"bar\", 1); // store the key \"foo\" and value \"bar\" along with timestamp = 1.\ntimeMap.get(\"foo\", 1); // return \"bar\"\ntimeMap.get(\"foo\", 3); // return \"bar\", since there is no value corresponding to foo at timestamp 3 and timestamp 2, then the only value is at timestamp 1 is \"bar\".\ntimeMap.set(\"foo\", \"bar2\", 4); // store the key \"foo\" and value \"bar2\" along with timestamp = 4.\ntimeMap.get(\"foo\", 4); // return \"bar2\"\ntimeMap.get(\"foo\", 5); // return \"bar2\"\n```" + } + ], + "readme_constraints": "- `1 <= key.length, value.length <= 100`\n- `key` and `value` consist of lowercase English letters and digits.\n- `1 <= timestamp <= 10^7`\n- All the timestamps `timestamp` of `set` are strictly increasing.\n- At most `2 * 10^5` calls will be made to `set` and `get`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { "name": "__init__", "parameters": "", "return_type": "None", "dummy_return": "" }, + { + "name": "set", + "parameters": "key: str, value: str, timestamp: int", + "return_type": "None", + "dummy_return": "" + }, + { + "name": "get", + "parameters": "key: str, timestamp: int", + "return_type": "str", + "dummy_return": "\"\"" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import TimeMap", + "test_class_name": "TimeBasedKeyValueStore", + "test_helper_methods": [], + "test_methods": [ + { + "name": "test_time_map_operations", + "parametrize": "operations, inputs, expected", + "parametrize_typed": "operations: list[str], inputs: list[list], expected: list", + "test_cases": "[(['TimeMap', 'set', 'get', 'get', 'set', 'get', 'get'], [[], ['foo', 'bar', 1], ['foo', 1], ['foo', 3], ['foo', 'bar2', 4], ['foo', 4], ['foo', 5]], [None, None, 'bar', 'bar', None, 'bar2', 'bar2'])]", + "body": "time_map: TimeMap | None = None\nresult: list[str | None] = []\nfor i, op in enumerate(operations):\n if op == 'TimeMap':\n time_map = TimeMap()\n result.append(None)\n elif op == 'set' and time_map is not None:\n time_map.set(*inputs[i])\n result.append(None)\n elif op == 'get' and time_map is not None:\n result.append(time_map.get(*inputs[i]))\nassert result == expected" + } + ], + "playground_imports": "from solution import TimeMap", + "playground_test_case": "# Example test case\ntime_map = TimeMap()\ntime_map.set('foo', 'bar', 1)\nresult1 = time_map.get('foo', 1)\nresult2 = time_map.get('foo', 3)\ntime_map.set('foo', 'bar2', 4)\nresult3 = time_map.get('foo', 4)\nresult4 = time_map.get('foo', 5)", + "playground_execution": "print(f'get(foo, 1): {result1}')\nprint(f'get(foo, 3): {result2}')\nprint(f'get(foo, 4): {result3}')\nprint(f'get(foo, 5): {result4}')", + "playground_assertion": "assert result1 == 'bar'\nassert result2 == 'bar'\nassert result3 == 'bar2'\nassert result4 == 'bar2'" +} diff --git a/.templates/leetcode/json/word_break.json b/.templates/leetcode/json/word_break.json new file mode 100644 index 0000000..83332be --- /dev/null +++ b/.templates/leetcode/json/word_break.json @@ -0,0 +1,50 @@ +{ + "problem_name": "word_break", + "solution_class_name": "Solution", + "problem_number": "139", + "problem_title": "Word Break", + "difficulty": "Medium", + "topics": "Array, Hash Table, String, Dynamic Programming, Trie, Memoization", + "tags": ["grind-75"], + "readme_description": "Given a string `s` and a dictionary of strings `wordDict`, return `true` if `s` can be segmented into a space-separated sequence of one or more dictionary words.\n\n**Note** that the same word in the dictionary may be reused multiple times in the segmentation.", + "readme_examples": [ + { + "content": "```\nInput: s = \"leetcode\", wordDict = [\"leet\",\"code\"]\nOutput: true\n```\n**Explanation:** Return true because \"leetcode\" can be segmented as \"leet code\"." + }, + { + "content": "```\nInput: s = \"applepenapple\", wordDict = [\"apple\",\"pen\"]\nOutput: true\n```\n**Explanation:** Return true because \"applepenapple\" can be segmented as \"apple pen apple\".\nNote that you are allowed to reuse a dictionary word." + }, + { + "content": "```\nInput: s = \"catsandog\", wordDict = [\"cats\",\"dog\",\"sand\",\"and\",\"cat\"]\nOutput: false\n```" + } + ], + "readme_constraints": "- `1 <= s.length <= 300`\n- `1 <= wordDict.length <= 1000`\n- `1 <= wordDict[i].length <= 20`\n- `s` and `wordDict[i]` consist of only lowercase English letters.\n- All the strings of `wordDict` are **unique**.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "word_break", + "parameters": "s: str, word_dict: list[str]", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "WordBreak", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_word_break", + "parametrize": "s, word_dict, expected", + "parametrize_typed": "s: str, word_dict: list[str], expected: bool", + "test_cases": "[('leetcode', ['leet', 'code'], True), ('applepenapple', ['apple', 'pen'], True), ('catsandog', ['cats', 'dog', 'sand', 'and', 'cat'], False), ('', [], True), ('a', ['a'], True), ('ab', ['a', 'b'], True), ('abcd', ['a', 'abc', 'd'], True)]", + "body": "result = self.solution.word_break(s, word_dict)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = 'leetcode'\nword_dict = ['leet', 'code']\nexpected = True", + "playground_execution": "result = Solution().word_break(s, word_dict)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 0e64917..ce78169 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= permutations +PROBLEM ?= word_break FORCE ?= 0 COMMA := , diff --git a/leetcode/combination_sum/tests.py b/leetcode/combination_sum/tests.py index 6456f2a..200b2da 100644 --- a/leetcode/combination_sum/tests.py +++ b/leetcode/combination_sum/tests.py @@ -24,6 +24,7 @@ def setup_method(self): [ [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 3, 3], + [2, 2, 2, 2, 3, 7], [2, 2, 2, 3, 3, 3, 3], [2, 2, 7, 7], [2, 3, 3, 3, 7], diff --git a/leetcode/sort_colors/README.md b/leetcode/sort_colors/README.md new file mode 100644 index 0000000..ada8cf0 --- /dev/null +++ b/leetcode/sort_colors/README.md @@ -0,0 +1,39 @@ +# Sort Colors + +**Difficulty:** Medium +**Topics:** Array, Two Pointers, Sorting +**Tags:** grind-75 + +**LeetCode:** [Problem 75](https://leetcode.com/problems/sort-colors/description/) + +## Problem Description + +Given an array `nums` with `n` objects colored red, white, or blue, sort them **in-place** so that objects of the same color are adjacent, with the colors in the order red, white, and blue. + +We will use the integers `0`, `1`, and `2` to represent the color red, white, and blue, respectively. + +You must solve this problem without using the library's sort function. + +## Examples + +### Example 1: + +``` +Input: nums = [2,0,2,1,1,0] +Output: [0,0,1,1,2,2] +``` + +### Example 2: + +``` +Input: nums = [2,0,1] +Output: [0,1,2] +``` + +## Constraints + +- `n == nums.length` +- `1 <= n <= 300` +- `nums[i]` is either `0`, `1`, or `2`. + +**Follow up:** Could you come up with a one-pass algorithm using only constant extra space? diff --git a/leetcode/sort_colors/__init__.py b/leetcode/sort_colors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/sort_colors/playground.ipynb b/leetcode/sort_colors/playground.ipynb new file mode 100644 index 0000000..33d81fa --- /dev/null +++ b/leetcode/sort_colors/playground.ipynb @@ -0,0 +1,80 @@ +{ + "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", + "nums = [2, 0, 2, 1, 1, 0]\n", + "expected = [0, 0, 1, 1, 2, 2]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 0, 1, 1, 2, 2]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nums_copy = nums.copy()\n", + "Solution().sort_colors(nums_copy)\n", + "nums_copy" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert nums_copy == 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/sort_colors/solution.py b/leetcode/sort_colors/solution.py new file mode 100644 index 0000000..003cf4a --- /dev/null +++ b/leetcode/sort_colors/solution.py @@ -0,0 +1,19 @@ +class Solution: + # Dutch National Flag Algorithm - partitions array into 3 regions using 2 pointers + # Creates: [0s][1s][2s] with left/right boundaries, mid processes unvisited elements + # Time: O(n) + # Space: O(1) + def sort_colors(self, nums: list[int]) -> None: + left = mid = 0 + right = len(nums) - 1 + + while mid <= right: + if nums[mid] == 0: + nums[left], nums[mid] = nums[mid], nums[left] + left += 1 + mid += 1 + elif nums[mid] == 1: + mid += 1 + else: + nums[mid], nums[right] = nums[right], nums[mid] + right -= 1 diff --git a/leetcode/sort_colors/tests.py b/leetcode/sort_colors/tests.py new file mode 100644 index 0000000..a500d9d --- /dev/null +++ b/leetcode/sort_colors/tests.py @@ -0,0 +1,27 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestSortColors: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, expected", + [ + ([2, 0, 2, 1, 1, 0], [0, 0, 1, 1, 2, 2]), + ([2, 0, 1], [0, 1, 2]), + ([0], [0]), + ([1], [1]), + ([2], [2]), + ([0, 1, 2], [0, 1, 2]), + ], + ) + @logged_test + def test_sort_colors(self, nums: list[int], expected: list[int]): + nums_copy = nums.copy() + self.solution.sort_colors(nums_copy) + assert nums_copy == expected diff --git a/leetcode/time_based_key_value_store/README.md b/leetcode/time_based_key_value_store/README.md new file mode 100644 index 0000000..fd46220 --- /dev/null +++ b/leetcode/time_based_key_value_store/README.md @@ -0,0 +1,49 @@ +# Time Based Key-Value Store + +**Difficulty:** Medium +**Topics:** Hash Table, String, Binary Search, Design +**Tags:** grind-75 + +**LeetCode:** [Problem 981](https://leetcode.com/problems/time-based-key-value-store/description/) + +## Problem Description + +Design a time-based key-value data structure that can store multiple values for the same key at different time stamps and retrieve the key's value at a certain timestamp. + +Implement the `TimeMap` class: + +- `TimeMap()` Initializes the object of the data structure. +- `void set(String key, String value, int timestamp)` Stores the key `key` with the value `value` at the given time `timestamp`. +- `String get(String key, int timestamp)` Returns a value such that `set` was called previously, with `timestamp_prev <= timestamp`. If there are multiple such values, it returns the value associated with the largest `timestamp_prev`. If there are no values, it returns `""`. + +## Examples + +### Example 1: + +``` +Input +["TimeMap", "set", "get", "get", "set", "get", "get"] +[[], ["foo", "bar", 1], ["foo", 1], ["foo", 3], ["foo", "bar2", 4], ["foo", 4], ["foo", 5]] +Output +[null, null, "bar", "bar", null, "bar2", "bar2"] +``` + +**Explanation:** + +``` +TimeMap timeMap = new TimeMap(); +timeMap.set("foo", "bar", 1); // store the key "foo" and value "bar" along with timestamp = 1. +timeMap.get("foo", 1); // return "bar" +timeMap.get("foo", 3); // return "bar", since there is no value corresponding to foo at timestamp 3 and timestamp 2, then the only value is at timestamp 1 is "bar". +timeMap.set("foo", "bar2", 4); // store the key "foo" and value "bar2" along with timestamp = 4. +timeMap.get("foo", 4); // return "bar2" +timeMap.get("foo", 5); // return "bar2" +``` + +## Constraints + +- `1 <= key.length, value.length <= 100` +- `key` and `value` consist of lowercase English letters and digits. +- `1 <= timestamp <= 10^7` +- All the timestamps `timestamp` of `set` are strictly increasing. +- At most `2 * 10^5` calls will be made to `set` and `get`. diff --git a/leetcode/time_based_key_value_store/__init__.py b/leetcode/time_based_key_value_store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/time_based_key_value_store/playground.ipynb b/leetcode/time_based_key_value_store/playground.ipynb new file mode 100644 index 0000000..d0567d4 --- /dev/null +++ b/leetcode/time_based_key_value_store/playground.ipynb @@ -0,0 +1,89 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import TimeMap" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Example test case\n", + "time_map = TimeMap()\n", + "time_map.set(\"foo\", \"bar\", 1)\n", + "result1 = time_map.get(\"foo\", 1)\n", + "result2 = time_map.get(\"foo\", 3)\n", + "time_map.set(\"foo\", \"bar2\", 4)\n", + "result3 = time_map.get(\"foo\", 4)\n", + "result4 = time_map.get(\"foo\", 5)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "get(foo, 1): bar\n", + "get(foo, 3): bar\n", + "get(foo, 4): bar2\n", + "get(foo, 5): bar2\n" + ] + } + ], + "source": [ + "print(f\"get(foo, 1): {result1}\")\n", + "print(f\"get(foo, 3): {result2}\")\n", + "print(f\"get(foo, 4): {result3}\")\n", + "print(f\"get(foo, 5): {result4}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result1 == \"bar\"\n", + "assert result2 == \"bar\"\n", + "assert result3 == \"bar2\"\n", + "assert result4 == \"bar2\"" + ] + } + ], + "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/time_based_key_value_store/solution.py b/leetcode/time_based_key_value_store/solution.py new file mode 100644 index 0000000..fcb11cd --- /dev/null +++ b/leetcode/time_based_key_value_store/solution.py @@ -0,0 +1,34 @@ +class TimeMap: + # Time: O(1) + # Space: O(n) + def __init__( + self, + ) -> None: + self.store: dict[str, list[tuple[int, str]]] = {} + + # Time: O(1) + # Space: O(1) + def set(self, key: str, value: str, timestamp: int) -> None: + if key not in self.store: + self.store[key] = [] + self.store[key].append((timestamp, value)) + + # Time: O(log n) + # Space: O(1) + def get(self, key: str, timestamp: int) -> str: + if key not in self.store: + return "" + + values = self.store[key] + left, right = 0, len(values) - 1 + result = "" + + while left <= right: + mid = (left + right) // 2 + if values[mid][0] <= timestamp: + result = values[mid][1] + left = mid + 1 + else: + right = mid - 1 + + return result diff --git a/leetcode/time_based_key_value_store/tests.py b/leetcode/time_based_key_value_store/tests.py new file mode 100644 index 0000000..f4c23a0 --- /dev/null +++ b/leetcode/time_based_key_value_store/tests.py @@ -0,0 +1,40 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import TimeMap + + +class TestTimeBasedKeyValueStore: + @pytest.mark.parametrize( + "operations, inputs, expected", + [ + ( + ["TimeMap", "set", "get", "get", "set", "get", "get"], + [ + [], + ["foo", "bar", 1], + ["foo", 1], + ["foo", 3], + ["foo", "bar2", 4], + ["foo", 4], + ["foo", 5], + ], + [None, None, "bar", "bar", None, "bar2", "bar2"], + ) + ], + ) + @logged_test + def test_time_map_operations(self, operations: list[str], inputs: list[list], expected: list): + time_map: TimeMap | None = None + result: list[str | None] = [] + for i, op in enumerate(operations): + if op == "TimeMap": + time_map = TimeMap() + result.append(None) + elif op == "set" and time_map is not None: + time_map.set(*inputs[i]) + result.append(None) + elif op == "get" and time_map is not None: + result.append(time_map.get(*inputs[i])) + assert result == expected diff --git a/leetcode/word_break/README.md b/leetcode/word_break/README.md new file mode 100644 index 0000000..7bb5206 --- /dev/null +++ b/leetcode/word_break/README.md @@ -0,0 +1,49 @@ +# Word Break + +**Difficulty:** Medium +**Topics:** Array, Hash Table, String, Dynamic Programming, Trie, Memoization +**Tags:** grind-75 + +**LeetCode:** [Problem 139](https://leetcode.com/problems/word-break/description/) + +## Problem Description + +Given a string `s` and a dictionary of strings `wordDict`, return `true` if `s` can be segmented into a space-separated sequence of one or more dictionary words. + +**Note** that the same word in the dictionary may be reused multiple times in the segmentation. + +## Examples + +### Example 1: + +``` +Input: s = "leetcode", wordDict = ["leet","code"] +Output: true +``` + +**Explanation:** Return true because "leetcode" can be segmented as "leet code". + +### Example 2: + +``` +Input: s = "applepenapple", wordDict = ["apple","pen"] +Output: true +``` + +**Explanation:** Return true because "applepenapple" can be segmented as "apple pen apple". +Note that you are allowed to reuse a dictionary word. + +### Example 3: + +``` +Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] +Output: false +``` + +## Constraints + +- `1 <= s.length <= 300` +- `1 <= wordDict.length <= 1000` +- `1 <= wordDict[i].length <= 20` +- `s` and `wordDict[i]` consist of only lowercase English letters. +- All the strings of `wordDict` are **unique**. diff --git a/leetcode/word_break/__init__.py b/leetcode/word_break/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/word_break/playground.ipynb b/leetcode/word_break/playground.ipynb new file mode 100644 index 0000000..3d5b07d --- /dev/null +++ b/leetcode/word_break/playground.ipynb @@ -0,0 +1,80 @@ +{ + "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", + "s = \"leetcode\"\n", + "word_dict = [\"leet\", \"code\"]\n", + "expected = True" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().word_break(s, word_dict)\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/word_break/solution.py b/leetcode/word_break/solution.py new file mode 100644 index 0000000..ed8dc30 --- /dev/null +++ b/leetcode/word_break/solution.py @@ -0,0 +1,15 @@ +class Solution: + # Time: O(n^2) + # Space: O(n) + def word_break(self, s: str, word_dict: list[str]) -> bool: + word_set = set(word_dict) + dp = [False] * (len(s) + 1) + dp[0] = True + + for i in range(1, len(s) + 1): + for j in range(i): + if dp[j] and s[j:i] in word_set: + dp[i] = True + break + + return dp[-1] diff --git a/leetcode/word_break/tests.py b/leetcode/word_break/tests.py new file mode 100644 index 0000000..f250fa1 --- /dev/null +++ b/leetcode/word_break/tests.py @@ -0,0 +1,27 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestWordBreak: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, word_dict, expected", + [ + ("leetcode", ["leet", "code"], True), + ("applepenapple", ["apple", "pen"], True), + ("catsandog", ["cats", "dog", "sand", "and", "cat"], False), + ("", [], True), + ("a", ["a"], True), + ("ab", ["a", "b"], True), + ("abcd", ["a", "abc", "d"], True), + ], + ) + @logged_test + def test_word_break(self, s: str, word_dict: list[str], expected: bool): + result = self.solution.word_break(s, word_dict) + assert result == expected diff --git a/tests/data_structures/test_doubly_list_node.py b/tests/data_structures/test_doubly_list_node.py new file mode 100644 index 0000000..e76a9df --- /dev/null +++ b/tests/data_structures/test_doubly_list_node.py @@ -0,0 +1,237 @@ +from typing import Any + +import pytest + +from leetcode_py.data_structures.doubly_list_node import DoublyListNode + + +class TestDoublyListNode: + @pytest.mark.parametrize( + "val, expected_val, expected_prev, expected_next", + [ + (5, 5, None, None), + ("hello", "hello", None, None), + ], + ) + def test_init(self, val: Any, expected_val: Any, expected_prev: Any, expected_next: Any) -> None: + node = DoublyListNode(val) + assert node.val == expected_val + assert node.prev == expected_prev + assert node.next == expected_next + + def test_init_with_prev_next(self) -> None: + prev_node = DoublyListNode[int](1) + next_node = DoublyListNode[int](3) + node = DoublyListNode[int](2, prev_node, next_node) + assert node.val == 2 + assert node.prev == prev_node + assert node.next == next_node + + @pytest.mark.parametrize( + "input_list, expected_result", + [ + ([], None), + ([1], "single_node"), + ([1, 2, 3], "multiple_nodes"), + (["a", "b"], "string_nodes"), + ], + ) + def test_from_list(self, input_list: list[Any], expected_result: str | None) -> None: + result = DoublyListNode.from_list(input_list) + + if expected_result is None: + assert result is None + elif expected_result == "single_node": + assert result is not None + assert result.val == 1 + assert result.prev is None + assert result.next is None + elif expected_result == "multiple_nodes": + assert result is not None + assert result.val == 1 + assert result.prev is None + assert result.next is not None + assert result.next.val == 2 + assert result.next.prev == result + assert result.next.next is not None + assert result.next.next.val == 3 + assert result.next.next.prev == result.next + assert result.next.next.next is None + elif expected_result == "string_nodes": + assert result is not None + assert result.val == "a" + assert result.prev is None + assert result.next is not None + assert result.next.val == "b" + assert result.next.prev == result + assert result.next.next is None + + @pytest.mark.parametrize( + "input_list, expected_output", + [ + ([1], [1]), + ([1, 2, 3], [1, 2, 3]), + (["x", "y"], ["x", "y"]), + ], + ) + def test_to_list(self, input_list: list[Any], expected_output: list[Any]) -> None: + node = DoublyListNode.from_list(input_list) + assert node is not None + assert node.to_list() == expected_output + + @pytest.mark.parametrize( + "input_list, expected_str, expected_repr", + [ + ([1, 2, 3], "1 <-> 2 <-> 3", "DoublyListNode([1, 2, 3])"), + (["a", "b"], "a <-> b", "DoublyListNode(['a', 'b'])"), + ], + ) + def test_string_representations( + self, input_list: list[Any], expected_str: str, expected_repr: str + ) -> None: + node = DoublyListNode.from_list(input_list) + assert node is not None + assert str(node) == expected_str + assert repr(node) == expected_repr + + @pytest.mark.parametrize( + "list1, list2, should_equal", + [ + ([1, 2, 3], [1, 2, 3], True), + ([1, 2, 3], [1, 2, 4], False), + ], + ) + def test_equality(self, list1: list[int], list2: list[int], should_equal: bool) -> None: + node1 = DoublyListNode.from_list(list1) + node2 = DoublyListNode.from_list(list2) + assert (node1 == node2) == should_equal + + @pytest.mark.parametrize("other_value", [[1], "1"]) + def test_equality_different_types(self, other_value: Any) -> None: + node = DoublyListNode[int](1) + assert node != other_value + + @pytest.mark.parametrize( + "test_list", + [ + [1, 2, 3, 4, 5], + [1], + [10, 20, 30], + ["hello", "world"], + [True, False, True], + ], + ) + def test_roundtrip_conversion(self, test_list: list[Any]) -> None: + node = DoublyListNode.from_list(test_list) + assert node is not None + result = node.to_list() + assert result == test_list + + def test_has_cycle_no_cycle(self) -> None: + node = DoublyListNode.from_list([1, 2, 3]) + assert node is not None + assert not node._has_cycle() + + def test_has_cycle_with_cycle(self) -> None: + node1 = DoublyListNode(1) + node2 = DoublyListNode(2) + node3 = DoublyListNode(3) + node1.next = node2 + node2.prev = node1 + node2.next = node3 + node3.prev = node2 + node3.next = node2 # Create cycle + + assert node1._has_cycle() + + def test_str_with_cycle(self) -> None: + node1 = DoublyListNode(1) + node2 = DoublyListNode(2) + node1.next = node2 + node2.prev = node1 + node2.next = node1 # Create cycle + + result = str(node1) + assert "<-> ... (cycle back to 1)" in result + + def test_equality_with_cycles(self) -> None: + node1 = DoublyListNode(1) + node2 = DoublyListNode(2) + node1.next = node2 + node2.prev = node1 + node2.next = node1 # Create cycle + + node3 = DoublyListNode(1) + node4 = DoublyListNode(2) + node3.next = node4 + node4.prev = node3 + node4.next = node3 # Create cycle + + assert node1 != node3 + + linear_node = DoublyListNode.from_list([1, 2]) + assert node1 != linear_node + + def test_to_list_with_cycle(self) -> None: + node1 = DoublyListNode(1) + node2 = DoublyListNode(2) + node1.next = node2 + node2.prev = node1 + node2.next = node1 # Create cycle + + result = node1.to_list() + assert result == [1, 2] + + def test_str_long_list(self) -> None: + long_list = list(range(1001)) + node = DoublyListNode.from_list(long_list) + assert node is not None + result = str(node) + assert "<-> ... (long list)" in result + + def test_repr_html_no_graphviz(self, monkeypatch) -> None: + node = DoublyListNode.from_list([1, 2, 3]) + assert node is not None + + def mock_import(name, *args): + if name == "graphviz": + raise ImportError("No module named 'graphviz'") + return __import__(name, *args) + + monkeypatch.setattr("builtins.__import__", mock_import) + result = node._repr_html_() + assert "
" in result
+        assert "1 <-> 2 <-> 3" in result
+
+    def test_repr_html_with_graphviz(self) -> None:
+        node = DoublyListNode.from_list([1, 2])
+        assert node is not None
+
+        try:
+            import importlib.util
+
+            if importlib.util.find_spec("graphviz") is None:
+                pytest.skip("graphviz not available")
+
+            result = node._repr_html_()
+            assert isinstance(result, str)
+        except ImportError:
+            pytest.skip("graphviz not available")
+
+    def test_repr_html_with_cycle(self) -> None:
+        node1 = DoublyListNode(1)
+        node2 = DoublyListNode(2)
+        node1.next = node2
+        node2.prev = node1
+        node2.next = node1  # Create cycle
+
+        try:
+            import importlib.util
+
+            if importlib.util.find_spec("graphviz") is None:
+                pytest.skip("graphviz not available")
+
+            result = node1._repr_html_()
+            assert isinstance(result, str)
+        except ImportError:
+            pytest.skip("graphviz not available")