Google Python Developer Interview Questions & Answers

google icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

11. How does Python's buffer protocol enable zero-copy data access?Language SpecificHardGoogle

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
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. 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.

How does Python's buffer protocol enable zero-copy data access? diagram
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

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.

Detailed Explanation

See the Code while reading this explanation.

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
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. 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.

How does Python's audit-hook mechanism observe security-sensitive events? diagram
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


def audit_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.
with open(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

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?utm_source=chatgpt.com))

Useful Questions to Ask the Interviewer
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. 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.

How does Python's cyclic import behavior produce partially initialized modules? diagram
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

Question Details

Explain interpreter isolation, object sharing constraints, extension-module considerations, communication, startup cost, and evolving parallelism support.

Short Interview Answer (30-60 seconds)

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
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. 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.

How do Python subinterpreters differ from processes and threads? diagram
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

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
  1. Should I focus on Python language behavior, or also explain the runtime and standard library?
  2. Which Python version and execution environment should I assume?
  3. 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.

How do Python's code objects, frames, and trace functions relate during execution? diagram
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

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.

Detailed Explanation

See the Code while reading this explanation.

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
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Find the maximum depth of a binary tree. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the root of a binary tree. The output is 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 import Optional


class TreeNode:
    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


class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        # Step 1: stop at an empty subtree.
        # An empty subtree contains zero nodes.
        if root is None:
            return 0

        # 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.
        return 1 + 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 the minimum window in a string containing all characters of another string.CodingHardGoogle

Question Details

Return the shortest substring satisfying all required character frequencies, or an empty result when none exists.

Short Interview Answer (30-60 seconds)

I would use a sliding window with two frequency maps. The need map stores the required count for each character in t. I expand the right pointer until the window contains every required frequency. Then I move the left pointer while the window stays valid and record the smallest window. This works because each valid window is minimized for its current right boundary. The expected time is O(|s| + |t|), and the auxiliary space is O(k), where k is the number of distinct required characters in t.

Detailed Explanation

See the Code while reading this explanation.

The problem asks for the shortest contiguous substring of s that contains every character from t with the required frequency. A sliding window fits because the answer must be one continuous part of s. The right pointer grows the window until it becomes valid. The left pointer then removes unnecessary characters. Frequency maps let us update and check the window without recounting every character.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Find the minimum window in a string containing all characters of another string. diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives two strings, s and t.

We must return a substring of s. A substring is a continuous section of the string. It is not a subsequence.

The substring must contain every character from t with the correct frequency. If no valid substring exists, we return an empty string.

For the example, s is "ADOBECODEBANC" and t is "ABC". The returned substring is "BANC". It starts at index 9, ends at index 12, and has length 4.

2. Choose the sliding window and frequency maps

The window is the part of s from index left through index right, including both boundaries.

The need map stores each required character and its required count. For the example, need is {A: 1, B: 1, C: 1}.

The window map stores the current counts of required characters inside the window. Characters that are not in need do not have to be stored.

The variable required is the number of distinct characters in need. It is 3 for this example.

The variable formed counts how many distinct required characters currently meet their full required frequency.

The central invariant is: when formed equals required, s[left:right + 1] contains every required character with the needed frequency.

3. Initialize the state

We begin with left equal to 0 and formed equal to 0.

The window map is empty because no characters have entered the window.

The best length starts as infinity because no valid window has been found. The best start index begins at 0 and is used after a valid window is recorded.

If s or t is empty, or if t is longer than s, a valid answer is impossible, so the function returns an empty string.

4. Walk through the exact example

At right index 0, the character is A. A is required, so its window count becomes 1. This reaches the required count for A, so formed becomes 1 out of 3.

At right index 3, the character is B. Its count reaches 1, so formed becomes 2 out of 3.

At right index 5, the character is C. Its count reaches 1, so formed becomes 3 out of 3. The first valid window is "ADOBEC", from index 0 to index 5. Its length is 6, so it becomes the best answer.

The algorithm now shrinks the window. It removes A at index

  1. The A count becomes 0, which is below the required count of
  2. Formed becomes 2, the window becomes invalid, and left moves to index 1.

The right pointer continues through the string. At right index 10, another A enters. The relevant counts are A: 1, B: 2, and C: 1, so formed becomes 3 again. The window "DOBECODEBA" is valid.

The algorithm repeatedly moves left while the window remains valid. It passes the irrelevant characters at indices 1 and 2. It can also remove the B at index 3 because another B remains in the window. It then passes E at index 4. The best length remains 6. When C at index 5 is removed, the C count becomes 0. Formed becomes 2, the window becomes invalid, and left becomes 6.

At right index 12, another C enters. The window "ODEBANC" becomes valid again.

The algorithm shrinks it to "DEBANC", then "EBANC", and then "BANC". The best answer changes from length 6 to length 5 and then to length 4.

After recording "BANC", the algorithm removes B at index 9. The B count falls below its required count, so formed becomes 2. The window is invalid, and shrinking stops. The right pointer has reached the end of s, so the final answer is "BANC".

5. Explain why the result is correct

Whenever formed equals required, the current window is valid.

For one fixed right boundary, the algorithm moves left while the window stays valid. This finds the smallest valid window ending at that right boundary.

The right pointer visits every possible ending position. Therefore, the algorithm considers the smallest valid window for every possible right boundary.

The smallest window recorded across those positions is the global minimum.

6. Explain the Python implementation

Counter(t) creates the need map.

defaultdict(int) creates the window map and gives unseen characters a starting count of zero.

The for loop moves right across s. Only characters found in need update the window map. When a character count exactly reaches its required count, formed increases.

When formed equals required, the while loop runs. It first records the current window if it is shorter than the best one. It then removes the leftmost character and moves left forward.

If removing a required character makes its count lower than the needed count, formed decreases. The window is no longer valid, so shrinking stops.

After the scan, the function returns an empty string if no valid window was found. Otherwise, it returns the slice starting at best_start with length best_len.

7. Explain complexity and edge cases

Building the need map takes O(|t|) time.

The right pointer moves through s once. The left pointer also moves forward at most |s| times. Python dictionary lookup and update are O(1) on average. Therefore, the expected total time is O(|s| + |t|).

The two maps store at most k distinct required characters, so the auxiliary space is O(k), where k is the number of distinct characters in t.

Important edge cases are empty s or t, t being longer than s, repeated required characters such as t = "AABC", no valid window, and a case where the whole string is the answer.

Key Insight / Why This Solution Works

The key insight is to maintain one adjustable window instead of generating every possible substring. The right pointer expands the window until all required frequencies are present. The left pointer then removes unnecessary characters while the window remains valid. The need map stores each required character and its target frequency. The window map stores the current frequency inside the window. The invariant is that formed == required exactly when every distinct required character has reached its needed count. Shrinking each valid window gives the smallest valid window for its current right boundary.

Code
from collections import Counter, defaultdict


def min_window(s: str, t: str) -> str:
    # A valid window is impossible for these inputs.
    if not s or not t or len(t) > len(s):
        return ""

    # Store the required frequency of each character in t.
    need = Counter(t)

    # Store frequencies of required characters in the current window.
    window = defaultdict(int)

    # Number of distinct required characters.
    required = len(need)

    # Number of distinct characters currently meeting their required count.
    formed = 0

    # Left boundary of the sliding window.
    left = 0

    # Best valid window found so far.
    best_len = float("inf")
    best_start = 0

    # Expand the right boundary across s.
    for right, ch in enumerate(s):
        # Only required characters affect window validity.
        if ch in need:
            window[ch] += 1

            # This character is satisfied when its count reaches the target.
            if window[ch] == need[ch]:
                formed += 1

        # Shrink repeatedly while the current window is valid.
        while formed == required:
            # Record the current valid window before removing its left character.
            if right - left + 1 < best_len:
                best_len = right - left + 1
                best_start = left

            # Remove the character at the left boundary.
            left_char = s[left]
            if left_char in need:
                window[left_char] -= 1

                # The window becomes invalid if this count falls below its target.
                if window[left_char] < need[left_char]:
                    formed -= 1

            # Move the left boundary forward.
            left += 1

    # Return the smallest valid substring, or an empty string if none exists.
    return "" if best_len == float("inf") else s[best_start : best_start + best_len]


if __name__ == "__main__":
    source = "ADOBECODEBANC"
    target = "ABC"
    print(min_window(source, target))  # BANC
Time & Space Complexity

Let |s| be the length of s and |t| be the length of t. Creating the need map takes O(|t|) time. The right pointer moves forward |s| times, and the left pointer moves forward at most |s| times. Python dictionary lookup and update are O(1) on average. Therefore, the expected total time is O(|s| + |t|). The need and window maps store at most k distinct required characters, so the auxiliary space is O(k).

Where it is used

This sliding window pattern is useful when software must find the smallest or largest continuous range that satisfies a condition. Examples include finding a short text segment containing required keywords, finding a log interval containing required event types, and finding a subarray that satisfies frequency or count limits.

Why Interviewers Ask This

This problem tests whether a candidate can recognize the sliding window pattern and maintain frequency counts correctly. It also checks repeated-character handling, pointer order, and the difference between a substring and a subsequence. The interviewer wants to see a clear invariant, correct Python dictionary use, accurate complexity analysis, and a walkthrough that matches the code.

Common interview mistakes

A common mistake is tracking only whether a character appears instead of tracking its required frequency. Another mistake is treating the answer as a subsequence rather than a contiguous substring. Some candidates shrink the window before recording the current valid answer. Others decrease formed whenever a required character is removed, even when enough copies remain. It is also incorrect to claim guaranteed O(|s| + |t|) time because Python dictionary operations are O(1) only on average.

Interview tip

State the invariant before coding: formed == required means the current window contains every required frequency. Then say that the best answer must be recorded before the left character is removed.

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

The algorithm does not need a new structure. The need map stores A with a required count of 2, B with 1, and C with 1. A is counted as satisfied only when the current window contains at least two copies. If one A is removed and the count falls below 2, formed decreases. The invariant and pointer logic stay the same. The expected time remains O(|s| + |t|), and the auxiliary space remains O(k).

Can we return the start and end indices instead of the substring?

Yes. The algorithm already stores best_start and best_len. When processing finishes, return [best_start, best_start + best_len - 1] instead of slicing s. For the example, the result is [9, 12]. The sliding-window logic and correctness proof do not change. The expected time remains O(|s| + |t|), and the auxiliary space remains O(k). Avoiding the final string slice also avoids allocating memory for the returned substring.

18. Compute the maximum path sum in a binary tree.CodingHardGoogle

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.

Detailed Explanation

See the Code while reading this explanation.

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
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Compute the maximum path sum in a binary tree. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is the root of a binary tree. 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 import Optional


class TreeNode:
    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


class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        # Start below every possible node value.
        # This makes all-negative trees work correctly.
        best = float("-inf")

        def dfs(node: Optional[TreeNode]) -> int:
            nonlocal best

            # A missing child adds no gain to a path.
            if node is None:
                return 0

            # 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.

19. Determine whether one string is a rotation of another.CodingEasyGoogle

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.

Detailed Explanation

See the Code while reading this explanation.

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
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Determine whether one string is a rotation of another. diagram
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
def is_rotation(original: str, candidate: str) -> bool:
    # Step 1: A valid rotation must have the same length.
    if len(original) != len(candidate):
        return False

    # 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.

20. Find the intersection of two integer arrays.CodingEasyGoogle

Question Details

Return the distinct values present in both arrays and explain the complexity of the chosen approach.

Short Interview Answer (30-60 seconds)

I would build a set from the first array, then scan the second array from left to right. For each value, I check whether it exists in the first set. If it does, I add it to a result set called common. The result set removes duplicates automatically. After the scan, I return list(common). The expected time is O(n + m), because Python set operations are O(1) on average. The auxiliary space is O(n + k), commonly reported as O(n).

Detailed Explanation

See the Code while reading this explanation.

The problem asks us to return the distinct integer values that appear in both arrays. We return values, not indices. The output order does not matter. A set-based solution fits well because a set supports fast membership checks and stores each value only once.

Useful Questions to Ask the Interviewer
  1. What input sizes, value ranges, and edge cases should the solution handle?
  2. What output should be returned for empty, invalid, or duplicate input?
  3. Should I prioritize execution time or memory use, and may I use the standard library?
Find the intersection of two integer arrays. diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives two integer arrays named nums1 and nums2.

For the diagram example:

nums1 = [1, 2, 2, 1, 3]

nums2 = [2, 2, 3, 4]

The values that appear in both arrays are 2 and 3. Each value must appear once in the result. One valid returned list is [2, 3]. The order may vary.

2. Choose the algorithm and data structures

First, convert nums1 into a set named values_in_nums1. For the example, it becomes {1, 2, 3}.

This set stores values only. It does not store indices or frequencies. It lets us check whether a value from nums2 appears in nums1 in O(1) time on average.

Create another empty set named common. This set stores the distinct values found in both arrays. Because common is a set, adding the same value more than once keeps only one copy.

The central invariant is this: after processing the first i elements of nums2, common contains exactly the distinct processed values that also appear in nums1.

3. Initialize the state

The initial state is:

values_in_nums1 = {1, 2, 3}

common = {}

Traversal begins at nums2[0]. The values in nums2 are processed from left to right in this order: 2, 2, 3, 4.

4. Walk through the example

Step 1 uses index 0. The current value is 2. common is {} before the check. Since 2 is in values_in_nums1, add 2 to common. common becomes {2}.

Step 2 uses index 1. The current value is 2 again. common is {2} before the check. Since 2 is in values_in_nums1, add it again. A set keeps only one copy, so common remains {2}.

Step 3 uses index 2. The current value is 3. common is {2} before the check. Since 3 is in values_in_nums1, add 3. common becomes {2, 3}.

Step 4 uses index

  1. The current value is
  2. common is {2, 3} before the check. Since 4 is not in values_in_nums1, make no change. common remains {2, 3}.

All four values from nums2 have now been processed. The final result set is {2, 3}. One valid returned list is [2, 3].

5. Explain why the result is correct

Before each iteration, common contains exactly the distinct matching values found in the part of nums2 already processed.

If the current value exists in values_in_nums1, it belongs to the intersection, so adding it is correct. If it does not exist in values_in_nums1, it cannot belong to the intersection, so skipping it is correct.

The set common removes repeated matches automatically. Therefore, when the scan finishes, common contains exactly the distinct values present in both arrays.

6. Explain the Python implementation

The code creates values_in_nums1 with set(nums1). It creates common as an empty set.

The for loop reads each value from nums2 in order. The if statement checks whether the current value appears in values_in_nums1. When the condition is true, common.add(value) stores the match.

After the loop, list(common) converts the result set into a list and returns it. The order of that list is not guaranteed, which is allowed by the problem.

7. Explain complexity and edge cases

Let n be the length of nums1 and m be the length of nums2.

Building set(nums1) takes O(n) time. Scanning nums2 takes O(m) time. Python set lookup and insertion take O(1) time on average. Therefore, the expected total time is O(n + m).

The first set can store up to n distinct values. The result set can store k distinct intersection values. The auxiliary space is O(n + k), commonly reported as O(n) because k cannot be greater than the number of distinct values in nums1.

The method handles duplicate values, negative values, zero, empty arrays, and arrays with no common values.

Key Insight / Why This Solution Works

The key idea is to use one set for fast membership checks and another set for distinct results. values_in_nums1 stores the distinct values from nums1. Then the algorithm scans nums2 from left to right. If a value is in values_in_nums1, it is added to common. The invariant is that after processing the first i elements of nums2, common contains exactly the distinct processed values that also appear in nums1. This avoids the slower nested-loop method that compares every pair of values.

Code
from typing import List


class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        # Build a set containing the distinct values from nums1.
        # Membership checks in a Python set take O(1) time on average.
        values_in_nums1 = set(nums1)

        # Store each distinct value that appears in both arrays.
        common: set[int] = set()

        # Scan nums2 from left to right.
        for value in nums2:
            # Add the value when it also appears in nums1.
            if value in values_in_nums1:
                # A set automatically keeps only one copy.
                common.add(value)

        # Convert the distinct intersection values to a list.
        return list(common)


if __name__ == "__main__":
    nums1 = [1, 2, 2, 1, 3]
    nums2 = [2, 2, 3, 4]

    solution = Solution()
    result = solution.intersection(nums1, nums2)

    # Output order may vary. Sorting is used only for stable display.
    print(sorted(result))  # [2, 3]
Time & Space Complexity

Let n be the number of elements in nums1 and m be the number of elements in nums2. Building values_in_nums1 takes O(n) time. Scanning nums2 takes O(m) time. Python set lookup and insertion take O(1) time on average, so the expected total time is O(n + m). values_in_nums1 uses O(n) extra space. common uses O(k) space, where k is the number of distinct matching values. The total auxiliary space is O(n + k), commonly reported as O(n).

Where it is used

This pattern is useful when software must find shared unique items between two collections. Examples include common user permissions, shared product IDs, matching tags, overlapping feature flags, and duplicate-free matches between datasets. It is especially useful when fast membership checks matter more than preserving the original order.

Why Interviewers Ask This

The interviewer is testing whether you recognize that the problem needs both fast membership checks and duplicate removal. They want to see whether you choose suitable sets, keep values separate from indices, handle repeated values correctly, and maintain a clear invariant. They also evaluate whether your Python code matches your explanation and whether you describe expected hash-set performance and auxiliary space accurately.

Common interview mistakes

A common mistake is storing matches in a list without checking for duplicates. Another mistake is returning indices even though the problem asks for values. Some candidates use nested loops, which can take O(n × m) time. Another mistake is claiming guaranteed O(n + m) time instead of expected O(n + m) time for Python sets. It is also wrong to claim that [2, 3] is the only valid output order.

Interview tip

State the invariant before writing the loop: common contains exactly the distinct processed values from nums2 that also appear in nums1. Then use the example to show how each iteration keeps that invariant true.

Interviewer may ask next
How would you preserve the order in which matching values first appear in nums2?

Keep values_in_nums1 for membership checks. Also keep a seen_result set and a result list. Scan nums2 from left to right. When a value is in values_in_nums1 and not in seen_result, add it to seen_result and append it to result. This preserves first appearance order and prevents duplicates. The expected time is O(n + m). The auxiliary space is O(n + k).

Can you reduce auxiliary space if both arrays are already sorted?

Yes. Use two pointers, one for each sorted array. If the values are equal, add the value once and move both pointers past duplicates. If one value is smaller, move that array's pointer forward. Sorted order makes each pointer movement safe. The time is O(n + m). The auxiliary space is O(1) excluding the output. The tradeoff is that this requires sorted input.

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.