46 Microsoft .NET Developer Interview Questions & Answers

microsoft icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

1. For a given array of positive integers, write a code that computes the sum of the elements.CodingEasyMicrosoft

Question Details

Add all values in a one-dimensional positive-integer array, and cover the expected input shape, zero-length behavior, overflow awareness, and the simplest loop or accumulator choice.

Short Interview Answer (30-60 seconds)

I would use a simple loop with a 64-bit long accumulator. I start sum at 0, then visit each array element from index 0 to the end and add its value to sum. If the array is null, I return 0. An empty array also returns 0 because the loop does not run. This works because every element is added exactly once. The time complexity is O(n), and the auxiliary space complexity is O(1).

Detailed Explanation

See the Code while reading this explanation.

The problem gives us a one-dimensional array of positive integers. We need to add every value and return the total. For the example [3, 7, 2, 9, 4], the result is 25. The simplest solution is to keep one running total. We start it at zero and add each array value in order. A long accumulator reduces the risk of overflowing a 32-bit integer. An empty array naturally returns 0 because no values are added. The shown implementation also returns 0 for a null array.

Useful Questions to Ask the Interviewer
  1. Should a null array return 0, as shown in this solution?
  2. Can the final sum be larger than int.MaxValue, so using long is appropriate?
For a given array of positive integers, write a code that computes the sum of the elements. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a one-dimensional array of positive integers. The output is the sum of all array values. For the example [3, 7, 2, 9, 4], the required result is 25. If the array has length 0, the result is 0. The shown implementation also returns 0 for a null array.

2. Choose the algorithm and state

We only need a running total. No extra collection is required. We use a long variable named sum. The central invariant is: after processing index i, sum equals the total of all elements from index 0 through index i.

3. Initialize the state

We first check whether the array is null. If it is, we return 0. Otherwise, we initialize long sum = 0. This is the correct starting value because no elements have been processed yet.

4. Walk through the example

Start with sum = 0.

At index 0, the value is 3. We calculate 0 + 3 = 3, so sum becomes 3.

At index 1, the value is 7. We calculate 3 + 7 = 10, so sum becomes 10.

At index 2, the value is 2. We calculate 10 + 2 = 12, so sum becomes 12.

At index 3, the value is 9. We calculate 12 + 9 = 21, so sum becomes 21.

At index 4, the value is 4. We calculate 21 + 4 = 25, so sum becomes 25.

The loop then ends because every array element has been processed. We return 25.

5. Explain why the result is correct

Before processing any element, sum is 0. Each loop iteration adds exactly the current array value to the previous total. Therefore, after the last element, sum equals the mathematical sum of all elements. For [3, 7, 2, 9, 4], this is 3 + 7 + 2 + 9 + 4 = 25.

6. Explain the C# implementation

The method receives an int[] and returns a long. It first checks for null input and returns 0. It then creates a long accumulator named sum. The for loop starts at index 0 and continues while i < array.Length. Each iteration adds array[i] to sum. After the loop completes, the method returns sum.

7. Explain complexity and edge cases

The loop visits each of the n elements once, so the time complexity is O(n). Only the loop index and running total are stored, so the auxiliary space complexity is O(1). A zero-length array returns 0 because the loop executes zero times. A null array returns 0 because of the initial check. Using long reduces overflow risk compared with int. If the mathematical total can exceed long.MaxValue, a larger numeric type such as System.Numerics.BigInteger would be needed.

Key Insight / Why This Solution Works

Use a running accumulator. Set sum to 0 and traverse the array from index 0 through array.Length - 1. For each element, add array[i] to sum. The central invariant is that after each iteration, sum equals the total of all values processed so far. No extra data structure is needed because the problem only asks for one total. A long accumulator is used instead of int to reduce the risk of 32-bit integer overflow.

Code
using System;

public static class Program
{
    public static long SumArray(int[] array)
    {
        // Handle a null input defensively by returning the neutral sum value.
        if (array == null)
        {
            return 0L;
        }

        // Start the 64-bit running total at zero to reduce 32-bit overflow risk.
        long sum = 0L;

        // Visit every element from index 0 through the last valid index.
        for (int i = 0; i < array.Length; i++)
        {
            // Add the current array value to the running total.
            sum += array[i];
        }

        // After all elements are processed, return their total.
        return sum;
    }

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

        // Compute the sum with the same accumulator algorithm shown in the diagram.
        long result = SumArray(data);

        // Print the expected result: 25.
        Console.WriteLine(result);
    }
}
Time & Space Complexity

Let n be the number of elements in the array. The algorithm visits each element once, so the time complexity is O(n). It uses only a long variable for the running total and an integer loop index, so the auxiliary space complexity is O(1). The amount of extra memory does not grow with the input size.

Where it is used

This running-total pattern is useful whenever software needs to aggregate numeric values into one result. Examples include totaling transaction amounts, adding quantities in an order, summing measurements, and calculating totals from numeric records when only the final sum is needed.

Why Interviewers Ask This

This question checks whether the candidate can translate a simple requirement into correct C# code. It tests array traversal, loop boundaries, accumulator variables, return values, zero-length behavior, null handling, and overflow awareness. It also shows whether the candidate can explain why every element must be processed, why the time complexity is O(n), and why the algorithm needs only O(1) auxiliary space.

Common interview mistakes

A common mistake is using an int accumulator and forgetting that the total can exceed the 32-bit range. Another mistake is starting the accumulator with the wrong value instead of 0. Candidates may also use an incorrect loop bound such as i <= array.Length, which would access an invalid index. Another mistake is adding the index i instead of the value array[i]. It is also incorrect to claim O(n) auxiliary space because this solution uses only constant extra memory.

Interview tip

Explain the invariant while you code: after each iteration, sum equals the total of every element processed so far. This connects the loop directly to the correctness argument.

Interviewer may ask next
What changes if the total can be larger than long.MaxValue?

The loop structure can stay the same, but the accumulator can use System.Numerics.BigInteger. Each array value is still added in the same order, so the correctness reasoning stays the same. We still process n input elements, but arithmetic on arbitrarily large BigInteger values is not constant-cost because its cost grows with the number of digits. Extra memory also grows with the size of the accumulated number. The tradeoff is a much larger numeric range with higher arithmetic and memory cost.

How would the solution change if the values arrive as a stream instead of an array?

The same running-total approach still works. Start sum at 0 and add each value as it arrives. The invariant remains the same: sum equals the total of all values seen so far. For n streamed values, processing is O(n) time and O(1) auxiliary space when the accumulator has fixed size. The benefit is that the full input does not need to be stored. The tradeoff is that earlier values are unavailable later unless they are stored separately.

2. For two unsorted arrays in ascending order, write a code to merge them such that the new array is in ascending order.CodingEasyMicrosoft

Question Details

Merge two sorted arrays into one sorted sequence, including how you walk both inputs, preserve order, and avoid unnecessary allocations when one side exhausts first.

Short Interview Answer (30-60 seconds)

I would use two pointers, one for each sorted array, and a third position for the result. I compare the current values and copy the smaller one into the result, then move that input pointer forward. When one array is exhausted, I copy the remaining values from the other array without more comparisons. This keeps the result sorted. The time complexity is O(m + n). The result array uses O(m + n) space, with O(1) auxiliary space beyond the output.

Detailed Explanation

See the Code while reading this explanation.

The two input arrays are already in ascending order. We need to combine all their values into one new array that is also in ascending order. We do not need to sort everything again. We start at the first value of each array. We compare those two values and copy the smaller one into the result. Then we move forward only in the array that supplied that value. When one input has no values left, we copy the rest of the other input directly.

Useful Questions to Ask the Interviewer
  1. Can I assume both input arrays are already sorted in ascending order?
  2. Should I return a new array rather than modify either input array?
  3. Should duplicate values be preserved in the merged result?
For two unsorted arrays in ascending order, write a code to merge them such that the new array is in ascending order. diagram
How to Explain It in an Interview
1. Understand the input and required output

The diagram uses A = [1, 4, 7, 9] and B = [2, 3, 6, 8, 10]. Their lengths are m = 4 and n = 5. The result therefore has nine positions. The required result is [1, 2, 3, 4, 6, 7, 8, 9, 10]. Every input element is copied into the output.

2. Choose the two-pointer merge

Use pointer i for A and pointer j for B. Use k for the next free position in the result array. While both arrays still have values, compare A[i] with B[j]. Copy the smaller current value to result[k]. Then move the pointer for the array that supplied that value and move k forward. If the values are equal, the C# code uses A[i] <= B[j], so the value from A is copied first.

3. Initialize the state

Create result with length m + n. Start with i = 0, j = 0, and k = 0. This means both input pointers begin at their first values and the result is empty. The main invariant is that result[0..k-1] is already sorted and contains exactly the values consumed so far from A and B in correct merged order.

4. Walk through the example

At i = 0 and j = 0, compare 1 and 2. Take 1 from A and place it at result[0]. Now i = 1 and k = 1. The result prefix is [1].

Compare 4 and 2. Take 2 from B and place it at result[1]. Now j = 1 and k = 2. The result prefix is [1, 2].

Compare 4 and 3. Take 3 from B and place it at result[2]. Now j = 2 and k = 3. The result prefix is [1, 2, 3].

Compare 4 and 6. Take 4 from A and place it at result[3]. Now i = 2 and k = 4. The result prefix is [1, 2, 3, 4].

Compare 7 and 6. Take 6 from B and place it at result[4]. Now j = 3 and k = 5. The result prefix is [1, 2, 3, 4, 6].

Compare 7 and 8. Take 7 from A and place it at result[5]. Now i = 3 and k = 6. The result prefix is [1, 2, 3, 4, 6, 7].

Compare 9 and 8. Take 8 from B and place it at result[6]. Now j = 4 and k = 7. The result prefix is [1, 2, 3, 4, 6, 7, 8].

Compare 9 and 10. Take 9 from A and place it at result[7]. Now i = 4 and k = 8, so A is exhausted. The main comparison loop stops. The only remaining value is 10 from B, so it is copied directly to result[8]. The final result is [1, 2, 3, 4, 6, 7, 8, 9, 10].

5. Explain why the result is correct

Before each write, A[i] and B[j] are the smallest values not yet copied from their own sorted arrays. Choosing the smaller of those two values therefore gives the smallest value still available overall. This keeps the result sorted. When one array ends, the remaining part of the other array is already sorted and every remaining value belongs after the values already copied, so that suffix can be copied directly.

6. Explain the C# implementation and complexity

The C# method creates one result array of size m + n. The first while loop performs the comparisons and advances i or j. Two final while loops copy any values left in A or B. Each input element is copied exactly once, so the running time is O(m + n). The returned result occupies O(m + n) space. Apart from that required output array, the algorithm uses only the integer variables m, n, i, j, and k, so auxiliary space beyond the output is O(1).

Key Insight / Why This Solution Works

The key insight is that both inputs are already sorted, so their smallest unmerged values are always at the two current pointer positions. Keep i on A and j on B. Compare A[i] and B[j], copy the smaller value to result[k], and move only the pointer that supplied that value. The invariant is that result[0..k-1] is sorted and contains exactly the values consumed so far from A and B in correct merged order. Once one input is exhausted, no more comparisons are needed because the remaining suffix of the other input is already sorted.

Code
using System;

public static class Program
{
    public static void Main()
    {
        // Use the exact example shown in the diagram.
        int[] A = { 1, 4, 7, 9 };
        int[] B = { 2, 3, 6, 8, 10 };

        // Merge both sorted inputs into one sorted result.
        int[] merged = MergeSorted(A, B);

        // Display the exact merged sequence from the walkthrough.
        Console.WriteLine("[" + string.Join(", ", merged) + "]");
    }

    public static int[] MergeSorted(int[] A, int[] B)
    {
        // Store the input lengths so the pointer limits are clear.
        int m = A.Length;
        int n = B.Length;

        // Allocate exactly enough space for every element from both inputs.
        int[] result = new int[m + n];

        // i points into A, j points into B, and k points into result.
        int i = 0;
        int j = 0;
        int k = 0;

        // Compare the two current values while both arrays still have data.
        while (i < m && j < n)
        {
            // Copy the smaller current value. Using <= takes A first on a tie.
            if (A[i] <= B[j])
            {
                result[k++] = A[i++];
            }
            else
            {
                result[k++] = B[j++];
            }
        }

        // If B finished first, copy the already-sorted remainder of A.
        while (i < m)
        {
            result[k++] = A[i++];
        }

        // If A finished first, copy the already-sorted remainder of B.
        while (j < n)
        {
            result[k++] = B[j++];
        }

        // Every input element has now been copied in ascending order.
        return result;
    }
}
Time & Space Complexity

Let m be the length of A and n be the length of B. Every value from both arrays is copied to the result once, so the time complexity is O(m + n). The returned result array contains m + n values, so it uses O(m + n) space. If auxiliary space means extra working memory excluding the required returned array, the algorithm uses O(1), because it only keeps a few integer variables such as i, j, and k.

Where it is used

This merge pattern is useful whenever two already sorted sequences must be combined while keeping sorted order. It appears in merge sort, combining sorted database or search results, merging ordered event streams, and joining sorted batches of data. The main benefit is that the inputs do not need to be sorted again.

Why Interviewers Ask This

This question checks whether the candidate recognizes that sorted inputs allow a linear two-pointer merge instead of sorting everything again. The interviewer can see whether the candidate manages several indices correctly, moves the right pointer after each comparison, handles the remaining suffix when one input ends, preserves duplicate values, writes correct C#, and explains the O(m + n) time and output-space cost accurately.

Common interview mistakes

A common mistake is moving both input pointers after every comparison. Only the pointer for the value that was copied should move. Another mistake is forgetting to copy the remaining suffix after one array is exhausted. Candidates may also allocate a result array with the wrong length instead of m + n. Another error is sorting the combined values again, which throws away the advantage of already-sorted inputs. Finally, the complexity should be stated as O(m + n), not O(m * n).

Interview tip

While coding, say what i, j, and k mean before writing the loop. Then state the invariant: the part of result before k is already the correctly merged sorted prefix. This makes each comparison and pointer movement easy to justify.

Interviewer may ask next
What happens if one of the input arrays is empty?

The same algorithm still works. The main comparison loop does not run because one pointer is already at the end of its array. The remaining-copy loop for the non-empty array copies all of its values into the result in the same order. If both arrays are empty, the result is an empty array. The time is O(m + n), and the returned result uses O(m + n) space.

What happens when both current values are equal?

The diagram's C# code uses A[i] <= B[j]. When A[i] and B[j] are equal, it copies the value from A first and advances i. The equal value from B remains and is copied later, so duplicate values are preserved. The result stays sorted. The time remains O(m + n), and the returned array still uses O(m + n) space.

3. Write a function to locate and delete duplicate elements from an ordered array.CodingEasyMicrosoft

Question Details

Remove repeated values from a sorted array while preserving order of first appearance, and explain the in-place pointer movement, returned length, and handling of consecutive duplicates.

Short Interview Answer (30-60 seconds)

I would use two pointers because the array is already sorted. I keep a write pointer at the next position for a unique value and scan from left to right with a read pointer. If nums[read] differs from nums[write - 1], I copy it to nums[write] and move write forward. Otherwise, I skip the duplicate. The returned write value is the new unique length. This takes O(n) time and O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a sorted array, so repeated values appear next to each other. We need to keep one copy of each value, preserve the original order, and modify the same array instead of building another collection. After processing, the first k positions contain the unique values, and the function returns k. For the example [1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 6], the valid prefix becomes [1, 2, 3, 4, 5, 6], so k is 6.

Useful Questions to Ask the Interviewer
  1. Can I modify the input array in place?
  2. Should I return only the new length while storing the unique values in the first k positions?
  3. Is the input guaranteed to be sorted?
Write a function to locate and delete duplicate elements from an ordered array. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a sorted integer array. We remove repeated values in place while preserving the order of first appearance. We return an integer k. Indices 0 through k - 1 contain the unique values. Positions from k through the end of the physical array are not part of the returned result.

2. Choose the two-pointer approach

Because the array is sorted, duplicate values are consecutive. We use a read pointer to inspect each remaining element and a write pointer to mark the next place where a new unique value should go. The central invariant is that indices 0 through write - 1 always contain exactly the unique values found so far, in their original order.

3. Initialize the state

If nums is null or empty, return 0. Otherwise, the first value is already unique, so set write = 1. Start read at index 1. At every step, compare nums[read] with nums[write - 1], which is the last unique value currently stored.

4. Walk through the example

Start with nums = [1, 1, 2, 2, 3, 3, 3, 4, 5, 5, 6] and write = 1.

At read = 1, nums[1] is 1 and nums[write - 1] is nums[0] = 1. They are equal, so this is a duplicate. Skip it. write remains 1.

At read = 2, nums[2] is 2 and nums[0] is

  1. They differ. Copy 2 to nums[1], then increment write to
  2. The valid prefix is [1, 2].

At read = 3, nums[3] is 2 and nums[1] is 2. They are equal, so skip it. write remains 2.

At read = 4, nums[4] is 3 and nums[1] is

  1. They differ. Copy 3 to nums[2], then increment write to
  2. The valid prefix is [1, 2, 3].

At read = 5, nums[5] is 3 and nums[2] is 3. Skip this duplicate. write remains 3.

At read = 6, nums[6] is 3 and nums[2] is 3. Skip this consecutive duplicate too. write remains 3.

At read = 7, nums[7] is 4 and nums[2] is

  1. They differ. Copy 4 to nums[3], then increment write to
  2. The valid prefix is [1, 2, 3, 4].

At read = 8, nums[8] is 5 and nums[3] is

  1. They differ. Copy 5 to nums[4], then increment write to
  2. The valid prefix is [1, 2, 3, 4, 5].

At read = 9, nums[9] is 5 and nums[4] is 5. They are equal, so skip it. write remains 5.

At read = 10, nums[10] is 6 and nums[4] is

  1. They differ. Copy 6 to nums[5], then increment write to
  2. The valid prefix is now [1, 2, 3, 4, 5, 6]. The loop ends and the function returns 6.
5. Explain why the result is correct

Before each comparison, the first write positions contain all unique values seen so far in the correct order. If nums[read] equals nums[write - 1], the current value is another copy of the last unique value, so skipping it is safe. If they differ, nums[read] is the next new unique value, so writing it at nums[write] extends the valid prefix correctly. When the scan finishes, write equals the number of unique values.

6. Explain the C# implementation

The method first handles null or empty input. It starts write at 1 because the first element is already unique. A for loop moves read from index 1 to the last index. When nums[read] differs from nums[write - 1], the code stores nums[read] at nums[write] and increments write. Duplicates cause no write operation. The method finally returns write as k.

7. Explain complexity and edge cases

The read pointer moves through the array once, so the time complexity is O(n). The algorithm uses only pointer variables and modifies the original array, so auxiliary space is O(1). Null or empty input returns 0. A single element returns 1. If all values are equal, the result is 1. If all values are already unique, the result is the original array length.

Key Insight / Why This Solution Works

Use two pointers on the sorted array. The read pointer scans from index 1 to the end. The write pointer marks the next position for a newly found unique value. At each step, compare nums[read] with nums[write - 1], the last unique value already stored. If they are equal, skip the duplicate. If they differ, copy nums[read] to nums[write] and increment write. The invariant is that indices 0 through write - 1 always contain exactly the unique values seen so far in their original order. This works because sorted input places equal values next to one another.

Code
using System;

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

        // Remove duplicate values in place and return the valid prefix length.
        int k = RemoveDuplicates(nums);

        // Display the returned number of unique values.
        Console.WriteLine($"k = {k}");

        // Only indices 0 through k - 1 belong to the final unique result.
        Console.Write("Unique prefix: [");
        for (int i = 0; i < k; i++)
        {
            // Add a separator only between printed values.
            if (i > 0)
            {
                Console.Write(", ");
            }

            Console.Write(nums[i]);
        }

        Console.WriteLine("]");
    }

    public static int RemoveDuplicates(int[] nums)
    {
        // A null or empty input contains no unique values.
        if (nums == null || nums.Length == 0)
        {
            return 0;
        }

        // nums[0] is already unique, so write marks the next free
        // position for a newly discovered unique value.
        int write = 1;

        // Scan each remaining element from left to right.
        for (int read = 1; read < nums.Length; read++)
        {
            // Compare the current value with the last unique value stored.
            // Because the array is sorted, equality means it is a duplicate.
            if (nums[read] != nums[write - 1])
            {
                // Store the new unique value at the next valid-prefix position.
                nums[write] = nums[read];

                // Advance write so it points to the next free position.
                write++;
            }
        }

        // write is the count of unique elements and the valid prefix length k.
        return write;
    }
}
Time & Space Complexity

The time complexity is O(n), where n is the number of elements in the input array. The read pointer moves from index 1 to the end, so each remaining element is examined once. The auxiliary space complexity is O(1) because the algorithm changes the original array and uses only a constant number of integer variables. It does not create another array, set, or other collection that grows with n.

Where it is used

This pattern is useful when sorted data must be compacted in place. Examples include removing repeated sorted IDs, compressing repeated sorted records, or keeping one copy of each adjacent repeated value while avoiding an additional collection.

Why Interviewers Ask This

This problem checks whether you notice that sorted input makes duplicates consecutive and whether you can use that fact to solve the problem without extra storage. It tests two-pointer reasoning, in-place array mutation, correct movement of the read and write pointers, handling of consecutive duplicates, and understanding of the returned length. It also checks whether you can write clear C# code and accurately explain O(n) time and O(1) auxiliary space.

Common interview mistakes

One mistake is incrementing write when the current value is a duplicate. That would put repeated values inside the valid prefix. Another mistake is comparing against the wrong value after earlier positions have been overwritten. The approved solution compares nums[read] with nums[write - 1], the last unique value stored. Candidates may also return the whole physical array instead of returning k and treating only indices 0 through k - 1 as valid. Another mistake is allocating a separate collection and then claiming O(1) auxiliary space. Finally, failing to handle an empty input can make the initial write = 1 state invalid.

Interview tip

Explain the invariant before writing the loop: indices 0 through write - 1 always contain the unique values found so far in the correct order. Then each comparison and write-pointer movement follows directly from that rule.

Interviewer may ask next
What changes if the input array is not sorted?

The same two-pointer comparison is no longer enough because equal values may be separated by other values. To preserve first-appearance order, I can scan from left to right and use a HashSet<int> to remember values already seen. When a value is new, I write it at the next write position. Correctness is preserved because the set tells us whether the value appeared earlier. The expected time is O(n), because HashSet lookup and insertion are O(1) on average. Auxiliary space becomes O(n). The tradeoff is extra memory.

Can we physically shrink the C# array after returning k?

A C# array has a fixed physical Length, so this in-place algorithm does not resize the array object. It guarantees that indices 0 through k - 1 contain the unique values. If the caller needs a new array whose physical length is exactly k, the first k values can be copied into a new array. That additional copy takes O(k) time and O(k) extra space. The original in-place duplicate-removal step still takes O(n) time and O(1) auxiliary space.

4. Write a program that will delete duplicate letters from a string.CodingEasyMicrosoft

Question Details

Remove repeated characters from a string while keeping the first occurrence of each letter, and describe how you track seen characters and preserve the remaining order.

Short Interview Answer (30-60 seconds)

I would scan the string from left to right and use a HashSet<char> to remember characters I have already seen. I would also use a StringBuilder to build the result in order. If the current character is not in the set, I add it to the set and append it to the result. Otherwise, I skip it. This keeps only the first occurrence of each character. The solution takes O(n) expected time and O(n) auxiliary space in the worst case.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to remove repeated characters from a string while keeping the first time each character appears. The remaining characters must stay in the same left-to-right order. For example, the diagram uses "programming" and produces "progamin". I process one character at a time. I remember the characters that have already been accepted. If a character is new, I keep it. If it has already appeared, I skip it. This approach directly matches the required behavior and avoids repeatedly searching the result string.

Useful Questions to Ask the Interviewer
  1. Should character comparison be case-sensitive?
  2. Should the first-occurrence order always be preserved?
  3. How should an empty string be handled?
Write a program that will delete duplicate letters from a string. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a string. The output is a new string that keeps only the first occurrence of each character. The order of those first occurrences must stay unchanged. In the diagram, the input is "programming". The expected output is "progamin".

2. Choose the algorithm and data structures

I use a HashSet<char> named seen. It stores the characters that have already been accepted. I also use a StringBuilder named result. It stores the output in the same order in which characters are accepted. The main rule is that a character is appended only when it is not already in seen.

3. Initialize the state

At the beginning, seen is empty and result is empty. Processing starts with the first character and moves from left to right. The invariant is that seen contains the characters already kept, and result contains those same characters exactly once in their first-occurrence order.

4. Walk through the example

The string is "programming" with indices 0 through 10.

At index 0, the character is 'p'. seen is empty, so 'p' is new. Add 'p' to seen and append it to result. result becomes "p".

At index 1, the character is 'r'. It is not in seen. Add it to seen and result becomes "pr".

At index 2, the character is 'o'. It is not in seen. Add it and result becomes "pro".

At index 3, the character is 'g'. It is not in seen. Add it and result becomes "prog".

At index 4, the character is 'r'. It is already in seen, so skip it. result stays "prog".

At index 5, the character is 'a'. It is new, so add it. result becomes "proga".

At index 6, the character is 'm'. It is new, so add it. result becomes "progam".

At index 7, the character is 'm'. It is already in seen, so skip it. result stays "progam".

At index 8, the character is 'i'. It is new, so add it. result becomes "progami".

At index 9, the character is 'n'. It is new, so add it. result becomes "progamin".

At index 10, the character is 'g'. It is already in seen, so skip it. result stays "progamin".

After the traversal finishes, the final returned string is "progamin".

5. Explain why the result is correct

A character is added to result only when it has not appeared before. As soon as the character is kept, it is also added to seen. Any later copy of that character is therefore skipped. Because the input is processed from left to right, the first occurrence is kept and the relative order of all kept characters is preserved.

6. Explain the C# implementation

The method first handles a null or empty string defensively. It then creates an empty HashSet<char> and an empty StringBuilder. The foreach loop reads characters from left to right. For each character, the code checks whether seen already contains it. If not, the code adds it to seen and appends it to result. If it is already present, no state change is made. After all characters are processed, result.ToString() returns the answer.

7. Explain complexity and edge cases

For n input characters, each character is processed once. HashSet<char> membership checks and insertions are O(1) on average, so the total expected time is O(n). The set can contain up to k distinct characters, where k is at most n, and the result can also grow with the input. Therefore, the auxiliary space is O(n) in the worst case. The diagram also shows important cases such as an empty string returning an empty string, "a" returning "a", "aaaa" returning "a", repeated characters mixed with numbers and symbols, and case-sensitive character handling.

Key Insight / Why This Solution Works

The key idea is to remember which characters have already been accepted. HashSet<char> seen provides an average O(1) membership check. StringBuilder result stores the characters that survive duplicate removal. We traverse from left to right. For each character, we first check seen. If the character is new, we add it to seen and append it to result. If it is already present, we skip it. The central invariant is that seen contains exactly the characters already accepted, while result contains those characters once and in first-occurrence order.

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

public static class Program
{
    public static string RemoveDuplicateLetters(string s)
    {
        // Handle the empty-input case before creating the working data structures.
        // This also defensively handles null, matching the behavior shown in the diagram code.
        if (string.IsNullOrEmpty(s))
        {
            return s;
        }

        // Store every character that has already been accepted into the output.
        HashSet<char> seen = new HashSet<char>();

        // Build the result efficiently while preserving first-occurrence order.
        StringBuilder result = new StringBuilder();

        // Traverse the input from left to right exactly once.
        foreach (char c in s)
        {
            // Keep the character only if this is its first occurrence.
            if (!seen.Contains(c))
            {
                // Mark the character as seen so later duplicates will be skipped.
                seen.Add(c);

                // Append the first occurrence to the result in its original order.
                result.Append(c);
            }
            // If c is already in seen, it is a duplicate and no state change is needed.
        }

        // Convert the completed StringBuilder into the required output string.
        return result.ToString();
    }

    public static void Main()
    {
        // Run the exact verified example from the diagram.
        string input = "programming";
        string output = RemoveDuplicateLetters(input);

        // Expected and actual output: progamin
        Console.WriteLine(output);
    }
}
Time & Space Complexity

Let n be the length of the input string. We process each character once. A HashSet<char> lookup or insertion is O(1) on average, so the overall expected time is O(n). This is expected time because hash-table operations are not guaranteed to be constant time in every theoretical worst case. The HashSet stores up to k distinct characters, where k is at most n. The StringBuilder also stores the output. Therefore, the auxiliary space is O(n) in the worst case.

Where it is used

This pattern is useful when software must remove repeated items while keeping the order of their first appearance. It can be used when cleaning text, filtering duplicate characters, preserving the first occurrence of tokens, or processing an ordered stream where repeated values should be ignored after their first appearance.

Why Interviewers Ask This

This problem checks whether the candidate can recognize duplicate detection as a good use for a set, preserve the required left-to-right order, and keep lookup state separate from the output. It also tests correct duplicate handling, basic C# collection knowledge, the ability to explain a simple invariant, and accurate complexity analysis. A strong answer should distinguish average hash-set performance from a guaranteed worst-case bound.

Common interview mistakes

One mistake is appending a character before checking whether it was already seen, which keeps duplicates. Another is sorting the input first, which destroys the required first-occurrence order. A candidate may also use frequency counting even though the problem only needs to know whether a character has appeared before. Repeated string concatenation can create unnecessary copies compared with StringBuilder. Another mistake is claiming guaranteed O(n) time instead of O(n) expected time when the solution depends on average O(1) HashSet operations.

Interview tip

Explain the invariant before writing the loop: seen contains every character already kept, and result contains those characters once in their original first-occurrence order. Then each loop decision becomes easy to justify.

Interviewer may ask next
Can the extra tracking space be reduced if the allowed character set is small and fixed?

Yes. If the input is guaranteed to use a small fixed alphabet, such as ASCII, a fixed-size Boolean array can replace the HashSet. Each character maps to one array position. We still process characters from left to right and keep a character only when its Boolean entry is false. Then we mark that entry true. Correctness is unchanged because the array records the same seen-or-not-seen state. Time remains O(n), and tracking space becomes O(1) relative to n because the alphabet size is fixed. The tradeoff is that the solution now depends on that fixed character range.

How would this approach work if the characters arrived as a stream instead of one complete string?

The same first-seen rule can be applied as characters arrive. Keep a HashSet of previously accepted characters. For each incoming character, check the set. If it is new, add it to the set and emit it immediately or append it to an output builder. If it is already present, skip it. The invariant stays the same, so correctness is preserved. Processing n characters takes O(n) expected time. Tracking uses O(k) space for k distinct characters. If output is emitted directly, there is no need to keep the complete result in memory.

5. Given an unordered array of integers, write a program that finds a contiguous subarray whose sum is equal to the given one.CodingHardMicrosoft

Question Details

Find a contiguous subarray with a target sum in an unsorted integer array, and explain the prefix-sum or window strategy you would use under the exact constraints of the prompt.

Short Interview Answer (30-60 seconds)

I would use a running prefix sum and a Dictionary<int, int>. The dictionary stores an earlier prefix sum and the index where it appeared. For each number, I update the running sum and compute need = prefixSum - target. If need is already in the dictionary, the elements after that stored index through the current index form the answer. I process each item at most once and stop when the answer is found. This takes O(n) expected time and O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We have an unordered integer array and a target number. We need to find neighboring elements whose total equals that target. The elements must stay next to each other in the original array. In the shown implementation, we return the inclusive start and end positions of one matching group. We keep a running total while moving from left to right. We also remember earlier running totals so we can quickly tell when the part between two positions has exactly the required sum. This approach also works when the array contains negative numbers.

Useful Questions to Ask the Interviewer
  1. Should I return the start and end indices, the values, or the subarray itself?
  2. If more than one valid subarray exists, is returning any one valid result acceptable?
  3. What should I return when no matching subarray exists?
Given an unordered array of integers, write a program that finds a contiguous subarray whose sum is equal to the given one. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an unordered integer array named nums and an integer target. We need one contiguous subarray whose values add up to target. Contiguous means the values must be next to each other in the original array. The implementation shown in the diagram returns two 0-based inclusive indices: the start index and the end index. If no matching subarray exists, it returns an empty integer array.

2. Choose the algorithm and data structure

Use a prefix sum with a Dictionary<int, int>. A prefix sum is the total of all values processed so far. The dictionary maps a retained prefix sum to an earlier index where that sum occurred. If the current prefix sum is currentSum, then a subarray ending at the current index has sum target when an earlier prefix sum equals currentSum - target. That earlier prefix sum tells us where the matching subarray starts.

3. Initialize the state

Set prefixSum to 0. Initialize the dictionary with 0 mapped to -1. The entry 0 -> -1 represents a prefix sum of zero before the array begins. This special entry lets the same logic find a valid subarray that starts at index 0. For example, if the running sum becomes 7, then need is 0 and the returned start index is -1 + 1, which is 0.

4. Walk through the example

The array is [3, 4, -7, 1, 3, 3, 1, -4] and target is 7.

At index 0, the current value is 3. prefixSum becomes 3. We calculate need = 3 - 7 = -4. The dictionary currently contains {0: -1}. It does not contain -4, so no answer ends at index 0. We store prefix sum 3 at index 0. The dictionary becomes {0: -1, 3: 0}.

At index 1, the current value is 4. prefixSum becomes 7. We calculate need = 7 - 7 = 0. The dictionary contains 0 at index -1. Therefore the matching subarray starts at -1 + 1 = 0 and ends at index 1. We return [0, 1]. The values are [3, 4], and 3 + 4 = 7. Processing stops immediately, so indices 2 through 7 are not processed.

5. Explain why the result is correct

Suppose prefixSum[i] is the running total through index i, and an earlier prefix sum at index j equals prefixSum[i] - target. Then prefixSum[i] - prefixSum[j] equals target. That difference is exactly the sum of nums[j + 1] through nums[i]. The dictionary stores retained earlier prefix sums and their indices. When the needed prefix sum appears, the algorithm can recover the correct start index. In the example, prefix sum 0 is stored at index -1, so the valid range is indices 0 through 1.

6. Explain the C# implementation

The C# method creates a Dictionary<int, int> with 0 mapped to -1 and sets prefixSum to 0. It loops through nums from left to right. For each element, it updates prefixSum and calculates need = prefixSum - target. It calls TryGetValue before inserting the current prefix sum. If need exists, it immediately returns [storedIndex + 1, currentIndex]. Otherwise, it stores the current prefix sum only if it is not already present. If the loop finishes without finding an answer, it returns Array.Empty<int>().

7. Explain complexity and edge cases

The expected time is O(n) because we process the input at most once, and C# Dictionary<int, int> lookup and insertion are O(1) on average. The auxiliary space is O(n) because the dictionary can grow with the number of processed prefix sums. The method handles an empty array, no matching subarray, a valid subarray starting at index 0, and negative numbers. Negative values are important because a normal sliding-window approach that relies on the sum changing monotonically is not reliable here.

Key Insight / Why This Solution Works

The key idea is to turn the subarray-sum problem into a prefix-sum lookup. Let prefixSum be the total from index 0 through the current index i. If an earlier prefix sum equals prefixSum - target, then the values after that earlier index through i sum to target. The invariant is that the dictionary contains retained prefix sums from earlier positions and the earlier index stored for each one. Lookup happens before insertion. This lets the algorithm return immediately when a valid range is found and works correctly even when the array contains negative numbers.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static int[] FindSubarrayWithTargetSum(int[] nums, int target)
    {
        // Map each retained prefix sum to an earlier index where it occurred.
        // Prefix sum 0 at index -1 represents the position before the array starts.
        Dictionary<int, int> prefixSumToIndex = new Dictionary<int, int> { [0] = -1 };

        // Keep the running sum of all elements processed so far.
        int prefixSum = 0;

        // Process each element from left to right and stop as soon as a match is found.
        for (int i = 0; i < nums.Length; i++)
        {
            // Add the current value to the running prefix sum.
            prefixSum += nums[i];

            // A previous prefix sum equal to prefixSum - target means the values
            // after that previous index through i add up exactly to target.
            int need = prefixSum - target;

            // Look up the needed earlier prefix sum before storing the current one.
            if (prefixSumToIndex.TryGetValue(need, out int startIndex))
            {
                // The matching subarray begins one position after startIndex
                // and ends at the current index i, both inclusive.
                return new int[] { startIndex + 1, i };
            }

            // Keep the earlier stored index if this prefix sum has appeared before.
            if (!prefixSumToIndex.ContainsKey(prefixSum))
            {
                prefixSumToIndex[prefixSum] = i;
            }
        }

        // The entire input was processed and no matching contiguous subarray was found.
        return Array.Empty<int>();
    }

    public static void Main()
    {
        // Use the exact verified example from the diagram.
        int[] nums = { 3, 4, -7, 1, 3, 3, 1, -4 };
        int target = 7;

        // Run the prefix-sum solution on the example input.
        int[] result = FindSubarrayWithTargetSum(nums, target);

        // Print the inclusive start and end indices when a matching range exists.
        if (result.Length == 2)
        {
            Console.WriteLine($"[{result[0]}, {result[1]}]");
        }
        else
        {
            // Print an empty result when no matching subarray exists.
            Console.WriteLine("[]");
        }
    }
}
Time & Space Complexity

Let n be the number of elements in nums. We process the input at most once. Each Dictionary<int, int> lookup and insertion is O(1) on average, so the overall expected time is O(n). This is expected time because hash-table operations are not guaranteed to take constant time in every possible case. The dictionary may store prefix sums for many processed positions, so the auxiliary space is O(n). Auxiliary space means extra memory used by the algorithm.

Where it is used

This prefix-sum pattern is useful when software needs to find a continuous range whose total matches a requested value, especially when the data can contain positive, zero, and negative numbers. Similar logic appears in analytics, financial transaction ranges, event-count ranges, and sequence processing where a program needs the sum between two positions without recomputing every possible range.

Why Interviewers Ask This

This problem tests whether a candidate recognizes the prefix-sum pattern instead of trying every possible subarray. It also checks whether the candidate can choose an appropriate hash-based data structure, maintain the correct mapping from prefix sums to indices, handle negative values, reason about a subarray starting at index 0, stop correctly after an early return, write valid C#, and describe Dictionary complexity using expected-time rather than guaranteed-time wording.

Common interview mistakes

A common mistake is using a simple sliding window even though negative values are allowed. With negative numbers, expanding or shrinking a window does not change the sum in a predictable direction. Another mistake is forgetting the initial dictionary entry 0 -> -1, which is needed to find a valid range starting at index 0. Candidates may also calculate the needed prefix sum incorrectly. It must be prefixSum - target. Another mistake is continuing to process later elements after this implementation has already returned a match. Finally, do not claim guaranteed O(n) time because Dictionary operations are O(1) on average.

Interview tip

Explain the equation before writing code: if currentPrefix - earlierPrefix = target, then earlierPrefix = currentPrefix - target. Once that relationship is clear, the dictionary lookup, returned indices, and 0 -> -1 initialization are easy to justify.

Interviewer may ask next
What changes if no matching subarray is guaranteed to exist?

The main algorithm does not need to change because the shown implementation already handles this case. We keep processing until either a valid prefix-sum match is found or the loop ends. If the loop ends, we return Array.Empty<int>(). Correctness is unchanged because every processed index checks whether the required earlier prefix sum exists. Expected time remains O(n), auxiliary space remains O(n), and the tradeoff is that an unsuccessful search must process the entire array.

What changes if we must return all contiguous subarrays whose sum equals the target?

We keep the same prefix-sum idea, but each prefix sum must store all earlier indices where it occurred instead of only one index. We initialize prefix sum 0 with index -1. At each current index, we compute need = prefixSum - target and create one result for every earlier index stored under need. We then store the current index under the current prefix sum and continue instead of returning early. This preserves correctness because every earlier index j with prefixSum[i] - prefixSum[j] = target defines a valid range j + 1 through i. Expected time is O(n + k), where k is the number of returned ranges. Auxiliary working space is O(n), excluding the returned results. Including the output itself, total space can be O(n + k). The tradeoff is that there may be many valid ranges.

6. What is the definition and application of stack and heap?CodingHardMicrosoft

Question Details

Use the Microsoft MLE prompt to explain how stack and heap differ, and cover which allocation or lifetime behaviors the interviewer would expect you to distinguish.

Short Interview Answer (30-60 seconds)

I would explain stack and heap as two different parts of the .NET memory model. Each thread has a call stack for active method frames and execution state. Most managed objects, such as class instances and arrays, live on the GC-managed heap. Value types are not automatically stored on the stack. Stack frames unwind when methods return, while heap objects remain alive while reachable from GC roots. Allocation and cleanup costs depend on the JIT, garbage collector, runtime configuration, and workload.

Detailed Explanation

See the Code while reading this explanation.

The question asks how the stack and heap differ in .NET and when each matters. The main distinction is lifetime. A thread's call stack represents active method calls and their execution state. The managed heap stores most managed objects. A value type is not automatically a stack value because its storage depends on where the containing value lives. A heap object can survive after its creating method returns if it is still reachable from a GC root. The JIT and garbage collector manage many physical details for us.

Useful Questions to Ask the Interviewer
  1. Do you want the conceptual .NET memory model, or should I also discuss JIT details such as registers and optimized-away locals?
  2. Should I cover explicit stack-oriented features such as stackalloc, Span<T>, and ref-like types?
  3. Should I explain GC reachability and why collection does not happen immediately when a method returns?
What is the definition and application of stack and heap? diagram
How to Explain It in an Interview
1. Define the stack and managed heap

Each thread has its own call stack. It represents active method calls and execution state. A method call creates a logical stack frame. That frame may contain information such as eligible locals and return information. When the method returns, its frame is unwound.

The managed heap is runtime-managed memory controlled by the .NET garbage collector. Most class instances, arrays, strings, boxed values, collections, delegates, and other managed objects live there.

2. Explain why value type does not mean stack

A common shortcut says value types live on the stack and reference types live on the heap. That is not generally correct.

A value type stores its data inline wherever its containing location lives. A value-type local may be held in a stack slot or register. A value-type field inside a class instance is part of that heap object. A boxed value is stored on the managed heap.

A reference-type object normally lives on the managed heap. A variable that holds its reference may be kept in a stack slot, register, heap object, or another location chosen by the JIT/runtime.

3. Compare allocation and lifetime

Method frames are managed automatically as calls execute and return. Frame setup and unwinding are typically very cheap.

Managed-object allocation is performed by the runtime. The new keyword commonly creates managed objects, but heap allocation is not defined only by that keyword.

A stack frame normally exists for the duration of its method invocation. A managed object follows a different rule. It remains live while reachable from GC roots. After it becomes unreachable, it becomes eligible for collection. A later GC may reclaim it. Collection timing is nondeterministic.

4. Walk through the diagram example

Program.Main calls Demo. Demo becomes the active method above Main on the call stack.

Demo creates int x = 10. x is a value-type local associated with Demo's execution. Its exact physical location is a JIT implementation detail.

Next, Person p = new Person() creates Person object A on the managed heap. p contains a reference to object A. The object initially has Name equal to null and Age equal to 0.

The code then sets Name to "Alice" and Age to 30. The program prints x = 10 and p.Name = Alice, p.Age = 30.

When Demo returns, its logical stack frame is unwound. x and p are no longer used by Demo. If Person object A is then unreachable from every GC root, it becomes eligible for collection. A later garbage collection may reclaim it.

5. Explain C# parameter passing

C# parameters are passed by value by default. For a value type, the value is copied. For a reference type, the reference value is copied, so both variables can refer to the same object. The ref, out, and in keywords change parameter-passing behavior.

6. Explain practical use and performance

Normally, I choose the correct C# type and lifetime rather than manually choosing stack or heap memory. Stack-specific features such as stackalloc, Span<T>, and ref-like types are useful when their lifetime rules, size limits, and performance characteristics fit the scenario.

Stack-frame setup and unwinding are usually cheap. Managed allocation is also optimized, but a high allocation rate can increase GC pressure. Garbage collections can affect latency and throughput. Actual performance depends on the workload, memory locality, runtime configuration, and environment.

Every thread has finite stack capacity. Deep recursion or very large frames can cause StackOverflowException. Managed-heap capacity depends on runtime configuration, process limits, allocation patterns, object lifetimes, and the environment.

7. Explain cleanup correctly

The garbage collector manages managed memory. It does not provide deterministic cleanup for unmanaged resources such as file handles. When deterministic cleanup is required, use IDisposable, using, or an appropriate SafeHandle pattern.

Key Insight / Why This Solution Works

This is a memory-model explanation rather than a search algorithm. The key insight is to separate two lifetime models. The per-thread call stack tracks active method calls and their execution state. The GC-managed heap stores most managed objects. The central invariant is that stack-frame lifetime follows method invocation, while managed-object lifetime follows reachability from GC roots. Type category alone does not decide physical storage. Value-type data is stored inline wherever its containing location lives, and the JIT may keep eligible locals or reference values in stack slots, registers, or other runtime-selected locations.

Code
using System;

public class Person
{
    public string Name;
    public int Age;
}

public static class Program
{
    public static void Demo()
    {
        // x is a value-type local associated with Demo's execution.
        // Its exact physical placement is chosen by the JIT and may be a stack slot or register.
        int x = 10;

        // Create a Person object. A normal class instance is allocated on the managed heap.
        // p contains the reference value that points to that object.
        Person p = new Person();

        // Update the fields of the same managed Person object.
        p.Name = "Alice";
        p.Age = 30;

        // Print the exact values used in the diagram's example.
        Console.WriteLine($"x = {x}");
        Console.WriteLine($"p.Name = {p.Name}, p.Age = {p.Age}");

        // When Demo returns, its logical call frame is unwound.
        // If the Person object is unreachable from all GC roots,
        // it becomes eligible for collection by a later GC cycle.
    }

    public static void Main()
    {
        // Main is the caller. Demo becomes the active method above it on the call stack.
        Demo();
    }
}
Time & Space Complexity

There is no single Big-O time or auxiliary-space complexity for this conceptual example. The diagram compares runtime behavior instead. Stack-frame setup and unwinding are typically cheap. Managed-heap allocation is also optimized, but allocation rate and garbage collection can affect latency and throughput. Each thread has finite stack capacity. Managed-heap capacity and GC behavior depend on runtime configuration, process limits, allocation patterns, object lifetimes, and the environment. These costs are implementation-dependent and workload-dependent rather than one fixed algorithmic complexity.

Where it is used

These ideas are useful in everyday .NET development, performance tuning, recursion, object allocation, garbage-collection analysis, and resource management. They also matter when using stack-oriented features such as stackalloc, Span<T>, and ref-like types. Understanding the distinction helps developers avoid incorrect assumptions about where values live, diagnose StackOverflowException or GC pressure, and choose the correct lifetime and cleanup model for managed and unmanaged resources.

Why Interviewers Ask This

The interviewer is checking whether you understand the .NET execution and memory model rather than memorized slogans. They want to see whether you can distinguish method-frame lifetime from object lifetime, explain GC reachability, describe value types and reference types correctly, and avoid assuming that physical storage is determined only by the C# type. They may also evaluate your understanding of JIT implementation freedom, allocation pressure, stack overflow, parameter passing, and deterministic cleanup of unmanaged resources.

Common interview mistakes

A common mistake is saying all value types live on the stack and all reference variables live on the heap. Storage depends on context and JIT decisions. Another mistake is saying an object is collected immediately when its creating method returns. It is only eligible for collection after it becomes unreachable from GC roots. Candidates may also treat a managed reference as a permanent physical address even though the GC can move objects. Another mistake is saying the heap is always slow while the stack is always fast. Finally, GC-managed memory should not be confused with deterministic cleanup of unmanaged resources.

Interview tip

Start with the two lifetime rules: call-stack frames follow method calls, while managed-heap objects follow GC reachability. Then correct the common shortcut that says value types always live on the stack. That shows that you understand the .NET memory model beyond the beginner-level simplification.

Interviewer may ask next
What happens if a managed object is still referenced after the method that created it returns?

The method's stack frame can unwind normally, but the heap object remains alive if it is still reachable from a GC root. The object does not disappear just because its creating method returned. The garbage collector may reclaim it only after it becomes unreachable. The tradeoff is that longer-lived objects keep using managed-heap memory and may survive additional garbage-collection cycles.

When would you use stackalloc or Span<T> instead of ordinary managed-heap allocation?

I would use stackalloc for small, short-lived buffers when stack lifetime and finite stack capacity are appropriate. Span<T> can provide safe access to contiguous memory and can work with stack-backed or other memory without requiring a new managed array in some cases. The benefit can be fewer managed allocations and less GC pressure. The tradeoff is stricter lifetime rules, limited stack capacity, and the need to avoid letting stack-backed data escape its valid scope.

7. Minimum Window Substring.CodingEasyMicrosoft

Question Details

Find the smallest substring that covers every character required by a second string, and describe how the sliding window expands, contracts, and handles missing characters.

Short Interview Answer (30-60 seconds)

I would use a sliding window with two frequency dictionaries. The right pointer expands the window until every required character has enough copies. Then the left pointer repeatedly shrinks the valid window, and I save a new answer whenever it becomes smaller. If removing a required character makes its count too low, shrinking stops and expansion continues. For the example, the result is "BANC" at indices [9,12]. The diagram reports O(n) time and O(n + m) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We have a source string s and a target string t. We need the shortest continuous part of s that contains every character required by t, including the correct number of repeated characters. For the diagram example, s is "ADOBECODEBANC" and t is "ABC". The answer is "BANC", from index 9 through index 12. A sliding window works well because we can grow the current range until it is valid, then make it smaller while it stays valid.

Useful Questions to Ask the Interviewer
  1. Should character matching be case-sensitive?
  2. What should I return if no valid substring exists?
  3. Can t contain repeated characters whose full counts must appear in the window?
Minimum Window Substring. diagram
How to Explain It in an Interview
1. Understand the input and output

The input is two strings, s and t. We must return a substring of s. A substring is a continuous range of characters. It must contain every character required by t with the required frequency. If no valid window exists, the implementation returns an empty string. In the diagram, s = "ADOBECODEBANC" and t = "ABC". The final answer is "BANC" at [9,12].

2. Choose the sliding window and frequency dictionaries

I use two pointers called left and right. They mark the current window s[left..right]. The need dictionary stores how many copies of each target character are required. For t = "ABC", need is A:1, B:1, C:1. The window dictionary stores character counts inside the current window. required is the number of distinct required characters, which is 3. have tells us how many of those required characters currently meet their needed frequency.

3. Initialize the state

At the start, left = 0 and have = 0. required = 3. The window dictionary is empty. bestLen starts at int.MaxValue, meaning that no valid answer has been found yet. bestLeft starts at -1. The right pointer then moves through s from index 0 to the end.

4. Walk through the example

At right = 0, we add A. Its required count is met, so have becomes 1. D and O are added next, but they are not required characters. At right = 3, B reaches its required count, so have becomes 2. E does not change have. At right = 5, C reaches its required count, so have becomes 3. The window [0,5] is "ADOBEC". It is valid and becomes the first best window with length 6. We then remove A at left = 0. A falls below its required count, so have becomes 2. left becomes 1, and shrinking stops.

The right pointer continues. At right = 6 we add O, at 7 we add D, at 8 we add E, and at 9 we add B. A is still missing, so have remains 2 and the best answer remains "ADOBEC".

At right = 10, we add A. Now have becomes 3 again. We can shrink. Window [1,10] is "DOBECODEBA". Removing D keeps the window valid. Window [2,10] is "OBECODEBA". Removing O keeps it valid. Window [3,10] is "BECODEBA". Removing B keeps it valid because another B remains. Window [4,10] is "ECODEBA". Removing E keeps it valid. Window [5,10] is "CODEBA". Its length is 6, so it does not replace "ADOBEC" because the code updates only for a strictly smaller window. Removing C makes the C count fall below its need. have becomes 2, left becomes 6, and shrinking stops.

At right = 11, we add N. It is not required, so have remains

  1. At right = 12, we add C. C reaches its required count again, so have becomes
  2. Window [6,12] is "ODEBANC". Removing O keeps it valid. Window [7,12] is "DEBANC". Removing D keeps it valid. Window [8,12] is "EBANC". It is shorter than the best window, so the best becomes "EBANC" at [8,12]. Removing E keeps the window valid. Window [9,12] is "BANC". It is shorter again, so the best becomes "BANC" at [9,12]. Removing B then makes B fall below its required count. have becomes 2, left becomes 10, and shrinking stops. The final answer is "BANC".
5. Explain why the result is correct

The key invariant is that have == required only when every required character currently meets its needed frequency. While that condition is true, the current window is valid. We record it if it is smaller than the best window, then move left to test a smaller candidate. When removing a required character makes its count fall below need, have decreases and shrinking stops. This keeps every recorded best window valid.

6. Explain the C# implementation

The code first builds need from t. It then expands right through s and updates window. When a required character reaches exactly its needed count, have increases. While have == required, the code checks the current window before removing s[left]. If that window is smaller, it saves its length and starting index. It then decreases the outgoing character count. If a required count falls below need, have decreases. Finally, left moves forward. After the scan, the method returns the saved substring or string.Empty when no valid window was found.

7. Explain complexity and edge cases

The diagram reports O(n) time for the sliding-window processing because both pointers move only forward and each source character is added at most once and removed at most once. C# Dictionary lookup and update are O(1) on average. The diagram reports O(n + m) auxiliary space in the worst case for the frequency dictionaries. Important edge cases are an empty source, an empty target, no valid window, and repeated characters in t whose multiplicities must all be satisfied.

Key Insight / Why This Solution Works

The key insight is to avoid checking every possible substring. The algorithm keeps one sliding window between left and right. The need dictionary stores required target frequencies, and the window dictionary stores frequencies inside the current window. The central invariant is that have == required means every required character currently has enough copies. The right pointer expands until this condition becomes true. Then left repeatedly moves forward while the window remains valid. The best answer is updated before removing the left character. Shrinking stops as soon as a required frequency falls below its needed value.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Run the exact example used by the diagram.
        string s = "ADOBECODEBANC";
        string t = "ABC";

        // Solve the problem and show the expected final substring.
        string result = MinWindow(s, t);
        Console.WriteLine(result); // BANC
    }

    public static string MinWindow(string s, string t)
    {
        // An empty source or target has no useful valid window.
        if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(t))
        {
            return string.Empty;
        }

        // need stores the required frequency of every character from t.
        Dictionary<char, int> need = new Dictionary<char, int>();
        foreach (char c in t)
        {
            need[c] = need.GetValueOrDefault(c, 0) + 1;
        }

        // window stores frequencies inside the current [left, right] window.
        Dictionary<char, int> window = new Dictionary<char, int>();

        // required is the number of distinct target characters to satisfy.
        // have is how many of those characters currently meet their needed count.
        int required = need.Count;
        int have = 0;

        // Track the current left boundary and the smallest valid window found.
        int left = 0;
        int bestLen = int.MaxValue;
        int bestLeft = -1;

        // Expand the window by moving right through the source string.
        for (int right = 0; right < s.Length; right++)
        {
            char current = s[right];
            window[current] = window.GetValueOrDefault(current, 0) + 1;

            // Count this required character as satisfied only when its exact need is reached.
            if (need.ContainsKey(current) && window[current] == need[current])
            {
                have++;
            }

            // A value of have == required means the current window is valid.
            // Keep shrinking to search for the smallest valid window ending at right.
            while (have == required)
            {
                int currentLen = right - left + 1;

                // Save the valid window before removing its left character.
                if (currentLen < bestLen)
                {
                    bestLen = currentLen;
                    bestLeft = left;
                }

                char outgoing = s[left];

                // Remove the outgoing character from the current window state.
                window[outgoing]--;

                // If a required count is now too small, the window becomes invalid.
                if (need.ContainsKey(outgoing) && window[outgoing] < need[outgoing])
                {
                    have--;
                }

                // Move left forward so the next candidate window is smaller.
                left++;
            }
        }

        // No valid window was found anywhere in s.
        if (bestLen == int.MaxValue)
        {
            return string.Empty;
        }

        // Return the smallest valid substring recorded during the scan.
        return s.Substring(bestLeft, bestLen);
    }
}
Time & Space Complexity

The diagram reports O(n) time for the sliding-window scan. The reason is that right only moves forward, and left also only moves forward. Each source character is added to the window at most once and removed at most once. Dictionary lookup and update in C# are O(1) on average. Auxiliary space means extra memory used by the algorithm. The need and window frequency dictionaries can use O(n + m) auxiliary space in the worst case, matching the final diagram.

Where it is used

This sliding-window pattern is useful when software needs the smallest or largest continuous range that satisfies a condition. Examples include finding a short text section containing required tokens, finding a time interval containing required event types, or maintaining a valid range while processing ordered records.

Why Interviewers Ask This

This problem tests whether you recognize the sliding-window pattern and can maintain changing state while two pointers move independently. It also tests frequency counting, repeated-character handling, and the difference between a substring and a subsequence. The interviewer can see whether you know exactly when to expand, when to shrink, and when to save the answer. It also tests correct C# Dictionary usage, accurate complexity reasoning, and your ability to explain a useful invariant.

Common interview mistakes
  1. Stopping the shrink step after moving left only once even though the window is still valid.
  2. Updating the best answer after removing s[left] instead of before removing it.
  3. Increasing have every time a required character appears instead of only when its required frequency is reached.
  4. Ignoring repeated characters in t and checking only whether each character appears once.
  5. Treating the answer as a subsequence instead of a continuous substring.
  6. Claiming constant auxiliary space even though the frequency dictionaries can grow with the input.
Interview tip

State the invariant before writing the shrinking loop: have == required means every required character currently has enough copies. Then explain that you record the valid window before removing s[left]. This makes the pointer order and correctness easy to justify.

Interviewer may ask next
What changes if t contains repeated characters, such as "AABC"?

The same sliding-window algorithm still works. The need dictionary would store A:2, B:1, and C:1. A contributes to have only when window['A'] reaches 2. During shrinking, if the A count later drops below 2, have decreases and the window becomes invalid. The invariant is unchanged because every required frequency still has to be satisfied. The diagram's sliding-window processing remains O(n), and auxiliary space remains O(n + m) in the worst case.

What changes if no valid window exists?

The main sliding-window logic does not change. bestLen stays int.MaxValue if have never reaches required for a valid candidate. After the scan, the method detects that value and returns string.Empty. This preserves the same correctness rule because only valid windows are recorded. The diagram's time and auxiliary-space bounds remain O(n) and O(n + m).

8. Find the shortest path visiting all nodes in a graph.CodingMediumMicrosoft

Question Details

Solve the shortest-walk-that-visits-every-node problem on an undirected connected graph, and clarify how state must include both position and which nodes have already been covered.

Short Interview Answer (30-60 seconds)

I would use multi-source BFS with a bitmask. Each state stores the current node and a mask showing which nodes have already been visited. I start BFS from every node at distance 0. From each state, I visit its neighbors and add the neighbor to the mask. BFS processes states in increasing distance, so the first state whose mask contains every node gives the shortest walk. The time complexity is O((n + m) * 2^n), and auxiliary space is O(n * 2^n).

Detailed Explanation

See the Code while reading this explanation.

We have a connected graph where every connection can be traveled in either direction. We need the smallest number of moves needed to visit every node at least once. We may begin at any node, and we are allowed to visit the same node or connection again. The important challenge is remembering both where we are now and which nodes we have already covered. The chosen method explores all useful possibilities in order from shorter walks to longer walks, so the first complete walk gives the minimum answer.

Useful Questions to Ask the Interviewer
  1. Is the graph guaranteed to be undirected and connected?
  2. Can I start from any node and revisit nodes and edges?
  3. Do you want only the minimum length, not the actual walk?
Find the shortest path visiting all nodes in a graph. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an undirected connected graph with nodes numbered from 0 to n - 1. The output is one integer: the minimum number of edges in a walk that visits every node at least once. We may start at any node. We may also revisit nodes and edges.

2. Choose the algorithm and state

I use multi-source BFS because every edge represents one move. Ordinary BFS that remembers only the current node is not enough. Reaching node 2 after visiting nodes {0, 2} is a different situation from reaching node 2 after visiting nodes {1, 2, 3}.

So each BFS state is (node, mask). The node is our current position. The mask is a bitmask that records which nodes have already been visited. The full mask is (1 << n) - 1. For the four-node example, the full mask is 1111 in binary, which is 15.

3. Initialize BFS from every possible starting node

The walk may start at any node, so I place one starting state for every node into the queue. For node i, the starting mask is 1 << i and the starting distance is 0.

For the example, the initial states are (0, 0001), (1, 0010), (2, 0100), and (3, 1000). Each state is marked as discovered when it is added to the queue.

4. Walk through the example

The example has nodes 0, 1, 2, and 3. Its undirected edges are (0-1), (0-2), (0-3), (1-2), (1-3), and (2-3).

At level 0, the queue contains the four starting states. Each mask has one bit set, so no state has visited all nodes.

At level 1, BFS reaches states with two visited nodes. For example, moving from node 0 to node 1 creates state (1, 0011).

At level 2, BFS can reach states with three visited nodes, such as (2, 0111). The full mask still has not been reached.

At level 3, BFS reaches a state such as (3, 1111). The mask 1111 means every node has been visited. BFS stops and returns 3. One valid shortest walk is 0 -> 1 -> 2 -> 3.

5. Explain why the result is correct

The main invariant is that BFS processes states in nondecreasing distance. A state is identified by both its current node and its visited mask. When a state is discovered for the first time, BFS has reached that state using the minimum possible number of edges. Therefore, the first dequeued state whose mask equals the full mask has the shortest possible walk length that visits every node.

6. Explain the C# implementation, complexity, and edge cases

The code uses dist[node, mask]. A value of -1 means that state has not been discovered. The queue stores pairs of current node and visited mask. For every neighbor, the next mask is currentMask | (1 << neighbor). This keeps every previously visited node and adds the neighbor.

If dist[neighbor, nextMask] is -1, the code stores currentDistance + 1 and adds the new state to the queue. When a dequeued state has the full mask, the code immediately returns its distance.

There are n * 2^n possible states. Processing their adjacency lists takes O((n + m) * 2^n) time, where m is the number of undirected edges. The distance table and queue use O(n * 2^n) auxiliary space. For n = 1, the answer is

  1. For two connected nodes, the answer is
  2. Dense graphs can have many valid shortest walks, but BFS still returns the minimum length.
Key Insight / Why This Solution Works

The key insight is that the current node alone is not enough to describe progress. We must also know which nodes have already been visited. I represent that visited set with a bitmask, so every BFS state is (currentNode, visitedMask). Because the walk may begin anywhere, the queue starts with one state for every node. The central invariant is that BFS processes states in increasing distance. Therefore, the first state whose mask has every node bit set has the minimum possible walk length. Marking each (node, mask) state when it is enqueued prevents repeated work.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Build the exact four-node undirected graph shown in the diagram.
        // Edges: 0-1, 0-2, 0-3, 1-2, 1-3, and 2-3.
        int[][] graph = { new[] { 1, 2, 3 }, new[] { 0, 2, 3 }, new[] { 0, 1, 3 },
                          new[] { 0, 1, 2 } };

        // Run the same multi-source BFS and bitmask algorithm as the diagram.
        int answer = ShortestPathLength(graph);

        // The expected result for this example is 3.
        Console.WriteLine(answer);
    }

    public static int ShortestPathLength(int[][] graph)
    {
        int n = graph.Length;

        // One node is already fully visited, so no edge is needed.
        if (n == 1)
        {
            return 0;
        }

        // fullMask has the lowest n bits set.
        // Reaching this mask means every node has been visited.
        int fullMask = (1 << n) - 1;

        // dist[node, mask] stores the shortest distance for that exact state.
        // A state contains both the current node and the visited-node mask.
        int[,] dist = new int[n, 1 << n];

        // Fill the table with -1 so -1 means the state is still unseen.
        for (int node = 0; node < n; node++)
        {
            for (int mask = 0; mask < (1 << n); mask++)
            {
                dist[node, mask] = -1;
            }
        }

        // Start BFS from every node because any node may be the starting point.
        Queue<(int Node, int Mask)> queue = new Queue<(int Node, int Mask)>();

        for (int node = 0; node < n; node++)
        {
            // Initially, only the starting node has been visited.
            int startMask = 1 << node;

            // Every starting state has distance 0.
            dist[node, startMask] = 0;
            queue.Enqueue((node, startMask));
        }

        // BFS processes states in nondecreasing distance order.
        while (queue.Count > 0)
        {
            (int currentNode, int currentMask) = queue.Dequeue();
            int currentDistance = dist[currentNode, currentMask];

            // The first dequeued full-mask state is the shortest valid walk.
            if (currentMask == fullMask)
            {
                return currentDistance;
            }

            // Try every one-edge move from the current node.
            foreach (int neighbor in graph[currentNode])
            {
                // Keep all previously visited nodes and add this neighbor.
                int nextMask = currentMask | (1 << neighbor);

                // Discover each exact (node, mask) state only once.
                // Its first discovery is its shortest distance because this is BFS.
                if (dist[neighbor, nextMask] == -1)
                {
                    dist[neighbor, nextMask] = currentDistance + 1;
                    queue.Enqueue((neighbor, nextMask));
                }
            }
        }

        // Defensive fallback. A connected graph should reach fullMask.
        return -1;
    }
}
Time & Space Complexity

Let n be the number of nodes and m be the number of undirected edges. There are n choices for the current node and 2^n possible visited masks, so there are at most n * 2^n states. Across these states, processing adjacency lists takes O((n + m) * 2^n) time. The distance table stores information for up to n * 2^n states, and the BFS queue can also contain many states. Therefore, the auxiliary space complexity is O(n * 2^n).

Where it is used

This state-space BFS pattern is useful when the shortest route depends on both the current location and a small set of things already completed. Examples include visiting required locations, collecting keys or items in a maze, and routing problems where the completed set can be represented by a bitmask.

Why Interviewers Ask This

This problem tests whether a candidate recognizes that ordinary node-only BFS state is not enough. The interviewer wants to see whether the candidate can combine graph traversal with a compact bitmask, start from multiple valid sources, maintain the correct visited-state invariant, and explain why the first full-mask state is optimal. It also tests accurate complexity analysis and the ability to implement the state transitions correctly in C#.

Common interview mistakes

A common mistake is marking only the graph node as visited. The same node can be reached after covering different sets of nodes, so the visited state must include both node and mask. Another mistake is starting BFS from only one node even though any node may be the start. Candidates may also mark states too late and enqueue the same state repeatedly. Another mistake is replacing the current mask instead of using OR to add the neighbor bit. Finally, the complexity must include adjacency traversal, not only the number of states.

Interview tip

Define the BFS state before writing code. Say clearly that (node, mask) means, "I am at this node, and this mask tells me every node I have already visited." Once that state is clear, the initialization, transition, stopping condition, and correctness proof become much easier to explain.

Interviewer may ask next
How would you return one actual shortest walk instead of only its length?

Keep the same multi-source BFS and the same (node, mask) state. Add a parent table that stores the previous state that first discovered each new state. When BFS reaches the first full-mask state, follow those parent links backward to a starting state and then reverse the collected nodes. Correctness stays the same because BFS still discovers each state first at its minimum distance. The time complexity remains O((n + m) * 2^n). The auxiliary space remains O(n * 2^n), with parent information using the same asymptotic amount of memory.

What changes if the graph is not guaranteed to be connected?

The BFS state and transitions do not change. We still start from every node and explore reachable (node, mask) states. If no walk can visit every node, the queue eventually becomes empty without reaching the full mask. In that version, returning -1 is a real possible result rather than only a defensive fallback. BFS still explores reachable states in increasing distance, so correctness is preserved. The time complexity remains O((n + m) * 2^n), and the auxiliary space remains O(n * 2^n).

9. For a given string L: write code to remove successive identical characters recursively.CodingEasyMicrosoft

Question Details

Collapse runs of adjacent identical characters repeatedly until no adjacent pair remains, and explain how recursion or iteration terminates on already-reduced strings.

Short Interview Answer (30-60 seconds)

I would repeatedly scan the string from left to right and process one maximal run of equal adjacent characters at a time. If a run has length one, I copy that character to the next string. If the run has length two or more, I remove the whole run. After each complete pass, I compare the new string with the current one. If they are equal, I return it. Worst-case time is O(n²), with O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is a string L. We need to remove complete groups of identical characters when those characters are next to each other. We remove the whole group when its length is at least two. Removing one group can make other equal characters become neighbors, so we repeat the process. We stop when one complete pass does not change the string. In the diagram example, "abbaca" becomes "aaca", then "ca", and the next pass leaves "ca" unchanged.

Useful Questions to Ask the Interviewer
  1. Should an entire adjacent run of length two or more be removed at once?
  2. Should removal continue until a complete pass makes no changes?
  3. Should an empty string be returned when every character is removed?
For a given string L: write code to remove successive identical characters recursively. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is one string, L. The output is the stable reduced string. A stable string has no adjacent run of two or more equal characters. For the diagram example, the input "abbaca" must finally become "ca".

2. Choose the repeated run-removal algorithm

I process the current string from left to right. I identify one maximal run at a time. A maximal run is the complete group of equal adjacent characters. If the run length is one, I copy that character into a new string. If its length is two or more, I copy nothing from that run. This removes the entire run.

3. Initialize and process one pass

I start with current = L. For each pass, I create an empty StringBuilder named next. Index i points to the start of a run. Index j moves forward while current[j] equals current[i]. Then j - i is the run length. If the length is one, I append current[i]. Otherwise, I omit the whole run. I then set i = j and continue with the next run.

4. Walk through the example

Start with current = "abbaca". In pass 1, the first "a" is a singleton, so it is copied. The maximal run "bb" has length two, so the whole run is removed. The following "a", "c", and "a" are singleton runs, so they are copied. The next string is "aaca".

In pass 2, current is "aaca". The maximal run "aa" has length two, so it is removed. The characters "c" and "a" are singleton runs, so they are copied. The next string is "ca".

In pass 3, current is "ca". Both characters are singleton runs, so the next string is also "ca". Because reduced == current, the algorithm stops and returns "ca".

5. Explain why the result is correct

During each pass, every maximal run of length two or more in the current string is removed, while singleton runs are kept in their original order. A removal can make previously separated equal characters become adjacent, so another pass may be needed. When a complete pass produces exactly the same string, no removable adjacent run remains. Therefore the returned string is fully reduced.

6. Explain the C# implementation and complexity

The C# code uses a StringBuilder to build the next string during each pass. The inner scan moves through the current string one maximal run at a time. After the pass, the code compares reduced with current. If they are equal, it returns current. Otherwise, it sets current = reduced and repeats. Each pass is linear in the current string length. Repeated passes give O(n²) worst-case time. The temporary result uses O(n) auxiliary space.

Key Insight / Why This Solution Works

The key idea is to remove complete maximal runs rather than cancel equal characters in pairs. During one pass, the algorithm scans from left to right and identifies each maximal run of equal adjacent characters. A run of length one is copied to the next string. A run of length two or more is omitted completely. The invariant is that after a pass, the next string contains exactly the singleton runs from the current string, in their original order. Because removing a run can create new adjacent equal characters, complete passes repeat until the newly built string equals the current string. At that point, no removable run remains.

Code
using System;
using System.Text;

public static class Program
{
    public static string RemoveSuccessiveDuplicates(string input)
    {
        // current is the string being processed in this reduction pass.
        string current = input;

        while (true)
        {
            // Build the result of one complete reduction pass.
            StringBuilder next = new StringBuilder();

            // Move through the string one maximal run at a time.
            for (int i = 0; i < current.Length;)
            {
                // j moves to the first position after the current run.
                int j = i + 1;
                while (j < current.Length && current[j] == current[i])
                {
                    j++;
                }

                // Keep singleton runs only. A run of length 2 or more is removed completely.
                if (j - i == 1)
                {
                    next.Append(current[i]);
                }

                // Continue at the beginning of the next run.
                i = j;
            }

            // Convert the result of this pass into the next candidate string.
            string reduced = next.ToString();

            // If a complete pass made no change, the string is fully reduced.
            if (reduced == current)
            {
                return current;
            }

            // A removal may create a new adjacent run, so process the reduced string again.
            current = reduced;
        }
    }

    public static void Main()
    {
        // Run the same verified example shown in the diagram.
        string input = "abbaca";
        string result = RemoveSuccessiveDuplicates(input);

        // The final stable result for "abbaca" is "ca".
        Console.WriteLine(result);
    }
}
Time & Space Complexity

Let n be the original string length. One pass scans the current string from left to right, so that pass is O(n) in the largest case. Several passes may be needed because removing one run can create another removable run. Therefore the repeated full-pass implementation has O(n²) worst-case time. During a pass, the algorithm builds a new string that can contain up to n characters. That means the auxiliary space is O(n).

Where it is used

This repeated reduction pattern is useful when text or symbolic data must be simplified until no more local removal rules apply. Similar ideas can appear in string cleanup, token reduction, elimination rules, and preprocessing steps where deleting one adjacent group can create a new group that must also be removed.

Why Interviewers Ask This

This question checks whether a candidate can translate a repeated string-reduction rule into correct code. It tests careful handling of maximal adjacent runs, state changes between passes, and a clear termination condition. It also checks whether the candidate notices that removing one run can create another removable run later. In C#, the interviewer can evaluate loop boundaries, StringBuilder use, equality checks, edge cases, and whether the stated complexity matches the actual implementation.

Common interview mistakes

One common mistake is removing duplicate characters only in pairs. That is not the same as removing the entire maximal run. For example, the run "aaa" must be removed completely. Another mistake is performing only one pass, even though one removal can create a new adjacent run. Candidates can also forget the reduced == current stopping condition and create an infinite loop. Another error is copying part of a run whose length is at least two. Finally, claiming O(n) total time is incorrect for this repeated full-pass implementation.

Interview tip

Explain the rule before writing code: scan one maximal run at a time, keep it only when its length is one, remove the whole run otherwise, and repeat full passes until the string no longer changes.

Interviewer may ask next
Can this solution be written recursively instead of using the outer while loop?

Yes. One recursive call can represent one complete reduction pass. Build the reduced string by removing every maximal run of length two or more. If the reduced string equals the input string, return it as the base case. Otherwise, recursively process the reduced string. The same correctness rule is preserved because every call performs one full reduction pass. The worst-case time remains O(n²). Auxiliary memory can be larger because recursion adds call-stack space in addition to the temporary strings.

What happens if the input string is already reduced?

The algorithm performs one verification pass. Every run has length one, so every character is copied and the reduced string is identical to current. The condition reduced == current is then true, so the method returns immediately. For an already-reduced string of length n, that verification pass takes O(n) time and uses O(n) auxiliary space for the temporary StringBuilder result.

10. Merge Overlapping Intervals.CodingEasyMicrosoft

Question Details

Merge interval ranges after sorting by start point, and explain how you treat touching endpoints, nested spans, and the exact shape of the merged output.

Short Interview Answer (30-60 seconds)

I would first sort the intervals by their start value. Then I scan them from left to right and keep a list of merged intervals. I compare each interval with the last interval in the result. If its start is less than or equal to the last end, they overlap or touch, so I extend the end using the larger end value. Otherwise, I append a new interval. The total time is O(n log n), and the result uses O(n) space.

Detailed Explanation

See the Code while reading this explanation.

We are given several closed number ranges. Some ranges may overlap, touch at an endpoint, or sit completely inside another range. We need to combine ranges that belong together. The final result must be sorted by start and must contain non-overlapping, maximal ranges. The main idea is to sort the ranges by their starting value first. After sorting, possible overlaps appear next to each other. We can then move from left to right, extend the current merged range when needed, and start a new range only when there is a real gap.

Useful Questions to Ask the Interviewer
  1. Should intervals that only touch at an endpoint be merged?
  2. May I sort the input array in place?
  3. Can the input array be empty?
Merge Overlapping Intervals. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of closed intervals. Each interval contains a start and an end. The output is a list of merged intervals sorted by start. The returned intervals must not overlap. Each returned interval must also be maximal, which means it cannot be extended by another interval that overlaps or touches it. In this solution, touching endpoints count as overlap because the intervals are treated as closed intervals.

2. Choose the algorithm

First, sort all intervals by their start value. Then scan the sorted intervals from left to right. Keep a result list containing the merged intervals built so far. For each current interval, compare it only with the last interval in the result. If currentStart <= lastEnd, the intervals overlap or touch. Merge them by setting the last end to max(lastEnd, currentEnd). Otherwise, there is a gap, so append the current interval as a new result interval.

3. Initialize the state

The diagram uses this input: [[5,7], [1,3], [2,6], [8,10], [15,18], [17,20]].

After sorting by start, it becomes [[1,3], [2,6], [5,7], [8,10], [15,18], [17,20]].

Start the result with the first sorted interval: [[1,3]].

The main invariant is that the result is always sorted and non-overlapping. Its last interval represents the complete merged span for the current group of overlapping or touching intervals processed so far.

4. Walk through the example

Start with result [[1,3]].

Step 1: The current interval is [2,6], and the last result interval is [1,3]. Since 2 <= 3, they overlap. Update the end to max(3,6) = 6. The result becomes [[1,6]].

Step 2: The current interval is [5,7], and the last result interval is [1,6]. Since 5 <= 6, they overlap. Update the end to max(6,7) = 7. The result becomes [[1,7]].

Step 3: The current interval is [8,10], and the last result interval is [1,7]. Since 8 > 7, there is a gap. Append [8,10]. The result becomes [[1,7], [8,10]].

Step 4: The current interval is [15,18], and the last result interval is [8,10]. Since 15 > 10, there is a gap. Append [15,18]. The result becomes [[1,7], [8,10], [15,18]].

Step 5: The current interval is [17,20], and the last result interval is [15,18]. Since 17 <= 18, they overlap. Update the end to max(18,20) = 20. The final result is [[1,7], [8,10], [15,20]].

5. Explain why the result is correct

Sorting by start places intervals that can belong to the same merged span next to each other. While scanning, the last result interval contains the complete merged range for the current group. If the next interval starts at or before the last end, extending the end keeps all covered values and also handles nested intervals correctly. If the next start is greater than the last end, there is a real gap. Because all later starts are at least as large, the previous merged interval can safely be finished.

6. Explain the C# implementation

The method first returns an empty array when the input is null or empty. It sorts the input array by interval start. It copies the first sorted interval into a List<int[]>. For every remaining interval, it reads the last merged interval. If the current start is less than or equal to the last end, it updates that end with Math.Max. Otherwise, it appends a copy of the current interval. Finally, it converts the result list to an array.

7. Explain complexity and edge cases

Sorting takes O(n log n) time. The merge scan takes O(n) time, so total time is O(n log n). The result can contain up to n intervals, so the result storage is O(n). Relevant edge cases are empty input, one interval, all intervals overlapping into one range, touching endpoints, negative values, and intervals completely nested inside another interval.

Key Insight / Why This Solution Works

The key insight is that sorting by start puts intervals that may need to merge next to each other. After sorting, one left-to-right scan is enough. The invariant is that the result is always sorted and non-overlapping, and its last interval is the maximal merged span for the current group. If currentStart <= lastEnd, merge by setting lastEnd = max(lastEnd, currentEnd). If currentStart > lastEnd, the previous span is complete, so append the current interval as a new span.

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

public static class Program
{
    public static void Main()
    {
        // Use the exact unsorted example shown in the diagram.
        int[][] intervals = { new[] { 5, 7 },  new[] { 1, 3 },   new[] { 2, 6 },
                              new[] { 8, 10 }, new[] { 15, 18 }, new[] { 17, 20 } };

        // Merge the intervals with the sort-and-scan algorithm from the diagram.
        int[][] merged = Merge(intervals);

        // Print the merged intervals in the same shape as the diagram result.
        Console.WriteLine(
            "[" + string.Join(", ", merged.Select(interval => $"[{interval[0]},{interval[1]}]")) +
            "]");
    }

    public static int[][] Merge(int[][] intervals)
    {
        // Handle empty input before trying to read the first interval.
        if (intervals == null || intervals.Length == 0)
        {
            return Array.Empty<int[]>();
        }

        // Sort by start value so intervals that may overlap appear next to each other.
        Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0]));

        // Start the result with a copy of the first sorted interval.
        List<int[]> result = new List<int[]> { intervals[0].ToArray() };

        // Process every remaining sorted interval from left to right.
        for (int i = 1; i < intervals.Length; i++)
        {
            // The last result interval is the current maximal merged span.
            int[] last = result[^1];
            int[] current = intervals[i];

            // Closed intervals overlap or touch when current.Start <= last.End.
            if (current[0] <= last[1])
            {
                // Extend the merged span only when the current interval reaches farther.
                last[1] = Math.Max(last[1], current[1]);
            }
            else
            {
                // A real gap starts a new merged interval, so append a copy.
                result.Add(current.ToArray());
            }
        }

        // Return the final sorted, non-overlapping, maximal intervals.
        return result.ToArray();
    }
}
Time & Space Complexity

Let n be the number of intervals. Sorting the intervals by start takes O(n log n) time. After sorting, the merge loop processes each remaining interval once, which takes O(n) time. Sorting is the larger cost, so the total time is O(n log n). The result list may contain up to n intervals, so the space used for the result is O(n). The code sorts the input array in place, so it does not create another full interval array just for sorting.

Where it is used

This interval-merging pattern is useful when software needs to combine overlapping ranges. Common examples include calendar time ranges, reservation windows, reporting periods, covered numeric ranges, memory or address ranges, and other systems where overlapping spans should be reduced to a smaller set of non-overlapping ranges.

Why Interviewers Ask This

This problem checks whether a candidate recognizes the sorting-and-interval pattern and can maintain a clear invariant during a scan. It also tests whether the candidate defines overlap correctly, handles touching endpoints and nested intervals, writes the merge condition correctly in C#, and explains why sorting makes the total time O(n log n). The interviewer can also see whether the candidate distinguishes merging from simply collecting intervals and considers relevant edge cases.

Common interview mistakes
  1. Forgetting to sort the intervals by start before scanning them.
  2. Using currentStart < lastEnd when touching closed endpoints are supposed to merge. The diagram uses currentStart <= lastEnd.
  3. Replacing the last end directly with the current end instead of using Math.Max. That can break nested intervals.
  4. Appending an interval even when it overlaps the last merged interval instead of updating the last interval.
  5. Claiming O(n) total time and forgetting that sorting costs O(n log n).
Interview tip

State the merge rule before coding: after sorting by start, compare each interval only with the last merged interval. If currentStart <= lastEnd, extend the end with Math.Max. Otherwise, append a new interval. This makes the walkthrough and code easy to follow.

Interviewer may ask next
What changes if the input intervals are already sorted by start?

The sorting step can be removed. Start with the first interval and run the same left-to-right merge scan. The overlap rule and invariant do not change, so correctness is preserved. The new time complexity is O(n) because the intervals only need one scan. The result still uses O(n) space in the worst case. The tradeoff is that this faster bound depends on the input already being correctly sorted.

What changes if touching endpoints should not be merged?

Change the overlap condition from currentStart <= lastEnd to currentStart < lastEnd. Then intervals such as [1,3] and [3,5] stay separate. Sorting and the rest of the scan stay the same. The invariant changes only in the definition of overlap. Time remains O(n log n), and result space remains O(n). The tradeoff is that the output now follows different interval semantics for touching ranges.

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.