Google .NET Developer Interview Questions & Answers

google icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

11. Basic RegexCodingEasyGoogle

Question Details

Implement the minimum regex behavior implied by the prompt, define which operators are supported, and explain what a non-match should return.

Short Interview Answer (30-60 seconds)

I would use dynamic programming with a Boolean table. I define dp[i, j] as whether the first i characters of the text match the first j characters of the pattern. I start with dp[0, 0] = true, handle '*' states that can match an empty text, and then fill the table from smaller prefixes to larger ones. Literals and '.' use the diagonal state, while '*' handles zero or more copies. Time is O(m × n), with O(m × n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input has a text string and a simple pattern. The pattern supports normal characters, '.' for any one character, and '*' for zero or more copies of the token immediately before it. We must match the whole text, not search for a matching part. The output is true when the complete text matches the complete pattern and false otherwise. In the diagram, text is "acb" and pattern is "a.*b". The result is true. A dynamic programming table fits because each larger match can be built from smaller prefix matches.

Useful Questions to Ask the Interviewer
  1. Can I assume every '*' has a valid token immediately before it?
  2. Should matching cover the entire text rather than search for a matching substring?
  3. Are literals, '.', and '*' the only supported pattern features?
Basic Regex diagram
How to Explain It in an Interview
1. Define the DP state and required result

Let m = text.Length and n = pattern.Length. Create a Boolean table dp with m + 1 rows and n + 1 columns. The state dp[i, j] means that the first i characters of text match the first j characters of pattern completely.

For the diagram example, text = "acb", so m =

  1. The pattern is "a.*b", so n =
  2. The final answer is stored in dp[3, 4].

This is full-string matching. A true final cell means the whole input matches. A false final cell means a non-match.

2. Initialize the table

Start with dp[0, 0] = true because an empty text matches an empty pattern.

Then initialize the first row for patterns containing '*'. A '*' can use zero copies of the token before it, so when pattern[j - 1] is '*', the empty-text state is dp[0, j] = dp[0, j - 2].

For this example, pattern[2] is '*'. Therefore dp[0, 3] = dp[0, 1] = false.

The invariant is that every filled dp[i, j] cell correctly describes whether those two complete prefixes match.

3. Handle literal characters and '.'

Process i from 1 through m and j from 1 through n. Look at pattern[j - 1].

If it is a normal literal, it must equal text[i - 1]. If it is '.', it can match any one current text character. When the current token matches, use dp[i - 1, j - 1]. This removes one character from both prefixes and asks whether the remaining prefixes already match.

For example, dp[1, 1] starts from dp[0, 0]. The current text character is 'a' and the current pattern character is 'a'. They match, and dp[0, 0] is true, so dp[1, 1] becomes true.

Later, dp[2, 2] uses dp[1, 1]. The current text character is 'c' and the current pattern token is '.'. The '.' matches 'c', and dp[1, 1] is true, so dp[2, 2] becomes true.

4. Handle '*'

When pattern[j - 1] is '*', there are two valid cases.

For zero occurrences, skip both '*' and the token before it. Use dp[i, j - 2]. In the example, dp[1, 3] is calculated from dp[1, 1]. Before this step, dp[1, 1] is true. The ".*" part can use zero characters, so dp[1, 3] becomes true.

For one or more occurrences, first check whether the token before '*' matches text[i - 1]. If it does, consume one text character but stay at the same pattern position. Use dp[i - 1, j]. Staying at j is important because '*' may match another character.

For dp[2, 3], the previous pattern token is '.'. It matches text[1] = 'c'. Before this step, dp[1, 3] is true, so extending '*' makes dp[2, 3] true.

For dp[3, 3], '.' also matches text[2] = 'b'. Before this step, dp[2, 3] is true, so extending '*' again makes dp[3, 3] true.

5. Walk through the exact successful states

The important true states shown in the diagram are:

  1. dp[1, 1] = true because 'a' matches 'a' and dp[0, 0] is true.
  2. dp[1, 3] = true because '*' uses zero occurrences and dp[1, 1] is true.
  3. dp[2, 2] = true because '.' matches 'c' and dp[1, 1] is true.
  4. dp[2, 3] = true because '*' extends the match from dp[1, 3].
  5. dp[3, 3] = true because '*' extends the match from dp[2, 3].
  6. dp[3, 4] = true because the final pattern character 'b' matches text[2] = 'b' and dp[2, 3] is true.

The final DP grid is: Ø: T F F F F a: F T F T F c: F F T T F b: F F F T T

Therefore dp[3, 4] = true, so "acb" matches "a.*b". The method returns true. If dp[m, n] were false, the correct non-match return value would be false.

6. Explain correctness, complexity, and edge cases

The solution is correct because each state follows the definition of dp[i, j]. A literal or '.' consumes exactly one character from the text and one token from the pattern, so it uses dp[i - 1, j - 1]. A '*' covers both legal choices: zero copies with dp[i, j - 2], or one or more copies with dp[i - 1, j] when the previous token matches.

There are O(m × n) table cells, and each cell needs constant work. Therefore the time complexity is O(m × n). The table uses O(m × n) auxiliary space.

Important edge cases include empty text with an empty pattern returning true, empty text matching a pattern such as "a*", '*' representing zero characters, and a non-match such as text = "ab" with pattern = "a*c" returning false.

Key Insight / Why This Solution Works

The key insight is to solve the match for every pair of text and pattern prefixes. The invariant is: dp[i, j] is true exactly when the first i characters of text match the first j characters of pattern completely. This works because each state depends on smaller states that are already known. A matching literal or '.' uses dp[i - 1, j - 1]. A '*' either skips itself and its previous token with dp[i, j - 2], or consumes another matching text character while staying at the same pattern position with dp[i - 1, j]. The final result is dp[m, n].

Code
using System;

public static class Program
{
    public static void Main()
    {
        // Run the exact example from the diagram.
        string text = "acb";
        string pattern = "a.*b";

        bool result = IsBasicRegexMatch(text, pattern);

        // The expected output for "acb" against "a.*b" is true.
        Console.WriteLine(result);
    }

    public static bool IsBasicRegexMatch(string text, string pattern)
    {
        // Store the input lengths so the DP table can represent every prefix,
        // including the empty prefix at index 0.
        int m = text.Length;
        int n = pattern.Length;

        // dp[i, j] is true when the first i text characters match
        // the first j pattern characters completely.
        bool[,] dp = new bool[m + 1, n + 1];

        // Empty text matches an empty pattern.
        dp[0, 0] = true;

        // Prefill states where '*' can make a pattern prefix match empty text
        // by using zero copies of the token immediately before '*'.
        for (int j = 2; j <= n; j++)
        {
            if (pattern[j - 1] == '*')
            {
                dp[0, j] = dp[0, j - 2];
            }
        }

        // Fill all non-empty text-prefix states from smaller prefixes
        // to larger prefixes so every dependency is already available.
        for (int i = 1; i <= m; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                char p = pattern[j - 1];

                // '*' always refers to the token immediately before it.
                // First try zero occurrences by skipping both tokens.
                if (p == '*' && j >= 2)
                {
                    dp[i, j] = dp[i, j - 2];
                    char previousPatternToken = pattern[j - 2];

                    // If the previous pattern token matches the current text
                    // character, consume one text character but stay at the
                    // same pattern position so '*' can match again.
                    if (previousPatternToken == '.' || previousPatternToken == text[i - 1])
                    {
                        dp[i, j] = dp[i, j] || dp[i - 1, j];
                    }
                }
                // A literal must equal the current text character.
                // '.' matches any single current text character.
                else if (p == '.' || p == text[i - 1])
                {
                    dp[i, j] = dp[i - 1, j - 1];
                }
            }
        }

        // This is full-string matching. False means the input is a non-match.
        return dp[m, n];
    }
}
Time & Space Complexity

Let m be text.Length and n be pattern.Length. The algorithm fills a table with (m + 1) × (n + 1) Boolean cells. Each cell needs constant work, so the time complexity is O(m × n). The dynamic programming table grows with both input lengths, so the auxiliary space complexity is O(m × n). Auxiliary space means the extra memory used by the algorithm.

Where it is used

This dynamic programming pattern is useful when a matching problem can be divided into smaller prefix-matching problems. It fits simple full-string pattern matching where special operators such as '.' and '*' make the current result depend on earlier text and pattern prefixes. This specific matcher supports only literals, '.', and '*', not advanced regex features such as alternation or character classes.

Why Interviewers Ask This

This question checks whether you can turn pattern-matching rules into a precise dynamic programming state. It tests whether you can define and maintain an invariant, initialize empty-prefix cases correctly, handle the two '*' transitions, and keep text and pattern indices consistent. It also checks whether your C# code matches your reasoning, whether you understand full-string matching, and whether you can explain the O(m × n) time and O(m × n) auxiliary space accurately.

Common interview mistakes

A common mistake is treating the task as substring search instead of full-string matching. Another is forgetting that '*' applies only to the immediately preceding token. Candidates may also use the wrong DP dependency for '*': zero occurrences must use dp[i, j - 2], while one or more occurrences use dp[i - 1, j] only when the previous token matches the current text character. A fourth mistake is forgetting the first-row initialization that lets patterns such as "a*" match an empty text.

Interview tip

Define dp[i, j] clearly before writing the recurrence. Then explain '*' as two separate choices: use zero copies and move back two pattern positions, or consume one matching text character and stay at the same pattern position.

Interviewer may ask next
Can the auxiliary space be reduced?

Yes. The recurrence needs values from the current row, such as dp[i, j - 2], and values from the previous row, such as dp[i - 1, j] and dp[i - 1, j - 1]. We can keep two rows instead of the full table. The time remains O(m × n), while auxiliary space becomes O(n), where n is the pattern length. The tradeoff is more careful indexing and less straightforward code.

What changes if '*' is not guaranteed to have a valid previous token?

The implementation must avoid applying the '*' recurrence unless there is a previous pattern token. The shown code already checks j >= 2 before using pattern[j - 2]. A leading '*' therefore cannot use the repetition transition and does not create a valid match. The dynamic programming method stays the same, with O(m × n) time and O(m × n) auxiliary space.

12. Job RecommendationCodingEasyGoogle

Question Details

Use the reported recommendation-style prompt and explain how you would map user history to recommended jobs while avoiding already-seen items and duplicate suggestions.

Short Interview Answer (30-60 seconds)

I would build a HashSet from the user’s seen job IDs, then sort candidate jobs by relevance score from highest to lowest. I would scan that ranked list, skip jobs already in the seen set, and use another HashSet so the same job ID cannot be suggested twice. I stop when I collect K valid jobs. The time is O(n log n) because of sorting. With the shown LINQ implementation, auxiliary space is O(n + m + k).

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to choose the best job suggestions for a user. We know which job IDs the user has already seen. We also have candidate jobs with relevance scores. Higher scores are better. We must return up to K jobs, ordered from highest score to lowest score. A returned job must not already be in the user history, and the same job ID must not appear twice. We rank the candidates first, then filter them while collecting the first K valid jobs.

Useful Questions to Ask the Interviewer
  1. Is the relevance score already calculated for every candidate job?
  2. Should higher scores always appear before lower scores in the result?
  3. Can the candidate list contain the same job ID more than once?
  4. What should we return when fewer than K unseen jobs exist?
Job Recommendation diagram
How to Explain It in an Interview
1. Understand the input and required output

The diagram uses seen job IDs [1, 4, 7], nine candidate jobs, and K = 3. Each candidate has a job ID and a relevance score. We need up to three job IDs that the user has not already seen. The returned IDs must also be unique. The output is ordered by relevance score from highest to lowest.

2. Choose the algorithm and data structures

I use a HashSet<int> called seen for the user history. This gives O(1) average-time membership checks. I sort all candidate jobs by descending relevance score. I also use another HashSet<int> called added. It records job IDs already placed in the result, so duplicate candidate IDs cannot create duplicate suggestions.

The main invariant is simple: the result always contains only unseen, unique job IDs, accepted in descending relevance-score order.

3. Initialize the state

Start with seen = {1, 4, 7}. The result list is empty, and the added set is empty. Sorting the example candidates by score gives job IDs in this order: 1, 2, 3, 4, 5, 6, 7, 8, 9.

4. Walk through the example

Job 1 has score 0.91. Its ID is in seen, so we skip it. The result remains [].

Job 2 has score 0.88. It is not in seen, and its ID has not been added before. We add it. The result becomes [2], and added becomes {2}.

Job 3 has score 0.87. It is unseen and unique, so we add it. The result becomes [2, 3], and added becomes {2, 3}.

Job 4 has score 0.85. Its ID is in seen, so we skip it. The result remains [2, 3].

Job 5 has score 0.83. It is unseen and unique, so we add it. The result becomes [2, 3, 5]. The result count is now equal to K, so processing stops. Jobs 6, 7, 8, and 9 are not processed.

5. Explain why the result is correct

Sorting makes us consider higher-scoring jobs before lower-scoring jobs. The seen set prevents an already-viewed job from entering the result. The added set prevents the same job ID from entering the result more than once. Therefore every accepted job is unseen and unique. Because we stop after the first K valid jobs in descending score order, the example correctly returns [2, 3, 5].

6. Explain the C# implementation

The code creates the seen set, sorts candidates with OrderByDescending, creates result and added, and then scans the ranked candidates. A seen job is skipped. A duplicate job ID is also skipped. Otherwise its ID is appended to the result. When result.Count == k, the loop stops immediately and the result is returned.

7. Explain complexity and edge cases

Sorting N candidates takes O(N log N) time. The filtering scan is O(N), with O(1) average-time HashSet lookup and insertion. The total time is therefore O(N log N). In the executable implementation below, OrderByDescending(...).ToList() creates a ranked copy, so auxiliary space is O(N + M + K), where M is the history size and K is the number of collected recommendations. Important cases are K <= 0, no candidates, all candidates already seen, fewer than K valid jobs, and duplicate candidate IDs.

Key Insight / Why This Solution Works

The key idea is to separate ranking from filtering. First, sort all candidate jobs from highest relevance score to lowest. Then scan that order and accept a candidate only if its job ID is not in the user's history and has not already been added. The central invariant is that the result always contains only unseen, unique jobs in the same descending-score order in which they were accepted. Once K valid jobs have been collected, later candidates cannot improve the required top-K result, so processing stops.

Code
using System;
using System.Collections.Generic;
using System.Linq;

public static class Program
{
    public static void Main()
    {
        // Use the exact user-history example shown in the diagram.
        List<int> userHistory = new List<int> { 1, 4, 7 };

        // Use the exact candidate IDs and relevance scores shown in the diagram.
        List<(int jobId, double score)> candidates =
            new List<(int jobId, double score)> { (1, 0.91), (2, 0.88), (3, 0.87),
                                                  (4, 0.85), (5, 0.83), (6, 0.80),
                                                  (7, 0.78), (8, 0.77), (9, 0.76) };

        // The example asks for the top three valid recommendations.
        int k = 3;

        List<int> recommendations = RecommendJobs(userHistory, candidates, k);

        // The exact expected output from the diagram is [2, 3, 5].
        Console.WriteLine($"[{string.Join(", ", recommendations)}]");
    }

    public static List<int> RecommendJobs(IList<int> userHistory,
                                          IList<(int jobId, double score)> candidates, int k)
    {
        // Return no recommendations when K is non-positive or there are no candidates.
        if (k <= 0 || candidates == null || candidates.Count == 0)
        {
            return new List<int>();
        }

        // Store all previously seen job IDs for O(1) average-time membership checks.
        HashSet<int> seen = new HashSet<int>(userHistory ?? Array.Empty<int>());

        // Create a ranked copy so candidates are considered from highest score to lowest.
        List<(int jobId, double score)> ranked =
            candidates.OrderByDescending(candidate => candidate.score).ToList();

        // Store accepted job IDs in their final recommendation order.
        List<int> result = new List<int>(Math.Min(k, ranked.Count));

        // Track IDs already returned so duplicate candidate rows cannot duplicate suggestions.
        HashSet<int> added = new HashSet<int>();

        foreach ((int jobId, double score)candidate in ranked)
        {
            // A job already present in user history must never be recommended again.
            if (seen.Contains(candidate.jobId))
            {
                continue;
            }

            // Add returns false if this job ID was accepted earlier, so skip duplicates.
            if (!added.Add(candidate.jobId))
            {
                continue;
            }

            // This candidate is unseen and unique, so append it to the result.
            result.Add(candidate.jobId);

            // Stop immediately after collecting the requested number of recommendations.
            if (result.Count == k)
            {
                break;
            }
        }

        // If fewer than K valid jobs exist, return every valid job that was found.
        return result;
    }
}
Time & Space Complexity

Let N be the number of candidate jobs, M be the number of job IDs in the user history, and K be the requested number of recommendations. Sorting takes O(N log N) time. The later scan takes O(N) time. HashSet lookup and insertion are O(1) on average, so the total time is O(N log N). In the shown executable C# implementation, the sorted List copy requires O(N) memory. Together with the history set, duplicate-prevention set, and result, auxiliary space is O(N + M + K).

Where it is used

This pattern is useful in job boards, product recommendations, news feeds, video suggestions, and other ranked feeds. A system can rank candidate items by relevance, remove items the user has already consumed, remove duplicate IDs, and return only the first K valid suggestions.

Why Interviewers Ask This

This question tests whether you can turn product requirements into clear algorithm rules. The interviewer can evaluate whether you separate ranking from filtering, choose a HashSet for fast membership checks, prevent duplicate suggestions, preserve score order, and stop after K results. It also checks whether you write clean C#, reason about practical edge cases, and include the sorting step when explaining time and memory complexity.

Common interview mistakes

A common mistake is filtering seen jobs but forgetting that candidate data can contain duplicate job IDs. Another mistake is not ranking candidates before selecting the first K valid jobs. It is also wrong to continue processing after K recommendations have already been collected. Candidates may forget to handle K <= 0 or a list with fewer than K valid jobs. Finally, claiming O(N) total time is incorrect for this implementation because sorting costs O(N log N), and ignoring the ranked copy would understate its auxiliary memory.

Interview tip

State the invariant before writing the loop: every item already in the result is unseen, unique, and was encountered in descending score order. That makes the seen check, added check, and early stop at K easy to justify.

Interviewer may ask next
What changes if the candidate list contains the same job ID with different scores?

The current approach still works because candidates are sorted by descending score before filtering. The highest-scoring occurrence of that job ID is encountered first. If it is unseen, it is added to the result and to added. Later occurrences of the same ID are skipped. Correctness is preserved because each output ID is unique and its accepted occurrence has the highest score among its duplicates. Time remains O(N log N), and auxiliary space remains O(N + M + K) for this implementation.

What changes if the candidate list is already sorted by descending relevance score?

We can remove the sorting step and scan the input directly. We still use seen to reject previously viewed jobs and added to reject duplicate IDs, and we still stop after K valid jobs. Correctness is preserved because the input already provides the required highest-to-lowest processing order. Expected time becomes O(N) because HashSet operations are O(1) on average. Auxiliary space becomes O(M + K). The tradeoff is that correctness now depends on the input-order guarantee.

13. Level Of Rain Water In 2D TerrainCodingMediumGoogle

Question Details

Work on the Google-reported 2D rain-water question and clarify the elevation grid, how trapped water is counted, and what makes a cell boundary relevant.

Short Interview Answer (30-60 seconds)

I would start from every outer-border cell because water can escape from the border. I put those cells in a min-heap and always process the lowest effective boundary first. I keep a running boundary level. For each unvisited neighbor, I add max(0, boundary level - terrain height), then push that neighbor with its effective height and mark it visited. The algorithm takes O(m × n × log(m × n)) time and O(m × n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We are given a grid where each number is the height of one terrain cell. Rain can move up, down, left, or right. Water can escape through the outside border, so border cells form the starting boundary. We need to find how much water remains above lower interior cells after raining. A min-heap fits this problem because it always gives us the lowest current boundary first. From that boundary, we can safely decide whether an unvisited neighboring cell can hold water.

Useful Questions to Ask the Interviewer
  1. Can I assume every terrain height is a non-negative integer?
  2. Does water move only in the four directions shown: up, down, left, and right?
  3. Should an empty grid or a grid with fewer than three rows or three columns return 0?
Level Of Rain Water In 2D Terrain diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an m × n elevation grid. Each number is the terrain height of one cell. The output is one integer: the total units of trapped rain water. Outer-border cells cannot trap water because water can escape directly outside the grid.

The diagram uses this 4 × 6 grid: [1, 4, 3, 1, 3, 2] [3, 2, 1, 3, 2, 4] [2, 3, 3, 2, 3, 1] [1, 2, 1, 2, 1, 2]

The final trapped-water result is 4.

2. Choose the algorithm and data structure

I use a min-heap. In .NET, PriorityQueue<TElement, TPriority> removes an element with the lowest priority first. Each heap element stores an effective height, row, and column. The priority is that effective height.

I also use a visited grid. A cell is marked visited when it is added to the heap. This prevents the same cell from being added more than once.

The main invariant is that the min-heap exposes the lowest effective boundary available, while the running boundary level never decreases.

3. Initialize the state

I add every outer-border cell to the min-heap and mark each one visited. I set water to 0 and maxHeight to 0.

The border is the correct starting point because those cells are where water can leave the terrain. Processing inward from the border lets the algorithm discover how high water may safely rise around each interior cell.

4. Walk through the verified example

Each time a cell is removed from the min-heap, I update maxHeight with max(maxHeight, poppedHeight). Then I examine the four neighboring positions.

The diagram highlights three positive-water discoveries in this order.

Step 1: Cell (1,2) has terrain height 1. The boundary level is 3. It adds 3 - 1 = 2 units of water.

Step 2: Cell (1,1) has terrain height

  1. The boundary level is
  2. It adds 3 - 2 = 1 unit of water.

Step 3: Cell (1,4) has terrain height

  1. The boundary level is
  2. It adds 3 - 2 = 1 unit of water.

All remaining processed cells add 0 water. The min-heap eventually becomes empty. The final total is 2 + 1 + 1 = 4.

The final water-level grid shown by the solution is: [1, 4, 3, 1, 3, 2] [3, 3, 3, 3, 3, 4] [2, 3, 3, 2, 3, 1] [1, 2, 1, 2, 1, 2]

5. Explain why the result is correct

The min-heap always exposes the lowest effective boundary available. The running boundary level never decreases. When an unvisited neighbor is lower than that boundary level, the difference is trapped water. If the neighbor is at least as high as the boundary, it traps no new water and becomes part of the boundary at its own height.

6. Explain the C# implementation

The code first returns 0 for a null, empty, or too-small grid. It creates a visited array and a PriorityQueue. It inserts every border cell exactly once. During processing, it removes a lowest-priority boundary cell, updates maxHeight, checks four neighbors, skips invalid or visited positions, marks each new neighbor visited, adds any trapped water, and pushes that neighbor with effective height max(neighborHeight, maxHeight). The heap stops when no cells remain.

7. Explain complexity and edge cases

There are m × n cells. Each cell is inserted into and removed from the min-heap at most once. Each heap operation costs O(log(m × n)), so total time is O(m × n × log(m × n)). The visited grid and heap together require O(m × n) auxiliary space.

The diagram's relevant edge cases are an empty or too-small grid, all cells having the same height, and flat or strictly increasing terrain. These cases trap 0 water.

Key Insight / Why This Solution Works

The key idea is to treat the outer border as the first water boundary and expand inward. Water can escape through an outer cell, so the algorithm starts by placing every border cell in a min-heap. The heap exposes a lowest effective boundary next. After a cell is popped, maxHeight becomes max(maxHeight, poppedHeight). For each unvisited neighbor, the algorithm adds max(0, maxHeight - neighborHeight), then pushes that neighbor with effective height max(neighborHeight, maxHeight). The invariant is that the available boundary is processed from low to high while the running boundary level never decreases. Equal-priority heap items do not need a stable order for the final trapped-water total to remain correct.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Run the exact 4 x 6 example shown in the approved diagram.
        int[][] height = { new[] { 1, 4, 3, 1, 3, 2 }, new[] { 3, 2, 1, 3, 2, 4 },
                           new[] { 2, 3, 3, 2, 3, 1 }, new[] { 1, 2, 1, 2, 1, 2 } };

        // The verified example traps 4 total units of water.
        int result = TrapRainWater(height);
        Console.WriteLine(result);
    }

    public static int TrapRainWater(int[][] height)
    {
        // A missing or empty grid cannot trap water.
        if (height == null || height.Length == 0 || height[0].Length == 0)
        {
            return 0;
        }

        int m = height.Length;
        int n = height[0].Length;

        // Fewer than three rows or columns means there is no enclosed interior cell.
        if (m < 3 || n < 3)
        {
            return 0;
        }

        // Mark cells when they are queued so no cell is inserted more than once.
        bool[,] visited = new bool[m, n];

        // Each heap element stores effective height, row, and column.
        // The integer priority is the same effective height, so the lowest boundary comes out
        // first.
        PriorityQueue<(int h, int r, int c), int> pq = new();

        // Add the left and right boundary cells for every row.
        for (int r = 0; r < m; r++)
        {
            pq.Enqueue((height[r][0], r, 0), height[r][0]);
            visited[r, 0] = true;

            pq.Enqueue((height[r][n - 1], r, n - 1), height[r][n - 1]);
            visited[r, n - 1] = true;
        }

        // Add the top and bottom boundary cells, excluding corners already inserted above.
        for (int c = 1; c < n - 1; c++)
        {
            pq.Enqueue((height[0][c], 0, c), height[0][c]);
            visited[0, c] = true;

            pq.Enqueue((height[m - 1][c], m - 1, c), height[m - 1][c]);
            visited[m - 1, c] = true;
        }

        int water = 0;
        int maxHeight = 0;

        // Check four-directional neighbors: down, up, right, and left.
        int[,] directions = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };

        // Keep expanding inward until no boundary cells remain to process.
        while (pq.Count > 0)
        {
            // Remove one cell with the lowest effective boundary height.
            (int h, int r, int c) = pq.Dequeue();

            // The running boundary level can stay the same or increase, but never decrease.
            maxHeight = Math.Max(maxHeight, h);

            // Examine every allowed neighboring cell.
            for (int d = 0; d < 4; d++)
            {
                int nr = r + directions[d, 0];
                int nc = c + directions[d, 1];

                // Ignore positions outside the grid and cells already placed in the heap.
                if (nr < 0 || nr >= m || nc < 0 || nc >= n || visited[nr, nc])
                {
                    continue;
                }

                // Mark immediately so another boundary cell cannot queue this neighbor again.
                visited[nr, nc] = true;

                int neighborHeight = height[nr][nc];

                // A lower neighbor traps the difference up to the current running boundary level.
                water += Math.Max(0, maxHeight - neighborHeight);

                // A low cell is raised to the current water boundary for future processing.
                int effectiveHeight = Math.Max(neighborHeight, maxHeight);

                // Store and prioritize the neighbor by that effective boundary height.
                pq.Enqueue((effectiveHeight, nr, nc), effectiveHeight);
            }
        }

        // All cells have been processed, so return the accumulated trapped water.
        return water;
    }
}
Time & Space Complexity

Let m be the number of rows and n be the number of columns. The grid has m × n cells. Every cell is added to the min-heap at most once and removed from it at most once. A heap operation costs O(log(m × n)), so total time is O(m × n × log(m × n)). The visited grid uses O(m × n) memory, and the heap can also contain O(m × n) cells. Therefore the auxiliary space is O(m × n).

Where it is used

This min-heap boundary-expansion pattern is useful in terrain and grid-processing software where values must be resolved from the safest or lowest known boundary inward. Examples include flood simulation, elevation analysis, and other grid problems where the smallest current boundary determines what can happen next.

Why Interviewers Ask This

This problem checks whether a candidate sees that local height comparisons are not enough. The interviewer is testing whether you can reason from the outside boundary inward, choose a min-heap for the correct processing order, maintain a clear invariant, prevent duplicate visits, and translate the idea into correct C#. It also tests whether you can justify the trapped-water calculation and state the O(m × n × log(m × n)) time and O(m × n) auxiliary space accurately.

Common interview mistakes

Starting from interior cells instead of the outer boundary is a common mistake. Another mistake is using a normal FIFO queue instead of a min-heap, which loses the lowest-boundary processing rule. Marking a cell visited too late can insert it into the heap more than once. Candidates may also calculate water from only the neighbor height without using the current effective boundary level. Another mistake is pushing a low neighbor with its original terrain height instead of its effective raised height. Finally, O(m × n) time is incorrect because heap insertion and removal add a logarithmic factor.

Interview tip

Explain the boundary invariant before coding: all outer cells enter the min-heap first, the lowest effective boundary is processed next, and any lower unvisited neighbor traps the difference up to the current boundary level.

Interviewer may ask next
Can the auxiliary space be reduced?

The min-heap can still contain O(m × n) cells in the worst case, so this approach remains O(m × n) auxiliary space even if the separate visited structure is made smaller. For example, a compact bit representation can reduce the constant memory used for visited state. The algorithm and correctness invariant stay the same. The time remains O(m × n × log(m × n)). The tradeoff is a more compact but slightly less simple implementation.

How does this solution behave on a very large terrain grid?

The algorithm remains correct. Each of the m × n cells enters and leaves the min-heap at most once, so the time remains O(m × n × log(m × n)). The visited grid and heap require O(m × n) auxiliary space. For a very large terrain, memory usage and heap operations become the main practical costs. The tradeoff is that the min-heap gives the correct boundary-processing order but uses more memory and CPU work than a simple linear scan.

14. How would you find the top K most frequent elements in a massive, continuous stream of data when memory is limited?CodingMediumGoogle

Question Details

Treat the input as an unbounded stream, define the memory constraint, and explain how the solution must return a top-K answer without storing the full history.

Short Interview Answer (30-60 seconds)

I would use the Space-Saving heavy-hitter algorithm with a fixed budget of M counters. I keep a Dictionary for fast lookup of tracked items and a min-ordered SortedSet for the counters. For each stream item, I increment its counter, insert it if space remains, or replace the smallest counter when memory is full. This gives an approximate top-K without storing the full history. Processing n items takes O(n log M) expected time, with O(M) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The stream can continue forever, so storing every item or every exact frequency may eventually use too much memory. We need a method that keeps only a fixed number of counters and still identifies the strongest frequency candidates. The diagram uses the Space-Saving algorithm. It stores at most M counters. Each counter has an item, an estimated count, and an error value. A dictionary finds tracked items quickly, while a min-ordered SortedSet lets us find and replace the smallest counter when memory is full.

Useful Questions to Ask the Interviewer
  1. Is an approximate top-K answer acceptable when the memory budget is strictly limited?
  2. Is M guaranteed to be at least K?
  3. If two candidates have the same estimated count, can they be returned in any order?
How would you find the top K most frequent elements in a massive, continuous stream of data when memory is limited? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an unbounded stream of elements, a target K, and a fixed memory budget M. We cannot keep the complete history. The output is the current top-K heavy-hitter candidates. Because memory is fixed, the answer is approximate rather than guaranteed exact for every possible stream.

2. Choose the algorithm and data structure

I use the Space-Saving algorithm. It keeps at most M counters. Each counter stores an Item, an Estimate, and an Error. A Dictionary maps each tracked item to its Counter object. A SortedSet orders counters by Estimate and then by a unique Id, so the smallest counter is available quickly. The main invariant is that the algorithm never stores more than M counters. For a tracked item, Estimate - Error is a lower bound on its true frequency, and Estimate is an upper bound.

3. Initialize the state

Start with an empty Dictionary and an empty SortedSet. The capacity is M. No counters are allocated yet. When a new item arrives and fewer than M counters are in use, create a counter with Estimate = 1 and Error = 0.

4. Walk through the verified example

Use stream [A, A, B, A, C, B, B, D, B, E, A, B], K = 2, and M = 3.

Step 1, A: state {}. A is not tracked and space remains. Insert A(1,0). State becomes {A(1,0)}. Step 2, A: A is tracked. Increment A. State becomes {A(2,0)}. Step 3, B: B is new and space remains. Insert B(1,0). State becomes {A(2,0), B(1,0)}. Step 4, A: increment A. State becomes {A(3,0), B(1,0)}. Step 5, C: C is new and space remains. Insert C(1,0). State becomes {A(3,0), B(1,0), C(1,0)}. Step 6, B: increment B. State becomes {A(3,0), B(2,0), C(1,0)}. Step 7, B: increment B. State becomes {A(3,0), B(3,0), C(1,0)}. Step 8, D: memory is full. C has the minimum Estimate of 1. Replace C with D. Set D.Estimate = 1 + 1 = 2 and D.Error = 1. State becomes {A(3,0), B(3,0), D(2,1)}. Step 9, B: increment B. State becomes {A(3,0), B(4,0), D(2,1)}. Step 10, E: memory is full. D has the minimum Estimate of 2. Replace D with E. Set E.Estimate = 2 + 1 = 3 and E.Error = 2. State becomes {A(3,0), B(4,0), E(3,2)}. Step 11, A: increment A. State becomes {A(4,0), B(4,0), E(3,2)}. Step 12, B: increment B. State becomes {A(4,0), B(5,0), E(3,2)}.

GetTopK sorts the tracked counters by Estimate descending. The returned top 2 is [B, A]. For offline verification of this small example, the true frequencies are B = 5, A = 4, C = 1, D = 1, and E = 1.

5. Explain why the result is correct

Every arrival follows one of three actions. If the item is tracked, its estimate increases. If it is new and capacity remains, it gets a fresh counter. If memory is full, the smallest counter is replaced. The replacement uses new Estimate = old minimum + 1 and Error = old minimum. This keeps memory bounded and preserves the Space-Saving error bound. Frequent items receive repeated increments, so they tend to remain among the strongest candidates.

6. Explain the C# implementation

The code stores counters in a Dictionary<T, Counter> and a SortedSet<Counter>. Because Estimate controls the SortedSet ordering, an existing counter is removed before its Estimate changes and added back afterward. When memory is full, the minimum counter is removed from both structures and replaced according to the Space-Saving rule. GetTopK orders the tracked counters by Estimate descending and returns the first K item, estimate, and error tuples.

7. Explain complexity and edge cases

For each stream item, Dictionary lookup is O(1) on average. SortedSet removal and insertion are O(log M). Processing n items therefore takes O(n log M) expected time. The persistent streaming state uses O(M) auxiliary space. The shown GetTopK implementation sorts at most M counters, so one query costs O(M log M). Important cases include invalid K, K greater than M, duplicate items, an empty stream, and more distinct values than the memory budget.

Key Insight / Why This Solution Works

The key insight is that exact counting of every distinct value can require memory that grows with the number of distinct items. That does not satisfy a strict fixed-memory requirement. Space-Saving instead keeps only M candidate counters.

If an arriving item is already tracked, its Estimate is incremented. If the item is new and fewer than M counters exist, a new counter starts at Estimate = 1 and Error = 0. If memory is full, the counter with the smallest Estimate is replaced. The replacement gets Estimate = old minimum + 1 and Error = old minimum.

The central invariant is that at most M counters are stored. For each tracked item, Estimate - Error is a lower bound on its true frequency and Estimate is an upper bound. The Dictionary provides average O(1) tracked-item lookup. The SortedSet provides O(log M) maintenance and quick access to the minimum counter.

Code
using System;
using System.Collections.Generic;
using System.Linq;

public sealed class SpaceSaving<T>
    where T : notnull
{
    public sealed class Counter
    {
        public required T Item { get; init; }
        public long Estimate { get; set; }
        public long Error { get; init; }
        public long Id { get; init; }
    }

    private sealed class CounterComparer : IComparer<Counter>
    {
        public int Compare(Counter? x, Counter? y)
        {
            // The same Counter object must compare equal to itself.
            if (ReferenceEquals(x, y))
                return 0;

            // Handle null values only to satisfy the comparer contract safely.
            if (x is null)
                return -1;
            if (y is null)
                return 1;

            // Order by Estimate so SortedSet.Min is the smallest counter.
            int byEstimate = x.Estimate.CompareTo(y.Estimate);
            if (byEstimate != 0)
                return byEstimate;

            // Use a unique Id to break ties without requiring T to be comparable.
            return x.Id.CompareTo(y.Id);
        }
    }

    private readonly int _capacity;
    private readonly Dictionary<T, Counter> _map = new();
    private readonly SortedSet<Counter> _ordered = new(new CounterComparer());
    private long _nextId;

    public SpaceSaving(int capacity)
    {
        // The sketch needs at least one counter.
        if (capacity <= 0)
            throw new ArgumentOutOfRangeException(nameof(capacity));

        _capacity = capacity;
    }

    public void Process(T item)
    {
        // Case 1: the item is already tracked.
        if (_map.TryGetValue(item, out Counter? counter))
        {
            // Remove it before changing Estimate because Estimate controls set order.
            _ordered.Remove(counter);
            counter.Estimate++;
            _ordered.Add(counter);
            return;
        }

        // Case 2: the item is new and an unused counter is available.
        if (_map.Count < _capacity)
        {
            Counter added = new() { Item = item, Estimate = 1, Error = 0, Id = _nextId++ };

            _map[item] = added;
            _ordered.Add(added);
            return;
        }

        // Case 3: memory is full, so replace the smallest tracked counter.
        Counter minimum = _ordered.Min!;
        _ordered.Remove(minimum);
        _map.Remove(minimum.Item);

        // Space-Saving rule:
        // new estimate = old minimum estimate + 1
        // new error = old minimum estimate
        Counter replacement = new() { Item = item, Estimate = minimum.Estimate + 1,
                                      Error = minimum.Estimate, Id = _nextId++ };

        _map[item] = replacement;
        _ordered.Add(replacement);
    }

    public List<(T Item, long Estimate, long Error)> GetTopK(int k)
    {
        // K must be positive.
        if (k <= 0)
            throw new ArgumentOutOfRangeException(nameof(k));

        // We cannot request more candidates than the configured counter capacity.
        if (k > _capacity)
            throw new ArgumentOutOfRangeException(nameof(k), "k must be <= capacity.");

        // Sort only the currently tracked counters by estimated frequency.
        return _map.Values.OrderByDescending(c => c.Estimate)
            .Take(k)
            .Select(c => (c.Item, c.Estimate, c.Error))
            .ToList();
    }

    public long GetLowerBound(T item)
    {
        // For a tracked item, Estimate - Error is its stored lower bound.
        // Returning 0 for an untracked item is a valid trivial lower bound.
        return _map.TryGetValue(item, out Counter? counter)
            ? counter.Estimate - counter.Error
            : 0;
    }
}

public static class Program
{
    public static void Main()
    {
        // Run the exact example shown in the diagram.
        string[] stream = { "A", "A", "B", "A", "C", "B", "B", "D", "B", "E", "A", "B" };

        const int k = 2;
        const int memoryBudget = 3;

        SpaceSaving<string> tracker = new(memoryBudget);

        // Process elements in the same arrival order as the walkthrough.
        foreach (string item in stream)
        {
            tracker.Process(item);
        }

        // Return the strongest two tracked candidates.
        List<(string Item, long Estimate, long Error)> topK = tracker.GetTopK(k);

        Console.WriteLine("Top 2 candidates:");

        foreach ((string item, long estimate, long error) in topK)
        {
            // Show the estimate, error, and lower bound for each result.
            Console.WriteLine(
                $"{item}: estimate={estimate}, error={error}, lowerBound={estimate - error}");
        }
    }
}
Time & Space Complexity

Let n be the number of stream elements processed so far, and let M be the fixed counter capacity.

For each incoming item, the Dictionary lookup is O(1) on average. Updating the SortedSet costs O(log M). Therefore, processing n stream items takes O(n log M) expected time.

The streaming state stores at most M counters in the Dictionary and SortedSet, so auxiliary space is O(M).

The shown GetTopK method orders at most M tracked counters before taking K of them. That query costs O(M log M) time. Its returned list contains at most K entries, so the output itself uses O(K) space.

Where it is used

Space-Saving is useful when software must find frequent items in a very large or continuous stream without keeping the complete history. Examples include popular search terms, hot product IDs, common telemetry keys, frequent log events, and network heavy hitters. It is a good fit when memory must remain bounded and approximate frequency information is acceptable.

Why Interviewers Ask This

The interviewer is testing whether you notice that an unbounded stream makes full exact counting incompatible with a strict fixed-memory budget. They want to see whether you can choose a bounded-memory heavy-hitter method, maintain its invariant, explain the replacement rule and its error bound, select appropriate C# data structures, and analyze their real costs. The problem also tests whether you distinguish approximate estimates from exact frequencies and whether your implementation stays consistent with the memory constraint.

Common interview mistakes

One mistake is treating every stored Estimate as an exact frequency after replacements occur. Another is replacing the minimum counter but forgetting that the new Error must equal the old minimum Estimate. In C#, changing Estimate while the Counter is still inside the SortedSet can break its ordering, so remove the counter first and insert it again after the update. Candidates may also incorrectly claim O(1) update time even though ordered-set maintenance is O(log M). Finally, K should not be larger than the configured counter capacity M.

Interview tip

State the memory contract first: "I can keep only M counters, so the result is approximate." Then explain the three update cases in this order: increment an existing counter, insert a new counter when space remains, or replace the minimum counter when memory is full.

Interviewer may ask next
What changes if I increase the memory budget M?

The algorithm does not change. It can keep more candidate counters, so replacements happen less often and the approximation usually improves. Per-item processing still uses average O(1) Dictionary lookup plus O(log M) SortedSet maintenance, so n processed items take O(n log M) expected time. Auxiliary space becomes O(M). The tradeoff is that using more memory usually gives more accurate heavy-hitter estimates.

Can GetTopK avoid sorting all M tracked counters?

Yes. Because the SortedSet already keeps counters ordered by Estimate and Id, the query can enumerate it in reverse order and take the first K counters. The Space-Saving update algorithm and its invariant do not change. Streaming updates remain O(log M) expected time per item. Returning K candidates from the already ordered set takes O(K) enumeration time, with O(K) output space, while the persistent streaming state remains O(M).

15. How would you parse and evaluate a complex string that contains deeply nested brackets and custom operational rules?CodingMediumGoogle

Question Details

Use the reported nested-string prompt, specify the bracket nesting rules, the custom operations, and the order in which parsing and evaluation must occur.

Short Interview Answer (30-60 seconds)

I would parse the expression from left to right with a stack of evaluation frames. Each frame stores its current value and the operator waiting for the next value. An opening parenthesis pushes a new frame. A closing parenthesis finishes that frame and applies its result to its parent. Inside each frame, operators run strictly left to right with no normal precedence. The example evaluates to 38. The solution takes O(n) time and O(d) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is one arithmetic string containing numbers, parentheses, and the operators +, -, *, and /. Parentheses may be nested deeply. We must evaluate the innermost parentheses first. Inside one parenthesis level, operators are applied strictly in the order they appear from left to right. Normal multiplication and division precedence does not apply. I keep one small evaluation frame for each active nesting level. When a nested expression closes, its completed value becomes the next value in its parent frame. The final output is one integer result.

Useful Questions to Ask the Interviewer
  1. Should division use integer division that truncates toward zero?
  2. Should invalid characters or unmatched parentheses produce an error?
  3. Are operators inside each nesting level always evaluated strictly from left to right?
How would you parse and evaluate a complex string that contains deeply nested brackets and custom operational rules? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a string expression. The diagram uses 12+(3*(4+5)-6/(1+2))*2. The grouping symbols are round parentheses. They may be nested to any valid depth. The operators are +, -, *, and /. Division is integer division that truncates toward zero. For example, 7 / 2 is 3, and -7 / 2 is -3. The required output is the final integer value. For this example, the result is 38.

2. Choose the stack of frames

I use a stack because the most recently opened parenthesis must be completed first. Each stack entry is a frame. A frame stores an accumulated Value and a pending Op. The frame on top of the stack represents the expression level currently being evaluated. The central invariant is that every completed number or child expression is applied exactly once to the nearest open frame using that frame's pending operator.

3. Initialize and process the string

Start with one root frame. Its value is 0, and its pending operator is +. Read the string from left to right. When digits are found, read the complete multi-digit number and immediately apply it to the top frame. When an operator is found, save it as the top frame's pending operator. When ( is found, push a fresh frame with value 0 and operator +. Spaces are skipped.

4. Walk through the exact example

After reading 12 and then +, the root frame is [12, +]. The first ( pushes a new frame. Reading 3 makes that frame's value 3, and * becomes its pending operator. The next ( pushes another frame. Inside it, 4 + 5 becomes 9. Closing that parenthesis pops the value 9 and applies it to the parent, giving 3 * 9 = 27.

The parent frame then reads - 6, so 27 - 6 = 21. It reads / and opens another frame for (1+2). That child frame evaluates 1 + 2 = 3. Closing it applies 3 to the parent, so 21 / 3 = 7. Closing the larger parenthesis applies 7 to the root, giving 12 + 7 = 19. The root then reads * 2, so 19 * 2 = 38. The remaining root frame contains the final result.

5. Explain why the result is correct

At every point, the top frame represents exactly one currently open expression level. A completed value is immediately combined with that frame's accumulated value by using its pending operator. Opening a parenthesis temporarily moves evaluation into a new child frame. Closing it reduces the complete child expression to one value and applies that value to its parent. This preserves both the nesting order and the custom left-to-right operator rule.

6. Explain the C# implementation

The implementation uses Stack<Frame>. Evaluate scans the characters from left to right. Consecutive digits are combined into one number without creating a substring. Apply performs the current frame's pending operation and resets that pending operator to +. An opening parenthesis pushes a frame. A closing parenthesis checks that a child exists, pops its completed value, and applies it to the parent. Invalid characters and unmatched parentheses cause FormatException. Division by zero throws DivideByZeroException.

7. Explain complexity and edge cases

Let n be the number of characters in the expression. Each character is processed at most once, so the time complexity is O(n). Let d be the maximum active parenthesis nesting depth. The stack contains at most one frame for each active level, so auxiliary space is O(d). Relevant cases include deep nesting, multi-digit numbers, integer division that truncates toward zero, negative intermediate results, division by zero, invalid characters, and unmatched parentheses.

Key Insight / Why This Solution Works

The key idea is to treat every open parenthesis level as its own running calculation. A stack fits this rule because nested groups close in last-opened, first-closed order. Each frame stores an accumulated value and the operator waiting for the next completed value. The top frame always represents the expression level currently being read. Values are applied immediately, so operators inside a frame are evaluated strictly left to right. When a child frame closes, its completed value is applied once to its parent. This invariant preserves the required nesting and evaluation order without building a full expression tree.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Run the exact example shown in the diagram.
        string expression = "12+(3*(4+5)-6/(1+2))*2";
        long result = Evaluate(expression);

        // The custom left-to-right rules produce 38.
        Console.WriteLine(result);
    }

    public static long Evaluate(string s)
    {
        // Each frame represents one currently open expression level.
        Stack<Frame> stack = new Stack<Frame>();
        stack.Push(new Frame());

        int i = 0;
        while (i < s.Length)
        {
            char ch = s[i];

            // Whitespace does not change the evaluation state.
            if (char.IsWhiteSpace(ch))
            {
                i++;
                continue;
            }

            // Read one complete multi-digit number before applying it.
            if (char.IsDigit(ch))
            {
                long value = 0;
                while (i < s.Length && char.IsDigit(s[i]))
                {
                    // Build the number and report overflow instead of silently wrapping.
                    value = checked(value * 10 + (s[i] - '0'));
                    i++;
                }

                // Apply the completed operand to the current nesting level.
                Apply(stack.Peek(), value);
                continue;
            }

            // There is no normal precedence. The operator waits for the next value.
            if (ch == '+' || ch == '-' || ch == '*' || ch == '/')
            {
                stack.Peek().Op = ch;
                i++;
                continue;
            }

            // Start a new nested expression with a fresh frame.
            if (ch == '(')
            {
                stack.Push(new Frame());
                i++;
                continue;
            }

            // Finish the innermost expression and apply it to its parent.
            if (ch == ')')
            {
                // The root frame cannot be closed by a parenthesis.
                if (stack.Count == 1)
                {
                    throw new FormatException("Unmatched closing bracket.");
                }

                long completed = stack.Pop().Value;
                Apply(stack.Peek(), completed);
                i++;
                continue;
            }

            // Reject characters that are outside the supported expression grammar.
            throw new FormatException("Invalid character.");
        }

        // Extra frames mean one or more opening parentheses were not closed.
        if (stack.Count != 1)
        {
            throw new FormatException("Unmatched opening bracket.");
        }

        // The remaining root frame contains the final result.
        return stack.Peek().Value;
    }

    private sealed class Frame
    {
        // The running value for this expression level.
        public long Value { get; set; }

        // The operation waiting to be applied to the next completed value.
        public char Op { get; set; } = '+';
    }

    private static void Apply(Frame frame, long value)
    {
        // Apply the pending operator immediately to enforce left-to-right evaluation.
        frame.Value = frame.Op switch { '+' => checked(frame.Value + value),
                                        '-' => checked(frame.Value - value),
                                        '*' => checked(frame.Value * value),
                                        '/' => value != 0 ? frame.Value / value
                                                          : throw new DivideByZeroException(),
                                        _ => throw new InvalidOperationException() };

        // Reset the frame after consuming the pending operation.
        frame.Op = '+';
    }
}
Time & Space Complexity

Let n be the number of characters in the input string. The parser moves from left to right, and each character is handled at most once, so the time complexity is O(n). Let d be the maximum number of nested parentheses that are open at the same time. The stack needs one frame for each active nesting level, so the auxiliary space complexity is O(d). The solution does not build a separate token list or full expression tree.

Where it is used

This stack-frame pattern is useful for nested configuration expressions, formula evaluators, rule engines, and small domain-specific languages where grouping must be processed from the inside out and the operator rules differ from normal arithmetic precedence.

Why Interviewers Ask This

This question tests whether a candidate can convert unusual parsing rules into precise state changes. It checks whether the candidate recognizes a stack as the right structure for nested groups, separates custom left-to-right evaluation from normal arithmetic precedence, handles multi-digit operands and malformed input, and writes C# that follows the stated rules. It also tests whether the candidate can maintain a clear invariant and explain O(n) time and O(d) auxiliary space accurately.

Common interview mistakes

A common mistake is using normal arithmetic precedence, which does not match the custom left-to-right rule. Another mistake is applying a parent operation before its nested child expression has finished. Candidates may also pop the root frame when an unmatched ) appears or forget to check for unmatched ( after the scan. Other relevant mistakes are misunderstanding integer division, forgetting division-by-zero handling, accepting invalid characters, or claiming O(1) auxiliary space even though the stack grows with nesting depth.

Interview tip

State the invariant before coding: the stack top is the expression level currently being evaluated, and its pending operator is applied to the next completed value. Then trace (4+5) becoming 9 and show how that 9 is popped and applied to the parent.

Interviewer may ask next
What changes if the expression can be nested extremely deeply?

The same iterative stack algorithm still works because it does not use recursive calls. That avoids call-stack overflow from deep recursive parsing. If the maximum nesting depth is d, the time complexity remains O(n) and auxiliary space remains O(d). A production parser could also enforce a maximum permitted nesting depth as a validation rule. The tradeoff is that deeper valid inputs require proportionally more stack memory.

How would you support square brackets and curly braces as additional grouping symbols?

Keep the same stack-frame evaluation pattern, but store the opening bracket type in each child frame. Push a frame for (, [, or {. When ), ], or } appears, verify that it matches the opening symbol for the top frame before popping it. The completed child value is then applied to the parent exactly as before. Time remains O(n) and auxiliary space remains O(d). The extra cost is a small amount of bracket-type state per frame.

16. You're given points with integer coordinates on a 2D plane. What's the area of the largest axis-aligned rectangle you can form using four of them as corners?CodingHardGoogle

Question Details

Work with the Google-reported geometry problem, spell out the corner-pair condition, and explain how you would confirm the rectangle is axis-aligned.

Short Interview Answer (30-60 seconds)

I would group the points by y-coordinate and sort the x-values in each row. For every row, I generate every pair of x-values. A dictionary stores the earliest y where each x-pair appeared. If the same pair appears on a later row, those four points form an axis-aligned rectangle, so I calculate width times height and update the maximum. The expected time is O(n log n + sum C(ky, 2)), with O(n + P) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We need to find four given points that make the biggest rectangle with horizontal and vertical sides. Two points must be on one horizontal row, and two matching x-positions must appear on another row. I group points that share the same y-coordinate. Then I look at every pair of x-values in each row. A dictionary remembers the earliest row where each pair appeared. When the same pair appears again, I can calculate the rectangle area. Keeping the earliest row gives that pair its greatest possible height.

Useful Questions to Ask the Interviewer
  1. Should the answer be 0 if no axis-aligned rectangle can be formed?
  2. Can the coordinates be large enough that the area should use a 64-bit integer?
  3. Can duplicate point entries appear in the input?
You're given points with integer coordinates on a 2D plane. What's the area of the largest axis-aligned rectangle you can form using four of them as corners? diagram
How to Explain It in an Interview
1. Understand the rectangle condition

An axis-aligned rectangle has horizontal top and bottom edges and vertical left and right edges. If its columns are x1 and x2 and its rows are y1 and y2, the four corners must be (x1, y1), (x2, y1), (x1, y2), and (x2, y2).

The row-pair method confirms this condition directly. If one row contains the pair (x1, x2), and a different row contains the same pair, all four required corners exist. The equal y-values create horizontal edges, and the equal x-values create vertical edges.

2. Build the row map and pair map

Create rowMap, where each y-coordinate maps to the x-coordinates that occur on that horizontal row. Sort the x-values in each row.

Process the rows in ascending y order. For every row, generate every pair (x1, x2). Create pairRow, where each x-pair maps to the earliest processed y-coordinate containing both x-values.

The central invariant is that pairRow[(x1, x2)] keeps the earliest processed y for that pair. If the pair appears again later, do not overwrite the stored y. The earliest y gives the greatest possible height for a rectangle ending on any later row.

3. Initialize and process the exact example

The points are (1,1), (4,1), (1,3), (4,3), (2,1), (2,3), (5,2), and (0,0).

The rows are: y = 0 -> [0] y = 1 -> [1, 2, 4] y = 2 -> [5] y = 3 -> [1, 2, 4]

Start with max = 0 and an empty pairRow.

At y = 0, the row contains only x = 0. No x-pair exists, so pairRow stays unchanged.

At y = 1, the pairs are (1,2), (1,4), and (2,4). They have not appeared before, so store (1,2) -> 1, (1,4) -> 1, and (2,4) -> 1. max is still 0.

At y = 2, the row contains only x = 5. No pair exists, so pairRow stays unchanged.

At y = 3, pair (1,2) was first seen at y = 1. Its width is 2 - 1 = 1. Its height is 3 - 1 = 2. Its area is 1 * 2 = 2, so max becomes 2.

Next, pair (1,4) was first seen at y = 1. Its width is 4 - 1 = 3. Its height is 3 - 1 = 2. Its area is 3 * 2 = 6, so max becomes 6.

Finally, pair (2,4) was first seen at y = 1. Its width is 4 - 2 = 2. Its height is 2. Its area is 4, so max stays 6.

The final answer is 6. The largest rectangle uses (1,1), (4,1), (1,3), and (4,3).

4. Explain why the result is correct

Every axis-aligned rectangle must have the same two x-coordinates on two different horizontal rows. The algorithm generates every x-pair in every row, so it considers the column pair of every possible axis-aligned rectangle.

Because rows are processed from smaller y to larger y, a stored previous y is never above the current row. pairRow keeps the earliest y for each x-pair, so a later occurrence uses the largest possible height available for that pair. Taking the maximum of all calculated areas therefore finds the largest rectangle.

5. Explain the C# implementation

The code first builds rowMap from the input points and sorts each row's x-values. It creates pairRow and sets max to 0. It processes rowMap in ascending y order. Two nested loops generate every x-pair in the current row.

If the pair has not appeared before, the code stores currentY in pairRow. If it has appeared, the code calculates width = x2 - x1, height = currentY - previousY, and area = width * height. It updates max when the area is larger. It does not overwrite previousY.

6. Explain complexity and edge cases

Let ky be the number of x-values on row y. Sorting all row values costs at most O(n log n). The algorithm generates C(ky, 2) pairs on each row. Dictionary lookup and insertion are O(1) on average, so the expected total time is O(n log n + sum C(ky, 2)).

rowMap needs O(n) space. If pairRow stores P distinct x-pairs, the total auxiliary space is O(n + P).

If fewer than four useful points exist, no pair appears on two different rows and the result stays 0. Points on only one row or only one column also produce 0. Negative coordinates work normally. Duplicate point entries do not change the maximum rectangle, although they can cause redundant pair checks. The area uses long so large coordinate differences are handled more safely.

Key Insight / Why This Solution Works

The key insight is that an axis-aligned rectangle can be identified by an x-pair that occurs on two different y-rows. rowMap groups x-values by y. The algorithm processes rows in ascending y order and generates every pair of x-values in each row. pairRow maps an x-pair to the earliest processed y where that pair occurred. The invariant is that pairRow[(x1, x2)] always stores that earliest y. When the same pair appears later, the four matching points form an axis-aligned rectangle. Using the earliest y gives the greatest possible height for that x-pair ending at the current row.

Code
using System;
using System.Collections.Generic;
using System.Linq;

public static class Program
{
    public static long LargestRectangleArea(int[][] points)
    {
        // Group every x-coordinate under its y-coordinate.
        // Each dictionary entry represents one horizontal row.
        Dictionary<int, List<int>> rowMap = new Dictionary<int, List<int>>();

        foreach (int[] point in points)
        {
            int x = point[0];
            int y = point[1];

            if (!rowMap.TryGetValue(y, out List<int> xs))
            {
                xs = new List<int>();
                rowMap[y] = xs;
            }

            // Add this point's x-coordinate to its horizontal row.
            xs.Add(x);
        }

        // Sort each row so every pair is generated as smaller x then larger x.
        foreach (List<int> xs in rowMap.Values)
        {
            xs.Sort();
        }

        // Map each x-pair to the earliest processed y where that pair appeared.
        // Keeping the earliest y gives the greatest possible future height.
        Dictionary<(int X1, int X2), int> pairRow = new Dictionary<(int X1, int X2), int>();

        long max = 0;

        // Process rows in ascending y order, matching the diagram walkthrough.
        foreach (KeyValuePair<int, List<int>> row in rowMap.OrderBy(entry => entry.Key))
        {
            int currentY = row.Key;
            List<int> xs = row.Value;

            // Generate every pair of x-values in this row.
            for (int i = 0; i < xs.Count; i++)
            {
                for (int j = i + 1; j < xs.Count; j++)
                {
                    int x1 = xs[i];
                    int x2 = xs[j];
                    (int X1, int X2) key = (x1, x2);

                    if (pairRow.TryGetValue(key, out int previousY))
                    {
                        // The same x-pair appeared on an earlier row.
                        // Therefore the four matching points form an axis-aligned rectangle.
                        long width = (long)x2 - x1;
                        long height = (long)currentY - previousY;
                        long area = width * height;

                        // Keep the largest rectangle area found so far.
                        if (area > max)
                        {
                            max = area;
                        }

                        // Do not overwrite previousY.
                        // The earliest row gives the largest height for later occurrences.
                    }
                    else
                    {
                        // First occurrence of this x-pair, so remember this row.
                        pairRow[key] = currentY;
                    }
                }
            }
        }

        // If no valid rectangle exists, max remains 0.
        return max;
    }

    public static void Main()
    {
        // Exact example from the approved diagram.
        int[][] points = { new[] { 1, 1 }, new[] { 4, 1 }, new[] { 1, 3 }, new[] { 4, 3 },
                           new[] { 2, 1 }, new[] { 2, 3 }, new[] { 5, 2 }, new[] { 0, 0 } };

        // The largest rectangle has width 3 and height 2, so its area is 6.
        Console.WriteLine(LargestRectangleArea(points));
    }
}
Time & Space Complexity

Let n be the number of points, and let ky be the number of x-values on row y. Sorting the x-values across all rows costs at most O(n log n). A row with ky values creates C(ky, 2) pairs. Dictionary lookup and insertion are O(1) on average, so the expected total time is O(n log n + sum C(ky, 2)). rowMap uses O(n) extra space. If P distinct x-pairs are stored in pairRow, pairRow uses O(P) space. Total auxiliary space is O(n + P).

Where it is used

This pattern is useful when a geometry problem can be changed into repeated coordinate combinations. Similar grouping and pair-key techniques can help in spatial analytics, grid processing, map data, and other problems where the same pair of columns must appear across different rows.

Why Interviewers Ask This

This problem tests whether you can turn geometry into a data-structure problem. The interviewer can evaluate whether you recognize the repeated x-pair pattern, choose a useful dictionary key, preserve the earliest-row invariant, and prove that the four corners are axis-aligned. It also tests careful nested-loop reasoning, correct C# collection use, safe area arithmetic, edge-case thinking, and whether you can describe complexity without hiding the cost of generating all pairs in a dense row.

Common interview mistakes

A common mistake is forgetting the exact corner condition. The same two x-coordinates must occur on two different y-rows. Another mistake is overwriting pairRow when an x-pair appears again. That can lose the earliest row and reduce the height of a future rectangle. Candidates may also process rows in an inconsistent order, mix up x and y, forget the O(ky^2) pair generation on a dense row, or incorrectly claim that all dictionary operations give guaranteed O(1) worst-case time.

Interview tip

Start by saying that an axis-aligned rectangle exists when the same x-pair appears on two different y-rows. Then define pairRow as x-pair to earliest y. That single invariant makes the implementation, area calculation, and correctness proof easy to explain.

Interviewer may ask next
Can auxiliary space be reduced?

The main memory cost is pairRow, which stores P distinct x-pairs. Removing that dictionary means the algorithm would need to compare rows again to discover repeated pairs, increasing running time. The shown solution uses O(n + P) auxiliary space and expected O(n log n + sum C(ky, 2)) time. A lower-memory approach would normally trade memory for more repeated comparisons, so the main tradeoff is space versus time.

What happens if one row contains a very large number of points?

A row with k x-values generates C(k, 2), which is O(k^2), pairs. The algorithm remains correct because it still checks every possible column pair, but that row can dominate both running time and the number of entries stored in pairRow. Dictionary operations are O(1) on average, so the expected total time remains O(n log n + sum C(ky, 2)), with O(n + P) auxiliary space. The tradeoff is that very dense rows can make this pair-based method expensive.

17. Nearest Common AncestorCodingHardGoogle

Question Details

Solve the reported Google tree question, state how you detect the split point between the two targets, and explain the special case where one target is an ancestor of the other.

Short Interview Answer (30-60 seconds)

I would use recursive DFS. At each node, I first return if the node is null, p, or q. Then I search the left and right subtrees. If both sides return non-null nodes, the current node is the split point, so it is the nearest common ancestor. Otherwise, I propagate the one non-null result upward. This takes O(n) time and O(h) auxiliary space for the recursion stack, where h is the tree height.

Detailed Explanation

See the Code while reading this explanation.

We are given a binary tree and two target nodes, p and q. We need to return the nearest node that is an ancestor of both targets. A target node can also be an ancestor of itself. The main idea is to search both sides of each node. If the two targets are found through different child subtrees, that node is their split point. If only one side returns a useful result, we pass that result upward. Recursive DFS fits this tree structure naturally.

Useful Questions to Ask the Interviewer
  1. Are p and q guaranteed to exist in the tree?
  2. Are p and q actual node references rather than only integer values?
  3. Should a node count as an ancestor of itself?
Nearest Common Ancestor diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the root of a binary tree and two target node references, p and q. The output is the TreeNode that is their nearest common ancestor. In the diagram, the tree is rooted at 6. The targets are p = node 3 and q = node 5. The expected result is node 4.

2. Use recursive DFS and define each return value

Each recursive call searches one subtree. A null return means that subtree did not produce a target or common-ancestor result. Returning p or q means the current search reached one of the targets. Returning another non-null node means a useful result has already been found lower in that subtree. The current node is the split point when both its left and right recursive results are non-null.

3. Handle the base cases first

If the current node is null, return null. If the current node is p or q, return that node immediately. This early return also handles the case where one target is an ancestor of the other when both targets are guaranteed to exist. If p is an ancestor of q, reaching p is enough to return p as their nearest common ancestor.

4. Walk through the diagram example

The example tree is rooted at 6. Node 6 has children 2 and 8. Node 2 has children 0 and 4. Node 4 has children 3 and 5. The targets are p = 3 and q = 5.

The executed non-null-node return order shown in the diagram is 0, 3, 5, 4, 2, 8, 6.

At node 0, neither target is found below it, so it returns null. At node 3, the current node equals p, so it returns node 3 immediately. Its child calls are not executed. At node 5, the current node equals q, so it returns node 5 immediately. Its child calls are not executed. At node 4, the left result is 3 and the right result is 5. Both are non-null. Node 4 is therefore the split point, so it returns node 4. At node 2, the left result is null and the right result is 4. The algorithm propagates node 4 upward. At node 8, neither target is found, so it returns null. At root node 6, the left result is 4 and the right result is null. The algorithm propagates node 4. The final result is node 4.

5. Explain why the result is correct

Each call returns p or q when the current node is a target. It returns the current node when both child subtrees return non-null results. Otherwise, it propagates the single non-null result. Therefore, the lowest node where the two target paths split is returned. In the example, node 4 is the node whose left recursive result is 3 and whose right recursive result is 5.

6. Explain the C# implementation

The method first checks the null and target base cases. It then recursively searches the left subtree and the right subtree. If both results are non-null, it returns the current root because that node is the split point. Otherwise, it returns whichever side is non-null. The executable example builds the exact tree from the diagram and prints 4.

7. Explain complexity and edge cases

The time complexity is O(n), where n is the number of nodes, because each node is visited at most once. The auxiliary space is O(h), where h is the tree height, because recursive calls use the call stack. If p equals q, that node is returned. If one target is an ancestor of the other, the ancestor is returned. If target presence is not guaranteed, this routine alone can propagate one found target even when the other target is missing, so presence must be validated separately.

Key Insight / Why This Solution Works

The key idea is to let every recursive call report the useful result from its subtree. Null means no target or common-ancestor result was found there. Returning p or q means the search reached that target. Returning another non-null node means a common-ancestor result has already been found lower in that subtree. The central invariant is that each call returns the useful target or ancestor result for its subtree. When both left and right return non-null values at the same node, the two target paths come from different child subtrees. That current node is the split point and is returned. If only one side is non-null, that result is propagated upward.

Code
using System;

public sealed class TreeNode
{
    public int val;
    public TreeNode? left;
    public TreeNode? right;

    // Create one tree node and optionally connect its left and right children.
    public TreeNode(int val = 0, TreeNode? left = null, TreeNode? right = null)
    {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

public static class Program
{
    public static void Main()
    {
        // Build the exact tree shown in the diagram.
        TreeNode node0 = new TreeNode(0);
        TreeNode node3 = new TreeNode(3);
        TreeNode node5 = new TreeNode(5);
        TreeNode node4 = new TreeNode(4, node3, node5);
        TreeNode node2 = new TreeNode(2, node0, node4);
        TreeNode node8 = new TreeNode(8);
        TreeNode root = new TreeNode(6, node2, node8);

        // p and q are the actual target node references from the diagram.
        TreeNode p = node3;
        TreeNode q = node5;

        // Run the same recursive DFS used in the diagram.
        TreeNode? result = LowestCommonAncestor(root, p, q);

        // The verified result for this example is node 4.
        Console.WriteLine(result?.val); // 4
    }

    public static TreeNode? LowestCommonAncestor(TreeNode? root, TreeNode p, TreeNode q)
    {
        // A null subtree contains no target or common-ancestor result.
        if (root is null)
        {
            return null;
        }

        // Stop immediately when the current node is one of the targets.
        // Compare node identity because p and q are node references.
        if (ReferenceEquals(root, p) || ReferenceEquals(root, q))
        {
            return root;
        }

        // Search the left subtree and keep the result it reports.
        TreeNode? left = LowestCommonAncestor(root.left, p, q);

        // Search the right subtree and keep the result it reports.
        TreeNode? right = LowestCommonAncestor(root.right, p, q);

        // Two non-null child results mean the target paths split here.
        // Therefore, the current node is the nearest common ancestor.
        if (left is not null && right is not null)
        {
            return root;
        }

        // If only one side is non-null, propagate that useful result upward.
        // If both sides are null, this expression correctly returns null.
        return left is not null ? left : right;
    }
}
Time & Space Complexity

Time complexity is O(n), where n is the number of nodes in the tree. Each node is visited at most once, and a target node returns immediately when reached. Auxiliary space is O(h), where h is the height of the tree. This extra memory comes from the recursion stack. In the worst case of a highly skewed tree, h can be n.

Where it is used

This recursive tree pattern is useful when data forms a hierarchy and we need the nearest shared parent. Examples include finding a common manager in an organization tree, a common parent folder in a directory tree, or a shared ancestor in a syntax or document tree.

Why Interviewers Ask This

This question tests whether you can reason about recursive tree return values instead of only traversal order. The interviewer wants to see whether you can define a useful recursive contract, recognize the split-point condition, handle the case where one target is an ancestor of the other, preserve node identity, and explain why the lowest qualifying split is the nearest common ancestor. It also checks whether you can write correct C# and include recursion-stack space in the complexity.

Common interview mistakes

A common mistake is comparing only node values when the inputs are target node references. Another mistake is forgetting the base case that immediately returns p or q, which breaks the ancestor special case. Candidates may also return the current node when only one recursive side is non-null. The current node is a split point only when both sides are non-null. Another mistake is claiming O(1) auxiliary space and forgetting the O(h) recursion stack. Finally, if target presence is not guaranteed, do not assume this routine proves that both targets were found.

Interview tip

Define the meaning of a recursive return value before writing code: null means no useful result from that subtree, one non-null side means propagate that result, and two non-null sides mean the current node is the split point. This makes the code and the ancestor special case much easier to explain.

Interviewer may ask next
What changes if p and q are not guaranteed to exist in the tree?

The current routine is not enough by itself because it can propagate one target even when the other target is missing. I would add presence validation before accepting the returned ancestor. One simple option is to verify that both node references exist in the tree and then use the same NCA recursion. The asymptotic time remains O(n), although validation can add another traversal. Auxiliary space remains O(h) because of recursion. The tradeoff is extra traversal work in exchange for a correct missing-target contract.

What if two different nodes have the same integer value?

The recursive algorithm can stay the same because the targets are node references, not just integer values. The C# code should compare node identity when checking whether the current node is p or q. Two different nodes with equal val fields are still different nodes. The time complexity remains O(n), and the auxiliary recursion space remains O(h). The important rule is not to replace the node-reference check with a value-only comparison.

18. Four Person ElevatorCodingHardGoogle

Question Details

Use the Google-reported elevator scheduling problem, define the capacity and ordering constraints, and explain what output the algorithm must produce for the constrained ride grouping.

Short Interview Answer (30-60 seconds)

I would process the people in arrival order and build one current elevator ride at a time. I add each destination floor to the current ride. When that ride reaches four people, I add it to the result and start a new ride. After the loop, I add the final partial ride if it is not empty. This preserves arrival order and never exceeds capacity. The time is O(n), and auxiliary space is O(1) beyond the returned rides.

Detailed Explanation

See the Code while reading this explanation.

The input is a list of destination floors in the order people are waiting. The elevator can carry at most four people in one ride. We must keep the same arrival order when creating the ride groups. The goal is to return a list of rides. Each ride contains at most four destination floors. The solution takes the next four waiting people for each full ride. If fewer than four people remain at the end, they form the final ride. This directly satisfies the capacity and ordering rules and gives the required constrained grouping.

Useful Questions to Ask the Interviewer
  1. Should the returned rides preserve the exact arrival order of the input? In this problem, yes.
  2. Can the final ride contain fewer than four people? In this problem, yes.
  3. Should an empty input defensively return an empty ride list? The diagram shows that behavior.
Four Person Elevator diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an integer array of destination floors. Each element represents one waiting person, and its position in the array represents that person's arrival order. The output is a list of rides. Each ride is another list containing at most four destination floors. For the example input [2, 9, 7, 4, 3, 1, 8, 6, 5], the output is [[2, 9, 7, 4], [3, 1, 8, 6], [5]].

2. Choose the grouping approach

We do not need sorting, searching, or a special lookup structure. We only need one result list and one small current-ride list. The central rule is simple: the current ride contains the next unassigned people in arrival order, and it never contains more than four people. When it reaches four, we dispatch it and start a new current ride.

3. Initialize the state

Create an empty list called rides. This stores the completed ride groups. Create another empty list called current with capacity four. This stores the ride currently being built. At the start, no person has been assigned to a ride.

4. Walk through the example

Start with input [2, 9, 7, 4, 3, 1, 8, 6, 5]. Add 2, so current becomes [2]. Add 9, so it becomes [2, 9]. Add 7, so it becomes [2, 9, 7]. Add 4, so current becomes [2, 9, 7, 4]. Its count is now four, so dispatch it. The rides list becomes [[2, 9, 7, 4]], and current becomes empty.

Next add 3, giving [3]. Add 1, giving [3, 1]. Add 8, giving [3, 1, 8]. Add 6, giving [3, 1, 8, 6]. The ride is full, so dispatch it. The rides list becomes [[2, 9, 7, 4], [3, 1, 8, 6]]. Start another empty current ride.

Finally add 5, so current is [5]. The input is finished. Because current is not empty, dispatch that final partial ride. The final result is [[2, 9, 7, 4], [3, 1, 8, 6], [5]].

5. Explain why the result is correct

At every point, current contains the next people who have not yet been assigned to a ride. We only append people, so their arrival order never changes. We dispatch immediately when current reaches four people, so no ride exceeds the capacity. Every input person is added exactly once to one ride. Therefore the final output contains every person in order and satisfies the capacity rule.

6. Explain the C# implementation

The method creates rides and current. It loops through floors from left to right. Each floor is appended to current. When current.Count becomes 4, that list is added to rides and a new empty current list is created. After the loop, a final non-empty current list is added. Then the method returns rides. The example in Main uses exactly [2, 9, 7, 4, 3, 1, 8, 6, 5] and prints the three rides shown in the diagram.

7. Explain complexity and edge cases

If n is the number of people, the loop processes each person once, so the time complexity is O(n). The in-progress ride holds at most four integers, so auxiliary space is O(1) beyond the returned output. The returned rides contain all n destination values, so the output itself uses O(n) space. A final partial group is valid. If the number of people is a multiple of four, every ride is full. The diagram also treats an empty input defensively by returning an empty ride list.

Key Insight / Why This Solution Works

The key insight is that the illustrated constraints are arrival order and a maximum capacity of four people per ride. Because order cannot change, the correct grouping is to take the next four waiting people whenever possible. The invariant is: before each new input value is processed, every earlier person is already in exactly one completed ride or in the current ride, and the current ride has fewer than four people. Adding the next person preserves order. Dispatching when the count reaches four preserves capacity. Under these shown constraints, filling each ride to four whenever possible minimizes the number of rides.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static List<List<int>> ScheduleElevator(int[] floors)
    {
        // Store completed elevator rides that will be returned.
        List<List<int>> rides = new List<List<int>>();

        // Build one ride at a time. It can contain at most four people.
        List<int> current = new List<int>(4);

        // Process people from left to right to preserve arrival order.
        foreach (int floor in floors)
        {
            // Add this person's destination to the ride currently being built.
            current.Add(floor);

            // Dispatch the ride immediately when it reaches capacity.
            if (current.Count == 4)
            {
                // Store this full ride in the output.
                rides.Add(current);

                // Start a new empty ride for the next waiting person.
                current = new List<int>(4);
            }
        }

        // Dispatch a final partial ride when one to three people remain.
        // For an empty input, this condition is false and the result stays empty.
        if (current.Count > 0)
        {
            rides.Add(current);
        }

        // Return all rides in the same order as the original queue.
        return rides;
    }

    public static void Main()
    {
        // Use the exact example shown in the approved diagram.
        int[] input = { 2, 9, 7, 4, 3, 1, 8, 6, 5 };

        // Group the waiting people into rides of at most four.
        List<List<int>> result = ScheduleElevator(input);

        // Print each ride in dispatch order.
        Console.WriteLine("Rides:");
        foreach (List<int> ride in result)
        {
            Console.WriteLine("[" + string.Join(", ", ride) + "]");
        }

        // Output:
        // [2, 9, 7, 4]
        // [3, 1, 8, 6]
        // [5]
    }
}
Time & Space Complexity

Let n be the number of people. We read each destination floor once, so the time complexity is O(n). The current ride stores at most four integers, so it uses constant extra memory. Therefore the auxiliary space is O(1) beyond the returned rides. The returned result stores all n destination values across its ride lists, so the output space is O(n).

Where it is used

This pattern is useful when items must stay in their original order and must be divided into fixed-capacity batches. Examples include batching queued jobs for workers, splitting ordered records into pages, or placing waiting customers into vehicles with a fixed capacity.

Why Interviewers Ask This

This problem checks whether you can turn simple capacity and ordering constraints into a precise algorithm. The interviewer wants to see whether you preserve arrival order, enforce the capacity exactly, handle the final partial group, and avoid unnecessary sorting or data structures. It also tests whether you can explain why filling each ride to four people whenever possible minimizes the ride count under the shown constraints, write correct C#, and describe time and auxiliary space accurately.

Common interview mistakes

A common mistake is sorting the destination floors, which breaks the required arrival order. Another mistake is allowing more than four people into a ride before dispatching it. Candidates may also forget to add the final partial ride after the loop. Another error is adding the current list to the result and then mutating that same list instead of creating a new current list. Finally, do not claim O(1) total space without separating auxiliary space from the O(n) returned output.

Interview tip

State the invariant before coding: the current ride always contains the next unassigned people in arrival order and has at most four people. Then make the code follow that rule directly by dispatching exactly when the count reaches four.

Interviewer may ask next
How would the solution change if people arrived as a continuous stream instead of one complete array?

The same grouping rule still works. Keep one current ride in memory as people arrive. Add each new destination to it. When it reaches four people, emit that ride and replace it with an empty ride. When the stream ends, emit the remaining partial ride if it is not empty. Correctness is preserved because people are still processed in arrival order and no ride exceeds four people. Processing n people still takes O(n) time. Auxiliary space remains O(1) because the current ride holds at most four people, excluding any output that is emitted or stored.

What changes if the elevator capacity is provided as a variable instead of always being four?

Replace the fixed value 4 with a positive capacity c. Create the current list for up to c people and dispatch whenever current.Count == c. The invariant stays the same: current contains the next unassigned people in arrival order and never contains more than c people. The algorithm still takes O(n) time. The current ride uses O(c) auxiliary space, while stored returned output uses O(n) space. The main tradeoff is that the extra working memory now depends on the supplied capacity.

19. Write code in a google doc to solve an editor large memory problem.CodingHardGoogle

Question Details

Use the shared-doc interview context, define the memory bottleneck in the editor, and explain what algorithmic or storage tradeoffs the candidate is expected to reason about.

Short Interview Answer (30-60 seconds)

I would keep each client memory-bounded instead of loading the full shared document. The client keeps only the visible 10,000-character viewport plus 5,000-character buffers on both sides. Edits go to an OT server with absolute revisions, an append-only operation log, and periodic durable snapshots. Reconnecting clients load a snapshot and replay later operations. Applying k operations is O(k) at the collaboration layer, while client memory stays bounded by the active window plus pending operation state.

Detailed Explanation

See the Code while reading this explanation.

The problem is that a shared document can become extremely large. If every browser keeps the whole document and all edit history in memory, memory use grows too much. The diagram solves this by keeping only the text near the user's screen in each client. Changes are sent to one collaboration server. The server orders and transforms concurrent edits. Durable snapshots and operation-log segments let clients rebuild state without keeping the full history forever.

Useful Questions to Ask the Interviewer
  1. Can each client keep only the visible document window plus a small buffer?
  2. Should concurrent edits use server-ordered Operational Transformation?
  3. Can persistent storage use durable snapshots plus append-only operation-log segments?
  4. May old log segments be compacted only after a durable snapshot covers them?
Write code in a google doc to solve an editor large memory problem. diagram
How to Explain It in an Interview
1. Define the memory bottleneck

The example document has 100,000,000 characters. The diagram treats that as about 100 MB of compact text storage. There are 50 active clients. Loading the complete document and all history into every client would waste memory.

2. Keep only the viewport window in client memory

The visible viewport is 10,000 characters. The client also keeps a 5,000-character buffer on each side. So it materializes 20,000 UTF-16 characters around the current editing area. That is about 40 KB of text. When the user scrolls, distant content can be evicted and a new nearby range can be loaded.

3. Use ordered OT operations

The user inserts "Hello" at global index 50,000. The client sends Insert(index=50,000, "Hello") with its BaseVersion. The server stores an absolute current revision and an absolute retained-log start revision. It transforms the incoming operation only against retained operations that happened after the client's base revision. Then it appends the transformed operation, advances the absolute server revision, and broadcasts the accepted operation.

4. Rebuild from snapshot plus later operations

The durable snapshot is snap_120 at revision 1,200,000. There are 25,000 operations after it. A client opens the document, loads snap_120, replays those 25,000 operations in server order, and then keeps only the viewport window and buffers in memory. The user edit is processed after that rebuilt revision.

5. Snapshot and compact safely

The logical operation log is append-only. Periodically the system creates a durable snapshot. Only after the new snapshot is successfully persisted may operations covered by that snapshot be compacted. The retained-log boundary then advances to the snapshot revision. BaseVersion values remain absolute revisions even though the retained list is shorter.

6. Explain why the design is correct

The OT server gives accepted edits one ordered revision history. A client with a retained base revision can transform or replay the missing operations in that order. A snapshot plus the operations after it can reconstruct a later revision. Viewport eviction removes only materialized client text. It does not delete logical document data.

7. Explain complexity and edge cases

At the collaboration layer, replaying k operations requires O(k) operation applications. In the simplified insert-versus-insert OT rule, one transform against one retained operation is O(1), while one incoming operation may need to transform against c newer retained operations, so that receive step is O(c). Snapshot creation is O(document size) and is done infrequently. Client working memory is bounded by its materialized window plus pending operation state. Relevant edge cases are large pastes, reconnects older than the retained-log boundary, memory pressure, and binary objects stored out of line.

Key Insight / Why This Solution Works

The key idea is to separate the complete logical document from the small working set that a browser needs. Each client materializes only the viewport and nearby buffers. Edits are represented as operations and sent to a server-ordered OT pipeline. The central invariant is that _logStartRevision is the absolute revision immediately before the first retained operation. Therefore an absolute BaseVersion is converted to a retained-list index by subtracting _logStartRevision. Durable snapshots are safe restart points. After snapshot revision R is persisted, operations covered by R may be compacted and the retained-log boundary can move to R.

Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

public static class Program
{
    public static void Main()
    {
        const long originalDocumentLength = 100_000_000;
        const int viewportStart = 45_000;
        const int viewportLength = 10_000;
        const int bufferSize = 5_000;
        const int activeClients = 50;
        const long snapshotRevision = 1_200_000;
        const int operationsAfterSnapshot = 25_000;

        // snap_120 represents the durable document state at absolute revision 1,200,000.
        Snapshot initialSnapshot = new("snap_120", snapshotRevision, originalDocumentLength);

        // Build the exact 25,000 post-snapshot operations from the diagram.
        // They are placed far from the active viewport so the visible example stays easy to verify.
        List<IOperation> replayOperations = new(operationsAfterSnapshot);
        for (int i = 0; i < operationsAfterSnapshot; i++)
        {
            replayOperations.Add(new InsertOperation { BaseVersion = snapshotRevision + i,
                                                       Index = 80_000_000 + i, Text = "x" });
        }

        // The document model stores pieces, not the entire 100,000,000-character text.
        PieceTableDocument clientDocument = new(originalDocumentLength);

        // Rebuild the client from snap_120 plus the 25,000 later operations.
        ClientSession client = new(clientDocument, bufferSize, viewportStart, viewportLength);
        client.Initialize(initialSnapshot, replayOperations);

        // The server keeps the same retained operations and absolute revision numbers.
        SegmentedOperationStore operationStore = new();
        CollaborationServer server = new(snapshotRevision, replayOperations, operationStore);

        // This is the exact user edit from the diagram.
        InsertOperation localEdit =
            new() { BaseVersion = server.CurrentRevision, Index = 50_000, Text = "Hello" };

        // Transform, append, advance the absolute revision, and broadcast the accepted operation.
        IOperation accepted = server.Receive(localEdit);

        // Apply the accepted operation to this client and refresh only its bounded window.
        accepted.Apply(clientDocument);
        client.CurrentRevision = server.CurrentRevision;
        client.UpdateWindow();

        // Create snap_121 after 25,001 operations beyond snap_120.
        // Compaction is allowed only after the snapshot has been durably persisted.
        InMemorySnapshotStore snapshotStore = new(nextSnapshotNumber: 121);
        SnapshotManager snapshotManager =
            new(intervalOps: 25_001, lastSnapshotRevision: snapshotRevision, store: snapshotStore);
        snapshotManager.MaybeCreateSnapshot(server.CurrentRevision, clientDocument, server);

        Console.WriteLine($"Document length: {originalDocumentLength:N0} chars");
        Console.WriteLine($"Active clients: {activeClients}");
        Console.WriteLine(
            $"Starting snapshot: {initialSnapshot.Id} at revision {initialSnapshot.Revision:N0}");
        Console.WriteLine($"Operations after snapshot: {operationsAfterSnapshot:N0}");
        Console.WriteLine($"Viewport: {viewportLength:N0} chars");
        Console.WriteLine($"Buffer on each side: {bufferSize:N0} chars");
        Console.WriteLine($"Materialized window: {client.VisibleText.Length:N0} chars");
        Console.WriteLine("Accepted edit: Insert(index=50,000, \"Hello\")");
        Console.WriteLine($"Current server revision: {server.CurrentRevision:N0}");
        Console.WriteLine($"Retained log starts after revision: {server.LogStartRevision:N0}");
        Console.WriteLine(
            $"Visible text contains Hello: {client.VisibleText.Contains("Hello", StringComparison.Ordinal)}");
    }
}

public interface IOperation
{
    long BaseVersion { get; set; }
    void Apply(PieceTableDocument document);
    IOperation Transform(IOperation other);
}

public sealed class InsertOperation : IOperation
{
    public long BaseVersion { get; set; }
    public long Index { get; set; }
    public string Text { get; set; } = string.Empty;

    public void Apply(PieceTableDocument document)
    {
        // Insert by splitting pieces. The original large text is never copied into one giant
        // string.
        document.Insert(Index, Text);
    }

    public IOperation Transform(IOperation other)
    {
        // Server order is the tie-breaker for concurrent insert-versus-insert operations.
        // A later accepted insert moves right when an already accepted insert is before or at its
        // index.
        if (other is InsertOperation earlierInsert && earlierInsert.Index <= Index)
        {
            Index += earlierInsert.Text.Length;
        }

        return this;
    }
}

public sealed class PieceTableDocument
{
    private readonly List<Piece> _pieces = new();
    private readonly StringBuilder _addBuffer = new();

    public PieceTableDocument(long originalLength)
    {
        if (originalLength < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(originalLength));
        }

        // One virtual original piece represents the large source without allocating its text.
        if (originalLength > 0)
        {
            _pieces.Add(new Piece(PieceSource.Original, 0, originalLength));
        }
    }

    public long Length => _pieces.Sum(piece => piece.Length);

    public void Insert(long index, string text)
    {
        ArgumentNullException.ThrowIfNull(text);

        if (index < 0 || index > Length)
        {
            throw new ArgumentOutOfRangeException(nameof(index));
        }

        if (text.Length == 0)
        {
            return;
        }

        long addStart = _addBuffer.Length;
        _addBuffer.Append(text);
        Piece newPiece = new(PieceSource.Add, addStart, text.Length);

        // Find the piece boundary for the logical insert position.
        long logicalStart = 0;
        for (int i = 0; i < _pieces.Count; i++)
        {
            Piece current = _pieces[i];
            long logicalEnd = logicalStart + current.Length;

            if (index <= logicalEnd)
            {
                long offset = index - logicalStart;

                // Insert exactly before this piece.
                if (offset == 0)
                {
                    _pieces.Insert(i, newPiece);
                    return;
                }

                // Insert exactly after this piece. Merge adjacent add-buffer text when possible.
                if (offset == current.Length)
                {
                    if (current.Source == PieceSource.Add &&
                        current.Start + current.Length == addStart)
                    {
                        _pieces[i] = current with { Length = current.Length + text.Length };
                    }
                    else
                    {
                        _pieces.Insert(i + 1, newPiece);
                    }

                    return;
                }

                // Split the current piece and place the new add-buffer piece between the two
                // halves.
                Piece left = current with { Length = offset };
                Piece right = current with { Start = current.Start + offset,
                                             Length = current.Length - offset };

                _pieces[i] = left;
                _pieces.Insert(i + 1, newPiece);
                _pieces.Insert(i + 2, right);
                return;
            }

            logicalStart = logicalEnd;
        }

        // This path is used only when the document is empty and index is zero.
        _pieces.Add(newPiece);
    }

    public string GetRange(long start, int length)
    {
        if (start < 0 || length < 0 || start + length > Length)
        {
            throw new ArgumentOutOfRangeException();
        }

        StringBuilder result = new(length);
        long requestedEnd = start + length;
        long logicalStart = 0;

        // Read only pieces that overlap the requested viewport-plus-buffer range.
        foreach (Piece piece in _pieces)
        {
            long logicalEnd = logicalStart + piece.Length;

            if (logicalEnd <= start)
            {
                logicalStart = logicalEnd;
                continue;
            }

            if (logicalStart >= requestedEnd)
            {
                break;
            }

            long overlapStart = Math.Max(start, logicalStart);
            long overlapEnd = Math.Min(requestedEnd, logicalEnd);
            int count = checked((int)(overlapEnd - overlapStart));
            long offsetInsidePiece = overlapStart - logicalStart;

            if (piece.Source == PieceSource.Original)
            {
                // The demo uses '.' as virtual original text. Production would page these bytes
                // from storage.
                result.Append('.', count);
            }
            else
            {
                int addOffset = checked((int)(piece.Start + offsetInsidePiece));
                result.Append(_addBuffer.ToString(addOffset, count));
            }

            logicalStart = logicalEnd;
        }

        return result.ToString();
    }

    private enum PieceSource
    {
        Original,
        Add
    }

    private sealed record Piece(PieceSource Source, long Start, long Length);
}

public sealed class ClientSession
{
    private readonly PieceTableDocument _document;
    private readonly int _bufferSize;
    private readonly int _viewportStart;
    private readonly int _viewportLength;

    public ClientSession(PieceTableDocument document, int bufferSize, int viewportStart,
                         int viewportLength)
    {
        _document = document;
        _bufferSize = bufferSize;
        _viewportStart = viewportStart;
        _viewportLength = viewportLength;
    }

    public long CurrentRevision { get; set; }
    public string VisibleText { get; private set; } = string.Empty;

    public void Initialize(Snapshot snapshot, IEnumerable<IOperation> operations)
    {
        ArgumentNullException.ThrowIfNull(operations);

        // Start at the durable snapshot revision.
        CurrentRevision = snapshot.Revision;

        // Replay only operations after the snapshot, in server revision order.
        foreach (IOperation operation in operations)
        {
            operation.Apply(_document);
            CurrentRevision++;
        }

        UpdateWindow();
    }

    public void UpdateWindow()
    {
        long start = Math.Max(0, (long)_viewportStart - _bufferSize);
        int requestedLength = _viewportLength + (2 * _bufferSize);
        int safeLength = checked((int)Math.Min(requestedLength, _document.Length - start));

        // Replace the old materialized text with only the viewport and its two buffers.
        VisibleText = _document.GetRange(start, safeLength);
    }
}

public sealed class CollaborationServer
{
    private readonly List<IOperation> _opLog;
    private readonly SegmentedOperationStore _operationStore;
    private long _currentRevision;
    private long _logStartRevision;

    public CollaborationServer(long logStartRevision, IEnumerable<IOperation> retainedOperations,
                               SegmentedOperationStore operationStore)
    {
        _logStartRevision = logStartRevision;
        _opLog = retainedOperations.ToList();
        _currentRevision = logStartRevision + _opLog.Count;
        _operationStore = operationStore;

        // Seed persistent retained segments so later snapshot compaction models the diagram's
        // storage path.
        for (int i = 0; i < _opLog.Count; i++)
        {
            long revision = _logStartRevision + i + 1;
            _operationStore.Append(revision, _opLog[i]);
        }
    }

    public long CurrentRevision => _currentRevision;
    public long LogStartRevision => _logStartRevision;

    public IOperation Receive(IOperation operation)
    {
        // An operation older than the retained boundary cannot be transformed from this shortened
        // log.
        if (operation.BaseVersion < _logStartRevision || operation.BaseVersion > _currentRevision)
        {
            throw new InvalidOperationException(
                "Client base revision is unavailable. Reload a durable snapshot.");
        }

        // Convert the absolute BaseVersion into an index in the retained operation list.
        int firstIndex = checked((int)(operation.BaseVersion - _logStartRevision));

        // Transform only against operations accepted after the client's base revision.
        for (int i = firstIndex; i < _opLog.Count; i++)
        {
            operation = operation.Transform(_opLog[i]);
        }

        // The transformed operation is now based on the current absolute server revision.
        operation.BaseVersion = _currentRevision;
        _opLog.Add(operation);

        // Advancing the revision means this operation produces the next document state.
        _currentRevision++;
        _operationStore.Append(_currentRevision, operation);

        Broadcast(operation);
        return operation;
    }

    public void CompactThrough(long snapshotRevision)
    {
        // Compaction may only move forward to a revision already represented by a durable snapshot.
        if (snapshotRevision <= _logStartRevision || snapshotRevision > _currentRevision)
        {
            return;
        }

        int removeCount = checked((int)(snapshotRevision - _logStartRevision));
        _opLog.RemoveRange(0, removeCount);
        _operationStore.CompactThrough(snapshotRevision);

        // BaseVersion values stay absolute even though the retained list is now shorter.
        _logStartRevision = snapshotRevision;
    }

    private static void Broadcast(IOperation operation)
    {
        // Real code would send the accepted server-ordered operation to connected clients.
        _ = operation;
    }
}

public sealed class SegmentedOperationStore
{
    private readonly List<StoredOperation> _operations = new();

    public void Append(long revision, IOperation operation)
    {
        // The logical log is append-only. Each stored entry has its absolute revision.
        _operations.Add(new StoredOperation(revision, operation));
    }

    public void CompactThrough(long snapshotRevision)
    {
        // Physical old segments can be removed only after a durable snapshot covers them.
        _operations.RemoveAll(item => item.Revision <= snapshotRevision);
    }

    private sealed record StoredOperation(long Revision, IOperation Operation);
}

public sealed record Snapshot(string Id, long Revision, long DocumentLength);

public interface ISnapshotStore
{
    Snapshot CreateSnapshot(long revision, PieceTableDocument document);
    void Persist(Snapshot snapshot);
}

public sealed class InMemorySnapshotStore : ISnapshotStore
{
    private readonly List<Snapshot> _snapshots = new();
    private int _nextSnapshotNumber;

    public InMemorySnapshotStore(int nextSnapshotNumber)
    {
        _nextSnapshotNumber = nextSnapshotNumber;
    }

    public Snapshot CreateSnapshot(long revision, PieceTableDocument document)
    {
        // The checkpoint represents the complete logical document at this absolute revision.
        return new Snapshot($"snap_{_nextSnapshotNumber++}", revision, document.Length);
    }

    public void Persist(Snapshot snapshot)
    {
        // In production this must be a durable write. Returning means persistence succeeded.
        _snapshots.Add(snapshot);
    }
}

public sealed class SnapshotManager
{
    private readonly int _intervalOps;
    private readonly ISnapshotStore _store;
    private long _lastSnapshotRevision;

    public SnapshotManager(int intervalOps, long lastSnapshotRevision, ISnapshotStore store)
    {
        _intervalOps = intervalOps;
        _lastSnapshotRevision = lastSnapshotRevision;
        _store = store;
    }

    public void MaybeCreateSnapshot(long currentRevision, PieceTableDocument document,
                                    CollaborationServer server)
    {
        // Snapshot creation is expensive, so do it only after the configured operation interval.
        if (currentRevision - _lastSnapshotRevision < _intervalOps)
        {
            return;
        }

        // Persist the new checkpoint before deleting any operation history it covers.
        Snapshot snapshot = _store.CreateSnapshot(currentRevision, document);
        _store.Persist(snapshot);
        _lastSnapshotRevision = currentRevision;

        // Only now may the server and persistent log compact covered operations.
        server.CompactThrough(currentRevision);
    }
}
Time & Space Complexity

Let k be the number of operations replayed after a snapshot and c be the number of retained operations newer than one incoming edit's BaseVersion. At the collaboration layer, replaying k operations is O(k) operation applications. The simplified insert-versus-insert transform itself is O(1), so transforming one incoming edit against c newer operations is O(c). Snapshot creation is O(document size) and is intentionally infrequent. The client keeps only the viewport, two buffers, and pending operation state. In the example, 20,000 UTF-16 characters are materialized, about 40 KB of text, and the diagram estimates about 3.5 MB total model-plus-operation memory per client. The runnable piece-table demo uses a simple list of pieces, so an arbitrary local insert can take O(p) to locate its piece, where p is the current number of pieces; a production balanced piece tree or rope can reduce that lookup to O(log p).

Where it is used

This pattern is useful in collaborative document editors, browser-based IDEs, large text viewers, and other applications where the user edits a small visible part of a much larger logical document. Viewport loading bounds client memory. Ordered operation logs support collaboration and recovery. Periodic snapshots limit replay length and allow old log segments to be compacted safely.

Why Interviewers Ask This

The interviewer is testing whether the candidate can find the real memory bottleneck instead of only changing a collection. They want reasoning about viewport virtualization, operation-based collaboration, revision ordering, durable snapshots, and safe log compaction. The question also tests whether the candidate handles reconnects correctly, understands that snapshots trade write cost for shorter replay history, and can turn the design into clear C# without loading the entire document into memory.

Common interview mistakes

A common mistake is loading the complete document or full history into every browser. Another is mixing OT and CRDT rules without defining one consistent collaboration model. Candidates also sometimes use BaseVersion directly as a zero-based list index after compaction. It must remain an absolute revision, so the retained index is BaseVersion minus _logStartRevision. Another serious mistake is deleting old operations before the covering snapshot is durably stored. Also, viewport eviction must remove only materialized client content, not logical document data. Finally, a reconnecting client older than the retained boundary must reload from a newer snapshot instead of transforming against incomplete history.

Interview tip

State the invariant early: the browser holds only a bounded viewport window, while the server keeps absolute revisions. Then connect snapshots to safe log compaction. This gives the interviewer one clear story for memory, collaboration, recovery, and revision correctness.

Interviewer may ask next
What happens if a reconnecting client's BaseVersion is older than _logStartRevision?

The server must not transform that edit using an incomplete retained log. It should send a newer durable snapshot and the operations after that snapshot. The client rebuilds to a retained revision and then rebases or resubmits pending edits. Replaying k later operations needs O(k) operation applications. Client working memory stays bounded by the active window and pending edits. The tradeoff is extra reconnect work in exchange for being able to compact old history.

How would you reduce server memory and replay time if the operation log grows quickly?

Create durable snapshots more often and compact log segments that are fully covered by those snapshots. This reduces retained history and later replay work. Correctness is preserved because compaction happens only after the snapshot write succeeds. The tradeoff is more snapshot I/O and O(document size) checkpoint work, so the interval should balance persistence cost, replay time, and retained memory.

20. Debug a REST API endpoint with authentication issues.API DesignEasyGoogle

Question Details

Use the Google junior developer prompt, specify that the endpoint is failing because of auth behavior, and explain the request/response and authorization pieces that must be inspected.

Short Interview Answer (30-60 seconds)

At a high level, I would trace the failing request from the client to the API and identify where authentication or authorization breaks. The client sends GET /api/orders/123 over HTTPS with an Authorization: Bearer <JWT> header. The reverse proxy or API gateway forwards it to the ASP.NET Core Web API. I would verify token signature, issuer, audience, lifetime, scopes, roles, middleware order, and identity-provider key information. Then I would check whether the API returns 401, 403, or another error. The trade-off is stronger security with more configuration to debug.

Detailed Explanation

This question asks me to find why a protected API request is failing. I need to follow one request from the client, through the gateway, into the application, and back again. I would check what the client sends and whether the gateway forwards it correctly. Then I would check whether the application can identify the caller and whether that caller has permission. I would also inspect the identity system and the data access shown in the diagram. The goal is to find the exact layer causing the failure instead of guessing.

Useful Questions to Ask the Interviewer
  • Is the problem happening for every caller or only some callers?
  • Are we receiving 401 Unauthorized, 403 Forbidden, 500 Error, or another response?
  • Did token settings, signing keys, roles, policies, or gateway rules change recently?
Debug a REST API endpoint with authentication issues. diagram
How to Explain It in an Interview
1. Reproduce and inspect the client request

I would start with the exact request shown in the diagram. The client sends GET /api/orders/123 over HTTPS. It includes Authorization: Bearer <JWT> and Accept: application/json. I would check the URL, HTTP method, query parameters, headers, and token format. I would verify that the bearer token is present and does not contain extra spaces or use the wrong authentication scheme. I would also inspect token expiry, not-before time, issuer, audience, scopes, and roles. If client credentials are used elsewhere in the authentication setup, I would verify the configured client ID and secret without changing this request flow.

2. Check the reverse proxy or API gateway

The request next reaches the reverse proxy or API gateway. The diagram shows this layer handling TLS termination, rate limiting, routing, and optional authentication. I would confirm that HTTPS works and that the gateway forwards the Authorization header to the ASP.NET Core Web API. I would also check routing, certificate problems, header limits, rate-limiting rules, and CORS behavior when the caller is a browser. A browser preflight problem or a gateway that strips the authorization header can look like an API authentication problem.

3. Trace authentication inside ASP.NET Core

The gateway forwards the request over HTTPS to the ASP.NET Core Web API. The application flow is routing, authentication, authorization, endpoint processing, and then the action or controller. I would verify that UseAuthentication() executes before UseAuthorization(). Authentication answers, “Who is this caller?” For the bearer JWT shown here, I would validate the signature, trusted issuer, expected audience, token lifetime, not-before time, signing key, and accepted algorithm. A missing token, bad signature, expired token, untrusted issuer, or invalid audience should cause authentication to fail with 401 Unauthorized.

4. Inspect identity-provider and key information

The diagram shows an Identity Provider such as Auth0, Azure AD, Google, or Okta. It can issue JWTs and provide JWKS signing-key information or token-introspection information. JWKS is a set of public keys that an API can use to verify JWT signatures. Depending on the configured authentication scheme, the Web API can obtain current key information or use an HTTPS introspection call and receive the corresponding token information. I would check for stale JWKS data after key rotation and confirm that the API trusts the correct issuer and keys.

5. Check authorization separately

After authentication succeeds, authorization answers, “May this caller perform this action?” I would inspect policies, roles, required scopes, endpoint attributes, and claims mapping. I would verify that [Authorize] and [AllowAnonymous] are applied as intended. I would also check whether role or identity claims are mapped to the names the application expects. If the token is valid but the caller lacks a required scope, role, or policy requirement, the correct result is 403 Forbidden. I would not weaken authorization merely to make the error disappear.

6. Trace the endpoint, data, response, and logs

If authentication and authorization both succeed, the endpoint and action or controller can continue. The diagram then shows access to a data store, which may be a database or cache. The result comes back to the Web API. The Web API sends its response through the reverse proxy or API gateway and then back to the client over HTTPS. The diagram shows 200 OK, 401 Unauthorized, 403 Forbidden, and 500 Error, with Content-Type: application/json. I would reproduce the problem, capture the full request and response, review application, gateway, and identity-provider logs, change one suspected setting at a time, retest, and confirm the fix.

Practical Complexity & Trade-offs

The benefit of this design is that each layer has a clear security job. The client supplies the bearer JWT. The gateway handles transport, routing, rate limiting, and optional authentication. ASP.NET Core performs authentication before authorization. The identity provider supplies trusted token or signing-key information. This makes problems easier to isolate. The downside is that many settings must agree. Issuer, audience, signing keys, token times, claims, roles, policies, gateway forwarding, and middleware order can all cause failures. JWKS reduces the need to hard-code public keys, but key rotation can cause temporary problems if cached information becomes stale. Token introspection can provide current token information, but it adds an HTTPS dependency on the identity provider.

Why Interviewers Ask This

Interviewers ask this to see whether I can debug an API by following the real request and response path. They want me to separate authentication from authorization, understand 401 versus 403, inspect bearer-token claims, and identify which component owns each check. They also test whether I understand the gateway, ASP.NET Core middleware, identity-provider key information, failure handling, and data path. Good answers diagnose one layer at a time instead of changing security settings blindly.

Interviewer may ask next
What would you check if valid callers suddenly start receiving 401 responses after a signing-key rotation?

I would focus first on token validation and the identity-provider key information. The client still sends GET /api/orders/123 with Authorization: Bearer <JWT>, and the gateway should still forward that header unchanged. After a signing-key rotation, the identity provider may sign new JWTs with a different key. I would inspect the token header and compare its key identifier with the current JWKS information available to the ASP.NET Core Web API. I would also verify issuer, audience, signature algorithm, expiry, not-before time, and clock skew so I do not incorrectly blame the rotation. If an HTTPS introspection flow is configured, I would verify that response as well. If the API cannot find a trusted key or otherwise validate the token, authentication should fail closed with 401 Unauthorized. Authorization, the endpoint, and data access should not run. The downside of caching JWKS data is that rotation can briefly expose stale-key problems until current key information is obtained.

What would you check if authentication succeeds but some callers receive 403 Forbidden?

I would move from authentication to authorization because 403 Forbidden means the caller was identified but is not allowed to perform the action. I would keep the same GET /api/orders/123 request path through the reverse proxy or API gateway and the ASP.NET Core Web API. I would inspect the authenticated caller's scopes, roles, and other claims. Then I would compare them with the endpoint's policies, role names, requirements, and authorization attributes. I would also check claims mapping because the token may contain a valid role under a claim name the application does not use. The middleware order should remain UseAuthentication() before UseAuthorization(), and [Authorize] or [AllowAnonymous] should match the intended endpoint behavior. If the caller truly lacks the required permission, returning 403 is correct. The main downside is that detailed policies and claims mappings add configuration that must stay synchronized with the identity provider.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.

Company Notice: This guide is an independent educational resource and is not affiliated with, endorsed by, sponsored by, or approved by the company named in this guide. Company names are used only to identify interview experiences commonly reported by candidates. Interview practices can change without notice, and inclusion of company-specific content does not mean these questions are official, complete, or guaranteed to be asked. To the fullest extent permitted by law, the author, contributors, and publisher are not responsible for outcomes related to use of this material.