Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2177 / 0 | Длительность: 63:16:00
Лекция 3:

Элементарные структуры данных

Связные списки

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

Определение 3.2. Связный список - это набор элементов, содержащихся в узлах (node), каждый из которых также содержит ссылку (link) на некоторый узел.

В определении узлов упоминаются ссылки на узлы, поэтому связные списки иногда называют самоссылочыми (self-referent) структурами. Более того, хотя ссылка узла обычно указывает на другой узел, возможны и ссылки на себя, поэтому связные списки могут представлять собой циклические (cyclic) структуры. Последствия этих двух фактов станут ясны при рассмотрении конкретных представлений и применений связных списков.

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

  • Это пустая (null) ссылка, не указывающая на какой-либо узел.
  • Ссылка указывает на фиктивный узел (dummy node), который не содержит элементов.
  • Ссылка указывает на первый узел, что делает список циклическим.

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

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

Связные списки являются примитивными конструкциями в некоторых языках программирования, но не в C++. Однако базовые строительные блоки, о которых шла речь в разделе 3.1, хорошо приспособлены для реализации связных списков. Указатели используются для ссылок, а структуры для узлов:

struct node { Item item; node *next; } ;
typedef node *link;
        

Эта пара выражений - ни что иное, как код C++ для определения 3.2. Узлы состоят из элементов (здесь типа Item) и указателей на узлы, которые называются также ссылками. В "Абстрактные типы данных" будут представлены более сложные случаи, которые обеспечивают большую гибкость и более эффективную реализацию определенных операций, но этот простой пример достаточен для понимания основ обработки списков. Подобные соглашения для связных структур будут использоваться на протяжении всей книги.

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

link x = new node;
        

содержится операция new, которая резервирует для узла достаточный объем памяти и возвращает указатель на него в переменной x. В разделе 3.5 будет кратко показано, как система резервирует память, поскольку это хороший пример применения связных списков!

В языке C++ широко принято инициализировать память, а не просто выделять ее. Поэтому в каждую описываемую структуру обычно включается конструктор (constructor). Конструктор представляет собой функцию, которая описывается внутри структуры и имеет такое же имя, что и сама структура. Конструкторы подробно рассматриваются в "Абстрактные типы данных" . Они предназначены для занесения исходных значений в данные структуры. Для этого конструкторы автоматически вызываются при создании экземпляра структуры. Например, если описать узел списка при помощи следующего кода:

struct node
  { Item item; node *next;
    node (Item x; node *t)
      { item = x; next = t; };
  };
  typedef node *link;
        

то оператор

link t = new node(x, t);
        

не только резервирует достаточный для узла объем памяти и возвращает указатель на него в переменной t, но и присваивает полю item узла значение x, а указателю поля - значение t. Конструкторы помогают избегать ошибок, связанных с не инициализированными данными.

Теперь, когда узел списка создан, возникает вопрос: как обращаться к находящейся в нем информации - элементу и ссылке? Мы уже ознакомились с базовыми операциями, необходимыми для выполнения этой задачи: достаточно разыменовать указатель, а затем использовать имена членов структуры. Элемент узла, на который указывает ссылка x (типа Item), имеет вид (*x).item, а ссылка (типа link) - (*x).link. Эти операции так часто используются, что в языке C++ для них предусмотрены эквивалентные сокращения: x->item и x->link. Кроме того, нам так часто будет нужна фраза "узел, на который указывает ссылка x", что мы будем говорить просто "узел x" - ссылка именует узел.

Соответствие между ссылками и указателями C++ имеет большое значение, но следует учитывать, что ссылки являются абстракцией, а указатели - конкретным представлением. Можно разрабатывать алгоритмы, где используются узлы и ссылки, и можно выбрать одну из многочисленных реализаций этой идеи. Например, в конце раздела будет показано, что ссылки можно представлять индексами массивов.

На рис. 3.3 и рис. 3.4 показаны две основные операции, выполняемые со связными списками. Можно удалить любой элемент связного списка, уменьшив его длину на 1; а также вставить элемент в любую позицию списка, увеличив длину на 1. В этих рисунках для простоты предполагается, что списки циклические и никогда не становятся пустыми.

Пустые ссылки, фиктивные узлы и пустые списки будут рассмотрены в разделе 3.4. Как показано на рисунках, и для вставки, и для удаления необходимо лишь два оператора C++. Для удаления узла, следующего за узлом x, используются операторы

t = x->next; x->next = t->next;
        

или проще:

x->next = x->next->next;
        

Для вставки в список узла t в позицию, следующую за узлом x, используется операторы

t->next = x->next; x->next = t;
        
 Удаление из связного списка

Рис. 3.3. Удаление из связного списка

Для удаления из связного списка узла, следующего за заданным узлом x, в t заносится указатель на удаляемый узел, а затем ссылка в x заменяется на t->next. Указатель t может использоваться для обращения к удаленному узлу. Хотя ссылка этого узла по-прежнему указывает куда-то внутрь списка, обычно такая ссылка не используется после удаления узла из списка - за исключением, возможно, сообщения системе с помощью операции delete о том, что задействованная память больше не нужна.

 Вставка в связный список

Рис. 3.4. Вставка в связный список

Для вставки узла t в позицию связного списка, следующую за заданным узлом x (вверху), в t->next заносится значение x->next (в середине), затем в x->next заносится значение t (внизу).

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

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

После удаления узла из связного списка операцией x->next = x->next->next к нему уже невозможно обратиться. Для небольших программ, вроде рассмотренных выше примеров, это не имеет существенного значения, но обычно хорошей практикой программирования считается применение операции delete (противоположной операции new) для любого узла, который уже не нужен. В частности, последовательность операторов

t = x->next; x->next = t->next; delete t;

        

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

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

Программа 3.9. Пример циклического списка (задача Иосифа)

Для представления людей, расставленных в круг, создается циклический связный список, где каждый элемент (человек) содержит ссылку на соседний элемент против часовой стрелки. Целое число i представляет i-го человека в круге. После создания циклического списка из одного узла вставляются узлы от 2 до N, и в результате образуется окружность с узлами от 1 до N. При этом переменная x указывает на узел N. Затем пропускаем M - 1 узлов, начиная с 1-го, и изменяем значение ссылки (M- 1)-го узла так, чтобы пропустить M-ый узел. Продолжаем эту операцию, пока не останется один узел.

#include <iostream.h>
#include <stdlib.h>
struct node
  { int item; node* next;
    node(int x, node* t)
      { item = x; next = t; }
  };
  typedef node *link;
  int main(int argc, char *argv[])
    { int i, N = atoi(argv[1]), M = atoi(argv[2]);
      link t = new node(1, 0); t->next = t;
      link x = t;
      for (i = 2; i <= N; i++)
        x = (x->next = new node(i, t));
      while (x != x->next)
        { for (i = 1; i < M; i++) x = x->next;
          x->next = x->next->next;
        }
      cout << x->item << endl;
    }
        

В качестве примера рассмотрим следующую программу решения задачи Иосифа Флавия - любопытного аналога решета Эратосфена.

Предположим, N человек решили выбрать главаря. Для этого они встали в круг и стали удалять каждого M-го человека, смыкая круг после каждого удаления. Задача состоит в определении, кто останется последним (чтобы потенциальный лидер с математическими способностями смог заранее выбрать нужную позицию в круге).

 Пример задачи Иосифа

Рис. 3.5. Пример задачи Иосифа

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

Номер выбираемого главаря является функцией от N и M, называемой функцией Иосифа. В более общем случае требуется выяснить порядок удаления людей. В примере, показанном на рис. 3.5 для N = 9 и M = 5, люди удаляются в порядке 5 1 7 4 3 6 9 2, а 8-ой номер становится избранным главарем. Программа 3.9 считывает значения N и M, а затем распечатывает эту последовательность.

Для прямой имитации процесса выбора в программе 3.9 используется циклический связный список. Сначала создается список элементов от 1 до N. Для этого создается циклический список с единственным узлом для участника 1, затем вставляются узлы для участников от 2 до N с помощью операции, показанной на рис. 3.4. Затем в списке отсчитывается M - 1 элемент и удаляется следующий за ним при помощи операции, показанной на рис. 3.3. Этот процесс продолжается до тех пор, пока не останется только один узел (который будет указывать на себя).

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

В языке C++ указатели служат прямой и удобной реализацией абстрактной концепции связных списков, но важность абстракции не зависит от конкретной реализации. Например, на рис. 3.6 показана реализация связного списка для задачи Иосифа с помощью массивов целых чисел. То есть связный список можно реализовать с помощью индексов массива вместо указателей. Связные списки применялись задолго до появления конструкций указателей в языках высокого уровня, таких как C++. И даже в современных системах иногда оказываются удобными реализации на основе массивов.

 Представление связного списка с помощью массивов

Рис. 3.6. Представление связного списка с помощью массивов

Здесь показана реализация связного списка для задачи Иосифа (см. рис. 3.5) с помощью индексов массива вместо указателей. Индекс элемента, следующего в списке за элементом с индексом 0, находится в next[0] и т.д. Сначала (три верхних строки) элемент для участника i имеет индекс i-1; циклический список формируется занесением значений i+1 в next[i] для i от 0 до 8, а в next[8] заносится значение 0. Для имитации процесса выбора Иосифа изменяются ссылки (элементы массива next), но элементы участников не перемещаются. Каждая пара строк показывает результат перемещения по списку четырехкратным выполнением x = next[x] с последующим удалением пятого элемента (отображаемого в крайнем левом столбце) путем занесения в элемент next[x] значения next[next[x]].

Упражнения

  • 3.23. Напишите функцию, которая возвращает количество узлов циклического списка по заданному указателю на один из узлов списка.
  • 3.24. Напишите фрагмент кода, который определяет количество узлов в циклическом списке между узлами, указанными двумя данными указателями x и t.
  • 3.25. Напишите фрагмент кода, который по указателям x и t двух раздельных связных списков вставляет список, указываемый t, в список, указываемый x - в позицию после узла x.
  • 3.26. Напишите фрагмент кода, который для данных указателей x и t на узлы циклического списка перемещает узел, следующий после t, в позицию после узла x.
  • 3.27. При построении списка программа 3.9 устанавливает в два раза больше ссылок, чем нужно, поскольку поддерживает цикличность списка после вставки каждого узла. Измените программу таким образом, чтобы циклический список создавался без выполнения этих лишних операций.
  • 3.28. Определите время выполнения программы 3.9 в виде функции от Mи N.
  • 3.29. Используйте программу 3.9, чтобы определить значения функции Иосифа для M = 2, 3, 5, 10 и N = 103, 104, 105 и 106 .
  • 3.30. Используйте программу 3.9, чтобы построить график зависимости функции Иосифа от N для M = 10 и N от 2 до 1000.
  • 3.31. Воспроизведите таблицу на рис. 3.6 для случая, когда элемент i первоначально находится в массиве в позиции N-i.
  • 3.32. Разработайте версию программы 3.9, в которой для реализации связного списка используется массив индексов (см. рис. 3.6).
Бактыгуль Асаинова
Бактыгуль Асаинова

Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат?

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?