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.
Identity, Image, and Privacy Notice
To respect individual privacy, some names, profile photographs, avatars, biographical details, and other identifying information displayed in this guide may be replaced with pseudonyms, licensed stock images, illustrative avatars, composite images, or representative descriptions. Unless a person is expressly identified as an actual contributor, a displayed name, image, or profile should not be understood as depicting or identifying a specific candidate, interviewer, employee, or other real individual. These representations are provided for editorial and illustrative purposes only and do not imply endorsement, employment, participation, or affiliation with this guide or any company mentioned in it. Any resemblance to an actual person is coincidental.
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.
Questions or comments?
Contact us for general questions, or share feedback, technical corrections, and comments with the community.
11. Given an input of two arrays that represent computers' power output (powerOutputs) and boosted output (boostedOutputs) and a maximum allowed power (powerMax), return the maximum amount of adjacent computers that are less than or equal to powerMax given the formula totalPowerOutput = powerOutputs[i] + powerOutputs[i + 1...] + (boostedOutputs[i] + boostedOutputs[i + 1...] * numOfAdjacentComputers)CodingHardAmazon
i Question Details
Interpret the two-array power constraint carefully, search for the longest feasible contiguous segment, and explain how you would validate the computed segment against the power limit.
Short Interview Answer (30-60 seconds)
I would build prefix sums for both arrays so I can calculate the two sums of any contiguous segment in constant time. Then I try segment lengths from n down to 1. For each length, I test every possible contiguous segment and calculate totalPowerOutput = sumPower + sumBoost * length. The first segment within powerMax gives the maximum possible length, so I return immediately. The worst-case time is O(n²), and the auxiliary space is O(n).
We have two arrays that describe the same computers. For any adjacent group, we add its normal power values. We also add its boosted values and multiply that boosted sum by the number of computers in the group. The group is valid when this total is at most powerMax. We need the largest valid adjacent group. I use prefix sums so each group's two sums are quick to calculate, then I test group sizes from largest to smallest. The first valid size is the answer.
Useful Questions to Ask the Interviewer
Can powerOutputs or boostedOutputs contain negative values?
Can the arrays be empty?
Are the input values and powerMax small enough for 64-bit arithmetic, or should overflow beyond long be considered?
How to Explain It in an Interview
1. Understand the input and required output
The inputs are powerOutputs, boostedOutputs, and powerMax. Matching positions in the two arrays describe the same computer. We must return a count, not indices or values. The count is the maximum length of a contiguous segment whose total power is at most powerMax.
For a segment of length L, the calculation used by the approved diagram is: totalPowerOutput = sumPower + sumBoost * L.
2. Choose prefix sums and search longest lengths first
I build one prefix array for powerOutputs and one for boostedOutputs. A prefix sum stores the running total before each position. This lets me calculate the sum of any window [left, right) in O(1) time.
I then try lengths n, n - 1, and so on down to 1. For each length, I test every contiguous window. The main invariant is that before I move to a smaller length, every possible window of every larger length has already failed. Therefore, the first valid window proves that its length is the maximum.
This method does not require the array values to be nonnegative.
For a window [left, right), I calculate: sumPower = prefixPower[right] - prefixPower[left] sumBoost = prefixBoost[right] - prefixBoost[left].
4. Walk through the example
First I test length 5. Window [0..4] has sumPower = 12 and sumBoost = 8. Its total is 12 + 8 * 5 = 52, which is greater than 16, so it fails.
Next I test length 4. Window [0..3] gives 10 + 7 * 4 = 38. Window [1..4] also gives 10 + 7 * 4 = 38. Both fail.
Next I test every length-3 window. Window [0..2] gives 7 + 5 * 3 = 22. Window [1..3] gives 8 + 6 * 3 = 26. Window [2..4] gives 9 + 4 * 3 = 21. All are greater than 16.
Then I test length 2. Window [0..1] has sumPower = 3 and sumBoost = 4. Its total is 3 + 4 * 2 = 11. Since 11 <= 16, this window is valid. I return 2 immediately. No later length-2 windows or length-1 windows are processed.
5. Explain why the result is correct
Lengths are tested from largest to smallest. For each length, every possible contiguous window is checked before moving to the next smaller length. By the time the algorithm reaches length 2, lengths 5, 4, and 3 have all been completely ruled out. The first valid length-2 window therefore proves that no longer valid segment exists. The prefix arrays also calculate each tested window's normal-power and boosted-power sums exactly.
6. Explain the C# implementation
The code first validates null inputs and requires the arrays to have the same length. It builds two long prefix arrays. It then uses an outer loop for lengths from n down to 1 and an inner loop for each possible left boundary. right is left + length, so the code represents the current segment as the half-open range [left, right). It gets both sums from the prefix arrays, calculates totalPowerOutput, and returns the length as soon as that value is at most powerMax. If no segment works, it returns 0.
7. Explain complexity and edge cases
Building the prefix arrays takes O(n) time. The nested search tests O(n²) windows in the worst case, and each test takes O(1) after the prefix arrays are built. Therefore, the worst-case time is O(n²). The two prefix arrays require O(n) auxiliary space. Relevant edge cases include empty arrays, one computer, negative values, unequal array lengths, and large totals where 64-bit arithmetic should be considered carefully.
Key Insight / Why This Solution Works
The key insight is to make each contiguous-window sum cheap and then search candidate lengths in an order that makes early return correct. Two prefix-sum arrays let us calculate sumPower and sumBoost for any window [left, right) in O(1). We test candidate lengths from n down to 1 and examine every possible window of each length. The invariant is that before a smaller length is considered, all windows of every larger length have already been proven invalid. Therefore, the first feasible window has the maximum possible length. This approach also remains correct when values are negative because it does not depend on sliding-window monotonicity.
Code
using System;
publicstaticclassProgram
{
publicstaticvoidMain()
{
// Use the exact example shown in the approved diagram.int[] powerOutputs = { 2, 1, 4, 3, 2 };
int[] boostedOutputs = { 1, 3, 1, 2, 1 };
long powerMax = 16;
// Run the algorithm. The expected maximum adjacent-computer count is 2.int result = MaxAdjacentComputers(powerOutputs, boostedOutputs, powerMax);
Console.WriteLine(result);
}
publicstaticintMaxAdjacentComputers(int[] powerOutputs, int[] boostedOutputs, long powerMax)
{
// Both arrays are required because each position describes the same computer.if (powerOutputs == null)
{
thrownew ArgumentNullException(nameof(powerOutputs));
}
if (boostedOutputs == null)
{
thrownew ArgumentNullException(nameof(boostedOutputs));
}
// Matching positions must refer to the same set of computers.if (powerOutputs.Length != boostedOutputs.Length)
{
thrownew ArgumentException("Arrays must have equal length.");
}
int n = powerOutputs.Length;
// Prefix arrays use long so practical accumulated sums have more range than int.// Entry i stores the sum of elements before index i.long[] prefixPower = newlong[n + 1];
long[] prefixBoost = newlong[n + 1];
// Build both prefix sums once so every later window sum is available in O(1).for (int i = 0; i < n; i++)
{
prefixPower[i + 1] = prefixPower[i] + powerOutputs[i];
prefixBoost[i + 1] = prefixBoost[i] + boostedOutputs[i];
}
// Try the largest possible length first.// This order makes the first valid length the maximum valid length.for (int length = n; length >= 1; length--)
{
// Test every contiguous window having this exact length.for (int left = 0; left + length <= n; left++)
{
// right is exclusive, so this window is [left, right).int right = left + length;
// Prefix subtraction gives both sums for the current window.long sumPower = prefixPower[right] - prefixPower[left];
long sumBoost = prefixBoost[right] - prefixBoost[left];
// Apply the exact power formula used by the approved diagram.long totalPowerOutput = sumPower + sumBoost * length;
// Every larger length has already failed, so this valid length is maximal.if (totalPowerOutput <= powerMax)
{
return length;
}
}
}
// Defensive result when no contiguous segment satisfies the power limit.return0;
}
}
Time & Space Complexity
Let n be the number of computers. Building the two prefix arrays takes O(n) time. After that, each window's normal-power sum and boosted-power sum take O(1) time to calculate. In the worst case, the nested loops test O(n²) windows, so the total worst-case time is O(n²). The prefixPower and prefixBoost arrays each grow with n, so the auxiliary space is O(n). The algorithm can finish earlier if it finds a valid window before all candidate lengths are tested.
Where it is used
This pattern is useful when software needs to find the largest contiguous range that satisfies a numeric rule and a simple sliding window is not safe because values may be negative. Prefix sums are common in analytics, monitoring, capacity checks, billing ranges, and time-series processing because they make repeated range-sum calculations fast.
Why Interviewers Ask This
This question tests whether the candidate can interpret an unusual range formula correctly, distinguish a contiguous segment from arbitrary elements, and avoid relying on an unstated monotonicity assumption. It also tests prefix-sum knowledge, careful boundary handling, early-return reasoning, arithmetic choices in C#, and the ability to justify the exact O(n²) worst-case time and O(n) auxiliary space of the selected solution.
Common interview mistakes
One common mistake is using a normal sliding window without a guarantee that the values make the cost monotonic. The approved solution avoids that assumption. Another mistake is applying the formula incorrectly by multiplying only one boosted value instead of the boosted sum for the whole segment. Candidates can also mix inclusive example indices such as [0..1] with the code's half-open [left, right) prefix-sum boundaries. Another mistake is returning a valid segment before larger lengths have been ruled out. Finally, candidates may claim O(n) time even though the approved nested search is O(n²) in the worst case.
Interview tip
State the invariant before coding: "I test lengths from largest to smallest, and I check every window of a length before moving down, so the first valid window must have the maximum possible length." Then explain that prefix sums make each individual window calculation O(1).
Interviewer may ask next
Can auxiliary space be reduced?
Yes, but there is a tradeoff. We could remove the prefix arrays and calculate each candidate window's two sums directly. That would reduce auxiliary space from O(n) to O(1), but repeatedly summing each window would make the worst-case time O(n³). The approved prefix-sum solution uses O(n) extra memory so each tested window can be evaluated in O(1), keeping worst-case time at O(n²).
What changes if the interviewer guarantees that all powerOutputs and boostedOutputs values are nonnegative?
With that new guarantee, a sliding-window solution becomes possible. Expanding the right side cannot decrease the cost, and shrinking the left side cannot increase it. We can maintain left, right, sumPower, and sumBoost, repeatedly shrink while sumPower + sumBoost * length is greater than powerMax, and track the largest valid window. Each index enters and leaves the window at most once, giving O(n) time and O(1) auxiliary space. The tradeoff is that this faster method depends on the new nonnegative-value guarantee, while the approved prefix-sum search does not.
12. Place a set of integers into N arrays such that the sum of the medians of all the arrays is maximizedCodingHardAmazon
i Question Details
Partition integers across a fixed number of arrays, maximize the total of the per-array medians, and explain the role of sorting or greedy placement in the answer.
Short Interview Answer (30-60 seconds)
I would sort the integers first. Then I keep the largest N minus 1 values as singleton arrays, so each of those values is guaranteed to be its array's median. I put every remaining value into one final array and use that array's median. Then I add the N minus 1 singleton medians. This keeps the smaller values from lowering several large medians. Sorting takes O(M log M) time. The algorithm uses O(1) auxiliary space beyond the sort implementation when only the maximum sum is returned.
We need to split all integers into exactly N non-empty arrays. Every input value must belong to one array. We want the sum of the N array medians to be as large as possible. The diagram uses a sorting and greedy approach. We protect the largest N minus 1 values by making each one a single-element array. All remaining values go into one final array. This means the smaller values can affect only one median instead of lowering several large medians. The diagram assumes that an even-length array uses the average of its two middle sorted values.
Useful Questions to Ask the Interviewer
How should the median be defined when an array has an even number of elements?
Should I return only the maximum sum, or also the actual N arrays?
Is it acceptable to sort the input array in place?
How to Explain It in an Interview
1. Understand the input and required output
The input is an integer array called nums and an integer N. We must use every integer exactly once and create exactly N non-empty arrays. The required result is the maximum possible sum of the N medians. The approved diagram assumes that for an even-sized array, the median is the average of the two middle sorted values.
2. Sort and make the greedy choice
Let M be nums.Length. Sort nums in non-decreasing order. Keep the largest N minus 1 values as N minus 1 singleton arrays. A singleton array contains one value, so that value is automatically its median. Put every other value into one final array. These remaining values form a sorted prefix of nums, so we can read that final array's median directly.
3. Walk through the approved example
The input is nums = [12, 1, 5, 10, 3, 8, 2, 9, 6, 11, 4, 7] and N = 4. After sorting, nums is [1,2,3,4,5,6,7,8,9,10,11,12]. Here M = 12. We need N minus 1 = 3 singleton arrays. The split index is M - (N - 1) = 12 - 3 = 9. Sorted indices 9, 10, and 11 contain 10, 11, and 12. They become singleton arrays. Sorted indices 0 through 8 form [1,2,3,4,5,6,7,8,9]. Its length is 9, so its zero-based middle index is 4 and its median is 5. The four medians are 10, 11, 12, and 5. Their sum is 10 + 11 + 12 + 5 = 38.
4. Explain why the greedy result is correct
The key invariant is that the largest N minus 1 values remain singleton medians. Each of those large values is therefore guaranteed to contribute its full value to the result. All smaller values are grouped into the one remaining array, so only that array's median is affected by them. Placing smaller values into one of the large singleton arrays cannot increase that singleton's median and can lower it. The greedy construction therefore protects the largest median contributions while concentrating the effect of the smaller values into one array.
5. Explain the C# implementation
The method first validates nums and N. It sorts nums in ascending order. remainingCount = M - (N - 1) is the number of values in the final non-singleton array. Because that final array is the sorted prefix, the code reads its median directly. For an odd remainingCount, it reads one middle value. For an even remainingCount, it averages the two middle values according to the diagram's stated assumption. It then adds every value from remainingCount through M - 1. Those values are the N minus 1 singleton medians. Finally, it returns the total.
6. Explain complexity and edge cases
Sorting M values takes O(M log M) time. Reading the remaining-array median is O(1), and adding the N minus 1 singleton medians takes O(N), so sorting dominates. The algorithm uses O(1) auxiliary space beyond the sort implementation when only the maximum sum is returned. If the actual arrays are materialized, storing them requires O(M) additional space. If N = 1, the result is the median of the whole sorted input. If N = M, every array is a singleton and the result is the sum of all values. Duplicate and negative integers follow the same greedy structure. N less than 1 or greater than M is invalid.
Key Insight / Why This Solution Works
Sort the integers in non-decreasing order. Let M be the number of integers. Reserve the largest N minus 1 values as N minus 1 singleton arrays. Put the first M - (N - 1) sorted values into the final array. The central invariant is that the largest N minus 1 values stay protected as singleton medians, while every smaller value affects only the one remaining array. Because the final array is already sorted, its median can be read directly from the sorted prefix. Add that median to the N minus 1 singleton values to get the maximum sum shown by the diagram.
Code
using System;
publicstaticclassProgram
{
publicstaticvoidMain()
{
// Run the exact example shown in the approved diagram.int[] nums = { 12, 1, 5, 10, 3, 8, 2, 9, 6, 11, 4, 7 };
int n = 4;
// Compute the maximum possible sum of the N medians.double result = MaximizeSumOfMedians(nums, n);
// The approved example produces 38.
Console.WriteLine(result);
}
publicstaticdoubleMaximizeSumOfMedians(int[] nums, int n)
{
// A null input cannot be partitioned.if (nums == null)
{
thrownew ArgumentNullException(nameof(nums));
}
int m = nums.Length;
// Exactly N non-empty arrays are required, so N must be from 1 through M.if (n < 1 || n > m)
{
thrownew ArgumentOutOfRangeException(nameof(n));
}
// Sort so the largest N - 1 values are at the end of the array.
Array.Sort(nums);
// The first remainingCount values form the one non-singleton array.// The last N - 1 values will each become a singleton median.int remainingCount = m - (n - 1);
double total;
// Read the median directly from the sorted prefix.if ((remainingCount & 1) == 1)
{
// Odd length has one middle value.
total = nums[remainingCount / 2];
}
else
{
// The diagram defines an even-length median as the average// of the two middle sorted values.int right = remainingCount / 2;
int left = right - 1;
// Cast before addition so two int values cannot overflow first.
total = ((long)nums[left] + nums[right]) / 2.0;
}
// Add the N - 1 largest values.// Each value represents a singleton array, so it is that array's median.for (int i = remainingCount; i < m; i++)
{
total += nums[i];
}
// Return the maximum sum of all N medians.return total;
}
}
Time & Space Complexity
Let M = nums.Length. Sorting the M integers takes O(M log M) time. After sorting, finding the final-array median takes O(1) time. Adding the N minus 1 singleton medians takes O(N) time, so the sorting step is the dominant cost. The algorithm uses O(1) auxiliary space beyond the sort implementation when it returns only the maximum sum. If we also build and return the actual partition arrays, storing those arrays requires O(M) additional space.
Where it is used
This sorting and greedy pattern is useful in allocation and partitioning problems where a group's contribution depends on an ordered position such as its median. Sorting exposes the largest values, and a greedy grouping rule can protect those high-value contributions while concentrating less useful values into fewer groups.
Why Interviewers Ask This
This question tests whether the candidate can recognize a sorting and greedy-partitioning pattern. The interviewer wants to see careful reasoning about how group size affects a median and why large values should be protected as singleton arrays. It also tests whether the candidate can maintain a clear invariant, derive the correct split point, handle the median convention explicitly, write correct C# code, analyze sorting complexity, and reason about boundary cases such as N = 1 and N = M.
Common interview mistakes
One mistake is choosing all N largest values as medians and then placing smaller values into those arrays. Adding smaller values can change a median, so those large values may no longer remain medians. Another mistake is using the wrong split index. The final array must contain M - (N - 1) values. Candidates may also forget to clarify how an even-length median is calculated. Another common error is forgetting the O(M log M) sorting cost. Finally, do not claim O(1) auxiliary space if the implementation actually materializes and stores all N partition arrays.
Interview tip
State the invariant before coding: the largest N minus 1 values stay as singleton medians, and every remaining value goes into one final array. Then derive remainingCount = M - (N - 1). This connects the greedy proof directly to the implementation.
Interviewer may ask next
What changes if I also need to return the actual N arrays instead of only the maximum sum?
The greedy partition does not change. After sorting, create one array from the first M - (N - 1) values. Then create N - 1 singleton arrays from the remaining largest values. The correctness argument stays the same because the grouping is identical. Sorting still takes O(M log M) time. Materializing the result requires O(M) additional space because all M values must be stored in the returned arrays. The tradeoff is extra memory in exchange for returning the complete partition.
How does the algorithm behave when N = 1 or N = M?
When N = 1, remainingCount equals M. There are no large singleton arrays, so the result is simply the median of the entire sorted input. When N = M, remainingCount equals 1. The first sorted value forms one singleton array, and every other value is also a singleton. Therefore every input value is a median and the result is the sum of all values. The same implementation handles both cases without changing the algorithm.
13. Reverse a binary treeCodingEasyAmazon
i Question Details
Swap the left and right children recursively or iteratively until the whole tree is mirrored, and note how the root remains the same node.
Short Interview Answer (30-60 seconds)
I would reverse the tree with recursion. At each non-null node, I swap its left and right child references. Then I recursively process the new left subtree and the new right subtree. This gives the pre-order sequence 1, 3, 7, 6, 2, 5, 4 for the shown example. Every node is processed once, so the time complexity is O(n). The recursion stack uses O(h) auxiliary space, where h is the tree height.
The problem gives a binary tree and asks us to mirror it. Mirroring means every left child becomes the right child, and every right child becomes the left child. We keep the same root node. We only change the child links below it. The diagram uses recursion. At each node, we swap its two children first. Then we process the new left side and the new right side. This works because the same rule is applied to every node in the tree.
Useful Questions to Ask the Interviewer
Can the input root be null?
Is it acceptable to modify the existing tree instead of creating a new tree?
Should I return the same root reference after the tree is mirrored?
How to Explain It in an Interview
1. Understand the input and required output
The input is the root node of a binary tree. The output is the same root node after the whole tree has been mirrored. We are changing node references, not node values. The root stays the same object. For the example, the original tree has root 1. Node 1 has children 2 and 3. Node 2 has children 4 and 5. Node 3 has children 6 and 7. The mirrored result has root 1, children 3 and 2, then children 7 and 6 under node 3, and 5 and 4 under node 2.
2. Choose the recursive algorithm
For each node, first check whether it is null. If it is null, return null. Otherwise, swap the left and right child references. After the swap, recursively reverse the new left subtree. Then recursively reverse the new right subtree. Finally, return the current node. The important rule is that when a node finishes, its own children have been swapped and both of its subtrees have also been mirrored.
3. Walk through the example
We start at node 1. Its children are 2 and 3. We swap them, so node 1 now has left child 3 and right child 2. Because the code recurses into the new left child first, the next node is 3.
At node 3, its children are 6 and 7. We swap them, so its children become 7 and 6. We then visit node 7. It is a leaf, so swapping its null children does not change the tree. Its recursive calls reach null and return. Next we visit node 6. It is also a leaf and returns without a structural change.
After the subtree rooted at 3 is complete, we process node 2. Its children are still 4 and 5 at this point. We swap them, so they become 5 and 4. Then node 5 is processed as a leaf, followed by node 4 as a leaf. The complete processing order is 1, 3, 7, 6, 2, 5, 4.
The final tree is root 1 with left child 3 and right child 2. Node 3 has children 7 and 6. Node 2 has children 5 and 4. This matches the mirrored tree in the diagram.
4. Explain why the result is correct
At every non-null node, the algorithm swaps exactly the two child references that must exchange sides in the mirrored tree. Then it applies the same operation to both resulting subtrees. The base case stops recursion when there is no node. Because every node is handled this way, every left and right relationship in the original tree is reversed. The root reference itself never changes.
5. Explain the C# implementation
The method receives a TreeNode reference. If the reference is null, it returns null. Otherwise, it saves the original left child in a temporary variable, assigns the original right child to Left, and assigns the saved child to Right. It then calls ReverseTree on root.Left and root.Right in that order. Those calls mirror both subtrees. Finally, the method returns root, which is the same node reference that was passed into that recursive call.
6. Explain complexity and edge cases
There are n nodes in the tree. Each node is visited and processed once, so the time complexity is O(n). The recursion stack can contain one call for each level of the tree, so the auxiliary space is O(h), where h is the tree height. In the worst case of a completely skewed tree, h can be n, giving O(n) stack space. A null tree returns null. A single-node tree stays unchanged. A skewed tree is still mirrored correctly. A complete or any arbitrary binary tree is handled by the same recursive rule.
Key Insight / Why This Solution Works
The key idea is to mirror the tree locally at every node. For the current node, swap its left and right child references. Then recursively apply the same operation to the new left subtree and the new right subtree. The central invariant is: when ReverseTree returns for a non-null node, that same node is still the subtree root, its left and right children have exchanged sides, and both child subtrees are already mirrored. The null base case stops recursion at the ends of the tree.
Code
using System;
using System.Collections.Generic;
publicclassTreeNode
{
publicint Val;
public TreeNode? Left;
public TreeNode? Right;
publicTreeNode(int val)
{
Val = val;
}
}
publicstaticclassProgram
{
publicstatic TreeNode? ReverseTree(TreeNode? root)
{
// Base case: there is no node to mirror.if (root == null)
{
returnnull;
}
// Swap this node's child references so left becomes right// and right becomes left.
TreeNode? temp = root.Left;
root.Left = root.Right;
root.Right = temp;
// Process the new left subtree first.// For the example, this makes node 3 follow node 1.
ReverseTree(root.Left);
// Process the new right subtree after the left subtree is complete.// For the example, node 2 is processed after nodes 3, 7, and 6.
ReverseTree(root.Right);
// Return the same node reference. Only its child links changed.return root;
}
publicstaticvoidMain()
{
// Build the exact input tree from the diagram.
TreeNode root = new TreeNode(
1) { Left = new TreeNode(2) { Left = new TreeNode(4), Right = new TreeNode(5) },
Right = new TreeNode(3) { Left = new TreeNode(6), Right = new TreeNode(7) } };
// Mirror the existing tree. The returned reference is the same root node.
TreeNode? mirroredRoot = ReverseTree(root);
// Print the mirrored tree in level order.// Expected values: 1 3 2 7 6 5 4
PrintLevelOrder(mirroredRoot);
}
privatestaticvoidPrintLevelOrder(TreeNode? root)
{
// Handle a null tree without trying to enqueue a missing node.if (root == null)
{
Console.WriteLine("null");
return;
}
Queue<TreeNode> queue = new Queue<TreeNode>();
queue.Enqueue(root);
// Visit each existing node so the final mirrored structure is visible.while (queue.Count > 0)
{
TreeNode current = queue.Dequeue();
Console.Write(current.Val + " ");
// Add children in left-to-right order to show the mirrored levels.if (current.Left != null)
{
queue.Enqueue(current.Left);
}
if (current.Right != null)
{
queue.Enqueue(current.Right);
}
}
Console.WriteLine();
}
}
Time & Space Complexity
Let n be the number of nodes and h be the height of the tree. The algorithm visits and processes every node once, so the time complexity is O(n). The extra memory comes from recursive calls. At most one chain from the root to a deepest node is active at one time, so the auxiliary space is O(h). For a balanced tree, h is about log n. For a completely skewed tree, h can be n, so the worst-case recursion space is O(n).
Where it is used
This pattern is useful when software needs to transform a tree by recursively applying the same change to every subtree. Mirroring can appear in tree visualization, structural transformations, testing symmetric tree behavior, and interview problems that check whether you understand recursion and node references.
Why Interviewers Ask This
This problem checks whether you understand recursive tree traversal, base cases, and reference updates. The interviewer can see whether you know the difference between changing node values and changing tree structure. It also tests whether you can reason about the order of operations after a swap, explain why the same root is returned, and give the correct O(n) time and O(h) recursion-space complexity, including the skewed-tree worst case.
Common interview mistakes
A common mistake is to forget the null base case, which can cause recursion to fail when it reaches missing children. Another mistake is to swap the children but then recurse using the wrong assumed order. In this solution, the swap happens first, so the recursive order follows the new left child and then the new right child. Candidates can also accidentally create a new root even though the diagram keeps the same root reference. Another mistake is to say the auxiliary space is O(1) and ignore the recursion stack. Finally, changing node values instead of swapping child references does not correctly mirror the tree structure.
Interview tip
Say the order out loud while coding: "swap first, recurse into the new left child, recurse into the new right child, return the same root." This makes it easier to keep your code and walkthrough consistent.
Interviewer may ask next
Can you reverse the same binary tree iteratively instead of recursively?
Yes. Use a queue starting with the root. Repeatedly remove one node, swap its left and right children, and add its non-null children to the queue. Every node is still processed once, so the time complexity is O(n). The queue can hold O(w) nodes, where w is the maximum width of the tree. The main tradeoff is that recursion uses stack depth O(h), while the iterative version uses an explicit queue.
What happens if the tree is very skewed and recursion depth is a concern?
A skewed tree can have height n, so the recursive version can use O(n) stack space and may risk stack overflow for a very deep tree. An iterative traversal can avoid recursive call-stack growth. It still takes O(n) time. Its extra space depends on the explicit traversal structure. For a queue-based version, the space is O(w), where w is the maximum tree width. Correctness is preserved because every visited node still has its two child references swapped exactly once.
14. Given string s, return the longest palindromic substring in sCodingMediumAmazon
i Question Details
Find the longest contiguous palindrome within one string and explain how the answer changes for odd-length versus even-length centers.
Short Interview Answer (30-60 seconds)
I would use expand around center. Every palindrome has a center, so for each index I check an odd-length center at (i, i) and an even-length center at (i, i + 1). I expand left and right while the characters match. When I find a longer palindrome, I save its start index and length. For "babad", the method returns "bab", while "aba" is also valid. The worst-case time is O(n²), and the auxiliary space is O(1).
We are given one string and need to return its longest contiguous part that reads the same forward and backward. That part is called a palindrome. A palindrome can have one character in the middle, such as "bab", or a center between two characters, such as "abba". The diagram uses expand around center because every palindrome has one of these two center forms. We test both forms at every position and remember the longest palindrome found.
Useful Questions to Ask the Interviewer
If more than one longest palindrome exists, can I return any one of them?
Should an empty input return an empty string?
Should character comparison remain case-sensitive?
How to Explain It in an Interview
1. Understand the input and required output
The input is a string s. We must return one contiguous substring of s that is a palindrome and has maximum length. We return the substring itself, not its indexes. For s = "babad", both "bab" and "aba" have length 3. The shown implementation keeps the first longest palindrome it finds, so its returned result is "bab".
2. Choose expand around center
Every palindrome has a center. An odd-length palindrome has one character as its center. An even-length palindrome has a center between two characters. For each index i, the algorithm first checks the odd center (i, i). It then checks the even center (i, i + 1). From each center, it moves left and right while both indexes remain inside the string and the two characters are equal.
3. Initialize the state
If the string is null, empty, or has one character, the method returns it immediately. Otherwise, n is the string length. bestStart starts at 0 and bestLen starts at 1. A single character is always a palindrome, so this is a valid initial answer. For each center, left and right mark the current palindrome boundaries.
4. Walk through the example
For s = "babad", the characters are b, a, b, a, d at indexes 0 through 4.
At odd center i = 0, s[0] equals s[0], so "b" is a palindrome of length 1. It does not beat bestLen = 1. After moving outward, left becomes -1, so expansion stops.
At even center (0, 1), s[0] is "b" and s[1] is "a". They do not match, so no even-length palindrome is found there.
At odd center i = 1, s[1] equals s[1], giving "a". The next comparison is s[0] with s[2]. Both are "b", so the palindrome becomes "bab" with length 3. After moving outward again, left becomes -1. Because 3 is greater than bestLen = 1, the saved state becomes bestStart = 0 and bestLen = 3.
At even center (1, 2), s[1] and s[2] are different, so expansion stops immediately.
At odd center i = 2, s[2] equals s[2], giving "b". Next, s[1] equals s[3], so the palindrome becomes "aba" with length 3. Next, s[0] is "b" and s[4] is "d", so expansion stops. Length 3 only ties the saved best length. The code updates only when currentLen is strictly greater than bestLen, so "bab" remains saved.
The remaining centers do not produce a longer palindrome. At odd center i = 4, s[4] equals s[4], giving "d". After moving outward, right becomes 5, which is outside the string, so expansion stops. The final state is bestStart = 0 and bestLen = 3. Therefore, s.Substring(0, 3) returns "bab".
5. Explain why the result is correct
Every palindrome is either odd-length or even-length, so every palindrome has one of the center forms that the algorithm checks. For one chosen center, expanding while the two characters match finds the largest palindrome for that center. Since the algorithm examines every possible center and keeps the longest palindrome seen, the saved substring is a longest palindromic substring.
6. Explain the C# implementation
The main loop visits every index from 0 through n - 1. For each i, it calls ExpandAroundCenter twice. The first call uses left = i and right = i for an odd-length center. The second uses left = i and right = i + 1 for an even-length center. The helper checks the bounds and character equality before each expansion. It calculates currentLen = right - left + 1 and updates bestStart and bestLen only when currentLen is larger. Finally, the method returns s.Substring(bestStart, bestLen).
7. Explain complexity and edge cases
There are O(n) possible centers. In the worst case, expansion from one center can inspect O(n) characters. Therefore, the total worst-case time is O(n²). The algorithm uses only a constant number of integer variables, so auxiliary space is O(1). Important cases shown in the diagram are an empty string, one character, all identical characters, and a string with no repeated characters.
Key Insight / Why This Solution Works
The key idea is that every palindrome has a center. An odd-length palindrome is centered on one character, while an even-length palindrome is centered between two characters. For each index, we expand from both center types. The important invariant is that the range from left through right is a palindrome whenever the indexes are valid and s[left] equals s[right]. Expanding until a mismatch or boundary finds the longest palindrome for that center. Checking all centers and keeping the longest result therefore finds a longest palindromic substring.
Code
using System;
publicstaticclassProgram
{
publicstaticvoidMain()
{
// Run the exact example shown in the diagram.string input = "babad";
string result = LongestPalindrome(input);
// This implementation keeps the first longest palindrome found.// For this input it prints "bab". "aba" is also a valid longest palindrome.
Console.WriteLine(result);
}
publicstaticstringLongestPalindrome(string s)
{
// Empty and one-character strings already have their final answer.if (string.IsNullOrEmpty(s) || s.Length == 1)
{
return s;
}
int n = s.Length;
// Any single character is a palindrome, so begin with index 0 and length 1.int bestStart = 0;
int bestLen = 1;
// Process every position as a possible palindrome center.for (int i = 0; i < n; i++)
{
// Odd-length palindrome: the center is the character at index i.
ExpandAroundCenter(s, i, i, ref bestStart, ref bestLen);
// Even-length palindrome: the center is between i and i + 1.
ExpandAroundCenter(s, i, i + 1, ref bestStart, ref bestLen);
}
// Return the longest contiguous palindrome found across all centers.return s.Substring(bestStart, bestLen);
}
privatestaticvoidExpandAroundCenter(string s, int left, int right, refint bestStart,
refint bestLen)
{
int n = s.Length;
// Expand only while both indexes are inside the string and the characters match.while (left >= 0 && right < n && s[left] == s[right])
{
// The inclusive range [left, right] is currently a palindrome.int currentLen = right - left + 1;
// Replace the saved answer only when this palindrome is strictly longer.// This keeps the earlier result when two longest palindromes have equal length.if (currentLen > bestLen)
{
bestStart = left;
bestLen = currentLen;
}
// Move one position outward on each side for the next comparison.
left--;
right++;
}
}
}
Time & Space Complexity
Let n be the string length. There are O(n) centers to examine. In the worst case, one center can expand across O(n) characters. This gives O(n²) worst-case time. The algorithm does not use a map, set, stack, queue, dynamic programming table, or copied working array. It keeps only a constant number of indexes and lengths, so the auxiliary space complexity is O(1).
Where it is used
Expand-around-center is useful when software needs to find symmetric ranges inside text without building a large table. It fits text analysis, pattern detection, and other string-processing tasks where constant auxiliary space is useful and O(n²) worst-case time is acceptable.
Why Interviewers Ask This
This problem tests whether a candidate recognizes the center structure of palindromes and handles both odd-length and even-length cases. It also tests careful index and boundary reasoning, the difference between a substring and a subsequence, correct handling of ties, and the ability to keep C# code consistent with the explanation. The interviewer can also evaluate whether the candidate explains the O(n²) worst-case time and O(1) auxiliary space correctly.
Common interview mistakes
A common mistake is checking only odd-length centers, which misses even-length palindromes such as "abba". Another is checking only even-length centers and missing values such as "bab". Candidates may confuse a substring with a subsequence, but this problem requires contiguous characters. It is also easy to calculate the inclusive length incorrectly; it must be right - left + 1. Another mistake is reading s[left] or s[right] before checking the bounds. Finally, changing the saved answer on an equal-length tie would not match the shown strict greater-than update rule.
Interview tip
Before coding, draw one odd center and one even center. Then explain that the helper expands outward while characters match, saves only a strictly longer palindrome, and stops at a mismatch or string boundary.
Interviewer may ask next
What changes if I must return all longest palindromic substrings instead of just one?
I would keep the same expand-around-center process. Instead of storing only bestStart and bestLen, I would keep a collection of ranges whose length equals the current maximum. If a longer palindrome appears, I would clear the collection and add the new range. If another palindrome has the same maximum length, I would add that range too. Correctness is preserved because every odd and even center is still checked. The search remains O(n²) worst-case time. Extra working space becomes O(k) for k stored longest ranges. The tradeoff is higher memory use so every tied result can be returned.
How could the solution be changed if O(n²) worst-case time is too slow for a very large string?
A related linear-time palindrome algorithm, Manacher's algorithm, can reduce the worst-case time to O(n). It stores palindrome-radius information for previously processed centers and reuses that information instead of expanding every center completely from scratch. It uses O(n) auxiliary space. Correctness still depends on representing the palindrome around each center and tracking the maximum valid radius. The tradeoff is significantly more complex code and reasoning compared with the simple expand-around-center approach shown in the diagram.
15. Given a 2D grid of M rows by N columns, where each cell can either be "land" or "water,” find the total number of islandsCodingMediumAmazon
i Question Details
Count connected land components in a grid, and specify whether diagonal adjacency is excluded from the connectivity rule.
Short Interview Answer (30-60 seconds)
I would scan the grid row by row. When I find a land cell that has not been visited, I increase the island count and run DFS from that cell. DFS marks every horizontally or vertically connected land cell as visited, so the same island is never counted twice. Diagonal cells are not connected. Each cell is processed at most once, so the time complexity is O(M × N). The visited matrix and recursion stack use O(M × N) auxiliary space.
The input is a grid with M rows and N columns. Each cell is either land or water. We need to count how many separate groups of land exist. Land cells belong to the same island only when they touch up, down, left, or right. Diagonal touching does not connect islands. I scan the grid from top-left to bottom-right. Whenever I find land that has not been visited, I count one new island and use DFS to mark every connected land cell in that island as visited.
Useful Questions to Ask the Interviewer
Should diagonal land cells be considered connected? In this solution, diagonal adjacency is excluded.
Can the grid be empty? The implementation handles an empty grid by returning 0.
How to Explain It in an Interview
1. Understand the input and required output
The input is a two-dimensional grid. A land cell is represented by '1' and a water cell by '0'. We return one integer: the total number of islands. Two land cells belong to the same island only when they are connected through up, down, left, or right. Diagonal neighbors do not connect islands.
2. Choose DFS and a visited matrix
I use depth-first search, or DFS. DFS starts from one unvisited land cell and reaches every land cell in the same four-directionally connected component. I also use a bool[,] visited matrix with the same M by N size as the grid. visited[r, c] tells us whether that cell has already been processed. The main invariant is that after one DFS finishes, every land cell in that island is marked visited.
3. Initialize and scan the grid
Set islandCount to 0. Create the visited matrix with every value initially false. Scan the cells in row-major order, from the top-left toward the bottom-right. For each cell, check whether it is land and has not been visited. If both conditions are true, increment islandCount and start DFS. DFS finishes marking the complete connected island before the outer scan continues.
4. Walk through the verified example
The example grid is [[1,1,0,0,1],[1,0,0,1,1],[0,0,0,0,0],[1,0,1,1,0]]. It has 4 rows, 5 columns, and 20 cells. The first unvisited land cell is (0,0). DFS visits (0,0), (0,1), and (1,0), so islandCount becomes
The next unvisited land cell is (0,4). DFS visits (0,4), (1,4), and (1,3), so islandCount becomes
The next unvisited land cell is (3,0). It is isolated, so islandCount becomes
The next unvisited land cell is (3,2). DFS also visits (3,3), so islandCount becomes
No unvisited land remains, so the returned result is 4.
5. Explain why the result is correct
Every DFS begins from one land cell that has not been visited. That DFS marks every land cell reachable from it using only the four allowed directions. Because all cells in that island become visited, the outer scan cannot count the same island again. Every island contains at least one land cell that the scan eventually reaches. Therefore each island starts exactly one DFS and increases islandCount exactly once.
6. Explain the C# implementation
The Main method creates the exact 4 by 5 example grid and prints the result. NumIslands handles an empty grid, creates the visited matrix, defines the four allowed direction offsets, and scans every row and column. When it finds unvisited land, it increments count and calls Dfs. Dfs stops for coordinates outside the grid, water cells, or already visited cells. Otherwise it marks the current cell visited and recursively explores up, down, left, and right.
7. Explain complexity and edge cases
There are M × N cells. Each cell is processed at most once as part of the traversal, so the time complexity is O(M × N). The visited matrix requires O(M × N) space. In the worst case, such as one large all-land island, the recursive DFS call stack can also grow to O(M × N). Relevant edge cases include an empty grid, an all-water grid, an all-land grid, a single land cell, and land cells that touch only diagonally.
Key Insight / Why This Solution Works
Use DFS to count four-directionally connected components. Scan the grid in row-major order. When grid[r][c] is land and visited[r, c] is false, that cell starts a new island. Increment the island count and run DFS from that cell. DFS marks the current cell and every land cell reachable through up, down, left, or right as visited. The central invariant is that after a DFS finishes, every cell in that island has been marked visited. Therefore the same island cannot be counted again.
Code
using System;
publicstaticclassProgram
{
publicstaticvoidMain()
{
// Use the exact 4 x 5 example shown in the diagram.char[][] grid = { newchar[] { '1', '1', '0', '0', '1' },
newchar[] { '1', '0', '0', '1', '1' },
newchar[] { '0', '0', '0', '0', '0' },
newchar[] { '1', '0', '1', '1', '0' } };
// Count four-directionally connected land components.int result = NumIslands(grid);
Console.WriteLine(result); // 4
}
publicstaticintNumIslands(char[][] grid)
{
// An empty grid contains no islands.if (grid == null || grid.Length == 0 || grid[0].Length == 0)
{
return0;
}
int rows = grid.Length;
int cols = grid[0].Length;
// visited[r, c] records whether that cell has already// been processed as part of a discovered island.bool[,] visited = newbool[rows, cols];
int count = 0;
// Explore only up, down, left, and right.// Diagonal adjacency is intentionally excluded.int[] dr = { -1, 1, 0, 0 };
int[] dc = { 0, 0, -1, 1 };
// Scan every cell in row-major order.for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
// An unvisited land cell begins exactly one new island.if (grid[r][c] == '1' && !visited[r, c])
{
count++;
// Mark the complete connected island before scanning continues.
Dfs(grid, visited, r, c, dr, dc);
}
}
}
// Every DFS start corresponds to one distinct island.return count;
}
privatestaticvoidDfs(char[][] grid, bool[,] visited, int r, int c, int[] dr, int[] dc)
{
int rows = grid.Length;
int cols = grid[0].Length;
// Stop when the coordinates move outside the grid.if (r < 0 || r >= rows || c < 0 || c >= cols)
{
return;
}
// Stop on water or on a cell already processed by this traversal.if (grid[r][c] != '1' || visited[r, c])
{
return;
}
// Mark before exploring neighbors so this land cell is not processed again.
visited[r, c] = true;
// Continue DFS through all four allowed neighboring positions.for (int k = 0; k < 4; k++)
{
int nextRow = r + dr[k];
int nextCol = c + dc[k];
Dfs(grid, visited, nextRow, nextCol, dr, dc);
}
}
}
Time & Space Complexity
Let M be the number of rows and N be the number of columns. The time complexity is O(M × N) because there are M × N cells, and each cell is processed at most once by the traversal. The auxiliary space is O(M × N). The visited matrix contains M × N Boolean entries. In the worst case, the recursive DFS call stack can also grow to O(M × N), such as when the grid contains one very large connected island.
Where it is used
This connected-component pattern is useful for grouping neighboring cells into separate regions. Practical examples include map regions, image segmentation, flood-fill tools, game boards, and finding separate clusters in grid-based data.
Why Interviewers Ask This
This problem tests whether you can recognize connected components in a grid and apply DFS correctly. It also checks whether you define connectivity precisely, especially that diagonal neighbors are excluded. The interviewer can evaluate your visited-state handling, recursion base cases, row and column indexing, traversal order, and C# implementation. It also shows whether you can explain why each island is counted once and give the correct O(M × N) time and auxiliary-space bounds.
Common interview mistakes
A common mistake is treating diagonal land cells as connected even though diagonal adjacency is excluded. Another mistake is incrementing the island count for every land cell instead of only when an unvisited land cell starts a new DFS. Marking a cell visited too late can cause repeated recursive work or cycles between neighboring land cells. Candidates may also forget the out-of-bounds, water, or already-visited base cases. Finally, claiming O(1) auxiliary space is incorrect because the visited matrix and recursive call stack can grow with the grid.
Interview tip
State the invariant before coding: every time DFS starts from an unvisited land cell, you have found exactly one new island, and that DFS marks the whole island so it cannot be counted again.
Interviewer may ask next
How would the solution change if diagonal land cells also counted as connected?
The main connected-component approach stays the same. I would expand the direction arrays from four directions to eight by adding up-left, up-right, down-left, and down-right. DFS would then mark every land cell connected through any of those eight directions. The correctness rule is unchanged because one DFS still marks exactly one complete connected component. The time complexity remains O(M × N), and the auxiliary space remains O(M × N).
How would you handle a very large grid if recursive DFS could overflow the call stack?
I would keep the same DFS connected-component approach but make it iterative with an explicit stack. When an unvisited land cell starts a new island, I would mark it visited and push it onto the stack. Then I would repeatedly pop a cell and push its unvisited four-directional land neighbors, marking each one before pushing it. This preserves the same correctness invariant while avoiding deep language-level recursion. The time complexity remains O(M × N), and the worst-case auxiliary space remains O(M × N).
16. Given a matrix of N by M elements. These elements will be one of the following: E for an entrance or an exit, _for a traversable path, or X for an untraversable path. Design an algorithm that can traverse this matrix and determine the shortest path between the two E nodes. Assume that there will always be exactly one entrance and one exit (again, both are represented by an E). Assume that there will always be a solution path, though there may be multiple. You may not move diagonally.CodingHardAmazon
i Question Details
Search the grid for the shortest route between the two E cells, obey the no-diagonal movement rule, and explain how blocked cells and multiple candidate paths are handled.
Short Interview Answer (30-60 seconds)
I would use breadth-first search, or BFS, from the first E cell. I keep a queue, mark each cell visited when I enqueue it, and store its parent so I can rebuild the route later. BFS explores the grid by increasing number of moves, so the first time I dequeue the second E, I have a shortest path. Blocked X cells are skipped. The time complexity is O(N × M), and the auxiliary space is O(N × M).
The matrix contains two E cells. One is the start and one is the goal. A cell marked _ can be used. A cell marked X cannot be used. We may move only up, down, left, or right. We need the route with the smallest number of moves. The diagram uses breadth-first search because every valid move has the same cost. It also stores parent links so the actual shortest route can be returned.
Useful Questions to Ask the Interviewer
Should I return the actual path as well as its length?
Can I assume, as stated, that there are exactly two E cells and that a valid path always exists?
If several shortest paths exist, is returning any one of them acceptable?
How to Explain It in an Interview
1. Understand the input and required output
The input is an N by M character matrix. Each cell is E, _, or X. There are exactly two E cells. In the diagram, the first E found in row-major order is the start at (0,0). The second E is the goal at (5,5). We return one shortest route and its length in moves. For the shown 6 by 6 example, the result is 10 moves and 11 cells.
2. Choose BFS and a queue
I use breadth-first search. BFS visits reachable cells in increasing distance from the start. A queue stores cells waiting to be processed. I also use a visited matrix so a cell is added only once. A parent matrix stores which cell led to each newly discovered cell. The main invariant is that when a cell is first discovered by BFS, the recorded route to that cell has the minimum possible number of moves.
3. Initialize the state
First, scan the matrix to find the two E cells. Create a queue, a visited matrix, and a parent matrix. Put the start cell into the queue and mark it visited immediately. The parent of the start remains null because it has no previous cell. The implementation checks the four legal moves in the order up, down, left, and right. It rejects cells that are outside the matrix, contain X, or were already visited.
4. Walk through the example
The start is (0,0). The distance levels shown in the diagram are: level 0: (0,0). Level 1: (0,1). Level 2: (0,2). Level 3: (1,2). Level 4: (2,2). Level 5 contains (2,3) and (2,1). Level 6 contains (2,4) and (2,0). Level 7 contains (3,4), (1,4), (2,5), and (3,0). Level 8 contains (4,4), (0,4), and (4,0). Level 9 contains (4,5), (4,3), (0,5), and (4,1). At distance 10, the goal (5,5) is reached. The listed cells describe BFS distance levels. Cells on the same level all have the same minimum distance.
One shortest route shown in the diagram is (0,0) → (0,1) → (0,2) → (1,2) → (2,2) → (2,3) → (2,4) → (3,4) → (4,4) → (4,5) → (5,5). This route has 11 cells, so it has 10 moves.
5. Explain why the result is correct
Every legal move has the same cost of one step. BFS processes cells by increasing distance from the start. Therefore, when a cell is first discovered, no shorter route to that cell can appear later. When the goal is dequeued, its stored parent chain represents a shortest route. We then follow parent links from the goal back to the start and reverse that list.
6. Explain the C# implementation
The code scans for the two E cells. It allocates bool[,] for visited and Cell?[,] for parent links. It enqueues the start and marks it visited before processing begins. For each dequeued cell, it checks up, down, left, and right. A valid unvisited neighbor is marked visited, given a parent, and enqueued. Processing stops when the goal is dequeued. Finally, the code follows parent links backward, reverses the collected cells, and returns path.Count - 1 as the number of moves.
7. Explain complexity and edge cases
There are N × M cells. Each cell is enqueued at most once, and each processed cell checks four directions. Therefore the time complexity is O(N × M). The visited matrix, parent matrix, queue, and reconstructed path can all grow with the matrix, so auxiliary space is O(N × M). Relevant cases include a single-row or single-column route, adjacent E cells, several equally short paths, large open areas with narrow corridors, and E cells that are not on the border.
Key Insight / Why This Solution Works
Treat each traversable grid cell as a node in an unweighted graph. A move to an adjacent non-X cell is an edge with cost one. BFS is appropriate because all allowed moves have equal cost. The queue processes cells by increasing shortest distance from the start. The central invariant is that when BFS first discovers a cell, its parent chain represents a shortest route to that cell. Marking a cell visited when it is enqueued prevents duplicate queue entries. Parent links allow one shortest route to be reconstructed after reaching the goal.
Code
using System;
using System.Collections.Generic;
publicstaticclassProgram
{
// A Cell stores one row and column position in the matrix.publicsealedclassCell
{
publicint R;
publicint C;
publicCell(int r, int c)
{
R = r;
C = c;
}
}
publicstaticvoidMain()
{
// This is the exact 6 x 6 example shown in the diagram.char[,] grid = { { 'E', '_', '_', 'X', '_', '_' }, { 'X', 'X', '_', 'X', '_', 'X' },
{ '_', '_', '_', '_', '_', '_' }, { '_', 'X', 'X', 'X', '_', 'X' },
{ '_', '_', '_', '_', '_', '_' }, { 'X', 'X', 'X', '_', 'X', 'E' } };
// Run BFS and receive both the number of moves and one shortest path.
(int length, List<(int r, int c)> path) result = FindShortestPath(grid);
Console.WriteLine($"Shortest path length: {result.length} steps");
Console.WriteLine($"Number of cells in path: {result.path.Count}");
Console.WriteLine("Path:");
// Print the reconstructed route from the first E to the second E.for (int i = 0; i < result.path.Count; i++)
{
Console.Write($"({result.path[i].r},{result.path[i].c})");
if (i + 1 < result.path.Count)
{
Console.Write(" -> ");
}
}
Console.WriteLine();
}
publicstatic (int length, List<(int r, int c)> path) FindShortestPath(char[,] grid)
{
int n = grid.GetLength(0);
int m = grid.GetLength(1);
Cell? start = null;
Cell? goal = null;
// Scan in row-major order to locate the two E cells.for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
if (grid[i, j] == 'E')
{
if (start == null)
{
start = new Cell(i, j);
}
else
{
goal = new Cell(i, j);
}
}
}
}
// The stated problem guarantees exactly two E cells.if (start == null || goal == null)
{
thrownew InvalidOperationException("The grid must contain exactly two E cells.");
}
// Check Up, Down, Left, and Right. Diagonal movement is not allowed.int[] dr = { -1, 1, 0, 0 };
int[] dc = { 0, 0, -1, 1 };
// visited prevents the same cell from being queued more than once.bool[,] visited = newbool[n, m];
// parent[r,c] stores the cell from which (r,c) was first discovered.
Cell?[,] parent = new Cell?[n, m];
// BFS starts with only the entrance in the queue.
Queue<Cell> queue = new Queue<Cell>();
queue.Enqueue(start);
// Mark visited at enqueue time so another path cannot enqueue it again.
visited[start.R, start.C] = true;
while (queue.Count > 0)
{
// Dequeue the next cell in BFS order.
Cell current = queue.Dequeue();
// The first time the goal is dequeued, its distance is minimal.if (current.R == goal.R && current.C == goal.C)
{
break;
}
// Explore the four allowed neighboring positions.for (int k = 0; k < 4; k++)
{
int nr = current.R + dr[k];
int nc = current.C + dc[k];
// Skip positions outside the matrix.if (nr < 0 || nr >= n || nc < 0 || nc >= m)
{
continue;
}
// Skip blocked cells and cells already discovered by BFS.if (grid[nr, nc] == 'X' || visited[nr, nc])
{
continue;
}
// Mark the neighbor before enqueueing it to prevent duplicates.
visited[nr, nc] = true;
// Record the previous cell so the shortest route can be rebuilt.
parent[nr, nc] = current;
// Add this newly discovered cell to the BFS frontier.
queue.Enqueue(new Cell(nr, nc));
}
}
// Defensive fallback only; the stated problem guarantees a solution path.if (!visited[goal.R, goal.C])
{
thrownew InvalidOperationException("No path exists between the two E cells.");
}
// Reconstruct the route by walking from the goal through parent links.
List<(int r, int c)> path = new List<(int r, int c)>();
Cell? at = goal;
while (at != null)
{
path.Add((at.R, at.C));
at = parent[at.R, at.C];
}
// Parent links point backward, so reverse to start-to-goal order.
path.Reverse();
// A path containing K cells contains exactly K - 1 moves.int length = path.Count - 1;
return (length, path);
}
}
Time & Space Complexity
Let N be the number of rows and M be the number of columns. There are N × M cells. Each cell can enter the queue at most once, and every processed cell checks only four directions. The time complexity is O(N × M). The visited matrix, parent matrix, BFS queue, and reconstructed path can together use O(N × M) extra memory. Auxiliary space means memory used by the algorithm in addition to the input matrix.
Where it is used
This BFS pattern is useful for shortest routes in unweighted grids and graphs. Examples include maze solving, tile-based games, warehouse movement, robot navigation on equal-cost cells, and finding the minimum number of transitions between states when every transition has the same cost.
Why Interviewers Ask This
This problem checks whether a candidate recognizes an unweighted shortest-path problem and chooses BFS instead of a traversal that does not guarantee minimum distance. It also tests correct queue use, visited-state timing, matrix boundary checks, blocked-cell handling, parent-based path reconstruction, and accurate complexity analysis. In C#, it also shows whether the candidate can work safely with multidimensional arrays and generic collections while keeping the code consistent with the movement rules.
Common interview mistakes
A common mistake is using DFS and assuming the first route found is the shortest. Another is marking a cell visited only when it is dequeued, which can put the same cell in the queue multiple times. Candidates may forget to reject X cells or accidentally allow diagonal moves. Another mistake is not storing parent links when the actual path must be returned. Finally, counting cells instead of edges gives 11 instead of the correct 10 moves in the diagram example.
Interview tip
State the BFS invariant before coding: the queue explores cells by increasing distance, and each cell is marked visited when it is enqueued. Then explain that the parent matrix is used only to rebuild one shortest route. This makes the correctness argument and code structure easy to follow.
Interviewer may ask next
What changes if the problem no longer guarantees that a path exists?
The BFS traversal does not change. If the queue becomes empty and the goal was never visited, return an explicit no-path result instead of reconstructing a route. BFS has then explored every cell reachable from the start, so no valid path exists. The time complexity remains O(N × M), and the auxiliary space remains O(N × M). The main change is the method's return contract because failure is now a valid outcome.
What changes if moves have different positive costs instead of every move costing one?
Ordinary BFS would no longer guarantee the minimum total cost because BFS assumes every edge has the same cost. For positive weighted moves, use Dijkstra's algorithm with a priority queue. Store the best known cost and parent for each cell, and update them when a cheaper route is found. With V = N × M cells and at most about 4V grid edges, the time complexity is O(V log V), which is O(N × M × log(N × M)), and auxiliary space is O(N × M). The tradeoff is extra complexity and priority-queue work in exchange for supporting weighted moves.
17. Given two strings, determine if they are equal after backspace operators are processedCodingEasyAmazon
i Question Details
Compare two strings after applying backspace semantics, including how to handle trailing deletions and mismatched lengths after cleanup.
Short Interview Answer (30-60 seconds)
I would compare the strings from right to left using two pointers and two skip counters. When I see '#', I increase that string's skip count. While the skip count is positive, I ignore earlier characters because they are deleted. I only compare characters that survive the backspaces. If the current valid characters differ, I return false. If both strings finish together, I return true. This takes O(n + m) time and O(1) auxiliary space.
We have two strings where '#' acts like a backspace. It removes the previous character that has not already been removed. We need to decide whether both strings contain the same characters after all backspaces are applied. Instead of building two new strings, we start from the end of each input. We count pending backspaces and skip characters that would be deleted. Then we compare only the characters that remain. This fits the problem because deletions affect characters to the left, so scanning backward lets us process them directly.
Useful Questions to Ask the Interviewer
Should '#' always mean one backspace that deletes the previous available character?
Can a string contain more backspaces than available characters?
Is character comparison case-sensitive?
How to Explain It in an Interview
1. Understand the input and required output
The input is two strings, s and t. The output is a bool. We return true when the two strings are equal after applying every '#'. Otherwise, we return false. In the diagram, s is "ab#c" and t is "ad#c". Both become "ac", so the result is true.
2. Choose two pointers and skip counters
I use pointer i for s and pointer j for t. Both start at the last character. I also use skipS and skipT. A skip counter means how many earlier characters must be ignored because of backspaces. The main invariant is that whenever the code reaches the comparison step, i and j either point to the next surviving character from the right or are already before the start of the string.
3. Initialize the state
For s = "ab#c", i starts at 3. For t = "ad#c", j also starts at 3. Both skip counters start at 0. At index 3, both strings contain 'c'. These characters are valid, so we compare them. They match. Then both pointers move left.
4. Walk through the example
At index 2, both strings contain '#'. We increase skipS and skipT from 0 to 1 and move both pointers left. At index 1, s contains 'b' and t contains 'd'. Because each skip counter is 1, those characters are deleted. We decrease both skip counters back to 0 and move left again. At index 0, both strings contain 'a'. They are valid and equal. After comparing them, both pointers move to -1. Both strings are exhausted, so the method returns true.
5. Explain why the result is correct
Each skip counter exactly represents how many earlier characters still need to be deleted. The algorithm never compares a deleted character. It compares surviving characters in the same right-to-left order. If two surviving characters differ, the processed strings cannot be equal. If both strings finish without a mismatch, every surviving character matched, so the processed strings are equal.
6. Explain the C# implementation
The outer loop continues while either string still has characters to process. Two inner loops find the next valid character in each string. A '#' increases the matching skip counter. A normal character is skipped when that counter is positive. After both inner loops finish, the code reads the current valid characters, using '\0' when a string is exhausted. If those characters differ, it returns false. Otherwise, both pointers move left. When the outer loop ends, it returns true.
7. Explain complexity and edge cases
If n is the length of s and m is the length of t, every character moves past a pointer at most once. The time complexity is O(n + m). The algorithm uses only two indices, two skip counters, and two temporary characters, so auxiliary space is O(1). It also handles empty strings, strings containing only backspaces, extra backspaces, different resulting lengths, long runs of '#', and strings with no backspaces.
Key Insight / Why This Solution Works
The key idea is to avoid creating cleaned copies of the strings. Backspaces delete characters to their left, so scanning from right to left makes it possible to count deletions before deciding whether a character should be compared. Pointer i scans s and pointer j scans t. skipS and skipT record pending deletions. The central invariant is that after each inner skip loop finishes, the pointer either identifies the next surviving character from the right or is before the start of the string. Therefore, comparing those surviving characters is enough to determine equality. This keeps the extra memory constant.
Code
using System;
publicstaticclassProgram
{
publicstaticboolBackspaceCompare(string s, string t)
{
// Start at the end because each backspace deletes a character to its left.int i = s.Length - 1;
int j = t.Length - 1;
// These counters track how many earlier characters still need to be deleted.int skipS = 0;
int skipT = 0;
// Continue while at least one string still has characters to process.while (i >= 0 || j >= 0)
{
// Find the next surviving character in s.// A '#' adds one pending deletion. A normal character is skipped// while a pending deletion exists.while (i >= 0)
{
if (s[i] == '#')
{
skipS++;
i--;
}
elseif (skipS > 0)
{
skipS--;
i--;
}
else
{
// s[i] is the next valid character from the right.break;
}
}
// Find the next surviving character in t using the same rule.while (j >= 0)
{
if (t[j] == '#')
{
skipT++;
j--;
}
elseif (skipT > 0)
{
skipT--;
j--;
}
else
{
// t[j] is the next valid character from the right.break;
}
}
// Use '\0' when a string has no surviving character left.// This also detects different resulting lengths.char cs = i >= 0 ? s[i] : '\0';
char ct = j >= 0 ? t[j] : '\0';
// The next surviving characters must match.// If they do not, the processed strings cannot be equal.if (cs != ct)
{
returnfalse;
}
// The current surviving characters matched, so move left// and search for the next surviving characters.
i--;
j--;
}
// Both strings were fully processed without finding a mismatch.returntrue;
}
publicstaticvoidMain()
{
// Run the same verified example shown in the diagram.string s = "ab#c";
string t = "ad#c";
bool result = BackspaceCompare(s, t);
// Both strings become "ac", so this prints True.
Console.WriteLine(result);
}
}
Time & Space Complexity
Let n be s.Length and m be t.Length. The time complexity is O(n + m). Each pointer only moves from right to left, and each character is processed at most once. A character may be recognized as '#', skipped because of a pending backspace, or compared as a surviving character. The algorithm does not build new strings or collections. It uses two indices, two integer skip counters, and two character variables. Therefore, the auxiliary space complexity is O(1).
Where it is used
This right-to-left pointer pattern is useful when later symbols affect earlier data. It can be used for text editing rules such as backspaces, delete markers, or compact command histories where we only need to compare the final logical content and do not need to build the edited strings.
Why Interviewers Ask This
This problem checks whether a candidate can recognize that backspaces affect earlier characters and choose a traversal direction that simplifies the work. It tests careful pointer movement, maintaining skip-counter state, handling extra deletions and mismatched resulting lengths, and stopping correctly on a mismatch. It also checks whether the candidate can write clean C# boundary conditions and explain why the solution uses O(n + m) time with O(1) auxiliary space.
Common interview mistakes
A common mistake is to compare characters before removing the effect of pending backspaces. Another is to move left after seeing '#' without remembering that an earlier character must also be skipped. Candidates may also forget that several '#' characters can create several pending deletions. Another mistake is assuming both processed strings have the same length and accessing a pointer that is already below zero. Finally, claiming O(n + m) extra space would be incorrect for this implementation because it does not build cleaned strings.
Interview tip
While coding, explain the invariant before writing the comparison: after each skip loop, the pointer is either on the next surviving character or before the beginning of the string. That makes the comparison logic much easier to justify.
Interviewer may ask next
Why not build the processed version of each string first and then compare them?
That approach can also produce the correct result. It would still take O(n + m) time because both strings must be processed. However, storing the processed strings needs O(n + m) additional space in the worst case. The diagram's right-to-left two-pointer method avoids those copies and keeps auxiliary space at O(1). The tradeoff is that the pointer logic and skip counters require more careful boundary handling.
How does this solution handle more backspaces than available characters, such as "a##"?
The skip counter can remain positive even after all earlier characters have been consumed. The pointer simply continues moving left until it becomes -1. There is no attempt to access a character before index 0. The same rule is applied independently to both strings. The algorithm is still correct because extra backspaces only mean there are no surviving earlier characters. The complexity remains O(n + m) time and O(1) auxiliary space.
18. Given a user id and a list of purchases where each item in the list has a product id and user id #, return a list of products that the user hasn't # yet purchased yet but have been purchased by other users that have already purchased at least one thing in common with that user.CodingHardAmazon
i Question Details
Build the recommendation-style product list from overlapping purchase histories, explain the common-item prerequisite, and describe how you avoid recommending items the user already owns.
Short Interview Answer (30-60 seconds)
I would build two hash-based indexes: each user to the set of products they bought, and each product to the set of users who bought it. For the target user, I find other users who share at least one purchased product. Then I collect products bought by those users and exclude products the target user already owns. The hash-based work is O(n) expected time. Because the diagram sorts the final recommendations, total time is O(n + r log r), with O(n) auxiliary space.
The input gives one target user and a list of purchases. Each purchase connects a user to a product. We need to recommend products the target user has not bought. A product is eligible only when another user bought it and that user shares at least one product with the target user. The diagram solves this by building two purchase indexes, finding overlapping users, collecting their products, and removing products already owned by the target user.
Useful Questions to Ask the Interviewer
Should the returned products be unique? The diagram returns unique products.
Does the output need a stable order? The diagram sorts the result for deterministic output.
What should happen when the target user has no purchases? The diagram returns an empty list.
How to Explain It in an Interview
1. Understand the input and required output
The target user is user 1. The purchases are (1, P1), (1, P2), (2, P2), (2, P3), (3, P1), (3, P4), and (4, P5). User 1 already owns P1 and P2. We must return products that user 1 does not own but that were bought by users who share at least one product with user 1.
2. Build the two indexes
First, build a user-to-products map. It contains 1 → {P1, P2}, 2 → {P2, P3}, 3 → {P1, P4}, and 4 → {P5}. Also build a product-to-users map. It contains P1 → {1, 3}, P2 → {1, 2}, P3 → {2}, P4 → {3}, and P5 → {4}. Hash sets also make duplicate purchase records harmless because each user-product relationship is stored once.
3. Find users with a common purchase
User 1 owns P1 and P2. From P1, the product-to-users map gives users {1, 3}. Excluding user 1 leaves user 3. From P2, it gives {1, 2}. Excluding user 1 leaves user 2. The candidate users are therefore {2, 3}. User 4 is not included because user 4 shares no product with user 1.
4. Collect candidate products and remove owned products
User 2 bought {P2, P3}. User 3 bought {P1, P4}. Their combined unique product set is {P1, P2, P3, P4}. User 1 already owns {P1, P2}, so those products are excluded. The remaining products are {P3, P4}.
5. Verify the final result
P3 is valid because user 2 bought P3 and user 2 shares P2 with user 1. P4 is valid because user 3 bought P4 and user 3 shares P1 with user 1. Neither P3 nor P4 is already owned by user 1. The code sorts the final list, so the returned result is [P3, P4].
6. Explain the C# implementation and complexity
The C# code builds both dictionaries in one pass over the purchase list. It reads the target user's product set, discovers candidate users through the product-to-users map, gathers their products in another hash set, skips products already owned by the target user, converts the set to a list, and sorts it. Dictionary and HashSet operations are O(1) on average. The hash-processing work is O(n) expected time. Sorting r recommendations adds O(r log r), so total expected time is O(n + r log r). Auxiliary space is O(n).
Key Insight / Why This Solution Works
The key idea is to represent the purchase history in both directions. The user-to-products dictionary tells us what each user owns. The product-to-users dictionary tells us who bought each product. The central invariant is that a candidate user is added only because that user bought at least one product already owned by the target user. Products are then collected only from those candidate users, and any product already in the target user's set is excluded. HashSet keeps candidate users and products unique. This directly enforces both rules in the question: a common purchase must exist, and an already-owned product must never be recommended.
Code
using System;
using System.Collections.Generic;
publicstaticclassProgram
{
publicstaticvoidMain()
{
// Use the exact example shown in the diagram.int userId = 1;
List<(int UserId, string ProductId)> purchases =
new() { (1, "P1"), (1, "P2"), (2, "P2"), (2, "P3"), (3, "P1"), (3, "P4"), (4, "P5") };
// Run the recommendation algorithm for user 1.
List<string> recommendations = Recommend(userId, purchases);
// Print the diagram's expected result: [P3, P4].
Console.WriteLine($"[{string.Join(", ", recommendations)}]");
}
publicstatic List<string> Recommend(int userId, List<(int UserId, string ProductId)> purchases)
{
// Build both directions of the purchase relationship.// userToProducts answers: which products did this user buy?
Dictionary<int, HashSet<string>> userToProducts = new();
// productToUsers answers: which users bought this product?
Dictionary<string, HashSet<int>> productToUsers = new(StringComparer.Ordinal);
foreach ((int currentUserId, string productId) in purchases)
{
// Create this user's product set the first time the user appears.if (!userToProducts.TryGetValue(currentUserId, out HashSet<string>? userProducts))
{
userProducts = new HashSet<string>(StringComparer.Ordinal);
userToProducts[currentUserId] = userProducts;
}
// Store the relationship once even if the input repeats the same purchase.
userProducts.Add(productId);
// Create this product's user set the first time the product appears.if (!productToUsers.TryGetValue(productId, out HashSet<int>? productUsers))
{
productUsers = new HashSet<int>();
productToUsers[productId] = productUsers;
}
// Record that this user bought this product.
productUsers.Add(currentUserId);
}
// Without target-user purchases, no other user can satisfy the common-item rule.if (!userToProducts.TryGetValue(userId, out HashSet<string>? myProducts) || myProducts.Count == 0)
{
returnnew List<string>();
}
// Collect only users who share at least one owned product with the target user.
HashSet<int> candidateUsers = new();
foreach (string productId in myProducts)
{
// Use the reverse index to find all users who bought this owned product.if (productToUsers.TryGetValue(productId, out HashSet<int>? users))
{
foreach (int otherUserId in users)
{
// The target user does not count as another overlapping user.if (otherUserId != userId)
{
candidateUsers.Add(otherUserId);
}
}
}
}
// Collect unique products from users who passed the common-item prerequisite.
HashSet<string> candidateProducts = new(StringComparer.Ordinal);
foreach (int candidateUserId in candidateUsers)
{
foreach (string productId in userToProducts[candidateUserId])
{
// Do not recommend anything the target user already owns.if (!myProducts.Contains(productId))
{
candidateProducts.Add(productId);
}
}
}
// Convert to a list and sort so the output order is deterministic.
List<string> result = new(candidateProducts);
result.Sort(StringComparer.Ordinal);
// For the diagram example, this returns [P3, P4].return result;
}
}
Time & Space Complexity
Let n be the number of unique purchase relationships represented by the input after set deduplication, and let r be the number of recommended products. Building the two dictionaries and sets takes O(n) expected time because Dictionary and HashSet lookup and insertion are O(1) on average. Finding overlapping users and collecting their products also takes O(n) expected time over the stored relationships. The code then sorts the r returned products, which costs O(r log r). Therefore, the complete code runs in O(n + r log r) expected time. The dictionaries and sets use O(n) auxiliary space.
Where it is used
This pattern is useful in simple recommendation systems and relationship-based filtering. It works when recommendations depend on shared behavior, such as customers who bought some of the same products, users who liked some of the same items, or accounts that share common actions. The two-way indexes make it easy to move from a user to products and from a product back to related users.
Why Interviewers Ask This
This problem tests whether you can turn relationship data into useful indexes and choose hash-based collections correctly. The interviewer can see whether you understand the common-item prerequisite, can avoid duplicate users and products with sets, and can prevent already-owned items from being recommended. It also tests whether your C# implementation matches your explanation, whether you handle edge cases such as a user with no purchases, and whether you describe hash-table and sorting complexity accurately.
Common interview mistakes
A common mistake is recommending products from every other user instead of only users who share at least one product with the target user. Another mistake is forgetting to exclude products the target user already owns. Using lists instead of sets can also produce duplicate users or duplicate recommended products. Another error is including the target user in the overlapping-user set even though the requirement refers to other users. Finally, claiming the complete code is O(n) ignores the final sort. The hash-processing part is O(n) expected time, while sorting adds O(r log r).
Interview tip
Explain the solution in two directions: user to products tells you what someone owns, and product to users tells you who overlaps with the target user. Then explain the two filters: first keep only users with a shared purchase, and then remove products the target user already owns. This makes the correctness argument easy to follow.
Interviewer may ask next
How would you handle a very large purchase history that does not fit comfortably in memory?
I would keep the same relationship logic but store the indexes in a persistent data store instead of holding the entire purchase history in process memory. I still need to fetch the target user's products, find users connected through those products, and then fetch products from those users while excluding the target user's set. The correctness rule stays the same. Runtime would depend on the storage engine and its indexes. Application memory can be reduced because the complete history is not held in memory. The tradeoff is extra storage access and more system complexity.
What changes if the output must preserve the order in which candidate products first appear instead of sorting them?
The overlap rule and exclusion rule stay the same, but I would remove the final sort. I would keep a HashSet to detect duplicate product IDs and a List to record each valid product the first time it is encountered while processing candidate users. Correctness is preserved because a product is added only if it comes from an overlapping user, is not already owned, and has not already been returned. The processing remains O(n) expected time, and auxiliary space remains O(n). The tradeoff is that the result order now depends on a clearly defined traversal order.
19. Complete the following rest API codebaseAPI DesignEasyAmazon
i Question Details
Complete a partially implemented REST API codebase and explain how the endpoints, routing, and request/response handling fit together.
Short Interview Answer (30-60 seconds)
At a high level, I would complete the product API so every request follows one clear path. The client sends an HTTPS request through the reverse proxy. ASP.NET Core runs middleware, maps the route, and calls the correct controller. The controller calls ProductService, which uses ProductRepository and EF Core to access the SQL database. JWT authentication checks the caller, while authorization controls access. The result returns as JSON with the shown HTTP status codes. The benefit is clear separation of responsibilities. The trade-off is more layers and configuration to maintain.
Detailed Explanation
This question asks me to finish a small product API and explain how all its parts work together. A client needs to read, create, update, or delete product information. Each request must reach the correct code. The application must process the request safely, access stored data, and send a clear answer back. The main challenge is keeping each responsibility in the right place. I will follow the diagram from the client, through the application, to the database, and then back to the client.
Useful Questions to Ask the Interviewer
Should I complete only the product endpoints already shown in the codebase?
Should I keep the controller, service, repository, and EF Core structure shown here?
Should authentication, logging, rate limiting, and error handling remain as shown?
How to Explain It in an Interview
1. Explain how the request enters the API
I would start with the client request. The client can be a web app, mobile app, or Postman. The diagram shows requests such as GET /api/products/42, POST /api/products, PUT /api/products/42, and DELETE /api/products/42.
The client sends the request over HTTPS. The reverse proxy is NGINX or IIS. It owns TLS termination and also shows compression and rate limiting. The proxy then forwards the request toward the ASP.NET Core application using the HTTPS flow shown in the diagram.
2. Run the ASP.NET Core middleware pipeline
Inside the .NET 8 application, the request goes through the shown middleware order: exception handling, HSTS or HTTPS handling, logging, routing, authentication, authorization, and controller or model validation.
Authentication uses JWT Bearer. It validates the token, extracts claims, and sets the user context. Authorization is a separate step. It decides whether the authenticated caller may continue. Logging uses Serilog for request logs, error logs, and audit logs.
3. Route to the correct API controller action
Routing and endpoint mapping use app.MapControllers();. Attribute routing then selects the matching controller action.
The controller exposes GET /api/products, GET /api/products/{id}, POST /api/products, PUT /api/products/{id}, and DELETE /api/products/{id}. The controller handles the HTTP request and delegates the application work to ProductService.
4. Put business logic in ProductService
ProductService contains the business operations shown in the diagram. These are GetAllAsync(), GetByIdAsync(id), CreateAsync(dto), UpdateAsync(id, dto), and DeleteAsync(id).
This keeps business logic separate from HTTP handling. The controller selects the operation. ProductService performs the application work. It then calls ProductRepository when stored data is needed.
5. Access SQL data through ProductRepository and EF Core
ProductRepository is inside the EF Core data access layer. It provides GetAllAsync(), GetByIdAsync(id), AddAsync(entity), UpdateAsync(entity), and DeleteAsync(entity).
The repository accesses the SQL Products DB. The diagram shows Products and Categories tables. Data returns from the database to ProductRepository, then through ProductService and the controller.
The application also shows configuration from appsettings.json, the built-in dependency injection container, IMemoryCache caching, and HttpClient for outbound calls. These are supporting infrastructure components rather than extra API endpoints.
6. Return JSON and handle failures
The controller returns an IActionResult. ASP.NET Core serializes the result with System.Text.Json. The response passes back through middleware in reverse order. The reverse proxy then sends the HTTPS JSON response to the client.
The diagram shows 200 OK, 201 Created, 400 Bad Request, 404 Not Found, and 500 Internal Server Error. Problem Details follows RFC 7807 and shows fields such as type, title, status, detail, and traceId.
The main benefit is clear separation between HTTP handling, business logic, and data access. The downside is extra layers and dependency wiring. For this API, that structure makes the code easier to test, debug, and change without mixing unrelated responsibilities.
Practical Complexity & Trade-offs
The design uses several layers, but each layer has one clear job. Controllers handle HTTP requests. ProductService handles business logic. ProductRepository and EF Core handle database access. The benefit is that one type of change is less likely to affect unrelated code. JWT authentication and authorization protect access before the product operation continues. Serilog helps record requests, errors, and audit activity. Problem Details gives errors a consistent structure. Rate limiting is shown at the reverse proxy. IMemoryCache can help with repeated data access, but caching adds extra state to manage. The downside is more configuration, dependency injection, and files. We accept that extra structure because responsibilities stay clear and the API becomes easier to test, debug, and maintain.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands the full life of an API request. They want correct routing, HTTP methods, controller responsibilities, business logic separation, and database access. They also check understanding of authentication, authorization, logging, error handling, and response codes. The key skill is engineering judgment. A strong answer shows where each responsibility belongs and explains how both the request and response move through the system correctly.
Interviewer may ask next
What happens if the SQL database fails while ProductRepository is handling a request?
The request should fail through the existing error-handling path rather than using an invented fallback. ProductRepository is the component that uses EF Core to access the SQL database. If that database operation produces an unexpected application error, normal product data cannot return from that operation. The error travels back through the application path. The exception-handling middleware is positioned to handle unhandled application exceptions. The diagram also includes Problem Details, so the API can return the standard error structure shown instead of exposing internal details. The visible server-error result is 500 Internal Server Error. Serilog records error information for investigation. The controller, ProductService, routing, authentication, and other components keep their existing responsibilities. I would not add automatic retries, another database, or a cache fallback because those behaviors are not shown. The main downside is that affected product operations depend on the SQL database being available.
How do authentication and authorization work in this API?
I would keep authentication and authorization as two separate checks in the existing ASP.NET Core pipeline. Authentication happens first. The JWT Bearer component validates the token, extracts its claims, and sets the user context. This establishes who the caller is. Authorization happens after authentication. It decides whether that authenticated caller may continue to the selected controller operation. If either check prevents the request from continuing, the product operation does not reach ProductService or ProductRepository. The diagram does not assign a specific HTTP status code to those security failures, so I would not invent one here. Serilog can record request and audit information around this flow. TLS is a separate concern handled at the reverse proxy, so the JWT is not responsible for encrypting the network connection. The benefit is clear separation between proving identity and deciding permission. The downside is additional token and authorization configuration for protected requests.
20. REST API vs GraphQL ? API version management ? What are CDNs and how many do you know ?API DesignMediumAmazon
i Question Details
Discuss the tradeoffs between REST and GraphQL, versioning strategy, and the CDN-related follow-ups as one API-focused interview prompt.
Short Interview Answer (30-60 seconds)
At a high level, I would separate this into API style, version management, and content delivery. REST is simple, resource based, and works well with normal HTTP caching. GraphQL uses one /graphql endpoint and lets clients request exact fields, but complex queries and caching need more care. The main API flow is client to API Gateway, then ASP.NET Core services and data stores, with the response returning the same way. The gateway handles routing, rate limiting, authentication, authorization, and version routing. For CDN-enabled traffic, edge caching reduces latency and origin load.
Detailed Explanation
This question asks me to make three practical choices. First, I need to choose how applications ask the server for data. Second, I need a safe way to change that interface without suddenly breaking existing users. Third, I need to explain how content can reach users faster from locations close to them. The goal is a design that is simple, flexible, secure, and fast. I would follow the diagram from clients, through the gateway and .NET services, to data stores, while also explaining where CDN caching fits.
Useful Questions to Ask the Interviewer
Is the API mainly public, internal, or both?
Do clients need fixed responses or flexible field selection?
How often do we expect breaking API changes?
Which responses or files are safe to cache at the CDN?
How to Explain It in an Interview
1. Compare REST and GraphQL
I would start with the client needs. REST is resource based and normally has multiple endpoints for related data. It uses standard HTTP behavior, is easy to learn, and is naturally cacheable. The downside is over-fetching because a fixed response may return fields a client does not need.
GraphQL uses one /graphql endpoint. The client selects the exact fields it wants. It also provides a strongly typed schema. This is useful for flexible client-driven screens. The trade-off is that complex queries can become expensive, and caching or batching needs more care.
2. Manage API versions
The diagram shows three versioning choices. URL versioning puts the version in the path, for example https://api.example.com/v1/orders. It is clear and cache or CDN friendly. Header versioning uses X-API-Version: 2 and keeps the URL clean. Query-string versioning uses ?version=2 and can be simple for internal APIs.
I would version breaking changes only. I would maintain backward compatibility when possible. Older versions should be deprecated with Sunset headers, documentation, and a clear migration plan.
3. Follow the main API request flow
Client applications include web, mobile, SPA, and third-party integrations. Their HTTPS request reaches the API Gateway. The gateway owns routing, rate limiting, authentication and authorization using the JWT or OAuth 2.0 mechanisms shown, request or response transformation, version routing, and optional caching.
The gateway forwards the request to the ASP.NET Core services. The response later returns from the .NET API to the gateway and then back to the client as JSON.
4. Process data inside the .NET API
The .NET API Services boundary contains REST controllers, a GraphQL endpoint, and business services. Shared concerns include validation, logging, metrics, and tracing.
The service reads or writes the data-store layer. The diagram shows relational databases such as SQL Server or Aurora, NoSQL databases such as DynamoDB or Cosmos DB, Redis or ElastiCache, and object storage such as S3 or Blob Storage. Data returns to the API before the response travels back toward the client.
5. Explain the CDN flow
A CDN is a distributed network of edge servers located closer to users. For CDN-enabled traffic, DNS resolves the user to a nearby edge. On a cache hit, the edge can serve the content directly. On a miss, it forwards toward the origin path. The response returns through the CDN and may be cached when eligible.
Good cache targets shown are static JavaScript, CSS, images, fonts, public GET responses, downloadable files, and streaming content. Benefits include lower latency, higher availability, origin offload, DDoS protection, and cost optimization.
6. Name CDN providers and close with trade-offs
The diagram names six examples: Amazon CloudFront, Akamai, Cloudflare, Fastly, Microsoft Azure CDN, and Google Cloud CDN. REST favors simplicity and standard caching. GraphQL favors flexible client queries. Explicit API versions protect clients but require migration work. A CDN improves delivery speed, but caching rules must match the content being served.
Why Interviewers Ask This
Interviewers ask this question to test API design judgment, not only definitions. They want to see whether I can choose between REST and GraphQL based on client needs, manage breaking changes safely, and explain why a CDN improves delivery. They also test whether I can trace requests and responses through the gateway, ASP.NET Core services, and data stores. A strong answer shows clear ownership, caching knowledge, security awareness, performance thinking, and realistic trade-offs.
Interviewer may ask next
What would you change if traffic grows heavily across many geographic regions?
I would keep the same API design and make greater use of the CDN for content that is safe to cache. For CDN-enabled traffic, DNS still sends the user toward a nearby CDN edge. A cache hit can serve static assets, downloadable files, streaming content, or eligible public GET responses without reaching the origin path. A cache miss continues toward the API Gateway. The gateway keeps its existing responsibilities for routing, rate limiting, authentication, authorization, version routing, transformation, and optional caching. It then forwards the request to the ASP.NET Core services, which still use REST controllers or the GraphQL endpoint and access the same data-store layer. Logging, metrics, and tracing remain important for understanding the distributed traffic. Correctness is maintained by caching only eligible content. The main downside is extra operational work around cache rules, cache invalidation, capacity, and observing requests across the CDN and API layers.
How would you introduce a breaking API change without suddenly breaking existing clients?
I would introduce a new API version and keep the existing version available during migration. The diagram gives three supported choices. With URL versioning, the current contract can remain at https://api.example.com/v1/orders while a newer breaking contract uses another versioned path. Header versioning can instead use a value such as X-API-Version: 2. Query-string versioning can use ?version=2. I would choose one strategy consistently rather than mixing them for the same API. The API Gateway keeps its version-routing responsibility and directs each request according to the selected version. Existing clients continue using their current contract while they migrate. I would maintain backward compatibility where possible and reserve new versions for breaking changes. The old version should be deprecated with Sunset headers, documentation, and a clear migration plan. The main downside is temporarily maintaining more than one API contract.
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.