43 Apple .NET Developer Interview Questions & Answers

apple icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

1. Groups of AnagramsCodingMediumApple

Question Details

Group strings by anagram class, explain the normalization key you use, and state how you preserve or order members within each group.

Short Interview Answer (30-60 seconds)

I would group the strings with a dictionary. For each string, I sort its characters and use that sorted string as the normalization key. Anagrams get the same key, so they go into the same list. I append each original string as I encounter it, which preserves member order inside each group. For n strings with average length k, the time is O(n * k log k). The auxiliary space is O(n * k).

Detailed Explanation

See the Code while reading this explanation.

We are given a list of strings. We need to put strings together when they contain the same characters with the same counts. For example, "eat", "tea", and "ate" belong together. The main idea is to turn every string into a common form by sorting its characters. Strings with the same sorted form belong to the same group. A dictionary stores one list for each sorted form. We process the input from left to right and append each original string to its group, so members keep their relative input order.

Useful Questions to Ask the Interviewer
  1. Should matching be case-sensitive?
  2. Does the order of the groups matter, or only the members inside each group?
  3. Should the relative input order of strings inside each group be preserved?
Groups of Anagrams diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an array of strings. The output is a list of lists. Each inner list contains one anagram group. In the diagram, the input is ["eat", "tea", "tan", "ate", "nat", "bat"]. One valid shown result is [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]. The diagram preserves the input encounter order of members inside each group.

2. Choose the normalization key and dictionary

For each string, sort its characters. The sorted string becomes the normalization key. For example, "eat", "tea", and "ate" all become "aet". The dictionary maps each sorted key to a list of the original strings that belong to that anagram class.

3. Initialize and process strings in input order

Start with an empty dictionary. Visit each string from left to right. Create its sorted key. If that key is not already in the dictionary, create a new empty list for it. Then append the original string to that list. Because we append strings in encounter order, the relative order of members inside every group is preserved.

4. Walk through the exact example

Step 1 processes "eat". Its sorted key is "aet", so the dictionary becomes {"aet": ["eat"]}.

Step 2 processes "tea". Its key is also "aet", so the dictionary becomes {"aet": ["eat", "tea"]}.

Step 3 processes "tan". Its key is "ant", so a new group is created: {"aet": ["eat", "tea"], "ant": ["tan"]}.

Step 4 processes "ate". Its key is "aet", so it is appended to the first group: {"aet": ["eat", "tea", "ate"], "ant": ["tan"]}.

Step 5 processes "nat". Its key is "ant", so the second group becomes ["tan", "nat"].

Step 6 processes "bat". Its key is "abt", so a third group is created. The shown result is [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]].

5. Explain why the result is correct

Two strings are anagrams when they contain the same characters with the same frequencies. Sorting those characters produces the same sequence for every string in the same anagram class. Therefore, all anagrams get the same dictionary key. Strings with different character multisets get different keys. Each dictionary key therefore represents one anagram class.

6. Explain the C# implementation

The code creates a Dictionary<string, IList<string>>. Each loop converts the current string to a char array, sorts it, and creates the normalization key. TryGetValue checks whether a group already exists. A new List<string> is created only when the key appears for the first time. The original string is appended to the group. Finally, the dictionary values are returned as the grouped result.

7. Explain complexity and edge cases

If there are n strings and the average string length is k, sorting one string costs O(k log k). Doing this for all strings costs O(n * k log k). Dictionary lookup and insertion are O(1) on average. The auxiliary space is O(n * k) for the stored normalization keys and groups. An empty input gives an empty result. One string gives one group. Duplicate strings stay together. Different lengths produce different sorted keys. The shown solution is case-sensitive.

Key Insight / Why This Solution Works

The key insight is that each anagram class can be represented by one normalized string. The diagram uses the characters sorted in ascending order as that normalization key. For example, "eat", "tea", and "ate" all produce "aet". A dictionary stores sorted key -> list of original strings. The central invariant is: after each input string is processed, every processed string is stored in the list for its exact sorted-character key. Because strings are appended as they are encountered, their relative input order is preserved inside each group.

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

public static class Program
{
    public static void Main()
    {
        // Use the exact example shown in the approved diagram.
        string[] input = { "eat", "tea", "tan", "ate", "nat", "bat" };

        // Group the input strings by their sorted-character normalization key.
        IList<IList<string>> groups = GroupAnagrams(input);

        // Print the result as a compact list of lists for the example run.
        string output =
            "[" +
            string.Join(",",
                        groups.Select(
                            group => "[" + string.Join(",", group.Select(s => $"\"{s}\"")) + "]")) +
            "]";

        Console.WriteLine(output);
    }

    public static IList<IList<string>> GroupAnagrams(string[] strs)
    {
        // Each key is a sorted string. Each value stores original strings in encounter order.
        var groups = new Dictionary<string, IList<string>>();

        // Process the input from left to right so order inside each group is preserved.
        foreach (string s in strs)
        {
            // Sort a copy of the current string's characters to create its normalization key.
            char[] chars = s.ToCharArray();
            Array.Sort(chars);
            string key = new string(chars);

            // Create a group only when this normalization key appears for the first time.
            if (!groups.TryGetValue(key, out IList<string>? list))
            {
                list = new List<string>();
                groups[key] = list;
            }

            // Append the original string, preserving relative input order inside this group.
            list.Add(s);
        }

        // Return all completed anagram groups after the full input has been processed.
        return groups.Values.ToList();
    }
}
Time & Space Complexity

Let n be the number of strings and k be the average string length. We sort the characters of every string. Sorting one string takes O(k log k), so the total time is O(n * k log k). Dictionary lookup and insertion are O(1) on average, so they do not change the sorting-based total. The auxiliary space is O(n * k) because the algorithm stores normalization keys and grouped strings. C# Dictionary<TKey, TValue> lookup and insertion are average-case O(1), not guaranteed worst-case O(1).

Where it is used

This pattern is useful when several original values need to be grouped by one shared canonical form. Examples include grouping equivalent words, organizing normalized identifiers, detecting duplicate records after normalization, and building indexes where many original values belong to the same normalized key.

Why Interviewers Ask This

This problem checks whether you can recognize that different strings can share one canonical representation. It tests dictionary design, string normalization, grouping logic, order preservation, and careful complexity analysis. The interviewer can also see whether you understand why sorting creates a correct key, whether duplicates are grouped correctly, whether you can write consistent C# collection types, and whether you distinguish average dictionary behavior from guaranteed worst-case behavior.

Common interview mistakes

A common mistake is using the original string as the dictionary key instead of a normalized key, which does not group anagrams. Another mistake is creating a new list every time a key appears and losing earlier members. Candidates may sort or reorder the original input instead of sorting a character copy only for the key. They may also unnecessarily sort members inside each group and lose the encounter order shown in the diagram. Another common mistake is claiming O(n) time while ignoring the O(k log k) character-sorting cost for each string.

Interview tip

Explain the normalization key first: "I sort each string to get a canonical form, and that sorted form identifies its anagram group." Then show one concrete example such as "tea" -> "aet" before discussing the dictionary and complexity.

Interviewer may ask next
How would you reduce the cost of sorting every string if the input is limited to lowercase English letters?

I would replace the sorted-string key with a fixed 26-count frequency key. For each string, I count how many times each letter appears and encode those counts into the dictionary key. Two strings are anagrams exactly when all 26 counts match, so correctness is preserved. Building one key takes O(k), and dictionary operations are O(1) on average, giving O(n * k) expected time. Auxiliary space remains O(n * k) overall for stored keys and groups. The tradeoff is that this approach depends on a known bounded alphabet.

How would the solution change if members inside each group had to be returned in lexicographical order instead of encounter order?

I would keep the same sorted-character normalization key and dictionary grouping step. After grouping, I would sort each group's list lexicographically before returning it. Membership remains correct because the normalization key still decides the anagram class. If group g has m_g strings, the added sorting cost is O(k * m_g log m_g) for that group when string comparisons can inspect up to k characters. Across all groups, the added cost is O(k * sum(m_g log m_g)), bounded by O(n * k log n). The overall auxiliary space remains O(n * k), with only small additional sorting stack space. The tradeoff is extra sorting work to get lexicographical member order.

2. Find the kth largest element in an unsorted arrayCodingHardApple

Question Details

Use a heap or selection strategy, explain why the structure is sized to k rather than n, and cover the follow-up choice between heap and quickselect.

Short Interview Answer (30-60 seconds)

I would keep a min-heap with at most k values. I process the array from left to right. Until the heap has k values, I add each number. After that, I replace the heap root only when the current number is larger. This keeps the k largest values seen so far, with the smallest of those values at the root. After all values are processed, the root is the kth largest value. The time is O(n log k), and the auxiliary space is O(k).

Detailed Explanation

See the Code while reading this explanation.

We are given an unsorted array and a number k. We need to return the kth largest value, not its index. For the example [7, 10, 4, 3, 20, 15] with k = 3, the answer is 10. I use a min-heap that stores at most k values. It keeps only the strongest k candidates seen so far, so we do not need to store all n values.

Useful Questions to Ask the Interviewer
  1. Is k always between 1 and the array length?
  2. Should duplicate values count as separate elements when finding the kth largest value?
  3. If we discuss Quickselect as a follow-up, is modifying the input array allowed?
Find the kth largest element in an unsorted array diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an integer array and an integer k. The output is the kth largest value in the array. We return a value, not an index. In the diagram example, nums = [7, 10, 4, 3, 20, 15] and k = 3. The three largest values are 20, 15, and 10, so the required result is 10.

2. Choose a min-heap of size k

I use a min-heap. In C#, PriorityQueue<int, int> works because each number can be stored as both the element and its priority. PriorityQueue removes the element with the lowest priority first, so the heap root is the smallest value among the values we keep.

The central invariant is that after each processed number, the heap contains the k largest values seen so far, or all values seen so far when fewer than k values have been processed. We use a heap of size k rather than n because only the top k values can affect the final answer. This gives O(k) auxiliary space.

3. Build the initial heap

Start with an empty min-heap. Process the array from left to right. While the heap contains fewer than k elements, insert the current value.

Process 7 first. The heap becomes [7].

Process 10 next. The heap contains 7 and 10, with 7 at the root.

Process 4 next. The heap now contains {4, 10, 7}, with 4 at the root. The heap has reached k = 3 values.

4. Process the remaining values

The next value is 3. The heap root is 4. Since 3 <= 4, it cannot be one of the largest three values seen so far. We ignore 3. The heap remains {4, 10, 7}.

The next value is 20. Since 20 > 4, remove the root 4 and insert 20. The heap contains {7, 10, 20}, with 7 as the root.

The final value is 15. Since 15 > 7, remove the root 7 and insert 15. The heap contains {10, 15, 20}, with 10 as the root.

All input values have now been processed, so processing stops. The heap root is 10, which is the third largest value.

5. Explain why the result is correct

The heap always keeps the k largest values seen so far. When the heap is full, its root is the smallest value among those k values. A new value that is not larger than the root cannot enter the top k, so it can safely be ignored. A value larger than the root must replace that smallest retained value. After the full array has been processed, the heap therefore contains the k largest values in the array. Its root is the smallest of those k values, which is exactly the kth largest value.

6. Explain the C# implementation

The code creates PriorityQueue<int, int>. Each array value is used as both its element and numeric priority. For every number, the code first checks whether the heap contains fewer than k values. If so, it enqueues the number. Once the heap is full, it compares the current number with Peek(). If the current number is larger, it removes the root with Dequeue() and inserts the new number. Otherwise, it leaves the heap unchanged. After the loop, Peek() returns the kth largest value.

7. Explain complexity, edge cases, and the Quickselect follow-up

There are n input values. The heap never contains more than k values. Heap insertion and removal each cost O(log k), so the overall time is O(n log k). The heap uses O(k) auxiliary space.

Relevant edge cases include k = 1, where the answer is the maximum value, and k = n, where the answer is the minimum value. Equal values, negative values, and duplicate values are handled naturally.

For the follow-up, Quickselect can find the kth largest value in expected or average O(n) time. An iterative in-place implementation can use O(1) auxiliary space, but its worst-case time is O(n²). A recursive Quickselect implementation also uses call-stack space and can reach O(n) stack space in the worst case. The heap approach is especially useful when k is small compared with n or when values arrive as a stream.

Key Insight / Why This Solution Works

The key insight is that we never need to keep more than the k largest candidates seen so far. A min-heap is useful because its root is the smallest value among those candidates. While the heap has fewer than k values, every new value is inserted. After the heap reaches size k, a new value matters only when it is larger than the root. In that case, the root is removed and the new value is inserted. The invariant is that after each processed value, the heap contains the k largest values seen so far, or every seen value when fewer than k values have been processed. At the end, the root is therefore the kth largest value.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static int FindKthLargest(int[] nums, int k)
    {
        // Keep at most k candidate values.
        // Lower numeric priorities dequeue first, so this works as a min-heap.
        PriorityQueue<int, int> minHeap = new PriorityQueue<int, int>();

        // Process every input value from left to right.
        foreach (int number in nums)
        {
            // Until the heap reaches size k, every value is a candidate.
            if (minHeap.Count < k)
            {
                // Store the number as both the element and its numeric priority.
                // The smallest number therefore stays at the front of the heap.
                minHeap.Enqueue(number, number);
            }
            // Once the heap is full, the root is the smallest current top-k value.
            else if (number > minHeap.Peek())
            {
                // The new number belongs in the top k.
                // Remove the smallest retained value first.
                minHeap.Dequeue();

                // Insert the larger replacement while keeping the heap size at k.
                minHeap.Enqueue(number, number);
            }
            // If number <= minHeap.Peek(), it cannot enter the current top k.
            // No state change is needed in that case.
        }

        // The final heap contains the k largest values.
        // Its smallest value is exactly the kth largest overall.
        return minHeap.Peek();
    }

    public static void Main()
    {
        // Exact example from the approved diagram.
        int[] nums = { 7, 10, 4, 3, 20, 15 };
        int k = 3;

        // The three largest values are 20, 15, and 10.
        // Therefore, the third largest value is 10.
        int result = FindKthLargest(nums, k);
        Console.WriteLine(result);
    }
}
Time & Space Complexity

Let n be the number of values in the array. We process all n values. The heap stores at most k values. Adding or removing a value from a heap of size at most k costs O(log k). Therefore, the total time is O(n log k). The auxiliary space is O(k) because the heap never stores more than k values. For the diagram example, k = 3, so the heap never stores more than three numbers.

Where it is used

This top-k heap pattern is useful when software needs to keep the largest k items without sorting or storing every candidate in another structure. Examples include keeping the highest scores, largest measurements, highest bids, or strongest results from a stream. It is especially useful when k is much smaller than the total number of values.

Why Interviewers Ask This

This problem checks whether a candidate recognizes a top-k pattern and chooses the correct heap direction. It tests whether the candidate can keep the heap limited to k values, maintain a clear invariant, handle duplicates and edge cases, and explain O(n log k) time with O(k) auxiliary space correctly. The follow-up also tests whether the candidate understands the tradeoff between a predictable heap-based method and Quickselect, which has better expected time but O(n²) worst-case time.

Common interview mistakes

A common mistake is using the wrong heap direction. With this size-k approach, we need a min-heap so the smallest value among the current top k is easy to remove. Another mistake is allowing the heap to grow to n elements, which loses the O(k) space benefit. Candidates may also forget to remove the root before inserting a larger replacement, compare the new number with the wrong heap value, or say the complexity is O(n log n) even though the heap is limited to k elements. Another mistake is claiming that Quickselect has guaranteed O(n) time. Its expected or average time is O(n), while its worst-case time is O(n²).

Interview tip

State the invariant early: the heap contains the k largest values seen so far, and its root is the smallest of those values. That single statement explains why smaller values can be ignored, why larger values replace the root, and why the final root is the answer.

Interviewer may ask next
When would you choose Quickselect instead of the size-k min-heap?

I would consider Quickselect when the complete array is already available, modifying its order is acceptable, and better expected running time is important. Quickselect partitions the array until the kth largest value reaches its target position. Its expected or average time is O(n), but its worst-case time is O(n²). An iterative in-place implementation can use O(1) auxiliary space. A recursive implementation uses call-stack space and can reach O(n) stack space in the worst case. The tradeoff is that the heap gives O(n log k) time with O(k) extra space and works especially well when k is small, while Quickselect offers better expected time for an in-memory array.

How would this solution change if the values arrived as a stream?

The same size-k min-heap works naturally for a stream. I would keep the heap between incoming values. If the heap contains fewer than k values, I insert the new value. Otherwise, I compare it with the root and replace the root only when the new value is larger. After at least k values have arrived, the root is the kth largest value seen so far. The heap still uses O(k) auxiliary space. Processing a new value takes O(log k) when the heap changes and O(1) for the comparison when it does not.

3. Validate that a binary tree is a valid binary search treeCodingHardApple

Question Details

Propagate lower and upper bounds through recursion, explain why parent-only checks fail, and cover strict versus non-strict comparisons.

Short Interview Answer (30-60 seconds)

I would validate the tree with recursive DFS while carrying a lower and an upper bound for every node. The root starts with the widest possible bounds. Each node must be strictly between its bounds. For the left child, I replace the upper bound with the current value. For the right child, I replace the lower bound. This checks constraints from every ancestor, not only the parent. The time complexity is O(n), and the recursion stack uses O(h) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is the root of a binary tree. We need to return true only when the whole tree follows the binary search tree rule. A node in a left subtree must respect every upper limit created by its ancestors. A node in a right subtree must respect every lower limit. The solution carries these allowed limits through recursion. This catches deeper violations that a simple parent-child check can miss.

Useful Questions to Ask the Interviewer
  1. Should duplicate values be rejected, meaning we are using a strict BST rule?
  2. Should an empty tree be considered a valid BST?
Validate that a binary tree is a valid binary search tree diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the root of a binary tree. The output is a boolean value. We return true when the whole tree is a valid BST. Otherwise, we return false.

The diagram uses a strict BST policy. Every value in a left subtree must be smaller than the ancestor value that limits it. Every value in a right subtree must be larger. Duplicates are therefore not allowed.

2. Use recursive DFS with lower and upper bounds

Each recursive call receives the current node and an allowed interval, written as (lower, upper). The current node must satisfy lower < node.Value < upper.

The root starts with lower = long.MinValue and upper = long.MaxValue. A null node returns true because an empty subtree cannot violate the BST rule.

If node.Value <= lower or node.Value >= upper, the current subtree is invalid and the helper returns false.

For the left child, the current node value becomes the new upper bound. The recursive call is Validate(node.Left, lower, node.Value).

For the right child, the current node value becomes the new lower bound. The recursive call is Validate(node.Right, node.Value, upper).

3. Explain why parent-only checks fail

Checking only the immediate parent is not enough. The diagram shows root 10, with left child 5, right child 15, and node 12 as the right child of 5.

Locally, 12 is greater than 5, so that parent-child relationship looks valid. But 12 is still inside the left subtree of 10. Therefore, it must also be less than 10. The propagated range for that node has upper = 10. Since 12 >= 10, the bounds-based method correctly returns false.

4. Walk through the verified example

The verified tree has root 20. Its left child is 8 and its right child is 22. Node 8 has children 4 and 12. Node 12 has children 10 and 14. Node 22 has right child 25.

Node 20 starts with the range (-infinity, +infinity). It is valid. Its left subtree receives (-infinity, 20), and its right subtree receives (20, +infinity).

Node 8 is valid inside (-infinity, 20). Its left child 4 receives (-infinity, 8). Its right child 12 receives (8, 20).

Node 4 is valid inside (-infinity, 8). Its null children return true.

Node 12 is valid inside (8, 20). Its left child 10 receives (8, 12). Its right child 14 receives (12, 20).

Node 10 satisfies 8 < 10 < 12. Node 14 satisfies 12 < 14 < 20. Their null children return true.

Node 22 satisfies 20 < 22 < +infinity. Its right child 25 receives (22, +infinity), and 25 is valid in that range. All subtrees return true, so the final result is true.

5. Explain strict versus non-strict comparisons

The code and main example use the strict BST rule. A node must satisfy lower < value < upper, so duplicates are rejected.

A non-strict BST definition can allow equality. Under the policy shown in the diagram, left-subtree values may be <= the node and right-subtree values may be >= the node. The important point is to choose one duplicate policy and apply it consistently throughout the whole tree.

6. Explain why the algorithm is correct

The invariant is that every recursive call carries the complete valid range created by all ancestors above the current node. A node is accepted only when its value is inside that range. Passing node.Value as the new upper bound on the left and as the new lower bound on the right keeps all ancestor restrictions active at every depth. Therefore, if both recursive calls succeed at every node, the whole tree satisfies the BST property.

7. Explain complexity and edge cases

Each node is visited once, so the time complexity is O(n), where n is the number of nodes. The recursion stack uses O(h) auxiliary space, where h is the tree height. For a balanced tree, this is O(log n). For a completely skewed tree, this can become O(n).

An empty tree returns true. A single-node tree returns true. A skewed tree is handled correctly. In strict mode, duplicate values make the tree invalid.

Key Insight / Why This Solution Works

The key insight is that BST validity is a global property. A node must satisfy restrictions created by every ancestor, not only its direct parent. The recursive helper therefore carries an open interval, (lower, upper). The invariant is: when Validate(node, lower, upper) starts, every valid value at that node must be strictly between those bounds. Going left changes only the upper bound to node.Value. Going right changes only the lower bound to node.Value. This is why the method catches deeper violations such as value 12 appearing in the left subtree of 10 even though 12 is greater than its direct parent 5.

Code
using System;

public sealed class TreeNode
{
    public int Value;
    public TreeNode? Left;
    public TreeNode? Right;

    public TreeNode(int value)
    {
        // Store the integer value represented by this node.
        Value = value;
    }
}

public sealed class Solution
{
    public bool IsValidBST(TreeNode? root)
    {
        // Start with bounds wider than the complete Int32 range.
        // This avoids boundary problems for int.MinValue and int.MaxValue.
        return Validate(root, long.MinValue, long.MaxValue);
    }

    private bool Validate(TreeNode? node, long lower, long upper)
    {
        // A null subtree cannot violate the BST ordering rule.
        if (node is null)
        {
            return true;
        }

        // Strict BST mode requires lower < node.Value < upper.
        // Equality is rejected, so duplicate values are not allowed.
        if (node.Value <= lower || node.Value >= upper)
        {
            return false;
        }

        // The left subtree inherits the lower bound and receives the
        // current value as its new upper bound.
        bool leftIsValid = Validate(node.Left, lower, node.Value);

        // If the left subtree already failed, the whole subtree is invalid.
        if (!leftIsValid)
        {
            return false;
        }

        // The right subtree receives the current value as its new lower
        // bound and keeps the existing upper bound.
        return Validate(node.Right, node.Value, upper);
    }
}

public static class Program
{
    public static void Main()
    {
        // Build the exact verified example from the diagram:
        //          20
        //        /    \n        //       8      22
        //      / \      \n        //     4   12      25
        //        /  \n        //       10  14
        TreeNode root = new TreeNode(
            20) { Left = new TreeNode(8) { Left = new TreeNode(4),
                                           Right = new TreeNode(12) { Left = new TreeNode(10),
                                                                      Right = new TreeNode(14) } },
                  Right = new TreeNode(22) { Right = new TreeNode(25) } };

        // Run the same strict bounds-based validation shown in the diagram.
        Solution solution = new Solution();
        bool result = solution.IsValidBST(root);

        // Every node satisfies its propagated range, so this prints True.
        Console.WriteLine(result);
    }
}
Time & Space Complexity

The time complexity is O(n), where n is the number of nodes. Each node is visited once, and each visit does constant work. The auxiliary space is O(h), where h is the height of the tree, because recursive calls use the call stack. For a balanced tree, h is O(log n). For a completely skewed tree, h can be O(n). No additional collection grows with the number of nodes.

Where it is used

This bounds-propagation pattern is useful when validating hierarchical data where every child must obey restrictions inherited from its ancestors. For binary search trees, it validates the global ordering rule without storing the nodes in another collection. The same recursive pattern can also be useful when deeper states must stay inside limits established earlier in the traversal.

Why Interviewers Ask This

This problem tests whether a candidate recognizes that BST validation needs a global constraint rather than only local parent-child checks. It also tests recursive reasoning, maintaining an invariant, choosing correct base cases, updating state differently for left and right subtrees, handling duplicate policies, and explaining time and recursion-stack complexity. In C#, it also checks whether the candidate handles integer boundary values safely by using a wider type for the propagated bounds.

Common interview mistakes

A common mistake is checking only the current node against its immediate children. That misses violations caused by earlier ancestors. Another mistake is updating the wrong bound: the left call must change the upper bound, while the right call must change the lower bound. Candidates may also forget the null base case or accidentally allow equality when the BST is strict. Another mistake is using int.MinValue and int.MaxValue as exclusive bounds without considering nodes that can contain those exact values. Finally, claiming O(1) auxiliary space is incorrect because recursive calls use O(h) stack space.

Interview tip

State the invariant before writing code: every recursive call receives the complete allowed range for that node from all of its ancestors. Then the two recursive calls follow naturally: left gets (lower, node.Value), and right gets (node.Value, upper).

Interviewer may ask next
How would the solution change if duplicate values were allowed?

The duplicate policy must be defined first. The diagram shows a non-strict policy where left-subtree values may be <= the node and right-subtree values may be >= the node. The recursive bounds would therefore need to track whether each boundary is inclusive or exclusive instead of using only strict comparisons. The same invariant still applies: every recursive call carries the full allowed range from all ancestors. The time complexity remains O(n), and auxiliary space remains O(h). The tradeoff is more complicated boundary handling.

What happens if the tree is extremely deep and skewed?

The recursive algorithm is still correct and still takes O(n) time, but a skewed tree has height h = n. That means the recursion stack can grow to O(n) and may risk stack overflow for a very deep tree. The same bounds-based logic can be implemented iteratively with an explicit stack that stores each node together with its lower and upper bounds. The iterative version still takes O(n) time and O(h) auxiliary space. The main tradeoff is using an explicit collection instead of the call stack.

4. Implement a stack with a getMinimum() function in O(1)CodingMediumApple

Question Details

Show how the auxiliary stack tracks the running minimum, explain push and pop behavior, and cover why a naive scan is not acceptable.

Short Interview Answer (30-60 seconds)

I would use two stacks. The main stack stores every pushed value, and a second minimum stack stores each new or equal running minimum. On push, I also push to the minimum stack when the value is less than or equal to its top. On pop, I remove from both stacks when the popped value is the current minimum. Then getMinimum() returns Min.Peek() directly. Each operation is O(1), and the auxiliary stack uses O(n) space in the worst case.

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to build a normal stack that can also tell us its smallest current value immediately. We need push, pop, top, and getMinimum operations. A simple solution could look through every value whenever getMinimum is called, but that becomes slower as the stack grows. Instead, we keep a second stack that remembers each new or equal minimum. This lets us update the minimum during push and pop, so getMinimum can return the answer immediately.

Useful Questions to Ask the Interviewer
  1. Should pop(), top(), and getMinimum() throw an exception when the stack is empty?
  2. Should duplicate minimum values be handled correctly when the same minimum is pushed more than once?
Implement a stack with a getMinimum() function in O(1) diagram
How to Explain It in an Interview
1. Understand the required behavior

We need a stack with four operations: push, pop, top, and getMinimum. The important requirement is that getMinimum must run in O(1) time. The diagram uses a main stack called S and an auxiliary stack called Min. S stores every pushed value. Min stores new or equal running minimum values.

2. Choose the two-stack approach

The key rule is that Min.Peek() must always be the current minimum value in S. When a new value x is pushed, it always goes into S. It also goes into Min when Min is empty or x is less than or equal to Min.Peek(). The less-than-or-equal check is important because equal minimum values must be tracked separately.

A naive approach would scan S whenever getMinimum() is called. That would make getMinimum() O(n). The Min stack avoids the scan because the minimum is already available at its top.

3. Explain push and pop behavior

For Push(x), first push x onto S. Then check Min. If Min is empty or x <= Min.Peek(), also push x onto Min.

For Pop(), first make sure S is not empty. Remove the top value from S. If that removed value equals Min.Peek(), also pop Min. This exposes the previous running minimum. Top() returns S.Peek(). GetMinimum() returns Min.Peek(). The diagram treats operations on an empty stack as errors.

4. Walk through the verified example

Start with S = [] and Min = [].

Step 1: Push(5). S becomes [5]. Min is empty, so 5 is also pushed to Min. Min becomes [5]. The minimum is 5.

Step 2: Push(3). S becomes [5, 3]. Since 3 <= 5, push 3 to Min. Min becomes [5, 3]. The minimum is 3.

Step 3: Push(7). S becomes [5, 3, 7]. Since 7 > 3, Min stays [5, 3]. The minimum stays 3.

Step 4: Push(2). S becomes [5, 3, 7, 2]. Since 2 <= 3, push 2 to Min. Min becomes [5, 3, 2]. The minimum is 2.

Step 5: GetMinimum() returns Min.Peek(), which is 2. Neither stack changes.

Step 6: Pop() removes and returns 2 from S. Because 2 equals Min.Peek(), 2 is also removed from Min. S becomes [5, 3, 7]. Min becomes [5, 3]. The current minimum is now 3.

Step 7: GetMinimum() returns 3. Neither stack changes.

Step 8: Pop() removes and returns 7 from S. Since 7 does not equal Min.Peek(), Min stays [5, 3]. S becomes [5, 3]. The current minimum remains 3.

5. Explain why the result is correct

The invariant is that Min contains the running minimum values needed for the current contents of S. From bottom to top, its values are non-increasing. Whenever a pushed value becomes a new minimum or equals the current minimum, that value is added to Min. Whenever the current minimum is popped from S, its matching entry is also removed from Min. Therefore, while the stack is not empty, Min.Peek() always equals the minimum value currently stored in S.

6. Explain the C# implementation

The class keeps two Stack<int> fields. Push updates both stacks when needed. Pop removes from S first and removes from Min only when the popped value equals the current minimum. Top reads S.Peek(), and GetMinimum reads Min.Peek(). The executable example performs the same eight operations shown in the diagram: Push(5), Push(3), Push(7), Push(2), GetMinimum(), Pop(), GetMinimum(), Pop().

7. Explain complexity and edge cases

Push, pop, top, and getMinimum are all O(1) because each performs only a constant number of stack operations. The auxiliary Min stack can contain up to n values, so auxiliary space is O(n). A strictly decreasing sequence is a worst-case example because every pushed value becomes a new minimum. Empty pop, top, and getMinimum operations throw an InvalidOperationException. Duplicate minima work because equal minimum values are also pushed onto Min.

Key Insight / Why This Solution Works

Use two stacks and maintain one invariant: while S is not empty, Min.Peek() equals the minimum value currently stored in S. The main stack S stores every pushed value. Min stores a value only when it is a new minimum or is equal to the current minimum. Push updates Min when x <= Min.Peek(). Pop removes from Min when the value removed from S equals Min.Peek(). A naive scan would make getMinimum() O(n), but maintaining Min lets getMinimum() return its top value in O(1).

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Create the two-stack data structure in its initial empty state.
        MinStack stack = new MinStack();

        // Execute the same four pushes shown in the diagram.
        stack.Push(5);
        stack.Push(3);
        stack.Push(7);
        stack.Push(2);

        // Step 5: the auxiliary stack exposes the current minimum, 2.
        int firstMinimum = stack.GetMinimum();
        Console.WriteLine(firstMinimum);

        // Step 6: pop 2. Because 2 is the current minimum, both stacks remove it.
        int firstPopped = stack.Pop();
        Console.WriteLine(firstPopped);

        // Step 7: the previous running minimum, 3, is now exposed again.
        int secondMinimum = stack.GetMinimum();
        Console.WriteLine(secondMinimum);

        // Step 8: pop 7. It is not the current minimum, so Min does not change.
        int secondPopped = stack.Pop();
        Console.WriteLine(secondPopped);
    }
}

public sealed class MinStack
{
    // The main stack stores every pushed value.
    private readonly Stack<int> s = new Stack<int>();

    // The auxiliary stack stores each new or equal running minimum.
    private readonly Stack<int> min = new Stack<int>();

    public void Push(int x)
    {
        // Every pushed value belongs in the main stack.
        s.Push(x);

        // Record x when it becomes a new minimum or duplicates the current minimum.
        // Keeping equal minima lets later pops restore the correct minimum.
        if (min.Count == 0 || x <= min.Peek())
        {
            min.Push(x);
        }
    }

    public int Pop()
    {
        // The diagram treats popping an empty stack as an error.
        if (s.Count == 0)
        {
            throw new InvalidOperationException("Stack is empty");
        }

        // Remove the top value from the main stack first.
        int value = s.Pop();

        // If the removed value was the current minimum, remove its matching entry.
        // The next Min entry then becomes the previous running minimum.
        if (value == min.Peek())
        {
            min.Pop();
        }

        // Return the value removed from the main stack.
        return value;
    }

    public int Top()
    {
        // The diagram treats reading the top of an empty stack as an error.
        if (s.Count == 0)
        {
            throw new InvalidOperationException("Stack is empty");
        }

        // The main stack directly exposes its current top value.
        return s.Peek();
    }

    public int GetMinimum()
    {
        // There is no current minimum when the stack is empty.
        if (min.Count == 0)
        {
            throw new InvalidOperationException("Stack is empty");
        }

        // By the invariant, the top of Min is the minimum value currently in s.
        return min.Peek();
    }
}
Time & Space Complexity

Each push, pop, top, and getMinimum operation takes O(1) time. Each method performs only a constant number of Stack<int> Push, Pop, Peek, or Count operations. The auxiliary Min stack can grow with the main stack. In the worst case, such as values pushed in strictly decreasing order, every value is also stored in Min. Therefore the auxiliary space is O(n), where n is the number of values currently in the stack.

Where it is used

This pattern is useful when software needs normal stack behavior and also needs a running minimum without scanning all active values. It can appear in algorithms that repeatedly push and pop states while querying the smallest active value, or in data-processing components that maintain nested state together with a fast minimum query.

Why Interviewers Ask This

This problem tests whether you can trade extra memory for faster operations. The interviewer wants to see whether you recognize that repeatedly scanning the main stack would make getMinimum too slow. It also tests whether you can maintain an invariant as state changes, handle duplicate minimum values correctly, preserve the right push and pop order, write safe C# stack operations, and explain why all four operations take O(1) time while the auxiliary stack can require O(n) space.

Common interview mistakes

A common mistake is scanning the main stack inside getMinimum(), which makes that operation O(n). Another mistake is using only x < Min.Peek() during push. The condition must use <= so duplicate minimum values are tracked correctly. Candidates may also forget to pop Min when the value removed from S equals Min.Peek(), which leaves a stale minimum. Another error is trying to pop or peek an empty stack without handling it. Finally, the extra space is O(n), not O(1), because Min can grow with S.

Interview tip

State the invariant before writing the methods: "While the stack is not empty, Min.Peek() is the minimum value currently in S." Then explain how every push and pop preserves that invariant.

Interviewer may ask next
What happens if duplicate minimum values are pushed?

Push every value that is less than or equal to Min.Peek() onto Min. For example, if the current minimum is 3 and another 3 is pushed, that second 3 is also stored in Min. When one 3 is later popped from S, only one matching 3 is popped from Min. The remaining 3 still represents the correct minimum. Each operation remains O(1), and auxiliary space remains O(n).

Can the auxiliary stack use less space when the same minimum value appears many times?

A variation can store each minimum together with a count. When the same minimum is pushed again, increment its count instead of storing another identical minimum entry. When that value is popped, decrement the count and remove the minimum entry only when the count reaches zero. Correctness is preserved because the top pair still represents the current minimum and how many active copies exist. Push, pop, and getMinimum remain O(1). Worst-case auxiliary space is still O(n) because a strictly decreasing input creates a different minimum for every pushed value. The tradeoff is slightly more bookkeeping.

5. Find the longest substring without repeating charactersCodingEasyApple

Question Details

Use a sliding-window approach on one string, explain how the left and right boundaries move when a duplicate appears, and cover how you would return the maximal window.

Short Interview Answer (30-60 seconds)

I would use a sliding window with two boundaries and a Dictionary that stores each character’s last seen index. I move right across the string. If the current character already appears inside the window, I move left just after its previous index. I keep the window unique at all times and save its start and length when it becomes strictly longer than the best window. This gives O(n) expected time and O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is one string. The goal is to return a longest continuous part of that string that has no repeated characters. The answer must be a substring, so the characters must stay next to each other in their original order. I use a moving window. Its left and right positions mark the current valid substring. A dictionary remembers where each character was last seen. When a duplicate appears inside the window, I move the left side past the earlier copy. I also remember the best window found so far.

Useful Questions to Ask the Interviewer
  1. If several longest substrings have the same length, is returning any one of them acceptable?
  2. Should the result be the actual substring rather than only its length?
Find the longest substring without repeating characters diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a string. The output is one longest substring that contains no repeated characters. For the example abcabcbb, the diagram returns abc. Its length is 3. Other length-3 windows exist, but this implementation keeps the first maximal window because it updates the best result only when a strictly longer window is found.

2. Choose the sliding window and dictionary

I keep a window from left to right, inclusive. The important rule is that this window always contains unique characters. The dictionary lastSeen maps each character to the most recent index where it appeared. As right moves forward, the dictionary lets me quickly decide whether the current character is a duplicate inside the current window.

3. Initialize the state

Start with left = 0, bestStart = 0, and bestLen = 0. The dictionary is empty. The right boundary begins at index 0 and moves to the end of the string. bestStart stores where the best window begins. bestLen stores its length.

4. Walk through the example

The string is abcabcbb.

At right = 0, the character is a. It has not been seen inside the window. The window is a, so its length is 1. Save bestStart = 0 and bestLen = 1.

At right = 1, the character is b. The window becomes ab. Its length is 2, so bestLen becomes 2.

At right = 2, the character is c. The window becomes abc. Its length is 3, so bestLen becomes 3. The saved best window is now abc starting at index 0.

At right = 3, the character is a. Its previous index is 0, which is inside the current window. Move left to 0 + 1 = 1. The current window becomes bca. Its length is still 3. Because it is not strictly longer, the saved best window remains abc.

At right = 4, the character is b. Its previous index is 1, which is inside the current window. Move left to 1 + 1 = 2. The current window becomes cab, with length 3. The best window does not change.

At right = 5, the character is c. Its previous index is 2, which is inside the current window. Move left to 2 + 1 = 3. The current window is abc, again with length 3. The saved result still stays at index 0.

At right = 6, the character is b. Its previous index is 4, which is inside the current window. Move left to 4 + 1 = 5. The current window becomes cb, with length 2.

At right = 7, the character is b. Its previous index is 6, which is inside the current window. Move left to 6 + 1 = 7. The window becomes b, with length 1.

After the loop, bestStart = 0 and bestLen = 3. The code returns s.Substring(0, 3), which is abc.

5. Explain why the result is correct

The window [left, right] always contains unique characters. When a duplicate appears inside the window, moving left just after the earlier occurrence removes that duplicate. For every right position, the algorithm considers the longest valid window ending there. Whenever that valid window is strictly longer than the saved best window, its start and length are stored. Therefore, after all right positions are processed, the saved window is a longest valid substring.

6. Explain the C# implementation and complexity

The C# code uses Dictionary<char, int> for lastSeen. Each key is a character. Each value is its most recent index. The loop processes each string position once as the right boundary. Dictionary lookup and insertion are O(1) on average, so the expected time is O(n). The dictionary can grow with the number of characters in the input, so the auxiliary space is O(n). The final Substring(bestStart, bestLen) returns the saved maximal window.

Key Insight / Why This Solution Works

The key idea is to maintain one valid sliding window instead of checking every possible substring. The window is [left, right], and its invariant is simple: every character inside it is unique. right expands the window. lastSeen stores each character’s most recent index. If the new character was already seen at an index greater than or equal to left, that duplicate is inside the current window, so left jumps to one position after the previous occurrence. After the window is valid, its length is right - left + 1. If that length is strictly greater than bestLen, the algorithm saves both bestStart = left and the new bestLen. Using a strict > comparison makes the illustrated solution keep the first maximal window when there is a tie.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Use the same verified example shown in the diagram.
        string input = "abcabcbb";

        // Find and print the first maximal unique-character window.
        string result = LongestUniqueSubstring(input);
        Console.WriteLine(result);
    }

    public static string LongestUniqueSubstring(string s)
    {
        // Map each character to the most recent index where it appeared.
        Dictionary<char, int> lastSeen = new Dictionary<char, int>();

        // left is the inclusive start of the current unique-character window.
        int left = 0;

        // Save the start and length of the best window found so far.
        int bestStart = 0;
        int bestLen = 0;

        // Expand the window by moving right across the string once.
        for (int right = 0; right < s.Length; right++)
        {
            char current = s[right];

            // Move left only when the repeated character is inside
            // the current window. This prevents left from moving backward.
            if (lastSeen.TryGetValue(current, out int previousIndex) && previousIndex >= left)
            {
                left = previousIndex + 1;
            }

            // Record or refresh the latest index of the current character.
            lastSeen[current] = right;

            // Both window boundaries are inclusive.
            int currentLength = right - left + 1;

            // Save this window only when it is strictly longer.
            // Equal-length windows do not replace the first maximal window.
            if (currentLength > bestLen)
            {
                bestLen = currentLength;
                bestStart = left;
            }
        }

        // Return the maximal window recorded during the traversal.
        return s.Substring(bestStart, bestLen);
    }
}
Time & Space Complexity

Let n be the length of the string. The expected running time is O(n). The right boundary visits each index once, and the left boundary only moves forward. Dictionary lookup and insertion are O(1) on average, so the scan is O(n) expected time. The auxiliary space is O(n) because lastSeen may store information for characters from the input. The returned substring also requires space for the returned string, but the algorithm’s auxiliary working space remains O(n).

Where it is used

This sliding-window pattern is useful when software needs to inspect a continuous part of a sequence while maintaining a rule. Examples include finding a longest valid section of text, processing a continuous range with no duplicate values, and maintaining a current range that must satisfy a constraint. A dictionary is useful here because the algorithm needs the most recent position of each character.

Why Interviewers Ask This

This problem tests whether a candidate can recognize the sliding-window pattern and maintain a correct window invariant. It also checks duplicate handling, index reasoning, and the ability to use a Dictionary appropriately. The interviewer can see whether the candidate understands when the left boundary should move and why it must never move backward. It also tests whether the candidate can save the actual maximal window, write correct C#, and describe expected hash-based complexity accurately.

Common interview mistakes
  1. Returning only the maximum length when the question asks for the actual substring.
  2. Moving left backward when a repeated character was last seen before the current window. The check previousIndex >= left prevents this.
  3. Moving left to the duplicate index instead of one position after it.
  4. Updating bestLen without also saving bestStart, which makes it impossible to return the correct substring later.
  5. Treating a substring like a subsequence. The result must be one continuous part of the original string.
  6. Claiming guaranteed O(n) time. The Dictionary operations used by this solution are O(1) on average, so the overall bound is expected O(n).
Interview tip

State the invariant before coding: the window from left to right always has unique characters. Then explain every left-boundary move using the exact rule previousIndex >= left, because that is the key detail that prevents the window from moving backward.

Interviewer may ask next
How would you change the solution if the interviewer wanted only the length of the longest substring?

The sliding-window logic does not change. We would still move right, move left past duplicates, and track the maximum valid window length. We would no longer need bestStart. At the end, we would return bestLen instead of calling Substring. The expected time remains O(n), and the auxiliary space remains O(n) because the Dictionary is still needed.

What would change if you wanted the last maximal substring when several windows have the same maximum length?

Change the best-window condition from currentLength > bestLen to currentLength >= bestLen. With >=, an equal-length window found later replaces the earlier saved window, so bestStart ends at the last maximal window. The sliding-window invariant and duplicate handling stay the same. Expected time remains O(n), and auxiliary space remains O(n). The main tradeoff is only which tied maximal window is returned.

6. Find all palindromic substrings in a stringCodingMediumApple

Question Details

Expand around every center, explain odd versus even palindromes, and cover how you count every valid substring without duplicates.

Short Interview Answer (30-60 seconds)

I would expand around every possible center. For each index, I first use the character itself as an odd-length center, then the gap after it as an even-length center. While both characters match, I record that palindrome and expand outward. Each palindrome occurrence has one unique center and index interval, so every valid occurrence is found once. Center expansion takes O(n^2) comparisons and O(1) auxiliary space, excluding the returned substrings.

Detailed Explanation

See the Code while reading this explanation.

The input is one string, and the goal is to return every contiguous part of that string that reads the same forward and backward. The diagram uses s = "ababa". We examine every possible middle position. Some palindromes have one middle character, while others have a middle gap between two characters. We expand outward while both sides match and record each valid occurrence. Equal text at different index positions is returned more than once because those are different substring occurrences.

Useful Questions to Ask the Interviewer
  1. Should equal palindrome text at different index positions be returned as separate occurrences?
  2. Should the returned substrings remain in the order in which the algorithm discovers them?
Find all palindromic substrings in a string diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is one string. We must return every palindromic substring occurrence. A substring uses consecutive characters. For s = "ababa", the algorithm returns ["a", "b", "aba", "a", "bab", "ababa", "b", "aba", "a"]. There are 9 occurrences. The two copies of "aba" are both kept because they come from different index intervals.

2. Use two kinds of centers

Every palindrome has one center. An odd-length palindrome has a character as its center, represented by (i, i). An even-length palindrome has a gap between two characters as its center, represented by (i, i + 1). For each index i, the code processes the odd center first and the even center second.

3. Expand while both sides match

For a chosen center, left and right mark the current substring boundaries. While left is at least 0, right is smaller than the string length, and s[left] equals s[right], the current interval is a palindrome. We add s[left..right] to the result. Then we decrement left and increment right. Expansion stops when either pointer leaves the string or the two characters differ.

4. Walk through the exact example

At i = 0, odd center (0,0) produces "a". Even center (0,1) compares 'a' with 'b', so it stops.

At i = 1, odd center (1,1) produces "b". Expanding to (0,2) produces "aba". The next left index would be outside the string. Even center (1,2) compares 'b' with 'a', so it stops.

At i = 2, odd center (2,2) produces "a". Expanding to (1,3) produces "bab". Expanding to (0,4) produces "ababa". The next expansion would leave the string. Even center (2,3) compares 'a' with 'b', so it stops.

At i = 3, odd center (3,3) produces "b". Expanding to (2,4) produces "aba". The next right index would be outside the string. Even center (3,4) compares 'b' with 'a', so it stops.

At i = 4, odd center (4,4) produces "a". The next expansion leaves the string. The even call starts at (4,5), so its loop does not run.

The final result, in discovery order, is ["a", "b", "aba", "a", "bab", "ababa", "b", "aba", "a"].

5. Explain why the result is correct

Every palindrome occurrence has one unique center and one unique index interval. Odd occurrences are discovered from character centers. Even occurrences are discovered from gap centers. Expanding from every possible center therefore reaches every valid palindrome interval. A specific interval can be generated only from its own center, so that same interval is not added twice. Equal text from different intervals can still appear multiple times, which is required here.

6. Explain the C# implementation

FindAllPalindromicSubstrings creates the result list and loops from i = 0 to i = s.Length - 1. For each i, it calls Expand(s, i, i, result) first for odd-length palindromes and Expand(s, i, i + 1, result) second for even-length palindromes. Expand keeps moving outward while both indices are valid and the characters match. It records the current substring before moving left and right outward. The function finally returns the complete list.

7. Explain complexity and edge cases

The center-expansion work performs O(n^2) character comparisons in the worst case because there are O(n) centers and one center can expand O(n) positions. The pointer algorithm itself uses O(1) auxiliary space. The result contains k palindrome occurrences, so the list holds O(k) entries and k can be O(n^2). In modern .NET, Substring creates new string data, so materializing every returned substring can copy more characters than the O(n^2) comparison count. In the all-equal-character worst case, the total number of characters across the returned strings can reach O(n^3). An empty string returns an empty list. A one-character string returns that character. Repeated characters can create many palindrome occurrences.

Key Insight / Why This Solution Works

The key insight is that every palindrome occurrence has exactly one center. Odd-length palindromes use a character center (i, i). Even-length palindromes use a gap center (i, i + 1). For each index, the algorithm tries both centers and moves left and right outward while the characters match. The central invariant is that a substring is added only when both boundaries are valid and s[left] equals s[right], so the current interval is palindromic. Because each index interval has one unique center, each occurrence is discovered exactly once.

Code
using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main()
    {
        // Use the exact example shown in the approved diagram.
        string input = "ababa";

        // Find every palindrome occurrence in center-expansion discovery order.
        List<string> result = FindAllPalindromicSubstrings(input);

        // Print the returned substrings in the same order as the walkthrough.
        Console.WriteLine("[" + string.Join(", ", result.ConvertAll(value => $"\"{value}\"")) +
                          "]");
    }

    public static List<string> FindAllPalindromicSubstrings(string s)
    {
        // Store every valid palindrome occurrence that is discovered.
        List<string> result = new List<string>();

        // Treat each index as both an odd center and the left side of an even center.
        for (int i = 0; i < s.Length; i++)
        {
            // Odd-length case: one character is the center.
            Expand(s, i, i, result);

            // Even-length case: the center is the gap between i and i + 1.
            Expand(s, i, i + 1, result);
        }

        // Return all occurrences in the exact order in which they were found.
        return result;
    }

    private static void Expand(string s, int left, int right, List<string> result)
    {
        // Continue while both pointers are valid and the boundary characters match.
        while (left >= 0 && right < s.Length && s[left] == s[right])
        {
            // The inclusive interval [left, right] is a palindrome, so record it once.
            result.Add(s.Substring(left, right - left + 1));

            // Grow around the same center to test the next larger interval.
            left--;
            right++;
        }
    }
}
Time & Space Complexity

The expand-around-center algorithm performs O(n^2) character comparisons in the worst case. There are O(n) possible centers, and one center can expand O(n) positions. The expansion logic uses O(1) auxiliary space because it needs only indices and loop variables. The returned list has k entries, where k can be O(n^2). One C# detail is important: Substring creates a new string. If we count all characters copied into the returned strings, the total output size and materialization work can be O(n^3) in a worst-case string such as many repeated characters.

Where it is used

Expand-around-center is useful when software needs to inspect symmetric regions inside text without building a full dynamic-programming table. It appears in palindrome analysis, text-processing utilities, validation logic, and interview problems. The same pattern can also find the longest palindromic substring. In that version, we keep only the best interval instead of returning every valid occurrence.

Why Interviewers Ask This

This question tests whether you recognize the expand-around-center pattern and understand why both odd and even centers are necessary. It also checks whether you distinguish substrings from subsequences, handle repeated palindrome text at different positions correctly, preserve the discovery order, and write safe boundary conditions. The interviewer can also evaluate your C# implementation, your correctness argument based on unique centers, and whether you explain time, auxiliary space, and output costs accurately.

Common interview mistakes

A common mistake is checking only character centers, which misses even-length palindromes. Another is checking only gaps, which misses odd-length palindromes. Candidates may stop after the first match instead of continuing to expand outward. Another mistake is treating a substring like a subsequence even though the characters must be contiguous. It is also incorrect to remove repeated text such as the second "aba" because it belongs to a different index interval. Finally, candidates should distinguish the O(n^2) center-expansion work from the cost of materializing all returned string contents.

Interview tip

After explaining the two center types, trace center i = 2 in "ababa". Show (2,2) = "a", then (1,3) = "bab", then (0,4) = "ababa". This single trace makes the expansion rule and stopping condition easy to understand before you write the helper method.

Interviewer may ask next
How would you change the solution if you only needed the number of palindromic substrings?

Keep the same odd-center and even-center expansion. Replace the result list with an integer count. Every time the while condition succeeds, increment the count instead of creating a substring. Correctness stays the same because every valid index interval still has one unique center. The center-expansion work remains O(n^2) in the worst case. Auxiliary space is O(1), and because no strings are materialized, there is no O(k) collection of returned substrings.

How would the solution change if you only needed the longest palindromic substring?

Use the same center-expansion process, but do not store every match. Track the start index and length of the longest interval found so far. After each successful expansion, compare the current interval length with the best length and update the best interval when necessary. Every possible palindrome center is still checked, so correctness is preserved. The worst-case center-expansion time remains O(n^2), auxiliary space remains O(1), and the output is only the longest substring.

7. Serialize and deserialize a binary tree (for iCloud syncing context)CodingMediumApple

Question Details

Encode a tree so it can be reconstructed exactly, explain why null markers matter, and compare level-order and preorder tradeoffs for this Apple-framed tree problem.

Short Interview Answer (30-60 seconds)

I would serialize the tree with level-order BFS and a queue. For each real node, I write its value and enqueue both child references. For a missing child, I write a null marker. Those null markers preserve the exact tree shape, so deserialization can rebuild the same left and right links. I then consume the tokens in the same order and enqueue only real children. Both operations take O(n) time. The BFS queue uses O(w) auxiliary space, where w is the maximum tree width.

Detailed Explanation

See the Code while reading this explanation.

The goal is to turn a binary tree into text and later rebuild exactly the same tree. We must keep both the node values and the positions of missing children. The diagram uses level-order traversal, also called BFS. A queue processes nodes from top to bottom. When a child is missing, we store the word null. This matters because values alone may not reveal the original shape. In the example, node 1 has children 2 and 3. Node 2 has children 4 and 5. Node 3 has no left child and has right child 6.

Useful Questions to Ask the Interviewer
  1. Should the serialized format preserve every missing left and right child exactly?
  2. Can node values be negative, and is a comma-separated string acceptable?
  3. Should an empty tree serialize to an empty string?
Serialize and deserialize a binary tree (for iCloud syncing context) diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a binary tree. It is not assumed to be a binary search tree. Serialization must produce a string that contains enough information to reconstruct exactly the same structure. Deserialization takes that string and returns the reconstructed root.

The diagram's example tree has root 1. Node 1 has left child 2 and right child 3. Node 2 has left child 4 and right child 5. Node 3 has no left child and has right child 6.

The exact level-order serialization is "1,2,3,4,5,null,6,null,null,null,null,null,null".

The preorder comparison shown in the diagram is "1,2,4,null,null,5,null,null,3,null,6,null,null".

2. Choose the algorithm and data structure

The selected solution uses level-order BFS with a queue. During serialization, the queue can contain both real node references and null references. A real node writes its value and adds both child references to the queue. A null reference writes "null" and adds no children.

During deserialization, the queue contains only real parent nodes. For each parent, we consume one token for the left child and then one token for the right child. We create, attach, and enqueue a child only when that token is not "null".

The central invariant is that serialization produces child positions in a fixed level-order sequence, and deserialization consumes those positions in that same sequence. This makes every value and every null marker return to the correct place.

3. Initialize the state

For serialization, if the root is null, return an empty string. Otherwise create a StringBuilder and a queue containing the root.

For deserialization, return null when the input is empty. The implementation also accepts the defensive input "null". Otherwise split the text by commas, create the root from token 0, enqueue the root, and start reading child tokens from index 1.

4. Walk through the example

Start with queue [1]. Dequeue 1, append "1", and enqueue 2 and 3. The queue becomes [2,3].

Dequeue 2, append "2", and enqueue 4 and 5. The queue becomes [3,4,5].

Dequeue 3, append "3", and enqueue null and 6. The queue becomes [4,5,null,6].

Dequeue 4, append "4", and enqueue null and null. The queue becomes [5,null,6,null,null].

Dequeue 5, append "5", and enqueue null and null. The queue becomes [null,6,null,null,null,null].

Dequeue the first null reference. Append "null" and add no children. The queue becomes [6,null,null,null,null].

Dequeue 6. Append "6" and enqueue null and null. The queue becomes [null,null,null,null,null,null].

Each remaining null reference is then dequeued. For each one, append "null" and add no children. After all thirteen queue entries are processed, the queue is empty.

The final serialized output is "1,2,3,4,5,null,6,null,null,null,null,null,null".

During deserialization, create root

  1. Read tokens 2 and 3 as its children and enqueue them. Then read 4 and 5 as the children of node
  2. For node 3, read null for the left child and 6 for the right child. Nodes 4, 5, and 6 receive null child tokens. The reconstructed tree therefore has exactly the same left and right links as the original tree.
5. Explain why null markers matter

Null markers preserve missing-child positions. Without them, different tree shapes can produce the same sequence of node values. The marker tells us whether a particular left or right child is absent.

With one explicit null marker for every missing child, a binary tree with n real nodes has n + 1 null child references. A full structural representation therefore has 2n + 1 tokens. This is true for the full level-order representation and for the full preorder-with-nulls representation shown in the diagram.

6. Explain the C# implementation

Serialize follows the same BFS queue trace as the diagram. It writes "null," for a missing reference. For a real node, it writes the value followed by a comma and enqueues both children. When the queue is empty, TrimEnd removes the final comma.

Deserialize splits the string into tokens. It creates the root from the first token and places only real parent nodes into its queue. Each parent consumes the next left token and then the next right token. Non-null tokens create child nodes that are attached and enqueued. Null tokens leave the corresponding child reference empty.

7. Explain complexity and edge cases

Serialization takes O(n) time because the number of real nodes plus explicit null references is O(n). Deserialization also takes O(n) time because it consumes the token sequence once.

The BFS queue uses O(w) auxiliary space, where w is the maximum tree width. The serialized output uses O(n) space. Deserialization also uses an O(w) queue.

Important edge cases are an empty tree, a single-node tree, a completely skewed tree, and a tree with many missing children. The same null-marker rule handles all of them.

Key Insight / Why This Solution Works

Use level-order BFS because it gives a direct queue-based way to record every parent and its left and right child positions. During serialization, a real node produces its value and adds both child references to the queue. A null reference produces the token "null" and adds nothing. During deserialization, only real parent nodes are queued. Each parent consumes the next left token and then the next right token. The central invariant is that serialization and deserialization use the same parent-child token order, so each value and null marker is restored to the same structural position. Preorder with null markers is also valid, but it naturally fits recursive or stack-based reconstruction rather than the queue-oriented solution used here.

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

public static class Program
{
    public static void Main()
    {
        // Build the exact example tree from the diagram.
        //        1
        //       / \
        //      2   3
        //     / \   \
        //    4   5   6
        TreeNode root = new TreeNode(
            1) { Left = new TreeNode(2) { Left = new TreeNode(4), Right = new TreeNode(5) },
                 Right = new TreeNode(3) { Right = new TreeNode(6) } };

        // Serialize the original tree using level-order BFS with null markers.
        string serialized = Serialize(root);
        Console.WriteLine(serialized);

        // Rebuild the exact tree from the serialized token sequence.
        TreeNode? rebuilt = Deserialize(serialized);

        // Serialize again to verify that the reconstructed shape is identical.
        string rebuiltSerialized = Serialize(rebuilt);
        Console.WriteLine(rebuiltSerialized);
    }

    public static string Serialize(TreeNode? root)
    {
        // The diagram represents an empty tree with an empty string.
        if (root is null)
        {
            return string.Empty;
        }

        StringBuilder output = new StringBuilder();

        // This queue may contain null references because missing children
        // must produce explicit tokens that preserve the exact tree shape.
        Queue < TreeNode ? > queue = new Queue < TreeNode ?>();
        queue.Enqueue(root);

        while (queue.Count > 0)
        {
            TreeNode? node = queue.Dequeue();

            // A missing child becomes a null marker and adds no new children.
            if (node is null)
            {
                output.Append("null,");
                continue;
            }

            // Record the current real node in level-order.
            output.Append(node.Value).Append(',');

            // Enqueue both child references, including missing children.
            // Their positions preserve the exact left/right structure.
            queue.Enqueue(node.Left);
            queue.Enqueue(node.Right);
        }

        // Remove only the final delimiter after all tokens have been written.
        return output.ToString().TrimEnd(',');
    }

    public static TreeNode? Deserialize(string data)
    {
        // Handle the diagram's empty-string representation.
        // Accept "null" defensively if such input is received.
        if (string.IsNullOrEmpty(data) || data == "null")
        {
            return null;
        }

        // Read the exact BFS token sequence produced by Serialize.
        string[] tokens = data.Split(',');

        // Token 0 is always the real root for a non-empty serialized tree.
        TreeNode root = new TreeNode(int.Parse(tokens[0]));

        // Only real nodes become parents during reconstruction.
        Queue<TreeNode> queue = new Queue<TreeNode>();
        queue.Enqueue(root);

        // The root used token 0, so child processing begins at token 1.
        int index = 1;

        while (queue.Count > 0 && index < tokens.Length)
        {
            TreeNode parent = queue.Dequeue();

            // Consume the next token as this parent's left child.
            if (index < tokens.Length)
            {
                string leftToken = tokens[index++];

                // Only a non-null token creates and enqueues a real child.
                if (leftToken != "null")
                {
                    parent.Left = new TreeNode(int.Parse(leftToken));
                    queue.Enqueue(parent.Left);
                }
            }

            // Consume the next token as this parent's right child.
            if (index < tokens.Length)
            {
                string rightToken = tokens[index++];

                // A null token leaves the corresponding child reference empty.
                if (rightToken != "null")
                {
                    parent.Right = new TreeNode(int.Parse(rightToken));
                    queue.Enqueue(parent.Right);
                }
            }
        }

        // All consumed tokens have been attached in the original BFS child order.
        return root;
    }

    public sealed class TreeNode
    {
        public int Value { get; }
        public TreeNode? Left { get; set; }
        public TreeNode? Right { get; set; }

        public TreeNode(int value)
        {
            Value = value;
        }
    }
}
Time & Space Complexity

Let n be the number of real nodes and w be the maximum number of real nodes on one level. Serialization takes O(n) time. A binary tree with n real nodes has n + 1 missing child references, so the explicit null markers still give only O(n) total tokens. Deserialization also takes O(n) time because it consumes the token stream once. The BFS queue uses O(w) auxiliary space. The serialized string uses O(n) output space. Deserialization also uses an O(w) queue.

Where it is used

This pattern is useful when a tree must cross a storage or communication boundary and later keep exactly the same structure. Examples include saving hierarchical application state, synchronizing tree-shaped data, caching trees, sending trees between services, and building deterministic test fixtures. In an iCloud-style synchronization scenario, one side can serialize the tree, store or transfer the string, and another side can reconstruct the same left and right child relationships.

Why Interviewers Ask This

This problem tests whether you can preserve tree structure, not just node values. The interviewer can evaluate your understanding of BFS traversal, queue state, node references, null markers, and inverse operations. It also tests whether your serializer and deserializer remain consistent with each other and whether you can explain time and space complexity correctly. The preorder comparison shows whether you understand that more than one traversal can work when the representation records enough structural information.

Common interview mistakes

A common mistake is writing only node values and skipping null markers. That loses information about missing children. Another mistake is using a different traversal order during deserialization than during serialization. Candidates may also enqueue null tokens as parent nodes during deserialization, but only real nodes should become parents. It is also easy to consume left and right child tokens in the wrong order or increment the token index incorrectly. Finally, do not claim constant extra memory. The BFS queue uses O(w) auxiliary space, and the serialized output uses O(n) space.

Interview tip

Use the diagram's missing left child under node 3 when you explain the solution. Show that the BFS sequence contains "null" before 6. That one position makes the purpose of null markers easy to see and gives you a concrete example for explaining both serialization and reconstruction.

Interviewer may ask next
Could we use preorder instead of level-order for the same problem?

Yes. Preorder with explicit null markers can also reconstruct the tree exactly. We would write the current node, then its left subtree, then its right subtree. A missing child would still write "null". Deserialization would consume the tokens recursively or with an explicit stack in the same preorder sequence. Correctness is preserved because every real node and every missing child is recorded deterministically. Time remains O(n). Recursive auxiliary space is O(h), where h is the tree height. The main tradeoff is that preorder naturally fits recursion or a stack, while the selected level-order solution naturally fits a queue.

How would this solution behave for a very deep or highly skewed tree?

The selected level-order solution does not use recursion, so a deep skewed tree does not create a deep call stack. Serialization and deserialization still take O(n) time. The queue uses O(w) auxiliary space. For a highly skewed tree, the real-node width is small, so w is small, even though serialization still records explicit null child references. A recursive preorder solution would use O(h) call-stack space. For a skewed tree, h can become O(n), which can make recursion depth a practical concern.

8. Find the lowest common ancestor of two nodes in a binary tree (framed around Finder's folder hierarchy)CodingHardApple

Question Details

Use post-order recursion on the tree, explain how to identify the split point between two targets, and clarify what the answer is when one node is an ancestor of the other.

Short Interview Answer (30-60 seconds)

I would use post-order recursion. At each folder node, I first get the result from the left subtree and then the right subtree. If the current node is p or q, I return that node. If both child calls return non-null nodes, the current node is the split point, so it is the LCA. Otherwise, I propagate the one non-null result upward. This takes O(n) time and O(h) auxiliary space for the recursion stack.

Detailed Explanation

See the Code while reading this explanation.

We have a folder hierarchy shaped like a tree. We are given two specific folders and must find the lowest folder that contains both of them below it, including the case where one requested folder already contains the other. The main idea is to start from the top and let each folder report what it finds below. We examine the left side and the right side before deciding at the current folder. In the diagram, the two folders are Documents (9) and Downloads (10), and their lowest shared folder is Users (2).

Useful Questions to Ask the Interviewer
  1. Can I assume both target node references are present in the tree?
  2. Should I compare the actual node references rather than only their stored values?
  3. Can one target node be an ancestor of the other?
Find the lowest common ancestor of two nodes in a binary tree (framed around Finder's folder hierarchy) diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the root of a binary tree and two target node references, p and q. We must return the TreeNode that is their lowest common ancestor. This is a normal binary tree, not a binary search tree, so the stored values do not tell us which direction to search. In the diagram, p is Documents (9), q is Downloads (10), and the required result is Users (2).

2. Choose post-order recursion

The recursive method lets each subtree report whether it found one of the targets or an LCA that was already discovered lower in the tree. An empty subtree returns null. If the current node is exactly p or q, it returns that node. Otherwise, the method recursively checks the left subtree and then the right subtree. These return values give the current node enough information to decide what to return upward.

3. Identify the split point

After the left and right recursive calls finish, the current node checks both results. If both are non-null, important results came from both child subtrees. That means the two targets split below the current node, so the current node is the LCA. Because this decision is made from the bottom upward, the first split point found is the lowest common ancestor. If only one side is non-null, that result is propagated upward. If both sides are null, the method returns null.

4. Walk through the exact diagram example

Start at Macintosh HD (1). The recursion goes into Users (2), then Alice (4), then Desktop (8). Desktop is neither target and has no useful child result, so it returns null. Next, Documents (9) matches p, so it immediately returns Documents. Alice receives null from one side and Documents from the other, so Alice propagates Documents upward. The recursion then explores Bob (5). Downloads (10) matches q and returns Downloads, so Bob propagates Downloads upward. Users now receives Documents from its left side and Downloads from its right side. Both results are non-null, so Users is the split point and returns Users (2). The System (3) subtree contributes null. Macintosh HD finally receives Users on one side and null on the other, so it propagates Users. The final answer is Users (2).

5. Handle the ancestor case

The check for root == p or root == q is important. If one target is an ancestor of the other, reaching that ancestor causes the function to return the ancestor node. Under the normal problem contract that both target nodes exist in the tree, that returned ancestor propagates upward and becomes the correct LCA. The two targets do not need to be in two different child subtrees for this case.

6. Explain why the result is correct

The invariant is that each recursive call returns null when its subtree contributes no target, or returns a target node or an LCA discovered inside that subtree. When both child calls return non-null results, the current node is where the two target paths meet from different sides. Since child subtrees are solved before the parent decides, this meeting point is the lowest such node. For Documents (9) and Downloads (10), that node is Users (2).

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

The C# method has two base checks, then recursively evaluates the left and right children, checks for a split point, and finally propagates one non-null result upward. The time complexity is O(n) because every visited node does constant work and, in the worst case, all n nodes are visited. Auxiliary space is O(h) for the recursion stack, where h is the tree height. This is O(log n) for a balanced tree and O(n) for a completely skewed tree. If p and q are the same node, that node is returned. If one is an ancestor of the other, the ancestor is returned. The core algorithm assumes both target nodes exist. If missing targets must be supported, extra validation is required because the standard recursive method can return the one target that is present.

Key Insight / Why This Solution Works

Use bottom-up post-order recursion. The key invariant is that a recursive call returns null when its subtree has no relevant target, and otherwise returns a target node or an LCA already found inside that subtree. First handle the base cases: null returns null, and a node that is p or q returns itself. Then recurse into the left child and the right child. If both results are non-null, the current node is the split point and therefore the LCA. If only one result is non-null, propagate it upward. This directly matches the diagram's Documents (9), Downloads (10), and Users (2) example.

Code
using System;

public sealed class TreeNode
{
    public int Val;
    public string Name;
    public TreeNode? Left;
    public TreeNode? Right;

    public TreeNode(int val, string name)
    {
        // Store the visible node value and Finder-style folder name.
        Val = val;
        Name = name;
    }
}

public static class Program
{
    public static TreeNode? LowestCommonAncestor(TreeNode? root, TreeNode p, TreeNode q)
    {
        // Base case: an empty subtree contains neither target.
        if (root is null)
        {
            return null;
        }

        // Match by node identity. Returning a target immediately also supports
        // the case where one target is an ancestor of the other.
        if (ReferenceEquals(root, p) || ReferenceEquals(root, q))
        {
            return root;
        }

        // Post-order recursion: first obtain the result from the left subtree.
        TreeNode? left = LowestCommonAncestor(root.Left, p, q);

        // Then obtain the result from the right subtree.
        TreeNode? right = LowestCommonAncestor(root.Right, p, q);

        // Two non-null child results mean the targets split below this node.
        // Therefore, the current node is their lowest common ancestor.
        if (left is not null && right is not null)
        {
            return root;
        }

        // If only one side has a useful result, propagate it upward.
        // If both sides are null, this expression also returns null.
        return left ?? right;
    }

    public static void Main()
    {
        // Create the exact nodes shown in the audited diagram.
        TreeNode macintoshHd = new TreeNode(1, "Macintosh HD");
        TreeNode users = new TreeNode(2, "Users");
        TreeNode system = new TreeNode(3, "System");
        TreeNode alice = new TreeNode(4, "alice");
        TreeNode bob = new TreeNode(5, "bob");
        TreeNode library = new TreeNode(6, "Library");
        TreeNode applications = new TreeNode(7, "Applications");
        TreeNode desktop = new TreeNode(8, "Desktop");
        TreeNode documents = new TreeNode(9, "Documents");
        TreeNode downloads = new TreeNode(10, "Downloads");

        // Connect the nodes using the exact binary-tree relationships in the diagram.
        macintoshHd.Left = users;
        macintoshHd.Right = system;
        users.Left = alice;
        users.Right = bob;
        alice.Left = desktop;
        alice.Right = documents;
        bob.Right = downloads;
        system.Left = library;
        system.Right = applications;

        // Use the exact targets from the diagram.
        TreeNode p = documents;
        TreeNode q = downloads;

        // Run the post-order recursive LCA search.
        TreeNode? lca = LowestCommonAncestor(macintoshHd, p, q);

        // Print the exact result shown in the diagram: Users (2).
        if (lca is not null)
        {
            Console.WriteLine($"LCA({p.Name}, {q.Name}) = {lca.Name} ({lca.Val})");
        }
    }
}
Time & Space Complexity

The time complexity is O(n), where n is the number of nodes in the tree. In the worst case, the algorithm may visit every node before the final answer is known. The auxiliary space is O(h), where h is the height of the tree, because recursive calls stay on the call stack. For a balanced tree, this is O(log n). For a completely skewed tree, it can become O(n). The algorithm does not need a hash map, queue, or another growing collection.

Where it is used

This pattern is useful for hierarchical data where we need the closest shared parent. Examples include file-system folder trees, organization hierarchies, UI component trees, syntax trees, and other parent-child structures. The same bottom-up recursion pattern is also useful when a parent must combine information returned by its child subtrees.

Why Interviewers Ask This

This problem tests whether you can reason about a tree from the bottom up. The interviewer wants to see whether you recognize post-order recursion, define correct base cases, understand what each recursive return value represents, and identify the split point between two targets. It also tests whether you handle the ancestor case, preserve node identity, write correct C#, explain O(n) time and O(h) recursion space, and notice the assumption behind missing-target behavior.

Common interview mistakes

A common mistake is comparing only stored values instead of the actual target node references. Another mistake is forgetting the null base case. A candidate may also return the current node when only one child result is non-null, but the current node is the split-point LCA only when both child results are non-null. Another mistake is forgetting that one target can be an ancestor of the other. It is also incorrect to claim O(1) auxiliary space because recursion uses O(h) stack space. Finally, do not claim the standard core returns null whenever exactly one target is missing. Without extra validation, it can return the target that is present.

Interview tip

Before writing the recursion, explain what one recursive return value means: null means the subtree found nothing relevant, while a non-null node means it found a target or an LCA. Once that invariant is clear, the split-point rule and the propagation rule are easy to justify.

Interviewer may ask next
What changes if one or both target nodes might be missing from the tree?

The standard recursive core is not enough because one existing target can propagate upward even when the other target is absent. I would add validation that tracks whether p and q were both found, or perform an equivalent presence check. The final LCA is accepted only when both targets exist. The time complexity remains O(n), and the recursion stack remains O(h). The tradeoff is a little more state and logic.

What changes if the tree can be extremely deep or completely skewed?

The recursive algorithm is still logically correct, and its time complexity remains O(n), but the recursion depth can become O(n). A very deep tree can overflow the call stack. For that environment, an iterative approach with an explicit stack and parent information can avoid deep recursive calls. It still needs O(n) extra memory in the worst case, so the main tradeoff is replacing call-stack risk with explicit data structures.

9. Merge overlapping intervalsCodingEasyApple

Question Details

Sort intervals by start time, explain how you merge nested and touching ranges, and state what you return when only one interval remains.

Short Interview Answer (30-60 seconds)

I would sort the intervals by start time, then walk from left to right while keeping one current interval. If the next interval starts before or at the current end, they overlap or touch, so I extend the current end to the larger end value. Otherwise, I add the current interval to the result and start a new one. After the loop, I add the last interval. Sorting takes O(n log n) time, and the result uses O(n) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

We are given a list of ranges. Each range has a start and an end. We need to combine ranges that cover the same area or touch at one point. For example, [1,3] and [2,6] become [1,6]. Touching ranges also merge, so [1,3] and [3,5] become [1,5]. The main idea is to sort the ranges by their start values. Then we compare each next range with one current merged range. This also handles a nested range because taking the larger end keeps the full outer range.

Useful Questions to Ask the Interviewer
  1. Should touching intervals such as [1,3] and [3,5] be merged? In this problem, yes.
  2. Can the input be empty or contain only one interval? The shown solution handles both cases.
Merge overlapping intervals diagram
How to Explain It in an Interview
1. Sort the intervals by start time

The example input is [[5,7],[1,3],[2,6],[8,10],[15,18],[17,20]]. After sorting by start time, it becomes [[1,3],[2,6],[5,7],[8,10],[15,18],[17,20]]. Sorting puts intervals that may overlap next to each other.

2. Initialize the current interval

Start with current = [1,3]. The result list is empty. The current interval represents the merged range we are still building.

3. Merge overlapping, nested, or touching intervals

Compare the next interval with current. The condition is current.end >= next.start. For [1,3] and [2,6], 3 >= 2 is true, so they merge. The new end is max(3,6) = 6, giving [1,6]. Next, [5,7] also overlaps because 6 >= 5. The new end is max(6,7) = 7, giving [1,7]. Using max also handles nesting correctly. If current were [1,5] and next were [2,3], the end would stay 5 instead of shrinking to 3.

4. Start a new current interval when there is no overlap

Next is [8,10]. The condition 7 >= 8 is false. Add [1,7] to the result and set current = [8,10]. Then compare [15,18]. Since 10 >= 15 is false, add [8,10] and set current = [15,18]. Finally, [17,20] overlaps because 18 >= 17, so current becomes [15,20].

5. Add the final interval and return the result

After the loop, add the last current interval. The final result is [[1,7],[8,10],[15,20]]. If merging leaves only one interval, the result simply contains that one interval. An empty input returns an empty array, and a one-interval input returns that interval as it is.

6. Explain why it is correct

After sorting, intervals that can overlap appear next to each other. The current interval always represents all overlapping or touching intervals processed since the last completed result interval. When the next interval overlaps, extending the end with the larger end value keeps the full covered range. When it does not overlap, current cannot merge with any later interval because later intervals start even farther to the right.

7. Explain complexity and edge cases

Sorting costs O(n log n), and the left-to-right merge loop costs O(n). The total time is O(n log n). The result can contain up to n intervals, so the auxiliary space used for the result is O(n), excluding sorting implementation space. Important cases are empty input, one interval, fully nested or overlapping intervals, intervals that only touch, and intervals that never overlap.

Key Insight / Why This Solution Works

The key insight is to sort intervals by start time first. After sorting, any interval that can merge with the current range appears before intervals that start farther to the right. Keep one current interval. If current.end >= next.start, the ranges overlap or touch, so update current.end to max(current.end, next.end). Using max is important for nested intervals because the current end must never shrink. Otherwise, current is finished, so add it to the result and make next the new current interval. The invariant is that current always represents the complete merged range for the unfinished group of overlapping or touching intervals processed so far.

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

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

        int[][] merged = Merge(intervals);

        // Print the merged intervals in the same form as the diagram result.
        Console.Write("[");
        for (int i = 0; i < merged.Length; i++)
        {
            if (i > 0)
            {
                Console.Write(",");
            }

            Console.Write($"[{merged[i][0]},{merged[i][1]}]");
        }
        Console.WriteLine("]");
    }

    public static int[][] Merge(int[][] intervals)
    {
        // Null, empty, or one-interval input needs no merging.
        if (intervals == null || intervals.Length <= 1)
        {
            return intervals;
        }

        // Sort by start time so intervals that may overlap become adjacent.
        Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0]));

        // Store each completed merged interval here.
        List<int[]> result = new List<int[]>();

        // Copy the first sorted interval because its end may be extended.
        int[] current = intervals[0].ToArray();

        // Process each remaining interval from left to right.
        for (int i = 1; i < intervals.Length; i++)
        {
            int[] next = intervals[i];

            // Merge overlap or touching ranges when current.end >= next.start.
            if (current[1] >= next[0])
            {
                // Use the larger end so nested intervals cannot shrink current.
                current[1] = Math.Max(current[1], next[1]);
            }
            else
            {
                // No overlap remains, so the current merged interval is complete.
                result.Add(current);

                // Copy the next interval and begin a new current merged range.
                current = next.ToArray();
            }
        }

        // The loop does not add the final current interval, so add it now.
        result.Add(current);

        // Return all completed merged intervals.
        return result.ToArray();
    }
}
Time & Space Complexity

Let n be the number of intervals. Sorting by start time takes O(n log n) time. After sorting, the code walks through the intervals once, which takes O(n) time. Therefore, the total time complexity is O(n log n). The result list can hold up to n intervals when none overlap, so the auxiliary space used for the result is O(n), excluding sorting implementation space. The algorithm also keeps one current interval while processing.

Where it is used

This pattern is useful when software must combine ranges that overlap. Examples include merging reservation periods, calendar time blocks, scheduled maintenance windows, numeric ranges, or covered portions of a timeline. Sorting first makes it possible to combine related ranges in one left-to-right pass.

Why Interviewers Ask This

This problem checks whether you recognize the sort-and-scan interval pattern. The interviewer can see whether you choose the correct sorting key, maintain a useful current-range invariant, handle nested and touching intervals correctly, and know when a merged range is complete. It also tests careful C# implementation, especially updating interval values safely, handling edge cases, and explaining why sorting makes the overall complexity O(n log n).

Common interview mistakes

A common mistake is forgetting to sort by start time before merging. Another is using current.end > next.start instead of current.end >= next.start, which fails to merge touching intervals such as [1,3] and [3,5]. Candidates may also replace the current end with next.end instead of using max(current.end, next.end), which breaks nested intervals because the merged range can incorrectly shrink. Another mistake is forgetting to add the last current interval after the loop. It is also incorrect to claim O(n) total time because sorting costs O(n log n).

Interview tip

State the merge rule clearly before coding: after sorting, merge when current.end >= next.start, and update the end with max(current.end, next.end). That one rule explains overlapping, nested, and touching intervals.

Interviewer may ask next
What changes if touching intervals should not be merged?

Only the overlap condition changes. Instead of current.end >= next.start, use current.end > next.start. Then [1,3] and [3,5] stay separate because 3 > 3 is false. Sorting and the rest of the algorithm stay the same. Correctness is preserved because current is merged only when the two ranges overlap rather than merely meet at one endpoint. Time remains O(n log n), and auxiliary result space remains O(n).

Can the auxiliary space be reduced?

Yes, if modifying the input array is allowed, the merged intervals can be written back into the front of the sorted input instead of storing them in a separate result list. The same sorted traversal and merge condition are used, so correctness does not change. Sorting still makes the time O(n log n). The merge phase can use O(1) additional working space apart from sorting internals, but returning the final set of intervals may still require constructing an output array depending on the required API.

10. Detect a cycle in a linked listCodingEasyApple

Question Details

Use the fast-and-slow pointer technique, explain how it proves a cycle without extra storage, and describe what you return if no cycle exists.

Short Interview Answer (30-60 seconds)

I would use two node references that start at the head. I move slow by one node and fast by two nodes each time. If the list has a cycle, fast eventually catches slow inside that cycle, so I return true. If fast reaches null, or fast.Next is null, the list ends and I return false. This takes O(n) time and O(1) auxiliary space because I only keep the two node references.

Detailed Explanation

See the Code while reading this explanation.

The input is the first node of a linked list. We need to decide whether following the next links can eventually bring us back to a node that was already reached. We do not want to keep a collection of visited nodes. Instead, we use two moving references. One moves one node at a time and the other moves two. If they meet, the links form a cycle. If the faster one reaches the end, there is no cycle. This gives the required true-or-false result with constant extra memory.

Useful Questions to Ask the Interviewer
  1. Should I return only true or false rather than the node where the cycle begins?
  2. Can the input be null or contain only one node?
  3. Should I use constant extra memory rather than storing visited nodes?
Detect a cycle in a linked list diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is head, which is a reference to the first node of a singly linked list. The output is a Boolean value. Return true when the list contains a cycle. Return false when the list ends normally. The question only asks whether a cycle exists. It does not ask for the node where the cycle begins.

2. Choose the fast-and-slow pointer method

I use Floyd's Tortoise and Hare technique. I keep two node references called slow and fast. slow moves one node per iteration. fast moves two nodes per iteration. This detects a cycle without storing visited nodes.

The key idea is that if both references enter a cycle, fast gains one node on slow during each iteration around that cycle. Because the cycle has a finite number of nodes, they must eventually point to the same node. If there is no cycle, fast eventually reaches the end.

3. Initialize the state

For the diagram's example, the nodes are 1 -> 2 -> 3 -> 4 -> 5, and node 5 points back to node 3. Both references start at node 1:

slow = 1 fast = 1

Before moving, the code also handles an empty list or a one-node list with no next node by returning false.

4. Walk through the verified example

The exact example is 1 -> 2 -> 3 -> 4 -> 5 -> 3, so the cycle is 3 -> 4 -> 5 -> 3.

Step 0: Both references start at node 1. State: slow = 1, fast = 1.

Step 1: Move slow one link from 1 to 2. Move fast two links from 1 to 3. State after the move: slow = 2, fast = 3. They are different, so processing continues.

Step 2: Move slow from 2 to 3. Move fast two links from 3 to 5. State after the move: slow = 3, fast = 5. They are still different, so processing continues.

Step 3: Move slow from 3 to 4. Starting from node 5, fast follows two links: 5 -> 3 -> 4. State after the move: slow = 4, fast = 4. Now both references point to the same node. The algorithm stops immediately and returns true.

If there were no cycle, such as 1 -> 2 -> 3 -> 4 -> null, the loop would eventually find fast == null or fast.Next == null. The method would then return false.

5. Explain why the result is correct

Inside a cycle, slow moves one node per iteration and fast moves two. Relative to slow, fast gains one node per iteration around the finite cycle. Therefore, the two references must eventually meet at the same node. If the list has no cycle, following next references must eventually reach null, so fast cannot keep moving forever.

6. Explain the C# implementation

The code first handles null and a single node with no next node. It then sets slow and fast to head. The loop runs only while fast and fast.Next are not null. Inside the loop, slow moves once and fast moves twice. After both moves, the code checks whether they are the same node reference. If they are, it returns true. If the loop ends, it returns false.

7. Explain complexity and edge cases

The time complexity is O(n). In a list without a cycle, the fast reference reaches the end in a linear number of moves. In a cyclic list, the references enter the cycle and meet after a linear number of moves. The auxiliary space complexity is O(1) because only the slow and fast references are stored.

Important edge cases from the diagram are an empty list, one node with no cycle, one node pointing to itself, and a two-node list with or without a cycle.

Key Insight / Why This Solution Works

Use Floyd's fast-and-slow pointer technique. Start both slow and fast at head. On every iteration, move slow one node and fast two nodes. Then compare the node references. If they are the same reference, a cycle exists and the method returns true immediately. The central invariant is that when both references are inside a cycle, fast gains one node on slow per iteration around that finite cycle, so they must eventually meet. If there is no cycle, fast reaches null or a node whose Next is null, and the method returns false. This approach is suitable because it detects the cycle without storing every visited node.

Code
using System;

public sealed class ListNode
{
    public int Val;
    public ListNode? Next;

    public ListNode(int val)
    {
        // Store the value carried by this node.
        Val = val;
    }
}

public static class Program
{
    public static bool HasCycle(ListNode? head)
    {
        // An empty list cannot contain a cycle.
        // A single node with no next node also cannot contain a cycle.
        if (head == null || head.Next == null)
        {
            return false;
        }

        // Both references start at the head, exactly as in the diagram.
        ListNode? slow = head;
        ListNode? fast = head;

        // Continue only while fast can safely move two nodes.
        while (fast != null && fast.Next != null)
        {
            // Slow advances by one node per iteration.
            slow = slow!.Next;

            // Fast advances by two nodes per iteration.
            fast = fast.Next.Next;

            // Compare node identity, not stored integer values.
            // Meeting at the same node proves that a cycle exists.
            if (ReferenceEquals(slow, fast))
            {
                return true;
            }
        }

        // Fast reached the end, so the list has no cycle.
        return false;
    }

    public static void Main()
    {
        // Build the exact diagram example: 1 -> 2 -> 3 -> 4 -> 5 -> 3.
        ListNode node1 = new ListNode(1);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(3);
        ListNode node4 = new ListNode(4);
        ListNode node5 = new ListNode(5);

        // Connect the forward part of the linked list.
        node1.Next = node2;
        node2.Next = node3;
        node3.Next = node4;
        node4.Next = node5;

        // Point node 5 back to node 3 to create the cycle shown in the diagram.
        node5.Next = node3;

        // Run the cycle detector. The expected result is true.
        bool hasCycle = HasCycle(node1);

        // Prints True for the verified example.
        Console.WriteLine(hasCycle);
    }
}
Time & Space Complexity

Time is O(n). In a list without a cycle, the fast reference reaches the end after a linear number of moves. In a cyclic list, the two references meet after a linear number of moves. Auxiliary space is O(1). Auxiliary space means extra memory used by the algorithm. We store only two node references, slow and fast, no matter how many nodes are in the list.

Where it is used

This pattern is useful whenever software follows a chain of references and needs to know whether the traversal can loop forever. Examples include checking linked data structures, detecting repeated states in pointer-based sequences, and protecting traversal code from an accidental cycle without keeping a growing set of visited objects.

Why Interviewers Ask This

This question checks whether you recognize Floyd's fast-and-slow pointer pattern and can reason about node identity rather than node values. It also tests whether you can maintain a simple invariant, handle null references safely in C#, explain why two moving references must meet inside a cycle, and state the correct O(n) time and O(1) auxiliary space. The interviewer can also see whether you stop as soon as the result is known and handle small edge cases correctly.

Common interview mistakes

A common mistake is moving fast two nodes without first checking that both fast and fast.Next are not null. Another mistake is comparing node values instead of node references. Two different nodes may contain the same value, so equal values do not prove a cycle. Candidates also sometimes move slow or fast by the wrong number of nodes. Another error is claiming that the method needs O(n) extra space even though it keeps only two references. Finally, once slow and fast meet, the method should return true immediately.

Interview tip

While coding, say the movement rule out loud: slow moves one node, fast moves two nodes, and then I compare their node references. Also explain that the loop condition protects the two-step fast move from a null reference.

Interviewer may ask next
How would you find the node where the cycle begins after detecting that a cycle exists?

After slow and fast meet, keep one reference at the meeting node and move the other reference back to head. Then move both references one node at a time. The node where they meet next is the cycle entry. This preserves correctness because Floyd's distance relationship makes those two references arrive at the entry together. The time complexity remains O(n), and the auxiliary space remains O(1). The tradeoff is an additional pointer phase after cycle detection.

What changes if you are allowed to use extra memory instead of the fast-and-slow technique?

You could store each visited node reference in a HashSet<ListNode>. Before processing a node, check whether its reference is already in the set. If it is, a cycle exists. Otherwise, add it and continue. If traversal reaches null, return false. This takes O(n) expected time because hash-set lookup and insertion are O(1) on average, and it uses O(n) auxiliary space. The tradeoff is simpler direct visited-node tracking in exchange for memory that grows with the list.

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.