
Ста: Лекция №3 - Реализация и использование простейших атд
Версия 2.0, 8 сентября 2013г.
(С) 2012-2013, Зайченко Сергей Александрович, к.т.н, ХНУРЭ, доцент кафедры АПВТ
Абстрактные типы данных (атд)
Абстрактный тип данных(АТД) - математическая модель и совокупность определенных на ней операций, используемая при неформальном определении алгоритмов. АТД определяет набор действий, иначе говоря, интерфейс, независимый от конкретной реализации типа, для оперирования его значениями.
При рассуждении на уровне АТД реализация операций полностью скрыта. В литературе по классическим алгоритмам часто можно встретить описание в виде псевдокода - некоторой смеси программных инструкций и естественного языка, оперирующего данными с использованием АТД и свойственных им операций. Алгоритмы, определение которых ограничено в терминах операций над АТД, не зависят от конкретных реализаций.
АТД реализуется при помощи структур данных в конкретных языках программирования. Может существовать несколько реализаций АТД в виде структур данных, при этом реализация каждой из операций может характеризоваться различной производительностью. Благодаря единому набору операций, реализации АТД могут быть взаимозаменяемыми. Такой подход к организации обработки данных способствует улучшению модульности разрабатываемых программ. Модульность, в свою очередь, существенно упрощает проектирование, разработку и развитие программ.
К простейшим АТД относятся:
список, или последовательность (list/sequence);
стек (stack);
очередь (queue);
отображение (map);
множество (set).
Операции АТД обычно разделаются на команды (command) изапросы (query). Операции-команды выполняют некоторые действия, которые изменяют внутреннее состояние объектов АТД, но не возвращают никакого результата. Операции-запросы, напротив, возвращают некоторое значение-результат, основанный на текущем состоянии объекта АТД, но при этом ни коим образом его не модифицируют. В названии операций-запросов обычно присутствуют типичные префиксы - GET, IS, TRY - что несвойственно операциям-командам. Операций АТД, одновременно являющихся и командами, и запросами принято избегать, поскольку они нарушают естественную логику и ухудшают восприятие алгоритмов и их реализаций.
Многие АТД обладают похожими друг на друга операциями. Например, содержимое любого АТД можно очистить (команда CLEAR), также можно определить пустоту содержимого АТД (запрос IS_EMPTY). Другие операции являются более специфическими для конкретного АТД (например, операция INTERSECT для множеств).
Аргументами операций АТД могут являться как элементарные типы данных, так и другие АТД или связанные с АТД вспомогательные элементы.
Атд “Список” ( “Последовательность” )
Список является наиболее распространенным в программировании абстрактным типом данных. Список представляет собой упорядоченную последовательность значений некоторого типа элементов. Длина списка - это количество содержащихся в нем элементов. Список, длина которого равна 0, называетсяпустым.
Списки предоставляют свободный доступ к своим элементам, различаемым по позиции. Список имеет начальнуюиконечную позицию. Позиция может быть такженедействительной, что применяется для ситуаций, когда требуется указать на ее некорректность или отсутствие в списке. Какое именно данное используется в качестве позиции в конкретной реализации АТД на абстрактном уровне не играет роли, достаточно обеспечить возможность доступа к элементам через позиции, а также операции сравнения значений, играющих роль позиции.
К операциям, определяемых на модели списков, относятся:
CLEAR( list ) - делает список пустым;
IS_EMPTY( list ) : bool - определяет является ли список пустым;
GET_LENGTH( list ) : int - возвращает длину списка;
GET_NEXT( list, pos ): pos - возвращает следущую позицию после указанной;
GET_PREV( list, pos ): pos - возвращает предыдущую позицию после указанной;
GET_FIRST( list ): pos - возвращает первую позицию в списке;
GET_LAST( list ) : pos - возвращает позицию в списке, следующую за последней;
INSERT ( list, pos, value ) - вставляет значение в указанную позицию в списке, перемещая все элементы от поизции и далее в следующую более “высокую” позицию;
RETREIVE ( list, pos ) : value - возвращает хранящееся в списке значение по указанной позиции;
DELETE( list, pos ) - удалает из списка элемент, хранящийся по указанной позиции.
Подразумевается, что конкретная реализация алгоритмов заменит все типы и названия операций АТД на конкретные, которые отвечают ожидаемому поведению. Операции над последовательностями можно реализовать многими способами. Наиболее ожидаемыми будут реализации на основе рассмотренных ранее в курсе базовых структур данных - векторов и связных списков. Основную массу операций оформляют в виде функций, однако, в целях упрощения, вместо тривиальных функций можно использовать соответствующие очевидные выражения напрямую.
Если в качестве реализации АТД “список” выступает вектор, то в качестве значений-позиций используются индексы в массиве данных. Операции с позициями сводятся к простейшим числовым действиям. Значение -1 можно использовать как недействительную позицию.
В случае выбора связных списков в качестве структуры для реализации, роль значений-позиций будут играть адреса элементов-узлов. Операции с позициями в таком случае будут задействовать структурные связи из объекта-списка и объектов-узлов. Недействительная позиция в списке - нулевой адрес узла.
Ниже в виде таблиц представлены эквивалентные реализации операций АТД “список” для целых чисел, использующие рассмотренные ранее базовые структуры данных. Ранее реализованные функции не расписываются, а для новых указана полная реализация.
Операция АТД “Список” |
Реализация на основе векторов |
CLEAR( list ) |
v.m_nUsed = 0; |
IS_EMPTY( list ) : bool |
return v.m_nUsed > 0; |
GET_LENGTH( list ) : int |
return v.m_nUsed; |
GET_NEXT( list, pos ): pos |
return pos + 1; |
GET_PREV( list, pos ): pos |
return pos - 1; |
GET_FIRST( list ): pos |
return 0; |
GET_LAST( list ) : pos |
return v.m_nUsed; |
INSERT ( list, pos, value ) |
IntegerVectorInsertAt( v, pos, value ); |
RETREIVE ( list, pos ) : value |
return v.m_pData[ pos ]; |
DELETE( list, pos ) |
IntegerVectorDeleteAt( v, pos ); |
Операция АТД “Список” |
Реализация на основе связных списков |
CLEAR( list ) |
IntegerListDestroy( l ); IntegerListInit( l ); |
IS_EMPTY( list ) : bool |
return ! l.m_pFirst; |
GET_LENGTH( list ) : int |
int IntegerListGetLength ( const IntegerList & _l ) { int length = 0; IntegerList::Node * pNode = _l.m_pFirst; while ( pNode ) { ++ length; pNode = pNode->m_pNext; } return length; } |
GET_NEXT( list, pos ): pos |
return pNode->m_pNext; |
GET_PREV( list, pos ): pos |
IntegerList::Node * IntegerListGetPrevNode ( const IntegerList & _l, IntegerList::Node * _pNode ) { assert( _pNode ); IntegerList::Node * prevNode = _l.m_pFirst; while ( prevNode && prevNode->m_pNext != _pNode ) prevNode = prevNode->m_pNext; return prevNode; }
Для двусвязных список используем связь m_pPrev сразу. |
GET_FIRST( list ): pos |
return l.m_pFirst; |
GET_LAST( list ) : pos |
return nullptr;
Связь l.m_pLast является последним значением, а не следующим за последним, как этого требует определение АТД |
INSERT ( list, pos, value ) |
IntegerListInsertAfter( l, pNode, value ); |
RETRIEVE ( list, pos ) : value |
return pNode->m_value; |
DELETE( list, pos ) |
void IntegerListDeleteNode ( IntegerList & _l, IntegerList::Node * _pNode ) { IntegerListDeleteBefore( _l, _pNode->m_pNext ); }
Можно оптимизировать, если известен адрес предыдущего узла:
IntegerListDeleteAfter( _l, _pPrevNode ); |
Предположим, требуется реализовать простейший алгоритм поиска интересующего значения в списке путем полного перебора. Ниже представлено описание данного алгоритма в виде псевдокода:
position FIND ( list myList, value v )
{
position p;
p = GET_FIRST( myList );
while ( p < GET_LAST( myList ) )
{
if ( RETRIEVE( myList, p ) == v )
return p;
p = GET_NEXT( myList, p );
}
return INVALID_POSITION;
}
Далее демонстрируются оба варианта реализации алгоритма FIND, использующие предложенные выше фрагменты кода для каждой из операций АТД “список”. Соответствующие реализации инструкции из изначального псевдокода размещены в комментариях.
Реализация алгоритма на основе векторов выглядит следующим образом:
// position FIND( list myList, value v )
int find ( const IntegerVector & _v, int _value )
{
// position p;
// p = GET_FIRST( myList );
int p = 0;
// while ( p < GET_LAST( myList ) )
while ( p < _v.m_nUsed )
{
// if ( RETRIEVE( myList, p ) == v )
// return p;
if ( _v.m_pData[ p ] == _value )
return p;
// p = GET_NEXT( myList, p );
++p;
}
// return INVALID_POSITION;
return -1;
}
Следующим образом реализуется альтернативная версия на основе связных списков:
// position FIND( list myList, value v )
IntegerList::Node * find ( const IntegerList & _l, int _value )
{
// position p;
// p = GET_FIRST( myList );
IntegerList::Node * p = _l.m_pFirst;
// while ( p < GET_LAST( myList ) )
while ( p )
{
// if ( RETRIEVE( myList, p ) == v )
// return p;
if ( p->m_value == _value )
return p;
// p = GET_NEXT( myList, p );
p = p->m_pNext;
}
// return INVALID_POSITION;
return nullptr;
}
Как видно из приведенного примера, элементы изначального неформально описанного алгоритма четко прослеживаются в коде с использованием каждой из реализаций АТД.
Приведем еще один пример алгоритма, определенного в терминах операций АТД - алгоритм удаления из списка повторяющихся элементов. В виде псевдокода потребуются следующие инструкции:
void PURGE ( list myList )
{
position p, q;
p = GET_FIRST( myList );
while ( p < GET_LAST( myList ) )
{
q = GET_NEXT( myList, p );
while ( q < GET_LAST( myList ) )
{
if ( RETRIEVE( myList, p ) == RETRIEVE( myList, q ) )
DELETE( myList, q );
else
q = GET_NEXT( myList, q );
}
p = GET_NEXT( myList, p );
}
}
Реализация на основе векторов:
#include “integer_vector.hpp”
// void PURGE ( list myList )
void purge ( IntegerVector & _v )
{
// p = GET_FIRST( myList );
int p = 0;
// while ( p < GET_LAST( myList ) )
while ( p < _v.m_nUsed )
{
// q = GET_NEXT( myList, p );
int q = p + 1;
// while ( q < GET_LAST( myList ) )
while ( q < _v.m_nUsed )
{
// if ( RETRIEVE( myList, p ) == RETRIEVE( myList, q ) )
// DELETE( myList, q );
// else
// q = GET_NEXT( myList, q );
if ( _v.m_pData[ p ] == _v.m_pData[ q ] )
IntegerVectorDeleteAt( _v, q );
else
++q;
}
// p = GET_NEXT( myList, p );
++p;
} }
Реализация на основе связных списков:
#include “integer_list.hpp”
// void PURGE ( list myList )
void purge ( IntegerList & _l )
{
// p = GET_FIRST( myList );
IntegerList::Node * p = _l.m_pFirst;
// while ( p < GET_LAST( myList ) )
while ( p )
{
// q = GET_NEXT( myList, p );
IntegerList::Node * q = p->m_pNext;
IntegerList::Node * prevQ = p;
// while ( q != GET_LAST( myList ) )
while ( q )
{
// if ( RETRIEVE( myList, p ) == RETRIEVE( myList, q ) )
// DELETE( myList, q );
// else
// q = GET_NEXT( myList, q );
if ( p->m_value == q->m_value )
{
IntegerListDeleteAfter( _l, prevQ );
q = prevQ->m_pNext;
}
else
{
prevQ = q;
q = q->m_pNext;
}
}
// p = GET_NEXT( myList, p );
p = p->m_pNext;
} }
Поскольку производительность каждой операции может различаться между реализациями (в частности, операция удаления элемента из списка в произвольной позиции), из второго примера очевидно, что быстродействие алгоритма существенно зависит от свойств выбранной в качестве основы структуры данных