
- •Структуры и алгоритмы обработки данных: план курса
- •Лабораторные работы
- •Литература
- •Ста: Лекция №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 “Рекурсия и деревья”.
Атд “Стек”
АТД “Стек” представляет собой набор данных, доступ, вставка и удаление элементов из которого может происходить только с конца. Стек организовывается по принципу LIFO (Last In - First Out). Чаще всего стек сравнивают со стопкой тарелок - тарелка сверху, которую поставили последней, будет использоваться первой, а тарелка на дне стопки, которую поставили первой, будет доступна только тогда, когда снимут все остальные тарелки. В литературе стек иногда называют магазином, что соответствует аналогии с патронами для огнестрельного оружия - первый выстрел использует патрон, помещенный в магазин последним.
При неформальном анализе стек принято изображать в вертикальной форме. При этом первое помещенное значение попадает на дно стека, а последнее находится на вершине стека. При добавлении очередного значения вершина растет. При удалении - уменьшается. В пустом стеке вершина и дно совпадают.
Максимальный размер стека, т.е. число элементов, которые можно в нем разместить, может быть ограниченным или неограниченным. Если размер ограничен, удобнее всего реализовывать стек при помощи простейшего массива. Если размер неограничен, стек не трудно выразить через уже известные структуры данных - вектора и списки.
Стеки предполагают наличие следующего набора абстрактных операций:
CLEAR( stack ) - делает стек пустым;
IS_EMPTY( stack ) : bool - определяет является ли стек пустым;
PUSH ( stack, value ) - помещает новое значение на вершину стека;
POP ( stack ) - удаляет значение с вершины стека;
TOP ( stack ) : value - возвращает значение на вершине стека;
Если стек имеет ограниченный размер, к перечисленному выше набору операций добавляется:
IS_FULL( stack ) : bool - определяет является ли стек полностью заполненным
Иногда в литературе можно встретить определение операции POP, которое помимо удаления значения с вершины стека также возвращает удаленное только что значение. Такой стиль не рекомендуется, поскольку он нарушает принцип разделения всех операций на команды и запросы.
Ниже приведен заголовочный файл, отражающий операции со стеком в виде объявлений функций. Отметим, что структура объявляется лишь предварительно, а ее содержимое в данный момент не указывается. Это способствует улучшению модульности программ, поскольку представляется возможным создавать программный код, использующий стек, не задумываясь ежеминутно о принципах его реализации. Такой подход также позволяет полностью скрыть внутреннюю структуру реализации от случайного разрушения при прямых манипуляциях с полями в клиентском коде, и позволяет ограничить интерфейс к структуре только предоставленными открытыми операциями. Сама структура, за которой стоит реальная реализация, используется в клиентском коде исключительно как описатель (handle). Описатели предоставляются при создании объекта и в дальнейшем используются в операциях для его идентификации, т.е., чтобы отличить один объект от другого. По такому принципу устроено большое количество существующих интерфейсов прикладного программирования (API), выкристаллизовавшихся до начала массовой популяризации принципов объектно-ориентированного программирования (например, Windows API, изучаемый в курсе системного программирования).
integer_stack.hpp
#ifndef _INTEGER_STACK_HPP_
#define _INTEGER_STACK_HPP_
struct IntegerStack;
IntegerStack * IntegerStackCreate ();
void IntegerStackDestroy ( IntegerStack * _pStack );
void IntegerStackClear ( IntegerStack & _stack );
bool IntegerStackIsEmpty ( const IntegerStack & _stack );
bool IntegerStackIsFull ( const IntegerStack & _stack );
void IntegerStackPush ( IntegerStack & _stack, int _value );
void IntegerStackPop ( IntegerStack & _stack );
int IntegerStackTop ( const IntegerStack & _stack );
#endif // _INTEGER_STACK_HPP_
На основе приведенных выше определений представляется возможным написать простую программу, которая проверяет корректность его работы. Отметим, что такая программа успешно компилируется, однако не может быть скомпонована до появления одной из реализаций:
#include "integer_stack.hpp"
#include <cassert>
int main ()
{
// Создаем новый стек, получаем описатель
IntegerStack * pStack = IntegerStackCreate();
// Помещаем 3 значения в стек
IntegerStackPush( * pStack, 1 );
IntegerStackPush( * pStack, 2 );
IntegerStackPush( * pStack, 3 );
// Убеждаемся, что на вершине стека значение 3
assert( IntegerStackTop( * pStack ) == 3 );
// Удалаем два значения из стека
IntegerStackPop( * pStack );
IntegerStackPop( * pStack );
// Ожидаем, что на вершине останется значение 1
assert( IntegerStackTop( * pStack ) == 1 );
// Удаляем последнее значение из стека
IntegerStackPop( * pStack );
// Стек должен быть пуст
assert( IntegerStackIsEmpty( * pStack ) );
// Уничтожаем объект-стек
IntegerStackDestroy( pStack );
}
Реализация стека на основе вектора или связного списка тривиальна, поскольку реализация всех операций сводится к ограничению набора существующих операций, нежели к созданию принципиально новой функциональности. Обе реализации можно подключать к проекту в паре с одним и тем же заголовочным файлом (только не одновременно - это вызовет ошибки компоновки из-за дупликации функций с одинаковыми именами).
Ниже приведена реализация стека через вектор:
integer_stack_vector_impl.cpp
#include "integer_stack.hpp"
#include "integer_vector.hpp"
struct IntegerStack
{
// Реализуем стек через вектор
IntegerVector m_Vector;
};
IntegerStack * IntegerStackCreate ()
{
// Создаем объект-стек в динамической памяти, // т.к. только здесь известен настоящий тип
IntegerStack * pStack = new IntegerStack;
// Инициализируем внутренний объект-вектор
IntegerVectorInit( pStack->m_Vector );
// Возвращаем указатель на реальный стек, // он будет использоваться в клиентском коде как описатель
return pStack;
}
void IntegerStackDestroy ( IntegerStack * _pStack )
{
// Уничтожаем внутренний объект-вектор
IntegerVectorDestroy( _pStack->m_Vector );
// Уничтожаем объект-стек, т.к. только мы знаем его настоящий тип
delete _pStack;
}
void IntegerStackClear ( IntegerStack & _stack )
{
// Сбрасываем счетчик используемых ячеек вектора
_stack.m_Vector.m_nUsed = 0;
}
bool IntegerStackIsEmpty ( const IntegerStack & _stack )
{
// Стек пуст, когда пуст вектор
return ! _stack.m_Vector.m_nUsed;
}
bool IntegerStackIsFull ( const IntegerStack & _stack )
{
// Такой стек в теории не переполняется никогда!
return false; }
void IntegerStackPush ( IntegerStack & _stack, int _value )
{
// Поместить в стек = Поместить в конец внутреннего вектора
IntegerVectorPushBack( _stack.m_Vector, _value );
}
void IntegerStackPop ( IntegerStack & _stack )
{
// Удалить с вершины стека = Удалить с конца внутреннего вектора
IntegerVectorPopBack( _stack.m_Vector );
}
int IntegerStackTop ( const IntegerStack & _stack )
{
// Вершина стека - последняя позиция в векторе
return _stack.m_Vector.m_pData[ _stack.m_Vector.m_nUsed - 1 ];
}
Далее следует альтернативная реалиация на основе связных списков:
integer_stack_list_impl.cpp
#include "integer_stack.hpp"
#include "integer_list.hpp"
struct IntegerStack
{
// Реализуем стек через связный список
IntegerList m_List;
};
IntegerStack * IntegerStackCreate ()
{
// Создаем объект-стек в динамической памяти, // т.к. только здесь известен настоящий тип
IntegerStack * pStack = new IntegerStack;
// Инициализируем внутренний объект-список
IntegerListInit( pStack->m_List );
// Возвращаем указатель на реальный стек, // он будет использоваться в клиентском коде как описатель
return pStack;
}
void IntegerStackDestroy ( IntegerStack * _pStack )
{
// Уничтожаем внутренний объект-список
IntegerListDestroy( _pStack->m_List );
// Уничтожаем объект-стек, т.к. только мы знаем его настоящий тип
delete _pStack;
}
void IntegerStackClear ( IntegerStack & _stack )
{
// Для списка очистка равносильна уничтожению
IntegerListDestroy( _stack.m_List );
}
bool IntegerStackIsEmpty ( const IntegerStack & _stack )
{
// Стек пуст, когда пуст список
return IntegerListIsEmpty( _stack.m_List );
}
bool IntegerStackIsFull ( const IntegerStack & _stack )
{
// Такой стек в теории не переполняется никогда!
return false; }
void IntegerStackPush ( IntegerStack & _stack, int _value )
{
// Поместить в стек = Поместить в конец внутреннего списка
IntegerListPushBack( _stack.m_List, _value );
}
void IntegerStackPop ( IntegerStack & _stack )
{
// Удалить с вершины стека = Удалить с конца внутреннего списка
IntegerListPopBack( _stack.m_List );
}
int IntegerStackTop ( const IntegerStack & _stack )
{
// Вершина стека - последний узел списка
return _stack.m_List.m_pLast->m_value;
}
Приведенная выше тестовая программа будет функционировать одинаково как при первой, так и при второй реализации стека. Могут отличаться лишь показатели производительности. Например, вызывает сомнения реализация операции POP на односвязных списках, поскольку это потребует просматривать список с первого до последнего узла. Если делать выбор при реализации стека в пользу связных списков, следует предпочесть двусвязный вариант.
Иногда задача предполагает совершенно четкое ограничение вернего размера стека, и в таком случае использование динамических структур данных может быть не оправдано. Реализация на основе простейшего однажды выделяемого массива будет наиболее эффективной.
Основное отличие от предыдущих реализаций состоит в том, что размер стека нужно передавать в момент его создания. Это повлияет на разработанную тестовую программу необходимостью указать конкретный размер в вызове IntegerStackCreate. Соответственно, в заголовочный файл следует дополнительно внести следующий прототип:
IntegerStack * IntegerStackCreate ( int _fixedSize );
Также, при помещении очередного значения в стек, потребуется проверка наличия сводобного места, т.е. защита от переполнения (stack overflow).
Ниже представлена реализация операций стека на основе массива фиксированного размера:
integer_stack_array_impl.cpp
#include "integer_stack.hpp"
#include <cassert>
struct IntegerStack
{
// Указатель на начало блока данных
int * m_pData;
// Вершина стека - указывает на ячейку в блоке данных для следующей записи
int * m_pTop;
// Общий размер стека
int m_Size;
};
IntegerStack * IntegerStackCreate ( int _fixedSize )
{
// Создаем объект-стек в динамической памяти, // т.к. только здесь известен настоящий тип
IntegerStack * pStack = new IntegerStack;
// Заполняем поля:
// - блок для хранения данных выделяем динамически (m_pData)
// - изначальное положение вершины - нулевая позиция (m_pTop)
// - запоминаем размер выделенного блока
pStack->m_pData = new int[ _fixedSize ];
pStack->m_pTop = pStack->m_pData;
pStack->m_Size = _fixedSize;
// Возвращаем указатель на реальный стек, // он будет использоваться в клиентском коде как описатель
return pStack;
}
void IntegerStackDestroy ( IntegerStack * _pStack )
{
// Уничтожаем внутренний блок данных
delete[] _pStack->m_pData;
// Уничтожаем объект-стек, т.к. только мы знаем его настоящий тип
delete _pStack;
}
void IntegerStackClear ( IntegerStack & _stack )
{
// Сбрасываем указатель на вершину в начальное состояние
_stack.m_pTop = _stack.m_pData;
}
void IntegerStackPush ( IntegerStack & _stack, int _value )
{
// В стеке должно быть достаточно места
assert( ! IntegerStackIsFull( _stack ) );
// Записываем новое данное в вершину
* _stack.m_pTop = _value;
// Повышаем указатель на вершину на 1 ячейку
++ _stack.m_pTop;
}
void IntegerStackPop ( IntegerStack & _stack )
{
// В стеке должно быть хотя бы одно данное
assert( ! IntegerStackIsEmpty( _stack ) );
// Опускаем указатель вершину на одну ячейку
-- _stack.m_pTop;
}
int IntegerStackTop ( const IntegerStack & _stack )
{
// В стеке должно быть хотя бы одно данное
assert( ! IntegerStackIsEmpty( _stack ) );
// Считываем значение, находящееся под указателем-вершиной
return * ( _stack.m_pTop - 1 );
}
bool IntegerStackIsEmpty ( const IntegerStack & _stack )
{
// Стек пуст, когда указатели на начало блока и вершину совпадают
return _stack.m_pData == _stack.m_pTop;
}
bool IntegerStackIsFull ( const IntegerStack & _stack )
{
// Стек полон, когда расстояние между вершиной и началом блока равно размеру стека
return ( _stack.m_pTop - _stack.m_pData ) == _stack.m_Size;
}