Chapter II · The containers of computation

Data Structures

Every problem you'll ever solve starts with picking the right container for your data. Learn the twelve that matter—why they exist, when to reach for them, and how to wield them in Java.

§ 1 · Array

Array

An array is a row of numbered lockers in a school hallway. Each locker holds one item. You can walk straight to locker №7 instantly because the lockers are evenly spaced and numbered—you don't have to check the other lockers first. That instant-access property is what makes arrays the fastest "go fetch me item X" structure that exists.

Lockers are fixed in place. To insert a new locker between №3 and №4, you'd have to physically shove every locker from №4 onward down the hallway. That's why inserting into the middle of an array is slow—everyone has to shift over.

When to reach for it

  • You know roughly how many elements you need.
  • You need fast index access (e.g., nums[i]).
  • You're iterating over a fixed list, not constantly inserting in the middle.

Java essentials

// Declare & initialize
int[] nums = new int[10];           // all zeros
int[] vals = {3, 1, 4, 1, 5, 9};   // literal

// Access — O(1)
int first = vals[0];
int len   = vals.length;        // no parentheses!

// Iterate
for (int i = 0; i < vals.length; i++) { ... }
for (int v : vals) { ... }      // enhanced-for

// 2D array
int[][] grid = new int[3][4];     // 3 rows, 4 cols

// Useful Arrays utility
Arrays.sort(vals);                // in-place, O(n log n)
Arrays.fill(nums, -1);             // set every element to -1
int[] copy = Arrays.copyOf(vals, vals.length);
Common pitfall
vals.length has no parentheses—it's a field, not a method. Strings use .length() (method). Collections use .size(). Mixing these up is a classic Java gotcha.

Complexity

Access by indexO(1)
Search for valueO(n)
Insert at end (fixed array)
Insert at middleO(n)
§ 2 · String

String

A String is just an array of characters wearing a fancy dress. The key fact in Java: Strings are immutable. Once created, you cannot change a character. Every "modification" actually creates a brand-new String. This is great for safety, terrible for performance if you build a string inside a loop.

Treat a String like a printed page. You can read any letter, count letters, copy the page—but you can't erase letter 7 and write a new one in its place. To "edit," you reprint the whole page. That's why we use StringBuilder for repeated edits—it's like writing in pencil.

Java essentials

String s = "hello";
int n   = s.length();          // method, with parens!
char c  = s.charAt(0);          // 'h'

// Compare with .equals() — NEVER ==
if (s.equals("hello")) { ... }
if (s.equalsIgnoreCase("HELLO")) { ... }

// Slicing
String sub = s.substring(1, 4);   // "ell" — [start, end)

// Convert to char array
char[] chars = s.toCharArray();

// Building strings — USE StringBuilder for loops
StringBuilder sb = new StringBuilder();
for (char ch : chars) sb.append(ch);
String result = sb.toString();
The == trap
s1 == s2 compares references, not content. Always use s1.equals(s2). This trips up every Java beginner exactly once—make sure it's just once.

Character tricks

// Lowercase letter to index 0-25
int idx = c - 'a';

// Digit char to int
int d = c - '0';

// Character helpers
Character.isLetter(c);
Character.isDigit(c);
Character.toLowerCase(c);
§ 3 · ArrayList

ArrayList

An ArrayList is an array that grows. Underneath, it's still a regular array—but when it fills up, it secretly creates a bigger one (typically 1.5× the size), copies everything over, and you keep adding. From the outside, it just feels like a list that never runs out of space.

Imagine a row of lockers that, whenever you fill the last one, magically clones itself into a longer hallway with more lockers. You barely notice—and on average, adding a new locker costs almost nothing. That's amortized O(1).

When to use ArrayList over array

  • You don't know the final size in advance.
  • You need built-in helpers like contains, indexOf, remove.
  • You want it to play nicely with Java's Collections framework (Collections.sort, streams, etc.).

Java essentials

List<Integer> list = new ArrayList<>();

list.add(10);                  // append, amortized O(1)
list.add(0, 99);               // insert at index, O(n)
list.get(2);                  // O(1)
list.set(2, 77);               // overwrite, O(1)
list.remove(2);               // remove by index, O(n)
list.remove(Integer.valueOf(77)); // remove by VALUE
list.size();                  // .size() — method
list.contains(10);            // O(n) — slow!

// Iterate
for (int v : list) { ... }
for (int i = 0; i < list.size(); i++) { ... }

// Sort
Collections.sort(list);
list.sort(Comparator.reverseOrder());
The remove() gotcha
list.remove(2) on a List<Integer> removes the element at index 2, not the value 2. To remove the value 2, write list.remove(Integer.valueOf(2)). This bug has hidden in production for decades.
§ 4 · Linked List

Linked List

A linked list is a paper chain. Each link holds one piece of data and a pointer—an arrow—to the next link. You can't jump to the middle: you have to follow the arrows from the start. The payoff? Inserting or deleting in the middle is instant—you just snip and re-attach two pointers.

Arrays are a row of houses with street numbers; you drive straight to №42. A linked list is a treasure hunt: each clue tells you where the next clue is. Finding clue 42 means following clues 1, 2, 3 … in order. But adding a new clue between №3 and №4? Trivial—just rewrite two arrows.

The node, hand-built

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

You'll see this exact class on almost every LeetCode linked-list problem. Memorize it.

The four moves you must know

// 1. Traverse
ListNode cur = head;
while (cur != null) {
    System.out.println(cur.val);
    cur = cur.next;
}

// 2. Reverse a linked list — THE classic technique
ListNode prev = null, curr = head;
while (curr != null) {
    ListNode next = curr.next;   // save next
    curr.next = prev;             // flip arrow
    prev = curr;                  // advance prev
    curr = next;                  // advance curr
}
return prev;                      // new head

// 3. Find the middle — slow/fast pointers
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
// slow is now at the middle

// 4. Detect a cycle — Floyd's tortoise & hare
ListNode t = head, h = head;
while (h != null && h.next != null) {
    t = t.next;
    h = h.next.next;
    if (t == h) return true;     // cycle exists
}
The dummy node trick
When building a new linked list, create a "dummy" head node first: ListNode dummy = new ListNode(0);. Build off dummy.next, and return dummy.next at the end. This eliminates ugly null-checks for the first element.
§ 5 · Stack

Stack

A stack is a pile of dirty plates. You add a plate on top; you take a plate from the top. Last in, first out (LIFO). You never reach into the middle. That's the entire structure—and it's astonishingly useful: undo buttons, function call stacks, matching brackets, "next greater element" problems, and DFS all run on stacks.

A stack is your browser's Back button. Each page you visit gets pushed on top. When you hit Back, you pop the most recent one and reveal whatever was beneath. You can't jump three pages back without popping the two in between.

Use cases that scream "stack"

  • Matching pairs — brackets, tags, parentheses.
  • "Next greater / smaller" problems — monotonic stack pattern.
  • Undo / history — last action first to reverse.
  • Reversing — pop order is the reverse of push order.
  • Recursive simulation — when you don't want true recursion.

Java essentials

// Don't use the legacy `Stack` class — use ArrayDeque
Deque<Integer> stack = new ArrayDeque<>();

stack.push(1);            // add to top
stack.push(2);
stack.push(3);
int top = stack.peek();   // look at top → 3
int out = stack.pop();    // remove top → 3
boolean empty = stack.isEmpty();
Why not java.util.Stack?
The old Stack class extends Vector, which is synchronized and slow. Every modern Java guide and interviewer prefers ArrayDeque for stack behavior.
§ 6 · Queue & Deque

Queue & Deque

A queue is a line at a chai stall. The first person to arrive is the first served. First in, first out (FIFO). A deque ("deck") is a double-ended queue—you can add or remove from either end. The queue is the bedrock of BFS (breadth-first search); the deque is what powers the sliding-window-maximum pattern.

Imagine a printer queue. Documents print in the order they were submitted. You can't make page 5 print before page 1 just because you really need it—you'd have to cancel everything before it. That fairness, that strict order, is the queue's whole personality.

Java essentials — Queue

Queue<Integer> queue = new ArrayDeque<>();
// or:  Queue<Integer> queue = new LinkedList<>();

queue.offer(1);          // enqueue (returns false if full)
queue.offer(2);
int head = queue.peek(); // look — null if empty
int out  = queue.poll(); // dequeue — null if empty
boolean e = queue.isEmpty();

Java essentials — Deque

Deque<Integer> dq = new ArrayDeque<>();

dq.offerFirst(1);      // add to front
dq.offerLast(2);       // add to back
dq.peekFirst();
dq.peekLast();
dq.pollFirst();        // remove from front
dq.pollLast();         // remove from back
Why prefer offer/poll/peek
The other set (add/remove/element) throws exceptions on edge cases. offer/poll/peek return special values (false, null)—much friendlier in loops.
§ 7 · HashMap & HashSet

HashMap & HashSet

If arrays are lockers in a hallway, a HashMap is the postal system. You hand the postman a name ("Riya Mehta"), and somehow—without checking every house—he delivers your letter to the right address in one step. The magic ingredient is a hash function that converts the name into an array index. HashMap stores key→value pairs; HashSet stores just keys ("is this name in the system?").

Imagine a library where every book's location is computed from its title. Hand me "To Kill a Mockingbird", I run it through a formula, and I'm at row 47, shelf 3 in one move—no scanning shelves. That's what hashing buys you: O(1) lookup, on average.

When to reach for it (almost always)

  • "Have I seen this before?" → HashSet.
  • "How many times has X appeared?" → HashMap<X, Integer> (frequency map).
  • "Given a value, fetch its associated data fast." → HashMap.
  • Deduplication, caching, lookups by key.

Java essentials

// HashMap
Map<String, Integer> map = new HashMap<>();

map.put("apple", 3);
map.get("apple");            // 3, or null if absent
map.getOrDefault("pear", 0); // 0 if missing — clutch for counters
map.containsKey("apple");
map.remove("apple");

// Frequency counter — the most common map pattern
for (char c : s.toCharArray()) {
    map.put(c, map.getOrDefault(c, 0) + 1);
}

// Iterate
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + " → " + e.getValue());
}

// HashSet
Set<Integer> seen = new HashSet<>();
seen.add(5);
if (seen.contains(5)) { ... }
seen.remove(5);
Custom keys
If you use your own class as a HashMap key, override both equals() and hashCode(). Forgetting either is one of Java's most common bugs. For DSA problems, String, Integer, Long, and similar wrappers do this for you.

For pair-keys (like grid coordinates), encode them as a String i + "," + j or as a long via (long)i * COLS + j.
§ 8 · TreeMap & TreeSet

TreeMap & TreeSet

A HashMap doesn't care about order—it scatters keys based on a hash. A TreeMap keeps its keys sorted at all times. It's slower per operation (O(log n) instead of O(1)), but in return it answers questions HashMap can't: "what's the smallest key bigger than X?" or "give me every key between A and B."

HashMap is a chaotic warehouse where every item has its own scanner-readable QR code. Fast pickup, terrible if you ever want to walk through items in alphabetical order. TreeMap is a librarian-curated bookshelf—every book where it belongs, slightly slower to add a new one, but you can find every book between "Hemingway" and "Hugo" in one walk.

When to use TreeMap over HashMap

  • You need keys in sorted order during iteration.
  • You need "ceiling" or "floor" queries — smallest key ≥ X, or largest key ≤ X.
  • You need range queries — all keys between A and B.

Java essentials

TreeMap<Integer, String> tm = new TreeMap<>();
tm.put(3, "c");
tm.put(1, "a");
tm.put(5, "e");

tm.firstKey();           // 1
tm.lastKey();            // 5
tm.ceilingKey(2);        // 3 (smallest ≥ 2)
tm.floorKey(4);          // 3 (largest ≤ 4)
tm.higherKey(3);         // 5 (strictly > 3)
tm.lowerKey(3);          // 1 (strictly < 3)
tm.subMap(2, 5);         // view: [2, 5)

// TreeSet is the same idea, no values
TreeSet<Integer> ts = new TreeSet<>();
§ 9 · Heap / PriorityQueue

Heap / PriorityQueue

A heap is a hospital ER. Patients aren't seen in arrival order—they're seen in order of urgency. The receptionist always knows who the most critical patient is. Whenever a new patient walks in, they're slotted into the right priority. That's a heap: peek at the smallest (or largest) item in O(1), insert in O(log n), remove the top in O(log n).

Picture a pyramid of bowling balls where the lightest is always on top. Add a new ball: it sinks until lighter balls float above it. Remove the lightest: the next-lightest bubbles up to take its place. The pyramid never sorts itself fully—it only guarantees the top is correct, which is exactly what you need most of the time.

Use cases that scream "heap"

  • K-th largest / smallest — keep a heap of size K.
  • Merge K sorted lists — heap of list heads.
  • Dijkstra's shortest path — extract the closest unvisited node.
  • Scheduling — always pick the most urgent next.
  • Running median — two heaps facing each other.

Java essentials

// Min-heap (default)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
minHeap.peek();      // 1 — smallest
minHeap.poll();      // 1, removes it

// Max-heap — supply a reverse comparator
PriorityQueue<Integer> maxHeap =
    new PriorityQueue<>(Comparator.reverseOrder());

// Heap of custom objects: sort by 2nd field ascending
PriorityQueue<int[]> pq =
    new PriorityQueue<>((a, b) -> a[1] - b[1]);

// K-th largest: maintain a min-heap of size K
public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> h = new PriorityQueue<>();
    for (int n : nums) {
        h.offer(n);
        if (h.size() > k) h.poll();
    }
    return h.peek();
}
§ 10 · Binary Tree

Binary Tree

A binary tree is a family tree where each person has at most two children. There's one root at the top, and every node has a left and a right (either or both may be empty). Trees model anything hierarchical: file systems, decision processes, the DOM, expression evaluation, organization charts.

Picture a tournament bracket. The final winner sits at the root. Below them, two finalists. Below them, four semi-finalists, and so on. To walk from the root to any leaf, you make a sequence of left/right choices. To visit every player, you have a few principled orders—we call those traversals.

The node

class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int val) { this.val = val; }
}

The three depth-first traversals

The difference is when you visit the current node relative to its children:

// Pre-order: ROOT → LEFT → RIGHT (good for cloning a tree)
void pre(TreeNode n) {
    if (n == null) return;
    visit(n);
    pre(n.left);
    pre(n.right);
}

// In-order: LEFT → ROOT → RIGHT (sorted output on a BST)
void in(TreeNode n) {
    if (n == null) return;
    in(n.left);
    visit(n);
    in(n.right);
}

// Post-order: LEFT → RIGHT → ROOT (good for deletion, bottom-up DP)
void post(TreeNode n) {
    if (n == null) return;
    post(n.left);
    post(n.right);
    visit(n);
}

Level-order traversal (BFS)

// Visit row by row — uses a queue
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    if (root == null) return res;
    Queue<TreeNode> q = new ArrayDeque<>();
    q.offer(root);
    while (!q.isEmpty()) {
        int size = q.size();
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode n = q.poll();
            level.add(n.val);
            if (n.left  != null) q.offer(n.left);
            if (n.right != null) q.offer(n.right);
        }
        res.add(level);
    }
    return res;
}
Tree problems = recursion
Almost every tree problem solves with a recursive function that handles three things: (1) the base case (usually if (node == null)), (2) what to do with the current node, and (3) recursive calls to left and right. If you can name those three pieces, you can solve it.
§ 11 · Binary Search Tree

Binary Search Tree

A BST is a binary tree with one extra rule: every node's left subtree contains smaller values, and the right subtree contains larger values. That rule turns every search into a halving game—just like binary search, but on a tree. As long as the tree stays balanced, every operation is O(log n).

Think of the way you might find a name in a thick phonebook: open to the middle, decide if your name is in the left half or right half, and keep narrowing. A BST encodes that decision tree directly. Start at the root, go left or right based on a comparison, and you're at your target in log n steps.

The killer property: in-order = sorted

If you in-order traverse a BST (left → root → right), you get the elements in sorted order. Use this. Many BST problems boil down to "run in-order and check something."

Search & insert

public TreeNode search(TreeNode root, int target) {
    while (root != null && root.val != target) {
        root = target < root.val ? root.left : root.right;
    }
    return root;
}

public TreeNode insert(TreeNode root, int val) {
    if (root == null) return new TreeNode(val);
    if (val < root.val) root.left  = insert(root.left,  val);
    else                root.right = insert(root.right, val);
    return root;
}
Balance matters
A BST stays O(log n) only if it's balanced. Insert sorted values into a plain BST and you'll get a glorified linked list — back to O(n). In production, Java's TreeMap and TreeSet use a Red-Black tree, which self-balances. For interviews, you rarely implement balancing yourself.
§ 12 · Trie

Trie (Prefix Tree)

A trie is what powers your phone's autocomplete. It's a tree where each edge is a letter, and walking from the root to a node spells out a prefix. If thousands of words start with "pre"—precision, prefix, prepare, prevent—they all share the same "p → r → e" path through the tree. That shared path is what makes prefix queries blazing fast.

Imagine a dictionary organized as a giant flowchart. To look up "cat", you go down branch C, then A, then T. If you stop at T and there's a little marker that says "word ends here," then "cat" is in the dictionary. The shared early letters of "car" and "cat" live in the same branch—no duplication.

When to reach for it

  • Autocomplete / type-ahead.
  • Prefix matching ("how many words start with 'pre'?").
  • Spell-check, word games like Boggle.
  • IP routing tables (longest prefix match).

Java implementation

class Trie {
    private Trie[] children = new Trie[26];
    private boolean isEnd = false;

    public void insert(String word) {
        Trie node = this;
        for (char c : word.toCharArray()) {
            int i = c - 'a';
            if (node.children[i] == null)
                node.children[i] = new Trie();
            node = node.children[i];
        }
        node.isEnd = true;
    }

    public boolean search(String word) {
        Trie node = find(word);
        return node != null && node.isEnd;
    }

    public boolean startsWith(String prefix) {
        return find(prefix) != null;
    }

    private Trie find(String s) {
        Trie node = this;
        for (char c : s.toCharArray()) {
            node = node.children[c - 'a'];
            if (node == null) return null;
        }
        return node;
    }
}
§ 13 · Graph

Graph

A graph is a tree that gave up on having a single root and let nodes connect to anyone. Cities and the roads between them, users and their friendships, web pages and their links—anything where things connect to other things is a graph. Trees are just graphs with rules; once you can do graphs, trees come for free.

Imagine a city. The intersections are nodes. The streets are edges. Some streets are one-way (directed graph); most go both ways (undirected). Some streets are toll roads with different prices (weighted graph). Asking "how do I get from intersection A to intersection B with minimum tolls?" is a shortest-path problem.

Two ways to store a graph

Adjacency list — for each node, a list of its neighbors. Compact when edges are sparse. This is what you'll use 95% of the time.

// Graph with n nodes (numbered 0 .. n-1)
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());

// edges: int[][] edges where edges[i] = {u, v}
for (int[] e : edges) {
    graph.get(e[0]).add(e[1]);
    graph.get(e[1]).add(e[0]);  // omit this for directed graph
}

Adjacency matrix — an n × n boolean grid. Wastes space for sparse graphs but lets you ask "is there an edge from i to j?" in O(1).

BFS — shortest path in an unweighted graph

public int shortestPath(List<List<Integer>> graph, int src, int dst) {
    Queue<Integer> q = new ArrayDeque<>();
    boolean[] seen = new boolean[graph.size()];
    q.offer(src);
    seen[src] = true;
    int dist = 0;
    while (!q.isEmpty()) {
        int size = q.size();
        for (int i = 0; i < size; i++) {
            int node = q.poll();
            if (node == dst) return dist;
            for (int nb : graph.get(node)) {
                if (!seen[nb]) {
                    seen[nb] = true;
                    q.offer(nb);
                }
            }
        }
        dist++;
    }
    return -1;
}

DFS — exploring all reachable

void dfs(int node, List<List<Integer>> graph, boolean[] seen) {
    if (seen[node]) return;
    seen[node] = true;
    for (int nb : graph.get(node)) dfs(nb, graph, seen);
}
Always carry a "seen" set
Graphs can have cycles. Without a visited array or set, your BFS/DFS will loop forever. Mark a node as seen when you enqueue it, not when you dequeue it—otherwise you'll add duplicates to the queue.
§ 14 · Union-Find

Union-Find (Disjoint Set)

Union-Find answers one question really fast: "are these two things in the same group?" Start with everyone in their own group. As you process connections, merge groups. At any moment you can ask "are X and Y connected?" in essentially O(1). It's the secret weapon behind Kruskal's MST, dynamic connectivity, and a surprising number of LeetCode hards.

Picture a wedding seating chart with name cards in piles. Each pile is a "family group." When two families discover they're related (someone shouts "we're cousins!"), you slide the two piles together into one. Want to check if Anjali and Karan are family? Just see which pile each is sitting in—if it's the same pile, yes.

Java implementation

class UnionFind {
    int[] parent, rank;

    UnionFind(int n) {
        parent = new int[n];
        rank   = new int[n];
        for (int i = 0; i < n; i++) parent[i] = i;
    }

    // Find with PATH COMPRESSION
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }

    // Union by RANK — keeps tree shallow
    boolean union(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return false;            // already same group
        if (rank[rx] < rank[ry])      parent[rx] = ry;
        else if (rank[rx] > rank[ry]) parent[ry] = rx;
        else { parent[ry] = rx; rank[rx]++; }
        return true;
    }
}

With path compression and union by rank, both operations run in nearly O(1) — technically O(α(n)) where α is the inverse Ackermann function, which is at most 4 for any input you'll ever encounter.

You now have the containers. Next, the patterns that operate on them.

Continue to Algorithms