Строить дерево быстрее

Я делаю мобильное приложение, которое требует тысяч быстрых поисков строк и проверок префиксов. Чтобы ускорить это, я сделал Trie из своего списка слов, который насчитывает около 180 000 слов.

Все отлично, но единственная проблема заключается в том, что создание этого огромного дерева (в нем около 400 000 узлов) в настоящее время на моем телефоне занимает около 10 секунд, что очень медленно.

Вот код, который строит дерево.

public SimpleTrie makeTrie(String file) throws Exception {
    String line;
    SimpleTrie trie = new SimpleTrie();

    BufferedReader br = new BufferedReader(new FileReader(file));
    while( (line = br.readLine()) != null) {
        trie.insert(line);
    }
    br.close();

    return trie;
}

insert метод, который работает на O(length of key)

public void insert(String key) {
    TrieNode crawler = root;
    for(int level=0 ; level < key.length() ; level++) {
        int index = key.charAt(level) - 'A';
        if(crawler.children[index] == null) {
            crawler.children[index] = getNode();
        }
        crawler = crawler.children[index];
    }
    crawler.valid = true;
}

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

Или есть какие-либо другие структуры данных префикса, создание которых займет меньше времени, но имеет аналогичную временную сложность поиска?

Любые предложения приветствуются. Заранее спасибо.

ИЗМЕНИТЬ

Кто-то предложил использовать сериализацию Java. Я попробовал, но этот код был очень медленным:

public void serializeTrie(SimpleTrie trie, String file) {
        try {
            ObjectOutput out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
            out.writeObject(trie);
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public SimpleTrie deserializeTrie(String file) {
        try {
            ObjectInput in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
            SimpleTrie trie = (SimpleTrie)in.readObject();
            in.close();
            return trie;
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

Можно ли сделать этот код быстрее?

Мое дерево: http://pastebin.com/QkFisi09

Список слов: http://www.isc.ro/lists/twl06.zip

Для запуска кода использовалась Android IDE: http://play.google.com/store/apps/details?id=com.jimmychen.app.sand


person Bruce    schedule 23.09.2013    source источник
comment
Не могу установить ide на пряник андроид?   -  person Gigamegs    schedule 29.09.2013
comment
Предлагаю начать с профилирования. По крайней мере, измерение того, какая часть тратится на (1) чтение из файла, (2) поиск местоположения в trie и (3) создание нового узла   -  person maxim1000    schedule 29.09.2013
comment
@ Брюс Вы когда-нибудь пробовали метод двоичного поиска? Я увидел хорошие результаты.   -  person Justin    schedule 01.10.2013
comment
@Justin: Да, я попробовал, но это не показалось мне слишком быстрым. Мне просто нужны два запроса: существует ли префикс и существует ли слово. Мне не нужны все строки, начинающиеся с префикса. Между прочим, я подсчитал количество поисков существования префикса, это было около 10 000 .. так что метод двоичного поиска был медленнее, потому что с dawg алгоритм целиком завершился за ~ 60 мс.   -  person Bruce    schedule 02.10.2013
comment
@ Брюс Хорошо, что вы нашли решение. Я никогда не находил префиксного запроса, который был бы медленнее, чем 1 миллисекунда, и то же самое для существования одной строки, но, возможно, у меня есть более быстрый телефон.   -  person Justin    schedule 02.10.2013
comment
Сравнение производительности Потребляемая память DAFSA: 16020976 DAFSA (мс): [100] 0 DAFSA (мс): [10000] 5 DAFSA (мс): [1000000] 28 --------------- trie потребляемая память: 12946984 trie (мс): [100] 0 trie (мс): [10000] 6 trie (мс): [1000000] 131 --------------- Список потребляемой памяти: 1761728 Список (мс): [100] 23 Список (мс): [10000] 696 Список (мс): [1000000] 71752 --------------- Установить потребляемую память: 2341616 Установить ( мс): [100] 0 Набор (мс): [10000] 1 Набор (мс): [1000000] 22   -  person Amit Kumar Gupta    schedule 04.08.2015


Ответы (10)


Сохранение / загрузка двойного массива выполняется очень быстро, поскольку все данные хранятся в линейных массивах. Их также очень быстро искать, но вставки могут быть дорогостоящими. Бьюсь об заклад, где-то есть реализация Java.

Кроме того, если ваши данные статичны (т.е. вы не обновляете их на телефоне), рассмотрите для своей задачи DAFSA. Это одна из наиболее эффективных структур данных для хранения слов (должно быть лучше, чем "стандартные" попытки и попытки счисления как по размеру, так и по скорости, лучше, чем сжатые попытки по скорости, часто лучше, чем сжатые попытки по размеру). Существует хорошая реализация C ++: dawgdic - вы можете использовать его для создания DAFSA из командной строки, а затем использовать программу чтения Java для результирующей структуры данных (пример реализации: здесь).

person Mikhail Korobov    schedule 27.09.2013
comment
Привет. После множества головокружений мне удалось создать DAWG и прочитать ее с Java. Он маленький (537K) и очень быстрый. Однако есть одна проблема, которая не позволяет мне закрыть этот вопрос навсегда - код Github может только проверять, является ли строка префиксом какого-либо слова в словаре, он не может проверить, является ли строка полным словом. Я потратил весь свой день, пытаясь понять это. Мое приложение не может работать без этого. Не могли бы вы взглянуть на это? - person Bruce; 29.09.2013
comment
@Bruce: Вы можете добавить какой-нибудь неиспользуемый символ (например, «$») в конце каждого словарного слова. Затем вы просто ищите слово для префикса и слово $ для полного слова. - person Evgeny Kluev; 29.09.2013
comment
@EvgenyKluev Да, я мог бы это сделать - но я действительно думаю, что эта функция есть в этом коде - я просто не могу ее найти. Жду ответа Михаила. Кстати, вот код для тестирования DAWG: dl.dropboxusercontent.com/u/19729481/ DawgTest.7z Не работает должным образом. - person Bruce; 29.09.2013
comment
Привет, @Bruce, в методе 'contains' отсутствует проверка - он должен возвращать True, только если есть значение, связанное с индексом (return hasValue(index) вместо return true должно работать). Я сам не тестировал / не использовал связанную реализацию Java; он может хорошо работать с программным обеспечением, для которого он был написан, но не как общая реализация Java. Извините за потраченное время. Эта реализация Python тщательно протестирована, и я почти уверен, что она работает правильно: github.com/kmike/DAWG-Python/blob/ - проконсультируйтесь с ним, если сомневаетесь. - person Mikhail Korobov; 30.09.2013
comment
ах, и, конечно же, есть канонический исходный код C ++: code.google.com/p/dawgdic/source/browse/trunk/src/dawgdic/ - person Mikhail Korobov; 30.09.2013
comment
Я пытаюсь прочитать dawg из dawg_python, чтобы проверить индексы, которые возвращаются против индекса Java. Почему этот код возвращает пустой массив? print dawg_python.DAWG().load('dawg.bin').prefixes(u'MAX') - person Bruce; 30.09.2013
comment
Я пробовал return hasValue(index), не получилось. Я проверил с кодом C ++ и Python. Не могли бы вы хоть раз взглянуть на код Java, когда у вас будет время? Вы, наверное, самый знакомый человек с реализацией! Если нет, где я могу узнать внутреннюю файловую структуру dawg, чтобы я мог отлаживать ее самостоятельно? - person Bruce; 01.10.2013
comment
Для вашего удобства .. Вот ссылка на проект Eclipse для тестирования Java-реализации dawg, поэтому вам не нужно делать это самостоятельно - dl.dropboxusercontent.com/u/19729481/DawgTest.7z - person Bruce; 01.10.2013
comment
Я думаю, что есть 2 возможных проблемы. 1) кажется, что вы использовали утилиту командной строки make-dawg, которая требует окончания строки LF, а файл TWL06.txt использует окончания строки CRLF. 'HELLO\r'in dawg_python.DAWG().load('dawg.bin') возвращает True. 2) Вторая проблема заключается в том, что вы не использовали опцию «-g», поэтому DAWG была создана без руководства, и быстрое завершение ключа (то есть поиск всех ключей, начинающихся с заданного префикса) не работает. Обратите внимание, что d.prefixes (u'MAX ') находит все слова , которые являются префиксами u'MAX' (пустые из-за проблемы с '\ r'), а не все слова, начинающиеся с u'MAX '. - person Mikhail Korobov; 01.10.2013
comment
@MikhailKorobov ДА. Вот и все. Концы строк CRLF и hasValue(index) вместе посеяли весь хаос. Я не могу выразить словами, насколько я благодарен за вашу помощь. Спасибо за то, что помогли мне с этим справиться. Вы классный человек. Теперь я отправлю запрос на перенос в это репозиторий Java и полностью приму этот замечательный ответ. Наслаждайтесь своими 5k! :) - person Bruce; 01.10.2013
comment
@Bruce У вас есть это где-нибудь на github или где-нибудь еще, я могу посмотреть? Мне нужно сделать что-то подобное в C #, и я был бы большим подспорьем, если бы у меня был образец кода для просмотра. Спасибо - person brainydexter; 26.04.2016

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

public class SimpleTrie {
    public class TrieNode {
        boolean valid;
        int[] children;
    }
    private TrieNode[] nodes;
    private int numberOfNodes;

    private TrieNode getNode() {
        TrieNode t = nodes[++numberOnNodes];
        return t;
    }
}
person el.pescado    schedule 27.09.2013
comment
Я думал об этом, но не мог дальше этого делать. Как мне представить рекурсивную структуру дерева? Как связаны родительский и дочерний индексы в массиве? Как можно гарантировать, что он генерирует точно такое же дерево, а не другое дерево с таким же байтовым представлением? - person Bruce; 28.09.2013
comment
@ Брюс - я не вижу проблемы. Рекурсивная структура дерева определяется теми значениями индекса, которые вы сериализуете вместе со всем остальным. Родительский и дочерний индексы связаны тем, что дочерний индекс хранится в родительском узле, заменяя дочернюю ссылку. Вы сериализуете, перебирая весь массив, игнорируя структуру Trie. Индекс - это индекс, будь он в файле или в массиве. Вам не нужно выполнять двоичную сериализацию (но можно, если хотите) - если вы сериализуете один узел на текстовую строку (например, файл CSV), номера узлов также являются номерами строк. - person Steve314; 28.09.2013
comment
Ой, извини, что вчера я совершенно неправильно это понял, наверное, слишком устал. Теперь я понял, так просто. Постараюсь сообщить вам. - person Bruce; 28.09.2013

Просто создайте большой String [] и отсортируйте его. Затем вы можете использовать двоичный поиск, чтобы найти местоположение строки. Вы также можете выполнить запрос на основе префиксов без особых усилий.

Пример поиска префикса:

Метод сравнения:

private static int compare(String string, String prefix) {
    if (prefix.length()>string.length()) return Integer.MIN_VALUE;

    for (int i=0; i<prefix.length(); i++) {
        char s = string.charAt(i);
        char p = prefix.charAt(i);
        if (s!=p) {
            if (p<s) {
                // prefix is before string
                return -1;
            }
            // prefix is after string
            return 1;
        }
    }
    return 0;
}

Находит вхождение префикса в массиве и возвращает его местоположение (MIN или MAX означают, что не найдено)

private static int recursiveFind(String[] strings, String prefix, int start, int end) {
    if (start == end) {
        String lastValue = strings[start]; // start==end
        if (compare(lastValue,prefix)==0)
            return start; // start==end
        return Integer.MAX_VALUE;
    }

    int low = start;
    int high = end + 1; // zero indexed, so add one.
    int middle = low + ((high - low) / 2);

    String middleValue = strings[middle];
    int comp = compare(middleValue,prefix);
    if (comp == Integer.MIN_VALUE) return comp;
    if (comp==0)
        return middle;
    if (comp>0)
        return recursiveFind(strings, prefix, middle + 1, end);
    return recursiveFind(strings, prefix, start, middle - 1);
}

Получает массив String и префикс, распечатывает вхождения префикса в массиве

private static boolean testPrefix(String[] strings, String prefix) {
    int i = recursiveFind(strings, prefix, 0, strings.length-1);
    if (i==Integer.MAX_VALUE || i==Integer.MIN_VALUE) {
        // not found
        return false;
    }
    // Found an occurrence, now search up and down for other occurrences
    int up = i+1;
    int down = i;
    while (down>=0) {
        String string = strings[down];
        if (compare(string,prefix)==0) {
            System.out.println(string);
        } else {
            break;
        }
        down--;
    }
    while (up<strings.length) {
        String string = strings[up];
        if (compare(string,prefix)==0) {
            System.out.println(string);
        } else {
            break;
        }
        up++;
    }
    return true;
}
person Justin    schedule 27.09.2013
comment
Словарные слова. Поверьте, мне нужно выяснить, является ли ключ префиксом любого слова в словаре за время O (длина). Все остальное дает огромные штрафы по времени. Как с помощью массива узнать, что ключ является префиксом любого слова? - person Bruce; 28.09.2013
comment
Используя двоичный поиск, вы сможете найти префиксы в O (log N), где N - количество слов в словаре. Я добавлю код к своему ответу, чтобы привести пример. - person Justin; 28.09.2013
comment
@Bruce Используя приведенный выше алгоритм, на моем телефоне потребовалось менее 1 миллисекунды, чтобы обнаружить наличие трехбуквенного префикса в массиве строк из 200000 элементов. - person Justin; 28.09.2013
comment
@Bruce Он также нашел более 1000 строк с заданным трехсимвольным префиксом за ~ 5 миллисекунд. - person Justin; 28.09.2013
comment
@Bruce По иронии судьбы, на моем телефоне этот подход работает так же, как Trie, выполняющий поиск по префиксу, и превосходит (с хорошим отрывом) Trie при возврате всех строк, содержащих префикс. - person Justin; 29.09.2013

Вот достаточно компактный формат для хранения дерева на диске. Я укажу его по (эффективному) алгоритму десериализации. Инициализируйте стек, исходным содержимым которого является корневой узел дерева. Прочтите символы по одному и интерпретируйте их следующим образом. Значение буквы A-Z - «выделить новый узел, сделать его дочерним по отношению к текущей вершине стека и поместить вновь выделенный узел в стек». Буква указывает, в какой позиции находится дочерний элемент. Значение пробела - «установить действительный флаг узла на вершине стека в значение true». Значение символа возврата (\ b) - «вытолкнуть стек».

Например, вход

TREE \b\bIE \b\b\bOO \b\b\b

дает список слов

TREE
TRIE
TOO

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

serialize(node):
    if node is valid: put(' ')
    for letter in A-Z:
        if node has a child under letter:
            put(letter)
            serialize(child)
            put('\b')
person David Eisenstat    schedule 27.09.2013

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

Я увидел ускорение примерно на 10% в приведенном ниже тестовом коде (C ++, а не Java, извините), когда я использовал «пул узлов» вместо того, чтобы полагаться на отдельные распределения:

#include <string>
#include <fstream>

#define USE_NODE_POOL

#ifdef USE_NODE_POOL
struct Node;
Node *node_pool;
int node_pool_idx = 0;
#endif

struct Node {
    void insert(const std::string &s) { insert_helper(s, 0); }
    void insert_helper(const std::string &s, int idx) {
        if (idx >= s.length()) return;
        int char_idx = s[idx] - 'A';
        if (children[char_idx] == nullptr) {
#ifdef USE_NODE_POOL
            children[char_idx] = &node_pool[node_pool_idx++];
#else
            children[char_idx] = new Node();
#endif
        }
        children[char_idx]->insert_helper(s, idx + 1);
    }
    Node *children[26] = {};
};

int main() {
#ifdef USE_NODE_POOL
    node_pool = new Node[400000];
#endif
    Node n;
    std::ifstream fin("TWL06.txt");
    std::string word;
    while (fin >> word) n.insert(word);
}
person Nate Kohl    schedule 28.09.2013

Попытки выделить пространство всем возможным дочерним элементам (256) имеют огромное количество потраченного впустую пространства. Вы заставляете свой кеш плакать. Сохраните эти указатели на дочерние элементы в структуре данных с изменяемым размером.

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

person DanielV    schedule 28.09.2013

Вместо простого файла вы можете использовать базу данных, такую ​​как sqlite и вложенный набор или дерево celko, для хранения дерева, а также можете построить более быстрое и короткое (с меньшим количеством узлов) дерево с троичным деревом поиска.

person Gigamegs    schedule 28.09.2013

Мне не нравится идея адресации узлов по индексу в массиве, но только потому, что это требует еще одного добавления (индекса к указателю). Но с массивом предварительно выделенных узлов вы, возможно, сэкономите время на выделении и инициализации. И вы также можете сэкономить много места, зарезервировав первые 26 индексов для листовых узлов. Таким образом, вам не нужно выделять и инициализировать 180000 листовых узлов.

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

Если вы проверили, что ваш исходный словарь отсортирован, вы также можете сэкономить время, сравнив некоторый префикс текущей строки с предыдущим. Например. первые 4 символа. Если они равны, вы можете начать свой

for (int level = 0; level ‹key.length (); level ++) {

петля с 5-го уровня.

person Mikhail M    schedule 29.09.2013

Это неэффективное пространство или неэффективное время? Если вы катите обычное дерево, то место может быть частью проблемы при работе с мобильным устройством. Ознакомьтесь с попытками patricia / radix, особенно если вы используете его в качестве средства поиска по префиксу.

Trie: http://en.wikipedia.org/wiki/Trie

Патрисия / Radix trie: http://en.wikipedia.org/wiki/Radix_tree

Вы не упомянули язык, но вот две реализации попытки префикса в Java.

Обычное дерево: http://github.com/phishman3579/java-algorithms-implementation/blob/master/src/com/jwetherell/algorithms/data_structures/Trie.java

Patricia / Radix (эффективное пространство) trie: http://github.com/phishman3579/java-algorithms-implementation/blob/master/src/com/jwetherell/algorithms/data_structures/PatriciaTrie.java

person Justin    schedule 23.09.2013
comment
Нет, как я уже сказал в вопросе, проблема - это время, а не пространство. Он должен занимать около 40 МБ, что вполне реально. Я уже все реализовал - просто хочу ускорить. Посмотрите отредактированный вопрос. - person Bruce; 24.09.2013
comment
@Bruce Я нахожу удивительным, что создание дерева из 180 тысяч слов занимает 10 секунд. Например; создание дерева из 200 КБ на моем локальном ПК (процессор 2,0 ГГц с 1 ГБ памяти) занимает 471 мс и потребляет 34 МБ, создание сжатого дерева из тех же данных занимает 541 мс и потребляет 22 МБ. Я бы попробовал версию с открытым исходным кодом и посмотрел, добьетесь ли вы лучших результатов. - person Justin; 24.09.2013
comment
10 секунд на моем телефоне - person Bruce; 24.09.2013
comment
@ Брюс, я понимаю, но производительность вашего дерева намного выше, это удивительно. Я запустил тот же код на своем HTC и снова зашел. - person Justin; 24.09.2013
comment
Спасибо! Кстати, вот мой пример: pastebin.com/QkFisi09 Список слов: isc.ro/lists/twl06.zip Я запускаю его в этой среде IDE: play.google.com/store/apps/. - person Bruce; 24.09.2013
comment
@ Брюс. Вы будете рады узнать, что мой Trie работает примерно так же, как и ваш на моем телефоне. Чтобы загрузить в Trie 200K элементов, потребовалось около 7 секунд. - person Justin; 29.09.2013

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

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

person jochenleidner    schedule 19.04.2020