Skip to content

Commit 4d81400

Browse files
committed
feat: add lowest_common_ancestor_of_a_binary_tree
1 parent 9199d8c commit 4d81400

File tree

12 files changed

+575
-55
lines changed

12 files changed

+575
-55
lines changed

.templates/leetcode/json/lowest_common_ancestor_of_a_binary_search_tree.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,19 @@
3030
"test_imports": "import pytest\nfrom leetcode_py import TreeNode\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
3131
"test_class_name": "LowestCommonAncestorOfABinarySearchTree",
3232
"test_helper_methods": [
33-
{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" },
34-
{
35-
"name": "_find_node",
36-
"parameters": "root: TreeNode[int], val: int",
37-
"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)"
38-
}
33+
{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
3934
],
4035
"test_methods": [
4136
{
4237
"name": "test_lowest_common_ancestor",
4338
"parametrize": "root_list, p_val, q_val, expected_val",
4439
"parametrize_typed": "root_list: list[int | None], p_val: int, q_val: int, expected_val: int",
4540
"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)]",
46-
"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"
41+
"body": "root = TreeNode[int].from_list(root_list)\nassert root is not None\np = root.find_node(p_val)\nq = root.find_node(q_val)\nassert p is not None and q is not None\nresult = self.solution.lowest_common_ancestor(root, p, q)\nassert result is not None\nassert result.val == expected_val"
4742
}
4843
],
4944
"playground_imports": "from leetcode_py import TreeNode\nfrom solution import Solution",
5045
"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",
51-
"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",
46+
"playground_execution": "root = TreeNode[int].from_list(root_list)\nassert root is not None\np = root.find_node(p_val)\nq = root.find_node(q_val)\nassert p is not None and q is not None\nresult = Solution().lowest_common_ancestor(root, p, q)\nresult.val if result else None",
5247
"playground_assertion": "assert result and result.val == expected_val"
5348
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"problem_name": "lowest_common_ancestor_of_a_binary_tree",
3+
"solution_class_name": "Solution",
4+
"problem_number": "236",
5+
"problem_title": "Lowest Common Ancestor of a Binary Tree",
6+
"difficulty": "Medium",
7+
"topics": "Tree, Depth-First Search, Binary Tree",
8+
"tags": ["grind-75"],
9+
"readme_description": "Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.\n\nAccording to the definition of LCA on Wikipedia: \"The lowest common ancestor is defined between two nodes `p` and `q` as the lowest node in `T` that has both `p` and `q` as descendants (where we allow **a node to be a descendant of itself**).\"",
10+
"readme_examples": [
11+
{
12+
"content": "<img alt=\"\" src=\"https://assets.leetcode.com/uploads/2018/12/14/binarytree.png\" style=\"width: 200px; height: 190px;\" />\n\n```\nInput: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1\nOutput: 3\nExplanation: The LCA of nodes 5 and 1 is 3.\n```"
13+
},
14+
{
15+
"content": "<img alt=\"\" src=\"https://assets.leetcode.com/uploads/2018/12/14/binarytree.png\" style=\"width: 200px; height: 190px;\" />\n\n```\nInput: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4\nOutput: 5\nExplanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.\n```"
16+
},
17+
{ "content": "```\nInput: root = [1,2], p = 1, q = 2\nOutput: 1\n```" }
18+
],
19+
"readme_constraints": "- The number of nodes in the tree is in the range [2, 10^5].\n- -10^9 <= Node.val <= 10^9\n- All Node.val are unique.\n- p != q\n- p and q will exist in the tree.",
20+
"readme_additional": "",
21+
"solution_imports": "from leetcode_py import TreeNode",
22+
"solution_methods": [
23+
{
24+
"name": "lowest_common_ancestor",
25+
"parameters": "root: TreeNode, p: TreeNode, q: TreeNode",
26+
"return_type": "TreeNode",
27+
"dummy_return": "root"
28+
}
29+
],
30+
"test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom leetcode_py import TreeNode\nfrom .solution import Solution",
31+
"test_class_name": "LowestCommonAncestorOfABinaryTree",
32+
"test_helper_methods": [
33+
{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
34+
],
35+
"test_methods": [
36+
{
37+
"name": "test_lowest_common_ancestor",
38+
"parametrize": "root_list, p_val, q_val, expected_val",
39+
"parametrize_typed": "root_list: list[int | None], p_val: int, q_val: int, expected_val: int",
40+
"test_cases": "[([3,5,1,6,2,0,8,None,None,7,4], 5, 1, 3), ([3,5,1,6,2,0,8,None,None,7,4], 5, 4, 5), ([1,2], 1, 2, 1)]",
41+
"body": "root = TreeNode.from_list(root_list)\nassert root is not None\np = root.find_node(p_val)\nq = root.find_node(q_val)\nassert p is not None and q is not None\nresult = self.solution.lowest_common_ancestor(root, p, q)\nassert result is not None\nassert result.val == expected_val"
42+
}
43+
],
44+
"playground_imports": "from solution import Solution\nfrom leetcode_py import TreeNode",
45+
"playground_test_case": "# Example test case\nroot_list = [3,5,1,6,2,0,8,None,None,7,4]\nroot = TreeNode.from_list(root_list)\nassert root is not None\np = root.find_node(5)\nq = root.find_node(1)\nexpected_val = 3",
46+
"playground_execution": "result = Solution().lowest_common_ancestor(root, p, q)\nresult.val",
47+
"playground_assertion": "assert result.val == expected_val"
48+
}

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PYTHON_VERSION = 3.13
2-
PROBLEM ?= product_of_array_except_self
2+
PROBLEM ?= lowest_common_ancestor_of_a_binary_search_tree
33
FORCE ?= 0
44
COMMA := ,
55

leetcode/lowest_common_ancestor_of_a_binary_search_tree/playground.ipynb

Lines changed: 158 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": null,
5+
"execution_count": 1,
66
"id": "imports",
77
"metadata": {},
88
"outputs": [],
@@ -14,7 +14,7 @@
1414
},
1515
{
1616
"cell_type": "code",
17-
"execution_count": null,
17+
"execution_count": 2,
1818
"id": "setup",
1919
"metadata": {},
2020
"outputs": [],
@@ -28,21 +28,171 @@
2828
},
2929
{
3030
"cell_type": "code",
31-
"execution_count": null,
31+
"execution_count": 3,
3232
"id": "execute",
3333
"metadata": {},
34-
"outputs": [],
34+
"outputs": [
35+
{
36+
"data": {
37+
"text/plain": [
38+
"6"
39+
]
40+
},
41+
"execution_count": 3,
42+
"metadata": {},
43+
"output_type": "execute_result"
44+
}
45+
],
3546
"source": [
3647
"root = TreeNode[int].from_list(root_list)\n",
37-
"p = find_node(root, p_val)\n",
38-
"q = find_node(root, q_val)\n",
48+
"assert root is not None\n",
49+
"p = root.find_node(p_val)\n",
50+
"q = root.find_node(q_val)\n",
51+
"assert p is not None and q is not None\n",
3952
"result = Solution().lowest_common_ancestor(root, p, q)\n",
4053
"result.val if result else None"
4154
]
4255
},
4356
{
4457
"cell_type": "code",
45-
"execution_count": null,
58+
"execution_count": 5,
59+
"id": "d84494ee",
60+
"metadata": {},
61+
"outputs": [
62+
{
63+
"data": {
64+
"text/html": [
65+
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
66+
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
67+
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
68+
"<!-- Generated by graphviz version 13.1.2 (20250808.2320)\n",
69+
" -->\n",
70+
"<!-- Pages: 1 -->\n",
71+
"<svg width=\"278pt\" height=\"260pt\"\n",
72+
" viewBox=\"0.00 0.00 278.00 260.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
73+
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 256)\">\n",
74+
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-256 274,-256 274,4 -4,4\"/>\n",
75+
"<!-- 0 -->\n",
76+
"<g id=\"node1\" class=\"node\">\n",
77+
"<title>0</title>\n",
78+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"135\" cy=\"-234\" rx=\"27\" ry=\"18\"/>\n",
79+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"135\" y=\"-228.95\" font-family=\"Times,serif\" font-size=\"14.00\">6</text>\n",
80+
"</g>\n",
81+
"<!-- 1 -->\n",
82+
"<g id=\"node2\" class=\"node\">\n",
83+
"<title>1</title>\n",
84+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"99\" cy=\"-162\" rx=\"27\" ry=\"18\"/>\n",
85+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"99\" y=\"-156.95\" font-family=\"Times,serif\" font-size=\"14.00\">2</text>\n",
86+
"</g>\n",
87+
"<!-- 0&#45;&gt;1 -->\n",
88+
"<g id=\"edge1\" class=\"edge\">\n",
89+
"<title>0&#45;&gt;1</title>\n",
90+
"<path fill=\"none\" stroke=\"black\" d=\"M126.65,-216.76C122.42,-208.55 117.19,-198.37 112.42,-189.09\"/>\n",
91+
"<polygon fill=\"black\" stroke=\"black\" points=\"115.68,-187.79 108,-180.49 109.46,-190.99 115.68,-187.79\"/>\n",
92+
"</g>\n",
93+
"<!-- 6 -->\n",
94+
"<g id=\"node7\" class=\"node\">\n",
95+
"<title>6</title>\n",
96+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"171\" cy=\"-162\" rx=\"27\" ry=\"18\"/>\n",
97+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"171\" y=\"-156.95\" font-family=\"Times,serif\" font-size=\"14.00\">8</text>\n",
98+
"</g>\n",
99+
"<!-- 0&#45;&gt;6 -->\n",
100+
"<g id=\"edge6\" class=\"edge\">\n",
101+
"<title>0&#45;&gt;6</title>\n",
102+
"<path fill=\"none\" stroke=\"black\" d=\"M143.35,-216.76C147.58,-208.55 152.81,-198.37 157.58,-189.09\"/>\n",
103+
"<polygon fill=\"black\" stroke=\"black\" points=\"160.54,-190.99 162,-180.49 154.32,-187.79 160.54,-190.99\"/>\n",
104+
"</g>\n",
105+
"<!-- 2 -->\n",
106+
"<g id=\"node3\" class=\"node\">\n",
107+
"<title>2</title>\n",
108+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"27\" cy=\"-90\" rx=\"27\" ry=\"18\"/>\n",
109+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"27\" y=\"-84.95\" font-family=\"Times,serif\" font-size=\"14.00\">0</text>\n",
110+
"</g>\n",
111+
"<!-- 1&#45;&gt;2 -->\n",
112+
"<g id=\"edge2\" class=\"edge\">\n",
113+
"<title>1&#45;&gt;2</title>\n",
114+
"<path fill=\"none\" stroke=\"black\" d=\"M84.08,-146.5C74.23,-136.92 61.14,-124.19 49.97,-113.34\"/>\n",
115+
"<polygon fill=\"black\" stroke=\"black\" points=\"52.59,-111 42.98,-106.54 47.71,-116.02 52.59,-111\"/>\n",
116+
"</g>\n",
117+
"<!-- 3 -->\n",
118+
"<g id=\"node4\" class=\"node\">\n",
119+
"<title>3</title>\n",
120+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"99\" cy=\"-90\" rx=\"27\" ry=\"18\"/>\n",
121+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"99\" y=\"-84.95\" font-family=\"Times,serif\" font-size=\"14.00\">4</text>\n",
122+
"</g>\n",
123+
"<!-- 1&#45;&gt;3 -->\n",
124+
"<g id=\"edge3\" class=\"edge\">\n",
125+
"<title>1&#45;&gt;3</title>\n",
126+
"<path fill=\"none\" stroke=\"black\" d=\"M99,-143.7C99,-136.41 99,-127.73 99,-119.54\"/>\n",
127+
"<polygon fill=\"black\" stroke=\"black\" points=\"102.5,-119.62 99,-109.62 95.5,-119.62 102.5,-119.62\"/>\n",
128+
"</g>\n",
129+
"<!-- 4 -->\n",
130+
"<g id=\"node5\" class=\"node\">\n",
131+
"<title>4</title>\n",
132+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"63\" cy=\"-18\" rx=\"27\" ry=\"18\"/>\n",
133+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"63\" y=\"-12.95\" font-family=\"Times,serif\" font-size=\"14.00\">3</text>\n",
134+
"</g>\n",
135+
"<!-- 3&#45;&gt;4 -->\n",
136+
"<g id=\"edge4\" class=\"edge\">\n",
137+
"<title>3&#45;&gt;4</title>\n",
138+
"<path fill=\"none\" stroke=\"black\" d=\"M90.65,-72.76C86.42,-64.55 81.19,-54.37 76.42,-45.09\"/>\n",
139+
"<polygon fill=\"black\" stroke=\"black\" points=\"79.68,-43.79 72,-36.49 73.46,-46.99 79.68,-43.79\"/>\n",
140+
"</g>\n",
141+
"<!-- 5 -->\n",
142+
"<g id=\"node6\" class=\"node\">\n",
143+
"<title>5</title>\n",
144+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"135\" cy=\"-18\" rx=\"27\" ry=\"18\"/>\n",
145+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"135\" y=\"-12.95\" font-family=\"Times,serif\" font-size=\"14.00\">5</text>\n",
146+
"</g>\n",
147+
"<!-- 3&#45;&gt;5 -->\n",
148+
"<g id=\"edge5\" class=\"edge\">\n",
149+
"<title>3&#45;&gt;5</title>\n",
150+
"<path fill=\"none\" stroke=\"black\" d=\"M107.35,-72.76C111.58,-64.55 116.81,-54.37 121.58,-45.09\"/>\n",
151+
"<polygon fill=\"black\" stroke=\"black\" points=\"124.54,-46.99 126,-36.49 118.32,-43.79 124.54,-46.99\"/>\n",
152+
"</g>\n",
153+
"<!-- 7 -->\n",
154+
"<g id=\"node8\" class=\"node\">\n",
155+
"<title>7</title>\n",
156+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"171\" cy=\"-90\" rx=\"27\" ry=\"18\"/>\n",
157+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"171\" y=\"-84.95\" font-family=\"Times,serif\" font-size=\"14.00\">7</text>\n",
158+
"</g>\n",
159+
"<!-- 6&#45;&gt;7 -->\n",
160+
"<g id=\"edge7\" class=\"edge\">\n",
161+
"<title>6&#45;&gt;7</title>\n",
162+
"<path fill=\"none\" stroke=\"black\" d=\"M171,-143.7C171,-136.41 171,-127.73 171,-119.54\"/>\n",
163+
"<polygon fill=\"black\" stroke=\"black\" points=\"174.5,-119.62 171,-109.62 167.5,-119.62 174.5,-119.62\"/>\n",
164+
"</g>\n",
165+
"<!-- 8 -->\n",
166+
"<g id=\"node9\" class=\"node\">\n",
167+
"<title>8</title>\n",
168+
"<ellipse fill=\"none\" stroke=\"black\" cx=\"243\" cy=\"-90\" rx=\"27\" ry=\"18\"/>\n",
169+
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"243\" y=\"-84.95\" font-family=\"Times,serif\" font-size=\"14.00\">9</text>\n",
170+
"</g>\n",
171+
"<!-- 6&#45;&gt;8 -->\n",
172+
"<g id=\"edge8\" class=\"edge\">\n",
173+
"<title>6&#45;&gt;8</title>\n",
174+
"<path fill=\"none\" stroke=\"black\" d=\"M185.92,-146.5C195.77,-136.92 208.86,-124.19 220.03,-113.34\"/>\n",
175+
"<polygon fill=\"black\" stroke=\"black\" points=\"222.29,-116.02 227.02,-106.54 217.41,-111 222.29,-116.02\"/>\n",
176+
"</g>\n",
177+
"</g>\n",
178+
"</svg>\n"
179+
],
180+
"text/plain": [
181+
"TreeNode([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5])"
182+
]
183+
},
184+
"execution_count": 5,
185+
"metadata": {},
186+
"output_type": "execute_result"
187+
}
188+
],
189+
"source": [
190+
"root"
191+
]
192+
},
193+
{
194+
"cell_type": "code",
195+
"execution_count": 4,
46196
"id": "test",
47197
"metadata": {},
48198
"outputs": [],
@@ -65,7 +215,7 @@
65215
"file_extension": ".py",
66216
"mimetype": "text/x-python",
67217
"name": "python",
68-
"nbconvert_exporter": "python3",
218+
"nbconvert_exporter": "python",
69219
"pygments_lexer": "ipython3",
70220
"version": "3.13.7"
71221
}

leetcode/lowest_common_ancestor_of_a_binary_search_tree/tests.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ class TestLowestCommonAncestorOfABinarySearchTree:
1010
def setup_method(self):
1111
self.solution = Solution()
1212

13-
def _find_node(self, root: TreeNode[int] | None, val: int):
14-
if not root:
15-
return None
16-
if root.val == val:
17-
return root
18-
left = self._find_node(root.left, val)
19-
if left:
20-
return left
21-
return self._find_node(root.right, val)
22-
2313
@pytest.mark.parametrize(
2414
"root_list, p_val, q_val, expected_val",
2515
[
@@ -37,8 +27,8 @@ def test_lowest_common_ancestor(
3727
):
3828
root = TreeNode[int].from_list(root_list)
3929
assert root is not None
40-
p = self._find_node(root, p_val)
41-
q = self._find_node(root, q_val)
30+
p = root.find_node(p_val)
31+
q = root.find_node(q_val)
4232
assert p is not None and q is not None
4333
result = self.solution.lowest_common_ancestor(root, p, q)
4434
assert result is not None
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Lowest Common Ancestor of a Binary Tree
2+
3+
**Difficulty:** Medium
4+
**Topics:** Tree, Depth-First Search, Binary Tree
5+
**Tags:** grind-75
6+
7+
**LeetCode:** [Problem 236](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/)
8+
9+
## Problem Description
10+
11+
Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
12+
13+
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**)."
14+
15+
## Examples
16+
17+
### Example 1:
18+
19+
<img alt="" src="https://assets.leetcode.com/uploads/2018/12/14/binarytree.png" style="width: 200px; height: 190px;" />
20+
21+
```
22+
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
23+
Output: 3
24+
Explanation: The LCA of nodes 5 and 1 is 3.
25+
```
26+
27+
### Example 2:
28+
29+
<img alt="" src="https://assets.leetcode.com/uploads/2018/12/14/binarytree.png" style="width: 200px; height: 190px;" />
30+
31+
```
32+
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
33+
Output: 5
34+
Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.
35+
```
36+
37+
### Example 3:
38+
39+
```
40+
Input: root = [1,2], p = 1, q = 2
41+
Output: 1
42+
```
43+
44+
## Constraints
45+
46+
- The number of nodes in the tree is in the range [2, 10^5].
47+
- -10^9 <= Node.val <= 10^9
48+
- All Node.val are unique.
49+
- p != q
50+
- p and q will exist in the tree.

leetcode/lowest_common_ancestor_of_a_binary_tree/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)