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

Атд “Очередь”

АТД “Очередь” (queue) - это набор данных, организованный таким образом, что вставка нового элемента производится только с конечной ячейки, а удаление - только с начальной. Очередь работает по принципу FIFO (First In - First Out), что соответствует общепринятому понятию очереди в жизни, например, возле кассы в супермаркете.

Также как и стек, очередь может быть ограниченного и неограниченного размера в зависимости от потребностей решаемой задачи.

Очередь предполагает наличие следующих абстрактных операций:

  • CLEAR( queue ) - делает очередь пустой;

  • IS_EMPTY( queue ) : bool - определяет является ли очередь пустой;

  • IS_FULL( queue ) : bool - определяет является ли очередь полностью заполненной (что имеет смысл только для очередей ограниченного размера);

  • PUSH ( queue , value ) - помещает новое значение в конец очереди;

  • POP ( queue ) - удаляет значение с начала очереди;

  • FRONT ( queue ) : value - возвращает значение в начале очереди.

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

integer_queue.hpp

#ifndef _INTEGER_QUEUE_HPP_

#define _INTEGER_QUEUE_HPP_

struct IntegerQueue;

IntegerQueue * IntegerQueueCreate ();

void IntegerQueueDestroy ( IntegerQueue * _pQueue );

void IntegerQueueClear ( IntegerQueue & _queue );

bool IntegerQueueIsEmpty ( const IntegerQueue & _queue );

bool IntegerQueueIsFull ( const IntegerQueue & _queue );

void IntegerQueuePush ( IntegerQueue & _queue, int _value );

void IntegerQueuePop ( IntegerQueue & _queue );

int IntegerQueueFront ( const IntegerQueue & _queue );

#endif // _INTEGER_QUEUE_HPP_

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

integer_queue_list_impl.cpp

#include "integer_queue.hpp"

#include "integer_list.hpp"

#include <cassert>

struct IntegerQueue

{

// Реализуем очередь через связный список

IntegerList m_List;

};

IntegerQueue * IntegerQueueCreate ()

{

// Создаем объект-очередь динамической памяти, // т.к. только здесь известен настоящий тип

IntegerQueue * pNewQueue = new IntegerQueue;

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

IntegerListInit( pNewQueue->m_List );

// Возвращаем указатель на реальную очередь, // он будет использоваться в клиентском коде как описатель

return pNewQueue;

}

void IntegerQueueDestroy ( IntegerQueue * _pQueue )

{

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

IntegerListDestroy( _pQueue->m_List );

// Уничтожаем объект-очередь, т.к. только мы знаем его настоящий тип

delete _pQueue;

}

void IntegerQueueClear ( IntegerQueue & _queue )

{

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

IntegerListDestroy( _queue.m_List );

}

bool IntegerQueueIsEmpty ( const IntegerQueue & _queue )

{

// Очередь пуста, когда пуст внутренний список

return IntegerListIsEmpty( _queue.m_List );

}

bool IntegerQueueIsFull ( const IntegerQueue & _queue )

{

// Такая очередь в теории никогда не переполнится

return false;

}

void IntegerQueuePush ( IntegerQueue & _queue, int _value )

{

// Помещение элемента в очередь = добавление элемента в конец списка

IntegerListPushBack( _queue.m_List, _value );

}

void IntegerQueuePop ( IntegerQueue & _queue )

{

// Удаление элемента из очереди = удаление элемента с начала списка

assert( ! IntegerQueueIsEmpty( _queue ) );

IntegerListPopFront( _queue.m_List );

}

int IntegerQueueFront ( const IntegerQueue & _queue )

{

// Начало очереди в начале списка

assert( ! IntegerQueueIsEmpty( _queue ) );

return _queue.m_List.m_pFirst->m_value;

}

Используя реализованную функциональность, решим следующую задачу: пользователь вводит последовательность целых чисел, а программа, начиная с 3-го числа, дублирует ввод с отставанием на 2 элемента. Т.е., при вводе последовательности “1 2 3 4 5” программа должна вывести поэлементно последовательность “1 2 3”, начиная с момента ввода числа “3”.

Ниже приведен исходный код данной программы:

#include "integer_queue.hpp"

#include <iostream>

int main ()

{

IntegerQueue * pQueue = IntegerQueueCreate();

int delayCounter = 2;

while ( std::cin )

{

std::cout << "Input a number: ";

int temp;

std::cin >> temp;

if ( std::cin )

{

IntegerQueuePush( * pQueue, temp );

if ( delayCounter > 0 )

-- delayCounter;

else

{

std::cout << "Queued: " << IntegerQueueFront( * pQueue ) << std::endl;

IntegerQueuePop( * pQueue );

}

}

else

break;

}

IntegerQueueDestroy( pQueue );

}

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

Количество хранимых элементов можно определить по разнице индексов, при этом следует учесть возможное “перехлестывание”. Однако, чтобы отличить пустую очередь от очереди, заполненной на 100%, следует зарезервировать дополнительную ячейку. Т.е., когда требуется очередь, скажем, из 5 элементов, следует выделить блок из 6 ячеек. В результате, когда очередь будет полна, должна быть сводобна ровно 1 ячейка.

Ниже приведены примеры состояния циклического массива в результате воздействия. В пустой очереди индексы начала и конца совпадут:

После добавления элемента в очередь индекс конца будет двигаться, а начала - стоять на месте:

Элементы можно будет добавлять до достижения максимально разрешенного числа хранимых ячеек, что соответствует размеру выделенного блока минус 1:

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

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

Ситуация может вернуться в обратную сторону, если вновь изъять несколько элементов:

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

integer_queue_cyclic_array_impl.cpp

#include "integer_queue.hpp"

#include <cassert>

struct IntegerQueue

{

int * m_pData;

int m_Size;

int m_FrontIndex;

int m_BackIndex;

};

IntegerQueue * IntegerQueueCreate ( int _fixedSize )

{

assert( _fixedSize > 0 );

IntegerQueue * pNewQueue = new IntegerQueue;

pNewQueue->m_Size = _fixedSize + 1;

pNewQueue->m_pData = new int[ pNewQueue->m_Size ];

IntegerQueueClear( * pNewQueue );

return pNewQueue;

}

void IntegerQueueDestroy ( IntegerQueue * _pQueue )

{

delete[] _pQueue->m_pData;

delete _pQueue;

}

void IntegerQueueClear ( IntegerQueue & _queue )

{

_queue.m_FrontIndex = _queue.m_BackIndex = 0;

}

int IntegerQueueSize ( const IntegerQueue & _queue )

{

// |-|-|-|-|-|-| |-|-|-|-|-|-|

// F B B F

return ( _queue.m_FrontIndex <= _queue.m_BackIndex ) ?

_queue.m_BackIndex - _queue.m_FrontIndex :

_queue.m_BackIndex + _queue.m_Size - _queue.m_FrontIndex;

}

bool IntegerQueueIsEmpty ( const IntegerQueue & _queue )

{

return IntegerQueueSize( _queue ) == 0;

}

bool IntegerQueueIsFull ( const IntegerQueue & _queue )

{

return IntegerQueueSize( _queue ) == ( _queue.m_Size - 1 );

}

int IntegerQueueNextIndex ( const IntegerQueue & _queue, int _index )

{

int index = _index + 1;

if ( index == _queue.m_Size )

index = 0;

return index;

}

void IntegerQueuePush ( IntegerQueue & _queue, int _value )

{

assert( ! IntegerQueueIsFull( _queue ) );

_queue.m_pData[ _queue.m_BackIndex ] = _value;

_queue.m_BackIndex = IntegerQueueNextIndex( _queue, _queue.m_BackIndex );

}

void IntegerQueuePop ( IntegerQueue & _queue )

{

assert( ! IntegerQueueIsEmpty( _queue ) );

_queue.m_FrontIndex = IntegerQueueNextIndex( _queue, _queue.m_FrontIndex );

}

int IntegerQueueFront ( const IntegerQueue & _queue )

{

assert( ! IntegerQueueIsEmpty( _queue ) );

return _queue.m_pData[ _queue.m_FrontIndex ];

}

Выводы

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

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