Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СТА (лекции+лабы) / Структуры и алгоритмы обработки данных.docx
Скачиваний:
97
Добавлен:
16.03.2016
Размер:
1.9 Mб
Скачать

Ста: Лекция №2 - Связные списки.

Версия 2.0, 4 сентября 2013г.

(С) 2012-2013, Зайченко Сергей Александрович, к.т.н, ХНУРЭ, доцент кафедры АПВТ

Всегда ли хорош вектор?

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

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

#include "integer_vector.hpp"

int main ()

{

   // Создаем и инициализируем вектор

   IntegerVector v;

   IntegerVectorInit( v );

   // Вводим числа до ввода 0

   std::cout << "Input integers, stop with zero: ";

   IntegerVectorReadTillZero( v, std::cin );

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

   std::cout << "Input position to delete: ";

   int position2Delete;

   std::cin >> position2Delete;

   // Проверяем корректность введенной позиции

   if ( position2Delete < 0 || position2Delete >= v.m_nUsed )

 // Сообщаем о некорректной позиции

       std::cout << "Error: invalid position specified." << std::endl;

   else

   {

 // Удаляем элемент вектора

       IntegerVectorDeleteAt( v, position2Delete );

   

       // Печатаем результирующую последовательность

       std::cout << "Result: ";

       IntegerVectorPrint( v, std::cout );

       std::cout << std::endl;

   }

   // Освобождаем память вектора

   IntegerVectorDestroy( v );

}

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

void IntegerVectorReadTillZero ( IntegerVector & _vector, std::istream & _stream );

void IntegerVectorPrint ( const IntegerVector & _vector,                          std::ostream & _stream,                          char _sep = ' ' );

void IntegerVectorDeleteAt ( IntegerVector & _vector, int _positionAt );

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

void IntegerVectorReadTillZero ( IntegerVector & _vector, std::istream & _stream )

{

   // Вводим числа до достижения условия выхода

   while ( true )

   {

       // Попытка ввода очередного числа

       int temp;

       _stream >> temp;

       // Если поток в корректном состоянии и введено число, отличное от 0,        // добавляем новые данные в конец  вектора

       if ( _stream && temp != 0 )

           IntegerVectorPushBack( _vector, temp );

       else

     // Иначе - конец ввода

           break;

   }

}

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

void IntegerVectorPrint ( const IntegerVector & _vector,                          std::ostream & _stream,                          char _sep )

{

   for ( int i = 0; i < _vector.m_nUsed; i++ )

       _stream << _vector.m_pData[ i ] << _sep;

}

Отметим, что в данной функции, в отличие от других, передается ссылка на объект-вектор с модификатором const, запрещающим модификацию внутренних переменных структуры. Так следует поступать каждый раз, когда функция не намеревается изменять состояние переданного объекта, а лишь воспользуется его данными без изменений. Во-первых, это позволит избежать ошибок, связанных со случайной непреднамеренной модификацией данных в результате ошибки программиста (например, случайно записали оператор присвоения “=” вместо оператора сравнения “==”). Во-вторых, такую функцию будет можно использовать на константных данных. В-третьих, константность ссылки-аргумента подчеркивает намерение функции не притрагиваться к содержимому объекта, что облегчает понимание поведения функции в использующем ее коде. Наконец, компилятор, зная, что объект не будет модифицирован внутри функции, может применить более эффективные способы оптимизации кода.

Третья функция выполняет удаление элемента в указанной позиции из вектора, и представляет для обсуждаемой в данной лекции темы наибольший интерес:

void IntegerVectorDeleteAt ( IntegerVector & _vector, int _position )

{

   // Проверяем корректность позиции в переданном векторе

   assert( _position >= 0 && _position < _vector.m_nUsed );

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

   for ( int i = _position + 1; i < _vector.m_nUsed; i++ )

       _vector.m_pData[ i - 1 ] = _vector.m_pData[ i ];

   // Уменьшаем счетчик занятых элементов

   -- _vector.m_nUsed;

}

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

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

Задача внутренней диагностики совсем другая - выявление некорректного использования кода со стороны программиста на этапе разработки и отладки программы. Речь о восстановлении работоспособности не идет, а наоборот, желательным является заметный однозначный сигнал о нарушении некоторого предположения. В такой ситуации, чем проблема виднее, тем лучше.

В частности, в рассмотренном выше примере проверка корректности позиции в векторе выполнялась дважды. Сначала в функции main осуществлялась обработка ошибок, и в случае ее обнаружения на консоли выдавалось сообщение об ошибке для внешнего пользователя программы. Внутри же вспомогательной функции была размещена диагностическая проверка при помощи макроса assert, созданная для разработчика.

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

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

   for ( int i = _position + 1; i < _vector.m_nUsed; i++ )

       _vector.m_pData[ i - 1 ] = _vector.m_pData[ i ];

Возникает вопрос - эффективно ли данное решение с точки зрения производительности? Ответ на этот вопрос зависит от конкретной указанной позиции.

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

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

#include "integer_vector.hpp"

int main ()

{

   IntegerVector v;

   IntegerVectorInit( v );

   std::cout << "Input integers, stop with zero: ";

   IntegerVectorReadTillZero( v, std::cin );

   std::cout << "Input position to insert: ";

   int position2Insert;

   std::cin >> position2Insert;

   if ( position2Insert < 0 || position2Insert >= v.m_nUsed )

       std::cout << "Error: invalid position specified." << std::endl;

   else

   {

       std::cout << "Input value to insert: ";

       int value2Insert;

       std::cin >> value2Insert;

       IntegerVectorInsertAt( v, position2Insert, value2Insert );

       

       std::cout << "Result: ";

       IntegerVectorPrint( v, std::cout );

       std::cout << std::endl;

   }

   IntegerVectorDestroy( v );

}

В заголовочный файл потребуется добавить объявление очередной полезной функции:

void IntegerVectorInsertAt ( IntegerVector & _vector, int _position, int _data );

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

void IntegerVectorGrow ( IntegerVector & _vector )

{

   int nAllocatedNew = _vector.m_nAllocated * 2;

   int * pNewData = new int[ nAllocatedNew ];

   memcpy( pNewData, _vector.m_pData, sizeof( int ) * _vector.m_nAllocated );

   delete[] _vector.m_pData;

   _vector.m_pData = pNewData;

   _vector.m_nAllocated = nAllocatedNew;

}

Соответственно, прежняя реализация функции добавления числа в конец вектора упрощается:

void IntegerVectorPushBack ( IntegerVector & _vector, int _data )

{

   // Если места в векторе для нового числа больше нет, следует вырасти

   if ( _vector.m_nUsed == _vector.m_nAllocated )

       IntegerVectorGrow( _vector );

   // Дописываем новый элемент в конец вектора

   _vector.m_pData[ _vector.m_nUsed++ ] = _data;

}

Наконец, можно приступать к реализации функции вставки числа в произвольную позицию:

void IntegerVectorInsertAt ( IntegerVector & _vector, int _position, int _data )

{

   // Проверяем корректность позиции в переданном векторе

   assert( _position >= 0 && _position < _vector.m_nUsed );

   // Определяем необходимость в росте вектора

   int newUsed = _vector.m_nUsed + 1;

   if ( newUsed > _vector.m_nAllocated )

       IntegerVectorGrow( _vector );

   // Перемещаем существующие элементы правее на 1 позицию, начиная с интересующей

   for ( int i = _vector.m_nUsed; i > _position; i-- )

       _vector.m_pData[ i ] = _vector.m_pData[ i - 1];

   // Вставляем новое данное в интересующую позицию

   _vector.m_pData[ _position ] = _data;

   // Изменяем счетчик занятых элементов

   _vector.m_nUsed = newUsed;

}

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

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