Конспект установочных лекций по комплексному курсу Информатика, Теория информации

       

Эффективное представление множеств


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

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

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

Вычислительная структура множеств с доступом по ключу

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

sort key.

fct key=(data) key.

Выше приведенная функция осуществляет доступ к порциям данных типа data, содержащих в себе компоненту типа key.

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

fct emptystore=store,

fct get=(store, key) data,



fct insert=(store, data) store,

fct delete=(store,key) store.

Способ действия этих функций можно представить следующими равенствами:

k = key(d) Þ get(insert(s, d), k) = d

k ¹ key(d) Þ get(insert(s, d), k) = get(s, k),

delete(emptystore, k) = emptystore,

k = key(d) Þ delete(insert(s, d), k) = delete(s, k),

k ¹ key(d) Þ delete(insert(s, d), k) = insert(delete(s, k), d).

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

 

Метод хэширования

Метод хэширования

позволяет хранить множество элементов в линейном массиве длиной z. Для этого нужна функция расстановки («рассыпания»):

h: keyà[0:z-1],

которая каждый элемент типа key отображает на индекс в множестве [0:z-1]. Эта функция устанавливает, под каким индексом будет храниться данный элемент в массиве. Используем h(m) в качестве индекса (также называемого ключом) для запоминания элемента данных в массиве

sort store = [0:z-1] array data a.

Как правило, число элементов типа key значительно больше, чем z. Тогда функция h наверняка неинъективна. Возможно хранение элемента b с ключом m в массиве a под индексом h(m). Получаются следующие реализации функций:

fct emptystore = store: emptyarray,

fct get = (store

a, key k) data: a[h(k)]

fct insert = (store a, data d) store: update(a, h(key(d)), d),

fct delete = (store a, key k) store: update(a, h(k), empty).

Здесь предполагается, что empty обозначает элемент типа data, который играет роль держателя места. Функции работают корректно, пока для всех встречающихся ключей значения функции расстановки различны. Возникает проблема, когда нужно запоминать два различных элемента с ключами m1 и m2, и при этом оказывается h(m1)=h(m2).


В этом случае говорят о коллизии.

Для использования метода хэширования надо справиться со следующими проблемами:

·         определения величины массива и тем самым числа z значений индексов,

·         выбор функции расстановки h,

·         определение способа разрешения коллизий.

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

Размер массива должен быть выбран таким, чтобы массив был занят не более чем на 90%.

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

Если сами ключи также заданы в виде натуральных чисел, например из интервала от 0 до s-1, где число s значительно больше числа z, то в качестве функции расстановки можно просто взять

h(i) = i mod z. 

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


Следует также обратить внимание, что вычисление этой функции не должно быть слишком трудоемким.

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

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

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

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

Если при вычислении значения индекса для заданного ключа выясняется, что по этому индексу уже занесен элемент данных с другим ключом, то по определенному правилу вычисляется следующий индекс, по которому и заносится элемент данных.


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

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

Различают линейное и

квадратичное зондирование. При линейном зондировании позиции хэш-массива просматриваются с постоянным шагом, а при квадратичном, исходя из значения h(i),—значения индекса

h(i)+1 mod z, h(i)+4 mod z, h(i)+9 mod

z,…,h(i)+j2 mod z.

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

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

Метод хэширования весьма эффективен, если используются только рассмотренные операции над элементами множеств.


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

Анализ статистики показывает, что метод хэширования чрезвычайно эффективен, если нет слишком большого заполнения хэш-массива. Даже его заполнение на 90% при достаточно удачно выбранном способе зондирования для занесения или выбора нужного элемента в среднем требует 2,56 шага зондирования. Это значение существенно зависит от степени загрузки массива. Поэтому размер хэш-массива следует выбирать таким, чтобы в процессе работы он заполнялся не более чем на 90%.

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

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

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


Содержание раздела