Форум МГОУ Прокопьевск

Объявление

Кому нужна квартира на сутки ночь час обрашяйтесь к TRojan

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Форум МГОУ Прокопьевск » Московский Государственный Открытый Университет » СТРУКТУРЫ И АЛГОРИТМЫ ОБРАБОТКИ ДАННЫХ


СТРУКТУРЫ И АЛГОРИТМЫ ОБРАБОТКИ ДАННЫХ

Сообщений 1 страница 5 из 5

1

вопрос №22
Связное представление данных в памяти

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

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

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

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

Достоинства связного представления данных - в возможности обеспечения значительной изменчивости структур;

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

Вместе с тем связное представление не лишено и недостатков, основные из которых:

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

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

5.2. Связные линейные списки

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

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

5.2.1. Машинное представление связных линейных списков

На рис. 5.1 приведена структура односвязного списка. На нем поле INF - информационное поле, данные, NEXT - указатель на следующий элемент списка. Каждый список должен иметь особый элемент, называемый указателем начала списка или головой списка, который обычно по формату отличен от остальных элементов. В поле указателя последнего элемента списка находится специальный признак nil, свидетельствующий о конце списка.
Структура односвязного списка

Рис.5.1. Структура односвязного списка

Однако, обработка односвязного списка не всегда удобна, так как отсутствует возможность продвижения в противоположную сторону. Такую возможность обеспечивает двухсвязный список, каждый элемент которого содержит два указателя: на следующий и предыдущий элементы списка. Структура линейного двухсвязного списка приведена на рис. 5.2, где поле NEXT - указатель на следующий элемент, поле PREV - указатель на предыдущий элемент. В крайних элементах соответствующие указатели должны содержать nil, как и показано на рис. 5.2.

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

Рис.5.2. Структура двухсвязного списка

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

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

Рис.5.3. Структура кольцевого двухсвязного списка

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

5.2.2. Реализация операций над связными линейными списками

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

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

В программных примерах подразумеваются определенными следующие типы данных:

    * любая структура информационной части списка:
      type data = ...;
    * элемент односвязного списка (sll - single linked list):

       type
         sllptr = ^slltype; { указатель в односвязном списке }
         slltype = record { элемент односвязного списка }
           inf : data;    { информационная часть }
           next : sllptr; { указатель на следующий элемент }
           end;

    * элемент двухсвязного списка (dll - double linked list):

       type
         dllptr = ^dlltype;     { указатель в двухсвязном списке }
         dlltype = record { элемент односвязного списка }
           inf : data;    { информационная часть }
           next : sllptr; { указатель на следующий элемент (вперед) }
           prev : sllptr; { указатель на предыдущий элемент (назад) }
           end;

Словесные описания алгоритмов даны в виде комментариев к программным примерам.

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

Перебор элементов списка.

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

Алгоритм перебора для односвязного списка представляется программным примером 5.1.

{==== Программный пример 5.1 ====}
{ Перебор 1-связного списка }
Procedure LookSll(head : sllptr);
  { head - указатель на начало списка }
  var cur : sllptr;  { адрес текущего элемента }
   begin
   cur:=head; { 1-й элемент списка назначается текущим }
   while cur <> nil do begin   
     < обработка c^.inf >
     {обрабатывается информационная часть того эл-та,
      на который указывает cur.
      Обработка может состоять в:
     

    * печати содержимого инф.части;
    * модификации полей инф.части;
    * сравнения полей инф.части с образцом при поиске по ключу;
    * подсчете итераций цикла при поиске по номеру;
    * и т.д., и т.п.

}
     cur:=cur^.next;

{ из текущего эл-та выбирается указатель на следующий эл-т и для следующей итерации следующий эл-т становится текущим; если текущий эл-т был последний, то его поле next содержит пустой указатель и, т.обр. в cur запишется nil, что приведет к выходу из цикла при проверке условия while } end; end;

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

           cur:=cur^.prev;

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

Вставка элемента в список.

Вставка элемента в середину односвязного списка показана на рис.5.4 и в примере 5.2.
Вставка элемента

Рис.5.4. Вставка элемента в середину 1-связного списка

  {==== Программный пример 5.2 ====}
{ Вставка элемента в середину 1-связного списка }
Procedure InsertSll(prev : sllptr; inf : data);
{ prev - адрес предыдущего эл-та; inf - данные нового эл-та }
  var cur : sllptr;  { адрес нового эл-та }
   begin
   { выделение памяти для нового эл-та и запись в его инф.часть }
   New(cur); cur^.inf:=inf;
   cur^.next:=prev^.next; { эл-т, следовавший за предыдущим теперь
будет следовать за новым }
   prev^.next:=cur;       { новый эл-т следует за предыдущим }
end;

Рисунок 5.5 представляет вставку в двухсвязный список.
Вставка элемента в середину 2-связного списка

Рис.5.5. Вставка элемента в середину 2-связного списка

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

Рис.5.6. Вставка элемента в начало 1-связного списка

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

{==== Программный пример 5.3 ====}
{ Вставка элемента в любое место 1-связного списка }
Procedure InsertSll
   var head : sllptr; { указатель на начало списка, может измениться в
процедуре, если head=nil - список пустой }
       prev : sllptr; { указатель на эл-т, после к-рого делается вставка,
если prev-nil - вставка перед 1-ым эл-том }
       inf : data { - данные нового эл-та }
  var cur : sllptr;  { адрес нового эл-та }
   begin
   { выделение памяти для нового эл-та и запись в его инф.часть }
   New(cur); cur^.inf:=inf;
   if prev <> nil then begin { если есть предыдущий эл-т - вставка в
середину списка, см. прим.5.2 }
     cur^.next:=prev^.next;  prev^.next:=cur;
     end
   else begin { вставка в начало списка }
     cur^.next:=head; { новый эл-т указывает на бывший 1-й эл-т списка;
если head=nil, то новый эл-т будет и последним эл-том списка }
     head:=cur; { новый эл-т становится 1-ым в списке, указатель на
начало теперь указывает на него }
  end; end;

Удаление элемента из списка.

Удаление элемента из односвязного списка показано на рис.5.7.
Удаление элемента из 1-связного списка

Рис.5.7. Удаление элемента из 1-связного списка

Очевидно, что процедуру удаления легко выполнить, если известен адрес элемента, предшествующего удаляемому (prev на рис.5.7.а). Мы, однако, на рис. 5.7 и в примере 5.4 приводим процедуру для случая, когда удаляемый элемент задается своим адресом (del на рис.5.7). Процедура обеспечивает удаления как из середины, так и из начала списка.

{==== Программный пример 5.4 ====}
{ Удаление элемента из любого места 1-связного списка }
Procedure DeleteSll(
   var head : sllptr; { указатель на начало списка, может
                        измениться в процедуре }
       del : sllptr   { указатель на эл-т, к-рый удаляется }  );
  var prev : sllptr;  { адрес предыдущего эл-та }
   begin
   if head=nil then begin { попытка удаления из пустого списка
   асценивается как ошибка (в последующих примерах этот случай
   учитываться на будет) }
       Writeln('Ошибка!'); Halt;
       end;
   if del=head then { если удаляемый эл-т - 1-й в списке, то
следующий за ним становится первым }
     head:=del^.next
   else begin { удаление из середины списка }

{ приходится искать эл-т, предшествующий удаляемому; поиск производится перебором списка с самого его начала, пока не будет найдет эл-т, поле next к-рого совпадает с адресом удаляемого элемента }

     prev:=head^.next;
     while (prev^.next<>del) and (prev^.next<>nil) do
       prev:=prev^.next;
     if prev^.next=nil then begin

{ это случай, когда перебран весь список, но эл-т не найден, он отсутствует в списке; расценивается как ошибка (в последующих примерах этот случай учитываться на будет) }

       Writeln('Ошибка!'); Halt;
       end;
     prev^.next:=del^.next; { предыдущий эл-т теперь указывает
на следующий за удаляемым }
     end;
   { элемент исключен из списка, теперь можно освободить
занимаемую им память }
   Dispose(del);
end;

Удаление элемента из двухсвязного списка требует коррекции большего числа указателей, как показано на рис.5.8.
Удаление элемента из 2-связного списка

Рис.5.8. Удаление элемента из 2-связного списка

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

Перестановка элементов списка.

Изменчивость динамических структур данных предполагает не только изменения размера структуры, но и изменения связей между элементами. Для связных структур изменение связей не требует пересылки данных в памяти, а только изменения указателей в элементах связной структуры. В качестве примера приведена перестановка двух соседних элементов списка. В алгоритме перестановки в односвязном списке (рис.5.9, пример 5.5) исходили из того, что известен адрес элемента, предшествующего паре, в которой производится перестановка. В приведенном алгоритме также не учитывается случай перестановки первого и второго элементов.
Перестановка соседних элементов 1-связного списка

Рис.5.9. Перестановка соседних элементов 1-связного списка

{==== Программный пример 5.5 ====}
{ Перестановка двух соседних элементов в 1-связном списке }
Procedure ExchangeSll(
       prev : sllptr   { указатель на эл-т, предшествующий
переставляемой паре }  );
  var p1, p2 : sllptr;  { указатели на эл-ты пары }
   begin
   p1:=prev^.next;     { указатель на 1-й эл-т пары }
   p2:=p1^.next;       { указатель на 2-й эл-т пары }
   p1^.next:=p2^.next; { 1-й элемент пары теперь указывает на
     следующий за парой }
   p2^.next:=p1;       { 1-й эл-т пары теперь следует за 2-ым }
   prev^.next:=p2;     { 2-й эл-т пары теперь становится 1-ым }
end;

В процедуре перестановки для двухсвязного списка (рис.5.10.) нетрудно учесть и перестановку в начале/конце списка.

Копирование части списка.

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

Рис.5.10. Перестановка соседних элементов 2-связного списка

Копирование для односвязного списка показано в программном примере 5.6.

{==== Программный пример 5.6 ====}
{ Копирование части 1-связного списка. head - указатель на
начало копируемой части; num - число эл-тов. Ф-ция возвращает
указатель на список-копию }
Function CopySll ( head : sllptr; num : integer) : sllptr;
  var cur, head2, cur2, prev2 : sllptr;
  begin
    if head=nil then { исходный список пуст - копия пуста }
      CopySll:=nil
    else begin
      cur:=head; prev2:=nil;
      { перебор исходного списка до конца или по счетчику num }
      while (num>0) and (cur<>nil) do begin
  { выделение памяти для эл-та выходного списка и запись в него
информационной части }
        New(cur2); cur2^.inf:=cur^.inf;
  { если 1-й эл-т выходного списка - запоминается указатель на
начало, иначе - записывается указатель в предыдущий элемент }
        if prev2<>nil then prev2^.next:=cur2 else head2:=cur2;
        prev2:=cur2;  { текущий эл-т становится предыдущим }
        cur:=cur^.next;  { продвижение по исходному списку }
        num:=num-1;   { подсчет эл-тов }
        end;
      cur2^.next:=nil; { пустой указатель - в последний эл-т
выходного списка }
      CopySll:=head2;  { вернуть указатель на начало вых.списка }
  end;   end;

Слияние двух списков.

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

{==== Программный пример 5.7 ====}
{ Слияние двух списков. head1 и head2 - указатели на начала
списков. На результирующий список указывает head1 }
Procedure Unite (var head1, head2 : sllptr);
  var cur : sllptr;
  begin          { если 2-й список пустой - нечего делать }
    if head2<>nil then begin
      { если 1-й список пустой, выходным списком будет 2-й }
    if head1=nil then head1:=head2
    else     { перебор 1-го списка до последнего его эл-та }
     begin  cur:=head1;
      while cur^.next<>nil do cur:=cur^.next;
      { последний эл-т 1-го списка указывает на начало 2-го }
      cur^.next:=head2;
     end;   head2:=nil; { 2-й список аннулируется }
   end; end;

5.2.3. Применение линейных списков

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

В программном примере 5.8 показана организация стека на односвязном линейном списке. Это пример функционально аналогичен примеру 4.1 с той существенной разницей, что размер стека здесь практически неограничен.

Стек представляется как линейный список, в котором включение элементов всегда производятся в начала списка, а исключение - также из начала. Для представления его нам достаточно иметь один указатель - top, который всегда указывает на последний записанный в стек элемент. В исходном состоянии (при пустом стеке) указатель top - пустой. Процедуры StackPush и StackPop сводятся к включению и исключению элемента в начало списка. Обратите внимание, что при включении элемента для него выделяется память, а при исключении - освобождается. Перед включением элемента проверяется доступный объем памяти, и если он не позволяет выделить память для нового элемента, стек считается заполненным. При очистке стека последовательно просматривается весь список и уничтожаются его элементы. При списковом представлении стека оказывается непросто определить размер стека. Эта операция могла бы потребовать перебора всего списка с подсчета числа элементов. Чтобы избежать последовательного перебора всего списка мы ввели дополнительную переменную stsize, которая отражает текущее число элементов в стеке и корректируется при каждом включении/исключении.

{==== Программный пример 5.8 ====}
{ Стек на 1-связном линейном списке }
unit Stack;
Interface
type data = ...; { эл-ты могут иметь любой тип }
Procedure StackInit;
Procedure StackClr;
Function StackPush(a : data) : boolean;
Function StackPop(Var a : data) : boolean;
Function StackSize : integer;
Implementation
type stptr = ^stit;  { указатель на эл-т списка }
      stit = record   { элемент списка }
        inf : data;   { данные }
        next: stptr;  { указатель на следующий эл-т }
        end;
Var top : stptr; { указатель на вершину стека }
     stsize : longint;  { размер стека }
{** инициализация - список пустой }
Procedure StackInit;
   begin   top:=nil; stsize:=0;  end; { StackInit }
{** очистка - освобождение всей памяти }
Procedure StackClr;
  var x : stptr;
   begin   { перебор эл-тов до конца списка и их уничтожение }
   while top<>nil do
     begin  x:=top; top:=top^.next; Dispose(x);  end;
   stsize:=0;
end; { StackClr }
Function StackPush(a: data) : boolean;   { занесение в стек }
  var x : stptr;
   begin      { если нет больше свободной памяти - отказ }
   if MaxAvail < SizeOf(stit) then StackPush:=false
   else   { выделение памяти для эл-та и заполнение инф.части }
     begin  New(x); x^.inf:=a;
                { новый эл-т помещается в голову списка }
       x^.next:=top; top:=x;
       stsize:=stsize+1; { коррекция размера }
       StackPush:=true;
     end;
end; { StackPush }
Function StackPop(var a: data) : boolean; { выборка из стека }
  var x : stptr;
   begin
   { список пуст - стек пуст }
   if top=nil then StackPop:=false
   else begin
     a:=top^.inf; { выборка информации из 1-го эл-та списка }
     { 1-й эл-т исключается из списка, освобождается память }
     x:=top; top:=top^.next; Dispose(top);
     stsize:=stsize-1; { коррекция размера }
     StackPop:=true;
end;  end; { StackPop }
Function StackSize : integer;  { определение размера стека }
   begin   StackSize:=stsize;   end; { StackSize }
END.

Программный пример для организация на односвязном линейном списке очереди FIFI разработайте самостоятельно. Для линейного списка, представляющего очередь, необходимо будет сохранять: top - на первый элемент списка, и bottom - на последний элемент.

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

     type lptr = ^item; { указатель на элемент списка }
          item = record    { элемент списка }
            key : integer; { ключ }
            inf : data;    { данные }
            next: lptr;    { указатель на элемент списка }
            end;

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

Пример 5.9 демонстрирует сортировку выборкой. Указатель newh является указателем на начало выходного списка, исходно - пустого. Во входном списке ищется максимальный элемент. Найденный элемент исключается из входного списка и включается в начало выходного списка. Работа алгоритма заканчивается, когда входной список станет пустым. Обратим внимание читателя на несколько особенностей алгоритма. Во-первых, во входном списке ищется всякий раз не минимальный, а максимальный элемент. Поскольку элемент включается в начало выходного списка (а не в конец выходного множества, как было в программном примере 3.7), элементы с большими ключами оттесняются к концу выходного списка и последний, таким образом, оказывается отсортированным по возрастанию ключей. Во-вторых, при поиске во входном списке сохраняется не только адрес найденного элемента в списке, но и адрес предшествующего ему в списке эле- мента - это впоследствии облегчает исключение элемента из списка (вспомните пример 5.4). В-третьих, обратите внимание на то, что у нас не возникает никаких проблем с пропуском во входном списке тех элементов, которые уже выбраны - они просто исключены из входной структуры данных.

{==== Программный пример 5.9 ====}
{ Сортировка выборкой на 1-связном списке }
Function Sort(head : lptr) : lptr;
  var newh, max, prev, pmax, cur : lptr;
   begin        newh:=nil;         { выходной список - пустой }
   while head<>nil do { цикл, пока не опустеет входной список }
     begin   max:=head; prev:=head; { нач.максимум - 1-й эл-т }
     cur:=head^.next;     { поиск максимума во входном списке }
     while cur<>nil do begin
       if cur^.key>max^.key then begin
{ запоминается адрес максимума и адрес предыдущего эл-та }
         max:=cur; pmax:=prev;
     end;    prev:=cur; cur:=cur^.next; { движение по списку }
       end;        { исключение максимума из входного списка }
     if max=head then head:=head^.next
     else pmax^.next:=max^.next;
     { вставка в начало выходного списка }
     max^.next:=newh; newh:=max;
   end;  Sort:=newh;
  end;

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

{==== Программный пример 5.10 ====}
{ Сортировка вставками на 1-связном списке }
type data = integer;
Function Sort(head : lptr) : lptr;
  var newh, cur, sel : lptr;
   begin
   newh:=nil;  { выходной список - пустой }
   while head <> nil do begin { цикл, пока не опустеет входной список }
     sel:=head;  { эл-т, который переносится в выходной список }
     head:=head^.next;         { продвижение во входном списке }
     if (newh=nil) or (sel^.key < newh^.key) then begin
{выходной список пустой или элемент меньше 1-го-вставка в начало}
     sel^.next:=newh; newh:=sel;   end
     else begin                { вставка в середину или в конец }
       cur:=newh;
{ до конца выходного списка или пока ключ следующего эл-та не будет
больше вставляемого }
       while (cur^.next <> nil) and (cur^.next^.key < sel^.key) do
         cur:=cur^.next;
       { вставка в выходной список после эл-та cur }
       sel^.next:=cur^.next;    cur^.next:=sel;
      end;   end;   Sort:=newh;
  end;

0

2

вопрос №23
Характерные особенности полустатических структур

Полустатические структуры данных характеризуются следующими признаками:

    * они имеют переменную длину и простые процедуры ее изменения;
    * изменение длины структуры происходит в определенных пределах, не превышая какого-то максимального (предельного) значения.

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

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

4.2. Стеки
4.2.1. Логическая структура стека

Стек - такой последовательный список с переменной длиной, включение и исключение элементов из которого выполняются только с одной стороны списка, называемого вершиной стека. Применяются и другие названия стека - магазин и очередь, функционирующая по принципу LIFO (Last - In - First- Out - "последним пришел - первым исключается"). Примеры стека: винтовочный патронный магазин, тупиковый железнодорожный разъезд для сортировки вагонов.

Основные операции над стеком - включение нового элемента (английское название push - заталкивать) и исключение элемента из стека (англ. pop - выскакивать).

Полезными могут быть также вспомогательные операции:

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

        x:=pop(stack);      push(stack,x).

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

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

    * а). пустого;
    * б-г). после последовательного включения в него элементов с именами 'A', 'B', 'C';
    * д, е). после последовательного удаления из стека элементов 'C' и 'B';
    * ж). после включения в стек элемента 'D'.

Рис 4.1. Включение и исключение элементов из стека.

Как видно из рис. 4.1, стек можно представить, например, в виде стопки книг (элементов), лежащей на столе. Присвоим каждой книге свое название, например A,B,C,D... Тогда в момент времени, когда на столе книг нет, про стек аналогично можно сказать, что он пуст, т.е. не содержит ни одного элемента. Если же мы начнем последовательно класть книги одну на другую, то получим стопку книг (допустим, из n книг), или получим стек, в котором содержится n элементов, причем вершиной его будет являться элемент n+1. Удаление элементов из стека осуществляется аналогичным образом т. е. удаляется последовательно по одному элементу, начиная с вершины, или по одной книге из стопки.

4.2.2. Машинное представление стека и реализация операций

При представлении стека в статической памяти для стека выделяется память, как для вектора. В дескрипторе этого вектора кроме обычных для вектора параметров должен находиться также указатель стека - адрес вершины стека. Указатель стека может указывать либо на первый свободный элемент стека, либо на последний записанный в стек элемент. (Все равно, какой из этих двух вариантов выбрать, важно в последствии строго придерживаться его при обработке стека.) В дальнейшем мы будем всегда считать, что указатель стека адресует первый свободный элемент и стек растет в сторону увеличения адресов.

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

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

Операция очистки стека сводится к записи в указатель стека начального значения - адреса начала выделенной области памяти.

Определение размера стека сводится к вычислению разности указателей: указателя стека и адреса начала области.

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

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

     const SIZE = ...;
     type data = ...;
{==== Программный пример 4.1 ====}
{ Стек }
unit Stack;
Interface
const SIZE=...;     { предельный размер стека }
type data = ...;    { эл-ты могут иметь любой тип }
Procedure StackInit;
Procedure StackClr;
Function StackPush(a : data) : boolean;
Function StackPop(Var a : data) : boolean;
Function StackSize : integer;
Implementation
Var StA : array[1..SIZE] of data;   { Стек - данные }
{ Указатель на вершину стека, работает на префиксное вычитание }
     top : integer;
Procedure StackInit;             {** инициализация - на начало }
begin top:=SIZE; end;            {** очистка = инициализация }
Procedure StackClr;
   begin top:=SIZE; end;
                                 { ** занесение элемента в стек }
Function StackPush(a: data) : boolean;
begin
   if top=0 then StackPush:=false
   else begin { занесение, затем - увеличение указателя }
     StA[top]:=a; top:=top-1; StackPush:=true;
   end;  end;       { StackPush }
                                 { ** выборка элемента из стека }
Function StackPop(var a: data) : boolean;
begin
   if top=SIZE then StackPop:=false
   else begin { указатель увеличивается, затем - выборка }
     top:=top+1; a:=StA[top]; StackPop:=true;
  end;  end;       { StackPop }
Function StackSize : integer;     {** определение размера }
   begin StackSize:=SIZE-top; end;
END.

0

3

вопрос №24
Логическая структура очереди

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

Основные операции над очередью - те же, что и над стеком - включение, исключение, определение размера, очистка, неразрушающее чтение.

4.3.2. Машинное представление очереди FIFO и реализация операций

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

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

В исходном состоянии указатели на начало и на конец указывают на начало области памяти. Равенство этих двух указателей (при любом их значении) является признаком пустой очереди. Если в процессе работы с кольцевой очередью число операций включения превышает число операций исключения, то может возникнуть ситуация, в которой указатель конца "догонит" указатель начала. Это ситуация заполненной очереди, но если в этой ситуации указатели сравняются, эта ситуация будет неотличима от ситуации пустой очереди. Для различения этих двух ситуаций к кольцевой очереди предъявляется требование, чтобы между указателем конца и указателем начала оставался "зазор" из свободных элементов. Когда этот "зазор" сокращается до одного элемента, очередь считается заполненной и дальнейшие попытки записи в нее блокируются. Очистка очереди сводится к записи одного и того же (не обязательно начального) значения в оба указателя. Определение размера состоит в вычислении разности указателей с учетом кольцевой природы очереди.

Программный пример 4.3 иллюстрирует организацию очереди и операции на ней.

{==== Программный пример 4.3 ====}
unit Queue;                 { Очередь FIFO - кольцевая }
Interface
const SIZE=...;             { предельный размер очереди }
type data = ...;            { эл-ты могут иметь любой тип }
Procesure QInit;
Procedure Qclr;
Function QWrite(a: data) : boolean;
Function QRead(var a: data) : boolean;
Function Qsize : integer;
Implementation              { Очередь на кольце  }
var QueueA : array[1..SIZE] of data;   { данные очереди }
    top, bottom : integer;   { начало  и конец  }
Procedure QInit;            {** инициализация - начало=конец=1 }
   begin top:=1; bottom:=1; end;
Procedure Qclr;             {**очистка - начало=конец }
   begin top:=bottom; end;
Function QWrite(a : data) : boolean;  {**  запись в конец  }
   begin
   if bottom mod SIZE+1=top then { очередь полна } QWrite:=false
   else begin
     { запись, модификация указ.конца с переходом по кольцу }
     Queue[bottom]:=a; bottom:=bottom mod SIZE+1; QWrite:=true;
   end;    end;  { QWrite }
Function QRead(var a: data) : boolean;  {** выборка из начала }
begin
   if top=bottom then QRead:=false  else
     { запись, модификация указ.начала с переходом по кольцу }
   begin  a:=Queue[top]; top:=top mod SIZE + 1; QRead:=true;
  end;   end; { QRead }
Function QSize : integer;    {** определение размера }
begin
   if top <= bottom then QSize:=bottom-top
   else QSize:=bottom+SIZE-top;
end; { QSize }
END.

4.3.3. Очереди с приоритетами

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

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

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

0

4

вопрос №27
Последовательный или линейный поиск

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

Для последовательного поиска в среднем требуется (N+1)/2 сравнений. Таким образом, порядок алгоритма - линейный - O(N).

Программная иллюстрация линейного поиска в неупорядоченном массиве приведена в следующем примере, где a - исходный массив, key - ключ, который ищется; функция возвращает индекс найденного элемента или EMPTY - если элементт отсутствует в массиве.

{===== Программный пример 3.4 =====}
Function LinSearch( a : SEQ; key : integer) : integer;
   var i : integer;
   for i:=1 to N do           { перебор эл-тов массива }
     if a[i]=key then begin   { ключ найден - возврат индекса }
       LinSearch:=i; Exit;   end;
   LinSearch:=EMPTY; {просмотрен весь массив, но ключ не найден }
end;

3.7.2. Бинарный поиск

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

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

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

Программная иллюстрация бинарного поиска в упорядоченном массиве приведена в следующем примере, где a - исходный массив, key - ключ, который ищется; функция возвращает индекс найденного элемента или EMPTY - если элементт отсутствует в массиве.

{===== Программный пример 3.5 =====}
Function BinSearch(a : SEQ; key : integer) : integer;
Var b, e, i : integer;
begin
    b:=1; e:=N;   { начальные значения границ }
    while b<=e do { цикл, пока интервал поиска не сузится до 0 }
    begin   i:=(b+e) div 2;     { середина интервала }
      if a[i]=key then
      begin BinSearch:=i; Exit; {ключ найден - возврат индекса }
      end         else
        if a[i] < key then b:=i+1  { поиск в правом подинтервале }
                    else e:=i-1; { поиск в левом подинтервале }
      end;    BinSearch:=EMPTY;  { ключ не найден }
end;

Трассировка бинарного поиска ключа 275 в исходной последовательности:

         75, 151, 203, 275, 318, 489, 524, 519, 647, 777

представлена в таблице 3.4.
Интерация b e i K[i]
1 1 10 5 318
2 1 4 2 151
3 3 4 3 203
4 4 4 4 275

Таблица 3.4

Алгоритм бинарного поиска можно представить и несколько иначе, используя рекурсивное описание. В этом случае граничные индексы интервала b и e являются параметрами алгоритма.

Рекурсивная процедура бинарного поиска представлена в программном примере 3.6. Для выполнения поиска необходимо при вызове процедуры задать значения ее формальных параметров b и е - 1 и N соответственно, где b, e - граничные индексы области поиска.

{===== Программный пример 3.6 =====}
Function BinSearch( a: SEQ; key, b, e : integer) : integer;
Var i : integer;
begin
   if b > e then BinSearch:=EMPTY { проверка ширины  интервала }
   else begin
     i:=(b+e) div 2;               { середина интервала }
     if a[i]=key then BinSearch:=i {ключ найден, возврат индекса}
          else   if a[i] < key then { поиск в правом подинтервале }
         BinSearch:=BinSearch(a,key,i+1,e)
                             else { поиск в левом подинтервале }
         BinSearch:=BinSearch(a,key,b,i-1);
  end; end;

0

5

вопрос №28 исходник по адресу: http://www.itdom.info/Tehnol/SD3.html
Сортировка

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

Из всех задач программирования сортировка, возможно, имеет самый богатый выбор алгоритмов решения. Назовем некоторые факторы, которые влияют на выбор алгоритма (помимо порядка алгоритма).

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

2). Исходная упорядоченность входного множества: во входном множестве (даже если оно сгенерировано датчиком случайных величин) могут попадаться упорядоченные участки. В предельном случае входное множество может оказаться уже упорядоченным. Одни алгоритмы не учитывают исходной упорядоченности и требуют одного и того же времени для сортировки любого (в том числе и уже упорядоченного) множества данного объема, другие выполняются тем быстрее, чем лучше упорядоченность на входе.

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

4). Сложность алгоритма является не последним соображением при его выборе. Простой алгоритм требует меньшего времени для его реализации и вероятность ошибки в реализации его меньше. При промышленном изготовлении программного продукта требования соблюдения сроков разработки и надежности продукта могут даже превалировать над требованиями эффективности функционирования.

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

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

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

3). Стратегия распределения. Входное множество разбивается на ряд подмножеств (возможно, меньшего объема) и сортировка ведется внутри каждого такого подмножества.

4). Стратегия слияния. Выходное множество получается путем слияния маленьких упорядоченных подмножеств.

Далее приводится обзор (далеко не полный) методов сортировки, сгруппированных по стратегиям, применяемым в их алгоритмах. Все алгоритмы рассмотрены для случая упорядочения по возрастанию ключей.

3.8.1. Сортировки выборкой

Сортировка простой выборкой.

Данный метод реализует практически "дословно" сформулированную выше стратегию выборки. Порядок алгоритма простой выборки - O(N^2). Количество пересылок - N.

Алгоритм сортировки простой выборкой иллюстрируется программным примером 3.7.

В программной реализации алгоритма возникает проблема значения ключа "пусто". Довольно часто программисты используют в качестве такового некоторое заведомо отсутствующее во входной последовательности значение ключа, например, максимальное из теоретически возможных значений. Другой, более строгий подход - создание отдельного вектора, каждый элемент которого имеет логический тип и отражает состояние соответствующего элемента входного множества ("истина" - "непусто", "ложь" - "пусто"). Именно такой подход реализован в нашем программном примере. Роль входной последовательности здесь выполняет параметр a, роль выходной - параметр b, роль вектора состояний - массив c. Алгоритм несколько усложняется за счет того, что для установки начального значения при поиске минимума приходится отбрасывать уже "пустые" элементы.

{===== Программный пример 3.7 =====}
Procedure Sort( a : SEQ; var b : SEQ);
Var  i, j, m : integer;
      c: array[1..N] of boolean; {состояние эл-тов вх.множества}
begin
   for i:=1 to N do c[i]:=true;  { сброс отметок}
   for i:=1 to N do {поиск 1-го невыбранного эл. во вх.множестве}
    begin j:=1;
          while not c[j] do j:=j+1;
          m:=j;      { поиск минимального элемент а}
     for j:=2 to N do
       if c[j] and (a[j] < a[m]) then m:=j;
     b[i]:=a[m]; { запись в выходное множество}
     c[m]:=false; { во входное множество - "пусто" }
end; end;

Обменная сортировка простой выборкой.

Алгоритм сортировки простой выборкой, однако, редко применяется в том варианте, в каком он описан выше. Гораздо чаще применяется его, так называемый, обменный вариант. При обменной сортировке выборкой входное и выходное множество располагаются в одной и той же области памяти; выходное - в начале области, входное - в оставшейся ее части. В исходном состоянии входное множество занимает всю область, а выходное множество - пустое. По мере выполнения сортировки входное множество сужается, а выходное - расширяется.

Обменная сортировка простой выборкой показана в программном примере 3.8. Процедура имеет только один параметр - сортируемый массив.

{===== Программный пример 3.8 =====}
Procedure Sort(var a : SEQ);
Var  x, i, j, m : integer;
begin
   for i:=1 to N-1 do    { перебор элементов выходного множества}
   { входное множество - [i:N]; выходное - [1:i-1] }
     begin  m:=i;
      for j:=i+1 to N do  { поиск минимума во входном множестве }
        if (a[j] < a[m]) then m:=j;
      { обмен 1-го элемента вх. множества с минимальным }
      if i<>m then begin
        x:=a[i]; a[i]:=a[m]; a[m]:=x;
  end;end; end;

Результаты трассировки программного примера 3.8 представлены в таблице 3.5. Двоеточием показана граница между входным и выходным множествами.
Шаг Содержимое массива а
Исходный :242 447 286 708_24_11 192 860 937 561
1 _11:447 286 708_ 24 242 192 860 937 561
2 _11_24:286 708 447 242 192 860 937 561
3 _11_24 192:708 447 242 286 860 937 561
4 _11_24 192 242:447 708 286 860 937 561
5 _11_24 192 242 286:708 447 860 937 561
6 _11_24 192 242 286 447:708 860 937 561
7 _11_24 192 242 286 447 561:860 937 708
8 _11_24 192 242 286 447 561 708:937 860
9 _11_24 192 242 286 447 561 708 860:937
Результат _11_24 192 242 286 447 561 708 860 937:

Таблица 3.5

Очевидно, что обменный вариант обеспечивает экономию памяти. Очевидно также, что здесь не возникает проблемы "пустого" значения. Общее число сравнений уменьшается вдвое - N*(N-1)/2, но порядок алгоритма остается степенным - O(n^2). Количество перестановок N-1, но перестановка, по-видимому, вдвое более времяемкая операция, чем пересылка в предыдущем алгоритме.

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

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

Пузырьковая сортировка.

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

Порядок пузырьковой сортировки - O(N^2). Среднее число сравнений - N*(N-1)/2 и таково же среднее число перестановок, что значительно хуже, чем для обменной сортировки простым выбором. Однако, то обстоятельство, что здесь всегда сравниваются и перемещаются только соседние элементы, делает пузырьковую сортировку удобной для обработки связных списков. Перестановка в связных списках также получается более экономной.

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

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

Во-вторых, может быть учтено то обстоятельство, что за один просмотр входного множества на свое место могут "всплыть" не один, а два и более элементов. Это легко учесть, запоминая в каждом просмотре позицию последней перестановки и установки этой позиции в качестве границы между множествами для следующего просмотра. Именно эта модификация реализована в программной иллюстрации пузырьковой сортировке в примере 3.9. Переменная nn в каждом проходе устанавливает верхнюю границу входного множества. В переменной x запоминается позиция перестановок и в конце просмотра последнее запомненное значение вносится в nn. Сортировка будет закончена, когда верхняя граница входного множества станет равной 1.

{===== Программный пример 3.9 =====}
Procedure Sort( var a : seq);
Var nn, i, x : integer;
begin
   nn:=N; { граница входного множества }
   repeat x:=1; { признак перестановок }
     for i:=2 to nn do { перебор входного множества }
     if a[i-1] > a[i] then begin { сравнение соседних эл-в }
       x:=a[i-1]; a[i-1]:=a[i]; a[i]:=x; { перестановка }
       x:=i-1; { запоминание позиции  }
     end;  nn:=x;   { сдвиг границы }
   until (nn=1); {цикл пока вых. множество не захватит весь мас.}
end;

Результаты трассировки программного примера 3.9 представлены в таблице 3.6.
Шаг nn Содержимое массива а
Исходный 10 717 473 313 160 949 764_34 467 757 800:
1 9 473 313 160 717 764_34 467 757 800:949
2 7 313 160 473 717_34 467 757:764 800 949
3 5 160 313 473_34 467:717 757 764 800 949
4 4 160 313_34 467:473 717 757 764 800 949
5 2 160_34:313 467 473 717 757 764 800 949
6 1 _34:160 313 467 473 717 757 764 800 949
Результат     : 34 160 313 467 473 717 757 764 800 949

Таблица 3.6

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

Сортировка Шелла.

Это еще одна модификация пузырьковой сортировки. Суть ее состоит в том, что здесь выполняется сравнение ключей, отстоящих один от другого на некотором расстоянии d. Исходный размер d обычно выбирается соизмеримым с половиной общего размера сортируемой последовательности. Выполняется пузырьковая сортировка с интервалом сравнения d. Затем величина d уменьшается вдвое и вновь выполняется пузырьковая сортировка, далее d уменьшается еще вдвое и т.д. Последняя пузырьковая сортировка выполняется при d=1. Качественный порядок сортировки Шелла остается O(N^2), среднее же число сравнений, определенное эмпирическим путем - log2(N)^2*N. Ускорение достигается за счет того, что выяв- ленные "не на месте" элементы при d>1, быстрее "всплывают" на свои места.

Пример 3.10 иллюстрирует сортировку Шелла.

{===== Программный пример 3.10 =====}
Procedure Sort( var a : seq);
Var d, i, t : integer;  k : boolean; { признак перестановки }
begin   d:=N div 2;     { начальное значение интервала }
   while d > 0 do          { цикл с уменьшением интервала до 1 }
     begin  k:=true;     {пузырьковая сортировка с интервалом d}
     while k do          { цикл, пока есть перестановки }
       begin  k:=false; i:=1;
       for i:=1 to N-d do { сравнение эл-тов на интервале d }
          begin  if a[i] > a[i+d] then begin
           t:=a[i]; a[i]:=a[i+d]; a[i+d]:=t; { перестановка }
           k:=true;  { признак перестановки }
           end; { if ... }  end; { for ... }  end; { while k }
     d:=d div 2;  { уменьшение интервала }
     end;  { while d>0 }  end;

Результаты трассировки программного примера 3.10 представлены в таблице 3.7.
Шаг d Содержимое массива а
Исходный     76 22_ 4 17 13 49_ 4 18 32 40 96 57 77 20_ 1 52
1 8 32 22_ 4 17 13 20_ 1 18 76 40 96 57 77 49_ 4 52
2 8 32 22_ 4 17 13 20_ 1 18 76 40 96 57 77 49_ 4 52
3 4 13 20_ 1 17 32 22_ 4 18 76 40_ 4 52 77 49 96 57
4 4 13 20_ 1 17 32 22_ 4 18 76 40_ 4 52 77 49 96 57
5 2 13 20_ 1 17 32 22_ 4 18 76 40_ 4 52 77 49 96 57
6 2 13 20_ 1 17 32 22_ 4 18 76 40_ 4 52 77 49 96 57
7 2 _1 17_ 4 18_ 4 20 13 22 32 40 76 49 77 52 96 57
8 2 _1 17_ 4 18_ 4 20 13 22 32 40 76 49 77 52 96 57
9 1 _1_ 4 17_ 4 18 13 20 22 32 40 49 76 52 77 57 96
10 1 _1_ 4_ 4 17 13 18 20 22 32 40 49 52 76 57 77 96
11 1 _1_ 4_ 4 13 17 18 20 22 32 40 49 52 57 76 77 96
12 1 _1_ 4_ 4 13 17 18 20 22 32 40 49 52 57 76 77 96
Результат     _1_ 4_ 4 13 17 18 20 22 32 40 49 52 57 76 77 96

Таблица 3.7

3.8.2. Сортировки включением

Сортировка простыми вставками.

Этот метод - "дословная" реализации стратегии включения. Порядок алгоритма сортировки простыми вставками - O(N^2), если учитывать только операции сравнения. Но сортировка требует еще и в среднем N^2/4 перемещений, что де- лает ее в таком варианте значительно менее эффективной, чем сортировка выборкой.

Алгоритм сортировки простыми вставками иллюстрируется программным примером 3.11.

{===== Программный пример 3.11 =====}
Procedure Sort(a : Seq; var b : Seq);
   Var i, j, k : integer;
   begin
     for i:=1 to N do begin  { перебор входного массива }
       { поиск места для a[i] в выходном массиве }
       j:=1; while (j < i) and (b[j] <= a[i]) do j:=j+1;
       { освобождение места для нового эл-та }
       for k:=i downto j+1 do b[k]:=b[k-1];
       b[j]:=a[i];        { запись в выходной массив }
       end; end;

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

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

Пузырьковая сортировка вставками.

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

{===== Программный пример 3.12 =====}
Procedure Sort (var a : Seq);
Var i, j, k, t : integer;
begin
   for i:=2 to N do begin  { перебор входного массива }
   {*** вх.множество - [i..N], вых.множество - [1..i] }
     t:=a[i]; { запоминается значение нового эл-та }
     j:=i-1; {поиск места для эл-та в вых. множестве со сдвигом}
       { цикл закончится при достижении начала или, когда
         будет встречен эл-т, меньший нового }
       while (j >= 1) and (a[j] > t) do begin
         a[j+1]:=a[j]; { все эл-ты, большие нового сдвигаются }
         j:=j-1; { цикл от конца к началу выходного множества }
         end;
       a[j+1]:=t; { новый эл-т ставится на свое место }
       end;
end;

Результаты трассировки программного примера 3.11 представлены в таблице 3.8.
Шаг Содержимое массива a
Исходный 48:43 90 39_ 9 56 40 41 75 72
1 43 48:90 39_ 9 56 40 41 75 72
2 43 48 90:39_ 9 56 40 41 75 72
3 39 43 48 90:_9 56 40 41 75 72
4 _9 39 43 48 90:56 40 41 75 72
5 _9 39 43 48 56 90:40 41 75 72
6 _9 39 40 43 48 56 90:41 75 72
7 _9 39 40 41 43 48 56 90:75 72
8 _9 39 40 41 43 48 56 75 90:72
Результат _9 39 40 41 43 48 56 72 75 90:

Таблица 3.8

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

Еще одна группа включающих алгоритмов сортировки использует структуру дерева. Мы рекомендуем читателю повторно вернуться к рассмотрению этих алгоритмов после ознакомления с главой 6.

Сортировка упорядоченным двоичным деревом.

Алгоритм складывается из построения упорядоченного двоичного дерева и последующего его обхода. Если нет необходимости в построении всего линейного упорядоченного списка значений, то нет необходимости и в обходе дерева, в этом случае применяется поиск в упорядоченном двоичном дереве. Алгоритмы работы с упорядоченными двоичными деревьями подробно рассмотрены в главе 6. Отметим, что порядок алгоритма - O(N*log2(N)), но в конкретных случаях все зависит от упорядоченности исходной последовательности, который влияет на степень сбалансированности дерева и в конечном счете - на эффективность поиска.

Турнирная сортировка.

Этот метод сортировки получил свое название из-за сходства с кубковой системой проведения спортивных соревнований: участники соревнований разбиваются на пары, в которых разыгрывается первый тур; из победителей первого тура составляются пары для розыгрыша второго тура и т.д. Алгоритм сортировки состоит из двух этапов. На первом этапе строится дерево: аналогичное схеме розыгрыша кубка.

Например, для последовательности чисел a:

               16 21 8 14 26 94 30 1

такое дерево будет иметь вид пирамиды, показанной на рис.3.13.
Пирамида турнирной сортировки

Рис.3.13. Пирамида турнирной сортировки

В примере 3.12 приведена программная иллюстрация алгоритма турнирной сортировки. Она нуждается в некоторых пояснениях. Построение пирамиды выполняется функцией Create_Heap. Пирамида строится от основания к вершине. Элементы, составляющие каждый уровень, связываются в линейный список, поэтому каждый узел дерева помимо обычных указателей на потомков - left и right - содержит и указатель на "брата" - next. При работе с каждым уровнем указатель содержит начальный адрес списка элементов в данном уровне. В первой фазе строится линейный список для нижнего уровня пирамиды, в элементы которого заносятся ключи из исходной последовательности. Следующий цикл while в каждой своей итерации надстраивает следующий уровень пирамиды. Условием завершения этого цикла является получение списка, состоящего из единственного элемента, то есть, вершины пирамиды. Построение очередного уровня состоит в попарном переборе элементов списка, составляющего предыдущий (нижний) уровень. В новый уровень переносится наименьшее значение ключа из каждой пары.

Следующий этап состоит в выборке значений из пирамиды и формирования из них упорядоченной последовательности (процедура Heap_Sort и функция Competit). В каждой итерации цикла процедуры Heap_Sort выбирается значение из вершины пирамиды - это наименьшее из имеющихся в пирамиде значений ключа. Узел-вершина при этом освобождается, освобождаются также и все узлы, занимаемые выбранным значением на более низких уровнях пирамиды. За освободившиеся узлы устраивается (снизу вверх) состязание между их потомками. Так, для пирамиды, исходное состояние которой было показано на рис 3. , при выборке первых трех ключей (1, 8, 14) пирамида будет последовательно принимать вид, показанный на рис.3.14 (символом x помечены пустые места в пирамиде).
Пирамида

Рис.3.14. Пирамида после последовательных выборок

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

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

{===== Программный пример 3.12 =====}
{ Турнирная сортировка }
type nptr = ^node;     { указатель на узел }
     node = record     { узел дерева }
       key : integer;     { данные }
       left, right : nptr; { указатели на потомков }
       next : nptr;        { указатель на "брата" }
       end;
{ Создание дерева - функция возвращает указатель на вершину
                     созданного дерева }
Function Heap_Create(a : Seq) : nptr;
   var i : integer;
       ph2 : nptr;       { адрес начала списка уровня }
       p1 : nptr;        { адрес нового элемента }
       p2 : nptr;        { адрес предыдущего элемента }
       pp1, pp2 : nptr;  { адреса соревнующейся пары }
   begin
   { Фаза 1 - построение самого нижнего уровня пирамиды }
   ph2:=nil;
   for i:=1 to n do begin
     New(p1);        { выделение памяти для нового эл-та }
     p1^.key:=a[i]; { запись данных из массива }
     p1^.left:=nil; p1^.right:=nil;   { потомков нет }
     { связывание в линейный список по уровню }
     if ph2=nil then ph2:=p1 else p2^.next:=p1;
     p2:=p1;
     end; { for }
   p1^.next:=nil;
   { Фаза 2 - построение других уровней }
   while ph2^.next<>nil do begin  { цикл до вершины пирамиды }
     pp1:=ph2; ph2:=nil;          { начало нового уровня }
     while pp1<>nil do begin      { цикл по очередному уровню }
       pp2:=pp1^.next;
       New(p1);
       { адреса потомков из предыдущего уровня }
       p1^.left:=pp1; p1^.right:=pp2;
       p1^.next:=nil;
       { связывание в линейный список по уровню }
       if ph2=nil then ph2:=p1 else p2^.next:=p1;
       p2:=p1;
       { состязание данных за выход на уровень }
       if (pp2=nil)or(pp2^.key > pp1^.key) then p1^.key:=pp1^.key
       else p1^.key:=pp2^.key;
       { переход к следующей паре }
       if pp2<>nil then pp1:=pp2^.next else pp1:=nil;
       end; { while pp1<>nil }
     end; { while ph2^.next<>nil }
   Heap_Create:=ph2;
end;
   { Реорганизация поддерева - функция возвращает
     указатель на вершину реорганизованного дерева }
Function Competit(ph : nptr) : nptr;
   begin
   { определение наличия потомков, выбор потомка для
     реорганизации, реорганизация его }
   if (ph^.left<>nil)and(ph^.left^.key=ph^.key) then
     ph^.left:=Competit(ph^.left)
   else if (ph^.right<>nil) then
     ph^.right:=Competit(ph^.right);
   if (ph^.left=nil)and(ph^.right=nil) then begin
     { освобождение пустого узла }
     Dispose(ph); ph:=nil;
     end;
   else
     { состязание данных потомков }
     if (ph^.left=nil) or
     ((ph^.right<>nil)and(ph^.left^.key > ph^.right^.key)) then
        ph^.key:=ph^.right^.key
      else ph^.key:=ph^.left^.key;
   Competit:=ph;
end;
{ Сортировка }
Procedure Heap_Sort(var a : Seq);
   var ph : nptr;   { адрес вершины дерева }
       i : integer;
   begin
   ph:=Heap_Create(a);  { создание дерева }
   { выборка из дерева }
   for i:=1 to N do begin
     { перенос данных из вершины в массив }
     a[i]:=ph^.key;
     { реорганизация дерева }
     ph:=Competit(ph);
     end;
end;

Построение дерева требует N-1 сравнений, выборка - N*log2(N) сравнений. Порядок алгоритма, таким образом, O(N*log2(N)). Сложность операций над связными структурами данных, однако, значительно выше, чем над статическими структурами. Кроме того, алгоритм неэкономичен в отношении памяти: дублирование данных на разных уровнях пирамиды приводит к тому, что рабочая область памяти содержит примерно 2*N узлов.

Сортировка частично упорядоченным деревом.

В двоичном дереве, которое строится в этом методе сортировки для каждого узла справедливо следующее утверждение: значения ключа, записанное в узле, меньше, чем ключи его потомков. Для полностью упорядоченного дерева имеются требования к соотношению между ключами потомков. Для данного дерева таких требований нет, поэтому такое дерево и называется частично упорядоченным. Кроме того, наше дерево должно быть абсолютно сбалансированным. Это означает не только то, что длины путей к любым двум листьям различаются не более, чем на 1, но и то, что при добавлении нового элемента в дерево предпочтение всегда отдается левой ветви/подветви, пока это не нарушает сбалансированность. Более подробно деревья рассматриваются в гл.6.

Например, последовательность чисел:

              3 20 12 58 35 30 32 28

будет представлена в виде дерева, показанного на рис.3.15 .
Частично упорядоченное дерево

Рис.3.15. Частично упорядоченное дерево

Представление дерева в виде пирамиды наглядно показывает нам, что для такого дерева можно ввести понятия "начала" и "конца". Началом, естественно, будет считаться вершина пирамиды, а концом - крайний левый элемент в самом нижнем ряду (на рис.3.15 это 58).

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

Алгоритм вставки состоит в следующем. Новый элемент вставляется на первое свободное место за концом дерева (на рис.3.15 это место обозначено символом "*"). Если ключ вставленного элемента меньше, чем ключ его предка, то предок и вставленный элемент меняются местами. Ключ вставленного элемента теперь сравнивается с ключом его предка на новом месте и т.д. Сравнения заканчиваются, когда ключ нового элемента окажется больше ключа предка или когда новый элемент "выплывет" в вершину пирамиды. Пирамида, показанная на рис.3.15, построена именно последовательным включением в нее чисел из приведенного ряда. Если мы включим в нее, например, еще число 16, то пирамида примет вид, представленный на рис.3.16. (Символом "*" помечены элементы, перемещенные при этой операции.)
Частично упорядоченное дерево

Рис.3.16. Частично упорядоченное дерево, включение элемента

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

Рис.3.17. Частично упорядоченное дерево, исключение элемента

Упорядоченность дерева восстановлена, но нарушено условие его сбалансированности, так как свободное место находится не в конце дерева. Для восстановления сбалансированности последний элемент дерева переносится на освободившееся место, а затем "всплывает" по тому же алгоритму, который применялся при вставке. Результат такой балансировки показан на рис.3.17.б.

Прежде, чем описывать программный пример, иллюстрирующий сортировку частично упорядоченным деревом - пример 3.13, обратим внимание читателей на способ, которым в нем дерево представлено в памяти. Это способ представления двоичных деревьев в статической памяти (в одномерном массиве), который может быть применен и в других задачах. Элементы дерева располагаются в соседних слотах памяти по уровням. Самый первый слот выделенной памяти занимает вершина. Следующие 2 слота - элементы второго уровня, следующие 4 слота - третьего и т.д. Дерево с рис.3.17.б, например, будет линеаризовано таким образом:

               12 16 28 20 35 30 32 58

В таком представлении отпадает необходимость хранить в составе узла дерева указатели, так как адреса потомков могут быть вычислены. Для узла, представленного элементом массива с индексом i индексы его левого и правого потомков будут 2*i и 2*i+1 соответственно. Для узла с индексом i индекс его предка будет i div 2.

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

    * процедура InitST - инициализация модуля, установка начального значения nt;
    * функция InsertST - вставка в дерево нового элемента; функция возвращает false, если в дереве нет свободного места, иначе - true;
    * функция DeleteST - выборка из дерева минимального элемента; функция возвращает false, если дерево пустое, иначе - true;
    * функция CheckST - проверка состояния дерева: ключ минимального элемента возвращается в выходном параметре, но элемент не исключается из дерева; а возвращаемое значение функции - 0 - если дерево пустое, 1 - если дерево заполнено не до конца, 2 - если дерево заполнено до конца.

Кроме того в модуле определены внутренние программные единицы:

    * функция Down - обеспечивает спуск свободного места из вершины пирамиды в ее основание, функция возвращает индекс свободного места после спуска;
    * процедура Up - обеспечивающая всплытие элемента с заданного места.

{===== Программный пример 3.13 =====}
{ Сортировка частично упорядоченным деревом }
Unit SortTree;
Interface
Procedure InitSt;
Function CheckST(var a : integer) : integer;
Function DeleteST(var a : integer) : boolean;
Function InsertST(a : integer) : boolean;
Implementation
Const NN=16;
var tr : array[1..NN] of integer;  { дерево }
     nt : integer;     { индекс последнего эл-та в дереве }
{** Всплытие эл-та с места с индексом l **}
Procedure Up(l : integer);
   var h : integer; { l - индекс узла, h - индекс его предка }
       x : integer;
   begin
   h:=l div 2;   { индекс предка }
   while h > 0 do  { до начала  дерева }
     if tr[l] < tr[h] then begin { ключ узла меньше, чем у предка }
      x:=tr[l]; tr[l]:=tr[h]; tr[h]:=x; { перестановка }
      l:=h; h:=l div 2; { предок становится текущим узлом }
      end
    else h:=0; { конец всплытия }
end;  { Procedure Up }
{** Спуск свободного места из начала дерева **}
Function Down : integer;
   var h, l : integer; { h - индекс узла, l - индекс его потомка }
   begin
   h:=1;    { начальный узел - начало дерева }
   while true do begin
     l:=h*2;  { вычисление индекса 1-го потомка }
     if l+1 <= nt then begin { у узла есть 2-й потомок }
       if tr[l] <= tr[l+1] then begin { 1-й потомок меньше 2-го }
         tr[h]:=tr[l]; { 1-й потомок переносится в текущ.узел }
         h:=l;  { 1-й потомок становится текущим узлом }
         end
       else begin  { 2-й потомок меньше 1-го }
         tr[h]:=tr[l+1]; { 2-й потомок переносится в текущ.узел }
         h:=l+1; { 2-й потомок становится текущим узлом }
         end;
       end
     else
       if l=nt then begin { 1-й потомок есть, 2-го нет }
         tr[h]:=tr[l]; { 1-й потомок переносится в текущ.узел }
         Down:=l;  Exit; { спуск закончен }
         end
       else begin { потомков нет - спуск закончен }
         Down:=h; Exit;
         end;
     end; { while }
end; { Function Down }
{** Инициализация сортировки деревом **}
Procedure InitSt;
   begin
   nt:=0; { дерево пустое }
   end; { Procedure InitSt }
{** Проверка состояния дерева **}
Function CheckST(var a : integer) : integer;
   begin
   a:=tr[1];   { выборка эл-та из начала }
   case nt of  { формирование возвращаемого значения функции }
     0: { дерево пустое } CheckSt:=0;
     NN: { дерево полное } CheckSt:=2;
     else { дерево частично заполнено } CheckSt:=1;
     end;
   end; {  Function CheckST }
{** Вставка эл-та a в дерево **}
Function InsertST(a : integer) : boolean;
   begin
   if nt=NN then { дерево заполнено - отказ } InsertST:=false
   else begin { в дереве есть место }
     nt:=nt+1; tr[nt]:=a; { запись в конец дерева }
     Up(nt);              { всплытие }
     InsertSt:=true;
     end;
   end; { Function InsertST }
{** Выборка эл-та из дерева **}
Function DeleteST(var a : integer) : boolean;
   var n : integer;
   begin
   if nt=0 then { дерево пустое - отказ } DeleteST:=false
   else begin { дерево не пустое }
     a:=tr[1];  { выборка эл-та из начала }
     n:=Down;   { спуск свободного места в позицию n }
     if n < nt then begin
       { если свободное место спустилось не в конец дерева }
       tr[n]:=tr[nt]; { эл-т из конца переносится на своб.место }
       Up(n);         { всплытие }
       end;
     nt:=nt-1;
     DeleteSt:=true;
   end;
end; { Function DeleteST }
END.

Если применять сортировку частично упорядоченным деревом для упорядочения уже готовой последовательности размером N, то необходимо N раз выполнить вставку, а затем N раз - выборку. Порядок алгоритма - O(N*log2(N)), но среднее значение количества сравнений примерно в 3 раза больше, чем для турнирной сортировки. Но сортировка частично упорядоченным деревом имеет одно существенное преимущество перед всеми другими алгоритмами. Дело в том, что это самый удобный алгоритм для "сортировки on-line", когда сортируемая последовательность не зафиксирована до начала сортировки, а меняется в процессе работы и вставки чередуются с выборками. Каждое изменение (добавление/удаление элемента) сортируемой последовательности потребует здесь не более, чем 2*log2(N) сравнений и перестановок, в то время, как другие алгоритмы потребуют при единичном изменении переупорядочивания всей последовательности "по полной программе".

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

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

{===== Программный пример 3.14 =====}
{ Формирование отсортированных последовательностей в файле }
Uses SortTree;
var x : integar;   { считанное число }
     y : integer;   { число в вершине дерева }
     old : integer; { последнее выведенное число }
     inp : text;    { входной файл }
     out : text;    { выходной файл }
     bf : boolean;  { признак начала вывода последовательности }
     bx : boolean;  { рабочая переменная }
begin
Assign(inp,'STX.INP'); Reset(inp);
Assign(out,'STX.OUT'); Rewrite(out);
InitST;      { инициализация сортировки }
bf:=false;   { вывод последовательности еще не начат }
while not Eof(inp) do begin
   ReadLn(inp,x);  { считывание числа из файла }
   { если в дереве есть свободное место - включить в дерево }
   if CheckST(y) <= 1 then bx:=InsertST(x)
   else { в дереве нет свободного места }
     if (bf and (x < old)) or (not bf and (x < y))  then begin
       { вывод содержимого дерева }
       while DeleteST(y) do Write(out,y:3,' ');
       WriteLn(out);
       bf:=false;       { начало новой последовательности }
       bx:=InsertST(x); { занесение считанного числа в дерево }
       end
     else begin { продолжение формирования последовательности }
       if x < y then begin { вывод считанного числа }
         Write(out,x:3,' '); old:=x;
         end;
       else begin { вывод числа из вершины дерева }
         bx:=DeleteST(y);
         Write(out,y:3,' '); old:=y;
         { занесение считанного в дерево }
         bx:=InsertST(x);
         end;
       bf:=true; { вывод последовательности начался }
       end;
     end;
   Close(inp);
   { вывод остатка }
   while DeleteST(y) do Write(out,y:3,' ');
   WriteLn(out);  Close(out);
   end.

3.8.3. Сортировки распределением.

Поразрядная цифровая сортировка.

Алгоритм требует представления ключей сортируемой последовательности в виде чисел в некоторой системе счисления P. Число проходов сортировка равно максимальному числу значащих цифр в числе - D. В каждом проходе анализируется значащая цифра в очередном разряде ключа, начиная с младшего разряда. Все ключи с одинаковым значением этой цифры объединяются в одну группу. Ключи в группе располагаются в порядке их поступления. После того, как вся исходная последовательность распределена по группам, группы располагаются в порядке возрастания связанных с группами цифр. Процесс повторяется для второй цифры и т.д., пока не будут исчерпаны значащие цифры в ключе. Основание системы счисления P может быть любым, в частном случае 2 или 10. Для системы счисления с основанием P требуется P групп.

Порядок алгоритма качественно линейный - O(N), для сортировки требуется D*N операций анализа цифры. Однако, в такой оценке порядка не учитывается ряд обстоятельств.

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

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

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

Область памяти, занимаемая массивом перераспределяется между входным и выходным множествами, как это делалось и в ряде предыдущих примеров. Выходное множество (оно размещается в начале массива) разбивается на группы. Разбиение отслеживается в массиве b. Элемент массива b[i] содержит индекс в массиве a,с которого начинается i+1-ая группа. Номер группы определяется значением анали- зируемой цифры числа, поэтому индексация в массиве b начинается с 0. Когда очередное число выбирается из входного множества и должно быть занесено в i-ую группу выходного множества, оно будет записано в позицию, определяемую значением b[i]. Но предварительно эта позиция должна быть освобождена: участок массива от b[i] до конца выходного множества включительно сдвигается вправо. После записи числа в i-ую группу i-ое и все последующие значения в массиве b корректируются - увеличиваются на 1.

{===== Программный пример 3.15 =====}
{ Цифровая сортировка (распределение) }
const D=...;   { максимальное количество цифр в числе }
      P=...;   { основание системы счисления }
Function Digit(v, n : integer) : integer;
{ возвращает значение n-ой цифры в числе v }
begin
   for n:=n downto 2 do v:=v div P;
   Digit:=v mod P;
end;
Procedure Sort(var a : Seq);
   Var b : array[0..P-2] of integer; { индекс элемента,
                          следующего за последним в i-ой группе }
       i, j, k, m, x : integer;
   begin
     for m:=1 to D do begin   { перебор цифр, начиная с младшей }
     for i:=0 to P-2 do b[i]:=1; { нач. значения индексов }
     for i:=1 to N do begin   { перебор массива }
       k:=Digit(a[i],m);      { определение m-ой цифры }
       x:=a[i];
       { сдвиг - освобождение места в конце k-ой группы }
       for j:=i downto b[k]+1 do a[j]:=a[j-1];
       { запись в конец k-ой группы }
       a[b[k]]:=x;
       { модификация k-го индекса и всех больших }
       for j:=k to P-2 do b[j]:=b[j]+1;
       end;
end;

Результаты трассировки программного примера 3.15 при P=10 и D=4 представлены в таблице 3.9.
Быстрая сортировка

Таблица 3.9

Быстрая сортировка Хоара.

Данный алгоритм относится к распределительным и обеспечивает показатели эффективности O(N*log2(N)) даже при наихудшем исходном распределении.

Используются два индекса - i и j - с начальными значениями 1 и N соответственно. Ключ K[i] сравнивается с ключом K[j]. Если ключи удовлетворяют критерию упорядоченности, то индекс j уменьшается на 1 и производится следующее сравнение. Если ключи не удовлетворяют критерию, то записи R[i] и R[j] меняются местами. При этом индекс j фиксируется и начинает меняться индекс i(увеличиваться на 1 после каждого сравнения). После следующей перестановки фиксируется i и начинает изменяться j и т.д. Проход заканчивается, когда индексы i и j становятся равными. Запись, находящаяся на позиции встречи индексов, стоит на своем месте в последовательности. Эта запись делит последовательность на два подмножества. Все записи, расположенные слева от нее имеют ключи, меньшие чем ключ этой записи, все записи справа - большие. Тот же самый алгоритм применяется к левому подмножеству, а затем к правому. Записи подмножества распределяются по двум еще меньшим подмножествам и т.д., и т.д. Распределение заканчивается, когда полученное подмножество будет состоять из единственного элемента - такое подмножество уже является упорядоченным.

Процедура сортировки в примере 3.16 рекурсивная. При ее вызове должны быть заданы значения границ сортируемого участка от 1 до N.

{===== Программный пример 3.16 =====}
{ Быстрая сортировка Хоара }
Procedure Sort(var a : Seq; i0,j0 : integer);
{ i0, j0 - границы сортируемого участка }
Var i, j : integer;  { текущие индексы в массиве }
     flag : boolean;  { признак меняющегося индекса: если
             flag=true - уменьшается j, иначе - увеличивается i }
     x : integer;
begin
   if j0 <= i0 Exit; { подмножество пустое или из 1 эл-та }
   i:=i0; j:=j0;
   flag:=true; { вначале будет изменяться j }
   while i < j do begin
     if a[i] > a[j] then begin
       x:=a[i]; a[i]:=a[j]; a[j]:=x; { перестановка }
       { после перестановки меняется изменяемый индекс }
       flag:= not flag;
       end;
     { реально изменяется только один индекс }
     if flag then j:=j-1 else i:=i+1;
     end;
   Sort(a,i0,i-1); { сортировка левого подмассива }
   Sort(a,i+1,j0); { сортировка правого подмассива }
end;

Результаты трассировки примера приведены в таблице 3.10. В каждой строке таблицы показаны текущие положения индексов i и j, звездочками отмечены элементы, ставшие на свои места. Для каждого прохода показаны границы подмножества, в котором ведется сортировка.
Сортировки слиянием

Таблица 3.10

3.8.4. Сортировки слиянием.

Алгоритмы сортировки слиянием, как правило, имеют порядок O(N*log2(N)), но отличаются от других алгоритмов большей сложностью и требуют большого числа пересылок. Алгоритмы слияния применяются в основном, как составная часть внеш- ней сортировки, которая более подробно будет рассматриваться нами во втором томе нашего пособия. Здесь же для понимания принципа слияния мы приводим простейший алгоритм слияния в оперативной памяти.

Сортировка попарным слиянием.

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

Важнейшей частью алгоритма является слияние двух упорядоченных множеств. Эту часть алгоритма мы опишем строго.

    * 1. [Начальные установки]. Определить длины первого и второго исходных множеств - l1 и l2 соответственно. Установить индексы текущих элементов в исходных множествах i1 и i2 в 1. Установить индекс в выходном множестве j=1.
    * 2. [Цикл слияния]. Выполнять шаг 3 до тех пор, пока i1<=l1 и i2<=l2.
    * 3. [Сравнение]. Сравнить ключ i1-го элемента из 1-го исходного множества с ключом i2-го элемента из второго исходного множества. Если ключ элемента из 1-го множества меньше, то записать i1-ый элемент из 1-го множества на j-ое место в выходное множество и увеличить i1 на 1. Иначе - записать i2-ой элемент из 2-го множества на j-ое место в выходное множество и увеличить i2 на 1. Увеличить j на 1.
    * 4. [Вывод остатка]. Если i1<=l1, то переписать часть 1-го исходного множества от i1 до l1 включительно в выходное множество. Иначе - переписать часть 2-го исходного множества от i2 до l2 включительно в выходное множество.

Программный пример 3.17 иллюстрирует сортировку попарным слиянием в ее обменном варианте - выходные множества формируются на месте входных.

{===== Программный пример 3.17 =====}
{ Сортировка слиянием }
Procedure Sort(var a :Seq);
Var i0,j0,i,j,si,sj,k,ke,t,m : integer;
   begin
   si:=1;  { начальный размер одного множества }
   while si < N do begin
     { цикл пока одно множество не составит весь массив }
     i0:=1; { нач. индекс 1-го множества пары }
     while i0 < N do begin
       { цикл пока не пересмотрим весь массив }
       j0:=i0+si;   { нач. индекс 2-го множества пары }
       i:=i0; j:=j0;
       { размер 2-го множества пары может ограничиваться
         концом массива }
       if si > N-j0+1 then sj:=N-j0+1 else sj:=si;
       if sj > 0 then begin
         k:=i0; { нач. индекс слитого множества }
         while (i < i0+si+sj) and (j a[j] then begin
       { если эл-т 1-го <= элемента 2-го, он остается на
         своем месте, но вых.множество расширяется иначе -
         освобождается место в вых.множестве и туда заносится
         эл-т из 2-го множества }
             t:=a[j];
             for m:=j-1 downto k do a[m+1]:=a[m];
             a[k]:=t;
             j:=j+1; { к след. эл-ту во 2-м множестве }
             end;  { if a[i] > a[j] }
           k:=k+1; { вых. множество увеличилось }
           i:=i+1; { если был перенос - за счет сдвига, если
                    не было - за счет перехода эл-та в вых. }
           end; { while }
         end; { if sj > 0 }
       i0:=i0+si*2;  { начало следующей пары }
      end; { while i0 < N }
    si:=si*2;  { размер эл-тов пары увеличивается вдвое }
    end; { while si < N }
end;

Результаты трассировки примера приведены в таблице 3.11. Для каждого прохода показаны множества, которые на этом проходе сливаются. Обратите внимание на обработку последнего множества, оставшегося без пары.

0


Вы здесь » Форум МГОУ Прокопьевск » Московский Государственный Открытый Университет » СТРУКТУРЫ И АЛГОРИТМЫ ОБРАБОТКИ ДАННЫХ