Apple Data Engineer Interview Questions & Answers

apple icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: September 8, 2026)

11. Operate an hourly updated ten-billion-row lakehouse table.Cloud Data PlatformsHardApple

Question Details

Choose table format, partition and sort strategy, merge mechanism, snapshot retention, compaction, metadata cleanup, and compute isolation. Explain how concurrent readers obtain stable snapshots while late corrections and hourly writes commit atomically, and how the platform recovers from an interrupted rewrite.

Short Interview Answer (30-60 seconds)

I would use Apache Iceberg format v2 on object storage, with separate Spark writer, reader, and maintenance compute. Hourly MERGE INTO jobs publish atomic snapshots, so readers keep stable views during corrections. The trade-off is added metadata, retention, and compaction work for stronger operational correctness.

Detailed Explanation

At ten billion rows, the hard part is not simply loading another hourly batch. Producer systems keep sending new records and late corrections while analysts, scientists, applications, and downstream data products may be reading the table at the same time. A one-off pipeline does not solve atomic publication, concurrent reads, file layout, snapshot retention, compaction, metadata growth, or failed rewrites. I would operate the table as a reusable Apache Iceberg lakehouse service with shared object storage and catalog metadata, while separating writer, reader, and maintenance compute so each workload can run without starving the others.

Useful Questions to Ask the Interviewer
  1. What are the dominant read patterns: recent-time scans, entity lookups, broad historical analytics, or a mixture?
  2. How late can corrections arrive, and can the same entity be corrected repeatedly?
  3. How much snapshot history must remain available for rollback or time travel?
  4. Which Spark or SQL engines must read the Iceberg table?
  5. Do writer, reader, and maintenance workloads need separate quotas or scheduling priorities?
Operate an hourly updated ten-billion-row lakehouse table. diagram
How to Explain It in an Interview
1. Make Apache Iceberg format v2 the transaction boundary

The logical table is an Apache Iceberg format-v2 table. Production records are stored in immutable data files on object storage. The catalog side stores table metadata, snapshots, and manifests rather than the production rows themselves.

That separation gives the platform a clean publication boundary. Writers can create new or replacement data files first, but readers do not treat those files as part of the table until a successful metadata commit publishes a new snapshot. A failed operation before that point does not expose a partially updated logical table.

The trade-off is that a table at this scale can accumulate many files, manifests, snapshots, and metadata objects, so lifecycle maintenance must be part of normal platform operation.

2. Partition by day(event_ts) and sort by (entity_id, event_ts)

The selected table layout partitions by day(event_ts). This gives useful pruning for time-based work while avoiding a finer hourly partition scheme that could create excessive partition management overhead.

Inside that layout, the selected sort order is (entity_id, event_ts). This improves locality for records belonging to the same entity while still retaining useful event-time ordering. The partition strategy and sort strategy solve different problems: partitioning reduces the file groups considered for relevant predicates, while sorting improves locality within the physical data layout.

Repeated hourly merges can still create small or uneven files, so this layout does not eliminate the need for compaction.

3. Route hourly writes and late corrections to isolated Spark writer compute

Transactional systems, application events, event streams, and explicit late corrections feed the writer side as an hourly batch. Isolated Spark writer compute first loads and validates that input and then applies MERGE INTO as an upsert.

The same merge path handles newly arrived records and corrections to existing records. The writer pool owns this work; reader and maintenance compute do not perform the hourly merge.

The table uses optimistic concurrency. If another writer commits a conflicting table change first, the losing operation detects that its expected table state is stale and retries rather than publishing a conflicting partial result.

4. Publish each successful hourly update with an atomic metadata commit

Writing data files is not the publication event. After Spark has produced the required files, the writer must successfully commit new Iceberg table metadata. That commit creates a new snapshot that becomes the table's new current state.

This commit boundary is what makes hourly writes and late corrections safe for concurrent readers. A consumer sees a committed snapshot, not an intermediate collection of newly created files. If the commit does not succeed, the previous snapshot remains the authoritative table state.

5. Give concurrent readers stable snapshots on separate compute

Reader workloads run on their own Spark or SQL-engine compute pool. Each query resolves a table snapshot and reads the data files referenced by that snapshot. Commits that happen after the query has established its snapshot do not turn the query into a mixture of old and new table versions.

That gives BI and analytics users, data scientists, applications, and downstream data products a stable, consistent view while the writer publishes hourly updates in parallel. A later query can observe the newer committed snapshot, while a query already using an earlier snapshot continues against that earlier table state.

The operational trade-off is that snapshot and file cleanup must be conservative enough that retained history and in-flight work are not invalidated by aggressive deletion.

6. Run table maintenance on isolated Spark maintenance compute

A third Spark compute pool owns maintenance. The selected maintenance operations are rewrite_data_files for compaction, optional rewrite_manifests, expire_snapshots for snapshot retention, and remove_orphan_files for unreferenced-file cleanup.

rewrite_data_files rewrites data files into a healthier physical layout after repeated merges. A successful rewrite publishes replacement files through a new snapshot. rewrite_manifests can reorganize manifest metadata when needed. expire_snapshots removes old snapshot history according to the retention policy. remove_orphan_files cleans files that are not referenced by valid table metadata after a conservative age threshold.

Separate maintenance compute prevents large rewrite work from taking the compute capacity needed by hourly writers or interactive readers. Storage and catalog state remain shared; the isolation is in compute resources.

7. Retain snapshots deliberately

The diagram keeps recent snapshots based on both age and a minimum retained count. This preserves a rollback and time-travel window without retaining every snapshot indefinitely.

Snapshot expiration removes old snapshot metadata when the retention policy permits it. Files that remain referenced by retained snapshots must remain protected. This means retention directly affects storage use: longer history gives operators more rollback and investigation flexibility but keeps more metadata and referenced data files alive.

The platform should therefore treat snapshot retention as an explicit operating policy rather than an accidental side effect of hourly commits.

8. Recover from an interrupted rewrite without exposing partial data

The important failure case is a rewrite that creates replacement files and then stops before committing replacement metadata. In that case, the current snapshot does not change. Readers continue using the last committed snapshot and never see a half-rewritten table.

The failed operation can leave newly created files in object storage that no committed snapshot references. Those files are not part of the visible logical table. The maintenance owner can retry the rewrite, and remove_orphan_files can later delete the unreferenced files after the conservative safe-age threshold.

This is different from rolling back a committed snapshot. The interrupted rewrite never became visible in the first place, so recovery is retry plus safe cleanup rather than undoing partially published table state.

9. Keep the shared table state separate from disposable compute

Object storage holds the table's data files. The Iceberg catalog and metadata track snapshots, manifests, and table state. Writer, reader, and maintenance Spark resources are separate compute boundaries that operate against that shared table state.

This means a failed Spark task does not itself redefine the logical table. The critical correctness state is the committed Iceberg metadata and the data files referenced by those commits. Producer teams provide new records and corrections. Consumer teams read committed table states. The platform owns compute separation, table maintenance, retention policy, metadata lifecycle, and recovery procedures.

10. Observe file, metadata, commit, and compute pressure

At this scale, raw row count is only one part of the problem. Likely operational pressure can come from merge amplification, small-file accumulation, manifest growth, snapshot accumulation, commit conflicts, maintenance backlog, or high reader concurrency.

The platform should therefore observe hourly job completion, whether a metadata commit succeeded, file counts and file sizes, snapshot growth, maintenance progress, query pressure, and saturation in each compute pool. A failed compute task and a failed metadata commit are different states and should be diagnosed separately.

The design deliberately pays for independent writer, reader, and maintenance compute because isolation protects hourly publication and interactive reads from heavy background rewrites. The cost is more compute administration, while the benefit is a smaller noisy-neighbor blast radius.

Technical Approach
  1. Use Apache Iceberg format v2 as the logical table and atomic publication boundary.
  2. Store production data files in object storage and keep table metadata, snapshots, and manifests in the Iceberg metadata and catalog layer.
  3. Partition by day(event_ts) and use the selected sort order (entity_id, event_ts).
  4. Route each hourly load, including late corrections, to isolated Spark writer compute.
  5. Load and validate the batch, then apply MERGE INTO as an upsert.
  6. Use optimistic concurrency; detect commit conflicts and retry against valid current table state instead of exposing overlapping partial updates.
  7. Publish a successful hourly change only through the atomic metadata commit that creates a new snapshot.
  8. Run consumers on isolated Spark or SQL reader compute and have each query read a stable snapshot.
  9. Run rewrite_data_files, optional rewrite_manifests, expire_snapshots, and remove_orphan_files on isolated Spark maintenance compute.
  10. Retain snapshots by age and minimum count, and clean orphan files only after a conservative safe-age threshold.
  11. If a rewrite stops before commit, leave the previous snapshot current, retry the rewrite, and later delete unreferenced files through orphan cleanup.
Practical Insights

Ten billion rows make unnecessary scans, rewrites, and metadata work expensive. Day-level partitioning limits how much data many time-based operations must consider without creating an hourly partition for every load. Sorting by entity_id and event_ts improves locality for the selected correction pattern. MERGE INTO can still rewrite affected files, so repeated hourly updates may create fragmentation and compaction work. Reader planning cost can grow when there are too many files or metadata objects. Snapshot retention also has a storage cost because data referenced by retained snapshots must remain available. Separate writer, reader, and maintenance compute costs more than one shared pool, but it prevents maintenance or query spikes from consuming the resources needed for hourly commits. The platform should watch file growth, metadata growth, commit conflicts, maintenance backlog, query concurrency, and compute saturation instead of treating row count as the only scaling limit.

Why Interviewers Ask This

This tests whether a candidate can operate a very large mutable lakehouse table instead of only loading data into it. The interviewer is looking for sound judgment about table layout, atomic commits, concurrent reads, late corrections, compaction, metadata lifecycle, failure recovery, and workload isolation.

Common interview mistakes

Common mistakes include partitioning too finely, treating partitioning and sorting as the same decision, performing row-by-row mutations instead of a table-format merge, assuming newly written files are visible before the metadata commit, and pausing readers during every hourly update. Another mistake is sharing one compute pool for readers, writers, and compaction, which allows maintenance or query spikes to starve hourly processing. Candidates also often describe an interrupted rewrite as table corruption even when no new metadata was committed. In this design, the old snapshot remains current and the extra unreferenced files can be cleaned later. Finally, snapshot expiration and orphan cleanup should not be made aggressively destructive because retained snapshot references and recently written files need a safe lifecycle boundary.

Interview tip

Center the explanation on the Iceberg commit boundary. State that Spark may create files during a merge or rewrite, but the logical table changes only when new metadata commits a snapshot. Then connect that rule to stable readers, late corrections, failed-rewrite recovery, retention, compaction, and isolated compute pools.

Interviewer may ask next
Suppose reader traffic becomes heavy enough to compete with the hourly update workload. What would you change?

I would keep the same Iceberg table, object storage, catalog, partitioning, sort order, and atomic commit model. The architecture already separates reader and writer compute, so I would scale or govern those pools independently rather than create a different normal data path. Reader queries would continue using stable snapshots, while the writer pool keeps enough dedicated capacity to complete MERGE INTO and its metadata commit. I would also inspect file fragmentation and metadata growth, because an unhealthy physical layout can increase work for both readers and writers. The trade-off is more compute capacity or stricter workload governance, but the correctness boundary remains unchanged.

What happens if compaction writes replacement files and then crashes before the replacement snapshot is committed?

The previous Iceberg snapshot remains current because the replacement metadata was never committed. Concurrent readers continue reading the old committed snapshot and do not see a mixture of old and replacement files. The maintenance owner can retry rewrite_data_files. Files produced by the failed attempt that are not referenced by committed metadata remain invisible to the logical table and become orphan-cleanup candidates. remove_orphan_files can delete them later after the conservative safe-age threshold. This recovery favors correctness over immediate cleanup: temporary extra files are acceptable, but partial table publication is not.

12. Compute the median of a large dataset without exhausting memory.PerformanceEasyApple

Question Details

Given values too large to materialize twice, compare a full sort, selection, streaming or approximate quantiles, and database-native percentile operations. State whether an exact median is required, how memory and I/O are measured, how even cardinality is handled, and which plan is appropriate for the reported data size.

Short Interview Answer (30-60 seconds)

First ask whether the median must be exact. Prefer an exact database-native percentile when the data already resides there; otherwise use external-memory sort or selection. If approximation is allowed, use a quantile sketch. Compare peak memory, spill, bytes read and written, and data passes.

Detailed Explanation

The dataset is too large to hold comfortably in memory or materialize twice, so the main decision is how much exactness is required and how much storage I/O is acceptable. An exact median can come from a database-native ordered percentile, an external-memory sort, or an exact selection algorithm. If an approximate result is acceptable, a streaming quantile sketch can estimate the 0.5 quantile with much smaller state. The correct plan depends on measured memory usage, spill, bytes read and written, passes over the data, data location, engine support, and the reported data size.

Useful Questions to Ask the Interviewer
  1. Must the median be exact, or is an approximate 0.5 quantile acceptable?
  2. Where does the dataset currently live: files or object storage, or a database or warehouse?
  3. Does the existing engine support an exact ordered percentile operation?
  4. What is the reported data size, and how much execution memory is available?
  5. What I/O limits matter, such as temporary-storage capacity, bytes read and written, or the number of passes over the dataset?
  6. For even cardinality, should the exact numeric median be the average of the two middle ordered values?
Compute the median of a large dataset without exhausting memory. diagram
How to Explain It in an Interview

I would start with the exactness requirement because it determines the algorithm family.

If an approximate median is acceptable, I would use a mergeable quantile sketch or approximate-percentile algorithm. It consumes the input as a stream, keeps bounded or configurable summary state instead of retaining every value, and estimates the 0.5 quantile directly. It does not find the two exact middle records and average them. The trade-off is much lower memory and typically one input pass in exchange for approximation error.

If the median must be exact and the data already lives in a database or warehouse, I would first check whether that engine supports an exact continuous ordered percentile. Running the operation where the data already resides avoids materializing the entire dataset in a client process. One ordered-set syntax used by engines that support it is percentile_cont(0.5) WITHIN GROUP (ORDER BY value). I would treat that only as an example syntax and verify the selected engine's exact semantics and physical execution.

If there is no suitable native exact percentile, an external-memory full sort is the straightforward exact approach. Read chunks that fit in execution memory, sort them, write sorted runs to temporary storage, merge the runs, and retrieve the middle value or values. The result is exact, but sorting the entire dataset can create substantial read, write, merge, and spill I/O.

An exact selection algorithm is another option. Instead of globally ordering all values, an external or distributed order-statistic algorithm partitions records around pivots and keeps following the partition that contains the required middle rank. This can avoid fully sorting the entire dataset, but an external implementation can require repeated partitioning and rereading. Whether it is better than external sort depends on the actual engine, storage system, data distribution, and measured I/O.

For exact numeric median semantics, if n is odd, return the middle ordered value. If n is even and indexing is zero-based, return (x[n/2 - 1] + x[n/2]) / 2. This rule applies to the exact path after ordering or exact selection; an approximate quantile sketch estimates the 0.5 quantile directly instead.

I would compare the alternatives using peak resident or execution memory, spill or temporary bytes, total bytes read and written, and the number of passes over the dataset. I would also record where the data resides and what the processing engine supports. The question provides no universal numeric cutoff, so I would not invent one. I would choose the method whose execution-memory requirement fits the available memory and whose I/O cost is acceptable for the reported workload.

The final decision is: use a supported exact database-native percentile when the data already resides in that system; otherwise use external-memory full sort or exact selection when exactness is mandatory; use a streaming quantile sketch when approximation is acceptable. Then rerun the same representative workload and verify the required exact or approximate result semantics while comparing memory and I/O.

Technical Approach
  1. Confirm whether the required median is exact or approximate.
  2. Record where the data resides and whether the existing database or warehouse supports an exact ordered percentile.
  3. Measure peak execution memory, spill or temporary bytes, total bytes read and written, and the number of passes over the data.
  4. If approximation is acceptable, stream the values through a mergeable quantile sketch and query the 0.5 quantile.
  5. If exactness is required and a suitable database-native percentile exists, compute it where the data already resides.
  6. Otherwise compare external-memory full sort with an external or distributed exact order-statistic selection algorithm.
  7. For an exact numeric result, return the middle value when n is odd; when n is even, return the average of the two middle ordered values.
  8. Choose the plan that fits the reported data size, available execution memory, storage and I/O budget, and actual engine capabilities without inventing a universal size threshold.
Practical Insights

A full sort usually performs O(n log n) comparison work and, when the values do not fit in memory, can require substantial temporary writes and rereads during run generation and merging. Exact selection can avoid globally sorting every value and may perform closer to linear comparison work depending on the algorithm, but an external implementation can still need several partition and reread passes. A quantile sketch normally processes each input once and keeps compact summary state, so its memory requirement is much smaller, but its result is approximate. A database-native percentile leaves the physical implementation to the database engine, so its real sort, spill, memory, and I/O costs should be measured rather than assumed. The most important operational metrics here are peak execution memory, temporary or spill bytes, bytes read and written, and passes over the data.

Why Interviewers Ask This

This question tests whether the candidate can separate exact and approximate requirements, reason about memory versus storage I/O, compare full sorting with exact selection, use database-native computation when appropriate, handle even cardinality correctly, and choose a scalable approach from measured workload constraints instead of assuming the dataset fits in memory.

Common interview mistakes

Common mistakes include collecting all values into application memory and sorting them there; assuming a two-heap online median solves the memory problem even though an exact two-heap method retains O(n) values; treating an approximate 0.5 quantile as if it were computed by averaging two exact middle records; assuming external selection is always a single-pass operation; using a percentile function without checking whether the engine provides exact or approximate semantics; ignoring temporary spill and reread I/O; forgetting the even-cardinality rule; and inventing a fixed data-size cutoff instead of comparing the reported size with available execution memory and storage I/O.

Interview tip

Lead with the exact-versus-approximate decision, then say where the data already lives. Compare native percentile, external sort, exact selection, and quantile sketches using measured memory and I/O, and explicitly state the even-cardinality rule. Do not invent a universal size threshold when none is given.

Interviewer may ask next
Why not maintain two heaps to compute an exact streaming median?

Two heaps are useful when an application needs the exact median after each new value arrives, but together they retain essentially all observed values split between a max-heap and a min-heap. Their memory therefore grows as O(n), so they do not satisfy this problem's goal of avoiding memory exhaustion for a very large finite dataset. An external-memory exact method or an approximate quantile sketch is more appropriate.

How would you choose between external sorting and an exact selection algorithm?

I would run both under the same representative workload when both are available and compare peak execution memory, spill or temporary bytes, total bytes read and written, and the number of storage passes. External sorting is straightforward and exact but orders the entire dataset. Exact selection can avoid full ordering, but an external implementation may need repeated partitioning and rereading. The better plan depends on measured storage I/O, data distribution, engine capabilities, and the requirement to preserve exact median semantics.

13. Determine whether a string is a palindrome after normalization.CodingEasyApple

Question Details

Implement isPalindrome(text). Convert uppercase letters to lowercase, discard every non-alphanumeric character, and return whether the remaining printable-ASCII characters read identically from both ends. The input length is from 1 to 200,000. Example: "H123!@321h" returns true, while "race a car" returns false.

Short Interview Answer (30-60 seconds)

I would use two pointers, one starting at each end of the string. I move each pointer past characters that are not ASCII letters or digits. When both pointers are on valid characters, I compare their lowercase forms. If they differ, I return False immediately. If they match, I move both pointers inward. When the pointers meet or cross, every retained pair has matched, so I return True. This takes O(n) time and uses O(1) auxiliary space.

Detailed Explanation

See the Code while reading this explanation.

The input is one string. We ignore every character that is not an ASCII letter or digit, and uppercase and lowercase versions of the same ASCII letter are treated as equal. We then check whether the characters that remain read the same from left to right and right to left. A two-pointer method fits this problem because one pointer can start at each end and move inward. It avoids creating a separate normalized copy of the whole string.

Useful Questions to Ask the Interviewer
  1. Should only ASCII letters and digits count as retained characters?
  2. Should uppercase and lowercase forms of the same ASCII letter compare as equal?
  3. If no alphanumeric characters remain after filtering, should the result be True?
Determine whether a string is a palindrome after normalization. diagram
How to Explain It in an Interview
1. Understand the input and required output

The function receives a string named text. Its length is from 1 to 200,000. We keep only ASCII letters A-Z or a-z and ASCII digits 0-9. Case does not matter. The function returns a Boolean value. It returns True when the retained characters form a palindrome and False when they do not.

2. Choose the two-pointer approach

I use two integer pointers called i and j. Pointer i starts at index

  1. Pointer j starts at index len(text) -
  2. Instead of building a normalized copy, each pointer skips characters that should be ignored. This keeps the extra memory constant.

The central invariant is that every retained character pair outside the current i-to-j range has already been compared and matched.

3. Initialize the state

For the example "H123!@321h", the string length is 10. Pointer i starts at index 0 on H. Pointer j starts at index 9 on h. The algorithm first skips invalid characters when needed, then compares the lowercase forms of the current retained characters.

4. Walk through the example

At i = 0 and j = 9, H and h are both ASCII alphanumeric characters. Their lowercase forms are h and h, so they match. The pointers move to i = 1 and j = 8.

At indices 1 and 8, the values are 1 and 1. They match, so the pointers move to 2 and 7.

At indices 2 and 7, the values are 2 and 2. They match, so the pointers move to 3 and 6.

At indices 3 and 6, the values are 3 and 3. They match, so the pointers move to 4 and 5.

Now i = 4 points to ! and j = 5 points to @. The left skip loop sees that ! is not ASCII alphanumeric and increments i from 4 to 5. Now i == j. Because both skip loops and the comparison are guarded by i < j, the right skip loop does not run and no comparison is made. The outer loop then ends.

The retained normalized sequence is "h123321h". Every required pair matched, so the function returns True.

5. Explain why the result is correct

The algorithm compares only characters that belong to the required normalized sequence. Every time it compares a pair, the characters come from opposite ends of the remaining normalized content. If their lowercase forms differ, the normalized string cannot be a palindrome, so returning False is correct. If no pair differs before the pointers meet or cross, every required symmetric pair has matched, so returning True is correct.

6. Explain the Python implementation

The code uses isascii() together with isalnum() so only ASCII alphanumeric characters are retained. The left skip loop moves i right over ignored characters. The right skip loop moves j left over ignored characters. If i is still less than j, the code compares text[i].lower() with text[j].lower(). A mismatch returns False immediately. A match moves both pointers inward. When the outer loop finishes, the function returns True.

7. Explain complexity and edge cases

The time complexity is O(n), where n is the input length. Both pointers move only toward the center, so the input is processed at most once overall. The auxiliary space complexity is O(1) because the solution stores only the two pointers and a few temporary values. Relevant edge cases include a string containing only ignored characters, a single retained alphanumeric character, mixed uppercase and lowercase ASCII letters, digits, and an early mismatch such as "race a car".

Key Insight / Why This Solution Works

The key idea is to normalize while comparing instead of first creating a second string. A left pointer and a right pointer move inward. Each pointer skips characters that are not both ASCII and alphanumeric. When both pointers are on retained characters, their lowercase forms must match. The invariant is that all retained character pairs outside the current pointer range have already matched. A mismatch proves that the normalized string is not a palindrome. If the pointers meet or cross without a mismatch, every required pair has matched. This gives the same result as building a normalized string but avoids O(n) extra storage.

Code
def isPalindrome(text: str) -> bool:
    # Start one pointer at each end of the original string.
    i = 0
    j = len(text) - 1

    # Continue while two different positions remain to be compared.
    while i < j:
        # Skip characters on the left that are not ASCII letters or digits.
        while i < j and not (text[i].isascii() and text[i].isalnum()):
            i += 1

        # Skip characters on the right that are not ASCII letters or digits.
        # Stop if the pointers have already met.
        while i < j and not (text[j].isascii() and text[j].isalnum()):
            j -= 1

        # Compare only when two separate retained positions still remain.
        if i < j:
            # Compare lowercase forms because ASCII letter case does not matter.
            if text[i].lower() != text[j].lower():
                # One mismatched retained pair proves the string is not a palindrome.
                return False

            # This pair matched, so move both pointers toward the center.
            i += 1
            j -= 1

    # No retained pair mismatched, so the normalized string is a palindrome.
    return True


def main() -> None:
    # Run the exact example used in the approved diagram.
    text = "H123!@321h"
    result = isPalindrome(text)
    print(result)


if __name__ == "__main__":
    # Execute the example when this file is run directly.
    main()
Time & Space Complexity

Let n be the length of the input string. The time complexity is O(n). Each pointer only moves toward the center, so characters are examined a constant number of times and the input is processed at most once overall. The function may also stop early when it finds a mismatch. The auxiliary space complexity is O(1). Auxiliary space means extra memory used by the algorithm. The solution stores only two pointer variables and a few temporary values. It does not create a normalized copy of the input.

Where it is used

This two-pointer pattern is useful when software needs to compare data from opposite ends while ignoring selected characters. It can be used for normalized text validation, symmetric identifier checks, and lightweight validation of large strings when creating a full filtered copy would use unnecessary memory.

Why Interviewers Ask This

This problem tests whether you can turn a normalization rule into precise code while avoiding unnecessary memory. The interviewer can evaluate whether you recognize the two-pointer pattern, handle ASCII filtering and case conversion correctly, maintain safe pointer movement, and reason about early stopping. It also checks whether you can trace exact indices through ignored characters, explain a useful invariant, and give accurate O(n) time and O(1) auxiliary space complexity.

Common interview mistakes

A common mistake is using isalnum() by itself and unintentionally accepting non-ASCII alphanumeric characters. Another mistake is building a normalized copy but still claiming O(1) auxiliary space. Candidates may forget to compare lowercase forms, move a pointer in the wrong direction, or continue processing after the pointers have met. In the shown example, it is also incorrect to say that both ! and @ are skipped. Skipping ! moves i from 4 to 5, so i == j and the right skip loop does not execute.

Interview tip

Trace the final pointer step exactly. When i = 4 and j = 5, the left pointer skips ! and becomes 5. At that moment i == j, so the right skip loop and comparison do not run. Explaining this precisely shows that you understand the actual loop conditions and not only the high-level two-pointer idea.

Interviewer may ask next
How would the solution change if Unicode letters and digits should also count as alphanumeric?

The two-pointer algorithm would stay the same, but I would remove the isascii() requirement and use the required Unicode-aware alphanumeric and case-comparison rules. Each pointer would still skip characters that are outside the accepted set and compare retained characters from opposite ends. Correctness is preserved because the same symmetric-pair invariant still holds. The time complexity remains O(n) and the auxiliary space remains O(1). The tradeoff is that character classification and case behavior become Unicode-aware instead of ASCII-only.

How would you handle this if the input were a very large forward-only stream?

The current method needs access to both ends, so it cannot directly compare a forward-only stream with two pointers. I would normalize the incoming characters and store the retained sequence in memory or durable storage, then compare from both ends after ingestion. The normalized data is still checked symmetrically, so correctness is preserved. Processing remains O(n) time, but auxiliary storage becomes O(k), where k is the number of retained characters. The tradeoff is extra storage in exchange for supporting a forward-only input source.

14. Find the length of the longest substring without repeated characters.CodingMediumApple

Question Details

Implement lengthOfLongestSubstring(s) for an ASCII string. Return the maximum length of a contiguous substring in which no character appears twice, and return 0 for the empty string. The target running time is O(n). Example: "abcabcbb" returns 3, "bbbbb" returns 1, and "" returns 0.

Short Interview Answer (30-60 seconds)

I would use a sliding window with a hash map that stores each character's most recent index. I move the right boundary through the string. If the current character already appears inside the active window, I move the left boundary to one position after its previous index. This keeps the active window free of repeated characters. I then update the maximum window length. The solution runs in O(n) expected time and uses O(min(n, 128)) auxiliary space for ASCII input.

Detailed Explanation

See the Code while reading this explanation.

The input is an ASCII string, and we need the length of its longest continuous part where every character is different. We return only the length, not the substring itself. For example, "abcabcbb" returns 3 because "abc" is a valid substring of length 3. An empty string returns 0. I keep a moving range of unique characters. When a repeated character appears inside that range, I move the range's start just after the earlier occurrence instead of restarting the search.

Useful Questions to Ask the Interviewer
  1. Can I assume the input contains only ASCII characters, as stated?
  2. Do you want only the maximum length, not the actual substring?
  3. Should the empty string return 0, as stated in the contract?
Find the length of the longest substring without repeated characters. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is an ASCII string s. The output is one integer: the maximum length of a contiguous substring with no repeated characters. A substring uses consecutive characters. The empty string must return 0.

2. Choose the sliding window and hash map

I use two boundaries named left and right. They define the current window s[left:right + 1]. I also use a hash map named last_seen. It stores character -> most recent index. After duplicate handling at each step, the active window contains no repeated characters.

3. Initialize the state

Start with left = 0, best = 0, and last_seen = {}. Then move right from left to right through the string. At each index, inspect the current character and its most recent index, if one exists.

4. Walk through the example

For s = "abcabcbb":

At right = 0, the character is a. It has not appeared in the active window. Keep left = 0, store a -> 0, and measure window "a". Its length is 1, so best = 1.

At right = 1, the character is b. It has not appeared in the active window. Store b -> 1. The window is "ab", length 2, so best = 2.

At right = 2, the character is c. It has not appeared in the active window. Store c -> 2. The window is "abc", length 3, so best = 3.

At right = 3, the character is a. Its previous index is 0, and 0 >= left, so that a is still inside the active window. Move left to 0 + 1 = 1. Then store a -> 3. The window is "bca", length 3, so best remains 3.

At right = 4, b was previously at index 1, and 1 >= left. Move left to 2. Store b -> 4. The window is "cab", length 3, so best remains 3.

At right = 5, c was previously at index 2, and 2 >= left. Move left to 3. Store c -> 5. The window is "abc", length 3, so best remains 3.

At right = 6, b was previously at index 4, and 4 >= left. Move left to 5. Store b -> 6. The window is "cb", length 2, so best remains 3.

At right = 7, b was previously at index 6, and 6 >= left. Move left to 7. Store b -> 7. The window is "b", length 1, so best remains 3.

The final returned result is 3.

5. Explain why the result is correct

After processing a repeated character, left is moved just after that character's previous occurrence. Therefore the active window contains no repeated characters before its length is measured. best stores the largest valid window length seen so far. Because right reaches every possible ending position, the largest recorded valid window is the required answer.

6. Explain the Python implementation and complexity

The loop uses enumerate(s) to get each right index and character. The map gives the character's most recent index. We move left only when that index is greater than or equal to the current left, which prevents the left boundary from moving backward. Then we record the newest index and calculate right - left + 1. Python dictionary lookup and insertion are O(1) on average, so the total running time is O(n) expected time. For ASCII input, the map holds at most 128 character entries, so auxiliary space is O(min(n, 128)).

Key Insight / Why This Solution Works

Use a sliding window and a hash map. right expands the window one character at a time. last_seen maps each character to its most recent index. If the current character was previously seen at an index greater than or equal to left, that previous occurrence is inside the active window, so set left = last_seen[char] + 1. Then update the character's most recent index and measure the current window. The central invariant is that after duplicate handling, s[left:right + 1] contains no repeated characters. This avoids restarting from every possible starting position.

Code
def lengthOfLongestSubstring(s: str) -> int:
    # Store each character's most recent index so duplicate handling is fast.
    last_seen: dict[str, int] = {}

    # left is the inclusive start of the current duplicate-free window.
    left = 0

    # best stores the largest valid window length found so far.
    best = 0

    # Expand the right boundary one character at a time.
    for right, char in enumerate(s):
        # If this character already appears inside the active window,
        # move left just after its previous occurrence.
        if char in last_seen and last_seen[char] >= left:
            left = last_seen[char] + 1

        # Record the newest index for this character.
        last_seen[char] = right

        # Measure the current valid window and keep the largest length.
        best = max(best, right - left + 1)

    # The empty string returns 0 because best starts at 0.
    return best


def main() -> None:
    # Run the same verified example shown in the diagram.
    s = "abcabcbb"
    result = lengthOfLongestSubstring(s)
    print(result)  # 3


if __name__ == "__main__":
    main()
Time & Space Complexity

Let n be the number of characters in the string. The right index processes each character once. Python dictionary lookup and insertion are O(1) on average, so the overall running time is O(n) expected time. The hash map stores at most one entry for each distinct character. Because the input is ASCII, there are at most 128 possible character entries. Therefore the auxiliary space is O(min(n, 128)). For a general unbounded character set, the extra space would usually be written as O(n).

Where it is used

This sliding-window pattern is useful when software must inspect continuous ranges while maintaining a rule about the current range. It can be used for duplicate-free text regions, bounded-window validation, and stream-like processing where the active range must satisfy a uniqueness condition.

Why Interviewers Ask This

This problem tests whether you recognize the sliding-window pattern and choose a suitable data structure for duplicate tracking. It also checks whether you can maintain a clear invariant, update pointer boundaries correctly, handle repeated characters without moving left backward, and distinguish a substring from a subsequence. The interviewer can also evaluate whether your Python implementation is correct, whether you handle the empty string, and whether you explain expected O(n) time and auxiliary memory accurately.

Common interview mistakes

One common mistake is moving left backward when the previous occurrence of a character is already outside the active window. The check last_seen[char] >= left prevents that. Another mistake is treating a substring like a subsequence. The characters must be contiguous. Candidates may also forget to store the character's most recent index, measure the window before restoring uniqueness, or calculate the inclusive length incorrectly as right - left instead of right - left + 1. Another mistake is describing Python dictionary operations as guaranteed worst-case O(1) instead of average O(1).

Interview tip

State the invariant early: after duplicate handling, s[left:right + 1] contains no repeated characters. Then use the repeated a at index 3 in "abcabcbb" to show exactly why left moves from 0 to 1.

Interviewer may ask next
How would you return the actual longest substring instead of only its length?

Keep the same sliding-window algorithm and last_seen map. Add a variable such as best_start. Whenever right - left + 1 becomes larger than best, update both best and best_start = left. At the end, return s[best_start:best_start + best]. The duplicate-handling invariant does not change. The scan still takes O(n) expected time. The algorithmic state still uses O(min(n, 128)) auxiliary space for ASCII, while constructing the returned substring requires space proportional to the substring length.

How would the solution change if the input arrived as a very large stream?

The same state can be updated as each character arrives. Keep a running right index, left, best, and the last_seen map. Each incoming character uses the same duplicate check, left-boundary update, map update, and length calculation. Over n received characters, the processing time remains O(n) expected time. For ASCII, the map still contains at most 128 entries. If only the maximum length is required, old characters do not need to be retained. Returning the actual substring would require storing enough stream data separately.

15. Merge multiple sorted linked lists into one sorted linked list.NEWCodingHardApple

Question Details

Implement mergeKLists(lists). The input contains k sorted singly linked lists, where 0 <= k <= 10,000, each list has at most 500 nodes, values are between -10,000 and 10,000, and the total node count is at most 10,000. Return one ascending list without losing duplicate values. Example: [[1,4,5],[1,3,4],[2,6]] returns [1,1,2,3,4,4,5,6]; [] and [[]] both return an empty list.

Short Interview Answer (30-60 seconds)

I would use a min-heap that keeps one current node from each non-empty sorted list. I first push every list head into the heap. Then I repeatedly pop the smallest node, save its next node, append the popped node to the merged list, and push the saved successor if it exists. A unique counter handles equal values safely. Because the heap always exposes the next smallest candidate, duplicates are preserved. With N total nodes and k lists, the time is O(N log k) and auxiliary space is O(k).

Detailed Explanation

See the Code while reading this explanation.

We have k singly linked lists. Each list is already sorted from smallest to largest. We need to join them into one ascending linked list without losing duplicate values. The main idea is to look only at the current first unmerged node from each non-empty list. A min-heap can quickly tell us which of those nodes has the smallest value. After taking that node, only its next node from the same list becomes a new candidate. This matches the diagram's k-way merge approach.

Useful Questions to Ask the Interviewer
  1. Can I reuse and relink the original nodes instead of creating a new node for every value?
  2. Should both [] and [[]] return an empty linked list?
  3. Can I assume every input linked list is already sorted in ascending order?
Merge multiple sorted linked lists into one sorted linked list. diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is a collection of k sorted singly linked lists. There can be from 0 to 10,000 lists. Each list has at most 500 nodes, and there are at most 10,000 nodes in total. Node values are between -10,000 and 10,000. We must return the head of one ascending linked list. Duplicate values must be preserved. The example is [[1,4,5],[1,3,4],[2,6]], and the required result is [1,1,2,3,4,4,5,6]. Both [] and [[]] return an empty list.

2. Choose the algorithm and data structure

I use a min-heap. The heap contains at most one current node from each unfinished list. Each heap entry is (node value, unique counter, node reference). The node value gives the priority. The unique counter prevents Python from trying to compare ListNode objects when two node values are equal. The main invariant is that the heap contains the smallest not-yet-merged node from every list that still has nodes available.

3. Initialize the state

I create an empty min-heap, a monotonic counter, a dummy head, and a tail pointer. I push the head node of every non-empty input list into the heap. For the diagram example, the starting candidates have values 1 from L0, 1 from L1, and 2 from L2. The dummy node gives the result list a simple starting point, and tail always points to the last node already attached to the merged result.

4. Walk through the example

The heap starts with candidates 1 from L0, 1 from L1, and 2 from L2.

Step 1: Pop 1 from L0. Before relinking it, save its next node, which has value 4. Attach the popped node to the result and push 4 from L0. The merged list is [1].

Step 2: Pop 1 from L1. Save its next node, which has value 3. Attach the 1 and push 3 from L1. The merged list is [1,1].

Step 3: Pop 2 from L2. Save 6, attach 2, and push 6. The merged list is [1,1,2].

Step 4: Pop 3 from L1. Save 4, attach 3, and push 4. The merged list is [1,1,2,3].

Step 5: Pop 4 from L0. Save 5, attach 4, and push 5. The merged list is [1,1,2,3,4].

Step 6: Pop 4 from L1. It has no successor, so nothing is pushed. The merged list is [1,1,2,3,4,4].

Step 7: Pop 5 from L0. It has no successor, so nothing is pushed. The merged list is [1,1,2,3,4,4,5].

Step 8: Pop 6 from L2. It has no successor, so nothing is pushed. The merged list is [1,1,2,3,4,4,5,6]. The heap is now empty, so processing stops.

5. Explain why the result is correct

Every source list is sorted. Therefore, after a node from one list is removed, only that node's successor can become the next candidate from that same list. The heap contains the smallest remaining candidate from each unfinished list. Its minimum is therefore the smallest node that can appear next in the final merged list. Appending that node and pushing only its successor restores the invariant. Repeating this until the heap is empty processes every node exactly once and preserves duplicate values.

6. Explain the Python implementation

The code uses heapq as a min-heap and itertools.count as the unique tie-breaker. Before attaching a popped node, it saves node.next in next_node. This is important because the implementation reuses and relinks the original nodes. After attaching the popped node, the code pushes next_node when it exists. When the heap becomes empty, all nodes have been merged. The code sets tail.next to None and returns dummy.next as the final head.

7. Explain complexity and edge cases

Let N be the total number of nodes and k be the number of input lists. Every node is pushed once and popped once. The heap contains at most k nodes, so each heap operation takes O(log k). Total time is O(N log k). Auxiliary space is O(k) for the heap. The implementation handles empty input, lists containing empty lists, duplicate values, negative values, and many empty lists mixed with non-empty lists.

Key Insight / Why This Solution Works

The key insight is that, because every input linked list is already sorted, only the current first unmerged node from each unfinished list can be the next smallest value. A min-heap stores those candidates. Each heap entry contains the node value, a unique increasing counter, and the node reference. The invariant is that the heap contains at most one smallest unmerged candidate from each non-empty remaining list. After popping the minimum, the algorithm saves that node's successor, relinks the popped node into the result, and pushes only the saved successor. This keeps the heap size at most k and preserves sorted order and duplicates.

Code
import heapq
from itertools import count
from typing import List, Optional


class ListNode:
    def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
        self.val = val
        self.next = next


def mergeKLists(lists: List[Optional[ListNode]]) -> Optional[ListNode]:
    # Keep at most one current candidate from each non-empty list.
    heap = []

    # Give every heap entry a unique tie-breaker so equal node values
    # never require Python to compare ListNode objects.
    counter = count()

    # Start with the head node of every non-empty sorted list.
    for node in lists:
        if node is not None:
            heapq.heappush(heap, (node.val, next(counter), node))

    # Use a dummy head so attaching the first result node is simple.
    dummy = ListNode(0)
    tail = dummy

    while heap:
        # Remove the smallest current node across all unfinished lists.
        _, _, node = heapq.heappop(heap)

        # Save the successor before rewiring the popped node into the result.
        next_node = node.next

        # Reuse the original node and advance the result tail.
        tail.next = node
        tail = node

        # Only this successor can become the next candidate from its source list.
        if next_node is not None:
            heapq.heappush(
                heap,
                (next_node.val, next(counter), next_node),
            )

    # Explicitly terminate the final merged list.
    tail.next = None
    return dummy.next


def build_list(values: List[int]) -> Optional[ListNode]:
    # Build linked lists for the exact runnable example.
    dummy = ListNode(0)
    tail = dummy

    for value in values:
        tail.next = ListNode(value)
        tail = tail.next

    return dummy.next


def to_values(head: Optional[ListNode]) -> List[int]:
    # Convert the merged linked list to values for easy verification.
    values: List[int] = []

    while head is not None:
        values.append(head.val)
        head = head.next

    return values


def main() -> None:
    # Use the exact example shown in the diagram.
    lists = [
        build_list([1, 4, 5]),
        build_list([1, 3, 4]),
        build_list([2, 6]),
    ]

    # Merge the lists and print the expected ascending result.
    merged = mergeKLists(lists)
    print(to_values(merged))


if __name__ == "__main__":
    main()
Time & Space Complexity

Let N be the total number of nodes across all input lists, and let k be the number of lists. Every node enters the min-heap once and leaves it once. The heap contains at most k nodes, so each push or pop takes O(log k) time. The total time is O(N log k). The heap needs at most one current node from each list, so auxiliary space is O(k). The merged result reuses the original linked-list nodes, so the algorithm does not allocate N replacement value nodes.

Where it is used

This k-way merge pattern is useful when several already-sorted sequences must be combined into one ordered sequence. Examples include merging sorted data batches, combining ordered event streams, merging sorted partitions, and external sorting workflows. A heap is especially useful when there are many input sequences because it avoids scanning every current head to find the next smallest value.

Why Interviewers Ask This

This problem tests whether you recognize a k-way merge and choose the right data structure for it. It also checks whether you can maintain a heap invariant, safely manipulate linked-list references, and handle duplicate values without invalid object comparisons. The interviewer can evaluate whether you understand why the heap contains at most k nodes, whether you can derive O(N log k) time and O(k) auxiliary space, and whether your code handles empty inputs and duplicates correctly.

Common interview mistakes

One common mistake is pushing only (value, node) into heapq. With duplicate values, Python may then try to compare ListNode objects. Another mistake is relinking a node before saving node.next, which can lose the remainder of that source list. A candidate may also push all N nodes into the heap at once instead of keeping only one current candidate per list. Another mistake is creating unnecessary replacement nodes instead of reusing the original nodes. Finally, the illustrated approach is O(N log k) time with O(k) auxiliary heap space, so claiming O(N log N) or O(1) auxiliary space would not match the implementation.

Interview tip

State the invariant before writing code: the min-heap contains at most one current node from every unfinished list, and the smallest heap entry is the next node that belongs in the merged result. Then explicitly say that you save node.next before relinking the popped node. Those two points explain both correctness and safe pointer handling.

Interviewer may ask next
How would the solution change if the sorted inputs arrived as streaming iterators instead of linked lists?

The same min-heap idea still works. I would keep one current value from each unfinished iterator in the heap, together with a unique counter and a reference to that iterator. After popping the smallest value, I would request one new value only from that same iterator and push it if the iterator is not exhausted. The invariant remains the same: the heap holds at most one candidate from each active stream. For N emitted values and k streams, the time stays O(N log k) and the auxiliary heap space stays O(k). The main tradeoff is that consumed stream values may not be available again unless the source supports replay.

Why does the heap entry need a unique counter when duplicate values are allowed?

Python compares tuple fields from left to right. If two heap entries have the same node value, it compares the second field next. A unique numeric counter settles the comparison there, so Python never needs to compare the ListNode objects in the third field. This preserves duplicate values without changing the merge order requirement. The algorithm still runs in O(N log k) time and uses O(k) auxiliary heap space. The only extra cost is one small integer stored in each heap entry.

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.