Data Structure Solutions
Data Structure Solutions
Code Implementation:
class ArrayQueue:
def __init__(self, capacity=10):
self.queue = [None] * capacity
self.front = 0
self.rear = 0
self.size = 0
self.capacity = capacity
def is_empty(self):
return self.size == 0
def is_full(self):
return self.size == self.capacity
self.queue[self.rear] = item
self.rear = (self.rear + 1) % self.capacity
self.size += 1
print(f"Enqueued {item}")
def dequeue(self):
if self.is_empty():
raise IndexError("Queue is empty")
item = self.queue[self.front]
self.queue[self.front] = None
self.front = (self.front + 1) % self.capacity
self.size -= 1
print(f"Dequeued {item}")
return item
def peek(self):
if self.is_empty():
raise IndexError("Queue is empty")
return self.queue[self.front]
def display(self):
if self.is_empty():
print("Queue is empty")
return
elements = []
current = self.front
for _ in range(self.size):
elements.append(str(self.queue[current]))
current = (current + 1) % self.capacity
print("Queue:", " -> ".join(elements))
Step-by-Step Simulation:
1. Initialize: Create queue with capacity 4
2. Enqueue 10: Place at index 0, rear moves to 1
3. Enqueue 20: Place at index 1, rear moves to 2
4. Dequeue: Remove from index 0, front moves to 1
5. Peek: Return element at front (index 1)
Time Complexity: O(1) for all operations
Space Complexity: O(n) where n is capacity
After enqueue(10):
front -> [10] -> None <- rear
After enqueue(20):
front -> [10] -> [20] -> None <- rear
After enqueue(30):
front -> [10] -> [20] -> [30] -> None <- rear
After dequeue():
front -> [20] -> [30] -> None <- rear
Code Implementation:
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedListQueue:
def __init__(self):
self.front = None
self.rear = None
self.size = 0
def is_empty(self):
return self.front is None
if self.is_empty():
self.front = self.rear = new_node
else:
self.rear.next = new_node
self.rear = new_node
self.size += 1
print(f"Enqueued {item}")
def dequeue(self):
if self.is_empty():
raise IndexError("Queue is empty")
item = self.front.data
self.front = self.front.next
self.size -= 1
print(f"Dequeued {item}")
return item
def peek(self):
if self.is_empty():
raise IndexError("Queue is empty")
return self.front.data
def display(self):
if self.is_empty():
print("Queue is empty")
return
elements = []
current = self.front
while current:
elements.append(str(current.data))
current = current.next
print("Queue:", " -> ".join(elements))
"programmingisfun"
p:1, r:2, o:2, g:2, m:2, i:2, n:4, s:2, f:1, u:1
Code Implementation:
def first_non_repeating_char(s):
# Count frequency of each character
char_count = {}
for char in s:
char_count[char] = char_count.get(char, 0) + 1
return -1
queue = deque()
char_count = defaultdict(int)
Index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
Char: p y t h o n i s p o p u l a r
Frequency count:
p: 3 (indices 0, 8, 10)
y: 1 (index 1) ← First non-repeating
t: 1 (index 2)
...
Answer: 1
Code Implementation:
return time
for i in range(len(tickets)):
if i <= k:
# People before or at position k
time += min(tickets[i], target_tickets)
else:
# People after position k
time += min(tickets[i], target_tickets - 1)
return time
tickets = [5,1,1,1], k = 0
Total time: 5 + 1 + 1 + 1 = 8
bills = [5,5,5,10,20]
Code Implementation:
def lemonade_change(bills):
count_5 = 0
count_10 = 0
return True
Result: false
A(0) Level 0
/||\
B C D Level 1
/ |\
E F G Level 2
|
H Level 3
Definitions:
Level: Distance from root (Level of node = Depth of node)
Depth: Number of edges from root to node
Height: Maximum number of edges from node to any leaf
Degree: Number of children of a node
Analysis for Each Node:
Code Implementation:
class TreeNode:
def __init__(self, val):
self.val = val
self.children = []
def find_height(root):
if not root:
return -1
if not root.children:
return 0
max_height = -1
for child in root.children:
max_height = max(max_height, find_height(child))
return max_height + 1
A
/ \
B C
/ \
D E
A
/ \
B C
/|\ /|\
D E F G H I
def is_full_binary_tree(root):
if not root:
return True
if not root.left and not root.right:
return True
if root.left and root.right:
return is_full_binary_tree(root.left) and is_full_binary_tree(root.right)
return False
def is_complete_binary_tree(root):
if not root:
return True
queue = [root]
flag = False
while queue:
node = queue.pop(0)
if node.left:
if flag:
return False
queue.append(node.left)
else:
flag = True
if node.right:
if flag:
return False
queue.append(node.right)
else:
flag = True
return True
def is_perfect_binary_tree(root):
def get_depth(node):
depth = 0
while node:
depth += 1
node = node.left
return depth
depth = get_depth(root)
return is_perfect_helper(root, depth)
def is_balanced_binary_tree(root):
def check_balance(node):
if not node:
return True, -1
balanced, _ = check_balance(root)
return balanced
1 Level 0 (Height 3)
/
2 Level 1 (Height 2)
/ \
3 4 Level 2 (Height 1)
/ / \
5 6 7 Level 3 (Height 0)
/ \
8 9 Additional nodes
Code Implementation:
class TreeNode:
def __init__(self, val=0):
self.val = val
self.left = None
self.right = None
def construct_tree_height3_min9nodes():
# Create tree with exactly 9 nodes and height 3
root = TreeNode(1)
# Level 1
root.left = TreeNode(2)
# Level 2
root.left.left = TreeNode(3)
root.left.right = TreeNode(4)
# Level 3
root.left.left.left = TreeNode(5)
root.left.right.left = TreeNode(6)
root.left.right.right = TreeNode(7)
return root
def get_height(root):
if not root:
return -1
return max(get_height(root.left), get_height(root.right)) + 1
def count_nodes(root):
if not root:
return 0
return 1 + count_nodes(root.left) + count_nodes(root.right)
1 Level 0
/ \
2 3 Level 1
/ \ / \
4 5 6 7 Level 2
/ \
8 9 Level 3
This design is more balanced while still achieving height 3 with 9 nodes.
Problem 5.4: Tree Traversals
Problem Statement: Traverse trees in pre-order, in-order, and post-order.
Sample Tree:
4
/ \
2 6
/ \ / \
1 3 5 7
Traversal Methods:
1. Pre-order: Root → Left → Right
2. In-order: Left → Root → Right
3. Post-order: Left → Right → Root
Step-by-Step Simulation:
Pre-order Traversal:
Visit order: 4 → 2 → 1 → 3 → 6 → 5 → 7
Steps:
1. Visit root (4)
2. Go to left subtree (2)
- Visit 2
- Go to left (1), visit 1
- Go to right (3), visit 3
3. Go to right subtree (6)
- Visit 6
- Go to left (5), visit 5
- Go to right (7), visit 7
Result: [4, 2, 1, 3, 6, 5, 7]
In-order Traversal:
Visit order: 1 → 2 → 3 → 4 → 5 → 6 → 7
Steps:
1. Go to leftmost node (1), visit 1
2. Visit parent (2)
3. Visit right child (3)
4. Visit root (4)
5. Go to right subtree, leftmost (5), visit 5
6. Visit parent (6)
7. Visit right child (7)
Visit order: 1 → 3 → 2 → 5 → 7 → 6 → 4
Steps:
1. Process left subtree completely
- Visit 1, then 3, then 2
2. Process right subtree completely
- Visit 5, then 7, then 6
3. Visit root (4)
Result: [1, 3, 2, 5, 7, 6, 4]
Code Implementation:
def preorder_traversal(root):
result = []
def preorder(node):
if node:
result.append(node.val) # Visit root
preorder(node.left) # Visit left subtree
preorder(node.right) # Visit right subtree
preorder(root)
return result
def inorder_traversal(root):
result = []
def inorder(node):
if node:
inorder(node.left) # Visit left subtree
result.append(node.val) # Visit root
inorder(node.right) # Visit right subtree
inorder(root)
return result
def postorder_traversal(root):
result = []
def postorder(node):
if node:
postorder(node.left) # Visit left subtree
postorder(node.right) # Visit right subtree
result.append(node.val) # Visit root
postorder(root)
return result
# Iterative implementations
def preorder_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
return result
def inorder_iterative(root):
result = []
stack = []
current = root
# Process node
current = stack.pop()
result.append(current.val)
return result
Tree Structure:
1
/ \
2 3
/ \ / \
4 5 6 7
Mapping:
- Index 1 (value 1): root
- Index 2 (value 2): left child of 1
- Index 3 (value 3): right child of 1
- Index 4 (value 4): left child of 2
- Index 5 (value 5): right child of 2
- Index 6 (value 6): left child of 3
- Index 7 (value 7): right child of 3
Code Implementation:
def array_to_binary_tree(arr):
if not arr or len(arr) <= 1:
return None
def build_tree(index):
if index >= len(arr) or arr[index] is None:
return None
node = TreeNode(arr[index])
node.left = build_tree(2 * index)
node.right = build_tree(2 * index + 1)
return node
def build_tree(index):
if index >= len(arr) or arr[index] is None:
return None
node = TreeNode(arr[index])
node.left = build_tree(2 * index + 1)
node.right = build_tree(2 * index + 2)
return node
result = []
queue = [root]
while queue:
node = queue.pop(0)
if node:
result.append(node.val)
queue.append(node.left)
queue.append(node.right)
else:
result.append(None)
return result
Step-by-Step Construction:
1
/ \
2 2
/ \ / \
3 4 4 3
Non-Symmetric Tree:
1
/ \
2 2
\ \
3 3
Solution Approach:
1. Compare left subtree with right subtree
2. For symmetry: left.left ≡ right.right and left.right ≡ right.left
Recursive Logic:
isSymmetric(root):
return isMirror(root.left, root.right)
isMirror(left, right):
- If both null: true
- If one null: false
- If values different: false
- Recursively check: isMirror(left.left, right.right) AND
isMirror(left.right, right.left)
Code Implementation:
def is_symmetric(root):
if not root:
return True
if left.val != right.val:
return False
return True
Step-by-Step Verification:
For symmetric tree:
1
/ \
2 2
/ \ / \
3 4 4 3
1
/ \
2 2
\ \
3 3
Tree 1: Tree 2:
1 1
/ \ / \
2 3 2 3
Non-Identical Trees:
Tree 1: Tree 2:
1 1
/ \ / \
2 3 3 2
Solution Approach:
1. If both nodes are null → identical
2. If one is null → not identical
3. If values different → not identical
4. Recursively check left and right subtrees
Code Implementation:
while queue:
node1, node2 = queue.popleft()
if node1.val != node2.val:
return False
queue.append((node1.left, node2.left))
queue.append((node1.right, node2.right))
return True
Step-by-Step Verification:
Case 1: Identical Trees
Result: true
Result: false
Original: Mirror:
1 1
/ \ / \
2 3 3 2
/ \ / \ / \
4 5 6 7 5 4
\ /
6
Before: [1,2,3,4,5,null,null,null,null,null,6]
After: [1,3,2,null,null,5,4,6,null,null,null]
Solution Approaches:
1. Recursive (Modify Original Tree)
2. Recursive (Create New Tree)
3. Iterative (Using Queue)
Code Implementation:
return mirrored
while queue:
node = queue.popleft()
Step-by-Step Simulation:
Original Tree:
1
/ \
2 3
/ \
4 5
Mirror Process:
Final Mirror:
1
/ \
3 2
/ \
5 4
Verification:
3
/ \
9 20
/ \
15 7
Unbalanced Tree:
1
/ \
2 2
/ \
3 3
/ \
4 4
Solution Approaches:
1. Naive Approach: Check balance at each node (O(n²))
2. Optimized Approach: Single pass with height calculation (O(n))
Visual Analysis:
Tree: 3
/ \
9 20
/ \
15 7
Result: Balanced
Code Implementation:
def check_balanced(node):
if not node:
return True
left_height = get_height(node.left)
right_height = get_height(node.right)
return check_balanced(root)
# Approach 2: Optimized solution (O(n))
def is_balanced_optimized(root):
def check_balance_and_height(node):
# Returns (is_balanced, height)
if not node:
return True, -1
balanced, _ = check_balance_and_height(root)
return balanced
left_height = get_height(node.left)
if left_height == -1: # Left subtree unbalanced
return -1
right_height = get_height(node.right)
if right_height == -1: # Right subtree unbalanced
return -1
return get_height(root) != -1
1
/ \
2 3
/ \
4 5
1
/
2
/
3
Original (Unbalanced):
1
\
2
\
3
\
4
\
5
Inorder: [1, 2, 3, 4, 5]
Balanced Result:
3
/ \
2 4
/ \
1 5
Code Implementation:
def balance_bst(root):
# Step 1: Get inorder traversal (sorted array)
def inorder(node):
if not node:
return []
return inorder(node.left) + [node.val] + inorder(node.right)
return root
if not root:
return None
sorted_values = inorder(root)
return build_balanced(sorted_values, 0, len(sorted_values) - 1)
return root
nodes = []
inorder(root, nodes)
return build_tree(nodes, 0, len(nodes) - 1)
Step-by-Step Example:
Original Unbalanced BST:
10
/
8
/
7
/
6
Height Comparison:
Original height: 3 (worst case)
Balanced height: 2 (optimal)
Time Complexity: O(n)
Space Complexity: O(n) for storing inorder traversal
70
/ \
50 90
/ / \
40 80 95
/ \
20 85
Insertion Algorithm:
1. Start from root
2. If key < current node, go left
3. If key > current node, go right
4. Insert at first available position
5. Maintain BST property
Step-by-Step Insertion:
Insert 65:
Before: After:
70 70
/ \ / \
50 90 50 90
/ / \
40 40 65
Insert 105:
Before: After:
70 70
/ \ / \
50 90 50 90
/ \ / \ / \ / \
40 65 80 95 40 65 80 95
\ \
85 105
Insert 69:
Final BST:
70
/ \
50 90
/ \ / \
40 65 80 95
\ \
69 105
Code Implementation:
# Recursive insertion
if key < root.val:
root.left = insert_bst(root.left, key)
elif key > root.val:
root.right = insert_bst(root.right, key)
# If key equals root.val, no insertion (BST has unique values)
return root
# Iterative approach
def insert_bst_iterative(root, key):
new_node = TreeNode(key)
if not root:
return new_node
current = root
while True:
if key < current.val:
if not current.left:
current.left = new_node
break
current = current.left
elif key > current.val:
if not current.right:
current.right = new_node
break
current = current.right
else:
# Key already exists
return root
return root
def print_inorder(root):
if root:
print_inorder(root.left)
print(root.val, end=" ")
print_inorder(root.right)
Insertion Analysis:
Key 65 insertion:
Key 69 insertion:
Time Complexity:
Average case: O(log n)
Worst case: O(n) for skewed tree
Space Complexity: O(1) iterative, O(h) recursive
70
/ \
50 90
/ \ / \
40 65 80 95
/ \ \
20 69 85
Before: After:
70 70
/ \ / \
50 90 50 90
/ \ / \ / \ / \
40 65 80 95 40 65 80 95
/ \ \ \ \
20 69 85 69 85
Before: After:
70 70
/ \ / \
50 90 50 90
/ \ / \ / \ / \
40 65 80 95 40 65 80 85
\ \ \
69 85 69
Delete 50 (Case 3: Two children):
Before: After:
70 70
/ \ / \
69 90 69 90
/ \ / \ / \ / \
40 65 80 85 40 65 80 85
\
69 (delete this)
Before: After:
70 80
/ \ / \
69 90 69 90
/ \ / \ / \ / \
40 65 80 85 40 65 85 (no children)
Code Implementation:
return root
def find_min(root):
"""Find minimum value node in BST"""
while root.left:
root = root.left
return root
def find_max(root):
"""Find maximum value node in BST"""
while root.right:
root = root.right
return root
if not root.left:
return root.right
if not root.right:
return root.left
return root
50
/ \
30 70
/ \ / \
20 40 60 80
Steps:
1. Visit rightmost: 80
2. Visit parent: 70
3. Visit left child: 60
4. Visit root: 50
5. Visit right child: 40
6. Visit parent: 30
7. Visit leftmost: 20
Output: 80 70 60 50 40 30 20
Code Implementation:
stack = []
current = root
# Process node
current = stack.pop()
print(current.val, end=" ")
while current:
if not current.right:
# No right subtree, print and go left
print(current.val, end=" ")
current = current.left
else:
# Find inorder predecessor in right subtree
predecessor = current.right
while predecessor.left and predecessor.left != current:
predecessor = predecessor.left
if not predecessor.left:
# Make current as left child of predecessor
predecessor.left = current
current = current.right
else:
# Revert the change
predecessor.left = None
print(current.val, end=" ")
current = current.left
values = []
inorder(root, values)
values.reverse()
def pop(self):
return self.items.pop()
def is_empty(self):
return len(self.items) == 0
def print_descending_custom_stack(root):
if not root:
return
stack = Stack()
current = root
# Process node
current = stack.pop()
print(current.val, end=" ")
# Move to left subtree
current = current.left
Final Output: 80 70 60 50 40 30 20
Complexity Analysis:
20
/ \
10 30
/ \ / \
8 12 25 35
BST: 20
/ \
10 30
/ \ / \
8 12 25 35
Path: 12 → 10 → 20
BST: 20
/ \
10 30
/ \ / \
8 12 25 35
Path: 25 → 30 → 20
Code Implementation:
def find_successor_with_parent(node):
if not node:
return None
return parent
def find_predecessor_with_parent(node):
if not node:
return None
return parent
while current:
if node_val < current.val:
successor = current # Potential successor
current = current.left
elif node_val > current.val:
current = current.right
else:
# Found the node
if current.right:
# Find leftmost in right subtree
current = current.right
while current.left:
current = current.left
successor = current
break
return successor
while current:
if node_val > current.val:
predecessor = current # Potential predecessor
current = current.right
elif node_val < current.val:
current = current.left
else:
# Found the node
if current.left:
# Find rightmost in left subtree
current = current.left
while current.right:
current = current.right
predecessor = current
break
return predecessor
# Method 3: Using inorder traversal
def find_successor_predecessor_inorder(root, target):
def inorder(node, values):
if node:
inorder(node.left, values)
values.append(node.val)
inorder(node.right, values)
values = []
inorder(root, values)
try:
index = values.index(target)
successor = values[index + 1] if index + 1 < len(values) else None
predecessor = values[index - 1] if index - 1 >= 0 else None
return successor, predecessor
except ValueError:
return None, None
Step-by-Step Examples:
Finding Successor of 10:
BST: 20
/ \
10 30
/ \ / \
8 12 25 35
BST: 20
/ \
10 30
/ \ / \
8 12 25 35
Array: [1, 2, 3, 4, 5, 6, 7]
Final BST:
4
/ \
2 6
/ \ / \
1 3 5 7
Level 0: 4
/ \
Level 1: 2 6
/ \ / \
Level 2: 1 3 5 7
Preorder: 4 → 2 → 1 → 3 → 6 → 5 → 7
Code Implementation:
def sorted_array_to_bst(nums):
def build_bst(start, end):
if start > end:
return None
return root
def preorder_traversal(root):
"""Print preorder traversal"""
result = []
def preorder(node):
if node:
result.append(node.val)
preorder(node.left)
preorder(node.right)
preorder(root)
return result
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
return result
# Build BST
root = sorted_array_to_bst(arr)
return check_height(node) != -1
return root
Array: [1, 3, 5, 7, 9]
build_bst(0, 4):
mid = 2, root = TreeNode(5)
Final Tree:
5
/ \
1 7
\ \
3 9
Preorder: [5, 1, 3, 7, 9]
Example 2: [1, 2, 3, 4, 5, 6]
build_bst(0, 5):
mid = (0+5)//2 = 2, root = TreeNode(3)
Final Tree:
3
/ \
1 5
\ / \
2 4 6
Preorder: [3, 1, 2, 5, 4, 6]
5
/ \
1 4
/ \
3 6
10
/ \
5 15
/ \ / \
1 8 12 20
Range validation:
- Node 10: (-∞, +∞) ✓
- Node 5: (-∞, 10) ✓
- Node 15: (10, +∞) ✓
- Node 1: (-∞, 5) ✓
- Node 8: (5, 10) ✓
- Node 12: (10, 15) ✓
- Node 20: (15, +∞) ✓
values = []
inorder(root, values)
return True
prev = None
is_valid = True
inorder(root)
return is_valid
stack = []
prev = float('-inf')
current = root
# Process node
current = stack.pop()
prev = current.val
current = current.right
return True
5
/ \
3 8
/ \ / \
2 4 7 9
Range validation:
validate(5, -∞, +∞):
5 in (-∞, +∞) ✓
validate(3, -∞, 5):
3 in (-∞, 5) ✓
validate(2, -∞, 3): 2 in (-∞, 3) ✓
validate(4, 3, 5): 4 in (3, 5) ✓
validate(8, 5, +∞):
8 in (5, +∞) ✓
validate(7, 5, 8): 7 in (5, 8) ✓
validate(9, 8, +∞): 9 in (8, +∞) ✓
Result: True
5
/ \
3 8
/ \ / \
2 6 7 9
Range validation:
validate(5, -∞, +∞):
5 in (-∞, +∞) ✓
validate(3, -∞, 5):
3 in (-∞, 5) ✓
validate(2, -∞, 3): 2 in (-∞, 3) ✓
validate(6, 3, 5): 6 NOT in (3, 5) ✗
Valid BST:
5
/ \
3 8
/ \ / \
2 4 7 9
Invalid BST:
5
/ \
3 8
/ \ / \
2 6 7 9
Complexity Comparison:
Summary
This comprehensive solution covers all requested problems with:
✅ Queue Problems (3.9-3.13): Array/LinkedList implementations, character finding, ticket
buying, lemonade change
✅ Tree Problems (5.1-5.10): Node properties, tree types, construction, traversals, symmetry,
balance checking
✅ BST Problems (6.1-6.7): Balancing, insertion/deletion, traversal orders,
successor/predecessor, array construction, validation
Each problem includes:
Visual diagrams showing step-by-step execution
Multiple solution approaches with trade-offs
Complete code implementations with error handling
Complexity analysis for time and space
Detailed examples with traced execution
Key Takeaways:
Data Structure Choice: Arrays vs LinkedLists vs Trees based on operation requirements
Algorithm Efficiency: Understanding when O(1), O(log n), and O(n) solutions are
appropriate
Tree Properties: Leveraging BST properties for efficient operations
Balance Importance: Maintaining balance for optimal performance
This solution set provides a solid foundation for understanding fundamental data structures and
their applications.