Data Structure using Python
Interview Questions with Answers
Explore Our Courses
1. What are the different data types in Python and how are they used in DS problems?
Answer:
Python has several built-in data types, commonly used in data structure problems:

Â
Advanced types include collections.deque, defaultdict, Counter, OrderedDict for optimized DS operations.
Ace your Python DS interviews with confidence—practice the most asked questions today!
2. How is memory managed in Python during list and dictionary operations?
Answer:
Python uses automatic memory management via the Python Memory Manager, which includes:
- Private heap space: All objects and data structures are stored here.
- Dynamic resizing:
- Lists grow using over-allocation (extra space reserved).
- Dicts grow using rehashing and resizing as elements increase.
List Memory:
- Internally uses a resizable array.
- Amortized O(1) time for append due to geometric resizing.
Dictionary Memory:
- Uses a hash table.
- Keys are hashed, and collisions are resolved using open addressing.
Python automatically manages memory via:
- Reference counting
- Garbage collector for cyclic references
3. What is the difference between list, tuple, and set in Python?
Answer:

4. Explain the concept of mutability with respect to Python data structures.
Answer:
Mutability refers to whether the object’s content can be changed after creation.

Examples:
lst = [1, 2]; lst[0] = 5Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â # Mutable
tup = (1, 2); tup[0] = 5Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â # Error (immutable)
In DS problems:
- Use immutable types for hashable keys in sets/dicts.
- Use mutable types for dynamic updates.
5. How does Python manage references and garbage collection?
Answer:
Python uses reference counting and a cyclic garbage collector.
- Reference Counting: Each object keeps track of how many references point to it.
- When count = 0 → memory is deallocated.
- Garbage Collector (gc module):
- Detects and collects cyclic references (e.g., mutually referring lists).
- Uses generational GC:
- Objects are grouped in generations (0, 1, 2) based on age.
- Younger objects are collected more frequently.
Example:
import gc
gc.collect()Â # Manually trigger garbage collection
6. What are Python iterators and generators? How do they support DS operations?
Answer:
Iterator: Object with __iter__() and __next__() methods.
lst = [1, 2, 3]
it = iter(lst)
print(next(it))Â # Output: 1
Generator: A special iterator using yield instead of return.
Benefits:
- Memory-efficient (lazy evaluation)
- Useful for large data streams (e.g., infinite series, tree traversal)
 Example:
def countdown(n):
  while n > 0:
      yield n
       n -= 1
Used in:
- Tree traversals
- Streaming data
- Efficient backtracking
7. What is the difference between is and == in Python?
Answer:

 Use is
- To compare identity (e.g., is None)
- When checking for singleton objects
Use ==
- When comparing values/contents
8. Explain how slicing works in Python and give DS use cases.
Answer:
Slicing Syntax: sequence[start:stop:step]
Examples:
lst = [10, 20, 30, 40, 50]
lst[1:4]Â Â Â Â # [20, 30, 40]
lst[::-1]   # [50, 40, 30, 20, 10] → reverse
Use Cases in DS:
- Reversing strings/lists
- Copying arrays (arr[:])
- Extracting subarrays or substrings
- Sliding window technique (arr[i:i+k])
9. How do you implement custom sorting using key and lambda in Python?
Answer:
Python’s sorted() and list.sort() accept a key argument for custom logic.
Examples:
# Sort by length
words = ['banana', 'apple', 'kiwi']
sorted_words = sorted(words, key=len)
# Sort by second element of tuple
pairs = [(1, 3), (2, 1), (5, 2)]
pairs.sort(key=lambda x: x[1])
Real-world DS Use:
- Sorting by frequency
- Custom comparators (e.g., sort by absolute value, sort strings by custom order)
10. Explain time complexity of built-in Python operations (append, pop, insert, sort).
Answer:

 Note: Worst-case dict/set operations may degrade to O(n) due to hash collisions, but are rare with good hash functions.
11. Implement two-pointer approach to reverse a string in Python
Answer:
def reverse_string(s):
  s = list(s) # Convert to list for in-place modification
  left, right = 0, len(s) - 1
  while left < right:
      s[left], s[right] = s[right], s[left]
      left += 1
      right -= 1
  return ''.join(s)
# Test
print(reverse_string("python"))Â # Output: "nohtyp"
12. How do you remove duplicates from a list while preserving order?
Answer:
def remove_duplicates(lst):
  seen = set()
  result = []
  for item in lst:
      if item not in seen:
          seen.add(item)
          result.append(item)
   return result
# Test
print(remove_duplicates([1, 2, 2, 3, 1, 4]))Â # Output: [1, 2, 3, 4]
13. Given a rotated sorted array, write a function to search a target element
Answer:
def search_rotated_array(nums, target):
  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 # Not found
# Test
print(search_rotated_array([4, 5, 6, 7, 0, 1, 2], 0))Â # Output: 4
14. Implement Kadane’s algorithm to find the maximum subarray sum
Answer:
def max_subarray_sum(nums):
  max_sum = current_sum = nums[0]
  for num in nums[1:]:
      current_sum = max(num, current_sum + num)
      max_sum = max(max_sum, current_sum)
 return max_sum
# Test
print(max_subarray_sum([-2,1,-3,4,-1,2,1,-5,4]))Â # Output: 6
15. Check if two strings are anagrams using Python dictionaries
Answer:
def are_anagrams(s1, s2):
  if len(s1) != len(s2):
      return False
 count = {}
 for c in s1:
      count[c] = count.get(c, 0) + 1
  for c in s2:
      if c not in count:
          return False
      count[c] -= 1
      if count[c] < 0:
         return False
 return True
# Test
print(are_anagrams("listen", "silent"))Â # Output: True16. How would you implement run-length encoding of a string?
Answer:
def run_length_encode(s):
  if not s:
      return ""
 result = []
  count = 1
  prev = s[0]
  for c in s[1:]:
      if c == prev:
          count += 1
      else:
          result.append(prev + str(count))
          prev = c
          count = 1
 result.append(prev + str(count))
  return ''.join(result)
# Test
print(run_length_encode("aaabbc"))Â # Output: "a3b2c1"
17. Implement string compression using counts of repeated characters
Answer:
def compress_string(s):
  if not s:
      return ""
 compressed = []
  count = 1
  prev = s[0]
  for c in s[1:]:
      if c == prev:
           count += 1
else:
     compressed.append(prev + str(count))
          prev = c
          count = 1
 compressed.append(prev + str(count))
  result = ''.join(compressed)
 # Return original if compression doesn't reduce size
   return result if len(result) < len(s) else s
# Test
print(compress_string("aabcccccaaa"))Â # Output: "a2b1c5a3"
18. Given a list of numbers, return indices of two numbers that add to a target (Two Sum)
Answer:
def two_sum(nums, target):
  index_map = {} # num → index
 for i, num in enumerate(nums):
      complement = target - num
      if complement in index_map:
          return [index_map[complement], i]
      index_map[num] = i
 return [] # No valid pair found
# Test
print(two_sum([2, 7, 11, 15], 9))Â # Output: [0, 1]
19. Explain sliding window technique with an example problem
Answer:
Sliding Window is used to optimize problems with contiguous subarrays.
 Example Problem: Find the maximum sum of any subarray of size k.
def max_sum_subarray_k(nums, k):
  window_sum = sum(nums[:k])
  max_sum = window_sum
 for i in range(k, len(nums)):
      window_sum += nums[i] - nums[i - k]
      max_sum = max(max_sum, window_sum)
 return max_sum
# Test
print(max_sum_subarray_k([1, 4, 2, 10, 2, 3, 1, 0, 20], 4))Â # Output: 24
20. Implement group anagrams problem using Python
Answer:
from collections import defaultdict
def group_anagrams(strs):
  anagrams = defaultdict(list)
 for word in strs:
      # Sort characters to form a key
      key = ''.join(sorted(word))
      anagrams[key].append(word)
 return list(anagrams.values())
# Test
print(group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]))
# Output: [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]
Get closer to your dream job—master Data Structures in Python with real interview questions.
21. Implement a Singly Linked List in Python
Answer:
class Node:
  def __init__(self, data):
      self.data = data
       self.next = None
class SinglyLinkedList:
  def __init__(self):
      self.head = None
  def insert_end(self, data):
      new_node = Node(data)
      if not self.head:
          self.head = new_node
          return
      curr = self.head
      while curr.next:
          curr = curr.next
      curr.next = new_node
  def display(self):
     curr = self.head
      while curr:
          print(curr.data, end=" -> ")
          curr = curr.next
       print("None")
22. Reverse a Linked List (Iterative and Recursive)
Answer:
Iterative Approach
def reverse_iterative(head):
  prev = None
  curr = head
  while curr:
      next_node = curr.next
      curr.next = prev
      prev = curr
      curr = next_node
   return prev
Recursive Approach
def reverse_recursive(head):
  if not head or not head.next:
      return head
  new_head = reverse_recursive(head.next)
  head.next.next = head
  head.next = None
   return new_head
23. Detect a Cycle in a Linked List (Floyd’s Algorithm)
Answer:
def has_cycle(head):
  slow = fast = head
  while fast and fast.next:
      slow = slow.next
      fast = fast.next.next
      if slow == fast:
          return True
   return False
24. Find the Middle of a Linked List
Answer:
def find_middle(head):
  slow = fast = head
  while fast and fast.next:
      slow = slow.next
      fast = fast.next.next
   return slow # Middle node
25. Merge Two Sorted Linked Lists
Answer:
def merge_sorted_lists(l1, l2):
  dummy = Node(0)
  tail = dummy
  while l1 and l2:
      if l1.data <= l2.data:
          tail.next = l1
          l1 = l1.next
      else:
          tail.next = l2
          l2 = l2.next
      tail = tail.next
  tail.next = l1 or l2
   return dummy.next
26. Remove the Nth Node from the End of a Linked List
Answer:
def remove_nth_from_end(head, n):
  dummy = Node(0)
  dummy.next = head
  fast = slow = dummy
  for _ in range(n):
      fast = fast.next
  while fast.next:
      fast = fast.next
      slow = slow.next
  slow.next = slow.next.next
   return dummy.next
27. Check if a Linked List is a Palindrome
Answer:
def is_palindrome(head):
  if not head or not head.next:
      return True
  # Find middle
  slow = fast = head
  while fast and fast.next:
      slow = slow.next
      fast = fast.next.next
  # Reverse second half
  prev = None
  while slow:
      next_node = slow.next
      slow.next = prev
      prev = slow
      slow = next_node
  # Compare halves
  left, right = head, prev
  while right:
      if left.data != right.data:
          return False
      left = left.next
      right = right.next
   return True
28. Add Two Numbers Represented by Linked Lists
Answer:
def add_two_numbers(l1, l2):
  dummy = Node(0)
  curr = dummy
  carry = 0
  while l1 or l2 or carry:
      v1 = l1.data if l1 else 0
      v2 = l2.data if l2 else 0
      total = v1 + v2 + carry
      carry = total // 10
      curr.next = Node(total % 10)
      curr = curr.next
      if l1: l1 = l1.next
      if l2: l2 = l2.next
   return dummy.next
29. Flatten a Multilevel Linked List
Answer:
Assume each node has next and child pointers (like a doubly linked list with children).
class MultiLevelNode:
  def __init__(self, data):
      self.data = data
      self.next = None
      self.child = None
def flatten(head):
  if not head:
      return head
  dummy = MultiLevelNode(0)
  stack = [head]
  prev = dummy
  while stack:
      curr = stack.pop()
      prev.next = curr
      prev = curr
      if curr.next:
          stack.append(curr.next)
      if curr.child:
          stack.append(curr.child)
          curr.child = None
   return dummy.next
30. Sort a Linked List Using Merge Sort
Answer:
def merge_sort(head):
  if not head or not head.next:
      return head
  # Split the list into two halves
  slow, fast = head, head.next
  while fast and fast.next:
      slow = slow.next
      fast = fast.next.next
  mid = slow.next
  slow.next = None
  left = merge_sort(head)
  right = merge_sort(mid)
   return merge_sorted_lists(left, right)
Note: merge_sorted_lists() is already defined in question 25.
31. Implement a Stack Using list and collections.deque
Answer:
Using list:
class StackList:
  def __init__(self):
      self.stack = []
  def push(self, x):
      self.stack.append(x) # O(1)
  def pop(self):
      return self.stack.pop() if self.stack else None # O(1)
  def peek(self):
      return self.stack[-1] if self.stack else None
  def is_empty(self):
       return not self.stack
Using collections.deque:
from collections import deque
class StackDeque:
  def __init__(self):
      self.stack = deque()
  def push(self, x):
      self.stack.append(x)
  def pop(self):
      return self.stack.pop() if self.stack else None
  def peek(self):
      return self.stack[-1] if self.stack else None
  def is_empty(self):
       return not self.stack
32. Evaluate a Postfix Expression Using Stack
Answer:
def evaluate_postfix(expression):
  stack = []
  for token in expression.split():
      if token.isdigit():
          stack.append(int(token))
      else:
          b = stack.pop()
          a = stack.pop()
          if token == '+': stack.append(a + b)
          elif token == '-': stack.append(a - b)
          elif token == '*': stack.append(a * b)
          elif token == '/': stack.append(int(a / b)) # Truncate towards 0
  return stack[0]
# Test
print(evaluate_postfix("2 3 1 * + 9 -"))Â # Output: -4
33. Implement a Queue Using Two Stacks
Answer:
class QueueTwoStacks:
  def __init__(self):
      self.in_stack = []
      self.out_stack = []
  def enqueue(self, x):
      self.in_stack.append(x)
  def dequeue(self):
      if not self.out_stack:
          while self.in_stack:
              self.out_stack.append(self.in_stack.pop())
      return self.out_stack.pop() if self.out_stack else None
  def peek(self):
      if not self.out_stack:
          while self.in_stack:
              self.out_stack.append(self.in_stack.pop())
      return self.out_stack[-1] if self.out_stack else None
  def is_empty(self):
       return not self.in_stack and not self.out_stack
34. Design a Min-Stack (Retrieve Min in O(1))
Answer:
class MinStack:
  def __init__(self):
      self.stack = []
      self.min_stack = []
  def push(self, val):
      self.stack.append(val)
      if not self.min_stack or val <= self.min_stack[-1]:
          self.min_stack.append(val)
  def pop(self):
      if self.stack:
          val = self.stack.pop()
          if val == self.min_stack[-1]:
              self.min_stack.pop()
  def top(self):
      return self.stack[-1] if self.stack else None
  def get_min(self):
       return self.min_stack[-1] if self.min_stack else None
35. Check for Balanced Parentheses Using Stack
Answer:
def is_balanced(expr):
  stack = []
  pairs = {')': '(', '}': '{', ']': '['}
  for char in expr:
      if char in '([{':
          stack.append(char)
      elif char in ')]}':
          if not stack or stack[-1] != pairs[char]:
              return False
          stack.pop()
  return not stack
# Test
print(is_balanced("{[()]}"))Â # Output: True
36. Implement an LRU Cache Using OrderedDict
Answer:
from collections import OrderedDict
class LRUCache:
  def __init__(self, capacity):
      self.cache = OrderedDict()
      self.capacity = capacity
  def get(self, key):
      if key not in self.cache:
          return -1
      self.cache.move_to_end(key) # Recently used
      return self.cache[key]
  def put(self, key, value):
      if key in self.cache:
          self.cache.move_to_end(key)
      self.cache[key] = value
      if len(self.cache) > self.capacity:
           self.cache.popitem(last=False) # Remove least recently used
37. Implement a Circular Queue in Python
Answer:
class CircularQueue:
  def __init__(self, k):
      self.queue = [None] * k
      self.capacity = k
      self.front = self.rear = -1
  def enqueue(self, val):
      if (self.rear + 1) % self.capacity == self.front:
          return False # Full
      if self.front == -1:
          self.front = 0
      self.rear = (self.rear + 1) % self.capacity
      self.queue[self.rear] = val
      return True
  def dequeue(self):
      if self.front == -1:
          return False # Empty
      if self.front == self.rear:
          self.front = self.rear = -1
      else:
          self.front = (self.front + 1) % self.capacity
      return True
  def Front(self):
      return -1 if self.front == -1 else self.queue[self.front]
  def Rear(self):
      return -1 if self.rear == -1 else self.queue[self.rear]
  def is_empty(self):
      return self.front == -1
  def is_full(self):
       return (self.rear + 1) % self.capacity == self.front
38. Solve the Sliding Window Maximum Using deque
Answer:
from collections import deque
def sliding_window_max(nums, k):
  dq = deque()
  result = []
  for i in range(len(nums)):
      while dq and dq[0] < i - k + 1:
          dq.popleft()
      while dq and nums[dq[-1]] < nums[i]:
          dq.pop()
      dq.append(i)
      if i >= k - 1:
          result.append(nums[dq[0]])
  return result
# Test
print(sliding_window_max([1,3,-1,-3,5,3,6,7], 3))Â # Output: [3,3,5,5,6,7]
39. What is the Difference Between collections.deque and queue.Queue?
Answer:

Use deque for single-threaded fast queue/stack.
Use queue.Queue for multi-threaded applications.
40. Simulate a Browser Back/Forward Feature Using Two Stacks
Answer:
class Browser:
  def __init__(self):
      self.back_stack = []
      self.forward_stack = []
      self.current = None
def visit(self, url):
      if self.current:
          self.back_stack.append(self.current)
      self.current = url
     self.forward_stack.clear()
  def back(self):
      if self.back_stack:
          self.forward_stack.append(self.current)
          self.current = self.back_stack.pop()
       return self.current
  def forward(self):
     if self.forward_stack:
          self.back_stack.append(self.current)
          self.current = self.forward_stack.pop()
      return self.current
# Test
b = Browser()
b.visit("A")
b.visit("B")
b.visit("C")
print(b.back())Â Â Â Â # Output: B
print(b.back())Â Â Â Â # Output: A
print(b.forward())Â # Output: B
Crack coding rounds faster with expert-level Python DS interview prep—start now!
41. Implement In-order, Pre-order, and Post-order Traversal
Answer:
class TreeNode:
  def __init__(self, val=0):
      self.val = val
      self.left = None
      self.right = None
# In-order: Left → Root → Right
def inorder(root):
  if root:
      inorder(root.left)
      print(root.val, end=' ')
      inorder(root.right)
# Pre-order: Root → Left → Right
def preorder(root):
  if root:
      print(root.val, end=' ')
      preorder(root.left)
      preorder(root.right)
# Post-order: Left → Right → Root
def postorder(root):
  if root:
      postorder(root.left)
      postorder(root.right)
       print(root.val, end=' ')
42. Check if a Binary Tree is Balanced
Answer:
A binary tree is balanced if for every node, the difference between the heights of left and right subtrees is not more than 1.
def is_balanced(root):
  def check(node):
      if not node:
          return 0
      left = check(node.left)
      if left == -1: return -1
      right = check(node.right)
      if right == -1: return -1
      if abs(left - right) > 1: return -1
      return max(left, right) + 1
   return check(root) != -1
43. Find the Lowest Common Ancestor in a Binary Tree
Answer:
def lowest_common_ancestor(root, p, q):
  if not root or root == p or root == q:
      return root
  left = lowest_common_ancestor(root.left, p, q)
  right = lowest_common_ancestor(root.right, p, q)
   return root if left and right else left or right
44. Serialize and Deserialize a Binary Tree
Answer:
Preorder-based Serialization
class Codec:
  def serialize(self, root):
      def dfs(node):
          if not node:
              vals.append("X")
              return
          vals.append(str(node.val))
          dfs(node.left)
          dfs(node.right)
      vals = []
      dfs(root)
      return ','.join(vals)
  def deserialize(self, data):
      def dfs():
          val = next(vals)
          if val == "X":
              return None
          node = TreeNode(int(val))
          node.left = dfs()
          node.right = dfs()
          return node
      vals = iter(data.split(','))
       return dfs()
45. Convert a BST to a Sorted Doubly Linked List
Answer:
def bst_to_dll(root):
  def inorder(node):
      nonlocal prev, head
      if not node:
          return
      inorder(node.left)
      if prev:
          prev.right = node
          node.left = prev
      else:
          head = node
      prev = node
      inorder(node.right)
  prev = head = None
  inorder(root)
   return head
46. Check if a Binary Tree is a Subtree of Another
Answer:
def is_subtree(s, t):
  def is_same(a, b):
      if not a and not b:
          return True
      if not a or not b or a.val != b.val:
          return False
      return is_same(a.left, b.left) and is_same(a.right, b.right)
  if not s:
      return False
  if is_same(s, t):
      return True
   return is_subtree(s.left, t) or is_subtree(s.right, t)
47. Count the Number of Leaf Nodes in a Binary Tree
Answer:
def count_leaves(root):
  if not root:
      return 0
  if not root.left and not root.right:
      return 1
   return count_leaves(root.left) + count_leaves(root.right)
48. Implement Level Order Traversal Using a Queue
Answer:
from collections import deque
def level_order(root):
  if not root:
      return []
  result = []
  queue = deque([root])
  while queue:
      node = queue.popleft()
      result.append(node.val)
      if node.left:
          queue.append(node.left)
      if node.right:
          queue.append(node.right)
   return result
49. Construct a Binary Tree from Inorder and Preorder Traversals
Answer:
def build_tree(preorder, inorder):
  if not preorder or not inorder:
      return None
  root = TreeNode(preorder[0])
  idx = inorder.index(preorder[0])
  root.left = build_tree(preorder[1:idx+1], inorder[:idx])
  root.right = build_tree(preorder[idx+1:], inorder[idx+1:])
   return root
50. Validate if a Given Binary Tree is a Binary Search Tree (BST)
Answer:
def is_valid_bst(root):
  def validate(node, low=float('-inf'), high=float('inf')):
      if not node:
          return True
      if not (low < node.val < high):
          return False
      return validate(node.left, low, node.val) and validate(node.right, node.val, high)
   return validate(root)
51. Implement BFS and DFS for a Graph in Python
Answer:
from collections import deque, defaultdict
# Assume graph is represented as an adjacency list: {node: [neighbors]}
def bfs(graph, start):
  visited = set()
  queue = deque([start])
  while queue:
      node = queue.popleft()
      if node not in visited:
          visited.add(node)
          print(node, end=' ')
          for neighbor in graph[node]:
              if neighbor not in visited:
                  queue.append(neighbor)
def dfs(graph, node, visited=None):
  if visited is None:
      visited = set()
  if node not in visited:
      visited.add(node)
      print(node, end=' ')
      for neighbor in graph[node]:
           dfs(graph, neighbor, visited)52. Detect a Cycle in a Directed Graph Using DFS
python
CopyEdit
def has_cycle(graph):
  visited = set()
  rec_stack = set()
  def dfs(node):
      if node in rec_stack:
          return True
      if node in visited:
          return False
      visited.add(node)
      rec_stack.add(node)
      for neighbor in graph[node]:
          if dfs(neighbor):
              return True
      rec_stack.remove(node)
      return False
  for node in graph:
      if dfs(node):
          return True
   return False
53. Find the Shortest Path in an Unweighted Graph Using BFS
Answer:
def shortest_path(graph, start, target):
  queue = deque([(start, [start])])
  visited = set([start])
  while queue:
      node, path = queue.popleft()
      if node == target:
          return path
      for neighbor in graph[node]:
          if neighbor not in visited:
              visited.add(neighbor)
              queue.append((neighbor, path + [neighbor]))
   return [] # No path
54. Implement Dijkstra’s Algorithm Using heapq
Answer:
import heapq
def dijkstra(graph, start):
  dist = {node: float('inf') for node in graph}
  dist[start] = 0
  pq = [(0, start)] # (distance, node)
  while pq:
      curr_dist, node = heapq.heappop(pq)
      if curr_dist > dist[node]:
          continue
      for neighbor, weight in graph[node]:
          new_dist = curr_dist + weight
          if new_dist < dist[neighbor]:
              dist[neighbor] = new_dist
              heapq.heappush(pq, (new_dist, neighbor))
   return dist
55. Explain Union-Find and Implement It With Path Compression
Answer:
Union-Find (Disjoint Set) tracks connected components.
- Find(x): Returns the root of the set.
- Union(x, y): Merges two sets.
- Optimized with:
- Path Compression
- Union by Rank
class UnionFind:
  def __init__(self, size):
      self.parent = list(range(size))
      self.rank = [0] * size
  def find(self, x):
      if self.parent[x] != x:
          self.parent[x] = self.find(self.parent[x]) # Path Compression
      return self.parent[x]
  def union(self, x, y):
      rootX, rootY = self.find(x), self.find(y)
      if rootX == rootY:
          return
      if self.rank[rootX] > self.rank[rootY]:
          self.parent[rootY] = rootX
      elif self.rank[rootX] < self.rank[rootY]:
          self.parent[rootX] = rootY
      else:
          self.parent[rootY] = rootX
           self.rank[rootX] += 1
56. Implement Topological Sort (Kahn’s Algorithm)
Answer:
from collections import deque, defaultdict
def kahn_topological_sort(graph):
  in_degree = {node: 0 for node in graph}
  for neighbors in graph.values():
      for neighbor in neighbors:
          in_degree[neighbor] += 1
  queue = deque([node for node in graph if in_degree[node] == 0])
  result = []
  while queue:
      node = queue.popleft()
      result.append(node)
      for neighbor in graph[node]:
          in_degree[neighbor] -= 1
          if in_degree[neighbor] == 0:
              queue.append(neighbor)
  if len(result) != len(graph):
      return [] # Cycle detected
   return result
57. Design a Trie for Prefix Search and Autocomplete
Answer:
class TrieNode:
  def __init__(self):
      self.children = {}
      self.is_end = False
class Trie:
  def __init__(self):
      self.root = TrieNode()
  def insert(self, word):
      node = self.root
      for ch in word:
          node = node.children.setdefault(ch, TrieNode())
      node.is_end = True
  def search(self, word):
      node = self.root
      for ch in word:
          if ch not in node.children:
              return False
          node = node.children[ch]
      return node.is_end
  def starts_with(self, prefix):
      node = self.root
      for ch in prefix:
          if ch not in node.children:
              return []
          node = node.children[ch]
      return self._autocomplete(node, prefix)
  def _autocomplete(self, node, prefix):
      results = []
      if node.is_end:
          results.append(prefix)
      for ch, child in node.children.items():
          results.extend(self._autocomplete(child, prefix + ch))
       return results
58. Find Connected Components in an Undirected Graph
Answer:
def connected_components(graph):
  visited = set()
  components = []
  def dfs(node, component):
      visited.add(node)
      component.append(node)
      for neighbor in graph[node]:
          if neighbor not in visited:
              dfs(neighbor, component)
  for node in graph:
      if node not in visited:
          component = []
          dfs(node, component)
          components.append(component)
   return components
59. Explain and Implement a Segment Tree for Range Sum Query
Answer:
Segment Tree: Binary tree structure for fast range queries and updates.
class SegmentTree:
  def __init__(self, arr):
      self.n = len(arr)
      self.tree = [0] * (4 * self.n)
      self.build(arr, 0, 0, self.n - 1)
  def build(self, arr, node, l, r):
      if l == r:
          self.tree[node] = arr[l]
      else:
          mid = (l + r) // 2
          self.build(arr, 2*node+1, l, mid)
          self.build(arr, 2*node+2, mid+1, r)
          self.tree[node] = self.tree[2*node+1] + self.tree[2*node+2]
  def query(self, node, l, r, ql, qr):
      if ql > r or qr < l: return 0 # No overlap
      if ql <= l and r <= qr: return self.tree[node] # Total overlap
      mid = (l + r) // 2
      return self.query(2*node+1, l, mid, ql, qr) + self.query(2*node+2, mid+1, r, ql, qr)
  def range_sum(self, left, right):
       return self.query(0, 0, self.n - 1, left, right)
60. What is a Heap in Python? Implement a Min/Max Heap Manually
Answer:
A heap is a binary tree where each node follows the heap property:
- Min-Heap: Parent ≤ Children
- Max-Heap: Parent ≥ Children
Python’s heapq supports min-heap by default.
Manual Min-Heap Implementation:
class MinHeap:
  def __init__(self):
      self.heap = []
  def insert(self, val):
      self.heap.append(val)
      self._heapify_up(len(self.heap) - 1)
  def extract_min(self):
      if not self.heap:
          return None
      min_val = self.heap[0]
      self.heap[0] = self.heap.pop()
      self._heapify_down(0)
      return min_val
  def _heapify_up(self, i):
      parent = (i - 1) // 2
      if i > 0 and self.heap[i] < self.heap[parent]:
          self.heap[i], self.heap[parent] = self.heap[parent], self.heap[i]
          self._heapify_up(parent)
  def _heapify_down(self, i):
      smallest = i
      l, r = 2*i + 1, 2*i + 2
      if l < len(self.heap) and self.heap[l] < self.heap[smallest]:
          smallest = l
      if r < len(self.heap) and self.heap[r] < self.heap[smallest]:
          smallest = r
      if smallest != i:
          self.heap[i], self.heap[smallest] = self.heap[smallest], self.heap[i]
           self._heapify_down(smallest)
