Each user has two HashSets:
following and followers. To check "Does Priya follow Ravi?" — O(1) lookup in Ravi's followers HashSet. They spend 2× the memory so the check is instant for 1 billion users.
Every structure costs you either time or space. Picking the wrong one turns a 1ms operation into a 10-second freeze.
Your phone has 500 contacts. You type "P" and in 0.2 seconds you see Priya, Pooja, Praveen. How does it find them so fast? It doesn't read all 500 names one by one — that would take 500 steps.
Now imagine WhatsApp receives a call from +91 98765 43210. It needs to show you whose number this is before you pick up. To do that instantly, it keeps a second copy of your contacts mapped by phone number — same data stored twice. That's 2× the memory, but the answer is instant.
This is the core trade-off of every data structure: you pay with time or you pay with space. Rarely both for free.
How many operations does it take to find, add, or delete an item?
O(1) = 1 step always. Instant. (HashMap lookup)
O(log n) = 10 steps for 1000 items. Fast. (Binary search)
O(n) = 1000 steps for 1000 items. Slow. (Linear scan)
How much extra memory does your data structure need beyond the raw data?
O(1) = no extra memory. (ArrayList — just the array)
O(n) = extra memory proportional to items. (HashMap — needs hash table overhead)
O(2n) = double your data. (Two HashMaps for bidirectional lookup)
Same data: 500 contacts (name + phone). Four different structures. Wildly different results.
Store contacts in a list. To find "Priya", start from index 0, check every name until you hit it.
⚠ Works fine for 50 contacts. For 5,000,000 contacts (like WhatsApp), this is 5 million checks per search. The app freezes.
Sort contacts A–Z. To find "Priya": open the middle (M), "P" is after M, open the middle of the right half (T), "P" is before T… found in 9 steps from 500 contacts.
⚠ Adding a new contact? You must shift every element after it to the right. Insert is O(n). Great for read-heavy apps, bad if contacts change often.
Map name → phone number. To find "Priya": the HashMap computes a hash of "Priya" and goes directly to that slot. No scanning.
✓ This is exactly what your phone's Contacts app uses internally. Fast to look up by name. You trade a bit of memory for instant speed.
WhatsApp needs both: look up number by name AND look up name by number (caller ID). Solution: store two HashMaps — one forward, one reverse.
This is a real trade-off: double the memory for instant two-way lookup. WhatsApp makes this trade because caller ID speed matters more than RAM cost on a phone.
We'll build all four versions and measure the difference.
import java.util.*; public class ContactBook { // ── Option 1: ArrayList ───────────────────────────────────────────────── private List<String[]> listContacts = new ArrayList<>(); public void addToList(String name, String phone) { listContacts.add(new String[]{name, phone}); } public String findByNameSlow(String name) { int steps = 0; for (String[] c : listContacts) { // O(n) — checks every single contact steps++; if (c[0].equals(name)) { System.out.println(" ArrayList: found in " + steps + " steps"); return c[1]; } } return null; } // ── Option 3: HashMap (one-way) ───────────────────────────────────────── private Map<String, String> nameToPhone = new HashMap<>(); public void addToMap(String name, String phone) { nameToPhone.put(name, phone); // O(1) insert } public String findByNameFast(String name) { System.out.println(" HashMap: found in 1 step (O(1))"); return nameToPhone.get(name); // O(1) — always 1 step } // ── Option 4: Two HashMaps (bidirectional — WhatsApp caller ID) ───────── private Map<String, String> phoneToName = new HashMap<>(); // EXTRA memory! public void addBidirectional(String name, String phone) { nameToPhone.put(name, phone); // store forward phoneToName.put(phone, name); // store REVERSE — costs 2x space } public String whoseNumberIsThis(String phone) { return phoneToName.get(phone); // O(1) — paid with space, not time } public static void main(String[] args) { ContactBook book = new ContactBook(); // Add 500 contacts to both structures for (int i = 1; i <= 499; i++) { book.addToList("Contact" + i, "9000000" + i); book.addToMap("Contact" + i, "9000000" + i); } // Add "Priya" as the LAST contact — worst case for ArrayList book.addToList("Priya", "9876543210"); book.addBidirectional("Priya", "9876543210"); System.out.println("Searching for Priya..."); book.findByNameSlow("Priya"); // 500 steps! book.findByNameFast("Priya"); // 1 step System.out.println("\nIncoming call: 9876543210"); String caller = book.whoseNumberIsThis("9876543210"); System.out.println("Caller: " + caller); // "Priya" — O(1), paid with 2x space } }
Searching for Priya... ArrayList: found in 500 steps HashMap: found in 1 step (O(1)) Incoming call: 9876543210 Caller: Priya
Every structure has one job it does best. Pick the one whose weakness you can afford to ignore.
| Structure | Search | Insert | Space | Use it when… | Real Example |
|---|---|---|---|---|---|
| ArrayList | O(n) | O(1) (end) | O(n) | You need ordered data, loop through it, rarely search by value | Zomato order history — your past orders shown newest first. You scroll through them in order, never jump to "order #347". |
| HashMap | O(1) | O(1) | O(n) +overhead | You need instant lookup by a key (name, ID, username, token) | Contacts app — type "Priya", instant result. Also: every login session (token → userId), every product page (productId → details). |
| Two HashMaps | O(1) both ways | O(1) | O(2n) | You need instant lookup in both directions (name↔phone, userId↔username) | WhatsApp caller ID — name→number when you call, number→name when someone calls you. Same data stored twice, O(1) both ways. |
| Stack (Deque) | O(n) | O(1) (top) | O(n) | Last-in-first-out: undo, back navigation, call stack | VS Code Ctrl+Z — every keystroke pushed onto a Stack. Ctrl+Z pops the last one. Browser Back button works the same way. |
| Queue | O(n) | O(1) (end) | O(n) | First-in-first-out: jobs processed in arrival order | Swiggy restaurant queue — orders arrive and are cooked in sequence. First order placed = first order prepared. Also: OTP delivery, email sending jobs. |
following and followers. To check "Does Priya follow Ravi?" — O(1) lookup in Ravi's followers HashSet. They spend 2× the memory so the check is instant for 1 billion users.
"abc123xyz" and stores it in a HashMap: token → userId. Every API request: look up the token in O(1). This is how every app knows who you are without re-logging you in every minute.
ContactBook class into IntelliJ and run it — see the 500-step vs 1-step output yourselfdeleteContact(String name) that removes from both HashMaps (think: if you remove forward, what happens to the reverse map?)renameContact(String oldName, String newName) — update both maps correctlyprivate List<String> recentSearches = new ArrayList<>()
and a method search(String name) that looks up the contact AND saves the search term to the list (max 10 recent searches — remove the oldest if over 10)
| O(1) | Constant — always 1 step (HashMap.get) |
| O(log n) | Logarithmic — halves each time (binary search) |
| O(n) | Linear — scans all items (ArrayList search) |
| O(n log n) | Sorting (Collections.sort) |
| O(n²) | Nested loops — avoid on large data |
| new ArrayList<>() | Create |
| list.add(x) | Add to end — O(1) |
| list.get(i) | By index — O(1) |
| list.contains(x) | Search — O(n) |
| list.remove(i) | Remove — O(n) shifts |
| Collections.sort(list) | Sort — O(n log n) |
| new HashMap<>() | Create |
| map.put(key, val) | Insert/update — O(1) |
| map.get(key) | Lookup — O(1) |
| map.getOrDefault(k, def) | Safe get |
| map.containsKey(k) | Key check — O(1) |
| map.remove(key) | Delete — O(1) |
| Need fast search? | Use HashMap — costs extra memory |
| Need both-way lookup? | Two HashMaps — costs 2× memory |
| Memory is tight? | ArrayList + binary search (sorted) |
| LIFO (undo/back)? | Stack (ArrayDeque) |
| FIFO (queue/jobs)? | Queue (LinkedList) |
| Unique items only? | HashSet — O(1) contains |