СТА (лекции+лабы) / СТА Лекция 2
.docx
void IntegerListPopBack ( IntegerList & _list )
{
// Предполагаем, что список не пустой
assert( ! IntegerListIsEmpty( _list ) );
// Обрабатываем случай удаления единственного узла
IntegerList::Node * pLast = _list.m_pLast;
if ( _list.m_pFirst == _list.m_pLast )
_list.m_pFirst = _list.m_pLast = nullptr;
else
{
// Иначе, находим предпоследний узел. Увы, только просмотрев весь список.
IntegerList::Node * pCurrent = _list.m_pFirst;
while ( pCurrent->m_pNext != _list.m_pLast )
pCurrent = pCurrent->m_pNext;
// Назначаем последним узлом предпоследний
_list.m_pLast = pCurrent;
// Разрывам связь между предпоследним и последним узлами
pCurrent->m_pNext = nullptr;
}
// Последний узел больше не нужен
delete pLast;
}
Так выглядит удаление после произвольной позиции:
void IntegerListDeleteAfter ( IntegerList & _list, IntegerList::Node * _pPrevNode )
{
// Предполагаем, что список не пустой
assert( ! IntegerListIsEmpty( _list ) );
// Предполагаем, что указан не последний элемент
assert( _list.m_pLast != _pPrevNode );
// Связываем предыдущий узел с узлом, следующим за удаляемым
IntegerList::Node * pDyingNode = _pPrevNode->m_pNext;
_pPrevNode->m_pNext = pDyingNode->m_pNext;
// Обрабатываем случай, когда удаляется последний узел списка
if ( _list.m_pLast == pDyingNode )
_list.m_pLast = _pPrevNode;
// Удалаемый узел больше не нужен
delete pDyingNode;
}
Наконец, приведем реализацию удаления до произвольной позиции :
void IntegerListDeleteBefore ( IntegerList & _list, IntegerList::Node * _pNextNode )
{
// Предполагаем, что список не пустой
assert( ! IntegerListIsEmpty( _list ) );
// Предполагаем, что указан не первый элемент
assert( _list.m_pFirst != _pNextNode );
// Обрабатываем случай удаления первого узла списка
IntegerList::Node * pPrevNode = _list.m_pFirst,
* pCurrentNode = _list.m_pFirst->m_pNext;
if ( pCurrentNode == _pNextNode )
{
delete _list.m_pFirst;
_list.m_pFirst = _pNextNode;
}
else
{
// Ищем узел, предшествующий удаляемому
while ( pCurrentNode->m_pNext != _pNextNode )
{
pPrevNode = pCurrentNode;
pCurrentNode = pCurrentNode->m_pNext;
}
// Связываем левого и правого соседа между собой
pPrevNode->m_pNext = _pNextNode;
// Удаляемый узел больше не нужен
delete pCurrentNode;
}
}
Функции ввода-вывода реализуются полностью аналогично векторам:
void IntegerListRead ( IntegerList & _list, std::istream & _stream )
{
while ( true )
{
int temp;
_stream >> temp;
if ( _stream )
IntegerListPushBack( _list, temp );
else
break;
}
}
void IntegerListReadTillZero ( IntegerList & _list, std::istream & _stream )
{
while ( true )
{
int temp;
_stream >> temp;
if ( _stream && temp != 0 )
IntegerListPushBack( _list, temp );
else
break;
}
}
void IntegerListPrint ( const IntegerList & _list, std::ostream & _stream, char _sep )
{
const IntegerList::Node * pCurrent = _list.m_pFirst;
while ( pCurrent )
{
_stream << pCurrent->m_value << _sep;
pCurrent = pCurrent->m_pNext;
}
}
Пример использования связных списков
В качестве удачного примера использования можно привести программу, в которой пользователь вводит последовательность чисел, программа удаляет первый элемент и выводит обновленную последовательность на экран. Воспользуемся реализованной выше функциональностью.
#include "integer_list.hpp"
int main ()
{
// Создаем и иницлиазируем связный список
IntegerList l;
IntegerListInit( l );
// Вводим числа в список
std::cout << "Input integers, stop with Ctrl+Z:";
IntegerListRead( l, std::cin );
// Удаляем первый элемент из списка
IntegerListPopFront( l );
// Выводим текущее состояние списка
std::cout << "Result: ";
IntegerListPrint( l, std::cout );
std::cout << std::endl;
// Уничтожаем список
IntegerListDestroy( l );
}

Решение этой задачи на векторах не будет эффективно в общем случае из-за сдвига данных.
Выбор между векторами и связными списками
Имея в распоряжении все средства для работы со связным списком, можем переделать рассмотренную в начале лекции программу об удалении элемента в заданной позиции, заменив вектора списками:
#include "integer_list.hpp"
// Функция сообщения о некорректной позиции
void reportPositionError ()
{
std::cout << "Error: invalid position specified." << std::endl;
}
// Функция распечатки результата программы
void printResult ( const IntegerList & _l )
{
std::cout << "Result: ";
IntegerListPrint( _l, std::cout );
std::cout << std::endl;
}
int main ()
{
// Создаем и инициализируем список
IntegerList l;
IntegerListInit( l );
// Вводим числа до ввода 0
std::cout << "Input integers, stop with zero: ";
IntegerListReadTillZero( l, std::cin );
// Вводим позицию, которую требуется удалить
std::cout << "Input position to delete: ";
int position2Delete;
std::cin >> position2Delete;
// Проверяем позицию на некорректность (нижняя граница)
if ( position2Delete < 0 || IntegerListIsEmpty( l ) )
reportPositionError();
// Специальный случай - удаление первого элемента
else if ( position2Delete == 0 )
{
IntegerListPopFront( l );
printResult( l );
}
else
{
// Общий случай. Находим узел-предшественник интересующей позиции
int currentIndex = 0;
IntegerList::Node * pCurrentNode = l.m_pFirst;
while ( pCurrentNode && ( currentIndex + 1 ) < position2Delete )
{
pCurrentNode = pCurrentNode->m_pNext;
++currentIndex;
}
// Проверяем корректность введенной позиции (верхняя граница)
if ( ! pCurrentNode || l.m_pLast == pCurrentNode )
reportPositionError();
else
{
// Удаляем элемент списка
IntegerListDeleteAfter( l, pCurrentNode );
// Печатаем результирующую последовательность
printResult( l );
}
}
// Освобождаем память списка
IntegerListDestroy( l );
}
Непосредственная стоимость удаления конкретного известного узла из связного списка не является такой дорогой, как в векторе. Однако, как видно из решения, списки обладают и существенными недостатками, не свойственными векторам. В частности, быстро найти элемент списка по порядковому номеру в общем случае не получается, т.к. требуется просматривать список с самого начала, увеличивая переменную-счетчик. Аналогично, определение размера списка также потребует полного прохода от начального до конечного узла. Такие операции при использовании векторов не вызывают ни малейших затруднений.
Еще один существенный минус связных список по сравнению с векторами состоит в нерациональном использовании памяти. Каждый узел выделяется в куче отдельной аллокацией. Узлы являются маленькими структурами, и при большом количестве узлов только лишь за счет “невидимой” служебной доли каждой динамической аллокации будет наблюдаться существенный расход памяти. Кроме того, поскольку время выделения памяти в куче мало зависит от выделяемого объема в байтах, N небольших выделений динамической памяти однозначно работает существенно медленней одного большого выделения памяти.
Утверждение, что вставка/удаление элементов в произвольной позиции в векторах работает медленнее, чем в списках, также может быть опровергнуто при определенных обстоятельствах. Строго говоря, это утверждение может не выполняться для небольших последовательностей (5-7 элементов) на большинстве современных компьютеров. Такая аномалия может быть связана с нюансами работы кэш-памяти. Как известно, элементы векторов соседствуют в физической памяти и с высокой долей вероятности будут считываться в кэш-память при обращении одновременно. Элементы же списков, напротив, скорее всего попадут в совершенно разные области физической памяти и чаще всего не будут находиться в кэш-памяти одновременно при последовательном проходе. В зависимости от свойств аппаратного обеспечения конкретного компьютера, время выполнения сдвига нескольких ячеек, находящихся рядом в более быстрой кэш-памяти может быть меньшим, чем время нескольких обращений к более медленным чипам оперативной памяти.
Учитывая такие неоднозначности, окончательный выбор между векторами и связными списками должен подтверждаться экспериментально. Если выбор не очевиден или не является критичным для задачи, рекомендуется использовать векторы, как более простую структуру.
Выводы
Таким образом, в данной лекции было показано, что структуры данных не являются универсальными для абсолютно всех случаев обработки. На примерах векторов и связных список было показано, что каждой из структур свойственны как эффективные для нее операции, так и менее подходящие, требующие громоздких вычислений. Выбор структуры данных должен осуществляться под конкретную задачу исходя из требуемых операций над данными.
