How to Prepare for Coding Tests: Core Patterns, Data Structures and Time Management

Key takeaways

Many candidates fail coding tests not from missing algorithms but from not recognizing which pattern a problem needs, or from sinking too long into one problem. This post orders topics by how often they appear, maps problem signals to techniques, and adds templates, edge-case checks and a three-month study plan.

Introduction: Coding Tests Are All About Preparation

Coding tests are largely pattern recognition. Most problems in a typical two-hour test are variations of a few dozen techniques, and the hard part is noticing which one a problem needs while the clock is running. Knowing an algorithm and recognizing when to use it are different skills, and the second only comes from solving many problems and reviewing why a given technique applied.


Essential Algorithms

Learning Order by Priority

graph TB
    A[Coding Test Algorithms] --> B[Stage 1: Essential]
    A --> C[Stage 2: Important]
    A --> D[Stage 3: Advanced]
    
    B --> B1[Two Pointers]
    B --> B2[Sliding Window]
    B --> B3[Binary Search]
    B --> B4[BFS/DFS]
    
    C --> C1[Dynamic Programming]
    C --> C2[Greedy]
    C --> C3[Backtracking]
    
    D --> D1[Shortest Path]
    D --> D2[Topological Sort]
    D --> D3[Trie]

Stage 1: Essential Algorithms (High Frequency)

Two Pointers:

# Example: Two sum in sorted array
def two_sum(arr, target):
    left, right = 0, len(arr) - 1
    
    while left < right:
        current_sum = arr[left] + arr[right]
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1
        else:
            right -= 1
    
    return None
# Time complexity: O(n)

Sliding Window:

# Example: Maximum sum subarray of length k
def max_sum_subarray(arr, k):
    window_sum = sum(arr[:k])
    max_sum = window_sum
    
    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i - k]
        max_sum = max(max_sum, window_sum)
    
    return max_sum
# Time complexity: O(n)

Binary Search:

# Example: Find value in sorted array
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1
# Time complexity: O(log n)

Exact-match binary search is rarely what a test asks for. The more common forms are “first index where arr[i] >= x” (Python’s bisect_left, C++‘s lower_bound) and binary search on the answer: when a problem asks for the minimum capacity, maximum distance or smallest time such that some check passes, and the check is monotonic (if 10 works, 11 works too), you binary-search the answer value and run an O(n) check at each step. Phrases like “minimize the maximum” or “at least k in time t” are the usual signal. The classic bug is an infinite loop from mid = (left + right) // 2 combined with left = mid; when the loop keeps left = mid, round up with (left + right + 1) // 2. BFS/DFS (Breadth/Depth First Search):

from collections import deque
# BFS
def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)
    
    while queue:
        node = queue.popleft()
        print(node)
        
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
# DFS (recursive)
def dfs(graph, node, visited=None):
    if visited is None:
        visited = set()
    
    visited.add(node)
    print(node)
    
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

Two details in these templates prevent the most common failures. BFS marks a node as visited when it is pushed, not when it is popped; marking on pop lets the same node enter the queue many times, which on dense graphs turns an O(V + E) search into a timeout. And the recursive DFS is fine in C++ but fragile in Python: CPython’s default recursion limit is about 1000 frames, so a DFS over a long chain of 10^5 nodes stops with RecursionError: maximum recursion depth exceeded. I now write DFS in Python with an explicit stack by default and only use recursion when the depth is clearly small (a tree of bounded height, a backtracking search over 10 items). sys.setrecursionlimit(10**6) works on many judges but can still crash the interpreter with a real stack overflow, which shows up as a runtime error with no traceback.

Stage 2: Important Algorithms

Dynamic Programming (DP):

# Example: Fibonacci sequence
def fibonacci(n):
    if n <= 1:
        return n
    
    dp = [0] * (n + 1)
    dp[1] = 1
    
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]
# Time complexity: O(n)
# Space complexity: O(n)

Greedy:

# Example: Coin change
def coin_change(coins, amount):
    coins.sort(reverse=True)
    count = 0
    
    for coin in coins:
        if amount >= coin:
            count += amount // coin
            amount %= coin
    
    return count if amount == 0 else -1

This greedy version is only correct for “canonical” coin systems such as 1, 5, 10, 25 or 10, 50, 100, 500, where each coin is a convenient multiple of the smaller ones. For coins [1, 3, 4] and amount 6 it takes 4 + 1 + 1 (three coins), but 3 + 3 uses two. It can also return -1 when an answer exists: for [5, 3] and amount 9 it takes 5, is left with 4, and fails, while 3 + 3 + 3 works. LeetCode 322 “Coin Change” is deliberately built so that greedy fails; the correct approach is DP over amounts, dp[a] = min(dp[a - c] + 1). This is the general lesson about greedy in tests: it is fast and short, but it needs an argument for why the local choice is safe, and a counterexample with three small numbers is usually enough to rule it out. When a problem statement says the coins are multiples of each other, that sentence is the hint that greedy is intended.


Essential Data Structures

Learning by Priority

PriorityData StructureFrequencyDifficulty
1ArrayVery HighEasy
2HashMapVery HighEasy
3StackHighEasy
4QueueHighEasy
5HeapMediumMedium
6TreeMediumMedium
7GraphMediumHard

Core Operations by Data Structure

Array:

# Essential operations
arr = [1, 2, 3, 4, 5]
arr.append(6)        # Add at end O(1)
arr.pop()            # Remove from end O(1)
arr.insert(0, 0)     # Add at front O(n)
arr.remove(3)        # Remove value O(n)
arr.sort()           # Sort O(n log n)

HashMap:

# Essential operations
d = {}
d['key'] = 'value'   # Insert O(1)
val = d.get('key')   # Lookup O(1)
del d['key']         # Delete O(1)
'key' in d           # Check existence O(1)

Stack:

# Stack (LIFO)
stack = []
stack.append(1)      # push
stack.append(2)
top = stack.pop()    # pop (2)

Queue:

from collections import deque
# Queue (FIFO)
queue = deque()
queue.append(1)      # enqueue
queue.append(2)
front = queue.popleft()  # dequeue (1)

Heap:

import heapq
# Min heap
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 2)
min_val = heapq.heappop(heap)  # 1
# Max heap (use negative)
max_heap = []
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -1)
max_val = -heapq.heappop(max_heap)  # 3

Problem-Solving Patterns

Pattern Recognition Flowchart

flowchart TD
    A[Read Problem] --> B{Sorted Array?}
    B -->|Yes| C[Binary Search or Two Pointers]
    B -->|No| D{Subarray/Substring?}
    D -->|Yes| E[Sliding Window]
    D -->|No| F{Graph/Tree?}
    F -->|Yes| G[BFS/DFS]
    F -->|No| H{Optimization Problem?}
    H -->|Yes| I[DP or Greedy]
    H -->|No| J[HashMap or Stack/Queue]

Treat the flowchart as a first guess, not a decision procedure. “Subarray” does not always mean sliding window: with negative numbers, “subarray sum equals k” needs prefix sums plus a hash map. “Optimization” does not always mean DP: “shortest path in an unweighted grid” is BFS, and “minimum cost with weights” is Dijkstra. The more reliable signal is often the constraint line. n ≤ 20 suggests bitmasks or backtracking over subsets, n ≤ 500 allows O(n³) such as Floyd–Warshall or interval DP, and n ≤ 2·10^5 rules out anything quadratic and points to sorting, a heap, binary search or a linear scan. Reading the limits before the story often tells you which family to look in.

Examples by Pattern

Pattern 1: Two Pointers

# LeetCode 15. 3Sum
def three_sum(nums):
    nums.sort()
    result = []
    
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i-1]:
            continue
        
        left, right = i + 1, len(nums) - 1
        while left < right:
            total = nums[i] + nums[left] + nums[right]
            if total == 0:
                result.append([nums[i], nums[left], nums[right]])
                left += 1
                right -= 1
                while left < right and nums[left] == nums[left-1]:
                    left += 1
            elif total < 0:
                left += 1
            else:
                right -= 1
    
    return result

Pattern 2: Sliding Window

# LeetCode 3. Longest Substring Without Repeating Characters
def length_of_longest_substring(s):
    char_set = set()
    left = 0
    max_length = 0
    
    for right in range(len(s)):
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        
        char_set.add(s[right])
        max_length = max(max_length, right - left + 1)
    
    return max_length

Pattern 3: BFS

# LeetCode 102. Binary Tree Level Order Traversal
from collections import deque
def level_order(root):
    if not root:
        return []
    
    result = []
    queue = deque([root])
    
    while queue:
        level_size = len(queue)
        level = []
        
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        
        result.append(level)
    
    return result

Pattern 4: Dynamic Programming

# LeetCode 70. Climbing Stairs
def climb_stairs(n):
    if n <= 2:
        return n
    
    dp = [0] * (n + 1)
    dp[1], dp[2] = 1, 2
    
    for i in range(3, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]

Time Management

Exam Time Allocation (2 hours, 4 problems)

Total time: 120 minutes
Problem 1 (Easy):   20 minutes
Problem 2 (Medium): 30 minutes
Problem 3 (Medium): 35 minutes
Problem 4 (Hard):   35 minutes
Buffer time: 10 minutes (review and debugging)

Problem-Solving Process

flowchart LR
    A[Read Problem 3min] --> B[Analyze Examples 2min]
    B --> C[Design Approach 5min]
    C --> D[Write Code 15min]
    D --> E[Test 3min]
    E --> F[Debug 2min]

Checklist for Each Stage: 1. Read Problem (3 min):

  • Check input format
  • Check output format
  • Check constraints (n range, time limit)
  • Identify edge cases 2. Analyze Examples (2 min):
  • Trace example input/output by hand
  • Pattern recognition
  • Think about exception cases 3. Design Approach (5 min):
  • Calculate brute force time complexity
  • Consider optimization methods
  • Select data structures
  • Write pseudocode 4. Write Code (15 min):
  • Write function signature
  • Implement core logic
  • Handle edge cases 5. Test (3 min):
  • Test with example inputs
  • Test edge cases (empty array, size 1, max size)
  • Check output format 6. Debug (2 min):
  • Check error messages
  • Print debugging
  • Fix logic errors

The per-problem budget matters less than a rule for when to stop. The costly failure is not a hard problem left unsolved; it is spending 70 minutes on problem 3 and never reading problem 4, which might have been easier. Read every problem in the first few minutes, solve in order of expected difficulty rather than order of appearance, and set a checkpoint: if there is no working approach after roughly 15–20 minutes, write down the brute force, move on, and come back. On platforms with partial scoring, a brute force that passes the small test groups is worth real points, and it doubles as a reference to check an optimized solution against. I have lost more points to one stubborn problem than to not knowing an algorithm.


Language Selection

Pros and Cons by Language

LanguageProsConsRecommended For
PythonConcise syntax, fast implementationSlow speed (TLE possible)Most cases
C++Fast speed, STLLong code, debugging difficultyPerformance-critical problems
JavaStable, rich APIVerbose syntaxEnterprise background
JavaScriptFamiliar to web developersLack of type safetyWeb developers

1. Concise Syntax:

# Python
def reverse_string(s):
    return s[::-1]
# C++
#include <string>
#include <algorithm>
string reverseString(string s) {
    reverse(s.begin(), s.end());
    return s;
}

2. Rich Built-in Functions:

# Sorting
arr.sort()
# Max/Min
max_val = max(arr)
min_val = min(arr)
# Sum
total = sum(arr)
# Count
from collections import Counter
count = Counter(arr)

3. Slicing:

arr = [1, 2, 3, 4, 5]
arr[1:4]    # [2, 3, 4]
arr[::-1]   # [5, 4, 3, 2, 1] (reverse)
arr[::2]    # [1, 3, 5] (every 2)

When to Use C++

When TLE (Time Limit Exceeded) Occurs:

Rough throughput for simple operations (order of magnitude only):
C++:    ~10^8 per second
Python: ~10^7 per second or less (pure-Python loops)

Problem constraint: n ≤ 10^6, O(n log n) solution → ~2 × 10^7 operations
C++:    comfortable
Python: tight; may TLE depending on the judge and the constant factor

Before switching languages, check the usual Python slowdowns: reading input with input() instead of sys.stdin.readline (or reading everything at once with sys.stdin.buffer.read().split()), printing results one line at a time instead of joining them into one string, and doing work in Python loops that sort, sum, Counter or slicing would do in C. Some judges also offer PyPy, which runs loop-heavy code many times faster than CPython but is slower on programs dominated by recursion or many small objects. C++ STL Essentials:

#include <vector>
#include <algorithm>
#include <queue>
#include <map>
#include <set>
// Vector
vector<int> v = {1, 2, 3};
v.push_back(4);
// Sort
sort(v.begin(), v.end());
// Binary search
bool found = binary_search(v.begin(), v.end(), 3);
// Priority queue (max heap)
priority_queue<int> pq;
pq.push(3);
int top = pq.top();

Templates and Habits

Tip 1: Prepare Templates

Python Template:

# Input processing
n = int(input())
arr = list(map(int, input().split()))
# Or
import sys
input = sys.stdin.readline
# Output
print(result)

C++ Template:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    cin >> n;
    
    vector<int> arr(n);
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    
    // Logic
    
    cout << result << '\n';
    
    return 0;
}

Two lines in these templates are easy to get wrong. In Python, sys.stdin.readline keeps the trailing newline, so input().split() still works but a string read as s = input() now ends in '\n' and has the wrong length; use input().rstrip() for strings. In C++, sync_with_stdio(false) makes cin/cout fast, but after it you must not mix them with scanf/printf, and endl flushes the stream on every call, which can turn a fast solution into a slow one when printing 10^5 lines; use '\n'.

Tip 2: Time Complexity Check

Estimate time complexity from n range (1-2 second limit):
n ≤ 10:        O(n!) possible
n ≤ 20:        O(2^n) possible
n ≤ 500:       O(n³) possible
n ≤ 5,000:     O(n²) possible (tight in Python)
n ≤ 100,000:   O(n log n) needed
n ≤ 1,000,000: O(n) or O(n log n) with a small constant
n ≥ 10^9:      O(log n) or O(1) (math, binary search, fast exponentiation)

These limits come from the ~10^8 simple operations per second rule of thumb for C++, so for Python shift each row down by roughly a factor of 10. They are a filter for ruling approaches out, not a guarantee: an O(n log n) solution with a heavy constant (a hash map in the inner loop, recursion in Python) can still time out.

Tip 3: Debugging Strategy

# Print debugging
def solve(arr):
    print(f"DEBUG: arr = {arr}")  # Check input
    
    result = []
    for i in range(len(arr)):
        print(f"DEBUG: i = {i}, arr[i] = {arr[i]}")  # Check process
        result.append(arr[i] * 2)
    
    print(f"DEBUG: result = {result}")  # Check output
    return result

Tip 4: Edge Case Check

# Cases to always test
test_cases = [
    [],              # Empty array
    [1],             # Size 1
    [1, 1, 1],       # All same values
    [1, 2, 3],       # Sorted array
    [3, 2, 1],       # Reverse order
    [-1, 0, 1],      # Contains negative
    [10**9],         # Max value
]

Tip 5: Common Mistakes to Avoid

1. Off-by-one errors:

# ❌ Wrong
for i in range(len(arr) - 1):  # Misses last element
# ✅ Correct
for i in range(len(arr)):

2. Integer overflow:

# Python: No overflow (arbitrary precision)
# C++: Use long long for large numbers
long long result = (long long)a * b;

3. Modifying while iterating:

# ❌ Wrong
for item in arr:
    if condition:
        arr.remove(item)  # Modifies during iteration
# ✅ Correct
arr = [item for item in arr if not condition]

Summary

3-Month Study Plan

Month 1: Build Foundation

  • Array, HashMap, Stack, Queue
  • Two Pointers, Sliding Window
  • LeetCode Easy 50 problems Month 2: Intermediate Algorithms
  • BFS/DFS, Binary Search
  • Dynamic Programming basics
  • LeetCode Medium 50 problems Month 3: Advanced Algorithms
  • Advanced Dynamic Programming
  • Graph algorithms
  • LeetCode Medium 50 + Hard 20 problems

Study Resources

Online Judges:

  • LeetCode - Global standard
  • HackerRank - Interview prep
  • Codeforces - Competitive programming Recommended Problem Sets:
  • LeetCode Top 100 Liked
  • LeetCode Blind 75
  • NeetCode 150

Next Steps

For detailed algorithm explanations, refer to these series:

Quick Study Guide

Week 1-2: Arrays, HashMap basics
Week 3-4: Two Pointers, Sliding Window
Week 5-6: Stack, Queue, BFS/DFS
Week 7-8: Binary Search, Sorting
Week 9-10: Dynamic Programming basics
Week 11-12: Advanced DP, Greedy, Backtracking

Practice Schedule

Daily routine (2-3 hours):
1. Review 1 concept (30 min)
2. Solve 2 new problems (60 min)
3. Review 1 past problem (30 min)
Weekly:
- Monday-Friday: New problems
- Saturday: Mock test (4 problems, 2 hours)
- Sunday: Review wrong answers

The review step is where most of the learning happens. For a problem you could not solve, the useful note is not the code but one sentence on what signal you missed, for example “n ≤ 16 meant bitmask DP” or “asked for the minimum maximum, so binary search on the answer”. Re-solving the same problem from scratch a week later, without looking, shows whether the pattern actually stuck.


Frequently Asked Questions (FAQ)

Q. My solution passes the examples but fails hidden test cases. What should I check first?

A. Start with the edge cases listed in Tip 4: an empty input, a single element, all-equal values, and maximum-size values. Next, recheck the time complexity against the input limits, because a solution that passes the small examples can still time out on the largest hidden case. In C++ or Java also look for integer overflow when multiplying or summing large values, and in any language look for off-by-one errors in loop bounds.