Эффективный способ сравнения двух строк (порядок символов не имеет значения) - PullRequest
2 голосов
/ 24 октября 2009

Я пытаюсь придумать алгоритм для сравнения двух строк. Было бы зарегистрировать совпадение любых слов, которые содержат одинаковые буквы. Например, рента и крачка будут эквивалентны, потому что они оба содержат буквы r, e, n, t.

РЕДАКТИРОВАТЬ Я прошу прощения за столь расплывчато. Сравнение будет сделано по двум наборам из нескольких тысяч слов сотни раз. Это лишь малая часть всего кода, поэтому я не хочу, чтобы он все мешал.

Для тех, кто спрашивал «да», было бы очень важно, чтобы совпадение имело место, например, рента также соответствовала бы тройне.

РЕДАКТИРОВАТЬ 2 Для совпадения типа арендной платы == ternicate, ternicate не будет соответствовать арендной плате. Это больше похоже на то, что слово два содержит буквы слова один. Поэтому, если у вас есть дополнительные буквы, это все равно будет совпадение, если слово содержит все буквы первого слова.

Ответы [ 12 ]

11 голосов
/ 24 октября 2009

Хорошо, это действительно плохая идея, но это просто безумие, это может сработать!

  1. Создать список первых 26 простых чисел.

    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, ...]
    
  2. Для каждой буквы слова найдите соответствующее простое число. A & rarr; 2, B & rarr; 3, C & rarr; 5 и т. Д.

  3. Умножьте эти простые числа вместе. Вы получите (очень большое) число.

Слова с одинаковыми буквами будут иметь одинаковые номера. Слова с разными буквами гарантированно будут иметь разные цифры. Почему это?

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

Например, возьмите слова «лицо» и «кафе».

FACE = 13 * 2 * 5 * 11 = 1430  
CAFE = 5 * 2 * 13 * 11 = 1430

Ха! Что может быть эффективнее простого целочисленного сравнения?

...

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

6 голосов
/ 24 октября 2009

Сначала просто отсортируйте символы каждой строки, затем сравните их.

rent == tern
enrt == enrt
4 голосов
/ 24 октября 2009

Ключевым моментом здесь, учитывая неоднозначность вопроса, является то, что не представляется необходимым для подсчета того, сколько раз появляется любая буква, только то, что появляется .

Следовательно, предполагая, что все буквы находятся в диапазоне a-z, а также предполагая, что можно индексировать исходные списки слов в виде массивов, используя целочисленные индексы:

1. создать два массива (одиндля каждого списка).

2. для каждого слова в обоих списках вычисляйте растровое изображение следующим образом:

bitmap = 0
foreach (character in word) {
    bitmap |= (1 << (character - 'a'))
}
arrayX[index] = bitmap;

это растровое изображение представляет собой набор всех букв, которые встречаются в этом слове.

3., затем для каждого слова в наборе A итерируйте по множеству B и сопоставьте, когда

arrayA[indexA] | arrayB[indexB] == arrayB[indexB]

Этот тест будет верным, только если набор символов в этом слове A является подмножеством символов словаB. Операция «или» для наборов битов является эквивалентом оператора объединения (for) для реальных наборов.

См. Статью в Википедии по set mathemtatics - A ⊆ B тогда и только тогда, когдаA ∪ B = B.

Кстати, шаг 3 - это O (n ^ 2), но он все равно должен быть очень быстрым, потому что это просто побитовое сравнение.Несколько тысяч слов в каждом списке (~ 4 млн тестов) должны занимать меньше секунды.

4 голосов
/ 24 октября 2009

Один из вариантов - подсчитать числа каждого символа в каждой строке и сравнить их. Простая реализация должна занять O(max(N, A)) время, где N - длина самой большой строки, а A - размер массива, который вы используете для хранения счетчиков. Например, в Java:

public boolean equalIgnoringOrder(String s1, String s2) {
    if (s1.length() != s2.length()) {
        return false;
    }
    // Assuming characters in the range ASCII 0 to 127 
    int[] c1 = new int[128];
    int[] c2 = new int[128];
    for (int i = 0; i < s1.length(); i++) {
        c1[s1.charAt(i)]++;
        c2[s2.charAt(i)]++;
    }
    for (int i = 0; i < c1.length; i++) {
        if (c1[i] != c2[i]) {
            return false;
        }
    }
    return true;
}

Есть несколько возможных улучшений в этом. Например, вы можете справиться с произвольным набором символов, выполнив уменьшение диапазона; то есть сделайте начальный проход через s1 и s2, ища самые маленькие и самые большие символы в каждом из них, и используйте это, чтобы определить размер c1 и c2 и базовое смещение. Это в среднем займет меньше места и сократит время на инициализацию массивов подсчета. Это также предлагает короткое замыкание для сравнения; например когда самые маленькие и самые большие символы для s1 и s2 не совпадают.

Для сравнения, сравнение строк, отсортированных с использованием heapsort или quicksort, в среднем составило бы O(NlogN) с пробелом O(N), где N - длина большей строки.

Однако, как указывает @pst, константы пропорциональности могут сделать алгоритм O(NlogN) или даже O(N*N) лучше, чем алгоритм O(N), если N невелико. В этом случае средняя длина сравниваемых строк, вероятно, является наиболее важным фактором.

Приведенный выше код эффективно выполняет сортировку по Radix с парой коротких замыканий. (Три, если вы включите короткое замыкание, связанное с уменьшением диапазона.) Таким образом, в конечном итоге все сводится к тому, будет ли лучше быстрая сортировка / сортировка по кучи или сортировка по радиксу. И это зависит от длины входной строки и диапазонов символов.


На другом галсе. @ В ответе Джона предлагается вычислить произведение простых чисел. Если мы выполняем вычисления с использованием представления произвольной точности, результирующие значения будут уникальными для каждого отдельного набора строк «равного порядка игнорирования». К сожалению, вычисление будет O(N*N). (Каждый промежуточный продукт имеет O(N) цифр, а умножение N-значного числа на константу составляет O(N). Сделайте это для N символов, и вы получите O(N*N).)

Но если мы сделаем вычисление по модулю (скажем) 64, результатом будет действительно хороший хеш, нечувствительный к порядку символов; например,

long hash = 1;
for (int i = 0; i < s.length(); i++) {
    hash = hash * primes[s.charAt(i)];
}

Итак, я бы сказал, что алгоритм, обеспечивающий наилучшую производительность и использование пространства в среднем для сравнения случайно сгенерированных строк, вероятно, будет иметь вид:

if (s1.length() != s2.length()) {
    return false;
}
if (hash(s1) != hash(s2)) { // computed as above
    return false;
}
// Compare using sorting or character counting as above.

Один последний момент. Если мы предположим, что строковые указатели не идентичны и строки имеют неодинаковую длину, любой алгоритм, который вычисляет этот equals предикат , должен иметь значение при O(N) или хуже. Он должен проверить каждый символ в обеих строках, чтобы сделать это определение, и для этого требуется O(N) операций.

Любой алгоритм, который выполняет менее 2 * N выборок или менее 2 * N дальнейших операций с извлеченными значениями в этом сценарии доказуемо неверен.

2 голосов
/ 24 октября 2009

Я должен согласиться со Стивеном С - это недостаточно хорошо определено, чтобы ответить .

Я не собираюсь понижать голос, но не могли бы вы объяснить, например, является ли рента эквивалентной терренту? У вас есть ответчики, которые предполагают, что это так (люди, думающие, что число случаев не имеет значения, и другие отвечающие, которые предполагают худшее. Одна из этих групп тратит свое время.

Кроме того, поскольку ваше беспокойство связано с производительностью, нам нужно больше узнать о вашем шаблоне вызовов. Не могли бы вы объяснить, будете ли вы смотреть на пару наборов более одного раза, или наборы будут разными?

И, как терминологическое подергивание, вы, возможно, уже знаете это, но с текущей формулировкой ваш алгоритм не симметричен.

Вы говорите, что рента будет соответствовать терникату, но, очевидно, терникат не будет соответствовать ренте. То есть вы на самом деле не ищете эквивалентности. Вы ищете что-то вроде «найден в» или «может быть сделан из».

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

Не поймите меня неправильно: это интересная проблема ... Я просто не знаю, в чем проблема.

1 голос
/ 24 октября 2009

Я написал много кода, который работал с играми в слова и анаграммами. Обычный подход состоит в том, чтобы преобразовать слово в отсортированный ключ, чтобы, как упоминалось выше, «rent» соответствовал «tern», потому что оба сопоставляются с «enrt». Однако, как только вы начинаете на этом маршруте, становится действительно полезным иметь словарь символов и количество появлений. Вот некоторый код Python, который преобразует несортированную строку в словарь с (ключ = символ, значение = счетчик):

import collections

# Create a defaultdict(int) from a string
def create_collections_dict(key):
    dk = collections.defaultdict(int)
    for k in key:
        dk[k] += 1
    return dk

Теперь вы можете сравнивать слова друг с другом, мгновенно видя, сколько букв у них общего:

# Score the similarity of a defaultdict(int) against a string
# (which is temporarily converted to a defaultdict(int))
def score(dk, cand) :
    dc = create_collections_dict(cand)
    return sum(min(dk[k], dc[k]) for k in dk.keys() if k in dc)

if __name__ == '__main__':
    base = create_collections_dict('rent')
    for word in ['tern', 'ternicate', 'foobar']:
        print word, score(base, word)

Результаты:

tern 4
ternicate 4
foobar 1
1 голос
/ 24 октября 2009

Предполагая, что:

  1. ваши слова состоят только из символов ascii
  2. дело не имеет значения
  3. abc соответствует abcde, а abcde не соответствует abc

Вы можете пройти через строку соответствия (s2), считая символы, затем пройти по значению (s1) и проверить, присутствуют ли все символы в другом, что-то вроде (псевдокод, не проверено):

boolean matches(String s1, String s2) {
   int[]  counts = new int[256];
   char[] c1;
   char[] c2;

   c1 = s1.getCharArray();
   c2 = c2.getCharArray();

   // count char occurences in longest string
   for (int n = 0; n < c2.length; n++) {
       counts[(int)c2[n]]++;
   }

   // check all chars in shortest string are foud in the longest
   for (int n = 0; n < c1.length; n++) {
       if (0 == counts[(int)c1[n]]) {
          return false;
       }
   }

   return true;
}

Это будет O (n) для суммы длин аргументов.

Редактировать: вопрос был изменен на асимметричную функцию между s1 и s2.

1 голос
/ 24 октября 2009

может быть не самый быстрый, но, вероятно, самое короткое решение с использованием java + google-collection + guava (для приведения char[] -> List<Character>)

import com.google.common.collect.ImmutableMultiset;
import com.google.common.primitives.Chars;

public class EqualsOrderignore {
private static boolean compareIgnoreOrder(final String s1, String s2) {
    return ImmutableMultiset.copyOf(Chars.asList(s1.toCharArray()))
            .equals(ImmutableMultiset.copyOf(Chars.asList(s2.toCharArray())));
} 
}

время выполнения этого алгоритма: O (s1.length + s2.length)

Я вполне убежден, что это решение будет работать наравне с решением O (N1 + N2), созданным вручную на виртуальной машине -server.

в качестве плюса это решение будет работать для любых экземпляров символов, а не только для a-Z.

0 голосов
/ 26 октября 2009

Для любого алгоритма, который вы выберете, может быть выполнена оптимизация для строк одинаковой длины. Все, что вам нужно сделать, это XOR каждого символа, если результат равен 0, то они содержат одинаковые буквы. Это не помогает в случае с подстрокой, но может помочь в коротком замыкании при более дорогом сравнении.

0 голосов
/ 25 октября 2009

Если вы просто ищете подмножества и ограничены распространенными английскими буквами, тогда подойдет эффективная гистограмма. Я хотел бы взглянуть на использование 64-разрядного целого числа без знака, с 2 битами для подсчета до 2 вхождений и дополнительных 12 битов для добавления флага переполнения и для подсчета до 3 вхождений 'e t a o i n s r h l d'. Биты заполняются, а не используются двоичными файлами (так что для трех у вас будет 111, в противном случае вам нужно что-то более сложное, чем двоичный файл & для проверки содержимого). Чтобы проверить отношение подмножества, вы проверяете бит переполнения тестируемого подмножества и, если не установлено, вы можете просто использовать побитовое значение и проверять подмножество. Вернитесь к проверке O (длины) отсортированного содержимого строки, если гистограмма переполняется.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...