This content originally appeared on DEV Community and was authored by AnkitDevCode
Introduction
While Java’s standard collections like List, Set, and Map cover most needs, complex applications often require advanced data structures for efficiency, concurrency, and memory optimization.
This guide explores key advanced Java data structures, their core operations, complexities, and best use cases to help developers build high-performance, scalable applications.
1. PriorityQueue (Heap)
PriorityQueue
in Java (from java.util
) is a queue that arranges elements according to their priority.
PriorityQueue in Java is a min-heap by default, implemented as a binary heap using an array. It maintains elements in a partially ordered tree where the smallest element is always at the root.
A heap is a specialized binary tree-based data structure that satisfies the heap property:
- In a Min-Heap, the parent node is always smaller (or equal) than its children.
- In a Max-Heap, the parent node is always larger (or equal) than its children.
Heaps are typically implemented using arrays (not linked nodes) because the tree is always a complete binary tree (all levels filled except possibly the last, which is filled left to right).
Key Properties
- Time Complexity: O(log n) for insertion and deletion, O(1) for peek
- Space Complexity: O(n)
- Not thread-safe
- Unbounded (grows dynamically)
- Natural ordering or custom Comparator
Basic Syntax for PriorityQueue
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
Core Operations
Operation | Description | Time Complexity |
---|---|---|
add(e) |
Inserts element e into the queue. |
O(log n) |
offer(e) |
Inserts element e into the queue. |
O(log n) |
poll() |
Removes and returns the top (priority) element. | O(log n) |
peek() |
Returns the top element without removing it. | O(1) |
remove() |
Removes a specified element from the queue. | O(n) |
size() |
Returns the number of elements in the queue. | O(1) |
clear() |
Removes all elements from the queue. | O(n) |
contains(e) |
Checks if the queue contains element e . |
O(n) |
Classic Use Cases & Problems
- K Largest/Smallest Elements
- Merge K Sorted Lists
- Dijkstra’s Shortest Path
- Task Scheduler with Priority
- Top K Frequent Elements
- Sliding Window Maximum
- Meeting Rooms II (Minimum Conference Rooms)
- Kth Largest Element in Stream
- Memory Management
Example
1. PriorityQueue
to implement Heap Sort
import java.util.PriorityQueue;
import java.util.Arrays;
public class HeapSortExample {
public static void main(String[] args) {
int[] nums = {20, 5, 15, 10};
// Min-Heap for ascending order
PriorityQueue<Integer> pq = new PriorityQueue<>();
// Add all elements to the heap
for (int num : nums) {
pq.add(num);
}
// Extract elements to get sorted order
int index = 0;
while (!pq.isEmpty()) {
nums[index++] = pq.poll();
}
System.out.println("Sorted array: " + Arrays.toString(nums)); // [5, 10, 15, 20]
}
}
- Add all elements to a Min-Heap (PriorityQueue by default).
- Poll elements one by one → smallest element comes first.
- Store them back → array becomes sorted in ascending order.
- For descending order, use a Max-Heap:
PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
2. PriorityQueue with Objects
import java.util.PriorityQueue;
import java.util.Comparator;
class Student {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return name + " (" + score + ")";
}
}
public class Main {
public static void main(String[] args) {
// Max-Heap: higher score = higher priority
PriorityQueue<Student> pq = new PriorityQueue<>(Comparator.comparingInt((Student s) -> s.score).reversed());
pq.add(new Student("Alice", 85));
pq.add(new Student("Bob", 92));
pq.add(new Student("Charlie", 78));
System.out.println("Students served by priority:");
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
}
}
Rule of Thumb:
- Use Comparable for natural/default sorting.
- Use Comparator for custom/multiple sorting logic.
// Comparable
class Student implements Comparable<Student> {
int score;
public int compareTo(Student s) { return this.score - s.score; }
}
// Comparator
Comparator<Student> byScoreDesc = (a, b) -> b.score - a.score;
Summary
PriorityQueue is the go-to choice when you need efficient access to the minimum/maximum element with frequent insertions and deletions, making it perfect for algorithms that require greedy selection based on priority.
2. Deque (ArrayDeque/LinkedDeque)
Deque (pronounced “deck” is part of the java.util
package ) is a double-ended queue that allows insertion and deletion at both ends. Java provides two main implementations:
- ArrayDeque: Resizable array implementation
- LinkedList: Doubly-linked list implementation (also implements Deque)
Key Properties
- Double-ended operations – Insert/remove from both front and rear
- No capacity restrictions – Dynamically resizable
- Not thread-safe – Requires external synchronization
- Null elements not allowed (ArrayDeque) vs Null allowed (LinkedList)
- Faster than Stack and LinkedList for stack/queue operations
- More memory efficient than LinkedList (ArrayDeque)
- Implements both Queue and Deque interfaces
- Supports both LIFO and FIFO operations
Basic Syntax for ArrayDeque
and LinkedList
import java.util.Deque;
import java.util.ArrayDeque;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
// Using ArrayDeque
Deque<Integer> arrayDeque = new ArrayDeque<>();
arrayDeque.addFirst(10);
arrayDeque.addLast(20);
System.out.println("ArrayDeque peekFirst: " + arrayDeque.peekFirst()); // 10
System.out.println("ArrayDeque peekLast: " + arrayDeque.peekLast()); // 20
arrayDeque.removeFirst();
arrayDeque.removeLast();
// Using LinkedList
Deque<Integer> linkedListDeque = new LinkedList<>();
linkedListDeque.addFirst(30);
linkedListDeque.addLast(40);
System.out.println("LinkedList peekFirst: " + linkedListDeque.peekFirst()); // 30
System.out.println("LinkedList peekLast: " + linkedListDeque.peekLast()); // 40
linkedListDeque.removeFirst();
linkedListDeque.removeLast();
}
}
Core Operations
Operation | ArrayDeque | LinkedList | Description |
---|---|---|---|
addFirst(e) | O(1) amortized | O(1) | Insert at front |
addLast(e) | O(1) amortized | O(1) | Insert at rear |
removeFirst() | O(1) | O(1) | Remove from front |
removeLast() | O(1) | O(1) | Remove from rear |
peekFirst() | O(1) | O(1) | Get first without removing |
peekLast() | O(1) | O(1) | Get last without removing |
pollFirst() | O(1) | O(1) | Remove first (null if empty) |
pollLast() | O(1) | O(1) | Remove last (null if empty) |
size() | O(1) | O(1) | Get number of elements |
isEmpty() | O(1) | O(1) | Check if empty |
contains(o) | O(n) | O(n) | Search for element |
remove(o) | O(n) | O(n) | Remove specific element |
clear() | O(1) | O(1) | Remove all elements |
ArrayDeque vs LinkedList Performance
Aspect | ArrayDeque | LinkedList |
---|---|---|
Memory | Lower overhead | Higher overhead (node pointers) |
Cache Performance | Better (array locality) | Worse (scattered nodes) |
Add/Remove Ends | Faster | Slightly slower |
Random Access | Not supported | Not efficient |
Iterator | Faster | Slower |
Memory Allocation | Bulk allocation | Per-element allocation |
Classic Use Cases & Problems
- Sliding Window Maximum/Minimum – Maintain elements in window
- Palindrome Checking – Compare characters from both ends
- Undo/Redo Functionality – Stack-like operations with flexibility
- BFS with Level Tracking – Process nodes level by level
- Expression Evaluation – Handle nested brackets and operators
- Implementing Stack and Queue – Single data structure for both
- Monotonic Deque Problems – Maintain increasing/decreasing order
- Circular Buffer Implementation – Ring buffer with dynamic size
- Browser History Navigation – Forward/backward navigation
- Task Scheduling – Priority-based insertion at both ends
Example
import java.util.*;
public class SlidingWindowMaximum {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || k <= 0) {
return new int[0];
}
Deque<Integer> deque = new ArrayDeque<>(); // Stores indices
int[] result = new int[nums.length - k + 1];
int resultIndex = 0;
for (int i = 0; i < nums.length; i++) {
// Remove indices that are out of current window
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// Remove indices whose corresponding values are smaller than current
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// Add current index
deque.offerLast(i);
// If we have processed at least k elements, add to result
if (i >= k - 1) {
result[resultIndex++] = nums[deque.peekFirst()];
}
}
return result;
}
public static void main(String[] args) {
SlidingWindowMaximum solution = new SlidingWindowMaximum();
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
int[] result = solution.maxSlidingWindow(nums, k);
System.out.println("Sliding Window Maximum: " + Arrays.toString(result));
// Output: [3, 3, 5, 5, 6, 7]
}
}
Summary
Use ArrayDeque for better performance and LinkedList when frequent insertions/removals in the middle are needed.
3. BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue)
BlockingQueue is a thread-safe queue interface that supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available when storing an element. It’s part of the java.util.concurrent
package.
Key Properties
- Thread-safe – Built-in synchronization for concurrent access
- Blocking operations – Threads wait when queue is full/empty
- Producer-Consumer pattern – Perfect for multi-threaded scenarios
- No null elements – Null values are not permitted
- Capacity management – Can be bounded or unbounded
- Multiple implementations – ArrayBlockingQueue, LinkedBlockingQueue, etc.
- Timeout support – Operations can have time limits
- Collection interface – Implements Queue and Collection interfaces
- Atomic operations – All operations are atomic and thread-safe
- Fair access – Optional fairness policy for thread scheduling
Basic Syntax for ArrayBlockingQueue
and LinkedBlockingQueue
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Main {
public static void main(String[] args) throws InterruptedException {
// ArrayBlockingQueue
BlockingQueue<Integer> arrayQueue = new ArrayBlockingQueue<>(3);
arrayQueue.put(1);
arrayQueue.put(2);
System.out.println("ArrayBlockingQueue peek: " + arrayQueue.peek()); // 1
System.out.println("ArrayBlockingQueue removed: " + arrayQueue.take()); // 1
// LinkedBlockingQueue
BlockingQueue<Integer> linkedQueue = new LinkedBlockingQueue<>(3);
linkedQueue.put(10);
linkedQueue.put(20);
System.out.println("LinkedBlockingQueue peek: " + linkedQueue.peek()); // 10
System.out.println("LinkedBlockingQueue removed: " + linkedQueue.take()); // 10
}
}
Core Operations
Operation / Method | ArrayBlockingQueue | LinkedBlockingQueue | Description |
---|---|---|---|
Insert (Block) put(e)
|
O(1) | O(1) | Blocks until space available |
Insert (Timeout) offer(e, time, unit)
|
O(1) | O(1) | Waits for specified time |
Insert (No Block) offer(e)
|
O(1) | O(1) | Returns false if full |
Insert (Exception) add(e)
|
O(1) | O(1) | Throws exception if full |
Remove (Block) take()
|
O(1) | O(1) | Blocks until element available |
Remove (Timeout) poll(time, unit)
|
O(1) | O(1) | Waits for specified time |
Remove (No Block) poll()
|
O(1) | O(1) | Returns null if empty |
Remove (Exception) remove()
|
O(1) | O(1) | Throws exception if empty |
Examine peek()
|
O(1) | O(1) | Returns head without removing |
Size size()
|
O(1) | O(1) | Current number of elements |
Capacity remainingCapacity()
|
O(1) | O(1) | Available space |
Contains contains(o)
|
O(n) | O(n) | Search for element |
Remove Specific remove(o)
|
O(n) | O(n) | Remove specific element |
Drain drainTo(collection)
|
O(n) | O(n) | Transfer all elements to collection |
ArrayBlockingQueue vs LinkedBlockingQueue features:
Feature | ArrayBlockingQueue | LinkedBlockingQueue |
---|---|---|
Storage | Fixed-size array | Dynamic linked nodes |
Capacity | Fixed at creation | Fixed or unbounded |
Memory | Pre-allocated | Allocated on demand |
Locks | Single lock | Two locks (put/take) |
Performance | Better for bounded queues | Better for high throughput |
GC Pressure | Lower | Higher (node allocation) |
Classic Use Cases & Problems
- Producer-Consumer Pattern – Multiple threads producing/consuming data
- Thread Pool Task Queues – Executor framework uses BlockingQueue
- Rate Limiting and Throttling – Control processing speed
- Batch Processing Systems – Accumulate work units for batch processing
- Event-Driven Architectures – Decouple event producers from consumers
- Message Passing Systems – Inter-thread communication
- Resource Pool Management – Manage limited resources among threads
- Data Pipeline Processing – Chain of processing stages
- Load Balancing – Distribute work across multiple worker threads
- Bounded Buffer Problem – Classic synchronization problem
- Work Stealing Algorithms – Task distribution in parallel computing
- Real-time Data Processing – Handle continuous data streams
- Microservices Communication – Async communication between services
- Cache Warming – Pre-load cache data in background threads
Example
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* Simple Producer-Consumer Example using BlockingQueue
*
* Real scenario: Online Store Order Processing
* - Customer places orders (Producer)
* - Warehouse processes orders (Consumer)
*/
public class SimpleProducerConsumer {
public static void main(String[] args) {
// Shared queue between producer and consumer (max 5 orders)
BlockingQueue<String> orderQueue = new ArrayBlockingQueue<>(5);
// Producer: Customer placing orders
Thread customer = new Thread(() -> {
String[] orders = {
"Order-1: iPhone 14",
"Order-2: MacBook Pro",
"Order-3: AirPods",
"Order-4: iPad",
"Order-5: Apple Watch",
"Order-6: Magic Mouse",
"Order-7: Keyboard"
};
try {
for (String order : orders) {
System.out.println("Customer placing: " + order);
orderQueue.put(order); // Blocks if queue is full
Thread.sleep(500); // Customer thinks for 500ms before next order
}
System.out.println("Customer finished placing all orders");
} catch (InterruptedException e) {
System.out.println("Customer interrupted");
}
});
// Consumer: Warehouse processing orders
Thread warehouse = new Thread(() -> {
try {
Thread.sleep(2000); // Warehouse starts working after 2 seconds
while (true) {
String order = orderQueue.take(); // Blocks if queue is empty
System.out.println("Warehouse processing: " + order);
Thread.sleep(1000); // Takes 1 second to process each order
System.out.println("Warehouse completed: " + order);
}
} catch (InterruptedException e) {
System.out.println("Warehouse stopped working");
}
});
// Start both threads
customer.start();
warehouse.start();
// Let them run for 10 seconds, then stop
try {
Thread.sleep(10000);
warehouse.interrupt(); // Stop the warehouse
customer.join(); // Wait for customer to finish
System.out.println("\n Store closed for the day!");
} catch (InterruptedException e) {
System.out.println("Main interrupted");
}
}
}
Summary
BlockingQueue is the cornerstone of concurrent programming in Java, providing thread-safe, blocking operations that are essential for producer-consumer patterns. ArrayBlockingQueue offers better memory efficiency for bounded scenarios, while LinkedBlockingQueue provides higher throughput for demanding applications. Choose based on your specific concurrency needs, memory constraints, and performance requirements.
4. BitSet
BitSet is a resizable array of bits that provides efficient storage and manipulation of boolean values. Each bit can be set (1) or cleared (0), making it extremely memory-efficient for representing large sets of boolean flags or performing set operations on integers.It’s part of the java.util
package.
Key Properties
- Memory efficient – Uses only 1 bit per boolean value (vs 8 bits for boolean array)
- Dynamically resizable – Grows automatically as needed
- Fast bitwise operations – Hardware-optimized bit manipulation
- Set operations support – AND, OR, XOR, NOT operations
- Thread-unsafe – Requires external synchronization for concurrent access
- Zero-based indexing – Bits are numbered from 0 to size-1
- Automatic bit clearing – Unset bits are automatically 0 (false)
- Efficient iteration – Special methods to iterate over set bits only
- Serializable – Can be serialized/deserialized
- Cloneable – Supports deep copying
- No null values – Only boolean states (set/clear)
- Compact representation – Internally uses long arrays for storage
Basic Syntax for BitSet
import java.util.BitSet;
public class Main {
public static void main(String[] args) {
// Create a BitSet
BitSet bits = new BitSet();
// Set bits
bits.set(0); // set bit at index 0
bits.set(2); // set bit at index 2
bits.set(4, 7); // set bits from index 4 to 6 (7 exclusive)
// Get bits
System.out.println("Bit at index 2: " + bits.get(2)); // true
System.out.println("Bit at index 3: " + bits.get(3)); // false
// Clear bits
bits.clear(2); // clear bit at index 2
// Flip bits
bits.flip(0); // flip bit at index 0
// Size and length
System.out.println("BitSet length: " + bits.length());
System.out.println("BitSet size: " + bits.size());
// Print BitSet
System.out.println("BitSet: " + bits);
}
}
Core Operations
Operation | Method | Time Complexity | Space Complexity | Description |
---|---|---|---|---|
Set Bit | set(int bitIndex) |
O(1) amortized | O(1) | Set bit at index to 1 |
Set Range | set(int from, int to) |
O(n) | O(1) | Set multiple bits in range |
Clear Bit | clear(int bitIndex) |
O(1) | O(1) | Set bit at index to 0 |
Clear Range | clear(int from, int to) |
O(n) | O(1) | Clear multiple bits in range |
Clear All | clear() |
O(1) | O(1) | Clear all bits |
Get Bit | get(int bitIndex) |
O(1) | O(1) | Get bit value at index |
Get Range | get(int from, int to) |
O(n) | O(n) | Get BitSet subset |
Flip Bit | flip(int bitIndex) |
O(1) | O(1) | Toggle bit at index |
Flip Range | flip(int from, int to) |
O(n) | O(1) | Toggle multiple bits |
AND Operation | and(BitSet set) |
O(n) | O(1) | Logical AND with another BitSet |
OR Operation | or(BitSet set) |
O(n) | O(1) | Logical OR with another BitSet |
XOR Operation | xor(BitSet set) |
O(n) | O(1) | Logical XOR with another BitSet |
AND-NOT | andNot(BitSet set) |
O(n) | O(1) | AND with complement of set |
Size | size() |
O(1) | O(1) | Number of bits of space used |
Length | length() |
O(1) | O(1) | Highest set bit index + 1 |
Cardinality | cardinality() |
O(n) | O(1) | Number of set bits |
Is Empty | isEmpty() |
O(1) | O(1) | Check if no bits are set |
Next Set Bit | nextSetBit(int from) |
O(1) avg | O(1) | Find next set bit from index |
Next Clear Bit | nextClearBit(int from) |
O(1) avg | O(1) | Find next clear bit from index |
Previous Set Bit | previousSetBit(int from) |
O(1) avg | O(1) | Find previous set bit |
Previous Clear Bit | previousClearBit(int from) |
O(1) avg | O(1) | Find previous clear bit |
Intersects | intersects(BitSet set) |
O(n) | O(1) | Check if any bits are common |
Clone | clone() |
O(n) | O(n) | Create deep copy |
To Byte Array | toByteArray() |
O(n) | O(n) | Convert to byte array |
To Long Array | toLongArray() |
O(n) | O(n) | Convert to long array |
BitSet vs Alternatives
Use Case | BitSet | boolean[] | Set | EnumSet |
---|---|---|---|---|
Memory Efficiency | Excellent | Poor | Very Poor | Excellent |
Performance | Excellent | Good | Poor | Excellent |
Set Operations | Native | Manual | Good | Native |
Range Operations | Native | Manual | Manual | Limited |
Thread Safety | No | No | Depends | No |
Sparse Data | Good | Poor | Excellent | N/A |
Classic Use Cases & Problems
- Sieve of Eratosthenes – Finding prime numbers efficiently
- Bloom Filters – Probabilistic membership testing
- Set Operations – Union, intersection, difference of integer sets
- Graph Algorithms – Visited node tracking in BFS/DFS
- Permutation Generation – Track used elements in backtracking
- Bit Manipulation Problems – Subset generation, bit counting
- Database Indexing – Bitmap indexes for fast queries
- Compiler Design – Register allocation, live variable analysis
- Image Processing – Binary image representation and operations
- Game Development – Collision detection, state flags
- Network Protocols – Flag management, packet filtering
- Cryptography – One-time pad, stream ciphers
- Data Compression – Run-length encoding preprocessing
- Memory Management – Free/allocated block tracking
- Scheduling Algorithms – Resource availability tracking
- Machine Learning – Feature selection, sparse data representation
Example
Finding Primes Using BitSet
import java.util.BitSet;
public class SieveBitSet {
public static void main(String[] args) {
int N = 50;
BitSet primes = new BitSet(N + 1);
// Initially assume all numbers >=2 are prime
primes.set(2, N + 1); // set bits 2..N
for (int i = 2; i * i <= N; i++) {
if (primes.get(i)) {
// Mark multiples of i as non-prime
for (int j = i * i; j <= N; j += i) {
primes.clear(j);
}
}
}
// Print primes
System.out.println("Primes up to " + N + ":");
for (int i = 2; i <= N; i++) {
if (primes.get(i)) {
System.out.print(i + " ");
}
}
}
}
Summary
BitSet is a specialized, highly efficient data structure for boolean operations and set manipulation. It shines in mathematical algorithms, large-scale boolean flag management, and memory-constrained environments. Choose BitSet when you need fast bitwise operations on large datasets, especially for algorithms like sieve methods, graph traversal state tracking, or any scenario involving set theory operations on integers. Its memory efficiency (1 bit per boolean vs 8 bits for boolean arrays) makes it invaluable for applications handling millions of boolean flags.
5. CopyOnWriteArrayList/CopyOnWriteArraySet
CopyOnWriteArrayList
is a thread-safe variant of ArrayList (in java.util.concurrent
) where all mutative operations (add, set, remove, etc.) create a fresh copy of the underlying array. Its iterators work on an immutable snapshot, so they never throw ConcurrentModificationException
.
CopyOnWriteArraySet
is a thread-safe Set that uses CopyOnWriteArrayList internally. It enforces uniqueness with addIfAbsent, and iterators also operate on a snapshot.
Both are ideal for read-heavy, infrequently modified data—iterations are fast and lock-free, while writes are costly due to array copying. Common use cases include listener registries, configuration snapshots, and caches. They are not suitable for write-heavy workloads because of high write overhead and GC pressure.
Key Properties
- Thread-safe, suitable for concurrent read-heavy scenarios.
- All mutative operations (add, set, remove) create a fresh copy of the underlying array.
- Iterators are weakly consistent: do not throw ConcurrentModificationException.
- Reads are lock-free, writes are expensive due to array copying.
Basic Syntax for CopyOnWriteArrayList
and CopyOnWriteArraySet
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
public class Main {
public static void main(String[] args) {
// -------------------------
// CopyOnWriteArrayList
// -------------------------
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
System.out.println("CopyOnWriteArrayList elements:");
for (String fruit : list) {
System.out.println(fruit);
}
list.remove("Banana");
System.out.println("After removal: " + list);
// -------------------------
// CopyOnWriteArraySet
// -------------------------
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // duplicate ignored
System.out.println("\nCopyOnWriteArraySet elements:");
for (String fruit : set) {
System.out.println(fruit);
}
set.remove("Banana");
System.out.println("After removal: " + set);
}
}
Core Operations
Operation | Method | Time Complexity | Description |
---|---|---|---|
Add | add(E e) |
O(n) | Adds element at the end |
Remove | remove(Object o) |
O(n) | Removes first occurrence |
Get | get(int index) |
O(1) | Retrieves element at index |
Set | set(int index, E element) |
O(n) | Replaces element at index |
Contains | contains(Object o) |
O(n) | Checks if element exists |
Size | size() |
O(1) | Returns number of elements |
Iterator | iterator() |
O(1) | Returns snapshot iterator |
Clear | clear() |
O(n) | Removes all elements |
Example
import java.util.concurrent.CopyOnWriteArrayList;
// Subscriber interface
interface Subscriber {
void notify(String message);
}
// Event Bus
class EventBus {
private CopyOnWriteArrayList<Subscriber> subscribers = new CopyOnWriteArrayList<>();
// Add a subscriber
public void subscribe(Subscriber subscriber) {
subscribers.add(subscriber);
}
// Remove a subscriber
public void unsubscribe(Subscriber subscriber) {
subscribers.remove(subscriber);
}
// Publish an event to all subscribers
public void publish(String message) {
for (Subscriber s : subscribers) {
s.notify(message);
}
}
}
// Demo
public class Main {
public static void main(String[] args) {
EventBus bus = new EventBus();
// Add subscribers
bus.subscribe(msg -> System.out.println("Subscriber 1 received: " + msg));
bus.subscribe(msg -> System.out.println("Subscriber 2 received: " + msg));
// Publish events
bus.publish("Event A");
bus.publish("Event B");
// Remove a subscriber
bus.unsubscribe(msg -> System.out.println("Subscriber 2 received: " + msg));
// Publish another event
bus.publish("Event C");
}
}
- Multiple threads can publish events (read/iterate) safely while subscribers are added/removed.
- Iteration is snapshot-based, so no ConcurrentModificationException occurs.
- Writes (adding/removing subscribers) are rare, which suits the cost of copying the array.
Classic Use Cases & Problems
- Event listener lists where reads are frequent but modifications are rare.
- Caching read-heavy data structures.
- Pub-sub systems where many threads read concurrently.
- Iterating safely without worrying about ConcurrentModificationException.
- Maintaining snapshots in concurrent applications.
Problems / Considerations:
- High memory and CPU overhead if writes are frequent (copying entire array on every write).
- Not suitable for write-heavy workloads.
Summary
Use CopyOnWriteArrayList / CopyOnWriteArraySet when reads far exceed writes and you need thread-safe, snapshot-style iteration without extra synchronization. Avoid them for write-heavy workloads due to the high cost of copying the array on every modification.
6. Disjoint Set Union (DSU) / Union-Find in Java
A Disjoint Set Union (DSU) is a data structure that keeps track of a partition of a set into disjoint (non-overlapping) subsets. It is especially useful in problems involving connectivity, merging groups, and cycle detection.
Supports two main operations:
- Find: Determine which subset a particular element belongs to.
- Union: Merge two subsets into a single subset.
Optimizations include:
- Path Compression in
find()
- Union by Rank / Size
in
union()`
Key Properties
- Efficient for connectivity problems in graphs (like detecting cycles, Kruskal’s MST algorithm).
- Each element belongs to exactly one subset.
- Supports dynamic connectivity queries.
- Almost constant time per operation: O(α(n)), where α(n) is the inverse Ackermann function.
Core Operations
Operation | Method | Time Complexity | Description |
---|---|---|---|
Find | find(int x) |
O(α(n)) | Returns the representative (root) of element x |
Union | union(int x, int y) |
O(α(n)) | Merges the sets containing x and y |
Connected | connected(int x, int y) |
O(α(n)) | Checks if x and y belong to the same set |
Size |
size() (optional) |
O(1) | Returns the number of disjoint sets |
Classic Use Cases
- Kruskal’s Minimum Spanning Tree algorithm.
- Detecting cycles in undirected graphs.
- Dynamic connectivity problems (e.g., network connectivity).
- Union of sets in clustering algorithms or percolation problems.
Example
public class DSU {
private int[] parent;
private int[] rank;
// Initialize n elements
public DSU(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // each element is its own parent
rank[i] = 0;
}
}
// Find with path compression
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// Union by rank
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return;
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
// Check if two elements are connected
public boolean connected(int x, int y) {
return find(x) == find(y);
}
}
// Demo
public class Main {
public static void main(String[] args) {
DSU dsu = new DSU(5);
dsu.union(0, 1);
dsu.union(1, 2);
System.out.println(dsu.connected(0, 2)); // true
System.out.println(dsu.connected(0, 3)); // false
}
}
Summary
Use Disjoint Set Union for connectivity problems and graph algorithms.
Highly efficient due to path compression and union by rank, almost constant time per operation. Ideal for Kruskal’s MST, dynamic connectivity, clustering, and percolation problems.
7. Fenwick Tree (Binary Indexed Tree) in Java
A Fenwick Tree, also called Binary Indexed Tree (BIT), is a data structure that efficiently supports:
-
Prefix sum queries (sum of first
k
elements) -
Point updates (update a single element)
- Time complexity: O(log n) for both query and update.
- Space complexity: O(n).
- Ideal for scenarios where you have frequent prefix sum or cumulative frequency queries with dynamic updates.
Key Properties
- Efficient for range sum queries on mutable arrays.
- Uses binary representation of indices to traverse parent/child relationships.
- Supports additive operations (sum, XOR, etc.).
- Not suitable for arbitrary range updates unless combined with advanced variants (like range update BIT).
Core Operations
Operation | Method | Time Complexity | Description |
---|---|---|---|
Update / Add | update(index, value) |
O(log n) | Adds value to element at index |
Query / PrefixSum | query(index) |
O(log n) | Returns sum of elements [0..index] |
Build Tree | Constructor / build(arr)
|
O(n) | Initializes tree from array |
Classic Use Cases
- Range sum queries with dynamic updates (e.g., cumulative frequency tables).
- Counting inversions in an array.
- Competitive programming problems involving prefix sums, frequencies, or 2D variants.
- Efficiently implementing histograms or cumulative statistics.
Example
public class FenwickTree {
private int[] bit; // Binary Indexed Tree array (1-based indexing)
private int size; // Size of the original array
/**
* Constructs a Fenwick Tree for an array of a given size.
* The tree is initialized with all zeros.
*
* @param size The size of the original array for which the Fenwick Tree is built.
*/
public FenwickTree(int size) {
this.size = size;
this.bit = new int[size + 1]; // 1-based indexing, so size + 1
}
/**
* Updates the value at a specific index in the original array by adding 'delta'.
* This update propagates through the Fenwick Tree.
*
* @param index The 0-based index in the original array to update.
* @param delta The value to add to the element at the given index.
*/
public void update(int index, int delta) {
// Convert to 1-based indexing for BIT operations
index++;
while (index <= size) {
bit[index] += delta;
// Move to the next parent node by adding the rightmost set bit
index += (index & -index);
}
}
/**
* Queries the prefix sum up to a specific index (inclusive) in the original array.
*
* @param index The 0-based index in the original array up to which to calculate the sum.
* @return The sum of elements from index 0 to 'index' in the original array.
*/
public int query(int index) {
int sum = 0;
// Convert to 1-based indexing for BIT operations
index++;
while (index > 0) {
sum += bit[index];
// Move to the next parent node by subtracting the rightmost set bit
index -= (index & -index);
}
return sum;
}
/**
* Calculates the sum of elements within a given range [left, right] (inclusive, 0-based).
*
* @param left The 0-based starting index of the range.
* @param right The 0-based ending index of the range.
* @return The sum of elements from 'left' to 'right'.
*/
public int rangeSum(int left, int right) {
if (left > right) {
throw new IllegalArgumentException("Left index cannot be greater than right index.");
}
return query(right) - (left > 0 ? query(left - 1) : 0);
}
// Example Usage
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
FenwickTree ft = new FenwickTree(arr.length);
// Initialize Fenwick Tree with values from the array
for (int i = 0; i < arr.length; i++) {
ft.update(i, arr[i]);
}
System.out.println("Prefix sum up to index 2 (0-based): " + ft.query(2)); // Expected: 1 + 2 + 3 = 6
System.out.println("Prefix sum up to index 4 (0-based): " + ft.query(4)); // Expected: 1 + 2 + 3 + 4 + 5 = 15
// Update value at index 2 (change 3 to 10)
ft.update(2, 7); // Add 7 to the existing value at index 2 (3 + 7 = 10)
System.out.println("Prefix sum up to index 2 after update: " + ft.query(2)); // Expected: 1 + 2 + 10 = 13
System.out.println("Range sum from index 1 to 3 (0-based): " + ft.rangeSum(1, 3)); // Expected: 2 + 10 + 4 = 16
}
}
Summary
Use Fenwick Tree for dynamic prefix sums or cumulative frequency queries.
Efficient alternative to segment trees for point updates and prefix queries. Time complexity per operation: O(log n), space complexity: O(n).
Ideal for competitive programming, range queries, and real-time cumulative statistics.
8. Trie (Prefix Tree) in Java
A Trie, also known as a Prefix Tree, is a tree-like data structure used to store a dynamic set of strings efficiently, particularly for operations involving prefixes.
- There is no built-in Trie in Java.
- Typically implemented manually using custom classes with
Map
or arrays. - Can use
java.util.Map
orjava.util.HashMap
for child nodes.
Key Properties
- Tree-based data structure storing strings or sequences.
- Each node represents a single character or part of the key.
- Supports fast prefix-based searches.
- Root node represents empty string.
- Efficient for word lookup, autocomplete, and dictionary operations.
- Can store boolean flag to mark end of a word.
Core Operations
Operation | Method/Description | Time Complexity | Description |
---|---|---|---|
Insert | insert(String word) |
O(L) | Add a word of length L |
Search Exact | search(String word) |
O(L) | Returns true if word exists |
Starts With | startsWith(String prefix) |
O(L) | Returns true if any word starts with given prefix |
Delete | delete(String word) |
O(L) | Removes a word |
Traverse | traverse() |
O(N*L) | Traverse all words stored (N words of avg length L) |
Classic Use Cases & Problems
-
Use Cases:
- Autocomplete / Search suggestions
- Spell checking
- IP routing (storing prefixes)
- Dictionary / word frequency problems
- Longest prefix matching
-
Problems / Considerations:
- High memory usage for large alphabets if implemented naïvely.
- Insertion and search depend on word length, not number of words.
- Typically implemented with arrays (fixed alphabet) or HashMaps for children.
Example
import java.util.HashMap;
import java.util.Map;
class TrieNode {
Map children = new HashMap<>();
boolean isEndOfWord = false;
}
class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// Insert a word
public void insert(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new TrieNode());
node = node.children.get(ch);
}
node.isEndOfWord = true;
}
// Search for exact word
public boolean search(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
node = node.children.get(ch);
if (node == null) return false;
}
return node.isEndOfWord;
}
// Search for prefix
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char ch : prefix.toCharArray()) {
node = node.children.get(ch);
if (node == null) return false;
}
return true;
}
}
// Demo
public class Main {
public static void main(String[] args) {
Trie trie = new Trie();
trie.insert("apple");
trie.insert("app");
trie.insert("banana");
System.out.println(trie.search("apple")); // true
System.out.println(trie.search("app")); // true
System.out.println(trie.search("appl")); // false
System.out.println(trie.startsWith("ap")); // true
System.out.println(trie.startsWith("ba")); // true
}
}
Summary
Use Trie when you need efficient prefix searches or word lookups.
Ideal for autocomplete, dictionary, spell check, and IP prefix matching.
Efficient in search time (O(L)) but can be memory-intensive for large datasets.
Use HashMap children for dynamic alphabets, or arrays for fixed small alphabets.
This content originally appeared on DEV Community and was authored by AnkitDevCode