A mix of easy, medium, and harder questions covering common interview themes: data
structures, strings, recursion, OOP, and Python-specific idioms.
Write a function to reverse a string without using [::-1] .
def reverse_string(s: str) -> str:
result = []
for char in s:
[Link](0, char)
return ''.join(result)
# More efficient version using two pointers
def reverse_string_v2(s: str) -> str:
chars = list(s)
left, right = 0, len(chars) - 1
while left < right:
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return ''.join(chars)
O(n) time, O(n) space.
Determine if a string is a palindrome, ignoring case and non-alphanumeric
characters.
def is_palindrome(s: str) -> bool:
cleaned = [[Link]() for c in s if [Link]()]
return cleaned == cleaned[::-1]
print(is_palindrome("A man, a plan, a canal: Panama")) # True
Do it with O(1) extra space using two pointers instead of building a new list.
Given a list, return all elements that appear more than once.
def find_duplicates(nums: list) -> list:
seen = set()
duplicates = set()
for n in nums:
if n in seen:
[Link](n)
else:
[Link](n)
return list(duplicates)
print(find_duplicates([1, 2, 3, 2, 4, 5, 1])) # [1, 2]
O(n) time, O(n) space.
Given a list of integers and a target, return indices of the two numbers that add
up to the target.
def two_sum(nums: list, target: int) -> list:
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
print(two_sum([2, 7, 11, 15], 9)) # [0, 1]
O(n) time, O(n) space (vs O(n²) brute force).
def fizzbuzz(n: int) -> list:
result = []
for i in range(1, n + 1):
if i % 15 == 0:
[Link]("FizzBuzz")
elif i % 3 == 0:
[Link]("Fizz")
elif i % 5 == 0:
[Link]("Buzz")
else:
[Link](str(i))
return result
Given a string of text, return the frequency of each word (case-insensitive).
from collections import Counter
import re
def word_frequency(text: str) -> dict:
words = [Link](r'\w+', [Link]())
return dict(Counter(words))
print(word_frequency("The cat sat on the mat. The cat ran."))
# {'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1, 'ran': 1}
def merge_sorted(a: list, b: list) -> list:
merged = []
i, j = 0, 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
[Link](a[i])
i += 1
else:
[Link](b[j])
j += 1
[Link](a[i:])
[Link](b[j:])
return merged
print(merge_sorted([1, 3, 5], [2, 4, 6])) # [1, 2, 3, 4, 5, 6]
O(n + m) time.
Given a string containing ()[]{} , determine if the brackets are balanced.
def is_valid(s: str) -> bool:
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in [Link]():
[Link](char)
elif char in pairs:
if not stack or [Link]() != pairs[char]:
return False
return not stack
print(is_valid("{[()]}")) # True
print(is_valid("{[(])}")) # False
O(n) time and space. Stack-based — a very common pattern to recognize.
Write an e�cient function to compute the nth Fibonacci number.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n: int) -> int:
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30)) # 832040
Explain why plain recursion is O(2^n) and memoization brings it to O(n). Also
be ready to write the iterative O(n) time / O(1) space version.
def fib_iterative(n: int) -> int:
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
def flatten(nested: list) -> list:
result = []
for item in nested:
if isinstance(item, list):
[Link](flatten(item))
else:
[Link](item)
return result
print(flatten([1, [2, 3, [4, 5]], 6])) # [1, 2, 3, 4, 5, 6]
def binary_search(arr: list, target: int) -> int:
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
print(binary_search([1, 3, 5, 7, 9, 11], 7)) # 3
O(log n). Requires sorted input — mention this constraint.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
[Link] = capacity
[Link] = OrderedDict()
def get(self, key: int) -> int:
if key not in [Link]:
return -1
[Link].move_to_end(key)
return [Link][key]
def put(self, key: int, value: int) -> None:
if key in [Link]:
[Link].move_to_end(key)
[Link][key] = value
if len([Link]) > [Link]:
[Link](last=False)
cache = LRUCache(2)
[Link](1, 1)
[Link](2, 2)
print([Link](1)) # 1
[Link](3, 3) # evicts key 2
print([Link](2)) # -1
Why OrderedDict gives O(1) get/put, and how you'd do it manually with a
hashmap + doubly linked list.
from collections import defaultdict
def group_anagrams(words: list) -> list:
groups = defaultdict(list)
for word in words:
key = ''.join(sorted(word))
groups[key].append(word)
return list([Link]())
print(group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]))
# [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]
class ListNode:
def __init__(self, val=0, next=None):
[Link] = val
[Link] = next
def has_cycle(head: ListNode) -> bool:
slow = fast = head
while fast and [Link]:
slow = [Link]
fast = [Link]
if slow == fast:
return True
return False
Floyd's Tortoise and Hare — O(n) time, O(1) space.
Write a decorator that prints how long a function takes to run.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
[Link](0.5)
return "done"
slow_function()
Why @wraps matters (preserves __name__ / __doc__ ), di�erence between
decorators with and without arguments.
Show you understand generators vs. returning a list.
def fib_generator():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
gen = fib_generator()
print([next(gen) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Lazy evaluation, memory e�ciency vs. lists, yield vs return .
Design a BankAccount class with deposit/withdraw and proper encapsulation.
class BankAccount:
def __init__(self, owner: str, balance: float = 0):
[Link] = owner
self._balance = balance # convention: "protected"
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
def withdraw(self, amount: float) -> None:
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
@property
def balance(self) -> float:
return self._balance
def __repr__(self) -> str:
return f"BankAccount(owner={[Link]!r}, balance={self._balance})"
acc = BankAccount("Alice", 100)
[Link](50)
[Link](30)
print(acc) # BankAccount(owner='Alice', balance=120)
_ vs __ naming conventions, @property , why raise exceptions instead of
returning error codes.
• lists/dicts/sets are mutable; strings/tuples/ints are
immutable. Why does this matter for default function arguments ( def f(x=[])
pitfall)?
• is == identity vs equality.
• [x for x in y] builds a list in
memory; (x for x in y) is lazy.
• *args **kwargs how they work and when to use them.
• what it is, and why it a�ects multi-threading for CPU-
bound tasks (use multiprocessing instead).
• [Link]() vs [Link]() .
• @staticmethod @classmethod
�. Try each problem yourself first (with a timer, ~15-20 min) before looking at the
answer.
�. For each, be ready to state time/space complexity out loud.
�. Practice explaining your approach before writing code — many interviewers weigh
this as much as the final solution.
�. Re-implement a few (like Two Sum, Valid Parentheses, LRU Cache) from memory a
day later to check retention.