
К9-12В. Вопросы и ответы к ГОСам 2013 / Программирование на языке высокого уровня / 08. Перемешанная таблица, использующая перемешиванием сложением. Определение, хэш-функция, возникающие проблемы, основны
.docx08. Перемешанная таблица, использующая перемешиванием сложением. Определение, хэш-функция, возникающие проблемы, основные операции, особенности их реализации.
Перемешанные таблицы
Поскольку
взаимную однозначность преобразования
ключа в адрес хранения элемента таблицы
в общем случае обеспечить практически
невозможно, от требования взаимной
однозначности отказываются. Это приводит
к тому, что для некоторых kikj
возможна ситуация, что I(ki) = I(kj).
Такая ситуация создает переполнение
позиций отображаемого вектора и носит
название коллизии. Чтобы таких
ситуаций было меньше, функцию расстановки
подбирают из условия возможно более
равномерного отображения ключей в
адреса хранения. Таблицы, построенные
по такому принципу, также являются
таблицами с вычисляемыми входами и
называются перемешанными таблицами.
Перемешанную
таблицу можно заполнять и обращаться
с ней как с таблицей произвольного
доступа до тех пор, пока не встретится
ключ kj такой, что kikj,
но I(ki) = I(kj), причем ki
– это ключ, который уже встречался ранее
(т.е. пока не возникнет коллизия). Элемент
с ключом ki уже был помещен в позицию
(элемент вектора с индексом) I(ki).
Проблема состоит в определении места
для хранения нового элемента таблицы
с ключом kj. Решение этой проблемы
предполагает разрешение коллизии
и носит название перемешивания.
Существуют различные вспомогательные
методы перемешивания, зависящие от
того, какой способ используется для
разрешения коллизии.
Часто перемешанные таблицы называют еще хэш таблицами (от английского hash–мешанина, путаница), функции расстановки – хэш функциями, а нахождение места хранения элемента – хешированием.
В перемешанной таблице можно выделить две области: основную, в которую элементы таблицы отображаются в результате вычисления производного ключа, и область переполнения, в которую попадают элементы в результате перемешивания при обнаружении коллизии. В зависимости от используемого способа перемешивания такое разделение может быть явным или скрытым. В любом случае основная область перемешанной таблицы отображается вектором. Область переполнения может быть отображена и вектором, и списком (точнее, семейством списков), в зависимости от того, какой способ перемешивания используется.
В каждом конкретном случае при использовании перемешанной таблицы используется какой-то один способ перемешивания. Он используется как при поиске элемента в таблице, так и при включении в таблицу нового элемента.
Рассмотрим два способа перемешивания, используемых наиболее часто: открытое перемешивание (этот способ называют еще перемешиванием сложением) и перемешивание сцеплением.
Открытое перемешивание
Перемешанная таблица, использующая открытое перемешивание (или перемешивание сложением), обладает следующими свойствами:
-
Вся перемешанная таблица отображается вектором.
-
Размер таблицы (вектора) должен быть достаточным для размещения всех элементов таблицы (что очевидно) и отображения их ключей (так как значение ключа отображается в индекс вектора).
-
Разделение таблицы на две области является весьма условным и зависит от конкретных значений ключей включаемых в таблицу элементов и порядка их включения в таблицу (в одной ситуации некоторый элемент вектора может быть отнесен к основной области, в другой – этот же элемент принадлежит области переполнения).
Рассмотрим способ открытого перемешивания на примере выполнения операции включения элементов в таблицу.
Пусть дана следующая последовательность ключей элементов, записываемых в таблицу (всего 15 элементов):
12, 48, 3, 5, 7, 63, 15, 202, 103, 188, 30, 43, 6, 18, 19
Прежде всего, выберем функцию расстановки. Возьмем для примера функцию
I(k) = k%10;
Тогда данная функция расстановки отобразит исходное множество ключей в следующее множество производных ключей:
номер элемента |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
исходный ключ |
12 |
48 |
3 |
5 |
7 |
63 |
15 |
202 |
103 |
188 |
30 |
43 |
6 |
18 |
19 |
производный ключ |
2 |
8 |
3 |
5 |
7 |
3 |
5 |
2 |
3 |
8 |
0 |
3 |
6 |
8 |
9 |
Пусть размер таблицы равен M = 14 элементам (размер выбран так, чтобы показать реакцию на переполнение таблицы). В исходном состоянии таблица пуста.
При занесении элемента в таблицу, прежде всего, вычисляется производный ключ и анализируется позиция таблицы, определенная значением производного ключа. Если эта позиция свободна (что имеет место для первых пяти элементов), элемент заносится в нее (в основную область), и для занесения потребуется только одно обращение к таблице. Таким образом, после записи в таблицу первых пяти элементов таблица будет иметь вид, представленный на рис. II-46,а.
Если позиция таблицы занята (что имеет место, например, при записи в таблицу элемента с ключом 63), тогда начинается процесс перемешивания. Последовательно выбираются следующие позиции таблицы с целью найти свободную. Определение "открытое перемешивание" обусловлено тем свойством, что элементы таблицы, просматриваемые, начиная с конкретной позиции, являются открытыми для ключей, отображаемых в другие позиции. Как только свободная позиция будет найдена, элемент записывается в нее, и эта позиция считается принадлежащей области переполнения (рис. II-46,b; позиции таблицы, относящиеся к области переполнения, выделены цветом). В соответствии с описанным выше алгоритмом в таблицу будут записаны первые 14 элементов, после чего таблица примет вид, представленный на рис. II-46,c.
Если же при поиске свободной позиции мы возвращаемся в исходную точку, что происходит при попытке записать в таблицу последний элемент с ключом 19 (рис. II-46,c), это означает, что в таблице нет свободного места.
В результате для занесения элемента в таблицу в общем случае требуется несколько обращений. Например, при занесении в таблицу элементов с ключами 63 и 15 потребуется по 2 обращения к таблице, а при занесении элемента с ключом 202 – 8 обращений. Количество обращений при занесении в таблицу каждого элементов представлено ниже:
исходный ключ |
12 |
48 |
3 |
5 |
7 |
63 |
15 |
202 |
103 |
188 |
30 |
43 |
6 |
18 |
производный ключ |
2 |
8 |
3 |
5 |
7 |
3 |
5 |
2 |
3 |
8 |
0 |
3 |
6 |
8 |
количество обращений |
1 |
1 |
1 |
1 |
1 |
2 |
2 |
8 |
8 |
4 |
1 |
10 |
8 |
8 |
Суммарное количество обращений к таблице равно 56, среднее – 56/14 =4
Важный результат, который всегда необходимо учитывать при построении функции расстановки, состоит в том, что для случайных записей средняя длина поиска минимизируется, если функция расстановки отображает одинаковое количество ключей на все позиции таблицы-вектора от 1 до M. Вообще говоря, это условие может оказаться невыполнимым по разным причинам (в приведенном выше примере это условие не выполняется). В таких случаях следует, насколько это возможно, приблизить отображение к такой желаемой форме (лучшим вариантом функции расстановки была бы функция I(k) = k % M).
В приведенном выше примере скопление записей образовалось после третьей позиции, в результате чего для занесения элемента с ключом 202 потребовалось 8 обращений к таблице. Подобное скопление неизбежно при использовании открытого перемешивания. Можно несколько улучшить результаты, если переопределить понятие следующей позиции таблицы. В общем случае использование открытого перемешивания при определении следующей позиции требует увеличения индекса элемента на некоторую величину h, называемую шагом перемешивания (другими словами, индекс складывается с шагом перемешивания; поэтому открытое перемешивание называют еще перемешиванием сложением). Значение шага перемешивания может быть любым, но обязательно взаимно простым с величиной, определяющей размер таблицы, чтобы обеспечить просмотр всех ее позиций (например, для таблицы размером 14 элементов можно использовать шаг перемешивания 3 или 5, но нельзя 2 или 6). Выбор шага перемешивания может повлиять на количество обращений к таблице.
В приведенном выше примере был использован шаг перемешивания, равный 1. Если для той же последовательности ключей использовать другое значение шага перемешивания, например, 5, получим следующее количество обращений к таблице:
h = 5
исходный ключ |
12 |
48 |
3 |
5 |
7 |
63 |
15 |
202 |
103 |
188 |
30 |
43 |
6 |
18 |
производный ключ |
2 |
8 |
3 |
5 |
7 |
3 |
5 |
2 |
3 |
8 |
0 |
3 |
6 |
8 |
количество обращений |
1 |
1 |
1 |
1 |
1 |
3 |
2 |
3 |
4 |
4 |
1 |
9 |
1 |
10 |
Суммарное количество обращений к таблице равно 42, среднее – 42/14=3
При занесении элементов в таблицу нужно иметь возможность определять, занята позиция таблицы или свободна. Для этого достаточно включить в элемент таблицы дополнительное поле занятости (по аналогии с динамической просматриваемой таблицей-вектором: значение 0 в этом поле указывает на то, что позиция свободна); тогда структура элемента таблицы может быть представлена следующим образом;
struct Item{
int busy; /* признак занятости позиции таблицы */
int key;
Type info;
};
Алгоритм занесения элемента в перемешанную таблицу с использованием открытого перемешивания приведен на рис. II-47.
Поиск элемента осуществляется по такому же правилу: вычисляется производный ключ и анализируется позиция таблицы, которая определяется значением производного ключа. Если искомый ключ находится в этой позиции, поиск завершен успешно, причем потребовалось только одно обращение к таблице. Если позиция свободна, поиск заканчивается не успешно – в таблице нет элемента с искомым ключом. Если позиция таблицы занята, но ключи не совпадают, выбирается и анализируется следующий элемент таблицы (с учетом используемого шага перемешивания), пока не будет найден искомый элемент или не будет обнаружена свободная позиция.
Из-за эффекта скопления записей, отмеченного выше, не так просто определить среднюю длину поиска в перемешанной таблице, использующей открытое перемешивание. Кроме того, средняя длина поиска будет зависеть также и от степени заполнения таблицы. В книге Ф.Хопгуда (см. Ф.Хопгуд. Методы компиляции. – М.: Мир, 1972. стр. 33) приведена следующая оценка средней длины поиска в предположении, что в скоплении все записи, отображенные в i-ю позицию, появились раньше записей, отображенных в (i+1)-ю позицию:
,
где
– степень заполнения таблицы (M – размер
таблицы, N – число записей, уже включенных
в таблицу).
Важно отметить, что значения, полученные для средней длины поиска, не зависят от размера таблицы, а зависят только от того, насколько заполнена таблица. Даже для таблицы, заполненной на 80%, средняя длина поиска все еще равна приблизительно 3.
Если для таблицы определена операция удаления элемента, нужно иметь в виду следующее:
-
При удалении элемента нельзя перемещать информацию в таблице, так как это может привести к нарушению пути доступа к элементу, и элемент, занесенный в таблицу до выполнения операции удаления, в дальнейшем может быть не найден. Достаточно отметить найденную позицию, в которой находится удаляемый элемент, как удаленную.
-
При занесении в таблицу нового элемента удаленная позиция должна рассматриваться как свободная.
-
При поиске элемента удаленная позиция не должна рассматриваться ни как свободная (иначе нельзя будет найти элементы, занесенные в таблицу перед удалением элемента и расположенные после удаленного элемента), ни как занятая; она должна просто пропускаться.
Алгоритм поиска элемента в перемешанной таблице, использующей открытое перемешивание, приведен на рис. II-48.
Алгоритм удаления элемента достаточно прост и здесь не приводится.
Функции занесения элемента и поиска элемента в перемешанной таблице приводятся ниже, а также в файле Programs/tab3vec.cpp.
struct Item{
int busy; /* 0 - позиция свободна, 1 - занята, (-1) - удалена */
int key;
Type info;
};
const int M = 100; /* размер таблицы */
Item ptable[M]; /* таблица */
/* хеш-функция */
int I(int k)
{
return k % M;
}
/* Занесение нового элемента в таблицу.
* Результат: 0, если элемент включен в таблицу,
* -1, если в таблице есть элемент с заданным ключом,
* -2, если в таблице нет свободного места
*/
int insert(int k, Type in)
{
int strt, i, h = 1; /* шаг перемешивания */
/* вычисление исходной позиции таблицы */
strt = i = I(k);
while(ptable[i].busy > 0){ /* позиция занята */
if(ptable[i].key == k)
return -1; /* элемент с заданным ключом есть в таблице */
i = (i+h) % M; /* следующая позиция */
if(i == strt)
return -2; /* вернулись в исходную позицию - таблица полна */
}
/* занесение нового элемента */
ptable[i].key = k;
ptable[i].busy = 1;
ptable[i].info = in;
return 0; /* элемент занесен */
}
/* Поиск элемента в таблице.
* Результат: индекс элемента таблицы, если элемент найден,
* -1, если в таблице нет элемента с заданным ключом
*/
int search(int k)
{
int strt, i, h = 1; /* шаг перемешивания */
/* вычисление исходной позиции таблицы */
strt = i = I(k);
while(ptable[i].busy>= 0){ /* позиция не свободна */
if(ptable[i].busy > 0 &&ptable[i].key == k)
return i; /* элемент найден */
i = (i+h) % M; /* следующая позиция */
if(i == strt)
break;
}
return -1; /* вернулись в исходную позицию – элемента в таблице нет */
}