0

Сортировка массива по "Расстоянию Левенштейна" с максимальной производительностью на JavaScript

24

Проблема с сортировкой массива строк по дистанции Левенштейна в JavaScript

У меня есть случайный массив строк с именами в JavaScript, например:

[@larry, @nicholas, @notch] и т.д.

Все они начинаются с символа @. Я хотел бы отсортировать их по дистанции Левенштейна, чтобы вверху списка были те, что ближе к искомому термину. На данный момент у меня есть код, который использует jQuery для фильтрации массива с помощью метода match() при нажатии клавиш:

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.children('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();
});

Здесь imTheCallback — это массив имен (хештеги или упоминания), а modArr — отсортированный массив. Элементы atRes и tagRes представляют собой элементы, к которым я добавляю каждый раз элементы из массива, чтобы сформировать список имен на основе введенных поисковых терминов.

У меня также есть алгоритм дистанции Левенштейна:

var levenshtein = function(min, split) {
    // Алгоритм Левенштейна
    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);

Как я могу использовать этот алгоритм (или аналогичный) в своем текущем коде для сортировки без ухудшения производительности?

Обновление:

Теперь я использую функцию дистанции Левенштейна от Джеймса Уэстгейта, и она работает очень быстро. Проблема производительности решена, но теперь мне нужно разобраться, как использовать её с методом sort():

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

Теперь у меня проблема с общим пониманием использования метода sort(). Любая помощь будет оценена, спасибо!

5 ответ(ов)

1

Ваш код реализации алгоритма Левенштейна кажется довольно неплохим! Вот несколько замечаний и предложений по оптимизации, которые могли бы помочь улучшить производительность и читаемость вашего кода:

  1. Инициализация матрицы: Вместо создания двумерного массива (d) с помощью вложенных массивов, можно использовать одномерный массив и обрабатывать индексы аккуратно. Это может сэкономить память и повысить скорость доступа.

  2. Упрощение условий: В строке, где проверяется максимальное значение дистанции, можно упростить логику проверки, если позволяют другие условия программы.

  3. Устранение повторных вычислений: Можно попробовать улучшить код, чтобы избежать не нужных вычислений (например, замены символов и их стоимости). Это может быть сделано с помощью выделения повторяющихся значений в переменные.

Ниже приведён переработанный вариант вашего кода с минимальными изменениями и комментариями:

var levDist = function(s, t) {
    var n = s.length, m = t.length;
    
    if (n === 0) return m;
    if (m === 0) return n;

    // Используем одномерный массив для хранения промежуточных значений
    var d = new Array(m + 1);
    for (var j = 0; j <= m; j++) d[j] = j;

    for (var i = 1; i <= n; i++) {
        var prev = d[0]; 
        d[0] = i;

        for (var j = 1; j <= m; j++) {
            var temp = d[j]; // Сохраняем значение до изменения
            var cost = (s.charAt(i - 1) == t.charAt(j - 1)) ? 0 : 1;
            d[j] = Math.min(d[j - 1] + 1, Math.min(d[j] + 1, prev + cost));
            prev = temp; // Присваиваем значение для следующей итерации
        }
    }

    return d[m];
}

Этот вариант должен работать быстрее и занимать меньше памяти, особенно для длинных строк. Попробуйте протестировать и посмотреть, улучшилась ли производительность вашего проверщика спеллинга!

0

Ваше решение выглядит очень хорошо! Этот код реализует алгоритм Левенштейна для вычисления расстояния между двумя строками, что позволяет оценить, насколько они похожи.

Вот краткое объяснение вашего кода:

  1. Вы используете замыкание для создания функции levenshtein, которая инициализирует массив row2 и возвращает внутреннюю функцию.
  2. Внутренняя функция принимает две строки s1 и s2 в качестве параметров.
  3. Если строки одинаковые, то расстояние равно 0. Если одна из строк пустая, то расстояние равно длине другой строки.
  4. Вы инициализируете массив row, который будет использоваться для хранения промежуточных результатов.
  5. Весь алгоритм основывается на циклах, которые итерируют по символам строк, сравнивая их и обновляя значения в массиве row для вычисления стоимости вставки, удаления и замены.

Вы упоминаете, что основное улучшение скорости достигнуто за счёт исключения некоторых массивов. Это действительно может значительно ускорить выполнение, так как уменьшает количество операций с памятью.

Если вам нужно больше оптимизаций, вы можете изучить применение более сложных стратегий, таких как использование одномерной матрицы или сравнение нескольких символов одновременно.

Спасибо за ваше решение и ссылку на jsPerf — это отлично помогает другим лучше понять производительность алгоритма!

0

Обновлено: http://jsperf.com/levenshtein-distance/5

Новое обновление полностью перекрывает все предыдущие бенчмарки. Я специально искал производительность в Chromium/Firefox, так как не имею тестовой среды для IE8/9/10, но оптимизации, которые были внесены, должны применяться ко многим другим браузерам.

Расстояние Левенштейна

Матрица для вычисления расстояния Левенштейна может быть переиспользована многократно. Это был очевидный объект для оптимизации (но будьте осторожны, теперь это накладывает ограничение на длину строки, если только вы не будете динамически изменять размер матрицы).

Единственная возможность для оптимизации, которую не преследовали в Revision 5 jsPerf, — это мемоизация. В зависимости от вашего использования расстояния Левенштейна это может помочь значительно, но было упущено из-за специфической природы реализации.

// Кешируем матрицу. Обратите внимание, что эта реализация ограничена
// строками длиной 64 символа или меньше. Это можно изменить для динамического обновления
// или использовать большее значение.
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;
}

// Функциональная реализация расстояния Левенштейна.
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;

    // Вычисляем матрицу.
    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) {
            // Проверяем текущий жёсткий ld
            if (i === j && matrix[i][j] > 4) return thisLength;

            that_j = that[j-1];
            cost = (this_i === that_j) ? 0 : 1;  // Если символы совпадают, никаких дополнительных операций.
            // Вычисляем минимум (гораздо быстрее, чем Math.min(...)).
            min    = matrix[i - 1][j    ] + 1;                      // Удаление.
            if ((t = matrix[i    ][j - 1] + 1) < min) min = t;     // Вставка.
            if ((t = matrix[i - 1][j - 1] + cost) < min) min = t;  // Замена.

            matrix[i][j] = min; // Обновляем матрицу.
        }
    }

    return matrix[thisLength][thatLength];
};

Расстояние Дамерау-Левенштейна

jsperf.com/damerau-levenshtein-distance

Расстояние Дамерау-Левенштейна — это небольшое изменение расстояния Левенштейна для учета перестановок. Оптимизировать здесь практически нечего.

// Перестановка Дамерау.
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;

Алгоритм сортировки

Вторая часть этого ответа — выбрать подходящую функцию сортировки. Я скоро загружу оптимизированные функции сортировки на http://jsperf.com/sort.

0

Очевидный способ сделать это — сопоставить каждую строку с парой (расстояние, строка), затем отсортировать этот список и, наконец, снова убрать расстояния. Таким образом, вы обеспечите, что расстояние по Левенштейну вычисляется только один раз. Возможно, стоит сначала объединить дубликаты.

0

Я определенно рекомендую использовать более эффективный метод Левенштейна, как указано в ответе @James Westgate.

Тем не менее, манипуляции с DOM часто требуют значительных затрат. Вы можете улучшить использование jQuery.

Ваши циклы достаточно малы в приведенном выше примере, но гораздо более эффективно будет объединить сгенерированный HTML для каждого oneResult в одну строку и выполнить один append в конце цикла.

Ваши селекторы работают медленно. Использование $('.oneResult') будет искать все элементы в DOM и проверять их className в старых версиях IE. Рассмотрите возможность использования такого подхода, как atRes.find('.oneResult'), чтобы сузить область поиска.

Когда дело касается добавления обработчиков click, лучше всего избегать установки обработчиков на каждый keyup. Вы можете использовать делегирование событий, установив один обработчик на atRest для всех результатов в том же блоке, где вы устанавливаете обработчик keyup:

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

Для получения дополнительной информации обратитесь к документации: http://api.jquery.com/on/.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь