Эффективно проверить строку на наличие одного из нескольких сотен возможных суффиксов

Мне нужно написать функцию C / C++, которая быстро проверит, заканчивается ли строка одним из ~1000 предопределенных суффиксов. В частности, строка-это имя хоста, и мне нужно проверить, принадлежит ли она одному из нескольких сотен предопределенных доменов второго уровня.

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

набор суффиксов предопределяется во время компиляции и не меняется.

Я думаю либо реализовать вариант Рабин-карпа, либо написать инструмент, который будет генерировать функцию с вложенными ifs и коммутаторами, которые будут адаптированы к определенному набору суффиксов. Поскольку рассматриваемое приложение является 64-битным для ускорения сравнения, я мог бы хранить суффиксы длиной до 8 байт в виде массива const и выполнять двоичный поиск в нем.

есть ли другие разумные варианты?

7 ответов


Если суффиксы не содержат никаких расширений / правил (например, регулярное выражение), Вы можете создать Бор суффиксов в обратном порядке, а затем соответствуют строке на основе этого. Например,

suffixes:
  foo
  bar
  bao

reverse order suffix trie:
  o
   -a-b  (matches bao)
   -o-f  (matches foo)
  r-a-b  (matches bar)

затем их можно использовать для соответствия вашей строке:

"mystringfoo" -> reverse -> "oofgnirtsym" -> trie match -> foo suffix

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

затем просто используйте hashtable. Измерьте его таким образом, чтобы не было столкновений, поэтому вам не нужны ведра; поиск будет точно O(1). Для небольших типов хэша (например, 32 бита) вы хотите проверить, действительно ли строки совпадают. Для 64-битного хэша вероятность другого домена столкновение с одним из хэшей в вашей таблице уже настолько низкое (порядка 10^-17), что вы, вероятно, можете жить с ним.


Я бы перевернул все строки суффикса, построил из них префиксное дерево, а затем протестировал обратную строку IP против этого.


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

Я думаю, что вы можете легко использовать гибкий график работы чтобы сделать это, заботясь о реверсировании ввода или обработке особым образом того факта, что вы ищете только суффиксы (только для эффективности)..

кстати, используя Рабин-Карп подход также будет эффективным, так как ваши суффиксы будут короткими. Вы можете поместить хэш-набор со всеми необходимыми суффиксами, а затем

  • возьмем строку
  • возьмите суффикс
  • вычислить хэш-суффикс
  • проверьте, есть ли суффикс в таблице

просто создайте массив 26x26 из набора доменов. например, thisArray[0] [0] будут доменами, которые заканчиваются на "aa", thisArray[0] [1] - это все домены, которые заканчиваются на " ab " и так далее...

Как только у вас это будет, просто найдите в своем массиве thisArray[2-й последний символ имени хоста][последний символ имени хоста], чтобы получить возможные Домены. Если на этом этапе их больше одного,просто заставьте остальных.


Я думаю, что решение должно быть очень разными в зависимости от типа входных строк. Если строки являются своего рода строковым классом, который может быть повторен с конца (например, строки stl), это намного проще, чем если бы они были нулевыми c-строками.

Строковый Класс

повторите строку назад (не делайте обратную копию-используйте какой-то обратный итератор). Постройте Trie, где каждый узел состоит из двух 64-разрядных слов, одного шаблона и одной битовой маски. Затем проверьте 8 символов одновременно на каждом уровне. Маска используется, если вы хотите сопоставить менее 8 символов-например, запретить"*.org " даст маску с набором 32 бит. Маска также используется в качестве критерия завершения.

C строк

создайте NDFA, который соответствует строкам на одном проходе над ними. Таким образом, вам не нужно сначала повторять до конца, но вместо этого вы можете использовать его за один проход. NDFA может быть преобразован в DFA, что, вероятно, сделает реализацию более эффективный. Как построение NDFA, так и преобразование в DFA, вероятно, будут настолько сложными, что вам придется писать инструменты для этого.


после некоторых исследований и обсуждений я решил пойти с трие/конечным государственным машинным подходом.

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

Так как поиск trie-это постоянное время, наихудшая сложность - o (максимальная длина суффикса). Функция оказалась довольно быстрой. На 2.8 Ghz Core i5 он может проверять 33,000,000 строк в секунду для 2K возможных суффиксов. 2K суффиксов на общую сумму 18 килобайт, расширен до 320kb trie / state machine table. Я думаю, что я мог бы сохранить его более эффективно, но это решение кажется пока работайте достаточно хорошо.

поскольку список суффиксов был настолько большим, я не хотел кодировать все это вручную, поэтому я закончил писать приложение c#, которое сгенерировало код C для функции проверки суффикса:

    public static uint GetFourBytes(string s, int index)
    {
        byte[] bytes = new byte[4] { 0, 0, 0, 0};
        int len = Math.Min(s.Length - index, 4);
        Encoding.ASCII.GetBytes(s, index, len, bytes, 0);
        return BitConverter.ToUInt32(bytes, 0);
    }

    public static string ReverseString(string s)
    {
        char[] chars = s.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }

    static StringBuilder trieArray = new StringBuilder();
    static int trieArraySize = 0;

    static void Main(string[] args)
    {
        // read all non-empty lines from input file
        var suffixes = File
            .ReadAllLines(@"suffixes.txt")
            .Where(l => !string.IsNullOrEmpty(l));

        var reversedSuffixes = suffixes
            .Select(s => ReverseString(s));

        int start = CreateTrieNode(reversedSuffixes, "");

        string outFName = @"checkStringSuffix.debug.h";
        if (args.Length != 0 && args[0] == "--release")
        {
            outFName = @"checkStringSuffix.h";
        }

        using (StreamWriter wrt = new StreamWriter(outFName))
        {
            wrt.WriteLine(
                "#pragma once\n\n" +
                "#define TRIE_NONE -1000000\n"+
                "#define TRIE_DONE -2000000\n\n"
                );

            wrt.WriteLine("const int trieArray[] = {{{0}\n}};", trieArray);

            wrt.WriteLine(
                "inline bool checkSingleSuffix(const char* str, const char* curr, const int* trie) {\n"+
                "   int len = trie[0];\n"+
                "   if (curr - str < len) return false;\n"+
                "   const char* cmp = (const char*)(trie + 1);\n"+
                "   while (len-- > 0) {\n"+
                "       if (*--curr != *cmp++) return false;\n"+
                "   }\n"+
                "   return true;\n"+
                "}\n\n"+
                "bool checkStringSuffix(const char* str, int len) {\n" +
                "   if (len < " + suffixes.Select(s => s.Length).Min().ToString() + ") return false;\n" +
                "   const char* curr = (str + len - 1);\n"+
                "   int currTrie = " + start.ToString() + ";\n"+
                "   while (curr >= str) {\n" +
                "       assert(*curr >= 0x20 && *curr <= 0x7f);\n" +
                "       currTrie = trieArray[currTrie + *curr - 0x20];\n" +
                "       if (currTrie < 0) {\n" +
                "           if (currTrie == TRIE_NONE) return false;\n" +
                "           if (currTrie == TRIE_DONE) return true;\n" +
                "           return checkSingleSuffix(str, curr, trieArray - currTrie - 1);\n" +
                "       }\n"+
                "       --curr;\n"+
                "   }\n" +
                "   return false;\n"+
                "}\n"
                );
        }        
    }

    private static int CreateTrieNode(IEnumerable<string> suffixes, string prefix)
    {
        int retVal = trieArraySize;

        if (suffixes.Count() == 1)
        {
            string theSuffix = suffixes.Single();
            trieArray.AppendFormat("\n\t/* {1} - {2} */ {0}, ", theSuffix.Length, trieArraySize, prefix);
            ++trieArraySize;
            for (int i = 0; i < theSuffix.Length; i += 4)
            {
                trieArray.AppendFormat("0x{0:X}, ", GetFourBytes(theSuffix, i));
                ++trieArraySize;
            }

            retVal = -(retVal + 1);
        }
        else
        {
            var groupByFirstChar =
                from s in suffixes
                let first = s[0]
                let remainder = s.Substring(1)
                group remainder by first;

            string[] trieIndexes = new string[0x60];
            for (int i = 0; i < trieIndexes.Length; ++i)
            {
                trieIndexes[i] = "TRIE_NONE";
            }

            foreach (var g in groupByFirstChar)
            {
                if (g.Any(s => s == string.Empty))
                {
                    trieIndexes[g.Key - 0x20] = "TRIE_DONE";
                    continue;
                }
                trieIndexes[g.Key - 0x20] = CreateTrieNode(g, g.Key + prefix).ToString();
            }
            trieArray.AppendFormat("\n\t/* {1} - {2} */ {0},", string.Join(", ", trieIndexes), trieArraySize, prefix);
            retVal = trieArraySize;
            trieArraySize += 0x60;
        }

        return retVal;
    }

таким образом, он генерирует код следующим образом:

    inline bool checkSingleSuffix(const char* str, const char* curr, const int* trie) {
       int len = trie[0];
       if (curr - str < len) return false;
       const char* cmp = (const char*)(trie + 1);
       while (len-- > 0) {
           if (*--curr != *cmp++) return false;
       }
       return true;
    }

    bool checkStringSuffix(const char* str, int len) {
       if (len < 5) return false;
       const char* curr = (str + len - 1);
       int currTrie = 81921;
       while (curr >= str) {
           assert(*curr >= 0x20 && *curr <= 0x7f);
           currTrie = trieArray[currTrie + *curr - 0x20];
           if (currTrie < 0) {
               if (currTrie == TRIE_NONE) return false;
               if (currTrie == TRIE_DONE) return true;
               return checkSingleSuffix(str, curr, trieArray - currTrie - 1);
           }
           --curr;
       }
       return false;
    }

Так как для моего конкретного набора данных len в checkSingleSuffix никогда не было больше 9, я попытался заменить цикл сравнения переключателем (len) и жестко закодированными подпрограммами сравнения, которые сравнивали до 8 байты данных за раз, но это не повлияло на общую производительность в любом случае.

спасибо всем, кто внес свои идеи!