TURBO PASCAL |
Новости
|
Ссылки и указатели. Линейные и циклические спискиЦель: изучение фундаментальной абстрактной структуры - связных списков. На сегодняшнем занятии мы рассмотрим фундаментальную абстрактную структуру связанный список. Связанный список - это набор
последовательно организованных данных. Главное преимущество связанных списков перед массивами состоит в том, что они могут уменьшать или увеличивать свои размеры во время выполнения программы. В частности, мы не должны заранее знать его максимальный размер. Второе преимущество состоит в том, что они обеспечивают гибкость при переорганизации их элементов. Такая гибкость получается за счет потери в скорости доступа к произвольному элементу списка. Это станет более очевидным ниже, после того, как мы ознакомимся с некоторыми основными свойствами связанных списков и базовыми операциями определенными над ними. Список можно представить в виде звеньев цепи. В массиве последовательная организация обеспечивается непосредственно (согласно индексу). Для связанного списка используется специальный способ организации данных, согласно которому его элемент содержится в "узле" содержащем данные, а также "ссылку" на следующий "узел". Узел и ссылку можно описать так: Type Для описания списка мы будем использовать дополнительно два вспомогательных узла head и z. Узел head будет указывать на первый элемент списка, а z - последний элемент списка. Head иногда называют "голова" списка, а z - "хвост". Тогда список можно будет представить в следующем виде. Такое представление данных позволяет производить некоторые операции гораздо более эффективными методами, чем над массивами. Например, если мы хотим передвинуть элемент 1 из конца в начало, то для произведения такой операции в массиве нам потребовалось бы передвинуть все его элементы чтобы освободить место. В связанном же списке мы только меняем ссылки. Оба варианта рисунка эквивалентны; они только по разному нарисованы. Мы просто переустанавливаем ссылку узла, содержащего 1, на узел содержащий 2, а ссылку пустого узла head на узел содержащий 1. Даже если бы список был очень большой, то нам все равно потребовалось бы только три перестановки ссылок для подобного обмена. Что более важно, теперь мы можем говорить о "вставке" элемента в связанный список (что увеличивает его длину на один элемент) - операции которая столь неестественна и неудобна на массивах. На рисунке показано как вставить в список новый узел. Нужно вначале добавить этот узел, затем создать ссылку на узел q, а затем изменить ссылку узла p на новый узел. Для этого требуется изменить лишь две ссылки в независимости от длины списка. Аналогично, мы можем вести речь об "удалении" элемента из списка (что уменьшает его размер на один элемент). Нужно просто переставить ссылку на узел, который следует за узлом q и после этого узел содержащий q должен быть уничтожен. С другой стороны, есть и операции для которых связанный список не очень хорошо подходит. Наиболее очевидная из них - это "найти k-й элемент" (найти элемент по его индексу): в массиве это делалось простым обращением типа a[k], но в связанном списке мы должны пройти по k ссылкам. Другой неестественной операцией над связанными списками является операция "найти элемент перед заданным". Если все, что у на есть - это ссылка на элемент q в нашем примере, то единственный способ найти элемент p, который указывает на q - это пройти по всем ссылкам начиная с узла head. На самом деле - эта операция необходима для удаления заданного элемента из списка: как же иначе мы можем переустановить ссылку предыдущего узла ? Во многих задачах, мы можем обойти эту проблему посредством изменения формулировки этой фундаментальной операции на "удалить узел следующий после данного". Аналогичная проблема может быть обойдена и для вставки посредством изменения определения операции на "вставить элемент после заданного узла". Паскаль предоставляет нам некоторые примитивные операции которые позволяют нам непосредственно создавать связанные списки. Следующий фрагмент кода - это пример реализации основных функций, которые мы только что обсуждали. Type Давайте рассмотрим их по подробнее. Link = ^Node; - здесь создается новый тип Link, который является типизированным указателем на тип Node. Тип Node представлен ниже. Указатель - это переменная целого типа, которая интерпретируется как адрес байта памяти, содержащий некоторый элемент данных. Давайте поподробней разберем это определение. Память компьютера можно представить в следующем виде. Она разбита на блоки каждый такой блок называется сегментом. В Dos номер каждого сегмента может содержать максимум 16 бит(число $FFFF, когда все биты равны 1)(символ $ - это знак шестнадцатеричной системы исчисления) , т.е. любой сегмент может иметь номер [0; $FFFF]. Предположим, что мы рассматриваем участок памяти с сегментами $592B, $592C, $592D. Чтобы найти конкретную ячейку памяти внутри каждого сегмента есть так называемое смещение, которое тоже может иметь номер [0; $FFFF]. К примеру пусть смещение внутри сегмента $592C будет $B401, тогда указатель на эту ячейку памяти будет иметь значение $592CB401. procedure list_initialize; new( head ); - процедура new создает новую динамическую переменную типа node и устанавливает значение переменной head таким образом, чтобы оно указывало на эту новую динамическую переменную, т.е. программа ищет в памяти свободный участок длиной 6($6) байт(запись node имеет два типа integer(2 байта) и pointer(4 байта)). Как только она его находит она резервирует этот участок, т.е. объявляет этот участок памяти занятым и другие переменные уже не могут размещать там информацию. Затем программа присваивает переменной head адрес этого свободного участка. Пусть программа нашла свободный участок памяти начиная с адреса $592CB401 и присвоила этот номер переменной head. new( z ) - пусть программа нашла участок памяти со следующим адресом $592D000A и присвоила его z. Как же происходит обращение к содержимому адреса памяти? С помощью оператора ^. К примеру, head^.key:=10; Что здесь происходит? Мы заносим в содержимое ячейки $592CB401, а конкретней в первые 2 байта (т.к. тип integer занимает 2 байта) в область называемую key - число 10. head^.next := z; - здесь мы заносим в ячейку $592CB401, начиная с 3 байта(т.к. первые два заняты key) в область называемую Next число $592D000A. z^.next := nil - ячейку памяти адресуемую z, а конкретней область next заполняем нулями. procedure insert_after( v : integer; t : link ); procedure delete_next( t : link ); {аналогично и см. рис.} Почему использование пустых узлов столь полезно? Во-первых, если бы мы условились держать указатель на начало списка вместо того, чтобы держать узел head, то процедуре вставки потребовалась бы специальная проверка для вставки в начало списка. Во-вторых, соглашение о пустом узле z предохраняет процедуру удаления от излишней проверки на удаление из пустого списка. А эти проверки существенно сказываются на времени работы программы. Выражение типа head^.next^.key дает нам первый элемент списка, а head^. Next^.next^.key - второй. Давайте создадим программу обрабатывающею информацию об автомобилях. program car; Следующий вид списков - это циклические списки. Циклический список - это список последний элемент, которого указывает на первый. Этот список позволяет программе снова и снова просматривать список по кругу до тех пор, пока в этом есть необходимость. В качестве примера, мы рассмотрим так называемую "проблему Джозефа". Она состоит в следующем: представьте себе, что N человек решили совершить массовое самоубийство, выстраивая себя в круг и убивая каждого М-го человека и постепенно сужая круг. Надо найти человека, который умрет последним (хотя возможно под конец он изменит свое мнение по этому вопросу), или, по-другому, найти порядок смерти этих людей. Например, если N=9 и М=5, то люди будут убиты в следующем порядке: 5 1 7 4 3 6 9 2 8. Данная программа читает значения N и M, и затем распечатывает этот порядок. Эта программа использует циклический связанный список для прямого симулирования порядка совершения самоубийств. Сперва создается список с ключами от 1 до N: переменная head указывает на начало списка в процессе его создания, а затем программа делает чтобы последний элемент в списке указывал на head. Затем программа проходит по этому циклическому списку, пропуская M-1 элементом и уничтожая следующий после них узел до тех пор, пока в списке не останется один единственный элемент (который указывает на себя самого). Program Josef; Итак, на сегодняшнем занятии мы рассмотрели ссылки и указатели, линейные и циклические списки |
(с)Все права защищены По всем интересующим вопросам прошу писать на электронный адрес |