Skip to content

Commit 71ca10e

Browse files
committed
Add a data structure keeping n unique last-recently-used items
This is similar to the LinkedHashMap structure, except that an already-existing item can be easily put to the front of the list, or the back. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent 6470e08 commit 71ca10e

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/*
2+
* #%L
3+
* SciJava Common shared library for SciJava software.
4+
* %%
5+
* Copyright (C) 2009 - 2014 Board of Regents of the University of
6+
* Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
7+
* Institute of Molecular Cell Biology and Genetics.
8+
* %%
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, are permitted provided that the following conditions are met:
11+
*
12+
* 1. Redistributions of source code must retain the above copyright notice,
13+
* this list of conditions and the following disclaimer.
14+
* 2. Redistributions in binary form must reproduce the above copyright notice,
15+
* this list of conditions and the following disclaimer in the documentation
16+
* and/or other materials provided with the distribution.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
* POSSIBILITY OF SUCH DAMAGE.
29+
* #L%
30+
*/
31+
32+
package org.scijava.util;
33+
34+
import java.util.HashMap;
35+
import java.util.HashSet;
36+
import java.util.Iterator;
37+
import java.util.Map;
38+
import java.util.Set;
39+
40+
/**
41+
* A simple container for {@code N} last-recently-used items.
42+
*
43+
* @author Johannes Schindelin
44+
*/
45+
public class LastRecentlyUsed<T> implements Iterable<T> {
46+
private final Object[] entries;
47+
private final Map<T, Integer> map;
48+
/**
49+
* The double-linked list pointers.
50+
* <p>
51+
* The {@code top} variable points to the most recently added, the
52+
* {@code bottom} variable to the oldest entry. The {@code next} and
53+
* {@code previous} arrays point to the next newer/next older entry.
54+
* </p>
55+
* <p>
56+
* For initialization performance, all of {@code next}, {@code previous},
57+
* {@code top} and {@code bottom} are initialized to {@code 0}, meaning that
58+
* you need to decrement the values by one in order to obtain the entry index.
59+
* Example: the index of the most recently added entry is {@code top -1}, and
60+
* {@code next[top - 1]} is {@code 0} because there is no newer entry than the
61+
* newest entry.
62+
* </p>
63+
*/
64+
private final int[] next, previous;
65+
private int top, bottom;
66+
67+
public LastRecentlyUsed(int size) {
68+
entries = new Object[2 * size];
69+
next = new int[2 * size];
70+
previous = new int[2 * size];
71+
map = new HashMap<T, Integer>();
72+
}
73+
74+
/**
75+
* Given the index of an entry, returns the index of the next newer entry.
76+
*
77+
* @param index the index of the current entry, or -1 to wrap around to the oldest entry.
78+
* @return the index of the next newer entry, or -1 when there is no such entry.
79+
*/
80+
public int next(int index) {
81+
return index < 0 ? bottom - 1 : next[index] - 1;
82+
}
83+
84+
/**
85+
* Given the index of an entry, returns the index of the next older entry.
86+
*
87+
* @param index the index of the current entry, or -1 to wrap around to the newest entry.
88+
* @return the index of the next older entry, or -1 when there is no such entry.
89+
*/
90+
public int previous(int index) {
91+
return index < 0 ? top - 1 : previous[index] - 1;
92+
}
93+
94+
/**
95+
* Returns the entry for the given index.
96+
*
97+
* @param index the index of the entry
98+
* @return the entry
99+
*/
100+
@SuppressWarnings("unchecked")
101+
public T get(int index) {
102+
return (T) entries[index];
103+
}
104+
105+
/**
106+
* Looks up the index for a given entry.
107+
*
108+
* @param value the value of the entry to find
109+
* @return the corresponding index, or {@code -1} if the entry was not found
110+
*/
111+
public int lookup(final T value) {
112+
final Integer result = map.get(value);
113+
return result == null ? -1 : (int) result;
114+
}
115+
116+
/**
117+
* Add a new newest entry.
118+
*
119+
* @param value the value of the entry
120+
* @return whether the entry was added
121+
*/
122+
public boolean add(final T value) {
123+
return add(value, false);
124+
}
125+
126+
/**
127+
* Add a new oldest entry.
128+
* <p>
129+
* This method helps recreating {@link LastRecentlyUsed} instances given the
130+
* entries in the order newest first, oldest last.
131+
* </p>
132+
*
133+
* @param value the value of the entry to add
134+
*/
135+
public void addToEnd(final T value) {
136+
add(value, true);
137+
}
138+
139+
public boolean replace(final int index, T newValue) {
140+
final Object previousValue = get(index);
141+
if (previousValue == null) {
142+
throw new IllegalArgumentException("No current entry at position " +
143+
index);
144+
}
145+
if (newValue.equals(previous)) return false;
146+
map.remove(previousValue);
147+
map.put(newValue, index);
148+
entries[index] = newValue;
149+
return true;
150+
}
151+
152+
/**
153+
* Empties the data structure.
154+
*/
155+
public void clear() {
156+
top = bottom = 0;
157+
map.clear();
158+
for (int i = 0; i < entries.length; i++) {
159+
entries[i] = null;
160+
next[i] = previous[i] = 0;
161+
}
162+
}
163+
164+
/**
165+
* Returns an {@link Iterator}.
166+
*
167+
* @return the iterator
168+
*/
169+
public Iterator<T> iterator() {
170+
return new Iterator<T>() {
171+
172+
private int position = top - 1;
173+
174+
@Override
175+
public boolean hasNext() {
176+
return position >= 0;
177+
}
178+
179+
@Override
180+
public T next() {
181+
@SuppressWarnings("unchecked")
182+
final T result = (T) entries[position];
183+
position = previous[position] - 1;
184+
return result;
185+
}
186+
187+
@Override
188+
public void remove() {
189+
LastRecentlyUsed.this.remove(position == 0 ? top - 1 : next[position] - 1);
190+
}
191+
192+
};
193+
}
194+
195+
// -- private methods
196+
197+
private void remove(int position) {
198+
assert(entries[position] != null);
199+
map.remove(entries[position]);
200+
entries[position] = null;
201+
if (next[position] == 0) {
202+
top = previous[position];
203+
}
204+
else {
205+
previous[next[position] - 1] = previous[position];
206+
}
207+
if (previous[position] == 0) {
208+
bottom = next[position];
209+
}
210+
else {
211+
next[previous[position] - 1] = next[position];
212+
}
213+
next[position] = previous[position] = 0;
214+
}
215+
216+
private boolean add(final T value, boolean addAtEnd) {
217+
final Integer existing = map.get(value);
218+
int insert;
219+
if (existing != null) {
220+
insert = existing;
221+
remove(insert);
222+
}
223+
else if (map.size() == entries.length / 2) {
224+
insert = bottom - 1;
225+
remove(insert);
226+
}
227+
else {
228+
insert = value.hashCode() % entries.length;
229+
if (insert < 0) insert += entries.length;
230+
while (insert < entries.length && entries[insert] != null) insert++;
231+
}
232+
add(insert, value, addAtEnd);
233+
return existing == null;
234+
}
235+
236+
private void add(int position, T value, boolean atEnd) {
237+
assert(next[position] == 0);
238+
assert(previous[position] == 0);
239+
assert(entries[position] == null);
240+
241+
map.put(value, position);
242+
entries[position] = value;
243+
if (atEnd) {
244+
next[position] = bottom;
245+
if (bottom > 0) previous[bottom - 1] = position + 1;
246+
bottom = position + 1;
247+
if (top == 0) top = bottom;
248+
}
249+
else {
250+
previous[position] = top;
251+
if (top > 0) {
252+
next[top - 1] = position + 1;
253+
}
254+
top = position + 1;
255+
if (bottom == 0) bottom = top;
256+
}
257+
}
258+
259+
// For testing
260+
protected void assertConsistency() {
261+
if (top == 0) {
262+
assert(bottom == 0);
263+
assert(map.size() == 0);
264+
for (int i = 0; i < entries.length; i++) {
265+
assert(entries[i] == null);
266+
assert(next[i] == 0);
267+
assert(previous[i] == 0);
268+
}
269+
return;
270+
}
271+
assert(bottom != 0);
272+
final Set<Integer> indices = new HashSet<Integer>(map.values());
273+
assert(indices.size() == map.size());
274+
for (int i = 0; i < entries.length; i++) {
275+
if (indices.contains(i)) {
276+
assert(entries[i] != null);
277+
assert(map.get(entries[i]) == i);
278+
if (i == top - 1 || top == bottom) {
279+
assert(next[i] == 0);
280+
}
281+
else {
282+
assert(next[i] > 0);
283+
assert(previous[next[i] - 1] == i + 1);
284+
}
285+
if (i == bottom - 1 || top == bottom) {
286+
assert(previous[i] == 0);
287+
}
288+
else {
289+
assert(previous[i] > 0);
290+
assert(next[previous[i] - 1] == i + 1);
291+
}
292+
}
293+
else {
294+
assert(entries[i] == null);
295+
assert(next[i] == 0);
296+
assert(previous[i] == 0);
297+
}
298+
}
299+
}
300+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* #%L
3+
* SciJava Common shared library for SciJava software.
4+
* %%
5+
* Copyright (C) 2009 - 2014 Board of Regents of the University of
6+
* Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
7+
* Institute of Molecular Cell Biology and Genetics.
8+
* %%
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, are permitted provided that the following conditions are met:
11+
*
12+
* 1. Redistributions of source code must retain the above copyright notice,
13+
* this list of conditions and the following disclaimer.
14+
* 2. Redistributions in binary form must reproduce the above copyright notice,
15+
* this list of conditions and the following disclaimer in the documentation
16+
* and/or other materials provided with the distribution.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
* POSSIBILITY OF SUCH DAMAGE.
29+
* #L%
30+
*/
31+
32+
package org.scijava.util;
33+
34+
import static org.junit.Assert.assertEquals;
35+
36+
import org.junit.Test;
37+
38+
/**
39+
* Tests the {@link LastRecentlyUsed} data structure.
40+
*
41+
* @author Johannes Schindelin
42+
*/
43+
public class LastRecentlyUsedTest {
44+
45+
@Test
46+
public void test() {
47+
int count = 3;
48+
final LastRecentlyUsed<String> lru = new LastRecentlyUsed<String>(count);
49+
50+
for (int i = 1; i <= count; i++) {
51+
lru.add("" + i);
52+
}
53+
54+
int position = -1;
55+
for (int i = 1; i <= count; i++) {
56+
position = lru.next(position);
57+
assertEquals("" + i, lru.get(position));
58+
}
59+
position = lru.next(position);
60+
assertEquals(-1, position);
61+
62+
for (int i = count; i >= 1; i--) {
63+
position = lru.previous(position);
64+
assertEquals("" + i, lru.get(position));
65+
}
66+
position = lru.previous(position);
67+
assertEquals(-1, position);
68+
}
69+
}

0 commit comments

Comments
 (0)