учебники, программирование, основы, введение в,

 

Сортировка списков

В этой лекции речь пойдет о списках, элементами которых являются числа. Хотя в большинстве задач, которые будут рассматриваться, неважно, к какому домену относятся элементы списка, для определенности будем считать, что это целые числа.
Таким образом, списки, с которыми мы планируем работать, могут быть представлены в разделе описания доменов примерно следующим образом:
DOMAINS
listI = integer*
Для разминки решим несложный пример.
Пример. Создадим предикат, позволяющий вычислить сумму элементов списка.
Решение будет напоминать подсчет количества элементов списка. Отличаться они будут шагом рекурсии. При подсчете количества элементов нам было неважно, чему равен первый элемент списка, мы просто добавляли единицу к уже подсчитанному количеству элементов хвоста. При вычислении суммы нужно будет учесть значение головы списка.
Так как в пустом списке элементов нет, сумма элементов пустого списка равна нулю. Для вычисления суммы элементов непустого списка нужно к сумме элементов хвоста добавить первый элемент списка. Запишем эту идею:
sum([], 0). /* сумма элементов пустого списка равна
нулю */
sum([H|T], S) :–
sum(T, S_T), /* S_T — сумма элементов хвоста */
S = S_T + H. /* S — сумма элементов исходного
списка */
Попробуйте самостоятельно изменить этот предикат так, чтобы он вычислял не сумму элементов списка, а их произведение.
Еще один вариант данного предиката можно написать, используя накопитель. В нем будем хранить уже насчитанную сумму. Начинаем с пустым накопителем. Переходя от вычисления суммы непустого списка к вычислению суммы элементов его хвоста, будем добавлять первый элемент к уже насчитанной сумме. Когда элементы списка будут исчерпаны (список опустеет), передадим "накопленную" сумму в качестве результата в третий аргумент.
Запишем:
sum_list([],S,S). /* список стал пустым, значит
в накопителе — сумма элементов
списка */
sum_list([H|T],N,S) :–
N_T=H+N,
/* N_T — результат добавления к сумме,
находящейся в накопителе, первого
элемента списка */
sum_list(T,N_T,S).
/* вызываем предикат от хвоста T и N_T */
Если нам нужно вызвать предикат от двух аргументов, а не от трех, то можно добавить вспомогательный предикат:
sum2(L,S):–
sum_list(L,0,S).
Последний вариант, в отличие от первого, реализует хвостовую рекурсию.
Разберем еще один простой пример.
Пример. Напишем предикат, вычисляющий среднее арифметическое элементов списка.
Конечно, можно, как всегда, опереться на рекурсию, но проще воспользоваться тем, что у нас уже есть предикаты, которые позволяют вычислить количество и сумму элементов списка. Для нахождения среднего нам достаточно будет сумму элементов списка разделить на их количество. Это можно записать следующим образом:
avg(L,A):–
summa(L,S), /* помещаем в переменную S сумму
элементов списка */
length(L,K), /* переменная K равна количеству
элементов списка */
A=S/K. /* вычисляем среднее как отношение суммы
к количеству */
Единственная проблема возникает при попытке найти среднее арифметическое элементов пустого списка. Если мы попытаемся вызвать цель avg([],A), то получим сообщение об ошибке "Division by zero" ("Деление на ноль"). Это произойдет, потому что предикат length([],K) конкретизирует переменную K нулем, а при попытке достижения третьей подцели A=S/K и произойдет вышеупомянутая ошибка. Можно посчитать это нормальной реакцией предиката. Раз в пустом списке нет элементов, значит, нет и их среднего арифметического. А можно изменить этот предикат так, чтобы он работал и с пустым списком.
Дабы обойти затруднение с пустым списком, добавим в нашу процедуру, в виде факта, информацию о том, что среднее арифметическое элементов пустого списка равно нулю. Полное решение будет выглядеть следующим образом.
avg([],0):–!.
avg(L,A):–
summa(L,S),
length(L,K),
A=S/K.
Описывая этот предикат в разделе описания предикатов PREDICATES, обратите внимание на то, что второй аргумент будет не целого типа, а вещественного (при делении одного целого числа на другое целое число частное может получиться нецелым).
Пример. Создадим предикат, находящий минимальный элемент списка.
Как обычно, наше решение будет рекурсивным. Но так как для пустого списка понятие минимального элемента не имеет смысла, базис рекурсии мы запишем не для пустого, а для одноэлементного списка. В одноэлементном списке, естественно, минимальным элементом будет тот самый единственный элемент списка ("при всем богатстве выбора другой альтернативы нет!").
Шаг рекурсии: найдем минимум из первого элемента списка и минимального элемента хвоста — это и будет минимальный элемент всего списка.
Оформим эти рассуждения:
min_list([X],X). /* единственный элемент одноэлементного
списка является минимальным элементом
списка */
min_list([H|T],M):–
min_list(T,M_T), /* M_T — минимальный элемент хвоста */
min(H,M_T,M). /* M — минимум из M_T и первого элемента
исходного списка */
Обратите внимание на то, что в правиле, позволяющем осуществить шаг рекурсии, использован предикат min, подобный предикату max, который был разобран нами в третьей лекции.
Слегка модифицировав предикат min_list (подставив в правило вместо предиката min предикат max и поменяв его название), получим предикат, находящий не минимальный, а максимальный элемент списка.
Перейдем теперь к более интересной задаче, а именно, к сортировке списков. Под сортировкой обычно понимают расстановку элементов в некотором порядке. Для определенности мы будем упорядочивать элементы списков по неубыванию. То есть, если сравнить любые два соседних элемента списка, то следующий должен быть не меньше предыдущего.
Существует множество алгоритмов сортировки. Заметим, что имеется два класса алгоритмов сортировки: сортировка данных, целиком расположенных в основной памяти (внутренняя сортировка), и сортировка файлов, хранящихся во внешней памяти (внешняя сортировка). Мы займемся исключительно методами внутренней сортировки.
Рассмотрим наиболее известные методы внутренней сортировки и выясним, как можно применить их для сортировки списков в Прологе.
Начнем с наиболее известного "пузырькового" способа сортировки. Его еще называют методом прямого обмена или методом простого обмена.

Пузырьковая сортировка
Идея этого метода заключается в следующем. На каждом шаге сравниваются два соседних элемента списка. Если оказывается, что они стоят неправильно, то есть предыдущий элемент меньше следующего, то они меняются местами. Этот процесс продолжаем до тех пор, пока есть пары соседних элементов, расположенные в неправильном порядке. Это и будет означать, что список отсортирован.
Аналогия с пузырьком вызвана тем, что при каждом проходе минимальные элементы как бы "всплывают" к началу списка.
Реализуем пузырьковую сортировку посредством двух предикатов. Один из них, назовем его permutation, будет сравнивать два первых элемента списка и в случае, если первый окажется больше второго, менять их местами. Если же первая пара расположена в правильном порядке, этот предикат будет переходить к рассмотрению хвоста.
Основной предикат bubble будет осуществлять пузырьковую сортировку списка, используя вспомогательный предикат permutation.
permutation([X,Y|T],[Y,X|T]):–
X>Y,!.
/* переставляем первые два
элемента, если первый больше
второго */
permutation([X|T],[X|T1]):–
permutation(T,T1).
/*переходим к перестановкам
в хвосте*/
bubble(L,L1):–
permutation(L,LL), /* вызываем предикат,
осуществляющий перестановку */
!,
bubble(LL,L1). /* пытаемся еще раз отсортировать
полученный список */
bubble(L,L). /* если перестановок не было, значит список
отсортирован */
Но наш пузырьковый метод работает только до тех пор, пока есть хотя бы пара элементов списка, расположенных в неправильном порядке. Как только такие элементы закончились, предикат permutation терпит неудачу, а bubble переходит от правила к факту и возвращает в качестве второго аргумента отсортированный список.
Сортировка вставкой
Теперь рассмотрим сортировку вставкой. Она основана на том, что если хвост списка уже отсортирован, то достаточно поставить первый элемент списка на его место в хвосте, и весь список будет отсортирован. При реализации этой идеи создадим два предиката.
Задача предиката insert — вставить значение (голову исходного списка) в уже отсортированный список (хвост исходного списка), так чтобы он остался упорядоченным. Его первым аргументом будет вставляемое значение, вторым — отсортированный список, третьим — список, полученный вставкой первого аргумента в нужное место второго аргумента так, чтобы не нарушить порядок.
Предикат ins_sort, собственно, и будет организовывать сортировку исходного списка методом вставок. В качестве первого аргумента ему дают произвольный список, который нужно отсортировать. Вторым аргументом он возвращает список, состоящий из элементов исходного списка, стоящих в правильном порядке.
ins_sort([ ],[ ]). /* отсортированный пустой список
остается пустым списком */
ins_sort([H|T],L):–
ins_sort(T,T_Sort),
/* T — хвост исходного списка,
T_Sort — отсортированный хвост
исходного списка */
insert(H,T_Sort,L).
/* вставляем H (первый элемент
исходного списка)в T_Sort,
получаем L (список, состоящий
из элементов исходного списка,
стоящих по неубыванию) */
insert(X,[],[X]). /* при вставке любого значения в пустой
список, получаем одноэлементный
список */
insert(X,[H|T],[H|T1]):–
X>H,!, /* если вставляемое значение
больше головы списка, значит
его нужно вставлять в хвост */
insert(X,T,T1).
/* вставляем X в хвост T,
в результате получаем
список T1 */
insert(X,T,[X|T]). /* это предложение (за счет отсечения
в предыдущем правиле) выполняется,
только если вставляемое значение
не больше головы списка T, значит,
добавляем его первым элементом
в список T */
Сортировка выбором
Идея алгоритма сортировки выбором очень проста. В списке находим минимальный элемент (используя предикат min_list, который мы придумали в начале этой лекции). Удаляем его из списка (с помощью предиката delete_one, рассмотренного в предыдущей лекции). Оставшийся список сортируем. Приписываем минимальный элемент в качестве головы к отсортированному списку. Так как этот элемент был меньше всех элементов исходного списка, он будет меньше всех элементов отсортированного списка. И, следовательно, если его поместить в голову отсортированного списка, то порядок не нарушится.
Запишем:
choice([ ],[ ]). /* отсортированный пустой список
остается пустым списком */
choice(L,[X|T]):– /* приписываем X (минимальный элемент
списка L) к отсортированному
списку T */
min_list(L,X), /* X — минимальный элемент
списка L */
delete_one(X,L,L1),
/* L1 — результат удаления
первого вхождения
элемента X из списка L */
choice(L1,T). /* сортируем список L1,
результат обозначаем T */

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

Автором так называемой "быстрой" сортировки является Хоар. Он назвал ее быстрой потому, что в общем случае эффективность этого алгоритма довольно высока. Идея метода следующая. Выбирается некоторый "барьерный" элемент, относительно которого мы разбиваем исходный список на два подсписка. В один мы помещаем элементы, меньшие барьерного элемента, во второй — большие либо равные. Каждый из этих списков мы сортируем тем же способом, после чего приписываем к списку тех элементов, которые меньше барьерного, вначале сам барьерный элемент, а затем — список элементов не меньших барьерного. В итоге получаем список, состоящий из элементов, стоящих в правильном порядке.
При воплощении в программный код этой идеи нам, как обычно, понадобится пара предикатов.
Вспомогательный предикат partition будет отвечать за разбиение списка на два подсписка. У него будет четыре аргумента. Первые два элемента — входные: первый — исходный список, второй — барьерный элемент. Третий и четвертый элементы — выходные, соответственно, список элементов исходного списка, которые меньше барьерного, и список, состоящий из элементов, которые не меньше барьерного элемента.
Предикат quick_sort будет реализовывать алгоритм быстрой сортировки Хоара. Он будет состоять из двух предложений. Правило будет осуществлять с помощью предиката partition разделение непустого списка на два подсписка, затем сортировать каждый из этих подсписков рекурсивным вызовом себя самого, после чего, используя предикат conc (созданный нами ранее), конкретизирует второй аргумент списком, получаемым объединением отсортированного первого подсписка и списка, сконструированного из барьерного элемента (головы исходного списка) и отсортированного второго подсписка. Запишем это:

quick_sort([],[]). /* отсортированный пустой список
                      остается пустым списком */
quick_sort([H|T],O):–
                partition(T,H,L,G),
                        /* делим список T на L (список
                           элементов меньших барьерного
                           элемента H) и G (список
                           элементов не меньших H) */
                quick_sort(L,L_s),
                        /* список L_s — результат
                           упорядочивания элементов
                           списка L */
                quick_sort(G,G_s),
                        /* аналогично, список G_s —
                           результат упорядочивания
                           элементов списка G */
                conc(L_s,[H|G_s],O).
                        /* соединяем список L_s со
                           списком, у которого голова H,
                           а хвост G_s, результат
                           обозначаем O */
partition([],_,[],[]). /* как бы мы ни делили элементы
                          пустого списка, ничего кроме
                          пустых списков не получим */
partition([X|T],Y,[X|T1],Bs):–
                X<Y,!,
                partition(T,Y,T1,Bs).
                     /* если элемент X меньше барьерного
                        элемента Y, то мы добавляем его
                        в третий аргумент */
partition([X|T],Y,T1,[X|Bs]):–
                partition(T,Y,T1,Bs).
                     /* в противном случае дописываем
                        его в четвертый аргумент */

Прежде чем перейти к изучению следующего алгоритма сортировки, решим одну вспомогательную задачу.
Пусть у нас есть два упорядоченных списка, и мы хотим объединить их элементы в один список так, чтобы объединенный список также остался отсортированным.
Идея реализации предиката, осуществляющего слияние двух отсортированных списков с сохранением порядка, довольно проста. Будем на каждом шаге сравнивать головы наших упорядоченных списков и ту из них, которая меньше, будем переписывать в результирующий список. И так до тех пор, пока один из списков не закончится. Когда один из списков опустеет, нам останется дописать остатки непустого списка к уже построенному итогу. В результате получим список, состоящий из элементов двух исходных списков, причем элементы его расположены в нужном нам порядке.
Создадим предикат (назовем его fusion) реализующий приведенное описание. Так как мы не знаем, какой из списков опустеет раньше, нам необходимо "гнать" рекурсию сразу по обоим базовым спискам. У нас будет два факта — основания рекурсии, которые будут утверждать, что если мы сливаем некий список с пустым списком, то в итоге получим, естественно, тот же самый список. Причем этот факт имеет место и в случае, когда первый список пуст, и в случае, когда пуст второй список.
Шаг рекурсии нам дадут два правила: первое будет утверждать, что если голова первого списка меньше головы второго списка, то именно голову первого списка и нужно дописать в качестве головы в результирующий список, после чего перейти к слиянию хвоста первого списка со вторым. Результат этого слияния будет хвостом итогового списка. Второе правило, напротив, будет дописывать голову второго списка в качестве головы результирующего списка, сливать первый список с хвостом второго списка. Итог этого слияния будет хвостом объединенного списка.

fusion(L1,[ ],L1):–!. /* при слиянии списка L1 с пустым
                         списком получаем список L1 */
fusion([ ],L2,L2):–!. /* при слиянии пустого списка
                         со списком L2 получаем список
                         L2 */
fusion([H1|T1],[H2|T2],[H1|T]):–
                   H1<H2,!,
                     /* если голова первого списка H1
                        меньше головы второго списка H2 */
                   fusion(T1, [H2|T2],T).
                     /* сливаем хвост первого списка T1
                        со вторым списком [H2|T2] */
fusion(L1, [H2|T2],[H2|T]):–
                   fusion(L1,T2,T).
                     /* сливаем первый список L1
                        с хвостом второго списка T2 */

Теперь можно перейти к изучению алгоритма сортировки слияниями.


Сортировка слияниями
Метод слияний — один из самых "древних" алгоритмов сортировки. Его придумал Джон фон Нейман еще в 1945 году. Идея этого метода заключается в следующем. Разобьем список, который нужно упорядочить, на два подсписка. Упорядочим каждый из них этим же методом, после чего сольем упорядоченные подсписки обратно в один общий список.
Для начала создадим предикат, который будет делить исходный список на два. Он будет состоять из двух фактов и правила. Первый факт будет утверждать, что пустой список можно разбить только на два пустых подсписка. Второй факт будет предлагать разбиение одноэлементного списка на тот же одноэлементный список и пустой список. Правило будет работать в случаях, не охваченных фактами, т.е. когда упорядочиваемый список содержит не менее двух элементов. В этой ситуации мы будем отправлять первый элемент списка в первый подсписок, второй элемент — во второй подсписок, и продолжать распределять элементы хвоста исходного списка.
splitting([],[],[])./* пустой список можно расщепить
только на пустые подсписки */
splitting([H],[H],[]). /* одноэлементный список разбиваем
на одноэлементный список
и пустой список */
splitting([H1,H2|T],[H1|T1],[H2|T2]):–
splitting(T,T1,T2).
/* элемент H1 отправляем в первый
подсписок, элемент H2 — во второй
подсписок, хвост T разбиваем
на подсписки T1 и T2 */
Теперь можно приступать к записи основного предиката, который, собственно, и будет осуществлять сортировку списка. Он будет состоять из трех предложений. Первое будет декларировать очевидный факт, что при сортировке пустого списка получается пустой список. Второе утверждает, что одноэлементный список также уже является упорядоченным. В третьем правиле будет содержаться суть метода сортировки слиянием. Вначале список расщепляется на два подсписка с помощью предиката splitting, затем каждый из них сортируется рекурсивным вызовом предиката fusion_sort, и, наконец, используя предикат fusion, сливаем полученные упорядоченные подсписки в один список, который и будет результатом упорядочивания элементов исходного списка.
Запишем изложенные выше соображения.
fusion_sort([],[]):–!./* отсортированный пустой список
остается пустым списком */
fusion_sort([H],[H]):–!. /* одноэлементный список
упорядочен */
fusion_sort(L,L_s):–
splitting(L,L1,L2),
/* расщепляем список L на два
подсписка */
fusion_sort(L1,L1_s),
/* L1_s — результат сортировки
L1 */
fusion_sort(L2,L2_s),
/* L2_s — результат сортировки
L2 */
fusion(L1_s,L2_s,L_s).
/* сливаем L1_s и L2_s
в список L_s */
Фактически этот алгоритм при прямом проходе дробит список на одноэлементные подсписки, после чего на обратном ходе рекурсии сливает их двухэлементные списки. На следующем этапе сливаются двухэлементные списки и т.д. На последнем шаге два подсписка сливаются в итоговый, отсортированный список.
В качестве завершения темы сортировки разработаем предикат, который будет проверять, является ли список упорядоченным. Это совсем не сложно. Для того чтобы список был упорядоченным, он должен быть либо пустым, либо одноэлементным, либо любые два его соседних элемента должны быть расположены в правильном порядке. Запишем эти рассуждения.
sorted([ ]). /* пустой список отсортирован */
sorted([_])./* одноэлементный список упорядочен */
sorted([X,Y|T]):–
X<=Y,
sorted([Y|T]).
/* список упорядочен, если первые
два элемента расположены
в правильном порядке и список,
образованный вторым элементом
и хвостом исходного, упорядочен */

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика