Подтвердить что ты не робот

Поиск, если строка содержит любую строку в коллекции

Я пытаюсь улучшить производительность Java-функции, которая у меня есть, которая определяет, содержит ли данная строка поискa > 0 строк в коллекции. Это может показаться преждевременной оптимизацией, но функция называется LOT, поэтому любая скорость будет очень полезной.

В настоящее время код выглядит следующим образом:

public static boolean containsAny(String searchString, List<String> searchCollection) {
    int size = searchCollection.size();
    for (int i = 0; i < size; i++) {
        String stringInCollection = searchCollection.get(i);
        if (!Util.isNullOrEmpty(stringInCollection)) {
            // This is a performance optimization of contains.
            if (searchString.indexOf(stringInCollection, 0) > -1) {
                return true;
            }
        }
    }
    return false;
}

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

Вышеприведенный код представляет собой довольно простой линейный поиск. Я не думаю, что это может быть значительно улучшено, если мы не изменим структуру данных, чтобы сделать ее лучше, чем O (n). Существуют ли какие-либо структуры данных, которые позволили бы мне это сделать?

4b9b3361

Ответ 1

Можно значительно ускорить его с помощью алгоритма Ахо-Корасика.

Вы можете построить автомат Aho-Corasick для коллекции, используя O (общая длина всех строк в коллекциях) время и пространство. Тогда можно будет проверить, является ли одна из строк в коллекции подстрокой заданной строки S в O (S.lenght), пройдя этот автомат.

Ответ 2

// Make a regex pattern (once only):
StringBuilder pattern = new StringBuilder();
for (String sought : searchCollection) {
    if (!Util.isNullOrEmpty(sought)) {
        if (pattern.length() != 0) {
            pattern.append('|');
        }
        pattern.append(Pattern.quote(sought));
    }
}
final Pattern PATTERN = Pattern.compile("(" + pattern + ")");

Это создает шаблон альтернатив, например "(abc|def|ghi)". Вы можете рассмотреть поиск без учета регистра.

И в функции containsAny:

Matcher m = PATTERN.matcher(searchString);
return m.find();

Компиляция регулярных выражений относительно умна. Это было бы сопоставимо с использованием дерева поиска вашей коллекции искомых слов: "agent" and "agitator" to ("ag", ("ent", "itator"))

Ответ 3

Это интенсивная работа с ЦП и не долгая работа или блокировка ввода-вывода. Если вы используете Java 8, вы можете использовать параллельные потоки для параллельной обработки, как показано ниже. Этот метод был изменен для использования Collection вместо List, чтобы он был более гибким.

public static boolean containsAny(final String searchString,
        final Collection<String> searchCollection) {
    return searchCollection.stream().parallel()
            .anyMatch(x -> searchString.indexOf(x) > -1);
}

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

Ответ 4

Вы можете завершить поиск примерно через 2/3 времени, используя алгоритм Aho Corasick.

Принятый ответ от @user2040251 среди других (включая меня) предложил алгоритм Ахо Корасика.

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

@Vlad создал возможный набор тестов для оценки некоторых предлагаемых решений.

Тесты, выполненные @Marco13 в реализации Java в http://ahocorasick.org/, показывают, что ваша первоначальная реализация была быстрее.

Ваши комментарии предоставили существенные дополнительные сведения о проблеме, которую вы пытаетесь решить:

  • Примерно 30 строк для поиска
  • Строки для поиска длиной от 10 до 40 символов.
  • Строка для поиска обычно составляет около 100 символов.
  • Строка, которую вы ищете, - это путь к файлу.

Я сделал пару быстрых изменений в @Vlad gist, чтобы лучше соответствовать специфике описанной вами проблемы.

Ранее я прокомментировал, что другие протестированные Ахо-Корасиком испытания находили все возможные матчи. Метод, который возвращался после первого совпадения, был намного быстрее. Чтобы узнать, была ли моя интуиция правильной, я создал ветку Robert Bor java Aho -Corasick. Эта ветвь теперь слита в Ахо-Корасик!

  • Завершено 100000 содержитAny в 4337 мс (avg 0 мс)
  • Завершено 100000 содержитAnyWithRegex в 41153 мс (avg 0 мс)
  • Завершено 100000 содержитAnyWithOffset в 23624 мс (avg 0 мс)
  • Завершено 100000 содержитAnyAhoCorasickDotOrg в 7956 мс (avg 0 мс)
  • Завершено 100000 содержитAnyAhoCorasickDotOrgMatches в 5351 мс (avg 0 мс)
  • Завершено 100000 содержитAnyAhoCorasickDYoo в 2948 мс (avg 0 мс)
  • Завершено 100000 содержитAnyHospool в 7052 мс (avg 0 мс)
  • Завершено 100000 содержит AnyRaita в 5397 мс (avg 0 мс)
  • Завершено 100000 содержитAnyJava8StreamParallel в 8285 мс (avg 0 мс)

Я также реализовал метод, который выполнял каждый поиск в своем потоке. Эта реализация была ужасной и выполнялась примерно в 10 раз медленнее.

Обновление: С момента моего первоначального тестирования я столкнулся с Еще быстрее Aho-Corasick реализация.

Я включил контрольный пример реализации параллельного потока Java 8, предложенный @GladwinB, а также две com.eaio.stringsearch.

По-прежнему можно получить прибыль. В этой статье, например, описывается набор вариантов соответствия Aho-Corasick, который кажется подходящим для вашей проблемы. К более быстрому соответствию строк для обнаружения вторжений

Ответ 5

Можете ли вы попробовать с этим решением:

    final String[] searchList = searchCollection.toArray(new String[0]);
    Arrays.sort(searchList, new Comparator<String>() {
        @Override
        public int compare(final String o1, final String o2) {
            if (o1 == null && o2 == null) {
                return 0;
            }
            if (o1 == null || o1.isEmpty()) {
                return 1;
            }
            if (o2 == null || o2.isEmpty()) {
                return -1;
            }
            return o1.compareTo(o2);
        }
    });
    final int result = Arrays.binarySearch(searchList, searchString);
    return result >= 0 ? true : false;

Ответ 6

Сравните с этим своего рода инвертированную и оптимизированную версию:

  public static boolean containsAny(String searchString, List<String> searchCollection) {
    for (int offset = 0; offset < searchString.length(); offset++) {
      for (String sought: searchCollection) {
        int remainder = searchString.length() - offset;
        if (remainder >= sought.length && searchString.startsWith(sought, offset)) {
          return true;
        }
      }
    }
    return false;
  }

Обратите внимание на использование startWith со смещением.

Ответ 7

Я считаю, что наиболее подходящая структура данных для этого - это Дерево суффикса. Для строки размера n, построение дерева принимает Theta(n) и ищет в нем подстроку длиной m, принимает O(m).

Это одна из тех структур данных, которые очень хорошо подходят (и предназначены) для поиска строк. Это очень распространенная структура данных со многими реализациями в Интернете.

Ответ 8

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

Не поймите меня неправильно, ваше узкое место - это строка indexOf. Похоже, что на нее приходится 95% обработки. Но если другие структуры данных не помогают (я попробовал готовый Aho-Corasick Trie, и это было в два раза медленнее), вот что проверить...

Комментарий об использовании indexOf, а не содержит сомнительный. В моих тестах. Я видел около 1,5 млн поисков в секунду с "содержит" и только около 700 тыс. С помощью indexOf. Если у вас есть те же результаты, это удвоит скорость прямо там.

Изменить

// This is a performance optimization of contains.
if (searchString.indexOf(stringInCollection, 0) > -1) {

[назад] на

if (searchString.contains(stringInCollection)) {

Если вам интересно, трю, с которым я тестировал, здесь: http://ahocorasick.org/, и код довольно прост. Проблема, которую я видел, это то, что у нее нет функции для раннего выхода после первого совпадения. Он анализирует всю строку и находит все совпадения. Это было быстрее, чем indexOf() для случаев, когда совпадений не было (830K/sec), но все еще медленнее, чем contains().

Ответ 9

@Yrlec из вашего комментария, что searchCollection можно считать постоянным с небольшим количеством изменений, вы можете сортировать его и кэшировать, или вы можете реализовать собственный класс List, который хранит ссылку на отсортированные элементы, которые добавляются в он.

Причиной этого является то, что у вас есть сортировка searchCollection, тогда вы можете использовать метод compareTo для String и уменьшить количество итераций, тем самым увеличивая производительность вашего метода.

public static boolean containsAny(String searchString, List<String> searchCollectionSorted) {
    int size = searchCollectionSorted.size();
    for (int i = 0; i < size; i++) {
            String stringInCollection = searchCollectionSorted.get(i);
            if (!Util.isNullOrEmpty(stringInCollection)) {
                if(stringInCollection.compareToIgnoreCase(searchString) > 0) {
                    if (searchString.startsWith(stringInCollection) {
                            return true;
                    } else {
                              // No point of iterating if we reach here as the searchstring is greater and hence iterations are saved improving performance
                            break;
                    }
                }
            }
        }    return false;
}

Ответ 10

Вы можете использовать структуру данных HashSet. Но хэш-набор не позволит дублировать. Например, вы не можете иметь строку "foo" дважды в HashSet.

С положительной стороны сложность должна быть O (1).

http://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html

Ответ 11

TreeSet, HashSet или PrefixTree - неплохие решения. Вы должны предпочесть PrefixTree, если вам нужно будет найти, существует ли данный префикс в коллекции (сложность O (длина (S)), в противном случае используйте HashSet. http://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html