Perché avviare una matrice con una capacità iniziale?


149

Il solito costruttore di ArrayListè:

ArrayList<?> list = new ArrayList<>();

Ma c'è anche un costruttore sovraccarico con un parametro per la sua capacità iniziale:

ArrayList<?> list = new ArrayList<>(20);

Perché è utile creare un ArrayListcon una capacità iniziale quando possiamo accedervi a nostro piacimento?


17
Hai provato a vedere il codice sorgente di ArrayList?
AmitG

@Joachim Sauer: A volte abbiamo una conoscenza quando leggiamo attentamente la fonte. Stavo provando se avesse letto la fonte. Ho capito il tuo aspetto. Grazie.
AmitG

ArrayList è un periodo di scarso rendimento, perché dovresti usare una struttura del genere
Positivo

Risposte:


196

Se si conosce in anticipo quale sarà la dimensione ArrayListdell'elemento, è più efficiente specificare la capacità iniziale. In caso contrario, l'array interno dovrà essere riallocato ripetutamente man mano che l'elenco cresce.

Più grande è l'elenco finale, più tempo risparmi evitando le riallocazioni.

Detto questo, anche senza pre-allocazione, l'inserimento di nelementi sul retro di un ArrayListè garantito per prendere il O(n)tempo totale . In altre parole, aggiungere un elemento è un'operazione a tempo costante ammortizzata. Ciò si ottiene facendo in modo che ogni riallocazione aumenti esponenzialmente le dimensioni dell'array, in genere di un fattore di 1.5. Con questo approccio, si può dimostrare cheO(n) il numero totale di operazioni è .


5
Mentre pre-allocare dimensioni note è una buona idea, non farlo di solito non è terribile: avrai bisogno di log (n) riassegnazioni per un elenco con una dimensione finale di n , che non è molto.
Joachim Sauer,

2
@PeterOlson O(n log n)avrebbe fatto i tempi di log nlavoro n. È una sopravvalutazione grossolana (anche se tecnicamente corretta con la grande O dovuta al fatto che è un limite superiore). Copia s + s * 1.5 + s * 1.5 ^ 2 + ... + s * 1.5 ^ m (in modo tale che s * 1.5 ^ m <n <s * 1.5 ^ (m + 1)) elementi in totale. Non sono bravo nelle somme, quindi non posso darti la matematica precisa dalla cima della mia testa (per ridimensionare il fattore 2, è 2n, quindi può essere 1,5n dare o prendere una piccola costante), ma non lo fa t prende troppo strabismo per vedere che questa somma è al massimo un fattore costante maggiore di n. Quindi prende O (k * n) copie, che ovviamente è O (n).

1
@delnan: non posso discutere con quello! ;) A proposito, mi è davvero piaciuta la tua discussione strabica; lo aggiungerò al mio repertorio di trucchi.
NPE,

6
È più facile fare l'argomento con il raddoppio. Supponi di raddoppiare quando è pieno, iniziando con un elemento. Supponiamo di voler inserire 8 elementi. Inserisci uno (costo: 1). Inserisci due - doppio, copia un elemento e inserisci due (costo: 2). Inserisci tre - doppio, copia due elementi, inserisci tre (costo: 3). Inserire quattro (costo: 1). Inserisci cinque: raddoppia, copia quattro elementi, inserisci cinque (costo: 5). Inserisci sei, sette e otto (costo: 3). Costo totale: 1 + 2 + 3 + 1 + 5 + 3 = 16, che è il doppio del numero di elementi inseriti. Da questo schizzo è possibile dimostrare che il costo medio è due per inserto in generale.
Eric Lippert,

9
Questo è il costo nel tempo . Puoi anche notare che la quantità di spazio sprecato è cambiata nel tempo, essendo lo 0% alcune volte e vicino al 100% alcune volte. La modifica del fattore da 2 a 1,5 o 4 o 100 o qualsiasi altra cosa modifica la quantità media di spazio sprecato e la quantità media di tempo impiegato per la copia, ma la complessità del tempo rimane mediamente lineare, indipendentemente dal fattore.
Eric Lippert,

41

Perché ArrayListè una struttura di dati di array ridimensionamento dinamico , il che significa che è implementato come un array con una dimensione fissa iniziale (predefinita). Quando questo viene riempito, l'array verrà esteso a uno di dimensioni doppie. Questa operazione è costosa, quindi vuoi il minor numero possibile.

Quindi, se sai che il limite superiore è di 20 elementi, creare l'array con una lunghezza iniziale di 20 è meglio che usare un valore predefinito di, diciamo, 15 e quindi ridimensionarlo 15*2 = 30e usare solo 20 sprecando i cicli per l'espansione.

PS: come afferma AmitG, il fattore di espansione è specifico dell'implementazione (in questo caso (oldCapacity * 3)/2 + 1)


9
in realtà èint newCapacity = (oldCapacity * 3)/2 + 1;
AmitG

25

La dimensione predefinita di Arraylist è 10 .

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
    this(10);
    } 

Quindi, se hai intenzione di aggiungere 100 o più record, puoi vedere l'overhead della riallocazione della memoria.

ArrayList<?> list = new ArrayList<>();    
// same as  new ArrayList<>(10);      

Quindi, se hai idea del numero di elementi che verranno memorizzati in Arraylist, è meglio creare Arraylist con quella dimensione invece di iniziare con 10 e continuare ad aumentarlo.


Non è garantito che la capacità predefinita sarà sempre 10 per le versioni JDK in futuro -private static final int DEFAULT_CAPACITY = 10
vikingsteve,

17

In realtà ho scritto un post sul blog sull'argomento 2 mesi fa. L'articolo è per C # List<T>ma Java ArrayListha un'implementazione molto simile. Poiché ArrayListè implementato utilizzando un array dinamico, aumenta di dimensioni su richiesta. Quindi il motivo del costruttore di capacità è a fini di ottimizzazione.

Quando si verifica una di queste operazioni di ridimensionamento, ArrayList copia il contenuto dell'array in un nuovo array che è il doppio della capacità di quello precedente. Questa operazione viene eseguita in O (n) tempo.

Esempio

Ecco un esempio di come ArrayListaumenterebbe la dimensione:

10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308

Quindi l'elenco inizia con una capacità di 10, quando viene aggiunto l'undicesimo elemento, aumenta di 50% + 1a 16. Al 17 ° oggetto ArrayListviene nuovamente aumentato a 25e così via. Consideriamo ora l'esempio in cui stiamo creando un elenco in cui la capacità desiderata è già nota come 1000000. La creazione del ArrayListcostruttore senza dimensione chiamerà i ArrayList.add 1000000tempi che richiedono normalmente O (1) o O (n) al ridimensionamento.

1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851 operazioni

Confronta questo usando il costruttore e poi chiamando ArrayList.addche è garantito per essere eseguito in O (1) .

1000000 + 1000000 = 2000000 operazioni

Java vs C #

Java è come sopra, a partire da 10e aumentando ogni ridimensionamento a 50% + 1. C # inizia da 4e aumenta in modo molto più aggressivo, raddoppiando ad ogni ridimensionamento. L' 1000000esempio sopra riportato per C # usa le 3097084operazioni.

Riferimenti


9

L'impostazione della dimensione iniziale di una ArrayList, ad esempio su ArrayList<>(100), riduce il numero di volte in cui deve avvenire la riassegnazione della memoria interna.

Esempio:

ArrayList example = new ArrayList<Integer>(3);
example.add(1); // size() == 1
example.add(2); // size() == 2, 
example.add(2); // size() == 3, example has been 'filled'
example.add(3); // size() == 4, example has been 'expanded' so that the fourth element can be added. 

Come vedi nell'esempio sopra, è ArrayListpossibile espandere se necessario. Ciò che ciò non ti mostra è che la dimensione dell'Arraylist di solito raddoppia (anche se nota che la nuova dimensione dipende dalla tua implementazione). Quanto segue è citato da Oracle :

"Ogni istanza di ArrayList ha una capacità. La capacità è la dimensione dell'array utilizzato per memorizzare gli elementi nell'elenco. È sempre almeno grande quanto la dimensione dell'elenco. Man mano che gli elementi vengono aggiunti ad un ArrayList, la sua capacità aumenta automaticamente. I dettagli della politica di crescita non sono specificati oltre al fatto che l'aggiunta di un elemento ha un costo temporale ammortizzato costante. "

Ovviamente, se non hai idea di quale tipo di intervallo manterrai, impostare le dimensioni probabilmente non sarà una buona idea - tuttavia, se hai in mente un intervallo specifico, l'impostazione di una capacità iniziale aumenterà l'efficienza della memoria .


3

ArrayList può contenere molti valori e quando si eseguono inserimenti iniziali di grandi dimensioni, si può dire ad ArrayList di allocare uno spazio di archiviazione più grande per iniziare per non sprecare i cicli della CPU quando si tenta di allocare più spazio per l'elemento successivo. Pertanto, allocare un po 'di spazio all'inizio è più efficace.


3

Questo per evitare possibili sforzi di riallocazione per ogni singolo oggetto.

int newCapacity = (oldCapacity * 3)/2 + 1;

new Object[]viene creato internamente .
JVM ha bisogno di sforzi per creare new Object[]quando aggiungi un elemento nell'arraylist. Se non si dispone di un codice sopra (nessun algo che si pensa) per la riallocazione, ogni volta che si invoca arraylist.add()si new Object[]deve creare il che è inutile e stiamo perdendo tempo per aumentare le dimensioni di 1 per ogni singolo oggetto da aggiungere. Quindi è meglio aumentare le dimensioni di Object[]con la seguente formula.
(JSL ha usato la formula di forcasting indicata di seguito per l'arraylist a crescita dinamica invece di crescere di 1 ogni volta. Perché per crescere ci vuole sforzo da JVM)

int newCapacity = (oldCapacity * 3)/2 + 1;

ArrayList non eseguirà la riallocazione per ogni singolo add: utilizza già una formula di crescita internamente. Quindi alla domanda non viene data risposta.
Ah

@AH La mia risposta è per test negativi . Si prega di leggere tra le righe. Ho detto "Se non hai il codice sopra (qualsiasi algo che pensi) per la riallocazione, ogni volta che invochi arraylist.add () allora deve essere creato un nuovo oggetto [] che è inutile e stiamo perdendo tempo." e il codice è int newCapacity = (oldCapacity * 3)/2 + 1;presente nella classe ArrayList. Pensi ancora che sia senza risposta?
AmitG

1
Continuo a pensare che non abbia una risposta: ArrayListnella riallocazione ammortizzata avviene in ogni caso con qualsiasi valore per la capacità iniziale. E la domanda è: perché usare un valore non standard per la capacità iniziale? Inoltre: "leggere tra le righe" non è qualcosa desiderato in una risposta tecnica. ;-)
AH

@AH sto rispondendo come, cosa sarebbe successo se non avessimo un processo di riallocazione in ArrayList. Quindi è la risposta. Prova a leggere lo spirito della risposta :-). Conosco meglio In ArrayList la riallocazione ammortizzata avviene in ogni caso con qualsiasi valore per la capacità iniziale.
AmitG

2

Penso che ogni ArrayList sia creato con un valore di capacità init di "10". Quindi, se si crea un ArrayList senza impostare la capacità all'interno del costruttore, verrà creato con un valore predefinito.


2

Direi che è un'ottimizzazione. ArrayList senza capacità iniziale avrà ~ 10 righe vuote e si espanderà quando si esegue un'aggiunta.

Per avere un elenco con esattamente il numero di elementi è necessario chiamare trimToSize ()


0

Secondo la mia esperienza con ArrayList, dare una capacità iniziale è un buon modo per evitare i costi di riallocazione. Ma porta un avvertimento. Tutti i suggerimenti sopra menzionati affermano che si dovrebbe fornire la capacità iniziale solo quando si conosce una stima approssimativa del numero di elementi. Ma quando proviamo a dare una capacità iniziale senza alcuna idea, la quantità di memoria riservata e inutilizzata sarà uno spreco in quanto potrebbe non essere mai richiesto una volta che l'elenco viene riempito per il numero richiesto di elementi. Quello che sto dicendo è che possiamo essere pragmatici all'inizio durante l'allocazione della capacità e quindi trovare un modo intelligente di conoscere la capacità minima richiesta in fase di esecuzione. ArrayList fornisce un metodo chiamato ensureCapacity(int minCapacity). Ma poi, si è trovato un modo intelligente ...


0

Ho testato ArrayList con e senza initialCapacity e ho ottenuto risultati sorprendenti
Quando ho impostato LOOP_NUMBER su 100.000 o meno, il risultato è che l'impostazione di InitCapacity è efficiente.

list1Sttop-list1Start = 14
list2Sttop-list2Start = 10


Ma quando imposto LOOP_NUMBER su 1.000.000 il risultato cambia in:

list1Stop-list1Start = 40
list2Stop-list2Start = 66


Infine, non sono riuscito a capire come funziona ?!
Codice di esempio:

 public static final int LOOP_NUMBER = 100000;

public static void main(String[] args) {

    long list1Start = System.currentTimeMillis();
    List<Integer> list1 = new ArrayList();
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list1.add(i);
    }
    long list1Stop = System.currentTimeMillis();
    System.out.println("list1Stop-list1Start = " + String.valueOf(list1Stop - list1Start));

    long list2Start = System.currentTimeMillis();
    List<Integer> list2 = new ArrayList(LOOP_NUMBER);
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list2.add(i);
    }
    long list2Stop = System.currentTimeMillis();
    System.out.println("list2Stop-list2Start = " + String.valueOf(list2Stop - list2Start));
}

Ho testato su Windows 8.1 e jdk1.7.0_80


1
ciao, sfortunatamente l'attuale tolleranza TimeMillis è di fino a cento millisecondi (a seconda), il che significa che il risultato non è certo affidabile. Suggerirei di utilizzare alcune librerie personalizzate per farlo bene.
Bogdan,
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.