These ten short exercises are the warm-up problems that appear in screening interviews and introductory courses. Each one is solvable in a few lines of Python, but each also illustrates a reusable technique: slicing, a hash map for constant-time lookup, two pointers over sorted input, counting with a dictionary, or set algebra. This page gives a working solution for each, then explains the complexity, traces two of the more instructive ones step by step, and lists the edge cases interviewers usually probe.
Most of the problems reduce to one of four patterns. Built-ins and slicing (reverse, largest, palindrome) lean on operations implemented in C, which are both shorter and faster than hand-written loops. Hash maps (two sum, first non-repeated character, vowels) trade O(n) memory for O(1) membership checks, turning nested loops into single passes. Two pointers (merge sorted lists) exploit existing order so each element is examined once. Sets (duplicates, intersection) express membership problems directly, at the cost of losing order. Recognizing which pattern applies is more valuable than memorizing the individual answers.
| # | Exercise | Time | Extra space | Technique |
|---|---|---|---|---|
| 1 | Reverse a string | O(n) | O(n) | Slicing |
| 2 | Largest number | O(n) | O(1) | Built-in max |
| 3 | Number palindrome | O(d) | O(d) | String compare (d digits) |
| 4 | Fibonacci list | O(n) | O(n) | Iteration |
| 5 | Two sum | O(n) | O(n) | Hash map |
| 6 | Remove duplicates | O(n) | O(n) | Set |
| 7 | Count vowels | O(n) | O(1) | Generator + membership |
| 8 | Merge sorted lists | O(n + m) | O(n + m) | Two pointers |
| 9 | First non-repeated char | O(n) | O(k) distinct chars | Counting dict |
| 10 | List intersection | O(n + m) | O(n + m) | Set intersection |
The naive alternatives are worth knowing because interviewers often ask for the improvement: two sum with nested loops is O(n²); merging by concatenating and calling sorted() is O((n + m) log(n + m)); intersection with [x for x in a if x in b] on lists is O(n·m).
def reverse_string(s: str) -> str:
return s[::-1]
# Example usage
print(reverse_string("hello")) # Output: "olleh"
def find_largest(nums: list) -> int:
return max(nums)
# Example usage
print(find_largest([1, 2, 3, 4, 5])) # Output: 5
def is_palindrome(n: int) -> bool:
return str(n) == str(n)[::-1]
# Example usage
print(is_palindrome(121)) # Output: True
print(is_palindrome(123)) # Output: False
def fibonacci(n: int) -> list:
fib_seq = [0, 1]
for i in range(2, n):
fib_seq.append(fib_seq[-1] + fib_seq[-2])
return fib_seq[:n]
# Example usage
print(fibonacci(5)) # Output: [0, 1, 1, 2, 3]
def two_sum(nums: list, target: int) -> list:
num_map = {}
for i, num in enumerate(nums):
complement = target - num
if complement in num_map:
return [num_map[complement], i]
num_map[num] = i
# Example usage
print(two_sum([2, 7, 11, 15], 9)) # Output: [0, 1]
def remove_duplicates(nums: list) -> list:
return list(set(nums))
# Example usage
print(remove_duplicates([1, 2, 2, 3, 4, 4, 5])) # Output: [1, 2, 3, 4, 5]
def count_vowels(s: str) -> int:
vowels = 'aeiouAEIOU'
return sum(1 for char in s if char in vowels)
# Example usage
print(count_vowels("hello world")) # Output: 3
def merge_sorted_lists(list1: list, list2: list) -> list:
merged = []
i = j = 0
while i < len(list1) and j < len(list2):
if list1[i] < list2[j]:
merged.append(list1[i])
i += 1
else:
merged.append(list2[j])
j += 1
merged.extend(list1[i:])
merged.extend(list2[j:])
return merged
# Example usage
print(merge_sorted_lists([1, 3, 5], [2, 4, 6])) # Output: [1, 2, 3, 4, 5, 6]
def first_non_repeated_char(s: str) -> str:
counts = {}
for char in s:
counts[char] = counts.get(char, 0) + 1
for char in s:
if counts[char] == 1:
return char
return None
# Example usage
print(first_non_repeated_char("swiss")) # Output: "w"
def list_intersection(list1: list, list2: list) -> list:
return list(set(list1) & set(list2))
# Example usage
print(list_intersection([1, 2, 3, 4], [3, 4, 5, 6])) # Output: [3, 4]
For each element the function asks one question: "have I already seen the number that would complete the sum?" The dictionary answers in O(1) on average, so the whole scan is a single pass. Storing after checking matters: with nums = [3, 3] and target 6, the first 3 is stored, and the second finds it, returning [0, 1]; with target 6 and nums = [3] the single 3 never matches itself. If no pair exists, the function as written falls off the end and returns None, which callers should handle.
Two indexes walk the lists in parallel, and the smaller of the two current heads is appended each time. When one list runs out, the remainder of the other is already sorted and is appended in one extend. Using < rather than <= means equal values come from list2 first; switching to <= makes the merge stable with respect to list1, which is the variant merge sort needs. The standard library offers the same operation lazily as heapq.merge(list1, list2).
max([]) raises ValueError; decide whether to raise, return None, or use max(nums, default=None).fib_seq[:n] already handles 0 and 1 correctly; make sure a rewrite keeps that behaviour.list(set(nums)) does not preserve the original order; use list(dict.fromkeys(nums)) when order matters.str(-121) is "-121", which is not a palindrome; state the convention.collections.Counter(a) & Counter(b) to keep multiplicities.