Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СТА (лекции+лабы) / СТА Лекция 4.docx
Скачиваний:
46
Добавлен:
16.03.2016
Размер:
37.26 Кб
Скачать

Простейшая реализация отображений при помощи массивов

Если ключи являются целыми числами от 0 до небольшого N, при этом корректными являются все ключи в диапазоне, можно реализовать отображения при помощи простейших массивов. При этом, ключи будут использоваться как индексы массива, а элементы будут храниться в ячейках. Если для какого-то из ключей значение не определено, то требуется хранить в соответствующей ячейке некоторое значение, означающее “неопределенность” (например, нулевой указатель), разумеется, если такое возможно для рассматриваемого типа хранимых элементов.

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

Пусть имеется структура, описывающая информацию о конкретном округе:

struct VotingDistrict

{

short m_districtNumber;

int m_votersCount;

char * m_locationDescription;

};

А информация обо всех округах хранится в виде массива указателей на объекты для каждого округа:

const int TOTAL_DISTRICTS_COUNT = 255;

VotingDistrict * g_districts[ TOTAL_DISTRICTS_COUNT ] ;

Чтобы заполнить данные о конкретном округе, следует обратиться к соответствующей ячейке массива по номеру округа (со смещением, т.к. массивы индексируются начиная с 0, а округа нумеруются с единицы):

void addDataAboutDistrict (

short _districtNumber

, int _votersCount

, const char * _locationDescription

)

{

assert( _districtNumber > 0 && _districtNumber <= TOTAL_DISTRICTS_COUNT );

int districtIndex = _districtNumber - 1 ; assert( ! g_districts[ districtIndex ] );

VotingDistrict * pDistrict = new VotingDistrict;

pDistrict->m_districtNumber = _districtNumber;

pDistrict->m_votersCount = _votersCount;

size_t descriptionLength = strlen( _locationDescription );

pDistrict->m_locationDescription = new char[ _descriptionLength + 1 );

strcpy( pDistrict->m_locationDescription, _locationDescription );

g_districts[ districtIndex ] = pDistrict;

}

Теперь, чтобы получить информацию об интересующем округе достаточно обратиться к массиву, использовав номер округа как индекс:

short districtNumber = 168;

VotingDistrict * pDistrict = g_Districts[ districtNumber - 1 ];

Если информации об округе еще не введено, массив будет содержать нулевой указатель. Если необходимо удалить информацию об округе, достаточно удалить объект в соответствующей ячейке массива и обнулить данную ячейку.

В такой структуре в качестве ключей иногда можно использовать перечисляемые типы, реализуемые в языке C как целые числа. Такие типы удобны для представления фиксированного набора значений, естественного для предметной области. Значения литералов-перечислений можно не указывать, по умолчанию, компилятор назначает значения 0, 1, 2, …, что также подходит для индексирования массивов. Например, по ключу-континенту, реализованному в виде перечисления:

enum Continents

{

Europe // 0

, Asia // 1

, Africa // 2

, NorthAmerica // 3

, SouthAmerica // 4

, Australia // 5

, ContinentsTotal // 6 - обозначает количество литералов в перечислении

};

могут храниться данные о текущей численности населения:

int g_populationByContinent[ ContinentsTotal ];

Соответственно, если появляются новые данные переписи населения Южной Америки, их можно внести в программу следующим образом:

g_populationByContinent[ SouthAmerica ] = 400103516;

Получить суммарную численность населения Австралии и Африки можно так:

int x = g_populationByContinent[ Australia ] + g_populationByContinent[ Africa ];

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

Может существовать ряд факторов, при которых отображение нельзя реализовать в виде подобного массива:

  • в качестве ключей выступают типы, отличные от целых чисел, например, строки ;

  • в области значений не существует значения “не определено”;

  • ключи-числа слишком большие для создания массива, используются не подряд.

Однако, если таких препятствий не существует, предложенное упрощение может быть очень эффективным.

Интерфейс множеств

Ниже представлен заголовочный файл с интерфейсом объекта-множества целых чисел:

#ifndef _INTEGER_SET_HPP_

#define _INTEGER_SET_HPP_

// Форвардное объявление структуры-множества

struct IntegerSet;

// Создание объекта-множества

IntegerSet * IntegerSetCreate ();

// Уничтожение объекта-множества

void IntegerSetDestroy ( IntegerSet * _pSet );

// Очистка множества

void IntegerSetClear ( IntegerSet & _set );

// Проверка множества на пустоту

bool IntegerSetIsEmpty ( const IntegerSet & _set );

// Возврат количества элементов в множестве

int IntegerSetSize ( const IntegerSet & _set );

// Проверка множества на наличие указанного ключа

bool IntegerSetHasKey ( const IntegerSet & _set, int _key );

// Добавление указанного ключа в множество

void IntegerSetInsertKey ( IntegerSet & _set, int _key );

// Удаление указанного ключа из множества

void IntegerSetRemoveKey ( IntegerSet & _set, int _key );

// Объединение двух множеств - результат в третьем множестве

void IntegerSetUnite ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet );

// Пересечение двух множеств - результат в третьем множестве

void IntegerSetIntersect ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet );

// Разница между двумя множествами - результат в третьем множестве

void IntegerSetDifference ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet );

// Симметрическая разница между двумя множествами - результат в третьем множестве

void IntegerSetSymmetricDifference ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet );

#endif // _INTEGER_SET_HPP_

Простая тестовая программа на основе таких множеств представлена ниже:

#include "integer_set.hpp"

#include <cassert>

int main ()

{

// Создаем первое множество и помещаем в него ключи 10 и 20

IntegerSet * pSet1 = IntegerSetCreate();

IntegerSetInsertKey( * pSet1, 10 );

IntegerSetInsertKey( * pSet1, 20 );

// Создаем второе множество и помещаем в него ключи 20 и 30

IntegerSet * pSet2 = IntegerSetCreate();

IntegerSetInsertKey( * pSet2, 20 );

IntegerSetInsertKey( * pSet2, 30 );

// Создаем пересечение множеств - ожидаем наличия 1 элемента 20

IntegerSet * pSetI = IntegerSetCreate();

IntegerSetIntersect( * pSet1, * pSet2, * pSetI );

assert( IntegerSetSize( * pSetI ) == 1 );

assert( IntegerSetHasKey( * pSetI, 20 ) );

// Создаем объединение множеств - ожидаем наличия 3 элементов - 10 20 30

IntegerSet * pSetU = IntegerSetCreate();

IntegerSetUnite( * pSet1, * pSet2, * pSetU );

assert( IntegerSetSize( * pSetU ) == 3 );

assert( IntegerSetHasKey( * pSetU, 10 ) );

assert( IntegerSetHasKey( * pSetU, 20 ) );

assert( IntegerSetHasKey( * pSetU, 30 ) );

// Создаем разность множеств - ожидаем наличия 1 элементов - 10

IntegerSet * pSetD = IntegerSetCreate();

IntegerSetDifference( * pSet1, * pSet2, * pSetD );

assert( IntegerSetSize( * pSetD ) == 1 );

// Уничтожаем все созданные множества

IntegerSetDestroy( pSet1 );

IntegerSetDestroy( pSet2 );

IntegerSetDestroy( pSetI );

IntegerSetDestroy( pSetU );

IntegerSetDestroy( pSetD );

}

Реализация множеств при помощи последовательностей

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

В основе реализации множества, приведенной ниже, лежит введенная ранее реализация односвязных списков:

#include "integer_set.hpp"

#include "integer_list.hpp"

#include <cassert>

// Структура-множество

struct IntegerSet

{

// Внутренний список с данными

IntegerList m_data;

};

// Создание объекта-множества

IntegerSet * IntegerSetCreate ()

{

// Создаем объект-множество в динамической памяти

IntegerSet * pSet = new IntegerSet;

// Инициализируем внутренний список

IntegerListInit( pSet->m_data );

// Возвращаем объект во внешний код

return pSet;

}

// Уничтожение объекта-множества

void IntegerSetDestroy ( IntegerSet * _pSet )

{

// Освобождаем ресурсы внутреннего списка

IntegerListDestroy( _pSet->m_data );

// Уничтожаем сам объект-множество

delete _pSet;

}

// Очистка множества

void IntegerSetClear ( IntegerSet & _set )

{

// Очищаем внутренний список

IntegerListClear( _set.m_data );

}

// Проверка множества на пустоту

bool IntegerSetIsEmpty ( const IntegerSet & _set )

{

// Множество пусто, когда пуст внутренний список

return IntegerListIsEmpty( _set.m_data );

}

// Возврат количества элементов в множестве

int IntegerSetSize ( const IntegerSet & _set )

{

// Равно размеру внутреннего списка

return IntegerListSize( _set.m_data );

}

// Определение принадлежности указанного ключа множеству

bool IntegerSetHasKey ( const IntegerSet & _set, int _key )

{

// Осуществляем поиск ключа с начала списка методом полного перебора

IntegerList::Node * pNode = _set.m_data.m_pFirst;

while ( pNode )

{

if ( pNode->m_value == _key )

// Ключ найден

return true;

pNode = pNode->m_pNext;

}

// Ключ не найден

return false;

}

// Вставка ключа во множество

void IntegerSetInsertKey ( IntegerSet & _set, int _key )

{

// Если ключа еще нет во внутреннем списке, его следует вставить в конец списка

if ( ! IntegerSetHasKey( _set, _key ) )

IntegerListPushBack( _set.m_data, _key );

}

// Удаление указанного ключа из множества

void IntegerSetRemoveKey ( IntegerSet & _set, int _key )

{

// Ищем существующий узел с таким ключом

IntegerList::Node * pNode = _set.m_data.m_pFirst;

while ( pNode )

{

if ( pNode->m_value == _key )

{

// Удаляем найденный узел и завершаем процедуру

IntegerListDeleteNode( _set.m_data, pNode );

return;

}

pNode = pNode->m_pNext;

}

// Ошибка - ключа не существует в данном множестве!

assert( !"Key is unavailble!" );

}

// Вспомогательная функция, копирущая все элементы входного множества в другое

void IntegerSetInsertAllKeys ( const IntegerSet & _sourceSet, IntegerSet & _targetSet )

{

// Перебираем все элементы исходного множества

IntegerList::Node * pNode = _sourceSet.m_data.m_pFirst;

while ( pNode )

{

// Вставляем очередной ключ в целевое множество и движемся далее

IntegerSetInsertKey( _targetSet, pNode->m_value );

pNode = pNode->m_pNext;

}

}

// Объединение двух множеств - результат в третьем множестве

void IntegerSetUnite ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Очищаем целевое множество

IntegerSetClear( _targetSet );

// Копируем все элементы первого множества

IntegerSetInsertAllKeys( _set1, _targetSet );

// Копируем все элементы второго множества.

// Функция вставки ключа гарантирует,

// что повторяющиеся значения будут игнорироваться

IntegerSetInsertAllKeys( _set2, _targetSet );

}

// Пересечение двух множеств - результат в третьем множестве

void IntegerSetIntersect ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Очищаем целевое множество

IntegerSetClear( _targetSet );

// Перебираем все элементы первого множества

IntegerList::Node * pNode = _set1.m_data.m_pFirst;

while ( pNode )

{

// Если выбранный элемент первого множества существует во втором,

// то вставляем его в целевое множество

if ( IntegerSetHasKey( _set2, pNode->m_value ) )

IntegerSetInsertKey( _targetSet, pNode->m_value );

pNode = pNode->m_pNext;

}

}

// Разность двух множеств - результат в третьем множестве

void IntegerSetDifference ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Очищаем целевое множество

IntegerSetClear( _targetSet );

// Перебираем все элементы первого множества

IntegerList::Node * pNode = _set1.m_data.m_pFirst;

while ( pNode )

{

// Если выбранный элемент первого множества НЕ существует во втором,

// то вставляем его в целевое множество

if ( ! IntegerSetHasKey( _set2, pNode->m_value ) )

IntegerSetInsertKey( _targetSet, pNode->m_value );

pNode = pNode->m_pNext;

}

}

// Симметрическая разность двух множеств - результат в третьем множестве

void IntegerSetSymmetricDifference ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Сохраняем разницу между первым и вторым во временном множестве

IntegerSet * pTemp1 = IntegerSetCreate();

IntegerSetDifference( _set1, _set2, * pTemp1 );

// Сохраняем разницу между вторым и первым в другом временном множестве

IntegerSet * pTemp2 = IntegerSetCreate();

IntegerSetDifference( _set2, _set1, * pTemp2 );

// Объединяем результирующие множества

IntegerSetUnion( * pTemp1, * pTemp2, _targetSet );

// Освобождаем ресурсы временных множеств

IntegerSetDestroy( pTemp1 );

IntegerSetDestroy( pTemp2 );

}

Множества на основе отсортированных последовательностей

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

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

void IntegerSetInsertKey ( IntegerSet & _set, int _key )

{

// Если список пока пуст, вставить новый ключ в список

if ( IntegerListIsEmpty( _set.m_data ) )

IntegerListPushBack( _set.m_data, _key );

else

{

// Если новый ключ меньше первого узла, вставить новый ключ в начало списка

IntegerList::Node * pNode = _set.m_data.m_pFirst;

if ( _key < pNode->m_value )

{

IntegerListPushFront( _set.m_data, _key );

return;

}

// Ищем узел, после которого нужно вставить новый ключ, не нарушив порядок

while ( pNode )

{

// Игнорируем ключи, которые уже есть в множестве

if ( pNode->m_value == _key )

return;

// Если текущий узел последний в списке, // новый элемент вставляется в конец списка

else if ( ! pNode->m_pNext )

{

IntegerListPushBack( _set.m_data, _key );

return;

}

// Если ключ в следующем узле больше нового ключа,

// нужно вставить новый ключ между текущим и следующим

else if ( pNode->m_pNext->m_value > _key )

{

IntegerListInsertAfter( _set.m_data, pNode, _key );

return;

}

// Движемся далее

else

pNode = pNode->m_pNext;

}

// Если ошибок нет, мы никогда не дойдем до этой точки

assert( ! "We should never get here" );

}

}

Сформировав таким образом упорядоченный во возрастанию список ключей, можно улучшить производительность поиска при переборе узлов. В частности, если искомый ключ меньше ключа в очередном обрабатываемом узле списка, то продолжать поиск не имеет смысла, т.к. в упорядоченном списке такого ключа уже точно не будет. Предположим, имеется множество из значений 10, 20, 30, 40, 50. Необходимо установить входит ли число 27 в состав множества. Значение 27 больше значений 10 и 20, однако меньше 30. Поскольку поиск доходит до большего значения 30, а значение 27 не найдено, дальнейший поиск не имеет смысла:

Улучшим процедуру поиска с учетом изложенного соображения:

bool IntegerSetHasKey ( const IntegerSet & _set, int _key )

{

IntegerList::Node * pNode = _set.m_data.m_pFirst;

while ( pNode )

{

if ( pNode->m_value == _key )

return true;

else if ( pNode->m_value > _key )

return false;

pNode = pNode->m_pNext;

}

return false;

}

Аналогичным образом можно усовершенствовать операцию удаления ключа:

void IntegerSetDeleteKey ( IntegerSet & _set, int _key )

{

IntegerList::Node * pNode = _set.m_data.m_pFirst;

while ( pNode )

{

if ( pNode->m_value == _key )

{

IntegerListDeleteNode( _set.m_data, pNode );

return;

}

else if ( pNode->m_value > _key )

break;

pNode = pNode->m_pNext;

}

assert( !"Key is unavailble!" );

}

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

Начинаем процедуру сравнения узлов в двух списках с первых позиций. 10 < 20, соответственно, необходимо выбрать следующий узел в первом множестве:

Сравнивая следующие элементы обнаруживаем, что в двух списках имеется значение 20. Заносим его в результирующее множество. Далее, передвигаемся к следующим узлам в обоих списках:

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

Более точно этот алгоритм можно представить в виде следующего программного кода:

void IntegerSetIntersect ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Очищаем целевое множество

IntegerSetClear( _targetSet );

// Начинаем анализ с первых позиций в обоих списках одновременно

IntegerList::Node * pNode1 = _set1.m_data.m_pFirst;

IntegerList::Node * pNode2 = _set2.m_data.m_pFirst;

// Пересечение возможно, пока остаются узлы для рассмотрения в обоих списках

while ( pNode1 && pNode2 )

{

// Если ключи в двух списках равны, этот ключ пересекается,

// и заносится в результирующее множество

if ( pNode1->m_value == pNode2->m_value )

IntegerListPushBack( _targetSet.m_data, pNode1->m_value );

else if ( pNode1->m_value < pNode2->m_value )

pNode1 = pNode1->m_pNext;

else

pNode2 = pNode2->m_pNext;

}

}

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

Аналогично можно представить реализации объединения и разности множеств:

void IntegerSetUnite ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Очищаем целевое множество

IntegerSetClear( _targetSet );

// Начинаем анализ с первых позиций в обоих списках одновременно

IntegerList::Node * pNode1 = _set1.m_data.m_pFirst;

IntegerList::Node * pNode2 = _set2.m_data.m_pFirst;

// Объединение возможно, пока остается хотя бы один узел в двух множествах

while ( pNode1 || pNode2 )

{

// Имеются узлы для рассмотрения в обоих множествах

if ( pNode1 && pNode2 )

{

// Если ключ в узле первого списка меньше ключа в узле второго списка,

// следует вставить ключ из первого множества

if ( pNode1->m_value < pNode2->m_value )

{

IntegerListPushBack( _targetSet.m_data, pNode1->m_value );

pNode1 = pNode1->m_pNext;

}

// Если ключ в узле второго списка меньше ключа в узле первого списка,

// следует вставить ключ из второго множества

else if ( pNode1->m_value > pNode2->m_value )

{

IntegerListPushBack( _targetSet.m_data, pNode2->m_value );

pNode2 = pNode2->m_pNext;

}

// Иначе - ключи в двух списках одинаковые. Следует вставить только 1 копию

else

{

IntegerListPushBack( _targetSet.m_data, pNode1->m_value );

pNode1 = pNode1->m_pNext;

pNode2 = pNode2->m_pNext;

}

}

// Если элементов во втором множестве не осталось, копируем первое до конца

else if ( pNode1 )

{

IntegerListPushBack( _targetSet.m_data, pNode1->m_value );

pNode1 = pNode1->m_pNext;

}

// Если же не осталось элементов в первом множестве, копируем второе до конца

else

{

IntegerListPushBack( _targetSet.m_data, pNode2->m_value );

pNode2 = pNode2->m_pNext;

}

}

}

void IntegerSetDifference ( const IntegerSet & _set1,

const IntegerSet & _set2,

IntegerSet & _targetSet )

{

// Очищаем целевое множество

IntegerSetClear( _targetSet );

// Начинаем анализ с первых позиций в обоих списках одновременно

IntegerList::Node * pNode1 = _set1.m_data.m_pFirst;

IntegerList::Node * pNode2 = _set1.m_data.m_pFirst;

// Разность возможна, пока остается хотя бы один узел в первом множестве

while ( pNode1 )

{

// Если еще есть элементы во втором множестве, необходимо сравнивать ключи

if ( pNode2 )

{

// Нельзя вставлять ключи, которые есть в обоих множествах

if ( pNode1->m_value == pNode2->m_value )

{

pNode1 = pNode1->m_pNext;

pNode2 = pNode2->m_pNext;

}

// Если найден ключ в первом множестве, меньший ключу из второго,

// это означает его уникальность, следует внести ключ в результаты

else if ( pNode1->m_value < pNode2->m_value )

{

IntegerListPushBack( _targetSet.m_data, pNode1->m_value );

pNode1 = pNode1->m_pNext;

}

// В противном случае нужно перейти к следующему элементу во втором м-ве

else

pNode2 = pNode2->m_pNext;

}

else

{

// Если второе множество закончилось, копируем элементы первого до конца

IntegerListPushBack( _targetSet.m_data, pNode1->m_value );

pNode1 = pNode1->m_pNext;

}

}

}

Такая реализация теоретико-множественных операций намного привлекательнее ранее описанной, поскольку в самом худшем случае осуществляет не более чем 2N сравнений, что существенно лучше квадратичной зависимости от N.

Реализация множеств при помощи характеристических векторов

Предположим, в академической группе числится не более 32 студентов и их состав не меняется в течение семестра (вполне допустимое для типичного случая ограничение). Смоделировать множество студентов, сдавших тот или иной зачет, можно более оптимальным образом, воспользовавшись простой структурой информации. Для хранения факта сдачи конкретным студентом зачета достаточно 1 бита - сдал или не сдал. Представляется возможным компактно хранить данную информацию внутри переменной целого типа, воспользовавшись побитовыми операциями. Такую структуру данных принято называть характеристическим вектором. При этом, каждому студенту будет соответствовать собственный бит в числе:

unsigned int g_studentPassData;

Допустим студент под номером X в списке сдал зачет, запишем данную информацию в множество:

// битовая маска, содержащая 1 в бите X-1, остальные биты 0

g_studentPassData |= 1 << ( X - 1 );

Если требуется узнать, сдал ли зачет студент под номером Y, также применяем битовые маски

if ( g_studentPassData & ( 1 << ( Y - 1 ) ) )

….

Предположим, выявлен плагиат, и преподаватель аннулировал сдачу зачета студентом Z:

g_studentPassData &= ~( 1 << ( Z - 1 ) );

Либо выявлен чудовищный обман, и деканат аннулировал результаты всей группы:

g_studentPassData = 0;

Представляется возможным реализовать подобную идею для множеств, содержащих и более 32 элементов. Во-первых, существуют 64-битные типы (например, __int64 в Visual C++ или long long в GCC). Во-вторых, можно получить множество и с большим максимальным количеством элементов, если реализовать собственный тип для больших чисел.

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

Пусть имеется информация о сдаче группой двух разных зачетов:

unsigned int g_studentPassData1;

unsigned int g_studentPassData2;

Множество студентов, получивших оба зачета (INTERSECT), можно получить так:

unsigned int g_studentsWith2Passes = g_studentPassData1 & g_studentPassData2;

Множество студентов, сдавших хотя бы один зачет (UNION), можно получить так:

unsigned int g_studentsWithAtLeast1Pass = g_studentPassData1 | g_studentPassData2;

Множество студентов, сдавших первый зачет, но не сдавших второй зачет (DIFFERENCE):

unsigned int g_studentsWithPass1ButWithoutPass2 =

g_studentPassData1 & ~( g_studentPassData2 );

Множество студентов, сдавших только один зачет - первый или второй (SYMMETRIC DIFFERENСE):

unsigned int g_studentsWithExactly1Pass = g_studentPassData1 ^ g_studentPassData2;

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

Выводы

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

Соседние файлы в папке СТА (лекции+лабы)