Skip to content

Google Coding Strategy (L4) ​

Google's coding rounds grade more dimensions than any other FAANG. You are not just being asked "can you solve this problem" — you are being asked "can you solve this problem the way a senior Googler would solve it, in a stripped-down environment, while narrating, while thinking about trade-offs, while catching edge cases, and while writing code that another engineer could merge tomorrow."

This file is the tactical playbook.


1. The Google Doc Environment ​

Every coding round (phone screen + onsite) happens in a shared Google Doc. Not CoderPad. Not a real IDE. A Doc.

What you don't have ​

  • No syntax highlighting — your if and your for look the same as your variable names
  • No autocomplete — you type every System.out.println or std::unordered_map in full
  • No code execution — you cannot run, compile, or lint. You dry-run with your eyes.
  • No linter squiggles — typos, missing semicolons, unclosed braces are invisible until you re-read
  • No tab-stops or smart indent — the Doc converts tabs inconsistently; prefer spaces

What you do have ​

  • A blank page and a ticking 45-minute clock
  • An interviewer watching every keystroke in real time

Strategies to survive ​

Practice in the Doc before the interview. Open a fresh Doc, paste a problem statement in, and solve it end-to-end in the Doc. Do this for 10-15 problems across languages. You will be shocked how much slower you are without autocomplete.

Memorize the boilerplate. You should be able to type the following without thinking:

  • Priority queue / heap construction in your language
  • HashMap/HashSet declarations
  • Input parsing if the problem gives you a string
  • Binary search template (mid = lo + (hi - lo) / 2 to avoid overflow)
  • BFS template with visited set
  • DFS with memoization template

Pick one language and stay there. Switching from Python to Java mid-interview to "show flexibility" is a red flag. Use the language you are fastest and most accurate in. TypeScript, Python, C++, Java, Go are all fine.

Slow down your typing by 20%. A typo in a Doc costs 2 minutes: 30 seconds to spot, 90 seconds to stare at the surrounding 5 lines wondering why the logic is wrong. Type slightly slower and you save typos that cost 10x more time.

Use blank lines as visual scope cues. Without indentation highlighting, a 20-line function is a wall of text. Two blank lines between logical blocks makes the Doc scannable for you and the interviewer.

Leave your helpers at the top. Declare utility functions before main() / the solve function. Readers (interviewer + HC) scan top-to-bottom.


2. Pre-Coding Ritual (2-3 min) ​

Do NOT start typing code when the interviewer finishes reading the problem. The first 2-3 minutes are the highest-leverage part of the round.

Step 1: Clarify requirements (1-2 min) ​

Ask at least 2-3 clarifying questions, even on problems you recognize. Interviewers grade you on whether you thought about ambiguity.

Template questions that always apply:

  • "What are the input size bounds? Is n up to 10^3, 10^5, or 10^9?"
  • "Can the input be empty? Can it contain duplicates? Can values be negative?"
  • "What's the expected output format — a list, a count, a boolean?"
  • "Can I assume the input is valid, or do I need to handle malformed input?"
  • "Is there a memory constraint beyond the obvious?"

For graphs: directed vs undirected, cyclic vs acyclic, connected vs multiple components, self-loops, multi-edges. For trees: balanced? BST? complete? leaf-values only? For strings: ASCII, lowercase only, Unicode, empty allowed, max length?

Step 2: State your approach verbally ​

Before writing a single line of code, say:

"My plan is to use a sliding window with a HashMap tracking character counts. I'll expand the window while the constraint holds and shrink when it's violated. This is O(n) time and O(k) space where k is the number of distinct characters."

The interviewer is now evaluating your approach, not your typing. If your approach is wrong, they will give you a hint NOW, saving you 20 minutes of wrong code.

Step 3: State complexity BEFORE coding ​

Google grades this heavily. Say:

"Target complexity: O(n log n) time for the sort, O(n) auxiliary space for the result array."

If the interviewer wanted a different complexity (e.g., O(n) expected), they'll say so. If they nod, proceed with confidence.


3. Code Cleanliness Expectations ​

At L4, code cleanliness is scored as its own dimension. Here is what "meets bar" looks like.

Variable naming ​

  • Bad: i, j, k, tmp, arr, res
  • Meets bar: leftIdx, rightIdx, prefixSum, currentNode, result
  • Strong: windowStart, windowEnd, charFrequencies, visitedNodes, mergedIntervals

Use i, j ONLY for trivial loops where the index has no semantic meaning (e.g., iterating a fixed-size 2D grid).

Function decomposition ​

  • Any block exceeding ~15 lines should be extracted into a helper
  • Follow single responsibility: each function does one conceptual thing
  • Helpers live above main logic in the Doc

Bad (80-line monster):

typescript
function solve(input: string): number {
  // parse input
  // build graph
  // BFS from each node
  // compute answer
  // format output
  // ... all inline, no helpers
}

Meets bar:

typescript
function parseEdges(input: string): Edge[] { ... }
function buildAdjacencyList(edges: Edge[], n: number): number[][] { ... }
function bfsShortestPath(graph: number[][], source: number, target: number): number { ... }

function solve(input: string): number {
  const edges = parseEdges(input);
  const graph = buildAdjacencyList(edges, countNodes(edges));
  return bfsShortestPath(graph, 0, graph.length - 1);
}

SOLID-lite in LC problems ​

You are NOT expected to apply full SOLID to a LeetCode problem. You ARE expected to:

  • Keep functions small and focused (SRP)
  • Not mutate inputs unless the problem requires it
  • Return values rather than using globals
  • Name functions by what they do, not how they do it (getShortestPath not runBfs)

Comments ​

  • No comments for obvious code (// increment i)
  • YES comments for non-obvious decisions:
    • // Using BFS instead of DFS because we need shortest path, not reachability
    • // Early return for empty input to avoid null pointer in the loop below
    • // O(n log n) — sorted array lets us two-pointer in O(n)

4. Narration While Coding ​

Silent coding = bad signal. The HC packet will explicitly note: "Candidate coded in silence for 15 minutes — hard to evaluate problem-solving."

What to narrate ​

  • Before each block: "Now I'll build the adjacency list..."
  • When picking a data structure: "I'm using a HashMap here because I need O(1) lookup by node ID."
  • When making a trade-off: "I could use a deque, but a simple array with two pointers is cleaner and O(1) amortized."
  • When handling an edge case: "If the input is empty, I return an empty array early — prevents the null check in the loop."
  • When you notice a bug: "Wait, this doesn't handle the case where the window is smaller than k. Let me fix that." (Catching your own bug is a positive signal.)

What NOT to narrate ​

  • Every variable assignment (noise)
  • Obvious facts ("I'm going to write a for loop now" — no)
  • Your uncertainty ("I'm not sure this will work but let me try" — says low confidence without adding info)

Balance ​

Aim for ~60% coding, 40% talking in a round. Silent stretches of >90 seconds are a warning sign. If you need to think, say "Let me think about the recurrence for 30 seconds." Then pause intentionally.


5. Post-Coding Checklist ​

Once your solution compiles in your head, do NOT say "I'm done." Instead, run the checklist:

1. Dry-run with a concrete example (2-3 min) ​

Pick a non-trivial example from the problem (or make one up). Trace through your code line by line, updating variable state in your head OR in the Doc below the code.

Input: [1, 3, -1, -3, 5, 3, 6, 7], k = 3

Iteration | window | deque | result
--------- | ------ | ----- | ------
i=0       | [1]    | [0]   | []
i=1       | [1,3]  | [1]   | []
i=2       | [1,3,-1]| [1,2]| [3]
...

2. Handle edge cases explicitly ​

State each one OUT LOUD as you verify:

  • "Empty input — my early return on line 3 handles this, returns []."
  • "Single element — the while condition fails, returns [nums[0]]."
  • "All duplicates — the map-update still works, O(n) time."
  • "Max input size 10^5 — O(n log n) is ~1.7M ops, well within 1s."

3. Write 2-3 test cases ​

Format as:

typescript
// assert solve("abc", 2) === 3
// assert solve("", 1) === 0
// assert solve("aaaa", 2) === 3 -- duplicates boundary

Interviewers write in the packet: "Candidate proactively wrote tests and caught an off-by-one in the process." That's a Strong Hire signal.

4. Discuss alternatives ​

"An alternative would be sort + two-pointer, O(n log n) time but O(1) space. I chose the HashMap because the constraints allow O(n) space and the coefficient is smaller for this input size."


6. Complexity Analysis Framework ​

The 2-sentence template ​

  1. Time: one sentence, state the dominant operation and its count
  2. Space: one sentence, state the auxiliary space (not counting input/output)

Example:

"Time is O(n log n) — we sort once, then do a linear two-pointer sweep. Space is O(n) for the result array; if we can overwrite the input, auxiliary space is O(1) for the sort (in-place quicksort)."

Trade-off one-liners to memorize ​

  • "O(n log n) time for O(1) extra space if we sort in place; O(n) time for O(n) extra space if we hash."
  • "O(n) query but O(n log n) preprocessing — worth it if we expect >log(n) queries."
  • "O(1) amortized but O(n) worst-case — acceptable here because we have a budget of n total operations."
  • "The sorting dominates, so the overall is O(n log n). The rest is O(n)."

Always mention ​

  • Recursion depth for recursive solutions (space cost of the call stack)
  • Hidden constants when relevant (hash map overhead, cache misses on a linked list)
  • Best / average / worst when they differ (quicksort, hash collisions)

7. "I've Seen This Before" ​

You WILL get a problem you've seen on LeetCode. What to do:

Option A: Full disclosure (preferred) ​

"I've seen a similar problem before. Let me work through it fresh to make sure I get the details right — is that okay?"

The interviewer almost always says yes and continues. They grade your problem-solving from the next minute onward, not your memorization.

Option B: Quiet solve (risky) ​

If you jump straight to the optimal solution with clean code in 10 minutes and then can't explain WHY it works, the interviewer will suspect memorization and give you a harder follow-up. This is a trap.

The rule ​

Google grades problem-solving, not memorization. Solve it fresh. If you stumble on a step you "know" is wrong, the interviewer sees a thinking engineer, not a flashcard.


8. When Stuck ​

Getting stuck is not a failure. Failing to recover IS.

Recovery ladder ​

  1. State where you're stuck. "I'm trying to find the recurrence relation for this DP, and I can't figure out what the state should include."

  2. Try the brute force. "Let me write the O(n^2) solution first to make sure I understand the problem. I'll optimize after."

  3. Look for a pattern. Try 2-3 small examples by hand. Trace the answer. Often the recurrence jumps out.

  4. Ask for a hint strategically. "I'm considering sliding window and two pointers — would you nudge me in a direction?" The interviewer will give you a binary signal: "Sliding window is a good thought." This is better than flailing silently.

  5. Degrade gracefully. If you cannot reach the optimal, write the brute force cleanly and explain the gap. "This is O(n^2). I believe we can get to O(n log n) with a segment tree but I'm not confident in the implementation. Would you like me to attempt it or move on?"

Golden rule ​

Narrating an imperfect solution beats silent clean solution. The packet will say:

  • Silent clean: "Candidate solved but unclear if they understood trade-offs. Leaning Hire."
  • Narrated imperfect: "Candidate struggled briefly, identified the pattern, degraded gracefully to O(n^2) with full explanation. Hire."

9. Common Google Coding Traps ​

These are the bugs HC writeups call out most often:

typescript
// Wrong: hi = nums.length makes 'mid' potentially out of bounds on read
// Wrong: while (lo < hi) vs while (lo <= hi) — depends on target semantics
// Template (lower_bound, first index where pred is true):
let lo = 0, hi = nums.length;  // hi is EXCLUSIVE
while (lo < hi) {
  const mid = lo + Math.floor((hi - lo) / 2);
  if (pred(nums[mid])) hi = mid;
  else lo = mid + 1;
}
return lo;

Integer overflow in sums ​

  • In C++, int + int overflows at ~2.1B. Use long long for sums over arrays of ~10^5 ints with values up to 10^5.
  • In JS/TS, Number.MAX_SAFE_INTEGER is ~9 quadrillion, but be aware when you mix in bitwise ops (which truncate to 32 bits).
  • Compute mid = lo + (hi - lo) / 2, not mid = (lo + hi) / 2, to avoid overflow on the sum.

Iterator invalidation when modifying structure ​

typescript
// Wrong: modifying map while iterating
for (const [key, val] of map) {
  if (val < 0) map.delete(key);  // undefined behavior in some langs
}
// Right: collect, then delete
const toDelete = [];
for (const [key, val] of map) if (val < 0) toDelete.push(key);
toDelete.forEach(k => map.delete(k));

Confusing indices with values ​

Naming saves you: idxOfMin vs valOfMin. A common bug is return arr[arr.indexOf(min)] when you meant return arr.indexOf(min).

Not handling duplicates ​

  • In sorted arrays, duplicates break binary search for "find any" vs "find first" — state which you're finding.
  • In graphs, duplicate edges or self-loops break naive BFS.
  • In strings, duplicate characters change sliding window behavior.

Recursion without memoization ​

Stating "this is O(2^n)" and getting a nod is fine if the interviewer wants brute force. If they expected DP, your silent exponential recursion will cost you 10 minutes and the round.

Forgetting the empty / single-element case ​

Always handle:

  • input === null || input === undefined
  • input.length === 0
  • input.length === 1

Write the early returns FIRST, then the main logic.


10. Sample Problems: Good vs Great ​

Three problems showing the jump from "correct solution" (Leaning Hire) to "Google-grade solution" (Strong Hire).


Problem 1: Two Sum (LC 1) ​

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

Good (Leaning Hire) ​

typescript
function twoSum(nums: number[], target: number): number[] {
  const map = new Map();
  for (let i = 0; i < nums.length; i++) {
    const c = target - nums[i];
    if (map.has(c)) return [map.get(c), i];
    map.set(nums[i], i);
  }
  return [];
}

Correct, O(n), but: no edge case discussion, single-letter variable, no comment on complexity.

Great (Strong Hire) ​

typescript
/**
 * Two Sum: returns indices of two numbers summing to target.
 * Time: O(n) — single pass through nums.
 * Space: O(n) — HashMap holds up to n entries.
 * Assumes exactly one solution exists (per problem constraint).
 */
function twoSum(nums: number[], target: number): [number, number] | null {
  if (nums.length < 2) return null;

  const seenValueToIndex = new Map<number, number>();

  for (let currentIdx = 0; currentIdx < nums.length; currentIdx++) {
    const complement = target - nums[currentIdx];

    if (seenValueToIndex.has(complement)) {
      return [seenValueToIndex.get(complement)!, currentIdx];
    }

    // Only record the current value AFTER checking complement
    // to prevent matching nums[i] + nums[i] when target = 2 * nums[i].
    seenValueToIndex.set(nums[currentIdx], currentIdx);
  }

  return null;  // no pair found
}

// Tests:
// twoSum([2, 7, 11, 15], 9) === [0, 1]
// twoSum([3, 3], 6) === [0, 1]              — duplicates boundary
// twoSum([1], 2) === null                    — fewer than 2 elements
// twoSum([], 0) === null                     — empty

Differences:

  • Complexity in a docstring
  • Descriptive names (seenValueToIndex, complement, currentIdx)
  • Explicit edge case handling AND a returned null rather than arbitrary []
  • Comment on the non-obvious "why set after check" decision
  • Test cases written

Problem 2: Number of Islands (LC 200) ​

Given an m x n 2D binary grid, return the number of islands. An island is surrounded by water and formed by connecting adjacent lands horizontally or vertically.

Good (Leaning Hire) ​

cpp
int numIslands(vector<vector<char>>& grid) {
    int count = 0;
    for (int i = 0; i < grid.size(); i++) {
        for (int j = 0; j < grid[0].size(); j++) {
            if (grid[i][j] == '1') {
                count++;
                dfs(grid, i, j);
            }
        }
    }
    return count;
}

void dfs(vector<vector<char>>& grid, int i, int j) {
    if (i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || grid[i][j] != '1') return;
    grid[i][j] = '0';
    dfs(grid, i+1, j);
    dfs(grid, i-1, j);
    dfs(grid, i, j+1);
    dfs(grid, i, j-1);
}

Works, but: mutates input silently, no comment on recursion depth (stack overflow risk), magic strings '1' and '0'.

Great (Strong Hire) ​

cpp
/**
 * Number of Islands.
 * Time: O(m * n) — each cell visited at most twice.
 * Space: O(m * n) worst case for recursion stack on a grid of all land.
 *        For very large grids (e.g., 10^6 x 10^6) switch to iterative BFS.
 *
 * Approach: DFS from each unvisited land cell, marking visited in-place.
 * NOTE: mutates input grid. If callers need the grid intact, copy first
 *       or use a separate `visited` 2D array at O(m*n) extra space.
 */
class Solution {
    static constexpr char LAND = '1';
    static constexpr char WATER = '0';
    static constexpr int DIRS[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};

public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) return 0;

        int islandCount = 0;
        const int rows = grid.size();
        const int cols = grid[0].size();

        for (int row = 0; row < rows; ++row) {
            for (int col = 0; col < cols; ++col) {
                if (grid[row][col] == LAND) {
                    ++islandCount;
                    sinkIsland(grid, row, col);
                }
            }
        }
        return islandCount;
    }

private:
    void sinkIsland(vector<vector<char>>& grid, int row, int col) {
        if (row < 0 || col < 0 ||
            row >= (int)grid.size() || col >= (int)grid[0].size() ||
            grid[row][col] != LAND) {
            return;
        }
        grid[row][col] = WATER;
        for (const auto& [dr, dc] : DIRS) {
            sinkIsland(grid, row + dr, col + dc);
        }
    }
};

Differences:

  • Complexity + space caveat for huge grids (shows scale awareness)
  • Named constants for '1' / '0' / directions
  • Named helper (sinkIsland) communicates intent
  • Explicit mutation warning in the comment (trade-off articulation)
  • Empty grid guarded

Problem 3: LRU Cache (LC 146) ​

Design a data structure for LRU cache with O(1) get and put.

Good (Leaning Hire) ​

typescript
class LRUCache {
  private map = new Map<number, number>();
  constructor(private cap: number) {}

  get(key: number): number {
    if (!this.map.has(key)) return -1;
    const v = this.map.get(key)!;
    this.map.delete(key);
    this.map.set(key, v);
    return v;
  }

  put(key: number, value: number): void {
    if (this.map.has(key)) this.map.delete(key);
    else if (this.map.size >= this.cap) {
      const oldest = this.map.keys().next().value;
      this.map.delete(oldest);
    }
    this.map.set(key, value);
  }
}

Clever (using Map's insertion-order property), correct, O(1). But the interviewer might say: "Implement it WITHOUT leveraging Map's ordering — build your own DLL." Be ready.

Great (Strong Hire) ​

typescript
/**
 * LRU Cache with O(1) get and put.
 *
 * Data structures:
 *   - HashMap<key, Node>    : O(1) key lookup
 *   - Doubly Linked List    : O(1) move-to-front and O(1) evict-from-tail
 *
 * The DLL maintains recency order: head = most recently used, tail = LRU.
 * HashMap points to the node so we can detach it in O(1) without searching.
 */
class Node {
  prev: Node | null = null;
  next: Node | null = null;
  constructor(public key: number, public value: number) {}
}

class LRUCache {
  private readonly keyToNode = new Map<number, Node>();
  private readonly head = new Node(-1, -1);  // sentinel: most-recent side
  private readonly tail = new Node(-1, -1);  // sentinel: LRU side

  constructor(private readonly capacity: number) {
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  get(key: number): number {
    const node = this.keyToNode.get(key);
    if (!node) return -1;
    this.moveToFront(node);
    return node.value;
  }

  put(key: number, value: number): void {
    const existing = this.keyToNode.get(key);
    if (existing) {
      existing.value = value;
      this.moveToFront(existing);
      return;
    }

    if (this.keyToNode.size >= this.capacity) {
      this.evictLRU();
    }

    const node = new Node(key, value);
    this.keyToNode.set(key, node);
    this.insertAtFront(node);
  }

  private moveToFront(node: Node): void {
    this.detach(node);
    this.insertAtFront(node);
  }

  private insertAtFront(node: Node): void {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next!.prev = node;
    this.head.next = node;
  }

  private detach(node: Node): void {
    node.prev!.next = node.next;
    node.next!.prev = node.prev;
  }

  private evictLRU(): void {
    const lru = this.tail.prev!;
    if (lru === this.head) return;  // empty
    this.detach(lru);
    this.keyToNode.delete(lru.key);
  }
}

Differences:

  • Sentinel nodes eliminate null checks (classic DLL hygiene)
  • Private helpers for each invariant operation (moveToFront, insertAtFront, detach, evictLRU)
  • Top-of-file comment explains the data-structure choice AND why
  • readonly on capacity and keyToNode signals intent
  • Works even if interviewer forbids Map insertion-order tricks

11. Quick-Reference Checklist ​

Keep this open during practice. Every problem, every time.

Before coding ​

  • [ ] 2-3 clarifying questions asked
  • [ ] Approach stated verbally
  • [ ] Target complexity stated (time + space)
  • [ ] Edge cases enumerated

While coding ​

  • [ ] Narrating key decisions
  • [ ] Meaningful variable names
  • [ ] Helper functions for non-trivial blocks
  • [ ] Comments on non-obvious decisions
  • [ ] Typing at 80% speed for accuracy

After coding ​

  • [ ] Dry-run with a concrete example
  • [ ] Edge cases verified explicitly
  • [ ] 2-3 test cases written
  • [ ] Alternative approach discussed
  • [ ] Complexity restated post-implementation

Cross-references ​

Frontend interview preparation reference.