Interview prep · Lesson 45 of 62 · about 60 min
The coding interview playbook
How FAANG coding interviews are scored and a step-by-step method for the 45 minutes: clarify, examples, brute force, optimize, code, test. Includes a worked example narrated out loud, a constraints-to-complexity table, the Go toolkit to have memorized, and the Go pitfalls that cost offers.
On this page · 10 sections
What interviewers score#
Most large companies use a rubric close to this one. Each area is rated separately, and a strong hire signal needs all four.
| Area | Strong signal | Weak signal |
|---|---|---|
| Problem solving | finds a correct approach, improves it, explains why it works | jumps to code, cannot justify the approach |
| Coding | clean, idiomatic, correct code with sensible names | messy code, many bugs, fights the language |
| Communication | thinks out loud, checks assumptions, takes hints well | long silences, ignores hints |
| Verification | traces examples, finds own bugs, covers edge cases | says "done" without testing |
An optimal solution with no explanation can score lower than a slightly slower one explained well. The interviewer has to write a report that justifies the decision, and they can only quote what you said.
The 45 minutes#
Drawing diagram…
1. Clarify. Restate the problem in your own words, then ask: How big is the input? Can it be empty? Are there duplicates or negative numbers? Is the input sorted? What should I return if there is no answer? Can I modify the input?
2. Examples. Work one normal example by hand and write down one or two edge cases (empty, one element, all equal, the answer at the very end). Small examples are where you find the pattern.
3. Brute force. Say the simplest correct solution and its complexity: "Check every substring, that's O(n²)." You are not committing to it. It proves you understand the problem and gives you something to improve.
4. Optimize. Look for the bottleneck (the step that dominates), unnecessary work (computing something you throw away), and duplicated work (recomputing what you computed before). Then reach for the matching tool: a hash map for repeated lookups, sorting for order, a window for contiguous ranges, a heap for "the best k". Agree on the approach before you write code: "I'd like to do X in O(n). Does that sound good?"
5. Code. Write clean code from top to bottom, narrating as you go. Use helper functions with good names instead of one dense block. If you are unsure about a small detail, say so and leave a note to come back to it.
6. Test. Trace your code by hand on your example, line by line, tracking variables. Then run the edge cases. Finding your own bug is a positive signal; a bug the interviewer finds is a negative one.
Constraints tell you the target#
Input limits reveal the intended complexity. A computer does roughly 10⁸ simple operations per second, and interview limits are set so the intended solution fits comfortably.
| Input size | Target complexity | Typical techniques |
|---|---|---|
| n ≤ 12 | O(n!) | permutations, brute force |
| n ≤ 20–25 | O(2ⁿ) | subsets, bitmask DP, backtracking |
| n ≤ 500 | O(n³) | interval DP, Floyd-Warshall |
| n ≤ 5,000 | O(n²) | 2D DP, all pairs |
| n ≤ 10⁶ | O(n log n) | sorting, heaps, binary search |
| n ≤ 10⁸ | O(n) | one pass, hashing, two pointers, window |
| n ≥ 10⁹ | O(log n) or O(1) | binary search on the answer, math |
A worked example, out loud#
Longest Repeating Character Replacement. Given a string of uppercase letters and an integer k, you may change at most k characters. Return the length of the longest substring that can be made of one repeated letter.
Clarify. "Only uppercase A–Z? Length up to 10⁵? k can be 0? If the string is AABABBA and k = 1, I can change one B to get AABAAA... wait, the substring has to be contiguous, so I'd get AAAA from AABA, length 4. Correct?"
Brute force. "For each substring, the letters I must change are its length minus the count of its most common letter. If that is at most k, the substring works. Checking all substrings with a running count is O(n² · 26), too slow for 10⁵, but it is a good correctness check."
Optimize. "The substrings are contiguous and I want the longest valid one, which suggests a sliding window. Validity is length - maxCount <= k. Extending the window can only break validity, and shrinking from the left restores it, so two pointers work: grow on the right, shrink on the left while invalid. Each index enters and leaves once: O(n)."
A subtle point an interviewer may ask about: after shrinking, maxFreq can be stale (higher than the true maximum in the window). That is fine: it only makes the window shrink less eagerly, and the answer only grows when a window with a truly higher count appears.
Code and test. Here is the solution, together with the brute force as a checker. This is also how you should practice: on random inputs, a slow solution you trust catches bugs in a fast one.
package main
import (
"fmt"
"math/rand/v2"
)
// brute checks every substring: O(n² · 26). Slow, obviously correct: the reference.
func brute(s string, k int) int {
best := 0
for i := range len(s) {
var count [26]int
maxFreq := 0
for j := i; j < len(s); j++ {
count[s[j]-'A']++
maxFreq = max(maxFreq, count[s[j]-'A'])
if j-i+1-maxFreq <= k {
best = max(best, j-i+1)
}
}
}
return best
}
// characterReplacement keeps a window where length - (count of its most common letter) <= k.
func characterReplacement(s string, k int) int {
var count [26]int
best, maxFreq, left := 0, 0, 0
for right := range len(s) {
count[s[right]-'A']++
maxFreq = max(maxFreq, count[s[right]-'A'])
for right-left+1-maxFreq > k {
count[s[left]-'A']--
left++
}
best = max(best, right-left+1)
}
return best
}
func main() {
examples := []struct {
s string
k int
}{{"ABAB", 2}, {"AABABBA", 1}, {"A", 0}, {"ABCDE", 1}, {"AAAA", 0}}
for _, e := range examples {
fmt.Printf("%-8s k=%d -> %d\n", e.s, e.k, characterReplacement(e.s, e.k))
}
// Randomized check against the brute force.
r := rand.New(rand.NewPCG(1, 2))
for range 2000 {
b := make([]byte, 1+r.IntN(12))
for i := range b {
b[i] = "ABC"[r.IntN(3)]
}
s, k := string(b), r.IntN(4)
if got, want := characterReplacement(s, k), brute(s, k); got != want {
fmt.Println("MISMATCH", s, k, got, want)
return
}
}
fmt.Println("2000 random cases agree with the brute force")
}ABAB k=2 -> 4
AABABBA k=1 -> 4
A k=0 -> 1
ABCDE k=1 -> 2
AAAA k=0 -> 4
2000 random cases agree with the brute forceThe Go toolkit to have memorized#
You will not have time to look things up. These are the pieces that come up again and again, written once so you can type them from memory. The generic heap alone covers top-k, Dijkstra, merging k lists, scheduling and median problems.
package main
import (
"cmp"
"container/heap"
"fmt"
"slices"
"sort"
"strings"
)
// Heap is a generic binary heap for container/heap: pass any "less" function.
type Heap[T any] struct {
data []T
less func(a, b T) bool
}
func (h *Heap[T]) Len() int { return len(h.data) }
func (h *Heap[T]) Less(i, j int) bool { return h.less(h.data[i], h.data[j]) }
func (h *Heap[T]) Swap(i, j int) { h.data[i], h.data[j] = h.data[j], h.data[i] }
func (h *Heap[T]) Push(x any) { h.data = append(h.data, x.(T)) }
func (h *Heap[T]) Pop() any {
n := len(h.data)
x := h.data[n-1]
h.data = h.data[:n-1]
return x
}
// DSU is union-find with path halving and union by size.
type DSU struct{ parent, size []int }
func NewDSU(n int) *DSU {
d := &DSU{make([]int, n), make([]int, n)}
for i := range n {
d.parent[i], d.size[i] = i, 1
}
return d
}
func (d *DSU) Find(x int) int {
for d.parent[x] != x {
d.parent[x] = d.parent[d.parent[x]]
x = d.parent[x]
}
return x
}
func (d *DSU) Union(a, b int) bool {
a, b = d.Find(a), d.Find(b)
if a == b {
return false
}
if d.size[a] < d.size[b] {
a, b = b, a
}
d.parent[b] = a
d.size[a] += d.size[b]
return true
}
var dirs = [4][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
// shortestPath is BFS on a grid: '#' is a wall. Returns -1 if unreachable.
func shortestPath(grid []string, sr, sc, tr, tc int) int {
dist := make([][]int, len(grid))
for i := range dist {
dist[i] = slices.Repeat([]int{-1}, len(grid[0]))
}
dist[sr][sc] = 0
queue := [][2]int{{sr, sc}}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
for _, d := range dirs {
r, c := cur[0]+d[0], cur[1]+d[1]
if r < 0 r >= len(grid) c < 0 c >= len(grid[0]) grid[r][c] == '#' dist[r][c] != -1 {
continue
}
dist[r][c] = dist[cur[0]][cur[1]] + 1
queue = append(queue, [2]int{r, c})
}
}
return dist[tr][tc]
}
func main() {
// Top 3 largest with a min-heap of size 3: O(n log k).
h := &Heap[int]{less: func(a, b int) bool { return a < b }}
for _, x := range []int{5, 1, 9, 3, 7, 8, 2} {
heap.Push(h, x)
if h.Len() > 3 {
heap.Pop(h)
}
}
top := slices.Clone(h.data)
slices.SortFunc(top, func(a, b int) int { return cmp.Compare(b, a) })
fmt.Println("top 3:", top)
// Priority queue of structs: earliest deadline first.
type task struct {
name string
deadline int
}
pq := &Heap[task]{less: func(a, b task) bool { return a.deadline < b.deadline }}
for _, t := range []task{{"deploy", 3}, {"review", 1}, {"email", 2}} {
heap.Push(pq, t)
}
var order []string
for pq.Len() > 0 {
order = append(order, heap.Pop(pq).(task).name)
}
fmt.Println("by deadline:", order)
// Union-find: count connected components.
d := NewDSU(6)
for _, e := range [][2]int{{0, 1}, {1, 2}, {3, 4}} {
d.Union(e[0], e[1])
}
roots := map[int]struct{}{} // a set
for i := range 6 {
roots[d.Find(i)] = struct{}{}
}
fmt.Println("components:", len(roots))
// BFS on a grid.
grid := []string{
"..#.",
"..#.",
"....",
}
fmt.Println("grid steps:", shortestPath(grid, 0, 0, 0, 3))
// Binary search on the answer: smallest x with x*x >= 50.
fmt.Println("smallest x with x*x >= 50:", sort.Search(100, func(x int) bool { return x*x >= 50 }))
// Sort intervals by end, then by start.
iv := [][2]int{{1, 4}, {2, 3}, {0, 3}}
slices.SortFunc(iv, func(a, b [2]int) int { return cmp.Or(cmp.Compare(a[1], b[1]), cmp.Compare(a[0], b[0])) })
fmt.Println("by end:", iv)
// Counting letters: an anagram key that is comparable, so it works as a map key.
key := func(s string) [26]byte {
var k [26]byte
for i := range len(s) {
k[s[i]-'a']++
}
return k
}
fmt.Println("anagrams:", key("listen") == key("silent"))
// Building strings: strings.Builder, never += in a loop.
var sb strings.Builder
for i := range 3 {
fmt.Fprintf(&sb, "[%d]", i)
}
fmt.Println("built:", sb.String())
}top 3: [9 8 7]
by deadline: [review email deploy]
components: 3
grid steps: 7
smallest x with x*x >= 50: 8
by end: [[0 3] [2 3] [1 4]]
anagrams: true
built: [0][1][2]Go pitfalls that cost offers#
| Pitfall | What happens | Fix |
|---|---|---|
| appending a reused slice in backtracking | every saved result changes later | append(res, slices.Clone(path)) |
| iterating a map when order matters | output order differs each run | collect and sort the keys |
s[i] on a string with non-ASCII text | you get bytes, not characters | for _, r := range s or []rune(s) |
queue = queue[1:] on a huge queue | memory of popped items is kept | fine in interviews; mention a ring buffer for production |
comparing slices with == | does not compile | slices.Equal, or use arrays as keys |
int overflow in products | wraps silently (int is 64-bit) | check bounds; use math/big or modular arithmetic |
shadowing with := inside an if | the outer variable never changes | use = for the outer variable |
When you are stuck#
Silence is the worst outcome. Say what you are trying: "I'm thinking about whether sorting helps... sorting loses the original positions, which I need, so no." Then try, in order: solve a smaller example by hand and watch what you do; simplify the problem (what if k were 0? what if values were small?); go through the data structure list (hash map, two pointers, stack, heap, binary search, BFS/DFS, DP) and ask whether each one helps. If the interviewer gives a hint, take it immediately and build on it. Using hints well is a skill they rate.
Summary#
- Interviewers score problem solving, coding, communication and testing; all four need to be strong.
- Follow six steps: clarify, examples, brute force, optimize, code, test. Agree on the approach before coding.
- Input limits tell you the target complexity: 10⁵ means O(n log n) or better.
- Keep a Go toolkit in your fingers: a generic heap, union-find, grid BFS,
sort.Search, multi-key sorting. - Avoid the Go pitfalls: shared slices in backtracking, map order, bytes vs runes.
- Test by tracing your own code; finding your own bug is a good signal.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.The interviewer gives you a problem. What should you do first?
Q2.The input size is n ≤ 100,000. Which complexity should you aim for?
Q3.You see a brute force but not the optimal solution yet. What is best?
Q4.In Go, you iterate over a map to build the answer, and the order matters. What goes wrong?
Q5.In backtracking, you append the current path to the results with
res = append(res, path). What is the bug?
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
Show hint
The worked example in this lesson. Redo it from memory in 20 minutes, talking out loud.
Show hint
Prefix products from the left, suffix products from the right.
- Valid Sudoku ↗medium
Show hint
Three arrays of 9 bitmasks: rows, columns, boxes. Box index is (r/3)*3 + c/3.
- Search a 2D Matrix ↗medium
Show hint
Treat the matrix as one sorted array of m×n and binary search index i → (i/n, i%n).
- Timed drillbuild
Pick any 3 medium problems you have not seen. Set a 25-minute timer for each and follow all six steps out loud, recording yourself. Watch it back and note where you went silent or skipped testing.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.