Опубликован: 02.02.2011 | Уровень: для всех | Доступ: платный
Лекция 39:

Алгоритмы хеширования данных

< Лекция 38 || Лекция 39: 1234 || Лекция 40 >

Закрытое хеширование

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

При поиске элемента х необходимо просмотреть все местоположения h(x),h1(х),h2(х),..., пока не будет найден х или пока не встретится пустой сегмент. Чтобы объяснить, почему можно остановить поиск при достижении пустого сегмента, предположим, что в хеш-таблице не допускается удаление элементов. Пусть h3(х) – первый пустой сегмент. В такой ситуации невозможно нахождение элемента х в сегментах h4(х),h5(х) и далее, так как при вставке элемент х вставляется в первый пустой сегмент, следовательно, он находится где-то до сегмента h3(х). Но если в хеш-таблице допускается удаление элементов, то при достижении пустого сегмента, не найдя элемента х, нельзя быть уверенным в том, что его вообще нет в таблице, так как сегмент может стать пустым уже после вставки элемента х. Поэтому, чтобы увеличить эффективность данной реализации, необходимо в сегмент, который освободился после операции удаления элемента, поместить специальную константу, которую назовем, например, DEL. В качестве альтернативы специальной константе можно использовать дополнительное поле таблицы, которое показывает состояние элемента. Важно различать константы DEL и NULL – последняя находится в сегментах, которые никогда не содержали элементов. При таком подходе выполнение поиска элемента не требует просмотра всей хеш-таблицы. Кроме того, при вставке элементов сегменты, помеченные константой DEL, можно трактовать как свободные, таким образом, пространство, освобожденное после удаления элементов, можно рано или поздно использовать повторно. Но если невозможно непосредственно сразу после удаления элементов пометить освободившиеся сегменты, то следует предпочесть закрытому хешированию схему открытого хеширования.

Существует несколько методов повторного хеширования, то есть определения местоположений h(x),h1(х),h2(х),...:

  • линейное опробование;
  • квадратичное опробование;
  • двойное хеширование.

Линейное опробование сводится к последовательному перебору сегментов таблицы с некоторым фиксированным шагом:

адрес=h(x)+ci,

где i – номер попытки разрешить коллизию;

c – константа, определяющая шаг перебора.

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

адрес=h(x)+ci+di2,

где i – номер попытки разрешить коллизию,

c и dконстанты.

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

Еще одна разновидность метода открытой адресации, которая называется двойным хешированием, основана на нелинейной адресации, достигаемой за счет суммирования значений основной и дополнительной хеш-функций:

адрес=h(x)+ih2(x).

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

Однако в случае многократного превышения адресного пространства и, соответственно, многократного циклического перехода к началу будет происходить просмотр одних и тех же ранее занятых сегментов, тогда как между ними могут быть еще свободные сегменты. Более корректным будет использование сдвига адреса на 1 в случае каждого циклического перехода к началу таблицы. Это повышает вероятность нахождения свободных сегментов.

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

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

Пример 2. Программная реализация закрытого хеширования.

#include "stdafx.h"
#include <iostream>
#include <fstream>
using namespace std;

typedef int T;  // тип элементов
typedef int hashTableIndex;// индекс в хеш-таблице
int hashTableSize;
T *hashTable;
bool *used;

hashTableIndex myhash(T data);
void insertData(T data);
void deleteData(T data);
bool findData (T data);
int dist (hashTableIndex a,hashTableIndex b);

int _tmain(int argc, _TCHAR* argv[]){
  int i, *a, maxnum;
  cout << "Введите количество элементов maxnum : ";
  cin >> maxnum;
    cout << "Введите размер хеш-таблицы hashTableSize : ";
  cin >> hashTableSize;
  a = new int[maxnum];
  hashTable = new T[hashTableSize];
  used = new bool[hashTableSize];
  for (i = 0; i < hashTableSize; i++){
    hashTable[i] = 0;
    used[i] = false;
  }
  // генерация массива
  for (i = 0; i < maxnum; i++)
    a[i] = rand();
  // заполнение хеш-таблицы элементами массива
  for (i = 0; i < maxnum; i++)
    insertData(a[i]);
  // поиск элементов массива по хеш-таблице
  for (i = maxnum-1; i >= 0; i--) 
    findData(a[i]);
  // вывод элементов массива в файл List.txt
  ofstream out("List.txt");
  for (i = 0; i < maxnum; i++){
    out << a[i];
    if ( i < maxnum - 1 ) out << "\t";
  }
  out.close();
  // сохранение хеш-таблицы в файл HashTable.txt
  out.open("HashTable.txt");
  for (i = 0; i < hashTableSize; i++){
    out << i << "  :  " << used[i] << " : " << hashTable[i] << endl;
  }
  out.close();
  // очистка хеш-таблицы
  for (i = maxnum-1; i >= 0; i--) {
    deleteData(a[i]);
  }
  system("pause");
  return 0;
}

// хеш-функция размещения величины
hashTableIndex myhash(T data) {
    return (data % hashTableSize);
}

// функция поиска местоположения и вставки величины в таблицу
void insertData(T data) {
  hashTableIndex bucket;
    bucket = myhash(data);
  while  ( used[bucket] && hashTable[bucket] != data)
    bucket = (bucket + 1) % hashTableSize;
  if ( !used[bucket] ) {
    used[bucket] = true;
    hashTable[bucket] = data;
  }
}

// функция поиска величины, равной data
bool findData (T data) {
  hashTableIndex bucket;
  bucket = myhash(data);
  while ( used[bucket] && hashTable[bucket] != data )
    bucket = (bucket + 1) % hashTableSize;
  return used[bucket] && hashTable[bucket] == data;
}

//функция удаления величины из таблицы
void deleteData(T data){
  int bucket, gap;
  bucket = myhash(data);
  while ( used[bucket] && hashTable[bucket] != data )
    bucket = (bucket + 1) % hashTableSize;
  if ( used[bucket] && hashTable[bucket] == data ){
    used[bucket] = false;
    gap = bucket;
    bucket = (bucket + 1) % hashTableSize;
    while ( used[bucket] ){
      if ( bucket == myhash(hashTable[bucket]) )
        bucket = (bucket + 1) % hashTableSize;
      else if ( dist(myhash(hashTable[bucket]),bucket) < dist(gap,bucket) )
        bucket = (bucket + 1) % hashTableSize;
      else {
        used[gap] = true;
        hashTable[gap] = hashTable[bucket];
        used[bucket] = false;
        gap = bucket;
        bucket++;
      }
    }
  }
}

// функция вычисления расстояние от a до b (по часовой стрелке, слева направо) 
int dist (hashTableIndex a,hashTableIndex b){
  return (b - a + hashTableSize) % hashTableSize;
}
Листинг .

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

Идея хеширования впервые была высказана Г.П. Ланом при создании внутреннего меморандума IBM в январе 1953 г. с предложением использовать для разрешения коллизий метод цепочек. Примерно в это же время другой сотрудник IBM, Жини Амдал, высказала идею использования открытой линейной адресации. В открытой печати хеширование впервые было описано Арнольдом Думи (1956 год), указавшим, что в качестве хеш-адреса удобно использовать остаток от деления на простое число. А. Думи описывал метод цепочек для разрешения коллизий, но не говорил об открытой адресации. Подход к хешированию, отличный от метода цепочек, был предложен А.П. Ершовым (1957 год), который разработал и описал метод линейной открытой адресации.

Ключевые термины

Вторичные ключи – это ключи, не позволяющие однозначно идентифицировать запись в таблице.

Закрытое хеширование или Метод открытой адресации – это технология разрешения коллизий, которая предполагает хранение записей в самой хеш-таблице.

Коллизия – это ситуация, когда разным ключам соответствует одно значение хеш-функции.

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

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

Первичные ключи – это ключи, позволяющие однозначно идентифицировать запись.

Повторное хеширование – это поиск местоположения для очередного элемента таблицы с учетом шага перемещения.

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

Пространство ключей – это множество всех теоретически возможных значений ключей записи.

Синонимы – это совпадающие ключи в хеш-таблице.

Хеширование – это преобразование входного массива данных определенного типа и произвольной длины в выходную битовую строку фиксированной длины.

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

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

Краткие итоги

  1. В настоящее время используется широко распространенный метод обеспечения быстрого доступа к большим объемам информации – хеширование.
  2. Для установления соответствия ключей и данных строится хеш-таблица.
  3. Хеш-таблица строится при помощи хеш-функций. Практическое применение получили функции прямого доступа, остатков от деления, середины квадрата, свертки.
  4. При построении хеш-таблиц могут возникать коллизии, то есть ситуации неоднозначного соответствия данных ключу.
  5. Разрешение коллизий проводится методом цепочек (открытое или внешнее хеширование) или методом открытой адресации (закрытое хеширование).
  6. Поиск свободных ключей в методе открытой адресации может проводиться методом повторного хеширования с помощью линейного опробования, квадратичного опробования или двойного хеширования.
  7. Идентификация данных в таблицах может осуществляться как по первичному, так и по вторичному ключу.
  8. Хеширование имеет широкое практическое применение в теории баз данных, кодировании, банковском деле, криптографии и других областях.
< Лекция 38 || Лекция 39: 1234 || Лекция 40 >
Денис Курбатов
Денис Курбатов
Владислав Нагорный
Владислав Нагорный

Подскажите, пожалуйста, планируете ли вы возобновление программ высшего образования? Если да, есть ли какие-то примерные сроки?

Спасибо!

Ольга Замятина
Ольга Замятина
Россия, Калиниград, РГУ им. И. Канта, 2009
Эдуард Санин
Эдуард Санин
Украина, Харьков, ХАИ