Sortierte Array-Liste in Java

85

Ich bin verblüfft, dass ich darauf keine schnelle Antwort finden kann. Ich suche im Wesentlichen nach einer Datenstruktur in Java, die die java.util.ListSchnittstelle implementiert , aber ihre Mitglieder in einer sortierten Reihenfolge speichert. Ich weiß, dass Sie ein normales verwenden ArrayListund es verwenden Collections.sort()können, aber ich habe ein Szenario, in dem ich gelegentlich Mitglieder zu meiner Liste hinzufüge und diese häufig abrufe, und ich möchte es nicht jedes Mal sortieren müssen, wenn ich ein Mitglied abrufe, falls a neue wurde hinzugefügt. Kann mich jemand auf so etwas hinweisen, das im JDK oder sogar in Bibliotheken von Drittanbietern existiert?

EDIT : Die Datenstruktur müssen Duplikate bewahren.

ANTWORT ZUSAMMENFASSUNG : Ich fand das alles sehr interessant und habe viel gelernt. Insbesondere Aioobe verdient Erwähnung für seine Beharrlichkeit bei dem Versuch, meine oben genannten Anforderungen zu erfüllen (hauptsächlich eine sortierte Implementierung von java.util.List, die Duplikate unterstützt). Ich habe seine Antwort als die genaueste für das akzeptiert, was ich gefragt habe, und am meisten zum Nachdenken über die Auswirkungen dessen, wonach ich gesucht habe, auch wenn das, was ich gefragt habe, nicht genau das war, was ich brauchte.

Das Problem mit dem, wonach ich gefragt habe, liegt in der List-Schnittstelle selbst und dem Konzept optionaler Methoden in einer Schnittstelle. Um den Javadoc zu zitieren:

Der Benutzer dieser Schnittstelle hat eine genaue Kontrolle darüber, wo in der Liste jedes Element eingefügt wird.

Das Einfügen in eine sortierte Liste hat keine genaue Kontrolle über die Einfügemarke. Dann müssen Sie sich überlegen, wie Sie mit einigen Methoden umgehen werden. Nehmen Sie addzum Beispiel:

public boolean add (Objekt o)

 Appends the specified element to the end of this list (optional operation).

Sie befinden sich jetzt in der unangenehmen Situation, entweder 1) den Vertrag zu brechen und eine sortierte Version von add zu implementieren. 2) addein Element am Ende der Liste hinzufügen zu lassen, Ihre sortierte Reihenfolge zu brechen. 3 add) durch Werfen auszulassen (optional) ein UnsupportedOperationExceptionund ein anderes Verfahren implementiert , welche Elemente in einer sortierten Reihenfolge hinzufügt.

Option 3 ist wahrscheinlich die beste, aber ich finde es unappetitlich, eine Add-Methode zu haben, die Sie nicht verwenden können, und eine andere sortierte Add-Methode, die nicht in der Benutzeroberfläche enthalten ist.

Andere verwandte Lösungen (in keiner bestimmten Reihenfolge):

  • java.util.PriorityQueue, die wahrscheinlich dem am nächsten kommt, was ich brauchte, als das, wonach ich gefragt habe. Eine Warteschlange ist in meinem Fall nicht die genaueste Definition einer Sammlung von Objekten, aber funktional macht sie alles, was ich brauche.
  • net.sourceforge.nite.util.SortedList . Diese Implementierung bricht jedoch den Vertrag der List-Schnittstelle, indem sie die Sortierung in der add(Object obj)Methode implementiert , und hat bizarrerweise eine Methode ohne Auswirkung für add(int index, Object obj). Allgemeiner Konsens legt nahe, throw new UnsupportedOperationException()dass in diesem Szenario eine bessere Wahl sein könnte.
  • Guavas TreeMultiSet Eine Set-Implementierung, die Duplikate unterstützt
  • ca.odell.glazedlists.SortedList Diese Klasse enthält die Einschränkung in ihrem Javadoc:Warning: This class breaks the contract required by List
Chris Knight
quelle
4
Wenn Sie gelegentlich einfügen und häufig lesen, warum nicht einfach beim Einfügen sortieren?
Serg

Antworten:

62

Minimalistische Lösung

Hier ist eine "minimale" Lösung.

class SortedArrayList<T> extends ArrayList<T> {

    @SuppressWarnings("unchecked")
    public void insertSorted(T value) {
        add(value);
        Comparable<T> cmp = (Comparable<T>) value;
        for (int i = size()-1; i > 0 && cmp.compareTo(get(i-1)) < 0; i--)
            Collections.swap(this, i, i-1);
    }
}

Das Einfügen wird in linearer Zeit ausgeführt, aber das würden Sie ohnehin mit einer ArrayList erhalten (alle Elemente rechts vom eingefügten Element müssten in die eine oder andere Richtung verschoben werden).

Das Einfügen von nicht vergleichbaren Ergebnissen führt zu einer ClassCastException. (Dies ist auch der Ansatz von PriorityQueue: Eine Prioritätswarteschlange, die auf natürlicher Reihenfolge beruht, erlaubt auch nicht das Einfügen nicht vergleichbarer Objekte (dies kann zu ClassCastException führen). )

Überschreiben List.add

Beachten Sie, dass das Überschreiben List.add(oder auch List.addAlldas Sortieren) zum sortierten Einfügen von Elementen eine direkte Verletzung der Schnittstellenspezifikation darstellt . Was Sie tun könnten , ist, diese Methode zu überschreiben, um eine zu werfen UnsupportedOperationException.

Aus den Dokumenten von List.add:

boolean add(E e)
    Hängt das angegebene Element an das Ende dieser Liste an (optionale Operation).

Die gleiche Begründung gilt für beide Versionen von add, beide Versionen von addAllund set. (All dies sind optionale Operationen gemäß der Listenschnittstelle.)


Einige Tests

SortedArrayList<String> test = new SortedArrayList<String>();

test.insertSorted("ddd");    System.out.println(test);
test.insertSorted("aaa");    System.out.println(test);
test.insertSorted("ccc");    System.out.println(test);
test.insertSorted("bbb");    System.out.println(test);
test.insertSorted("eee");    System.out.println(test);

.... druckt:

[ddd]
[aaa, ddd]
[aaa, ccc, ddd]
[aaa, bbb, ccc, ddd]
[aaa, bbb, ccc, ddd, eee]
aioobe
quelle
Ein guter Anfang, aber das Aufrufen von add oder addall würde Mitglieder auf unsortierte Weise hinzufügen.
Chris Knight
Ja. Alles andere als das Anhängen an die Liste wäre eine direkte Verletzung der List-Schnittstelle. Siehe meine aktualisierte Antwort.
Aioobe
@aioobe Guter Punkt. Aber ist eine nicht unterstützte Operation einer Schnittstellenmethode kein Codegeruch? Der richtige Weg könnte sein, ArrayList nicht zu erweitern, sondern List zu implementieren, aber selbst dann war List vielleicht einfach nicht für diesen Zweck gedacht. Aus der Javadoc for List: Dies The user of this interface has precise control over where in the list each element is insertedist nicht die beste Beschreibung zum sortierten Einfügen von Elementen, und Sie müssen sich immer noch mit der add(int index, Object obj)Schnittstellenmethode befassen . Diese Probleme erklären wahrscheinlich, warum List nicht sortiert implementiert wurde.
Chris Knight
Nun, die Operation ist aus einem bestimmten Grund optional. Es würde mich nicht wundern, wenn ich beim Ausführen .addeiner SortedArrayList eine UnsupportedExceptionOperation erhalten würde. Ja, die gleiche Begründung gilt für beide Versionen von add, beide Versionen von addAll und set. (All dies sind optionale Operationen gemäß der
Listenschnittstelle
Ah, ich wusste nicht, dass es sich um optionale Operationen handelt. Die Handlung verdickt sich ...;)
Chris Knight
11

Verwenden Sie java.util.PriorityQueue.

Gadolin
quelle
7
das ist keine Liste, dh kein wahlfreier Zugriff.
Thilo
1
Es handelt sich um einen warteschlangenbasierten Prioritätsheap, der List nicht implementiert.
Zengr
3
Bei einer Liste, die die Sortierreihenfolge beibehält, ändern sich die Indizes natürlich ständig, sodass ein zufälliger Zugriff wahrscheinlich sowieso nicht erforderlich ist.
Thilo
5
@Qwerky, beachte, dass die genaue Antwort nicht immer die beste Antwort ist oder die Antwort, nach der das OP tatsächlich sucht.
Aioobe
3
Die Prioritätswarteschlange gewährt bei der Iteration keine sortierte Reihenfolge.
Marcorossi
6

Schauen Sie sich SortedList an

Diese Klasse implementiert eine sortierte Liste. Es besteht aus einem Komparator, der zwei Objekte vergleichen und Objekte entsprechend sortieren kann. Wenn Sie der Liste ein Objekt hinzufügen, wird es an der richtigen Stelle eingefügt. Objekte, die laut Komparator gleich sind, werden in der Reihenfolge in der Liste aufgeführt, in der sie dieser Liste hinzugefügt wurden. Fügen Sie nur Objekte hinzu, die der Komparator vergleichen kann.


Wenn die Liste bereits Objekte enthält, die laut Komparator gleich sind, wird das neue Objekt unmittelbar nach diesen anderen Objekten eingefügt.

Jigar Joshi
quelle
5
Das sieht gut aus, sieht aber auch fehlerhaft aus: Beide Versionen von addAll werden nicht überschrieben, sodass die Liste nach dem Aufrufen dieser nicht sortiert wird.
Tom Anderson
3
Und die Add-Methode "hat keine Auswirkung". Es sollte lieber eine UnsupportedOperationException auslösen, wenn sie nicht verwendet werden kann.
Thilo
@ Tom Anderson @ Thilo, stimme euch beiden zu.
Jigar Joshi
1
Interessant, aber ich bin ziemlich vorsichtig, wenn jemand in Zukunft addAll()alle Elemente sortiert verwendet und denkt. Stimmen Sie auch der UnsupportedOperationException zu.
Chris Knight
1
Was ist die zeitliche Komplexität der Hinzufügung zu dieser Liste?
Shrini1000
6

Sie können versuchen , Guava des TreeMultiSet .

 Multiset<Integer> ms=TreeMultiset.create(Arrays.asList(1,2,3,1,1,-1,2,4,5,100));
 System.out.println(ms);
Emil
quelle
+1. Dies ist eine großartige Bibliothek. MultiSet istA collection that supports order-independent equality, like Set, but may have duplicate elements
Shervin Asgari
5

Aioobes Ansatz ist der richtige Weg. Ich möchte jedoch die folgende Verbesserung gegenüber seiner Lösung vorschlagen.

class SortedList<T> extends ArrayList<T> {

    public void insertSorted(T value) {
        int insertPoint = insertPoint(value);
        add(insertPoint, value);
    }

    /**
     * @return The insert point for a new value. If the value is found the insert point can be any
     * of the possible positions that keeps the collection sorted (.33 or 3.3 or 33.).
     */
    private int insertPoint(T key) {
        int low = 0;
        int high = size() - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = (Comparable<T>) get(mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else {
                return mid; // key found
            }
        }

        return low;  // key not found
    }
}

Die Lösung von aioobe wird bei Verwendung großer Listen sehr langsam. Durch die Verwendung der sortierten Liste können wir den Einfügepunkt für neue Werte mithilfe der binären Suche finden.

Ich würde auch Komposition über Vererbung verwenden, etwas in der Art von

SortedList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
Emanuel Moecklin
quelle
4

Listen behalten normalerweise die Reihenfolge bei, in der Elemente hinzugefügt werden. Benötigen Sie definitiv eine Liste oder wäre ein sortiertes Set (zB TreeSet<E>) für Sie in Ordnung? Müssen Sie grundsätzlich Duplikate aufbewahren?

Jon Skeet
quelle
2
Danke Jon, aber ich muss Duplikate aufbewahren
Chris Knight
1

Sie können ArrayList in eine Unterklasse unterteilen und Collections.sort (this) aufrufen, nachdem ein Element hinzugefügt wurde. Dazu müssten Sie zwei Versionen von add und zwei von addAll überschreiben.

Die Leistung wäre nicht so gut wie eine intelligentere Implementierung, bei der Elemente an der richtigen Stelle eingefügt wurden, aber sie würde den Job erledigen. Wenn das Hinzufügen zur Liste selten ist, sollten die über alle Vorgänge auf der Liste amortisierten Kosten niedrig sein.

Tom Anderson
quelle
1

Erstelle einfach eine neue Klasse wie diese:

public class SortedList<T> extends ArrayList<T> {

private final Comparator<? super T> comparator;

public SortedList() {
    super();
    this.comparator = null;
}

public SortedList(Comparator<T> comparator) {
    super();
    this.comparator = comparator;
}

@Override
public boolean add(T item) {
    int index = comparator == null ? Collections.binarySearch((List<? extends Comparable<? super T>>)this, item) :
            Collections.binarySearch(this, item, comparator);
    if (index < 0) {
        index = index * -1 - 2;
    }
    super.add(index+1, item);
    return true;
}

@Override
public void add(int index, T item) {
    throw new UnsupportedOperationException("'add' with an index is not supported in SortedArrayList");
}

@Override
public boolean addAll(Collection<? extends T> items) {
    boolean allAdded = true;
    for (T item : items) {
        allAdded = allAdded && add(item);
    }
    return allAdded;
}

@Override
public boolean addAll(int index, Collection<? extends T> items) {
    throw new UnsupportedOperationException("'addAll' with an index is not supported in SortedArrayList");
}

}

Sie können es so testen:

    List<Integer> list = new SortedArrayList<>((Integer i1, Integer i2) -> i1.compareTo(i2));
    for (Integer i : Arrays.asList(4, 7, 3, 8, 9, 25, 20, 23, 52, 3)) {
        list.add(i);
    }
    System.out.println(list);
user13750620
quelle
0

Ich denke, die Wahl zwischen SortedSets / Lists und 'normalen' sortierbaren Sammlungen hängt davon ab, ob Sie nur zu Präsentationszwecken oder zu fast jedem Zeitpunkt zur Laufzeit sortieren müssen. Die Verwendung einer sortierten Sammlung kann viel teurer sein, da die Sortierung jedes Mal erfolgt, wenn Sie ein Element einfügen.

Wenn Sie sich im JDK nicht für eine Sammlung entscheiden können, können Sie sich die Apache Commons-Sammlungen ansehen

Codeporn
quelle
0

Da die derzeit vorgeschlagenen Implementierungen, die eine sortierte Liste durch Aufheben der Sammlungs-API implementieren, eine eigene Implementierung eines Baums oder ähnliches haben, war ich neugierig, wie eine Implementierung auf der Basis der TreeMap funktionieren würde. (Insbesondere, da das TreeSet auch auf TreeMap basiert)

Wenn sich auch jemand dafür interessiert, kann er oder sie sich gerne darum kümmern:

Baumstruktur

Es ist Teil der Kernbibliothek und kann natürlich über die Maven-Abhängigkeit hinzugefügt werden. (Apache-Lizenz)

Derzeit scheint sich die Implementierung auf derselben Ebene wie die Guave SortedMultiSet und die TreeList der Apache Commons-Bibliothek recht gut zu vergleichen.

Aber ich würde mich freuen, wenn mehr als nur ich die Implementierung testen würde, um sicherzugehen, dass ich nichts Wichtiges verpasst habe.

Freundliche Grüße!

Omnaest
quelle
0

Ich hatte das gleiche Problem. Also nahm ich den Quellcode von java.util.TreeMap und schrieb IndexedTreeMap . Es implementiert meine eigene IndexedNavigableMap :

public interface IndexedNavigableMap<K, V> extends NavigableMap<K, V> {
   K exactKey(int index);
   Entry<K, V> exactEntry(int index);
   int keyIndex(K k);
}

Die Implementierung basiert auf der Aktualisierung der Knotengewichte im rot-schwarzen Baum, wenn diese geändert werden. Das Gewicht ist die Anzahl der untergeordneten Knoten unter einem bestimmten Knoten plus eins. Zum Beispiel, wenn ein Baum nach links gedreht wird:

    private void rotateLeft(Entry<K, V> p) {
    if (p != null) {
        Entry<K, V> r = p.right;

        int delta = getWeight(r.left) - getWeight(p.right);
        p.right = r.left;
        p.updateWeight(delta);

        if (r.left != null) {
            r.left.parent = p;
        }

        r.parent = p.parent;


        if (p.parent == null) {
            root = r;
        } else if (p.parent.left == p) {
            delta = getWeight(r) - getWeight(p.parent.left);
            p.parent.left = r;
            p.parent.updateWeight(delta);
        } else {
            delta = getWeight(r) - getWeight(p.parent.right);
            p.parent.right = r;
            p.parent.updateWeight(delta);
        }

        delta = getWeight(p) - getWeight(r.left);
        r.left = p;
        r.updateWeight(delta);

        p.parent = r;
    }
  }

updateWeight aktualisiert einfach die Gewichte bis zur Wurzel:

   void updateWeight(int delta) {
        weight += delta;
        Entry<K, V> p = parent;
        while (p != null) {
            p.weight += delta;
            p = p.parent;
        }
    }

Und wenn wir das Element anhand des Index finden müssen, ist hier die Implementierung, die Gewichte verwendet:

public K exactKey(int index) {
    if (index < 0 || index > size() - 1) {
        throw new ArrayIndexOutOfBoundsException();
    }
    return getExactKey(root, index);
}

private K getExactKey(Entry<K, V> e, int index) {
    if (e.left == null && index == 0) {
        return e.key;
    }
    if (e.left == null && e.right == null) {
        return e.key;
    }
    if (e.left != null && e.left.weight > index) {
        return getExactKey(e.left, index);
    }
    if (e.left != null && e.left.weight == index) {
        return e.key;
    }
    return getExactKey(e.right, index - (e.left == null ? 0 : e.left.weight) - 1);
}

Es ist auch sehr praktisch, den Index eines Schlüssels zu finden:

    public int keyIndex(K key) {
    if (key == null) {
        throw new NullPointerException();
    }
    Entry<K, V> e = getEntry(key);
    if (e == null) {
        throw new NullPointerException();
    }
    if (e == root) {
        return getWeight(e) - getWeight(e.right) - 1;//index to return
    }
    int index = 0;
    int cmp;
    index += getWeight(e.left);

    Entry<K, V> p = e.parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        while (p != null) {
            cmp = cpr.compare(key, p.key);
            if (cmp > 0) {
                index += getWeight(p.left) + 1;
            }
            p = p.parent;
        }
    } else {
        Comparable<? super K> k = (Comparable<? super K>) key;
        while (p != null) {
            if (k.compareTo(p.key) > 0) {
                index += getWeight(p.left) + 1;
            }
            p = p.parent;
        }
    }
    return index;
}

Das Ergebnis dieser Arbeit finden Sie unter http://code.google.com/p/indexed-tree-map/

TreeSet / TreeMap (sowie deren indizierte Gegenstücke aus dem Projekt "Indexed Tree Map") erlauben keine doppelten Schlüssel. Sie können 1 Schlüssel für ein Array von Werten verwenden. Wenn Sie ein SortedSet mit Duplikaten benötigen, verwenden Sie TreeMap mit Werten als Arrays. Ich würde das tun.

Vitaly Sazanovich
quelle