web-dev-qa-db-fra.com

Trier un tableau par la "distance de Levenshtein" avec les meilleures performances en Javascript

J'ai donc un tableau de noms javascript aléatoire ...

[@ larry, @ nicholas, @ notch] etc.

Ils commencent tous par le symbole @. Je voudrais les trier par la distance de Levenshtein afin que ceux en haut de la liste soient les plus proches du terme de recherche. À l'heure actuelle, j'ai du javascript qui utilise la fonction .grep() de jQuery dessus en utilisant la méthode javascript .match() autour du terme de recherche entré lors de la pression de la touche:

(code modifié depuis la première publication)

limitArr = $.grep(imTheCallback, function(n){
    return n.match(searchy.toLowerCase())
});
modArr = limitArr.sort(levenshtein(searchy.toLowerCase(), 50))
if (modArr[0].substr(0, 1) == '@') {
    if (atRes.childred('div').length < 6) {
        modArr.forEach(function(i){
            atRes.append('<div class="oneResult">' + i + '</div>');
        });
    }
} else if (modArr[0].substr(0, 1) == '#') {
    if (tagRes.children('div').length < 6) {
        modArr.forEach(function(i){
            tagRes.append('<div class="oneResult">' + i + '</div>');
        });
    }
}

$('.oneResult:first-child').addClass('active');

$('.oneResult').click(function(){
    window.location.href = 'http://hashtag.ly/' + $(this).html();
});

Il contient également des instructions if qui détectent si le tableau contient des hashtags (#) ou des mentions (@). Ignore ça. imTheCallback est le tableau des noms, soit des hashtags ou des mentions, puis modArr est le tableau trié. Ensuite, les éléments .atResults Et .tagResults Sont les éléments qu'il ajoute à chaque fois dans le tableau, ce qui forme une liste de noms basée sur les termes de recherche saisis.

I aussi ai l'algorithme de distance de Levenshtein:

var levenshtein = function(min, split) {
    // Levenshtein Algorithm Revisited - WebReflection
    try {
        split = !("0")[0]
    } catch(i) {
        split = true
    };

    return function(a, b) {
        if (a == b)
            return 0;
        if (!a.length || !b.length)
            return b.length || a.length;
        if (split) {
            a = a.split("");
            b = b.split("")
        };
        var len1 = a.length + 1,
            len2 = b.length + 1,
            I = 0,
            i = 0,
            d = [[0]],
            c, j, J;
        while (++i < len2)
            d[0][i] = i;
        i = 0;
        while (++i < len1) {
            J = j = 0;
            c = a[I];
            d[i] = [i];
            while(++j < len2) {
                d[i][j] = min(d[I][j] + 1, d[i][J] + 1, d[I][J] + (c != b[J]));
                ++J;
            };
            ++I;
        };
        return d[len1 - 1][len2 - 1];
    }
}(Math.min, false);

Comment puis-je travailler avec un algorithme (ou un similaire) dans mon code actuel pour le trier sans mauvaises performances?

METTRE À JOUR:

J'utilise donc maintenant la fonction Lev Dist de James Westgate. Fonctionne rapidement WAYYYY. Les performances sont donc résolues, le problème est maintenant de les utiliser avec la source ...

modArr = limitArr.sort(function(a, b){
    levDist(a, searchy)
    levDist(b, searchy)
});

Mon problème est maintenant la compréhension générale de l'utilisation de la méthode .sort(). L'aide est appréciée, merci.

Merci!

48
alt

J'ai écrit un correcteur orthographique en ligne il y a quelques années et j'ai implémenté un algorithme Levenshtein - car il était en ligne et pour IE8, j'ai fait beaucoup d'optimisation des performances.

var levDist = function(s, t) {
    var d = []; //2d matrix

    // Step 1
    var n = s.length;
    var m = t.length;

    if (n == 0) return m;
    if (m == 0) return n;

    //Create an array of arrays in javascript (a descending loop is quicker)
    for (var i = n; i >= 0; i--) d[i] = [];

    // Step 2
    for (var i = n; i >= 0; i--) d[i][0] = i;
    for (var j = m; j >= 0; j--) d[0][j] = j;

    // Step 3
    for (var i = 1; i <= n; i++) {
        var s_i = s.charAt(i - 1);

        // Step 4
        for (var j = 1; j <= m; j++) {

            //Check the jagged ld total so far
            if (i == j && d[i][j] > 4) return n;

            var t_j = t.charAt(j - 1);
            var cost = (s_i == t_j) ? 0 : 1; // Step 5

            //Calculate the minimum
            var mi = d[i - 1][j] + 1;
            var b = d[i][j - 1] + 1;
            var c = d[i - 1][j - 1] + cost;

            if (b < mi) mi = b;
            if (c < mi) mi = c;

            d[i][j] = mi; // Step 6

            //Damerau transposition
            if (i > 1 && j > 1 && s_i == t.charAt(j - 2) && s.charAt(i - 2) == t_j) {
                d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
            }
        }
    }

    // Step 7
    return d[n][m];
}
101
James Westgate

Je suis arrivé à cette solution:

var levenshtein = (function() {
        var row2 = [];
        return function(s1, s2) {
            if (s1 === s2) {
                return 0;
            } else {
                var s1_len = s1.length, s2_len = s2.length;
                if (s1_len && s2_len) {
                    var i1 = 0, i2 = 0, a, b, c, c2, row = row2;
                    while (i1 < s1_len)
                        row[i1] = ++i1;
                    while (i2 < s2_len) {
                        c2 = s2.charCodeAt(i2);
                        a = i2;
                        ++i2;
                        b = i2;
                        for (i1 = 0; i1 < s1_len; ++i1) {
                            c = a + (s1.charCodeAt(i1) === c2 ? 0 : 1);
                            a = row[i1];
                            b = b < a ? (b < c ? b + 1 : c) : (a < c ? a + 1 : c);
                            row[i1] = b;
                        }
                    }
                    return b;
                } else {
                    return s1_len + s2_len;
                }
            }
        };
})();

Voir aussi http://jsperf.com/levenshtein-distance/12

La plupart de la vitesse a été gagnée en éliminant certaines utilisations de la baie.

13
Marco de Wit

Mise à jour: http://jsperf.com/levenshtein-distance/5

La nouvelle révision annihile tous les autres repères. Je cherchais spécifiquement les performances de Chromium/Firefox car je n'ai pas d'environnement de test IE8/9/10, mais les optimisations apportées devraient s'appliquer en général à la plupart des navigateurs.

Distance Levenshtein

La matrice pour effectuer Levenshtein Distance peut être réutilisée encore et encore. C'était une cible évidente pour l'optimisation (mais attention, cela impose maintenant une limite sur la longueur de la chaîne (sauf si vous deviez redimensionner la matrice dynamiquement)).

La seule option d'optimisation non poursuivie dans jsPerf Revision 5 est la mémoisation. Selon votre utilisation de Levenshtein Distance, cela pourrait aider considérablement, mais a été omis en raison de sa nature spécifique à la mise en œuvre.

// Cache the matrix. Note this implementation is limited to
// strings of 64 char or less. This could be altered to update
// dynamically, or a larger value could be used.
var matrix = [];
for (var i = 0; i < 64; i++) {
    matrix[i] = [i];
    matrix[i].length = 64;
}
for (var i = 0; i < 64; i++) {
    matrix[0][i] = i;
}

// Functional implementation of Levenshtein Distance.
String.levenshteinDistance = function(__this, that, limit) {
    var thisLength = __this.length, thatLength = that.length;

    if (Math.abs(thisLength - thatLength) > (limit || 32)) return limit || 32;
    if (thisLength === 0) return thatLength;
    if (thatLength === 0) return thisLength;

    // Calculate matrix.
    var this_i, that_j, cost, min, t;
    for (i = 1; i <= thisLength; ++i) {
        this_i = __this[i-1];

        for (j = 1; j <= thatLength; ++j) {
            // Check the jagged ld total so far
            if (i === j && matrix[i][j] > 4) return thisLength;

            that_j = that[j-1];
            cost = (this_i === that_j) ? 0 : 1;  // Chars already match, no ++op to count.
            // Calculate the minimum (much faster than Math.min(...)).
            min    = matrix[i - 1][j    ] + 1;                      // Deletion.
            if ((t = matrix[i    ][j - 1] + 1   ) < min) min = t;   // Insertion.
            if ((t = matrix[i - 1][j - 1] + cost) < min) min = t;   // Substitution.

            matrix[i][j] = min; // Update matrix.
        }
    }

    return matrix[thisLength][thatLength];
};

Distance Damerau-Levenshtein

jsperf.com/damerau-levenshtein-distance

La distance de Damerau-Levenshtein est une petite modification de la distance de Levenshtein pour inclure les transpositions. Il y a très peu à optimiser.

// Damerau transposition.
if (i > 1 && j > 1 && this_i === that[j-2] && this[i-2] === that_j
&& (t = matrix[i-2][j-2]+cost) < matrix[i][j]) matrix[i][j] = t;

Algorithme de tri

La deuxième partie de cette réponse consiste à choisir une fonction de tri appropriée. Je téléchargerai bientôt des fonctions de tri optimisées dans http://jsperf.com/sort .

6
TheSpanishInquisition

J'ai implémenté une implémentation très performante du calcul de la distance du levenshtein si vous en avez encore besoin.

function levenshtein(s, t) {
    if (s === t) {
        return 0;
    }
    var n = s.length, m = t.length;
    if (n === 0 || m === 0) {
        return n + m;
    }
    var x = 0, y, a, b, c, d, g, h, k;
    var p = new Array(n);
    for (y = 0; y < n;) {
        p[y] = ++y;
    }

    for (; (x + 3) < m; x += 4) {
        var e1 = t.charCodeAt(x);
        var e2 = t.charCodeAt(x + 1);
        var e3 = t.charCodeAt(x + 2);
        var e4 = t.charCodeAt(x + 3);
        c = x;
        b = x + 1;
        d = x + 2;
        g = x + 3;
        h = x + 4;
        for (y = 0; y < n; y++) {
            k = s.charCodeAt(y);
            a = p[y];
            if (a < c || b < c) {
                c = (a > b ? b + 1 : a + 1);
            }
            else {
                if (e1 !== k) {
                    c++;
                }
            }

            if (c < b || d < b) {
                b = (c > d ? d + 1 : c + 1);
            }
            else {
                if (e2 !== k) {
                    b++;
                }
            }

            if (b < d || g < d) {
                d = (b > g ? g + 1 : b + 1);
            }
            else {
                if (e3 !== k) {
                    d++;
                }
            }

            if (d < g || h < g) {
                g = (d > h ? h + 1 : d + 1);
            }
            else {
                if (e4 !== k) {
                    g++;
                }
            }
            p[y] = h = g;
            g = d;
            d = b;
            b = c;
            c = a;
        }
    }

    for (; x < m;) {
        var e = t.charCodeAt(x);
        c = x;
        d = ++x;
        for (y = 0; y < n; y++) {
            a = p[y];
            if (a < c || d < c) {
                d = (a > d ? d + 1 : a + 1);
            }
            else {
                if (e !== s.charCodeAt(y)) {
                    d = c + 1;
                }
                else {
                    d = c;
                }
            }
            p[y] = d;
            c = a;
        }
        h = d;
    }

    return h;
}

C'était ma réponse à une question similaire SO Implémentation Levenshtein Javascript la plus rapide

Mise à jour

Une version améliorée de ce qui précède est maintenant sur github/npm voir https://github.com/gustf/js-levenshtein

4
gustf

Je suggérerais certainement d'utiliser une meilleure méthode Levenshtein comme celle de la réponse de @James Westgate.

Cela dit, les manipulations DOM sont souvent très coûteuses. Vous pouvez certainement améliorer votre utilisation de jQuery.

Vos boucles sont plutôt petites dans l'exemple ci-dessus, mais concaténer le code HTML généré pour chaque oneResult en une seule chaîne et en faire une append à la fin de la boucle sera beaucoup plus efficace.

Vos sélecteurs sont lents. $('.oneResult') recherchera tous les éléments dans le DOM et testera leur className dans les anciens IE navigateurs. Vous voudrez peut-être envisager quelque chose comme atRes.find('.oneResult') pour étendre la recherche.

Dans le cas de l'ajout des gestionnaires click, nous souhaitons peut-être mieux éviter de définir des gestionnaires sur chaque keyup. Vous pouvez tirer parti de la délégation d'événements en définissant un seul gestionnaire sur atRest pour tous les résultats dans le même bloc que vous définissez le gestionnaire keyup:

atRest.on('click', '.oneResult', function(){
  window.location.href = 'http://hashtag.ly/' + $(this).html();
});

Voir http://api.jquery.com/on/ pour plus d'informations.

2
Jacob Swartwood

La façon évidente de le faire est de mapper chaque chaîne à une paire (distance, chaîne), puis de trier cette liste, puis de supprimer à nouveau les distances. De cette façon, vous vous assurez que la distance de Levenstein ne doit être calculée qu'une seule fois. Peut-être aussi fusionner les doublons en premier.

2
Anony-Mousse

Je viens d'écrire une nouvelle révision: http://jsperf.com/levenshtein-algorithms/16

function levenshtein(a, b) {
  if (a === b) return 0;

  var aLen = a.length;
  var bLen = b.length;

  if (0 === aLen) return bLen;
  if (0 === bLen) return aLen;

  var len = aLen + 1;
  var v0 = new Array(len);
  var v1 = new Array(len);

  var i = 0;
  var j = 0;
  var c2, min, tmp;

  while (i < len) v0[i] = i++;

  while (j < bLen) {
    c2 = b.charAt(j++);
    v1[0] = j;
    i = 0;

    while (i < aLen) {
      min = v0[i] - (a.charAt(i) === c2 ? 1 : 0);
      if (v1[i] < min) min = v1[i];
      if (v0[++i] < min) min = v0[i];
      v1[i] = min + 1;
    }

    tmp = v0;
    v0 = v1;
    v1 = tmp;
  }
  return v0[aLen];
}

Cette révision est plus rapide que les autres. Fonctionne même sur IE =)

1
gtournie