Русский
Русский
English
Статистика
Реклама

Хеш-таблицы

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

16.06.2021 22:19:34 | Автор: admin

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

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

В результате родился проект Объясняем код. Посмотреть, что это такое можно на code-explained.com. Код проекта выложен на Гитхаб.

Чем я вдохновлялся

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

Например, если вы редактируете программный код, то результат должен отображаться в реальном времени, без повторной компиляции. Иначе, вы не увидите промежуточные состояния, и у вас не родятся связанные с ними идеи. Я не буду подробно пересказывать эту мысль, тем более, что на Хабре уже подробно о ней рассказали. Почитайте, если еще не успели.

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

Как сегодня изучают алгоритмы

Преподаватель записывает на доске код, а потом по шагам объясняет, как он работает.

В видео на Youtube происходит нечто подобное ведущий берет код алгоритма и отрисовывает этапы его работы

На ИТ-ресурсах создают анимации.

А кто-то даже пытается объяснить работу алгоритма через танец.

Почему эти подходы казались мне неэффективными

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

Как я перешел от технозависимости к человечности

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

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

Выглядит это как-то так. Можно вводить свои данные. А затем с помощью плеера запускать и останавливать исполнение кода, перематывать его вперед и назад. При этом визуализация в реальном времени показывает, что делает алгоритм.

Как это технически реализовано

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

Чтобы сэкономить на копировании данных, я применил Immutable.js. Immutable.js это библиотека неизменяемых коллекций. Модификации таких коллекций всегда возвращают новую коллекцию, которую легко сохранить. Так я избегаю глубокого копирования. Не буду вдаваться в подробности, на Хабре про immutable.js уже писали. Для примера кусочек кода с итерированием по хеш-таблице:

while (true) { // Итерируемся по списку    this.addBP('check-not-found'); // Метод сохраняем состояние    if (this.newList.get(this.newListIdx) === null) {        // this.newList -- это немутабельный список        break;    }    this.addBP('check-found'); // Выполнена очередная строчка, сохраняем состояние    if (EQ(this.newList.get(this.newListIdx), this.number)) {        this.addBP('found-key');        return true;    }    this.fmtCollisionCount += 1; // Для динамических комментариев иногда нужно сохранять статистикуу    this.newListIdx = (this.newListIdx + 1) % this.newList.size; // Переходим к следующему индекксу    this.addBP('next-idx');}

В любой момент пользователь может отмотать время слайдером, и мы должны быть готовы анимировать переход. Поэтому анимации сделаны декларативно на React и CSS. На React описываются нужное состояние, а CSS transitions автоматически высчитывают и анимируют переход. Это позволяет избежать совсем сложного императивного кода с запутанным состоянием и ручными вычислениями. Получается тоже не просто, и в итоге анимациями заведует огромный класс на 1000 строк. Но я уверен, что императивно реализовать то же самое у меня вообще не получилось бы.

Много строк возникает из-за особенностей браузерных API и производительности в разных браузерах. Например, большой проблемой оказалось сделать так, чтобы браузеры не склеивали последовательные изменения друг с другом. Если добавить div с определённой начальной позицией, и потом сразу же поменять координаты на конечные, то браузер склеит эти два изменения в одно. Div сразу окажется в конечной позиции без анимации. Чтобы такое не происходило, приходится вставлять задержку в два фрейма анимации с помощью window.requestAnimationFrame().

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

Код проекта на гитхабе

Что дальше?

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

Подробнее..

Из песочницы Хеш-таблицы

02.07.2020 14:05:51 | Автор: admin

Предисловие


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


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


image


Мотивация использовать хеш-таблицы


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


Контейнер \ операция insert remove find
Array O(N) O(N) O(N)
List O(1) O(1) O(N)
Sorted array O(N) O(N) O(logN)
Бинарное дерево поиска O(logN) O(logN) O(logN)
Хеш-таблица O(1) O(1) O(1)

Из этой таблицы очень хорошо понятно, почему же стоит использовать хеш-таблицы. Но тогда возникает противоположный вопрос: почему же тогда ими не пользуются постоянно?
Ответ очень прост: как и всегда, невозможно получить все сразу, а именно: и скорость, и память. Хеш-таблицы тяжеловесные, и, хоть они и быстро отвечают на вопросы основных операций, пользоваться ими все время очень затратно.


Понятие хеш-таблицы


Хеш-таблица это контейнер. Реализация у него, возможно, и не очевидная, но довольно простая.


Для начала объяснение в нескольких словах. Мы определяем функцию хеширования, которая по каждому входящему элементу будет определять натуральное число. А уже дальше по этому натуральному числу мы будем класть элемент в (допустим) массив. Тогда имея такую функцию мы можем за O(1) обработать элемент.


Теперь стало понятно, почему же это именно хеш-таблица.


Проблема коллизии


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


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


Решения проблемы коллизии методом двойного хеширования


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


Одна хеш-функция (при входе g) будет возвращать натуральное число s, которое будет для нас начальным. То есть первое, что мы сделаем, попробуем поставить элемент g на позицию s в нашем массиве. Но что, если это место уже занято? Именно здесь нам пригодится вторая хеш-функция, которая будет возвращать t шаг, с которым мы будем в дальнейшем искать место, куда бы поставить элемент g.


Мы будем рассматривать сначала элемент s, потом s + t, затем s + 2*t и т.д. Естественно, чтобы не выйти за границы массива, мы обязаны смотреть на номер элемента по модулю (остатку от деления на размер массива).


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




Реализация хеш-таблицы


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


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)        hash_result = (key * hash_result + s[i]) % table_size;    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1); // ключи должны быть взаимопросты, используем числа <размер таблицы> плюс и минус один.    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};

Чтобы идти дальше, нам необходимо разобраться с проблемой: что же будет, если мы удалим элемент из таблицы? Так вот, его нужно пометить флагом deleted, но просто удалять его безвозвратно нельзя. Ведь если мы так сделаем, то при попытке найти элемент (значение хеш-функции которого совпадет с ее значением у нашего удаленного элемента) мы сразу наткнемся на пустую ячейку. А это значит, что такого элемента и не было никогда, хотя, он лежит, просто где-то дальше в массиве. Это основная сложность использования данного метода решения коллизий.


Помня о данной проблеме построим наш класс.


template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8; // начальный размер нашей таблицы    constexpr static const double rehash_size = 0.75; // коэффициент, при котором произойдет увеличение таблицы    struct Node    {        T value;        bool state; // если значение флага state = false, значит элемент массива был удален (deleted)        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr; // соответственно в массиве будут хранится структуры Node*    int size; // сколько элементов у нас сейчас в массиве (без учета deleted)    int buffer_size; // размер самого массива, сколько памяти выделено под хранение нашей таблицы    int size_all_non_nullptr; // сколько элементов у нас сейчас в массиве (с учетом deleted)};

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


...public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr; // заполняем nullptr - то есть если значение отсутствует, и никто раньше по этому адресу не обращался    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }

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


void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value); // добавляем элементы в новый массив        }        // удаление предыдущего массива        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Немаловажным является поддержание асимптотики O(1) стандартных операций. Но что же может повлиять на скорость работы? Наши удаленные элементы (deleted). Ведь, как мы помним, мы ничего не можем с ними сделать, но и окончательно обнулить их не можем. Так что они тянутся за нами огромным балластом. Для ускорения работы нашей хеш-таблицы воспользуемся рехешом (как мы помним, мы уже выделяли под это очень странные переменные).


Теперь воспользуемся ими, если процент реальных элементов массива стало меньше 50 %, то мы производим Rehash, а именно делаем то же самое, что и при увеличении таблицы (resize), но не увеличиваем. Возможно, это звучит глуповато, но попробую сейчас объяснить. Мы вызовем наши хеш-функции от всех элементов, переместим их в новых массив. Но с deleted-элементами это не произойдет, мы не будем их перемещать, и они удалятся вместе со старой таблицей.


Но к чему слова, код все разъяснит:


void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        // удаление предыдущего массива        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Ну теперь мы уже точно на финальной, хоть и длинной, и полной колючих кустарников, прямой. Нам необходимо реализовать вставку (Add), удаление (Remove) и поиск (Find) элемента.


Начнем с самого простого метод Find элемент по значению.


bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size); // значение, отвечающее за начальную позицию        int h2 = hash2(value, buffer_size); // значение, ответственное за "шаг" по таблице        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true; // такой элемент есть            h1 = (h1 + h2) % buffer_size;            ++i; // если у нас i >=  buffer_size, значит мы уже обошли абсолютно все ячейки, именно для этого мы считаем i, иначе мы могли бы зациклиться.        }        return false;    }

Далее мы реализуем удаление элемента Remove. Как мы это делаем? Находим элемент (как в методе Find), а затем удаляем, то есть просто меняем значение state на false, но сам Node мы не удаляем.


bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }

Ну и последним мы реализуем метод Add. В нем есть несколько очень важных нюансов. Именно здесь мы будем проверять на необходимость рехеша.


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


bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash(); // происходит рехеш, так как слишком много deleted-элементов        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1; // запоминаем первый подходящий (удаленный) элемент        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false; // такой элемент уже есть, а значит его нельзя вставлять повторно            if (!arr[h1]->state && first_deleted == -1) // находим место для нового элемента                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1) // если не нашлось подходящего места, создаем новый Node        {            arr[h1] = new Node(value);            ++size_all_non_nullptr; // так как мы заполнили один пробел, не забываем записать, что это место теперь занято        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size; // и в любом случае мы увеличили количество элементов        return true;    }

В заключение приведу полную реализацию хеш-таблицы:


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)    {        hash_result = (key * hash_result + s[i]) % table_size;    }    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1);    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8;    constexpr static const double rehash_size = 0.75;    struct Node    {        T value;        bool state;        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr;    int size;    int buffer_size;    int size_all_non_nullptr;    void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }    void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr;    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }    bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash();        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false;            if (!arr[h1]->state && first_deleted == -1)                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1)        {            arr[h1] = new Node(value);            ++size_all_non_nullptr;        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size;        return true;    }    bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }    bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true;            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }
Подробнее..

Идеальное хэширование

11.01.2021 16:10:01 | Автор: admin

Какова сложность поиска элемента по ключу?

Это зависит от того, какую структуру данных использовать.

В односвязном списке - линейная сложность.

В отсортированном массиве или в двоичном дереве поиска - логарифмическая сложность.

В хэш-таблице - сложность константная. Но это в лучшем случае. А в худшем стремится к линейной

А можно ли создать идеальную хэш-таблицу, чтобы сложность поиска элемента даже в худшем случае оставалась константной?

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

Идеально! Поговорим о том, как это сделать.
Кстати, на курсе "Алгоритмы и структуры данных" на платформе OTUS у нас есть отдельный модуль, посвящённый вопросам создания хэш-таблиц.


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

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

Как такое возможно?

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

Если же ограничиваться простейшей хэш-функцией вида (A * k + B) % N, с возможностью подбора лишь двух коэффициентов A и В, то создать волшебную биекцию вряд ли удастся - коллизий не избежать. В этой формуле k - уникальный хэшкод (ulong) уникального ключа, N - количество ключей.

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

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

Это напоминает метод разрешения коллизий методом цепочек. Но если в методе цепочек в каждой квартире расположен односвязный список комнат, то в идеальном хэшировании мы в каждой квартире создадим отдельную хэш-таблицу с матрицей комнат. Это будут квадрокомнатные квартиры, но обо всём по порядку.

Первый этап

Минимизация количества комнат в квартирах.

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

Например, за 100 итераций подбора коэффициентов для словаря из 10.000 ключей максимальное количество коллизий для одного элемента не превышает 10.

Итак, на первом этапе мы подбираем глобальные коэффициенты А и В для хэш-функции вида (A * k + B) % N. Эта функция вычисляет номер квартиры, в которой находится значение для заданного ключа. При этом минимизируется максимальное количество комнат (коллизий) в каждой квартире.

Второй этап

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

Вот и всё!

Для поиска любого элемента в идеальной хэш-таблице необходимо выполнить следующие действия:

  1. Найти номер квартиры i по хэш-коду ключа k: (A * k + B) % N.

  2. Взять коэффициенты для вычисления номера комнаты: Ai, Bi, K.

  3. Найти номер комнаты по формуле: (Ai * k + Bi) % K

  4. Вернуть значение из найденной комнаты.

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

Расход памяти

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

Возьмём, для примера, хэш-таблицу из N = 10 элементов. В каждом элементе может быть от 0 до 10 коллизий. Вычислим вероятность коллизий, умножим на затраты по памяти и найдём математическое ожидание объёма памяти для размещения одного элемента.

Итого: для хранения квартир нужно N ячеек памяти, для хранения комнат - примерно 1.9 * N. Суммарный расход памяти: 2.9 * N = О(N) - линейный.

Заключение

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


А теперь небольшой бонус. 15 января я проведу Demo Day курса "Алгоритмы и структуры данных" на платформе OTUS, приглашаю всех желающих узнать подробно о курсе и задать вопросы лично.

Также хочу посоветовать еще несколько статей из своего блога, которые уже успели получить хороший отклик у читателей:

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru