Хеширование с большими наборами данных и реализацией C - PullRequest
1 голос
/ 13 ноября 2011

У меня есть большое количество значений в диапазоне от 0 до 5463458053. Каждому значению я хочу отобразить набор, содержащий строки, чтобы операция lookup , i. е. поиск присутствия строки в этом наборе занимает наименьшее количество времени. Обратите внимание, что этот набор значений может содержать не все значения из (0 - 5463458053), но да, их большое количество.

Мое текущее решение состоит в том, чтобы хэшировать эти значения (между 0 - 5463458053) и для каждого значения иметь связанный список строк, соответствующих этому значению. Каждый раз, когда я хочу проверить строку в данном наборе, я хэширую значение (между 0 - 5463458053), получаю связанный список и просматриваю его, чтобы узнать, содержит ли он вышеупомянутую строку.

Хотя это может показаться более легким, но это занимает немного времени. Можете ли вы придумать более быстрое решение? Кроме того, столкновения будут ужасны. Они приведут к неверным результатам.

Другая часть посвящена реализации этого в C. Как бы я это сделал?

ПРИМЕЧАНИЕ : Кто-то предложил вместо этого использовать базу данных. Интересно, будет ли это полезно.

Я немного волнуюсь из-за нехватки оперативной памяти, естественно. : -)

Ответы [ 5 ]

3 голосов
/ 13 ноября 2011

У вас может быть хеш-таблица хеш-наборов.Первая хеш-таблица содержит ключи ваших целых чисел.Значения внутри него являются хеш-наборами, то есть хеш-таблицами, ключи которых являются строками.

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

ЕстьМногие библиотеки реализуют такие структуры данных (а в C ++ стандартная библиотека реализует их как std::map & std::set).Для C я думал о Glib от GTK.

При использовании методов хеширования использование памяти пропорционально размеру рассматриваемых наборов (или отношений).Например, вы можете принять 30% пустоты.

1 голос
/ 13 ноября 2011

Вы можете использовать одно двоичное дерево поиска (AVL / Red-black / ...), чтобы содержать все строки из всех наборов, указав их лексикографически как (set_number, string). Вам не нужно хранить наборы явно где-либо. Например, компаратор, определяющий порядок узлов для дерева, может выглядеть так:

function compare_nodes (node1, node2) {
    if (node1.set_number < node2.set_number) return LESS;
    if (node1.set_number > node2.set_number) return GREATER;
    if (node1.string < node2.string) return LESS;
    if (node1.string > node2.string) return GREATER;
    return EQUAL;
}

При такой структуре возможны некоторые общие операции (но, возможно, не простые).

Чтобы определить, существует ли строка s в наборе set_number, просто найдите (set_number, s) в дереве, для точного соответствия.

Чтобы найти все строки в наборе set_number:

function iterate_all_strings_in_set (set_number) {
    // Traverse the tree from root downwards, looking for the given key. Return
    // wherever the search ends up, whether it found the value or not.
    node = lookup_tree_weak(set_number, "");

    // tree empty?
    if (node == null) {
        return;
    }

    // We may have gotten the greatest node from the previous set,
    // instead of the first node from the set we're interested in.
    if (node.set_number != set_number) {
        node = successor(node);
    }

    while (node != null && node.set_number == set_number) {
        do_something_with(node.string);
        node = successor(node);
    }
}

Приведенное выше требует O((k+1)*log(n)) времени, где k - это число строк в set_number, а n - это количество всех строк.

Чтобы найти все заданные числа с хотя бы одной связанной строкой:

function iterate_all_sets ()
{
    node = first_node_in_tree();

    while (node != null) {
        current_set = node.set_number;
        do_something_with(current_set);

        if (cannot increment current_set) {
            return;
        }

        node = lookup_tree_weak(current_set + 1, "");
        if (node.set_number == current_set) {
            node = successor(node);
        }
    }
}

Приведенное выше требует O((k+1)*log(n)) времени, где k - это число наборов, по крайней мере, с одной строкой, а n - это число всех строк.

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

Кроме того, вот некоторый настоящий код C, который демонстрирует это, используя мою собственную реализацию универсального дерева AVL . Чтобы скомпилировать его, достаточно скопировать куда-нибудь папки misc/ и structure/ из источника BadVPN и добавить туда путь включения.

Обратите внимание, что мое дерево AVL не содержит каких-либо «данных» в своих узлах и как оно не выполняет своего собственного выделения памяти. Это удобно, когда у вас много данных для работы. Чтобы было понятно: программа ниже делает только один malloc(), который выделяет массив узлов.

#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <assert.h>

#include <structure/BAVL.h>
#include <misc/offset.h>

struct value {
    uint32_t set_no;
    char str[3];
};

struct node {
    uint8_t is_used;
    struct value val;
    BAVLNode tree_node;
};

BAVL tree;

static int value_comparator (void *unused, void *vv1, void *vv2)
{
    struct value *v1 = vv1;
    struct value *v2 = vv2;

    if (v1->set_no < v2->set_no) {
        return -1;
    }
    if (v1->set_no > v2->set_no) {
        return 1;
    }

    int c = strcmp(v1->str, v2->str);
    if (c < 0) {
        return -1;
    }
    if (c > 0) {
        return 1;
    }
    return 0;
}

static void random_bytes (unsigned char *out, size_t n)
{
    while (n > 0) {
        *out = rand();
        out++;
        n--;
    }
}

static void random_value (struct value *out)
{
    random_bytes((unsigned char *)&out->set_no, sizeof(out->set_no));

    for (size_t i = 0; i < sizeof(out->str) - 1; i++) {
        out->str[i] = (uint8_t)32 + (rand() % 94);
    }
    out->str[sizeof(out->str) - 1] = '\0';
}

static struct node * find_node (const struct value *val)
{
    // find AVL tree node with an equal value
    BAVLNode *tn = BAVL_LookupExact(&tree, (void *)val);
    if (!tn) {
        return NULL;
    }

    // get node pointer from pointer to its value (same as container_of() in Linux kernel)
    struct node *n = UPPER_OBJECT(tn, struct node, tree_node);
    assert(n->val.set_no == val->set_no);
    assert(!strcmp(n->val.str, val->str));

    return n;
}

static struct node * lookup_weak (const struct value *v)
{
    BAVLNode *tn = BAVL_Lookup(&tree, (void *)v);
    if (!tn) {
        return NULL;
    }

    return UPPER_OBJECT(tn, struct node, tree_node);
}

static struct node * first_node (void)
{
    BAVLNode *tn = BAVL_GetFirst(&tree);
    if (!tn) {
        return NULL;
    }

    return UPPER_OBJECT(tn, struct node, tree_node);
}

static struct node * next_node (struct node *node)
{
    BAVLNode *tn = BAVL_GetNext(&tree, &node->tree_node);
    if (!tn) {
        return NULL;
    }

    return UPPER_OBJECT(tn, struct node, tree_node);
}

size_t num_found;

static void iterate_all_strings_in_set (uint32_t set_no)
{
    struct value v;
    v.set_no = set_no;
    v.str[0] = '\0';
    struct node *n = lookup_weak(&v);

    if (!n) {
        return;
    }

    if (n->val.set_no != set_no) {
        n = next_node(n);
    }

    while (n && n->val.set_no == set_no) {
        num_found++; // "do_something_with_string"
        n = next_node(n);
    }
}

static void iterate_all_sets (void)
{
    struct node *node = first_node();

    while (node) {
        uint32_t current_set = node->val.set_no;
        iterate_all_strings_in_set(current_set); // "do_something_with_set"

        if (current_set == UINT32_MAX) {
            return;
        }

        struct value v;
        v.set_no = current_set + 1;
        v.str[0] = '\0';
        node = lookup_weak(&v);

        if (node->val.set_no == current_set) {
            node = next_node(node);
        }
    }
}

int main (int argc, char *argv[])
{
    size_t num_nodes = 10000000;

    // init AVL tree, using:
    //   key=(struct node).val,
    //   comparator=value_comparator
    BAVL_Init(&tree, OFFSET_DIFF(struct node, val, tree_node), value_comparator, NULL);

    printf("Allocating...\n");

    // allocate nodes (missing overflow check...)
    struct node *nodes = malloc(num_nodes * sizeof(nodes[0]));
    if (!nodes) {
        printf("malloc failed!\n");
        return 1;
    }

    printf("Inserting %zu nodes...\n", num_nodes);

    size_t num_inserted = 0;

    // insert nodes, giving them random values
    for (size_t i = 0; i < num_nodes; i++) {
        struct node *n = &nodes[i];

        // choose random set number and string
        random_value(&n->val);

        // try inserting into AVL tree
        if (!BAVL_Insert(&tree, &n->tree_node, NULL)) {
            printf("Insert collision: (%"PRIu32", '%s') already exists!\n", n->val.set_no, n->val.str);
            n->is_used = 0;
            continue;
        }

        n->is_used = 1;
        num_inserted++;
    }

    printf("Looking up...\n");

    // lookup all those values
    for (size_t i = 0; i < num_nodes; i++) {
        struct node *n = &nodes[i];
        struct node *lookup_n = find_node(&n->val);

        if (n->is_used) { // this node is the only one with this value

            ASSERT(lookup_n == n)
        } else { // this node was an insert collision; some other
                 // node must have this value
            ASSERT(lookup_n != NULL) 
            ASSERT(lookup_n != n)
        }
    }

    printf("Iterating by sets...\n");

    num_found = 0;
    iterate_all_sets();
    ASSERT(num_found == num_inserted)

    printf("Removing all strings...\n");

    for (size_t i = 0; i < num_nodes; i++) {
        struct node *n = &nodes[i];
        if (!n->is_used) { // must not remove it it wasn't inserted
            continue;
        }
        BAVL_Remove(&tree, &n->tree_node);
    }

    return 0;
}
1 голос
/ 13 ноября 2011

Большое количество строк + быстрый поиск + ограниченная память ----> вы хотите префиксный три, дерево критических битов или что-нибудь из этого семейства (много разных имен для очень похожих вещей, например, PATRICIA ... Judy is одна такая вещь тоже). См. Например это .

Эти структуры данных допускают сжатие префиксов, поэтому они могут очень эффективно хранить множество строк (которые каким-то образом обязательно будут иметь общие префиксы). Кроме того, поиск очень быстрый. Из-за эффектов кэширования и подкачки, которые обычные нотации big-O не учитывают, они могут быть такими же быстрыми или даже более быстрыми, чем хеш, на долю памяти (хотя, согласно big-O, ничего, кроме массива, может не быть. может разбить хэш).

1 голос
/ 13 ноября 2011

A Judy Array с библиотекой C, которая его реализует, может быть именно тем, что вам нужно. Вот цитата, которая описывает это:

Judy - это библиотека C, которая предоставляет самую современную базовую технологию который реализует разреженный динамический массив. Джуди массивы объявлены просто с нулевым указателем. Массив Джуди потребляет память только тогда, когда он заполнен, но может расти, чтобы воспользоваться всей доступной памятью при желании Основными преимуществами Джуди являются масштабируемость, высокая производительность и эффективность памяти. Массив Джуди является расширяемым и может масштабироваться до очень большое количество элементов, ограниченных только машинной памятью. поскольку Джуди выполнена в виде неограниченного массива, размер массива Джуди равен не выделяется заранее, но динамически растет и сжимается вместе с массивом Население. Джуди сочетает в себе масштабируемость с простотой использования. Джуди API доступ с помощью простых вставок, извлечения и удаления вызовов, которые не требует обширного программирования. Настройка и настройка не требуются (на самом деле даже не возможно). Кроме того, сортировка, поиск, подсчет и Возможности последовательного доступа встроены в дизайн Джуди.

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

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

1 голос
/ 13 ноября 2011

Если записи от 0 до N и последовательные: используйте массив. (Индексирование достаточно быстро для вас?)

РЕДАКТИРОВАТЬ: цифры не кажутся последовательными. Существует большое количество пар {ключ, значение}, где ключ - это большое число (> 32 бита, но <64 бита), а значение представляет собой набор строк. </p>

Если память доступна, хеш-таблица проста, если куча строк не слишком велика, вы можете проверить их последовательно. Если одни и те же строки встречаются (много) более одного раза, вы можете перечислить эти строки (поместить на них указатели в массиве char * array [] и использовать вместо этого индекс в этом массиве. Поиск индекса по заданной строке, вероятно, включает другую хеш-таблицу)

Для хэш-таблицы "master" запись, вероятно, будет такой:

struct entry {
  struct entry *next; /* for overflow chain */
  unsigned long long key; /* the 33bits number */
  struct list *payload;
  } entries[big_enough_for_all] ; /* if size is known in advance
                          , preallocation avoids a lot of malloc overhead */

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

struct entry *heads[SOME_SIZE] = {NULL, };

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

Обработка коллизий проста: проходя цепочку переполнения, просто сравните свой ключ с ключом в записи. Если они неравны: иди дальше. Если они равны: найдено; теперь иди по струнам.

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