Professional Documents
Culture Documents
М.Ю. Галкина
ФУНКЦИОНАЛЬНОЕ И ЛОГИЧЕСКОЕ
ПРОГРАММИРОВАНИЕ
Методические указания
Новосибирск 2008
УДК 681.3.06
2
ОГЛАВЛЕНИЕ
Введение. Классификация языков программирования....................................4
Глава 1 Функциональное программирование. Основы языка Лисп.......................6
1.1 Типы данных в Лиспе............................................................................................7
1.2 Функции..................................................................................................................8
1.3 Предикаты............................................................................................................11
1.4 Псевдофункции SET и SETQ.............................................................................13
1.5 Интерпретатор языка Лисп EVAL.....................................................................14
1.6 Определение функций пользователем..............................................................15
1.7 Функции ввода-вывода и работа с файлами.....................................................17
1.8 Последовательные вычисления..........................................................................19
1.9 Разветвление вычислений...................................................................................20
1.10 Рекурсия.............................................................................................................21
1.11 Внутреннее представление s-выражений........................................................34
1.12 Точечная пара....................................................................................................37
1.13 Функционалы.....................................................................................................39
Глава 2 Логическое программирование. Основы языка Пролог..........................43
2.1 Факты и правила..................................................................................................45
2.2 Поиск решений Пролог-системой......................................................................48
2.3 Структура программы Турбо-Пролога..............................................................50
2.4 Рекурсия...............................................................................................................55
2.5 Семантика Пролога.............................................................................................57
2.6 Внелогические предикаты управления поиском решений..............................60
2.7 Списки..................................................................................................................65
2.8 Решение логических задач с использованием списков....................................72
2.9 Строки...................................................................................................................76
2.10 Предикаты для работы с файлами...................................................................78
2.11 Динамические базы данных.............................................................................82
2.12 Экранные окна и создание меню.....................................................................86
2.13 Операции над структурами данных................................................................89
2.14 Стратегии решения задач..................................................................................97
Расчетно-графическое задание.................................................................................99
Рекомендуемая литература.....................................................................................106
3
Введение. Классификация языков программирования
4
В определенном смысле использование рекурсивных функций “экономит
мышление” – позволяет компактно записывать решение задачи. Кроме того,
часто весьма трудные для алгоритмических языков задачи с помощью
рекурсивных функций естественно и легко формулируются и изящно
решаются. В качестве примера рассмотрим известную задачу о Ханойских
башнях. Эту задачу придумали буддийские монахи. Они верили, что время
решения этой задачи для 64 дисков, соответствует времени наступления конца
света. Задача заключается в следующем. Имеется три вертикальных стержня A,
B, C и совокупность n круглых дисков различного диаметра с отверстием. В
исходном состоянии диски нанизаны по порядку в соответствии со своим
размером на диск A.
5
декларативных языках описываются объекты предметной области, зависимости
между ними и цель задачи, а решение задачи находится автоматически.
Представителями логических языков являются Пролог, Плэннер, Mandala.
Первое время программирование на Лиспе и Прологе будет похоже на
игру в волейбол одной рукой и будет вызывать чувство протеста. Но затем это
чувство заменится на восхищение мощностью и лаконичностью этих языков.
6
синтаксической ошибкой в Лиспе. Часто встречаются ошибки, когда
выражение для определения функции выглядит “осмысленно”, но семантика
этого выражения не совпадает с тем, что в него хотели вложить. Существует
даже шутливое толкование названия языка: Lots of Idiotic Silly Parentheses.
Неудобством языка является то, что существует много диалектов и, к
сожалению, ни один из них не принят в качестве стандарта. Все реализации
языка являются интерпретаторами, т.е. любая команда сразу обрабатывается.
Далее мы рассмотрим muLisp – один из самых удачных диалектов языка,
созданный фирмой Soft WareHouse Inc (США).
Для запуска интерпретатора следует запустить файл mulisp.com. На экране
появится значок $ - приглашение ввести команду. Когда пользователь
заканчивает ввод вызова функции или выражения, то интерпретатор сразу же
вычисляет значение этого выражения, выдает на экран это значение и
очередное приглашение для ввода команды. Для повторения команды,
содержащейся в предыдущей строке достаточно нажать функциональную
клавишу F3. Для завершения работы следует ввести команду (system).
7
Все структуры данных в Лиспе строятся из атомов. Списком называется
упорядоченная последовательность, элементами которой являются атомы или
списки. Список заключается в скобки, а элементы разделяются пробелами.
Примеры:
1. (a) – список из одного элемента;
2. () или nil – пустой список;
3. (my house) has (big windows)) – список из трех элементов.
Часто бывает удобно не различать атомы и списки. В этом случае говорят
о символьных выражениях (s–выражениях).
Про точечные пары будет изложено дальше.
1.2 Функции
8
− Взятие модуля. Обозначается как ABS и имеет один аргумент. Например, |a|
на Лиспе будет иметь вид (ABS a).
Суперпозиция функций всегда вычисляется “изнутри наружу”.
Примеры: Запишем на Лиспе следующие выражения:
1. (1+2)⋅(3+4)
2. (a+b)/2
2
3. x +2x-5
В первом случае, получаем (* (+ 1 2) (+ 3 4)), во втором – (/ (+ a b)), в
третьем – (+ (* x x) (* 2 x) –5).
9
5. (CONS nil nil)) → (nil).
Если второй аргумент функции CONS не является списком, то образуется
так называемая точечная пара. О ней речь пойдет позже.
Пример: (CONS 1 2)) → (1.2).
Можно заметить, что функции CAR и CDR являются обратными для
CONS.
Примеры:
1. (CONS (CAR ‘(1 2 3)) (CDR ‘(1 2 3)) → (1 2 3);
2. (CAR (CONS 1 ‘(2 3)) → 1;
3. (CDR (CONS 1 ‘(2 3))) → (2 3).
Функция LIST создает новый список, элементами которого являются
аргументы функции. Результатом работы функции будет список. Вызов
функции имеет вид:
(LIST s1 … sn), где si – s-выражение.
Примеры:
1. (LIST ‘a ‘b ‘(+ 1)) → (a b (+ 1));
2. (LIST ‘((a)) nil) → (((a)) nil).
Для любой функции LIST можно составить эквивалентную композицию
функций CONS. Для примера 1 это будет
(CONS ‘a (CONS ‘b (CONS ‘(+ 1) nil))).
Функция APPEND создает новый список, элементами которого являются
элементы списков - аргументов функции. Результатом работы функции будет
список. Вызов функции имеет вид:
(APPEND sp1 … spn), где spi – список.
Примеры:
1. (APPEND ‘(1 2) ‘(3) ‘(+ *)) → (1 2 3 + *));
2. (APPEND ‘((1)) ‘()) →((1)).
Функция LAST создает список из одного элемента – последнего элемента
списка - аргумента функции. Результатом работы функции будет список. Вызов
функции имеет вид:
(LAST список).
Примеры:
1. (LAST ‘(1 2 3)) → (3);
2. (LAST ‘()) → (nil).
Функция REVERSE возвращает список, являющийся “перевернутым”
списком – аргументом. Перестановка элементов осуществляется только на
верхнем уровне. Результатом работы функции будет список. Вызов функции
имеет вид:
(REVERSE список).
10
Примеры:
1. (REVERSE ‘(1 2 3)) → (3 2 1);
2. (REVERSE ‘(1 (2 3) ((4)))) → (((4)) (2 3) 1);
3. (REVERSE ‘()) → nil.
1.3 Предикаты
11
2. (NUMBERP ‘t) → nil.
Предикат NULL возвращает значение t, если аргумент является пустым
списком. Вызов предиката имеет вид:
(NULL s-выражение).
Примеры:
1. (NULL ‘(5 6)) → nil;
2. (NULL (ATOM ‘(1 2)) → t;
3. (NULL (LIST ‘(1 2)) → nil.
Рассмотрим еще несколько предикатов, аргументами которых являются
числа.
Предикат = возвращает значение t, если все аргументы – числа равны
между собой. Вызов предиката имеет вид:
(= n1 …nm), где ni - число.
Примеры:
1. (= 1 1 2) → nil;
2. (= 4 4) → t.
Предикат < возвращает значение t, если все аргументы – числа
упорядочены в порядке возрастания. Вызов предиката имеет вид:
(< n1 …nm), где ni - число.
Этот предикат можно использовать для проверки попадания числового
значения в заданный диапазон.
Примеры:
1. (< 1 1 2) → nil;
t , если значение x попадает в интервал (1;2);
2. (< 1 x 2) →
nil, если значение x не попадает в интервал (1;2);
3. (< 3 6 8 9) → t.
Аналогично определяются предикаты: > (проверка аргументов – чисел на
упорядоченность по убыванию); ≤ (проверка аргументов – чисел на
упорядоченность по неубыванию); ≥ (проверка аргументов – чисел на
упорядоченность по невозрастанию); /= (проверка аргументов – чисел на
неравенство всех пар рядом стоящих аргументов).
Примеры:
1. (> 4 1 -5) → t;
2. (<= 3 3 8 9) → t;
3. (>= 9 9 5 5) → t;
4. (/= 1 4 1) → t.
Для сравнения s-выражений используется предикаты EQUAL и EQ.
Предикат EQUAL возвращает значение t, если совпадают внешние структуры
s-выражений. Вызов предиката имеет вид:
12
(EQUAL s1 s2), где si (i=1,2) – s-выражение.
Предикат EQ возвращает значение t, если совпадают внешние структуры
s-выражений и их физические адреса. При использовании EQ следует
помнить, что одноименные переменные всегда хранятся в одном месте, а
списки – нет. Кроме того, малые целые числа (меньшие 65536 по абсолютной
величине) определяются уникально, а большие целые числа (большие 65536 по
абсолютной величине) и дробные числа не определяются уникально. Вызов
предиката EQ производится аналогично вызову EQUAL.
Примеры:
1. (EQUAL a a) → t;
2. (EQUAL ‘(a b) ‘(a b)) → t;
3. (EQUAL ‘(a) a) → nil;
4. (EQ a a) → t;
5. (EQ 100000 100000) → nil;
6. (EQ ‘(a) ‘(a)) → nil; Здесь список (a) создан дважды и хранится по разным
адресам;
7. (EQ 2.25 2.25) → nil.
Заметим, что функции EQUAL и EQ иногда называют примитивными
функциями сопоставления с образцом.
13
(SET (car a) 3) → 3, побочный эффект – связывание: b → 3;
(SET b 4) → ошибка, не может выполниться связывание: 3 → 4 (символ b
получил значение 3 в предыдущей строке);
Для изменения значения b следовало обратиться к функции следующим
образом: (SET ‘b 4). Тогда результатом работы функции будет 4, а побочным
эффектом – связывание b → 4.
В случае если вычислять значение символа – первого аргумента не
требуется, то вместо того, чтобы перед символом писать ‘, можно использовать
функцию SETQ. Об автоматическом блокировании вычисления первого
аргумента напоминает буква Q (от QUOTE) в имени функции.
Функция SETQ имеет четное количество аргументов. Аргументы с
нечетными номерами должны быть символами, а с четными – s-выражениями.
Функция вычисляет значения аргументов с четными номерами и возвращает
значение последнего вычисленного s-выражения. Побочным эффектом работы
этой функции является связывание символов - аргументов с нечетными
номерами со значениями вычисленных s-выражений. Вызов функции имеет
вид:
(SETQ p1 s1 … pn sn), где pi-символ, si-s-выражение.
Пример: Рассмотрим пример работы предиката SETQ:
(SETQ a 1 b 2 c a) → 1, побочный эффект – связывания:
a → 1, b → 2, c → 1.
Все образовавшиеся связи действительны в течение всего сеанса работы с
интерпретатором Лиспа. Если необходимо разорвать все образовавшиеся связи,
следует выполнить команду (RESTART).
Интерпретатор Лиспа называется EVAL и его можно так же, как и другие
функции вызывать из программы. “Лишний” вызов интерпретатора может,
например, снять эффект блокировки вычисления от функции QUOTE или
найти значение значения выражения, т.е. осуществить двойное вычисление.
Функция EVAL вычисляет значение значения аргумента и возвращает его.
Вызов функции имеет вид:
(EVAL s-выражение).
Примеры:
1. (EVAL ‘(+ 4 8)) → 12;
2. (SETQ x ‘(a b c)) → (a b c), побочный эффект – связывание: x → (a b c);
(EVAL ‘x) → (a b c);
(EVAL x) → ошибка, т.к. сначала вычисляется значение x, а потом делается
попытка вычислить функцию с именем a;
3.(SETQ a ‘b) → b, побочный эффект – связывание: a → b;
(SETQ b ‘c) → c, побочный эффект – связывание: b → c;
14
(EVAL a) → c, вычисляется значение значения a, т.е. значение b;
(EVAL ‘a) → b.
4.(SETQ a 3) → 3, побочный эффект – связывание: a → 3;
(SETQ b ‘a) → a, побочный эффект – связывание: b → a;
(EVAL b) → 3, вычисляется значение значения a;
(SETQ a 4) → 4, побочный эффект – связывание: a → 4;
(EVAL b) → 4, вычисляется значение значения b, т.е. значение a;
(EVAL ‘b) → a.
Для функции EVAL нет аналогий в процедурных языках
программирования. Используя EVAL, мы можем выполнить “оператор”,
который создан Лисп-программой и который может меняться в процессе
выполнения программы. Лисп позволяет с помощью одних функций
формировать определения других функций, программно анализировать и
редактировать эти определения как s-выражения, а затем , используя функцию
EVAL, исполнять их.
Диалог с интерпретатором языка Лисп на самом верхнем, командном,
уровне можно описать простым циклом:
(PRINT ‘$) % На экран выводится приглашение;
(PRINT (EVAL (READ))) % Ввод выражения с клавиатуры, вычисление его
значения и вывод этого значения на экран
(PRINT ‘$) % Повторение вывода приглашения.
………………
16
(NEWFUN ‘SUM ‘(LAMBDA (x y) (+ (* x x) (* y y)))) → SUM
Теперь можно обратиться к функции SUM:
(SUM 2 3) → 13 (22+32)
3. Определим функцию без аргументов PI, которая приближенно вычисляет
число π:
(DEFUN PI() 3.141592) → pi;
Обращение к функции:
(PI) → 3.141592.
17
указанным параметром именем для вывода, и nil в противном случае. Открытие
файла – побочный эффект работы этих функций. Вызов функций имеет вид:
(OPEN-INPUT-FILE ‘имя_файла),
(OPEN-OUTPUT-FILE ‘имя_файла).
Файл ищется в текущей папке, откуда был запущен Лисп. Для доступа к
файлу, находящемуся во вложенной в текущую папку папке, следует указать
путь от текущей папки. В остальных случаях следует набрать полный путь,
заменив каждый \, встречающийся в описанном пути, на \\.
После окончания работы с файлами их следует закрыть с помощью
функций CLOSE-INPUT-FILE или CLOSE-OUTPUT-FILE, обращение к
которым имеет вид:
(CLOSE-INPUT-FILE ‘имя_файла),
(CLOSE-OUTPUT-FILE ‘имя_файла).
Для ввода с клавиатуры (по умолчанию) или из открытого для ввода
файла, используется функция READ. Функция возвращает значение введенного
s-выражения. Вызов функции имеет вид:
(READ [‘имя_открытого_для_ввода_файла]).
Параметр у функции READ является необязательным. Если он не указан,
то ввод производится либо с последнего назначенного устройства, либо по
умолчанию с клавиатуры. Для перенаправления ввода из файла на ввод с
клавиатуры на месте параметра используют слово keyboard.
Для ввода значений в переменную используют композицию функций
SETQ и READ:
(SETQ переменная (READ[‘имя_открытого_для_ввода_файла])).
Мы уже видели, что все значения, возвращаемые функциями, и значения
переменных, набранных в командной строке, автоматически выводятся на
экран. Если обращение к функции производится из файла, который
обрабатывается интерпретатором, то для вывода возвращаемых значений на
устройство вывода используют функции PRINT, PRINC.
Функция PRINT возвращает значение аргумента и, в качестве побочного
эффекта, выводит на экран или в открытый для вывода файл это значение и
осуществляет переход на следующую строку. Вызов функции имеет вид:
(PRINT S [‘имя_открытого_для_вывода_файла]), где S – s-выражение,
функция или строка, заключенная в кавычки.
Функция PRINC возвращает значение аргумента и, в качестве побочного
эффекта, выводит на экран или в открытый для вывода файл это значение.
Вызов функции имеет вид:
(PRINC S [‘имя_открытого_для_вывода_файла]), где S – s-выражение,
функция или строка, заключенная в кавычки.
Параметр у функций PRINT и PRINC так же является необязательным.
Если он не указан, то вывод производится либо на последнее назначенное
устройство, либо по умолчанию на экран. Для перенаправления вывода из
файла на экран на месте параметра используют слово screen.
Вывод строки сопровождается заключением ее в специальные символы: ||.
Для того чтобы убрать эти специальные символы, следует установить значение
18
специальной системной переменной print-escape равным nil с помощью
функции SETQ: (SETQ *print-escape* nil).
Функция TERPRI всегда возвращает nil и, в качестве побочного эффекта,
осуществляет переход на следующую строку (на экране или в открытом для
вывода файле) указанное значением параметрa количество раз. Вызов функции
имеет вид:
(TERPRI вычислимое_выражение [‘имя_открытого_для_вывода_файла]).
Если не был открыт файл для вывода, функции PRINT, PRINC и TERPRI
осуществляют вывод на экран.
Пример:
Пусть с клавиатуры вводятся два числа и знак операции. Следует вывести
в файл сформированное для вычисления выражение и результат вычисления.
(TERPRI 2) ; Перевод строки 2 раза
(SETQ *print-escape* nil) ; Запрет выводить специальные символы
(PRINT “vvedite chislo znak chislo”) ; Приглашение ввести число, знак, число
(SETQ a (READ) zn (READ) b (READ)) ; Ввод данных с клавиатуры
(SETQ rez (EVAL (LIST zn a b))) ; Формируется выражение для вычисления в
прединфиксной форме, вычисляется и результат вычислений связывается с
переменной rez
(OPEN-OUTPUT-FILE ‘itog.txt) ; Открывается файл для записи результатов
работы
(TERPRI 3) ; Перевод строки в файле 3 раза
(PRINC a) ; Вывод в файл первого числа
(PRINC zn) ; Вывод в файл знака операции
(PRINC b) ; Вывод в файл второго числа
(PRINC “=”) ; Вывод в файл знака равно
(PRINC rez) ; Вывод в файл результата операции
(CLOSE-OUTPUT-FILE ‘itog.txt) ; Закрытие файла
После точек с запятыми идут комментарии. Если с клавиатуры будет
введено 3 + 5, то в файл запишется выражение 3 + 5 = 8 с четвертой строки.
20
Пример: Определим функцию TWO_EQ, возвращающую t, если в двух
списках совпадают два первых элемента.
(DEFUN TWO_EQ (l1 l2)
(COND
((OR (NULL l1) (NULL l2)) nil) ; Нет элементов в одном из списков
((OR (NULL (CDR l1)) (NULL (CDR l2))) nil) ; В одном из списков один
элемент
((EQUAL (CAR l1) (CAR l2)) (EQUAL (CADR l1) (CADR l2))
(t nil)
)
)
Рассмотрим примеры обращения к функции TWO_EQ.
( TWO_EQ ‘(1 2) (1)) –> nil
( TWO_EQ ‘(1 2 (3 4)) ‘(1 2 8)) –> t
В условном предложении может отсутствовать вычислимое выражение Vi
(соответствующая строка имеет вид (Pi)) или на его месте может стоять
несколько вычислимых выражений (соответствующая строка имеет вид
(Pi Vi1 Vi2 … Vik)). Если предикат Pi возвращает значение отличное от nil, в
первом случае результатом предложения COND будет являться значение Pi, а
во втором – выражения Vi1, Vi2, …, Vik последовательно вычисляться слева
направо и результатом предложения COND будет значение Vik (неявный
PROGN).
1.10 Рекурсия
22
(DEFUN STEP (x n)
(COND
((= n 0) 1) ; Условие остановки рекурсии
(t (* x (STEP x (– n 1)))
)
)
В данном случае имеем рекурсию по аргументу (рекурсивный вызов STEP
является аргументом функции *).
Обращение к функции STEP:
(STEP 2 3) –> 8
Рассмотрим работу рекурсии для рассмотренного выше обращения к
функции STEP. Числами в скобках обозначена последовательность
выполнения вычислении.
(10)
(STEP 2 3) → 8
↓(1) (8) (9)
(* 2 (STEP 2 2)) → (* 2 4) → 8
↓(2) (6) (7)
(* 2 (STEP 2 1)) → (* 2 2) → 4
↓(3) (4) (5)
(* 2 (STEP 2 0)) → (* 2 1) → 2
Шаги 1, 2, 3, 4 соответствуют прямому ходу рекурсии, а шаги 5, 6, 7, 8, 9,
10 – обратному. При прямом ходе рекурсии образуется стек прерванных
процессов, состоящий из вызовов (STEP 2 3), (* 2 (STEP 2 2)),
(* 2 (STEP 2 1)), (* 2 (STEP 2 0)), которые не могут быть вычислены.
Пример 2: Определим предикат ATOM_IN_LIST, проверяющий есть ли
среди элементов списка атомы. Запишем определение функции словесно:
♦ если голова списка является атомом, то предикат возвращает t, и дальше
список не проверяем (условие остановки рекурсии);
♦ в противном случае проверяем наличие атомов в хвосте;
♦ если дошли до конца списка, то атомов нет (условие остановки рекурсии).
(DEFUN ATOM_IN_LIST (l)
(COND
((NULL l) nil)
((ATOM (CAR l)) t)
(t (ATOM_IN_LIST (CDR l)))
)
)
В данном случае имеем рекурсию по значению (рекурсивный вызов
ATOM_IN_LIST определяет результат функции).
Обращение к функции ATOM_IN_LIST:
(ATOM_IN_LIST ‘((1 2) (3))) –> nil
Рассмотрим работу рекурсии для рассмотренного выше обращения к
функции ATOM_IN_LIST.
23
(5)
(ATOM_IN_LIST ‘((1 2) (3))) → nil
↓(1) (4)
(ATOM_IN_LIST ‘((3))) → nil
↓(2) (3)
(ATOM_IN_LIST ‘()) → nil
Шаги 1, 2, 3 соответствуют прямому ходу рекурсии, а шаги 4, 5 –
обратному. При прямом ходе рекурсии образуется стек прерванных процессов,
состоящий из вызовов (ATOM_IN_LIST ‘((1 2) (3))), (ATOM_IN_LIST ‘((3))).
Заметим, что если в предложении COND поменять порядок проверки
условий (переставить первую и вторую строку), то получим
(ATOM_IN_LIST ‘()) –> t
Это связано с тем, что в Лиспе голова пустого списка – это nil, а nil является
атомом. Следовательно, важно в каком порядке проверяются условия в
предложении COND в рекурсивной функции.
Пример 3: Определим функцию COPY, копирующую список на
верхнем уровне (без учета вложенностей). Запишем определение функции
словесно:
♦ скопировать список – это значит соединить голову списка со
скопированным хвостом;
♦ копией пустого списка является пустой список (условие остановки
рекурсии).
(DEFUN COPY (l)
(COND
((NULL l) l)
(t (CONS (CAR l) (COPY (CDR l)) ) )
)
)
В данном случае имеем рекурсию по аргументу (рекурсивный вызов
COPY является аргументом функции CONS).
Обращение к функции COPY:
(COPY ‘((1 2) 3 4)) –> ((1 2) 3 4)
Рассмотрим работу рекурсии для рассмотренного выше обращения к
функции COPY.
(10)
(COPY ‘((1 2) 3 4)) → ((1 2) 3 4)
↓(1) (8) (9)
(CONS ‘(1 2) (COPY ‘((3 4)) → (CONS ‘(1 2) (3 4)) → ((1 2) 3 4)
↓(2) (6) (7)
(CONS 3 (COPY ‘((4)) → (CONS 3 (4))→ (3 4)
↓(3) (4) (5)
(CONS 4 (COPY ‘()) → (CONS 4 nil) → (4)
Шаги 1, 2, 3, 4 соответствуют прямому ходу рекурсии, а шаги 5, 6, 7, 8, 9,
10 – обратному.
24
Выполнив небольшие изменения, эту функцию можно использовать для
различных преобразований списка на верхнем уровне.
Пример 4: Определим функцию MEMBER_S, проверяющую
принадлежность s-выражения списку на верхнем уровне. В случае, если s-
выражение принадлежит списку, функция возвращает часть списка,
начинающуюся с первого вхождения s-выражения в список. В Лиспе имеется
аналогичная встроенная функция MEMBER (но она использует в своем теле
функцию EQ, поэтому не работает для вложенных списков). Запишем
определение функции MEMBER_S словесно:
♦ если s-выражение совпадает с головой списка, то возвращаем этот список
(условие остановки рекурсии);
♦ в противном случае ищем первое вхождение s-выражения в хвосте
списка;
♦ если дошли до конца списка, то s-выражение не входит в список (условие
остановки рекурсии).
(DEFUN MEMBER_S (x l)
(COND
((NULL l) l)
((EQUAL x (CAR l)) l)
(t (MEMBER_S x (CDR l)) )
)
)
В данном случае имеем рекурсию по значению.
Обращения к функции MEMBER_S:
(MEMBER_S 1 ‘(2 (3) 1 5)) –> (1 5)
(MEMBER_S 1 ‘(2 (1) 3 5)) –> nil
(MEMBER_S ‘(1 2 3) ‘(6 (8) (1 2 3) 1 5)) –> ((1 2 3) 1 5))
Заметим, что встроенная функция MEMBER для последнего примера
вернула бы nil, т.к. в теле функции для сравнения используется функция EQ.
( MEMBER ‘(1 2 3) ‘(6 (8) (1 2 3) 1 5)) –> nil
Если в предложении COND поменять порядок проверки условий
(переставить первую и вторую строку), то получим неверно работающую
функцию. Например,
(MEMBER_S nil ‘(nil)) –> (nil)
Пример 5: Определим функцию MAX_SP ,определяющую
максимальный элемент списка. Запишем определение функции словесно:
♦ если список состоит из одного элемента, то его максимальным элементом
является этот элемент (условие остановки рекурсии);
♦ в противном случае максимальный элемент списка равен максимальному
числу из двух элементов: головы списка и максимального элемента хвоста
списка.
(DEFUN MAX_SP (l)
(COND
((NULL (CDR l)) (CAR l))
25
(t (MAX (CAR l) (MAX_SP (CDR l)) ) )
)
)
В примере используется встроенная функция MAX, которая находит
максимальный элемент из своих аргументов. В примере использовалась
рекурсию по значению.
Обращения к функции MAX_SP:
( MAX_SP ‘(2 7 6 1) –> 7
( MAX_SP ‘(6 1 4 9) –> 9
Пример 6: Определим функцию APPEND_TWO от двух аргументов,
работающую аналогично встроенной функции APPEND, через обращение к
функции CONS . Запишем определение функции словесно:
♦ соединить два списка – это значит соединить голову первого списка с
результатом соединения хвоста первого списка и второго списка;
♦ если первый список пуст, то результат соединения – второй список
(условие остановки рекурсии).
(DEFUN APPEND_TWO (l1 l2)
(COND
((NULL l1) l2)
(t (CONS (CAR l1) (APPEND_TWO (CDR l1) l2) ) )
)
)
В данном случае имеем рекурсию по аргументу (обращение к
APPEND_TWO является аргументом функции CONS) .
Обращения к функции APPEND_TWO:
(APPEND_TWO ‘(1 2 3) ‘(4 5)) –> (1 2 3 4 5)
Пример 7: Определим функцию REMOVE_S, удаляющую все
вхождения заданного s-выражения в список на верхнем уровне. Запишем
определение функции словесно:
♦ если s-выражение совпало с головой списка, то результат функции –
хвост списка с удаленными всеми вхождениями s-выражения;
♦ если s-выражение не совпало с головой списка, то результат функции –
соединение головы списка и его хвоста с удаленными всеми вхождениями s-
выражения;
♦ если список пуст, то результат удаления – пустой список (условие
остановки рекурсии).
(DEFUN REMOVE_S (x l)
(COND
((NULL l) nil)
((EQUAL x (CAR l)) (REMOVE_S x (CDR l)))
(t (CONS (CAR l) (REMOVE_S x (CDR l))) )
))
Для этой функции для разных условий предложения COND имеем
рекурсию по аргументу и по значению.
26
Обращения к функции REMOVE_S:
(REMOVE_S 1 ‘(2 1 1 3)) –> (2 3)
Рассмотрим работу рекурсии для рассмотренного выше обращения к
функции REMOVE_S.
(11)
(REMOVE_S 1 ‘(2 1 1 3) → (2 3)
↓(1) (9) (10)
(CONS 2 (REMOVE_S 1 ‘(1 1 3)) → (CONS 2 ‘(3)) → (2 3)
↓(2) (8)
(REMOVE_S 1 ‘(1 3)) → (3)
↓(3) (7)
(REMOVE_S 1 ‘(3)) → (3)
↓(4) (5) (6)
(CONS 3 (REMOVE_S 1 ‘()) → (CONS 3 nil) → (3)
Еще одно обращение к функции REMOVE_S:
(REMOVE_S ‘(a b) ‘(1 (a b) ((a b)) 3 (a b))) –> (1 ((a b)) 3)
Заметим, что встроенная функция REMOVE вернула бы исходный список,
т.к. в теле функции для сравнения используется функция EQ.
(REMOVE ‘(a b) ‘(1 (a b) ((a b)) 3 (a b))) –> (1 (a b) ((a b)) 3 (a b))
Пример 8: Определим функцию REVERSE1, обращающую список на
верхнем уровне (в mulisp имеется встроенная функция REVERSE) . Запишем
определение функции словесно:
♦ обратить список – это значит соединить обращенный хвост и голову;
♦ если список пуст, то результат обращения – пустой список (условие
остановки рекурсии).
(DEFUN REVERSE1 (l)
(COND
((NULL l) nil)
(t (APPEND (REVERSE1 (CDR l)) (LIST (CAR l))))
)
)
27
вспомогательную функцию REVERSE3 с дополнительным параметром для
накапливания результата обращения списка. Запишем определение функции
REVERSE3 словесно:
♦ обратить список – это значит обратить хвост, добавив голову исходного
списка во вспомогательный список;
♦ если список пуст, то результат обращения – вспомогательный список
(условие остановки рекурсии).
Заметим, что на обратном ходе рекурсии ничего не выполняется.
(DEFUN REVERSE2 (l)
(REVERSE3 (l nil)
)
(DEFUN REVERSE3 (l1 l2)
(COND
((NULL l1) l2)
(t (REVERSE3 (CDR l1) (CONS (CAR l1) l2)))
)
)
Пример 2: Определим функцию COMPARE, возвращающую t, если в
числовом списке положительных элементов больше, чем отрицательных.
Чтобы не просматривать список два раза для подсчета положительных и
отрицательных элементов, определим вспомогательную функцию
COMPARE_V с двумя вспомогательными параметрами-счетчиками. Запишем
определение функции COMPARE_V словесно:
♦ если головой списка является положительный число, то сравниваем
количество положительных и отрицательных элементов в хвосте, при этом
увеличив на 1 первый счетчик;
♦ если головой списка является отрицательное число, то сравниваем
количество положительных и отрицательных элементов в хвосте, увеличив на 1
второй счетчик;
♦ если головой списка является 0, то сравниваем количество
положительных и отрицательных элементов в хвосте списка, не изменяя
счетчиков;
♦ если список пуст, то сравниваем накопленные значения счетчиков и
возвращаем t в случае, если счетчик положительных чисел больше счетчика
отрицательных чисел (условие остановки рекурсии).
(DEFUN COMPARE (l)
(COMPARE_V (l 0 0)
)
(DEFUN COMPARE_V (l np no)
(COND
((NULL l1) (> np no) )
((PLUSP (CAR l)) (COMPARE_V (CDR l) (+ np 1) no))
((MINUSP (CAR l)) (COMPARE_V (CDR l) np (+ no 1)))
(t (COMPARE_V (CDR l) np no))
28
)
)
В данном примере имеем рекурсию по аргументам.
Пример 3: Определим функцию POSITION, определяющую позицию
первого вхождения s-выражения в список (на верхнем уровне), через
вспомогательную функцию POSITION_V с дополнительным параметром для
позиции элемента. Запишем определение функции POSITION_V словесно:
♦ если голова списка совпадает с s-выражением, то результат работы
функции – накопленный к этому моменту номер позиции головы (условие
остановки рекурсии);
♦ если голова списка не совпадает с s-выражением, то определяется
позиция первого вхождения s-выражения в хвост списка, при этом значение
счетчика позиций увеличивается на 1;
♦ если список пуст, то данное s-выражение в списке отсутствует (условие
остановки рекурсии).
(DEFUN POSITION (s l)
(POSITION_V (s l 1)
)
(DEFUN POSITION_V (s l n)
(COND
((NULL l) nil )
((EQUAL (CAR l) s) n)
(t (POSITION_V (CDR l) (+ n 1)))
)
)
В данном примере имеем рекурсию по значению.
29
(t (CONS (COPY_ALL (CAR l)) (COPY_ALL (CDR l))))
)
)
Параллельность рекурсии не временная, а текстуальная. При выполнении
тела функции в глубину идет сначала левый вызов (рекурсия «в глубину»), а
потом правый (рекурсия «в ширину».
Пример 2: Определим функцию MEMBER_A, проверяющую
принадлежность атома списку на всех уровнях. Функция возвращает t, если
атом встречается в списке на любом уровне, и nil в противном случае. Запишем
определение функции словесно:
♦ атом может принадлежать либо голове списка, либо хвосту;
♦ если аргументом функции является атом, то сравниваются два атома
(условие остановки рекурсии);
♦ если список пуст, то атом не принадлежит списку (условие остановки
рекурсии).
(DEFUN MEMBER_A (x l)
(COND
((ATOM l) (EQUAL x l)
((NULL l) nil )
(t (OR (MEMBER_A x (CAR l)) (MEMBER_A x (CDR l))))
)
)
Обращения к функции MEMBER_A:
(MEMBER_A 1 ‘((4) ((((1) 2) 3) 4) 5)) –> t
(MEMBER_A nil ‘(nil)) –> t
Заметим, что в данном примере важен порядок условий выхода из
рекурсии. Если поменять местами условия остановки рекурсии, то
(MEMBER_A nil ‘(nil)) –> nil, т.к. (NULL nil) –>t
Пример 3: Определим функцию IN_ONE, преобразующую список в
одноуровневый (удаление вложенных скобок). Запишем определение функции
словесно:
♦ удалить вложенные скобки в списке – значит соединить два списка:
голову исходного списка с удаленными вложенными скобками и хвост с
удаленными вложенными скобками;
♦ если аргумент функции – атом, то возвращается список из атома;
♦ если список пуст, то все вложенные скобки удалены (условие остановки
рекурсии).
(DEFUN IN_ONE (l)
(COND
((NULL l) nil )
((ATOM l) (LIST l)
(t (APPEND (IN_ONE (CAR l)) (IN_ONE (CDR l))))
)
30
)
Обращение к функции IN_ONE:
(IN_ONE ‘((4) ((((1) 2) 3) 4) 5)) –> (4 1 2 3 4 5)
Пример 4: Определим функцию REVERSE_ALL, обращающую список
на всех уровнях. Запишем определение функции словесно:
♦ обратить список – значит соединить два списка: обращенный на всех
уровнях хвост и обращенную на всех уровнях голову исходного списка;
♦ если аргумент функции – атом, то возвращается атом (условие остановки
рекурсии);
♦ если список из одного элемента, то возвращается список из обращенной
головы.
(DEFUN REVERSE_ALL (l)
(COND
((ATOM l) l)
((NULL (CDR l)) (LIST (REVERSE_ALL (CAR l))))
(t (APPEND (REVERSE_ALL (CDR l)) (REVERSE_ALL (LIST (CAR
l) ))))
)
)
Обращения к функции REVERSE_ALL:
(REVERSE_ALL ‘((a) (((b c) d e) f) r)) –> (r (f (e d (c b))) (a))
Пример 5: Определим функцию MAX_IN_LIST, находящую
максимальный элемент в числовом списке, содержащем подсписки. Запишем
определение функции словесно:
♦ максимальный элемент в списке является максимальным из двух
элементов: максимального элемента в голове списка и максимального элемента
в хвосте списка;
♦ если список из одного элемента, то ищется максимальный элемент в
голове списка;
♦ если аргумент функции – атом, то он и есть максимальный элемент
(условие остановки рекурсии).
(DEFUN MAX_IN_LIST (l)
(COND
((ATOM l) l)
((NULL (CDR l)) (MAX_IN_LIST (CAR l))))
(t (MAX (MAX_IN_LIST (CAR l)) (MAX_IN_LIST (CAR l))))
)
)
Обращение к функции MAX_IN_LIST:
(MAX_IN_LIST ‘((2) (((1 5) 0 4) 8) 3)) –> 8
Пример 6: Определим функцию REMOVE_A, удаляющую все
вхождения атома во вложенный список. Запишем определение функции
словесно:
31
♦ удалить атом – соединить голову, из которой удалены все вхождения
атома, и хвост, из которого удалены все вхождения атома;
♦ если список пуст, то атом удалять не надо (условие остановки рекурсии);
♦ если голова списка – атом, то если это тот атом, который необходимо
удалить, то удаляются все вхождения этого атома из хвоста списка, иначе
соединяется голова и хвост со всеми удаленными вхождениями атома;
(DEFUN REMOVE_A (x l)
(COND
((NULL l) nil)
((ATOM (CAR l))
(COND
((EQUAL (CAR l) x)( REMOVE_A x (CDR l)))
(t (CONS (CAR l)( REMOVE_A x (CDR l))))
)
)
(t (APPEND (LIST (REMOVE_A x (CAR l)))( REMOVE_A x (CDR l))))
)
)
Обращения к функции REMOVE_A:
(REMOVE_A ‘a ‘((a) (b (a c)) a d)) –> (nil (b (c)) d)
Пример 7: Определим функцию FIB_1, вычисляющую n-ый член
последовательности Фибоначчи: f1=1, f2=1, fn=fn-1+fn-2.
(DEFUN FIB_1 (n)
(COND
((OR (= n 1) (= n 2) 1)
(t (+ (FIB_1 (- n 1)) (FIB_1 (- n 2))))
)
)
Недостатком такой рекурсии является то, что происходит дублирование
вычислений. При такой организации рекурсии число вызовов растет как a∙bn-2,
где a ≈1.8, b ≈1.6, хотя необходим только n-1 вызов. Напишем другой вариант
рекурсивной функции FIB_2. Новая функция будет вызывать вспомогательную
функцию FIB_V с двумя накапливающими параметрами – соседними членами
последовательности, с помощью которых вычисляется следующий член
последовательности.
(DEFUN FIB_2 (n)
(FIB_V (n 1 1))
(DEFUN FIB_V (n a b)
(COND
((OR (= n 1) (= n 2) b)
(t (FIB_V (- n 1) b (+ a b)))
)
)
32
В этом случае члены последовательности вычисляются от 3-го к n-му. При
вычислении n-го члена последовательности Фибоначчи функция вызывается n-
1 раз.
a b c
примере это указатель на атом а), а функция CDR – значение правой списочной
ячейки (в примере это указатель на список (b c)). Функция CONS создает
новую списочную ячейку, содержимое левого поля которого – это указатель на
первый аргумент функции, а содержимое правого поля – это указатель на
второй аргумент функции. Таким образом, новая списочная ячейка связывает
две существующие структуры в одну новую структуру. Это довольно
эффективное решение с точки зрения создания новых структур и их
представления в памяти. Заметим, что применение функции CONS не
изменяет структур аргументов функции.
Пример: Рассмотрим последовательность вызовов:
(SETQ ‘x ‘(b c)) → (b с), побочный эффект – связывание: x → (b с);
(SETQ ‘y ‘(a b c)) → (a b с), побочный эффект – связывание: y → (a b с);
(SETQ ‘z (CONS x y)) → ((b с) a b c), побочный эффект – связывание:
z → ((b с) a b c).
Структуру, связанную с переменной z, можно представить графически:
35
Рисунок 6 – Работа функций (CAR z) и (CDDR z)
Полученные списки логически идентичны, а физические адреса для них
будут разные, поэтому
(EQUAL (CAR z) (CDDR z)) → t;
(EQ (CAR z) (CDDR z)) → nil.
Список ((b с) a b c) можно было создать другим способом:
(SETQ ‘x ‘(b c)) → (b с), побочный эффект – связывание: x → (b с);
(SETQ ‘y (CONS ‘a x)) → (a b с), побочный эффект – связывание: y → (a b с);
(SETQ ‘z1 (CONS x y)) → ((b с) a b c), побочный эффект – связывание:
z1 → ((b с) a b c).
Структуру, связанную с переменной z1, можно представить графически:
36
Рисунок 8 - Созданная структура spis
После присваивания нового значения переменной spis побочным эффектом
функции
(SETQ spis (CDR spis)) → (с d), побочный эффект – связывание:
spis → (с d),
уже нельзя будет “добраться” до двух списочных ячеек. Они стали мусором.
2. Значение вызова (CONS a (LIST b)) лишь выводится на экран дисплея,
после чего созданная им в памяти структура станет мусором.
Для повторного использования ставшей мусором памяти в Лиспе
предусмотрен специальный сборщик мусора, который автоматически
запускается, когда в памяти остается мало свободного места. Сборщик мусора
перебирает все ячейки и собирает являющиеся мусором ячейки в список
свободной памяти для того, чтобы их можно было использовать заново.
Все рассмотренные до сих пор функции манипулировали выражениями, не
вызывая каких-либо изменений в уже существующих структурах. Значения
переменных можно было изменить лишь целиком. Единственное, что могло
произойти со старым значением – это лишь то, что оно могло пропасть. В
Лиспе имеются специальные функции, которые изменяют внутреннюю
структуру списков. Такие функции называются структуроразрушающими. В
чисто функциональном программировании такие функции не используются. В
практическом программировании функции, изменяющие структуры, иногда
используют. Например, если нужно сделать небольшие изменения в большой
структуре данных. С помощью структуроразрушающих функций можно
эффективно менять за один раз несколько структур, если у этих структур есть
совмещенные в памяти подструктуры. Но такие изменения могут привести к
сюрпризам в тех частях программы, которые не знают об изменениях, что
осложняет написание программ и их документирование. Обычно
использование структуроразрушающих функций стараются избегать.
37
Пример: (CONS ‘a ‘b) → (a .b). Графически созданную структуру
можно представить следующим образом:
38
небольшого заранее известного количества элементов, точечные конструкции
предпочтительнее списков. Если же число элементов велико и может
изменяться, то целесообразнее использовать списочные конструкции. Точечные
пары применяются в теории, книгах и справочниках по Лиспу, системном
программировании. Большинство программистов не используют точечные
пары, поскольку по сравнению с требуемой в таком случае внимательностью
получаемый выигрыш в объеме памяти и скорости вычислений не заметен.
1.13 Функционалы
39
(DEFUN ALL (p l)
(COND
((NULL l) t)
((APPLY p (LIST (CAR l))) (ALL p (CDR l)))
(t nil)
)
)
Рассмотрим примеры работы функционала ALL.
(ALL ‘atom ‘((1 2) 3 4)) → nil;
(ALL ‘atom ‘(1 2 3 4)) → t;
(ALL ‘plusp‘(1 2 3 4)) → t;
(ALL ‘(LAMBDA (x) (<= x 0)) ‘(-1-3 -4)) → t.
Пример 2: Напишем функционал FUNC_LIST, который имеет два
аргумента-списка: список из функциональных аргументов и списка данных.
Этот функционал возвращает список из результатов применения каждого
элемента списка функциональных аргументов к соответствующему элементу
списка данных.
(DEFUN FUNC_LIST (f l)
(COND
((NULL f) nil)
(t (CONS (APPLY (CAR f) (LIST (CAR l))) (FUNC_LIST (CDR f) (CDR l))))
)
)
Рассмотрим пример работы функционала FUNC_LIST.
(FUNC_LIST ‘(atom symbolp numberp) ‘(a 1 name)) → (t nil nil).
Функционал FUNCALL является функцией не менее двух аргументов:
функционального аргумента и данных. Этот функционал работает аналогично
APPLY, но аргументы функционального аргумента (функции от n переменных)
задаются не списком, а как аргументы FUNCALL, начиная со второго. Вызов
функционала имеет вид:
(FUNCALL fn a1 a2 … an),
где fn – имя функции или лямбда-выражение, ai (i = 1,…,n)– значение i-го
аргумента для fn.
Примеры:
1. (FUNCALL ‘+ 1 2 3) →(+ 1 2 3) →6;
2. (FUNCALL ‘(LAMBDA (x y) (+ x y)) 1 2) →(+ 1 2) →6;
3. (FUNCALL ‘LIST ‘(a b)) →(LIST ‘(a b)) →((a b)).
Заметим, что если в последнем примере заменить FUNCALL на APPLY,
то получим: (APPLY ‘LIST ‘(a b)) →(LIST a b) →(a b)
Пример 3: Напишем функционал FUNC_LIST1, работающий аналогично
функционалу FUNC_LIST из примера2 с использованием применяющего
функционала FUNCALL.
(DEFUN FUNC_LIST1 (f l)
40
(COND
((NULL f) nil)
(t (CONS (FUNСALL (CAR f) (CAR l)) (FUNC_LIST1 (CDR f) (CDR l)))) )
)
)
Рассмотрим пример работы функционала FUNC_LIST1.
(FUNC_LIST1 ‘(atom car) ‘(nil (1 2))) → (t 1).
Пример 4: Переделаем написанную ранее в п.1.10.5 функцию сортировки
числового списка в функционал, у которого функциональный аргумент будет
задавать порядок сортировки.
(DEFUN SORT1 (l p)
(COND
((NULL l) l)
(t (ADD_ORD (CAR l) (SORT1 (CDR l) p) p))
)
)
(DEFUN ADD_ORD (x l p)
(COND
((NULL l) (LIST x))
((FUNCALL p x (CAR l)) (CONS x l))
(t (CONS (CAR l) (ADD_ORD x (CDR l) p)))
)
)
Рассмотрим примеры работы функционала SORT1.
(SORT1 ‘(5 1 3 2 4) ‘<) → (1 2 3 4 5).
(SORT1 ‘(5 1 3 1 2 4 2) ‘(LAMBDA (x y) (>= x y))) → (1 1 2 2 3 4 5).
(SORT1 ‘(b a d c s) ‘(LAMBDA (x y) (CHAR< x y))) → (a b c d s).
(SORT1 ‘(ab sc aefg srt) ‘string>) → (ab aefd sc srt).
41
Примеры:
1. (MAPCAR ‘atom ‘(a b c)) → ((ATOM a) (ATOM b) (ATOM c)) → (t t t);
2. (MAPCAR ‘(LAMBDA (y) (LIST y))) ‘(a b c)) →
→ ((LIST a) (LIST b) (LIST c)) → ((a) (b) (c));
3. (DEFUN PARA(x) (LIST x ‘*))→ para;
(MAPCAR ‘para ‘(a b c)) → ((a *) (b *) (c *));
4. (MAPCAR ‘+ ‘(1 2) ‘(2 3) ‘(3 4)) → (6 9);
5. (MAPCAR ‘cons ‘(a b c) ‘((1) (2) (3))) → ((a 1) (b 2) (c 3)).
Как правило, MAP-функция применяется к одному аргументу-списку, т.е.
функциональный аргумент является функцией одной переменной.
Использование MAP-функций используется при программировании
специальных циклов, поскольку с их помощью можно сократить запись
повторяющихся вычислений.
Пример 1 Напишем функцию SUM3, вычисляющую сумму кубов
элементов списка.
(DEFUN SUM3 (l)
(EVAL (CONS ‘+ (MAPCAR ‘* l l l))))
Рассмотрим пример работы функции SUM3.
(SUM3 ‘(1 2 3)) → 36.
Пример 2 Напишем функцию DECART, вычисляющую декартово
произведение двух множеств, через композицию двух вложенных вызовов
функционала MAPCAR.
(DEFUN DECART (l1 l2)
(MAPCAR ‘(LAMBDA (x) (MAPCAR ‘(LAMBDA (y) (LIST x y)) l2)) l1))
Рассмотрим пример работы функции DECART.
(DECART ‘(1 2 3) ‘(a b)) → (((1 a) (1 b)) ((2 a) (2 b)) ((3 a) (3 b))).
Отображающий функционал MAPLIST действует подобно MAPCAR, но
действия осуществляются не над элементами списков, а над
последовательными хвостами этих списков, начиная с самих списков. Вызов
функционала имеет вид:
(MAPLIST fn sp1 sp2 … spn),
где fn – имя функции или лямбда-выражение, spi – список (i = 1,…,n).
Примеры:
1. (MAPLIST ‘reverse ‘(a b c)) → ((c b a) (c b) (c));
2. (MAPLIST ‘(LAMBDA (x) x) ‘(a b c)) → ((a b c) (b c) c);
3. (MAPLIST ‘append ‘(a b) ‘(1 2)) → ((a b 1 2) (b 2)).
Среди отображающих функционалов выделяют объединяющие
функционалы MAPCAN и MAPCON. Работа их аналогична соответственно
MAPCAR и MAPLIST. Различие заключается в способе построения
результирующего списка. Если функционалы MAPCAR и MAPLIST строят
новый список из результатов применения функционального аргумента с
помощью функции LIST, то функционалы MAPCAN и MAPCON для
построения нового списка используют структуроразрушающую
42
псевдофункцию NCONC, соединяющую списки физически. Функция NCONC
делает то же самое, что и APPEND с той лишь разницей, что NCONC изменяет
указатель в поле CDR последней ячейки списка – первого аргумента на начало
списка – второго аргумента. Для того, чтобы функционалы могли применить
функцию NCONC, их функциональные аргументы должны возвращать списки.
Функционалы MAPCAN и MAPCON удобно использовать в качестве фильтров
для удаления нежелательных элементов из списка.
Вызов функционала MAPCAN имеет вид:
(MAPCAN fn sp1 sp2 … spn),
где fn – имя функции или лямбда-выражение, spi – список (i = 1,…,n).
Пример: Удаление из числового списка всех элементов, кроме
отрицательных.
(MAPCAN ‘(LAMBDA (x)
(COND
((MINUSP x) (LIST x))
(t nil)
)) ‘(-3 1 4 -5 0)) → (-3 -5)
Заметим, что использование MAPCAR не привело бы к такому результату,
т.к. (APPEND ‘(1) nil) → (1), а (LIST 1 nil) → (1 nil).
Вызов функционала MAPCON имеет вид:
(MAPCON fn sp1 sp2 … spn),
где fn – имя функции или лямбда-выражение, spi – список (i = 1,…,n).
Примеры:
1. (MAPCON ‘reverse (1 2 3)) → (3 2 1 3 2 3).
2. Преобразование одноуровнего списка в множество:
(MAPCON ‘(LAMBDA (l)
(COND
((MEMBER (CAR l) (CDR l)) nil)
(t (LIST (CAR l)))
)) ‘(1 2 3 1 1 4 2 3)) → (1 4 2 3)
Отметим, что функционал MAPCON может легко «зацикливать» списки,
поэтому использовать его нужно осторожно.
43
Идея использования логики исчисления предикатов I порядка в качестве
основы языка программирования возникла в 60-е годы, когда создавались
многочисленные системы автоматического доказательства теорем и вопросно-
ответные системы. В 1965 г. Робинсон предложил принцип резолюции,
который в настоящее время лежит в основе большинства систем поиска
логического вывода. Метод резолюций был использован в системе GPS (general
problem solver). В нашей стране была разработана система ПРИЗ, которая
может доказать любую теорему из школьного учебника геометрии.
Язык программирования PROLOG (programming in logic) был разработан и
впервые реализован в 1972 г. группой сотрудников Марсельского университета
во главе с Колмероэ. Группа занималась проблемой автоматического перевода с
одного языка на другой. Основа этого языка - исчисления предикатов I порядка
и метод резолюций. Суть Пролога – программирование в терминах целей.
Программист описывает условие задачи, пользуясь понятиями объектов
различных типов и отношений между ними, и формулирует вопрос. PROLOG-
система обеспечивает ответ на вопрос, находя автоматически
последовательность вычисления решения, используя встроенную процедуру
поиска. До 1981 г. число исследователей, занимавшихся логическим
программированием, составляло около сотни во всем мире. В 1981 году
PROLOG был выбран в качестве базового языка компьютеров пятого
поколения, и количество исследователей логического программирования резко
возросло. Одной из наиболее интересных тем исследований является связь
логического программирования с параллелизмом.
Где же используется Пролог в настоящее время? Это область
автоматического доказательства теорем, построение экспертных систем,
машинные игры с эвристиками (например, шахматы), автоматический перевод
с одного языка на другой.
В настоящее время создано достаточно много реализаций языка Пролог:
Wisdom Prolog, Micro Prolog, Turbo Prolog, PDS-Prolog, Arity Prolog и т.д.
Файлы, содержащие программы, написанные на языке Prolog, имеют
расширение pro.
В нашем курсе будут использоваться Turbo Prolog. Эта система является
компилятором (все остальные – интерпретаторы). У нее имеется оболочка,
состоящая из четырех окон и меню, предусмотрена строгая типизация данных.
В нижней части оболочки имеется строка, в которой указаны подсказки о
назначении функциональных клавиш.
Для запуска системы Turbo Prolog необходимо запустить файл prolog.exe.
Опишем горячие клавиши при работе с системой Turbo Prolog (знак «+»
означает одновременное нажатие клавиш):
F2 – сохранение файла, если он уже сохранялся под каким-нибудь именем;
F3 – загрузка файла;
ALT+R – запуск программы;
ALT+E – переход в окно редактора;
F5 – развернуть окно на весь экран или свернуть до первоначальных размеров;
F6 – переход между окнами;
44
F10 – переход в верхнее меню;
ALT+T – включение или выключение трассировки;
F8 – повтор ранее введенной цели при запуске программы без раздела goal;
ALT+X – выход из Пролога.
Команды при работе в окне редактора:
CTRL+Y – удаление строки;
F5 – нахождение и замена указанного текста;
CTRL+K,B – начало выделения блока;
CTRL+K,K – конец выделения блока;
CTRL+K,C – копирование выделенного блока в место, указанное курсором;
CTRL+K,V – перемещение выделенного блока в место, указанное курсором;
CTRL+K,Y – удаление выделенного блока;
CTRL+K,H –отмена выделения блока;
Блочные операции можно выполнить с использованием функциональных
клавиш:
CTRL+F5 – начало выделения блока, конец выделения блока, копирование
выделенного блока в место, указанное курсором (для выполнения операции
копирования полностью данную комбинацию клавиш следует нажать 3 раза);
ALT+F5 – начало выделения блока, конец выделения блока и копирование его в
другой файл (для выполнения операции копирования полностью данную
комбинацию клавиш следует нажать 2 раза);
ALT+F6 – начало выделения блока, конец выделения блока и перемещение его
в место, указанное курсором (для выполнения операции перемещения
полностью данную комбинацию клавиш следует нажать 3 раза);
ALT+F6 – начало выделения блока, конец выделения блока и удаление его (для
выполнения операции удаления данную комбинацию клавиш следует нажать 2
раза);
ESC – отмена выделения.
45
Пример 1: Это дерево можно описать следующей Пролог-программой.
predicates
родитель(symbol, symbol)
clauses
родитель(пам, боб).
родитель(том, боб).
родитель(том, лиз).
родитель(боб, энн).
родитель(боб, пат).
родитель(пат, джим).
Раздел predicates описывает отношения между объектами (имя
отношения, его арность и типы объектов). У нас имеется отношение родитель
между двумя объектами символьного типа. В разделе clauses описаны 6
фактов наличия отношений между конкретными объектами. Имена объектов
начинаются с маленьких букв (они являются константами). В Прологе принято
соглашение, что константы начинаются с маленькой буквы, а переменные – с
большой. После ввода такой Пролог-программы можно запустить программу на
выполнение (ALT+R) и в специальном окне диалога после слова «Цель:»
задавать вопросы, касающиеся отношения родитель. Вопросы могут быть
простые и сложные (в качестве связки «и» при составлении сложного вопроса
используется запятая). Ответы Пролог-системы выводятся сразу после вопроса.
При этом могут быть следующие варианты ответов:
• Да;
• Не (соответствует нет);
• Не решен (не найдены значения переменных в вопросе);
• Перечисляются все возможные значения переменных в вопросе и
указывается количество найденных решений.
Вопрос относительно Ответ Пролог-
Вопрос в Пролог-системе
отношения родитель системы
Боб является родителем Пат? родитель(боб,пат) Да
Пат – ребенок Лиз? родитель(лиз,пат) Не
X=том
Кто родители Лиз? родитель(X,лиз)
1 решен. s
Кто дети Пат? родитель(пат, X) Не решен.
X=энн
Кто дети Боба? родитель(боб,X) X=пат
2 решен. s
Есть ли дети у Тома? родитель(том,_) Не
X=пам, Y=боб
X=том, Y=боб
X=том, Y=лиз
Кто чей родитель? родитель(X,Y) X=боб, Y=энн
X=боб, Y=пат
X=пат, Y=джим
6 решен. s
46
X=боб, Y=энн
родитель(том,X),
Кто внуки Тома? X=боб, Y=пат
родитель(X,Y)
2 решен. s
Кто родители родителей родитель(X,джим), X=пат, Y=боб
Джима? родитель(Y,X) 1 решен. s
47
X является предком Y, если X – родитель Y или существует цепочка людей
между Х и Y, связанных отношением родитель.
предок(X,Y):–родитель(X,Y).
предок(X,Y):–родитель(X,Z),предок(Z,Y).
Эти правила можно записать по-другому:
предок(X,Y):–родитель(X,Y);
родитель(X,Z),предок(Z,Y).
В данном примере получили рекурсивное определение отношения предок.
Пример 4 Определим двуместное отношение дальний_родственник с
помощью правила, используя имеющееся отношение предок. X является
дальним родственником Y, если они связаны отношением предок, но при этом
не связаны отношением родитель.
дальний_родственник (X,Y):–предок(X,Y),not(родитель(X,Y));
предок(Y,X),not(родитель(Y,X)).
В правой части правила можно использовать знаки = и <> для проверки
числовых или строковых значений переменных на равенство и неравенство,
знаки < и > для сравнения значений.
48
сопоставление выполнялось с головой правила). Связанные переменные
освобождаются, если цель достигнута или сопоставление неуспешно.
Процесс сопоставления похож на использование оператора «=».
Интерпретация этого оператора Прологом зависит от того, известны ли оба
значения, связанные этим оператором или только одно из них. Если оба
значения известны, то оператор интерпретируется как оператор сравнения.
Если известно только одно значение, то оператор интерпретируется как
присваивание, причем присваивание может выполняться как слева направо, так
и справа налево в зависимости от того, слева или справа от оператора
находится известное значение.
Для достижения целей Пролог использует механизм отката. При
вычислении цели выполняется сопоставление с фактами и головами правил.
Сопоставления выполняются слева направо. Возможно, некоторые подцели
будут неуспешны при сопоставлении с некоторыми фактами или правилами,
поэтому Пролог должен запоминать «точки», в которых он может поискать
альтернативные пути решения. Если цель была неуспешной, то выполняется
откат влево к ближайшему указателю отката и выполняется новая попытка
достичь цели. Этот откат будет повторяться, пока цель не будет достигнута или
исчерпаются все указатели отката. Если все цели были достигнуты, но
использовались не все указатели отката, то будет продолжен поиск других
решений.
Пример: Рассмотрим работу Пролог-системы при вопросе:
предок(том,энн).
Сначала выполняется сопоставление цели и головы первого правила,
сопоставление успешно и переменные X и Y становятся связанными (X
получает значение том, а Y – значение энн). При этом Пролог-система
запоминает, что имеется второе правило, по которому можно будет продолжить
поиск решения. Далее начинает выполняться тело первого правила (новая цель
родитель(том,энн)). Опять выполняются все возможные сопоставления с
фактами предиката родитель. Сопоставление неуспешно, все связи разорваны.
Далее происходит возврат для сопоставления цели и головы второго
правила. Сопоставление успешно и переменные X и Y становятся связанными
(X получает значение том, а Y – значение энн). Далее начинает выполняться
тело второго правила (новая цель родитель(том,Z)). В нем имеется две цели,
их достижение проверяется по порядку. Первая цель успешна, т.к.
сопоставляется с фактом родитель(том,боб), при этом Z получает значение
боб. Новая цель - предок(боб,энн). Опять выполняется составление цели и
головы первого правила. Оно успешно, при этом X получает значение боб, а Y
– значение энн. Новая цель – правая часть первого правила родитель(боб,энн)
успешна.
Следовательно, цель успешна и ответ Пролог-системы: да.
Представим шаги вычислений графически.
49
Рисунок 11 - Поиск решений
Существует два способа просмотреть в окне трассировки, как Пролог-
система ищет решения:
1. Включить опцию трассировки всех предикатов, выполнив команды Options -
Compiler directives – Trace – Trace.
2. В начале программы ввести директиву компилятору:
trace
или
trace <p1>, <p2>,…,<pk>
где pi (i = 1,…,k) – имена предикатов, которые будут трассироваться (в первом
случае будут трассироваться все предикаты).
После запуска программы продолжить пошаговое выполнение программы
можно клавишей F10.
В окне трассировки могут появиться следующие слова:
• CALL
Далее указывается текущая цель: имя предиката и значения его аргументов.
• REDO
Возврат в отмеченную точку возврата .
• RETURN
Указывается подцель, которая успешна. После RETURN стоит «*», если
остались неиспользованные указатели откатов.
• FAIL
Цель не была достигнута.
50
domains
<описание доменов>
database
<описание предикатов динамической базы данных>
predicates
<описание предикатов>
goal
<предложение, описывающее внутреннюю цель программы>
/*В конце предложения всегда ставится точка*/
clauses
<факты и правила>
Программа может не содержать всех вышеперечисленных разделов.
Комментарии в программе обрамляются символами /* и */, как это показано
при описании структуры программы.
52
Пример 2: Опишем предикат сумма, находящий сумму двух, трех или
четырех целых чисел.
domains
слагаемое,сумма=integer
predicates
сумма(слагаемое,слагаемое,сумма)
сумма(слагаемое,слагаемое,слагаемое,сумма)
сумма(слагаемое,слагаемое,слагаемое,слагаемое,сумма)
clauses
сумма(X1,X2,S):-S=X1+X2.
сумма(X1,X2,X3,S):-S=X1+X2+X3.
сумма(X1,X2,X3,X4,S):-S=X1+X2+X3+X4.
При этом можно в качестве цели указать как сумма(1,2,S), так и
сумма(1,2,3,S), так и сумма(1,2,3,4,S).
В разделе domains так же можно описывать структуры.
Пример 3: Пусть у Васи в сумке лежит книга И.Братко
“Программирование на языке ПРОЛОГ для искусственного интеллекта”. Этот
факт можно описать следующей программой с использованием структуры в
разделе domains.
domains
книга=книга(автор, заголовок)
где,автор,заголовок=symbol
predicates
находится(где,книга)
clauses
находится(сумка,книга(“И.Братко”,”Программирование на языке
ПРОЛОГ для искусственного интеллекта ”).
Пусть в сумке у Васи лежит так же синяя ручка. Тогда можно ввести
альтернативный домен, вместо того, чтобы описывать новый предикат.
domains
вещь=книга(автор,заголовок);
ручка(цвет)
где,автор,заголовок,цвет=symbol
predicates
находится(где,вещь)
clauses
находится(сумка,книга(“И.Братко”,”Программирование на языке
ПРОЛОГ для искусственного интеллекта ”).
находится(сумка, ручка(синий)).
53
2.3.2 Ввод-вывод в Турбо-Прологе
Мы уже видели, что цель можно задавать во время диалога. При этом, если
в цели присутствовали переменные, то Пролог-система находит все возможные
значения этой переменной. После достижения внешней цели выполнение
программы продолжается (можно задавать новую цель). При этом конечно,
целевая формулировка не должна быть очень длинной.
В Турбо-Прологе имеется возможность описать внутреннюю цель в
разделе goal. После достижения внутренней цели выполнение программы
прекращается. Если во внутренней цели имеется переменная, то находится
только одно возможное значение этой переменной (первое в соответствии с
алгоритмом поиска). Значения этой переменной выводится на экран с помощью
предиката write. Для получения всех наборов допустимых решений необходимо
включение в правила специальных средств управлений поиском решений.
Внутренняя цель может содержать достаточно длинную формулировку.
Предложение внешней цели должно заканчиваться точкой.
Пример: Рассмотрим дерево семейных отношений из примера 1 п.2.1.
Зададим внутреннюю цель для ответа на вопрос: Кто родители Боба? В
программе следует описать раздел goal.
goal
clearwindow,
родитель(X,боб),
write(X,”-родитель боба”),
nl.
Можно описать то же самое по-другому, с использованием предиката без
аргументов и соответствующего правила:
predicates родитель_боба
54
goal
родитель_боба.
clauses
родитель_боба:-
clearwindow,
родитель(X,боб),
write(X,”-родитель боба”),
nl.
Заметим, что внутренняя цель найдет только одного родителя в
соответствии с последовательностью описанных фактов отношения родитель,
а именно Пам. Для поиска всех родителей следует применить специальные
средства поиска решений, о которых речь пойдет в дальнейшем.
2.4 Рекурсия
55
N1=N–1,
вывод_строки(N1).
Пример 2: Напишем программу, выводит на экран целые числа от 5 до 15
в одну строку.
predicates
вывод_чисел(integer)
goal
clearwindow,
write(“Числа от 5 до 15”),nl,
вывод_чисел(5),nl,nl,
write(“Вывод закончен”),nl.
clauses
вывод_чисел(16).
вывод_чисел(N):-write(N, “ “),
N1=N+1,
вывод_чисел(N1).
Пример 3: Напишем программу вычисления n!, где n вводится с
клавиатуры.
domains
число,результат=integer
predicates
факториал(число,результат)
goal
clearwindow,
write(“Введите число для вычисления факториала ”),nl,
readint(N),
факториал(N,P),
write(N,“!=”,P),nl.
clauses
факториал(0,1).
факториал(N,P):-N>0,
N1=N-1,
факториал(N1,P1),
P=P1*N.
Пример 4: Напишем программу размена денежной суммы самыми
«крупными» монетами, если имеется ограниченное количество монет
достоинством 1, 2 и 5 рублей. Количество наличных разменных монет вводится
с клавиатуры.
domains
кол_1,кол_2,кол_5,итого_1,итого_2,итого_5,сумма=integer
predicates
размен(сумма,кол_1,кол_2,кол_5,итого_1,итого_2,итого_5)
мин(integer,integer,integer)
56
goal
clearwindow,
write(“Введите сумму для размена ”), readint(S),
write(“Введите количество наличных монет для размена”),nl,
write(“По 1 рублю - ”), readint(K1),
write(“По 2 рубля - ”), readint(K2),
write(“По 5 рублей - ”), readint(K3),
размен (S,K1,K2,K3,0,0,0).
clauses
размен(0,_,_,_,N1,N2,N3):-S=5*N3+2*N2+N1,
write(S,”=5*,N3,”+2*”,N2,”+”,N1).
размен(S,K1,K2,K3,N1,N2,N3):-S>=5,NN=S div 5,K3>0,
мин(NN,K3,MIN),S1=S-5*MIN,
L3=K3-MIN,M3=N3+MIN,
размен(S1,K1,K2,L3,N1,N2,M3).
размен(S,K1,K2,K3,N1,N2,N3):-S>=2,NN=S div 2,K2>0,
min(NN,K2,MIN),S1=S-2*MIN,
L2=K2-MIN,M2=N2+MIN,
размен(S1,K1,L2,K3,N1,M2,N3).
размен(S,K1,K2,K3,N1,N2,N3):-S>=1,K1>0,
min(S,K1,MIN),S1=S-MIN,
L1=K1-MIN,M1=N1+MIN,
размен(S1,L1,K2,K3,M1,N2,N3).
размен(_,_,_,_,_,_,_):-write(“Разменять не удалось”),nl.
мин(X,Y,X):-X<Y.
мин(_,Y,Y).
57
Рассмотрим насколько порядок предложений и целей, который не
затрагивает декларативную семантику, может изменить процедурную
семантику.
Пример: Рассмотрим определение предиката предок из примера 3 п.2.1:
предок(X,Y):–родитель(X,Y).
предок(X,Y):–родитель(X,Z),предок(Z,Y).
Попробуем поменять порядок следования правил и подцелей в правиле.
Рассмотрим еще три варианта этого отношения предок1, предок2, предок3.
1. предок1(X,Y):–родитель(X,Z),предок1(Z,Y).
предок1(X,Y):–родитель(X,Y).
2. предок2(X,Y):–родитель(X,Y).
предок2(X,Y):– предок2(Z,Y), родитель(X,Z).
3. предок3(X,Y):– предок3(Z,Y), родитель(X,Z).
предок3(X,Y):–родитель(X,Y).
При ответе на вопрос предок(том, боб) получим ответ «да» в результате
применения первого правила.
При ответе на вопрос предок1(том, боб) получим ответ «да» в результате
применения второго правила, но прежде, чем применить это правило Пролог-
система будет пытаться применить первое правило для Боба и всех его
потомков. Решение будет найдено, но его поиск будет идти очень долго.
При ответе на вопрос предок2(том, боб) получим сразу ответ «да» в
результате применения первого правила. Но заметим, что этот предикат не
всегда будет давать верный ответ. Например, на вопрос предок2(лиз, боб) не
будет получено ответа, т.к. на первой подцели второго правила, Пролог-
система уйдет в бесконечную рекурсию.
Ответ на вопрос предок3(том, боб) не будет получен, т.к. на первой
подцели первого правила, Пролог-система уйдет в бесконечную рекурсию.
Таким образом, правильные с декларативной точки зрения программы
могут работать неправильно. При составлении правил следует
руководствоваться следующим:
более простое правило следует ставить на первое место;
по возможности избегать левой рекурсии.
58
Будем считать, что мир обезьяны всегда находится в некотором состоянии,
которое может изменяться со временем. Состояние обезьяньего мира
определяется четырьмя компонентами: горизонтальная позиция обезьяны,
вертикальная позиция обезьяны (на ящике или на полу), позиция ящика,
наличие у обезьяны банана (есть или нет). Объединим эти компоненты в
структуру функтором состояние(symbol, symbol, symbol, symbol).
Задачу можно рассматривать как игру для одного игрока – обезьяны.
Формализуем правила этой игры. Начальное состояние игры:
состояние(удвери, наполу, уокна, нет), цель игры: состояние(_,_,_, да).
Определим возможные ходы обезьяны: перейти в другое место, подвинуть
ящик, залезть на ящик, схватить банан. Не всякий ход допустим для каждого
состояния. Например, ход «схватить банан» допустим только для состояния,
когда обезьяна стоит на ящике в середине комнаты и еще не имеет банана, т.е.
состояние(середина, наящике, середина, нет). В результате хода система
переходит из одного состояния в другое. Для отражения таких переходов
определим предикат ход(состояние, действие, состояние).
domains
состояние=состояние(symbol, symbol, symbol, symbol)
действие=symbol;
подвинуть(symbol, symbol);
перейти(symbol, symbol)
predicates
ход(состояние, действие, состояние)
может_достать(состояние)
goal
может_достать(состояние(удвери, наполу, уокна, нет)).
clauses
ход(состояние(середина, наящике, середина, нет),
схватить,
состояние(середина, наящике, середина,да)).
ход(состояние(P, наполу, P, H),
залезть,
состояние(P, наящике, P, H)).
ход(состояние(P1, наполу, P1, H),
подвинуть(P1, P2),
состояние(P2, наполу, P2, H)).
ход(состояние(P1, наполу, P, H),
перейти (P1, P2),
состояние(P2, наполу, P, H)).
может_достать(состояние(_,_,_,да)).
может_достать(S1):-ход(S1, H, S2),
может_достать(S2).
59
Рассмотрим, как для такой декларативной программы Пролог-система
будет искать решение. Для того чтобы ответить на вопрос, Пролог-системе
пришлось сделать лишь один возврат. Причина такой эффективности –
правильно выбранный порядок следования предложений, описывающих ходы.
Однако возможен и другой порядок, когда обезьяна будет ходить туда-сюда, не
касаясь ящика, или бесцельно двигать ящик в разные стороны. Порядок
предложений и целей важен в программе.
3
y
0
-2 0 2 4 6 8
x
62
Так же на декларативный смысл программы может повлиять перестановка
правил, содержащих отсечения.
Отсечения, которые не затрагивают декларативный смысл программы,
называются зелеными. Отсечения, меняющие декларативный смысл программы
называются красными. Их следует применять с большой осторожностью.
Сформулируем более точно, как работает механизм отсечений. Пусть
имеется правило вида: H : − B1 , , Bk , !, , Bn . Если цели B1 , , B k успешны, то
это решение замораживается, и другие альтернативы для этого решения больше
не рассматриваются (отсекается правая часть дерева решений, которая
находится выше B1 , , B k ).
Пример 2: Напишем предикат, находящий максимум из двух чисел, с
использованием отсечения.
x , если x > y
max( x, y) =
y, иначе
predicates
max(real,real)
clauses
max(X,Y,X):-X>=Y,!.
max(_,Y,Y).
Как видно из приведенных примеров отсечения позволяют описать
конструкцию если-то-иначе.
64
Такие циклы используются для описания взаимодействия с внешней
системой путем повторяющегося ввода или вывода. В таком цикле обязательно
должен быть предикат (в примере – это проверка), приводящий к
безуспешным вычислениям. Рекурсивные циклы предпочтительнее циклов,
управляемых отказами, поскольку последние не имеют логической
интерпретации. Но на практике такие циклы необходимы при выполнении
большого объема вычислений, поскольку рекурсия требует много памяти.
2.7 Списки
65
Пример 1: Деление списка на хвост и голову.
66
2.7.2.2 Соединение двух списков
68
Например, для цели split(3,[5,1,6,3,4],L1,L2) получим решение:
L1=[1,3],L2=[5,6,4].
Предикат split так же можно использовать для слияния списков.
2.7.2.6 Подсписок
69
2.7.3.1 Сортировка вставкой
70
их. Если новые списки получаются примерно одинаковой длины, то временная
сложность алгоритма быстрой сортировки будет примерно n⋅logn. Если же
длины списков сильно различаются, то сложность будет порядка n2. В среднем
время быстрой сортировки ближе к лучшему случаю, чем к худшему. В
программе будем использовать определенные в п.2.8.2.2 и 2.8.2.4 предикатов
append и split.
domains
list=integer*
predicates
q_sort (list,list)
append(list,list,list)
split (integer,list,list,list)
clauses
q_sort([Head|Tail],Sort_list):-
split(Head,Tail,Less,More),
q_sort(Less,Sort_less),
q_sort(More,Sort_more),
append(Sort_less,[Head|Sort_more],Sort_list).
q_sort([],[]).
72
not(member(S1,L)),
path(S1,G,[S1|L],L1),!.
Итак, окончательный вариант программы будет иметь вид:
domains
state=state(symbol,symbol,symbol,symbol)
spisok=state*
predicates
move(state,state)
opposite(symbol,symbol)
danger(state)
path(state,state,spisok,spisok)
member(state,spisok)
goal
S=state(left,left,left,left),
G=state(right,right,right,right),
path(S,G,[S],L),
nl,write('A solution is:'),nl,
write(L).
clauses
move(state(X,X,G,C),state(Y,Y,G,C)):-opposite(X,Y).
move(state(X,W,X,C),state(Y,W,Y,C)):-opposite(X,Y).
move(state(X,W,G,X),state(Y,W,G,Y)):-opposite(X,Y).
move(state(X,W,G,C),state(Y,W,G,C)):-opposite(X,Y).
opposite(right,left).
opposite(left,right).
danger(state(F,_,X,X)):-opposite(F,X).
danger(state(F,X,X,_)):-opposite(F,X).
path(G,G,T,T):-!.
path(S,G,L,L1):- move(S,S1),
not(danger(S1) ),
not(member(S1,L)),
path(S1,G,[S1|L],L1),!.
member(X,[X|_]).
member(X,[_|L]):-member(X,L).
Пролог найдет следующее решение задачи:
[state(left,left,left,left), state(right,left,right,left), state(left,left,right,left),
state(right,left,right,right), state(left,left,left,right), state(right,right,left,right),
state(left,right,left,right), state(right,right,right,right)]
Это соответствует следующему алгоритму:
1. Фермер перевозит козу с левого берега на правый.
2. Фермер перемещается с правого берега на левый.
3. Фермер перевозит капусту с левого берега на правый.
4. Фермер перевозит козу с правого берега на левый.
5. Фермер перевозит волка с левого берега на правый.
6. Фермер перемещается с правого берега на левый.
73
7. Фермер перевозит козу с левого берега на правый.
74
sum1([],[],[],0,0,L,L).
sum1([D1|N1],[D2|N2],[D|N],C1,C,L1,L):-
sum1(N1,N2,N,C1,C2,L1,L2),
sumc(D1,D2,D,C2,C,L2,L).
Осталось определить правила для предиката sumc. Первые три аргумента
предиката должны быть цифрами. Если какая-нибудь из этих переменных не
конкретизирована, то ее необходимо конкретизировать какой-нибудь цифрой из
списка L2. После конкретизации цифру следует удалить из списка доступных
цифр. Если переменная уже имела значение, то удалять из списка доступных
цифр ничего не надо.
sumc(D1,D2,D,C2,C,L2,L):-
delete(D1,L2,L3),
delete(D2,L3,L4),
delete(D,L4,L),
S=D1+D2+C2,
D=S mod 10,
C=S div 10.
Для удаления цифры из списка или для получения значения неозначенной
переменной будем использовать предикат delete. Для проверки означенности
переменной будет использоваться встроенный предикат bound(<переменная>).
delete(X,L,L):-bound(X),!.
delete(X,[X|L],L).
delete(X,[Y|L],[Y|L1]):-delete(X,L,L1).
Итак, окончательный вариант программы будет иметь вид:
domains
list=integer*
predicates
sum(list,list,list)
sum1(list,list,list,integer,integer,list,list)
sumc(integer,integer,integer,integer,integer,list,list)
delete(integer,list,list)
clauses
sum(N1,N2,N):-sum1(N1,N2,N,0,0,[0,1,2,3,4,5,6,7,8,9],_).
sum1([],[],[],0,0,L,L).
sum1([D1|N1],[D2|N2],[D|N],C1,C,L1,L):-sum1(N1,N2,N,C1,C2,L1,L2),
sumc(D1,D2,D,C2,C,L2,L).
sumc(D1,D2,D,C2,C,L2,L):-delete(D1,L2,L3),
delete(D2,L3,L4),
delete(D,L4,L),
S=D1+D2+C2,
D=S mod 10,
C=S div 10.
delete(X,L,L):-bound(X),!.
delete(X,[X|L],L).
75
delete(X,[Y|L],[Y|L1]):-delete(X,L,L1).
Для решения заданного в начале пункта ребуса следует ввести цель:
sum([D,O,N,A,L,D],[G,E,R,A,L,D],[R,O,B,E,R,T])
Для данного ребуса получим единственное решение: D=5, O=2, N=6, A=4,
L=8, G=1, E=9, R=7, B=3, T=0.
2.9 Строки
76
domains
list=char*
predicates
string_listc(string,list)
clauses
string_listc(“”,[]).
string_listc(Str,[Head|Tail]):- frontchar(Str,Head,Str1),
string_listc(Str1,Tail).
5.fronttoken(<строка>, <атом>, <остаток строки>) – выделяет в строке
атом.
Атом (token) - особый вид строки. Атомами являются: последовательности
из букв, цифр и символа подчеркивания, начинающиеся с букв; специальные
символы #, $, ! и т.д.; числа.
Пример 8: Напишем предикат, преобразующий строку в список атомов.
Декларативное описание предиката: первый атом в строке является головой
списка, хвост списка получается из подстроки без первого атома строки; если в
строке нет атомов (но могут остаться пробелы), то список пуст.
domains
list=symbol*
predicates
string_listt(string,list)
clauses
string_listt(Str,[Head|Tail]):-fronttoken(Str,Head,Str1),!,
string_listt(Str1,Tail).
string_listt(_,[]).
Пример 9: Напишем предикат, преобразующий строку в список атомов c
функторами. Такой предикат может использоваться для записи в базу данных
фактов, относящихся к некоторому одноместному предикату, объектами
которого могут быть атомы из строки символов. Пусть в качестве функтора
выступает имя предиката женщина, а строка состоит из имен женщин.
domains
fact=женщина(symbol)
list=fact*
predicates
string_listf(string,list)
create_fact(symbol,fact)
clauses
string_listf(Str,[Head|Tail]):- fronttoken(Str,Token,Str1),!,
create_fact(Token,Head),
string_listf(Str1,Tail).
string_listf(_,[]).
create_fact(Token,женщина(Token)).
Для цели string_ listf(“энн пат лиз джейн”, L) получим решение:
77
L=[женщина(энн),женщина(пат),женщина(лиз),женщина(джейн)]
79
clauses
write_to_file:-readchar(X), /* Вводимый символ на экране не отображается */
write(X), /* Вывод символа на экран*/
openwrite(myfile,”t.txt”), /* Открытие файла для записи*/
writedevice(myfile), /* Перенаправление вывода в файл*/
write_char(X), /* Запись символа в файл */
closefile(myfile), /* Закрытие файла*/
writedevice(screen), /* Перенаправление вывода на экран*/
nl,write(“Данные записаны в файл”),nl.
write_char(‘#’).
write_char(‘\13’):-
nl, /* Если нажата клавиша Enter, то переход на следующую строку в файле*/
writedevice(screen), /* Перенаправление вывода на экран*/
nl, /* Переход на следующую строку на экране*/
readchar(X), /* Ввод следующего символа*/
write(X), /* Вывод символа на экран*/
writedevice(myfile), /* Перенаправление вывода в файл*/
write_char(X). /* Продолжение ввода*/
write_char(X):-write(X), /* Вывод символа в файл*/
writedevice(screen), /* Перенаправление вывода на экран*/
readchar(Y), /* Ввод следующего символа*/
write(Y), /* Вывод символа на экран*/
writedevice(myfile), /* Перенаправление вывода в файл*/
write_char(Y). /* Запись символа в файл */
Пример 4: Напишем предикат add_to_file, который записывает вводимые
с клавиатуры строки в файл, имя которого вводится с клавиатуры. Причем, если
файл уже существовал, введенные строки добавляются в конец файла.
Окончание ввода – строка “end”.
domains
file=myfile
predicates
add_to_file
write_string(string)
check_exist(string)
goal
add_to_file.
clauses
add_to_file:-write(“Введите имя файла: “),
readln(Filename), /* Ввод с клавиатуры имени файла */
check_exist(Filename), /* Проверка существования файла*/
write(“Введите строки для добавления в файл”),nl,
writedevice(myfile), /* Перенаправление вывода в файл*/
readln(String), /* Ввод с клавиатуры строки*/
write_string(String), /* Запись введенной строки в файл*/
80
closefile(myfile). /* Закрытие файла*/
check_exist(Filename):-existfile(Filename),!,
openappend(myfile,Filename). /* Открытие файла для добавления*/
check_exist(Filename):-write(“Создан новый файл”),nl,
openwrite(myfile,Filename). /* Создание нового файла для записи*/
write_string(“end”):-!.
write_string(String):-
write(String), nl, /* Запись строки в файл с переводом строки*/
readln(String1), /* Ввод с клавиатуры новой строки*/
write_string(String1). /* Запись новой строки в файл*/
Пример 5: Напишем предикат read_file, который выводит на экран строки
из файла, начиная с некоторого номера. Имя файла и номер строки вводятся с
клавиатуры.
domains
file=myfile
predicates
read_file(integer)
write_screen
check_exist(string)
goal
write(“Введите имя файла: “),
readln(Filename), /* Ввод с клавиатуры имени файла */
check_exist(Filename), /* Проверка существования файла*/
write(“Введите номер строки для вывода строк из файла на экран ”),
readint(N),
openread (myfile,Filename), /* Открытие файла для чтения*/
readdevice(myfile), /* Перенаправление ввода на файл*/
read_file(N), /* Пропуск в файле N-1 строки*/
write(“Содержимое файла, начиная с ”,N,”-ой строки:”),nl,
write_screen, /* Вывод на экран строк, начиная с N-ой*/
closefile(myfile). /* Закрытие файла*/
clauses
check_exist(Filename):-existfile(Filename),!.
check_exist(_):-write(“Такого файла нет”),nl,
fail.
read_file(1):-!.
read_file(N):-readln(_), /*Пропускаем строку*/
N1=N-1,
read_file(N1).
read_file(N):-eof(myfile),!,
write(“В файле меньше, чем ”,N,” строк”),nl,
fail.
write_screen:- eof(myfile),!.
write_screen:- readln(String), /* Чтение из файла строки*/
81
write(String),nl, /* Вывод строки на экран и перевод строки*/
write_screen.
Иногда бывает полезен предикат
file_str(<имя физического файла >,<строка>), который считывает в указанную
строку всех символов из файла или наоборот содержимое строки записывает в
файл. Размер файла должен быть не больше 64K. Если файл не существовал, то
он создастся.
83
2.11.2 Заполнение динамической базы данных фактами из файла,
сохранение динамической базы данных в файле
84
Z=X*Y,
assertz(multiply (X,Y,Z)), /* Добавление факта в конец базы данных */
count(N), /* Чтение значения счетчика из динамической базы данных */
N1=N+1, /* Новое значение счетчика */
retract(count(N)), /* Удаление старого значения счетчика из
динамической базы данных */
asserta(count(N1)), /* Добавление нового значения счетчика в
динамическую базу данных */
N1=81. /* Проверка последней записи */
Как видно из примера, в динамической базе данных можно накапливать
уже вычисленные ответы на вопросы.
Пример 2: Формирование динамической базы данных «Читатель
библиотеки» с клавиатуры и сохранение ее в файле.
domains
фамилия=string
число,номер_билета=integer
месяц=string
дата_посещ=дата_посещ(число,месяц)
database
читатель(фамилия,номер_билета,дата_посещ)
predicates
повтор
запись_в_базу
ответ(char)
goal
clearwindow,
write("Формирование б.д."),nl,
retractall(_), /* Очистка динамической памяти */
повтор,
write("Будете вводить новые факты? Y/N"),
readchar(A), /* Ввод ответа, на экране не отображается */
upper_lower(Answer,A),nl, /* Преобразование маленькой буквы в большую*/
ответ(Answer), /* Определяет дальнейшую работу программы */
save("reader.dba"), /* Сохранение содержимого динамической базы данных
в файле */
retractall(_). /* Очистка динамической памяти */
clauses
повтор.
повтор:-повтор.
ответ('N').
ответ('Y'):-запись_в_базу,
fail.
ответ('_'):-fail. /* Если введен неверный ответ, повторяется вопрос */
запись_в_базу:-write("Фамилия: "),
85
readln(Name),
write("Номер билета: "),
readint(Number),
write("Число посещения: "),
readint(Data),
write("Месяц посещения: "),
readln(Mounth),
asserta(читатель(Name,Number,дата_посещ(Data,Mounth))).
/* Сохранение введенной записи в динамической базе данных */
Пример 3: Определение количества читателей, посетивших библиотеку в
марте, по информации, находящейся в файле reader.dba, сформированном в
примере 2.
domains
фамилия=string
число,номер_билета=integer
месяц=string
дата_посещ=дата_посещ(число,месяц)
database
читатель(фамилия,номер_билета,дата_посещ)
счетчик(integer)
predicates
счет
goal
consult("reader.dba"),
asserta(счетчик(0)),
счет,
счетчик(N),
write("Число читателей= ",N),
retractall(_).
clauses
счет:-читатель(_,_,дата_посещ(_,”март”)),
счетчик(N),
N1=N+1,
retract(счетчик(N)),
asserta(счетчик(N1)),
fail.
счет.
86
• WN – целое число, определяет номер окна (нумерация начинается с 1);
это число используется для в качестве ссылки в предикате gotowindow и др.
• ScrAttr – целое число, определяет атрибут окна, который является
суммой трех чисел (цвет текста, цвет фона, мерцание). Цвет текста может
изменяться от 0 до 15, цвет фона – от 0 до 112 с шагом 16, мерцание – число
128.
• FrAttr – целое число, определяет атрибут рамки. Если рамки нет, то
атрибут равен 0. Значения от 1 до 8 соответствуют разным цветам рамки, а
значения от -8 до -1 соответствуют разным цветам мерцающей рамки.
• Caption – строка, определяет текст заголовка окна в верхней части рамки.
Если строка пустая, то заголовок отсутствует.
• Row, Col - целые числа, определяют строку и столбец верхнего левого
угла создаваемого окна. В текстовом режиме на экране 25 строк и 80 столбцов.
Нумерация строк идет слева направо, а столбцов – сверху вниз.
• Height, Width - целые числа, высота и ширина окна, включая рамку.
При создании окно заполняется цветом фона, а курсор устанавливается в
его верхний левый угол.
Пример 1: makewindow(1,4,1,“Menu”,4,20,16,40) создаст окно с номером
1, заголовком Menu. Цвет текста в окне будет красным, фон окна - черный
(4=4+0+0), рамка – синего цвета (1). Левый верхний угол окна расположен в 4-
ой строке и 20-ом столбце экрана, окно занимает 16 строк и 40 столбцов.
Если все аргументы предиката makewindow являются неозначенными
переменными, то им присваивается значение текущего окна.
Когда окно создано, оно становится активным, и вся выводимая
информация будет направляться в активное окно.
При помощи предиката shiftwindow(WN) активным может быть назначено
другое окно. При этом курсор устанавливается в позицию, в которой он был в
момент предыдущего обращения к этому окну. Если аргумент предиката
неозначен, то определяется номер активного окна.
Для более быстрого переключения между окнами, содержащими большое
количество текста, используется предикат gotowindow(WN). Этот предикат не
сохраняет содержимое активного окна и не обновляет содержимое нового
активного окна из буфера, поэтому при ипользовании этого предиката окна
должны быть неперекрывающимися.
Предикат removewindow удаляет активное окно, при этом активным
становится окно, активизированное непосредственно перед удаляемым. При
удалении окна содержимое экрана «за окном» автоматически
восстанавливается.
Предикат clearwindow, который ранее использовали для очистки экрана,
очищает так же активное окно.
Для перемещения курсора в нужную позицию окна можно использовать
предикат cursor(Row,Col), где Row,Col – координаты курсора относительно
верхнего левого угла окна. В случае неозначенности аргументов предиката,
возвращаются координаты текущего положения курсора в активном окне.
87
Для ввода и вывода текста в окна используются уже известные предикаты:
write, nl, readchar, readint, readln, readreal, readterm. Ввод и вывод текста
осуществляется в активное окно в текущую позицию курсора.
Для задержки окна до нажатия любой клавиши можно в программе после
активации окна добавить цели:
write(“Для продолжения нажмите любую клавишу”),
readchar(_),
……………
Пример 2: Напишем предикат show_menu, который создает окно с
главным меню, состоящим из трех пунктов. При выборе пользователем
соответствующего пункта меню, появляется дочернее окно, в котором
выполняются необходимые действия. По окончанию действий дочернее окно
закрывается и снова можно выбирать пункты главного меню до тех пор, пока
не будет выбран выход.
predicates
repeat
process(integer)
show_menu
goal
show_menu.
clauses
repeat.
repeat:-repeat.
show_menu:-repeat,
makewindow(1,7,7,“Menu”,4,10,16,36),nl,
write(“1 – процесс1”),nl,
write(“2 – процесс2”),nl,
write(“3 – выход”),nl,nl,
write(“Введите Ваш выбор: (1-3)”),
readint(X),
X<4,
process(X),
X=0,!.
process(3).
process(1):- makewindow(2,7,7,“Процесс1”,12,36,10,36),
………………
write(“Для продолжения нажмите любую клавишу”),
readchar(_),
removewindow.
process(2):- makewindow(3,7,7,“Процесс2”,10,40,10,36),
………………
write(“Для продолжения нажмите любую клавишу”),
readchar(_),
removewindow.
88
2.13 Операции над структурами данных
2.13.1 Деревья
89
tree(4,
tree(8,
tree(6,nil,nil),
tree(2,nil,nil)),
tree(3,nil,nil)),
tree(1,
tree(7,nil,nil),
nil)),
write("Введите элемент: "),
readint(X),
answer(X,Tree).
clauses
answer(X,Tree):-member_tree(X,Tree),!,
write("Да").
answer(_,_):-write("Нет").
member_tree(X,tree(X,_,_)).
member_tree(X,tree(_,Left,_)):-member_tree(X,Left).
member_tree(X,tree(_,_,Right)):-member_tree(X,Right).
Очевидно, что если элемент находится в самом низу дерева, процедура
поиска становится такой же неэффективной, как и в случае списков. Введем
отношение порядка между элементами множества. Тогда элементы множества
можно упорядочить слева направо в соответствии с этим отношением. Выберем
отношение «меньше» для упорядочивания элементов множества, содержащего
целые числа. Двоичным справочником назовем непустое дерево, для которого
выполняется:
− все вершины левого поддерева меньше корня;
− все вершины правого поддерева больше корня;
− левое и правое поддеревья являются двоичными справочниками.
Пример 2: Приведенное ниже дерево является двоичным справочником:
91
− выводит корень дерева;
− отображает левое поддерево с отступом вправо.
Для вывода дерева с заданным отступом определим предикат
show_tree(tree, space), а для печати нужного количества пробелов – предикат
print_blank(space).
domains
tree=tree(root,tree,tree);
nil
root=integer
space=integer
predicates
show_tree(tree)
show_tree(tree,space)
print_blank(space)
clauses
show_tree(Tree):-show_tree(Tree,0).
show_tree(nil,_).
show_tree(tree(X,Left,Right),Space):-
Space1=Space+4,
show_tree(Right,Space1),
print_blank(Space),
write(X),nl,
show_tree(Left,Space1).
print_blank(0).
print_blank(Space):-
write(“ “),
Space1=Space-1,
print_blank(Space1).
Попробуйте написать предикат, печатающий дерево сверху вниз.
2.13.2 Графы
94
path=vertex*
predicates
member(vertex,path)
neighbour(vertex,vertex)
edge(vertex,vertex)
path(vertex,vertex,path)
path1(vertex,path,path)
find_path(vertex,vertex)
edge(vertex,vertex)
goal
write("Введите начальную вершину\n"),
readln(A),
write("Введите конечную вершину\n"),
readln(B),
find_path(A,B).
clauses
edge(a,b).
edge(a,d).
edge(a,e).
edge(b,e).
edge(c,d).
edge(c,e).
edge(e,d).
edge(d,e).
find_path(A,B):-path(A,B,Path),!,
write("Путь=",Path).
find_path(_,_):-write("Пути нет").
path(A,Z,Path):-path1(A,[Z],Path).
path1(A,[A|Path1],[A|Path1]).
path1(A,[Y|Path1],Path):-
neighbour(X,Y),
not(member(X,Path1)),
path1(A,[X,Y|Path1],Path).
member(X,[X|_]).
member(X,[_|Tail]):-member(X,Tail).
neighbour(X,Y):-edge(X,Y);
edge(Y,X).
Заметим, что данная программа находит первый попавшийся путь из
вершины A в B. Предикат path можно использовать для решения
разнообразных задач. Например, для поиска всех вершин, достижимых из
заданной, для поиска пути заданной длины от выделенной вершины, для
нахождения диаметра графа (максимального расстояния между двумя
вершинами), гамильтонова цикла (цикла, проходящего через все вершины),
количества компонент связности графа. При решении этих задач удобно
95
использовать для обмена значениями между предикатами динамическую базу
данных.
Пример 3: Модифицируем приведенную в примере 2 программу для
поиска пути минимальной стоимости между двумя вершинами во взвешенном
графе.
domains
vertex=symbol
path=vertex*
length_path=integer
weigth=integer
database
bd_path(path,length_path)
predicates
member(vertex,path)
neighbour(vertex,vertex, weigth)
edge(vertex,vertex,weigth)
path(vertex,vertex,path,length_path)
path1(vertex,path, length_path,path,length_path)
find_begining(vertex,vertex)
find_path(vertex,vertex)
edge(vertex,vertex)
goal
write("Введите начальную вершину\n"),
readln(A),
write("Введите конечную вершину\n"),
readln(B),
find_begining(A,B),
find_path(A,B),
bd_path(Path,Length),
write("Путь=",Path,”Длина=”,Length),
retractall(_).
clauses
find_begining(A,Z):-
path(A,Z,Path,Length),
assertz(bd_path(Path,Length)).
path(A,Z,Path,Length):-path1(A,[Z],0,Path,Length).
path1(A,[A|Path1],Length,[A|Path1], Length).
path1(A,[Y|Path1],Length1,Path,Length):-
neighbour(X,Y,Weigth),
not(member(X,Path1)),
Length2=Length1+1+Weigth;
path1(A,[X,Y|Path1],Length2,Path,Length).
member(X,[X|_]).
member(X,[_|Tail]):-member(X,Tail).
96
neighbour(X,Y):-edge(X,Y);
edge(Y,X).
find_path(A,B):-
bd_path(Path,Length),
path(A,B,Path1,Length1),
Length1<Length,!,
retract(bd_path(Path,Length)),
assertz(bd_path(Path1,Length1)),
find_path(A,B).
find_path(_,_).
97
path1_depth(A,[X,Y|Path1],Path, Length).
98
Расчетно-графическое задание
99
1.14 В списке слов найти слово максимальной длины и поставить его
последним.
1.15 В списке слов найти слово минимальной длины и поставить его первым.
1.16 Проверить является ли слово палиндромом, т.е. одинаково ли оно читается
слева направо и наоборот.
1.17 Найдите элемент списка по его номеру.
1.18 Удалите из списка все вхождения элемента X.
Например: [1,2,1,3,1,1,2,5,6],X=1->[2,3,2,5,6].
1.19 Возведите в квадрат каждый третий элемент списка.
1.20 В списке из каждой группы подряд идущих элементов оставьте только
один.
Например: [1,1,2,2,3,3,3,4,4,4,4]->[1,2,3,4].
1.21 Вставьте в список новый элемент X перед всеми вхождениями Y, если Y
входит в исходный список.
Например: [1,2,1,5,8,1,0],X=5,Y=1->[5,1,2,5,1,5,8,5,1,0].
1.22 Удалите из списка перед каждым вхождением X один элемент, если такой
имеется и отличен от X.
Например: [1,2,1,1,3],X=1->[1,1,1,3].
1.23 Удалите из списка за каждым вхождением X один элемент, если такой
имеется и отличен от X.
Например: [1,8,2,1,1,5,1],X=1->[1,2,1,1,1].
1.24 Сформируйте новый список без повторений из тех элементов исходного
списка, которые встречаются больше одного раза.
Например:[5,1,2,1,3,5,5,7]->[5,1].
1.25 Вставьте в список элементы другого списка после каждого вхождения X,
если X входит в исходный список.
Например: [1,2,1,5,3],X=1,[0,7,9]->[1,0,7,9,2,1,0,7,9,5,3].
2. Строки, файлы.
100
2.5 Файл содержит сведения о сотрудниках учреждения в виде:
фамилия имя отчество
(сведения о каждом сотруднике размещаются на новой строке). Запишите эти
сведения в другой файл по образцу:
фамилия и. о.
2.6 В текстовом файле, состоящем из нескольких строк, упорядочите слова
лексикографически.
2.7 В каждой строке текстового файла переставьте слова в обратном порядке.
2.8 В текстовом файле, состоящем из нескольких строк, подсчитайте
количество символов, исключая пробелы.
2.9 В текстовом файле, состоящем из нескольких строк, найдите слова,
содержащие наибольшее число гласных букв (a,e,i,o,u).
2.10 Найдите в файле все слова минимальной длины.
2.11 Найдите в файле все слова максимальной длины.
2.12 Удалите все повторные вхождения слов в текстовом файле.
2.13 Переставьте строки текстового файла в обратном порядке.
2.14 Обратите все слова в текстовом файле, сохраняя порядок слов.
2.15 Преобразуйте текстовый файл, состоящий из нескольких строк, таким
образом, чтобы все слова в каждой строке разделялись символом *.
2.16 Файл содержит сведения о сотрудниках учреждения в виде:
фамилия имя отчество
(сведения о каждом сотруднике размещаются на новой строке). Запишите эти
сведения в другой файл по образцу:
имя отчество фамилия.
2.17 Преобразуйте текстовый файл, состоящий из нескольких строк, таким
образом, чтобы все символы в каждой строке разделялись одним пробелом.
2.18 Удалите из текстового файла, состоящего из нескольких строк, все
однобуквенные слова и лишние пробелы.
2.19 В текстовом файле, состоящем из нескольких строк, найдите все
одинаковые слова и их количество и информацию об этом занесите в
отдельную строку нового файла в виде:
<слово> - <количество повторений> повторов
Строки в новом файле не должны повторяться.
2.20 В текстовом файле, состоящем из нескольких строк, определите
количество слов в каждой строке и информацию об этом занесите в отдельную
строку нового файла в виде:
cтрока №<номер строки> содержит <количество слов в строке> слов
101
2.21 В текстовом файле, состоящем из нескольких строк, удалите все пробелы и
занесите полученную последовательность символов в новый файл по 20
элементов в строке.
2.22 Каждая строка текстового файла f2 состоит из пары слов: первое считается
заменяемым, второе - заменяющим. Замените в текстовом файле f1, состоящем
из нескольких строк, все заменяемые слова на соответствующие заменяющие.
2.23 В текстовом файле удалите в каждой строке лишние пробелы и найдите
строки максимальной длины.
2.24 В текстовом файле удалите в каждой строке лишние пробелы и найдите
строки минимальной длины.
2.25 Из текстового файла, содержащего несколько строк, удалите слова
максимальной длины и лишние пробелы.
3.1 Создайте базу данных о студентах вашей группы: фамилия, имя, год
рождения.
Выясните, имеются ли в группе однофамильцы; однофамильцы старше 19 лет.
3.2 Создайте базу данных об игрушках:
название, стоимость, возрастные границы
Получите названия наиболее дорогих игрушек (цены которых отличаются от
самой дорогой не более, чем на 10 рублей).
3.3 Создайте базу данных городского транспорта:
название транспорта, номер маршрута, список остановок
Определите, как добраться от остановки "Площадь Ленина" до остановки
"ГПНТБ" без пересадок.
3.4 Создайте базу данных об итогах сессии по вашей группе:
фамилия, имя, предмет1(оценка1), предмет2(оценка2), предмет3(оценка3),
предмет4(оценка4)
Сформируйте список студентов на отчисление, имеющих не менее двух
двоек.
3.5 Создайте базу данных с расписанием движения поездов:
номер поезда, пункт назначения, время отправления, время прибытия, время в
пути. Найдите номер и время отправления скорого поезда до Москвы.
3.6 Создайте базу данных с расписанием движения самолетов:
номер рейса, пункт прибытия, время отправления, время прибытия, время в
пути, стоимость билетов(по классам)
Определите маршрут до Лондона и стоимость билетов всех классов.
3.7 Создайте базу данных с книжным каталогом:
102
Ф.И.О. автора, Название книги, Издательство, Год издания.
Найдите все книги, изданные в издательстве "Наука" после 1980 года.
3.8 Создайте базу данных о товарах:
наименование товара, фасовка, срок годности, стоимость.
Найдите товары с минимальной и максимальной ценой.
3.9 Создайте базу данных с таблицей игр чемпионата по футболу: первая
команда, вторая команда, счет игры.
Определите чемпиона.
3.10 Создайте базу данных о сотрудниках: фамилия, имя, должность, оклад.
Сформируйте список сотрудников с окладом выше среднего по предприятию.
3.11 Создайте базу данных об итогах сессии по вашей группе:
фамилия, имя, предмет1(оценка1), предмет2(оценка2), предмет3(оценка3),
предмет4(оценка4)
Сформируйте список студентов сдавших сессию на 4 и 5 не менее чем по трем
предметам.
3.12 Создайте базу данных об игрушках:
название, стоимость, возрастные границы
Получите названия самых дешевых игрушек, подходящих ребенку 3 лет.
3.13 Создайте базу данных городского транспорта: название транспорта, номер
маршрута, список остановок
Определите, как добраться от остановки "Площадь Ленина" до остановки
"ГПНТБ" без пересадок.
3.14 Создайте базу о студентах вашей группы: фамилия, имя, год рождения.
Выясните, имеются ли в группе однофамильцы; однофамильцы старше 19 лет.
3.15 Создайте базу данных об игрушках: название, стоимость, возрастные
границы
Получите названия наиболее дорогих игрушек (цены которых отличаются от
самой дорогой не более, чем на 10 рублей).
3.16 Создайте базу данных городского транспорта:
название транспорта, номер маршрута, список остановок
Определите, как добраться от остановки "Площадь Ленина" до остановки
"ГПНТБ" без пересадок.
3.17 Создайте базу данных об итогах сессии по вашей группе:
фамилия, имя, предмет1(оценка1), предмет2(оценка2), предмет3(оценка3),
предмет4(оценка4)
Сформируйте список студентов на отчисление, имеющих не менее двух
двоек.
3.18 Создайте базу данных с расписанием движения поездов:
номер поезда, пункт назначения, время отправления, время прибытия, время в
пути
Найдите номер и время отправления скорого поезда до Москвы.
103
3.19 Создайте базу данных ,содержащую сведения о пассажирах: фамилия, имя,
отчество, количество мест багажа, вес багажа Определите общий вес багажа
всех пассажиров.
3.20 Создайте базу данных о товарах:
наименование товара, фасовка, срок годности, стоимость. Найдите товары с
минимальной и максимальной ценой.
3.21 Создайте базу данных с таблицей игр чемпионата по футболу: первая
команда, вторая команда, счет игры.
Определите чемпиона.
3.22 Создайте базу данных о сотрудниках: фамилия, имя, должность, оклад.
Сформируйте список сотрудников с окладом выше среднего по предприятию.
3.23 Создайте базу данных об итогах сессии по вашей группе:
фамилия, имя, предмет1(оценка1), предмет2(оценка2), предмет3(оценка3),
предмет4(оценка4)
Сформируйте список студентов сдавших сессию на 4 и 5 не менее чем по трем
предметам.
3.24 Создайте базу данных о металлах: наименование, удельная проводимость,
стоимость
Определите металлы с максимальной проводимостью и минимальной
стоимостью.
3.25 Создайте базу данных городского транспорта: название транспорта, номер
маршрута, список остановок
Определите, как добраться от остановки "Площадь Ленина" до остановки
"ГПНТБ" без пересадок.
4.1 Найдите все вершины графа, к которым существует путь заданной длины от
выделенной вершины графа.
4.2 Найдите все вершины графа, достижимые из заданной.
4.3 Подсчитайте количество компонент связности заданного графа.
4.4 Найдите диаметр графа, т.е. максимум расстояний между всевозможными
парами его вершин.
4.5 Задан взвешенный граф. Постройте остовное дерево минимальной
стоимости.
4.6 Определите, является ли граф гамильтоновым. Найдите гамильтонов цикл,
т.е. цикл, проходящий через все вершины графа.
4.7 Задана система односторонних дорог. Найдите путь, соединяющий города
А1 и А2 и не проходящий через заданное множество городов.
4.8 В заданном графе определить все его четырехвершинные полные подграфы.
104
4.9 Проверьте является ли граф свободным деревом. Свободное дерево - это
связный граф, не имеющий циклов.
4.10 Найдите глубину двоичного дерева. Глубина пустого дерева равна 0,
одноэлементного - 1.
4.11 Найдите самый длинный цикл в направленном графе.
4.12 Найдите максимальный элемент двоичного дерева.
4.13 Найдите минимальный элемент двоичного дерева.
4.14 Преобразовать двоичное дерево в двоичный справочник.
4.15 Найдите все вершины графа, к которым существует путь заданной длины
от выделенной вершины графа.
4.16 Найдите все вершины графа, достижимые из заданной.
4.17 Подсчитайте количество компонент связности заданного графа.
4.18 Найдите диаметр графа, т.е. максимум расстояний между всевозможными
парами его вершин.
4.19 Задан взвешенный граф. Постройте остовное дерево минимальной
стоимости.
4.20 Определите, является ли граф гамильтоновым. Найдите гамильтонов цикл,
т.е. цикл, проходящий через все вершины графа.
4.21 Задана система односторонних дорог. Найдите путь, соединяющий города
А1 и А2 и не проходящий через заданное множество городов.
4.22 В заданном графе определить все его четырехвершинные полные
подграфы.
4.23 Проверьте является ли граф свободным деревом. Свободное дерево - это
связный граф, не имеющий циклов.
4.24 Найдите самый длинный цикл в направленном графе.
4.25 Преобразовать двоичное дерево в двоичный справочник.
105
Рекомендуемая литература
106
Марина Юрьевна Галкина
Методические указания
Подписано в печать..........
Формат бумаги 60 x 84/16, отпечатано на ризографе, шрифт № 10,
изд. л......,заказ №.....,тираж-150 экз., СибГУТИ.
630102, г. Новосибирск, ул. Кирова, 86.
107