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.
Content Accuracy and Verification
To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.
Questions or comments?
Contact us for general questions, or share feedback, technical corrections, and comments with the community.
11. How does Python's buffer protocol enable zero-copy data access?Language SpecificHardGoogle
i Question Details
Explain buffer exporters and consumers, memoryview, contiguous versus strided data, mutability, lifetime, and interoperability with binary libraries.
Short Interview Answer (30-60 seconds)
Python's buffer protocol lets an exporter expose its existing binary memory to a consumer. memoryview is the main built in Python interface for accessing that memory without copying the payload. The consumer must still respect the exporter's format, shape, strides, writable state, and lifetime. Operations that request new bytes or a different layout can still create a copy.
Detailed Explanation
Use the buffer protocol when large binary data should be shared without duplicating its payload. For example, memoryview(bytearray_data) creates a small view object that refers to the bytearray's existing storage. It allocates metadata for the view, but it does not allocate another payload buffer.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
The object that owns and exposes the memory is the exporter. bytes, bytearray, array objects, and many binary libraries can be exporters. The object or function that requests the memory is the consumer. The exported description can include the element format, item size, dimensions, shape, strides, and writable state.
Contiguous data stores the requested elements next to each other. Strided data can contain gaps or represent selected rows, columns, or steps. A consumer must support that layout. Otherwise, conversion to contiguous storage may copy the data.
Mutability is controlled by the exporter. A view of bytes is read only. A view of bytearray is normally writable, so changes affect the original bytearray. The view keeps the exporter alive. Exporters such as bytearray also prevent resizing while an active view exists.
Zero copy therefore describes compatible access to the same payload, not every later operation. tobytes, incompatible layout conversion, and ownership requirements create copies.
Where it is used
The buffer protocol is useful in network input and output, binary file parsing, image and audio processing, memory mapped files, compression, serialization, and numerical computing. Functions such as socket operations can accept buffer compatible objects, and libraries such as NumPy can create views over compatible memory. It is most valuable for large or frequently processed buffers because it reduces payload copying and temporary memory use. It should not be used to force shared mutation when independent ownership is safer, or when the receiving library requires a different format or contiguous layout.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands how Python objects can share binary memory without duplicating the payload. It also checks judgment about memory layout, writable access, object lifetime, library compatibility, hidden copies, and safe production use.
Common interview mistakes
Common mistakes include claiming that every memoryview operation is zero copy, ignoring shape and stride information, writing through a read only view, and assuming every external library accepts noncontiguous data. Another mistake is calling tobytes and still describing the result as shared memory. Developers may also try to resize a bytearray while it has an active exported view, which raises BufferError. Keeping a small view can also keep a much larger exporter alive, so long lived views may retain more memory than expected.
Interview tip
Explain the idea in this order: exporter, consumer, shared payload, and then limitations. State clearly that memoryview avoids copying the payload only when the requested format and layout are compatible.
Interviewer may ask next
What happens if code tries to resize a bytearray while a memoryview of it is active?
Python raises BufferError because the bytearray has an active exported buffer. Resizing could move or replace its storage and make the existing view invalid. The view must be released, deleted, or leave its context, and no other active exports may remain before resizing can succeed. This rule protects memory safety, but it limits structural changes while memory is shared.
When does a buffer consumer need to copy data instead of using the original memory?
A copy is needed when the consumer requires a layout, format, ownership model, or lifetime that the exporter cannot provide. For example, a library that requires contiguous memory may copy a strided view into a new contiguous buffer. Calling tobytes also creates an independent bytes object. The copy costs time and memory proportional to the payload size, but it provides compatibility, independent ownership, or a stable layout.
12. How does Python's audit-hook mechanism observe security-sensitive events?Language SpecificHardGoogle
i Question Details
Explain sys.addaudithook, event emission, native hooks, limitations, security uses, and why hooks are not a complete sandbox.
Short Interview Answer (30-60 seconds)
Python audit hooks observe sensitive operations by receiving named events and a tuple of event arguments. I can add a hook for the current interpreter with sys.addaudithook, while an embedding application can install a native hook with PySys_AddAuditHook. Hooks can log an event or raise an exception to abort many operations, but they are not a complete sandbox because code inside the same process may disable or bypass Python level hooks.
Use audit hooks for observation and carefully tested policy checks, not as the only security boundary. Python and its standard library emit named events by calling sys.audit or the native PySys_Audit API. Each event has a stable name and a defined tuple of arguments.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
A callback added with sys.addaudithook belongs to the current interpreter. Hooks run synchronously in the thread that emits the event, in registration order. Native hooks added with PySys_AddAuditHook run first and apply to all interpreters created by that runtime. For security sensitive monitoring, a native hook should be installed before Python is initialized.
A hook can record the event, raise an exception, or end the process. sys.audit rethrows the first hook exception. This can abort an operation, but the result depends on where that event is emitted. The behavior must be tested for each event.
Calling sys.addaudithook also emits a sys.addaudithook event. An existing hook can prevent the new Python hook from being registered by raising RuntimeError.
Hooks add synchronous callback work to every observed event. Their direct memory cost is usually small, but logging or retaining arguments can increase memory use. They are not a sandbox because malicious in process code may alter state, use unsafe native access, or bypass Python level hooks.
Example
The example registers one Python audit hook for the current interpreter. The hook filters events so it only prints the file open event and one custom application event. Opening the operating system null device causes Python to emit an open event. Calling sys.audit emits the custom event with one string argument. The hook observes these events synchronously. It does not create a sandbox or claim that every sensitive operation can be blocked.
Code
import os
import sys
defaudit_hook(event, args):
# Process only the two events used by this example.if event == "open":
# The open event provides details about the requested file operation.print("Observed open event:", args)
elif event == "application.record_access":
# This custom event carries the values passed to sys.audit.print("Observed custom event:", args)
# Add a Python audit hook to the current interpreter.
sys.addaudithook(audit_hook)
# Opening the null device causes Python to emit an open audit event.withopen(os.devnull, "r", encoding="utf8") as file:
file.read(0)
# An application can emit its own audit event and arguments.
sys.audit("application.record_access", "example record")
Where it is used
Audit hooks are used for security logging, compliance records, incident investigation, plugin monitoring, embedded Python runtimes, and policy checks around actions such as opening files, importing modules, creating sockets, and starting processes. A production hook should perform very little synchronous work, avoid exposing secrets from event arguments, avoid recursive audited operations where possible, and move expensive processing to a trusted logging system. Untrusted code should still run in a separate process with operating system permissions and resource limits.
Why Interviewers Ask This
Interviewers ask this to test whether a candidate understands how the Python runtime exposes sensitive operations to monitoring code. They also want to see whether the candidate knows the difference between interpreter hooks and native hooks, understands exception behavior, and avoids treating runtime observation as a secure sandbox.
Common interview mistakes
A common mistake is treating sys.addaudithook as a secure sandbox. Another is assuming every sensitive action emits an event before any work occurs. Developers may also assume that raising an exception always reverses an operation, perform slow network or file work inside the hook, retain large argument objects, or log passwords and tokens contained in event arguments. It is also incorrect to assume that calling sys.addaudithook guarantees registration, because an existing hook can reject the new hook.
Interview tip
Start with the main limit: audit hooks provide visibility, not complete isolation. Then explain event names, argument tuples, synchronous execution, native hook order, exception behavior, registration blocking, and the need for operating system security controls.
Interviewer may ask next
Can an existing audit hook prevent a new Python audit hook from being added?
Yes. Calling sys.addaudithook emits the sys.addaudithook event with no arguments. If an existing hook raises RuntimeError or a subclass of RuntimeError, Python does not add the new hook and suppresses that exception. This matters because code cannot assume registration succeeded unless it controls the existing hooks and the interpreter environment.
When should PySys_AddAuditHook be preferred over sys.addaudithook?
PySys_AddAuditHook should be preferred when the host controls the runtime and the monitoring is security sensitive. A native hook can be installed before Python initialization, runs before interpreter hooks, and receives events from all interpreters created by that runtime. The tradeoff is native implementation complexity and the risk of errors in native code. It still does not replace process isolation, operating system permissions, or resource controls.
13. How does Python's cyclic import behavior produce partially initialized modules?Language SpecificHardGoogle
i Question Details
Explain import execution order, module caching during initialization, failure patterns, and design techniques that remove the cycle.
Short Interview Answer (30-60 seconds)
A cyclic import can expose a module before Python has finished initializing it. Python creates the module object and places it in sys.modules before executing the module code. If that code imports another module that imports the first module again, Python returns the same cached but incomplete object. A name defined later may not exist yet. The preferred fix is to remove the cycle by changing the dependency direction or moving shared definitions into a separate module.
Detailed Explanation
The practical solution is to remove the dependency cycle rather than depend on a fragile import order. Suppose module a imports module b. Python creates the module object for a and places it in sys.modules before executing the code in a. This prevents endless recursive loading. ([docs.python.org](https://docs.python.org/3/library/importlib.html))
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
While a is still running, it imports b. Python creates and starts executing b. If b imports a, Python finds a in sys.modules and returns the same object. However, a may not have executed the lines that define the requested class, function, or constant. The object is therefore partially initialized.
A statement such as from a import Service can raise ImportError if Service is not ready. Access through a.Service during the cycle can raise AttributeError. Access may work when it is delayed until both modules finish importing.
The reliable production fix is to move shared definitions into a third module, place common interfaces in a lower level module, or pass dependencies into functions and classes. A local import inside a function can delay the lookup, but it may only hide the design problem. Python normally reuses the cached module object, so the cycle does not create a second initialized copy. The main risks are failed startup, confusing errors, and code that breaks when import order changes.
Where it is used
This behavior appears in larger Python applications where models, services, routes, configuration modules, and utility modules depend on one another. It is often discovered during application startup because top level import code runs before requests, workers, or scheduled jobs begin. Production teams prevent it by keeping dependency direction clear, moving shared types and constants into a separate module, and passing required objects into functions or constructors.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands that importing a Python module executes its top level code. They also want to check knowledge of sys.modules, import order, failure patterns, and production design choices that prevent tightly connected modules.
Common interview mistakes
A common mistake is assuming Python fully executes one module before an imported module can refer to it. Another mistake is rearranging import statements until the error disappears, because a later change can break that fragile order again. Developers may also believe Python creates a second copy of the first module, but the cyclic import normally receives the same object from sys.modules. Moving every import inside functions can hide the cycle instead of removing it. Another mistake is using from imports between closely connected modules because the requested name must already exist when that statement runs.
Interview tip
Explain the order clearly. Python creates the module object, caches it in sys.modules, starts executing its code, enters the second module, and then returns the unfinished first module. Give one failure pattern and finish with the preferred design fix.
Interviewer may ask next
Why can import a behave differently from from a import Service during a cyclic import?
The difference is when the requested name must exist. import a can receive the partially initialized module object from sys.modules without immediately requesting Service. from a import Service must find Service at that moment. If module a has not executed the Service definition yet, the statement raises ImportError. This matters because importing the module object may appear to work while immediate access to one of its unfinished names still fails.
Is moving an import inside a function a good production solution for a cyclic import?
It can be a valid temporary solution, but it is usually not the best structural solution. A local import delays the import lookup until the function runs, when both modules may already be initialized. Later calls normally reuse the entry in sys.modules, although each call still performs an import lookup. The tradeoff is that the dependency becomes less visible and the design cycle remains. Moving shared code into a separate module or reversing the dependency is usually clearer and safer.
14. How do Python subinterpreters differ from processes and threads?Language SpecificHardGoogle
My practical rule is to use threads when tasks need shared memory, processes when strong isolation matters, and subinterpreters when I want isolated Python runtimes inside one process. Each subinterpreter has separate modules, global variables, and interpreter state. In current CPython, it can also have its own global interpreter lock, which allows parallel Python execution across CPU cores. Mutable objects are not shared directly, so data must be copied, serialized, or sent through supported communication tools. I would verify every native extension before using subinterpreters in production.
Detailed Explanation
The practical choice depends on isolation and communication. Threads are simplest when tasks need shared objects. Processes provide the strongest memory and failure isolation. Subinterpreters sit between them because they provide separate Python runtimes inside one operating system process.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
Each subinterpreter has its own imported modules, global variables, builtins module, and interpreter state. Normal mutable objects cannot be used directly across interpreter boundaries. Data must be copied, serialized, or transferred through a supported communication mechanism.
Current CPython can give each isolated interpreter its own global interpreter lock. This allows interpreters to execute Python code on different CPU cores. Normal threads on a standard CPython build still share one interpreter lock. A free threaded CPython build changes that thread limitation, so the exact runtime build matters.
Subinterpreters usually avoid some operating system process overhead, but they still require interpreter creation, separate imports, object copying, and communication. Their speed and memory advantage is therefore workload dependent.
Native extension modules are an important limitation. An extension may reject subinterpreters or behave incorrectly if it keeps unsafe process wide state. Production use requires dependency testing, realistic benchmarks, controlled task boundaries, and a process based fallback when stronger isolation is needed.
Where it is used
Subinterpreters fit CPU focused tasks whose inputs and results are easy to serialize, isolated plugin workers that use trusted code, and services that want several independent Python runtimes inside one process. They are most useful when workers share little mutable state and every imported extension supports multiple interpreters. Threads are usually better for network, disk, or database waiting when shared state is useful. Processes are usually better when a worker needs a separate memory space, stronger crash containment, or compatibility with libraries that do not support subinterpreters.
Why Interviewers Ask This
Interviewers ask this question to test whether a candidate understands CPython isolation, object ownership, the global interpreter lock, native extension compatibility, communication costs, and how to choose a safe production concurrency model.
Common interview mistakes
A common mistake is treating a subinterpreter as an ordinary thread. A subinterpreter may run on a thread, but it owns separate Python runtime state. Another mistake is assuming that mutable Python objects can be shared directly across interpreters. They normally must be copied, serialized, or transferred through supported tools. Candidates may also claim that subinterpreters provide process level failure isolation. They do not, because all interpreters remain inside one process and a serious native failure can terminate that process. Another mistake is assuming that every native extension is compatible. Extensions with unsafe process wide state may reject multiple interpreters or behave incorrectly. Finally, lower operating system overhead does not guarantee better performance or lower memory use because imports, serialization, copied data, and workload size affect the result.
Interview tip
Compare threads, subinterpreters, and processes using five points: isolation, object sharing, parallel execution, startup and memory cost, and failure boundaries. Mention the runtime build and extension module support before recommending subinterpreters.
Interviewer may ask next
Can two subinterpreters directly share the same mutable Python list?
No. Isolated subinterpreters cannot use the same mutable Python list as ordinary threads can. Each interpreter owns its Python objects and runtime state. The list must be copied, serialized, or transferred through a supported communication mechanism. This preserves interpreter isolation, but it adds copying and communication cost.
When should a process be chosen instead of a subinterpreter?
Choose a process when a separate memory space, stronger crash containment, or wider native library compatibility matters more than lower process overhead. A subinterpreter remains inside the same process, so a serious native crash can affect every interpreter in that process. Processes usually cost more to start and may use more memory, but they provide a clearer isolation boundary.
15. How do Python's code objects, frames, and trace functions relate during execution?Language SpecificHardGoogle
i Question Details
Explain compiled code objects, execution frames, local and global mappings, call stacks, tracing and profiling hooks, and runtime overhead.
Short Interview Answer (30-60 seconds)
The main idea is that a code object contains reusable compiled instructions, while a frame contains the live state of one execution of those instructions. Each active function call has its own frame, even when several calls use the same code object. Frames expose local, global, and builtin mappings and link active calls into a call stack. Trace and profile functions receive events from those frames. They help debuggers, coverage tools, and profilers, but they add callback cost and can keep local objects alive if frame references are stored.
Detailed Explanation
The practical rule is to separate reusable code from live execution state. Python compiles a function body into a code object. The code object stores instructions, constants, names, variable information, flags, and source position information. Calling the function starts an execution frame that refers to that code object. The frame tracks the current execution position and exposes local, global, and builtin mappings through f_locals, f_globals, and f_builtins. Its f_back reference points to the calling frame when one exists, so active frames form the call stack.
Useful Questions to Ask the Interviewer
Should I focus on Python language behavior, or also explain the runtime and standard library?
Which Python version and execution environment should I assume?
Would you like a small code example together with production tradeoffs and edge cases?
Many calls can use the same code object, but every call has separate frame state. This is why recursion keeps different local values for each call. A suspended generator or coroutine can retain its frame and referenced objects until it finishes, closes, or is collected.
sys.settrace installs a trace function for the current thread. It can receive call, line, return, exception, and optional opcode events. sys.setprofile receives fewer events focused on Python calls, returns, and calls into C code. These hooks support debugging, coverage, and profiling. They can be expensive because callbacks run during execution. Stored frames and tracebacks can also retain local objects, so production tools should limit collection and release references promptly.
Where it is used
Python uses code objects and execution frames whenever functions, methods, generators, or coroutines run. Debuggers inspect frames to display the call stack, the current source location, and variable values. Coverage tools use trace events to record which lines execute. Profilers observe calls and returns to find expensive functions. Error reporting systems inspect traceback frames to collect diagnostic context. In production, tracing should be enabled only for a clear purpose, limited to selected threads or code when possible, and measured under realistic load. Tools should avoid collecting secrets from local mappings and should remove saved frame and traceback references after processing.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate can separate compiled program information from live execution state. It tests knowledge of function calls, variable lookup, call stacks, exception inspection, debugging hooks, profiling hooks, runtime cost, and the memory risks of keeping frames or tracebacks alive.
Common interview mistakes
A common mistake is saying that a code object stores the current values of local variables. Those values belong to a particular execution frame. Another mistake is assuming Python recompiles a function for every call. Normal calls reuse the function code object and create separate execution state. Developers may also confuse the local mapping with a normal dictionary that always acts as the interpreter's internal storage. Function scopes may use optimized local storage that Python exposes through mapping behavior, so code should use documented frame and locals rules rather than old assumptions. Another mistake is treating tracing as free. Line and opcode events can create many callbacks and greatly slow a program. Finally, storing frames or tracebacks for too long can retain local objects and create reference cycles, so diagnostic code should process them and release them promptly.
Interview tip
Explain the relationship in three steps. First, a code object holds reusable compiled information. Second, each execution has a separate frame, and active frames form the call stack. Third, trace and profile hooks observe frame events but add runtime and memory cost. Mention recursion, suspended generators, and retained traceback references to show practical understanding.
Interviewer may ask next
What happens to a frame when a generator is suspended?
The frame remains available while the generator is suspended because Python needs its execution position and local state to continue later. The same code object is still used, but the retained frame preserves that generator instance's state. This matters because objects referenced by the frame can stay alive until the generator finishes, is closed, or is collected.
When should a production tool use sys.setprofile instead of sys.settrace?
A production tool should prefer sys.setprofile when function call information is enough. It observes Python calls and returns and also reports relevant calls into C code, while sys.settrace can observe detailed line, exception, and optional opcode events. The tradeoff is detail against overhead. Profiling usually creates fewer callbacks, while tracing provides deeper visibility but can slow execution much more. Both hooks apply per thread, so additional threads need their own setup or the related threading support.
16. Find the maximum depth of a binary tree.CodingEasyGoogle
i Question Details
Return the number of nodes on the longest root-to-leaf path and handle an empty tree.
Short Interview Answer (30-60 seconds)
I would use recursive depth-first search. For each node, I first find the maximum depth of its left subtree and then its right subtree. The base case is an empty node, which returns 0. A real node returns 1 plus the larger child depth. This works because every recursive call returns the correct depth for its own subtree. The time complexity is O(n), and the auxiliary space is O(h) for the recursion stack.
The problem asks for the number of nodes on the longest path from the root to any leaf. I use recursive depth-first search because the depth of a node depends on the depths of its two children. I compute both child depths first. Then I keep the larger one and add 1 for the current node.
Useful Questions to Ask the Interviewer
What input sizes, value ranges, and edge cases should the solution handle?
What output should be returned for empty, invalid, or duplicate input?
Should I prioritize execution time or memory use, and may I use the standard library?
How to Explain It in an Interview
1. Understand the input and required output
The input is the root of a binary tree. The output is one integer.
The integer is the number of nodes on the longest root-to-leaf path. Depth counts nodes, not edges.
For the example tree, the root is 3. Its left child is 9. Its right child is 20. Node 20 has children 15 and 7.
The correct result is 3. Two deepest paths are 3 -> 20 -> 15 and 3 -> 20 -> 7.
If the root is None, the tree is empty, so the answer is 0.
2. Choose recursive depth-first search
I use recursive depth-first search in postorder style. Postorder style means I compute the child results before I compute the parent result.
For each node, the recursive call returns the maximum depth of the subtree rooted at that node.
The base case is a None child. Its depth is 0.
For a real node, the depth is:
1 + max(left_depth, right_depth)
The 1 counts the current node.
3. Initialize the recursion
I call maxDepth on node 3.
The traversal starts at the root. For each real node, the function recursively processes the left child first and the right child second.
The central invariant is that every recursive call returns the correct maximum depth of the subtree rooted at its current node.
4. Walk through the example
The first completed node-level calculation is for node 9. Its left child is None, so that call returns 0. Its right child is also None, so that call returns 0. Node 9 returns 1 + max(0, 0), which is 1.
The recursion then enters the right subtree of node 3. Node 15 is a leaf. Both child calls return 0, so node 15 returns 1.
Node 7 is also a leaf. Both child calls return 0, so node 7 returns 1.
Node 20 receives left_depth = 1 from node 15 and right_depth = 1 from node 7. It returns 1 + max(1, 1), which is 2.
Finally, node 3 receives left_depth = 1 from node 9 and right_depth = 2 from node 20. It returns 1 + max(1, 2), which is 3.
5. Explain why the result is correct
The base case is correct because an empty subtree contains zero nodes.
Assume the recursive calls return the correct depths for the left and right subtrees. Any longest path from the current node must continue through either the left child or the right child.
Taking max(left_depth, right_depth) selects the deeper subtree. Adding 1 counts the current node. Therefore, the current call returns the correct subtree depth.
This reasoning continues up to the root, so the final result is correct.
6. Explain the Python implementation
The function receives root, which is either a TreeNode or None.
If root is None, it returns 0 immediately.
Otherwise, it recursively calculates left_depth from root.left and right_depth from root.right.
It then returns 1 + max(left_depth, right_depth).
The TreeNode class stores the node value and references to the left and right children.
7. Explain complexity and edge cases
The time complexity is O(n), where n is the number of nodes. Each node is visited once.
The auxiliary space is O(h), where h is the tree height. This space is used by the recursion stack.
For a balanced tree, h is O(log n). For a completely skewed tree, h can be O(n).
Important edge cases are an empty tree, a single-node tree, a skewed tree, and a balanced tree.
Key Insight / Why This Solution Works
The key insight is that the depth of a node can be built from the depths of its children. A recursive call returns the maximum depth of the subtree rooted at its current node. This is the central invariant. A None child returns 0. A real node returns 1 plus the larger of its left and right subtree depths. Postorder-style DFS fits because both child results must be known before the parent result can be calculated.
Code
from typing importOptionalclassTreeNode:
def__init__(
self,
val: int = 0,
left: Optional["TreeNode"] = None,
right: Optional["TreeNode"] = None,
) -> None:
# Store the value of the current node.self.val = val
# Store the reference to the left child.self.left = left
# Store the reference to the right child.self.right = right
classSolution:
defmaxDepth(self, root: Optional[TreeNode]) -> int:
# Step 1: stop at an empty subtree.# An empty subtree contains zero nodes.if root isNone:
return0# Step 2: recursively find the left subtree depth.
left_depth = self.maxDepth(root.left)
# Step 3: recursively find the right subtree depth.
right_depth = self.maxDepth(root.right)
# Step 4: count the current node and keep the deeper child path.return1 + max(left_depth, right_depth)
if __name__ == "__main__":
# Build the exact example tree:## 3# / \# 9 20# / \# 15 7
root = TreeNode(3)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
# Run the solution. The expected output is 3.
result = Solution().maxDepth(root)
print(result)
Time & Space Complexity
The time complexity is O(n), where n is the number of nodes in the tree. The algorithm visits each node once and does constant work at that node. The auxiliary space is O(h), where h is the tree height. This extra memory is used by the recursion stack. A balanced tree uses O(log n) stack space. A completely skewed tree can use O(n) stack space.
Where it is used
This recursive tree pattern is useful when a parent result depends on results from its children. It appears in folder-size calculations, syntax-tree analysis, organization hierarchies, file-system traversal, tree-height checks, and other problems where child results are combined at a parent node.
Why Interviewers Ask This
The interviewer is checking whether you can recognize a recursive tree pattern, define a correct base case, combine child results, and explain a clear invariant. They also want to see whether you preserve the given tree structure without assuming binary search tree rules. The question tests clean Python recursion, correct handling of an empty tree, and accurate complexity analysis that includes the recursion stack.
Common interview mistakes
A common mistake is forgetting the None base case, which causes the recursion to continue incorrectly. Another mistake is returning max(left_depth, right_depth) without adding 1 for the current node. Some candidates count edges instead of nodes, which makes the result one too small. Others claim O(1) auxiliary space and forget the recursion stack. It is also incorrect to assume the tree is a binary search tree because the problem only says binary tree.
Interview tip
State the invariant before writing code: each call returns the maximum depth of the subtree rooted at its node. Then write the None base case and the recurrence 1 + max(left_depth, right_depth).
Interviewer may ask next
How would you return one deepest root-to-leaf path instead of only its depth?
Each recursive call can return both the subtree depth and one deepest path. The node compares the left and right depths, selects the deeper path, and places its own value at the front. This preserves correctness because the chosen child path has the greater subtree depth. The time complexity remains O(n). The recursion stack uses O(h), and the returned path uses up to O(h) additional space. The tradeoff is storing and combining path information instead of returning only an integer.
How could you solve the same problem without recursion?
Use breadth-first search with a queue. Start with the root and process the tree one level at a time. Increase a depth counter after each complete level. When the queue becomes empty, the counter is the maximum depth. This is correct because BFS visits nodes level by level. The time complexity is O(n). The auxiliary space is O(w), where w is the maximum number of nodes in one level. The tradeoff is using an explicit queue instead of the recursion stack.
17. Find all nodes at distance K from a target node in a binary tree.CodingMediumGoogle
i Question Details
Return every node exactly K edges from the target, accounting for child and parent directions.
Short Interview Answer (30-60 seconds)
I would first traverse the tree and store each node’s parent. Then I would run breadth-first search from the target node. For every node, I check its left child, right child, and parent. A visited set prevents the search from moving back and forth between the same nodes. BFS processes the tree one distance level at a time, so when the distance reaches K, the queue contains the answer. The solution takes O(n) time and O(n) auxiliary space.
The problem asks us to return the values of all nodes exactly K edges from a given target node. A normal binary-tree node only points to its children, but a valid path may also move through its parent. I first build parent links for every node. I then run level-order BFS from the target, treating children and the parent as neighboring nodes.
Useful Questions to Ask the Interviewer
What input sizes, value ranges, and edge cases should the solution handle?
What output should be returned for empty, invalid, or duplicate input?
Should I prioritize execution time or memory use, and may I use the standard library?
How to Explain It in an Interview
1. Understand the input and output
The inputs are the tree root, a reference to the target node, and an integer k.
The output is a list of node values. Every returned node must be exactly k edges from the target.
In the example, the target node has value 5 and k is 2. One valid result is [7, 4, 1]. The output order is not required to be unique.
2. Build a node-to-parent map
Tree nodes already point to their left and right children. They do not normally point to their parents.
I traverse the tree once with DFS. For each node, I store a mapping from that node reference to its parent reference.
For example, node 5 maps to parent 3. Node 2 maps to parent 5. Nodes 7 and 4 map to parent 2.
After this step, each node can have up to three neighbors: its left child, its right child, and its parent.
3. Initialize BFS from the target
The initial queue is [5]. The initial visited set is {5}. The current distance is 0.
The visited set stores node references, not node values. This matters because different nodes may contain the same value.
The main invariant is: at the start of each BFS level, every node in the queue is exactly the current distance from the target.
4. Walk through the example
At distance 0, the queue is [5].
We process node 5. Its unvisited neighbors are node 6, node 2, and parent node 3. We add them to the queue and visited set.
The next queue is [6, 2, 3]. These nodes are one edge from the target.
At distance 1, we process nodes 6, 2, and 3.
Node 6 adds nothing because node 5 is already visited.
Node 2 adds nodes 7 and 4. Its parent, node 5, is already visited.
Node 3 adds node 1. Node 5 is already visited.
The next queue is [7, 4, 1]. These nodes are two edges from the target.
Now distance equals k, so these nodes are not processed further. We return their values: [7, 4, 1].
The paths are 5 to 2 to 7, 5 to 2 to 4, and 5 to 3 to 1. Each path has exactly two edges.
5. Explain why the algorithm is correct
The parent map lets the search move through every valid tree connection. The visited set prevents a node from being discovered more than once.
BFS processes nodes in increasing distance order. The first queue contains nodes at distance
The next queue contains nodes at distance
The following queue contains nodes at distance 2.
Therefore, when the current distance equals k, the queue contains exactly the nodes that are k edges from the target.
6. Explain the Python implementation
The build_parents function performs DFS and stores each node’s parent.
The deque stores the current BFS frontier. The visited set stores nodes that have already been discovered.
At the start of every BFS level, the code checks whether distance equals k. If it does, it returns the values currently in the queue.
Otherwise, the code processes exactly len(queue) nodes. This keeps the current BFS level separate from the next level.
For each node, it examines the left child, right child, and parent. It marks an unvisited neighbor before adding it to the queue.
7. Explain complexity and edge cases
Building the parent map takes O(n) time. BFS also visits each node at most once, so the total time is O(n).
The parent map, visited set, and queue can each use O(n) memory. The recursive DFS call stack uses O(h), where h is the tree height. The total auxiliary space is O(n).
If k is 0, the result is [target.val]. The target may be the root. A skewed tree still works. If no node exists at distance k, the function returns an empty list.
Key Insight / Why This Solution Works
The key idea is to make upward movement possible. I first build a map from each node reference to its parent reference. This makes the tree behave like an undirected graph. I then run BFS from the target. The queue processes nodes one distance level at a time. The central invariant is that every node in the queue at the start of a level is exactly the current distance from the target. Therefore, when the distance reaches k, the queue contains the complete answer.
Code
from collections import deque
from typing importDict, List, Optional, Set, TupleclassTreeNode:
def__init__(
self,
val: int = 0,
left: Optional["TreeNode"] = None,
right: Optional["TreeNode"] = None,
) -> None:
self.val = val
self.left = left
self.right = right
classSolution:
defdistanceK(
self,
root: TreeNode,
target: TreeNode,
k: int,
) -> List[int]:
# Step 1: Store the parent of every node.
parent: Dict[TreeNode, Optional[TreeNode]] = {}
defbuild_parents(
node: Optional[TreeNode],
par: Optional[TreeNode],
) -> None:
# Stop when the DFS moves past a leaf.if node isNone:
return# Record the current node's parent.
parent[node] = par
# Continue through both child subtrees.
build_parents(node.left, node)
build_parents(node.right, node)
build_parents(root, None)
# Step 2: Start level-order BFS from the target.
queue = deque([target])
visited: Set[TreeNode] = {target}
distance = 0while queue:
# Every node currently in the queue is at this distance.if distance == k:
return [node.val for node in queue]
# Process only the current BFS level.for _ inrange(len(queue)):
node = queue.popleft()
# The valid directions are left child, right child, and parent.for neighbor in (
node.left,
node.right,
parent[node],
):
# Mark the node before enqueueing it to prevent duplicates.if neighbor isnotNoneand neighbor notin visited:
visited.add(neighbor)
queue.append(neighbor)
# The next frontier is one edge farther from the target.
distance += 1# No nodes exist at distance k.return []
defbuild_example_tree() -> Tuple[TreeNode, TreeNode]:
# Build the exact example tree from the diagram.
root = TreeNode(3)
root.left = TreeNode(5)
root.right = TreeNode(1)
root.left.left = TreeNode(6)
root.left.right = TreeNode(2)
root.right.left = TreeNode(0)
root.right.right = TreeNode(8)
root.left.right.left = TreeNode(7)
root.left.right.right = TreeNode(4)
# The target is the node whose value is 5.
target = root.left
return root, target
if __name__ == "__main__":
root, target = build_example_tree()
result = Solution().distanceK(root, target, 2)
print(result) # One valid output: [7, 4, 1]
Time & Space Complexity
Let n be the number of nodes and h be the tree height. Building the parent map visits every node once, which takes O(n) time. BFS also visits each node at most once, so it takes O(n) time. The parent map, visited set, and BFS queue can use O(n) extra memory. The recursive DFS call stack uses O(h) memory. Because h can be as large as n, the total auxiliary space is O(n).
Where it is used
This pattern is useful when a tree search must move both downward and upward. Examples include finding nodes within a fixed distance, simulating infection or signal spread through a tree, and finding nearby relatives in hierarchical data. The parent map changes the tree into an undirected graph, and BFS groups nodes by distance.
Why Interviewers Ask This
This question tests whether the candidate can turn a one-directional tree into a structure that supports movement in both directions. It checks recognition of BFS as the right pattern for exact edge distance. It also tests correct use of parent links, node references, a visited set, and level boundaries. The interviewer also wants to see clean Python, accurate complexity analysis, and careful handling of cases such as k equal to zero or a target at the root.
Common interview mistakes
A common mistake is searching only through left and right children. This misses paths that move through a parent. Another mistake is forgetting the visited set, which can make the search move repeatedly between a child and its parent. Candidates may also increase the distance after each node instead of after one full BFS level. Marking nodes visited only after removing them from the queue can add duplicates. Using node values instead of node references in the visited set is also unsafe when duplicate values exist. Finally, the auxiliary space is O(n), not O(1).
Interview tip
Before writing code, say this invariant clearly: at the start of each BFS level, every node in the queue is exactly the current distance from the target. This makes the stopping condition and level updates easy to explain.
Interviewer may ask next
What changes if every node already stores a parent pointer?
The parent-map DFS is no longer needed. BFS can start directly from the target and examine the left child, right child, and stored parent pointer. The same visited set and level-order invariant preserve correctness. The time becomes O(m), where m is the number of nodes examined before or at distance k. The auxiliary space is O(m) for the queue and visited set. The tradeoff is that every tree node must permanently store an extra parent reference.
How would you answer many distance-K queries on the same tree?
Build the parent map once and reuse it for every query. Each query can then run BFS from its target node. Building the map takes O(n) time and O(n) space once. A query still takes up to O(n) time and O(n) temporary space in the worst case. This is useful when the tree stays unchanged, because repeated queries avoid rebuilding the parent links.
18. Return the vertical order traversal of a binary tree.CodingMediumGoogle
i Question Details
Group nodes by horizontal position, define ordering for ties, and analyze complexity.
Short Interview Answer (30-60 seconds)
I assign each node a row and column. The root starts at row 0, column 0. I use BFS with a queue, so I visit nodes level by level. For each node, I store its pair of row and value in a map keyed by column. After traversal, I process columns from left to right and sort each column by row, then by value for ties. The time complexity is O(n log n), and the auxiliary space complexity is O(n).
The problem asks us to group tree nodes by their horizontal position. Each vertical column becomes one list in the result. Inside a column, nodes are ordered from top to bottom. When two nodes have the same row and column, the smaller value comes first. The diagram uses BFS, a queue, and a map from column to a list of row and value pairs.
Useful Questions to Ask the Interviewer
What input sizes, value ranges, and edge cases should the solution handle?
What output should be returned for empty, invalid, or duplicate input?
Should I prioritize execution time or memory use, and may I use the standard library?
How to Explain It in an Interview
1. Define the row and column positions
The root starts at row 0 and column 0.
For a node at row r and column c:
Its left child is at row r + 1 and column c - 1.
Its right child is at row r + 1 and column c + 1.
The row tells us the vertical level. The column tells us the horizontal position.
2. Use BFS and a column map
I use a queue for BFS. BFS means breadth-first search. It processes the tree level by level.
Each queue item stores three values:
the node reference
its row
its column
I also use a map called col_table. Its key is a column number. Its value is a list of pairs in the form row and node value.
The main invariant is that every visited node is stored in the list for its correct column with its correct row.
3. Initialize the state
The queue starts with the root:
[(3, 0, 0)]
The column map starts empty.
The minimum and maximum columns both start at 0.
These bounds help us later read the columns from left to right.
4. Walk through the exact example
The tree is:
Root 3 at row 0, column 0.
Left child 9 at row 1, column -1.
Right child 20 at row 1, column 1.
Node 20 has left child 15 at row 2, column 0.
Node 20 has right child 7 at row 2, column 2.
Step 1:
The queue contains node 3 at row 0, column 0.
We remove 3 from the queue. We add the pair (0, 3) to column 0.
Then we add its children:
9 with row 1 and column -1
20 with row 1 and column 1
The queue becomes [(9, 1, -1), (20, 1, 1)].
Step 2:
We remove 9. We add (1, 9) to column -1.
Node 9 has no children.
The queue becomes [(20, 1, 1)].
Step 3:
We remove 20. We add (1, 20) to column 1.
Then we add its children:
15 with row 2 and column 0
7 with row 2 and column 2
The queue becomes [(15, 2, 0), (7, 2, 2)].
Step 4:
We remove 15. We add (2, 15) to column 0.
Node 15 has no children.
The queue becomes [(7, 2, 2)].
Step 5:
We remove 7. We add (2, 7) to column 2.
Node 7 has no children.
The queue becomes empty, so traversal stops.
The final map is:
column -1: [(1, 9)]
column 0: [(0, 3), (2, 15)]
column 1: [(1, 20)]
column 2: [(2, 7)]
We process columns from -1 to 2. Inside each column, we sort by row first and value second.
The final result is [[9], [3, 15], [20], [7]].
5. Explain why the result is correct
Every node receives a row and column based on its position from the root.
Every node is stored under its exact column.
Columns are read from the smallest column to the largest column, so the result goes from left to right.
Inside each column, sorting by row places higher nodes before lower nodes. Sorting by value after row resolves ties when two nodes share the same row and column.
Therefore, the output follows the required vertical traversal order.
6. Explain the Python implementation
The code uses deque so removing an item from the front is efficient.
The col_table dictionary groups nodes by column.
The BFS loop removes one node at a time, records it, and adds its children with updated row and column values.
After BFS, the code loops from min_col to max_col. It sorts each column list using row first and value second. It then keeps only the node values.
7. Explain complexity and edge cases
BFS visits each of the n nodes once, so traversal takes O(n) time.
Sorting all stored row and value pairs takes O(n log n) time in the worst case.
The total time complexity is O(n log n).
The queue and column map together store O(n) items, so the auxiliary space complexity is O(n).
Important edge cases are an empty tree, a single node, a skewed tree, and multiple nodes that share the same row and column.
Key Insight / Why This Solution Works
The key idea is to give every node a coordinate. The row measures depth. The column measures horizontal position. BFS visits the tree level by level and stores each node as a pair of row and value inside a map keyed by column. The invariant is that every visited node is placed in the list for its exact column with its exact row. Reading columns from left to right and sorting each column by row, then value, produces the required order.
Code
from collections import defaultdict, deque
from typing importList, OptionalclassTreeNode:
def__init__(
self,
val: int = 0,
left: Optional["TreeNode"] = None,
right: Optional["TreeNode"] = None,
) -> None:
self.val = val
self.left = left
self.right = right
classSolution:
defverticalTraversal(self, root: Optional[TreeNode]) -> List[List[int]]:
# An empty tree has no vertical columns.if root isNone:
return []
# Map each column to a list of (row, node value) pairs.
col_table = defaultdict(list)
# Each queue item is: (node, row, column).
queue = deque([(root, 0, 0)])
# Track the leftmost and rightmost columns.
min_col = 0
max_col = 0# Visit every node with BFS.while queue:
node, row, col = queue.popleft()
# Store this node in its vertical column.
col_table[col].append((row, node.val))
# Update the visible column range.
min_col = min(min_col, col)
max_col = max(max_col, col)
# The left child moves one row down and one column left.if node.left isnotNone:
queue.append((node.left, row + 1, col - 1))
# The right child moves one row down and one column right.if node.right isnotNone:
queue.append((node.right, row + 1, col + 1))
result: List[List[int]] = []
# Read columns from left to right.for col inrange(min_col, max_col + 1):
nodes = col_table[col]
# Sort first by row, then by node value for ties.
nodes.sort(key=lambda item: (item[0], item[1]))
# Keep only the node values in the final output.
result.append([value for _, value in nodes])
return result
if __name__ == "__main__":
# Build the exact example tree from the diagram.## 3# / \# 9 20# / \# 15 7
root = TreeNode(3)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
answer = Solution().verticalTraversal(root)
print(answer)
# Expected output: [[9], [3, 15], [20], [7]]
Time & Space Complexity
Let n be the number of nodes. BFS visits every node once, which takes O(n) time. The algorithm then sorts the stored row and value pairs. In the worst case, this sorting takes O(n log n) time. Therefore, the total time complexity is O(n log n). The queue and the column map can together hold O(n) items, so the auxiliary space complexity is O(n).
Where it is used
This pattern is useful when tree nodes must be grouped by position. Similar ideas appear in tree visualization, hierarchical layout, coordinate-based tree reports, and problems that ask for top view, bottom view, or vertical grouping.
Why Interviewers Ask This
This problem checks whether you can model a tree with coordinates, choose a suitable traversal, and group data with a map. It also tests whether you notice the tie-breaking rule. The interviewer wants to see correct queue handling, correct row and column updates, and accurate complexity analysis. They may also check whether your explanation, walkthrough, and code all use the same ordering rules.
Common interview mistakes
A common mistake is grouping by row instead of column. Another mistake is changing the left and right column updates. The left child must use column minus one, and the right child must use column plus one. Some candidates sort only by row and forget the value tie-breaker. Others return row and value pairs instead of returning only values. Using list.pop(0) as a queue is also slower than using deque.popleft(). Finally, claiming O(n) total time is incorrect because sorting can take O(n log n).
Interview tip
State the coordinate rule first: left means column minus one, right means column plus one, and every child moves to the next row. Then explain that the final sort uses the exact key (row, value).
Interviewer may ask next
Can we reduce the sorting work by relying only on BFS order?
Not for the full rule shown here. BFS gives increasing row order, but two nodes may share the same row and column. Those tied nodes must be ordered by value. We still need a way to sort or otherwise order tied values. The shown solution keeps the simple map and sort design. Its time complexity remains O(n log n), and its auxiliary space remains O(n).
How does the solution handle two nodes with the same row and column?
Both nodes are stored in the same column list with the same row value. The list is sorted by the pair (row, value). Because their rows are equal, the smaller node value comes first. This preserves the required ordering. The total time complexity remains O(n log n), and the auxiliary space remains O(n).
19. Compute the maximum path sum in a binary tree.CodingHardGoogle
i Question Details
Return the largest sum of values along any nonempty path, where the path may start and end at any nodes.
Short Interview Answer (30-60 seconds)
I use postorder depth-first search. Each recursive call returns the best one-branch path that starts at the current node and can continue to its parent. I ignore negative child gains by comparing them with zero. At every node, I also test a complete path that joins the left branch, the node, and the right branch. A global variable stores the best result. This takes O(n) time and O(h) auxiliary space for the recursion stack.
The problem asks for the largest sum along any nonempty path in a binary tree. The path may start and end at any nodes. It does not need to pass through the root. Postorder depth-first search works well because each node needs the results from both children before it can calculate its own path values.
Useful Questions to Ask the Interviewer
What input sizes, value ranges, and edge cases should the solution handle?
What output should be returned for empty, invalid, or duplicate input?
Should I prioritize execution time or memory use, and may I use the standard library?
How to Explain It in an Interview
1. Understand the input and required output
The input is the root of a binary tree. Each node contains an integer value.
The output is one integer. It is the maximum sum of any nonempty path.
A path follows parent-child links. The final best path may use both children of one node. However, a path returned to a parent can continue through only one child branch.
2. Choose postorder DFS
I use postorder depth-first search. Postorder means I process the left child and right child before the current node.
Each call to dfs(node) returns the maximum gain of a path that starts at node and can extend upward to its parent.
The global variable best stores the largest complete path sum found so far.
3. Initialize the state
I initialize best to negative infinity. This is important because every node may have a negative value. Starting with zero would give the wrong answer for an all-negative tree.
A missing child returns zero. For each real child, I compare its returned gain with zero. A negative gain is ignored because adding it would reduce the path sum.
4. Walk through the verified example
The tree is:
-10 / \ 9 20 / \ 15 7
The postorder traversal is 9, 15, 7, 20, -10.
At node 9, left_gain is 0 and right_gain is 0. The through sum is 9. best becomes 9. The call returns 9.
At node 15, both gains are 0. The through sum is 15. best becomes 15. The call returns 15.
At node 7, both gains are 0. The through sum is 7. best stays 15. The call returns 7.
At node 20, left_gain is 15 and right_gain is 7. The through sum is 20 + 15 + 7 = 42. best becomes 42. The upward gain is 20 + max(15, 7) = 35.
At node -10, left_gain is 9 and right_gain is 35. The through sum is -10 + 9 + 35 = 34. This is smaller than 42, so best stays 42. The upward gain returned by the root call is -10 + max(9, 35) = 25.
The final answer is 42. The maximum path is 15 → 20 → 7, and 15 + 20 + 7 = 42.
5. Explain why the result is correct
Every valid path has one highest node. At that node, the path may use the best downward branch from the left child and the best downward branch from the right child.
The algorithm calculates this through sum at every node. Therefore, every possible highest point of a valid path is considered.
The recursive return uses only one child branch. This is correct because a path that continues to the parent cannot split into two child directions.
6. Explain the Python implementation
The nested dfs function first handles the base case. A missing node returns 0.
It recursively calculates the left and right gains. It clips each gain to 0 when the gain is negative.
It calculates through_sum by joining both usable child gains through the current node. It then updates best.
Finally, it returns the current node value plus the larger child gain. After the root call finishes, maxPathSum returns best.
7. Explain complexity and edge cases
Each 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. A balanced tree uses O(log n) stack space. A skewed tree can use O(n).
Important edge cases are a single-node tree, all-negative values, a skewed tree, and a best path that does not pass through the root.
Key Insight / Why This Solution Works
The key idea is to calculate two different values at each node. The first value is through_sum. It may use the best left branch, the current node, and the best right branch. This value can update the final answer. The second value is upward_gain. It uses the current node and only one child branch because a parent cannot extend a path that already splits in two directions. Postorder DFS gives each node the child results first. Negative child gains are replaced with zero because excluding a harmful branch gives a larger path sum. The invariant is that dfs(node) returns the best extendable path starting at node, while best stores the largest complete path found so far.
Code
from typing importOptionalclassTreeNode:
def__init__(
self,
val: int = 0,
left: Optional["TreeNode"] = None,
right: Optional["TreeNode"] = None,
) -> None:
# Store the value of this node.self.val = val
# Store references to the left and right children.self.left = left
self.right = right
classSolution:
defmaxPathSum(self, root: Optional[TreeNode]) -> int:
# Start below every possible node value.# This makes all-negative trees work correctly.
best = float("-inf")
defdfs(node: Optional[TreeNode]) -> int:
nonlocal best
# A missing child adds no gain to a path.if node isNone:
return0# Process both children before the current node.# Ignore a negative gain because it would reduce the sum.
left_gain = max(dfs(node.left), 0)
right_gain = max(dfs(node.right), 0)
# This complete path uses the current node as its highest point.# It may include one branch from each child.
through_sum = node.val + left_gain + right_gain
# Store the best complete path found anywhere so far.
best = max(best, through_sum)
# Only one child branch can continue upward to the parent.return node.val + max(left_gain, right_gain)
# Run postorder DFS from the root.
dfs(root)
# Return the largest path sum found in the tree.return best
if __name__ == "__main__":
# Build the verified example tree:# -10# / \# 9 20# / \# 15 7
root = TreeNode(-10)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
answer = Solution().maxPathSum(root)
print(answer) # 42
Time & Space Complexity
The time complexity is O(n), where n is the number of nodes. The algorithm visits each node once and does constant work at that node. The auxiliary space is O(h), where h is the height of the tree. This extra memory is used by the recursion stack. For a balanced tree, h is O(log n). For a fully skewed tree, h can be O(n).
Where it is used
This postorder tree pattern is useful when a node must combine results from its children. Similar ideas appear in tree scoring, longest-path calculations in trees, organization hierarchies, expression trees, and problems where each node returns one extendable result while also updating a global answer.
Why Interviewers Ask This
This problem tests whether you can reason about recursive tree state. The interviewer wants to see if you recognize postorder traversal, define what each recursive call returns, and separate a complete path from a path that can still be extended. It also checks handling of negative values, use of a global result, correctness reasoning, recursion-stack complexity, and the ability to explain why two child branches may update the answer but only one branch may be returned.
Common interview mistakes
A common mistake is returning node.val + left_gain + right_gain to the parent. That returned path would already contain two branches, so the parent could not extend it as one valid path. Another mistake is initializing best to 0. That fails when every node value is negative. Candidates may also forget to ignore negative child gains, update best using only one branch instead of both branches, assume the best path must pass through the root, or forget the base case for a missing child.
Interview tip
Clearly separate the two values calculated at each node: through_sum may use both children and updates the global answer, while upward_gain may use only one child and is returned to the parent.
Interviewer may ask next
How would you return the actual nodes in the maximum-sum path instead of only the sum?
Each DFS call would return the best upward gain and enough path information to rebuild that one-branch path. When through_sum creates a new global best, I would save the left branch, the current node, and the right branch as the best complete path. Correctness is preserved because every node is still considered as the possible highest point. The tree traversal remains O(n), but careless copying of long path lists can make the implementation O(n²) on a skewed tree. Extra space is O(h) for recursion plus space for stored path information.
What changes if the tree is extremely skewed and Python recursion depth is unsafe?
I would replace recursive postorder DFS with an explicit stack. Each stack entry can store a node and a visited flag. The first visit schedules the children. The second visit processes the node after both child gains are available. A dictionary can store the upward gain for each node. Correctness is preserved because the processing order is still postorder. Time remains O(n). Extra space becomes O(n) in the worst case for the stack and stored gains. The tradeoff is more code, but it avoids recursion-depth errors.
20. Determine whether one string is a rotation of another.CodingEasyGoogle
i Question Details
Given two strings, return whether one can be obtained by rotating the other without changing character order.
Short Interview Answer (30-60 seconds)
The main idea is to check whether the second string appears inside the first string joined with itself. I first compare the lengths because a rotation cannot add or remove characters. If the lengths match, I build doubled = original + original and test whether candidate is a contiguous substring of it. For example, cdeab appears in abcdeabcde, so I return True. The standard interview analysis is O(n) time and O(n) auxiliary space.
The problem asks whether one string can be formed by rotating another string. A rotation keeps every character in the same circular order. It only changes the starting position. The useful pattern is to join the original string with itself. Every valid rotation then appears as one contiguous substring inside that doubled string.
Useful Questions to Ask the Interviewer
What input sizes, value ranges, and edge cases should the solution handle?
What output should be returned for empty, invalid, or duplicate input?
Should I prioritize execution time or memory use, and may I use the standard library?
How to Explain It in an Interview
1. Understand the input and output
The function receives two strings named original and candidate.
It returns True when candidate is a rotation of original. Otherwise, it returns False.
A rotation must preserve all characters and their circular order. It cannot add or remove characters.
2. Check the lengths
The first condition compares the string lengths.
If len(original) != len(candidate), the function returns False immediately.
This check is required because two strings with different lengths cannot be rotations of each other.
In the diagram example, original = "abcde" and candidate = "cdeab". Both strings have length 5, so the algorithm continues.
3. Build the doubled string
The algorithm creates:
doubled = original + original
For the example:
"abcde" + "abcde" = "abcdeabcde"
This places every possible cut point beside the characters that follow it in circular order.
4. Walk through the example
The initial state is:
original = "abcde"
candidate = "cdeab"
len(original) = 5
len(candidate) = 5
The length check passes, so there is no early return.
The code then builds doubled = "abcdeabcde".
Next, it checks whether "cdeab" appears as a contiguous substring inside the doubled string.
The match starts at index 2:
abcdeabcde
cdeab
The condition is true, so the function returns True and stops.
5. Explain why the result is correct
Suppose original = x + y.
Moving the prefix x to the end produces the rotation y + x.
The doubled string is:
original + original = x + y + x + y
The sequence y + x appears contiguously inside this doubled string. Therefore, when the lengths are equal, candidate is a valid rotation exactly when it appears inside original + original.
6. Explain the Python implementation
The first if statement handles the required length check.
The next statement creates the doubled string once.
The expression candidate in doubled performs a substring test and directly returns either True or False.
The code follows the same three executed steps shown in the diagram: verify lengths, build the doubled string, and search for the candidate.
7. Explain complexity and edge cases
Let n be the length of each string.
The standard interview analysis is O(n) time. The algorithm builds a string of length 2n and performs one substring search.
The auxiliary space is O(n) because the doubled string grows with the input.
Relevant edge cases include strings with different lengths, identical strings, two empty strings, repeated characters, and strings that contain similar characters but are not rotations.
Key Insight / Why This Solution Works
The key insight is that every rotation of a string appears inside that string concatenated with itself. If original = x + y, then moving the prefix x to the end gives candidate = y + x. The doubled string is x + y + x + y, which contains y + x as a contiguous substring. The central invariant is: after the equal-length check passes, candidate is a valid rotation exactly when it appears inside original + original. This avoids constructing and comparing every possible rotation separately.
Code
defis_rotation(original: str, candidate: str) -> bool:
# Step 1: A valid rotation must have the same length.iflen(original) != len(candidate):
returnFalse# Step 2: Joining the original string with itself exposes every rotation.
doubled = original + original
# Step 3: Check whether the candidate is one contiguous substring.return candidate in doubled
# Example from the approved diagram
original = "abcde"
candidate = "cdeab"
result = is_rotation(original, candidate)
print(result) # True
Time & Space Complexity
Let n be the length of each input string. The standard interview analysis is O(n) time because the algorithm creates a doubled string of length 2n and performs one substring test. If the lengths differ, it returns before creating that string. The auxiliary space is O(n) because doubled stores two copies of original. The output itself uses only one Boolean value.
Where it is used
This pattern is useful when comparing circular sequences. Examples include checking rotated text patterns, matching cyclic schedules, comparing circular-buffer contents, and deciding whether two repeated sequences differ only by their starting position.
Why Interviewers Ask This
The interviewer is testing whether you can recognize a useful string pattern instead of generating every possible rotation. They also want to see whether you understand why the length check is required, whether you distinguish a substring from a subsequence, and whether your correctness argument matches your code. The problem also checks concise Python, accurate complexity analysis, and careful handling of cases such as empty strings and repeated characters.
Common interview mistakes
A common mistake is skipping the length check. A shorter string may appear inside the doubled string even though it is not a rotation. Another mistake is treating a subsequence as valid. The candidate must be one contiguous substring. Sorting both strings is also incorrect because sorting loses the required circular order. Some candidates generate every rotation, which adds unnecessary work and temporary strings. Another mistake is claiming O(1) auxiliary space even though the doubled string grows with the input.
Interview tip
Explain the proof before writing the code: if original = x + y, then its rotation is y + x, and y + x must appear inside x + y + x + y.
Interviewer may ask next
What result should the function return for two empty strings?
It should return True. Both strings have length 0, so the length check passes. The doubled string is also empty, and Python considers the empty string to be a substring of the empty string. This behavior is consistent with the definition because rotating an empty string still produces an empty string. The time and auxiliary space are O(1) for this specific input.
How would the solution change if letter case should be ignored?
Normalize both strings before applying the same algorithm. In Python, use casefold() on original and candidate, then compare the normalized lengths and search for the normalized candidate inside the doubled normalized original. The correctness argument stays the same because both inputs use the same normalization rule. The standard interview analysis remains O(n) time and O(n) auxiliary space. The tradeoff is that the comparison no longer preserves the original capitalization.
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.
Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.