Whiteboard and Chromebook Coding Proficiency Questions
Coding interview readiness focusing on solving algorithmic problems on a whiteboard or in a minimal IDE/terminal environment (e.g., Chromebook). Emphasis is on problem-solving approach, data structures and algorithms, time and space complexity analysis, correct and efficient implementation under constraints, and effective communication of thought process during interviews.
HardTechnical
68 practiced
Implement serialize and deserialize for a binary tree. Choose a compact text format suitable for whiteboard and Chromebook coding without external libraries. Your functions should be inverses: deserialize(serialize(root)) yields an equivalent tree. Discuss choice of traversal, null markers, and handling large trees without recursion.
Sample Answer
I'll use level-order (BFS) with a compact null marker "#" and comma-separated tokens. BFS produces a short, natural representation for sparse or skewed trees and is easy to implement iteratively (important for large trees / stack-depth limits).Code (iterative, handles large trees):Why BFS + "#":- Compact: trailing nulls trimmed to reduce size.- Predictable ordering: easy to stream, checkpoint, or chunk large trees.- Iterative: both serialize/deserialize use queues, avoiding recursion depth issues.Complexity:- Time: O(n) to read/write all nodes.- Space: O(n) worst-case for token list or queue; streaming variants can reduce peak memory by yielding tokens as produced.Edge cases:- Empty tree -> empty string.- Non-integer values -> adapt parsing.- Extremely large trees -> use streaming I/O instead of collecting all tokens in memory.
python
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def serialize(root):
"""Level-order -> comma-separated tokens, '#' for null."""
if not root:
return ""
q = deque([root])
out = []
while q:
node = q.popleft()
if node:
out.append(str(node.val))
q.append(node.left)
q.append(node.right)
else:
out.append('#')
# trim trailing nulls to keep compact
i = len(out) - 1
while i >= 0 and out[i] == '#':
i -= 1
return ",".join(out[:i+1])
def deserialize(data):
"""Rebuild tree from level-order tokens iteratively."""
if not data:
return None
tokens = data.split(',')
root = TreeNode(int(tokens[0]))
q = deque([root])
i = 1
while q and i < len(tokens):
node = q.popleft()
# left
if tokens[i] != '#':
node.left = TreeNode(int(tokens[i]))
q.append(node.left)
i += 1
if i >= len(tokens):
break
# right
if tokens[i] != '#':
node.right = TreeNode(int(tokens[i]))
q.append(node.right)
i += 1
return rootHardTechnical
54 practiced
Explain and compare maximum flow algorithms (Ford-Fulkerson, Edmonds-Karp, Dinic). For a given network with thousands of nodes and tens of thousands of edges, which algorithm would you pick and why? Discuss implementation complexity and edge-case behavior important for whiteboard design decisions.
Sample Answer
Maximum flow solves for the greatest flow from source to sink in a capacitated directed graph. Three classic algorithms differ in approach, complexity, and practical behavior:Ford–Fulkerson- Idea: Repeatedly find any augmenting path in the residual graph and push flow until none remain.- Complexity: O(E * max_flow) (depends on capacities); can be unbounded with irrational capacities.- Implementation: Very simple (DFS/BFS). Good for teaching or small integer-capacity graphs.- Edge behavior: Vulnerable to pathological choices of paths causing many iterations.Edmonds–Karp- Idea: Ford–Fulkerson with BFS to choose shortest (in edges) augmenting paths.- Complexity: O(V * E^2). Guarantees termination and polynomial time.- Implementation: Moderate complexity—BFS per augmentation, update residuals.- Edge behavior: Predictable progress (distance labels nondecreasing), but too slow for large dense graphs.Dinic- Idea: Repeatedly build level graph with BFS and find blocking flows via DFS, using multiple augmentations per BFS.- Complexity: O(min(V^(2/3), sqrt(E)) * E) for unit networks, O(V^2 * E) worst-case; in practice often close to O(E * sqrt(V)) or better.- Implementation: More complex (level graph, current-edge optimization for amortized DFS), but well-structured and efficient.- Edge behavior: Handles large graphs and high capacities robustly; performs especially well on layered or bipartite-like graphs.Which to pick for thousands of nodes and tens of thousands of edges?- Choose Dinic. It balances strong theoretical guarantees with excellent practical performance on medium-to-large sparse graphs. Use current-edge optimization and adjacency lists. If graphs are very large or flow values small/integers, consider push-relabel with gap/HLPP optimizations; for unit-capacity bipartite matching Dinic is excellent.Implementation/whiteboard considerations- Use adjacency lists with edge structs storing capacity, to, and reverse-index.- Maintain level[] from BFS and ptr[] (current edge) for DFS to avoid revisiting edges.- Watch for integer overflows: use 64-bit types for capacities and sums.- Handle parallel edges and self-loops explicitly.- For reproducible complexity, explain worst-case vs typical-case and why you prefer Dinic in production: more deterministic, fewer augmentations, and straightforward to optimize (scaling, heuristics).
HardSystem Design
57 practiced
You must sort a 1TB file with only 1GB of RAM available on a single machine. Describe an external sort algorithm and implementation plan: chunking, on-disk sorts, k-way merge, I/O buffering, and performance considerations on limited hardware. Include estimated number of passes and trade-offs for minimizing disk seeks.
Sample Answer
Requirements & constraints:- Input: 1 TB file on single machine- RAM: 1 GB available to process- Goal: produce sorted output on disk with minimal passes and disk seeksHigh-level approach: External merge sort (produce sorted runs that fit in RAM, then k-way merge runs). Steps: chunking, in-memory sort to produce runs, write runs to disk, then multi-way merge using buffered I/O.1) Chunking & on-disk sorts- Read the input sequentially in chunks slightly smaller than available memory to allow program overhead. Use chunk_size = 900 MB.- Number of initial runs = ceil(1 TB / 900 MB) ≈ 1138 runs.- For each chunk: - Read chunk into memory using a streaming reader. - Sort in-memory with an efficient O(n log n) sort (std::sort / Timsort). For records, sort by key. - Write sorted run to disk sequentially (use large write block sizes, fsync at end of run).2) k-way merge plan & I/O buffering- Multi-way merge reads head elements from each run and writes to output.- Memory must be split between: - Input buffers for each run - An output buffer - A small heap/priority queue of size k (store current head per run)- Trade-off: larger k reduces number of merge passes but reduces per-run buffer size (increasing seeks/reads).- Example configurations: - Aggressive k to finish in 1 pass: need k >= 1138. With 1 GB RAM, if we give 1 MB per input buffer, that's ~1138 MB already — too tight after program overhead. So 1-pass might be infeasible. - Practical: choose k = 64. Then passes = ceil(log_k(num_runs)) = ceil(log_64(1138)) ≈ 2 passes (64^2 = 4096 > 1138). Memory: allocate 64 input buffers + 1 output buffer + heap. If buffers are 15 MB each: 64*15 = 960 MB, output 20 MB, leaving ~20 MB for heap/overhead — okay but tight. Alternatively choose k = 32 to be safer: 32*25 = 800 MB + output 50 MB + overhead.- Buffer sizes: tune to disk block and OS read-ahead (e.g., 1–8 MB). Larger sequential reads reduce seeks and per-byte overhead.3) Merge algorithm details- Use a min-heap keyed by current record from each run; heap size = k.- For each run i maintain an in-memory buffer of B bytes; maintain read pointer and refill asynchronously/with background I/O when buffer drained.- On output, fill an output buffer (e.g., 8–64 MB) and flush sequentially to disk.- Use asynchronous or multi-threaded I/O (one thread for I/O, one for heap/pop/push) if OS allows to hide latency.4) Estimated passes & I/O cost- With chunk_size 900 MB -> 1138 runs.- With k = 64 -> 2 merge passes (first pass merges groups of up to 64 runs into intermediate runs; second pass merges intermediates into final output).- I/O volume: each byte is read and written once per pass. Initial write of runs = 1 TB write. Merge passes: 2 passes read+write => additional ~2 TB read + 2 TB write? Careful: initial read is included — total I/O ~ (reads + writes) = initial read (1 TB) + write runs (1 TB) + for two merge passes: read intermediate runs (1 TB + 1 TB) and final writes (1 TB + 1 TB) — but this double-counts; simpler: each pass reads and writes the whole data once. With initial sort pass (in-memory sort) we read once and write runs (counts as one read + one write). Then two merge passes = 2 reads + 2 writes. Total ≈ 3 reads + 3 writes ≈ 6 TB transferred. Minimizing passes reduces I/O.5) Minimizing disk seeks / performance considerations- Favor sequential I/O: write runs as large contiguous files; during merges, read in large blocks and avoid jumping frequently.- Reduce k to allow larger per-run buffers to amortize seeks; but that increases number of passes (more total I/O). Two-pass merge is a good balance.- Use OS file read-ahead and posix_fadvise(SEQUENTIAL) to hint sequential access.- Use direct I/O or O_DIRECT for predictability, or rely on kernel cache if RAM allows.- If using HDDs, prefer fewer large sequential reads; if SSDs, higher k is acceptable because random reads are cheaper.- If possible, compress runs on disk (like LZ4) to reduce I/O at cost of CPU; choose if CPU idle and I/O bound.6) Implementation plan (practical)- Phase 1: streaming reader -> sort -> write run files named run0001..runNN.- Phase 2: iterative merging: - Group runs in batches of k, spawn a merger that reads k runs using buffered I/O, writes merged output run. - Repeat until single run remains.- Use robust error handling, temporary files, and make merges resumable.- Monitor throughput and tune B buffer size and k.7) Trade-offs summary- Large k: fewer passes, higher memory pressure and smaller per-run buffers → more small reads if buffers too small.- Small k: more passes → more total I/O, but larger buffers reduce seeks per pass.- Disk type matters: HDD → favor fewer large sequential reads (smaller k, larger buffers); SSD → can afford higher k.- Compression trades CPU for reduced I/O.This plan yields a reliable two-pass external sort for 1 TB with 1 GB RAM by choosing k ≈ 32–64 and buffer sizes tuned to ~8–25 MB per run, balancing pass count, memory, and disk-seek minimization.
HardTechnical
58 practiced
Design and implement a thread-safe LRU cache API for a multi-threaded environment in Java. Describe synchronization strategy (locks, concurrent data structures, or lock-free), potential contention hotspots, and how you would test correctness under concurrent access on a Chromebook where you cannot run heavy concurrency tests.
Sample Answer
Approach (brief)- Use a ConcurrentHashMap<K, Node> for O(1) lookup and a custom doubly-linked list to maintain LRU order. Protect only list manipulations with a single ReentrantLock (fair=false) to keep contention minimal; reads can often be lock-free for map lookup then try-lock to update order. This gives a balance between correctness and throughput.Code implementation (Java):Key concepts & reasoning- Map is concurrent to allow parallel lookups/writes without global locking.- Linked list operations that change order or evict are protected by a single short-lived lock; this is the minimal critical section.- We insert into map before linking to avoid a window where a reader sees a key absent after a put started.- get() keeps lock time short (only list ops), enabling high read concurrency.Potential contention hotspots- The ReentrantLock on the list becomes a hotspot under massive concurrent get/put churn (many updates move nodes). Mitigations: - Use striping (multiple LRU segments) for very high throughput. - Use optimistic tryLock in get() and skip moving to head if lock unavailable (approximate LRU) to reduce latency. - Batch evictions or use background eviction thread for heavy workloads.Correctness testing under constrained Chromebook environment- Deterministic small-scale concurrency tests: - Use CountDownLatch to start N threads together and limited thread counts (e.g., 4–8) to exercise races deterministically. - Use Executors to run mixed read/write workloads with assertions: size <= capacity, recently accessed items present, no duplicate keys.- Use property-based and linearizability-style checks: - Record a timeline of operations (with timestamps and thread ids) and validate a serialized execution exists that matches results. - Use atomic counters and invariants (e.g., number of successful puts minus evictions == map.size()).- Simulated interleavings: - Inject small sleeps at critical points (after map.putIfAbsent, before list add) to increase chances of race exposure deterministically.- Static analysis & code review: - Run findbugs/SpotBugs, thread-safety linters.- Lightweight fuzzing: - Randomized sequences with assertions run in many short iterations (1000s) rather than heavy stress tests — feasible on Chromebook.- If possible, run a few CI builds on a more powerful runner for longer stress tests; but local deterministic tests + static checks catch most logic bugs.Trade-offs- Single global lock simplifies correctness but limits scalability at extreme concurrency; striping or lock-free linked lists are alternatives but more complex and error-prone.- Approximate LRU (skip moving on contention) improves throughput for caches where strict ordering isn't required.This design balances simplicity, correctness, and reasonable concurrency for typical server-side workloads while providing clear paths to scale further if needed.
java
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class LRUCache<K,V> {
private final int capacity;
private final ConcurrentHashMap<K, Node> map;
private final Node head, tail; // sentinels
private final ReentrantLock lock = new ReentrantLock();
private class Node {
final K key;
V val;
Node prev, next;
Node(K k, V v){ key=k; val=v; }
}
public LRUCache(int capacity){
this.capacity = capacity;
this.map = new ConcurrentHashMap<>();
head = new Node(null,null);
tail = new Node(null,null);
head.next = tail; tail.prev = head;
}
public V get(K key){
Node n = map.get(key);
if(n==null) return null;
// try to move to head; acquire lock briefly
lock.lock();
try {
removeNode(n);
addAfterHead(n);
} finally { lock.unlock(); }
return n.val;
}
public void put(K key, V value){
Node existing = map.get(key);
if(existing!=null){
// update value and move to head
existing.val = value;
lock.lock();
try {
removeNode(existing);
addAfterHead(existing);
} finally { lock.unlock(); }
return;
}
Node n = new Node(key, value);
// put in map first to avoid races reading null
Node prev = map.putIfAbsent(key, n);
if(prev != null){
// someone inserted concurrently
prev.val = value;
lock.lock();
try {
removeNode(prev);
addAfterHead(prev);
} finally { lock.unlock(); }
return;
}
lock.lock();
try {
addAfterHead(n);
if(map.size() > capacity){
Node lru = tail.prev;
removeNode(lru);
map.remove(lru.key);
}
} finally { lock.unlock(); }
}
// internal helpers - caller must hold lock
private void addAfterHead(Node n){
n.next = head.next;
n.prev = head;
head.next.prev = n;
head.next = n;
}
private void removeNode(Node n){
if(n.prev!=null){
n.prev.next = n.next;
n.next.prev = n.prev;
n.prev = n.next = null;
}
}
}MediumTechnical
62 practiced
Given a sorted array that has been rotated at an unknown pivot, implement a function in Java or Python to search for a target value and return its index, or -1 if not found. Example: nums = [4,5,6,7,0,1,2], target = 0 -> 4. Explain how you detect which side is sorted and how you ensure O(log n) time.
Sample Answer
Approach: Use a modified binary search. At each step compare nums[mid] to nums[left] to determine which half is sorted. If left..mid is sorted, check if target lies in that range; otherwise search right half. If mid..right is sorted, check there. This keeps halving the search space → O(log n).Key points:- Detect sorted side by comparing nums[left] and nums[mid].- Narrow search to the half where target can exist.Time complexity: O(log n). Space: O(1).Edge cases:- Empty array, single-element array, duplicates (the above assumes distinct elements; with duplicates you may need left++/right-- fallback).
python
def search_rotated(nums, target):
"""
Search target in rotated sorted array. Return index or -1.
O(log n) time, O(1) space.
"""
if not nums:
return -1
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# If left..mid is sorted
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
# Otherwise mid..right must be sorted
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1Unlock Full Question Bank
Get access to hundreds of Whiteboard and Chromebook Coding Proficiency interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.