54 Google .NET Developer Interview Questions & Answers

google icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

1. Maximum Subarray SumCodingEasyGoogle

Question Details

Find the maximum subarray sum in an integer array, and explain how you handle an all-negative input and how you would keep the solution linear-time.

Short Interview Answer (30-60 seconds)

I would use Kadane’s algorithm. I start currentSum and bestSum with the first value, so an all-negative array is handled correctly. Then I process the remaining elements from left to right. At each index, I choose between starting a new subarray at the current value or extending the previous subarray. I update bestSum whenever the running sum is better. In the example, [4, -1, 2, 1] gives the maximum sum of 6. The time is O(n) and the auxiliary space is O(1).

Detailed Explanation

See the Code while reading this explanation.

The question asks me to find the largest sum from any contiguous part of an integer array. The chosen values must stay next to each other in the original order. The goal is to solve it with one left-to-right pass and constant extra space. Kadane’s algorithm fits this problem because it keeps the best sum ending at the current position and the best sum found anywhere so far. It also handles an all-negative array correctly by starting from the first element instead of starting from zero.

Useful Questions to Ask the Interviewer
  1. Should I return only the maximum sum, or also the subarray values and their indices?
  2. Can the input array be empty?
Maximum Subarray Sum diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an integer array named nums. The required output is one integer, the largest possible sum of any contiguous subarray. The example is [-2, 1, -3, 4, -1, 2, 1, -5, 4]. The answer is 6.

The subarray that gives this result is [4, -1, 2, 1]. These values are at indices 3 through 6.

2. Choose the algorithm

I use Kadane’s algorithm. It keeps two running values:

  • currentSum: the best sum of a contiguous subarray that ends at the current index.
  • bestSum: the best sum found anywhere so far.

At each element, I have two choices. I can start a new subarray with the current value, or I can extend the previous subarray.

The main rule is:

currentSum = max(nums[i], currentSum + nums[i])

Then I update:

bestSum = max(bestSum, currentSum)

This avoids trying every possible subarray.

3. Initialize the state

For the example, index 0 contains -2.

So I start with:

  • currentSum = -2
  • bestSum = -2

The loop starts at index 1 because index 0 already initialized the running state.

Starting from the first element is important for all-negative input. For example, [-5, -2, -7] returns -2. The algorithm does not incorrectly return 0.

4. Walk through the example

At index 1, the value is 1.

max(1, -2 + 1 = -1) = 1

Starting a new subarray at 1 is better.

Now:

  • currentSum = 1
  • bestSum = 1

At index 2, the value is -3.

max(-3, 1 + -3 = -2) = -2

Extending the current subarray is better than restarting at -3.

Now:

  • currentSum = -2
  • bestSum = 1

At index 3, the value is 4.

max(4, -2 + 4 = 2) = 4

Starting a new subarray at 4 is better.

Now:

  • currentSum = 4
  • bestSum = 4

At index 4, the value is -1.

max(-1, 4 + -1 = 3) = 3

I extend the current subarray.

Now:

  • currentSum = 3
  • bestSum = 4

At index 5, the value is 2.

max(2, 3 + 2 = 5) = 5

I extend again.

Now:

  • currentSum = 5
  • bestSum = 5

At index 6, the value is 1.

max(1, 5 + 1 = 6) = 6

This creates the new best result.

Now:

  • currentSum = 6
  • bestSum = 6

At index 7, the value is -5.

max(-5, 6 + -5 = 1) = 1

The running sum becomes 1, but bestSum stays 6.

At index 8, the value is 4.

max(4, 1 + 4 = 5) = 5

The final best value is still 6.

The best subarray is [4, -1, 2, 1], at indices 3 through 6.

5. Explain why the result is correct

The central invariant is: after processing index i, currentSum is the maximum sum of any contiguous subarray that ends at i, and bestSum is the maximum value seen across all processed indices.

A maximum subarray ending at the current index must either start at the current value or extend the best subarray that ended at the previous index. The max calculation checks exactly those two cases.

Because that invariant remains correct at every step, bestSum is the maximum subarray sum when the loop finishes.

6. Explain the C# implementation

The method first checks for null or empty input and throws an ArgumentException for that defensive case.

It initializes currentSum and bestSum from nums[0].

The loop runs from index 1 to the last index. Each iteration first updates currentSum, then updates bestSum.

Finally, the method returns bestSum.

The Main method uses the same example from the diagram and prints 6.

7. Explain complexity and edge cases

The algorithm processes the input at most once.

  • Time: O(n)
  • Auxiliary space: O(1)

Important edge cases are an all-negative array, a single-element array, and arrays containing zeros. Empty input is rejected defensively by the code.

Key Insight / Why This Solution Works

The key insight is to keep the best subarray sum that ends at the current index. That value is stored in currentSum. For each new value, I compare starting a new subarray with extending the previous one. Then I update bestSum with the better running result. The central invariant is: currentSum is the best sum for a contiguous subarray ending at the current index, and bestSum is the best sum seen anywhere so far. This lets the algorithm solve the problem in linear time with constant auxiliary space.

Code
using System;

public static class Program
{
    public static void Main()
    {
        // Use the exact example shown in the diagram.
        int[] nums = { -2, 1, -3, 4, -1, 2, 1, -5, 4 };

        // Run the algorithm and print the maximum subarray sum.
        int result = MaxSubArray(nums);

        // The expected result for the diagram's example is 6.
        Console.WriteLine(result);
    }

    public static int MaxSubArray(int[] nums)
    {
        // Reject null or empty input before reading nums[0].
        // The empty-input case is handled defensively in the diagram.
        if (nums == null || nums.Length == 0)
        {
            throw new ArgumentException("Input array must not be empty.", nameof(nums));
        }

        // Start from the first element so an all-negative array
        // keeps its least negative value as the best result.
        int currentSum = nums[0];
        int bestSum = nums[0];

        for (int i = 1; i < nums.Length; i++)
        {
            // Choose between starting a new subarray at nums[i]
            // and extending the best subarray ending at i - 1.
            currentSum = Math.Max(nums[i], currentSum + nums[i]);

            // Keep the best sum seen anywhere in the processed input.
            bestSum = Math.Max(bestSum, currentSum);
        }

        // Return the maximum contiguous-subarray sum found.
        return bestSum;
    }
}
Time & Space Complexity

The time complexity is O(n). We process the array from left to right and process each element at most once. The auxiliary space is O(1) because we only keep currentSum, bestSum, and the loop index. We do not create another array or a collection that grows with the input.

Where it is used

This pattern is useful when software needs the largest sum over any contiguous range of values. For example, it can be used when a program needs the strongest continuous gain or total over a consecutive segment of measurements. The important condition is that the selected values stay contiguous and keep their original order.

Why Interviewers Ask This

This question checks whether I can recognize a common array pattern and turn it into a simple linear-time solution. It also tests whether I understand a running invariant, handle all-negative input, keep values and indices separate, write correct C#, and state time and auxiliary space complexity accurately. The interviewer can also see whether I can explain why the local start-or-extend decision leads to the global maximum.

Common interview mistakes

1. Initializing currentSum and bestSum to 0. That gives the wrong result for an all-negative array because the correct answer can be negative. 2. Confusing a subarray with a subsequence. A subarray must contain contiguous elements. 3. Starting the loop at index 0 after already using nums[0] for initialization. The shown solution starts at index 1. 4. Updating bestSum from the wrong state. bestSum should be updated after currentSum is calculated. 5. Claiming extra storage grows with the input. The shown algorithm uses O(1) auxiliary space.

Interview tip

Explain the two choices at every index: start fresh with nums[i] or extend the previous subarray. Then state the invariant for currentSum and bestSum. This makes the reason for the max formula easy to follow.

Interviewer may ask next
How would you return the actual maximum subarray instead of only its sum?

I would keep the same Kadane’s algorithm and add the start index of the current subarray plus the best start and end indices. When starting fresh is better, I would move the current start to i. When bestSum improves, I would save the current start and i as the best range. The time stays O(n). The auxiliary space stays O(1).

Can the solution be changed to process a very large or streaming input?

Yes. The same running state can process values as they arrive because only currentSum and bestSum are needed from the past. The algorithm still uses one pass over the values and O(1) algorithmic state. The tradeoff is that if I also need the actual subarray values or indices, I must keep the corresponding best range information while the stream is processed.

2. Given a matrix representing a map with dynamic obstacles, how would you find the shortest path between two points if certain paths unlock only after specific conditions are met?CodingMediumGoogle

Question Details

Work with the reported grid-and-obstacles scenario, define what changes when a path becomes available later, and explain the state you must track to preserve shortest-path correctness.

Short Interview Answer (30-60 seconds)

I would use BFS, but I would include both the position and the unlock condition in each state. So a state is (row, column, hasKey). BFS processes these states in increasing distance order. When I enter K, hasKey becomes true. I can enter D only after that. I mark each full state visited before enqueueing it. The first time I dequeue the goal, its distance is shortest. Time is O(rows × cols), and auxiliary space is O(rows × cols).

Detailed Explanation

See the Code while reading this explanation.

The map is a grid with a start S, goal G, walls #, key K, and door D. The door cannot be crossed until the key has been reached. We need the smallest number of up, right, down, or left moves from the start to the goal. The important idea is that reaching the same place before and after getting the key can lead to different future moves. BFS fits because every valid move costs 1.

Useful Questions to Ask the Interviewer
  1. Are movements limited to the four directions: right, down, left, and up?
  2. Does every valid move have the same cost of 1?
  3. Should I return -1 when the goal cannot be reached even after all possible unlocks?
  4. Can the starting cell already satisfy the unlock condition?
Given a matrix representing a map with dynamic obstacles, how would you find the shortest path between two points if certain paths unlock only after specific conditions are met? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a grid together with start and goal coordinates. In the diagram's example, the grid has 3 rows and 4 columns. S is at (0,0). K is at (0,2). D is at (1,3). G is at (2,3). The grid rows are S . K #, # # . D, and # # # G. Walls cannot be crossed. The door can be crossed only when hasKey is true. The required output is the minimum number of moves. The result for this example is 5.

2. Choose BFS and include the unlock state

I use a queue for BFS. BFS is correct for this problem because every move costs 1. Each queue item stores (row, column, hasKey, distance). The visited structure is visited[row, column, state], where state is 0 without the key and 1 with the key. This keeps (row, column, false) separate from (row, column, true). That distinction is necessary because the second state may be able to cross D even when the first state could not.

3. Initialize the state

The initial queue contains (0,0,F,0). The matching visited entry is marked before the state is enqueued. Here F means hasKey is false and T means it is true. The central invariant is that the first time a complete state (row, column, hasKey) leaves the BFS queue, its stored distance is the shortest distance to that exact state.

4. Walk through the verified example

The neighbor order is right, down, left, up.

Step 0: Dequeue (0,0,F,0). The only open new neighbor is (0,1). Enqueue (0,1,F,1).

Step 1: Dequeue (0,1,F,1). Moving right reaches K at (0,2). Collecting the key changes the state from false to true. Enqueue (0,2,T,2).

Step 2: Dequeue (0,2,T,2). Moving down reaches the open cell (1,2). Enqueue (1,2,T,3).

Step 3: Dequeue (1,2,T,3). The cell to the right is D. Because the key is already held, the door is traversable. Enqueue (1,3,T,4).

Step 4: Dequeue (1,3,T,4). Moving down reaches G at (2,3). Enqueue (2,3,T,5).

Step 5: Dequeue (2,3,T,5). This state is the goal, so processing stops and the method returns 5.

One valid shortest route is S -> (0,1) -> K -> (1,2) -> D -> G.

5. Explain why the result is correct

Every move costs 1, so BFS processes reachable states in nondecreasing distance order. The state includes the unlock information, so a coordinate reached without the key is different from the same coordinate reached with the key. The visited array therefore removes cycles without throwing away a later state that has more capability. The first time the goal is dequeued, its distance is the shortest valid distance in this expanded state space.

6. Explain the C# implementation

The queue stores row, column, key state, and distance. The code marks a complete state visited before enqueueing it, which avoids duplicate queue entries for that same state. Entering K changes nextHasKey to true. Entering D is rejected unless nextHasKey is true. The four direction arrays implement right, down, left, and up. When the goal is dequeued, the code returns its distance. If the queue becomes empty first, it returns -1.

7. Explain complexity and edge cases

There are at most rows × cols × 2 states because every cell can be represented without the key or with the key. Each state checks four neighbors. The time complexity is therefore O(rows × cols), and the auxiliary space is O(rows × cols). Relevant edge cases are starting with the key condition already true, encountering D before K, the goal being unreachable even after unlocking, and reaching the same coordinate again with a different unlock state.

Key Insight / Why This Solution Works

The key insight is that the grid position alone is not enough to describe the search. Reaching K changes which future moves are legal, so the search state must be (row, column, hasKey). BFS is the right algorithm because every movement edge has equal cost. The central invariant is that the first time a complete state leaves the BFS queue, the stored distance is the shortest distance to that exact state. A three-dimensional visited array prevents repeated work while still allowing the same coordinate to be revisited when its unlock state is different.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Use the exact 3 x 4 example shown in the approved diagram.
        char[][] grid = { new[] { 'S', '.', 'K', '#' }, new[] { '#', '#', '.', 'D' },
                          new[] { '#', '#', '#', 'G' } };

        // Start is (0,0), goal is (2,3), and the expected answer is 5.
        int result = ShortestPath(grid, 0, 0, 2, 3);
        Console.WriteLine(result);
    }

    public static int ShortestPath(char[][] grid, int startRow, int startCol, int goalRow,
                                   int goalCol)
    {
        int rows = grid.Length;
        int cols = grid[0].Length;

        // The third dimension separates the same cell by unlock state:
        // 0 means the key is not held, and 1 means the key is held.
        bool[,,] visited = new bool[rows, cols, 2];

        // Each BFS state stores position, unlock state, and distance from the start.
        Queue<(int Row, int Col, bool HasKey, int Distance)> queue = new();

        // Support the edge case where the starting cell already represents K.
        bool startHasKey = grid[startRow][startCol] == 'K';

        // Mark the full state before enqueueing so it cannot be queued twice.
        int startState = startHasKey ? 1 : 0;
        visited[startRow, startCol, startState] = true;
        queue.Enqueue((startRow, startCol, startHasKey, 0));

        // Check neighbors in the diagram's order: right, down, left, up.
        int[] rowChange = { 0, 1, 0, -1 };
        int[] colChange = { 1, 0, -1, 0 };

        while (queue.Count > 0)
        {
            // BFS dequeues the oldest state, so states are processed by distance.
            (int row, int col, bool hasKey, int distance) = queue.Dequeue();

            // The first dequeued goal state has the shortest valid distance.
            if (row == goalRow && col == goalCol)
            {
                return distance;
            }

            for (int direction = 0; direction < 4; direction++)
            {
                int nextRow = row + rowChange[direction];
                int nextCol = col + colChange[direction];

                // Ignore positions outside the grid.
                if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols)
                {
                    continue;
                }

                char cell = grid[nextRow][nextCol];

                // A wall is never traversable.
                if (cell == '#')
                {
                    continue;
                }

                // Entering K changes the unlock state to true and it stays true.
                bool nextHasKey = hasKey || cell == 'K';

                // The door can be entered only after the key condition is true.
                if (cell == 'D' && !nextHasKey)
                {
                    continue;
                }

                int nextState = nextHasKey ? 1 : 0;

                // Skip only an already-seen copy of this exact expanded state.
                if (visited[nextRow, nextCol, nextState])
                {
                    continue;
                }

                // Mark before enqueueing to avoid duplicate queue entries.
                visited[nextRow, nextCol, nextState] = true;
                queue.Enqueue((nextRow, nextCol, nextHasKey, distance + 1));
            }
        }

        // No valid expanded-state path can reach the goal.
        return -1;
    }
}
Time & Space Complexity

Let rows be the number of rows and cols be the number of columns. Each cell has at most two relevant states: key not held and key held. So the search has at most rows × cols × 2 states. Each state checks four directions, which is constant work. Therefore the time complexity is O(rows × cols). The queue and visited array can also grow with the number of states, so auxiliary space is O(rows × cols). With k independent binary unlock conditions, the number of state combinations can grow to 2^k, giving O(rows × cols × 2^k) states.

Where it is used

This state-aware BFS pattern is useful when movement rules change after something happens. Examples include games where a key opens a door, robot maps where a switch activates a passage, access-control systems where a permission unlocks an area, and navigation problems where collected capabilities change which transitions are legal. The important idea is to store both the location and the changing capability in the search state.

Why Interviewers Ask This

This question tests whether you notice that a changing condition expands the search state. The interviewer is evaluating whether you choose BFS for an unweighted shortest path, represent the unlock condition correctly, mark visited states at the right time, and preserve shortest-path correctness when the same coordinate can have different capabilities. It also checks whether you can translate that reasoning into correct C# and explain time, space, edge cases, and tradeoffs accurately.

Common interview mistakes

A common mistake is using only visited[row, col]. That can incorrectly remove a later visit to the same cell after the key has been collected. Another mistake is allowing D before the unlock state becomes true. Candidates may also mark a state visited only after dequeuing it, which can add duplicate states to the queue. Using DFS and assuming it automatically returns the shortest route is another error. Finally, the complexity analysis must account for both unlock states of each cell.

Interview tip

Define the state before writing any BFS code. Say clearly that (row, col, false) and (row, col, true) are two different search states because they allow different future moves. That explanation makes the visited structure, the door rule, and the shortest-path proof much easier to justify.

Interviewer may ask next
How would the solution change if there were several independent keys or switches?

Keep BFS because each movement still costs 1, but replace the single boolean with a bitmask. Each bit represents whether one key or switch has been activated. The state becomes (row, column, mask), and visited becomes visited[row, column, mask]. A transition updates the mask before checking rules that depend on it. This preserves correctness because BFS still separates states with different capabilities. With k independent binary conditions, there can be O(rows × cols × 2^k) states, so both time and auxiliary space become O(rows × cols × 2^k). The tradeoff is exponential growth in the number of condition combinations.

What changes if different valid moves have different nonnegative costs?

Ordinary BFS is no longer enough because queue order would not necessarily match total path cost. Keep the same expanded state (row, column, hasKey), but use Dijkstra's algorithm with a min-priority queue ordered by the best known distance. Relax a transition only when it produces a smaller cost for the destination state. With nonnegative costs, Dijkstra preserves shortest-path correctness. If V is the number of expanded states and E is the number of legal transitions, a binary-heap implementation takes O((V + E) log V) time and O(V + E) auxiliary space in the standard implementation. The tradeoff is extra priority-queue work compared with BFS.

3. Given a long string, how do you find the longest continuous substring that contains at most K distinct characters using an optimal runtime?CodingMediumGoogle

Question Details

Explain the sliding-window constraints for the Google prompt, including what counts as a distinct character and how to keep the runtime optimal.

Short Interview Answer (30-60 seconds)

I would use a sliding window with a Dictionary<char, int> that stores each character and its frequency in the current window. I move the right pointer to expand the window. If the number of distinct characters becomes greater than k, I move the left pointer and reduce frequencies until the window is valid again. Then I update the longest valid window. This gives O(n) expected time and uses O(min(n, number of distinct characters in s)) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a string s and an integer k. We need the longest continuous part of the string that contains no more than k different characters. A repeated character does not create another distinct character. We only count the different characters inside the current continuous window. A sliding window fits this problem because we can grow the window from the right and shrink it from the left only when it has too many distinct characters.

Useful Questions to Ask the Interviewer
  1. Should character comparisons be case-sensitive?
  2. If several longest valid substrings have the same length, is returning any one of them acceptable?
  3. Should an empty string or k <= 0 return an empty string?
Given a long string, how do you find the longest continuous substring that contains at most K distinct characters using an optimal runtime? diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is string s and integer k. The output is the longest contiguous substring containing at most k distinct characters. Contiguous means the characters must stay next to each other in the original string. For the diagram example, s = "eceba" and k = 2. The returned substring is "ece".

2. Choose the sliding window and frequency map

Use two boundaries called left and right. They describe the current substring s[left..right]. Use Dictionary<char, int> called counts. Each key is one distinct character currently in the window. Its value is that character's frequency. Repeated copies increase the frequency but do not increase counts.Count. The central invariant is: after shrinking finishes, the current window contains at most k distinct characters.

3. Initialize the state

Start with left = 0, bestStart = 0, and bestLength = 0. The counts dictionary is empty. The right pointer has not started processing the string yet. As right moves forward, the character at s[right] is added to the frequency map.

4. Walk through the example

For s = "eceba" and k = 2:

At right = 0, the character is 'e'. counts changes from {} to {e:1}. The current window is "e". It is valid, so bestStart stays 0 and bestLength becomes 1.

At right = 1, the character is 'c'. counts changes from {e:1} to {e:1,c:1}. The current window is "ec". It has two distinct characters, so it is valid. bestStart stays 0 and bestLength becomes 2.

At right = 2, the character is 'e'. counts changes from {e:1,c:1} to {e:2,c:1}. The current window is "ece". It still has only two distinct characters. bestStart stays 0 and bestLength becomes 3.

At right = 3, the character is 'b'. counts first becomes {e:2,c:1,b:1}. That has three distinct characters, so the window is invalid. We shrink repeatedly from the left. We remove one 'e' at left index 0, giving {e:1,c:1,b:1}, and left becomes 1. There are still three distinct characters, so we continue shrinking. We remove 'c' at left index 1. Its frequency becomes 0, so we remove the 'c' key. counts becomes {e:1,b:1}, and left becomes 2. The current window is now "eb". Its length is 2, so the best result remains "ece" with length 3.

At right = 4, the character is 'a'. counts first becomes {e:1,b:1,a:1}. There are three distinct characters, so we shrink again. We remove 'e' at left index

  1. Its frequency becomes 0, so the key is removed. counts becomes {b:1,a:1}, and left becomes
  2. The current window is "ba". Its length is 2, so the best result still remains "ece" with length 3.
5. Explain why the result is correct

The frequency map always represents the current window. Whenever counts.Count becomes greater than k, the left pointer moves until the window contains at most k distinct characters again. We update the best answer only after the window is valid. For each right position, the algorithm therefore considers a valid window ending at that position, and the longest valid window seen during the traversal is recorded.

6. Explain the C# implementation

The method first returns string.Empty when s is empty or k <= 0. It then creates the Dictionary<char, int> frequency map and initializes left, bestStart, and bestLength. The for loop moves right through the string. Each new character increases its frequency. While counts.Count > k, the code decreases the frequency of s[left], removes that dictionary key if the frequency becomes 0, and increments left. After the window becomes valid, it computes right - left + 1 and updates the best window when the new length is larger. At the end, it returns s.Substring(bestStart, bestLength).

7. Explain complexity and edge cases

The expected running time is O(n). The right pointer visits each character once, and the left pointer only moves forward, so each character can leave the window at most once. Dictionary operations are O(1) on average, with normal hashing and collision caveats. The auxiliary space shown in the diagram is O(min(n, number of distinct characters in s)). Relevant edge cases are an empty string, k <= 0, a string containing only one repeated character when k >= 1, and k being at least the number of distinct characters in the whole string.

Key Insight / Why This Solution Works

The key insight is that we do not need to restart from every possible substring. We maintain one sliding window. The right pointer expands it, and Dictionary<char, int> stores character -> frequency for that current window. counts.Count is therefore the number of distinct characters currently present. If counts.Count becomes greater than k, we repeatedly move left and reduce frequencies until the window is valid again. A key must be removed when its frequency reaches 0. The invariant is: after shrinking, s[left..right] contains at most k distinct characters. Only then do we compare the current window with the best result.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Run the exact example shown in the approved diagram.
        string s = "eceba";
        int k = 2;

        string result = LongestSubstringAtMostKDistinct(s, k);

        // Expected output: ece
        Console.WriteLine(result);
    }

    public static string LongestSubstringAtMostKDistinct(string s, int k)
    {
        // No non-empty valid substring exists for these defensive edge cases.
        if (string.IsNullOrEmpty(s) || k <= 0)
        {
            return string.Empty;
        }

        // Store character -> frequency for the current sliding window.
        Dictionary<char, int> counts = new Dictionary<char, int>();

        // left is the starting index of the current window.
        int left = 0;

        // Track the longest valid window found so far.
        int bestStart = 0;
        int bestLength = 0;

        // Expand the window by moving right through each character.
        for (int right = 0; right < s.Length; right++)
        {
            char rightChar = s[right];

            // Add the new character or increase its frequency in the window.
            if (counts.ContainsKey(rightChar))
            {
                counts[rightChar]++;
            }
            else
            {
                counts[rightChar] = 1;
            }

            // Repeatedly shrink until the window has at most k distinct characters.
            while (counts.Count > k)
            {
                char leftChar = s[left];

                // One occurrence of leftChar is leaving the current window.
                counts[leftChar]--;

                // Remove zero-frequency keys so counts.Count stays accurate.
                if (counts[leftChar] == 0)
                {
                    counts.Remove(leftChar);
                }

                // Move the left boundary to the next character.
                left++;
            }

            // The window is valid now, so compare its length with the best result.
            int windowLength = right - left + 1;
            if (windowLength > bestLength)
            {
                bestLength = windowLength;
                bestStart = left;
            }
        }

        // Return the exact longest valid contiguous substring that was recorded.
        return s.Substring(bestStart, bestLength);
    }
}
Time & Space Complexity

The expected time is O(n). The right pointer moves from the start of the string to the end. The left pointer also moves only forward, and each character can leave the window at most once. Dictionary<char, int> lookup, insertion, update, and removal are O(1) on average, so the total expected work is linear. The auxiliary space shown in the diagram is O(min(n, number of distinct characters in s)) because the dictionary stores frequencies for distinct characters from the current window.

Where it is used

This sliding-window pattern is useful when software needs to find or track a continuous range while maintaining a limit. Examples include text analysis, event streams, recent activity windows, and substring or subarray problems where a window can expand and shrink without checking every possible range from the beginning.

Why Interviewers Ask This

This question tests whether the candidate recognizes a sliding-window problem and can maintain a precise invariant while two boundaries move independently. It also tests correct use of a frequency dictionary, especially repeated shrinking and removing zero-frequency keys. The interviewer can evaluate whether the candidate understands contiguous substrings, handles repeated characters correctly, writes clear C#, and explains expected O(n) time without treating average Dictionary performance as a guaranteed worst-case bound.

Common interview mistakes

A common mistake is shrinking only once when counts.Count > k. The code must keep shrinking until counts.Count <= k. Another mistake is decreasing a frequency to 0 but leaving that key in the dictionary, which makes the distinct-character count wrong. Candidates may also update the best answer before restoring a valid window. Another error is treating the required result as a subsequence instead of a contiguous substring. Finally, it is inaccurate to describe Dictionary operations as guaranteed O(1); they are O(1) on average.

Interview tip

State the invariant before coding: after the shrinking loop finishes, the current window contains at most k distinct characters. Then make the implementation follow that sentence directly: expand right, shrink left while invalid, and update the best answer only after the window is valid.

Interviewer may ask next
How would the solution change if the input arrived as a stream and you could not keep the whole string?

The same sliding-window idea can be used, but we must keep the characters that are still inside the active window because the left side may need to remove them later. An ordered buffer such as a queue can hold the current window while the same frequency dictionary tracks distinct characters. Each incoming character is added at the right. While the distinct count is greater than k, characters are removed from the front and their frequencies are reduced. Processing remains O(n) expected time, with extra memory proportional to the active window and its frequency map.

What is the worst-case behavior of Dictionary<char, int> compared with its average-case behavior?

The diagram's O(n) bound is an expected-time bound because Dictionary operations are O(1) on average. Pathological hash collisions can make individual dictionary operations slower, so the hash-table assumption does not provide a strict guaranteed O(n) worst-case bound. The sliding-window pointer logic does not change. The tradeoff is that Dictionary gives simple and fast expected performance, while a data structure with stronger worst-case lookup guarantees would usually add extra logarithmic cost.

4. In a binary tree, find the longest path where every node has the same value. The path can start and end anywhere, it doesn't need to go through the root.CodingHardGoogle

Question Details

Treat the tree as the reported Google interview setting, define how same-value paths are extended or stopped, and explain what information each recursive step must return.

Short Interview Answer (30-60 seconds)

I would use post-order DFS. Each recursive call returns the longest downward same-value path that can continue through its parent, measured in edges. After I process both children, I extend only the sides whose child value matches the current node. I combine the two matching sides to update a global maximum, then return only the longer side upward. Each node is visited once, so the time is O(n), with O(h) auxiliary space for the recursion stack.

Detailed Explanation

See the Code while reading this explanation.

We have a binary tree, and we want the longest connected path whose nodes all have the same value. The path may begin and end anywhere, so it does not need to contain the root. In this solution, path length means the number of edges. We process children before their parent. Each node learns how far a matching path can continue downward on its left and right sides. It combines those two sides for the best path through that node and keeps the largest result seen anywhere.

Useful Questions to Ask the Interviewer
  1. Should the path length be counted in edges? In this solution, yes.
  2. Can the path start and end anywhere in the tree? Yes.
  3. Can a valid path use both a matching left branch and a matching right branch through the same node? Yes.
In a binary tree, find the longest path where every node has the same value. The path can start and end anywhere, it doesn't need to go through the root. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the root of a binary tree. Each node contains an integer value. We return one integer: the maximum number of edges in a connected path where every node has the same value. The path may be completely inside a subtree or may pass through the root.

The diagram's example has root value 1. Its left child is 1 and its right child is 2. The left child has a left child 1 and a right child 3. That lower 1 has two leaf children with value 1. On the right side, the top 2 has a matching child 2, which has a matching leaf 2. The final answer is 3 edges.

2. Use post-order DFS

I use depth-first search in post-order. This means I process the left subtree and right subtree before finishing the current node. Each recursive call returns the longest one-sided same-value path, measured in edges, that starts at that node and goes downward.

The base case is a null node. It returns 0.

After both recursive calls return, I extend a side only when that child exists and has the same value as the current node. If the values do not match, that side becomes 0 because the same-value path stops there.

3. Compute the two matching sides

For a matching left child, leftLen becomes the child's returned length plus 1 for the edge from the current node to that child. Otherwise leftLen becomes 0. The same rule is used for rightLen.

A complete path through the current node may use both matching sides. Its length is leftLen + rightLen. I compare this value with the global maxLen.

Only one side can be returned to the parent because a downward path cannot branch. Therefore the recursive call returns Math.Max(leftLen, rightLen).

4. Walk through the verified example

The two deepest leaf nodes with value 1 each return 0 because they have no downward edges.

Their parent has value 1. Both children also have value 1, so leftLen = 1 and rightLen = 1. The path through this node is 1 + 1 = 2. maxLen becomes 2. The node returns 1 to its parent.

The node with value 3 is a leaf, so it returns 0. Its parent has value 1. The matching left child returned 1, so adding the connecting edge gives leftLen = 2. The right child has value 3, so rightLen = 0. The path through this node is 2. maxLen stays 2, and the node returns 2.

On the right side, the bottom leaf with value 2 returns

  1. Its parent extends that matching child to length 1 and returns
  2. The next 2 extends it again to length 2 and returns
  3. The largest path on that side is 2 edges, so maxLen remains 2.

Finally, the root has value 1. Its left child also has value 1, so the returned 2 becomes leftLen = 3 after adding the connecting edge. Its right child has value 2, so rightLen = 0. The path through the root is 3 + 0 = 3. maxLen becomes 3. The final answer is 3.

5. Explain why the result is correct

At each node, leftLen and rightLen contain only paths that can connect directly to the current node and still have the same value. A different child value makes that side 0. Therefore leftLen + rightLen is exactly the longest same-value path that passes through the current node.

The DFS performs this calculation for every node. The global maximum stores the largest complete same-value path among all nodes processed so far. This covers paths inside either subtree and paths that pass through any node.

6. Explain the C# implementation

The Solution class stores maxLen as global state. LongestSameValuePath resets it to 0, calls Dfs on the root, and returns the final maximum.

Dfs first handles the null base case. It recursively processes both children. A matching child contributes its returned length plus 1 edge. A nonmatching child contributes 0. The method updates maxLen with leftLen + rightLen and returns Math.Max(leftLen, rightLen), because the parent can extend only one downward branch.

7. Explain complexity and edge cases

The time complexity is O(n) because each of the n nodes is visited once. The auxiliary space is O(h), where h is the height of the tree, because recursive calls use the call stack. For a skewed tree, h can be n.

An empty tree returns 0. A single node returns 0 because it contains no edges. If all adjacent node values are different, the result is 0. If every node has the same value, the result depends on the tree shape. A chain of n nodes has a longest path of n - 1 edges.

Key Insight / Why This Solution Works

The key insight is that a parent does not need the complete best path from a child. It only needs the longest same-value path that starts at that child and continues downward, because that is the only path the parent can extend. Post-order DFS gives this information naturally because both children are solved before their parent. The invariant is: each Dfs return value is the longest downward same-value path in edges starting at that node, and maxLen is the largest complete same-value path among all nodes processed so far. A node may combine matching left and right paths for the global answer, but it returns only the longer one-sided path upward.

Code
using System;

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

    public TreeNode(int val = 0, TreeNode? left = null, TreeNode? right = null)
    {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

public sealed class Solution
{
    // Stores the best complete same-value path found anywhere in the tree.
    // The path length is measured in edges.
    private int maxLen;

    public int LongestSameValuePath(TreeNode? root)
    {
        // Reset the state so this Solution object can be reused safely.
        maxLen = 0;

        // Post-order DFS computes the extendable path returned by each node.
        Dfs(root);

        // The global maximum is the required longest same-value path length.
        return maxLen;
    }

    private int Dfs(TreeNode? node)
    {
        // A missing node contributes no downward edges.
        if (node is null)
        {
            return 0;
        }

        // Process both children first because the current node needs their results.
        int leftLen = Dfs(node.left);
        int rightLen = Dfs(node.right);

        // Extend through the left edge only when the child value matches.
        // Otherwise a same-value path cannot continue on this side.
        if (node.left is not null && node.left.val == node.val)
        {
            leftLen += 1;
        }
        else
        {
            leftLen = 0;
        }

        // Apply the same matching rule to the right child.
        if (node.right is not null && node.right.val == node.val)
        {
            rightLen += 1;
        }
        else
        {
            rightLen = 0;
        }

        // A complete path through this node can use both matching sides.
        // leftLen and rightLen already count edges, so no extra 1 is added.
        maxLen = Math.Max(maxLen, leftLen + rightLen);

        // The parent can extend only one downward branch.
        return Math.Max(leftLen, rightLen);
    }
}

public static class Program
{
    public static void Main()
    {
        // Build the same example tree shown in the approved diagram.
        //
        //             1
        //           /   \
        //          1     2
        //         / \     \
        //        1   3     2
        //       / \       /
        //      1   1     2
        TreeNode root = new TreeNode(
            1, new TreeNode(1, new TreeNode(1, new TreeNode(1), new TreeNode(1)), new TreeNode(3)),
            new TreeNode(2, null, new TreeNode(2, new TreeNode(2), null)));

        Solution solution = new Solution();

        // The longest same-value path is a chain of 1s containing 3 edges.
        int result = solution.LongestSameValuePath(root);
        Console.WriteLine(result); // 3
    }
}
Time & Space Complexity

Time is O(n), where n is the number of nodes. Each node is visited once, and the work done at each node is constant. Auxiliary space is O(h), where h is the tree height. This extra memory comes from the recursion stack. In a balanced tree, h is O(log n). In a completely skewed tree, h can be O(n). The algorithm does not use an extra map, set, queue, or other collection.

Where it is used

This post-order tree pattern is useful when each parent needs a small summary from its children and combines those summaries into a larger answer. Similar ideas are used for tree diameter, longest constrained paths, subtree heights, and other tree calculations where the best complete answer may use two child branches but only one branch can continue upward.

Why Interviewers Ask This

This problem tests whether you can define a precise recursive return value and separate local information from a global answer. It checks your understanding of post-order traversal, tree path reasoning, and when two child results may be combined. It also tests whether you stop paths when values differ, count edges correctly, handle recursion base cases, write correct C#, maintain a clear invariant, and explain the O(n) time and O(h) recursion-space costs accurately.

Common interview mistakes

1. Counting nodes instead of edges. A single node has length 0, and the diagram's final result is 3 edges. 2. Adding an extra 1 when updating the global maximum. The correct formula is leftLen + rightLen because both values already count connecting edges. 3. Extending through a child whose value does not match the current node. That side must become 0. 4. Returning leftLen + rightLen to the parent. The parent can continue through only one branch, so Dfs returns Math.Max(leftLen, rightLen). 5. Forgetting that the best path may be completely inside a subtree. A global maximum is needed because the answer does not have to pass through the root. 6. Claiming O(1) auxiliary space and forgetting the O(h) recursion stack.

Interview tip

State the recursive contract before writing code: "Dfs returns the longest downward same-value path from this node in edges." Then separate the two jobs clearly: use leftLen + rightLen to update the global answer, but return only Math.Max(leftLen, rightLen) to the parent.

Interviewer may ask next
What changes if the interviewer asks you to return the actual longest same-value path instead of only its length?

The same post-order idea can be kept, but each recursive result must carry enough information to identify its downward chain, such as its length and endpoint. Whenever leftLen + rightLen produces a new global maximum, we also record the current node and the endpoints of the two matching branches. After DFS, those recorded references let us build the actual path. Every node is still processed once, so time remains O(n). The recursion stack remains O(h), and the returned path itself needs O(k) output space for k nodes. The tradeoff is extra bookkeeping for node references.

What happens if the tree is extremely skewed and very deep?

The algorithm is still correct and still takes O(n) time because each node is visited once. The problem is recursion depth. In a skewed tree, the height h can equal n, so the call stack can use O(n) space and may overflow for a very deep tree. The recursive version matches the approved diagram and is simple for interviews. If extremely deep input must be supported, the same post-order processing can be implemented with an explicit stack. It still uses O(h) auxiliary space, but it avoids relying on the process call stack.

5. Empty NeighborhoodsCodingEasyGoogle

Question Details

Work from the reported Google prompt and explain how you would interpret the neighborhood model, what output is expected, and how you would cover empty or isolated regions.

Short Interview Answer (30-60 seconds)

I would first state my interpretation: '.' is an empty cell, 'W' is blocked, and a neighborhood is a maximal group of empty cells connected up, down, left, or right. I scan the grid in row-major order. When I find an unvisited empty cell, I increment the count and run DFS to mark that entire component visited. This counts every neighborhood exactly once. The time complexity is O(m × n), and the auxiliary space is O(m × n).

Detailed Explanation

See the Code while reading this explanation.

The reported prompt does not fully define what a neighborhood means, so I would first make the model explicit. In the approved diagram, '.' means an empty location and 'W' means a blocked location. Empty cells belong to the same neighborhood only when they connect through a shared side. The goal is to count the separate empty regions. I scan every cell. When I reach an empty cell that has not been visited, I start DFS, mark the whole connected region, and add one to the count.

Useful Questions to Ask the Interviewer
  1. Should neighborhoods use only up, down, left, and right connections, or should diagonal cells also connect?
  2. Which value represents an empty location and which value represents a blocked location?
  3. What should be returned for an empty grid?
Empty Neighborhoods diagram
How to Explain It in an Interview
1. Understand the input and required output

I use the interpretation shown in the diagram. The input is a rectangular character grid. A '.' cell is empty. A 'W' cell is blocked. Two empty cells belong to the same neighborhood when a path of empty cells connects them using only up, down, left, and right moves. The output is one integer: the number of separate empty neighborhoods.

The verified example has 5 rows and 6 columns: Row 0: W W . . W . Row 1: W . W . W . Row 2: . . . W . W Row 3: W . W W . . Row 4: . . . . W W

The expected output is 4.

2. Choose DFS and a visited matrix

I use depth-first search, or DFS. DFS starts from one empty cell and visits every empty cell connected to it through the four allowed directions.

I also use a boolean visited matrix with the same dimensions as the grid. visited[r, c] tells me whether that cell has already been assigned to a neighborhood.

The main invariant is: every visited empty cell already belongs to a neighborhood that has been counted. Therefore, when the outer scan reaches an empty cell that is still unvisited, that cell must start a new neighborhood.

3. Initialize and scan the grid

The grid has m rows and n columns. I create visited[m, n], with every value initially false. I set count to 0.

I scan the grid in row-major order. That means I process row 0 from left to right, then row 1, and continue until the last row.

For each cell, I check whether it contains '.' and whether visited[r, c] is false. If either condition is false, I continue. If both are true, I increment count and run DFS from that cell.

4. Walk through the verified example

The first unvisited empty cell is at (0,2). This starts component 1. DFS marks exactly (0,2), (0,3), and (1,3). Component 1 has 3 cells.

The next unvisited empty cell in row-major order is (0,5). This starts component 2. DFS marks (0,5) and (1,5). Component 2 has 2 cells.

The next unvisited empty cell is (1,1). This starts component 3. DFS marks (1,1), (2,0), (2,1), (2,2), (3,1), (4,0), (4,1), (4,2), and (4,3). Component 3 has 9 cells.

The next unvisited empty cell is (2,4). This starts component 4. DFS marks (2,4), (3,4), and (3,5). Component 4 has 3 cells.

After that, all remaining empty cells have already been visited. The component sizes are {1:3, 2:2, 3:9, 4:3}. The final count is 4, so the algorithm returns 4.

5. Explain why the result is correct

Under the interpretation above, each DFS starts from an empty cell that has not been assigned to an earlier component. DFS follows every valid four-directional empty connection, so it marks exactly one maximal connected neighborhood.

When that DFS finishes, every cell in that neighborhood is marked visited. The outer scan cannot count the same neighborhood again. Every separate neighborhood starts exactly one DFS, so the final count equals the number of neighborhoods.

6. Explain the C# implementation

The code creates a visited matrix and two direction arrays. dr stores row changes and dc stores column changes for up, down, left, and right.

The nested loops scan every cell in row-major order. If the cell is blocked or already visited, the code skips it. Otherwise, it increments count and calls Dfs.

Dfs marks the current cell visited immediately. It then checks the four neighboring coordinates. A neighbor is visited recursively only if it is inside the grid, contains '.', and has not already been visited.

After every cell has been processed, the function returns count. For the diagram's example, Main prints 4.

7. Explain complexity and edge cases

There are m × n cells. The outer loops inspect the grid, and each empty cell is visited by DFS at most once. Therefore, the time complexity is O(m × n).

The visited matrix needs O(m × n) extra memory. Recursive DFS can also use O(m × n) call-stack space in the worst case when one neighborhood contains most of the grid.

If there are no empty cells, the result is

  1. If a non-empty grid contains only empty cells, the result is
  2. Multiple isolated empty cells each count as separate neighborhoods. A single row or a single column works the same way. For a very large grid, iterative DFS or BFS can avoid recursion-stack overflow.
Key Insight / Why This Solution Works

Treat the empty cells as connected components. Scan the grid in row-major order. When the scan finds a '.' cell that has not been visited, increment the neighborhood count once and run DFS from that cell. DFS marks every four-directionally connected empty cell in the same component before the outer scan continues. The central invariant is that every visited empty cell already belongs to a neighborhood that has been counted. Because the entire component is marked before scanning continues, the same neighborhood cannot be counted twice.

Code
using System;

public static class Program
{
    public static void Main()
    {
        // Use the exact 5 x 6 example shown in the approved diagram.
        char[][] grid = { new[] { 'W', 'W', '.', '.', 'W', '.' },
                          new[] { 'W', '.', 'W', '.', 'W', '.' },
                          new[] { '.', '.', '.', 'W', '.', 'W' },
                          new[] { 'W', '.', 'W', 'W', '.', '.' },
                          new[] { '.', '.', '.', '.', 'W', 'W' } };

        // Count the maximal four-directionally connected regions of '.'.
        int result = CountEmptyNeighborhoods(grid);

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

    public static int CountEmptyNeighborhoods(char[][] grid)
    {
        // A null or empty grid contains no empty neighborhoods.
        if (grid == null || grid.Length == 0)
        {
            return 0;
        }

        int rows = grid.Length;
        int cols = grid[0].Length;

        // visited[r, c] records whether an empty cell has already
        // been assigned to a component by an earlier DFS traversal.
        bool[,] visited = new bool[rows, cols];
        int count = 0;

        // These paired offsets represent up, down, left, and right.
        int[] dr = { -1, 1, 0, 0 };
        int[] dc = { 0, 0, -1, 1 };

        // Scan every cell in row-major order, matching the diagram.
        for (int r = 0; r < rows; r++)
        {
            for (int c = 0; c < cols; c++)
            {
                // A blocked cell or a cell already reached by DFS
                // cannot start a new neighborhood.
                if (grid[r][c] != '.' || visited[r, c])
                {
                    continue;
                }

                // Each unvisited empty cell found here starts exactly
                // one new connected component.
                count++;

                // Mark the complete component before the outer scan
                // continues, which prevents counting it again.
                Dfs(grid, visited, r, c, dr, dc);
            }
        }

        // Each DFS start corresponds to exactly one neighborhood.
        return count;
    }

    private static void Dfs(char[][] grid, bool[,] visited, int r, int c, int[] dr, int[] dc)
    {
        // Mark the cell immediately so another recursive path cannot
        // revisit the same cell within this component.
        visited[r, c] = true;

        // Explore exactly the four allowed neighboring positions.
        for (int k = 0; k < 4; k++)
        {
            int nr = r + dr[k];
            int nc = c + dc[k];

            // Recurse only when the neighbor is inside the grid,
            // is empty, and has not already been visited.
            if (nr >= 0 && nr < grid.Length && nc >= 0 && nc < grid[0].Length &&
                grid[nr][nc] == '.' && !visited[nr, nc])
            {
                // Continue exploring the same connected neighborhood.
                Dfs(grid, visited, nr, nc, dr, dc);
            }
        }
    }
}
Time & Space Complexity

Let m be the number of rows and n be the number of columns. The algorithm takes O(m × n) time. The outer loops inspect the cells, and each empty cell is visited by DFS at most once. The visited boolean matrix uses O(m × n) extra memory. Recursive DFS can also use O(m × n) call-stack space in the worst case if one connected empty region contains most of the grid.

Where it is used

This connected-component pattern is useful when software needs to count separate regions in grid-shaped data. Examples include finding disconnected areas on a map, grouping open cells in a game board, identifying separate regions in image data, or detecting isolated zones in a floor plan. The same DFS pattern works whenever neighboring items form groups through explicit connections.

Why Interviewers Ask This

This problem tests whether a candidate can turn an ambiguous description into a precise model, recognize a connected-components problem, and implement DFS correctly in C#. It checks row and column handling, boundary conditions, visited-state management, recursion, and reasoning about why a component is counted exactly once. It also tests whether the candidate can explain O(m × n) time correctly and include both the visited matrix and recursion stack when discussing extra memory.

Common interview mistakes
  1. Counting every empty cell instead of counting connected components.
  2. Treating diagonal cells as connected even though this interpretation allows only up, down, left, and right.
  3. Forgetting the visited matrix, which can cause repeated visits or recursive cycles.
  4. Marking a cell visited too late instead of marking it when DFS enters that cell.
  5. Claiming O(1) extra space and forgetting both the O(m × n) visited matrix and the possible O(m × n) recursion stack.
Interview tip

State the neighborhood interpretation before writing code because the reported prompt is ambiguous. Then explain the invariant in one sentence: every unvisited empty cell found by the outer scan starts exactly one new component, and DFS marks that whole component so it cannot be counted again.

Interviewer may ask next
What changes if diagonal empty cells should also belong to the same neighborhood?

The overall approach stays the same. I still scan the grid and start DFS from every unvisited empty cell. The only algorithm change is the neighbor set. DFS would check eight directions instead of four, adding the four diagonal positions. Correctness is preserved because each DFS still visits exactly one complete component under the new connectivity rule. The time complexity remains O(m × n), and the auxiliary space remains O(m × n). The tradeoff is a slightly larger constant amount of neighbor checking per cell.

How would you handle a very large grid where recursive DFS could overflow the call stack?

I would keep the same connected-component logic but replace recursive DFS with iterative DFS using an explicit stack. When a new unvisited empty cell is found, I increment count, mark that cell visited, and push it. I repeatedly pop a cell and push each valid unvisited empty neighbor, marking each neighbor visited before pushing it. This still explores exactly one component at a time. The time complexity stays O(m × n), and auxiliary space stays O(m × n). The tradeoff is slightly more code, but it avoids recursion-stack overflow.

6. String ShiftCodingEasyGoogle

Question Details

Interpret the string-shift operation precisely, including left versus right movement, wraparound behavior, and what the answer should look like for large shifts.

Short Interview Answer (30-60 seconds)

I would combine all shift operations into one net rotation instead of changing the string after every operation. I treat a left shift as negative movement and a right shift as positive movement. After processing all operations, I normalize the total with modulo by the string length. Then I rotate the string once by splitting it and joining the suffix before the prefix. This takes O(m + n) time and O(n) auxiliary space for the returned string.

Detailed Explanation

See the Code while reading this explanation.

The input is a string and a list of shift operations. Each operation has a direction and an amount. Direction 0 moves characters left. Direction 1 moves them right. Characters that pass one end wrap around to the other end. Large shifts can wrap around many times. Instead of changing the string after every operation, I combine all movements into one total shift. I then reduce that total using the string length and rotate the string only once.

Useful Questions to Ask the Interviewer
  1. Can the string be empty or null?
  2. Can a shift amount be larger than the string length?
  3. Are shift directions always represented by 0 for left and 1 for right?
String Shift diagram
How to Explain It in an Interview
1. Understand the input and required output

The input string in the diagram is "abcdef". The shift operations are [[0, 8], [1, 2], [0, 1]]. A direction of 0 means shift left. A direction of 1 means shift right. When a character moves past one end, it wraps around to the other end. The required final string for this example is "bcdefa".

A large shift can contain complete wraps. The string length is 6, so shifting left by 8 has the same final effect as shifting left by 2 because shifting by 6 returns every character to its starting position.

2. Choose the algorithm

The key idea is to combine all rotations first and rotate the string once. I use one integer named netRight. Positive values represent movement to the right. Negative values represent movement to the left.

For each operation, a left shift subtracts its amount from netRight. A right shift adds its amount. After all operations are processed, modulo by the string length removes complete wraps.

The invariant is: after processing the first i operations, netRight equals the effective signed right rotation of those processed operations.

3. Initialize and process the shifts

The string length is n = 6, and netRight starts at 0.

For [0, 8], the direction is left, so I subtract 8. The state changes from netRight = 0 to netRight = -8.

For [1, 2], the direction is right, so I add 2. The state changes from netRight = -8 to netRight = -6.

For [0, 1], the direction is left, so I subtract 1. The state changes from netRight = -6 to netRight = -7.

All three shift operations have now been processed.

4. Normalize the movement and build the result

The raw total is netRight = -7. I convert it to a non-negative right shift with k = ((netRight % n) + n) % n.

For this example, k = ((-7 % 6) + 6) % 6 = 5. A right shift of 5 on a string of length 6 is equivalent to a left shift of 1.

I calculate split = n - k = 1. The prefix is "a" and the suffix is "bcdef". Returning suffix + prefix gives "bcdefa".

This matches the direct sequence in the diagram: "abcdef" shifted left by 8 becomes "cdefab", shifted right by 2 becomes "abcdef", and shifted left by 1 becomes "bcdefa".

5. Explain why the result is correct

Rotations on a circular string combine by addition. Left movement is represented by a negative amount and right movement by a positive amount. Adding these signed movements preserves the final position of every character. Taking modulo n removes complete rotations without changing the final circular arrangement. Therefore, one final rotation produces the same result as applying all original operations in order.

6. Explain the C# implementation and complexity

The C# method first handles a null or empty string defensively. It then accumulates every shift into netRight. The expression ((netRight % n) + n) % n safely converts a negative total into a valid right-shift amount from 0 through n - 1. If k is 0, the original string is already the answer. Otherwise, the code splits at n - k and returns the suffix followed by the prefix.

If m is the number of shift operations and n is the string length, the time complexity is O(m + n). Processing the operations costs O(m), and building the final rotated string costs O(n). Auxiliary space is O(n) for the strings created to produce the returned result.

Key Insight / Why This Solution Works

The key insight is that every operation is a rotation of the same circular string, so all shifts can be combined before rebuilding the string. Store one signed total called netRight. Subtract left-shift amounts and add right-shift amounts. After all operations, normalize the total with ((netRight % n) + n) % n. This removes complete wraps and produces one right-shift amount from 0 through n - 1. The central invariant is that after processing the first i operations, netRight represents the effective signed right rotation of those processed operations. Finally, split at n - k and return suffix + prefix. This performs only one final rotation instead of rebuilding the string after every operation.

Code
using System;

public class Solution
{
    public string StringShift(string s, int[][] shift)
    {
        // Handle a null or empty string defensively because there is nothing to rotate.
        if (string.IsNullOrEmpty(s))
        {
            return s;
        }

        // Store the string length and one signed total for all shift operations.
        int n = s.Length;
        int netRight = 0;

        // Combine every shift into one net rotation instead of rebuilding the string each time.
        foreach (int[] operation in shift)
        {
            int direction = operation[0];
            int amount = operation[1];

            // A left shift contributes negative movement to the signed right-shift total.
            if (direction == 0)
            {
                netRight -= amount;
            }
            else
            {
                // A right shift contributes positive movement to the signed total.
                netRight += amount;
            }
        }

        // Normalize the signed total to a right shift from 0 through n - 1.
        // C# can produce a negative remainder, so add n before taking modulo again.
        int k = ((netRight % n) + n) % n;

        // A zero normalized shift means complete wraps canceled all movement.
        if (k == 0)
        {
            return s;
        }

        // For a right shift by k, split before the final k characters.
        int split = n - k;
        string suffix = s.Substring(split);
        string prefix = s.Substring(0, split);

        // Move the suffix to the front to perform exactly one final rotation.
        return suffix + prefix;
    }
}

public static class Program
{
    public static void Main()
    {
        // Run the exact example shown in the approved diagram.
        string s = "abcdef";
        int[][] shift = { new int[] { 0, 8 }, new int[] { 1, 2 }, new int[] { 0, 1 } };

        Solution solution = new Solution();
        string result = solution.StringShift(s, shift);

        // Expected output: bcdefa
        Console.WriteLine(result);
    }
}
Time & Space Complexity

Let m be the number of shift operations and n be the string length. Processing the m shift operations takes O(m) time. Building the final rotated string takes O(n) time. The total time is O(m + n). The code also creates string data whose total size grows with n while building the answer, so auxiliary space is O(n). The integer variables such as n, netRight, k, and split use only constant-size extra memory.

Where it is used

This pattern is useful when many operations describe circular movement but only the final state matters. Examples include rotating text, cyclic sequences, circular buffers, and wraparound schedules. Combining the movements first avoids rebuilding the same data after every small rotation.

Why Interviewers Ask This

This question checks whether you understand circular string movement and modulo arithmetic. It also tests whether you recognize that many rotations can be combined into one net rotation instead of repeatedly rebuilding the string. The interviewer can evaluate whether you handle left versus right movement correctly, normalize negative values safely in C#, reason about large shifts and wraparound, maintain a useful invariant, write correct string manipulation code, and explain the O(m + n) complexity accurately.

Common interview mistakes

A common mistake is rotating the whole string after every operation, which can repeat O(n) work for each shift. Another mistake is reversing the direction signs. In this solution, left subtracts and right adds. Candidates may also use netRight % n directly and forget that C# can produce a negative remainder, so the correct normalization is ((netRight % n) + n) % n. Another mistake is splitting at k instead of n - k for the final right rotation. Large shift amounts must also wrap around instead of being treated as invalid.

Interview tip

State the signed-rotation rule before coding: left subtracts, right adds, then normalize once with modulo n. Walk through netRight as 0, -8, -6, -7, and then k = 5. This makes the final split at index 1 and the result "bcdefa" easy to justify.

Interviewer may ask next
What happens if the total shift is zero or a multiple of the string length?

After normalization, k becomes 0. Every character is already in its original position, so the method returns s unchanged. The same algorithm still applies. It takes O(m) time to combine the operations in this case, and the final string rotation is skipped.

Why not apply each shift directly to the string as it appears?

Applying every shift immediately can rebuild or copy up to n characters for each of the m operations, so it can take O(m * n) time. Combining the shifts first is correct because circular rotations compose by addition. The shown method reduces all operations to one normalized rotation, giving O(m + n) time and O(n) auxiliary space. The tradeoff is that we first compute a signed total and normalize it before building the final string.

7. Top 3 UsersCodingEasyGoogle

Question Details

Use the reported ranking-style prompt and explain how to compute the top three users, how ties should be handled, and what data you would retain if the input is large.

Short Interview Answer (30-60 seconds)

I would process the events and keep one running total for each user in a dictionary. After aggregation, I would sort users by total score descending and then by UserId ascending so ties are deterministic. I would take the first three users, or all users if fewer than three exist. For large input, I keep only the per-user totals instead of the raw events. The expected time is O(n + u log u), and the auxiliary space is O(u).

Detailed Explanation

See the Code while reading this explanation.

The input is a sequence of events. Each event has a UserId and a Score. The same user can appear more than once. We add all scores for each user, then rank users by their final total. A larger total comes first. If two totals are equal, the smaller UserId comes first. We return up to three users. For large input, we can read events one at a time and keep only each user's running total.

Useful Questions to Ask the Interviewer
  1. If two users have the same total score, should UserId ascending be the tie-breaker?
  2. If fewer than three unique users exist, should I return every available user?
  3. Can the input be processed as a stream so I do not need to keep every event in memory?
Top 3 Users diagram
How to Explain It in an Interview
1. Understand the input and required output

Each input item contains a UserId and a Score. A user can have several events. We need the three users with the highest total scores. The output contains UserId and TotalScore. Higher totals rank first. For equal totals, UserId ascending decides the order. If fewer than three unique users exist, we return all of them.

2. Aggregate each user's total

I create a dictionary named totals. Its key is UserId. Its value is that user's running total score. I process the events in input order. When a user appears again, I add the new score to the existing total. This means I do not need to keep the complete event history.

3. Walk through the diagram example

The events are (u1,10), (u2,15), (u1,7), (u3,20), (u2,5), (u4,20), and (u3,8). After the first event, totals is {u1:10}. After u2 scores 15, it is {u1:10, u2:15}. The next u1 event changes u1 to 17. Then u3 becomes 20. The next u2 event changes u2 to 20. Then u4 becomes 20. Finally, u3 receives 8 more points and becomes 28. The final totals are u1=17, u2=20, u3=28, and u4=20.

4. Sort and take the top three

I sort the aggregated users by TotalScore descending. For equal scores, I sort by UserId ascending. The complete order is (u3,28), (u2,20), (u4,20), (u1,17). Taking the first three gives (u3,28), (u2,20), and (u4,20). The tie between u2 and u4 is resolved by UserId ascending.

5. Explain why the result is correct

The dictionary always contains the exact sum of all processed scores for each user. After all events are processed, it contains the final score for every unique user. Sorting these totals by the required ranking rules puts the users in the correct order. Taking the first three therefore returns the correct top three users.

6. Explain the C# implementation

The code uses Dictionary<string, long> for the running totals. It processes each event once and updates that user's value. Then LINQ orders the dictionary by total descending and UserId ascending. Take(3) returns up to three entries. Because Take does not require three items to exist, fewer than three users are handled naturally. The example prints u3 28, u2 20, and u4 20.

7. Explain complexity and edge cases

Let n be the number of input events and u be the number of unique users. Aggregating takes O(n) expected time because dictionary lookup and update are O(1) on average. Sorting u users costs O(u log u). Total expected time is O(n + u log u). The dictionary and ordered result require O(u) auxiliary space. Empty input returns an empty result. Fewer than three users returns all available users. Negative scores still work because ranking uses the final totals.

Key Insight / Why This Solution Works

The key idea is to separate aggregation from ranking. First, use a dictionary that maps UserId to that user's running TotalScore. The invariant is: after processing any prefix of the input, totals[userId] equals the sum of all scores seen so far for that user. After every event has been processed, the dictionary contains each user's exact final total. We then sort those totals by score descending and UserId ascending. Taking the first three entries gives the required result. This also fits large input because the raw event history does not need to stay in memory. Only O(u) aggregated user totals are retained.

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

public static class Program
{
    public static void Main()
    {
        // Use the exact event sequence shown in the diagram.
        (string UserId, int Score)[] events = { ("u1", 10), ("u2", 15), ("u1", 7), ("u3", 20),
                                                ("u2", 5),  ("u4", 20), ("u3", 8) };

        // Run the aggregation-and-sort solution on the diagram example.
        List<(string UserId, long TotalScore)> result = GetTop3Users(events);

        // Print the final ranked users from the diagram.
        foreach ((string userId, long totalScore) in result)
        {
            Console.WriteLine($"{userId} {totalScore}");
        }
    }

    public static List<(string UserId, long TotalScore)> GetTop3Users(
        IEnumerable<(string UserId, int Score)> events)
    {
        // Store one running total per user so raw events do not need to be retained.
        Dictionary<string, long> totals = new Dictionary<string, long>();

        // Process each event once and update that user's accumulated score.
        foreach ((string userId, int score) in events)
        {
            // Read the current total when this user has appeared before.
            if (totals.TryGetValue(userId, out long currentTotal))
            {
                // Add the new score to the user's previous total.
                totals[userId] = currentTotal + score;
            }
            else
            {
                // The first event for a user starts that user's running total.
                totals[userId] = score;
            }
        }

        // Rank larger totals first, then use UserId ascending to resolve equal totals.
        // Take returns up to three items, so fewer than three users are handled naturally.
        return totals.OrderByDescending(entry => entry.Value)
            .ThenBy(entry => entry.Key, StringComparer.Ordinal)
            .Take(3)
            .Select(entry => (UserId: entry.Key, TotalScore: entry.Value))
            .ToList();
    }
}
Time & Space Complexity

Let n be the number of events and u be the number of unique users. We process every event once. Dictionary lookup and update are O(1) on average, so aggregation takes O(n) expected time. We then sort u aggregated users, which takes O(u log u) time. The total expected time is O(n + u log u). The dictionary stores one total for each unique user, and the ordered result may hold up to u users during sorting, so the auxiliary space is O(u).

Where it is used

This pattern is useful for leaderboards, activity rankings, sales rankings, and usage reports where many records belong to the same entity. The raw records can be processed as a stream while the program keeps one accumulated value per user. After aggregation, the users are ranked using the required tie-break rule.

Why Interviewers Ask This

This question checks whether you can turn repeated records into aggregated state and then apply a precise ranking rule. The interviewer can see whether you choose an appropriate dictionary, handle repeated users correctly, define tie behavior clearly, reason about large inputs, and write deterministic C# sorting code. It also tests whether you distinguish the O(n) aggregation step from the O(u log u) sorting step and explain memory usage accurately.

Common interview mistakes

A common mistake is ranking individual events instead of first adding all scores for the same user. Another mistake is sorting equal scores without the UserId tie-breaker, which can make the order nondeterministic or incorrect for the stated rule. Candidates may also sort scores ascending instead of descending. For large input, keeping every raw event wastes memory when only the running total for each user is needed. Finally, claiming O(n) total time is incorrect for this implementation because sorting the u unique users costs O(u log u).

Interview tip

State the ranking rule before writing the sort: total score descending, then UserId ascending. Then explain that the dictionary removes the need to keep the raw event history and that the final sort is why the total complexity includes O(u log u).

Interviewer may ask next
How would you handle the input if it were too large to keep all events in memory?

The aggregation logic already supports streaming. I would read one event at a time and update Dictionary<UserId, TotalScore>. I would not retain the raw events. After the stream ends, I would sort the u aggregated users and take the first three. Correctness is unchanged because the dictionary still stores the exact accumulated total for every processed user. Expected time remains O(n + u log u), and auxiliary space remains O(u). The tradeoff is that memory still grows with the number of unique users.

Can you reduce the ranking work when there are a very large number of unique users?

Yes. After building the same per-user totals, I could maintain a top-three structure instead of sorting every user. Because k is fixed at 3, each aggregated user can be compared against a constant-size structure using the same score-descending and UserId-ascending ranking rule. This changes the ranking phase from O(u log u) to O(u), so the expected total time becomes O(n + u). Auxiliary space is still O(u) because the dictionary must retain one total per unique user. The tradeoff is more complicated top-three maintenance logic.

8. Find the First Non-Repeating Character in a StringCodingEasyGoogle

Question Details

Return the first character that appears exactly once, and clarify the expected output when no unique character exists.

Short Interview Answer (30-60 seconds)

I would use a Dictionary<char, int> to count how many times each character appears. First, I scan the string from left to right and build the frequency map. Then I scan the string again in the original order. The first character whose count is exactly 1 is the answer, so I return it immediately. For "swiss", the first unique character is 'w'. This takes O(n) expected time and O(n) auxiliary space in the worst case.

Detailed Explanation

See the Code while reading this explanation.

The input is a string. We need to return the first character that appears exactly once. The word "first" means the earliest such character when we read from left to right. If no character appears exactly once, this solution returns '\0'. The diagram uses two passes. The first pass counts every character. The second pass keeps the original order and finds the first count of 1. For the example "swiss", the answer is 'w'.

Useful Questions to Ask the Interviewer
  1. Should uppercase and lowercase letters be treated as different characters?
  2. What should I return when there is no unique character? In this solution, I will return '\0'.
Find the First Non-Repeating Character in a String diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is one string. We return a character, not an index. We need the first character whose total frequency is exactly 1. If no such character exists, we return '\0'. The diagram treats character comparison as case-sensitive.

2. Choose the algorithm and data structure

I use a Dictionary<char, int>. Each key is a character. Its value is the number of times that character appears in the string. The important idea is to count first and then check characters in their original order. Counting tells us which characters are unique. The second pass tells us which unique character comes first.

3. Build the frequency map

Start with an empty dictionary. Process "swiss" from left to right. At index 0, read 's', so the map becomes { s: 1 }. At index 1, read 'w', so it becomes { s: 1, w: 1 }. At index 2, read 'i', so it becomes { s: 1, w: 1, i: 1 }. At index 3, read 's', so s becomes

  1. At index 4, read 's' again, so s becomes
  2. The completed map is { s: 3, w: 1, i: 1 }.
4. Find the first non-repeating character

Now scan "swiss" again from left to right. At index 0, 's' has frequency 3, so it is not unique. Continue. At index 1, 'w' has frequency 1. That satisfies the condition, so return 'w' immediately. No later character needs to be checked.

5. Explain why the result is correct

After the first pass, the dictionary contains the exact frequency of every character in the string. During the second pass, we visit characters in their original left-to-right order. Therefore, the first character we see with frequency 1 must be the first non-repeating character. For "swiss", that character is 'w'.

6. Explain the C# implementation

The method first handles a null or empty string by returning '\0'. It then creates a Dictionary<char, int>. The first foreach loop counts characters. The second foreach loop checks those counts in original order. As soon as a count equals 1, the method returns that character. If the second loop finishes without finding one, the method returns '\0'.

7. Explain complexity and edge cases

There are at most two passes over the string. Dictionary lookup and insertion are O(1) on average, so the overall expected time is O(n). The dictionary stores one entry for each distinct character, so it uses O(k) extra memory where k is the number of distinct characters. In the worst case, k can equal n, so the auxiliary space is O(n). Important cases are an empty string, all characters repeating, one unique character, and uppercase versus lowercase characters.

Key Insight / Why This Solution Works

The key insight is to separate two jobs: count every character, then preserve the original order while finding the answer. A Dictionary<char, int> stores character -> frequency. After the first pass, the central invariant is that the dictionary contains the exact frequency of every character in the full string. The second pass reads the original string from left to right. Therefore, the first character whose stored frequency is 1 is exactly the first non-repeating character. This avoids repeatedly rescanning the string to count each candidate character.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Use the exact example shown in the diagram.
        string input = "swiss";
        char result = FirstNonRepeatingChar(input);

        // Print the expected result: w.
        Console.WriteLine(result);
    }

    public static char FirstNonRepeatingChar(string s)
    {
        // Return the designated sentinel when there is no input character to return.
        if (string.IsNullOrEmpty(s))
        {
            return '\0';
        }

        // Store each character as a key and its total frequency as the value.
        Dictionary<char, int> frequency = new Dictionary<char, int>();

        // First pass: build the complete frequency map.
        foreach (char c in s)
        {
            if (frequency.ContainsKey(c))
            {
                // This character was seen before, so increase its stored count.
                frequency[c]++;
            }
            else
            {
                // This is the first occurrence, so start its count at 1.
                frequency[c] = 1;
            }
        }

        // Second pass: preserve the original left-to-right order.
        foreach (char c in s)
        {
            // The first character whose total frequency is 1 is the answer.
            if (frequency[c] == 1)
            {
                return c;
            }
        }

        // No non-repeating character exists.
        return '\0';
    }
}
Time & Space Complexity

Let n be the length of the string and k be the number of distinct characters. The code makes at most two passes through the input. Dictionary lookup and insertion are O(1) on average, so the total expected time is O(n). The dictionary stores one entry for each distinct character, which is O(k) extra memory. Because k can grow to n, the worst-case auxiliary space is O(n), matching the diagram.

Where it is used

This frequency-map pattern is useful when software needs to count items and then make a decision while preserving original order. Examples include character analysis, duplicate detection, validating text input, counting event types, and finding the first value that occurs a specific number of times.

Why Interviewers Ask This

This question checks whether a candidate can recognize a frequency-counting pattern and choose a suitable data structure. It also tests whether the candidate understands why original order matters, can separate counting from selection, handles the no-result case, writes correct C# with Dictionary<char, int>, and explains expected hash-table complexity accurately. The interviewer can also see whether the candidate understands early return during the second pass.

Common interview mistakes

A common mistake is returning any character with frequency 1 instead of the first one in the original string. Another mistake is deciding that a character is unique during the first pass before all frequencies are known. Candidates may also forget the no-result case and fail to return '\0'. Another error is claiming guaranteed O(n) time without noting that Dictionary operations are O(1) on average. Finally, do not accidentally treat uppercase and lowercase characters as the same when using char keys as shown here.

Interview tip

Explain why there are two passes: the first pass learns the final frequency of every character, and the second pass preserves original order so the first character with count 1 can be returned immediately.

Interviewer may ask next
Can we reduce the auxiliary space used by the dictionary?

Yes, but only when the possible character set is known and bounded. For example, with a small fixed alphabet, we could use a fixed-size count array instead of Dictionary<char, int>. We would still count first and scan the string again in original order. The time remains O(n). The auxiliary space becomes O(1) relative to n because the array size is fixed. The tradeoff is that this approach depends on a known limited character set.

How would the solution change if the input arrived as a stream and could not be scanned twice?

The current solution depends on a second pass, so a non-replayable stream needs more state. We can keep a frequency dictionary plus an order-preserving collection of candidates. Each incoming character updates its frequency, and repeated characters are removed or marked invalid as candidates. The expected processing time can remain O(n) with hash-based structures, and the extra space is O(n) in the general case. The tradeoff is more state and more complicated update logic.

9. Find BigramsCodingEasyGoogle

Question Details

Build the bigram extraction logic for the reported Google prompt, including tokenization assumptions and how repeated pairs should be counted or deduplicated.

Short Interview Answer (30-60 seconds)

I would lowercase the text, extract word tokens with the regex [A-Za-z0-9']+, and then process every adjacent pair from left to right. I store each bigram in a Dictionary<string, int>, where the value is its frequency. If the pair is new, I store 1. If it already exists, I increment the count. This correctly counts repeated pairs, and the dictionary keys also give the unique bigrams. The solution takes O(n) expected time and O(u) auxiliary space, which is O(n) in the worst case.

Detailed Explanation

See the Code while reading this explanation.

The input is one text string. We need to find every pair of neighboring words while keeping their original order. We first make the text lowercase and extract words while ignoring punctuation. Then we join each word with the next word to form a bigram. We count how many times each bigram appears. For the example "The cat sat on the cat sat.", the repeated pairs "the cat" and "cat sat" each appear twice. A dictionary fits well because it stores one count for each unique pair.

Useful Questions to Ask the Interviewer
  1. Should matching ignore letter case?
  2. Should punctuation be ignored while preserving the original word order?
  3. Should repeated bigrams be returned with their counts, or should the result contain only unique bigrams?
Find Bigrams diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a text string. The diagram uses "The cat sat on the cat sat." After lowercase tokenization, the tokens are ["the", "cat", "sat", "on", "the", "cat", "sat"]. We must form every adjacent two-word pair in order and count its frequency. The final frequency map is {"the cat":2, "cat sat":2, "sat on":1, "on the":1}. If a deduplicated result is needed, the dictionary keys give the unique bigrams: ["the cat", "cat sat", "sat on", "on the"].

2. Choose the algorithm and data structure

I use a Dictionary<string, int>. Each key is one bigram. Each value is the number of times that bigram has appeared. I process the token list from left to right. At position i, I combine tokens[i] and tokens[i + 1]. The central invariant is: before processing position i, the dictionary stores the exact frequencies for all bigrams from positions 0 through i - 1.

3. Initialize the state

The text is converted to lowercase. Regex [A-Za-z0-9']+ extracts the word tokens while ignoring punctuation and preserving token order. The dictionary starts empty. Traversal starts at i = 0. If there are fewer than two tokens, there is no adjacent pair, so the loop runs zero times and the returned dictionary is empty.

4. Walk through the example

There are 7 tokens, so there are 6 adjacent bigram positions.

Step 1, i = 0: tokens[0] is "the" and tokens[1] is "cat". They form "the cat". The state before is {}. The pair is new, so it is added with count 1. The state becomes {"the cat":1}.

Step 2, i = 1: "cat" and "sat" form "cat sat". The state before is {"the cat":1}. The pair is new, so the state becomes {"the cat":1, "cat sat":1}.

Step 3, i = 2: "sat" and "on" form "sat on". The pair is new. The state becomes {"the cat":1, "cat sat":1, "sat on":1}.

Step 4, i = 3: "on" and "the" form "on the". The pair is new. The state becomes {"the cat":1, "cat sat":1, "sat on":1, "on the":1}.

Step 5, i = 4: "the" and "cat" form "the cat" again. The dictionary already contains this key, so its count changes from 1 to 2. The state becomes {"the cat":2, "cat sat":1, "sat on":1, "on the":1}.

Step 6, i = 5: "cat" and "sat" form "cat sat" again. Its count changes from 1 to 2. The final state is {"the cat":2, "cat sat":2, "sat on":1, "on the":1}.

After step 6, all adjacent positions have been processed. The loop stops because there is no later token available to form another pair.

5. Explain why the result is correct

Every adjacent pair has exactly one starting position. The loop processes every valid starting position exactly once. A new pair is inserted with count 1. A repeated pair updates the same dictionary key by increasing its count. Therefore, after all six positions are processed, each stored value equals the true frequency of that bigram. Dictionary keys are unique, so the same structure also provides the deduplicated bigram set.

6. Explain the C# implementation

GetBigramCounts first converts the text to lowercase and uses Regex.Matches with [A-Za-z0-9']+ to build the token list. It creates an empty Dictionary<string, int>. The loop runs while i < tokens.Count - 1. At every position it builds tokens[i] + " " + tokens[i + 1]. ContainsKey checks whether the bigram already exists. Existing counts are incremented. New bigrams are inserted with count 1. The method returns the frequency map. GetUniqueBigrams reuses GetBigramCounts and returns its keys as a list.

7. Explain complexity and edge cases

Let n be the number of tokens and u be the number of unique bigrams. The loop processes n - 1 adjacent positions. Dictionary lookup and insertion are O(1) on average, so the overall expected time is O(n). The dictionary stores u entries, so its auxiliary space is O(u), which is O(n) in the worst case. Relevant edge cases are fewer than two tokens, repeated pairs, punctuation, and differences in letter case.

Key Insight / Why This Solution Works

The key idea is to count each adjacent pair as it is encountered. After tokenization, position i forms exactly one bigram from tokens[i] and tokens[i + 1]. A Dictionary<string, int> stores bigram -> frequency. If the bigram is new, its count starts at 1. If it is already present, its existing count is incremented.

The invariant is: before processing position i, the dictionary contains the exact frequencies of all bigrams from positions 0 through i - 1. Processing position i updates exactly the bigram that starts there. After the last valid position is processed, the dictionary contains the correct frequency for every bigram. Its unique keys also provide the deduplicated result.

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

public static class Program
{
    public static void Main()
    {
        // Run the exact example shown in the diagram.
        string text = "The cat sat on the cat sat.";

        // Build the bigram frequency map with the diagram's counting algorithm.
        Dictionary<string, int> counts = GetBigramCounts(text);

        // Reuse the same counting logic and take the dictionary keys as unique bigrams.
        List<string> uniqueBigrams = GetUniqueBigrams(text);

        // Display the counted bigrams from the example.
        Console.WriteLine("Frequency map:");
        foreach (KeyValuePair<string, int> entry in counts)
        {
            Console.WriteLine($"{entry.Key} -> {entry.Value}");
        }

        // Display the deduplicated bigram keys.
        Console.WriteLine();
        Console.WriteLine("Unique bigrams:");
        Console.WriteLine($"[\"{string.Join("\", \"", uniqueBigrams)}\"]");
    }

    public static Dictionary<string, int> GetBigramCounts(string text)
    {
        // Normalize the text to lowercase and extract word tokens in their original order.
        // This regex ignores punctuation while keeping letters, digits, and apostrophes.
        List<string> tokens = Regex.Matches(text.ToLowerInvariant(), @"[A-Za-z0-9']+")
                                  .Cast<Match>()
                                  .Select(match => match.Value)
                                  .ToList();

        // Key = bigram text. Value = number of times that bigram has appeared.
        Dictionary<string, int> counts = new Dictionary<string, int>();

        // Process every valid adjacent pair from left to right.
        // With fewer than two tokens, this loop runs zero times and returns an empty map.
        for (int i = 0; i < tokens.Count - 1; i++)
        {
            // Build the bigram starting at the current token position.
            string bigram = tokens[i] + " " + tokens[i + 1];

            if (counts.ContainsKey(bigram))
            {
                // A repeated pair updates the existing frequency instead of adding another key.
                counts[bigram]++;
            }
            else
            {
                // The first occurrence of a pair starts with frequency 1.
                counts[bigram] = 1;
            }
        }

        // The final dictionary contains the exact frequency of every observed bigram.
        return counts;
    }

    public static List<string> GetUniqueBigrams(string text)
    {
        // Dictionary keys are unique, so they provide the deduplicated bigram result.
        return GetBigramCounts(text).Keys.ToList();
    }
}
Time & Space Complexity

Let n be the number of tokens and u be the number of unique bigrams. There are n - 1 adjacent pairs to process. A C# Dictionary lookup or insertion is O(1) on average, so the total expected time is O(n).

The dictionary stores one entry for each unique bigram. That needs O(u) auxiliary space. In the worst case, nearly every adjacent pair is different, so u grows with n and the auxiliary space is O(n).

Where it is used

This pattern is useful in text processing when software needs to count neighboring word pairs. It can be used for phrase-frequency analysis, simple language statistics, text indexing, log analysis, and basic n-gram features. The same dictionary-counting idea is also useful whenever repeated items must be counted while keeping one stored entry for each distinct item.

Why Interviewers Ask This

This problem checks whether a candidate can turn a small text requirement into precise processing rules. The interviewer can evaluate whether the candidate defines tokenization clearly, preserves adjacent order, chooses an appropriate dictionary, handles repeated pairs correctly, and separates frequency counting from deduplication. It also tests C# collection usage, loop boundaries, reasoning about short inputs and punctuation, maintaining a simple invariant, and describing expected-time complexity accurately for a hash-based dictionary.

Common interview mistakes
  1. Splitting only on spaces. This can leave punctuation attached to a word, so "sat." and "sat" become different tokens.
  2. Forgetting to normalize case. Then "The" and "the" can create different bigrams even though the diagram treats them as the same token.
  3. Resetting a repeated bigram to 1 instead of incrementing its existing count.
  4. Building pairs from non-adjacent tokens. A bigram here must use tokens[i] and tokens[i + 1] in the original order.
  5. Claiming guaranteed O(n) time. Dictionary lookup and insertion are O(1) on average, so the correct overall wording is O(n) expected time.
Interview tip

Before coding, state exactly what the dictionary stores: each key is one adjacent bigram and each value is its frequency. Then use the repeated pairs "the cat" and "cat sat" to show why an existing key must be incremented instead of inserted again.

Interviewer may ask next
What would change if the input were too large to keep all tokens in memory?

I would tokenize the input as a stream and keep only the previous token plus the same frequency dictionary. When the next token arrives, I form previous + " " + current, update its count, and then set previous = current. Every adjacent pair is still processed exactly once and in order. Expected time remains O(n). Auxiliary space is O(u) for the dictionary plus O(1) streaming state. The main tradeoff is that the tokenizer and input source must support streaming.

What would change if only unique bigrams were required and frequencies were not needed?

I could use a HashSet<string> instead of Dictionary<string, int>. For each adjacent pair, I would add the bigram to the set. A set stores each bigram only once, so duplicates are removed automatically. The traversal order and adjacency rule stay the same. Expected time remains O(n) because HashSet insertion is O(1) on average. Auxiliary space is O(u), which is O(n) in the worst case. The tradeoff is that frequency counts are no longer available.

10. Sort StringsCodingEasyGoogle

Question Details

Sort the input strings according to the rule implied by the prompt, and explain what comparison key you would use and how you would break ties.

Short Interview Answer (30-60 seconds)

I would sort the strings with two comparison keys. The first key is the string length, so shorter strings come first. If two strings have the same length, I would compare the full strings with StringComparer.Ordinal as the tie-break. In C#, OrderBy handles the length and ThenBy handles the ordinal text comparison. For the example, the result is ["an", "fig", "kiwi", "pear", "plum", "apple"]. The worst-case time is O(n log n * k), with O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is an array of strings. We need to return the same strings in sorted order. Shorter strings must come before longer strings. When two strings have the same length, their text decides which one comes first. The diagram uses case-sensitive ordinal comparison for that tie. The main idea is to use two comparison keys for every string: its length first and the full string second. C# LINQ supports this directly with OrderBy followed by ThenBy, so the implementation is short and matches the required order.

Useful Questions to Ask the Interviewer
  1. Should equal-length strings use case-sensitive ordinal comparison?
  2. Should the method return a new sorted array instead of changing the input array?
  3. Can the input contain duplicate strings or be empty?
Sort Strings diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a string array. The output is also a string array. We return the strings themselves, not their indices. The first rule is shorter length first. If two strings have the same length, we compare their full text with StringComparer.Ordinal. This comparison is case-sensitive and culture-independent.

For the diagram example, the input is ["pear", "fig", "apple", "kiwi", "plum", "an"]. The output is ["an", "fig", "kiwi", "pear", "plum", "apple"].

2. Choose the comparison keys

For every word, I use a two-part comparison key. The first part is the string length. The second part is the string itself, compared with StringComparer.Ordinal.

The example keys are:

  • pear -> (4, "pear")
  • fig -> (3, "fig")
  • apple -> (5, "apple")
  • kiwi -> (4, "kiwi")
  • plum -> (4, "plum")
  • an -> (2, "an")

The central invariant is that length always decides first. Text is compared only when the lengths are equal.

3. Walk through the example

Start with ["pear", "fig", "apple", "kiwi", "plum", "an"]. Their lengths are 4, 3, 5, 4, 4, and 2.

"an" has length 2, so it comes first. "fig" has length 3, so it comes next. "pear", "kiwi", and "plum" all have length 4. Their lengths are tied, so StringComparer.Ordinal compares their text. That gives the order "kiwi", "pear", "plum". Finally, "apple" has length 5, so it comes last.

The ordered keys are (2, "an"), (3, "fig"), (4, "kiwi"), (4, "pear"), (4, "plum"), and (5, "apple"). The final returned array is ["an", "fig", "kiwi", "pear", "plum", "apple"].

4. Explain how the algorithm produces the order

OrderBy(word => word.Length) creates the primary ordering by length. ThenBy(word => word, StringComparer.Ordinal) resolves equal-length ties with ordinal text comparison. ToArray() materializes the ordered sequence into the final string array.

There is no hash map, stack, queue, or custom data structure. The solution uses the built-in LINQ sorting pipeline with two comparison keys.

5. Explain why the result is correct

Every comparison follows the same composite rule: (Length, String). A string with a smaller length must come before a string with a larger length. If the lengths match, ordinal text comparison gives a consistent order inside that equal-length group. Because both required ordering rules are applied in this exact sequence, the returned array satisfies the sorting rule.

6. Explain the C# implementation

The SortStrings method receives the input string array. OrderBy reads word.Length as the primary key. ThenBy uses each word itself as the secondary key with StringComparer.Ordinal. ToArray creates the final returned array. Main runs the exact example from the diagram and prints ["an", "fig", "kiwi", "pear", "plum", "apple"].

7. Explain complexity and edge cases

Let n be the number of strings. Let k represent the cost of comparing string characters when equal-length strings need the tie-break. The worst-case time shown by the diagram is O(n log n * k). The auxiliary space is O(n) for the ordered result and sorting buffers.

An empty array returns an empty array. Duplicate strings remain together naturally. Equal-length strings rely on the ordinal tie-break. Uppercase and lowercase follow case-sensitive ordinal ordering.

Key Insight / Why This Solution Works

Use a two-key sort. The primary key is word.Length. The secondary key is the complete string compared with StringComparer.Ordinal. The central invariant is that every comparison decides by length first, and only equal-length strings use ordinal text order. This exactly matches the required rule. In C#, OrderBy expresses the primary key and ThenBy expresses the tie-break key. For the example's length-4 group, "pear", "kiwi", and "plum" are tied on length, so ordinal comparison orders them as "kiwi", "pear", "plum".

Code
using System;
using System.Linq;

public static class Program
{
    public static void Main()
    {
        // Use the exact input shown in the approved diagram.
        string[] words = { "pear", "fig", "apple", "kiwi", "plum", "an" };

        // Apply the same two-key sorting rule shown in the walkthrough.
        string[] sortedWords = SortStrings(words);

        // Print the returned values so the diagram's final result can be verified.
        Console.WriteLine("[" + string.Join(", ", sortedWords.Select(word => $"\"{word}\"")) + "]");
    }

    public static string[] SortStrings(string[] words)
    {
        // Use string length as the primary key so shorter strings come first.
        // Use ordinal text as the secondary key when two lengths are equal.
        // Materialize the ordered sequence into the final returned array.
        return words.OrderBy(word => word.Length)
            .ThenBy(word => word, StringComparer.Ordinal)
            .ToArray();
    }
}
Time & Space Complexity

Let n be the number of strings. Let k be the cost of comparing string characters when equal-length strings need the secondary comparison. The worst-case time is O(n log n * k). The n log n part comes from sorting. The k part accounts for text comparisons that may inspect several characters. The auxiliary space is O(n) because LINQ's ordering operation uses storage for the ordered data and the final result is materialized into an array. This complexity includes the sorting work shown in the diagram.

Where it is used

This pattern is useful when software needs a deterministic custom sort with a primary field and a tie-break field. Examples include sorting labels by length and then text, ordering records by one main property and then another property, or producing predictable output for reports and tests. In .NET, OrderBy followed by ThenBy is a common way to express this type of multi-key ordering.

Why Interviewers Ask This

This question checks whether you can turn a plain-language ordering rule into precise comparison keys. The interviewer can see whether you understand primary sorting, tie-breaking, and the effect of choosing a specific .NET string comparer. It also tests whether your example, implementation, correctness explanation, and complexity stay consistent. A strong candidate should explain why OrderBy handles the main length rule and ThenBy handles only the equal-length tie.

Common interview mistakes

A common mistake is sorting only by length and forgetting the required tie-break for equal-length strings. Another mistake is using a culture-sensitive string comparison instead of StringComparer.Ordinal. A candidate may also reverse the keys and sort by text before length, which produces a different order. Another mistake is returning indices even though this problem asks for the sorted strings themselves. It is also incorrect to ignore the cost of string comparison when explaining the diagram's O(n log n * k) worst-case time.

Interview tip

Before writing code, state the comparison rule clearly: "I will sort by the pair (Length, String), using StringComparer.Ordinal for the second key." That makes the OrderBy and ThenBy implementation easy to explain and verify.

Interviewer may ask next
What would change if equal-length strings should be compared without considering uppercase and lowercase differences?

The main algorithm would stay the same. I would still use string length as the primary key, but I would replace StringComparer.Ordinal with StringComparer.OrdinalIgnoreCase for the secondary comparison. The correctness rule becomes length first and case-insensitive ordinal text second. The worst-case time remains O(n log n * k), and the auxiliary space remains O(n). The tradeoff is that strings that differ only by letter case can compare as equal under the tie-break.

What would change if equal-length strings should keep their original input order instead of using a text tie-break?

I would remove the ThenBy call and keep only OrderBy(word => word.Length). LINQ OrderBy is stable, so strings with the same length keep their original relative order. The correctness rule changes to shorter strings first and original order inside each equal-length group. The sorting time is O(n log n) based on the length key, and the auxiliary space remains O(n). The tradeoff is that the output no longer uses the deterministic ordinal tie-break shown in the current diagram.

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.