Skip to main content

Java Collections Framework

Description

The Java Collections Framework (JCF) is a unified architecture built into the java.util package that provides ready-to-use data structures and algorithms for storing, retrieving, and manipulating groups of objects. Instead of writing your own linked list, hash table, or sorting routine, you use well-tested, high-performance implementations that follow standard interfaces.

The framework is organized around a hierarchy of interfaces and their concrete implementations:

  1. Collection Interface — The root interface for most collections, with three main sub-interfaces:

    • List — Ordered collections that allow duplicates (e.g., ArrayList, LinkedList)
    • Set — Collections that forbid duplicates (e.g., HashSet, TreeSet)
    • Queue / Deque — Collections designed for holding elements before processing (e.g., PriorityQueue, ArrayDeque)
  2. Map Interface — Stores key-value pairs where each key is unique (e.g., HashMap, TreeMap). Maps are NOT part of the Collection hierarchy but are a core part of the framework.

  3. Utility ClassesCollections (static helper methods like sort, binarySearch, reverse) and Arrays (array manipulation utilities).

The key innovation of the JCF is programming to interfaces: you declare variables using interface types (List, Set, Map) and can swap implementations (ArrayListLinkedList) without changing the rest of your code. This makes programs flexible, maintainable, and easy to optimize.

Java Collections Framework hierarchy diagram showing Collection interface branching into List, Set, and Queue, with Map as a separate hierarchy, and concrete implementations under each
Java Collections Framework hierarchy diagram showing Collection interface branching into List, Set, and Queue, with Map as a separate hierarchy, and concrete implementations under each

Examples

Example 1

Using ArrayList (Dynamic Array)

import java.util.*;

public class Example1 {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");
        fruits.add("Banana"); // Duplicates allowed

        System.out.println(fruits);       // [Apple, Banana, Cherry, Banana]
        System.out.println(fruits.get(1)); // Banana (index-based access)
        System.out.println(fruits.size()); // 4
    }
}

Output: [Apple, Banana, Cherry, Banana]

Explanation: ArrayList is the most commonly used List implementation. It stores elements in a resizable internal array, providing O(1) random access via get(index) and amortized O(1) add() at the end. Duplicates are allowed, and insertion order is preserved. Notice the variable is declared as List<String> (interface type) but instantiated as ArrayList — this is the "program to interfaces" principle.

Example 2

Using HashMap (Key-Value Store)

import java.util.*;

public class Example2 {
    public static void main(String[] args) {
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob", 87);
        scores.put("Charlie", 92);

        System.out.println(scores.get("Bob")); // 87
        System.out.println(scores.containsKey("Alice")); // true
        System.out.println(scores.size()); // 3

        // Iterate over entries
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

Output: 87, true, 3, followed by all entries (in no guaranteed order)

Explanation: HashMap stores key-value pairs using a hash table internally. It provides O(1) average-case put, get, and containsKey operations. Keys must be unique — inserting a duplicate key overwrites the old value. Unlike TreeMap, HashMap does NOT guarantee any particular iteration order.

Example 3

Using HashSet (Unique Elements) and Collections Utility

import java.util.*;

public class Example3 {
    public static void main(String[] args) {
        // HashSet - no duplicates
        Set<Integer> nums = new HashSet<>(Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5));
        System.out.println(nums); // [1, 2, 3, 4, 5, 6, 9] (no duplicates, unordered)

        // Collections utility
        List<Integer> list = new ArrayList<>(Arrays.asList(5, 2, 8, 1, 9));
        Collections.sort(list);             // [1, 2, 5, 8, 9]
        int idx = Collections.binarySearch(list, 5); // 2
        Collections.reverse(list);          // [9, 8, 5, 2, 1]
        int maxVal = Collections.max(list); // 9
        
        System.out.println("Sorted then reversed: " + list);
        System.out.println("Max value: " + maxVal);
    }
}

Output: Set with duplicates removed, sorted then reversed list, max value

Explanation: HashSet automatically eliminates duplicates using hash-based equality checking. The Collections utility class provides static methods like sort (O(n log n) using TimSort), binarySearch (O(log n) on sorted list), reverse (O(n)), and max (O(n)). These algorithms work on any List implementation.

Constraints

When choosing a Java Collection implementation, consider these guidelines:

  • Need fast random access by index? → Use ArrayList (O(1) access)
  • Need fast insertion/removal in the middle? → Use LinkedList (O(1) with iterator, but O(n) to find position)
  • Need unique elements, no order requirement? → Use HashSet (O(1) add/contains)
  • Need unique elements in sorted order? → Use TreeSet (O(log n) operations)
  • Need unique elements in insertion order? → Use LinkedHashSet (O(1) operations + insertion order)
  • Need key-value pairs, fastest lookup? → Use HashMap (O(1) average)
  • Need key-value pairs in sorted key order? → Use TreeMap (O(log n) operations)
  • Need key-value pairs in insertion order? → Use LinkedHashMap (O(1) + insertion order)
  • Need FIFO (First In, First Out)? → Use ArrayDeque or LinkedList as Queue
  • Need priority-based ordering? → Use PriorityQueue (O(log n) add/poll)
  • Need LIFO (Last In, First Out)? → Use ArrayDeque as stack (preferred over legacy Stack class)

Common complexity guarantees:

  • ArrayList.get(i) → O(1), ArrayList.add() → Amortized O(1), ArrayList.add(i, e) → O(n)
  • HashMap.put/get/containsKey → O(1) average, O(n) worst case
  • TreeMap.put/get/containsKey → O(log n) guaranteed
  • HashSet.add/contains/remove → O(1) average
  • TreeSet.add/contains/remove → O(log n) guaranteed
  • PriorityQueue.add/poll → O(log n), peek → O(1)
  • Collections.sort → O(n log n) using TimSort

Editorial

List Implementations

Intuition

The List interface represents an ordered collection where elements are accessed by their integer index (position). Duplicates are allowed, and insertion order is preserved.

Java provides two primary List implementations, each with different performance characteristics:

  • ArrayList — Backed by a resizable array. Fast random access O(1) and fast append O(1) amortized. Slow insertion/removal in the middle O(n) because elements must be shifted. This is the default choice for most situations — just like vector in C++ or list in Python.

  • LinkedList — Backed by a doubly-linked list. Fast insertion/removal at any position O(1) if you already have an iterator there. But random access is slow O(n) because you must traverse from the head. LinkedList also implements Deque, so it can be used as a queue or stack.

Rule of thumb: Use ArrayList unless you specifically need frequent insertions/removals in the middle of the list and already have iterators to those positions. In practice, ArrayList wins 95% of the time due to better cache performance from contiguous memory layout.

Step-by-Step Explanation

Let's trace through common ArrayList operations:

Step 1: Create an empty ArrayList.

  • List<Integer> list = new ArrayList<>();
  • Internal array: [], Size = 0, Capacity = 10 (default initial capacity)

Step 2: Add element 10.

  • list.add(10);
  • Internal array: [10], Size = 1

Step 3: Add element 20.

  • list.add(20);
  • Internal array: [10, 20], Size = 2

Step 4: Add element 30.

  • list.add(30);
  • Internal array: [10, 20, 30], Size = 3

Step 5: Insert 15 at index 1 (shifts elements right).

  • list.add(1, 15);
  • Elements at index 1, 2, 3 shift right by one position.
  • Internal array: [10, 15, 20, 30], Size = 4

Step 6: Get element at index 2.

  • list.get(2) → returns 20 in O(1)

Step 7: Remove element at index 0.

  • list.remove(0);
  • All elements shift left by one position.
  • Internal array: [15, 20, 30], Size = 3

Step 8: Final state: [15, 20, 30].

ArrayList — Add, Insert, and Remove Operations — Watch how ArrayList manages its internal array: appending at end is fast, but inserting and removing in the middle requires shifting elements.

Algorithm

Key List Operations:

  1. add(element) — Append to end. O(1) amortized for ArrayList.
  2. add(index, element) — Insert at position. O(n) for ArrayList (shifting), O(1) for LinkedList (with iterator).
  3. get(index) — Access by position. O(1) for ArrayList, O(n) for LinkedList.
  4. set(index, element) — Replace at position. O(1) for ArrayList.
  5. remove(index) — Remove by position. O(n) for ArrayList (shifting).
  6. remove(Object) — Remove first occurrence by value. O(n) for both.
  7. contains(Object) — Search. O(n) for both (linear scan).
  8. size() / isEmpty() — O(1) for both.
  9. indexOf(Object) — Find position. O(n).
  10. subList(from, to) — Create a view of a range. O(1).

Choosing Between ArrayList and LinkedList:

  • Default to ArrayList for almost everything.
  • Use LinkedList only if you frequently add/remove near the beginning or use it as a Deque.
  • Never use LinkedList if you need random access by index.

Code

import java.util.*;

public class ListDemo {
    public static void main(String[] args) {
        // --- ArrayList (default choice) ---
        List<Integer> arrayList = new ArrayList<>();
        arrayList.add(10);                // Append: [10]
        arrayList.add(20);                // Append: [10, 20]
        arrayList.add(30);                // Append: [10, 20, 30]
        arrayList.add(1, 15);             // Insert at index 1: [10, 15, 20, 30]
        System.out.println(arrayList.get(2));  // Random access: 20 (O(1))
        arrayList.remove(0);              // Remove first: [15, 20, 30]
        System.out.println(arrayList);    // [15, 20, 30]

        // --- LinkedList (also implements Deque) ---
        LinkedList<String> linkedList = new LinkedList<>();
        linkedList.add("B");              // [B]
        linkedList.addFirst("A");          // [A, B]
        linkedList.addLast("C");           // [A, B, C]
        System.out.println(linkedList.getFirst()); // A
        linkedList.removeFirst();          // [B, C]
        System.out.println(linkedList);   // [B, C]

        // --- Common operations ---
        List<Integer> nums = new ArrayList<>(Arrays.asList(5, 2, 8, 2, 1));
        System.out.println(nums.contains(8));     // true
        System.out.println(nums.indexOf(2));      // 1 (first occurrence)
        System.out.println(nums.lastIndexOf(2));  // 3 (last occurrence)
        System.out.println(nums.subList(1, 4));   // [2, 8, 2] (view)
    }
}
# Python equivalents of Java List implementations

# --- list (equivalent to Java ArrayList) ---
array_list = []
array_list.append(10)           # Append: [10]
array_list.append(20)           # Append: [10, 20]
array_list.append(30)           # Append: [10, 20, 30]
array_list.insert(1, 15)        # Insert at index 1: [10, 15, 20, 30]
print(array_list[2])            # Random access: 20 (O(1))
array_list.pop(0)               # Remove first: [15, 20, 30]
print(array_list)               # [15, 20, 30]

# --- collections.deque (equivalent to Java LinkedList as Deque) ---
from collections import deque
linked = deque()
linked.append("B")              # [B]
linked.appendleft("A")          # [A, B]
linked.append("C")              # [A, B, C]
print(linked[0])                # A
linked.popleft()                # [B, C]

# --- Common operations ---
nums = [5, 2, 8, 2, 1]
print(8 in nums)                # True (O(n))
print(nums.index(2))            # 1 (first occurrence)
nums_slice = nums[1:4]          # [2, 8, 2] (copy, not view)
#include <vector>
#include <list>
#include <deque>
#include <algorithm>
#include <iostream>
using namespace std;

int main() {
    // --- vector (equivalent to Java ArrayList) ---
    vector<int> vec;
    vec.push_back(10);                   // Append: {10}
    vec.push_back(20);                   // Append: {10, 20}
    vec.push_back(30);                   // Append: {10, 20, 30}
    vec.insert(vec.begin() + 1, 15);     // Insert at index 1: {10, 15, 20, 30}
    cout << vec[2] << endl;              // Random access: 20 (O(1))
    vec.erase(vec.begin());              // Remove first: {15, 20, 30}

    // --- list (equivalent to Java LinkedList) ---
    list<string> lst;
    lst.push_back("B");                  // {B}
    lst.push_front("A");                 // {A, B}
    lst.push_back("C");                  // {A, B, C}
    cout << lst.front() << endl;         // A
    lst.pop_front();                     // {B, C}

    // --- Common operations ---
    vector<int> nums = {5, 2, 8, 2, 1};
    bool found = find(nums.begin(), nums.end(), 8) != nums.end(); // true
    auto it = find(nums.begin(), nums.end(), 2);
    int idx = distance(nums.begin(), it); // 1 (first occurrence)

    return 0;
}

Complexity Analysis

List Implementation Complexities:

OperationArrayListLinkedList
get(index)O(1)O(n)
add(element) (end)O(1)*O(1)
add(index, element)O(n)O(1)**
remove(index)O(n)O(1)**
contains(Object)O(n)O(n)
size()O(1)O(1)
iterator.next()O(1)O(1)

*Amortized O(1) — when the internal array is full, ArrayList grows by 50% (newCapacity = oldCapacity + oldCapacity/2), copies all elements, then adds. The copy is O(n) but happens rarely enough that the average cost per add is O(1).

**O(1) only if you already have an iterator/reference to the node. Finding the node by index is O(n).

Space Complexity: ArrayList uses O(n) space with up to 50% wasted capacity. LinkedList uses O(n) space with additional overhead per node (~3 references: prev, next, element).

Why ArrayList usually wins: Contiguous memory layout means CPU cache prefetching works efficiently. LinkedList nodes are scattered in heap memory, causing frequent cache misses. For most workloads, ArrayList's O(n) shifting is faster in practice than LinkedList's O(n) traversal due to this cache effect.

Set Implementations

Intuition

The Set interface represents a collection that contains no duplicate elements. Attempting to add a duplicate is silently ignored (the add method returns false). Sets are ideal when you need to track unique items, check membership, or eliminate duplicates.

Java provides three main Set implementations:

  • HashSet — Backed by a hash table (HashMap internally). Fastest operations: O(1) average for add, remove, and contains. Elements are stored in no particular order — the iteration order can change between runs. This is the default choice when you just need uniqueness and fast lookups.

  • LinkedHashSet — Extends HashSet with a linked list that maintains insertion order. Same O(1) performance as HashSet, but iterating always produces elements in the order they were first added. Use when you need both uniqueness and predictable iteration order.

  • TreeSet — Backed by a red-black tree (TreeMap internally). All operations are O(log n). Elements are stored in sorted order (natural ordering or a custom Comparator). Use when you need elements sorted or need range operations like headSet, tailSet, subSet.

Think of it this way:

  • HashSet = a bag with unique items tossed in randomly
  • LinkedHashSet = a numbered checklist of unique items
  • TreeSet = a sorted shelf of unique items

Step-by-Step Explanation

Let's trace through building a HashSet to find unique elements and detect duplicates:

Input array: [4, 2, 7, 2, 9, 4, 1]

Step 1: Initialize an empty HashSet.

  • Set<Integer> seen = new HashSet<>();
  • Set: {}

Step 2: Process element 4.

  • seen.add(4) → returns true (new element)
  • Set: {4}

Step 3: Process element 2.

  • seen.add(2) → returns true (new element)
  • Set: {4, 2}

Step 4: Process element 7.

  • seen.add(7) → returns true (new element)
  • Set: {4, 2, 7}

Step 5: Process element 2 (duplicate!).

  • seen.add(2) → returns false (2 already exists)
  • Set unchanged: {4, 2, 7}
  • We detected a duplicate!

Step 6: Process element 9.

  • seen.add(9) → returns true
  • Set: {4, 2, 7, 9}

Step 7: Process element 4 (duplicate!).

  • seen.add(4) → returns false (4 already exists)
  • Set unchanged: {4, 2, 7, 9}
  • Another duplicate detected.

Step 8: Process element 1.

  • seen.add(1) → returns true
  • Set: {4, 2, 7, 9, 1}

Result: 5 unique elements found, 2 duplicates detected.

HashSet — Building a Set and Detecting Duplicates — Watch how a HashSet stores unique elements and silently rejects duplicates, enabling O(1) membership testing.

Algorithm

Key Set Operations:

  1. add(element) — Adds if not already present. Returns true if added, false if duplicate.
  2. remove(element) — Removes the element. Returns true if found and removed.
  3. contains(element) — Checks membership. The most common Set operation.
  4. size() / isEmpty() — Count elements.
  5. clear() — Remove all elements.
  6. iterator() — Iterate over elements (order depends on implementation).

Set Operations (Mathematical):

  • Union: setA.addAll(setB) — A ∪ B
  • Intersection: setA.retainAll(setB) — A ∩ B
  • Difference: setA.removeAll(setB) — A \ B
  • Subset check: setB.containsAll(setA) — A ⊆ B?

TreeSet Bonus Operations (because elements are sorted):

  • first() / last() — Smallest / largest element
  • headSet(e) — All elements < e
  • tailSet(e) — All elements ≥ e
  • subSet(from, to) — Elements in range [from, to)
  • floor(e) / ceiling(e) — Greatest ≤ e / Smallest ≥ e

Code

import java.util.*;

public class SetDemo {
    public static void main(String[] args) {
        // --- HashSet (unordered, fastest) ---
        Set<Integer> hashSet = new HashSet<>();
        hashSet.add(5);
        hashSet.add(2);
        hashSet.add(8);
        hashSet.add(2);  // Duplicate - ignored
        System.out.println(hashSet);            // e.g., [2, 5, 8] (order varies)
        System.out.println(hashSet.contains(5)); // true (O(1))

        // --- LinkedHashSet (insertion order preserved) ---
        Set<String> linkedSet = new LinkedHashSet<>();
        linkedSet.add("Banana");
        linkedSet.add("Apple");
        linkedSet.add("Cherry");
        linkedSet.add("Apple");  // Duplicate - ignored
        System.out.println(linkedSet);  // [Banana, Apple, Cherry] (insertion order)

        // --- TreeSet (sorted) ---
        TreeSet<Integer> treeSet = new TreeSet<>(Arrays.asList(5, 2, 8, 1, 9));
        System.out.println(treeSet);         // [1, 2, 5, 8, 9] (sorted)
        System.out.println(treeSet.first()); // 1 (smallest)
        System.out.println(treeSet.last());  // 9 (largest)
        System.out.println(treeSet.headSet(5));    // [1, 2] (elements < 5)
        System.out.println(treeSet.tailSet(5));    // [5, 8, 9] (elements >= 5)
        System.out.println(treeSet.subSet(2, 9));  // [2, 5, 8] (2 <= x < 9)
        System.out.println(treeSet.floor(6));      // 5 (greatest <= 6)
        System.out.println(treeSet.ceiling(6));    // 8 (smallest >= 6)

        // --- Set operations ---
        Set<Integer> a = new HashSet<>(Arrays.asList(1, 2, 3, 4));
        Set<Integer> b = new HashSet<>(Arrays.asList(3, 4, 5, 6));

        Set<Integer> union = new HashSet<>(a);
        union.addAll(b);  // {1, 2, 3, 4, 5, 6}

        Set<Integer> intersection = new HashSet<>(a);
        intersection.retainAll(b);  // {3, 4}

        Set<Integer> difference = new HashSet<>(a);
        difference.removeAll(b);  // {1, 2}

        System.out.println("Union: " + union);
        System.out.println("Intersection: " + intersection);
        System.out.println("Difference: " + difference);
    }
}
# Python equivalents of Java Set implementations

# --- set (equivalent to Java HashSet — unordered, unique) ---
hash_set = {5, 2, 8, 2}  # Duplicate removed: {2, 5, 8}
hash_set.add(10)
print(5 in hash_set)       # True (O(1))

# --- No direct equivalent to LinkedHashSet ---
# Use dict.fromkeys() to maintain insertion order with uniqueness
linked_set = dict.fromkeys(["Banana", "Apple", "Cherry", "Apple"])
print(list(linked_set.keys()))  # ['Banana', 'Apple', 'Cherry']

# --- SortedContainers (equivalent to Java TreeSet) ---
# from sortedcontainers import SortedSet
# tree_set = SortedSet([5, 2, 8, 1, 9])  # Sorted: [1, 2, 5, 8, 9]

# --- Set operations (built-in in Python!) ---
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)    # Union: {1, 2, 3, 4, 5, 6}
print(a & b)    # Intersection: {3, 4}
print(a - b)    # Difference: {1, 2}
print(a <= b)   # Subset check: False
#include <set>
#include <unordered_set>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // --- unordered_set (equivalent to Java HashSet) ---
    unordered_set<int> hashSet = {5, 2, 8, 2}; // {5, 2, 8}
    hashSet.insert(10);
    cout << hashSet.count(5) << endl; // 1 (true, O(1))

    // --- set (equivalent to Java TreeSet — sorted) ---
    set<int> treeSet = {5, 2, 8, 1, 9}; // {1, 2, 5, 8, 9}
    cout << *treeSet.begin() << endl;   // 1 (smallest)
    cout << *treeSet.rbegin() << endl;  // 9 (largest)

    // Range query: elements >= 2 and < 9
    auto lo = treeSet.lower_bound(2);
    auto hi = treeSet.lower_bound(9);
    for (auto it = lo; it != hi; ++it)
        cout << *it << " "; // 2 5 8
    cout << endl;

    // --- Set operations ---
    set<int> a = {1, 2, 3, 4};
    set<int> b = {3, 4, 5, 6};

    vector<int> uni, inter, diff;
    set_union(a.begin(), a.end(), b.begin(), b.end(), back_inserter(uni));
    set_intersection(a.begin(), a.end(), b.begin(), b.end(), back_inserter(inter));
    set_difference(a.begin(), a.end(), b.begin(), b.end(), back_inserter(diff));

    return 0;
}

Complexity Analysis

Set Implementation Complexities:

OperationHashSetLinkedHashSetTreeSet
add(e)O(1) avgO(1) avgO(log n)
remove(e)O(1) avgO(1) avgO(log n)
contains(e)O(1) avgO(1) avgO(log n)
IterationO(n) unorderedO(n) insertion orderO(n) sorted order
first() / last()N/AN/AO(log n)
headSet / tailSetN/AN/AO(log n)

Space Complexity: All three use O(n) space. HashSet has hash table overhead (bucket array + entries). LinkedHashSet adds two extra references per entry for the linked list. TreeSet has tree node overhead (left, right, parent, color).

When to use which:

  • HashSet — Default choice. Fastest operations when you don't care about order.
  • LinkedHashSet — When you need insertion-order iteration (e.g., maintaining a unique history).
  • TreeSet — When you need sorted elements or range queries. The O(log n) cost is worth it for floor(), ceiling(), subSet() capabilities that hash-based sets cannot provide.

Map Implementations

Intuition

The Map interface stores data as key-value pairs where each key is unique. If you insert a pair with a key that already exists, the old value is replaced. Maps are the most versatile data structure in the JCF — used for caching, counting, grouping, indexing, and much more.

Java provides three primary Map implementations:

  • HashMap — The workhorse. Uses a hash table internally. O(1) average for put, get, containsKey, and remove. No guaranteed iteration order. This is your default choice.

  • LinkedHashMap — Extends HashMap to maintain insertion order (or optionally access order for LRU cache implementations). Same O(1) performance as HashMap.

  • TreeMap — Uses a red-black tree. O(log n) for all operations. Keys are stored in sorted order. Provides additional navigation methods like firstKey(), lastKey(), floorKey(), ceilingKey(), and range views.

Key insight: Map is NOT part of the Collection interface hierarchy, but it's tightly integrated with the framework. You can get collection views via keySet(), values(), and entrySet().

A common pattern is map.getOrDefault(key, defaultValue) which returns the value for the key if present, or a default value if not — avoiding null checks. Another powerful method is map.merge(key, value, remappingFunction) for combining old and new values.

Step-by-Step Explanation

Let's trace building a word frequency counter using HashMap:

Input: words = ["cat", "dog", "cat", "bird", "dog", "cat"]

Step 1: Initialize an empty HashMap.

  • Map<String, Integer> freq = new HashMap<>();
  • Map: {}

Step 2: Process "cat".

  • Not in map → freq.put("cat", 1)
  • Map: {"cat": 1}

Step 3: Process "dog".

  • Not in map → freq.put("dog", 1)
  • Map: {"cat": 1, "dog": 1}

Step 4: Process "cat" (second occurrence).

  • Already in map with value 1 → freq.put("cat", 2)
  • Map: {"cat": 2, "dog": 1}

Step 5: Process "bird".

  • Not in map → freq.put("bird", 1)
  • Map: {"cat": 2, "dog": 1, "bird": 1}

Step 6: Process "dog" (second occurrence).

  • Already in map with value 1 → freq.put("dog", 2)
  • Map: {"cat": 2, "dog": 2, "bird": 1}

Step 7: Process "cat" (third occurrence).

  • Already in map with value 2 → freq.put("cat", 3)
  • Map: {"cat": 3, "dog": 2, "bird": 1}

Result: cat=3, dog=2, bird=1.

HashMap — Building a Word Frequency Counter — Watch how HashMap stores word counts: new words get inserted with count 1, while repeated words have their count incremented.

Algorithm

Key Map Operations:

  1. put(key, value) — Insert or update a key-value pair.
  2. get(key) — Retrieve value by key. Returns null if not found.
  3. getOrDefault(key, default) — Retrieve value or return default if absent.
  4. containsKey(key) / containsValue(value) — Check existence.
  5. remove(key) — Remove entry by key.
  6. size() / isEmpty() — Count entries.
  7. keySet() — Get all keys as a Set.
  8. values() — Get all values as a Collection.
  9. entrySet() — Get all key-value pairs as a Set<Map.Entry>.
  10. putIfAbsent(key, value) — Only insert if key is not already present.
  11. merge(key, value, remappingFunction) — Combine old and new values.
  12. computeIfAbsent(key, function) — Compute value lazily if key absent.

TreeMap Bonus Operations (sorted keys):

  • firstKey() / lastKey() — Smallest / largest key
  • floorKey(k) / ceilingKey(k) — Greatest key ≤ k / Smallest key ≥ k
  • headMap(k) / tailMap(k) / subMap(from, to) — Range views

Code

import java.util.*;

public class MapDemo {
    public static void main(String[] args) {
        // --- HashMap (default choice) ---
        Map<String, Integer> hashMap = new HashMap<>();
        hashMap.put("Alice", 95);
        hashMap.put("Bob", 87);
        hashMap.put("Charlie", 92);
        System.out.println(hashMap.get("Bob"));       // 87
        System.out.println(hashMap.containsKey("Alice")); // true
        System.out.println(hashMap.getOrDefault("Dave", 0)); // 0

        // Frequency counting pattern
        String[] words = {"cat", "dog", "cat", "bird", "dog", "cat"};
        Map<String, Integer> freq = new HashMap<>();
        for (String w : words) {
            freq.merge(w, 1, Integer::sum); // Elegant counting
        }
        System.out.println(freq); // {cat=3, bird=1, dog=2}

        // --- LinkedHashMap (insertion order) ---
        Map<String, Integer> linkedMap = new LinkedHashMap<>();
        linkedMap.put("Banana", 2);
        linkedMap.put("Apple", 5);
        linkedMap.put("Cherry", 3);
        System.out.println(linkedMap); // {Banana=2, Apple=5, Cherry=3}

        // --- TreeMap (sorted by key) ---
        TreeMap<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("Charlie", 92);
        treeMap.put("Alice", 95);
        treeMap.put("Bob", 87);
        System.out.println(treeMap);           // {Alice=95, Bob=87, Charlie=92}
        System.out.println(treeMap.firstKey()); // Alice
        System.out.println(treeMap.lastKey());  // Charlie
        System.out.println(treeMap.floorKey("Bob")); // Bob
        System.out.println(treeMap.headMap("Charlie")); // {Alice=95, Bob=87}

        // --- Iterating over Map ---
        for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

        // Using forEach with lambda
        hashMap.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}
# Python equivalents of Java Map implementations

# --- dict (equivalent to Java HashMap + LinkedHashMap) ---
# Python dicts maintain insertion order since Python 3.7
hash_map = {}
hash_map["Alice"] = 95
hash_map["Bob"] = 87
hash_map["Charlie"] = 92
print(hash_map.get("Bob"))        # 87
print("Alice" in hash_map)        # True
print(hash_map.get("Dave", 0))    # 0 (default)

# Frequency counting (using Counter — cleanest)
from collections import Counter
words = ["cat", "dog", "cat", "bird", "dog", "cat"]
freq = Counter(words)
print(freq)  # Counter({'cat': 3, 'dog': 2, 'bird': 1})

# Frequency counting (manual — like Java)
freq2 = {}
for w in words:
    freq2[w] = freq2.get(w, 0) + 1

# --- SortedDict (equivalent to Java TreeMap) ---
# from sortedcontainers import SortedDict
# tree_map = SortedDict({"Charlie": 92, "Alice": 95, "Bob": 87})

# --- Iterating over dict ---
for key, value in hash_map.items():
    print(f"{key} -> {value}")
#include <map>
#include <unordered_map>
#include <string>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // --- unordered_map (equivalent to Java HashMap) ---
    unordered_map<string, int> hashMap;
    hashMap["Alice"] = 95;
    hashMap["Bob"] = 87;
    hashMap["Charlie"] = 92;
    cout << hashMap["Bob"] << endl; // 87
    cout << (hashMap.count("Alice") > 0) << endl; // 1 (true)

    // Frequency counting
    vector<string> words = {"cat", "dog", "cat", "bird", "dog", "cat"};
    unordered_map<string, int> freq;
    for (const auto& w : words) {
        freq[w]++;  // Auto-initializes to 0, then increments
    }
    // freq: {cat:3, dog:2, bird:1}

    // --- map (equivalent to Java TreeMap — sorted by key) ---
    map<string, int> treeMap;
    treeMap["Charlie"] = 92;
    treeMap["Alice"] = 95;
    treeMap["Bob"] = 87;
    cout << treeMap.begin()->first << endl;  // Alice (smallest)
    cout << treeMap.rbegin()->first << endl; // Charlie (largest)

    // Iterating (sorted order)
    for (const auto& [key, val] : treeMap) {
        cout << key << " -> " << val << endl;
    }

    return 0;
}

Complexity Analysis

Map Implementation Complexities:

OperationHashMapLinkedHashMapTreeMap
put(k, v)O(1) avgO(1) avgO(log n)
get(k)O(1) avgO(1) avgO(log n)
containsKey(k)O(1) avgO(1) avgO(log n)
remove(k)O(1) avgO(1) avgO(log n)
containsValue(v)O(n)O(n)O(n)
Iteration orderNo guaranteeInsertion orderSorted by key
firstKey()/lastKey()N/AN/AO(log n)
Range viewsN/AN/AO(log n)

Space Complexity: O(n) for all three. HashMap uses a bucket array (default initial capacity 16, load factor 0.75). When the load factor threshold is exceeded, the table rehashes — doubles in size and redistributes entries.

HashMap internals (Java 8+): Each bucket starts as a linked list for collision handling. When a bucket exceeds 8 entries (and total capacity ≥ 64), the bucket converts to a balanced binary tree, improving worst-case lookup from O(n) to O(log n). This treeification is why Java 8 HashMap has O(log n) worst case instead of O(n).

Choosing the right Map:

  • HashMap — Default for 95% of use cases. Fastest overall.
  • LinkedHashMap — When you need predictable iteration order or LRU cache.
  • TreeMap — When you need sorted keys, range queries, or navigable operations.

Queue and Deque Implementations

Intuition

The Queue interface represents a collection designed for holding elements before processing, typically in FIFO (First-In-First-Out) order. The Deque (Double-Ended Queue) interface extends Queue to support insertion and removal at both ends.

Key implementations:

  • ArrayDeque — A resizable circular array that implements both Queue and Deque. It is the recommended implementation for both queue and stack use cases. It's faster than LinkedList for queue operations and faster than the legacy Stack class for stack operations.

  • PriorityQueue — A min-heap by default that always gives you the smallest element first via poll(). Elements are not stored in sorted order internally — only the heap property is maintained (parent ≤ children). Use for priority-based processing, like Dijkstra's algorithm or processing tasks by urgency.

  • LinkedList — Also implements Queue and Deque, but ArrayDeque is preferred for most cases.

Important: Java's PriorityQueue is a min-heap by default (smallest element first). For a max-heap, pass Collections.reverseOrder() or Comparator.reverseOrder() to the constructor.

Modern best practices:

  • For stack behavior: Use ArrayDeque with push() and pop(). Do NOT use the legacy Stack class.
  • For queue behavior: Use ArrayDeque with offer() and poll().
  • For priority-based processing: Use PriorityQueue.

Step-by-Step Explanation

Let's trace through PriorityQueue operations to see how a min-heap works:

Step 1: Create an empty PriorityQueue.

  • Queue<Integer> pq = new PriorityQueue<>();
  • Heap: []

Step 2: Add 30.

  • Heap: [30]
  • Min element: 30

Step 3: Add 10.

  • Insert 10 at end, then "bubble up" because 10 < 30.
  • After heapify: [10, 30]
  • Min element: 10

Step 4: Add 50.

  • Insert 50 at end. 50 > 10 (parent), so no bubbling needed.
  • Heap: [10, 30, 50]
  • Min element: 10

Step 5: Add 20.

  • Insert 20 at end (position 3). Parent is 30 (position 1). 20 < 30, so bubble up.
  • After heapify: [10, 20, 50, 30]
  • Min element: 10

Step 6: Poll (remove min) → returns 10.

  • Move last element (30) to root. Sink down: 30 > 20 (left child), so swap.
  • After heapify: [20, 30, 50]
  • New min: 20

Step 7: Peek → returns 20 (without removing).

  • Heap unchanged: [20, 30, 50]

Step 8: Poll → returns 20.

  • Heap: [30, 50]
  • New min: 30

PriorityQueue (Min-Heap) — Insert and Poll Operations — Watch how a min-heap maintains the smallest element at the top through bubble-up on insert and sink-down on removal.

Algorithm

Queue and Deque Operations:

OperationDescriptionThrows ExceptionReturns Special Value
InsertAdd elementadd(e)offer(e) → returns false
RemoveRemove headremove()poll() → returns null
ExaminePeek at headelement()peek() → returns null

Deque-specific (both ends):

  • offerFirst(e) / offerLast(e) — Add to front/back
  • pollFirst() / pollLast() — Remove from front/back
  • peekFirst() / peekLast() — Examine front/back
  • push(e) / pop() — Stack operations (front end)

PriorityQueue-specific:

  • Always dequeues the minimum element (by natural order or Comparator)
  • add(e) / offer(e) — O(log n) insert with bubble-up
  • poll() — O(log n) remove-min with sink-down
  • peek() — O(1) view minimum without removing

Common Use Cases:

  • ArrayDeque as Queue: BFS traversal, task scheduling
  • ArrayDeque as Stack: DFS, parenthesis matching, backtracking
  • PriorityQueue: Dijkstra's algorithm, K-th smallest/largest, median finding, merge K sorted lists

Code

import java.util.*;

public class QueueDemo {
    public static void main(String[] args) {
        // --- ArrayDeque as Queue (FIFO) ---
        Queue<String> queue = new ArrayDeque<>();
        queue.offer("Task A");
        queue.offer("Task B");
        queue.offer("Task C");
        System.out.println(queue.peek());  // Task A (front)
        System.out.println(queue.poll());  // Task A (removed)
        System.out.println(queue.poll());  // Task B (removed)
        System.out.println(queue);         // [Task C]

        // --- ArrayDeque as Stack (LIFO) ---
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(10);
        stack.push(20);
        stack.push(30);
        System.out.println(stack.peek());  // 30 (top)
        System.out.println(stack.pop());   // 30 (removed)
        System.out.println(stack.pop());   // 20 (removed)

        // --- PriorityQueue (Min-Heap) ---
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        minHeap.add(30);
        minHeap.add(10);
        minHeap.add(50);
        minHeap.add(20);
        System.out.println(minHeap.peek()); // 10 (minimum)
        System.out.println(minHeap.poll()); // 10 (removed)
        System.out.println(minHeap.poll()); // 20 (next minimum)

        // --- PriorityQueue (Max-Heap) ---
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
        maxHeap.add(30);
        maxHeap.add(10);
        maxHeap.add(50);
        System.out.println(maxHeap.peek()); // 50 (maximum)

        // --- PriorityQueue with custom comparator ---
        PriorityQueue<int[]> taskQueue = new PriorityQueue<>(
            (a, b) -> a[0] - b[0]  // Sort by priority (first element)
        );
        taskQueue.offer(new int[]{3, 1}); // priority 3, task 1
        taskQueue.offer(new int[]{1, 2}); // priority 1, task 2
        taskQueue.offer(new int[]{2, 3}); // priority 2, task 3
        System.out.println(Arrays.toString(taskQueue.poll())); // [1, 2] (highest priority)
    }
}
# Python equivalents of Java Queue/Deque/PriorityQueue

# --- deque as Queue (FIFO) ---
from collections import deque
queue = deque()
queue.append("Task A")    # enqueue
queue.append("Task B")
queue.append("Task C")
print(queue[0])            # Task A (peek front)
print(queue.popleft())     # Task A (dequeue)
print(queue.popleft())     # Task B

# --- list as Stack (LIFO) or deque ---
stack = []
stack.append(10)   # push
stack.append(20)
stack.append(30)
print(stack[-1])    # 30 (peek top)
print(stack.pop())  # 30 (pop)

# --- heapq (Min-Heap — equivalent to Java PriorityQueue) ---
import heapq
min_heap = []
heapq.heappush(min_heap, 30)
heapq.heappush(min_heap, 10)
heapq.heappush(min_heap, 50)
heapq.heappush(min_heap, 20)
print(min_heap[0])          # 10 (peek min)
print(heapq.heappop(min_heap))  # 10 (remove min)
print(heapq.heappop(min_heap))  # 20

# --- Max-Heap (negate values) ---
max_heap = []
heapq.heappush(max_heap, -30)
heapq.heappush(max_heap, -10)
heapq.heappush(max_heap, -50)
print(-max_heap[0])         # 50 (peek max)
#include <queue>
#include <stack>
#include <iostream>
#include <functional>
using namespace std;

int main() {
    // --- queue (FIFO — equivalent to Java ArrayDeque as Queue) ---
    queue<string> q;
    q.push("Task A");
    q.push("Task B");
    q.push("Task C");
    cout << q.front() << endl;  // Task A
    q.pop();                    // Remove Task A
    cout << q.front() << endl;  // Task B

    // --- stack (LIFO) ---
    stack<int> st;
    st.push(10);
    st.push(20);
    st.push(30);
    cout << st.top() << endl;   // 30
    st.pop();                   // Remove 30
    cout << st.top() << endl;   // 20

    // --- priority_queue (Max-Heap by default in C++) ---
    priority_queue<int> maxPQ;
    maxPQ.push(30);
    maxPQ.push(10);
    maxPQ.push(50);
    maxPQ.push(20);
    cout << maxPQ.top() << endl;  // 50 (max)
    maxPQ.pop();                  // Remove 50

    // --- Min-Heap ---
    priority_queue<int, vector<int>, greater<int>> minPQ;
    minPQ.push(30);
    minPQ.push(10);
    minPQ.push(50);
    cout << minPQ.top() << endl;  // 10 (min)

    return 0;
}

Complexity Analysis

Queue/Deque Implementation Complexities:

OperationArrayDequePriorityQueueLinkedList
offer / addO(1)*O(log n)O(1)
poll / removeO(1)O(log n)O(1)
peekO(1)O(1)O(1)
push / pop (stack)O(1)N/AO(1)
containsO(n)O(n)O(n)
remove(Object)O(n)O(n)O(n)

*Amortized O(1) — ArrayDeque uses a circular array that doubles when full.

Space Complexity: O(n) for all.

Why ArrayDeque > LinkedList for Queue/Stack:

  • ArrayDeque uses contiguous memory (circular array), leading to much better cache performance.
  • LinkedList allocates a separate node object for each element, scattered across the heap.
  • Benchmarks show ArrayDeque is typically 2-3× faster than LinkedList for queue operations.

Why ArrayDeque > Stack class:

  • The legacy Stack class extends Vector, which is synchronized — unnecessary overhead in single-threaded code.
  • Stack allows indexed access via get(), breaking the stack abstraction.
  • ArrayDeque provides a clean Deque interface with better performance.

Collections Utility Class

Intuition

The java.util.Collections class is a utility class containing only static methods. It provides algorithms that operate on collections (sorting, searching, shuffling, reversing) and factory methods for creating special collection wrappers (unmodifiable, synchronized, singleton collections).

Think of Collections as the Swiss Army knife for collections — instead of writing your own sort or binary search, you call a well-optimized library method. Java uses TimSort internally for Collections.sort(), which is a stable, O(n log n) sorting algorithm optimized for real-world data.

Key capabilities:

  • Sorting: sort(list), sort(list, comparator)
  • Searching: binarySearch(list, key) (requires sorted list)
  • Extremes: min(collection), max(collection)
  • Ordering: reverse(list), shuffle(list), rotate(list, distance)
  • Frequency: frequency(collection, object)
  • Wrappers: unmodifiableList(list), synchronizedMap(map), singleton(object)

Step-by-Step Explanation

Let's trace through sorting and binary searching a list:

Input: list = [5, 2, 8, 1, 9, 3], search target = 8

Step 1: Call Collections.sort(list).

  • TimSort runs: the algorithm identifies existing sorted runs and merges them.
  • After sort: list = [1, 2, 3, 5, 8, 9]

Step 2: Call Collections.binarySearch(list, 8).

  • Binary search on sorted list.
  • Check middle index 2: value 3. Since 3 < 8, search right half.
  • Check index 4: value 8. Found! Return index 4.

Step 3: Call Collections.binarySearch(list, 4).

  • 4 is not in the list.
  • Returns -(insertion point) - 1 = -(3) - 1 = -4.
  • This tells us: 4 would be inserted at index 3 to maintain sorted order.

Step 4: Call Collections.max(list) → returns 9.

Step 5: Call Collections.frequency(list, 5) → returns 1.

Step 6: Call Collections.reverse(list).

  • List becomes: [9, 8, 5, 3, 2, 1].

Collections.sort + binarySearch — Sort Then Search — Watch the array get sorted by TimSort, then see binary search find a target value in O(log n) time.

Algorithm

Most Important Collections Utility Methods:

  1. Sorting: Collections.sort(list) — O(n log n), stable sort. Use sort(list, comparator) for custom ordering.
  2. Searching: Collections.binarySearch(list, key) — O(log n) on sorted list. Returns index if found, or -(insertion point)-1 if not.
  3. Extremes: Collections.min(collection), Collections.max(collection) — O(n).
  4. Ordering: Collections.reverse(list) O(n), Collections.shuffle(list) O(n), Collections.rotate(list, distance) O(n).
  5. Counting: Collections.frequency(collection, object) — O(n).
  6. Immutable: Collections.unmodifiableList(list) — Wraps list to prevent modification.
  7. Thread-safe: Collections.synchronizedMap(map) — Wraps map for thread safety.
  8. Singletons: Collections.singletonList(element) — Immutable single-element list.
  9. Fill: Collections.fill(list, value) — Set all elements to value. O(n).
  10. Swap: Collections.swap(list, i, j) — Swap elements at two positions. O(1).

Code

import java.util.*;

public class CollectionsUtilDemo {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(Arrays.asList(5, 2, 8, 1, 9, 3));

        // --- Sorting ---
        Collections.sort(nums);                    // [1, 2, 3, 5, 8, 9]
        Collections.sort(nums, Comparator.reverseOrder()); // [9, 8, 5, 3, 2, 1]
        Collections.sort(nums);                    // Back to [1, 2, 3, 5, 8, 9]

        // --- Binary Search ---
        int idx = Collections.binarySearch(nums, 8);  // 4
        int missing = Collections.binarySearch(nums, 4); // -4 (not found)
        System.out.println("Index of 8: " + idx);      // 4
        System.out.println("Search for 4: " + missing); // -4

        // --- Min, Max, Frequency ---
        System.out.println("Max: " + Collections.max(nums)); // 9
        System.out.println("Min: " + Collections.min(nums)); // 1

        List<Integer> withDups = Arrays.asList(1, 2, 2, 3, 3, 3);
        System.out.println("Freq of 3: " + Collections.frequency(withDups, 3)); // 3

        // --- Reverse, Shuffle, Swap ---
        Collections.reverse(nums);              // [9, 8, 5, 3, 2, 1]
        Collections.shuffle(nums);              // Random order
        Collections.swap(nums, 0, nums.size() - 1); // Swap first and last

        // --- Immutable collections ---
        List<String> immutable = Collections.unmodifiableList(
            Arrays.asList("A", "B", "C")
        );
        // immutable.add("D"); // Throws UnsupportedOperationException!

        // --- Java 9+ List.of / Map.of ---
        List<String> imm2 = List.of("X", "Y", "Z");    // Immutable
        Map<String, Integer> imm3 = Map.of("a", 1, "b", 2); // Immutable
    }
}
# Python equivalents of Java Collections utility methods
import random
from bisect import bisect_left, insort

nums = [5, 2, 8, 1, 9, 3]

# --- Sorting ---
nums.sort()                        # [1, 2, 3, 5, 8, 9]
nums.sort(reverse=True)            # [9, 8, 5, 3, 2, 1]
nums.sort()                        # [1, 2, 3, 5, 8, 9]

# --- Binary Search (bisect module) ---
idx = bisect_left(nums, 8)         # 4
found = idx < len(nums) and nums[idx] == 8  # True

# --- Min, Max ---
print(max(nums))                   # 9
print(min(nums))                   # 1

# --- Reverse, Shuffle ---
nums.reverse()                     # [9, 8, 5, 3, 2, 1]
random.shuffle(nums)               # Random order

# --- Frequency ---
with_dups = [1, 2, 2, 3, 3, 3]
print(with_dups.count(3))          # 3

# --- Immutable (tuple) ---
immutable = tuple(["A", "B", "C"])
# immutable[0] = "X"  # TypeError!
#include <vector>
#include <algorithm>
#include <numeric>
#include <random>
#include <iostream>
using namespace std;

int main() {
    vector<int> nums = {5, 2, 8, 1, 9, 3};

    // --- Sorting ---
    sort(nums.begin(), nums.end());       // {1, 2, 3, 5, 8, 9}
    sort(nums.begin(), nums.end(), greater<int>()); // {9, 8, 5, 3, 2, 1}
    sort(nums.begin(), nums.end());       // {1, 2, 3, 5, 8, 9}

    // --- Binary Search ---
    bool found = binary_search(nums.begin(), nums.end(), 8); // true
    auto it = lower_bound(nums.begin(), nums.end(), 8);      // Iterator to 8
    int idx = distance(nums.begin(), it);                     // 4

    // --- Min, Max ---
    cout << *max_element(nums.begin(), nums.end()) << endl; // 9
    cout << *min_element(nums.begin(), nums.end()) << endl; // 1

    // --- Reverse, Shuffle ---
    reverse(nums.begin(), nums.end());    // {9, 8, 5, 3, 2, 1}
    random_device rd;
    mt19937 gen(rd());
    shuffle(nums.begin(), nums.end(), gen); // Random

    // --- Count ---
    vector<int> dups = {1, 2, 2, 3, 3, 3};
    cout << count(dups.begin(), dups.end(), 3) << endl; // 3

    // --- Sum ---
    int total = accumulate(nums.begin(), nums.end(), 0);

    return 0;
}

Complexity Analysis

Collections Utility Method Complexities:

MethodTime ComplexityNotes
sort(list)O(n log n)TimSort — stable, adaptive
binarySearch(list, key)O(log n)Requires sorted list
min(collection) / max(collection)O(n)Scans entire collection
reverse(list)O(n)Swaps pairs from ends
shuffle(list)O(n)Fisher-Yates shuffle
frequency(collection, obj)O(n)Linear scan
swap(list, i, j)O(1)Direct swap
fill(list, value)O(n)Sets every element
rotate(list, distance)O(n)Rotates elements

TimSort Details: Java's Collections.sort() uses TimSort, which:

  • Finds existing sorted "runs" in the data (ascending or descending sequences)
  • Merges these runs using a merge sort strategy
  • Falls back to insertion sort for very small runs (< 32 elements)
  • Is stable: equal elements maintain their relative order
  • Is adaptive: nearly-sorted data is sorted in O(n) time
  • Uses O(n) auxiliary space for merging

TimSort was invented by Tim Peters in 2002 for Python and later adopted by Java (since Java 7). It performs exceptionally well on real-world data that often contains partially sorted sequences.