Rationale for Ada 2005: 6a Containers
RUSTOPBACKNEXT
ENG |
3. Maps
@ We will now turn to the maps and sets packages. We will start by considering maps which are more exciting than sets and begin with ordered maps which are a little simpler and then consider hashed maps. @ Remember that a map is just a means of getting from a value of one type (the key) to another type (the element). This is not a one-one relationship. Given a key there is a unique element (if any), but several keys may correspond to the same element. A simple example is an array. This is a map from the index type to the component type. Thus if we have
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rationale for Ada 2005: 6a Containers
ENGRUSTOPBACKNEXT3. Карты
@ Теперь обратимся к пакетам карт и пакетам наборов. Мы начнём с рассмотрения карт, которые являются более объемлющими чем наборы и начнём с упорядоченных карт, которые несколько проще и затем рассмотрим хэш карты.
@ Запомните, что карта - только средство получения из значения одного типа (ключа) значения другого типа (элемента). Это не взаимно-однозначное отношение. На каждый ключ имеется свой уникальный элемент (если таковой имеется), но несколько ключей могут соответствовать одному и тому же элементу. Простой пример - массив. Это - карта от индексного типа до составляющего типа. Таким образом, если мы имеем:
|
@ то это обеспечивает нам карту от целых чисел в диапазоне 1 .. 6 к небольшому количеству значений типа Character.
@ Здесь целому числу 3 соответствует уникальный символ 'i', но некоторому символу 'a' может соответствовать несколько целых чисел (в данном случае 1 и 5).
@ Более интересным примером является случай когда набор используемых ключевых значений весьма разрежен. Пусть, у нас имеется склад запчастей. Каждая запчасть идентифицируется пятизначный кододом. Запчасти хранятся в стойках обозначаемых буквенным литералом. Если запчастей не очень много, то только часть пятизначных чисел реально используются, и поэтому было бы слишком расточительно использовать для этого массив размером 10^5. Вместо этого мы можем использовать контейнер, который содержал бы только пары следующего вида (34618, 'F'), (27134, 'C') и так далее. Мы можем сделать это используя карту. Мы обычно именуем пару значений узлами карты.
@ Есть два пакета карт которые почти не отличаются друг от друга. Первый использует упорядоченные ключи, а другой использует хеш-функцию. Вот спецификация пакета упорядоченных карт где показаны только средства свойственные им обоим.
|
@ Настраиваемые параметры включают отношение упорядочения "<" ключей и отношение эквивалентности "=" для элементов.
@ Предполагается, что отношение упорядочения правильное если X < Y является истиной а Y < X - ложь. Мы говорим, что два ключа X и Y эквивалентны если и X < Y и Y < X являются ложью. Другими словами, это определяет эквивалентный класс ключей. Отношения должны также быть транзитивны, то есть, если X < Y и Y < Z истинны, тогда X < Z также должно быть истинно.
@ Это понятие эквивалентных отношений принято для любых карт и наборов.
@ Иногда (как здесь) оно определено в терминах упорядочения, в других случаях, как мы увидим, оно определено функцией эквивалентности.
@ Жизненно важно чтобы отношения эквивалентности были определены должным образом и отвечали вышеупомянутым требованиям. Для контейнерных пакетов это не возможно проверить и если операции являются неправильными тогда, специфическое поведение почти неизбежно.
@ Для удобства пользователя функция Equivalent_Keys объявлена явно. Она выполняет примерно следующее:
|
@ Операция равенства для элементов не настолько требовательна. Она должна быть симметрична так, чтобы X = Y и Y = X имели один и тот же результат, но транзитивность здесь не требуется (за редким исключением). Операция только используется для функции "=" на контейнерах в целом.
@ Отметим, что Find и подобные операции для карт и наборов работают в терминах эквивалентных отношений, а не равенства как это имело место со списками и векторами.
|
@ Типы Map и Cursor и константы Empty_Map и No_Element подобны соответствующим объектам в списковых и векторных контейнерах.
|
@ Они также подобны соответствующим объектам для списков. Отметим, что две карты, как говорится, равны, если у них есть то же самое число узлов с эквивалентными ключами (как определено "<"), чьи соответствующие элементы равны (как определено "=").
|
@ В этом случае есть функция Key такая же как и функция Element. Но нет никакой процедуры Replace_Key, так как не имеет смысла изменять ключ, не изменяя также и элемент, и это в действительности сводится к удалению старого узла и затем вставке нового.
@ Процедуры Query_Element и Update_Element немного различаются в том что процедура Process также берет ключ и елемент в качестве параметра, который будет читаться или обновляться. Отметим также, что ключ не может быть изменён. Однако значение ключа задаётся, так как это может быть полезно в случае если обновление должно быть выполнено. Напомним, что мы не можем добраться непосредственно от элемента к ключу, это возможно только от ключа к элементу.
|
@ Эта процедура перемещает карту из Target в Source после первоначальной очистки Target. При этом после выполнения операции Source очищается, и его Length (Source) становился равным нулю.
|
@ Эти процедуры вставляют новый узел в карту, но только если узел с таким ключом отсутствует в карте. Если узел с таким ключом уже существует, тогда первые две процедуры возвращают его позицию в поле Position, a поле Inserted устанавливается в False, тогда как третя процедура поднимает исключение Constraint_Error (при этом значение элемента не изменяется). Если узел с эквивалентным ключом не найден тогда создаётся новый узел с данным ключом, значение элемента устанавливается в New_Item, когда он задан, в противном случае он берет свое значение по умолчанию (если оно имеется), и поле Position установливается когда оно дано.
@ В отличие от векторов и списков, мы не должны указывать, где новый узел должен быть вставлен потому что это упорядоченная карта, и поэтому узел помещается в правильное место согласно порядка установленного настраиваемым параметром "<".
|
@ Это несколько походит на последнюю процедуру Insert за исключением того, что если уже имеется узел с эквивалентным ключом тогда он заменяется (а не возбуждается исключение Constraint_Error). Заметим, что и ключ и элемент обновляются т.к. наличие эквивалентных ключей в пределах одной карты не допускается.
@ Например, ключевая часть может быть записью с номером запчасти и годом производства
|
@ и мы могли бы определить отношение упорядочивания просто в терминах номера запчасти которое будет использоваться как настраиваемый параметр
|
@ В этой ситуации ключи будут соответствовать без компоненты Year, и таким образом, это должно быть обновлено. Другими словами, с этим определением отношения упорядочения два ключа эквивалентны только если номера запчастей одинаковы.
|
@ В этом случае будет возбуждено исключение Constraint_Error если такого узла не существует. При замене и ключ и элемент обновляются так же как и для Include.
@ Возможно лучший пример эквивалентных ключей, не являющихся полностью равными, это если бы ключ был бы строкой. Мы могли бы решить, что не все символы должны соответствовать в тесте на эквивалентность, но мы могли бы захотеть обновить строку как используется в параметре Replace.
|
@ Если имеется узел с эквивалентным ключом, то он удаляется. Если нет, то ничто не происходит.
|
@ Эти процедуры удаляют узел. В первом случае, если нет эквивалентного ключа, тогда возбуждается исключение Constraint_Error (в отличие от Exclude). Во втором случае, если курсор - No_Element возбуждается исключение Constraint_Error. Выполняется также проверка, для гарантии того, что курсор определяет узел в правильной карте (помните, что курсоры определяют и объект и контейнер); если эта проверка терпит неудачу тогда возбуждается исключение Program_Error.
@ Обратите внимание, что последовательность Insert, Include, Replace, Exclude и Delete это своего рода прогрессия: вставка со строгими условиями; вставка с менее строгими условиями; строгая замена; менее строгая замена и строгое удаление. Отметим также, что Include, Replace и Exclude не относятся к спискам и векторам.
|
@ Назначение этих процедур самоочевидно. В отличие от операций в векторах и списках, Find осуществляет логический поиск целой карты, а не только начинающийся в некоторой точке (а так как она ищет целую карту то нет никакого смысла в наличии Reverse_Find). (В терминах реализации она не будет фактически искать целую карту, потому что это будет структурировано в пути, который делает это ненужным - как сбалансированное дерево возможно). Кроме того, Find использует отношение эквивалентности основанное на параметре "<" как в предыдущем примере, это только должно соответствовать номеру части а не году. Вызов функции Element (My_Map, My_Key) эквивалентен вызову Element (Find (My_Map, My_Key)).
|
@ Они также касаются и других контейнеров.
@ И в конце мы имеем
|
@ Мы опустили упоминание о довольно большом количестве операций у которых нет никакого эквивалента в хаш картах, мы возвратимся к ним через мгновение.
@ Как пример, мы можем сделать контейнер, содержащий информацию о запасных частях. Мы можем использовать тип Part_Key и функцию "<" рассмотренные выше. Мы можем предположить, что тип элемента будет такой:
|
@ Это дает и символьный код стойки и числовой код в запчасти.
@ Мы можем тогда объявить контейнер таким образом:
|
@ Последний параметр может быть опущен, потому что формально он имеет значение по умолчанию <> .
@ Мы можем теперь добавить элементы к нашей памяти следующим образом:
|
@ Теперь у нас может быть процедура, которая по числовому коду запчасти выясняла бы имеется ли такая запчасть в наличии, и если да, то возвращаля бы литерный код полки и год производства, уменьшая при этом итоготовый счетчик.
|
@ Отметим, что мы должны были поместить фиктивное значение года в запрос Find. Мы могли, конечно, использовать новую нотацию <> для этого:
|
@ Читатель может улучшить этот пример на досуге используя Update_Element, например.
@ Как другой пример предположим, что мы желаем проверить все полки, ища запчасти, полка которых низка, возможно меньше чем некоторый данный параметр. Мы можем использовать Iterate для этого:
|
@ Заметим, что сдесь используется так называемое нисходящее замкнутое выражение. Процедура Check_It должна быть объявлена локальной внутри Check_Stock чтобы обратиться к параметру Low. (Конечно, мы могли объявить её и снаружи, а затем скопировать параметр Low для глобальной переменной, но это - дурная практика, которую нужно делать на отстойных языках (в том числе и на Ада 95). ??? Это не сейф задачи с одной стороны ???). Другой подход должен использовать функции First и Next (и так далее...) следующим образом:
|
@ Рассмотрим теперь хэш-карты. Неприятность с упорядоченными картами в основном состоит в том, что когда у карты много элементов то поиск в ней может быть очень медленным. Могут использоваться различные методики, например бинарное дерево, но даже в этом случае время поиска увеличится по крайней мере как логарифм от числа элементов. Лучший подход состоит в использовании хеш-функции. Это знакомо многим читателям (особенно тем, кто писал компиляторы). Общее представление следующие.
@ Мы определяем функцию, которая берет ключ и возвращает некоторое значение в заданном диапазоне. В случае Ада контейнеров она должна возвратить значение модульного типа Hash_Type, который объявлен в корневом пакете Ada.Containers. Тогда мы можем преобразовать это значение в диапазон, представляющий индекс массива, размер которого соответствует ёмкости карты. Это индексное значение - привилегированное место, чтобы сохранить вход. Если уже есть вход в этом месте (например, некоторый другой ключ уже хэширован к этому же самому значению), тогда возможны разные варианты. Один из них состоит в том, чтобы создать список входов с одним и тем же индексным значением (часто называемый кластером памяти); иначе должен просто поместить это в следующий доступный слот.
@ Подробности не имеют значения. Но конечный эффект состоит в том, что если карта не слишком полна, и выбранная хеш-функция хороша тогда, мы можем найти элемент почти немедленно более или менее независимо от размера карты.
@ Всё что должен сделать пользователь - это определить подходящую хеш-функцию. Она должна давать хорошее распределение значений на диапазон Hash_Type для используемой совокупности ключей, она должна избегать кластеризации, и прежде всего, для данного ключа она должна всегда возвращать одно и то же значение хеш-функции. Хороший материал по хеш-функциям можно найти в Knuth [1].
@ Определение хороших хеш-функций важная часть работы. В случае числовых кодов запчастей мы могли бы умножить код запчасти на некоторое простое число и затем усечь результат по модулю Hash_Type. Автор смущается давать пример, но возможно:
|
@ Это - вероятно очень плохой пример для применения, потому что это близко к половине величины 2 ** 32 для типичного значения Hash_Type'Last+1. Конечно оно не должно быть простым, но относительно простым к этому, такие как 5 ** 13. Knuth предлагает делить диапазон на золотое число t = (v5+1) / 2 = 1.618... и затем брать самое близкое относительно простое число, которое является фактически самым близким нечетным числом (в данном случае, это 2654435769).
@ Вот историческая справка. Marin Mersenne (1588-1648) был монахом, который жил в Париже.
@ Он изучил числа вида Mp = 2p - 1, где p является простым числом. Многие из них сами являются простыми.
@ Mersenne дал список таких чисел вподь до 257 которые были простыми (а именно, 2, 3, 5, 7, 13, 17, 19, 31, 67, 127, 257). Только в 1947 было обнаружено, что некоторые из них неправильны (61, 89, и 107 являются также простыми, но 67 и 257 - нет). К настоящему времени известно 42 числа Mersenne, наибольшее из которых является также наибольшим известным простым числом - M25964951 - см. www.mersenne.org.
@ Спецификация пакета хэш - карт очень похожа на спецификацию для упорядоченных карт. Она начинается:
|
@ В отличие от пакета упорядоченных карт здесь есть дополнительный настраиваемый параметр Hash, задающий хеш-функцию, а также параметр упорядочения "<" здесь заменен функцией Equivalent_Keys. Именно эта функция определяет эквивалентные отношения для хэш - карт; при этом важно, что Equivalent_Keys (X, Y) и Equivalent_Keys (Y, X) равнозначны. Кроме того, если X и Y эквивалентны и Y и Z эквивалентны, тогда X и Z также должны быть эквивалентными.
@ Отметим, что функция Equivalent_Keys в пакете упорядоченных карт, обсужденном выше, соответствует формальному настраиваемому параметру того же самого названия в этом пакете хэш - карт. Это должно облегчить преобразование между двумя формами пакетов.
@ Возвращаясь к нашему примеру, если мы теперь пишем:
|
@ тогда мы можем иллюстрировать пакет хэш - карт следующим образом:
|
@ и затем остальная часть нашего примера будет точно такая же. Таким образом, мы можем легко преобразовать от упорядоченной карты к хэш - карте и наоборот, что обеспечивается тем что мы используем средства обычные для обоих.
@ Заканчивая обсуждение карт, кратко рассмотрим дополнительные средства этих пакетов.
@ У пакета упорядоченных карт есть следующие дополнительные подпрограммы:
|
@ Их назначение самоочевидно из названия. Здесь функции Floor и Ceiling представляют особый интерес. Функция Floor ищет последний узел, ключ которого не больше чем Key, функция Ceiling ищет первый узел, ключ которого не меньше чем Key, если такой элемент не найден - обе функции возвращают No_Element. Функция Previous является противоположностью Next, а Reverse_Iterate подобна Iterate но выполняется в обратном направлении.
@ Функции "<" и ">" введены, главным образом, для удобства. Таким образом, первая эквивалентна следующему:
|
@ Ясно этих дополнительных операций можно было бы избежать, если мы пожелаем сохранить опцию преобразования в хэш - карту и далее.
@ У хэш - карт есть очень важное средство отсутствующее в упорядоченных картах, это возможность определить ёмкость векторного пакета. (В этом смысле хэш - карты немного походят на векторы, тогда как упорядоченные карты больше похожи на списки). Таким образом, мы имеем:
|
@ ??? Поведение очень что касается векторов ???. Мы вовсе не обязаны устанавливать ёмкость самостоятельно, так как она расширяется автоматически по мере необходимости, но мы можем значительно улучшить производительность, если заранее укажем предполагаемую ёмкость. В случае карт увеличение ёмкости требует чтобы хеширование было восстановлено, которое может быть весьма трудоёмким, поэтому, если мы знаем, что наша карта будет больших размеров, лучше заранее установить соответствующую ёмкость. Отметим, что Length (M) не может превысить Capacity (M), но может быть намного меньше.
@ Другие дополнительные подпрограммы для хэш - карт:
|
@ Они (как и дополнительное "<" и ">" для упорядоченных карт) опять главным образом для удобства. Первое эквивалентно:
|
@ Перед переходом к наборам нужно заметить, что есть также некоторые полезные функции в строковых пакетах. Главная из которых:
|
@ Есть подобная функция Ada.Strings.Unbounded.Hash где у параметра Key есть тип Unbounded_String. Она просто преобразовывает параметр в тип String и затем вызывает Ada.Strings.Hash. Есть также настраиваемая функция для ограниченных строк, которая снова вызывает основную функцию Ada.Strings.Hash. Для законченности функция Ada.Strings.Fixed.Hash - переименована из Ada.Strings.Hash.
@ Они предоставлены, потому что часто имеет место, что ключ - строка, и они оберегают пользователя от изобретения собственных хеш-функций для строк, которые могли бы вызвать противную головную боль.
@ Мы могли например сохранить самостоятельно беспокойство определения хорошей хеш-функции в вышеупомянутом примере, превращая числовой код зачасти в 5-символьную строку. Таким образом, мы могли бы написать
|
@ и если это не будет работать хорошо тогда, мы можем обвинить в этом разработчика пакета.
2010-10-24 00:26:57
. .