Files
codetutor/backend/data/questions/convert-sorted-array-to-binary-search-tree.yaml
2025-05-25 10:16:13 +01:00

214 lines
9.0 KiB
YAML

title: Convert Sorted Array to Binary Search Tree
slug: convert-sorted-array-to-binary-search-tree
difficulty: easy
leetcode_id: 108
leetcode_url: https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/
categories:
- arrays
- trees
- recursion
patterns:
- binary-search
- dfs
description: |
Given an integer array `nums` where the elements are sorted in **ascending order**, convert *it to a* ***height-balanced*** *binary search tree*.
A **height-balanced** binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one.
constraints: |
- `1 <= nums.length <= 10^4`
- `-10^4 <= nums[i] <= 10^4`
- `nums` is sorted in a **strictly increasing** order
examples:
- input: "nums = [-10,-3,0,5,9]"
output: "[0,-3,9,-10,null,5]"
explanation: "[0,-10,5,null,-3,null,9] is also accepted. Both represent valid height-balanced BSTs."
- input: "nums = [1,3]"
output: "[3,1]"
explanation: "[1,null,3] and [3,1] are both height-balanced BSTs."
explanation:
intuition: |
Think of a sorted array as a flattened BST — specifically, the **inorder traversal** of a BST produces a sorted sequence. Our task is to reverse this process: reconstruct a balanced BST from its sorted elements.
The key insight comes from how BSTs work: the **middle element** of a sorted array naturally becomes the **root** of a balanced tree. Why? Because half the elements are smaller (they go in the left subtree) and half are larger (they go in the right subtree). This equal distribution is exactly what makes a tree height-balanced.
Imagine you're organising books on a balanced shelf. If you pick the middle book as your centre point, you'll have roughly the same number of books on each side. Now recursively apply this same logic to each side — pick the middle of the left half for the left shelf's centre, and the middle of the right half for the right shelf's centre.
This "divide and conquer" approach naturally produces a balanced structure because at every level of recursion, we're splitting our remaining elements as evenly as possible.
approach: |
We solve this using a **Recursive Divide and Conquer** approach:
**Step 1: Define the recursive function**
- Create a helper function that takes the left and right boundaries of the current subarray
- The function will return the root of the subtree built from elements in `nums[left..right]`
&nbsp;
**Step 2: Base case**
- If `left > right`, we've exhausted this subarray — return `None` (empty subtree)
&nbsp;
**Step 3: Find the middle element**
- Calculate `mid = (left + right) // 2` (or `left + (right - left) // 2` to avoid overflow in other languages)
- The element at `nums[mid]` becomes the root of this subtree
&nbsp;
**Step 4: Recursively build subtrees**
- Left subtree: recursively process `nums[left..mid-1]`
- Right subtree: recursively process `nums[mid+1..right]`
- Attach these as the left and right children of the current root
&nbsp;
**Step 5: Return the root**
- Return the constructed node, which connects upward to its parent (or is the final tree root)
&nbsp;
This approach guarantees a height-balanced tree because we always choose the middle element, ensuring each subtree has at most one more element than its sibling.
common_pitfalls:
- title: Off-by-One Errors in Boundaries
description: |
When setting up recursive boundaries, it's easy to make mistakes:
- Left subtree should use `[left, mid - 1]`, not `[left, mid]` (which would include the root again)
- Right subtree should use `[mid + 1, right]`, not `[mid, right]`
Including the middle element in a subtree creates duplicates and infinite recursion.
wrong_approach: "Using [left, mid] for left subtree"
correct_approach: "Using [left, mid - 1] for left subtree"
- title: Forgetting the Base Case
description: |
Without a proper base case (`left > right`), the recursion never terminates. This causes a stack overflow.
The base case handles empty subarrays — when there are no elements left to process in a branch, we return `None` to represent an empty child.
wrong_approach: "No base case or incorrect condition"
correct_approach: "Return None when left > right"
- title: Creating an Unbalanced Tree
description: |
If you don't choose the middle element as the root, you'll create an unbalanced tree.
For example, always choosing the first element creates a right-skewed tree (like a linked list), which defeats the purpose of a BST for efficient operations.
With `nums = [1, 2, 3, 4, 5]`, choosing `1` as root puts all other elements in the right subtree, giving height 4 instead of 2.
wrong_approach: "Using first or last element as root"
correct_approach: "Always use the middle element as root"
key_takeaways:
- "**Divide and conquer**: Split problems into smaller subproblems by choosing a pivot (middle element) and recursing on each half"
- "**BST property from sorted input**: The middle of a sorted array is the natural root for a balanced BST"
- "**Foundation for tree construction**: This technique extends to problems like converting sorted linked lists to BSTs, or building balanced trees from other traversals"
- "**Logarithmic height guarantee**: By always splitting evenly, the tree height is `O(log n)`, enabling efficient search, insert, and delete operations"
time_complexity: "O(n). We visit each element exactly once to create its corresponding tree node."
space_complexity: "O(log n). The recursion stack depth equals the tree height, which is `O(log n)` for a balanced tree. The output tree uses O(n) space, but that's required by the problem."
solutions:
- approach_name: Recursive Divide and Conquer
is_optimal: true
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def sorted_array_to_bst(nums: list[int]) -> TreeNode | None:
def build_tree(left: int, right: int) -> TreeNode | None:
# Base case: no elements in this range
if left > right:
return None
# Choose middle element as root for balance
mid = (left + right) // 2
# Create node with middle element
node = TreeNode(nums[mid])
# Recursively build left subtree from left half
node.left = build_tree(left, mid - 1)
# Recursively build right subtree from right half
node.right = build_tree(mid + 1, right)
return node
return build_tree(0, len(nums) - 1)
explanation: |
**Time Complexity:** O(n) — Each element is visited exactly once.
**Space Complexity:** O(log n) — Recursion stack depth for a balanced tree.
We use the classic divide-and-conquer pattern: select the middle element as root, then recursively construct left and right subtrees from the remaining elements. This guarantees the tree is height-balanced.
- approach_name: Iterative with Stack
is_optimal: false
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def sorted_array_to_bst(nums: list[int]) -> TreeNode | None:
if not nums:
return None
# Stack holds tuples: (node, left_bound, right_bound, is_left_child, parent)
# We'll process nodes and attach them to parents
n = len(nums)
mid = n // 2
root = TreeNode(nums[mid])
# Stack entries: (left, right, parent_node, is_left)
stack = []
# Add left and right ranges to process
if mid - 1 >= 0:
stack.append((0, mid - 1, root, True))
if mid + 1 < n:
stack.append((mid + 1, n - 1, root, False))
while stack:
left, right, parent, is_left = stack.pop()
if left > right:
continue
mid = (left + right) // 2
node = TreeNode(nums[mid])
# Attach to parent
if is_left:
parent.left = node
else:
parent.right = node
# Add children ranges to stack
if left <= mid - 1:
stack.append((left, mid - 1, node, True))
if mid + 1 <= right:
stack.append((mid + 1, right, node, False))
return root
explanation: |
**Time Complexity:** O(n) — Each element is processed once.
**Space Complexity:** O(log n) — Stack depth mirrors tree height.
This iterative approach simulates the recursion using an explicit stack. While it achieves the same result, the recursive solution is more intuitive and readable for tree problems. The iterative version is useful when recursion depth is a concern.