
- •Структуры и алгоритмы обработки данных: план курса
- •Лабораторные работы
- •Литература
- •Ста: Лекция №1 - Введение. Данные в памяти программ
- •Введение
- •Модель памяти в прикладных программах
- •Int main ()
- •Int main ()
- •Int main ()
- •Int main ()
- •Char buf[ 2000000 ];
- •Return f(); // бесконечно долго вызываем сами себя // вызываем переполнение стека через время
- •Int main ()
- •Return f();
- •Int main ()
- •Delete[] p; // Освобождаем память
- •Int main ()
- •Int a[ n ]; // ошибка, размер нельзя вычислить во время компиляции
- •Delete[] p;
- •Int main ()
- •Проблема фиксированного размера массивов
- •Int main ()
- •Int main ()
- •Int main ()
- •Динамически растущие массивы (векторы)
- •Int main ()
- •Struct IntegerVector
- •Int * m_pData;
- •Int m_nUsed;
- •Int m_nAllocated;
- •Void IntegerVectorDestroy ( IntegerVector & _vector )
- •Int main ()
- •#Ifndef _integer_vector_hpp_
- •#Include "integer_vector.Hpp"
- •Void IntegerVectorRead ( IntegerVector & _vector, std::istream & _stream );
- •Ста: Лекция №2 - Связные списки.
- •Всегда ли хорош вектор?
- •Связные списки
- •Ста: Лекция №3 - Реализация и использование простейших атд
- •Абстрактные типы данных (атд)
- •Атд “Список” ( “Последовательность” )
- •Атд “Стек”
- •Атд “Очередь”
- •Ста: Лекция №7 - Деревья
- •Основные сведения о деревьях
- •Обход деревьев
- •Атд “Дерево”
- •Типичные структуры данных для n-арных деревьев
- •1. Массив меток и массив родительских индексов.
- •2. Массив меток и заголовок со списками дочерних узлов.
- •3. Динамическая структура с указателями
- •В результате ее выполнения в динамической памяти формируется структура объектов, в существенной степени напоминающая оригинальный пример из описания понятия деревьев:
- •Бинарные деревья
- •Глава 3 “Элементарные структуры данных”
- •Глава 4 “Абстрактные типы данных”
- •Глава 10 “Элементарные структуры данных” (подразделы 10.1-10.3)
- •Глава 2 “Основные абстрактные типы данных” (подразделы 2.1-2.4)
- •Глава 6 “Элементарные методы сортировки”.
- •Глава 5 “Рекурсия и деревья”.
Ста: Лекция №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;
} }
Поскольку производительность каждой операции может различаться между реализациями (в частности, операция удаления элемента из списка в произвольной позиции), из второго примера очевидно, что быстродействие алгоритма существенно зависит от свойств выбранной в качестве основы структуры данных