Apple Software Engineer (Entry Level) Interview Preparation Guide
Apple's Software Engineer interview process is a rigorous, multi-stage assessment designed to evaluate technical fundamentals, problem-solving ability, coding proficiency, cultural fit, and learning potential. As an entry-level candidate, you will progress through a recruiter screening, a technical phone screen focusing on algorithmic coding, and an onsite loop consisting of 5 interviews covering coding problems, basic system design, behavioral questions, and domain-specific knowledge related to Apple's ecosystem. The entire process typically spans 3-6 weeks and emphasizes clean code, clear communication, collaboration, and alignment with Apple's core values of excellence, innovation, and attention to detail.
Interview Rounds
Recruiter Screening
What to Expect
Your initial conversation with Apple's recruiting team, typically 15-30 minutes, with potential follow-up discussions. The recruiter reviews your resume, discusses your background and career journey, assesses your motivation for joining Apple, and evaluates your cultural alignment with Apple's values. This round covers role expectations, interview timeline, and your opportunity to ask preliminary questions. The recruiter seeks candidates with clear communication, genuine passion for Apple's products, and appropriate technical foundation.
Tips & Advice
Be genuine and specific about why you want to join Apple—reference products you admire or company values that resonate with you. Articulate clear career goals and how this role aligns with them. Research Apple's engineering culture beforehand and reflect on how your values match. Keep answers conversational and concise. Ask thoughtful questions about the team, projects, and success metrics in the role. Maintain positive energy and smile (even on video calls). Prepare a 2-3 minute summary of your background highlighting relevant coursework in data structures and algorithms, internships, personal projects, or competitive programming experience. Show eagerness to learn and grow with Apple.
Focus Topics
Understanding of Apple's Ecosystem and Products
Be knowledgeable about Apple products relevant to the role you're interviewing for. If it's iOS, discuss apps you use and enjoy. If it's services, understand iCloud, Apple Music, or other platforms. Show that you're an Apple user and understand what makes Apple products special.
Practice Interview
Study Questions
Thoughtful Questions for the Recruiter
Prepare intelligent questions about the specific team, current projects, engineering practices, growth opportunities for entry-level engineers, onboarding process, and what success looks like in the first 90 days. Avoid basic questions answerable from the Apple website.
Practice Interview
Study Questions
Curiosity and Learning Mindset
Show genuine interest in technology and learning. Discuss technologies you're excited about or skills you're actively developing. Demonstrate that you seek to understand not just how to solve problems, but why certain approaches work better than others.
Practice Interview
Study Questions
Communication Skills and Cultural Alignment
Demonstrate clear, professional communication and alignment with Apple's core values: simplicity, excellence, innovation, and meticulous attention to detail. Show yourself as collaborative, thoughtful, and respectful of diverse perspectives. Be authentic and show enthusiasm without overconfidence.
Practice Interview
Study Questions
Technical Background Summary
Prepare a concise overview of your education, relevant coursework in data structures, algorithms, and software engineering, internships, personal projects, or competitive programming participation. Highlight technical skills and demonstrate understanding of foundational CS concepts. Show evidence of coding experience and structured technical learning.
Practice Interview
Study Questions
Career Motivation and Authentic Apple Interest
Articulate specific reasons for wanting to join Apple, not just any tech company. Reference Apple products you use or admire, engineering achievements that inspire you, or company values that align with your own. Show that you've researched Apple and understand what makes it unique in the industry.
Practice Interview
Study Questions
Technical Phone Screen
What to Expect
A 45-60 minute remote technical interview on a shared coding platform (e.g., CoderPad, HackerRank). You'll solve 1-2 algorithmic coding problems focused on data structures and algorithms. The interviewer evaluates your problem-solving methodology, coding correctness, code quality, communication skills, and handling of feedback. You may receive follow-up optimization questions or Apple-specific considerations. For entry-level, problems are typically LeetCode Easy-Medium difficulty. The interviewer assesses whether you have solid CS fundamentals and can write clean code under time pressure.
Tips & Advice
Speak your thought process aloud from the beginning—never code in silence. Start by clarifying the problem: ask about constraints, input format, expected output, and edge cases. Before coding, outline your approach verbally and discuss time/space complexity trade-offs. Write clean, readable code with meaningful variable names and avoid magic numbers. Test your solution with examples, including edge cases, before declaring it complete. If stuck, think aloud and ask for hints rather than suffering silently. For Apple roles, be prepared for follow-ups about memory efficiency, optimization for mobile platforms, or using platform-specific APIs (though this is minimal at entry level). Use the language your team uses; clarify with the interviewer if unsure (Swift for iOS, Python for data/backend). Aim to solve the main problem in 30-35 minutes, leaving 15-20 minutes for optimization, discussion, and questions.
Focus Topics
Apple Platform Awareness and Optimization
For iOS/macOS roles, understand basic memory considerations without going too deep. Be aware that Apple values simplicity and elegance. Recognize that Apple devices have constraints (battery, memory) compared to servers. When optimizing, think about these factors. Show awareness of how your algorithm would perform on resource-constrained devices.
Practice Interview
Study Questions
Time and Space Complexity Analysis
Be fluent in Big O notation: O(1), O(log n), O(n), O(n log n), O(n²), O(2^n). Understand why your solution has certain complexity. Identify optimization opportunities—e.g., using a hash table to reduce from O(n²) to O(n). Discuss trade-offs between time and space.
Practice Interview
Study Questions
Code Quality and Edge Case Handling
Write readable code with meaningful variable names (not a, b, x). Use comments for non-obvious logic. Explicitly identify and handle edge cases: empty input, single element, null values, negative numbers, duplicates. Code should be production-quality, not hack-ish.
Practice Interview
Study Questions
Linked Lists and Basic Tree Operations
Understand linked list basics: traversal, insertion, deletion, reversal. Understand singly and doubly linked lists. Master binary tree fundamentals: in-order, pre-order, post-order traversals, level-order traversal. Practice problems: reverse linked list, merge two sorted lists, symmetric tree, path sum, and balanced binary tree validation.
Practice Interview
Study Questions
Problem-Solving Methodology and Communication
Develop a structured approach: (1) understand the problem thoroughly, (2) identify patterns and similar problems, (3) think of brute force first, then optimize, (4) code clearly and test, (5) discuss complexity. Practice verbalizing your reasoning in real-time. Emphasize clarity over speed.
Practice Interview
Study Questions
Array and String Manipulation
Master common operations: searching, sorting, two-pointer technique, sliding window, prefix sums. Practice string problems: reversal, substring search, pattern matching, anagram detection. Focus on LeetCode Easy-Medium problems like two sum, merge sorted arrays, container with most water, longest substring without repeating characters, and valid parentheses.
Practice Interview
Study Questions
Onsite Technical Interview - Coding Round 1
What to Expect
First onsite coding interview with a senior engineer or team member, 45-60 minutes. LeetCode-style algorithmic problem, possibly with Apple-specific context or twists. You may write and run code live, so be prepared to handle compiler errors or runtime issues gracefully. The interviewer evaluates problem-solving methodology, code quality, communication, handling of feedback, and optimization ability under pressure. This tests core CS fundamentals and whether you perform well under the intensity of an onsite setting. This is the most formal technical interview of your onsite loop.
Tips & Advice
Apply lessons from the phone screen but with higher intensity and formality. Since you're running code live, syntax matters—carefully write code and ensure it compiles before declaring it complete. If encountering compiler errors, stay calm and debug methodically; the interviewer will observe how you troubleshoot, which is valuable signal. Verbalize your thinking more than in the phone screen; in-person communication is paramount. Be receptive to hints or feedback—if the interviewer suggests a different approach, listen respectfully and adapt without defensiveness. For entry-level candidates, showing humility and teachability is important. Practice writing code in the actual IDE/environment beforehand if possible. Bring water; take breaths if you need thinking time. Remember: the interviewer wants you to succeed and is looking for potential and learning ability, not perfection.
Focus Topics
Testing and Validation of Solutions
After coding, systematically test: simple cases, edge cases, larger cases. Trace through code manually for at least one example. Discuss test cases with the interviewer. Show that you think about correctness beyond just reaching a solution. Refactor if you spot inefficiencies.
Practice Interview
Study Questions
Debugging and Error Recovery
If code doesn't compile or has runtime errors, debug systematically: check variable types, loop conditions, boundary conditions, off-by-one errors. Ask the interviewer for help if needed, but show you're trying to understand the issue. Trace through code with concrete examples. Remain calm and methodical.
Practice Interview
Study Questions
Communication and Collaboration Under Pressure
In an onsite setting, intensity is higher. Maintain clear communication even when thinking hard. If stuck, articulate where you're stuck and think aloud. Accept interviewer hints gracefully. Explain optimizations and trade-offs clearly. Show that you can collaborate effectively.
Practice Interview
Study Questions
Hashing and Caching Optimization Techniques
Learn to use hash tables/maps/sets to optimize from nested loops. Understand memoization for dynamic programming problems. Recognize when caching or hashing can improve time complexity from O(n²) to O(n). Practice problems that naturally benefit from these techniques: two sum, first unique character, contains duplicate, valid sudoku.
Practice Interview
Study Questions
Edge Case Identification and Robust Code
Before coding, explicitly identify edge cases: empty input, single element, negative numbers, duplicates, null values, maximum/minimum values. Write code that handles these gracefully. Test with edge cases systematically. For entry-level, showing you think about robustness is crucial.
Practice Interview
Study Questions
Graph Algorithms and Traversals
Understand graph representations (adjacency list, adjacency matrix). Master depth-first search (DFS) and breadth-first search (BFS) traversals. Solve classic graph problems: island count (number of islands), clone graph, course schedule (topological sort), number of connected components, and path finding. Understand when DFS vs. BFS is appropriate.
Practice Interview
Study Questions
Onsite Technical Interview - Coding Round 2
What to Expect
Second onsite coding interview with a different interviewer, 45-60 minutes. Tests breadth and depth of algorithmic knowledge. Problems may be similar difficulty to Round 1 or slightly harder. You might encounter a different problem category (e.g., if Round 1 was graphs, Round 2 might focus on dynamic programming). The purpose is confirming you have solid fundamentals across diverse problem types, not just excelling in one area. Evaluation criteria are identical to Round 1: problem-solving, code quality, communication, and optimization.
Tips & Advice
Apply lessons from Round 1 immediately. Manage energy—by your 4th interview (3rd today), mental fatigue sets in. Take deep breaths, stay focused, maintain intensity. Each interviewer is independent; they don't know your Round 1 performance. Treat this round with fresh energy and focus. Don't carry Round 1 struggles into Round 2—be confident and methodical. Similarly, if Round 1 went well, don't get overconfident; stay thorough and humble. The goal is demonstrating consistency—showing you're reliably strong, not getting lucky once. Use bathroom breaks between interviews to refresh. Drink water and maintain composure.
Focus Topics
Bit Manipulation Basics
Understand bitwise operations: AND, OR, XOR, left shift, right shift. Solve easier bit problems: counting set bits, power of two checks, missing number, single number. These are less common but occasionally appear in Apple interviews, particularly for performance-critical roles.
Practice Interview
Study Questions
Adaptability and Learning from Interviewer Feedback
Throughout the interview, be adaptable. If the interviewer corrects you or suggests a different approach, accept it gracefully without defensiveness. Show that you can learn and adjust in real-time. This demonstrates coachability, a critical trait for junior engineers.
Practice Interview
Study Questions
Recursive Thinking and Backtracking
Master recursive problem decomposition. Clearly identify base cases and recursive cases. Practice backtracking problems: permutations, combinations, generate parentheses, subsets. Learn to recognize when recursion is more elegant than iteration.
Practice Interview
Study Questions
Stack and Queue Data Structures
Master stack and queue basics and applications. Solve problems: valid parentheses, next greater element, trapping rain water (using stack), sliding window maximum (using deque). Understand LIFO and FIFO properties and when each is appropriate.
Practice Interview
Study Questions
Sorting and Searching Algorithms
Understand different sorting algorithms: merge sort, quick sort, heap sort and their time/space complexity. Implement binary search and its variations. Solve problems involving sorted data: finding peak elements, search in rotated sorted array, kth smallest element. Understand when sorting as a preprocessing step is optimal.
Practice Interview
Study Questions
Dynamic Programming Fundamentals
Understand the key DP concept: solving overlapping subproblems by storing results (memoization). Recognize DP problems: these often involve optimization (maximize/minimize) or counting with constraints. Learn classic DP problems: climbing stairs, coin change, longest increasing subsequence, house robber, 0/1 knapsack. Start with 1D DP before 2D.
Practice Interview
Study Questions
Onsite Technical Interview - System Design (Basic)
What to Expect
A 45-60 minute interview focused on basic system design concepts and architectural thinking. For entry-level candidates, this is not about designing global-scale distributed systems but rather understanding how to think about building systems, make trade-offs, and consider scalability. You might be asked to design a simple application or service (e.g., URL shortener, photo storage app, simple notification system, ride-sharing service) at a high level. The interviewer assesses whether you understand basic architectural concepts, ask good clarifying questions, and make reasonable design decisions. Focus is on learning potential and systems thinking, not expert-level knowledge.
Tips & Advice
Start by asking clarifying questions about scope, users, scale (daily active users, requests per second), key features, and constraints. This shows systematic thinking. Propose a basic architecture (frontend, backend, database) and justify your choices. Discuss trade-offs (SQL vs. NoSQL, monolith vs. microservices). Don't aim for perfection; the goal is demonstrating logical thinking about systems. Entry-level candidates are not expected to know all best practices; you're expected to reason through problems thoughtfully. If you make mistakes, the interviewer will guide you—be receptive. Use diagrams (boxes and arrows) to illustrate your design. Discuss key components: load balancing, caching, databases, APIs. Mention scalability concerns even if not going deep. For Apple products, consider Apple's values: simplicity, elegance, privacy, performance. When discussing Apple services, think about these values.
Focus Topics
API Design and Communication Protocols
Understand REST APIs: HTTP methods (GET, POST, PUT, DELETE), status codes (200, 404, 500), endpoint design principles. Know that APIs are interfaces between system components. Discuss API design briefly, focusing on clarity and usability. Understand request-response flow and asynchronous vs. synchronous communication.
Practice Interview
Study Questions
Clarification, Trade-offs, and Design Iteration
Practice asking clarifying questions rather than making assumptions. Explicitly discuss trade-offs (e.g., consistency vs. availability, latency vs. cost, reliability vs. simplicity). Show flexibility in your design; be willing to iterate based on feedback or new information.
Practice Interview
Study Questions
Apple Product Philosophy and Constraints
If the design problem involves Apple products or services, consider Apple-specific constraints and values: simplicity, privacy, performance on edge devices, seamless cross-device experience. Discuss how design decisions align with Apple's philosophy. For example, privacy-first design, minimal data collection, device-local processing where possible.
Practice Interview
Study Questions
Relational vs. Non-Relational Databases
Know when to use relational (SQL) vs. non-relational (NoSQL) databases. SQL is for structured, relational data with ACID guarantees. NoSQL is for unstructured, high-volume data with flexible schemas. Understand examples: PostgreSQL, MySQL for SQL; MongoDB, DynamoDB for NoSQL. Discuss trade-offs: consistency vs. availability, schema flexibility, scalability.
Practice Interview
Study Questions
Scalability and Performance Fundamentals
Understand vertical scaling (bigger, more powerful servers) vs. horizontal scaling (more servers). Know about load balancing to distribute traffic. Discuss caching strategies (in-memory caching, CDNs) to reduce latency and load. Understand that scalability is about supporting growth in users, data, or traffic without degrading performance.
Practice Interview
Study Questions
Basic System Architecture Patterns
Understand the three-tier architecture: presentation layer (frontend), business logic layer (backend), data layer (database). Understand monolithic vs. microservices architectures at a high level. Know client-server models. Be able to draw and explain a basic system diagram with components: frontend, backend, database, external services, and communication flows between them.
Practice Interview
Study Questions
Onsite Behavioral and Collaboration Interview
What to Expect
A 45-60 minute interview focused on behavioral and soft skills. The interviewer assesses teamwork, communication, learning ability, conflict resolution, resilience, and cultural alignment with Apple. You'll be asked questions like: 'Tell me about a time you worked in a team', 'Describe a challenge you faced and how you overcame it', 'How do you handle disagreement with teammates?', 'Tell me about a project you're proud of', 'Describe a mistake you made and what you learned.' This round evaluates passion for product, attention to detail, and alignment with Apple's core values: innovation, simplicity, and excellence. Entry-level candidates are assessed on coachability, humility, collaboration, and learning potential.
Tips & Advice
Prepare 3-5 solid stories using the STAR method: Situation, Task, Action, Result. Stories should showcase teamwork, learning from mistakes, overcoming challenges, taking initiative, attention to detail, and resilience. Practice telling them concisely (2-3 minutes each). Be authentic—don't invent stories; interviewers detect inauthenticity. Listen carefully to questions and answer what's asked, not a memorized script. Show genuine interest in Apple products and engineering culture. Give specific examples, not vague generalities. For entry-level candidates, emphasize learning potential and eagerness to grow. Show humility—you don't know everything and are excited to learn. Discuss how you handle feedback constructively and adapt. Mention teamwork experiences even if from academic or volunteer settings. Be ready for follow-up questions that dig deeper into your stories.
Focus Topics
Clear Communication and Articulation
Throughout the interview, speak clearly, organize thoughts logically, and explain ideas in understandable terms. Use the STAR method for behavioral questions: Situation, Task, Action, Result. Be concise but complete. Listen fully before answering. Ask clarifying questions if needed.
Practice Interview
Study Questions
Handling Challenges and Problem-Solving Resilience
Describe a technical or non-technical challenge you faced: a difficult bug, a tight deadline, a conflict with a teammate, a failed project. Explain the situation clearly, what you did to address it, and what you learned. Show resilience, positive attitude toward challenges, and thoughtful problem-solving.
Practice Interview
Study Questions
Attention to Detail and Quality Focus
Tell stories showcasing meticulousness: catching a subtle bug, improving code readability, enhancing documentation, giving thoughtful code review feedback. Show that you care about quality and correctness, not just shipping quickly.
Practice Interview
Study Questions
Passion for Apple Products and Vision
Articulate what you admire about Apple specifically: product features you love, innovations that impressed you, Apple's approach to design and privacy, or the company's mission. Be specific and genuine. Show that you understand Apple's strategic direction and that your values align.
Practice Interview
Study Questions
Growth Mindset and Learning Ability
Share examples of skills you've learned, mistakes you've recovered from, or challenges you've overcome through persistence. Show that you're driven to improve and actively seek feedback. Discuss how you learn new technologies or programming languages. Emphasize curiosity, willingness to step outside your comfort zone, and eagerness for feedback.
Practice Interview
Study Questions
Teamwork and Effective Collaboration
Prepare stories demonstrating effective collaboration: working on group projects, contributing to team success, supporting teammates, helping others learn. Show that you listen to others, respect diverse perspectives, and work toward common goals. Discuss how you communicate in teams and handle disagreements constructively and respectfully.
Practice Interview
Study Questions
Onsite Domain-Specific Interview
What to Expect
A 45-60 minute interview focused on Apple ecosystem knowledge and domain-specific technology relevant to the specific role. If interviewing for iOS, expect questions about Swift language, UIKit or SwiftUI frameworks, iOS design patterns, and platform-specific considerations. For backend/cloud roles, questions involve server architecture, databases, APIs, and scalability. For macOS, expect macOS-specific frameworks and considerations. This round assesses your familiarity with Apple's technology stack, ability to work within Apple's ecosystem, and understanding of platform-specific constraints (memory on iOS, battery life, performance). For entry-level, you're not expected to be an expert, but demonstrating foundational knowledge and genuine interest is important.
Tips & Advice
If you know the role beforehand (iOS, backend, macOS), dedicate study time to that technology. For iOS: learn Swift fundamentals, UIView hierarchy, view controller lifecycle, and key frameworks. For backend: understand server architecture, databases, APIs, scalability principles. For macOS: similar to iOS but macOS-specific frameworks. Watch Apple's WWDC videos for the relevant platform—they're excellent and show Apple's engineering perspective and direction. If possible, build a small project using the relevant technology and be prepared to discuss it. Don't claim expertise you lack; instead, show genuine interest and eagerness to learn. Entry-level candidates are expected to have foundational knowledge, not expert depth. Discuss your learning journey and projects you've completed. Be ready to explain why you're interested in that particular platform or technology.
Focus Topics
Apple-Specific Technologies and Strategic Direction
Show awareness of unique Apple technologies relevant to your role: on-device machine learning (Core ML), privacy features, augmented reality (ARKit), spatial computing. Understand that Apple prioritizes user privacy and security. Show that you've thought about why these technologies matter to Apple's vision.
Practice Interview
Study Questions
Design Patterns and Best Practices in Apple Development
Understand MVC (Model-View-Controller), MVVM, or MVP architectural patterns at a basic level. Know about delegation and protocols in Swift. Understand code modularity, testability, and reusability. Know about dependency injection concepts. Apple emphasizes clean architecture; code should reflect this from the start.
Practice Interview
Study Questions
Cross-Device Experience and Apple Ecosystem
Understand Apple's ecosystem: iPhone, iPad, Mac, Apple Watch, Apple TV. Many apps run on multiple devices with different screen sizes and capabilities. Discuss how you'd adapt UI/functionality across devices. Understand iCloud synchronization and seamless experiences across Apple products. Appreciate that Apple's strength is the integrated ecosystem.
Practice Interview
Study Questions
Key Apple Frameworks and APIs
For iOS: Foundation (collections, strings, numbers, file I/O), UIKit (UI components), Core Data (persistence), Network (networking), Vision, Core ML. For macOS: Cocoa, AppKit. Understand what each framework provides and when to use it. Know about GCD (Grand Central Dispatch) for concurrency basics. Understand REST APIs and how apps communicate with backends.
Practice Interview
Study Questions
Memory Management and Performance Optimization
Understand ARC (Automatic Reference Counting) and how it manages memory. Know about memory leaks and retain cycles, especially with closures and delegation. For iOS: understand that mobile devices have limited memory and battery compared to servers. Discuss basic optimization: avoiding unnecessary allocations, using lightweight data structures, and understanding performance profiling with Instruments.
Practice Interview
Study Questions
Swift Programming Language and iOS Fundamentals
For iOS roles: master Swift syntax fundamentals, value types vs. reference types (structs vs. classes), optionals and unwrapping, error handling, and closures. Understand UIView hierarchy, view controllers and their lifecycle (viewDidLoad, viewWillAppear, viewDidDisappear), and delegation pattern. Know foundational iOS frameworks: Foundation, UIKit or SwiftUI. Understand Auto Layout or SwiftUI for UI construction. Be able to explain why Apple chose Swift over Objective-C.
Practice Interview
Study Questions
Frequently Asked Software Engineer Interview Questions
Discuss how isKnownUniquelyReferenced helps implement copy-on-write semantics by testing whether a class instance backing a struct is uniquely referenced. Explain potential race conditions if used unsafely in multithreaded contexts and how to mitigate them (locks, actor isolation, atomic operations).
Sample Answer
isKnownUniquelyReferenced(_:) checks at runtime whether a given class instance is the sole strong reference to that instance. In Swift this is the canonical building block for safe copy‑on‑write (COW) of a reference‑type buffer that backs a value type (struct): before mutating the buffer, test uniqueness; if not unique, clone the buffer and mutate the new one so other owners see the original unchanged.
Example COW pattern:
final class Buffer {
var storage: [Int]
init(_ s: [Int]) { storage = s }
func copy() -> Buffer { Buffer(storage) }
}
struct IntArray {
private var box: Buffer
init(_ s: [Int]) { box = Buffer(s) }
mutating func append(_ x: Int) {
if !isKnownUniquelyReferenced(&box) {
box = box.copy() // ensure uniqueness before in-place mutation
}
box.storage.append(x)
}
}
Race conditions: isKnownUniquelyReferenced reads the reference count and can report “unique” while another thread concurrently retains the same instance. If you rely on that result without synchronization, two threads might both observe uniqueness and both mutate the same underlying Buffer — breaking logical isolation and causing data races or memory-safety violations.
Mitigations:
- Locks: Surround the uniqueness check + possible copy + mutation with a mutex. Simple and safe; introduces blocking and potential contention.
- Acquire lock
- if not unique { copy }
- perform mutation
- release lock
- Actor isolation / serial queues: In Swift concurrency, confine the value or its mutating operations to a single actor or serial DispatchQueue. The actor ensures only one concurrent mutating task runs, eliminating race with no manual locks.
- Atomic reference management: Use atomic primitives for reference counting or an atomic “copy-on-write needed” flag (e.g., swift-atomics or UnsafeAtomic) to make the check-and-copy an atomic compare-and-swap sequence. Implementing correct lock‑free COW is hard and error-prone; it requires ensuring atomicity across the check-and-swap and careful memory ordering.
- Design alternatives: Prefer immutable shared instances (no mutation), or make copies eagerly when passing across threads.
Recommendation: For most code, prefer actor isolation or a lightweight lock around the COW path. Reserve atomic, lock‑free implementations for performance‑critical hotspots and validate thoroughly (stress tests, Thread Sanitizer).
After receiving feedback that a UI interaction feels slow or confusing, how would you turn that feedback into a quick measurement or testable change during the session?
Sample Answer
I’d turn vague feedback like “this feels slow” into something measurable right away. First I’d ask whether the issue is perceived latency, actual render time, or too many interaction steps. Then I’d pick one quick metric and one testable change.
For example, if a dropdown feels sluggish, I might measure:
- Time to first visible response
- Number of clicks or keystrokes to complete the task
- Re-render count in the component
A quick change could be adding a loading skeleton, debouncing input, or reducing unnecessary re-renders with memoization. I’d say, “Let’s test whether the user feels progress within 100–200 ms and whether we can reduce the interaction from three steps to two.” That shows I can translate qualitative feedback into a concrete experiment.
When would you reach for a depth-first traversal over level-order for a tree problem, and which DFS order? Compare memory use on a balanced tree versus a skewed one.
Sample Answer
Direct answer
Reach for depth-first search (DFS) when the answer is built from subtrees (heights, sums, validity, paths from the root, copies) or when the tree is shallow (DFS only remembers one root-to-node path, so a short tree needs little memory however wide it is); reach for level-order (breadth-first search, BFS) when the answer depends on distance from the root, such as the shallowest leaf or one result per level. The DFS order is chosen by when you need the parent relative to its children: preorder to use the parent first, inorder for sorted order in a binary search tree (BST), postorder to use the children first. On memory: DFS needs O(h) (h = height), BFS needs O(w) (w = widest level, the largest number of nodes on any single level). On a balanced tree that favours DFS strongly; on a skewed tree it depends on how the tree is skewed.
Height convention: number of nodes on the longest root-to-leaf path (empty tree = 0).
Choosing the DFS order
- Preorder (node, left, right): parent before children. Use it to copy or serialize a tree, to pass information down from the root (the current path, a running sum such as the total of values from the root to the current node, or the depth), and to print a directory-style outline.
- Inorder (left, node, right): on a BST this visits keys in ascending order. Use it for k-th smallest, validating a BST (each key must exceed the previous one), range queries and converting a BST to a sorted list.
- Postorder (left, right, node): children before the parent. Use it whenever a node's answer needs its children's answers: height, diameter (the longest path between any two nodes), subtree sum, balance check (does every node have left and right heights within 1 of each other), evaluating an expression tree (a tree whose leaves are numbers and whose inner nodes are operators, such as
*with children+and 4 for (2 + 3) * 4: both sides must be computed before the operator), and freeing a tree safely (children are released before their parent).
When BFS is the better tool
- The question is about the shallowest or nearest thing: minimum depth, the nearest leaf, or the first node satisfying a condition. BFS stops as soon as it reaches that level; a plain DFS has to look at every branch before it can be sure (a DFS that abandons any branch already deeper than the best leaf found so far does less work, but only after it has found a shallow leaf).
- The output is organised by level: level-order rows, zigzag, right-side view, level averages.
The program below shows the shallowest-leaf case on a tree whose left side is a 500-node chain and whose right child is a leaf. BFS finds the nearest leaf at depth 2 after visiting 3 nodes. A plain DFS that has to rule out the other branch visits 502 nodes to report the same depth.
Memory on balanced versus skewed trees
BFS keeps a frontier: the nodes discovered but not yet processed, which is the unfinished part of the current level plus the children already discovered for the next one, so at most two neighbouring levels (still O(w), where w is the widest level). DFS keeps a path: either recursion frames (always about h) or an explicit stack (whose size depends on the order and the implementation). Measured peaks, counted from the code (an explicit stack counts entries, recursion frames equal the height):
from collections import deque
class Node:
__slots__ = ("val", "left", "right")
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def complete_tree(levels):
"""Perfect tree with `levels` levels: 2**levels - 1 nodes (built level by level, no recursion)."""
root = Node(0)
level, count = [root], 1
for _ in range(levels - 1):
nxt = []
for n in level:
n.left, n.right = Node(count), Node(count + 1)
count += 2
nxt += [n.left, n.right]
level = nxt
return root
def chain_tree(n):
root = cur = Node(0)
for v in range(1, n):
cur.left = Node(v)
cur = cur.left
return root
def bfs_peak(root):
"""Largest number of nodes waiting in the queue."""
queue, peak = deque([root]), 1
while queue:
node = queue.popleft()
for c in (node.left, node.right):
if c:
queue.append(c)
peak = max(peak, len(queue))
return peak
def dfs_peak(root):
"""Largest number of nodes waiting on an explicit preorder stack."""
stack, peak = [root], 1
while stack:
node = stack.pop()
for c in (node.right, node.left):
if c:
stack.append(c)
peak = max(peak, len(stack))
return peak
def inorder_peak(root):
"""Largest explicit-stack size during an iterative inorder walk."""
stack, cur, peak = [], root, 0
while cur or stack:
while cur:
stack.append(cur)
cur = cur.left
peak = max(peak, len(stack))
cur = stack.pop().right
return peak
def caterpillar_tree(spine):
"""A left spine of `spine` nodes where every spine node also has a leaf on its right."""
root = cur = Node(0)
for v in range(1, spine):
cur.right = Node(-v)
cur.left = Node(v)
cur = cur.left
return root
def height(root): # nodes on the longest path = recursion frames for DFS
level, h = [root], 0
while level:
h += 1
level = [c for n in level for c in (n.left, n.right) if c]
return h
def count(root):
stack, n = [root], 0
while stack:
node = stack.pop()
n += 1
stack += [c for c in (node.left, node.right) if c]
return n
print(f"{'shape':20}{'nodes':>6}{'height':>7}{'BFS queue':>10}{'preorder stack':>15}{'inorder stack':>14}")
for name, t in [("perfect, 10 levels", complete_tree(10)),
("left chain", chain_tree(1023)),
("caterpillar", caterpillar_tree(512))]:
print(f"{name:20}{count(t):>6}{height(t):>7}{bfs_peak(t):>10}{dfs_peak(t):>15}{inorder_peak(t):>14}")
# Nearest leaf: root with a 500-node left chain and a leaf on the right
lopsided = Node(0, chain_tree(500), Node(1))
def min_depth_bfs(root):
visited, queue = 0, deque([(root, 1)])
while queue:
node, d = queue.popleft()
visited += 1
if not node.left and not node.right:
return d, visited
for c in (node.left, node.right):
if c:
queue.append((c, d + 1))
def min_depth_dfs(root):
visited, best, stack = 0, None, [(root, 1)]
while stack:
node, d = stack.pop()
visited += 1
if not node.left and not node.right:
best = d if best is None else min(best, d)
for c in (node.right, node.left):
if c:
stack.append((c, d + 1))
return best, visited
print("nearest leaf, BFS (depth, nodes visited):", min_depth_bfs(lopsided))
print("nearest leaf, DFS (depth, nodes visited):", min_depth_dfs(lopsided))
Output:
shape nodes height BFS queue preorder stack inorder stack
perfect, 10 levels 1023 10 512 10 10
left chain 1023 1023 1 1 1023
caterpillar 1023 512 3 512 512
nearest leaf, BFS (depth, nodes visited): (2, 3)
nearest leaf, DFS (depth, nodes visited): (2, 502)
Reading the table, with n = 1023 for each shape:
- Perfect tree (balanced): DFS needs about h = 10 entries; BFS queues up to 512, the width of the last level (about n / 2). DFS uses roughly 50 times less. This is the usual reason to prefer DFS.
- Left chain (skewed, one child each): the tree is 1023 levels deep. Recursive DFS needs 1023 frames (and fails near 1000 in CPython). An iterative inorder walk holds the whole left spine: 1023. A preorder walk with an explicit stack holds 1 entry, because there is never a pending right sibling. BFS holds 1. So here BFS is the lean one.
- Caterpillar (a left spine of 512 nodes, each with a leaf on the right): height 512; preorder and inorder stacks peak at 512, BFS peaks at 3. Again BFS wins.
The same two shapes at 4 and 5 nodes
The two skewed shapes are small enough to follow by hand. The program prints the explicit stack after every visit of an iterative preorder and an iterative inorder walk on a left chain of 4 nodes (1 -> 2 -> 3 -> 4, each the left child of the one before) and on a caterpillar with a 3-node spine (a left spine, with a leaf hanging on the right of each spine node except the last).
class Node:
def __init__(self, val, left=None, right=None):
self.val, self.left, self.right = val, left, right
def preorder_steps(root):
"""Iterative preorder; prints the explicit stack after each node is visited."""
stack, peak = [root], 1
while stack:
node = stack.pop()
for c in (node.right, node.left):
if c:
stack.append(c)
peak = max(peak, len(stack))
print(f" visit {node.val:>2} stack = {[n.val for n in stack]}")
return peak
def inorder_steps(root):
"""Iterative inorder; prints the explicit stack after each pop."""
stack, cur, peak = [], root, 0
while cur or stack:
while cur:
stack.append(cur)
cur = cur.left
peak = max(peak, len(stack))
cur = stack.pop()
print(f" visit {cur.val:>2} stack = {[n.val for n in stack]} (peak so far {peak})")
cur = cur.right
return peak
chain = Node(1, Node(2, Node(3, Node(4))))
caterpillar = Node(0, Node(1, Node(2), Node(-2)), Node(-1))
for name, tree in (("left chain of 4", chain), ("caterpillar with a 3-node spine", caterpillar)):
print(name)
print(" preorder")
p = preorder_steps(tree)
print(" inorder")
i = inorder_steps(tree)
print(f" largest stack: preorder {p}, inorder {i}")
It prints:
left chain of 4
preorder
visit 1 stack = [2]
visit 2 stack = [3]
visit 3 stack = [4]
visit 4 stack = []
inorder
visit 4 stack = [1, 2, 3] (peak so far 4)
visit 3 stack = [1, 2] (peak so far 4)
visit 2 stack = [1] (peak so far 4)
visit 1 stack = [] (peak so far 4)
largest stack: preorder 1, inorder 4
caterpillar with a 3-node spine
preorder
visit 0 stack = [-1, 1]
visit 1 stack = [-1, -2, 2]
visit 2 stack = [-1, -2]
visit -2 stack = [-1]
visit -1 stack = []
inorder
visit 2 stack = [0, 1] (peak so far 3)
visit 1 stack = [0] (peak so far 3)
visit -2 stack = [0] (peak so far 3)
visit 0 stack = [] (peak so far 3)
visit -1 stack = [] (peak so far 3)
largest stack: preorder 3, inorder 3
On the chain, preorder pops a node and pushes its only child, so the stack never holds more than 1 entry. Inorder must go all the way down the left side before it can visit anything: it pushes 1, 2, 3 and 4 and only then visits 4, so the stack reaches 4, the full height. On the caterpillar, preorder pushes the right leaf (-1, then -2) of each spine node before descending, and those leaves wait on the stack until the walk comes back; they pile up to the length of the spine, which at 512 spine nodes is the 512 in the table. BFS holds about one level at a time. The caterpillar has at most 2 nodes per level (a spine node and its leaf), and the queue peaks at 3 because it briefly holds the rest of one level plus the children already queued for the next. That is the 3 in the table.
So "DFS is O(h), BFS is O(w)" is the whole rule, and which one is smaller depends on shape. Deep and thin: BFS is smaller. Wide and shallow: DFS is smaller. A bushy tree of small height favours DFS when DFS keeps one position per level, as recursion does. With many children per node, an explicit stack that pushes all of a node's children at once can hold as many entries as BFS: on a root with 1000 leaf children both hold 1000, while a DFS that keeps one position per level holds 2. A long, narrow tree (list-like) favours BFS, as the chain and caterpillar rows show.
Complexity
All four traversals take O(n) time. Space: DFS O(h) for recursion frames or the explicit stack (worst case O(n) when h = n); BFS O(w) for the queue (worst case about n / 2 for a perfect tree). In Python, recursive DFS raises RecursionError beyond about 1000 levels, so for untrusted or possibly deep trees use an explicit stack.
Common mistakes
- Saying "BFS uses more memory than DFS" without conditions: the chain and caterpillar rows above show it is shape-dependent.
- Using a Python
listwithpop(0)as a BFS queue: O(n) per pop; usecollections.deque. - Picking inorder for a problem on a non-BST tree and expecting sorted output.
- Using a plain DFS for nearest-leaf and forgetting that, without a bound from the best leaf found so far, it must visit every branch to confirm minimality.
Running the code
Save the first listing (the measurements table and the nearest-leaf comparison) as dfs_bfs_demo.py and the second listing (the step-by-step stacks on the 4-node chain and the 3-node-spine caterpillar) as stack_trace_demo.py. Each is a complete program:
python3 dfs_bfs_demo.py
python3 stack_trace_demo.py
Standard library only (Python 3.12).
You have a long, detailed report or analysis and one paragraph of a stakeholder's attention. Condense it into a short executive-style summary that leads with the headline conclusion, the top risk or driver, and a clear recommendation or next step.
Sample Answer
Direct answer
Read for the conclusion the source material is actually building toward, state that conclusion as the first sentence, then compress the two or three things a reader needs to trust it: the biggest risk or driver, and what you want them to do next.
Structured elaboration
- Find the real headline first. Before writing a single summary sentence, identify what decision or fact the full document is ultimately arguing for. If you cannot state it in one sentence, you have not finished reading it.
- Pick the two or three supporting points that matter most, not the ones that are easiest to quote. A common failure is summarizing the document's structure (section 1 covers X, section 2 covers Y) instead of its substance.
- State the risk or the catch. A summary that hides the caveat the full report surfaces on page 8 is not a summary, it is spin. Include the single biggest risk or open question in the same paragraph as the good news.
- End with the ask. What do you want the reader to approve, decide, or do. If there is no ask, say so explicitly ("for awareness only, no action needed") so the reader doesn't hunt for one.
- Cut ruthlessly for length last. Once the four pieces above are on the page, tighten wording, not content: remove hedging phrases ("we believe that," "it seems"), redundant qualifiers, and any sentence that restates something already said.
Worked example
Source material (excerpt of a longer report): a 1,400-word analysis of a marketing campaign covering channel-by-channel spend, a methodology section, a table of conversion rates by week, a note that attribution data for the last two weeks is incomplete, and a recommendation to shift 20% of budget from display to search.
Executive summary (under 80 words): "Recommendation: shift 20% of display budget to search next quarter. Search converts at roughly double the rate of display in this campaign, and the gap has held for six of the eight weeks measured. Caveat: the final two weeks of data are incomplete due to a tracking gap, so we're treating the 2x figure as directional rather than final. Full channel breakdown and methodology in the appendix."
This keeps the conclusion, the one supporting number, the caveat, and the ask, and drops the methodology walkthrough and the week-by-week table, which belong in an appendix a reader can choose to open.
Trade-offs and pitfalls
- The most common failure mode is summarizing evenly across sections instead of unevenly toward the conclusion; a good summary is lopsided on purpose.
- Compressing away a real caveat to make the summary look cleaner is a trust problem, not an editing win. It's the fastest way to have your future summaries doubted.
- For a highly technical audience, "top risk" might be a methodology limitation; for an executive audience it might be a cost or timeline risk. The compression target changes with audience even when the underlying facts don't.
Implement an LSD radix sort for non-negative 32-bit integers using byte-sized digits. Explain why each pass must be stable, why the total is O(n) for fixed word size, and how you would extend it to signed integers and 64-bit keys.
Sample Answer
Direct answer
In short: instead of comparing keys, sort the numbers one byte at a time, lowest byte first, using a cheap counting step each time. LSD (least significant digit first) radix sort sorts integers by splitting each key into fixed-size digits, here 8-bit bytes, and doing one stable counting sort per byte, starting from the lowest byte and ending at the highest. A 32-bit key has four bytes, so four passes. Each pass touches every element a constant number of times, so the total is about 4 * (n + 256) steps, which is O(n) because the word size is fixed. Each pass must be stable (equal digits keep their previous order) because the order produced by the lower bytes is the tie-break for the higher byte. Signed keys need the sign bit handled (add 2^31, which flips the top bit), and 64-bit keys need eight passes instead of four.
The idea in plain words
Think of sorting a pile of numbered cards by the last digit, then by the tens digit, then by the hundreds digit, each time dealing the cards into ten piles and picking them back up in order. After the last deal the pile is fully sorted. This works only if, within one pile of the tens-digit deal, the cards stay in the order the previous deal left them. That "equal keys stay in their incoming order" property is called stability.
Here a "digit" is a byte (256 possible values), so the piles are 256 buckets. Terms: counting sort is a sort for small integer keys that counts how many items fall in each bucket, turns the counts into starting positions, and drops each item into its slot. Fixed word size means every key has the same number of bits (32 or 64), so the number of passes does not grow with n.
Pulling one byte out of a number
A byte is 8 bits, so it holds one of 256 values (0 to 255). Writing a key in hexadecimal (base 16) makes bytes visible: one hex character stands for 4 bits, so two hex characters are exactly one byte. That is why the trace below prints keys as 8 hex characters: 70000 is 00011170, whose four bytes are 00, 01, 11, 70 from highest to lowest.
The expression (key >> shift) & MASK extracts one of those bytes in two steps. key >> shift is a right shift: it slides every bit shift places to the right and drops the bits that fall off the end, which is the same as integer division by 2^shift. & MASK is a bitwise AND with MASK = 255 (binary 11111111): it keeps the low 8 bits and zeroes everything above them. Together they read the byte that starts shift bits up. In the code, 1 << RADIX_BITS is a left shift that computes 2^8 = 256, the number of buckets, and MASK = BUCKETS - 1 is 255. This small program prints the steps for 70000 and shows the signed fix on three keys:
key = 70000
print("70000 in binary:", format(key, "032b"))
print("70000 in hex :", format(key, "08x"), "(two hex characters = one byte = 8 bits)")
for shift in (0, 8, 16, 24):
byte = (key >> shift) & 255
print(f"shift {shift:2d}: key >> {shift:2d} = {key >> shift:5d}; & 255 keeps the low 8 bits = {byte:3d} (hex {byte:02x})")
BIAS = 2**31
for x in (-3, 0, 2**31 - 1):
raw = x & 0xFFFFFFFF # the 32-bit two's complement pattern of x
print(f"x = {x:11d}: raw bits {raw:032b}; after adding 2**31: {x + BIAS:10d} = {x + BIAS:032b}")
Output (python:3.12):
70000 in binary: 00000000000000010001000101110000
70000 in hex : 00011170 (two hex characters = one byte = 8 bits)
shift 0: key >> 0 = 70000; & 255 keeps the low 8 bits = 112 (hex 70)
shift 8: key >> 8 = 273; & 255 keeps the low 8 bits = 17 (hex 11)
shift 16: key >> 16 = 1; & 255 keeps the low 8 bits = 1 (hex 01)
shift 24: key >> 24 = 0; & 255 keeps the low 8 bits = 0 (hex 00)
x = -3: raw bits 11111111111111111111111111111101; after adding 2**31: 2147483645 = 01111111111111111111111111111101
x = 0: raw bits 00000000000000000000000000000000; after adding 2**31: 2147483648 = 10000000000000000000000000000000
x = 2147483647: raw bits 01111111111111111111111111111111; after adding 2**31: 4294967295 = 11111111111111111111111111111111
Reading it: shift 0 gives the low byte 112 (hex 70), shift 8 gives 17 (hex 11), shift 16 gives 1 and shift 24 gives 0, which are the four bytes of 00011170 from lowest to highest. For signed keys, -3 in 32-bit two's complement (the usual machine form of negative numbers) is 111...101, a pattern with the top bit set, so as raw unsigned bits it would sort after every non-negative number. Adding 2^31 turns the top bit off for negatives and on for non-negatives: -3 becomes 2147483645 and 0 becomes 2147483648, so -3 sorts before 0, and 2^31 - 1 becomes 4294967295, the largest 32-bit value. x ^ 0x80000000 (bitwise exclusive-or with the top bit) gives the same result as adding 2^31 when it is applied to the 32-bit pattern, for example (x & 0xFFFFFFFF) ^ 0x80000000. Applied to a negative Python integer itself it stays negative (-3 ^ 0x80000000 is -2147483651), which is why the code adds 2**31 instead.
Brute force and why it is not enough
A comparison sort (merge sort, sorted) needs on the order of n log2 n comparisons, because it learns about the order only by comparing pairs of keys. Radix sort reads the key's bits directly and never compares two keys, so that bound does not apply to it. The price is that it works only for keys that can be viewed as fixed-width digit strings (integers, fixed-length strings, floats after a bit transformation).
Approach
- Convert each key to an unsigned integer (for signed keys, add 2^(bits-1)).
- For shift = 0, 8, 16, 24 (low byte first): count how many keys have each byte value, turn the counts into start positions with a running sum (each digit's start is the total of all smaller digits' counts, so it is the first output slot reserved for that digit), then scan the input from front to back and place each item at
start[digit], incrementing that start position. - Scanning front to back is what makes the pass stable: two items with the same byte land in their slots in the order they were read.
- After the top byte the list is sorted.
Code
radix_sort carries items (so records can be sorted by a key function) and raises an error for a key that does not fit the stated width. The driver prints the array after every pass (the trace), checks stability with tagged equal keys, shows what a back-to-front scan does, and compares against sorted() on random cases.
import random
RADIX_BITS = 8
BUCKETS = 1 << RADIX_BITS # 256 possible digit values
MASK = BUCKETS - 1
def counting_pass(pairs, shift, backwards=False):
"""One stable counting-sort pass on the byte at `shift`. pairs = [(key, item)]."""
count = [0] * BUCKETS
for key, _ in pairs:
count[(key >> shift) & MASK] += 1
start, running = [0] * BUCKETS, 0
for digit in range(BUCKETS): # start[d] = first output slot for digit d
start[digit] = running
running += count[digit]
out = [None] * len(pairs)
# Scanning front to back keeps equal digits in their input order (stable).
for key, item in (reversed(pairs) if backwards else pairs):
digit = (key >> shift) & MASK
out[start[digit]] = (key, item)
start[digit] += 1
return out
def radix_sort(items, key=lambda x: x, bits=32, signed=False, trace=False):
"""Stable LSD radix sort of `items` by an integer key that fits in `bits` bits."""
bias = 1 << (bits - 1) if signed else 0 # shifts signed keys into the unsigned range, negatives first
low, high = -bias, (1 << bits) - bias
pairs = []
for item in items:
k = key(item)
if not low <= k < high:
raise ValueError(f"key {k} does not fit in {bits} bits (signed={signed})")
pairs.append((k + bias, item))
for shift in range(0, bits, RADIX_BITS): # least significant byte first
pairs = counting_pass(pairs, shift)
if trace:
print(f"after byte {shift // RADIX_BITS}:", [f"{k:08x}" for k, _ in pairs])
return [item for _, item in pairs]
if __name__ == "__main__":
data = [170, 45, 75, 90, 802, 24, 2, 66, 70000, 65536, 300]
print("input :", [f"{k:08x}" for k in data])
result = radix_sort(data, trace=True)
print("sorted :", result)
# Stability: records with equal keys must keep their input order.
records = [(5, "a"), (3, "b"), (5, "c"), (3, "d"), (5, "e")]
print("stable :", radix_sort(records, key=lambda r: r[0], bits=8))
for shift_backwards in (False, True): # two passes on 16-bit keys 0x0102, 0x0101
p = counting_pass([(0x0102, "x"), (0x0101, "y")], 0)
p = counting_pass(p, 8, backwards=shift_backwards)
print("high pass scans", "back to front" if shift_backwards else "front to back", "->", [f"{k:04x}" for k, _ in p])
# Signed 32-bit and 64-bit keys.
print("signed :", radix_sort([5, -3, 0, -2147483648, 2147483647, -1], signed=True))
big = [2**64 - 1, 0, 2**40, 2**63, 7]
print("64-bit :", radix_sort(big, bits=64))
try:
radix_sort([2**32], bits=32)
except ValueError as err:
print("rejected :", err)
# Check against sorted() on random inputs, including duplicates and tagged equal keys.
rng = random.Random(41)
for _ in range(300):
n = rng.randint(0, 40)
recs = [(rng.randint(-50, 50), i) for i in range(n)]
assert radix_sort(recs, key=lambda r: r[0], bits=32, signed=True) == sorted(recs, key=lambda r: r[0])
recs64 = [(rng.getrandbits(64), i) for i in range(n)]
assert radix_sort(recs64, key=lambda r: r[0], bits=64) == sorted(recs64, key=lambda r: r[0])
print("300 random cases match sorted() including tie order")
# Cost model: passes d = ceil(bits / r), work per pass about n + 2^r.
for n in (1_000, 1_000_000):
for r in (4, 8, 16):
passes = -(-32 // r)
print(f"n={n:>9,} r={r:2d}: passes={passes}, total work ~ passes * (n + 2^r) = {passes * (n + 2**r):,}")
Output (python:3.12):
input : ['000000aa', '0000002d', '0000004b', '0000005a', '00000322', '00000018', '00000002', '00000042', '00011170', '00010000', '0000012c']
after byte 0: ['00010000', '00000002', '00000018', '00000322', '0000012c', '0000002d', '00000042', '0000004b', '0000005a', '00011170', '000000aa']
after byte 1: ['00010000', '00000002', '00000018', '0000002d', '00000042', '0000004b', '0000005a', '000000aa', '0000012c', '00000322', '00011170']
after byte 2: ['00000002', '00000018', '0000002d', '00000042', '0000004b', '0000005a', '000000aa', '0000012c', '00000322', '00010000', '00011170']
after byte 3: ['00000002', '00000018', '0000002d', '00000042', '0000004b', '0000005a', '000000aa', '0000012c', '00000322', '00010000', '00011170']
sorted : [2, 24, 45, 66, 75, 90, 170, 300, 802, 65536, 70000]
stable : [(3, 'b'), (3, 'd'), (5, 'a'), (5, 'c'), (5, 'e')]
high pass scans front to back -> ['0101', '0102']
high pass scans back to front -> ['0102', '0101']
signed : [-2147483648, -3, -1, 0, 5, 2147483647]
64-bit : [0, 7, 1099511627776, 9223372036854775808, 18446744073709551615]
rejected : key 4294967296 does not fit in 32 bits (signed=False)
300 random cases match sorted() including tie order
n= 1,000 r= 4: passes=8, total work ~ passes * (n + 2^r) = 8,128
n= 1,000 r= 8: passes=4, total work ~ passes * (n + 2^r) = 5,024
n= 1,000 r=16: passes=2, total work ~ passes * (n + 2^r) = 133,072
n=1,000,000 r= 4: passes=8, total work ~ passes * (n + 2^r) = 8,000,128
n=1,000,000 r= 8: passes=4, total work ~ passes * (n + 2^r) = 4,001,024
n=1,000,000 r=16: passes=2, total work ~ passes * (n + 2^r) = 2,131,072
Worked trace
The printed passes read as follows.
- After byte 0 the list is ordered by the lowest byte only:
00010000(low byte00),00000002(02),00000018(18),00000322(22), and so on up to000000aa(aa). - After byte 1 the list is ordered by the lowest two bytes. Ties on byte 1 keep the byte-0 order, which is where stability does its work:
00010000stays ahead of00000002because both have byte 1 equal to00and the previous pass had already put it first. - After byte 2,
00010000and00011170(byte 2 equal to01) move behind every key below 65536. - Byte 3 is
00for every key, so that pass changes nothing, and the final list is[2, 24, 45, 66, 75, 90, 170, 300, 802, 65536, 70000].
Why each pass must be stable
Pass j orders the keys by byte j alone. Two keys with the same byte j look equal to pass j. If the pass shuffled them, the work of passes 0 to j-1, which ordered those two keys by their lower bytes, would be lost. The driver shows it on two 16-bit keys, 0x0102 and 0x0101, which share the high byte 01: after the low-byte pass they are in the right order. A high-byte pass that scans front to back keeps that order (['0101', '0102']), while one that scans back to front prints ['0102', '0101'], which is wrong.
Why the total is O(n) for a fixed word size, and choosing the digit size
One pass costs n steps to count, 2^r steps for the prefix sum, and n steps to place: about n + 2^r up to a constant factor. With b-bit keys and r-bit digits there are d = ceil(b / r) passes, so the total is d * (n + 2^r). For b = 32 and r = 8, d = 4 and the total is 4n + 1,024, which is O(n) because d and 2^r are constants. The factor d is hidden inside that O(n), and the digit size trades passes against table size:
| Digit size r | Passes for 32 bits | Count table | Fits when |
|---|---|---|---|
| 4 bits | 8 | 16 entries | the table must be tiny |
| 8 bits | 4 | 256 entries | small and medium n, the usual default |
| 16 bits | 2 | 65,536 entries | n is much larger than 2^16 |
The last six printed lines evaluate the model. At n = 1,000 the 16-bit choice costs 133,072 modelled steps against 5,024 for 8 bits, because the 65,536-entry table is rebuilt every pass. At n = 1,000,000 it halves the passes and costs 2,131,072 against 4,001,024. The rule from d * (n + 2^r): pick r near log2 of n, capped by what fits comfortably in fast memory. These are modelled step counts from the formula, not measurements.
Signed integers, 64-bit keys, and limited memory
- Signed 32-bit. In two's complement (the standard way machines store negative integers, shown on -3 above), a negative number has its top bit set, so as raw unsigned patterns every negative key sorts after every non-negative one. Adding 2^31 to each key (identical to flipping the top bit,
x ^ 0x80000000in C on auint32_t, an unsigned 32-bit integer type) maps -2^31 to 0 and 2^31 - 1 to 2^32 - 1, in numeric order. The code does this withsigned=True, and the output shows-2147483648first. It needs no extra memory and still takes four passes. A second option is to sort as unsigned and then rotate the output so the block of negatives goes first. - 64-bit. The same algorithm with
bits=64: eight passes with 8-bit digits, or four with 16-bit digits. The output shows18446744073709551615last. In C useuint64_tand keep every shift below 64, because shifting by the type's width or more is undefined behaviour (the C standard places no requirement on what the program then does). - Limited memory. LSD needs a second buffer of n items (two arrays swapped after each pass). If even that is too much, an in-place MSD (most significant digit first, so the top byte is sorted first and each bucket is then sorted by the next byte) variant such as American flag sort permutes items into their buckets directly and recurses per bucket. It is not stable, which matters only when records with equal keys carry other data. A pass can also be skipped when one bucket holds all n keys, for example the top byte in the trace.
Interviews typically stop at the stable four-pass version and the sign-bit fix; lower-memory MSD variants and 64-bit details in C are deeper material that rarely comes up.
Complexity
Time O(d * (n + 2^r)), here O(n) for 32-bit keys. Extra space O(n + 2^r): the shipped code holds the old and new pair lists during a pass plus a 256-entry count table. It is not in place.
Edge cases and common mistakes
- Empty and one-element lists pass through every loop with nothing to do (the random check includes n = 0).
- Duplicates are fine and keep their input order, checked with tagged keys against
sorted(). - A key outside the width is rejected (
2**32withbits=32raised the error shown). - Mistake: scanning the input backwards while using start positions, which reverses ties.
- Mistake: running the passes from the most significant byte first with a stable pass; the low byte then decides last and the order is wrong.
- Mistake: forgetting the sign bit, which puts negatives after positives.
- Mistake: a digit size whose count table is bigger than n, so the table costs more than the data.
Running the code
python:3.12, standard library only
save the code above as radix_sort.py
python radix_sort.py
Write functions to test, set, clear and toggle bit k (0-indexed from the least significant bit) of a 32-bit integer. Decide how to handle out-of-range k, and say what would change if the value lived in a shared hardware status register.
Sample Answer
The idea
Every one of the four operations is a one-bit mask plus one operator. Build the mask with 1u << k, which is a word that has a 1 only at position k (counted from the least significant bit, bit 0). Then:
| Operation | Expression | Why it works |
|---|---|---|
| test | (v >> k) & 1u | shift bit k down to position 0 and mask everything else off |
| set | v | mask | OR with 1 forces the bit on, OR with 0 changes nothing |
| clear | v & ~mask | ~mask is all ones except at k, so AND keeps every other bit |
| toggle | v ^ mask | XOR with 1 flips the bit, XOR with 0 changes nothing |
A small worked example makes the four rows concrete. Take an 8-bit value v = 00101100 (0x2C, decimal 44) and work on bit 4, then bit 3, then bit 5:
| Step | Mask | Computation | Result |
|---|---|---|---|
| test bit 4 | 00010000 | v >> 4 is 00000010; AND with 1 gives the lowest bit, 0 | false (bit 4 is 0) |
| set bit 4 | 00010000 | 00101100 | 00010000 | 00111100 (0x3C) |
| clear bit 3 | ~00001000 = 11110111 | 00101100 & 11110111 | 00100100 (0x24) |
| toggle bit 5 | 00100000 | 00101100 ^ 00100000 | 00001100 (0x0C) |
Each row changes only the bit named by the mask; every other bit passes through unchanged. The 32-bit versions below do exactly the same thing with a wider word.
For several bits at once, use a multi-bit mask: v ^ 0xF flips four bits in one operation, and (v & ~mask) | (value & mask) replaces a whole field.
Out-of-range k
For a 32-bit word the valid values of k are 0 to 31. 1u << 32 is undefined behaviour in C (the standard places no requirement on what the program then does, so the compiler may produce any result, and -fsanitize=undefined reports it), so the check must happen before the shift, not after. The choice to make is what the function does when k is bad. The options are to silently mask k (k & 31, which quietly operates on the wrong bit), to assert (good while developing, absent in release builds), or to report an error. For a library function the safe choice is to report it: mutators return false and leave the value unchanged, and the test returns false. Declaring k unsigned also makes a negative argument wrap to a huge value that the same check rejects. If k is a compile-time constant, a static assertion (_Static_assert, a check the compiler performs while compiling) is better than a runtime check because the mistake cannot ship.
Code
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#define WORD_BITS 32u
/* k is unsigned, so a negative argument becomes a huge value and is rejected too. */
static bool bit_test(uint32_t v, unsigned k) {
return k < WORD_BITS && ((v >> k) & 1u) != 0;
}
static bool bit_set(uint32_t *v, unsigned k) {
if (k >= WORD_BITS) return false;
*v |= 1u << k;
return true;
}
static bool bit_clear(uint32_t *v, unsigned k) {
if (k >= WORD_BITS) return false;
*v &= ~(1u << k);
return true;
}
static bool bit_toggle(uint32_t *v, unsigned k) {
if (k >= WORD_BITS) return false;
*v ^= 1u << k;
return true;
}
/* Several bits at once: flip every bit that is 1 in mask. */
static void bits_toggle_mask(uint32_t *v, uint32_t mask) { *v ^= mask; }
/* Replace a multi-bit field: clear it with the mask, then OR in the new value. */
static void field_write(uint32_t *v, uint32_t mask, uint32_t value) {
*v = (*v & ~mask) | (value & mask);
}
/* Memory-mapped register where writing 1 to a bit clears it (write-one-to-clear),
simulated here by a plain variable. */
static uint32_t sim_status = 0;
static void sim_write_w1c(uint32_t written) { sim_status &= ~written; }
int main(void) {
uint32_t v = 0;
bit_set(&v, 0); bit_set(&v, 31);
printf("set 0 and 31 : 0x%08x\n", v);
printf("test 31, 30 : %d %d\n", bit_test(v, 31), bit_test(v, 30));
bit_toggle(&v, 5);
printf("toggle 5 : 0x%08x\n", v);
bit_clear(&v, 31);
printf("clear 31 : 0x%08x\n", v);
bits_toggle_mask(&v, 0x0000000Fu);
printf("toggle 0xF : 0x%08x\n", v);
uint32_t f = 0xFFFFFFFFu;
field_write(&f, 0x00F0u, 0x5u << 4);
printf("field 7:4 = 5: 0x%08x\n", f);
bool ok = bit_set(&v, 32);
printf("k=32 set ok? : %d, value still 0x%08x\n", ok, v);
printf("k=-1 test : %d\n", bit_test(v, (unsigned)-1));
/* Write-one-to-clear: acknowledge bit 1 while bit 2 is also pending. */
sim_status = 0x6u; /* bits 1 and 2 pending */
uint32_t tmp = sim_status; /* read-modify-write (wrong here) */
tmp &= ~(1u << 1);
sim_write_w1c(tmp); /* writes 1 to bit 2, clearing it */
printf("RMW ack bit 1 -> status 0x%x (bit 1 not acknowledged, bit 2 cleared unseen)\n", sim_status);
sim_status = 0x6u;
sim_write_w1c(1u << 1); /* write only the bit to acknowledge */
printf("direct ack bit 1 -> status 0x%x (bit 2 kept)\n", sim_status);
return 0;
}
Output:
set 0 and 31 : 0x80000001
test 31, 30 : 1 0
toggle 5 : 0x80000021
clear 31 : 0x00000021
toggle 0xF : 0x0000002e
field 7:4 = 5: 0xffffff5f
k=32 set ok? : 0, value still 0x0000002e
k=-1 test : 0
RMW ack bit 1 -> status 0x2 (bit 1 not acknowledged, bit 2 cleared unseen)
direct ack bit 1 -> status 0x4 (bit 2 kept)
Reading the output
- Setting bits 0 and 31 gives
0x80000001. Test 31 is true and test 30 is false. - Toggling bit 5 adds
0x20, giving0x80000021. Clearing 31 leaves0x00000021. Toggling mask0xFflips four bits: bits 0 (was 1, now 0) and 1, 2, 3 (were 0, now 1), giving0x2e. field_writewith mask0x00F0and value 5 shifted into place turns0xffffffffinto0xffffff5f: the four bits 7 to 4 become0101, nothing else moves.k = 32returns false and the value is unchanged.k = -1converts to 4294967295 and is rejected.
If the value lives in a shared hardware status register
A hardware register is a location in memory-mapped I/O (MMIO): the address is wired to a peripheral, not to RAM. The same masks apply, but one fact drives everything that changes: the hardware, an interrupt handler or another processor can read and write that location at the same moment your code does. A read-modify-write is the sequence read the whole word, change one bit in a copy, write the whole word back. Every "set bit k", "clear bit k" and "toggle bit k" above is one of those, and that is what becomes unsafe. Three things follow.
volatile. Declare the pointervolatile uint32_t *. Without it the compiler may cache the value in a CPU register, merge two accesses into one or drop a read whose result is unused, all of which are wrong when reading the location itself has an effect.- Read-modify-write is not atomic.
*reg |= maskis three steps: read, OR, write. If an interrupt service routine (ISR, a function the hardware runs when an event fires) or another bus master (any other device that can write the same register, such as a second CPU core or a DMA controller that copies data without the CPU) changes a different bit between the read and the write, the write puts back a stale value and that change is lost. For example, your code reads0x1, an ISR sets bit 3 so the register holds0x9, and your write of0x1 | 0x2 = 0x3silently turns bit 3 back off. Fixes, in order of preference: use the peripheral's separate set and clear registers if it offers them (one write touches only the bits you name); otherwise disable interrupts around the read-modify-write; for shared plain memory (not MMIO) use an atomic operation (one that completes as a single indivisible step, so no other observer can see it half done) such asatomic_fetch_orfrom<stdatomic.h>, which ORs a mask into a variable atomically. - Write-one-to-clear (W1C) status bits. Many status registers clear a flag when you write a 1 to it, and writing 0 changes nothing. Then the read-modify-write "clear" is dangerous: the demo above has bits 1 and 2 pending, so the register reads
0x6(binary 110). Clearing bit 1 in a temporary gives0x4(binary 100), and writing that back writes a 1 to bit 2, which clears bit 2. So0x6minus the bits named by the write0x4leaves0x2. The output shows that result: status0x2, so bit 2 was cleared without anyone handling it and bit 1 was never acknowledged. Writing only the mask (1u << 1) leaves status0x4, bit 2 intact. For a W1C register the correct "clear bit k" is a single plain write of the mask, and "set" and "toggle" usually do not exist.
Check the peripheral's datasheet for each register's access type (read/write, read-only, W1C, write-only) before choosing the operation.
Complexity and mistakes
Each function is O(1) time and O(1) space: one shift, one mask, one store. Common mistakes: 1 << k with a signed 1 (undefined for k = 31), forgetting the parentheses in (v >> k) & 1u, writing v & mask to "clear" (that keeps the bit), and using || instead of |.
Running the code
gcc -std=c11 -Wall -Wextra -fsanitize=undefined bits.c -o bits && ./bits
Apply equivalence partitioning to the following input field: 'Quantity' accepts integer values between 1 and 1000 inclusive. Identify the equivalence partitions (both valid and invalid), provide representative test values for each partition, and explain why you chose those representative values. Also describe one negative test and one unexpected input test you would include.
Sample Answer
Direct answer
For a 'Quantity' field valid on integers 1 to 1000, there are three equivalence classes: below-range (invalid), in-range (valid), and above-range (invalid). One representative test value per class is sufficient because equivalence partitioning assumes the system treats every value inside a class identically, so testing one member of a class exercises the same code path as testing all of them.
Structured elaboration
- Valid partition: integers 1 through 1000 inclusive. Representative value: 500 (a value comfortably inside the range, not near either edge, so it is not accidentally doubling as a boundary test).
- Invalid partition (below): any value less than 1, including 0 and negative integers. Representative value: -5.
- Invalid partition (above): any value greater than 1000. Representative value: 5000.
Why 500 rather than, say, 1 or 1000: picking a mid-range value keeps EP's purpose distinct from BVA's. If you pick 1 or 1000 as your EP representative you are silently doing boundary testing and calling it equivalence partitioning, which hides the fact that BVA (min-1, min, min+1, max-1, max, max+1) is a separate, necessary pass.
Worked example: negative and unexpected-input tests
- Negative test: submit Quantity = 0. Expected result: the system rejects the value with a validation message (e.g. "Quantity must be at least 1"), not a silent clamp to 1 or a crash. This is a negative test because it is a value inside the representable input space (an integer) but outside the valid business range.
- Unexpected-input test: submit Quantity = "abc" (non-numeric) or Quantity = 3.5 (a decimal where an integer is required). Expected result: the field rejects non-integer input before it ever reaches the range check, distinct from the range-validation error message above. This matters because a form that only guards the numeric range but not the type can crash downstream (e.g. a database insert expecting an INT) when handed a string or float.
Trade-offs & pitfalls
The most common mistake with EP is treating the invalid-below and invalid-above partitions as one "invalid" bucket and testing only one of them; they are separate classes because the failure code path (or business meaning, e.g. "quantity too small to fulfill" vs. "exceeds available stock") can legitimately differ. A second pitfall is stopping at EP and assuming the three representative values are a complete test set: EP tells you which classes exist, not where the class boundaries actually break, which is exactly the gap BVA fills.
Count how many distinct subsequences of s equal t, modulo 1e9+7. For example s = 'rabbbit' and t = 'rabbit' gives 3. Define the state and recurrence and optimize the space.
Sample Answer
The idea
A subsequence of s keeps some of its characters, in their original order, and deletes the rest. Two selections count as different if they use different positions, even when the letters they spell are the same. In rabbbit the target rabbit can be spelled by three different position sets, because any two of the three bs can serve as the two bs of the target.
Look at the last character of the part of s you are considering. Either it is not used, and then the count equals the count for the shorter s; or it is used as the last character of the target part, which is only possible when the two characters match, and then the rest of the target is matched inside the shorter s. Add the two cases.
The five things to state
- State.
(i, j): the first i characters of s and the first j characters of t. - What one cell means.
ways[i][j]is the number of different index sets ins[:i]whose characters, read in order, spell exactlyt[:j]. - Recurrence.
ways[i][j] = ways[i-1][j], plusways[i-1][j-1]whens[i-1] == t[j-1]. Reduce modulo 10^9+7 after each addition. - Base cases.
ways[i][0] = 1for every i (spell the empty string by picking nothing) andways[0][j] = 0for j > 0 (no characters left to spell something non-empty). - Fill order. i ascending, and j ascending within the row in the full table, since cell (i, j) reads row i - 1 only. The answer is
ways[m][n].
Brute force first
Trying every index set of size n in s means C(m, n) candidates. The plain recursion go(i, j) branches twice whenever the characters match, and the program counts its calls, root included: 1 call for two empty strings, 1 for ("a", ""), 1 for ("", "a"), 6 for ("ab", "ab"), 52 for rabbbit and rabbit, 1,275 for ten as against five and 78,405 for sixteen as against eight. The table has only (m+1)(n+1) cells (56, 66 and 153 for the last three), because the recursion keeps revisiting the same (i, j).
The code
import random
from itertools import combinations
from math import comb
MOD = 10**9 + 7
def count_table(s, t, mod=None):
"""ways[i][j] = number of index sets in s[:i] whose characters spell t[:j]."""
m, n = len(s), len(t)
ways = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
ways[i][0] = 1 # the empty target: pick nothing
for i in range(1, m + 1):
for j in range(1, n + 1):
ways[i][j] = ways[i - 1][j] # leave s[i-1] unused
if s[i - 1] == t[j - 1]:
ways[i][j] += ways[i - 1][j - 1] # or use s[i-1] as t[j-1]
if mod:
ways[i][j] %= mod
return ways
def count_rolling(s, t, mod=None, ascending=False, show=False):
"""One row: ways[j] for the prefix of s seen so far. j must go DOWNWARDS."""
n = len(t)
ways = [1] + [0] * n
if show:
print("start ", ways)
for ch in s:
js = range(1, n + 1) if ascending else range(n, 0, -1)
for j in js:
if ch == t[j - 1]:
ways[j] += ways[j - 1]
if mod:
ways[j] %= mod
if show:
print(f"after {ch!r:5} ", ways)
return ways[n]
def count_recursive(s, t):
"""Plain recursion on suffixes; returns (count, calls with the root counted)."""
calls = 0
def go(i, j):
nonlocal calls
calls += 1
if j == len(t):
return 1
if i == len(s):
return 0
total = go(i + 1, j)
if s[i] == t[j]:
total += go(i + 1, j + 1)
return total
return go(0, 0), calls
def count_brute(s, t):
return sum("".join(s[i] for i in idx) == t
for r in range(len(t), len(t) + 1) for idx in combinations(range(len(s)), r))
s, t = "rabbbit", "rabbit"
table = count_table(s, t)
print("table for s = rabbbit, t = rabbit (rows: prefixes of s, columns: prefixes of t)")
print(" " + "".join(f"{c:>3}" for c in " " + t))
for i, row in enumerate(table):
print(f"{('' if i == 0 else s[i - 1]):>2} {i}: " + "".join(f"{v:>3}" for v in row))
print("answer:", table[-1][-1])
print("rolling row:")
print("answer:", count_rolling(s, t, show=True))
print("same input with j ascending (wrong):", count_rolling(s, t, ascending=True))
print("s t count recursion_calls (m+1)*(n+1)")
for s2, t2 in (("", ""), ("a", ""), ("", "a"), ("ab", "ab"), ("rabbbit", "rabbit"),
("aaaaaaaaaa", "aaaaa"), ("a" * 16, "a" * 8)):
value, calls = count_recursive(s2, t2)
print(f"{s2:10s} {t2:8s} {value:5d} {calls:15d} {(len(s2) + 1) * (len(t2) + 1)}")
big_s, big_t = "a" * 60, "a" * 30
exact = count_rolling(big_s, big_t)
print("60 a's, target 30 a's: exact", exact, "= C(60, 30):", exact == comb(60, 30),
"| mod 1e9+7:", count_rolling(big_s, big_t, MOD), "|", exact % MOD)
random.seed(64)
for _ in range(300):
m, n = random.randint(0, 11), random.randint(0, 5)
s2 = "".join(random.choice("ab") for _ in range(m))
t2 = "".join(random.choice("ab") for _ in range(n))
expected = count_brute(s2, t2)
assert count_table(s2, t2)[m][n] == count_rolling(s2, t2) == count_recursive(s2, t2)[0] == expected, (s2, t2)
assert count_rolling(s2, t2, MOD) == expected % MOD
print("300 seeded pairs (s of length 0..11, t of length 0..5, alphabet a/b): table, rolling row and recursion equal brute force")
Output:
table for s = rabbbit, t = rabbit (rows: prefixes of s, columns: prefixes of t)
r a b b i t
0: 1 0 0 0 0 0 0
r 1: 1 1 0 0 0 0 0
a 2: 1 1 1 0 0 0 0
b 3: 1 1 1 1 0 0 0
b 4: 1 1 1 2 1 0 0
b 5: 1 1 1 3 3 0 0
i 6: 1 1 1 3 3 3 0
t 7: 1 1 1 3 3 3 3
answer: 3
rolling row:
start [1, 0, 0, 0, 0, 0, 0]
after 'r' [1, 1, 0, 0, 0, 0, 0]
after 'a' [1, 1, 1, 0, 0, 0, 0]
after 'b' [1, 1, 1, 1, 0, 0, 0]
after 'b' [1, 1, 1, 2, 1, 0, 0]
after 'b' [1, 1, 1, 3, 3, 0, 0]
after 'i' [1, 1, 1, 3, 3, 3, 0]
after 't' [1, 1, 1, 3, 3, 3, 3]
answer: 3
same input with j ascending (wrong): 6
s t count recursion_calls (m+1)*(n+1)
1 1 1
a 1 1 2
a 0 1 2
ab ab 1 6 9
rabbbit rabbit 3 52 56
aaaaaaaaaa aaaaa 252 1275 66
aaaaaaaaaaaaaaaa aaaaaaaa 12870 78405 153
60 a's, target 30 a's: exact 118264581564861424 = C(60, 30): True | mod 1e9+7: 737009364 | 737009364
300 seeded pairs (s of length 0..11, t of length 0..5, alphabet a/b): table, rolling row and recursion equal brute force
Reading the output
- The first table is the real
waystable forrabbbitandrabbit. Rowb 3ends in 1 at the column forrab, rowb 4has 2 there, and rowb 5has 3:rabbbcontainsrabin three ways (the threebs). The last row, last column is the answer 3. - Column
rabb(j = 4) climbs 0, 1, 3 over the threebs, then stays at 3 through theiand thet: 1 way after the secondb, 3 after the third. These are the 3 pairs ofbs that can form thebbof the target. - The rolling run prints the single row after every character of s, and ends at the same 3. In that version row i overwrites row i - 1 in place. The inner loop must go from j = n down to 1, so that
ways[j-1]still holds the value from before this character. With the loop ascending, the same input prints 6 instead of 3, because eachbfirst updatesways[3]and thenways[4]reads that fresh value, so a single character is used for two positions of the target. - Sixty
as against thirtyas: the exact count is 118264581564861424, equal tocomb(60, 30), and reduced modulo 10^9+7 it is 737009364, the value the modular run also returns. This is why the modulus is needed: counts grow like binomial coefficients. - The seeded test draws 300 pairs with s of length 0 to 11 and t of length 0 to 5 over the letters
aandb(so empty s, empty t, and t longer than s all occur) and checks the table, the rolling row and the recursion against a brute force over index sets, and the modular run against the exact value reduced.
Complexity
- Time. O(m * n): (m + 1)(n + 1) cells in the table, one addition each.
- Space. O(m * n) for the full table, O(n) for the rolling row (a list of n + 1 numbers). The recursion uses O(m) stack but takes exponential time, so it is not used.
- If n is greater than m, the answer is 0, which the loops give without special handling (covered in the random test).
Edge cases and common mistakes
- Empty t returns 1 for any s (including empty s), and empty s with non-empty t returns 0; both appear in the output.
- Looping j upward in the one-row version, shown above (6 instead of 3).
- Forgetting the modulus when counts get large; the exact value for 60 and 30 is 18 digits long. In a fixed-width language the unreduced value would overflow a 64-bit integer once it exceeds 9.22 * 10^18, so reduce at every addition.
Running the code
python3.12 distinct_subsequences.py
Standard library only. Save the listing as distinct_subsequences.py and run the command above; it prints exactly the output shown.
Write the SQL for keyset pagination against a table posts(id BIGINT PRIMARY KEY, created_at TIMESTAMP, title TEXT), returning the next 50 posts after a given cursor of (last_created_at, last_id). Say which index supports this query efficiently, and how you break ties when created_at is not unique across rows.
Sample Answer
Direct answer. Order by (created_at, id) and filter for rows strictly greater than the cursor's (created_at, id) pair, using a compound comparison so ties on created_at are broken by id rather than skipping or duplicating rows.
The query, against posts(id BIGINT PRIMARY KEY, created_at TIMESTAMP, title TEXT):
SELECT id, created_at, title
FROM posts
WHERE (created_at, id) > (:last_created_at, :last_id)
ORDER BY created_at ASC, id ASC
LIMIT 50;
The index. A composite index on (created_at, id) is what turns this into a single ordered index range scan instead of a sort-then-filter: CREATE INDEX idx_posts_created_at_id ON posts (created_at, id);. Without it, the database can still produce correct results, just by sorting the whole table first, which is exactly the cost keyset pagination exists to avoid.
Handling ties. Two posts inserted in the same second will have identical created_at values. The compound comparison (created_at, id) > (:last_created_at, :last_id) is what makes this safe: a row with the SAME created_at as the cursor but a LARGER id is still correctly included on the next page, and a row with the same created_at and a smaller-or-equal id is correctly excluded, so no row is skipped or returned twice even when many rows share one timestamp. Ordering by created_at alone, without id as a tiebreaker, would make the result of paging through tied rows depend on the database's arbitrary internal ordering, which can differ between two runs of the exact same query.
Verified with a small SQLite database (the query shape and the index it relies on are identical for Postgres): 120 rows seeded, three of them (ids 50, 51, 52) sharing one timestamp on purpose to exercise the tie-break.
import sqlite3
from datetime import datetime, timedelta
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE posts (id INTEGER PRIMARY KEY, created_at TEXT NOT NULL, title TEXT NOT NULL)")
conn.execute("CREATE INDEX idx_posts_created_at_id ON posts (created_at, id)")
base_time = datetime(2026, 1, 1, 0, 0, 0)
rows = []
for i in range(1, 121):
ts = base_time + timedelta(minutes=50) if i in (50, 51, 52) else base_time + timedelta(minutes=i)
rows.append((i, ts.isoformat(), f"Post {i}"))
conn.executemany("INSERT INTO posts (id, created_at, title) VALUES (?, ?, ?)", rows)
conn.commit()
def next_page(conn, last_created_at, last_id, page_size=50):
cur = conn.execute(
"SELECT id, created_at, title FROM posts WHERE (created_at, id) > (?, ?) "
"ORDER BY created_at ASC, id ASC LIMIT ?",
(last_created_at, last_id, page_size),
)
return cur.fetchall()
page1 = next_page(conn, "", 0, page_size=50)
last_id, last_created_at, _ = page1[-1]
page2 = next_page(conn, last_created_at, last_id, page_size=50)
last_id, last_created_at, _ = page2[-1]
page3 = next_page(conn, last_created_at, last_id, page_size=50)
all_ids = [r[0] for r in page1] + [r[0] for r in page2] + [r[0] for r in page3]
print(f"page 1: {len(page1)} rows, ids {page1[0][0]}..{page1[-1][0]}")
print(f"page 2: {len(page2)} rows, ids {page2[0][0]}..{page2[-1][0]}")
print(f"page 3: {len(page3)} rows, ids {page3[0][0]}..{page3[-1][0]}")
print("total rows returned:", len(all_ids))
print("no duplicates:", len(all_ids) == len(set(all_ids)))
print("every id 1..120 returned exactly once:", sorted(all_ids) == list(range(1, 121)))
Output (actually run):
page 1: 50 rows, ids 1..50
page 2: 50 rows, ids 51..100
page 3: 20 rows, ids 101..120
total rows returned: 120
no duplicates: True
every id 1..120 returned exactly once: True
Complexity. With the composite index, each page fetch is an index range scan of roughly O(page_size + log n) rather than an O(n) or O(offset) scan, and that cost does not grow with how many pages the client has already fetched.
Edge cases. A row inserted with a timestamp that ties the CURRENT cursor value but a lower id than the cursor's last_id is correctly excluded (it would have already been included on the current page if it existed at fetch time); a row deleted after being handed to the client simply does not reappear, since nothing about the query depends on that row still existing.
The DRY principle says don't repeat yourself, but removing duplication can sometimes make code worse. When is a forced abstraction (the 'wrong abstraction') more costly than the duplication it replaced, and how do you tell the difference in the moment?
Sample Answer
Direct answer. DRY is right for logic that represents ONE decision that should always change together; it's wrong when the forced abstraction merges things that only look similar today but represent DIFFERENT decisions that happen to coincide -- the 'wrong abstraction' costs more than the duplication it replaced, because a bad shared abstraction is often harder to safely split apart than duplication is to later consolidate.
When abstraction beats duplication
Three services independently implementing the SAME tax-rounding rule: this is one business decision (how does the company round tax) expressed three times. A rule change means finding and fixing three places, with real risk of missing one -- extracting a shared round_tax() function is the right call here because all three copies are genuinely the same concept and should change in lockstep.
When the 'wrong abstraction' is worse
Two onboarding flows for different customer segments both validate a phone number with the SAME regex today. It's tempting to extract validate_phone() and use it in both. Six months later, one segment needs to support international formats and the other doesn't -- now the shared function needs a parameter (allow_international: bool), then another distinction, then another, until the 'shared' function is a tangle of flags serving two purposes that were never actually the same thing. At that point you're paying MORE cognitive cost reading the merged function's branches than you would have paid maintaining two small, independent, honestly-different validators.
How to tell the difference in the moment
- Ask: 'if this changes, is there a REASON both callers would need the same change?' If the reason is coincidental (they both happen to use a US-only regex today, for unrelated reasons), that's a signal to keep them separate.
- Watch for a shared function accumulating boolean flags or optional parameters that only ONE caller ever sets -- that's the tell-tale sign an abstraction is being stretched to cover cases it wasn't designed for.
- If you're not sure yet (early in a project, requirements still unclear), tolerating duplication for a while costs little and preserves the option to extract correctly later, once real usage reveals which parts genuinely need to move together.
Recovering from a bad abstraction
When you notice a shared function has accumulated flags for divergent needs, the fix is usually to SPLIT it back into purpose-specific functions (possibly still sharing a smaller, GENUINELY common piece), not to add a third flag on top of the first two.
Trade-offs and pitfalls
- DRY applied to superficial text similarity rather than conceptual identity (two unrelated pieces of code that happen to both loop over a list and sum a field) is a common over-application -- the sameness has to be about MEANING, not appearance.
- Recognize that 'we already extracted this, so let's keep using it even though it doesn't quite fit' is a sunk-cost trap; a growing flag list on a shared function is a stronger signal to split than to add one more flag.
Recommended Additional Resources
- LeetCode (leetcode.com): Practice algorithmic coding problems with filters for difficulty and topic
- HackerRank (hackerrank.com): Coding challenges and structured interview preparation
- Cracking the Coding Interview by Gayle Laakmann McDowell: Essential comprehensive book for technical interview preparation
- Swift Official Documentation (developer.apple.com): Learn Swift language fundamentals and best practices
- Apple's WWDC Videos (developer.apple.com/wwdc): Watch platform-specific technical talks to understand Apple's engineering philosophy
- Blind (teamblind.com): Read actual anonymous interview experiences and questions from Apple
- Levels.fyi: Research Apple compensation, interview processes, and company culture insights
- System Design Interviews on YouTube: Watch walkthroughs and approaches to system design problems
- CodeSignal (codesignal.com) and Interviewing.io: Practice mock interviews with real engineers
- Glassdoor Apple Interview Questions: Review actual questions asked by past candidates
- InterviewQuery Apple Interview Guide: Comprehensive guide with example problems
- UIKit/SwiftUI Documentation: For iOS roles, study framework documentation with examples
Search Results
Apple's Interview Process (2025) | TechPrep
The interview process at Apple is designed to rigorously assess candidates' technical skills, problem-solving abilities, and cultural fit within ...
Apple Software Engineer Interview Guide 2025 — Process, Coding ...
Master the Apple software engineer interview with our 2025 guide: detailed hiring stages, senior-level insights, and 10+ real coding ...
Apple ICT2 Software Engineer 2025 Interview Questions - Onsites.fyi
Interview Process Overview · 2 Coding Rounds · 1 System Design · 1 Behavioral/Collaboration · 1 Domain-Specific Round ...
Apple Interview Process & Timeline (7 steps to getting an offer)
If you're applying for the software engineer role, you can expect up to 6 rounds of onsite interviews. They can either be virtual or in-person.
Apple Software Engineer Interview Experience - Cupertino, California
Apple's interview process for their Software Engineer roles in Cupertino, California is very selective, failing most engineers who go through it ...
Apple Software Engineer (SWE) Interview Guide - Exponent
Apple's SWE interview loop consists mainly of technical and behavioral questions, as well as systems design and algorithmic / coding questions.
Senior Engineer's Guide to Apple Interviews + Questions
The recruiter spends 30 minutes or an hour per debrief where engineers are talking about the details about the code. So if the recruiter is paying attention or ...
This interview preparation guide was generated using AI-powered research from the sources listed above. While we strive for accuracy, we recommend verifying critical information from official company sources.
Want to create your own tailored preparation guide using our deep research?
Get Started for FreeInterview-Ready Courses
Visual-first, interactive, structured learning paths
Browse Software Engineer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs