Skip to content

Commit 98f2540

Browse files
feat: add IndexedPriorityQueue implementation and tests
1 parent 0a55165 commit 98f2540

File tree

2 files changed

+610
-0
lines changed

2 files changed

+610
-0
lines changed
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package com.thealgorithms.datastructures.heaps;
2+
3+
import java.util.Comparator;
4+
import java.util.IdentityHashMap;
5+
import java.util.Objects;
6+
import java.util.Arrays;
7+
import java.util.function.Consumer;
8+
9+
/**
10+
* An addressable (indexed) min-priority queue with O(log n) updates.
11+
*
12+
* <p>Key features:
13+
* <ul>
14+
* <li>Each element E is tracked by a handle (its current heap index) via a map,
15+
* enabling O(log n) {@code remove(e)} and O(log n) key updates
16+
* ({@code changeKey/decreaseKey/increaseKey}).</li>
17+
* <li>The queue order is determined by the provided {@link Comparator}. If the
18+
* comparator is {@code null}, elements must implement {@link Comparable}
19+
* (same contract as {@link java.util.PriorityQueue}).</li>
20+
* <li>By default this implementation uses {@link IdentityHashMap} for the index
21+
* mapping to avoid issues with duplicate-equals elements or mutable equals/hashCode.
22+
* If you need value-based equality, switch to {@code HashMap} and read the caveats
23+
* in the class-level Javadoc carefully.</li>
24+
* </ul>
25+
*
26+
* <h2>IMPORTANT contracts</h2>
27+
* <ul>
28+
* <li><b>Do not mutate comparator-relevant fields of an element directly</b> while it is
29+
* inside the queue. Always use {@code changeKey}/{@code decreaseKey}/{@code increaseKey}
30+
* so the heap can be restored accordingly.</li>
31+
* <li>If you replace {@link IdentityHashMap} with {@link HashMap}, you must ensure:
32+
* (a) no two distinct elements are {@code equals()}-equal at the same time in the queue, and
33+
* (b) {@code equals/hashCode} of elements remain stable while enqueued.</li>
34+
* <li>{@code peek()} returns {@code null} when empty (matching {@link java.util.PriorityQueue}).</li>
35+
* <li>Not thread-safe.</li>
36+
* </ul>
37+
*
38+
* <p>Complexities:
39+
* {@code offer, poll, remove(e), changeKey, decreaseKey, increaseKey} are O(log n);
40+
* {@code peek, isEmpty, size, contains} are O(1).
41+
*/
42+
public class IndexedPriorityQueue<E> {
43+
44+
/** Binary heap storage (min-heap). */
45+
private Object[] heap;
46+
47+
/** Number of elements in the heap. */
48+
private int size;
49+
50+
/** Comparator used for ordering; if null, elements must be Comparable. */
51+
private final Comparator<? super E> cmp;
52+
53+
/**
54+
* Index map: element -> current heap index.
55+
* <p>We use IdentityHashMap by default to:
56+
* <ul>
57+
* <li>allow duplicate-equals elements;</li>
58+
* <li>avoid corruption when equals/hashCode are mutable or not ID-based.</li>
59+
* </ul>
60+
* If you prefer value-based semantics, replace with HashMap<E,Integer> and
61+
* respect the warnings in the class Javadoc.
62+
*/
63+
private final IdentityHashMap<E, Integer> index;
64+
65+
private static final int DEFAULT_INITIAL_CAPACITY = 11;
66+
67+
public IndexedPriorityQueue() {
68+
this(DEFAULT_INITIAL_CAPACITY, null);
69+
}
70+
71+
public IndexedPriorityQueue(Comparator<? super E> cmp) {
72+
this(DEFAULT_INITIAL_CAPACITY, cmp);
73+
}
74+
75+
public IndexedPriorityQueue(int initialCapacity, Comparator<? super E> cmp) {
76+
if (initialCapacity < 1) throw new IllegalArgumentException("initialCapacity < 1");
77+
this.heap = new Object[initialCapacity];
78+
this.cmp = cmp;
79+
this.index = new IdentityHashMap<>();
80+
}
81+
82+
/** Returns current number of elements. */
83+
public int size() { return size; }
84+
85+
/** Returns {@code true} if empty. */
86+
public boolean isEmpty() { return size == 0; }
87+
88+
/**
89+
* Returns the minimum element without removing it, or {@code null} if empty.
90+
* Matches {@link java.util.PriorityQueue#peek()} behavior.
91+
*/
92+
@SuppressWarnings("unchecked")
93+
public E peek() { return size == 0 ? null : (E) heap[0]; }
94+
95+
/**
96+
* Inserts the specified element (O(log n)).
97+
* @throws NullPointerException if {@code e} is null
98+
* @throws ClassCastException if {@code cmp == null} and {@code e} is not Comparable,
99+
* or if incompatible with other elements
100+
*/
101+
public boolean offer(E e) {
102+
Objects.requireNonNull(e, "element is null");
103+
if (size >= heap.length) grow(size + 1);
104+
// Insert at the end and bubble up. siftUp will maintain 'index' for all touched nodes.
105+
siftUp(size, e);
106+
size++;
107+
return true;
108+
}
109+
110+
/**
111+
* Removes and returns the minimum element (O(log n)), or {@code null} if empty.
112+
*/
113+
@SuppressWarnings("unchecked")
114+
public E poll() {
115+
if (size == 0) return null;
116+
E min = (E) heap[0];
117+
removeAt(0); // updates map and heap structure
118+
return min;
119+
}
120+
121+
/**
122+
* Removes one occurrence of the specified element e (O(log n)) if present.
123+
* Uses the index map for O(1) lookup.
124+
*/
125+
public boolean remove(Object o) {
126+
Integer i = index.get(o);
127+
if (i == null) return false;
128+
removeAt(i);
129+
return true;
130+
}
131+
132+
/** O(1): returns whether the queue currently contains the given element reference. */
133+
public boolean contains(Object o) {
134+
return index.containsKey(o);
135+
}
136+
137+
/** Clears the heap and the index map. */
138+
public void clear() {
139+
Arrays.fill(heap, 0, size, null);
140+
index.clear();
141+
size = 0;
142+
}
143+
144+
// ------------------------------------------------------------------------------------
145+
// Key update API
146+
// ------------------------------------------------------------------------------------
147+
148+
/**
149+
* Changes comparator-relevant fields of {@code e} via the provided {@code mutator},
150+
* then restores the heap in O(log n) by bubbling in the correct direction.
151+
*
152+
* <p><b>IMPORTANT:</b> The mutator must not change {@code equals/hashCode} of {@code e}
153+
* if you migrate this implementation to value-based indexing (HashMap).
154+
*
155+
* @throws IllegalArgumentException if {@code e} is not in the queue
156+
*/
157+
public void changeKey(E e, Consumer<E> mutator) {
158+
Integer i = index.get(e);
159+
if (i == null) throw new IllegalArgumentException("Element not in queue");
160+
// Mutate fields used by comparator (do NOT mutate equality/hash if using value-based map)
161+
mutator.accept(e);
162+
// Try bubbling up; if no movement occurred, bubble down.
163+
if (!siftUp(i)) siftDown(i);
164+
}
165+
166+
/**
167+
* Faster variant if the new key is strictly smaller (higher priority).
168+
* Performs a single sift-up (O(log n)).
169+
*/
170+
public void decreaseKey(E e, Consumer<E> mutator) {
171+
Integer i = index.get(e);
172+
if (i == null) throw new IllegalArgumentException("Element not in queue");
173+
mutator.accept(e);
174+
siftUp(i);
175+
}
176+
177+
/**
178+
* Faster variant if the new key is strictly larger (lower priority).
179+
* Performs a single sift-down (O(log n)).
180+
*/
181+
public void increaseKey(E e, Consumer<E> mutator) {
182+
Integer i = index.get(e);
183+
if (i == null) throw new IllegalArgumentException("Element not in queue");
184+
mutator.accept(e);
185+
siftDown(i);
186+
}
187+
188+
// ------------------------------------------------------------------------------------
189+
// Internal utilities
190+
// ------------------------------------------------------------------------------------
191+
192+
/** Grows the internal array to accommodate at least {@code minCapacity}. */
193+
private void grow(int minCapacity) {
194+
int old = heap.length;
195+
int pref = (old < 64) ? old + 2 : old + (old >> 1); // +2 if small, else +50%
196+
int newCap = Math.max(minCapacity, pref);
197+
heap = Arrays.copyOf(heap, newCap);
198+
}
199+
200+
@SuppressWarnings("unchecked")
201+
private int compare(E a, E b) {
202+
if (cmp != null) return cmp.compare(a, b);
203+
return ((Comparable<? super E>) a).compareTo(b);
204+
}
205+
206+
/**
207+
* Inserts item {@code x} at position {@code k}, bubbling up while maintaining the heap.
208+
* Also maintains the index map for all moved elements.
209+
*/
210+
@SuppressWarnings("unchecked")
211+
private void siftUp(int k, E x) {
212+
while (k > 0) {
213+
int p = (k - 1) >>> 1;
214+
E e = (E) heap[p];
215+
if (compare(x, e) >= 0) break;
216+
heap[k] = e;
217+
index.put(e, k);
218+
k = p;
219+
}
220+
heap[k] = x;
221+
index.put(x, k);
222+
}
223+
224+
/**
225+
* Attempts to bubble up the element currently at {@code k}.
226+
* @return true if it moved; false otherwise.
227+
*/
228+
@SuppressWarnings("unchecked")
229+
private boolean siftUp(int k) {
230+
int orig = k;
231+
E x = (E) heap[k];
232+
while (k > 0) {
233+
int p = (k - 1) >>> 1;
234+
E e = (E) heap[p];
235+
if (compare(x, e) >= 0) break;
236+
heap[k] = e;
237+
index.put(e, k);
238+
k = p;
239+
}
240+
if (k != orig) {
241+
heap[k] = x;
242+
index.put(x, k);
243+
return true;
244+
}
245+
return false;
246+
}
247+
248+
/** Bubbles down the element currently at {@code k}. */
249+
@SuppressWarnings("unchecked")
250+
private void siftDown(int k) {
251+
int n = size;
252+
E x = (E) heap[k];
253+
int half = n >>> 1; // loop while k has at least one child
254+
while (k < half) {
255+
int child = (k << 1) + 1; // assume left is smaller
256+
E c = (E) heap[child];
257+
int r = child + 1;
258+
if (r < n && compare(c, (E) heap[r]) > 0) {
259+
child = r;
260+
c = (E) heap[child];
261+
}
262+
if (compare(x, c) <= 0) break;
263+
heap[k] = c;
264+
index.put(c, k);
265+
k = child;
266+
}
267+
heap[k] = x;
268+
index.put(x, k);
269+
}
270+
271+
/**
272+
* Removes the element at heap index {@code i}, restoring the heap afterwards.
273+
* <p>Returns nothing; the standard {@code PriorityQueue} returns a displaced
274+
* element in a rare case to help its iterator. We don't need that here, so
275+
* we keep the API simple.
276+
*/
277+
@SuppressWarnings("unchecked")
278+
private void removeAt(int i) {
279+
int n = --size; // last index after removal
280+
E moved = (E) heap[n];
281+
E removed = (E) heap[i];
282+
heap[n] = null; // help GC
283+
index.remove(removed); // drop mapping for removed element
284+
285+
if (i == n) return; // removed last element; done
286+
287+
heap[i] = moved;
288+
index.put(moved, i);
289+
290+
// Try sift-up first (cheap if key decreased); if no movement, sift-down.
291+
if (!siftUp(i)) siftDown(i);
292+
}
293+
}

0 commit comments

Comments
 (0)