Elenco di array ordinati in Java


85

Sono sconcertato dal fatto che non riesco a trovare una risposta rapida a questo. Sto essenzialmente cercando una struttura dati in Java che implementa l' java.util.Listinterfaccia, ma che memorizza i suoi membri in un ordine ordinato. So che puoi usare un normale ArrayListe usarlo Collections.sort(), ma ho uno scenario in cui occasionalmente aggiungo e spesso recupero membri dal mio elenco e non voglio doverlo ordinare ogni volta che recupero un membro nel caso in cui un ne è stato aggiunto uno nuovo. Qualcuno può indicarmi una cosa del genere che esiste nel JDK o anche nelle librerie di terze parti?

EDIT : La struttura dei dati dovrà conservare i duplicati.

SOMMARIO DELLA RISPOSTA : Ho trovato tutto questo molto interessante e ho imparato molto. Aioobe in particolare merita una menzione per la sua perseveranza nel cercare di soddisfare i miei requisiti sopra (principalmente un'implementazione java.util.List ordinata che supporta i duplicati). Ho accettato la sua risposta come la più accurata per ciò che ho chiesto e la maggior parte del pensiero mi ha provocato le implicazioni di ciò che stavo cercando anche se ciò che ho chiesto non era esattamente ciò di cui avevo bisogno.

Il problema con ciò che ho chiesto risiede nell'interfaccia List stessa e nel concetto di metodi opzionali in un'interfaccia. Per citare il javadoc:

L'utente di questa interfaccia ha un controllo preciso sulla posizione in cui viene inserito ciascun elemento nell'elenco.

L'inserimento in un elenco ordinato non ha un controllo preciso sul punto di inserimento. Quindi, devi pensare a come gestirai alcuni metodi. Prendiamo addad esempio:

public boolean add (Object o)

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

Ora ti trovi nella scomoda situazione di 1) Rompere il contratto e implementare una versione ordinata di add 2) Lasciando addaggiungere un elemento alla fine della lista, rompendo il tuo ordine 3) Tralasciando add(come opzionale) lanciando un UnsupportedOperationExceptioned attuare un altro metodo che aggiunge elementi in un modo ordinato.

L'opzione 3 è probabilmente la migliore, ma trovo sgradevole avere un metodo di aggiunta che non puoi usare e un altro metodo ordinato che non è nell'interfaccia.

Altre soluzioni correlate (in nessun ordine particolare):

  • java.util.PriorityQueue che è probabilmente il più vicino a ciò di cui avevo bisogno rispetto a quello che ho chiesto. Una coda non è la definizione più precisa di una raccolta di oggetti nel mio caso, ma funzionalmente fa tutto ciò di cui ho bisogno.
  • net.sourceforge.nite.util.SortedList . Tuttavia, questa implementazione rompe il contratto dell'interfaccia List implementando l'ordinamento nel add(Object obj)metodo e stranamente non ha un metodo di effetto per add(int index, Object obj). Il consenso generale suggerisce che throw new UnsupportedOperationException()potrebbe essere una scelta migliore in questo scenario.
  • TreeMultiSet di Guava Un'implementazione dell'insieme che supporta i duplicati
  • ca.odell.glazedlists.SortedList Questa classe viene fornita con l'avvertenza nel suo javadoc:Warning: This class breaks the contract required by List

4
Se inserisci di tanto in tanto e leggi frequentemente, perché non ordinarlo durante l'inserimento?
serg

Risposte:


62

Soluzione minimalista

Ecco una soluzione "minima".

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);
    }
}

L'inserimento viene eseguito in tempo lineare, ma sarebbe quello che otterresti usando comunque un ArrayList (tutti gli elementi a destra dell'elemento inserito dovrebbero essere spostati in un modo o nell'altro).

L'inserimento di qualcosa di non confrontabile produce un'eccezione ClassCastException. (Questo è l'approccio adottato anche da PriorityQueue: una coda di priorità che si basa sull'ordinamento naturale non consente anche l'inserimento di oggetti non comparabili (così facendo potrebbe risultare in ClassCastException). )

Overriding List.add

Si noti che sovrascrivere List.add(o List.addAllper quella materia) per inserire elementi in modo ordinato sarebbe una violazione diretta delle specifiche dell'interfaccia . Quello che potresti fare è sovrascrivere questo metodo per lanciare un file UnsupportedOperationException.

Dai documenti di List.add:

boolean add(E e)
    Aggiunge l'elemento specificato alla fine di questo elenco (operazione facoltativa).

Lo stesso ragionamento vale per entrambe le versioni di add, entrambe le versioni di addAlle set. (Tutte operazioni facoltative in base all'interfaccia dell'elenco.)


Alcuni test

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);

.... stampe:

[ddd]
[aaa, ddd]
[aaa, ccc, ddd]
[aaa, bbb, ccc, ddd]
[aaa, bbb, ccc, ddd, eee]

Un buon inizio, ma chiamare add o addall aggiungerebbe membri in modo non ordinato.
Chris Knight,

Sì. Qualsiasi cosa tranne aggiungerli alla lista sarebbe una diretta violazione dell'interfaccia della lista. Vedi la mia risposta aggiornata.
aioobe

@aioobe Buon punto. Ma un'operazione non supportata di un metodo di interfaccia non è un odore di codice? Il modo corretto potrebbe essere quello di non estendere ArrayList ma implementare List, ma anche allora forse List non era pensato per questo scopo. Dal Javadoc per List: The user of this interface has precise control over where in the list each element is insertedche non è la migliore descrizione per inserire elementi in modo ordinato e devi ancora occuparti del add(int index, Object obj)metodo di interfaccia. Questi problemi probabilmente spiegano perché List non è stato implementato in modo ordinato.
Chris Knight,

Bene, l'operazione è facoltativa per un motivo. Non sarei sorpreso se ottenessi un UnsupportedExceptionOperation durante l'esecuzione .addsu un SortedArrayList. Sì, lo stesso ragionamento si applica a entrambe le versioni di add, entrambe le versioni di addAll e set. (Tutte operazioni opzionali in base all'interfaccia dell'elenco.)
aioobe

Ah, non mi rendevo conto che fossero operazioni facoltative. La trama si infittisce ...;)
Chris Knight,

10

7
quella non è una lista, cioè nessun accesso casuale.
Thilo

1
È un heap prioritario basato sulla coda che non implementa List.
zengr

3
Ovviamente, con un elenco che mantiene l'ordinamento, gli indici cambiano continuamente, quindi probabilmente l'accesso casuale non è comunque necessario.
Thilo

5
@Qwerky, nota che la risposta esatta non è sempre la risposta migliore, o la risposta che l'OP è effettivamente dopo.
aioobe

3
la coda prioritaria non garantisce l'ordine ordinato durante l'iterazione.
marcorossi

6

Dai un'occhiata a SortedList

Questa classe implementa un elenco ordinato. È costruito con un comparatore che può confrontare due oggetti e ordinare gli oggetti di conseguenza. Quando aggiungi un oggetto all'elenco, viene inserito nella posizione corretta. Gli oggetti che sono uguali secondo il comparatore, saranno nell'elenco nell'ordine in cui sono stati aggiunti a questo elenco. Aggiungi solo oggetti che il comparatore può confrontare.


Quando l'elenco contiene già oggetti uguali secondo il comparatore, il nuovo oggetto verrà inserito immediatamente dopo questi altri oggetti.


5
Sembra buono, ma sembra anche difettoso: non c'è override di nessuna delle versioni di addAll, quindi l'elenco non sarà ordinato dopo aver chiamato quelli.
Tom Anderson,

3
E il metodo di aggiunta "non ha effetto". Dovrebbe piuttosto generare un'eccezione UnsupportedOperationException se non può essere utilizzata.
Thilo

@Tom Anderson @Thilo, sono d'accordo con entrambi.
jmj

1
Interessante, ma sono piuttosto diffidente nei confronti di qualcuno in futuro che utilizza addAll()e pensa che tutti gli elementi sarebbero tutti in modo ordinato. D'accordo anche con UnsupportedOperationException.
Chris Knight,

1
Qual è la complessità temporale dell'aggiunta a questo elenco?
shrini1000

6

Puoi provare TreeMultiSet di Guava .

 Multiset<Integer> ms=TreeMultiset.create(Arrays.asList(1,2,3,1,1,-1,2,4,5,100));
 System.out.println(ms);

+1. Questa è un'ottima libreria. MultiSet èA collection that supports order-independent equality, like Set, but may have duplicate elements
Shervin Asgari,

5

L'approccio di Aioobe è la strada da percorrere. Tuttavia, vorrei suggerire il seguente miglioramento rispetto alla sua soluzione.

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
    }
}

La soluzione di aioobe diventa molto lenta quando si utilizzano elenchi di grandi dimensioni. L'uso del fatto che l'elenco è ordinato ci consente di trovare il punto di inserimento per nuovi valori utilizzando la ricerca binaria.

Vorrei anche usare la composizione sull'ereditarietà, qualcosa sulla falsariga di

SortedList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

4

Gli elenchi in genere mantengono l'ordine in cui vengono aggiunti gli elementi. Hai sicuramente bisogno di un elenco o un insieme ordinato (ad esempio TreeSet<E>) andrebbe bene per te? Fondamentalmente, hai bisogno di conservare i duplicati?


2
Grazie Jon, ma devo conservare i duplicati
Chris Knight


1

È possibile creare una sottoclasse ArrayList e chiamare Collections.sort (this) dopo che qualsiasi elemento è stato aggiunto: per fare ciò, è necessario sovrascrivere due versioni di add e due di addAll.

Le prestazioni non sarebbero buone quanto un'implementazione più intelligente che inserisse gli elementi nel posto giusto, ma farebbe il lavoro. Se l'aggiunta alla lista è rara, il costo ammortizzato su tutte le operazioni sulla lista dovrebbe essere basso.


1

Crea una nuova classe come questa:

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");
}

}

Puoi testarlo in questo modo:

    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);

0

Penso che la scelta tra SortedSets / Lists e raccolte ordinabili 'normali' dipenda dal fatto che sia necessario l'ordinamento solo per scopi di presentazione o in quasi ogni momento durante il runtime. L'utilizzo di una raccolta ordinata può essere molto più costoso perché l'ordinamento viene eseguito ogni volta che si inserisce un elemento.

Se non puoi optare per una raccolta nel JDK, puoi dare un'occhiata alle Collezioni di Apache Commons


0

Poiché le implementazioni attualmente proposte che implementano un elenco ordinato interrompendo l'API di raccolta, hanno una propria implementazione di un albero o qualcosa di simile, ero curioso di sapere come si sarebbe comportata un'implementazione basata su TreeMap. (Soprattutto dal momento che anche il TreeSet si basa su TreeMap)

Se qualcuno è interessato anche a questo, può sentirsi libero di esaminarlo:

TreeList

Fa parte della libreria principale , puoi aggiungerlo tramite la dipendenza Maven ovviamente. (Licenza Apache)

Attualmente l'implementazione sembra confrontarsi abbastanza bene sullo stesso livello del guava SortedMultiSet e del TreeList della libreria Apache Commons.

Ma sarei felice se più di me solo testasse l'implementazione per essere sicuro di non aver perso qualcosa di importante.

I migliori saluti!


0

Ho avuto lo stesso problema. Quindi ho preso il codice sorgente di java.util.TreeMap e ho scritto IndexedTreeMap . Implementa la mia IndexedNavigableMap :

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

L'implementazione si basa sull'aggiornamento dei pesi dei nodi nell'albero rosso-nero quando viene modificato. Il peso è il numero di nodi figlio sotto un dato nodo, più uno - self. Ad esempio, quando un albero viene ruotato a sinistra:

    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 aggiorna semplicemente i pesi fino alla radice:

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

E quando abbiamo bisogno di trovare l'elemento per indice, ecco l'implementazione che usa i pesi:

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);
}

Inoltre è molto utile trovare l'indice di una chiave:

    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;
}

Puoi trovare il risultato di questo lavoro su http://code.google.com/p/indexed-tree-map/

TreeSet / TreeMap (così come le loro controparti indicizzate dal progetto indexed-tree-map) non consentono chiavi duplicate, puoi usare 1 chiave per un array di valori. Se hai bisogno di un SortedSet con duplicati usa TreeMap con valori come array. Io lo farei.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.