Practice Session 4: HashMap Patterns

Problem 1: Top K Frequent Elements

Link: LeetCode 347

Quick Summary: Find k most frequent elements in array.

Industry Context: Analytics (top products, trending topics), monitoring (frequent errors), log analysis

Solutions

Approach 1: HashMap + Sorting

public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> freq = new HashMap<>();
    for (int num : nums) {
        freq.put(num, freq.getOrDefault(num, 0) + 1);
    }
    
    List<Integer> keys = new ArrayList<>(freq.keySet());
    keys.sort((a, b) -> freq.get(b) - freq.get(a));
    
    return keys.stream().limit(k).mapToInt(i -> i).toArray();
}
  • Time: O(n log n) - dominated by sorting
  • Space: O(n)
  • Tradeoff: Simple but not optimal

Approach 2: HashMap + Min Heap

public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> freq = new HashMap<>();
    for (int num : nums) {
        freq.put(num, freq.getOrDefault(num, 0) + 1);
    }
    
    PriorityQueue<Integer> minHeap = new PriorityQueue<>(
        (a, b) -> freq.get(a) - freq.get(b)
    );
    
    for (int num : freq.keySet()) {
        minHeap.offer(num);
        if (minHeap.size() > k) {
            minHeap.poll();
        }
    }
    
    int[] result = new int[k];
    for (int i = 0; i < k; i++) {
        result[i] = minHeap.poll();
    }
    return result;
}
  • Time: O(n log k)
  • Space: O(n + k)
  • Tradeoff: Better for small k

Approach 3: Bucket Sort (Optimal)

public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> freq = new HashMap<>();
    for (int num : nums) {
        freq.put(num, freq.getOrDefault(num, 0) + 1);
    }
    
    // Bucket[i] contains numbers with frequency i
    List<Integer>[] bucket = new List[nums.length + 1];
    for (int num : freq.keySet()) {
        int frequency = freq.get(num);
        if (bucket[frequency] == null) {
            bucket[frequency] = new ArrayList<>();
        }
        bucket[frequency].add(num);
    }
    
    int[] result = new int[k];
    int idx = 0;
    
    // Iterate from highest frequency
    for (int i = bucket.length - 1; i >= 0 && idx < k; i--) {
        if (bucket[i] != null) {
            for (int num : bucket[i]) {
                result[idx++] = num;
                if (idx == k) break;
            }
        }
    }
    return result;
}
  • Time: O(n)
  • Space: O(n)
  • Tradeoff: Most optimal for general case

Problem 2: Design HashMap

Link: LeetCode 706

Quick Summary: Implement HashMap without using built-in hash table libraries.

Industry Context: Understanding cache implementation, database indexing internals

Solution: Array of Linked Lists (Chaining)

class MyHashMap {
    private static final int SIZE = 10000;
    
    class Node {
        int key, value;
        Node next;
        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    
    private Node[] buckets;
    
    public MyHashMap() {
        buckets = new Node[SIZE];
    }
    
    private int hash(int key) {
        return key % SIZE;
    }
    
    public void put(int key, int value) {
        int idx = hash(key);
        
        if (buckets[idx] == null) {
            buckets[idx] = new Node(key, value);
            return;
        }
        
        Node prev = null, curr = buckets[idx];
        while (curr != null) {
            if (curr.key == key) {
                curr.value = value;
                return;
            }
            prev = curr;
            curr = curr.next;
        }
        prev.next = new Node(key, value);
    }
    
    public int get(int key) {
        int idx = hash(key);
        Node curr = buckets[idx];
        
        while (curr != null) {
            if (curr.key == key) {
                return curr.value;
            }
            curr = curr.next;
        }
        return -1;
    }
    
    public void remove(int key) {
        int idx = hash(key);
        Node curr = buckets[idx];
        
        if (curr == null) return;
        
        if (curr.key == key) {
            buckets[idx] = curr.next;
            return;
        }
        
        while (curr.next != null) {
            if (curr.next.key == key) {
                curr.next = curr.next.next;
                return;
            }
            curr = curr.next;
        }
    }
}
  • Time: O(1) average, O(n) worst case
  • Space: O(n + m) where m = table size
  • Key Concepts: Hash function, collision resolution, load factor

Problem 3: Valid Sudoku

Link: LeetCode 36

Quick Summary: Validate 9x9 Sudoku board (partially filled).

Industry Context: Input validation, constraint checking, state verification

Solutions

Approach 1: Three HashSets

public boolean isValidSudoku(char[][] board) {
    Set<String> seen = new HashSet<>();
    
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            char num = board[i][j];
            if (num != '.') {
                if (!seen.add(num + " in row " + i) ||
                    !seen.add(num + " in col " + j) ||
                    !seen.add(num + " in box " + i/3 + "-" + j/3)) {
                    return false;
                }
            }
        }
    }
    return true;
}
  • Time: O(1) - fixed 9x9 board = 81 cells
  • Space: O(1) - max 399 = 243 strings
  • Tradeoff: Clever string encoding, single pass

Approach 2: Separate Arrays (Faster)

public boolean isValidSudoku(char[][] board) {
    boolean[][] rows = new boolean[9][9];
    boolean[][] cols = new boolean[9][9];
    boolean[][] boxes = new boolean[9][9];
    
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            if (board[i][j] != '.') {
                int num = board[i][j] - '1'; // Convert to 0-8
                int boxIdx = (i / 3) * 3 + j / 3;
                
                if (rows[i][num] || cols[j][num] || boxes[boxIdx][num]) {
                    return false;
                }
                
                rows[i][num] = true;
                cols[j][num] = true;
                boxes[boxIdx][num] = true;
            }
        }
    }
    return true;
}
  • Time: O(1)
  • Space: O(1) - fixed size arrays
  • Tradeoff: Faster than HashSet approach, more memory efficient

Problem 4: Insert Delete GetRandom O(1)

Link: LeetCode 380

Quick Summary: Design data structure with O(1) insert, delete, and getRandom.

Industry Context: Load balancer (pick random server), A/B testing (random user selection)

Solution: HashMap + ArrayList

class RandomizedSet {
    private List<Integer> list;
    private Map<Integer, Integer> map; // val -> index in list
    private Random rand;
    
    public RandomizedSet() {
        list = new ArrayList<>();
        map = new HashMap<>();
        rand = new Random();
    }
    
    public boolean insert(int val) {
        if (map.containsKey(val)) return false;
        
        map.put(val, list.size());
        list.add(val);
        return true;
    }
    
    public boolean remove(int val) {
        if (!map.containsKey(val)) return false;
        
        // Swap with last element
        int idx = map.get(val);
        int lastVal = list.get(list.size() - 1);
        
        list.set(idx, lastVal);
        map.put(lastVal, idx);
        
        // Remove last element
        list.remove(list.size() - 1);
        map.remove(val);
        
        return true;
    }
    
    public int getRandom() {
        return list.get(rand.nextInt(list.size()));
    }
}
  • Time: O(1) all operations
  • Space: O(n)
  • Key Insight: HashMap for O(1) lookup, ArrayList for O(1) random access
  • Trick: Swap with last element for O(1) deletion

Problem 5: Copy List with Random Pointer

Link: LeetCode 138

Quick Summary: Deep copy linked list where each node has random pointer to any node.

Industry Context: Object cloning, distributed systems (graph replication), state snapshots

Solutions

Approach 1: HashMap (Two Pass)

class Node {
    int val;
    Node next;
    Node random;
    Node(int val) { this.val = val; }
}
 
public Node copyRandomList(Node head) {
    if (head == null) return null;
    
    Map<Node, Node> map = new HashMap<>();
    
    // First pass: create all nodes
    Node curr = head;
    while (curr != null) {
        map.put(curr, new Node(curr.val));
        curr = curr.next;
    }
    
    // Second pass: link pointers
    curr = head;
    while (curr != null) {
        map.get(curr).next = map.get(curr.next);
        map.get(curr).random = map.get(curr.random);
        curr = curr.next;
    }
    
    return map.get(head);
}
  • Time: O(n)
  • Space: O(n) for HashMap
  • Tradeoff: Clean, straightforward

Approach 2: Interweaving (O(1) Space)

public Node copyRandomList(Node head) {
    if (head == null) return null;
    
    // Step 1: Create copy nodes interleaved
    Node curr = head;
    while (curr != null) {
        Node copy = new Node(curr.val);
        copy.next = curr.next;
        curr.next = copy;
        curr = copy.next;
    }
    
    // Step 2: Assign random pointers
    curr = head;
    while (curr != null) {
        if (curr.random != null) {
            curr.next.random = curr.random.next;
        }
        curr = curr.next.next;
    }
    
    // Step 3: Separate lists
    curr = head;
    Node copyHead = head.next;
    while (curr != null) {
        Node copy = curr.next;
        curr.next = copy.next;
        copy.next = (copy.next != null) ? copy.next.next : null;
        curr = curr.next;
    }
    
    return copyHead;
}
  • Time: O(n)
  • Space: O(1) excluding output
  • Tradeoff: More complex but space-optimal

Java 21+ Features with HashMap

Record Patterns for Cleaner HashMap Operations

// Java 21+
record Pair(int key, int value) {}
 
public void processEntries(List<Pair> pairs) {
    Map<Integer, Integer> map = new HashMap<>();
    
    for (Pair pair : pairs) {
        switch (pair) {
            case Pair(int k, int v) when v > 0 -> map.put(k, v);
            case Pair(int k, int v) when v < 0 -> map.remove(k);
            default -> {} // Skip zeros
        }
    }
}

Enhanced Switch for Frequency Analysis

// Java 21+
public String categorizeFrequency(int count) {
    return switch (count) {
        case int c when c == 1 -> "Unique";
        case int c when c <= 5 -> "Rare";
        case int c when c <= 20 -> "Common";
        default -> "Frequent";
    };
}

Key Takeaways

HashMap Selection Guide

Operation PatternData StructureWhy
Insert/Delete/SearchHashMapO(1) average
Order mattersLinkedHashMapInsertion order
Sorted keys neededTreeMapO(log n) sorted
Concurrent accessConcurrentHashMapThread-safe
Fixed size, no deleteArrayO(1) guaranteed

Common Patterns

  1. Frequency Counting: map.getOrDefault(key, 0) + 1
  2. Complement Lookup: Store seen elements, check if complement exists
  3. Grouping: Use computed key (sorted string, sum, etc.)
  4. Caching: HashMap + LRU eviction policy
  5. Index Mapping: Value → index for O(1) removal from array

Time Complexity

  • Average: O(1) for get/put/delete
  • Worst: O(n) when all keys hash to same bucket
  • Java’s HashMap uses O(log n) for large buckets (tree-ification)

Space Optimization

  • Load factor: 0.75 default (resize at 75% capacity)
  • Initial capacity: Set if size known upfront
  • Custom hash: Override hashCode() for custom keys

Watch Out For

  1. Mutability: Don’t modify keys after insertion
  2. Null Handling: HashMap allows one null key, null values
  3. Iteration Order: Use LinkedHashMap if order matters
  4. Hash Collisions: Good hash function is critical