Обработка списков: суммирование, вычисление произведения элементов списка, поиск минимального и максимального элементов.
Сумму элементов списка.
Решение будет напоминать подсчет количества элементов списка. Отличаться они будут шагом рекурсии. При подсчете количества элементов нам было неважно, чему равен первый элемент списка, мы просто добавляли единицу к уже подсчитанному количеству элементов хвоста. При вычислении суммы нужно будет учесть значение головы списка.
Так как в пустом списке элементов нет, сумма элементов пустого списка равна нулю. Для вычисления суммы элементов непустого списка нужно к сумме элементов хвоста добавить первый элемент списка. Запишем эту идею:
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).Последний вариант, в отличие от первого, реализует хвостовую рекурсию.
Минимальный(максимальный) элемент списка.
Как обычно, наше решение будет рекурсивным. Но так как для пустого списка понятие минимального элемента не имеет смысла, базис рекурсии мы запишем не для пустого, а для одноэлементного списка. В одноэлементном списке, естественно, минимальным элементом будет тот самый единственный элемент списка ("при всем богатстве выбора другой альтернативы нет!").
Шаг рекурсии: найдем минимум из первого элемента списка и минимального элемента хвоста — это и будет минимальный элемент всего списка.
Оформим эти рассуждения:
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 */Сортировка слияниями
Метод слияний — один из самых "древних" алгоритмов сортировки. Его придумал Джон фон Нейман еще в 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]). /* список упорядочен, если первые два элемента расположены в правильном порядке и список, образованный вторым элементом и хвостом исходного, упорядочен */Быстрая сортировка
Автором так называемой "быстрой" сортировки является Хоар. Он назвал ее быстрой потому, что в общем случае эффективность этого алгоритма довольно высока. Идея метода следующая. Выбирается некоторый "барьерный" элемент, относительно которого мы разбиваем исходный список на два подсписка. В один мы помещаем элементы, меньшие барьерного элемента, во второй — большие либо равные. Каждый из этих списков мы сортируем тем же способом, после чего приписываем к списку тех элементов, которые меньше барьерного, вначале сам барьерный элемент, а затем — список элементов не меньших барьерного. В итоге получаем список, состоящий из элементов, стоящих в правильном порядке.
При воплощении в программный код этой идеи нам, как обычно, понадобится пара предикатов.
Вспомогательный предикат 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 */