227 lines
9.7 KiB
YAML
227 lines
9.7 KiB
YAML
title: Balance a Binary Search Tree
|
|
slug: balance-a-binary-search-tree
|
|
difficulty: medium
|
|
leetcode_id: 1382
|
|
leetcode_url: https://leetcode.com/problems/balance-a-binary-search-tree/
|
|
categories:
|
|
- trees
|
|
patterns:
|
|
- tree-traversal
|
|
- dfs
|
|
|
|
function_signature: "def balance_bst(root: TreeNode) -> TreeNode:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { root: [1, null, 2, null, 3, null, 4] }
|
|
expected: [2, 1, 3, null, null, null, 4]
|
|
- input: { root: [2, 1, 3] }
|
|
expected: [2, 1, 3]
|
|
hidden:
|
|
- input: { root: [1] }
|
|
expected: [1]
|
|
- input: { root: [1, null, 2] }
|
|
expected: [1, null, 2]
|
|
- input: { root: [3, 2, null, 1] }
|
|
expected: [2, 1, 3]
|
|
- input: { root: [1, null, 2, null, 3, null, 4, null, 5] }
|
|
expected: [3, 1, 4, null, 2, null, 5]
|
|
- input: { root: [5, 4, null, 3, null, 2, null, 1] }
|
|
expected: [3, 1, 4, null, 2, null, 5]
|
|
|
|
description: |
|
|
Given the `root` of a binary search tree, return *a **balanced** binary search tree with the same node values*. If there is more than one answer, return **any of them**.
|
|
|
|
A binary search tree is **balanced** if the depth of the two subtrees of every node never differs by more than `1`.
|
|
|
|
constraints: |
|
|
- The number of nodes in the tree is in the range `[1, 10^4]`
|
|
- `1 <= Node.val <= 10^5`
|
|
|
|
examples:
|
|
- input: "root = [1,null,2,null,3,null,4,null,null]"
|
|
output: "[2,1,3,null,null,null,4]"
|
|
explanation: "This is not the only correct answer, [3,1,4,null,2] is also correct."
|
|
- input: "root = [2,1,3]"
|
|
output: "[2,1,3]"
|
|
explanation: "The tree is already balanced, so the same structure is returned."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Picture an unbalanced BST that's essentially become a linked list — each node only has a right child, forming a long chain. This degrades search operations from O(log n) to O(n).
|
|
|
|
The key insight is that a **BST's in-order traversal always produces a sorted array**. This property is fundamental: as you traverse left-root-right, you visit nodes in ascending order.
|
|
|
|
Think of it like this: if you have a sorted array and want to build the most balanced BST possible, you would naturally pick the **middle element** as the root. This ensures roughly half the elements go to the left subtree and half to the right — perfectly balanced!
|
|
|
|
By combining these two insights, the problem becomes straightforward:
|
|
1. Convert the BST to a sorted array (in-order traversal)
|
|
2. Build a balanced BST from the sorted array (recursive divide-and-conquer)
|
|
|
|
The beauty of this approach is that picking the middle element recursively guarantees the tree will be height-balanced.
|
|
|
|
approach: |
|
|
We solve this using **In-order Traversal + Divide-and-Conquer Reconstruction**:
|
|
|
|
**Step 1: Extract nodes via in-order traversal**
|
|
|
|
- Perform an in-order DFS traversal (left → root → right)
|
|
- Store each node's value in a list as we visit
|
|
- This produces a **sorted array** of all values
|
|
|
|
|
|
|
|
**Step 2: Build balanced BST from sorted array**
|
|
|
|
- Use a recursive helper function that takes a range `[left, right]`
|
|
- Find the middle index: `mid = (left + right) // 2`
|
|
- Create a new node with the middle value — this becomes the subtree's root
|
|
- Recursively build the left subtree from `[left, mid - 1]`
|
|
- Recursively build the right subtree from `[mid + 1, right]`
|
|
|
|
|
|
|
|
**Step 3: Handle base case**
|
|
|
|
- When `left > right`, the range is empty — return `None`
|
|
- This terminates the recursion at leaf boundaries
|
|
|
|
|
|
|
|
**Step 4: Return the new root**
|
|
|
|
- The initial call with the full range `[0, n - 1]` returns the root of the balanced BST
|
|
|
|
|
|
|
|
By always choosing the middle element, we ensure each subtree has at most half the remaining elements, guaranteeing the tree is balanced.
|
|
|
|
common_pitfalls:
|
|
- title: Modifying the Original Tree In-Place
|
|
description: |
|
|
Attempting to rebalance by rotating nodes in the original tree is complex and error-prone. While AVL or Red-Black tree rotations exist, they're overkill here.
|
|
|
|
The cleaner approach is to extract all values and rebuild from scratch — this is O(n) time and O(n) space regardless, which matches any rotation-based solution.
|
|
wrong_approach: "Complex tree rotations on the original structure"
|
|
correct_approach: "Extract values, rebuild from sorted array"
|
|
|
|
- title: Forgetting BST In-Order Property
|
|
description: |
|
|
If you try to extract values using pre-order or post-order traversal, you won't get a sorted array. Only **in-order traversal** (left → root → right) produces sorted output from a BST.
|
|
|
|
This property is essential — the reconstruction algorithm relies on the array being sorted to place elements correctly.
|
|
wrong_approach: "Using pre-order or BFS to extract values"
|
|
correct_approach: "In-order DFS traversal for sorted output"
|
|
|
|
- title: Off-By-One in Middle Calculation
|
|
description: |
|
|
When calculating the middle index, using `(left + right) // 2` works correctly. However, be careful with the recursive ranges:
|
|
- Left subtree: `[left, mid - 1]` — excludes the middle
|
|
- Right subtree: `[mid + 1, right]` — excludes the middle
|
|
|
|
Including the middle in either subtree would duplicate values.
|
|
wrong_approach: "Overlapping ranges that include mid twice"
|
|
correct_approach: "Exclusive ranges: [left, mid-1] and [mid+1, right]"
|
|
|
|
- title: Not Handling Empty Ranges
|
|
description: |
|
|
The base case `left > right` must return `None`. This happens when:
|
|
- A node has no left child: recursive call gets an empty range
|
|
- A node has no right child: recursive call gets an empty range
|
|
|
|
Missing this base case causes infinite recursion or index errors.
|
|
|
|
key_takeaways:
|
|
- "**BST property**: In-order traversal of a BST always produces a sorted sequence — this is fundamental to many BST algorithms"
|
|
- "**Divide-and-conquer**: Building a balanced structure from sorted data by recursively picking the middle element is a powerful pattern"
|
|
- "**Rebuild vs modify**: Sometimes reconstructing a data structure is simpler and equally efficient as modifying in place"
|
|
- "**Related problems**: This technique applies to *Convert Sorted Array to BST* (LC 108) and *Convert Sorted List to BST* (LC 109)"
|
|
|
|
time_complexity: "O(n). We visit each node exactly twice — once during in-order traversal to extract values, and once during reconstruction."
|
|
space_complexity: "O(n). We store all `n` values in an array, plus O(log n) recursion stack depth for the balanced tree construction."
|
|
|
|
solutions:
|
|
- approach_name: In-order Traversal + Rebuild
|
|
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 balance_bst(root: TreeNode) -> TreeNode:
|
|
# Step 1: Extract all values via in-order traversal (produces sorted array)
|
|
values = []
|
|
|
|
def inorder(node):
|
|
if not node:
|
|
return
|
|
inorder(node.left) # Visit left subtree
|
|
values.append(node.val) # Record current node
|
|
inorder(node.right) # Visit right subtree
|
|
|
|
inorder(root)
|
|
|
|
# Step 2: Build balanced BST from sorted array
|
|
def build(left: int, right: int) -> TreeNode | None:
|
|
if left > right:
|
|
return None # Base case: empty range
|
|
|
|
# Pick middle element as root for balance
|
|
mid = (left + right) // 2
|
|
node = TreeNode(values[mid])
|
|
|
|
# Recursively build left and right subtrees
|
|
node.left = build(left, mid - 1)
|
|
node.right = build(mid + 1, right)
|
|
|
|
return node
|
|
|
|
return build(0, len(values) - 1)
|
|
explanation: |
|
|
**Time Complexity:** O(n) — In-order traversal visits each node once, and reconstruction visits each value once.
|
|
|
|
**Space Complexity:** O(n) — The array stores all values. Recursion stack is O(log n) for the balanced tree.
|
|
|
|
This solution elegantly combines two fundamental operations: BST in-order traversal (which guarantees sorted output) and divide-and-conquer tree construction (which guarantees balance). The middle element becomes the root at each level, ensuring subtrees have equal sizes.
|
|
|
|
- approach_name: Iterative In-order + Rebuild
|
|
is_optimal: false
|
|
code: |
|
|
def balance_bst(root: TreeNode) -> TreeNode:
|
|
# Iterative in-order traversal using explicit stack
|
|
values = []
|
|
stack = []
|
|
current = root
|
|
|
|
while stack or current:
|
|
# Go as far left as possible
|
|
while current:
|
|
stack.append(current)
|
|
current = current.left
|
|
|
|
# Process node and move right
|
|
current = stack.pop()
|
|
values.append(current.val)
|
|
current = current.right
|
|
|
|
# Build balanced BST (same as recursive approach)
|
|
def build(left: int, right: int) -> TreeNode | None:
|
|
if left > right:
|
|
return None
|
|
|
|
mid = (left + right) // 2
|
|
node = TreeNode(values[mid])
|
|
node.left = build(left, mid - 1)
|
|
node.right = build(mid + 1, right)
|
|
return node
|
|
|
|
return build(0, len(values) - 1)
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Same as recursive approach.
|
|
|
|
**Space Complexity:** O(n) — Array for values plus O(h) for the traversal stack where h is tree height.
|
|
|
|
This variant uses iterative in-order traversal with an explicit stack, which can be useful if recursion depth is a concern for extremely unbalanced trees. The reconstruction phase remains the same.
|