In Java you can build up an ArrayList
with items and then call:
Collections.sort(list, comparator);
Is there anyway to pass in the Comparator at the time of list, creation like you can do with TreeMap
?
The goal is to be able add an element to the list and instead of having it automatically appended to the end of the list, the list would keep itself sorted based on the Comparator
and insert the new element at the index determined by the Comparator
. So basically the list might have to re-sort upon every new element added.
Is there anyway to achieve this in this way with the Comparator
or by some other similar means?
You can change the behaviour of ArrayList
List<MyType> list = new ArrayList<MyType>() {
public boolean add(MyType mt) {
super.add(mt);
Collections.sort(list, comparator);
return true;
}
};
Note: a PriorityQueue is NOT a List, if you didn't care what type of collection it was, the simplest would be to use a TreeSet, which is just like a TreeMap but is a collection. The only advantage PriorityQueue has is to allow duplicates.
Note: resorting is not very efficient for large collections, Using a binary search and inserting an entry would be faster. (but more complicated)
EDIT: A lot depends on what you need the "list" to do. I suggest you write a List wrapper for an ArrayList, LinkedList, PriorityQueue, TreeSet, or one of the other sorted collections and implement the methods which will actually be used. That way you have a good understanding of the requirements for the collection and you can make sure it works correctly for you.
EDIT(2): Since there was so much interest in using binarySearch instead. ;)
List<MyType> list = new ArrayList<MyType>() {
public boolean add(MyType mt) {
int index = Collections.binarySearch(this, mt);
if (index < 0) index = ~index;
super.add(index, mt);
return true;
}
};
Everyone is suggesting PriorityQueue
. However, it is important to realize that if you iterate over the contents of a PriorityQueue
, the elements will not be in sorted order. You are only guaranteed to get the "minimum" element from the methods peek()
, poll()
, etc.
A TreeSet
seems to be a better fit. The caveats would be that, as a Set
, it can't contain duplicate elements, and it doesn't support random access with an index.
Commentary
There is probably a good reason that there is no SortedList
implementation in the JDK. I personally can't think of a reason to have one auto-sort in the JDK.
It reeks of premature optimization gone wrong. If the list is not read from as often as it is inserted into, then you are wasting cycles sorting repeatedly for no reason. Sorting right before a read would be much more reactive and having a boolean
somewhere indicating that the list does or does not need to be sorted before reading it would be even better.
The thing is you only really care about order when traversing the list with an Iterator
or for each
loop, so calling Collections.sort()
before any code that iterates would probably be more performant than trying to keep the list sorted all the time on every insertion.
There are ambiguities with List
because of duplicates, how do you order duplicates deterministically? There is SortedSet
, and that makes sense because of the uniqueness. But sorting a List
can have more complications from the side effects of duplicates and other constraints like making every object Comparable
or as I show in my code having to have a Comparator
that can do the work instead.
Sorting on .add()
If you have some very special situation where a auto-sorting List
would be useful then one thing you might do is sub-class a List
implementation and over-ride .add()
to do a Collections.sort(this, comparator)
that you pass into a custom constructor. I used LinkedList
instead of ArrayList
for a reason, ArrayList
is a natural insertion sorted order List
to begin with. It also has the ability to .add()
at an index which is pretty useless if you want a constantly sorted List
, that would have to be handled in someway that would probably be less than ideal. According to the Javadoc;
void add(int index, Object element)
Inserts the specified element at the specified position in this list (optional operation).
So it just throwing UnSupportedOperationException
would be acceptable, or you could just ignore the index
and delegate to .add(Object element);
if you document it in a JavaDoc on the method.
Usually when you want to lots of inserts/removals and sorting you would use a LinkedList
because of better performance characteristics given the usage of the `List'.
Here is a quick example:
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
public class SortedList<E> extends LinkedList<E>
{
private Comparator<E> comparator;
public SortedList(final Comparator<E> comparator)
{
this.comparator = comparator;
}
/**
* this ignores the index and delegates to .add()
* so it will be sorted into the correct place immediately.
*/
@Override
public void add(int index, Object element)
{
this.add(element);
}
@Override
public boolean add(final E e)
{
final boolean result = super.add(e);
Collections.sort(this, this.comparator);
return result;
}
}
Most Efficient Solution:
Alternatively you could only sort when getting the Iterator
and this would be more performance oriented if the sorted order was only really important when iterating over the List
. This would cover the use case of the client code not having to call, Collections.sort()
before every iteration and encapsulate that behavior into the class.
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
public class SortedList<E> extends LinkedList<E>
{
private Comparator<E> comparator;
public SortedList(final Comparator<E> comparator)
{
this.comparator = comparator;
}
@Override
public Iterator<E> iterator()
{
Collections.sort(this, this.comparator);
return super.iterator();
}
}
Of course there would need to be error checking and handling to see if the Comparator
was null
or not and what to do if that was the case, but this gives you the idea. You still don't have any deterministic way to deal with duplicates.
Guava Solution:
If you are using Guava and you should be, you can just use
Ordering.immutableSortedCopy()
only when you need to iterate and be done with it.
Something like TreeSet (or TreeMultiset in case you need duplicates) with more efficient random access is possible, but I doubt it was implemented in Java. Making each node of the tree remembers the size of its left subtree allows accessing an element by index in time O(log(size))
which is not bad.
In order to implement it, you'd need to rewrite a good portion of the underlying TreeMap.
The main difference between SortedSet and List is:
- SortedSet keeps it's element in the right order, but you can't really access a specific element by index.
- List allows indexed access, and arbitrary ordering of the elements. It also allows changing any element (by index or Iterator) to another element, without that the location changes.
You seem to want kind of a fusion of both: automatic sorting, and allowing (reasonable fast) index access. Depending on the size of data and how often indexed reading or adding new elements occur, these are my ideas:
- a wrapped ArrayList, where the add method used a ListIterator to find the insertion spot, then inserting the element there. This is O(n) for insertions, O(1) for indexed access.
- a wrapped LinkedList, where the add method used a ListIterator to find the insertion spot, then inserting the element there. (This still is O(n) for insertions (with sometimes quite smaller factor as ArrayList, sometimes even more), as well as indexed access.)
- a modified binary tree keeping track of the sizes of both halves on each level, thus enabling indexed access. (This would be O(log n) for each access, but needs some extra programming, as it is not yet included in the Java SE. Or you find some library that can this.)
In any case, the interfaces and contracts of SortedSet and List are not really compatible, so you'll want the List part be read-only (or read-and-delete-only), not allowing setting and adding, and having an extra object (maybe implementing the Collection interface) for adding Objects.
I would use a Guava TreeMultiset assuming you want a List
because you may have duplicate elements. It'll do everything you want. The one thing it won't have is index-based access, which doesn't make much sense given that you aren't putting elements at indices of your choosing anyway. The other thing to be aware of is that it won't actually store duplicates of equal
objects... just a count of the total number of them.
commons-collections have TreeBag
Initially I suggested PriorityQueue
, but its iteration order is undefined, so it's no use, unless you iterate it by getting the head of a clone of the queue until it gets empty.
Since you are most likely concerned with the iteration order, I believe you can override the iterator()
method:
public class OrderedIterationList<E> extends ArrayList<E> {
@Override
public Iterator<E> iterator() {
Object[] array = this.toArray(); // O(1)
Arrays.sort(array);
return Arrays.asList(array).iterator(); // asList - O(1)
}
}
You can improve this by storing a snapshot of the sorted collection, and use modCount
to verify whether the collection is not changed.
Depending on the use-cases, this may be less or more efficient than Peter's suggestion. For example if you add multiple items, and iterate. (without adding items between iterations), then this might be more efficient.
The obvious solution is to create your own class that implements the java.util.List
interface and takes a Comparator
as an argument to the constructor. You would use the comparator in the right spots, i.e. the add
method would iterate through the existing items and insert the new item at the right spot. You would disallow calls to methods like add(int index, Object obj)
and so on.
In fact, someone has to have created this already... a quick Google search reveals at least one example:
http://www.ltg.ed.ac.uk/NITE/nxt/apidoc/net/sourceforge/nite/util/SortedList.html
The only way to have any sorted structure with less than O(n) time to add/indexOf/remove/get element is using a tree. In that case operations generally have O(log2n) and traverse is like O(1).
O(n) is just a linked list.
Edit: inserting into linked list w/ binary search. For inserts operations, not using binary structure, and not small sizes, that should be optimal.
@Peter: There is the algo w/ O(log2n) compares (which are slow) to insert and O(n) moves. If you need to override LinkedList, so be it. But that's as neat as it can get. I keep the algorithm as clean as possible to be easily understandable, it can be optimized a little.
package t1;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;
public class SortedList {
private static <T> int binarySearch(ListIterator<? extends Comparable<? super T>> i, T key){
int low = 0;
int high= i.previousIndex();
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = get(i, mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid;
}
return -(low + 1); // key not found
}
private static <T> T get(ListIterator<? extends T> i, int index) {
T obj = null;
int pos = i.nextIndex();
if (pos <= index) {
do {
obj = i.next();
} while (pos++ < index);
} else {
do {
obj = i.previous();
} while (--pos > index);
}
return obj;
}
private static void move(ListIterator<?> i, int index) {
int pos = i.nextIndex();
if (pos==index)
return;
if (pos < index) {
do {
i.next();
} while (++pos < index);
}
else {
do {
i.previous();
} while (--pos > index);
}
}
@SuppressWarnings("unchecked")
static <T> int insert(List<? extends Comparable<? super T>> list, T key){
ListIterator<? extends Comparable<? super T>> i= list.listIterator(list.size());
int idx = binarySearch(i, key);
if (idx<0){
idx=~idx;
}
move(i, idx);
((ListIterator<T>)i).add(key);
return i.nextIndex()-1;
}
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<Integer>();
LinkedList<Integer> unsorted = new LinkedList<Integer>();
Random r =new Random(11);
for (int i=0;i<33;i++){
Integer n = r.nextInt(17);
insert(list, n);
unsorted.add(n);
}
System.out.println(" sorted: "+list);
System.out.println("unsorted: "+unsorted);
}
Consider indexed-tree-map that I created while facing a similar problem, you will be able to access elements by index and get index of elements while keeping the sort order. Duplicates can be put into arrays as values under the same key.
In the JavaFX TransformationList hierarchy, there is something called a SortedList. The list is entirely observable so that additions/removals will notify any other listeners watching the list.
The basic approach to doing this is you watch another ObservableList for changes and strategically use Collections.binarySearch() as others have suggested to locate the index of the addition or removal in Olog(n) time.
There is one problem that I have not seen mentioned here and that is the ability to track items added that have the same compareTo signature, i.e. T1.compareTo(T2) == 0. In this case the sorted list (I will post my own source code below) must have a wrapper element type, that I will call Element. This is similar to what the creators in JavaFX did with SortedList. The reason for this is entirely due to removal operations, it is impossible to locate the original element if there are compareTo duplicates. Normally in a NavigableSet implementation like TreeSet, these duplicates would never enter the Set. A list is different.
I have a library of observable lists that can be effectively chained together (very similar to Java Streams) that fully propagate results downstream as the previous source in the chain updates.
Class Hierarchy
Interface
/**
* Binds the elements of this list to the function application of each element of a
* source observable list.
* <p>
* While a {@code IListContentBinding} is bound, any attempt to modify its contents
* will result in an {@code UnsupportedOperationException}. To unbind the list, call
* {@link #unbind() unbind}.
*
* @param <S> The element type of the input source list that will generate change
* events.
* @param <T> The element type of this output list.
*/
public interface IListContentBinding<S, T> extends ObservableList<T>, ObservableListValue<T>, IContentBinding {... details not shown ....}
Abstract Base Class for all Binding Types (Sort, Distinct, Map, FlatMap, etc.)
/**
* Binds the elements of this list to the function application of each element of a
* source observable list.
* <p>
* While a {@code ListContentBinding} is bound, any attempt to modify its contents
* will result in an {@code UnsupportedOperationException}. To unbind the list, call
* {@link #unbind() unbind}.
*
* @param <S> The element type of the source list that will generate change events.
* @param <T> The element type of this binding.
*/
public abstract class ListContentBinding<S, T> extends ObservableListWrapper<T>
implements IListContentBinding<S, T> {.... details not shown ....}
Sort Binding Class
/**
* A {@code ListContentBinding} implementation that generates sorted elements from a
* source list. The comparator can be set to another {@code Comparator} function at
* any time through the {@link #comparatorProperty() comparator} property.
* <p>
* Unlike the Collections {@link Collections#sort(List) list sort} or Arrays
* {@link Arrays#sort(Object[]) array sort}, once this binding has been added to the
* order of duplicate elements cannot be guaranteed to match the original order of
* the source list. That is the insertion and removal mechanism do not guarantee that
* the original order of duplicates (those items where T1.compareTo(T2) == 0) is
* preserved. However, any removal from the source list is <i>guaranteed</i> to
* remove the exact object from this sorted list. This is because an int <i>ID</i> field
* is added to the wrapped item through the {@link Element} class to ensure that
* matching duplicates can be further compared.
* <p>
* Added/Removed objects from the source list are placed inside this sorted list
* through the {@link Arrays#binarySearch(Object[], Object, Comparator) array binary
* search} algorithm. For any duplicate item in the sorted list, a further check on
* the ID of the {@code Element} corresponding to that item is compared to the
* original, and that item. Each item added to this sorted list increases the
* counter, the maximum number of items that should be placed in this list should be
* no greater than {@code Integer.MAX_VALUE - Integer.MIN_VALUE}, or 4,294,967,295
* total elements. Sizes greater than this value for an instance of this class
* may produce unknown behavior.
* <p>
* Removal and additions to this list binding are proportional to <i>O(logn)</i>
* runtime, where <i>n</i> is the current total number of elements in this sorted
* list.
*
* @param <T> The element type of the source and this list binding.
*/
class ListContentSortBinding<T> extends ListContentBinding<T, T> implements IListContentSortBinding<T> {
/**
* Each location in the source list has a random value associated it with to deal
* with duplicate elements that would return T1.compareTo(T2) == 0.
*/
private Element[] elements = newElementArray(10);
/**
* The same elements from {@link #elements} but placed in their correct sorted
* position according to the {@link #elementComparator element comparator}.
*/
protected Element[] sortedElements = newElementArray(10);
/**
* Create a new instance.
*
* @param source The source observable list. Sorted elements will be generated
* from the source and set as the content of this list binding.
* @param comparator The sorter. An observable that will update the comparator of
* this binding when invalidated. The sorter can be set to another
* {@code Comparator} function at anytime through the
* {@link #comparatorProperty() comparator} property.
* @param options The options of this binding. Considers {@code DependencyOption}
* instances.
* <p>
* All bindings consider {@code BeforeChangeOption} and
* {@code AfterChangeOption}.
*/
@SafeVarargs
ListContentSortBinding(ObservableList<T> source, ObservableObjectValue<Comparator<? super T>> comparator,
BindingOption<T, T>... options) {
this(source, comparator.get(), options);
comparatorProperty().bind(comparator);
}
/**
* Create a new instance.
*
* @param source The source observable list. Sorted elements will be generated
* from the source and set as the content of this list binding.
* @param comparator The sorter. The sorter can be set to another
* {@code Comparator} function at anytime through the
* {@link #comparatorProperty() comparator} property.
* @param options The options of this binding. Considers {@code DependencyOption}
* instances.
* <p>
* All bindings consider {@code BeforeChangeOption} and
* {@code AfterChangeOption}.
*/
@SafeVarargs
ListContentSortBinding(ObservableList<T> source, Comparator<? super T> comparator,
BindingOption<T, T>... options) {
super(new ArrayList<>(), options);
List<Observable> observables = new ArrayList<>(
Arrays.asList(BindingOptionBuilder.extractDependencies(options)));
setComparator(comparator);
observables.add(comparatorProperty());
bind(source, observables.toArray(new Observable[observables.size()]));
}
@Override
protected void sourceChanged(Change<? extends T> change) {
List<? extends T> source = change.getList();
while (change.next()) {
int from = change.getFrom();
if (change.wasPermutated() || change.wasUpdated()) {
List<? extends T> srcMod = source.subList(from, change.getTo());
removed(source, from, srcMod.size());
added(source, from, srcMod);
} else {
List<? extends T> removed = change.getRemoved();
List<? extends T> added = change.getAddedSubList();
if (change.wasReplaced()) {
int min = Math.min(added.size(), removed.size());
replaced(source, from, added.subList(0, min));
added = added.subList(min, added.size());
removed = removed.subList(min, removed.size());
}
if (removed.size() > 0) {
removed(source, from, removed.size());
}
if (added.size() > 0) {
if (source.size() >= elements.length) {
ensureSize(source.size());
}
added(source, from, added);
}
ensureSize(source.size());
}
}
}
/**
* Replace the items in this sorted list binding resulting from a replacement
* operation in the source list. For each of the items added starting at the
* <i>from</i> index in the source list, and items was removed at the same source
* position.
*
* @param source The source list.
* @param from The index of where the replacement started in the source
* (inclusive). The removed and added elements occurred starting at
* the same source position.
* @param added The added source elements from the change.
*/
@SuppressWarnings({})
private void replaced(List<? extends T> source, int from, List<? extends T> added) {
int oldSize = size();
for (int i = 0; i < added.size(); i++) {
int index = from + i;
Element e = elements[index];
// Find the old element and remove it
int pos = findPosition(e, index, oldSize);
System.arraycopy(sortedElements, pos + 1, sortedElements, pos, oldSize - pos - 1);
remove(pos);
T t = added.get(i);
// Create a new element and add it
e = new Element(t);
elements[index] = e;
pos = findPosition(e, index, oldSize - 1);
if (pos < 0) {
pos = ~pos;
}
System.arraycopy(sortedElements, pos, sortedElements, pos + 1, oldSize - pos - 1);
sortedElements[pos] = e;
add(pos, t);
}
}
/**
* Add the elements from the source observable list to this binding.
*
* @param source The source list.
* @param from The index of where the addition started in the source (inclusive).
* @param added The added source elements from the change.
*/
@SuppressWarnings({})
private void added(List<? extends T> source, int from, List<? extends T> added) {
if (size() == 0) {
int size = added.size();
Element[] temp = newElementArray(size);
for (int i = 0; i < added.size(); i++) {
T t = added.get(i);
Element e = new Element(t);
elements[i] = e;
temp[i] = e;
}
if (elementComparator == null) {
addAll(added);
return;
}
Arrays.sort(temp, elementComparator);
System.arraycopy(temp, 0, sortedElements, 0, temp.length);
addAll(Arrays.stream(temp).map(e -> (T) e.t).collect(Collectors.toList()));
return;
}
int size = size();
System.arraycopy(elements, from, elements, from + added.size(), size - from);
for (int i = 0; i < added.size(); i++) {
int index = from + i;
T t = added.get(i);
Element e = new Element(t);
int pos = findPosition(e, index, size);
if (pos < 0) {
pos = ~pos;
}
elements[index] = e;
if (pos < size) {
System.arraycopy(sortedElements, pos, sortedElements, pos + 1, size - pos);
}
sortedElements[pos] = e;
add(pos, t);
size++;
}
}
/**
* Remove the elements from this binding that were removed from the source list.
* Update the {@link #elements} mapping.
*
* @param source The source list.
* @param from The index of where the removal started in the source (inclusive).
* @param removedSize The total number of removed elements from the source list
* for the change.
*/
@SuppressWarnings({})
private void removed(List<? extends T> source, int from, int removedSize) {
if (source.size() == 0) {
elements = newElementArray(10);
sortedElements = newElementArray(10);
elementCounter = Integer.MIN_VALUE;
clear();
return;
}
int oldSize = size();
int size = oldSize;
for (int i = 0; i < removedSize; i++) {
int index = from + i;
Element e = elements[index];
int pos = findPosition(e, index, size);
System.arraycopy(sortedElements, pos + 1, sortedElements, pos, size - pos - 1);
remove(pos);
sortedElements[--size] = null;
}
System.arraycopy(elements, from + removedSize, elements, from, oldSize - from - removedSize);
for (int i = size; i < oldSize; i++) {
elements[i] = null;
}
}
/**
* Locate the position of the element in this sorted binding by performing a
* binary search. A binary search locates the index of the add in Olog(n) time.
*
* @param e The element to insert.
* @param sourceIndex The index of the source list of the modification.
* @param size The size of the array to search, exclusive.
*
* @return The position in this binding that the element should be inserted.
*/
private int findPosition(Element e, int sourceIndex, int size) {
if (size() == 0) {
return 0;
}
int pos;
if (elementComparator != null) {
pos = Arrays.binarySearch(sortedElements, 0, size, e, elementComparator);
} else {
pos = sourceIndex;
}
return pos;
}
/**
* Ensure that the element array is large enough to handle new elements from the
* source list. Also shrinks the size of the array if it has become too large
* with respect to the source list.
*
* @param size The minimum size of the array.
*/
private void ensureSize(int size) {
if (size >= elements.length) {
int newSize = size * 3 / 2 + 1;
Element[] replacement = newElementArray(newSize);
System.arraycopy(elements, 0, replacement, 0, elements.length);
elements = replacement;
replacement = newElementArray(newSize);
System.arraycopy(sortedElements, 0, replacement, 0, sortedElements.length);
sortedElements = replacement;
} else if (size < elements.length / 4) {
int newSize = size * 3 / 2 + 1;
Element[] replacement = newElementArray(newSize);
System.arraycopy(elements, 0, replacement, 0, replacement.length);
elements = replacement;
replacement = newElementArray(newSize);
System.arraycopy(sortedElements, 0, replacement, 0, replacement.length);
sortedElements = replacement;
}
}
/**
* Combines the {@link #comparatorProperty() item comparator} with a secondary
* comparison if the items are equal through the <i>compareTo</i> operation. This
* is used to quickly find the original item when 2 or more items have the same
* comparison.
*/
private Comparator<Element> elementComparator;
/**
* @see #comparatorProperty()
*/
private ObjectProperty<Comparator<? super T>> comparator =
new SimpleObjectProperty<Comparator<? super T>>(this, "comparator") {
@Override
protected void invalidated() {
Comparator<? super T> comp = get();
if (comp != null) {
elementComparator = Comparator.nullsLast((e1, e2) -> {
int c = comp.compare(e1.t, e2.t);
return c == 0 ? Integer.compare(e1.id, e2.id) : c;
});
} else {
elementComparator = null;
}
}
};
@Override
public final ObjectProperty<Comparator<? super T>> comparatorProperty() {
return comparator;
}
@Override
public final Comparator<? super T> getComparator() {
return comparatorProperty().get();
}
@Override
public final void setComparator(Comparator<? super T> comparator) {
comparatorProperty().set(comparator);
}
@Override
protected void onInvalidating(ObservableList<T> source) {
clear();
ensureSize(source.size());
added(source, 0, source);
}
/**
* Counter starts at the Integer min value, and increments each time a new
* element is requested. If this list becomes empty, the counter is restarted at
* the min value.
*/
private int elementCounter = Integer.MIN_VALUE;
/**
* Generate a new array of {@code Element}.
*
* @param size The size of the array.
*
* @return A new array of null Elements.
*/
@SuppressWarnings("unchecked")
private Element[] newElementArray(int size) {
return new ListContentSortBinding.Element[size];
}
Wrapper Element Class
/**
* Wrapper class to further aid in comparison of two object types <T>. Since
* sorting in a list allows duplicates we must assure that when a removal occurs
* from the source list feeding this binding that the removed element matches. To
* do this we add an arbitrary <i>int</i> field inside this element class that
* wraps around the original object type <T>.
*/
final class Element {
/** Object */
private final T t;
/** ID helper for T type duplicates */
private int id;
Element(T t) {
this.t = Objects.requireNonNull(t);
this.id = elementCounter++;
}
@Override
public String toString() {
return t.toString() + " (" + id + ")";
}
}
}
JUNIT VERIFICATION TEST
@Test
public void testSortBinding() {
ObservableList<IntWrapper> source = FXCollections.observableArrayList();
int size = 100000;
for (int i = 0; i < size / 2; i++) {
int index = (int) (Math.random() * size / 10);
source.add(new IntWrapper(index));
}
ListContentSortBinding<IntWrapper> binding =
(ListContentSortBinding<IntWrapper>) CollectionBindings.createListBinding(source).sortElements();
Assert.assertEquals("Sizes not equal for sorted binding | Expected: " +
source.size() + ", Actual: " + binding.size(),
source.size(), binding.size());
List<IntWrapper> sourceSorted = new ArrayList<>(source);
Collections.sort(sourceSorted);
for (int i = 0; i < source.size(); i++) {
IntWrapper expected = sourceSorted.get(i);
IntWrapper actual = binding.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.value, actual.value);
}
System.out.println("Sorted Elements Equal: Complete.");
// Randomly add chunks of elements at random locations in the source
int addSize = size / 10000;
for (int i = 0; i < size / 4; i++) {
List<IntWrapper> added = new ArrayList<>();
int toAdd = (int) (Math.random() * addSize);
for (int j = 0; j < toAdd; j++) {
int index = (int) (Math.random() * size / 10);
added.add(new IntWrapper(index));
}
int atIndex = (int) (Math.random() * source.size());
source.addAll(atIndex, added);
}
sourceSorted = new ArrayList<>(source);
Collections.sort(sourceSorted);
for (int i = 0; i < source.size(); i++) {
IntWrapper expected = sourceSorted.get(i);
IntWrapper actual = binding.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.value, actual.value);
}
System.out.println("Sorted Elements Equal - Add Multiple Elements: Complete.");
// Remove one element at a time from the source list and compare
// to the elements that were removed from the sorted binding
// as a result. They should all be identical index for index.
List<IntWrapper> sourceRemoved = new ArrayList<>();
List<IntWrapper> bindingRemoved = new ArrayList<>();
ListChangeListener<IntWrapper> bindingListener = change -> {
while (change.next()) {
if (change.wasRemoved()) {
bindingRemoved.addAll(change.getRemoved());
}
}
};
// Watch the binding for changes after the upstream source changes
binding.addListener(bindingListener);
for (int i = 0; i < size / 4; i++) {
int index = (int) (Math.random() * source.size());
IntWrapper removed = source.remove(index);
sourceRemoved.add(removed);
}
for (int i = 0; i < bindingRemoved.size(); i++) {
IntWrapper expected = bindingRemoved.get(i);
IntWrapper actual = sourceRemoved.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected + ", Actual: " + actual,
expected.value, actual.value);
Assert.assertEquals("Element refs not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.r, actual.r, 0);
}
System.out.println("Sorted Remove Single Element: Complete.");
// Replace random elements from the source list
bindingRemoved.clear();
sourceRemoved.clear();
int removeSize = size / 10000;
for (int i = 0; i < size / 1000; i++) {
int replaceIndex = (int) (Math.random() * source.size());
int index = (int) (Math.random() * size / 10);
IntWrapper replace = new IntWrapper(index);
source.set(replaceIndex, replace);
}
sourceSorted = new ArrayList<>(source);
Collections.sort(sourceSorted);
for (int i = 0; i < source.size(); i++) {
IntWrapper expected = sourceSorted.get(i);
IntWrapper actual = binding.get(i);
Assert.assertEquals("Elements not equal in expected sorted lists | Expected: " +
expected.value + ", Actual: " + actual.value,
expected.value, actual.value);
}
System.out.println("Sorted Elements Replace: Complete.");
// Remove random chunks from the source list
bindingRemoved.clear();
sourceRemoved.clear();
Set<IntWrapper> sourceRemovedSet =
Collections.newSetFromMap(new IdentityHashMap<>()); // set for speed
while (source.size() > 0) {
int index = (int) (Math.random() * source.size());
int toRemove = (int) (Math.random() * removeSize);
toRemove = Math.min(toRemove, source.size() - index);
List<IntWrapper> removed = source.subList(index, index + toRemove);
sourceRemovedSet.addAll(new ArrayList<>(removed));
removed.clear(); // triggers list change update to binding
}
Assert.assertEquals(bindingRemoved.size(), sourceRemovedSet.size());
// The binding removed will not necessarily be placed in the same order
// since the change listener on the binding will make sure that the final
// order of the change from the binding is in the same order as the binding
// element sequence. We therefore must do a contains() to test.
for (int i = 0; i < bindingRemoved.size(); i++) {
IntWrapper expected = bindingRemoved.get(i);
Assert.assertTrue("Binding Removed Did Not Contain Source Removed",
sourceRemovedSet.contains(expected));
}
System.out.println("Sorted Removed Multiple Elements: Complete.");
}
JUNIT BENCHMARK TEST
@Test
public void sortBindingBenchmark() {
ObservableList<IntWrapper> source = FXCollections.observableArrayList();
ObservableList<IntWrapper> binding =
(ListContentSortBinding<IntWrapper>) CollectionBindings.createListBinding(source).sortElements();
int size = 200000;
Set<IntWrapper> toAdd = new TreeSet<>();
while (toAdd.size() < size) {
int index = (int) (Math.random() * size * 20);
toAdd.add(new IntWrapper(index));
}
// Randomize the order
toAdd = new HashSet<>(toAdd);
System.out.println("Sorted Binding Benchmark Setup: Complete.");
long time = System.currentTimeMillis();
for (IntWrapper w : toAdd) {
source.add(w);
}
long bindingTime = System.currentTimeMillis() - time;
System.out.println("Sorted Binding Time: Complete.");
source.clear(); // clear the list and re-add
ObservableList<IntWrapper> sortedList = new SortedList<>(source);
time = System.currentTimeMillis();
for (IntWrapper w : toAdd) {
source.add(w);
}
long sortedListTime = System.currentTimeMillis() - time;
System.out.println("JavaFX Sorted List Time: Complete.");
// Make the test "fair" by adding a listener to an observable
// set that populates the sorted set
ObservableSet<IntWrapper> obsSet = FXCollections.observableSet(new HashSet<>());
Set<IntWrapper> sortedSet = new TreeSet<>();
obsSet.addListener((SetChangeListener<IntWrapper>) change -> {
sortedSet.add(change.getElementAdded());
});
time = System.currentTimeMillis();
for (IntWrapper w : toAdd) {
obsSet.add(w);
}
long setTime = System.currentTimeMillis() - time;
System.out.println("Sorted Binding Benchmark Time: Complete");
Assert.assertEquals(sortedSet.size(), binding.size());
System.out.println("Binding: " + bindingTime + " ms, " +
"JavaFX Sorted List: " + sortedListTime + " ms, " +
"TreeSet: " + setTime + " ms");
}
Wrapper Class for Tests Only
/**
* Wrapper class for testing sort bindings. Verifies that duplicates were sorted
* and removed correctly based on the object instance.
*/
private static class IntWrapper implements Comparable<IntWrapper> {
static int counter = Integer.MIN_VALUE;
final int value;
final int id;
IntWrapper(int value) {
this.value = value;
this.id = counter++;
}
I also find it mind-boggling that this does not exist in the Java standard libraries. (But good luck with proposing the addition of any new class to the JDK team! I have never had luck with that.)
Assuming that your compareTo
function is a proper transitive relation, then the fastest algorithm for this (assuming the list is read approximately as many times as it is written) is to override List.add
with a method that performs a binary search to find the insertion point of the new item before inserting it. This is O(log(N)) in the number of added elements.
The best way to do this would be to override the add implementation of a list. I'm going to use a LinkedList to demonstrate it, as it allows for efficient insertion.
public boolean add(Integer e)
{
int i = 0;
for (Iterator<Integer> it = this.iterator(); it.hasNext();)
{
int current = it.next();
if(current > e)
{
super.add(i, e);
return true;
}
i++;
}
return super.add(e);
}
The above code creates a sorted list of integers, that is always sorted. It can easily be modified to work with any other datatype. However here you will have to avoid using the add(index, value)
function, as that would obviously break the sorting.
Although people above suggested using Arrays.sort(), I would avoid that, as it can be a significantly less efficient approach, especially since the sort method must be called with every addition to the list.
The contract of the ListIterator interface makes it a bit cumbersome, but this method will perform the insertion using a single scan of the list (up to the insertion point):
private void add(Integer value) {
ListIterator<Integer> listIterator = list.listIterator();
Integer next = null;
while (listIterator.hasNext()) {
next = listIterator.next();
if (next.compareTo(value) > 0) {
break;
}
}
if (next == null || next.compareTo(value) < 0) {
listIterator.add(value);
} else {
listIterator.set(value);
listIterator.add(next);
}
}
SortedSet
Any implementation of the SortedSet
interface carries your desired behavior.
By default, objects added are sorted in their natural order, that is, based on their implementation of the Comparable::compareTo
interface method.
Alternatively, you can pass a Comparator
implementation to determine the sort.
TreeSet
The TreeSet
is a commonly used implantation of SortedSet
. You can also find others.
Duplicates
The major difference between a List
and a SortedSet
is duplicates, objects that compare as equal. A List
allows duplicates whereas a SortedSet
, like any Set
, does not.
Access by index
Another difference is that a Set
cannot be accessed by index. You can not locate an object by its position number within the collection.
If you need such access after constructing your SortedSet
, make a List
. There are multiple ways to do this, such as passing the SortedSet
to constructor of ArrayList
. A mote recent way, as of Java 10, is to make an umodifiable List
by passing the SortedSet
to List.copyOf
.
As stated clearly prior, the need for a sorted list over a SortedSet
is the need for indexing and duplicates. I needed both.
As of Java8, java.util.List<E>
has a "conscientious" List<E>.sort()
method.
This implementation is a stable, adaptive, iterative mergesort that requires far fewer than n lg(n) comparisons when the input array is partially sorted.
My list is pre-loaded prior to being referenced and load-order is almost always the order; references need to be very fast. Therefore, the only change to user177800
's answer is for me to defer to the now provided sort:
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
public class SortedList<E> extends LinkedList<E>
{
private Comparator<E> comparator;
public SortedList(final Comparator<E> comparator)
{
this.comparator = comparator;
}
/**
* this ignores the index and delegates to .add()
* so it will be sorted into the correct place immediately.
*/
@Override
public void add(int index, Object element)
{
this.add(element);
}
@Override
public boolean add(final E e)
{
final boolean result = super.add(e);
this.sort(this, this.comparator);
return result;
}
}
I don't know how many items will be held ahead, so LinkedList<E>
.
Aaaand now I notice that Collections.sort(List<T> list, Comparator<? super T> c)
now defers to List.sort(c)
.
精彩评论