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

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

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

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

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

Таблица прямого доступа

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

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

В целях экономии памяти можно назначать размер пространства записей равным размеру фактического множества записей или превосходящим его незначительно. В этом случае необходимо иметь некоторую функцию, обеспечивающую отображение точки из пространства ключей в точку в пространстве записей, то есть, преобразование ключа в адрес записи: a=h(k), где a – адрес, k – ключ.

Идеальной хеш-функцией является инъективная функция, которая для любых двух неодинаковых ключей дает неодинаковые адреса.

Метод остатков от деления

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

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

Key % HashTableSize

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

На практике, метод деления – самый распространенный.

//функция создания хеш-таблицы метод деления по модулю
int Hash(int Key, int HashTableSize) {
//HashTableSize
    return Key % HashTableSize;
}

Метод функции середины квадрата

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

Метод свертки

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

В качестве хеш-функции также применяют функцию преобразования системы счисления. Ключ, записанный как число в некоторой системе счисления P, интерпретируется как число в системе счисления Q>P. Обычно выбирают Q=P+1. Это число переводится из системы Q обратно в систему P, приводится к размеру пространства записей и интерпретируется как адрес.

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

Основная идея базовой структуры при открытом (внешнем) хешировании заключается в том, что потенциальное множество (возможно, бесконечное) разбивается на конечное число классов. Для В классов, пронумерованных от 0 до В-1, строится хеш-функция h(x) такая, что для любого элемента х исходного множества функция h(x) принимает целочисленное значение из интервала 0,1,...,В-1, соответствующее классу, которому принадлежит элемент х. Часто классы называют сегментами, поэтому будем говорить, что элемент х принадлежит сегменту h(x). Массив, называемый таблицей сегментов и проиндексированный номерами сегментов 0,1,...,В-1, содержит заголовки для B списков. Элемент х, относящийся к i -му списку – это элемент исходного множества, для которого h(x)=i.

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

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

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

typedef int T;  // тип элементов
typedef int hashTableIndex; // индекс в хеш-таблице
#define compEQ(a,b) (a == b)
typedef struct Node_ {
       T data;// данные, хранящиеся в вершине
       struct Node_ *next; // следующая вершина
} Node;

Node **hashTable;
int hashTableSize;
hashTableIndex myhash(T data);
Node *insertNode(T data);
void deleteNode(T data);
Node *findNode (T data);

int _tmain(int argc, _TCHAR* argv[]){
  int i, *a, maxnum;
  cout << "Введите количество элементов maxnum : ";
  cin >> maxnum;
  cout << "Введите размер хеш-таблицы HashTableSize : ";
  cin >> hashTableSize;
  a = new int[maxnum];
  hashTable = new Node*[hashTableSize];
  for (i = 0; i < hashTableSize; i++)
    hashTable[i] = NULL;
  // генерация массива
  for (i = 0; i < maxnum; i++)
    a[i] = rand();
  // заполнение хеш-таблицы элементами массива
  for (i = 0; i < maxnum; i++) {
    insertNode(a[i]);
  }
  // поиск элементов массива по хеш-таблице
  for (i = maxnum-1; i >= 0; i--) {
    findNode(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 << "  :  "; 
    Node *Temp = hashTable[i];
    while ( Temp ){
      out << Temp->data << " -> ";
      Temp = Temp->next;
    }
    out << endl;
  }
  out.close();
  // очистка хеш-таблицы
    for (i = maxnum-1; i >= 0; i--) {
        deleteNode(a[i]);
    }
  system("pause");
  return 0;
}

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

// функция поиска местоположения и вставки вершины в таблицу
Node *insertNode(T data) {
  Node *p, *p0;
  hashTableIndex bucket;
  // вставка вершины в начало списка
  bucket = myhash(data);
  if ((p = new Node) == 0) {
    fprintf (stderr, "Нехватка памяти (insertNode)\n");
    exit(1);
  }
  p0 = hashTable[bucket];
  hashTable[bucket] = p;
  p->next = p0;
  p->data = data;
  return p;
}

//функция удаления вершины из таблицы
void deleteNode(T data) {
  Node *p0, *p;
  hashTableIndex bucket;
  p0 = 0;
  bucket = myhash(data);
  p = hashTable[bucket];
  while (p && !compEQ(p->data, data)) {
    p0 = p;
    p = p->next;
  }
  if (!p) return;
  if (p0)
    p0->next = p->next;
  else
    hashTable[bucket] = p->next;
  free (p);
}

// функция поиска вершины со значением data
Node *findNode (T data) {
  Node *p;
  p = hashTable[myhash(data)];
  while (p && !compEQ(p->data, data)) 
    p = p->next;
  return p;
}
Листинг .
< Лекция 38 || Лекция 39: 1234 || Лекция 40 >
Денис Курбатов
Денис Курбатов
Владислав Нагорный
Владислав Нагорный

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

Спасибо!