Modo efficiente per inserire un numero in una matrice ordinata di numeri?


143

Ho un array JavaScript ordinato e desidero inserire un altro elemento nell'array in modo che l'array risultante rimanga ordinato. Potrei certamente implementare una semplice funzione di inserimento in stile quicksort:

var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
  array.splice(locationOf(element, array) + 1, 0, element);
  return array;
}

function locationOf(element, array, start, end) {
  start = start || 0;
  end = end || array.length;
  var pivot = parseInt(start + (end - start) / 2, 10);
  if (end-start <= 1 || array[pivot] === element) return pivot;
  if (array[pivot] < element) {
    return locationOf(element, array, pivot, end);
  } else {
    return locationOf(element, array, start, pivot);
  }
}

console.log(insert(element, array));

[ATTENZIONE] questo codice ha un bug quando si tenta di inserire all'inizio dell'array, ad esempio insert(2, [3, 7 ,9]) produce errato [3, 2, 7, 9].

Tuttavia, ho notato che le implementazioni della funzione Array.sort potrebbero potenzialmente farlo per me e nativamente:

var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
  array.push(element);
  array.sort(function(a, b) {
    return a - b;
  });
  return array;
}

console.log(insert(element, array));

C'è una buona ragione per scegliere la prima implementazione rispetto alla seconda?

Modifica : si noti che per il caso generale, un inserimento O (log (n)) (come implementato nel primo esempio) sarà più veloce di un algoritmo di ordinamento generico; tuttavia questo non è necessariamente il caso di JavaScript in particolare. Nota che:

  • Il caso migliore per diversi algoritmi di inserimento è O (n), che è ancora significativamente diverso da O (log (n)), ma non è così male come O (n log (n)) come menzionato di seguito. Dipenderebbe dal particolare algoritmo di ordinamento utilizzato (vedi l' implementazione di Javascript Array.sort? )
  • Il metodo di ordinamento in JavaScript è una funzione nativa, quindi potenzialmente realizza enormi vantaggi - O (log (n)) con un coefficiente enorme può ancora essere molto peggio di O (n) per insiemi di dati di dimensioni ragionevoli.

usare la giunzione nella seconda implementazione è un po 'dispendioso. Perché non usare push?
Bretone,

Bene, l'ho appena copiato dal primo.
Elliot Kroo,

4
Tutto ciò che contiene splice()(ad esempio il tuo primo esempio) è già O (n). Anche se non crea internamente una nuova copia dell'intero array, potenzialmente deve spostare tutti gli n elementi indietro di 1 posizione se l'elemento deve essere inserito in posizione 0. Forse è veloce perché è una funzione nativa e la costante è basso, ma è comunque O (n).
j_random_hacker il

6
inoltre, per riferimento futuro per le persone che utilizzano questo codice, il codice presenta un bug quando si tenta di inserire all'inizio dell'array. Guarda più in basso per il codice corretto.
Pinocchio,

3
Non parseIntusare Math.floorinvece invece. Math.floorè molto più veloce di parseInt: jsperf.com/test-parseint-and-math-floor
Hubert Schölnast,

Risposte:


58

Proprio come un singolo punto dati, per i calci ho provato questo inserendo 1000 elementi casuali in un array di 100.000 numeri pre-ordinati usando i due metodi usando Chrome su Windows 7:

First Method:
~54 milliseconds
Second Method:
~57 seconds

Quindi, almeno su questa configurazione, il metodo nativo non lo compensa. Questo vale anche per piccoli set di dati, inserendo 100 elementi in un array di 1000:

First Method:
1 milliseconds
Second Method:
34 milliseconds

1
arrays.sort sembra abbastanza terribile
njzk2

2
Sembra che array.splice debba fare qualcosa di veramente intelligente, per inserire un singolo elemento entro 54 microsecondi.
gnasher729,

@ gnasher729 - Non credo che gli array Javascript siano davvero gli stessi degli array fisicamente continui come quelli che abbiamo in C. Penso che i motori JS possano implementarli come una mappa / dizionario hash che consente l'inserimento rapido.
Ian,

1
quando si utilizza una funzione di confronto con Array.prototype.sort, si perdono i vantaggi di C ++ perché la funzione JS è chiamata così tanto.
aleclarson,

Come si confronta il primo metodo ora che Chrome utilizza TimSort ? Da TimSort Wikipedia : "Nel migliore dei casi, che si verifica quando l'input è già ordinato, [TimSort] viene eseguito in tempo lineare".
più elegante il

47

Semplice ( demo ):

function sortedIndex(array, value) {
    var low = 0,
        high = array.length;

    while (low < high) {
        var mid = (low + high) >>> 1;
        if (array[mid] < value) low = mid + 1;
        else high = mid;
    }
    return low;
}

4
Bel tocco. Non ho mai sentito parlare dell'utilizzo di operatori bit per bit per trovare il valore medio di due numeri. Normalmente vorrei solo moltiplicare per 0,5. C'è un significativo aumento delle prestazioni in questo modo?
Jackson,

2
@Jackson x >>> 1è lo spostamento binario a destra di 1 posizione, che è effettivamente solo una divisione per 2. ad esempio per 11: 1011-> 101risultati a 5.
Qwerty

3
@Qwerty @Web_Designer Essendo già su questa traccia, potresti spiegare la differenza tra >>> 1e ( visto qui e ) >> 1?
yckart,

4
>>>è uno spostamento a destra senza segno, mentre >>si estende il segno - tutto si riduce alla rappresentazione in memoria di numeri negativi, dove il bit alto è impostato se negativo. Quindi se spostati a 0b1000destra di 1 posto con >>otterrai 0b1100, se invece utilizzi >>>otterrai 0b0100. Mentre nel caso indicato nella risposta non ha molta importanza (il numero che viene spostato con né più grande del valore massimo di un intero positivo con segno a 32 bit né negativo), è importante usare quello giusto in questi due casi (tu devi scegliere quale caso devi gestire).
asherkin,

2
@asherkin - Questo non è giusto: "se si sposta a 0b1000destra di 1 posto con >>si ottiene 0b1100". No, hai capito 0b0100. Il risultato dei diversi operatori di spostamento a destra sarà lo stesso per tutti i valori tranne i numeri negativi e i numeri maggiori di 2 ^ 31 (ovvero i numeri con 1 nel primo bit).
gilly3,

29

Molto bella e notevole domanda con una discussione molto interessante! Stavo anche usando la Array.sort()funzione dopo aver inserito un singolo elemento in un array con alcune migliaia di oggetti.

Ho dovuto estendere la tua locationOffunzione per il mio scopo a causa della presenza di oggetti complessi e quindi della necessità di una funzione di confronto come in Array.sort():

function locationOf(element, array, comparer, start, end) {
    if (array.length === 0)
        return -1;

    start = start || 0;
    end = end || array.length;
    var pivot = (start + end) >> 1;  // should be faster than dividing by 2

    var c = comparer(element, array[pivot]);
    if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;

    switch (c) {
        case -1: return locationOf(element, array, comparer, start, pivot);
        case 0: return pivot;
        case 1: return locationOf(element, array, comparer, pivot, end);
    };
};

// sample for objects like {lastName: 'Miller', ...}
var patientCompare = function (a, b) {
    if (a.lastName < b.lastName) return -1;
    if (a.lastName > b.lastName) return 1;
    return 0;
};

7
Vale la pena notare, per la cronaca, che questa versione FUNZIONA correttamente quando si tenta di inserire all'inizio dell'array. (Vale la pena menzionarlo perché la versione nella domanda originale ha un bug e non funziona correttamente per quel caso.)
Garyrob

3
Non sono sicuro che la mia implementazione fosse diversa, ma ho dovuto cambiare il ternario return c == -1 ? pivot : pivot + 1;per restituire l'indice corretto. Altrimenti per un array con lunghezza 1 la funzione restituirebbe -1 o 0.
Niel

3
@James: i parametri start e end sono usati solo su chiamata ricorsiva e non saranno usati su chiamata iniziale. Poiché si tratta di valori di indice per l'array, devono essere di tipo intero e in caso di chiamata ricorsiva ciò viene implicitamente indicato.
kwrl,

1
@TheRedPea: no, volevo dire che >> 1dovrebbe essere più veloce (o non più lento) di/ 2
kwrl,

1
Vedo un potenziale problema con il risultato della comparerfunzione. In questo algoritmo viene confrontato +-1ma potrebbe essere un valore arbitrario <0/ >0. Vedi funzione di confronto . La parte problematica non è solo l' switchaffermazione ma anche la linea: if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;dove cviene confrontata -1anche.
eXavier,

19

C'è un bug nel tuo codice. Dovrebbe leggere:

function locationOf(element, array, start, end) {
  start = start || 0;
  end = end || array.length;
  var pivot = parseInt(start + (end - start) / 2, 10);
  if (array[pivot] === element) return pivot;
  if (end - start <= 1)
    return array[pivot] > element ? pivot - 1 : pivot;
  if (array[pivot] < element) {
    return locationOf(element, array, pivot, end);
  } else {
    return locationOf(element, array, start, pivot);
  }
}

Senza questa correzione il codice non sarà mai in grado di inserire un elemento all'inizio dell'array.


perché stai ordinando un int con 0? cioè cosa inizia || 0 fare?
Pinocchio,

3
@Pinocchio: inizio || 0 è un breve equivalente di: if (! Start) start = 0; - Tuttavia, la versione "più lunga" è più efficace, perché non assegna una variabile a se stessa.
SuperNova,

11

So che questa è una vecchia domanda che ha già una risposta e ci sono molte altre risposte decenti. Vedo alcune risposte che suggeriscono che puoi risolvere questo problema cercando l'indice di inserimento corretto in O (log n) - puoi, ma non puoi inserire in quel momento, perché l'array deve essere parzialmente copiato per rendere spazio.

In conclusione: se hai davvero bisogno che O (log n) inserisca ed elimini in un array ordinato, hai bisogno di una struttura di dati diversa, non di un array. Dovresti usare un B-Tree . I guadagni in termini di prestazioni ottenuti dall'utilizzo di un albero a B per un set di dati di grandi dimensioni ridurranno qualsiasi miglioramento offerto qui.

Se è necessario utilizzare un array. Offro il seguente codice, basato sull'ordinamento per inserzione, che funziona, se e solo se l'array è già ordinato. Ciò è utile nel caso in cui sia necessario ricorrere dopo ogni inserimento:

function addAndSort(arr, val) {
    arr.push(val);
    for (i = arr.length - 1; i > 0 && arr[i] < arr[i-1]; i--) {
        var tmp = arr[i];
        arr[i] = arr[i-1];
        arr[i-1] = tmp;
    }
    return arr;
}

Dovrebbe funzionare in O (n), che penso sia il meglio che puoi fare. Sarebbe più bello se js supportasse assegnazioni multiple. ecco un esempio con cui giocare:

Aggiornare:

questo potrebbe essere più veloce:

function addAndSort2(arr, val) {
    arr.push(val);
    i = arr.length - 1;
    item = arr[i];
    while (i > 0 && item < arr[i-1]) {
        arr[i] = arr[i-1];
        i -= 1;
    }
    arr[i] = item;
    return arr;
}

Collegamento JS Bin aggiornato


In JavaScript il tipo di inserzione che proponi sarà più lento del metodo binario di ricerca e splice, poiché splice ha un'implementazione rapida.
trincot

a meno che javascript non possa in qualche modo violare le leggi della complessità temporale, sono scettico. Hai un esempio eseguibile di come la ricerca binaria e il metodo splice siano più veloci?
domoarigato

Riprendo il mio secondo commento ;-) In effetti, ci sarà una dimensione dell'array oltre la quale una soluzione B-tree supererà la soluzione di splicing.
trincot

9

La tua funzione di inserimento presuppone che l'array specificato sia ordinato, cerca direttamente la posizione in cui è possibile inserire il nuovo elemento, di solito semplicemente guardando alcuni degli elementi dell'array.

La funzione di ordinamento generale di un array non può prendere queste scorciatoie. Ovviamente deve almeno ispezionare tutti gli elementi dell'array per vedere se sono già stati ordinati correttamente. Questo fatto da solo rende l'ordinamento generale più lento della funzione di inserimento.

Un algoritmo di ordinamento generico è generalmente in media O (n ⋅ log (n)) e, a seconda dell'implementazione, potrebbe effettivamente essere il caso peggiore se l'array è già ordinato, portando a complessità di O (n 2 ) . La ricerca diretta della posizione di inserimento ha invece solo una complessità di O (log (n)) , quindi sarà sempre molto più veloce.


Vale la pena notare che l'inserimento di un elemento in un array ha una complessità di O (n), quindi il risultato finale dovrebbe essere più o meno lo stesso.
NemPlayer il

5

Per un numero limitato di articoli, la differenza è piuttosto banale. Tuttavia, se stai inserendo molti elementi o stai lavorando con un array molto grande, chiamare .sort () dopo ogni inserimento causerà un'enorme quantità di sovraccarico.

Ho finito per scrivere una funzione binaria di ricerca / inserimento piuttosto liscia per questo preciso scopo, quindi ho pensato di condividerla. Dal momento che utilizza un whileciclo invece della ricorsione, non c'è nessun ascolto per chiamate di funzione extra, quindi penso che le prestazioni saranno persino migliori di uno dei metodi pubblicati in origine. Ed emula il Array.sort()comparatore predefinito per impostazione predefinita, ma accetta una funzione di confronto personalizzata se lo si desidera.

function insertSorted(arr, item, comparator) {
    if (comparator == null) {
        // emulate the default Array.sort() comparator
        comparator = function(a, b) {
            if (typeof a !== 'string') a = String(a);
            if (typeof b !== 'string') b = String(b);
            return (a > b ? 1 : (a < b ? -1 : 0));
        };
    }

    // get the index we need to insert the item at
    var min = 0;
    var max = arr.length;
    var index = Math.floor((min + max) / 2);
    while (max > min) {
        if (comparator(item, arr[index]) < 0) {
            max = index;
        } else {
            min = index + 1;
        }
        index = Math.floor((min + max) / 2);
    }

    // insert the item
    arr.splice(index, 0, item);
};

Se sei aperto all'uso di altre librerie, lodash fornisce le funzioni sortIndex e SortLastIndex , che potrebbero essere utilizzate al posto del whileciclo. I due potenziali aspetti negativi sono 1) le prestazioni non sono buone come il mio metodo (pensato non sono sicuro di quanto sia peggio) e 2) non accetta una funzione di confronto personalizzata, solo un metodo per ottenere il valore da confrontare (suppongo che usando il comparatore predefinito).


la chiamata a arr.splice()è sicuramente O (n) complessità temporale.
domoarigato,

4

Ecco alcuni pensieri: in primo luogo, se sei veramente preoccupato per il runtime del tuo codice, assicurati di sapere cosa succede quando chiami le funzioni integrate! Non so dal basso in javascript, ma un rapido google della funzione splice ha restituito questo , il che sembra indicare che stai creando un array completamente nuovo ogni chiamata! Non so se sia davvero importante, ma è sicuramente legato all'efficienza. Vedo che Breton, nei commenti, lo ha già sottolineato, ma sicuramente vale per qualsiasi funzione di manipolazione di array scelta.

Ad ogni modo, a risolvere davvero il problema.

Quando ho letto che volevi ordinare, il mio primo pensiero è usare l'ordinamento per inserzione! . È utile perché viene eseguito in tempo lineare su elenchi ordinati o quasi ordinati . Dato che i tuoi array avranno solo 1 elemento fuori servizio, questo conta come quasi ordinato (tranne per, bene, array di dimensioni 2 o 3 o qualsiasi altra cosa, ma a quel punto, andiamo). Ora, implementare l'ordinamento non è troppo male, ma è una seccatura che potresti non voler affrontare e, di nuovo, non so nulla di javascript e se sarà facile o difficile o quant'altro. Questo elimina la necessità della tua funzione di ricerca e basta premere (come suggerito Breton).

In secondo luogo, la tua funzione di ricerca "quicksort-esque" sembra essere un algoritmo di ricerca binaria ! È un algoritmo molto carino, intuitivo e veloce, ma con un solo problema: è notoriamente difficile da implementare correttamente. Non oserò dire se il tuo è corretto o meno (spero che lo sia, ovviamente! :)), ma fai attenzione se vuoi usarlo.

Comunque, riassunto: l'uso di "push" con l'ordinamento di inserzione funzionerà in tempo lineare (supponendo che il resto dell'array sia ordinato) ed eviti qualsiasi requisito di algoritmo di ricerca binaria disordinato. Non so se questo sia il modo migliore (l'implementazione di base degli array, forse una pazza funzione integrata lo fa meglio, chissà), ma mi sembra ragionevole. :) - Agor.


1
+1 perché tutto ciò che contiene splice()è già O (n). Anche se non crea internamente una nuova copia dell'intero array, deve potenzialmente spostare tutti gli n elementi indietro di 1 posizione se l'elemento deve essere inserito in posizione 0.
j_random_hacker

Credo che l'inserimento sia anche il caso migliore di O (n) e il caso peggiore di O (n ^ 2) (sebbene il caso d'uso dell'OP sia probabilmente il caso migliore).
domoarigato,

Meno uno per parlare con l'OP. Il primo paragrafo sembrò un ammonimento unessessary di non sapere come funziona la giuntura sotto il cofano
Matt Zera,

2

Ecco un confronto di quattro diversi algoritmi per ottenere questo risultato: https://jsperf.com/sorted-array-insert-comparison/1

algoritmi

L'ingenuo è sempre orribile. Sembra per array di piccole dimensioni, gli altri tre non differiscono troppo, ma per array più grandi, gli ultimi 2 superano il semplice approccio lineare.


Perché non testare strutture di dati progettate per implementare inserimenti e ricerche veloci? ex. salta elenchi e BST. stackoverflow.com/a/59870937/3163618
QWR

Come si confronta Nativo ora che Chrome utilizza TimSort ? Da TimSort Wikipedia : "Nel migliore dei casi, che si verifica quando l'input è già ordinato, viene eseguito in tempo lineare".
più elegante il

2

Ecco una versione che utilizza lodash.

const _ = require('lodash');
sortedArr.splice(_.sortedIndex(sortedArr,valueToInsert) ,0,valueToInsert);

nota: sortIndex esegue una ricerca binaria.


1

La migliore struttura di dati che mi viene in mente è un elenco di salto indicizzato che mantiene le proprietà di inserimento degli elenchi collegati con una struttura gerarchica che consente le operazioni del tempo di log. In media, la ricerca, l'inserimento e le ricerche ad accesso casuale possono essere eseguite in tempo O (log n).

Una struttura statistica degli ordini consente l'indicizzazione del tempo di log con una funzione di classificazione.

Se non è necessario un accesso casuale ma è necessario l'inserimento O (log n) e la ricerca di chiavi, è possibile abbandonare la struttura dell'array e utilizzare qualsiasi tipo di albero di ricerca binario .

Nessuna delle risposte che usano array.splice()è affatto efficiente poiché è in media O (n) tempo. Qual è la complessità temporale di array.splice () in Google Chrome?


Come fa questa rispostaIs there a good reason to choose [splice into location found] over [push & sort]?
greybeard il

1
@greybeard Risponde al titolo. cinicamente nessuna delle due scelte è efficace.
qwr

Nessuna delle due opzioni potrebbe essere efficace se comportano la copia di molti elementi di un array.
qwr

1

Ecco la mia funzione, utilizza la ricerca binaria per trovare l'elemento e quindi inserisce in modo appropriato:

function binaryInsert(val, arr){
    let mid, 
    len=arr.length,
    start=0,
    end=len-1;
    while(start <= end){
        mid = Math.floor((end + start)/2);
        if(val <= arr[mid]){
            if(val >= arr[mid-1]){
                arr.splice(mid,0,val);
                break;
            }
            end = mid-1;
        }else{
            if(val <= arr[mid+1]){
                arr.splice(mid+1,0,val);
                break;
            }
            start = mid+1;
        }
    }
    return arr;
}

console.log(binaryInsert(16, [
    5,   6,  14,  19, 23, 44,
   35,  51,  86,  68, 63, 71,
   87, 117
 ]));


0

Non riordinare dopo ogni articolo, è eccessivo.

Se è presente un solo elemento da inserire, è possibile trovare la posizione da inserire utilizzando la ricerca binaria. Quindi utilizzare memcpy o simile per copiare in blocco gli elementi rimanenti per fare spazio a quello inserito. La ricerca binaria è O (log n) e la copia è O (n), fornendo O (n + log n) totale. Usando i metodi sopra, stai facendo un riordinamento dopo ogni inserimento, che è O (n log n).

Importa? Supponiamo che tu stia inserendo casualmente k elementi, dove k = 1000. L'elenco ordinato è 5000 elementi.

  • Binary search + Move = k*(n + log n) = 1000*(5000 + 12) = 5,000,012 = ~5 million ops
  • Re-sort on each = k*(n log n) = ~60 million ops

Se gli elementi k da inserire arrivano ogni volta, allora devi fare ricerca + sposta. Tuttavia, se ti viene fornito un elenco di k elementi da inserire in un array ordinato, in anticipo, allora puoi fare ancora meglio. Ordina gli elementi k, separatamente dall'array n già ordinato. Quindi eseguire un ordinamento di scansione, in cui si spostano contemporaneamente verso il basso entrambi gli array ordinati, unendo l'uno nell'altro. - Ordinamento Merge in un solo passaggio = k log k + n = 9965 + 5000 = ~ 15.000 operazioni

Aggiornamento: riguardo alla tua domanda.
First method = binary search+move = O(n + log n). Second method = re-sort = O(n log n)Spiega esattamente i tempi che stai ricevendo.


si, ma no, dipende dal tuo algoritmo di ordinamento. Utilizzando un ordinamento a bolle nell'ordine inverso, il tuo ordinamento se l'ultimo elemento non è ordinato è sempre in o (n)
njzk2

-1
function insertOrdered(array, elem) {
    let _array = array;
    let i = 0;
    while ( i < array.length && array[i] < elem ) {i ++};
    _array.splice(i, 0, elem);
    return _array;
}
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.