From f16489895863dcdc856ffb3956bc6405d92994c3 Mon Sep 17 00:00:00 2001 From: Wisaroot <66859294+wisarootl@users.noreply.github.com> Date: Mon, 1 Sep 2025 07:48:13 +0700 Subject: [PATCH 01/68] feat: update generic data structures (#17) --- .../leetcode/examples/linked_list.json5 | 8 +-- .templates/leetcode/examples/tree.json5 | 8 +-- .../leetcode/json/invert_binary_tree.json | 8 +-- .../leetcode/json/reverse_linked_list_ii.json | 8 +-- leetcode/invert_binary_tree/playground.ipynb | 2 +- leetcode/invert_binary_tree/solution.py | 10 +-- leetcode/invert_binary_tree/tests.py | 4 +- leetcode/lru_cache/solution.py | 2 +- .../reverse_linked_list_ii/playground.ipynb | 2 +- leetcode/reverse_linked_list_ii/solution.py | 4 +- leetcode/reverse_linked_list_ii/tests.py | 4 +- leetcode_py/data_structures/list_node.py | 16 +++-- leetcode_py/data_structures/tree_node.py | 21 +++--- tests/data_structures/test_list_node.py | 51 +++++++++----- tests/data_structures/test_tree_node.py | 70 +++++++++++-------- 15 files changed, 126 insertions(+), 92 deletions(-) diff --git a/.templates/leetcode/examples/linked_list.json5 b/.templates/leetcode/examples/linked_list.json5 index 0485c53..f0a77b2 100644 --- a/.templates/leetcode/examples/linked_list.json5 +++ b/.templates/leetcode/examples/linked_list.json5 @@ -41,8 +41,8 @@ solution_methods: [ { name: "reverse_between", // snake_case method name - parameters: "head: ListNode | None, left: int, right: int", // Use ListNode | None for nullable parameters - return_type: "ListNode | None", // Modern union syntax + parameters: "head: ListNode[int] | None, left: int, right: int", // Use ListNode[int] | None for nullable parameters + return_type: "ListNode[int] | None", // Modern union syntax with explicit generic type dummy_return: "None", // None for linked list problems }, ], @@ -65,14 +65,14 @@ parametrize_typed: "head_list: list[int], left: int, right: int, expected_list: list[int]", test_cases: "[([1, 2, 3, 4, 5], 2, 4, [1, 4, 3, 2, 5]), ([5], 1, 1, [5])]", // IMPORTANT: Linked list test body converts arrays to ListNode and compares objects directly - body: "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)\nresult = self.solution.reverse_between(head, left, right)\nassert result == expected", + body: "head = ListNode[int].from_list(head_list)\nexpected = ListNode[int].from_list(expected_list)\nresult = self.solution.reverse_between(head, left, right)\nassert result == expected", }, ], // === PLAYGROUND NOTEBOOK === // IMPORTANT: Linked list playground needs ListNode import and conversion playground_imports: "from solution import Solution\n\nfrom leetcode_py import ListNode", - playground_test_case: "# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode.from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode.from_list([1, 4, 3, 2, 5])", + playground_test_case: "# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode[int].from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode[int].from_list([1, 4, 3, 2, 5])", playground_execution: "result = Solution().reverse_between(head, left, right)\nresult", playground_assertion: "assert result == expected", } diff --git a/.templates/leetcode/examples/tree.json5 b/.templates/leetcode/examples/tree.json5 index ecbcd92..fd5cf13 100644 --- a/.templates/leetcode/examples/tree.json5 +++ b/.templates/leetcode/examples/tree.json5 @@ -45,8 +45,8 @@ solution_methods: [ { name: "invert_tree", // snake_case method name - parameters: "root: TreeNode | None", // Use TreeNode | None for nullable tree parameters - return_type: "TreeNode | None", // Modern union syntax (not Optional[TreeNode]) + parameters: "root: TreeNode[int] | None", // Use TreeNode[int] | None for nullable tree parameters + return_type: "TreeNode[int] | None", // Modern union syntax with explicit generic type dummy_return: "None", // None for tree problems }, ], @@ -69,14 +69,14 @@ parametrize_typed: "root_list: list[int | None], expected_list: list[int | None]", test_cases: "[([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), ([2, 1, 3], [2, 3, 1]), ([], [])]", // IMPORTANT: Tree test body converts arrays to TreeNode and compares objects directly - body: "root = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list(expected_list)\nresult = self.solution.invert_tree(root)\nassert result == expected", + body: "root = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list(expected_list)\nresult = self.solution.invert_tree(root)\nassert result == expected", }, ], // === PLAYGROUND NOTEBOOK === // IMPORTANT: Tree playground needs TreeNode import and conversion playground_imports: "from solution import Solution\n\nfrom leetcode_py import TreeNode", - playground_test_case: "# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])", + playground_test_case: "# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list([4, 7, 2, 9, 6, 3, 1])", playground_execution: "result = Solution().invert_tree(root)\nresult", playground_assertion: "assert result == expected", } diff --git a/.templates/leetcode/json/invert_binary_tree.json b/.templates/leetcode/json/invert_binary_tree.json index 3abdcd5..8e1f324 100644 --- a/.templates/leetcode/json/invert_binary_tree.json +++ b/.templates/leetcode/json/invert_binary_tree.json @@ -18,8 +18,8 @@ "solution_methods": [ { "name": "invert_tree", - "parameters": "root: TreeNode | None", - "return_type": "TreeNode | None", + "parameters": "root: TreeNode[int] | None", + "return_type": "TreeNode[int] | None", "dummy_return": "None" } ], @@ -34,11 +34,11 @@ "parametrize": "root_list, expected_list", "parametrize_typed": "root_list: list[int | None], expected_list: list[int | None]", "test_cases": "[([4, 2, 7, 1, 3, 6, 9], [4, 7, 2, 9, 6, 3, 1]), ([2, 1, 3], [2, 3, 1]), ([], [])]", - "body": "root = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list(expected_list)\nresult = self.solution.invert_tree(root)\nassert result == expected" + "body": "root = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list(expected_list)\nresult = self.solution.invert_tree(root)\nassert result == expected" } ], "playground_imports": "from solution import Solution\n\nfrom leetcode_py import TreeNode", - "playground_test_case": "# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])", + "playground_test_case": "# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list([4, 7, 2, 9, 6, 3, 1])", "playground_execution": "result = Solution().invert_tree(root)\nresult", "playground_assertion": "assert result == expected" } diff --git a/.templates/leetcode/json/reverse_linked_list_ii.json b/.templates/leetcode/json/reverse_linked_list_ii.json index 8c66969..7c904e2 100644 --- a/.templates/leetcode/json/reverse_linked_list_ii.json +++ b/.templates/leetcode/json/reverse_linked_list_ii.json @@ -17,8 +17,8 @@ "solution_methods": [ { "name": "reverse_between", - "parameters": "head: ListNode | None, left: int, right: int", - "return_type": "ListNode | None", + "parameters": "head: ListNode[int] | None, left: int, right: int", + "return_type": "ListNode[int] | None", "dummy_return": "None" } ], @@ -33,11 +33,11 @@ "parametrize": "head_list, left, right, expected_list", "parametrize_typed": "head_list: list[int], left: int, right: int, expected_list: list[int]", "test_cases": "[([1, 2, 3, 4, 5], 2, 4, [1, 4, 3, 2, 5]), ([5], 1, 1, [5])]", - "body": "head = ListNode.from_list(head_list)\nexpected = ListNode.from_list(expected_list)\nresult = self.solution.reverse_between(head, left, right)\nassert result == expected" + "body": "head = ListNode[int].from_list(head_list)\nexpected = ListNode[int].from_list(expected_list)\nresult = self.solution.reverse_between(head, left, right)\nassert result == expected" } ], "playground_imports": "from solution import Solution\n\nfrom leetcode_py import ListNode", - "playground_test_case": "# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode.from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode.from_list([1, 4, 3, 2, 5])", + "playground_test_case": "# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode[int].from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode[int].from_list([1, 4, 3, 2, 5])", "playground_execution": "result = Solution().reverse_between(head, left, right)\nresult", "playground_assertion": "assert result == expected" } diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb index b63bfcd..452ecf4 100644 --- a/leetcode/invert_binary_tree/playground.ipynb +++ b/leetcode/invert_binary_tree/playground.ipynb @@ -14,7 +14,7 @@ "id": "setup", "metadata": {}, "outputs": [], - "source": ["# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode.from_list(root_list)\nexpected = TreeNode.from_list([4, 7, 2, 9, 6, 3, 1])"] + "source": ["# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list([4, 7, 2, 9, 6, 3, 1])"] }, { "cell_type": "code", diff --git a/leetcode/invert_binary_tree/solution.py b/leetcode/invert_binary_tree/solution.py index 452457c..1e11b18 100644 --- a/leetcode/invert_binary_tree/solution.py +++ b/leetcode/invert_binary_tree/solution.py @@ -10,7 +10,7 @@ class Solution: # DFS recursive # Time: O(n) # Space: O(h) where h is height of tree - def invert_tree(self, root: TreeNode | None) -> TreeNode | None: + def invert_tree(self, root: TreeNode[int] | None) -> TreeNode[int] | None: if not root: return None @@ -22,11 +22,11 @@ class SolutionDFS: # DFS iterative # Time: O(n) # Space: O(h) where h is height of tree - def invert_tree(self, root: TreeNode | None) -> TreeNode | None: + def invert_tree(self, root: TreeNode[int] | None) -> TreeNode[int] | None: if not root: return None - stack: list[TreeNode | None] = [root] + stack: list[TreeNode[int] | None] = [root] while stack: node = stack.pop() if node is None: @@ -42,11 +42,11 @@ def invert_tree(self, root: TreeNode | None) -> TreeNode | None: class SolutionBFS: # Time: O(n) # Space: O(w) where w is maximum width of tree - def invert_tree(self, root: TreeNode | None) -> TreeNode | None: + def invert_tree(self, root: TreeNode[int] | None) -> TreeNode[int] | None: if not root: return None - queue: deque[TreeNode | None] = deque([root]) + queue: deque[TreeNode[int] | None] = deque([root]) while queue: node = queue.popleft() if node is None: diff --git a/leetcode/invert_binary_tree/tests.py b/leetcode/invert_binary_tree/tests.py index 5bf066f..dff6608 100644 --- a/leetcode/invert_binary_tree/tests.py +++ b/leetcode/invert_binary_tree/tests.py @@ -20,7 +20,7 @@ def test_invert_tree( solution_class: type[Solution | SolutionDFS | SolutionBFS], ): solution = solution_class() - root = TreeNode.from_list(root_list) - expected = TreeNode.from_list(expected_list) + root = TreeNode[int].from_list(root_list) + expected = TreeNode[int].from_list(expected_list) result = solution.invert_tree(root) assert result == expected diff --git a/leetcode/lru_cache/solution.py b/leetcode/lru_cache/solution.py index 817b786..33cdbb6 100644 --- a/leetcode/lru_cache/solution.py +++ b/leetcode/lru_cache/solution.py @@ -3,7 +3,7 @@ class LRUCache: # Space: O(capacity) - def __init__(self, capacity: int): + def __init__(self, capacity: int) -> None: self.capacity = capacity self.cache: OrderedDict[int, int] = OrderedDict() diff --git a/leetcode/reverse_linked_list_ii/playground.ipynb b/leetcode/reverse_linked_list_ii/playground.ipynb index 87d95ac..8fdc5e9 100644 --- a/leetcode/reverse_linked_list_ii/playground.ipynb +++ b/leetcode/reverse_linked_list_ii/playground.ipynb @@ -14,7 +14,7 @@ "id": "setup", "metadata": {}, "outputs": [], - "source": ["# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode.from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode.from_list([1, 4, 3, 2, 5])"] + "source": ["# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode[int].from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode[int].from_list([1, 4, 3, 2, 5])"] }, { "cell_type": "code", diff --git a/leetcode/reverse_linked_list_ii/solution.py b/leetcode/reverse_linked_list_ii/solution.py index 7db8da6..db9a3a0 100644 --- a/leetcode/reverse_linked_list_ii/solution.py +++ b/leetcode/reverse_linked_list_ii/solution.py @@ -4,11 +4,11 @@ class Solution: # Time: O(n) # Space: O(1) - def reverse_between(self, head: ListNode | None, left: int, right: int) -> ListNode | None: + def reverse_between(self, head: ListNode[int] | None, left: int, right: int) -> ListNode[int] | None: if not head or left == right: return head - dummy = ListNode(0) + dummy = ListNode[int](0) dummy.next = head prev = dummy diff --git a/leetcode/reverse_linked_list_ii/tests.py b/leetcode/reverse_linked_list_ii/tests.py index 546cd35..e3ea5b7 100644 --- a/leetcode/reverse_linked_list_ii/tests.py +++ b/leetcode/reverse_linked_list_ii/tests.py @@ -18,7 +18,7 @@ def setup_method(self): def test_reverse_between( self, head_list: list[int], left: int, right: int, expected_list: list[int] ): - head = ListNode.from_list(head_list) - expected = ListNode.from_list(expected_list) + head = ListNode[int].from_list(head_list) + expected = ListNode[int].from_list(expected_list) result = self.solution.reverse_between(head, left, right) assert result == expected diff --git a/leetcode_py/data_structures/list_node.py b/leetcode_py/data_structures/list_node.py index 6759952..c5b5409 100644 --- a/leetcode_py/data_structures/list_node.py +++ b/leetcode_py/data_structures/list_node.py @@ -1,10 +1,16 @@ -class ListNode: - def __init__(self, val: int = 0, next: "ListNode | None" = None): +from typing import Generic, TypeVar + +# TODO: Remove TypeVar when minimum Python version is 3.12+ (use class ListNode[T]: syntax) +T = TypeVar("T") + + +class ListNode(Generic[T]): + def __init__(self, val: T, next: "ListNode[T] | None" = None): self.val = val self.next = next @classmethod - def from_list(cls, arr: list[int]) -> "ListNode | None": + def from_list(cls, arr: list[T]) -> "ListNode[T] | None": if not arr: return None head = cls(arr[0]) @@ -14,9 +20,9 @@ def from_list(cls, arr: list[int]) -> "ListNode | None": current = current.next return head - def to_list(self) -> list[int]: + def to_list(self) -> list[T]: result = [] - current: "ListNode | None" = self + current: "ListNode[T] | None" = self while current: result.append(current.val) current = current.next diff --git a/leetcode_py/data_structures/tree_node.py b/leetcode_py/data_structures/tree_node.py index 796c9f3..ca96921 100644 --- a/leetcode_py/data_structures/tree_node.py +++ b/leetcode_py/data_structures/tree_node.py @@ -1,8 +1,13 @@ +from typing import Any, Generic, TypeVar + import graphviz from anytree import Node, RenderTree +# TODO: Remove TypeVar when minimum Python version is 3.12+ (use class TreeNode[T]: syntax) +T = TypeVar("T") + -def build_anytree(node: "TreeNode | None", parent: Node | None = None) -> Node | None: +def build_anytree(node: "TreeNode[Any] | None", parent: Node | None = None) -> Node | None: if not node: return Node("None", parent=parent) if parent else None current = Node(str(node.val), parent=parent) @@ -12,7 +17,7 @@ def build_anytree(node: "TreeNode | None", parent: Node | None = None) -> Node | return current -def add_nodes(dot: graphviz.Digraph, node: "TreeNode | None", node_id: int = 0) -> int: +def add_nodes(dot: graphviz.Digraph, node: "TreeNode[Any] | None", node_id: int = 0) -> int: if not node: return node_id @@ -31,14 +36,14 @@ def add_nodes(dot: graphviz.Digraph, node: "TreeNode | None", node_id: int = 0) return next_id - 1 -class TreeNode: - def __init__(self, val: int = 0, left: "TreeNode | None" = None, right: "TreeNode | None" = None): +class TreeNode(Generic[T]): + def __init__(self, val: T, left: "TreeNode[T] | None" = None, right: "TreeNode[T] | None" = None): self.val = val self.left = left self.right = right @classmethod - def from_list(cls, arr: list[int | None]) -> "TreeNode | None": + def from_list(cls, arr: list[T | None]) -> "TreeNode[T] | None": """Convert array representation to binary tree.""" if not arr or arr[0] is None: return None @@ -66,10 +71,10 @@ def from_list(cls, arr: list[int | None]) -> "TreeNode | None": return root - def to_list(self) -> list[int | None]: + def to_list(self) -> list[T | None]: """Convert binary tree to array representation.""" - result: list[int | None] = [] - queue: list[TreeNode | None] = [self] + result: list[T | None] = [] + queue: list[TreeNode[T] | None] = [self] while queue: node = queue.pop(0) diff --git a/tests/data_structures/test_list_node.py b/tests/data_structures/test_list_node.py index 236f188..cc1bb9d 100644 --- a/tests/data_structures/test_list_node.py +++ b/tests/data_structures/test_list_node.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from leetcode_py import ListNode @@ -5,32 +7,33 @@ class TestListNode: @pytest.mark.parametrize( - "val,expected_val,expected_next", + "val, expected_val, expected_next", [ - (None, 0, None), # default (5, 5, None), # with value + ("hello", "hello", None), # string value ], ) - def test_init(self, val, expected_val, expected_next): - node = ListNode() if val is None else ListNode(val) + def test_init(self, val: Any, expected_val: Any, expected_next: Any) -> None: + node = ListNode(val) assert node.val == expected_val assert node.next == expected_next - def test_init_with_next(self): - next_node = ListNode(2) - node = ListNode(1, next_node) + def test_init_with_next(self) -> None: + next_node = ListNode[int](2) + node = ListNode[int](1, next_node) assert node.val == 1 assert node.next == next_node @pytest.mark.parametrize( - "input_list,expected_result", + "input_list, expected_result", [ ([], None), ([1], "single_node"), ([1, 2, 3], "multiple_nodes"), + (["a", "b"], "string_nodes"), ], ) - def test_from_list(self, input_list, expected_result): + def test_from_list(self, input_list: list[Any], expected_result: str | None) -> None: result = ListNode.from_list(input_list) if expected_result is None: @@ -47,26 +50,36 @@ def test_from_list(self, input_list, expected_result): assert result.next.next is not None assert result.next.next.val == 3 assert result.next.next.next is None + elif expected_result == "string_nodes": + assert result is not None + assert result.val == "a" + assert result.next is not None + assert result.next.val == "b" + assert result.next.next is None @pytest.mark.parametrize( - "input_list,expected_output", + "input_list, expected_output", [ ([1], [1]), ([1, 2, 3], [1, 2, 3]), + (["x", "y"], ["x", "y"]), ], ) - def test_to_list(self, input_list, expected_output): + def test_to_list(self, input_list: list[Any], expected_output: list[Any]) -> None: node = ListNode.from_list(input_list) assert node is not None assert node.to_list() == expected_output @pytest.mark.parametrize( - "input_list,expected_str,expected_repr", + "input_list, expected_str, expected_repr", [ ([1, 2, 3], "1 -> 2 -> 3", "ListNode([1, 2, 3])"), + (["a", "b"], "a -> b", "ListNode(['a', 'b'])"), ], ) - def test_string_representations(self, input_list, expected_str, expected_repr): + def test_string_representations( + self, input_list: list[Any], expected_str: str, expected_repr: str + ) -> None: node = ListNode.from_list(input_list) assert node is not None assert str(node) == expected_str @@ -74,20 +87,20 @@ def test_string_representations(self, input_list, expected_str, expected_repr): assert node._repr_html_() == expected_str @pytest.mark.parametrize( - "list1,list2,should_equal", + "list1,list2, should_equal", [ ([1, 2, 3], [1, 2, 3], True), ([1, 2, 3], [1, 2, 4], False), ], ) - def test_equality(self, list1, list2, should_equal): + def test_equality(self, list1: list[int], list2: list[int], should_equal: bool) -> None: node1 = ListNode.from_list(list1) node2 = ListNode.from_list(list2) assert (node1 == node2) == should_equal @pytest.mark.parametrize("other_value", [[1], "1"]) - def test_equality_different_types(self, other_value): - node = ListNode(1) + def test_equality_different_types(self, other_value: Any) -> None: + node = ListNode[int](1) assert node != other_value @pytest.mark.parametrize( @@ -96,9 +109,11 @@ def test_equality_different_types(self, other_value): [1, 2, 3, 4, 5], [1], [10, 20, 30], + ["hello", "world"], + [True, False, True], ], ) - def test_roundtrip_conversion(self, test_list): + def test_roundtrip_conversion(self, test_list: list[Any]) -> None: node = ListNode.from_list(test_list) assert node is not None result = node.to_list() diff --git a/tests/data_structures/test_tree_node.py b/tests/data_structures/test_tree_node.py index 4d07f9f..992dec7 100644 --- a/tests/data_structures/test_tree_node.py +++ b/tests/data_structures/test_tree_node.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from leetcode_py import TreeNode @@ -6,42 +8,42 @@ class TestTreeNode: @pytest.mark.parametrize( - "val,expected_val", + "val, expected_val", [ - (None, 0), # default - (5, 5), # with value + (5, 5), # integer value + ("hello", "hello"), # string value ], ) - def test_init(self, val, expected_val): - node = TreeNode() if val is None else TreeNode(val) + def test_init(self, val: Any, expected_val: Any) -> None: + node = TreeNode(val) assert node.val == expected_val assert node.left is None assert node.right is None - def test_init_with_children(self): - left = TreeNode(1) - right = TreeNode(2) - node = TreeNode(0, left, right) + def test_init_with_children(self) -> None: + left = TreeNode[int](1) + right = TreeNode[int](2) + node = TreeNode[int](0, left, right) assert node.val == 0 assert node.left == left assert node.right == right - def test_from_list_empty(self): - result = TreeNode.from_list([]) + def test_from_list_empty(self) -> None: + result: TreeNode[Any] | None = TreeNode.from_list([]) assert result is None - def test_from_list_none_root(self): + def test_from_list_none_root(self) -> None: result = TreeNode.from_list([None]) assert result is None - def test_from_list_single(self): + def test_from_list_single(self) -> None: result = TreeNode.from_list([1]) assert result is not None assert result.val == 1 assert result.left is None assert result.right is None - def test_from_list_complete_tree(self): + def test_from_list_complete_tree(self) -> None: result = TreeNode.from_list([1, 2, 3, 4, 5, 6, 7]) assert result is not None assert result.val == 1 @@ -58,7 +60,7 @@ def test_from_list_complete_tree(self): assert result.right.right is not None assert result.right.right.val == 7 - def test_from_list_sparse_tree(self): + def test_from_list_sparse_tree(self) -> None: result = TreeNode.from_list([1, None, 2]) assert result is not None assert result.val == 1 @@ -69,38 +71,40 @@ def test_from_list_sparse_tree(self): assert result.right.right is None @pytest.mark.parametrize( - "input_list,expected_output", + "input_list, expected_output", [ ([1], [1]), ([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7]), ([1, None, 2], [1, None, 2]), + (["a", "b", "c"], ["a", "b", "c"]), ], ) - def test_to_list(self, input_list, expected_output): + def test_to_list(self, input_list: list[Any], expected_output: list[Any]) -> None: node = TreeNode.from_list(input_list) assert node is not None assert node.to_list() == expected_output @pytest.mark.parametrize( - "input_list,expected_values", + "input_list, expected_values", [ ([1], ["1"]), ([1, 2, 3], ["1", "2", "3"]), + (["x", "y"], ["x", "y"]), ], ) - def test_str_representation(self, input_list, expected_values): + def test_str_representation(self, input_list: list[Any], expected_values: list[str]) -> None: node = TreeNode.from_list(input_list) assert node is not None result = str(node) for val in expected_values: assert val in result - def test_repr_representation(self): + def test_repr_representation(self) -> None: node = TreeNode.from_list([1, 2, 3]) assert node is not None assert repr(node) == "TreeNode([1, 2, 3])" - def test_repr_html_generates_svg(self): + def test_repr_html_generates_svg(self) -> None: node = TreeNode.from_list([1, 2, 3]) assert node is not None result = node._repr_html_() @@ -108,13 +112,15 @@ def test_repr_html_generates_svg(self): assert "svg" in result.lower() @pytest.mark.parametrize( - "list1,list2,should_equal", + "list1,list2, should_equal", [ ([1, 2, 3], [1, 2, 3], True), ([1, 2, 3], [1, 3, 2], False), ], ) - def test_equality(self, list1, list2, should_equal): + def test_equality( + self, list1: list[int | None], list2: list[int | None], should_equal: bool + ) -> None: node1 = TreeNode.from_list(list1) node2 = TreeNode.from_list(list2) assert node1 is not None @@ -122,8 +128,8 @@ def test_equality(self, list1, list2, should_equal): assert (node1 == node2) == should_equal @pytest.mark.parametrize("other_value", [[1], "1"]) - def test_equality_different_types(self, other_value): - node = TreeNode(1) + def test_equality_different_types(self, other_value: Any) -> None: + node = TreeNode[int](1) assert node != other_value @pytest.mark.parametrize( @@ -132,26 +138,28 @@ def test_equality_different_types(self, other_value): [1, 2, 3, 4, 5, None, 6], [1], [1, None, 2], + ["root", "left", "right"], + [True, False, None, True], ], ) - def test_roundtrip_conversion(self, test_list): + def test_roundtrip_conversion(self, test_list: list[Any]) -> None: node = TreeNode.from_list(test_list) assert node is not None result = node.to_list() assert result == test_list - def test_build_anytree_none(self): + def test_build_anytree_none(self) -> None: result = build_anytree(None) assert result is None - def test_build_anytree_single_node(self): - node = TreeNode(1) + def test_build_anytree_single_node(self) -> None: + node = TreeNode[int](1) result = build_anytree(node) assert result is not None assert result.name == "1" assert len(result.children) == 0 - def test_str_with_none_tree(self): + def test_str_with_none_tree(self) -> None: # Create a scenario where build_anytree returns None # This happens when we have a node but build_anytree fails import unittest.mock @@ -159,6 +167,6 @@ def test_str_with_none_tree(self): with unittest.mock.patch( "leetcode_py.data_structures.tree_node.build_anytree", return_value=None ): - node = TreeNode(1) + node = TreeNode[int](1) result = str(node) assert result == "None" From 297a91c94bf3b655f91147e670bfd980e269ee61 Mon Sep 17 00:00:00 2001 From: Wisaroot <66859294+wisarootl@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:43:53 +0700 Subject: [PATCH 02/68] feat: add more questions (#18) --- .../json/binary_tree_right_side_view.json | 49 ++++ .templates/leetcode/json/clone_graph.json | 50 +++++ .../evaluate_reverse_polish_notation.json | 50 +++++ .../leetcode/json/minimum_height_trees.json | 47 ++++ .templates/leetcode/json/task_scheduler.json | 50 +++++ .../json/validate_binary_search_tree.json | 47 ++++ Makefile | 2 +- .../binary_tree_right_side_view/README.md | 50 +++++ .../binary_tree_right_side_view/__init__.py | 0 .../playground.ipynb | 82 +++++++ .../binary_tree_right_side_view/solution.py | 67 ++++++ leetcode/binary_tree_right_side_view/tests.py | 30 +++ leetcode/clone_graph/README.md | 72 ++++++ leetcode/clone_graph/__init__.py | 0 leetcode/clone_graph/playground.ipynb | 209 ++++++++++++++++++ leetcode/clone_graph/solution.py | 73 ++++++ leetcode/clone_graph/tests.py | 25 +++ .../playground.ipynb | 3 +- .../README.md | 56 +++++ .../__init__.py | 0 .../playground.ipynb | 79 +++++++ .../solution.py | 20 ++ .../evaluate_reverse_polish_notation/tests.py | 23 ++ leetcode/insert_interval/playground.ipynb | 3 +- leetcode/invert_binary_tree/playground.ipynb | 22 +- leetcode/minimum_height_trees/README.md | 47 ++++ leetcode/minimum_height_trees/__init__.py | 0 .../minimum_height_trees/playground.ipynb | 80 +++++++ leetcode/minimum_height_trees/solution.py | 29 +++ leetcode/minimum_height_trees/tests.py | 23 ++ .../reverse_linked_list_ii/playground.ipynb | 23 +- leetcode/spiral_matrix/playground.ipynb | 3 +- leetcode/task_scheduler/README.md | 54 +++++ leetcode/task_scheduler/__init__.py | 0 leetcode/task_scheduler/playground.ipynb | 88 ++++++++ leetcode/task_scheduler/solution.py | 65 ++++++ leetcode/task_scheduler/tests.py | 30 +++ .../validate_binary_search_tree/README.md | 44 ++++ .../validate_binary_search_tree/__init__.py | 0 .../playground.ipynb | 82 +++++++ .../validate_binary_search_tree/solution.py | 60 +++++ leetcode/validate_binary_search_tree/tests.py | 30 +++ leetcode_py/__init__.py | 3 +- leetcode_py/data_structures/graph_node.py | 159 +++++++++++++ tests/data_structures/test_graph_node.py | 161 ++++++++++++++ 45 files changed, 2077 insertions(+), 13 deletions(-) create mode 100644 .templates/leetcode/json/binary_tree_right_side_view.json create mode 100644 .templates/leetcode/json/clone_graph.json create mode 100644 .templates/leetcode/json/evaluate_reverse_polish_notation.json create mode 100644 .templates/leetcode/json/minimum_height_trees.json create mode 100644 .templates/leetcode/json/task_scheduler.json create mode 100644 .templates/leetcode/json/validate_binary_search_tree.json create mode 100644 leetcode/binary_tree_right_side_view/README.md create mode 100644 leetcode/binary_tree_right_side_view/__init__.py create mode 100644 leetcode/binary_tree_right_side_view/playground.ipynb create mode 100644 leetcode/binary_tree_right_side_view/solution.py create mode 100644 leetcode/binary_tree_right_side_view/tests.py create mode 100644 leetcode/clone_graph/README.md create mode 100644 leetcode/clone_graph/__init__.py create mode 100644 leetcode/clone_graph/playground.ipynb create mode 100644 leetcode/clone_graph/solution.py create mode 100644 leetcode/clone_graph/tests.py create mode 100644 leetcode/evaluate_reverse_polish_notation/README.md create mode 100644 leetcode/evaluate_reverse_polish_notation/__init__.py create mode 100644 leetcode/evaluate_reverse_polish_notation/playground.ipynb create mode 100644 leetcode/evaluate_reverse_polish_notation/solution.py create mode 100644 leetcode/evaluate_reverse_polish_notation/tests.py create mode 100644 leetcode/minimum_height_trees/README.md create mode 100644 leetcode/minimum_height_trees/__init__.py create mode 100644 leetcode/minimum_height_trees/playground.ipynb create mode 100644 leetcode/minimum_height_trees/solution.py create mode 100644 leetcode/minimum_height_trees/tests.py create mode 100644 leetcode/task_scheduler/README.md create mode 100644 leetcode/task_scheduler/__init__.py create mode 100644 leetcode/task_scheduler/playground.ipynb create mode 100644 leetcode/task_scheduler/solution.py create mode 100644 leetcode/task_scheduler/tests.py create mode 100644 leetcode/validate_binary_search_tree/README.md create mode 100644 leetcode/validate_binary_search_tree/__init__.py create mode 100644 leetcode/validate_binary_search_tree/playground.ipynb create mode 100644 leetcode/validate_binary_search_tree/solution.py create mode 100644 leetcode/validate_binary_search_tree/tests.py create mode 100644 leetcode_py/data_structures/graph_node.py create mode 100644 tests/data_structures/test_graph_node.py diff --git a/.templates/leetcode/json/binary_tree_right_side_view.json b/.templates/leetcode/json/binary_tree_right_side_view.json new file mode 100644 index 0000000..a5165db --- /dev/null +++ b/.templates/leetcode/json/binary_tree_right_side_view.json @@ -0,0 +1,49 @@ +{ + "problem_name": "binary_tree_right_side_view", + "solution_class_name": "Solution", + "problem_number": "199", + "problem_title": "Binary Tree Right Side View", + "difficulty": "Medium", + "topics": "Tree, Depth-First Search, Breadth-First Search, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given the `root` of a binary tree, imagine yourself standing on the **right side** of it, return *the values of the nodes you can see ordered from top to bottom*.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2024/11/24/tmpd5jn43fs-1.png)\n\n```\nInput: root = [1,2,3,null,5,null,4]\nOutput: [1,3,4]\n```" + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2024/11/24/tmpkpe40xeh-1.png)\n\n```\nInput: root = [1,2,3,4,null,null,null,5]\nOutput: [1,3,4,5]\n```" + }, + { "content": "```\nInput: root = [1,null,3]\nOutput: [1,3]\n```" }, + { "content": "```\nInput: root = []\nOutput: []\n```" } + ], + "readme_constraints": "- The number of nodes in the tree is in the range `[0, 100]`.\n- `-100 <= Node.val <= 100`", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "right_side_view", + "parameters": "root: TreeNode[int] | None", + "return_type": "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": "BinaryTreeRightSideView", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_right_side_view", + "parametrize": "root_list, expected", + "parametrize_typed": "root_list: list[int | None], expected: list[int]", + "test_cases": "[([1, 2, 3, None, 5, None, 4], [1, 3, 4]), ([1, 2, 3, 4, None, None, None, 5], [1, 3, 4, 5]), ([1, None, 3], [1, 3]), ([], [])]", + "body": "root = TreeNode.from_list(root_list)\nresult = self.solution.right_side_view(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, None, 5, None, 4]\nexpected = [1, 3, 4]", + "playground_execution": "root = TreeNode.from_list(root_list)\nresult = Solution().right_side_view(root)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/clone_graph.json b/.templates/leetcode/json/clone_graph.json new file mode 100644 index 0000000..2e338d6 --- /dev/null +++ b/.templates/leetcode/json/clone_graph.json @@ -0,0 +1,50 @@ +{ + "problem_name": "clone_graph", + "solution_class_name": "Solution", + "problem_number": "133", + "problem_title": "Clone Graph", + "difficulty": "Medium", + "topics": "Hash Table, Depth-First Search, Breadth-First Search, Graph", + "tags": ["grind-75"], + "readme_description": "Given a reference of a node in a **connected** undirected graph.\n\nReturn a **deep copy** (clone) of the graph.\n\nEach node in the graph contains a value (`int`) and a list (`List[Node]`) of its neighbors.\n\n```\nclass Node {\n public int val;\n public List neighbors;\n}\n```\n\n**Test case format:**\n\nFor simplicity, each node's value is the same as the node's index (1-indexed). For example, the first node with `val == 1`, the second node with `val == 2`, and so on. The graph is represented in the test case using an adjacency list.\n\n**An adjacency list** is a collection of unordered **lists** used to represent a finite graph. Each list describes the set of neighbors of a node in the graph.\n\nThe given node will always be the first node with `val = 1`. You must return the **copy of the given node** as a reference to the cloned graph.", + "readme_examples": [ + { + "content": "\"\"\n\n```\nInput: adjList = [[2,4],[1,3],[2,4],[1,3]]\nOutput: [[2,4],[1,3],[2,4],[1,3]]\nExplanation: There are 4 nodes in the graph.\n1st node (val = 1)'s neighbors are 2nd node (val = 2) and 4th node (val = 4).\n2nd node (val = 2)'s neighbors are 1st node (val = 1) and 3rd node (val = 3).\n3rd node (val = 3)'s neighbors are 2nd node (val = 2) and 4th node (val = 4).\n4th node (val = 4)'s neighbors are 1st node (val = 1) and 3rd node (val = 3).\n```" + }, + { + "content": "\"\"\n\n```\nInput: adjList = [[]]\nOutput: [[]]\nExplanation: Note that the input contains one empty list. The graph consists of only one node with val = 1 and it does not have any neighbors.\n```" + }, + { + "content": "```\nInput: adjList = []\nOutput: []\nExplanation: This an empty graph, it does not have any nodes.\n```" + } + ], + "readme_constraints": "- The number of nodes in the graph is in the range `[0, 100]`.\n- `1 <= Node.val <= 100`\n- `Node.val` is unique for each node.\n- There are no repeated edges and no self-loops in the graph.\n- The Graph is connected and all nodes can be visited starting from the given node.", + "readme_additional": "", + "solution_imports": "from leetcode_py import GraphNode", + "solution_methods": [ + { + "name": "clone_graph", + "parameters": "node: GraphNode | None", + "return_type": "GraphNode | None", + "dummy_return": "None" + } + ], + "test_imports": "import pytest\n\nfrom leetcode_py import GraphNode\nfrom leetcode_py.test_utils import logged_test\n\nfrom .solution import Solution", + "test_class_name": "CloneGraph", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_clone_graph", + "parametrize": "adj_list", + "parametrize_typed": "adj_list: list[list[int]]", + "test_cases": "[[[2, 4], [1, 3], [2, 4], [1, 3]], [[]], []]", + "body": "node = GraphNode.from_adjacency_list(adj_list)\nresult = self.solution.clone_graph(node)\nassert result.is_clone(node) if result else node is None" + } + ], + "playground_imports": "from solution import Solution\n\nfrom leetcode_py import GraphNode", + "playground_test_case": "# Example test case\nadj_list = [[2,4],[1,3],[2,4],[1,3]]\nnode = GraphNode.from_adjacency_list(adj_list)", + "playground_execution": "result = Solution().clone_graph(node)\nresult", + "playground_assertion": "assert result.is_clone(node) if result else node is None" +} diff --git a/.templates/leetcode/json/evaluate_reverse_polish_notation.json b/.templates/leetcode/json/evaluate_reverse_polish_notation.json new file mode 100644 index 0000000..08e7061 --- /dev/null +++ b/.templates/leetcode/json/evaluate_reverse_polish_notation.json @@ -0,0 +1,50 @@ +{ + "problem_name": "evaluate_reverse_polish_notation", + "solution_class_name": "Solution", + "problem_number": "150", + "problem_title": "Evaluate Reverse Polish Notation", + "difficulty": "Medium", + "topics": "Array, Math, Stack", + "tags": ["grind-75"], + "readme_description": "You are given an array of strings `tokens` that represents an arithmetic expression in a **Reverse Polish Notation**.\n\nEvaluate the expression. Return *an integer that represents the value of the expression*.\n\n**Note that:**\n\n- The valid operators are `'+'`, `'-'`, `'*'`, and `'/'`.\n- Each operand may be an integer or another expression.\n- The division between two integers always **truncates toward zero**.\n- There will not be any division by zero.\n- The input represents a valid arithmetic expression in a reverse polish notation.\n- The answer and all the intermediate calculations can be represented in a **32-bit** integer.", + "readme_examples": [ + { + "content": "```\nInput: tokens = [\"2\",\"1\",\"+\",\"3\",\"*\"]\nOutput: 9\n```\n**Explanation:** ((2 + 1) * 3) = 9" + }, + { + "content": "```\nInput: tokens = [\"4\",\"13\",\"5\",\"/\",\"+\"]\nOutput: 6\n```\n**Explanation:** (4 + (13 / 5)) = 6" + }, + { + "content": "```\nInput: tokens = [\"10\",\"6\",\"9\",\"3\",\"+\",\"-11\",\"*\",\"/\",\"*\",\"17\",\"+\",\"5\",\"+\"]\nOutput: 22\n```\n**Explanation:** ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = 22" + } + ], + "readme_constraints": "- `1 <= tokens.length <= 10^4`\n- `tokens[i]` is either an operator: `\"+\"`, `\"-\"`, `\"*\"`, or `\"/\"`, or an integer in the range `[-200, 200]`.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "eval_rpn", + "parameters": "tokens: 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": "EvaluateReversePolishNotation", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_eval_rpn", + "parametrize": "tokens, expected", + "parametrize_typed": "tokens: list[str], expected: int", + "test_cases": "[([\"2\", \"1\", \"+\", \"3\", \"*\"], 9), ([\"4\", \"13\", \"5\", \"/\", \"+\"], 6), ([\"10\", \"6\", \"9\", \"3\", \"+\", \"-11\", \"*\", \"/\", \"*\", \"17\", \"+\", \"5\", \"+\"], 22)]", + "body": "result = self.solution.eval_rpn(tokens)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ntokens = [\\\"2\\\", \\\"1\\\", \\\"+\\\", \\\"3\\\", \\\"*\\\"]\nexpected = 9", + "playground_execution": "result = Solution().eval_rpn(tokens)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/minimum_height_trees.json b/.templates/leetcode/json/minimum_height_trees.json new file mode 100644 index 0000000..86669a2 --- /dev/null +++ b/.templates/leetcode/json/minimum_height_trees.json @@ -0,0 +1,47 @@ +{ + "problem_name": "minimum_height_trees", + "solution_class_name": "Solution", + "problem_number": "310", + "problem_title": "Minimum Height Trees", + "difficulty": "Medium", + "topics": "Depth-First Search, Breadth-First Search, Graph, Topological Sort", + "tags": ["grind-75"], + "readme_description": "A tree is an undirected graph in which any two vertices are connected by *exactly* one path. In other words, any connected graph without simple cycles is a tree.\n\nGiven a tree of `n` nodes labelled from `0` to `n - 1`, and an array of `n - 1` `edges` where `edges[i] = [ai, bi]` indicates that there is an undirected edge between the two nodes `ai` and `bi` in the tree, you can choose any node of the tree as the root. When you select a node `x` as the root, the result tree has height `h`. Among all possible rooted trees, those with minimum height (i.e. `min(h)`) are called **minimum height trees** (MHTs).\n\nReturn *a list of all **MHTs'** root labels*. You can return the answer in **any order**.\n\nThe **height** of a rooted tree is the number of edges on the longest downward path between the root and a leaf.", + "readme_examples": [ + { + "content": "\"\"\n\n```\nInput: n = 4, edges = [[1,0],[1,2],[1,3]]\nOutput: [1]\nExplanation: As shown, the height of the tree is 1 when the root is the node with label 1 which is the only MHT.\n```" + }, + { + "content": "\"\"\n\n```\nInput: n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]\nOutput: [3,4]\n```" + } + ], + "readme_constraints": "- `1 <= n <= 2 * 10^4`\n- `edges.length == n - 1`\n- `0 <= ai, bi < n`\n- `ai != bi`\n- All the pairs `(ai, bi)` are distinct.\n- The given input is **guaranteed** to be a tree and there will be **no repeated** edges.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "find_min_height_trees", + "parameters": "n: int, edges: list[list[int]]", + "return_type": "list[int]", + "dummy_return": "[]" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "MinimumHeightTrees", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_find_min_height_trees", + "parametrize": "n, edges, expected", + "parametrize_typed": "n: int, edges: list[list[int]], expected: list[int]", + "test_cases": "[(4, [[1,0],[1,2],[1,3]], [1]), (6, [[3,0],[3,1],[3,2],[3,4],[5,4]], [3,4]), (1, [], [0])]", + "body": "result = self.solution.find_min_height_trees(n, edges)\nassert sorted(result) == sorted(expected)" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nn = 4\nedges = [[1,0],[1,2],[1,3]]\nexpected = [1]", + "playground_execution": "result = Solution().find_min_height_trees(n, edges)\nresult", + "playground_assertion": "assert sorted(result) == sorted(expected)" +} diff --git a/.templates/leetcode/json/task_scheduler.json b/.templates/leetcode/json/task_scheduler.json new file mode 100644 index 0000000..aa7481b --- /dev/null +++ b/.templates/leetcode/json/task_scheduler.json @@ -0,0 +1,50 @@ +{ + "problem_name": "task_scheduler", + "solution_class_name": "Solution", + "problem_number": "621", + "problem_title": "Task Scheduler", + "difficulty": "Medium", + "topics": "Array, Hash Table, Greedy, Sorting, Heap (Priority Queue), Counting", + "tags": ["grind-75"], + "readme_description": "You are given an array of CPU `tasks`, each labeled with a letter from A to Z, and a number `n`. Each CPU interval can be idle or allow the completion of one task. Tasks can be completed in any order, but there's a constraint: there has to be a gap of **at least** `n` intervals between two tasks with the same label.\n\nReturn the **minimum** number of CPU intervals required to complete all tasks.", + "readme_examples": [ + { + "content": "```\nInput: tasks = [\"A\",\"A\",\"A\",\"B\",\"B\",\"B\"], n = 2\nOutput: 8\n```\n**Explanation:** A possible sequence is: A -> B -> idle -> A -> B -> idle -> A -> B.\n\nAfter completing task A, you must wait two intervals before doing A again. The same applies to task B. In the 3rd interval, neither A nor B can be done, so you idle. By the 4th interval, you can do A again as 2 intervals have passed." + }, + { + "content": "```\nInput: tasks = [\"A\",\"C\",\"A\",\"B\",\"D\",\"B\"], n = 1\nOutput: 6\n```\n**Explanation:** A possible sequence is: A -> B -> C -> D -> A -> B.\n\nWith a cooling interval of 1, you can repeat a task after just one other task." + }, + { + "content": "```\nInput: tasks = [\"A\",\"A\",\"A\", \"B\",\"B\",\"B\"], n = 3\nOutput: 10\n```\n**Explanation:** A possible sequence is: A -> B -> idle -> idle -> A -> B -> idle -> idle -> A -> B.\n\nThere are only two types of tasks, A and B, which need to be separated by 3 intervals. This leads to idling twice between repetitions of these tasks." + } + ], + "readme_constraints": "- `1 <= tasks.length <= 10^4`\n- `tasks[i]` is an uppercase English letter.\n- `0 <= n <= 100`", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "least_interval", + "parameters": "tasks: list[str], n: 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": "TaskScheduler", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_least_interval", + "parametrize": "tasks, n, expected", + "parametrize_typed": "tasks: list[str], n: int, expected: int", + "test_cases": "[([\"A\", \"A\", \"A\", \"B\", \"B\", \"B\"], 2, 8), ([\"A\", \"C\", \"A\", \"B\", \"D\", \"B\"], 1, 6), ([\"A\", \"A\", \"A\", \"B\", \"B\", \"B\"], 3, 10)]", + "body": "result = self.solution.least_interval(tasks, n)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ntasks = [\\\"A\\\", \\\"A\\\", \\\"A\\\", \\\"B\\\", \\\"B\\\", \\\"B\\\"]\nn = 2\nexpected = 8", + "playground_execution": "result = Solution().least_interval(tasks, n)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/validate_binary_search_tree.json b/.templates/leetcode/json/validate_binary_search_tree.json new file mode 100644 index 0000000..fd834b8 --- /dev/null +++ b/.templates/leetcode/json/validate_binary_search_tree.json @@ -0,0 +1,47 @@ +{ + "problem_name": "validate_binary_search_tree", + "solution_class_name": "Solution", + "problem_number": "98", + "problem_title": "Validate Binary Search Tree", + "difficulty": "Medium", + "topics": "Tree, Depth-First Search, Binary Search Tree, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given the `root` of a binary tree, determine if it is a valid binary search tree (BST).\n\nA **valid BST** is defined as follows:\n\n- The left subtree of a node contains only nodes with keys **strictly less than** the node's key.\n- The right subtree of a node contains only nodes with keys **strictly greater than** the node's key.\n- Both the left and right subtrees must also be binary search trees.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2020/12/01/tree1.jpg)\n\n```\nInput: root = [2,1,3]\nOutput: true\n```" + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2020/12/01/tree2.jpg)\n\n```\nInput: root = [5,1,4,null,null,3,6]\nOutput: false\n```\n**Explanation:** The root node's value is 5 but its right child's value is 4." + } + ], + "readme_constraints": "- The number of nodes in the tree is in the range `[1, 10^4]`.\n- `-2^31 <= Node.val <= 2^31 - 1`", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "is_valid_bst", + "parameters": "root: TreeNode[int] | 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": "ValidateBinarySearchTree", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_is_valid_bst", + "parametrize": "root_list, expected", + "parametrize_typed": "root_list: list[int | None], expected: bool", + "test_cases": "[([2, 1, 3], True), ([5, 1, 4, None, None, 3, 6], False), ([2, 1, 3], True), ([1], True), ([1, 1], False)]", + "body": "root = TreeNode.from_list(root_list)\nresult = self.solution.is_valid_bst(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] = [2, 1, 3]\nexpected = True", + "playground_execution": "root = TreeNode.from_list(root_list)\nresult = Solution().is_valid_bst(root)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index e9b4dc7..91a7d24 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= invert_binary_tree +PROBLEM ?= task_scheduler FORCE ?= 0 sync_submodules: diff --git a/leetcode/binary_tree_right_side_view/README.md b/leetcode/binary_tree_right_side_view/README.md new file mode 100644 index 0000000..b44249a --- /dev/null +++ b/leetcode/binary_tree_right_side_view/README.md @@ -0,0 +1,50 @@ +# Binary Tree Right Side View + +**Difficulty:** Medium +**Topics:** Tree, Depth-First Search, Breadth-First Search, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 199](https://leetcode.com/problems/binary-tree-right-side-view/description/) + +## Problem Description + +Given the `root` of a binary tree, imagine yourself standing on the **right side** of it, return _the values of the nodes you can see ordered from top to bottom_. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2024/11/24/tmpd5jn43fs-1.png) + +``` +Input: root = [1,2,3,null,5,null,4] +Output: [1,3,4] +``` + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2024/11/24/tmpkpe40xeh-1.png) + +``` +Input: root = [1,2,3,4,null,null,null,5] +Output: [1,3,4,5] +``` + +### Example 3: + +``` +Input: root = [1,null,3] +Output: [1,3] +``` + +### Example 4: + +``` +Input: root = [] +Output: [] +``` + +## Constraints + +- The number of nodes in the tree is in the range `[0, 100]`. +- `-100 <= Node.val <= 100` diff --git a/leetcode/binary_tree_right_side_view/__init__.py b/leetcode/binary_tree_right_side_view/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/binary_tree_right_side_view/playground.ipynb b/leetcode/binary_tree_right_side_view/playground.ipynb new file mode 100644 index 0000000..1deced0 --- /dev/null +++ b/leetcode/binary_tree_right_side_view/playground.ipynb @@ -0,0 +1,82 @@ +{ + "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, None, 5, None, 4]\n", + "expected = [1, 3, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 3, 4]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root = TreeNode.from_list(root_list)\n", + "result = Solution().right_side_view(root)\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/binary_tree_right_side_view/solution.py b/leetcode/binary_tree_right_side_view/solution.py new file mode 100644 index 0000000..841c2c8 --- /dev/null +++ b/leetcode/binary_tree_right_side_view/solution.py @@ -0,0 +1,67 @@ +from collections import deque + +from leetcode_py import TreeNode + + +class Solution: + # Time: O(n) + # Space: O(h) + def right_side_view(self, root: TreeNode[int] | None) -> list[int]: + result: list[int] = [] + + def dfs(node: TreeNode[int] | None, level: int) -> None: + if not node: + return + if level == len(result): + result.append(node.val) + dfs(node.right, level + 1) + dfs(node.left, level + 1) + + dfs(root, 0) + return result + + +class SolutionDFS: + # Time: O(n) + # Space: O(h) + def right_side_view(self, root: TreeNode[int] | None) -> list[int]: + if not root: + return [] + + result: list[int] = [] + stack = [(root, 0)] + + while stack: + node, level = stack.pop() + if level == len(result): + result.append(node.val) + if node.left: + stack.append((node.left, level + 1)) + if node.right: + stack.append((node.right, level + 1)) + + return result + + +class SolutionBFS: + # Time: O(n) + # Space: O(w) + def right_side_view(self, root: TreeNode[int] | None) -> list[int]: + if not root: + return [] + + result: list[int] = [] + queue = deque([root]) + + while queue: + level_size = len(queue) + for i in range(level_size): + node = queue.popleft() + if i == level_size - 1: # rightmost node + result.append(node.val) + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + return result diff --git a/leetcode/binary_tree_right_side_view/tests.py b/leetcode/binary_tree_right_side_view/tests.py new file mode 100644 index 0000000..acfd3ad --- /dev/null +++ b/leetcode/binary_tree_right_side_view/tests.py @@ -0,0 +1,30 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution, SolutionBFS, SolutionDFS + + +class TestBinaryTreeRightSideView: + @pytest.mark.parametrize("solution_class", [Solution, SolutionDFS, SolutionBFS]) + @pytest.mark.parametrize( + "root_list, expected", + [ + ([1, 2, 3, None, 5, None, 4], [1, 3, 4]), + ([1, 2, 3, 4, None, None, None, 5], [1, 3, 4, 5]), + ([1, None, 3], [1, 3]), + ([], []), + ], + ) + @logged_test + def test_right_side_view( + self, + root_list: list[int | None], + expected: list[int], + solution_class: type[Solution | SolutionDFS | SolutionBFS], + ): + solution = solution_class() + root = TreeNode.from_list(root_list) + result = solution.right_side_view(root) + assert result == expected diff --git a/leetcode/clone_graph/README.md b/leetcode/clone_graph/README.md new file mode 100644 index 0000000..eeda5d5 --- /dev/null +++ b/leetcode/clone_graph/README.md @@ -0,0 +1,72 @@ +# Clone Graph + +**Difficulty:** Medium +**Topics:** Hash Table, Depth-First Search, Breadth-First Search, Graph +**Tags:** grind-75 + +**LeetCode:** [Problem 133](https://leetcode.com/problems/clone-graph/description/) + +## Problem Description + +Given a reference of a node in a **connected** undirected graph. + +Return a **deep copy** (clone) of the graph. + +Each node in the graph contains a value (`int`) and a list (`List[Node]`) of its neighbors. + +``` +class Node { + public int val; + public List neighbors; +} +``` + +**Test case format:** + +For simplicity, each node's value is the same as the node's index (1-indexed). For example, the first node with `val == 1`, the second node with `val == 2`, and so on. The graph is represented in the test case using an adjacency list. + +**An adjacency list** is a collection of unordered **lists** used to represent a finite graph. Each list describes the set of neighbors of a node in the graph. + +The given node will always be the first node with `val = 1`. You must return the **copy of the given node** as a reference to the cloned graph. + +## Examples + +### Example 1: + + + +``` +Input: adjList = [[2,4],[1,3],[2,4],[1,3]] +Output: [[2,4],[1,3],[2,4],[1,3]] +Explanation: There are 4 nodes in the graph. +1st node (val = 1)'s neighbors are 2nd node (val = 2) and 4th node (val = 4). +2nd node (val = 2)'s neighbors are 1st node (val = 1) and 3rd node (val = 3). +3rd node (val = 3)'s neighbors are 2nd node (val = 2) and 4th node (val = 4). +4th node (val = 4)'s neighbors are 1st node (val = 1) and 3rd node (val = 3). +``` + +### Example 2: + + + +``` +Input: adjList = [[]] +Output: [[]] +Explanation: Note that the input contains one empty list. The graph consists of only one node with val = 1 and it does not have any neighbors. +``` + +### Example 3: + +``` +Input: adjList = [] +Output: [] +Explanation: This an empty graph, it does not have any nodes. +``` + +## Constraints + +- The number of nodes in the graph is in the range `[0, 100]`. +- `1 <= Node.val <= 100` +- `Node.val` is unique for each node. +- There are no repeated edges and no self-loops in the graph. +- The Graph is connected and all nodes can be visited starting from the given node. diff --git a/leetcode/clone_graph/__init__.py b/leetcode/clone_graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/clone_graph/playground.ipynb b/leetcode/clone_graph/playground.ipynb new file mode 100644 index 0000000..0be0e9e --- /dev/null +++ b/leetcode/clone_graph/playground.ipynb @@ -0,0 +1,209 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import GraphNode" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "1--2\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1--4\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "2--3\n", + "\n", + "\n", + "\n", + "\n", + "3--4\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "GraphNode({1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]})" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Example test case\n", + "adj_list = [[2, 4], [1, 3], [2, 4], [1, 3]]\n", + "node = GraphNode.from_adjacency_list(adj_list)\n", + "node" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "1--2\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "1--4\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "3\n", + "\n", + "\n", + "\n", + "2--3\n", + "\n", + "\n", + "\n", + "\n", + "3--4\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "GraphNode({1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]})" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().clone_graph(node)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result.is_clone(node) if result else node is None" + ] + } + ], + "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/clone_graph/solution.py b/leetcode/clone_graph/solution.py new file mode 100644 index 0000000..bd53c0b --- /dev/null +++ b/leetcode/clone_graph/solution.py @@ -0,0 +1,73 @@ +from collections import deque + +from leetcode_py import GraphNode + + +class Solution: + # Time: O(V + E) + # Space: O(V) + def clone_graph(self, node: GraphNode | None) -> GraphNode | None: + if node is None: + return None + + def dfs(node: GraphNode, visited: dict[int, GraphNode]): + if node.val in visited: + return visited[node.val] + + clone = GraphNode(node.val) + visited[node.val] = clone + + for neighbor in node.neighbors: + clone.neighbors.append(dfs(neighbor, visited)) + + return clone + + return dfs(node, visited={}) + + +class SolutionDFS: + # DFS Iterative + # Time: O(V + E) + # Space: O(V) + def clone_graph(self, node: GraphNode | None) -> GraphNode | None: + if node is None: + return None + + stack = [node] + visited = {node.val: GraphNode(node.val)} + + while stack: + current = stack.pop() + clone = visited[current.val] + + for neighbor in current.neighbors: + if neighbor.val not in visited: + visited[neighbor.val] = GraphNode(neighbor.val) + stack.append(neighbor) + clone.neighbors.append(visited[neighbor.val]) + + return visited[node.val] + + +class SolutionBFS: + # BFS + # Time: O(V + E) + # Space: O(V) + def clone_graph(self, node: GraphNode | None) -> GraphNode | None: + if node is None: + return None + + queue = deque([node]) + visited = {node.val: GraphNode(node.val)} + + while queue: + current = queue.popleft() + clone = visited[current.val] + + for neighbor in current.neighbors: + if neighbor.val not in visited: + visited[neighbor.val] = GraphNode(neighbor.val) + queue.append(neighbor) + clone.neighbors.append(visited[neighbor.val]) + + return visited[node.val] diff --git a/leetcode/clone_graph/tests.py b/leetcode/clone_graph/tests.py new file mode 100644 index 0000000..fa0a211 --- /dev/null +++ b/leetcode/clone_graph/tests.py @@ -0,0 +1,25 @@ +import pytest + +from leetcode_py import GraphNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution, SolutionBFS, SolutionDFS + + +class TestCloneGraph: + @pytest.mark.parametrize("solution_class", [Solution, SolutionDFS, SolutionBFS]) + @pytest.mark.parametrize( + "adj_list", + [[[2, 4], [1, 3], [2, 4], [1, 3]], [[]], []], + ) + @logged_test + def test_clone_graph( + self, + adj_list: list[list[int]], + solution_class: type[Solution | SolutionDFS | SolutionBFS], + ): + solution = solution_class() + node = GraphNode.from_adjacency_list(adj_list) + result = solution.clone_graph(node) + + assert result.is_clone(node) if result else node is None diff --git a/leetcode/container_with_most_water/playground.ipynb b/leetcode/container_with_most_water/playground.ipynb index 48e3823..539853a 100644 --- a/leetcode/container_with_most_water/playground.ipynb +++ b/leetcode/container_with_most_water/playground.ipynb @@ -29,7 +29,8 @@ "metadata": {}, "outputs": [], "source": [ - "result = Solution().max_area(height)\nresult" + "result = Solution().max_area(height)\n", + "result" ] }, { diff --git a/leetcode/evaluate_reverse_polish_notation/README.md b/leetcode/evaluate_reverse_polish_notation/README.md new file mode 100644 index 0000000..4015f66 --- /dev/null +++ b/leetcode/evaluate_reverse_polish_notation/README.md @@ -0,0 +1,56 @@ +# Evaluate Reverse Polish Notation + +**Difficulty:** Medium +**Topics:** Array, Math, Stack +**Tags:** grind-75 + +**LeetCode:** [Problem 150](https://leetcode.com/problems/evaluate-reverse-polish-notation/description/) + +## Problem Description + +You are given an array of strings `tokens` that represents an arithmetic expression in a **Reverse Polish Notation**. + +Evaluate the expression. Return _an integer that represents the value of the expression_. + +**Note that:** + +- The valid operators are `'+'`, `'-'`, `'*'`, and `'/'`. +- Each operand may be an integer or another expression. +- The division between two integers always **truncates toward zero**. +- There will not be any division by zero. +- The input represents a valid arithmetic expression in a reverse polish notation. +- The answer and all the intermediate calculations can be represented in a **32-bit** integer. + +## Examples + +### Example 1: + +``` +Input: tokens = ["2","1","+","3","*"] +Output: 9 +``` + +**Explanation:** ((2 + 1) \* 3) = 9 + +### Example 2: + +``` +Input: tokens = ["4","13","5","/","+"] +Output: 6 +``` + +**Explanation:** (4 + (13 / 5)) = 6 + +### Example 3: + +``` +Input: tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] +Output: 22 +``` + +**Explanation:** ((10 _ (6 / ((9 + 3) _ -11))) + 17) + 5 = 22 + +## Constraints + +- `1 <= tokens.length <= 10^4` +- `tokens[i]` is either an operator: `"+"`, `"-"`, `"*"`, or `"/"`, or an integer in the range `[-200, 200]`. diff --git a/leetcode/evaluate_reverse_polish_notation/__init__.py b/leetcode/evaluate_reverse_polish_notation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/evaluate_reverse_polish_notation/playground.ipynb b/leetcode/evaluate_reverse_polish_notation/playground.ipynb new file mode 100644 index 0000000..fc7ba18 --- /dev/null +++ b/leetcode/evaluate_reverse_polish_notation/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", + "tokens = [\"2\", \"1\", \"+\", \"3\", \"*\"]\n", + "expected = 9" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().eval_rpn(tokens)\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/evaluate_reverse_polish_notation/solution.py b/leetcode/evaluate_reverse_polish_notation/solution.py new file mode 100644 index 0000000..b3b2ea1 --- /dev/null +++ b/leetcode/evaluate_reverse_polish_notation/solution.py @@ -0,0 +1,20 @@ +class Solution: + # Time: O(n) + # Space: O(n) + def eval_rpn(self, tokens: list[str]) -> int: + stack: list[int] = [] + ops = { + "+": lambda a, b: a + b, + "-": lambda a, b: a - b, + "*": lambda a, b: a * b, + "/": lambda a, b: int(a / b), + } + + for token in tokens: + if token in ops: + b, a = stack.pop(), stack.pop() + stack.append(ops[token](a, b)) + else: + stack.append(int(token)) + + return stack[0] diff --git a/leetcode/evaluate_reverse_polish_notation/tests.py b/leetcode/evaluate_reverse_polish_notation/tests.py new file mode 100644 index 0000000..82c126b --- /dev/null +++ b/leetcode/evaluate_reverse_polish_notation/tests.py @@ -0,0 +1,23 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestEvaluateReversePolishNotation: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "tokens, expected", + [ + (["2", "1", "+", "3", "*"], 9), + (["4", "13", "5", "/", "+"], 6), + (["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"], 22), + ], + ) + @logged_test + def test_eval_rpn(self, tokens: list[str], expected: int): + result = self.solution.eval_rpn(tokens) + assert result == expected diff --git a/leetcode/insert_interval/playground.ipynb b/leetcode/insert_interval/playground.ipynb index 783109a..921d04b 100644 --- a/leetcode/insert_interval/playground.ipynb +++ b/leetcode/insert_interval/playground.ipynb @@ -30,7 +30,8 @@ "metadata": {}, "outputs": [], "source": [ - "result = Solution().insert(intervals, new_interval)\nresult" + "result = Solution().insert(intervals, new_interval)\n", + "result" ] }, { diff --git a/leetcode/invert_binary_tree/playground.ipynb b/leetcode/invert_binary_tree/playground.ipynb index 452ecf4..c4a7c4c 100644 --- a/leetcode/invert_binary_tree/playground.ipynb +++ b/leetcode/invert_binary_tree/playground.ipynb @@ -6,7 +6,11 @@ "id": "imports", "metadata": {}, "outputs": [], - "source": ["from solution import Solution\n\nfrom leetcode_py import TreeNode"] + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import TreeNode" + ] }, { "cell_type": "code", @@ -14,7 +18,12 @@ "id": "setup", "metadata": {}, "outputs": [], - "source": ["# Example test case\nroot_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\nroot = TreeNode[int].from_list(root_list)\nexpected = TreeNode[int].from_list([4, 7, 2, 9, 6, 3, 1])"] + "source": [ + "# Example test case\n", + "root_list: list[int | None] = [4, 2, 7, 1, 3, 6, 9]\n", + "root = TreeNode[int].from_list(root_list)\n", + "expected = TreeNode[int].from_list([4, 7, 2, 9, 6, 3, 1])" + ] }, { "cell_type": "code", @@ -22,7 +31,10 @@ "id": "execute", "metadata": {}, "outputs": [], - "source": ["result = Solution().invert_tree(root)\nresult"] + "source": [ + "result = Solution().invert_tree(root)\n", + "result" + ] }, { "cell_type": "code", @@ -30,7 +42,9 @@ "id": "test", "metadata": {}, "outputs": [], - "source": ["assert result == expected"] + "source": [ + "assert result == expected" + ] } ], "metadata": { diff --git a/leetcode/minimum_height_trees/README.md b/leetcode/minimum_height_trees/README.md new file mode 100644 index 0000000..15ad725 --- /dev/null +++ b/leetcode/minimum_height_trees/README.md @@ -0,0 +1,47 @@ +# Minimum Height Trees + +**Difficulty:** Medium +**Topics:** Depth-First Search, Breadth-First Search, Graph, Topological Sort +**Tags:** grind-75 + +**LeetCode:** [Problem 310](https://leetcode.com/problems/minimum-height-trees/description/) + +## Problem Description + +A tree is an undirected graph in which any two vertices are connected by _exactly_ one path. In other words, any connected graph without simple cycles is a tree. + +Given a tree of `n` nodes labelled from `0` to `n - 1`, and an array of `n - 1` `edges` where `edges[i] = [ai, bi]` indicates that there is an undirected edge between the two nodes `ai` and `bi` in the tree, you can choose any node of the tree as the root. When you select a node `x` as the root, the result tree has height `h`. Among all possible rooted trees, those with minimum height (i.e. `min(h)`) are called **minimum height trees** (MHTs). + +Return _a list of all **MHTs'** root labels_. You can return the answer in **any order**. + +The **height** of a rooted tree is the number of edges on the longest downward path between the root and a leaf. + +## Examples + +### Example 1: + + + +``` +Input: n = 4, edges = [[1,0],[1,2],[1,3]] +Output: [1] +Explanation: As shown, the height of the tree is 1 when the root is the node with label 1 which is the only MHT. +``` + +### Example 2: + + + +``` +Input: n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] +Output: [3,4] +``` + +## Constraints + +- `1 <= n <= 2 * 10^4` +- `edges.length == n - 1` +- `0 <= ai, bi < n` +- `ai != bi` +- All the pairs `(ai, bi)` are distinct. +- The given input is **guaranteed** to be a tree and there will be **no repeated** edges. diff --git a/leetcode/minimum_height_trees/__init__.py b/leetcode/minimum_height_trees/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/minimum_height_trees/playground.ipynb b/leetcode/minimum_height_trees/playground.ipynb new file mode 100644 index 0000000..51e0605 --- /dev/null +++ b/leetcode/minimum_height_trees/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", + "n = 4\n", + "edges = [[1, 0], [1, 2], [1, 3]]\n", + "expected = [1]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().find_min_height_trees(n, edges)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert sorted(result) == sorted(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/minimum_height_trees/solution.py b/leetcode/minimum_height_trees/solution.py new file mode 100644 index 0000000..5b9a6d2 --- /dev/null +++ b/leetcode/minimum_height_trees/solution.py @@ -0,0 +1,29 @@ +from collections import defaultdict, deque + + +class Solution: + # Time: O(V) + # Space: O(V) + def find_min_height_trees(self, n: int, edges: list[list[int]]) -> list[int]: + if n == 1: + return [0] + + graph = defaultdict(set) + for u, v in edges: + graph[u].add(v) + graph[v].add(u) + + leaves = deque([i for i in range(n) if len(graph[i]) == 1]) + + remaining = n + while remaining > 2: + size = len(leaves) + remaining -= size + for _ in range(size): + leaf = leaves.popleft() + neighbor = graph[leaf].pop() + graph[neighbor].remove(leaf) + if len(graph[neighbor]) == 1: + leaves.append(neighbor) + + return list(leaves) diff --git a/leetcode/minimum_height_trees/tests.py b/leetcode/minimum_height_trees/tests.py new file mode 100644 index 0000000..11fd934 --- /dev/null +++ b/leetcode/minimum_height_trees/tests.py @@ -0,0 +1,23 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMinimumHeightTrees: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "n, edges, expected", + [ + (4, [[1, 0], [1, 2], [1, 3]], [1]), + (6, [[3, 0], [3, 1], [3, 2], [3, 4], [5, 4]], [3, 4]), + (1, [], [0]), + ], + ) + @logged_test + def test_find_min_height_trees(self, n: int, edges: list[list[int]], expected: list[int]): + result = self.solution.find_min_height_trees(n, edges) + assert sorted(result) == sorted(expected) diff --git a/leetcode/reverse_linked_list_ii/playground.ipynb b/leetcode/reverse_linked_list_ii/playground.ipynb index 8fdc5e9..bfdc6d9 100644 --- a/leetcode/reverse_linked_list_ii/playground.ipynb +++ b/leetcode/reverse_linked_list_ii/playground.ipynb @@ -6,7 +6,11 @@ "id": "imports", "metadata": {}, "outputs": [], - "source": ["from solution import Solution\n\nfrom leetcode_py import ListNode"] + "source": [ + "from solution import Solution\n", + "\n", + "from leetcode_py import ListNode" + ] }, { "cell_type": "code", @@ -14,7 +18,13 @@ "id": "setup", "metadata": {}, "outputs": [], - "source": ["# Example test case\nhead_list = [1, 2, 3, 4, 5]\nhead = ListNode[int].from_list(head_list)\nleft, right = 2, 4\nexpected = ListNode[int].from_list([1, 4, 3, 2, 5])"] + "source": [ + "# Example test case\n", + "head_list = [1, 2, 3, 4, 5]\n", + "head = ListNode[int].from_list(head_list)\n", + "left, right = 2, 4\n", + "expected = ListNode[int].from_list([1, 4, 3, 2, 5])" + ] }, { "cell_type": "code", @@ -22,7 +32,10 @@ "id": "execute", "metadata": {}, "outputs": [], - "source": ["result = Solution().reverse_between(head, left, right)\nresult"] + "source": [ + "result = Solution().reverse_between(head, left, right)\n", + "result" + ] }, { "cell_type": "code", @@ -30,7 +43,9 @@ "id": "test", "metadata": {}, "outputs": [], - "source": ["assert result == expected"] + "source": [ + "assert result == expected" + ] } ], "metadata": { diff --git a/leetcode/spiral_matrix/playground.ipynb b/leetcode/spiral_matrix/playground.ipynb index 318e556..c219f95 100644 --- a/leetcode/spiral_matrix/playground.ipynb +++ b/leetcode/spiral_matrix/playground.ipynb @@ -29,7 +29,8 @@ "metadata": {}, "outputs": [], "source": [ - "result = Solution().spiral_order(matrix)\nresult" + "result = Solution().spiral_order(matrix)\n", + "result" ] }, { diff --git a/leetcode/task_scheduler/README.md b/leetcode/task_scheduler/README.md new file mode 100644 index 0000000..be7b1aa --- /dev/null +++ b/leetcode/task_scheduler/README.md @@ -0,0 +1,54 @@ +# Task Scheduler + +**Difficulty:** Medium +**Topics:** Array, Hash Table, Greedy, Sorting, Heap (Priority Queue), Counting +**Tags:** grind-75 + +**LeetCode:** [Problem 621](https://leetcode.com/problems/task-scheduler/description/) + +## Problem Description + +You are given an array of CPU `tasks`, each labeled with a letter from A to Z, and a number `n`. Each CPU interval can be idle or allow the completion of one task. Tasks can be completed in any order, but there's a constraint: there has to be a gap of **at least** `n` intervals between two tasks with the same label. + +Return the **minimum** number of CPU intervals required to complete all tasks. + +## Examples + +### Example 1: + +``` +Input: tasks = ["A","A","A","B","B","B"], n = 2 +Output: 8 +``` + +**Explanation:** A possible sequence is: A -> B -> idle -> A -> B -> idle -> A -> B. + +After completing task A, you must wait two intervals before doing A again. The same applies to task B. In the 3rd interval, neither A nor B can be done, so you idle. By the 4th interval, you can do A again as 2 intervals have passed. + +### Example 2: + +``` +Input: tasks = ["A","C","A","B","D","B"], n = 1 +Output: 6 +``` + +**Explanation:** A possible sequence is: A -> B -> C -> D -> A -> B. + +With a cooling interval of 1, you can repeat a task after just one other task. + +### Example 3: + +``` +Input: tasks = ["A","A","A", "B","B","B"], n = 3 +Output: 10 +``` + +**Explanation:** A possible sequence is: A -> B -> idle -> idle -> A -> B -> idle -> idle -> A -> B. + +There are only two types of tasks, A and B, which need to be separated by 3 intervals. This leads to idling twice between repetitions of these tasks. + +## Constraints + +- `1 <= tasks.length <= 10^4` +- `tasks[i]` is an uppercase English letter. +- `0 <= n <= 100` diff --git a/leetcode/task_scheduler/__init__.py b/leetcode/task_scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/task_scheduler/playground.ipynb b/leetcode/task_scheduler/playground.ipynb new file mode 100644 index 0000000..7f76881 --- /dev/null +++ b/leetcode/task_scheduler/playground.ipynb @@ -0,0 +1,88 @@ +{ + "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", + "tasks = [\"A\", \"A\", \"A\", \"B\", \"B\", \"B\"]\n", + "n = 2\n", + "expected = 8" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Counter({'A': 3, 'B': 3})\n", + "[-3, -3]\n" + ] + }, + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().least_interval(tasks, 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/task_scheduler/solution.py b/leetcode/task_scheduler/solution.py new file mode 100644 index 0000000..d6a6f15 --- /dev/null +++ b/leetcode/task_scheduler/solution.py @@ -0,0 +1,65 @@ +import heapq +from collections import Counter, deque + + +class Solution: + # Time: O(T * n + m log m) where T = len(tasks), worst case with many idle periods + # Space: O(m) where m ≤ 26, so O(1) + def least_interval(self, tasks: list[str], n: int) -> int: + counts = Counter(tasks) + max_heap = [-count for count in counts.values()] + heapq.heapify(max_heap) + + step_num = 0 + queue: deque[tuple[int, int]] = deque() # (count, available_time) + + while max_heap or queue: + step_num += 1 + + while queue and queue[0][1] <= step_num: + count, _ = queue.popleft() + heapq.heappush(max_heap, count) + + if max_heap: + count = heapq.heappop(max_heap) + count += 1 # Decrease count (was negative) + if count < 0: # Still has tasks left + queue.append((count, step_num + n + 1)) + + return step_num + + +class SolutionGreedy: + # Time: O(T + m) where T = len(tasks), m = unique tasks ≤ 26, so O(T) + # Space: O(m) where m ≤ 26, so O(1) + def least_interval(self, tasks: list[str], n: int) -> int: + """ + Mathematical approach: + + Key insight: The most frequent task determines the minimum time. + + Example: tasks=["A","A","A","B","B","B"], n=2 + + 1. Find max frequency: max_freq = 3 (A and B both appear 3 times) + 2. Count tasks with max frequency: max_count = 2 (A and B) + 3. Create frame structure: + Frame: A B _ | A B _ | A B + - (max_freq - 1) complete frames of size (n + 1) + - Last frame contains only max frequency tasks + + 4. Calculate minimum intervals: + - Frame intervals: (max_freq - 1) * (n + 1) = 2 * 3 = 6 + - Plus max frequency tasks: 6 + 2 = 8 + + 5. Return max(total_tasks, calculated_min) to handle cases where + we have enough variety to fill all gaps without idle time. + """ + counts = Counter(tasks) + max_freq = max(counts.values()) + max_count = sum(1 for freq in counts.values() if freq == max_freq) + + # Minimum intervals needed based on most frequent tasks + min_intervals = (max_freq - 1) * (n + 1) + max_count + + # Return max to handle cases with sufficient task variety + return max(len(tasks), min_intervals) diff --git a/leetcode/task_scheduler/tests.py b/leetcode/task_scheduler/tests.py new file mode 100644 index 0000000..c04842f --- /dev/null +++ b/leetcode/task_scheduler/tests.py @@ -0,0 +1,30 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution, SolutionGreedy + + +class TestTaskScheduler: + @pytest.mark.parametrize("solution_class", [Solution, SolutionGreedy]) + @pytest.mark.parametrize( + "tasks, n, expected", + [ + (["A", "A", "A", "B", "B", "B"], 2, 8), + (["A", "C", "A", "B", "D", "B"], 1, 6), + (["A", "A", "A", "B", "B", "B"], 3, 10), + (["A", "A", "A", "A", "A", "A", "B", "C", "D", "E", "F", "G"], 2, 16), + (["A"], 2, 1), + ], + ) + @logged_test + def test_least_interval( + self, + tasks: list[str], + n: int, + expected: int, + solution_class: type[Solution | SolutionGreedy], + ): + solution = solution_class() + result = solution.least_interval(tasks, n) + assert result == expected diff --git a/leetcode/validate_binary_search_tree/README.md b/leetcode/validate_binary_search_tree/README.md new file mode 100644 index 0000000..4b8b99f --- /dev/null +++ b/leetcode/validate_binary_search_tree/README.md @@ -0,0 +1,44 @@ +# Validate Binary Search Tree + +**Difficulty:** Medium +**Topics:** Tree, Depth-First Search, Binary Search Tree, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 98](https://leetcode.com/problems/validate-binary-search-tree/description/) + +## Problem Description + +Given the `root` of a binary tree, determine if it is a valid binary search tree (BST). + +A **valid BST** is defined as follows: + +- The left subtree of a node contains only nodes with keys **strictly less than** the node's key. +- The right subtree of a node contains only nodes with keys **strictly greater than** the node's key. +- Both the left and right subtrees must also be binary search trees. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2020/12/01/tree1.jpg) + +``` +Input: root = [2,1,3] +Output: true +``` + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2020/12/01/tree2.jpg) + +``` +Input: root = [5,1,4,null,null,3,6] +Output: false +``` + +**Explanation:** The root node's value is 5 but its right child's value is 4. + +## Constraints + +- The number of nodes in the tree is in the range `[1, 10^4]`. +- `-2^31 <= Node.val <= 2^31 - 1` diff --git a/leetcode/validate_binary_search_tree/__init__.py b/leetcode/validate_binary_search_tree/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/validate_binary_search_tree/playground.ipynb b/leetcode/validate_binary_search_tree/playground.ipynb new file mode 100644 index 0000000..4b972d5 --- /dev/null +++ b/leetcode/validate_binary_search_tree/playground.ipynb @@ -0,0 +1,82 @@ +{ + "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] = [2, 1, 3]\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": [ + "root = TreeNode.from_list(root_list)\n", + "result = Solution().is_valid_bst(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/validate_binary_search_tree/solution.py b/leetcode/validate_binary_search_tree/solution.py new file mode 100644 index 0000000..3b4dcbf --- /dev/null +++ b/leetcode/validate_binary_search_tree/solution.py @@ -0,0 +1,60 @@ +from collections import deque + +from leetcode_py import TreeNode + + +class Solution: + @classmethod + def validate(cls, node: TreeNode[int] | None, min_val: float, max_val: float) -> bool: + if not node: + return True + if node.val <= min_val or node.val >= max_val: + return False + return cls.validate(node.left, min_val, node.val) and cls.validate(node.right, node.val, max_val) + + # Time: O(n) + # Space: O(h) + def is_valid_bst(self, root: TreeNode[int] | None) -> bool: + return self.validate(root, float("-inf"), float("inf")) + + +class SolutionDFS: + # Time: O(n) + # Space: O(h) + def is_valid_bst(self, root: TreeNode[int] | None) -> bool: + if not root: + return True + + stack = [(root, float("-inf"), float("inf"))] + + while stack: + node, min_val, max_val = stack.pop() + if node.val <= min_val or node.val >= max_val: + return False + if node.right: + stack.append((node.right, node.val, max_val)) + if node.left: + stack.append((node.left, min_val, node.val)) + + return True + + +class SolutionBFS: + # Time: O(n) + # Space: O(w) where w is max width + def is_valid_bst(self, root: TreeNode[int] | None) -> bool: + if not root: + return True + + queue = deque([(root, float("-inf"), float("inf"))]) + + while queue: + node, min_val, max_val = queue.popleft() + if node.val <= min_val or node.val >= max_val: + return False + if node.right: + queue.append((node.right, node.val, max_val)) + if node.left: + queue.append((node.left, min_val, node.val)) + + return True diff --git a/leetcode/validate_binary_search_tree/tests.py b/leetcode/validate_binary_search_tree/tests.py new file mode 100644 index 0000000..b03d47e --- /dev/null +++ b/leetcode/validate_binary_search_tree/tests.py @@ -0,0 +1,30 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution, SolutionBFS, SolutionDFS + + +class TestValidateBinarySearchTree: + @pytest.mark.parametrize("solution_class", [Solution, SolutionDFS, SolutionBFS]) + @pytest.mark.parametrize( + "root_list, expected", + [ + ([2, 1, 3], True), + ([5, 1, 4, None, None, 3, 6], False), + ([1], True), + ([1, 1], False), + ], + ) + @logged_test + def test_is_valid_bst( + self, + root_list: list[int | None], + expected: bool, + solution_class: type[Solution | SolutionDFS | SolutionBFS], + ): + solution = solution_class() + root = TreeNode.from_list(root_list) + result = solution.is_valid_bst(root) + assert result == expected diff --git a/leetcode_py/__init__.py b/leetcode_py/__init__.py index d9be26e..0300fc0 100644 --- a/leetcode_py/__init__.py +++ b/leetcode_py/__init__.py @@ -1,4 +1,5 @@ +from leetcode_py.data_structures.graph_node import GraphNode from leetcode_py.data_structures.list_node import ListNode from leetcode_py.data_structures.tree_node import TreeNode -__all__ = ["ListNode", "TreeNode"] +__all__ = ["GraphNode", "ListNode", "TreeNode"] diff --git a/leetcode_py/data_structures/graph_node.py b/leetcode_py/data_structures/graph_node.py new file mode 100644 index 0000000..26e17d2 --- /dev/null +++ b/leetcode_py/data_structures/graph_node.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from pprint import pformat + +import graphviz + + +class GraphNode: + """ + Graph node for undirected graph problems. + + Each node contains a value and a list of neighbors. + """ + + def __init__(self, val: int = 0, neighbors: list[GraphNode] | None = None) -> None: + self.val = val + self.neighbors = neighbors if neighbors is not None else [] + + def _dfs_traverse(self, visit_fn) -> set[int]: + """Generic DFS traversal with custom visit function.""" + visited = set() + + def dfs(node: GraphNode) -> None: + if node.val in visited: + return + visited.add(node.val) + visit_fn(node) + for neighbor in node.neighbors: + dfs(neighbor) + + dfs(self) + return visited + + def _get_adjacency_dict(self) -> dict[int, list[int]]: + """Get adjacency dictionary representation.""" + adj_dict = {} + + def visit(node: GraphNode) -> None: + adj_dict[node.val] = sorted([n.val for n in node.neighbors]) + + self._dfs_traverse(visit) + return dict(sorted(adj_dict.items())) + + def __eq__(self, other: object) -> bool: + """Compare two graph nodes by adjacency list structure.""" + if not isinstance(other, GraphNode): + return False + return self.to_adjacency_list(self) == self.to_adjacency_list(other) + + def is_clone(self, other: GraphNode | None) -> bool: + """ + Check if other is a proper clone (same structure, different objects). + """ + if other is None: + return False + + # First check if structures are equal + if self != other: + return False + + # Then check all nodes are different objects + visited: set[int] = set() + + def dfs_check_identity(node1: GraphNode, node2: GraphNode) -> bool: + if node1.val in visited: + return True + + visited.add(node1.val) + + # Same object = not a clone + if node1 is node2: + return False + + # Check neighbors (sorted for consistency) + neighbors1 = sorted(node1.neighbors, key=lambda x: x.val) + neighbors2 = sorted(node2.neighbors, key=lambda x: x.val) + + for n1, n2 in zip(neighbors1, neighbors2): + if not dfs_check_identity(n1, n2): + return False + return True + + return dfs_check_identity(self, other) + + def __str__(self) -> str: + """Human-readable string representation using pprint.""" + return pformat(self._get_adjacency_dict(), width=40) + + def _repr_html_(self) -> str: + """HTML representation for Jupyter notebooks using Graphviz.""" + dot = graphviz.Graph(engine="neato") + dot.attr("node", shape="circle", style="filled", fillcolor="lightblue") + dot.attr("edge", color="gray") + + edges = set() + + def visit(node: GraphNode) -> None: + dot.node(str(node.val), str(node.val)) + for neighbor in node.neighbors: + edge = (min(node.val, neighbor.val), max(node.val, neighbor.val)) + if edge not in edges: + edges.add(edge) + dot.edge(str(node.val), str(neighbor.val)) + + self._dfs_traverse(visit) + return dot.pipe(format="svg", encoding="utf-8") + + def __repr__(self) -> str: + """Developer representation showing adjacency dict.""" + return f"GraphNode({self._get_adjacency_dict()})" + + @classmethod + def from_adjacency_list(cls, adj_list: list[list[int]]) -> GraphNode | None: + """ + Create a graph from adjacency list representation. + + Args: + adj_list: List where adj_list[i] contains neighbors of node (i+1) + + Returns: + First node of the graph, or None if empty + """ + if not adj_list: + return None + + # Create all nodes first + nodes: dict[int, GraphNode] = {} + for i in range(len(adj_list)): + nodes[i + 1] = cls(val=i + 1) + + # Connect neighbors + for i, neighbors in enumerate(adj_list): + node_val = i + 1 + for neighbor_val in neighbors: + if neighbor_val in nodes: + nodes[node_val].neighbors.append(nodes[neighbor_val]) + + return nodes.get(1) + + @staticmethod + def to_adjacency_list(node: GraphNode | None) -> list[list[int]]: + """ + Convert graph to adjacency list representation. + + Args: + node: Starting node of the graph + + Returns: + Adjacency list where result[i] contains neighbors of node (i+1) + """ + if node is None: + return [] + + adj_dict = node._get_adjacency_dict() + if not adj_dict: + return [] + + max_val = max(adj_dict.keys()) + return [adj_dict.get(i + 1, []) for i in range(max_val)] diff --git a/tests/data_structures/test_graph_node.py b/tests/data_structures/test_graph_node.py new file mode 100644 index 0000000..704f189 --- /dev/null +++ b/tests/data_structures/test_graph_node.py @@ -0,0 +1,161 @@ +import pytest + +from leetcode_py.data_structures.graph_node import GraphNode + + +class TestGraphNode: + @pytest.mark.parametrize( + "val, neighbors, expected_val, expected_neighbors", + [ + (None, None, 0, []), + (5, None, 5, []), + (1, [GraphNode(2)], 1, 1), # 1 neighbor count + ], + ) + def test_init(self, val, neighbors, expected_val, expected_neighbors) -> None: + if val is None: + node = GraphNode() + elif neighbors is None: + node = GraphNode(val) + else: + node = GraphNode(val, neighbors) + + assert node.val == expected_val + if isinstance(expected_neighbors, int): + assert len(node.neighbors) == expected_neighbors + else: + assert node.neighbors == expected_neighbors + + def test_repr(self) -> None: + node1 = GraphNode(1) + node2 = GraphNode(2) + node1.neighbors = [node2] + node2.neighbors = [node1] + assert repr(node1) == "GraphNode({1: [2], 2: [1]})" + + def test_str(self) -> None: + node1 = GraphNode(1) + node2 = GraphNode(2) + node1.neighbors = [node2] + node2.neighbors = [node1] + expected = "{1: [2], 2: [1]}" + assert str(node1) == expected + + @pytest.mark.parametrize( + "val1, val2, expected", + [ + (1, 1, True), + (1, 2, False), + ], + ) + def test_eq_single_nodes(self, val1, val2, expected) -> None: + node1 = GraphNode(val1) + node2 = GraphNode(val2) + assert (node1 == node2) == expected + + @pytest.mark.parametrize("other", ["not a node", 1, None]) + def test_eq_with_non_graph_node(self, other) -> None: + node = GraphNode(1) + assert node != other + + def test_eq_simple_graph(self) -> None: + # Graph 1: 1-2 + node1_a = GraphNode(1) + node2_a = GraphNode(2) + node1_a.neighbors = [node2_a] + node2_a.neighbors = [node1_a] + + # Graph 2: 1-2 + node1_b = GraphNode(1) + node2_b = GraphNode(2) + node1_b.neighbors = [node2_b] + node2_b.neighbors = [node1_b] + + assert node1_a == node1_b + + def test_eq_different_neighbor_count(self) -> None: + node1_a = GraphNode(1) + node2_a = GraphNode(2) + node1_a.neighbors = [node2_a] + + node1_b = GraphNode(1) + node2_b = GraphNode(2) + node3_b = GraphNode(3) + node1_b.neighbors = [node2_b, node3_b] + + assert node1_a != node1_b + + @pytest.mark.parametrize( + "adj_list, expected_val, expected_neighbors", + [ + ([], None, None), + ([[]], 1, []), + ([[2], [1]], 1, [2]), + ([[2, 4], [1, 3], [2, 4], [1, 3]], 1, [2, 4]), + ], + ) + def test_from_adjacency_list(self, adj_list, expected_val, expected_neighbors) -> None: + result = GraphNode.from_adjacency_list(adj_list) + + if expected_val is None: + assert result is None + else: + assert result is not None + assert result.val == expected_val + if expected_neighbors == []: + assert result.neighbors == [] + else: + neighbor_vals = sorted([n.val for n in result.neighbors]) + assert neighbor_vals == expected_neighbors + + @pytest.mark.parametrize( + "node, expected", + [ + (None, []), + (GraphNode(1), [[]]), + ], + ) + def test_to_adjacency_list(self, node, expected) -> None: + result = GraphNode.to_adjacency_list(node) + assert result == expected + + def test_to_adjacency_list_two_nodes(self) -> None: + node1 = GraphNode(1) + node2 = GraphNode(2) + node1.neighbors = [node2] + node2.neighbors = [node1] + + result = GraphNode.to_adjacency_list(node1) + assert result == [[2], [1]] + + @pytest.mark.parametrize( + "original", + [ + [], + [[]], + [[2], [1]], + [[2, 4], [1, 3], [2, 4], [1, 3]], + ], + ) + def test_roundtrip_conversion(self, original) -> None: + graph = GraphNode.from_adjacency_list(original) + result = GraphNode.to_adjacency_list(graph) + assert result == original + + def test_cycle_handling(self) -> None: + # Create a cycle: 1-2-3-1 + node1 = GraphNode(1) + node2 = GraphNode(2) + node3 = GraphNode(3) + + node1.neighbors = [node2] + node2.neighbors = [node1, node3] + node3.neighbors = [node2, node1] + + # Should not infinite loop + adj_list = GraphNode.to_adjacency_list(node1) + assert len(adj_list) == 3 + + # Recreate and compare + recreated = GraphNode.from_adjacency_list(adj_list) + assert node1 == recreated From 3e18fdbda56b7ad34471e48961e87d6174e3043b Mon Sep 17 00:00:00 2001 From: Wisaroot <66859294+wisarootl@users.noreply.github.com> Date: Wed, 3 Sep 2025 07:49:44 +0700 Subject: [PATCH 03/68] feat: add more problems (#19) --- .amazonq/rules/problem-creation.md | 18 +- .templates/leetcode/json/binary_search.json | 47 +++++ .../json/k_closest_points_to_origin.json | 47 +++++ .../json/kth_smallest_element_in_a_bst.json | 47 +++++ .../leetcode/json/linked_list_cycle.json | 55 ++++++ ...mmon_ancestor_of_a_binary_search_tree.json | 53 ++++++ .../leetcode/json/maximum_subarray.json | 50 ++++++ .../json/minimum_window_substring.json | 50 ++++++ .../leetcode/json/reverse_linked_list_ii.json | 2 +- .../json/search_in_rotated_sorted_array.json | 44 +++++ .../leetcode/json/valid_palindrome.json | 50 ++++++ Makefile | 2 +- leetcode/binary_search/README.md | 40 +++++ leetcode/binary_search/__init__.py | 0 leetcode/binary_search/playground.ipynb | 69 ++++++++ leetcode/binary_search/solution.py | 17 ++ leetcode/binary_search/tests.py | 39 +++++ leetcode/container_with_most_water/tests.py | 24 ++- .../evaluate_reverse_polish_notation/tests.py | 15 ++ leetcode/insert_interval/tests.py | 17 ++ leetcode/k_closest_points_to_origin/README.md | 42 +++++ .../k_closest_points_to_origin/__init__.py | 0 .../playground.ipynb | 57 +++++++ .../k_closest_points_to_origin/solution.py | 16 ++ leetcode/k_closest_points_to_origin/tests.py | 38 +++++ .../kth_smallest_element_in_a_bst/README.md | 39 +++++ .../kth_smallest_element_in_a_bst/__init__.py | 0 .../playground.ipynb | 160 ++++++++++++++++++ .../kth_smallest_element_in_a_bst/solution.py | 41 +++++ .../kth_smallest_element_in_a_bst/tests.py | 21 +++ leetcode/linked_list_cycle/README.md | 58 +++++++ leetcode/linked_list_cycle/__init__.py | 0 leetcode/linked_list_cycle/playground.ipynb | 87 ++++++++++ leetcode/linked_list_cycle/solution.py | 18 ++ leetcode/linked_list_cycle/tests.py | 59 +++++++ .../README.md | 52 ++++++ .../__init__.py | 0 .../playground.ipynb | 75 ++++++++ .../solution.py | 20 +++ .../tests.py | 45 +++++ leetcode/maximum_subarray/README.md | 47 +++++ leetcode/maximum_subarray/__init__.py | 0 leetcode/maximum_subarray/playground.ipynb | 57 +++++++ leetcode/maximum_subarray/solution.py | 11 ++ leetcode/maximum_subarray/tests.py | 27 +++ leetcode/minimum_window_substring/README.md | 51 ++++++ leetcode/minimum_window_substring/__init__.py | 0 .../minimum_window_substring/playground.ipynb | 80 +++++++++ leetcode/minimum_window_substring/solution.py | 48 ++++++ leetcode/minimum_window_substring/tests.py | 39 +++++ leetcode/reverse_linked_list_ii/README.md | 2 +- .../search_in_rotated_sorted_array/README.md | 48 ++++++ .../__init__.py | 0 .../playground.ipynb | 80 +++++++++ .../solution.py | 26 +++ .../search_in_rotated_sorted_array/tests.py | 41 +++++ leetcode/spiral_matrix/tests.py | 18 ++ leetcode/valid_palindrome/README.md | 47 +++++ leetcode/valid_palindrome/__init__.py | 0 leetcode/valid_palindrome/playground.ipynb | 57 +++++++ leetcode/valid_palindrome/solution.py | 19 +++ leetcode/valid_palindrome/tests.py | 28 +++ leetcode_py/data_structures/list_node.py | 97 ++++++++++- tests/data_structures/test_list_node.py | 47 ++++- 64 files changed, 2368 insertions(+), 16 deletions(-) create mode 100644 .templates/leetcode/json/binary_search.json create mode 100644 .templates/leetcode/json/k_closest_points_to_origin.json create mode 100644 .templates/leetcode/json/kth_smallest_element_in_a_bst.json create mode 100644 .templates/leetcode/json/linked_list_cycle.json create mode 100644 .templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json create mode 100644 .templates/leetcode/json/maximum_subarray.json create mode 100644 .templates/leetcode/json/minimum_window_substring.json create mode 100644 .templates/leetcode/json/search_in_rotated_sorted_array.json create mode 100644 .templates/leetcode/json/valid_palindrome.json create mode 100644 leetcode/binary_search/README.md create mode 100644 leetcode/binary_search/__init__.py create mode 100644 leetcode/binary_search/playground.ipynb create mode 100644 leetcode/binary_search/solution.py create mode 100644 leetcode/binary_search/tests.py create mode 100644 leetcode/k_closest_points_to_origin/README.md create mode 100644 leetcode/k_closest_points_to_origin/__init__.py create mode 100644 leetcode/k_closest_points_to_origin/playground.ipynb create mode 100644 leetcode/k_closest_points_to_origin/solution.py create mode 100644 leetcode/k_closest_points_to_origin/tests.py create mode 100644 leetcode/kth_smallest_element_in_a_bst/README.md create mode 100644 leetcode/kth_smallest_element_in_a_bst/__init__.py create mode 100644 leetcode/kth_smallest_element_in_a_bst/playground.ipynb create mode 100644 leetcode/kth_smallest_element_in_a_bst/solution.py create mode 100644 leetcode/kth_smallest_element_in_a_bst/tests.py create mode 100644 leetcode/linked_list_cycle/README.md create mode 100644 leetcode/linked_list_cycle/__init__.py create mode 100644 leetcode/linked_list_cycle/playground.ipynb create mode 100644 leetcode/linked_list_cycle/solution.py create mode 100644 leetcode/linked_list_cycle/tests.py create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_search_tree/README.md create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_search_tree/__init__.py create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_search_tree/solution.py create mode 100644 leetcode/lowest_common_ancestor_of_a_binary_search_tree/tests.py create mode 100644 leetcode/maximum_subarray/README.md create mode 100644 leetcode/maximum_subarray/__init__.py create mode 100644 leetcode/maximum_subarray/playground.ipynb create mode 100644 leetcode/maximum_subarray/solution.py create mode 100644 leetcode/maximum_subarray/tests.py create mode 100644 leetcode/minimum_window_substring/README.md create mode 100644 leetcode/minimum_window_substring/__init__.py create mode 100644 leetcode/minimum_window_substring/playground.ipynb create mode 100644 leetcode/minimum_window_substring/solution.py create mode 100644 leetcode/minimum_window_substring/tests.py create mode 100644 leetcode/search_in_rotated_sorted_array/README.md create mode 100644 leetcode/search_in_rotated_sorted_array/__init__.py create mode 100644 leetcode/search_in_rotated_sorted_array/playground.ipynb create mode 100644 leetcode/search_in_rotated_sorted_array/solution.py create mode 100644 leetcode/search_in_rotated_sorted_array/tests.py create mode 100644 leetcode/valid_palindrome/README.md create mode 100644 leetcode/valid_palindrome/__init__.py create mode 100644 leetcode/valid_palindrome/playground.ipynb create mode 100644 leetcode/valid_palindrome/solution.py create mode 100644 leetcode/valid_palindrome/tests.py diff --git a/.amazonq/rules/problem-creation.md b/.amazonq/rules/problem-creation.md index d67a88a..2fc2a30 100644 --- a/.amazonq/rules/problem-creation.md +++ b/.amazonq/rules/problem-creation.md @@ -6,11 +6,17 @@ When user requests a problem by **number** or **name/slug**, the assistant will: 1. **Scrape** problem data using `.templates/leetcode/scrape.py` 2. **Transform** data into proper JSON template format -3. **Create** JSON file in `.templates/leetcode/json/{problem_name}.json` -4. **Update** Makefile with `PROBLEM ?= {problem_name}` -5. **Generate** problem structure using `make p-gen` -6. **Verify** with `make lint` - fix template issues in JSON if possible, or manually fix generated files if template limitations -7. **Iterate** if JSON fixes: re-run `make p-gen PROBLEM={problem_name} FORCE=1` and `make lint` until passes to ensure reproducibility +3. **CRITICAL: Include images** - Extract image URLs from scraped data and add to readme_examples with format: `![Example N](image_url)\n\n` before code blocks + - Check scraped data for image URLs in the `raw_content` field + - Look for patterns: `https://assets.leetcode.com/uploads/...` or `` + - Common patterns: `kthtree1.jpg`, `kthtree2.jpg`, `clone_graph.png`, `container.jpg` + - Images provide crucial visual context, especially for tree and graph problems + - Always verify images are included in `readme_examples` and accessible +4. **Create** JSON file in `.templates/leetcode/json/{problem_name}.json` +5. **Update** Makefile with `PROBLEM ?= {problem_name}` +6. **Generate** problem structure using `make p-gen` +7. **Verify** with `make lint` - fix template issues in JSON if possible, or manually fix generated files if template limitations +8. **Iterate** if JSON fixes: re-run `make p-gen PROBLEM={problem_name} FORCE=1` and `make lint` until passes to ensure reproducibility ## Scraping Commands @@ -46,7 +52,7 @@ Required fields for `.templates/leetcode/json/{problem_name}.json`: "readme_description": "Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`.", "readme_examples": [ { - "content": "```\nInput: nums = [2,7,11,15], target = 9\nOutput: [0,1]\n```\n**Explanation:** Because nums[0] + nums[1] == 9, we return [0, 1]." + "content": "![Example 1](https://example.com/image1.jpg)\n\n```\nInput: nums = [2,7,11,15], target = 9\nOutput: [0,1]\n```\n**Explanation:** Because nums[0] + nums[1] == 9, we return [0, 1]." } ], "readme_constraints": "- 2 <= nums.length <= 10^4\n- -10^9 <= nums[i] <= 10^9\n- -10^9 <= target <= 10^9\n- Only one valid answer exists.", diff --git a/.templates/leetcode/json/binary_search.json b/.templates/leetcode/json/binary_search.json new file mode 100644 index 0000000..ed3b7c4 --- /dev/null +++ b/.templates/leetcode/json/binary_search.json @@ -0,0 +1,47 @@ +{ + "problem_name": "binary_search", + "solution_class_name": "Solution", + "problem_number": "704", + "problem_title": "Binary Search", + "difficulty": "Easy", + "topics": "Array, Binary Search", + "tags": ["grind-75"], + "readme_description": "Given an array of integers `nums` which is sorted in ascending order, and an integer `target`, write a function to search `target` in `nums`. If `target` exists, then return its index. Otherwise, return `-1`.\n\nYou must write an algorithm with `O(log n)` runtime complexity.", + "readme_examples": [ + { + "content": "```\nInput: nums = [-1,0,3,5,9,12], target = 9\nOutput: 4\n```\n**Explanation:** 9 exists in nums and its index is 4" + }, + { + "content": "```\nInput: nums = [-1,0,3,5,9,12], target = 2\nOutput: -1\n```\n**Explanation:** 2 does not exist in nums so return -1" + } + ], + "readme_constraints": "- `1 <= nums.length <= 10^4`\n- `-10^4 < nums[i], target < 10^4`\n- All the integers in `nums` are **unique**.\n- `nums` is sorted in ascending order.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "search", + "parameters": "nums: list[int], target: 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": "BinarySearch", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_search", + "parametrize": "nums, target, expected", + "parametrize_typed": "nums: list[int], target: int, expected: int", + "test_cases": "[([\u22121, 0, 3, 5, 9, 12], 9, 4), ([\u22121, 0, 3, 5, 9, 12], 2, \u22121), ([5], 5, 0), ([5], \u22125, \u22121), ([1, 3, 5, 7, 9], 1, 0), ([1, 3, 5, 7, 9], 9, 4), ([1, 3, 5, 7, 9], 4, \u22121)]", + "body": "result = self.solution.search(nums, target)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [-1, 0, 3, 5, 9, 12]\ntarget = 9\nexpected = 4", + "playground_execution": "result = Solution().search(nums, target)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/k_closest_points_to_origin.json b/.templates/leetcode/json/k_closest_points_to_origin.json new file mode 100644 index 0000000..25e7cfa --- /dev/null +++ b/.templates/leetcode/json/k_closest_points_to_origin.json @@ -0,0 +1,47 @@ +{ + "problem_name": "k_closest_points_to_origin", + "solution_class_name": "Solution", + "problem_number": "973", + "problem_title": "K Closest Points to Origin", + "difficulty": "Medium", + "topics": "Array, Math, Divide and Conquer, Geometry, Sorting, Heap (Priority Queue), Quickselect", + "tags": ["grind-75"], + "readme_description": "Given an array of `points` where `points[i] = [xi, yi]` represents a point on the **X-Y** plane and an integer `k`, return the `k` closest points to the origin `(0, 0)`.\n\nThe distance between two points on the **X-Y** plane is the Euclidean distance (i.e., `\u221a(x1 - x2)\u00b2 + (y1 - y2)\u00b2`).\n\nYou may return the answer in **any order**. The answer is **guaranteed** to be **unique** (except for the order that it is in).", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/03/03/closestplane1.jpg)\n\n```\nInput: points = [[1,3],[-2,2]], k = 1\nOutput: [[-2,2]]\n```\n**Explanation:** The distance between (1, 3) and the origin is sqrt(10). The distance between (-2, 2) and the origin is sqrt(8). Since sqrt(8) < sqrt(10), (-2, 2) is closer to the origin. We only want the closest k = 1 points from the origin, so the answer is just [[-2,2]]." + }, + { + "content": "```\nInput: points = [[3,3],[5,-1],[-2,4]], k = 2\nOutput: [[3,3],[-2,4]]\n```\n**Explanation:** The answer [[-2,4],[3,3]] would also be accepted." + } + ], + "readme_constraints": "- `1 <= k <= points.length <= 10^4`\n- `-10^4 <= xi, yi <= 10^4`", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "k_closest", + "parameters": "points: list[list[int]], k: 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": "KClosestPointsToOrigin", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_k_closest", + "parametrize": "points, k, expected", + "parametrize_typed": "points: list[list[int]], k: int, expected: list[list[int]]", + "test_cases": "[([[1, 3], [-2, 2]], 1, [[-2, 2]]), ([[3, 3], [5, -1], [-2, 4]], 2, [[3, 3], [-2, 4]]), ([[0, 1], [1, 0]], 2, [[0, 1], [1, 0]]), ([[1, 1], [1, 1], [1, 1]], 2, [[1, 1], [1, 1]])]", + "body": "result = self.solution.k_closest(points, k)\n# Sort both result and expected for comparison since order doesn't matter\nresult_sorted = sorted(result)\nexpected_sorted = sorted(expected)\nassert result_sorted == expected_sorted" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\npoints = [[1, 3], [-2, 2]]\nk = 1\nexpected = [[-2, 2]]", + "playground_execution": "result = Solution().k_closest(points, k)\nresult", + "playground_assertion": "assert sorted(result) == sorted(expected)" +} diff --git a/.templates/leetcode/json/kth_smallest_element_in_a_bst.json b/.templates/leetcode/json/kth_smallest_element_in_a_bst.json new file mode 100644 index 0000000..3239379 --- /dev/null +++ b/.templates/leetcode/json/kth_smallest_element_in_a_bst.json @@ -0,0 +1,47 @@ +{ + "problem_name": "kth_smallest_element_in_a_bst", + "solution_class_name": "Solution", + "problem_number": "230", + "problem_title": "Kth Smallest Element in a BST", + "difficulty": "Medium", + "topics": "Tree, Depth-First Search, Binary Search Tree, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given the `root` of a binary search tree, and an integer `k`, return the `k`th smallest value (1-indexed) of all the values of the nodes in the tree.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2021/01/28/kthtree1.jpg)\n\n```\nInput: root = [3,1,4,null,2], k = 1\nOutput: 1\n```" + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2021/01/28/kthtree2.jpg)\n\n```\nInput: root = [5,3,6,2,4,null,null,1], k = 3\nOutput: 3\n```" + } + ], + "readme_constraints": "- The number of nodes in the tree is `n`.\n- `1 <= k <= n <= 10^4`\n- `0 <= Node.val <= 10^4`", + "readme_additional": "**Follow up:** If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize?", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "kth_smallest", + "parameters": "root: TreeNode | None, k: int", + "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": "KthSmallestElementInABst", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_kth_smallest", + "parametrize": "root_list, k, expected", + "parametrize_typed": "root_list: list[int | None], k: int, expected: int", + "test_cases": "[([3, 1, 4, None, 2], 1, 1), ([5, 3, 6, 2, 4, None, None, 1], 3, 3), ([1], 1, 1)]", + "body": "root = TreeNode.from_list(root_list)\nresult = self.solution.kth_smallest(root, k)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode", + "playground_test_case": "# Example test case\nroot_list = [3, 1, 4, None, 2]\nk = 1\nexpected = 1", + "playground_execution": "root = TreeNode.from_list(root_list)\nresult = Solution().kth_smallest(root, k)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/linked_list_cycle.json b/.templates/leetcode/json/linked_list_cycle.json new file mode 100644 index 0000000..ae5dd54 --- /dev/null +++ b/.templates/leetcode/json/linked_list_cycle.json @@ -0,0 +1,55 @@ +{ + "problem_name": "linked_list_cycle", + "solution_class_name": "Solution", + "problem_number": "141", + "problem_title": "Linked List Cycle", + "difficulty": "Easy", + "topics": "Hash Table, Linked List, Two Pointers", + "tags": ["grind-75"], + "readme_description": "Given `head`, the head of a linked list, determine if the linked list has a cycle in it.\n\nThere is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the `next` pointer. Internally, `pos` is used to denote the index of the node that tail's `next` pointer is connected to. **Note that `pos` is not passed as a parameter**.\n\nReturn `true` *if there is a cycle in the linked list*. Otherwise, return `false`.", + "readme_examples": [ + { + "content": "![Example 1](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist.png)\n\n```\nInput: head = [3,2,0,-4], pos = 1\nOutput: true\n```\n**Explanation:** There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed)." + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist_test2.png)\n\n```\nInput: head = [1,2], pos = 0\nOutput: true\n```\n**Explanation:** There is a cycle in the linked list, where the tail connects to the 0th node." + }, + { + "content": "![Example 3](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist_test3.png)\n\n```\nInput: head = [1], pos = -1\nOutput: false\n```\n**Explanation:** There is no cycle in the linked list." + } + ], + "readme_constraints": "- The number of the nodes in the list is in the range `[0, 10^4]`.\n- `-10^5 <= Node.val <= 10^5`\n- `pos` is `-1` or a **valid index** in the linked-list.", + "readme_additional": "**Follow up:** Can you solve it using `O(1)` (i.e. constant) memory?", + "solution_imports": "from leetcode_py import ListNode", + "solution_methods": [ + { + "name": "has_cycle", + "parameters": "head: ListNode[int] | None", + "return_type": "bool", + "dummy_return": "False" + } + ], + "test_imports": "import pytest\nfrom leetcode_py import ListNode\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "LinkedListCycle", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }, + { + "name": "create_cycle_list", + "parameters": "values: list[int], pos: int", + "body": "if not values:\n return None\n\nnodes = []\nhead = ListNode(values[0])\nnodes.append(head)\ncurrent = head\n\nfor i in range(1, len(values)):\n current.next = ListNode(values[i])\n current = current.next\n nodes.append(current)\n\nif pos != -1 and pos < len(nodes):\n current.next = nodes[pos]\n\nreturn head" + } + ], + "test_methods": [ + { + "name": "test_has_cycle", + "parametrize": "values, pos, expected", + "parametrize_typed": "values: list[int], pos: int, expected: bool", + "test_cases": "[([3, 2, 0, -4], 1, True), ([1, 2], 0, True), ([1], -1, False), ([], -1, False), ([1, 2, 3], -1, False), ([1, 2, 3, 4, 5], 0, True), ([1, 2, 3, 4, 5], 2, True), ([1, 2, 3, 4, 5], 4, True), ([1], 0, True), ([1, 2], 1, True), ([1, 2, 3, 4, 5, 6, 7, 8], 3, True), ([1, 2, 3, 4, 5, 6, 7, 8], -1, False), ([1, 2], -1, False), ([5, 10], 0, True), ([5, 10], 1, True), ([0], -1, False), ([-1, -2, -3], 1, True), ([100, 200, 300], 0, True)]", + "body": "head = self.create_cycle_list(values, pos)\nresult = self.solution.has_cycle(head)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "import os\nimport sys\nsys.path.append(os.path.join(os.getcwd(), \\\"..\\\"))\nfrom linked_list_cycle.tests import TestLinkedListCycle\n\n# Example test case\nvalues = [3, 2, 0, -4]\npos = 1\nexpected = True", + "playground_execution": "head = TestLinkedListCycle().create_cycle_list(values, pos)\nresult = Solution().has_cycle(head)\nresult", + "playground_assertion": "assert result == expected" +} 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 new file mode 100644 index 0000000..eb71d4b --- /dev/null +++ b/.templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json @@ -0,0 +1,53 @@ +{ + "problem_name": "lowest_common_ancestor_of_a_binary_search_tree", + "solution_class_name": "Solution", + "problem_number": "235", + "problem_title": "Lowest Common Ancestor of a Binary Search Tree", + "difficulty": "Medium", + "topics": "Tree, Depth-First Search, Binary Search Tree, Binary Tree", + "tags": ["grind-75"], + "readme_description": "Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST.\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": "![Example 1](https://assets.leetcode.com/uploads/2018/12/14/binarysearchtree_improved.png)\n\n```\nInput: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8\nOutput: 6\n```\n**Explanation:** The LCA of nodes 2 and 8 is 6." + }, + { + "content": "![Example 2](https://assets.leetcode.com/uploads/2018/12/14/binarysearchtree_improved.png)\n\n```\nInput: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4\nOutput: 2\n```\n**Explanation:** The LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition." + }, + { "content": "```\nInput: root = [2,1], p = 2, q = 1\nOutput: 2\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 BST.", + "readme_additional": "", + "solution_imports": "from leetcode_py import TreeNode", + "solution_methods": [ + { + "name": "lowest_common_ancestor", + "parameters": "root: TreeNode[int] | None, p: TreeNode[int], q: TreeNode[int]", + "return_type": "TreeNode[int] | None", + "dummy_return": "None" + } + ], + "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)" + } + ], + "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": "[([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" + } + ], + "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_assertion": "assert result and result.val == expected_val" +} diff --git a/.templates/leetcode/json/maximum_subarray.json b/.templates/leetcode/json/maximum_subarray.json new file mode 100644 index 0000000..50da7c2 --- /dev/null +++ b/.templates/leetcode/json/maximum_subarray.json @@ -0,0 +1,50 @@ +{ + "problem_name": "maximum_subarray", + "solution_class_name": "Solution", + "problem_number": "53", + "problem_title": "Maximum Subarray", + "difficulty": "Medium", + "topics": "Array, Divide and Conquer, Dynamic Programming", + "tags": ["grind-75"], + "readme_description": "Given an integer array `nums`, find the subarray with the largest sum, and return its sum.", + "readme_examples": [ + { + "content": "```\nInput: nums = [-2,1,-3,4,-1,2,1,-5,4]\nOutput: 6\n```\n**Explanation:** The subarray [4,-1,2,1] has the largest sum 6." + }, + { + "content": "```\nInput: nums = [1]\nOutput: 1\n```\n**Explanation:** The subarray [1] has the largest sum 1." + }, + { + "content": "```\nInput: nums = [5,4,-1,7,8]\nOutput: 23\n```\n**Explanation:** The subarray [5,4,-1,7,8] has the largest sum 23." + } + ], + "readme_constraints": "- `1 <= nums.length <= 10^5`\n- `-10^4 <= nums[i] <= 10^4`", + "readme_additional": "**Follow up:** If you have figured out the `O(n)` solution, try coding another solution using the **divide and conquer** approach, which is more subtle.", + "solution_imports": "", + "solution_methods": [ + { + "name": "max_sub_array", + "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": "MaximumSubarray", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_max_sub_array", + "parametrize": "nums, expected", + "parametrize_typed": "nums: list[int], expected: int", + "test_cases": "[([-2, 1, -3, 4, -1, 2, 1, -5, 4], 6), ([1], 1), ([5, 4, -1, 7, 8], 23), ([-1], -1), ([-2, -1], -1), ([1, 2, 3, 4, 5], 15), ([-5, -2, -8, -1], -1)]", + "body": "result = self.solution.max_sub_array(nums)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]\nexpected = 6", + "playground_execution": "result = Solution().max_sub_array(nums)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/minimum_window_substring.json b/.templates/leetcode/json/minimum_window_substring.json new file mode 100644 index 0000000..a4d151b --- /dev/null +++ b/.templates/leetcode/json/minimum_window_substring.json @@ -0,0 +1,50 @@ +{ + "problem_name": "minimum_window_substring", + "solution_class_name": "Solution", + "problem_number": "76", + "problem_title": "Minimum Window Substring", + "difficulty": "Hard", + "topics": "Hash Table, String, Sliding Window", + "tags": ["grind-75"], + "readme_description": "Given two strings `s` and `t` of lengths `m` and `n` respectively, return the **minimum window substring** of `s` such that every character in `t` (including duplicates) is included in the window. If there is no such substring, return the empty string `\"\"`.\n\nThe testcases will be generated such that the answer is unique.", + "readme_examples": [ + { + "content": "```\nInput: s = \"ADOBECODEBANC\", t = \"ABC\"\nOutput: \"BANC\"\n```\n**Explanation:** The minimum window substring \"BANC\" includes 'A', 'B', and 'C' from string t." + }, + { + "content": "```\nInput: s = \"a\", t = \"a\"\nOutput: \"a\"\n```\n**Explanation:** The entire string s is the minimum window." + }, + { + "content": "```\nInput: s = \"a\", t = \"aa\"\nOutput: \"\"\n```\n**Explanation:** Both 'a's from t must be included in the window. Since the largest window of s only has one 'a', return empty string." + } + ], + "readme_constraints": "- `m == s.length`\n- `n == t.length`\n- `1 <= m, n <= 10^5`\n- `s` and `t` consist of uppercase and lowercase English letters.", + "readme_additional": "**Follow up:** Could you find an algorithm that runs in `O(m + n)` time?", + "solution_imports": "", + "solution_methods": [ + { + "name": "min_window", + "parameters": "s: str, t: str", + "return_type": "str", + "dummy_return": "\"\"" + } + ], + "test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution", + "test_class_name": "MinimumWindowSubstring", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_min_window", + "parametrize": "s, t, expected", + "parametrize_typed": "s: str, t: str, expected: str", + "test_cases": "[(\"ADOBECODEBANC\", \"ABC\", \"BANC\"), (\"a\", \"a\", \"a\"), (\"a\", \"aa\", \"\")]", + "body": "result = self.solution.min_window(s, t)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = \\\"ADOBECODEBANC\\\"\nt = \\\"ABC\\\"\nexpected = \\\"BANC\\\"", + "playground_execution": "result = Solution().min_window(s, t)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/reverse_linked_list_ii.json b/.templates/leetcode/json/reverse_linked_list_ii.json index 7c904e2..4eeb724 100644 --- a/.templates/leetcode/json/reverse_linked_list_ii.json +++ b/.templates/leetcode/json/reverse_linked_list_ii.json @@ -5,7 +5,7 @@ "problem_title": "Reverse Linked List II", "difficulty": "Medium", "topics": "Linked List", - "tags": ["grind-75"], + "tags": [], "readme_description": "Given the `head` of a singly linked list and two integers `left` and `right` where `left <= right`, reverse the nodes of the list from position `left` to position `right`, and return the reversed list.", "readme_examples": [ { "content": "```\nInput: head = [1,2,3,4,5], left = 2, right = 4\nOutput: [1,4,3,2,5]\n```" }, diff --git a/.templates/leetcode/json/search_in_rotated_sorted_array.json b/.templates/leetcode/json/search_in_rotated_sorted_array.json new file mode 100644 index 0000000..1235416 --- /dev/null +++ b/.templates/leetcode/json/search_in_rotated_sorted_array.json @@ -0,0 +1,44 @@ +{ + "problem_name": "search_in_rotated_sorted_array", + "solution_class_name": "Solution", + "problem_number": "33", + "problem_title": "Search in Rotated Sorted Array", + "difficulty": "Medium", + "topics": "Array, Binary Search", + "tags": ["grind-75"], + "readme_description": "There is an integer array `nums` sorted in ascending order (with **distinct** values).\n\nPrior to being passed to your function, `nums` is **possibly left rotated** at an unknown index `k` (`1 <= k < nums.length`) such that the resulting array is `[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]` (**0-indexed**). For example, `[0,1,2,4,5,6,7]` might be left rotated by 3 indices and become `[4,5,6,7,0,1,2]`.\n\nGiven the array `nums` **after** the possible rotation and an integer `target`, return *the index of* `target` *if it is in* `nums`*, or* `-1` *if it is not in* `nums`.\n\nYou must write an algorithm with `O(log n)` runtime complexity.", + "readme_examples": [ + { "content": "```\nInput: nums = [4,5,6,7,0,1,2], target = 0\nOutput: 4\n```" }, + { "content": "```\nInput: nums = [4,5,6,7,0,1,2], target = 3\nOutput: -1\n```" }, + { "content": "```\nInput: nums = [1], target = 0\nOutput: -1\n```" } + ], + "readme_constraints": "- `1 <= nums.length <= 5000`\n- `-10^4 <= nums[i] <= 10^4`\n- All values of `nums` are **unique**.\n- `nums` is an ascending array that is possibly rotated.\n- `-10^4 <= target <= 10^4`", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "search", + "parameters": "nums: list[int], target: 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": "SearchInRotatedSortedArray", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_search", + "parametrize": "nums, target, expected", + "parametrize_typed": "nums: list[int], target: int, expected: int", + "test_cases": "[([4, 5, 6, 7, 0, 1, 2], 0, 4), ([4, 5, 6, 7, 0, 1, 2], 3, -1), ([1], 0, -1), ([1], 1, 0), ([3, 1], 1, 1)]", + "body": "result = self.solution.search(nums, target)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\nnums = [4, 5, 6, 7, 0, 1, 2]\ntarget = 0\nexpected = 4", + "playground_execution": "result = Solution().search(nums, target)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/.templates/leetcode/json/valid_palindrome.json b/.templates/leetcode/json/valid_palindrome.json new file mode 100644 index 0000000..cb4b861 --- /dev/null +++ b/.templates/leetcode/json/valid_palindrome.json @@ -0,0 +1,50 @@ +{ + "problem_name": "valid_palindrome", + "solution_class_name": "Solution", + "problem_number": "125", + "problem_title": "Valid Palindrome", + "difficulty": "Easy", + "topics": "Two Pointers, String", + "tags": ["grind-75"], + "readme_description": "A phrase is a **palindrome** if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers.\n\nGiven a string `s`, return `true` if it is a **palindrome**, or `false` otherwise.", + "readme_examples": [ + { + "content": "```\nInput: s = \"A man, a plan, a canal: Panama\"\nOutput: true\n```\n**Explanation:** \"amanaplanacanalpanama\" is a palindrome." + }, + { + "content": "```\nInput: s = \"race a car\"\nOutput: false\n```\n**Explanation:** \"raceacar\" is not a palindrome." + }, + { + "content": "```\nInput: s = \" \"\nOutput: true\n```\n**Explanation:** s is an empty string \"\" after removing non-alphanumeric characters. Since an empty string reads the same forward and backward, it is a palindrome." + } + ], + "readme_constraints": "- `1 <= s.length <= 2 * 10^5`\n- `s` consists only of printable ASCII characters.", + "readme_additional": "", + "solution_imports": "", + "solution_methods": [ + { + "name": "is_palindrome", + "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": "ValidPalindrome", + "test_helper_methods": [ + { "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" } + ], + "test_methods": [ + { + "name": "test_is_palindrome", + "parametrize": "s, expected", + "parametrize_typed": "s: str, expected: bool", + "test_cases": "[(\"A man, a plan, a canal: Panama\", True), (\"race a car\", False), (\" \", True), (\"\", True), (\"a\", True), (\"Madam\", True), (\"No 'x' in Nixon\", True), (\"Mr. Owl ate my metal worm\", True)]", + "body": "result = self.solution.is_palindrome(s)\nassert result == expected" + } + ], + "playground_imports": "from solution import Solution", + "playground_test_case": "# Example test case\ns = \"A man, a plan, a canal: Panama\"\nexpected = True", + "playground_execution": "result = Solution().is_palindrome(s)\nresult", + "playground_assertion": "assert result == expected" +} diff --git a/Makefile b/Makefile index 91a7d24..d49ed4d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VERSION = 3.13 -PROBLEM ?= task_scheduler +PROBLEM ?= k_closest_points_to_origin FORCE ?= 0 sync_submodules: diff --git a/leetcode/binary_search/README.md b/leetcode/binary_search/README.md new file mode 100644 index 0000000..16d0461 --- /dev/null +++ b/leetcode/binary_search/README.md @@ -0,0 +1,40 @@ +# Binary Search + +**Difficulty:** Easy +**Topics:** Array, Binary Search +**Tags:** grind-75 + +**LeetCode:** [Problem 704](https://leetcode.com/problems/binary-search/description/) + +## Problem Description + +Given an array of integers `nums` which is sorted in ascending order, and an integer `target`, write a function to search `target` in `nums`. If `target` exists, then return its index. Otherwise, return `-1`. + +You must write an algorithm with `O(log n)` runtime complexity. + +## Examples + +### Example 1: + +``` +Input: nums = [-1,0,3,5,9,12], target = 9 +Output: 4 +``` + +**Explanation:** 9 exists in nums and its index is 4 + +### Example 2: + +``` +Input: nums = [-1,0,3,5,9,12], target = 2 +Output: -1 +``` + +**Explanation:** 2 does not exist in nums so return -1 + +## Constraints + +- `1 <= nums.length <= 10^4` +- `-10^4 < nums[i], target < 10^4` +- All the integers in `nums` are **unique**. +- `nums` is sorted in ascending order. diff --git a/leetcode/binary_search/__init__.py b/leetcode/binary_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/binary_search/playground.ipynb b/leetcode/binary_search/playground.ipynb new file mode 100644 index 0000000..d7a58c0 --- /dev/null +++ b/leetcode/binary_search/playground.ipynb @@ -0,0 +1,69 @@ +{ + "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", + "nums = [-1, 0, 3, 5, 9, 12]\n", + "target = 9\n", + "expected = 4" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "result = Solution().search(nums, target)\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_search/solution.py b/leetcode/binary_search/solution.py new file mode 100644 index 0000000..7bc15e6 --- /dev/null +++ b/leetcode/binary_search/solution.py @@ -0,0 +1,17 @@ +class Solution: + # Time: O(log n) + # Space: O(1) + def search(self, nums: list[int], target: int) -> int: + left, right = 0, len(nums) - 1 + + while left <= right: + mid = (left + right) // 2 + + if nums[mid] == target: + return mid + elif nums[mid] < target: + left = mid + 1 + else: + right = mid - 1 + + return -1 diff --git a/leetcode/binary_search/tests.py b/leetcode/binary_search/tests.py new file mode 100644 index 0000000..0842386 --- /dev/null +++ b/leetcode/binary_search/tests.py @@ -0,0 +1,39 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestBinarySearch: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, target, expected", + [ + # Original examples + ([-1, 0, 3, 5, 9, 12], 9, 4), + ([-1, 0, 3, 5, 9, 12], 2, -1), + # Single element + ([5], 5, 0), + ([5], -5, -1), + # Target at boundaries + ([1, 3, 5, 7, 9], 1, 0), + ([1, 3, 5, 7, 9], 9, 4), + # Target not found + ([1, 3, 5, 7, 9], 4, -1), + # Two elements + ([1, 3], 1, 0), + ([1, 3], 3, 1), + ([1, 3], 2, -1), + # Negative numbers + ([-5, -2, 0, 3, 7], -2, 1), + ([-5, -2, 0, 3, 7], 0, 2), + ([-5, -2, 0, 3, 7], -1, -1), + ], + ) + @logged_test + def test_search(self, nums: list[int], target: int, expected: int): + result = self.solution.search(nums, target) + assert result == expected diff --git a/leetcode/container_with_most_water/tests.py b/leetcode/container_with_most_water/tests.py index ebc66be..2713d8d 100644 --- a/leetcode/container_with_most_water/tests.py +++ b/leetcode/container_with_most_water/tests.py @@ -10,7 +10,29 @@ def setup_method(self): self.solution = Solution() @pytest.mark.parametrize( - "height, expected", [([1, 8, 6, 2, 5, 4, 8, 3, 7], 49), ([1, 1], 1), ([1, 2, 1], 2)] + "height, expected", + [ + # Original cases + ([1, 8, 6, 2, 5, 4, 8, 3, 7], 49), + ([1, 1], 1), + ([1, 2, 1], 2), + # Edge cases + ([2, 1], 1), + ([1, 2], 1), + ([0, 2], 0), + ([2, 0], 0), + # Increasing heights + ([1, 2, 3, 4, 5], 6), + # Decreasing heights + ([5, 4, 3, 2, 1], 6), + # Same heights + ([3, 3, 3, 3], 9), + # Large differences + ([1, 1000, 1], 2), + ([1000, 1, 1000], 2000), + # Multiple peaks + ([2, 3, 4, 5, 18, 17, 6], 17), + ], ) @logged_test def test_max_area(self, height: list[int], expected: int): diff --git a/leetcode/evaluate_reverse_polish_notation/tests.py b/leetcode/evaluate_reverse_polish_notation/tests.py index 82c126b..f353e1f 100644 --- a/leetcode/evaluate_reverse_polish_notation/tests.py +++ b/leetcode/evaluate_reverse_polish_notation/tests.py @@ -12,9 +12,24 @@ def setup_method(self): @pytest.mark.parametrize( "tokens, expected", [ + # Original cases (["2", "1", "+", "3", "*"], 9), (["4", "13", "5", "/", "+"], 6), (["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"], 22), + # Single number + (["42"], 42), + # Negative numbers + (["-1"], -1), + (["1", "-1", "+"], 0), + # Basic operations + (["3", "4", "+"], 7), + (["5", "2", "-"], 3), + (["6", "3", "*"], 18), + (["8", "2", "/"], 4), + # Division with negatives + (["-3", "4", "+", "2", "*", "1", "-"], 1), + # Complex expression + (["15", "7", "1", "1", "+", "/", "/", "3", "*", "2", "1", "1", "+", "+", "-"], 11), ], ) @logged_test diff --git a/leetcode/insert_interval/tests.py b/leetcode/insert_interval/tests.py index 6968af7..ee51e8f 100644 --- a/leetcode/insert_interval/tests.py +++ b/leetcode/insert_interval/tests.py @@ -12,8 +12,25 @@ def setup_method(self): @pytest.mark.parametrize( "intervals, new_interval, expected", [ + # Original cases ([[1, 3], [6, 9]], [2, 5], [[1, 5], [6, 9]]), ([[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]], [4, 8], [[1, 2], [3, 10], [12, 16]]), + # Empty intervals + ([], [5, 7], [[5, 7]]), + # Insert at beginning + ([[3, 5], [6, 9]], [1, 2], [[1, 2], [3, 5], [6, 9]]), + # Insert at end + ([[1, 3], [6, 9]], [10, 12], [[1, 3], [6, 9], [10, 12]]), + # No overlap + ([[1, 2], [4, 5]], [3, 3], [[1, 2], [3, 3], [4, 5]]), + # Complete overlap + ([[1, 5]], [2, 3], [[1, 5]]), + # Merge all intervals + ([[1, 2], [3, 4], [5, 6]], [0, 7], [[0, 7]]), + # Adjacent intervals + ([[1, 3], [6, 9]], [4, 5], [[1, 3], [4, 5], [6, 9]]), + # Touch boundaries + ([[1, 3], [6, 9]], [3, 6], [[1, 9]]), ], ) @logged_test diff --git a/leetcode/k_closest_points_to_origin/README.md b/leetcode/k_closest_points_to_origin/README.md new file mode 100644 index 0000000..54395ba --- /dev/null +++ b/leetcode/k_closest_points_to_origin/README.md @@ -0,0 +1,42 @@ +# K Closest Points to Origin + +**Difficulty:** Medium +**Topics:** Array, Math, Divide and Conquer, Geometry, Sorting, Heap (Priority Queue), Quickselect +**Tags:** grind-75 + +**LeetCode:** [Problem 973](https://leetcode.com/problems/k-closest-points-to-origin/description/) + +## Problem Description + +Given an array of `points` where `points[i] = [xi, yi]` represents a point on the **X-Y** plane and an integer `k`, return the `k` closest points to the origin `(0, 0)`. + +The distance between two points on the **X-Y** plane is the Euclidean distance (i.e., `√(x1 - x2)² + (y1 - y2)²`). + +You may return the answer in **any order**. The answer is **guaranteed** to be **unique** (except for the order that it is in). + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/03/03/closestplane1.jpg) + +``` +Input: points = [[1,3],[-2,2]], k = 1 +Output: [[-2,2]] +``` + +**Explanation:** The distance between (1, 3) and the origin is sqrt(10). The distance between (-2, 2) and the origin is sqrt(8). Since sqrt(8) < sqrt(10), (-2, 2) is closer to the origin. We only want the closest k = 1 points from the origin, so the answer is just [[-2,2]]. + +### Example 2: + +``` +Input: points = [[3,3],[5,-1],[-2,4]], k = 2 +Output: [[3,3],[-2,4]] +``` + +**Explanation:** The answer [[-2,4],[3,3]] would also be accepted. + +## Constraints + +- `1 <= k <= points.length <= 10^4` +- `-10^4 <= xi, yi <= 10^4` diff --git a/leetcode/k_closest_points_to_origin/__init__.py b/leetcode/k_closest_points_to_origin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/k_closest_points_to_origin/playground.ipynb b/leetcode/k_closest_points_to_origin/playground.ipynb new file mode 100644 index 0000000..49a9238 --- /dev/null +++ b/leetcode/k_closest_points_to_origin/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\npoints = [[1, 3], [-2, 2]]\nk = 1\nexpected = [[-2, 2]]"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().k_closest(points, k)\nresult"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": ["assert sorted(result) == sorted(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/k_closest_points_to_origin/solution.py b/leetcode/k_closest_points_to_origin/solution.py new file mode 100644 index 0000000..d053d9b --- /dev/null +++ b/leetcode/k_closest_points_to_origin/solution.py @@ -0,0 +1,16 @@ +import heapq + + +class Solution: + # Time: O(n log k) + # Space: O(k) + def k_closest(self, points: list[list[int]], k: int) -> list[list[int]]: + heap: list[tuple[int, list[int]]] = [] + + for x, y in points: + dist = x * x + y * y + heapq.heappush(heap, (-dist, [x, y])) + if len(heap) > k: + heapq.heappop(heap) + + return [point for _, point in heap] diff --git a/leetcode/k_closest_points_to_origin/tests.py b/leetcode/k_closest_points_to_origin/tests.py new file mode 100644 index 0000000..296af1b --- /dev/null +++ b/leetcode/k_closest_points_to_origin/tests.py @@ -0,0 +1,38 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestKClosestPointsToOrigin: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "points, k, expected", + [ + # Basic examples + ([[1, 3], [-2, 2]], 1, [[-2, 2]]), + ([[3, 3], [5, -1], [-2, 4]], 2, [[3, 3], [-2, 4]]), + ([[0, 1], [1, 0]], 2, [[0, 1], [1, 0]]), + ([[1, 1], [1, 1], [1, 1]], 2, [[1, 1], [1, 1]]), + # Edge cases + ([[0, 0]], 1, [[0, 0]]), # Origin point + ([[1, 0], [0, 1], [-1, 0], [0, -1]], 1, [[1, 0]]), # Unit circle points + ([[2, 2], [1, 1], [3, 3]], 3, [[1, 1], [2, 2], [3, 3]]), # All points + # Negative coordinates + ([[-1, -1], [-2, -2], [1, 1]], 2, [[-1, -1], [1, 1]]), + # Large coordinates + ([[100, 100], [1, 1], [50, 50]], 1, [[1, 1]]), + # Same distances + ([[1, 0], [0, 1], [-1, 0], [0, -1]], 2, [[1, 0], [0, 1]]), + ], + ) + @logged_test + def test_k_closest(self, points: list[list[int]], k: int, expected: list[list[int]]): + result = self.solution.k_closest(points, k) + # Sort both result and expected for comparison since order doesn't matter + result_sorted = sorted(result) + expected_sorted = sorted(expected) + assert result_sorted == expected_sorted diff --git a/leetcode/kth_smallest_element_in_a_bst/README.md b/leetcode/kth_smallest_element_in_a_bst/README.md new file mode 100644 index 0000000..fefde64 --- /dev/null +++ b/leetcode/kth_smallest_element_in_a_bst/README.md @@ -0,0 +1,39 @@ +# Kth Smallest Element in a BST + +**Difficulty:** Medium +**Topics:** Tree, Depth-First Search, Binary Search Tree, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 230](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) + +## Problem Description + +Given the `root` of a binary search tree, and an integer `k`, return the `k`th smallest value (1-indexed) of all the values of the nodes in the tree. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2021/01/28/kthtree1.jpg) + +``` +Input: root = [3,1,4,null,2], k = 1 +Output: 1 +``` + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2021/01/28/kthtree2.jpg) + +``` +Input: root = [5,3,6,2,4,null,null,1], k = 3 +Output: 3 +``` + +## Constraints + +- The number of nodes in the tree is `n`. +- `1 <= k <= n <= 10^4` +- `0 <= Node.val <= 10^4` + +**Follow up:** If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize? diff --git a/leetcode/kth_smallest_element_in_a_bst/__init__.py b/leetcode/kth_smallest_element_in_a_bst/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/kth_smallest_element_in_a_bst/playground.ipynb b/leetcode/kth_smallest_element_in_a_bst/playground.ipynb new file mode 100644 index 0000000..da34058 --- /dev/null +++ b/leetcode/kth_smallest_element_in_a_bst/playground.ipynb @@ -0,0 +1,160 @@ +{ + "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, 1, 4, None, 2]\n", + "k = 1\n", + "expected = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root = TreeNode.from_list(root_list)\n", + "result = Solution().kth_smallest(root, k)\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6dc42838", + "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", + "1\n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "4\n", + "\n", + "\n", + "\n", + "0->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "2\n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "TreeNode([3, 1, 4, None, 2])" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root" + ] + }, + { + "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/kth_smallest_element_in_a_bst/solution.py b/leetcode/kth_smallest_element_in_a_bst/solution.py new file mode 100644 index 0000000..57bd5db --- /dev/null +++ b/leetcode/kth_smallest_element_in_a_bst/solution.py @@ -0,0 +1,41 @@ +from leetcode_py import TreeNode + + +class Solution: + # Inorder Recursive + # Time: O(k) + # Space: O(h) + def kth_smallest(self, root: TreeNode | None, k: int) -> int: + def inorder(node: TreeNode | None): + if not node: + return + yield from inorder(node.left) + yield node.val + yield from inorder(node.right) + + for i, val in enumerate(inorder(root)): + if i == k - 1: + return val + + raise ValueError(f"Tree has fewer than {k} nodes") + + +# Binary Tree Traversal Patterns +# +# def inorder(node): +# if node: +# inorder(node.left) +# print(node.val) +# inorder(node.right) +# +# def preorder(node): +# if node: +# print(node.val) +# preorder(node.left) +# preorder(node.right) +# +# def postorder(node): +# if node: +# postorder(node.left) +# postorder(node.right) +# print(node.val) diff --git a/leetcode/kth_smallest_element_in_a_bst/tests.py b/leetcode/kth_smallest_element_in_a_bst/tests.py new file mode 100644 index 0000000..1e52c52 --- /dev/null +++ b/leetcode/kth_smallest_element_in_a_bst/tests.py @@ -0,0 +1,21 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestKthSmallestElementInABst: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "root_list, k, expected", + [([3, 1, 4, None, 2], 1, 1), ([5, 3, 6, 2, 4, None, None, 1], 3, 3), ([1], 1, 1)], + ) + @logged_test + def test_kth_smallest(self, root_list: list[int | None], k: int, expected: int): + root = TreeNode.from_list(root_list) + result = self.solution.kth_smallest(root, k) + assert result == expected diff --git a/leetcode/linked_list_cycle/README.md b/leetcode/linked_list_cycle/README.md new file mode 100644 index 0000000..4ec75c4 --- /dev/null +++ b/leetcode/linked_list_cycle/README.md @@ -0,0 +1,58 @@ +# Linked List Cycle + +**Difficulty:** Easy +**Topics:** Hash Table, Linked List, Two Pointers +**Tags:** grind-75 + +**LeetCode:** [Problem 141](https://leetcode.com/problems/linked-list-cycle/description/) + +## Problem Description + +Given `head`, the head of a linked list, determine if the linked list has a cycle in it. + +There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the `next` pointer. Internally, `pos` is used to denote the index of the node that tail's `next` pointer is connected to. **Note that `pos` is not passed as a parameter**. + +Return `true` _if there is a cycle in the linked list_. Otherwise, return `false`. + +## Examples + +### Example 1: + +![Example 1](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist.png) + +``` +Input: head = [3,2,0,-4], pos = 1 +Output: true +``` + +**Explanation:** There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed). + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist_test2.png) + +``` +Input: head = [1,2], pos = 0 +Output: true +``` + +**Explanation:** There is a cycle in the linked list, where the tail connects to the 0th node. + +### Example 3: + +![Example 3](https://assets.leetcode.com/uploads/2018/12/07/circularlinkedlist_test3.png) + +``` +Input: head = [1], pos = -1 +Output: false +``` + +**Explanation:** There is no cycle in the linked list. + +## Constraints + +- The number of the nodes in the list is in the range `[0, 10^4]`. +- `-10^5 <= Node.val <= 10^5` +- `pos` is `-1` or a **valid index** in the linked-list. + +**Follow up:** Can you solve it using `O(1)` (i.e. constant) memory? diff --git a/leetcode/linked_list_cycle/__init__.py b/leetcode/linked_list_cycle/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/linked_list_cycle/playground.ipynb b/leetcode/linked_list_cycle/playground.ipynb new file mode 100644 index 0000000..7f74fb3 --- /dev/null +++ b/leetcode/linked_list_cycle/playground.ipynb @@ -0,0 +1,87 @@ +{ + "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": [ + "import os\n", + "import sys\n", + "\n", + "sys.path.append(os.path.join(os.getcwd(), \"..\"))\n", + "from linked_list_cycle.tests import TestLinkedListCycle\n", + "\n", + "# Example test case\n", + "values = [3, 2, 0, -4]\n", + "pos = 1\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": [ + "head = TestLinkedListCycle().create_cycle_list(values, pos)\n", + "result = Solution().has_cycle(head)\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/linked_list_cycle/solution.py b/leetcode/linked_list_cycle/solution.py new file mode 100644 index 0000000..6c3f838 --- /dev/null +++ b/leetcode/linked_list_cycle/solution.py @@ -0,0 +1,18 @@ +from leetcode_py import ListNode + + +class Solution: + # Time: O(n) + # Space: O(1) + def has_cycle(self, head: ListNode[int] | None) -> bool: + fast = head + slow = head + + while fast and fast.next: + assert slow is not None + fast = fast.next.next + slow = slow.next + if fast is slow: + return True + + return False diff --git a/leetcode/linked_list_cycle/tests.py b/leetcode/linked_list_cycle/tests.py new file mode 100644 index 0000000..8af1b82 --- /dev/null +++ b/leetcode/linked_list_cycle/tests.py @@ -0,0 +1,59 @@ +import pytest + +from leetcode_py import ListNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestLinkedListCycle: + def setup_method(self): + self.solution = Solution() + + def create_cycle_list(self, values: list[int], pos: int): + if not values: + return None + + nodes = [] + head = ListNode(values[0]) + nodes.append(head) + current = head + + for i in range(1, len(values)): + current.next = ListNode(values[i]) + current = current.next + nodes.append(current) + + if pos != -1 and pos < len(nodes): + current.next = nodes[pos] + + return head + + @pytest.mark.parametrize( + "values, pos, expected", + [ + ([3, 2, 0, -4], 1, True), + ([1, 2], 0, True), + ([1], -1, False), + ([], -1, False), + ([1, 2, 3], -1, False), + ([1, 2, 3, 4, 5], 0, True), + ([1, 2, 3, 4, 5], 2, True), + ([1, 2, 3, 4, 5], 4, True), + ([1], 0, True), + ([1, 2], 1, True), + ([1, 2, 3, 4, 5, 6, 7, 8], 3, True), + ([1, 2, 3, 4, 5, 6, 7, 8], -1, False), + ([1, 2], -1, False), + ([5, 10], 0, True), + ([5, 10], 1, True), + ([0], -1, False), + ([-1, -2, -3], 1, True), + ([100, 200, 300], 0, True), + ], + ) + @logged_test + def test_has_cycle(self, values: list[int], pos: int, expected: bool): + head = self.create_cycle_list(values, pos) + result = self.solution.has_cycle(head) + assert result == expected diff --git a/leetcode/lowest_common_ancestor_of_a_binary_search_tree/README.md b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/README.md new file mode 100644 index 0000000..a5b2f27 --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/README.md @@ -0,0 +1,52 @@ +# Lowest Common Ancestor of a Binary Search Tree + +**Difficulty:** Medium +**Topics:** Tree, Depth-First Search, Binary Search Tree, Binary Tree +**Tags:** grind-75 + +**LeetCode:** [Problem 235](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) + +## Problem Description + +Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST. + +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: + +![Example 1](https://assets.leetcode.com/uploads/2018/12/14/binarysearchtree_improved.png) + +``` +Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 +Output: 6 +``` + +**Explanation:** The LCA of nodes 2 and 8 is 6. + +### Example 2: + +![Example 2](https://assets.leetcode.com/uploads/2018/12/14/binarysearchtree_improved.png) + +``` +Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 +Output: 2 +``` + +**Explanation:** The LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition. + +### Example 3: + +``` +Input: root = [2,1], p = 2, q = 1 +Output: 2 +``` + +## 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 BST. diff --git a/leetcode/lowest_common_ancestor_of_a_binary_search_tree/__init__.py b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/__init__.py new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..763d0ea --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb @@ -0,0 +1,75 @@ +{ + "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 = [6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]\n", + "p_val = 2\n", + "q_val = 8\n", + "expected_val = 6" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": [ + "root = TreeNode[int].from_list(root_list)\n", + "p = find_node(root, p_val)\n", + "q = find_node(root, q_val)\n", + "result = Solution().lowest_common_ancestor(root, p, q)\n", + "result.val if result else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test", + "metadata": {}, + "outputs": [], + "source": [ + "assert result and 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_search_tree/solution.py b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/solution.py new file mode 100644 index 0000000..f6f5dcd --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/solution.py @@ -0,0 +1,20 @@ +from leetcode_py import TreeNode + + +class Solution: + # Time: O(log n) average, O(n) worst case + # Space: O(1) iterative, O(log n) recursive + def lowest_common_ancestor( + self, root: TreeNode[int] | None, p: TreeNode[int], q: TreeNode[int] + ) -> TreeNode[int] | None: + while root: + # Both nodes are in left subtree + if p.val < root.val and q.val < root.val: + root = root.left + # Both nodes are in right subtree + elif p.val > root.val and q.val > root.val: + root = root.right + # Split point - one node on each side or one is the root + else: + return root + return None 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 new file mode 100644 index 0000000..33767fe --- /dev/null +++ b/leetcode/lowest_common_ancestor_of_a_binary_search_tree/tests.py @@ -0,0 +1,45 @@ +import pytest + +from leetcode_py import TreeNode +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +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", + [ + ([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), + ], + ) + @logged_test + def test_lowest_common_ancestor( + self, root_list: list[int | None], p_val: int, q_val: int, expected_val: int + ): + 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) + 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/maximum_subarray/README.md b/leetcode/maximum_subarray/README.md new file mode 100644 index 0000000..e4d7afa --- /dev/null +++ b/leetcode/maximum_subarray/README.md @@ -0,0 +1,47 @@ +# Maximum Subarray + +**Difficulty:** Medium +**Topics:** Array, Divide and Conquer, Dynamic Programming +**Tags:** grind-75 + +**LeetCode:** [Problem 53](https://leetcode.com/problems/maximum-subarray/description/) + +## Problem Description + +Given an integer array `nums`, find the subarray with the largest sum, and return its sum. + +## Examples + +### Example 1: + +``` +Input: nums = [-2,1,-3,4,-1,2,1,-5,4] +Output: 6 +``` + +**Explanation:** The subarray [4,-1,2,1] has the largest sum 6. + +### Example 2: + +``` +Input: nums = [1] +Output: 1 +``` + +**Explanation:** The subarray [1] has the largest sum 1. + +### Example 3: + +``` +Input: nums = [5,4,-1,7,8] +Output: 23 +``` + +**Explanation:** The subarray [5,4,-1,7,8] has the largest sum 23. + +## Constraints + +- `1 <= nums.length <= 10^5` +- `-10^4 <= nums[i] <= 10^4` + +**Follow up:** If you have figured out the `O(n)` solution, try coding another solution using the **divide and conquer** approach, which is more subtle. diff --git a/leetcode/maximum_subarray/__init__.py b/leetcode/maximum_subarray/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/maximum_subarray/playground.ipynb b/leetcode/maximum_subarray/playground.ipynb new file mode 100644 index 0000000..2cbaa51 --- /dev/null +++ b/leetcode/maximum_subarray/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 = [-2, 1, -3, 4, -1, 2, 1, -5, 4]\nexpected = 6"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().max_sub_array(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/maximum_subarray/solution.py b/leetcode/maximum_subarray/solution.py new file mode 100644 index 0000000..2176165 --- /dev/null +++ b/leetcode/maximum_subarray/solution.py @@ -0,0 +1,11 @@ +class Solution: + # Time: O(n) + # Space: O(1) + def max_sub_array(self, nums: list[int]) -> int: + max_sum = current_sum = nums[0] + + for i in range(1, len(nums)): + current_sum = max(nums[i], current_sum + nums[i]) + max_sum = max(max_sum, current_sum) + + return max_sum diff --git a/leetcode/maximum_subarray/tests.py b/leetcode/maximum_subarray/tests.py new file mode 100644 index 0000000..f186921 --- /dev/null +++ b/leetcode/maximum_subarray/tests.py @@ -0,0 +1,27 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMaximumSubarray: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, expected", + [ + ([-2, 1, -3, 4, -1, 2, 1, -5, 4], 6), + ([1], 1), + ([5, 4, -1, 7, 8], 23), + ([-1], -1), + ([-2, -1], -1), + ([1, 2, 3, 4, 5], 15), + ([-5, -2, -8, -1], -1), + ], + ) + @logged_test + def test_max_sub_array(self, nums: list[int], expected: int): + result = self.solution.max_sub_array(nums) + assert result == expected diff --git a/leetcode/minimum_window_substring/README.md b/leetcode/minimum_window_substring/README.md new file mode 100644 index 0000000..f54b7da --- /dev/null +++ b/leetcode/minimum_window_substring/README.md @@ -0,0 +1,51 @@ +# Minimum Window Substring + +**Difficulty:** Hard +**Topics:** Hash Table, String, Sliding Window +**Tags:** grind-75 + +**LeetCode:** [Problem 76](https://leetcode.com/problems/minimum-window-substring/description/) + +## Problem Description + +Given two strings `s` and `t` of lengths `m` and `n` respectively, return the **minimum window substring** of `s` such that every character in `t` (including duplicates) is included in the window. If there is no such substring, return the empty string `""`. + +The testcases will be generated such that the answer is unique. + +## Examples + +### Example 1: + +``` +Input: s = "ADOBECODEBANC", t = "ABC" +Output: "BANC" +``` + +**Explanation:** The minimum window substring "BANC" includes 'A', 'B', and 'C' from string t. + +### Example 2: + +``` +Input: s = "a", t = "a" +Output: "a" +``` + +**Explanation:** The entire string s is the minimum window. + +### Example 3: + +``` +Input: s = "a", t = "aa" +Output: "" +``` + +**Explanation:** Both 'a's from t must be included in the window. Since the largest window of s only has one 'a', return empty string. + +## Constraints + +- `m == s.length` +- `n == t.length` +- `1 <= m, n <= 10^5` +- `s` and `t` consist of uppercase and lowercase English letters. + +**Follow up:** Could you find an algorithm that runs in `O(m + n)` time? diff --git a/leetcode/minimum_window_substring/__init__.py b/leetcode/minimum_window_substring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/minimum_window_substring/playground.ipynb b/leetcode/minimum_window_substring/playground.ipynb new file mode 100644 index 0000000..1f99f98 --- /dev/null +++ b/leetcode/minimum_window_substring/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 = \"ADOBECODEBANC\"\n", + "t = \"ABC\"\n", + "expected = \"BANC\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "execute", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'BANC'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = Solution().min_window(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/minimum_window_substring/solution.py b/leetcode/minimum_window_substring/solution.py new file mode 100644 index 0000000..60bcb89 --- /dev/null +++ b/leetcode/minimum_window_substring/solution.py @@ -0,0 +1,48 @@ +from collections import Counter + + +class Solution: + # Sliding Window + # Time: O(m + n) where m = len(s), n = len(t) + # Space: O(k) where k is unique chars in t + def min_window(self, s: str, t: str) -> str: + if not t or len(t) > len(s): + return "" + + need = Counter(t) + + left = 0 + formed = 0 + required = len(need) + window_counts: dict[str, int] = {} + + # Result: (window length, left, right) + ans: tuple[float, int | None, int | None] = (float("inf"), None, None) + + for right in range(len(s)): + char = s[right] + window_counts[char] = window_counts.get(char, 0) + 1 + + # Check if current char frequency matches desired frequency in t + if char in need and window_counts[char] == need[char]: + formed += 1 + + # Contract window until it's no longer valid + while left <= right and formed == required: + char = s[left] + + # Update result if this window is smaller + if right - left + 1 < ans[0]: + ans = (right - left + 1, left, right) + + # Remove from left + window_counts[char] -= 1 + if char in need and window_counts[char] < need[char]: + formed -= 1 + + left += 1 + + if ans[0] == float("inf"): + return "" + assert ans[1] is not None and ans[2] is not None + return s[ans[1] : ans[2] + 1] diff --git a/leetcode/minimum_window_substring/tests.py b/leetcode/minimum_window_substring/tests.py new file mode 100644 index 0000000..8a3e16a --- /dev/null +++ b/leetcode/minimum_window_substring/tests.py @@ -0,0 +1,39 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestMinimumWindowSubstring: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, t, expected", + [ + # Basic cases + ("ADOBECODEBANC", "ABC", "BANC"), + ("a", "a", "a"), + ("a", "aa", ""), + # Edge cases + ("", "a", ""), # Empty s + ("a", "", ""), # Empty t + ("", "", ""), # Both empty + ("ab", "ba", "ab"), # Same length + ("abc", "cba", "abc"), # Entire string needed + # Duplicates + ("ADOBECODEBANC", "AABC", "ADOBECODEBA"), # Correct: needs 2 A's, 1 B, 1 C + ("aa", "aa", "aa"), + # No solution + ("abc", "def", ""), + ("a", "b", ""), + # Multiple valid windows + ("ADOBECODEBANC", "AB", "BA"), # Correct: "BA" is shorter than "ADOB" + ("abcdef", "cf", "cdef"), + ], + ) + @logged_test + def test_min_window(self, s: str, t: str, expected: str): + result = self.solution.min_window(s, t) + assert result == expected diff --git a/leetcode/reverse_linked_list_ii/README.md b/leetcode/reverse_linked_list_ii/README.md index 7c005a6..1f7a098 100644 --- a/leetcode/reverse_linked_list_ii/README.md +++ b/leetcode/reverse_linked_list_ii/README.md @@ -2,7 +2,7 @@ **Difficulty:** Medium **Topics:** Linked List -**Tags:** grind-75 +**Tags:** **LeetCode:** [Problem 92](https://leetcode.com/problems/reverse-linked-list-ii/description/) diff --git a/leetcode/search_in_rotated_sorted_array/README.md b/leetcode/search_in_rotated_sorted_array/README.md new file mode 100644 index 0000000..f74397c --- /dev/null +++ b/leetcode/search_in_rotated_sorted_array/README.md @@ -0,0 +1,48 @@ +# Search in Rotated Sorted Array + +**Difficulty:** Medium +**Topics:** Array, Binary Search +**Tags:** grind-75 + +**LeetCode:** [Problem 33](https://leetcode.com/problems/search-in-rotated-sorted-array/description/) + +## Problem Description + +There is an integer array `nums` sorted in ascending order (with **distinct** values). + +Prior to being passed to your function, `nums` is **possibly left rotated** at an unknown index `k` (`1 <= k < nums.length`) such that the resulting array is `[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]` (**0-indexed**). For example, `[0,1,2,4,5,6,7]` might be left rotated by 3 indices and become `[4,5,6,7,0,1,2]`. + +Given the array `nums` **after** the possible rotation and an integer `target`, return _the index of_ `target` _if it is in_ `nums`_, or_ `-1` _if it is not in_ `nums`. + +You must write an algorithm with `O(log n)` runtime complexity. + +## Examples + +### Example 1: + +``` +Input: nums = [4,5,6,7,0,1,2], target = 0 +Output: 4 +``` + +### Example 2: + +``` +Input: nums = [4,5,6,7,0,1,2], target = 3 +Output: -1 +``` + +### Example 3: + +``` +Input: nums = [1], target = 0 +Output: -1 +``` + +## Constraints + +- `1 <= nums.length <= 5000` +- `-10^4 <= nums[i] <= 10^4` +- All values of `nums` are **unique**. +- `nums` is an ascending array that is possibly rotated. +- `-10^4 <= target <= 10^4` diff --git a/leetcode/search_in_rotated_sorted_array/__init__.py b/leetcode/search_in_rotated_sorted_array/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/search_in_rotated_sorted_array/playground.ipynb b/leetcode/search_in_rotated_sorted_array/playground.ipynb new file mode 100644 index 0000000..357fb33 --- /dev/null +++ b/leetcode/search_in_rotated_sorted_array/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 = [4, 5, 6, 7, 0, 1, 2]\n", + "target = 0\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": [ + "result = Solution().search(nums, target)\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/search_in_rotated_sorted_array/solution.py b/leetcode/search_in_rotated_sorted_array/solution.py new file mode 100644 index 0000000..d8af000 --- /dev/null +++ b/leetcode/search_in_rotated_sorted_array/solution.py @@ -0,0 +1,26 @@ +class Solution: + # Time: O(log n) + # Space: O(1) + def search(self, nums: list[int], target: int) -> int: + left, right = 0, len(nums) - 1 + + while left <= right: + mid = (left + right) // 2 + + if nums[mid] == target: + return mid + + # Left half is sorted + if nums[left] <= nums[mid]: + if nums[left] <= target < nums[mid]: + right = mid - 1 + else: + left = mid + 1 + # Right half is sorted + else: + if nums[mid] < target <= nums[right]: + left = mid + 1 + else: + right = mid - 1 + + return -1 diff --git a/leetcode/search_in_rotated_sorted_array/tests.py b/leetcode/search_in_rotated_sorted_array/tests.py new file mode 100644 index 0000000..90cc27f --- /dev/null +++ b/leetcode/search_in_rotated_sorted_array/tests.py @@ -0,0 +1,41 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestSearchInRotatedSortedArray: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "nums, target, expected", + [ + # Original test cases + ([4, 5, 6, 7, 0, 1, 2], 0, 4), + ([4, 5, 6, 7, 0, 1, 2], 3, -1), + ([1], 0, -1), + ([1], 1, 0), + ([3, 1], 1, 1), + # No rotation (sorted array) + ([1, 2, 3, 4, 5], 3, 2), + ([1, 2, 3, 4, 5], 6, -1), + # Different rotation points + ([6, 7, 0, 1, 2, 4, 5], 0, 2), + ([6, 7, 0, 1, 2, 4, 5], 4, 5), + ([6, 7, 0, 1, 2, 4, 5], 7, 1), + ([2, 3, 4, 5, 6, 7, 0, 1], 0, 6), + # Target at boundaries + ([4, 5, 6, 7, 0, 1, 2], 4, 0), + ([4, 5, 6, 7, 0, 1, 2], 2, 6), + # Two elements + ([2, 1], 1, 1), + ([2, 1], 2, 0), + ([1, 3], 3, 1), + ], + ) + @logged_test + def test_search(self, nums: list[int], target: int, expected: int): + result = self.solution.search(nums, target) + assert result == expected diff --git a/leetcode/spiral_matrix/tests.py b/leetcode/spiral_matrix/tests.py index bb12705..8e3233c 100644 --- a/leetcode/spiral_matrix/tests.py +++ b/leetcode/spiral_matrix/tests.py @@ -12,8 +12,26 @@ def setup_method(self): @pytest.mark.parametrize( "matrix, expected", [ + # Original cases ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], [1, 2, 3, 6, 9, 8, 7, 4, 5]), ([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]), + # Single element + ([[1]], [1]), + # Single row + ([[1, 2, 3, 4]], [1, 2, 3, 4]), + # Single column + ([[1], [2], [3]], [1, 2, 3]), + # 2x2 matrix + ([[1, 2], [3, 4]], [1, 2, 4, 3]), + # 1x2 matrix + ([[1, 2]], [1, 2]), + # 2x1 matrix + ([[1], [2]], [1, 2]), + # Larger square matrix + ( + [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]], + [1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10], + ), ], ) @logged_test diff --git a/leetcode/valid_palindrome/README.md b/leetcode/valid_palindrome/README.md new file mode 100644 index 0000000..e2444ef --- /dev/null +++ b/leetcode/valid_palindrome/README.md @@ -0,0 +1,47 @@ +# Valid Palindrome + +**Difficulty:** Easy +**Topics:** Two Pointers, String +**Tags:** grind-75 + +**LeetCode:** [Problem 125](https://leetcode.com/problems/valid-palindrome/description/) + +## Problem Description + +A phrase is a **palindrome** if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers. + +Given a string `s`, return `true` if it is a **palindrome**, or `false` otherwise. + +## Examples + +### Example 1: + +``` +Input: s = "A man, a plan, a canal: Panama" +Output: true +``` + +**Explanation:** "amanaplanacanalpanama" is a palindrome. + +### Example 2: + +``` +Input: s = "race a car" +Output: false +``` + +**Explanation:** "raceacar" is not a palindrome. + +### Example 3: + +``` +Input: s = " " +Output: true +``` + +**Explanation:** s is an empty string "" after removing non-alphanumeric characters. Since an empty string reads the same forward and backward, it is a palindrome. + +## Constraints + +- `1 <= s.length <= 2 * 10^5` +- `s` consists only of printable ASCII characters. diff --git a/leetcode/valid_palindrome/__init__.py b/leetcode/valid_palindrome/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/leetcode/valid_palindrome/playground.ipynb b/leetcode/valid_palindrome/playground.ipynb new file mode 100644 index 0000000..981f5a4 --- /dev/null +++ b/leetcode/valid_palindrome/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\ns = \"A man, a plan, a canal: Panama\"\nexpected = True"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "execute", + "metadata": {}, + "outputs": [], + "source": ["result = Solution().is_palindrome(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_palindrome/solution.py b/leetcode/valid_palindrome/solution.py new file mode 100644 index 0000000..e2eba71 --- /dev/null +++ b/leetcode/valid_palindrome/solution.py @@ -0,0 +1,19 @@ +class Solution: + # Time: O(n) + # Space: O(1) + def is_palindrome(self, s: str) -> bool: + left, right = 0, len(s) - 1 + + while left < right: + while left < right and not s[left].isalnum(): + left += 1 + while left < right and not s[right].isalnum(): + right -= 1 + + if s[left].lower() != s[right].lower(): + return False + + left += 1 + right -= 1 + + return True diff --git a/leetcode/valid_palindrome/tests.py b/leetcode/valid_palindrome/tests.py new file mode 100644 index 0000000..a029400 --- /dev/null +++ b/leetcode/valid_palindrome/tests.py @@ -0,0 +1,28 @@ +import pytest + +from leetcode_py.test_utils import logged_test + +from .solution import Solution + + +class TestValidPalindrome: + def setup_method(self): + self.solution = Solution() + + @pytest.mark.parametrize( + "s, expected", + [ + ("A man, a plan, a canal: Panama", True), + ("race a car", False), + (" ", True), + ("", True), + ("a", True), + ("Madam", True), + ("No 'x' in Nixon", True), + ("Mr. Owl ate my metal worm", True), + ], + ) + @logged_test + def test_is_palindrome(self, s: str, expected: bool): + result = self.solution.is_palindrome(s) + assert result == expected diff --git a/leetcode_py/data_structures/list_node.py b/leetcode_py/data_structures/list_node.py index c5b5409..0d873b7 100644 --- a/leetcode_py/data_structures/list_node.py +++ b/leetcode_py/data_structures/list_node.py @@ -20,24 +20,111 @@ def from_list(cls, arr: list[T]) -> "ListNode[T] | None": current = current.next return head - def to_list(self) -> list[T]: - result = [] + def _has_cycle(self) -> bool: + """Use Floyd's algorithm to detect if list has a cycle.""" + 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: # Use identity comparison to avoid recursion + return True + return False + + def to_list(self, max_length: int = 1000) -> list[T]: + result: list[T] = [] current: "ListNode[T] | None" = self - while current: + visited: set[int] = set() + + while current and len(result) < max_length: + if id(current) in visited: + # Cycle detected + break + visited.add(id(current)) result.append(current.val) current = current.next return result def __str__(self) -> str: - return " -> ".join(str(val) for val in self.to_list()) + if self._has_cycle(): + # Show cycle with target value + result: list[str] = [] + current: "ListNode[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: - return self.__str__() + """Generate HTML representation using Graphviz for Jupyter notebooks.""" + try: + import graphviz + except ImportError: + return f"
{self.__str__()}
" + + dot = graphviz.Digraph(comment="LinkedList") + dot.attr(rankdir="LR") # Left to right layout + dot.attr("node", shape="box", style="rounded,filled", fillcolor="lightblue") + dot.attr("edge", color="black") + + current: "ListNode[T] | None" = self + visited: dict[int, int] = {} + node_id = 0 + + # First pass: create all nodes and track positions + while current: + if id(current) in visited: + # Cycle detected - add edge back to existing node + 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)) + + # Only add edge if next node exists and we haven't seen it (no cycle) + if current.next and id(current.next) not in visited: + dot.edge(f"node_{node_id}", f"node_{node_id + 1}") + elif current.next and id(current.next) in visited: + # Next iteration will detect cycle, don't add regular edge + pass + + current = current.next + node_id += 1 + + return dot.pipe(format="svg", encoding="utf-8") def __eq__(self, other: object) -> bool: if not isinstance(other, ListNode): return False + + # If either has a cycle, we can't do simple comparison + if self._has_cycle() or other._has_cycle(): + return False # For simplicity, consider cyclic lists as not equal + return self.to_list() == other.to_list() diff --git a/tests/data_structures/test_list_node.py b/tests/data_structures/test_list_node.py index cc1bb9d..233e20f 100644 --- a/tests/data_structures/test_list_node.py +++ b/tests/data_structures/test_list_node.py @@ -84,7 +84,6 @@ def test_string_representations( assert node is not None assert str(node) == expected_str assert repr(node) == expected_repr - assert node._repr_html_() == expected_str @pytest.mark.parametrize( "list1,list2, should_equal", @@ -118,3 +117,49 @@ def test_roundtrip_conversion(self, test_list: list[Any]) -> None: assert node is not None result = node.to_list() assert result == test_list + + def test_has_cycle_no_cycle(self) -> None: + # Test linear list has no cycle + node = ListNode.from_list([1, 2, 3]) + assert node is not None + assert not node._has_cycle() + + def test_has_cycle_with_cycle(self) -> None: + # Create a cycle: 1 -> 2 -> 3 -> 2 (cycle back to node 2) + node1 = ListNode(1) + node2 = ListNode(2) + node3 = ListNode(3) + node1.next = node2 + node2.next = node3 + node3.next = node2 # Create cycle + + assert node1._has_cycle() + + def test_str_with_cycle(self) -> None: + # Create a cycle and test string representation + node1 = ListNode(1) + node2 = ListNode(2) + node1.next = node2 + node2.next = node1 # Create cycle + + result = str(node1) + assert "-> ... (cycle back to 1)" in result + + def test_equality_with_cycles(self) -> None: + # Create two cyclic lists + node1 = ListNode(1) + node2 = ListNode(2) + node1.next = node2 + node2.next = node1 # Create cycle + + node3 = ListNode(1) + node4 = ListNode(2) + node3.next = node4 + node4.next = node3 # Create cycle + + # Cyclic lists should not be equal (for simplicity) + assert node1 != node3 + + # Test cyclic vs non-cyclic + linear_node = ListNode.from_list([1, 2]) + assert node1 != linear_node From 9fd855460d95ad608d607ee48a9f99b0a40c7728 Mon Sep 17 00:00:00 2001 From: Wisaroot <66859294+wisarootl@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:10:50 +0700 Subject: [PATCH 04/68] feat: add Longest Palindromic Substring (#23) --- .amazonq/rules/problem-creation.md | 14 +- .templates/leetcode/examples/README.md | 136 ++++-------------- .templates/leetcode/examples/basic.json5 | 17 ++- .templates/leetcode/examples/design.json5 | 3 +- .../leetcode/examples/linked_list.json5 | 78 ---------- .templates/leetcode/examples/matrix.json5 | 75 ---------- .templates/leetcode/examples/tree.json5 | 82 ----------- .templates/leetcode/json/accounts_merge.json | 47 ++++++ .../json/longest_palindromic_substring.json | 45 ++++++ Makefile | 2 +- leetcode/accounts_merge/README.md | 41 ++++++ leetcode/accounts_merge/__init__.py | 0 leetcode/accounts_merge/playground.ipynb | 93 ++++++++++++ leetcode/accounts_merge/solution.py | 34 +++++ leetcode/accounts_merge/tests.py | 112 +++++++++++++++ .../longest_palindromic_substring/README.md | 34 +++++ .../longest_palindromic_substring/__init__.py | 0 .../playground.ipynb | 79 ++++++++++ .../longest_palindromic_substring/solution.py | 52 +++++++ .../longest_palindromic_substring/tests.py | 74 ++++++++++ tests/data_structures/test_graph_node.py | 81 +++++++++++ tests/data_structures/test_list_node.py | 69 +++++++++ 22 files changed, 819 insertions(+), 349 deletions(-) delete mode 100644 .templates/leetcode/examples/linked_list.json5 delete mode 100644 .templates/leetcode/examples/matrix.json5 delete mode 100644 .templates/leetcode/examples/tree.json5 create mode 100644 .templates/leetcode/json/accounts_merge.json create mode 100644 .templates/leetcode/json/longest_palindromic_substring.json create mode 100644 leetcode/accounts_merge/README.md create mode 100644 leetcode/accounts_merge/__init__.py create mode 100644 leetcode/accounts_merge/playground.ipynb create mode 100644 leetcode/accounts_merge/solution.py create mode 100644 leetcode/accounts_merge/tests.py create mode 100644 leetcode/longest_palindromic_substring/README.md create mode 100644 leetcode/longest_palindromic_substring/__init__.py create mode 100644 leetcode/longest_palindromic_substring/playground.ipynb create mode 100644 leetcode/longest_palindromic_substring/solution.py create mode 100644 leetcode/longest_palindromic_substring/tests.py diff --git a/.amazonq/rules/problem-creation.md b/.amazonq/rules/problem-creation.md index 2fc2a30..a673a25 100644 --- a/.amazonq/rules/problem-creation.md +++ b/.amazonq/rules/problem-creation.md @@ -32,13 +32,19 @@ poetry run python .templates/leetcode/scrape.py -s "two-sum" Required fields for `.templates/leetcode/json/{problem_name}.json`: +**CRITICAL: Use single quotes for Python strings in playground fields to avoid JSON escaping issues with Jupyter notebooks.** + +**JSON Escaping Rules:** + +- `playground_test_case`: Use single quotes for string literals (e.g., `s = 'hello'` not `s = "hello"`) +- `playground_execution`: Use single quotes for string literals +- `playground_assertion`: Use single quotes for string literals +- Double quotes in JSON + cookiecutter + Jupyter notebook = triple escaping issues + **Reference examples in `.templates/leetcode/examples/` for complete templates:** -- `basic.json5` - Array, string, number problems +- `basic.json5` - All standard problems (array, string, tree, linked list, etc.) - `design.json5` - Data structure design problems (LRU Cache, etc.) -- `tree.json5` - Binary tree problems -- `linked_list.json5` - Linked list problems -- `matrix.json5` - 2D array/matrix problems ````json { diff --git a/.templates/leetcode/examples/README.md b/.templates/leetcode/examples/README.md index 6dbfef8..df7d99b 100644 --- a/.templates/leetcode/examples/README.md +++ b/.templates/leetcode/examples/README.md @@ -1,117 +1,41 @@ -# LeetCode Problem Template Examples +# JSON Template Examples -This directory contains comprehensive JSON5 template examples for different types of LeetCode problems. These examples serve as references when creating new problems using the universal cookiecutter template. +This directory contains comprehensive examples for creating LeetCode problem templates. -## Template Types +## Files -### 1. `basic.json5` - Basic Algorithm Problems +- **`basic.json5`** - Covers all standard problem types: + - Array problems (Container With Most Water) + - String problems (with JSON escaping notes) + - Tree problems (import and parameter examples) + - Linked list problems (import and parameter examples) + - Matrix problems + - Number problems -**Use for:** Array, string, number, hash table problems -**Examples:** Container With Most Water, Two Sum, Valid Palindrome -**Key features:** +- **`design.json5`** - Data structure design problems: + - Custom class names (LRUCache, not Solution) + - Multiple methods including `__init__` + - Complex test setup with operation sequences + - Custom imports -- Simple `Solution` class with single method -- Standard test parametrization -- Basic playground setup +## Key Differences -### 2. `tree.json5` - Binary Tree Problems +### Standard Problems (basic.json5) -**Use for:** Binary tree, BST, tree traversal problems -**Examples:** Invert Binary Tree, Maximum Depth, Serialize Tree -**Key features:** +- `solution_class_name`: Always "Solution" +- Single method (usually) +- Simple test cases with direct assertions +- Standard imports -- `TreeNode` imports and conversions -- `TreeNode.from_list()` and `TreeNode.to_list()` in tests -- Tree visualization support +### Design Problems (design.json5) -### 3. `linked_list.json5` - Linked List Problems +- `solution_class_name`: Custom class name (e.g., "LRUCache") +- Multiple methods including constructor +- Operation sequence testing +- Import custom class in tests -**Use for:** Singly/doubly linked list problems -**Examples:** Reverse Linked List, Merge Lists, Detect Cycle -**Key features:** +## Critical Notes -- `ListNode` imports and conversions -- `ListNode.from_list()` and `ListNode.to_list()` in tests -- Arrow visualization support - -### 4. `design.json5` - Data Structure Design Problems - -**Use for:** Design problems requiring custom classes -**Examples:** LRU Cache, Implement Trie, Design HashMap -**Key features:** - -- Custom class names (not `Solution`) -- Multiple methods including `__init__` -- Complex operation sequence testing -- Type annotations for complex test logic - -### 5. `matrix.json5` - 2D Array/Matrix Problems - -**Use for:** Matrix manipulation, 2D array problems -**Examples:** Spiral Matrix, Rotate Image, Search 2D Matrix -**Key features:** - -- 2D array type annotations (`list[list[int]]`) -- Visual examples with images -- Matrix-specific test cases - -## Usage Guidelines - -### Problem Type Detection - -1. **Basic**: Single algorithm, simple input/output -2. **Tree**: Mentions "tree", "node", uses tree terminology -3. **Linked List**: Mentions "linked list", "node", list operations -4. **Design**: "Design", "Implement", multiple operations -5. **Matrix**: "matrix", "2D array", "grid", visual layout - -### Key Template Fields - -#### Required Fields - -- `problem_name`: snake_case identifier -- `solution_class_name`: "Solution" or custom class name -- `problem_number`: LeetCode number as string -- `problem_title`: Exact LeetCode title -- `difficulty`: "Easy", "Medium", or "Hard" -- `topics`: Comma-separated topic string -- `solution_methods`: Array of method definitions - -#### Important Patterns - -- **Type Hints**: Use modern syntax (`list[int]`, `dict[str, int]`, `Type | None`) -- **Method Names**: Always snake_case -- **Test Cases**: String representation of Python data structures -- **Imports**: Include necessary helper classes (TreeNode, ListNode) - -#### PascalCase Naming Rules - -For `solution_class_name` and `test_class_name` properties: - -- **Acronyms**: Keep all caps ("LRUCache" not "LruCache") -- **Roman numerals**: Keep all caps ("ReverseLinkedListII" not "ReverseLinkedListIi") -- **Common patterns**: "BST", "DFS", "BFS", "API", "URL", "HTML", "JSON", "XML" - -### Template Selection Process - -1. Identify problem type from description/title -2. Choose appropriate template from examples -3. Customize fields for specific problem -4. Ensure imports match problem requirements -5. Verify test setup matches data structures used - -## Validation - -All templates are validated against: - -- Cookiecutter template compatibility -- Linting requirements (black, isort, ruff, mypy) -- Test framework integration -- Notebook JSON format compliance - -## Notes - -- JSON5 format allows comments for documentation -- All examples are based on working, tested templates -- Templates are designed for the universal cookiecutter system -- Examples include both simple and complex problem patterns +- **JSON Escaping**: Use single quotes for Python strings in playground fields +- **Type Hints**: Use modern syntax (`list[int]`, `TreeNode | None`) +- **PascalCase**: Keep acronyms ALL CAPS (LRUCache, ReverseLinkedListII) diff --git a/.templates/leetcode/examples/basic.json5 b/.templates/leetcode/examples/basic.json5 index c00a02f..391c7c6 100644 --- a/.templates/leetcode/examples/basic.json5 +++ b/.templates/leetcode/examples/basic.json5 @@ -37,13 +37,22 @@ "readme_additional": "", // Optional: additional notes, follow-up questions // === SOLUTION TEMPLATE === - "solution_imports": "", // Empty for basic problems, add imports if needed + "solution_imports": "", // Empty for basic problems + // For tree: "from leetcode_py import TreeNode" + // For linked list: "from leetcode_py import ListNode" "solution_methods": [ { "name": "max_area", // snake_case method name "parameters": "height: list[int]", // Modern Python type hints (list[int], not List[int]) + // For tree: "root: TreeNode | None" + // For linked list: "head: ListNode | None" + // For string: "s: str" "return_type": "int", // Return type annotation - "dummy_return": "0" // Default return value (auto-set by generator) + "dummy_return": "0" // Default return value + // For string: "\"\"" + // For bool: "False" + // For list: "[]" + // For tree/linked list: "None" } ], @@ -68,8 +77,12 @@ ], // === PLAYGROUND NOTEBOOK === + // CRITICAL: Use single quotes for Python strings to avoid JSON escaping issues with Jupyter notebooks + // Double quotes in JSON + cookiecutter + Jupyter notebook = triple escaping issues "playground_imports": "from solution import Solution", "playground_test_case": "# Example test case\nheight = [1,8,6,2,5,4,8,3,7]\nexpected = 49", + // For string problems: "s = 'hello'\nexpected = 'olleh'" + // For tree: "root_list = [3,9,20,None,None,15,7]\nroot = TreeNode.from_list(root_list)" "playground_execution": "result = Solution().max_area(height)\nresult", "playground_assertion": "assert result == expected" } diff --git a/.templates/leetcode/examples/design.json5 b/.templates/leetcode/examples/design.json5 index 9839fc7..dbe2096 100644 --- a/.templates/leetcode/examples/design.json5 +++ b/.templates/leetcode/examples/design.json5 @@ -75,7 +75,8 @@ ], // === PLAYGROUND NOTEBOOK === - // IMPORTANT: Design playground uses operation sequences like tests + // CRITICAL: Use single quotes for Python strings to avoid JSON escaping issues with Jupyter notebooks + // Double quotes in JSON + cookiecutter + Jupyter notebook = triple escaping issues playground_imports: "from solution import LRUCache", playground_test_case: "# Example test case\noperations = ['LRUCache', 'put', 'put', 'get', 'put', 'get', 'put', 'get', 'get', 'get']\ninputs = [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]\nexpected = [None, None, None, 1, None, -1, None, -1, 3, 4]", playground_execution: "cache = None\nresults: list[int | None] = []\nfor i, op in enumerate(operations):\n if op == 'LRUCache':\n cache = LRUCache(inputs[i][0])\n results.append(None)\n elif op == 'get' and cache is not None:\n results.append(cache.get(inputs[i][0]))\n elif op == 'put' and cache is not None:\n cache.put(inputs[i][0], inputs[i][1])\n results.append(None)\nresults", diff --git a/.templates/leetcode/examples/linked_list.json5 b/.templates/leetcode/examples/linked_list.json5 deleted file mode 100644 index f0a77b2..0000000 --- a/.templates/leetcode/examples/linked_list.json5 +++ /dev/null @@ -1,78 +0,0 @@ -{ - // Linked List problem template - for linked list problems - // Example: Reverse Linked List II - // Key differences: ListNode imports, list-specific test setup - // NOTE: PascalCase naming - keep acronyms/Roman numerals ALL CAPS (LRUCache, ReverseLinkedListII) - - // === PROBLEM IDENTIFICATION === - problem_name: "reverse_linked_list_ii", // snake_case: used for directory/file names - solution_class_name: "Solution", // Always "Solution" for algorithm problems - problem_number: "92", // LeetCode problem number as string - problem_title: "Reverse Linked List II", // Exact title from LeetCode - difficulty: "Medium", // Easy, Medium, Hard - topics: "Linked List", // Linked list related topics - tags: ["grind-75"], // Optional: common problem set tags - - // === README CONTENT === - // IMPORTANT: Preserve rich HTML content from LeetCode including: - // - Code snippets with backticks: `code` - // - Bold text: **bold** or bold - // - Italic text: *italic* or italic - // - Images: tags with proper src and styling - // - HTML formatting:

,
,