web-dev-qa-db-fra.com

Un moyen efficace d’insérer un nombre dans un tableau de nombres trié?

J'ai un tableau JavaScript trié et je souhaite insérer un autre élément dans le tableau, de sorte que le tableau résultant reste trié. Je pourrais certainement implémenter une simple fonction d'insertion de type 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));

[AVERTISSEMENT] ce code présente un bogue lors de la tentative d'insertion au début du tableau, par exemple insert(2, [3, 7 ,9]) produit une valeur incorrecte [3, 2, 7, 9].

Cependant, j'ai remarqué que les implémentations de la fonction Array.sort pourraient potentiellement le faire pour moi, et de manière native:

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

Existe-t-il une bonne raison de choisir la première implémentation plutôt que la seconde?

Edit : Notez que dans le cas général, une insertion O(log(n)) (telle que mise en œuvre dans le premier exemple) sera plus rapide qu'un algorithme de tri générique; Cependant, ce n'est pas nécessairement le cas pour JavaScript en particulier. Notez que:

  • Le meilleur des cas pour plusieurs algorithmes d’insertion est O (n), qui est toujours très différent de O (log (n)), mais pas aussi mauvais que O (n log (n)) comme indiqué ci-dessous. Cela dépendrait de l'algorithme de tri particulier utilisé (voir Implémentation de Javascript Array.sort? )
  • La méthode de tri en JavaScript est une fonction native, ce qui permet de réaliser d’énormes avantages - O(log(n)) avec un coefficient énorme peut encore être bien pire que O(n) pour des ensembles de données de taille raisonnable.
107
Elliot Kroo

En tant que point de données unique, j’ai testé cela en insérant 1000 éléments aléatoires dans un tableau de 100 000 nombres prédéfinis à l’aide des deux méthodes d’utilisation de Chrome sous Windows 7:

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

Donc, au moins sur cette configuration, la méthode native ne l’a pas compensée. Cela est vrai même pour de petits ensembles de données, en insérant 100 éléments dans un tableau de 1000:

First Method:
1 milliseconds
Second Method:
34 milliseconds
51
Sam Phillips

Simple ( Démo ):

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

Très bonne et remarquable question avec une discussion très intéressante! J'utilisais aussi la fonction Array.sort() après avoir poussé un seul élément dans un tableau contenant des milliers d'objets.

J'ai dû étendre votre fonction locationOf à mon propos à cause de la présence d'objets complexes et donc du besoin d'une fonction de comparaison comme dans 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;
};
26
kwrl

Il y a un bug dans votre code. Il devrait lire:

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

Sans cette correction, le code ne pourra jamais insérer un élément au début du tableau.

15
syntheticzero

Votre fonction d'insertion suppose que le tableau donné est trié. Elle recherche directement l'emplacement où le nouvel élément peut être inséré, généralement en ne regardant que quelques-uns des éléments du tableau.

La fonction de tri général d'un tableau ne peut pas prendre ces raccourcis. Évidemment, il doit au moins inspecter tous les éléments du tableau pour voir s’ils ont déjà été correctement commandés. Ce seul fait rend le tri général plus lent que la fonction d'insertion.

Un algorithme de tri générique est généralement en moyenne O (n ⋅ log (n)) et selon l’implémentation, il pourrait en fait être le pire des cas si le tableau est déjà trié, ce qui entraîne une complexité de O (n2) . La recherche directe de la position d’insertion à la place a juste une complexité de O(log(n)) , donc ce sera toujours beaucoup plus rapide.

9
sth

Je sais que c'est une vieille question qui a déjà une réponse, et il existe un certain nombre d'autres réponses décentes. Je vois des réponses qui suggèrent que vous pouvez résoudre ce problème en recherchant le bon index d'insertion dans O (log n). Vous pouvez le faire, mais vous ne pouvez pas insérer à ce moment-là, car le tableau doit être partiellement copié pour pouvoir être créé. espace. 

Ligne inférieure: Si vous avez vraiment besoin que O (log n) insère et supprime dans un tableau trié, vous avez besoin d'une structure de données différente - pas d'un tableau. Vous devriez utiliser un B-Tree . Les gains de performances que vous obtiendrez de l’utilisation d’un arbre B-Tree pour un grand ensemble de données contrediront toutes les améliorations proposées ici.

Si vous devez utiliser un tableau. Je propose le code suivant, basé sur le tri par insertion, qui fonctionne, si et seulement si le tableau est déjà trié. Ceci est utile dans le cas où vous devez recourir après chaque insertion:

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

Il devrait fonctionner dans O (n), ce qui, à mon avis, est le mieux que vous puissiez faire. Serait plus agréable si js supportait plusieurs assignations . voici un exemple pour jouer avec:

Mettre à jour:

cela pourrait être plus rapide:

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

Mise à jour du lien JS Bin

6
domoarigato

Voici quelques réflexions: Premièrement, si vous êtes réellement préoccupé par le temps d’exécution de votre code, assurez-vous de savoir ce qui se passe lorsque vous appelez les fonctions intégrées! Je ne sais pas du haut de javascript, mais un rapide google de la fonction d'épissure a renvoyé this , qui semble indiquer que vous créez un nouveau tableau à chaque appel! Je ne sais pas si c'est vraiment important, mais c'est certainement lié à l'efficacité. Je vois que Breton, dans les commentaires, l'a déjà souligné, mais cela est certainement valable pour la fonction de manipulation de tableau que vous choisissez.

Quoi qu'il en soit, sur réellement résoudre le problème.

Quand j'ai lu que vous vouliez trier, ma première pensée est d'utiliser tri par insertion! . Il est pratique car il s'exécute en temps linéaire sur des listes triées ou presque triées. Comme vos tableaux n'auront qu'un élément en panne, cela compte comme étant presque trié (sauf pour les tableaux de taille 2 ou 3 ou autre, mais à ce moment-là, allez). Maintenant, implémenter ce type n’est pas trop grave, mais c’est un problème que vous ne voudrez peut-être pas traiter, et encore une fois, je ne connais rien au javascript et si ce sera facile, difficile ou non. Cela supprime la nécessité de votre fonction de recherche et vous appuyez simplement sur Push (comme suggéré par Breton).

Deuxièmement, votre fonction de recherche "quicksort-esque" semble être un algorithme recherche binaire ! C'est un algorithme très agréable, intuitif et rapide, mais avec un problème: il est notoirement difficile à implémenter correctement. Je n'oserai pas dire si le tien est correct ou non (j'espère que c'est le cas, bien sûr! :)), mais méfie-toi si tu veux l'utiliser.

Quoi qu'il en soit, résumé: l'utilisation de "Push" avec le tri par insertion fonctionnera en temps linéaire (en supposant que le reste du tableau est trié), et évitera toute exigence d'algorithme de recherche binaire compliquée. Je ne sais pas si c'est le meilleur moyen (l'implémentation sous-jacente de tableaux, peut-être une fonction intégrée folle le fait-elle mieux, qui sait), mais cela me semble raisonnable. :) - Agor.

5
agorenst

Pour un petit nombre d'articles, la différence est assez triviale. Cependant, si vous insérez beaucoup d'éléments ou travaillez avec un très grand tableau, appeler .sort () après chaque insertion engendrera une surcharge considérable.

J'ai fini par écrire une fonction de recherche/insertion binaire assez astucieuse dans ce but précis, alors j'ai pensé la partager. Puisqu'elle utilise une boucle while au lieu de la récursivité, il n'y a pas d'écoute pour les appels de fonction supplémentaires. Je pense donc que les performances seront encore meilleures que l'une ou l'autre des méthodes publiées à l'origine. Et il émule le comparateur par défaut Array.sort() par défaut, mais accepte une fonction de comparateur personnalisée si vous le souhaitez.

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

Si vous êtes ouvert à l’utilisation d’autres bibliothèques, lodash fournit les fonctions sortIndex et sortLastIndex , qui pourraient être utilisées à la place de la boucle while. Les deux inconvénients potentiels sont 1) les performances ne sont pas aussi bonnes que ma méthode (bien que je ne sache pas à quel point c'est pire) et 2) il n'accepte pas une fonction de comparaison personnalisée, mais uniquement une méthode permettant d'obtenir la valeur à comparer. (en utilisant le comparateur par défaut, je suppose).

4
Sean the Bean

Voici une comparaison de quatre algorithmes différents pour y parvenir: https://jsperf.com/sorted-array-insert-comparison/1

Algorithmes

Naïf est toujours horrible. Il semble que pour les petites tailles de tableau, les trois autres ne diffèrent pas trop, mais pour les plus grands, les deux derniers sont plus performants que l'approche linéaire simple.

0
gabtub
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;
}
0
Marina

Voici une version qui utilise lodash.

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

remarque: triésIndex effectue une recherche binaire.

0
I. Cantrell

Ne pas re-trier après chaque article, son excès ..

S'il n'y a qu'un seul élément à insérer, vous pouvez trouver l'emplacement à insérer à l'aide de la recherche binaire. Ensuite, utilisez memcpy ou similaire pour copier en bloc les éléments restants afin de laisser de la place pour ceux qui ont été insérés. La recherche binaire est O (log n) et la copie est O (n), ce qui donne O (n + log n) total. En utilisant les méthodes ci-dessus, vous effectuez un nouveau tri après chaque insertion, qui est O (n log n).

Est-ce que ça importe? Disons que vous insérez au hasard k éléments, où k = 1000. La liste triée contient 5 000 éléments.

  • 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

Si les k éléments à insérer arrivent à tout moment, vous devez alors rechercher + déplacer. Cependant, si on vous donne une liste de k éléments à insérer dans un tableau trié - à l'avance -, vous pourrez faire encore mieux. Triez les k éléments séparément du tableau n déjà trié. Effectuez ensuite un tri par balayage dans lequel vous déplacez simultanément les deux tableaux triés, en les fusionnant. - Tri par fusion en une étape = k log k + n = 9965 + 5000 = ~ 15 000 ops

Mise à jour: en ce qui concerne votre question.
First method = binary search+move = O(n + log n). Second method = re-sort = O(n log n) explique exactement les horaires que vous obtenez.

0
Rama Hoetzlein