Design a deck of cards with shuffle/draw

Quick Overview

This question evaluates object-oriented design skills, understanding of randomness and probability in algorithms, and analysis of time/space complexity for data structures and operations.

Design a deck of cards with shuffle/draw

Company: Apple

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Technical Screen

## Object-Oriented Design + Randomness: A Deck of Cards Design an in-memory model of a standard 52-card playing deck. Your design will be exercised by client code that builds a fresh deck, shuffles it, and draws cards one at a time until the deck is exhausted. Create classes to represent at least: - A **card** holding a rank (Ace through King) and a suit (Clubs, Diamonds, Hearts, Spades). - A **deck** that owns a collection of cards. The deck must support two operations: 1. `shuffle()` — randomizes the order of the cards currently remaining in the deck. 2. `draw()` — removes one card from the deck and returns it. Your design must also satisfy two correctness/probability requirements: - After `shuffle()`, every ordering (permutation) of the remaining cards is equally likely — i.e., the shuffle is **unbiased**. - Each `draw()` returns each currently-remaining card with **equal probability** at that moment. Walk through your class design, justify your data-structure choice, give the algorithm for `shuffle()` and `draw()`, argue why the probability requirements hold, and state the time/space complexity of each operation. Sketch the core methods in your language of choice. ```hint Where to start Separate the *value* (an immutable card) from the *container* (a mutable deck). Build the full 52-card deck by taking the cross product of the 4 suits and 13 ranks. Pick the deck's internal representation by asking which operation has to be fast: removing a card. ``` ```hint Data structure A dynamic array / list backs both operations cheaply if you treat one *end* as the "top." Removing from the end (pop) is $O(1)$ amortized; removing from the front forces an $O(n)$ shift. Avoid a structure (like a singly linked list) that makes random index access $O(n)$ — the shuffle needs random indexing. ``` ```hint Unbiased shuffle The standard correct algorithm is the **Fisher–Yates (Knuth) shuffle**: iterate $i$ from $n-1$ down to $1$, pick $j$ uniformly in $[0, i]$, and swap positions $i$ and $j$. Beware the two classic traps — picking $j$ from the full range $[0, n-1]$ on every step, or "sort by a random key" — both produce biased orderings. ``` ```hint Uniform draw without relying on shuffle If you want `draw()` to be uniform on its own (not depending on a prior shuffle), pick a random index $k \in [0, n-1]$, swap that card with the last card, then pop the last. This keeps `draw()` at $O(1)$ and never invalidates the deck. ``` ### Constraints & Assumptions - A standard deck has exactly 52 distinct cards (4 suits × 13 ranks); no jokers unless you choose to support them. - The deck holds no duplicates; a drawn card is gone until the deck is rebuilt/reset. - A good pseudo-random number generator is available (e.g., a `Random`-style API that yields a uniform integer in a half-open range). - The deck is small and fully in memory; persistence, networking, and multi-deck shoes are out of scope unless you raise them. ### Clarifying Questions to Ask - Should `shuffle()` shuffle only the *remaining* cards, or always reset to a full 52 and then shuffle? - What is the desired behavior of `draw()` on an empty deck — exception, sentinel/`null`, or an `Optional`-style empty value? - Does the deck need to be thread-safe (drawn from multiple threads concurrently), or is single-threaded use acceptable? - Should the PRNG be injectable (for deterministic, seedable tests), or is a built-in default acceptable? - Are duplicate cards or multi-deck "shoes" (e.g., blackjack's 6-deck shoe) ever required? - Do we need card equality/ordering (e.g., for comparing hands), or only identity and printing? ### What a Strong Answer Covers - **Clean class decomposition:** an immutable card value object (rank + suit, with sensible equality and a readable string form) and a deck that owns the card collection plus its randomness source. - **Justified data structure:** a list/array with one end as the "top," explaining why pop-from-end gives $O(1)$ draw and supports in-place shuffling, and why other choices are worse. - **Correct unbiased shuffle:** Fisher–Yates stated precisely, with a probability argument that all $n!$ orderings are equally likely, and explicit mention of the biased anti-patterns to avoid. - **Correct uniform draw:** either "draw the top after an unbiased shuffle" with a uniformity argument, or the random-index swap-and-pop technique that is uniform without a prior shuffle. - **Complexity targets:** $O(n)$ time / $O(1)$ extra space for `shuffle()`; $O(1)$ time and space for `draw()`. - **Edge cases & API design:** explicit, documented behavior for an empty deck; testability via an injectable PRNG; awareness of thread-safety and secure-randomness trade-offs raised at the right altitude. ### Follow-up Questions - How would you unit-test that `shuffle()` is unbiased and `draw()` is uniform, given that the output is random? (Hint: seed the PRNG for determinism; use a chi-square / frequency test over many trials for the distribution.) - Extend the design to a multi-deck "shoe" (e.g., 6 decks) and a "burn/discard" pile, then a `reshuffle()` that folds the discards back in. What changes? - If two players draw concurrently from the same deck, how do you guarantee no card is dealt twice and the draw stays uniform? Compare a coarse lock vs. a single-threaded dealer. - Suppose you must support an "infinite" stream of uniformly random cards with replacement (a card can repeat). How does the data structure and `draw()` change?

Overview: This question evaluates object-oriented design skills, understanding of randomness and probability in algorithms, and analysis of time/space complexity for data structures and operations.

|Home/Software Engineering Fundamentals/Apple
Apple logo
Apple
Mar 1, 2026
mediumSoftware EngineerTechnical ScreenSoftware Engineering Fundamentals
54
0

Object-Oriented Design + Randomness: A Deck of Cards

Design an in-memory model of a standard 52-card playing deck. Your design will be exercised by client code that builds a fresh deck, shuffles it, and draws cards one at a time until the deck is exhausted.

Create classes to represent at least:

  • A card holding a rank (Ace through King) and a suit (Clubs, Diamonds, Hearts, Spades).
  • A deck that owns a collection of cards.

The deck must support two operations:

  1. shuffle() — randomizes the order of the cards currently remaining in the deck.
  2. draw() — removes one card from the deck and returns it.

Your design must also satisfy two correctness/probability requirements:

  • After shuffle() , every ordering (permutation) of the remaining cards is equally likely — i.e., the shuffle is unbiased .
  • Each draw() returns each currently-remaining card with equal probability at that moment.

Walk through your class design, justify your data-structure choice, give the algorithm for shuffle() and draw(), argue why the probability requirements hold, and state the time/space complexity of each operation. Sketch the core methods in your language of choice.

Constraints & Assumptions

  • A standard deck has exactly 52 distinct cards (4 suits × 13 ranks); no jokers unless you choose to support them.
  • The deck holds no duplicates; a drawn card is gone until the deck is rebuilt/reset.
  • A good pseudo-random number generator is available (e.g., a Random -style API that yields a uniform integer in a half-open range).
  • The deck is small and fully in memory; persistence, networking, and multi-deck shoes are out of scope unless you raise them.

Clarifying Questions to Ask Guidance

  • Should shuffle() shuffle only the remaining cards, or always reset to a full 52 and then shuffle?
  • What is the desired behavior of draw() on an empty deck — exception, sentinel/ null , or an Optional -style empty value?
  • Does the deck need to be thread-safe (drawn from multiple threads concurrently), or is single-threaded use acceptable?
  • Should the PRNG be injectable (for deterministic, seedable tests), or is a built-in default acceptable?
  • Are duplicate cards or multi-deck "shoes" (e.g., blackjack's 6-deck shoe) ever required?
  • Do we need card equality/ordering (e.g., for comparing hands), or only identity and printing?

What a Strong Answer Covers Guidance

  • Clean class decomposition: an immutable card value object (rank + suit, with sensible equality and a readable string form) and a deck that owns the card collection plus its randomness source.
  • Justified data structure: a list/array with one end as the "top," explaining why pop-from-end gives O(1)O(1) draw and supports in-place shuffling, and why other choices are worse.
  • Correct unbiased shuffle: Fisher–Yates stated precisely, with a probability argument that all n!n! orderings are equally likely, and explicit mention of the biased anti-patterns to avoid.
  • Correct uniform draw: either "draw the top after an unbiased shuffle" with a uniformity argument, or the random-index swap-and-pop technique that is uniform without a prior shuffle.
  • Complexity targets: O(n)O(n) time / O(1)O(1) extra space for shuffle() ; O(1)O(1) time and space for draw() .
  • Edge cases & API design: explicit, documented behavior for an empty deck; testability via an injectable PRNG; awareness of thread-safety and secure-randomness trade-offs raised at the right altitude.

Follow-up Questions Guidance

  • How would you unit-test that shuffle() is unbiased and draw() is uniform, given that the output is random? (Hint: seed the PRNG for determinism; use a chi-square / frequency test over many trials for the distribution.)
  • Extend the design to a multi-deck "shoe" (e.g., 6 decks) and a "burn/discard" pile, then a reshuffle() that folds the discards back in. What changes?
  • If two players draw concurrently from the same deck, how do you guarantee no card is dealt twice and the draw stays uniform? Compare a coarse lock vs. a single-threaded dealer.
  • Suppose you must support an "infinite" stream of uniformly random cards with replacement (a card can repeat). How does the data structure and draw() change?
Loading comments...