- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 1. Введение
- •1.1. Новые инструменты
- •1.2. Новые приемы
- •1.3. Новый подход
- •Глава 2. Добро пожаловать в Лисп
- •2.1. Форма
- •2.2. Вычисление
- •2.3. Данные
- •2.4. Операции со списками
- •2.5. Истинность
- •2.6. Функции
- •2.7. Рекурсия
- •2.8. Чтение Лиспа
- •2.9. Ввод и вывод
- •2.10. Переменные
- •2.11. Присваивание
- •2.12. Функциональное программирование
- •2.13. Итерация
- •2.14. Функции как объекты
- •2.15. Типы
- •2.16. Заглядывая вперед
- •Итоги главы
- •Упражнения
- •Глава 3. Списки
- •3.1. Ячейки
- •3.2. Равенство
- •3.3. Почему в Лиспе нет указателей
- •3.4. Построение списков
- •3.5. Пример: сжатие
- •3.6. Доступ
- •3.7. Отображающие функции
- •3.8. Деревья
- •3.9. Чтобы понять рекурсию, нужно понять рекурсию
- •3.10. Множества
- •3.11. Последовательности
- •3.12. Стопка
- •3.13. Точечные пары
- •3.14. Ассоциативные списки
- •3.15. Пример: поиск кратчайшего пути
- •3.16. Мусор
- •Итоги главы
- •Упражнения
- •Глава 4. Специализированные структуры данных
- •4.1. Массивы
- •4.2. Пример: бинарный поиск
- •4.3. Строки и знаки
- •4.4. Последовательности
- •4.5. Пример: разбор дат
- •4.6. Структуры
- •4.7. Пример: двоичные деревья поиска
- •4.8. Хеш-таблицы
- •Итоги главы
- •Упражнения
- •Глава 5. Управление
- •5.1. Блоки
- •5.2. Контекст
- •5.3. Условные выражения
- •5.4. Итерации
- •5.5. Множественные значения
- •5.6. Прерывание выполнения
- •5.7. Пример: арифметика над датами
- •Итоги главы
- •Упражнения
- •Глава 6. Функции
- •6.1. Глобальные функции
- •6.2. Локальные функции
- •6.3. Списки параметров
- •6.4. Пример: утилиты
- •6.5. Замыкания
- •6.6. Пример: строители функций
- •6.7. Динамический диапазон
- •6.8. Компиляция
- •6.9. Использование рекурсии
- •Итоги главы
- •Упражнения
- •Глава 7. Ввод и вывод
- •7.1. Потоки
- •7.2. Ввод
- •7.3. Вывод
- •7.4. Пример: замена строк
- •7.5. Макрознаки
- •Итоги главы
- •Упражнения
- •Глава 8. Символы
- •8.1. Имена символов
- •8.2. Списки свойств
- •8.3. А символы-то не маленькие
- •8.4. Создание символов
- •8.5. Использование нескольких пакетов
- •8.6. Ключевые слова
- •8.7. Символы и переменные
- •8.8. Пример: генерация случайного текста
- •Итоги главы
- •Упражнения
- •Глава 9. Числа
- •9.1. Типы
- •9.2. Преобразование и извлечение
- •9.3. Сравнение
- •9.4. Арифметика
- •9.5. Возведение в степень
- •9.6. Тригонометрические функции
- •9.7. Представление
- •9.8. Пример: трассировка лучей
- •Итоги главы
- •Упражнения
- •Глава 10. Макросы
- •10.1. Eval
- •10.2. Макросы
- •10.3. Обратная кавычка
- •10.4. Пример: быстрая сортировка
- •10.5. Проектирование макросов
- •10.6. Обобщенные ссылки
- •10.7. Пример: макросы-утилиты
- •10.8. На Лиспе
- •Итоги главы
- •Упражнения
- •Глава 11. CLOS
- •11.1. Объектно-ориентированное программирование
- •11.2. Классы и экземпляры
- •11.3. Свойства слотов
- •11.4. Суперклассы
- •11.5. Предшествование
- •11.6. Обобщенные функции
- •11.7. Вспомогательные методы
- •11.8. Комбинация методов
- •11.9. Инкапсуляция
- •11.10. Две модели
- •Итоги главы
- •Упражнения
- •Глава 12. Структура
- •12.1. Разделяемая структура
- •12.2. Модификация
- •12.3. Пример: очереди
- •12.4. Деструктивные функции
- •12.5. Пример: двоичные деревья поиска
- •12.6. Пример: двусвязные списки
- •12.7. Циклическая структура
- •12.8. Неизменяемая структура
- •Итоги главы
- •Упражнения
- •Глава 13. Скорость
- •13.1. Правило бутылочного горлышка
- •13.2. Компиляция
- •13.3. Декларации типов
- •13.4. Обходимся без мусора
- •13.5. Пример: заранее выделенные наборы
- •13.6. Быстрые операторы
- •13.7. Две фазы разработки
- •Итоги главы
- •Упражнения
- •Глава 14. Более сложные вопросы
- •14.1. Спецификаторы типов
- •14.2. Бинарные потоки
- •14.3. Макросы чтения
- •14.4. Пакеты
- •14.5. Loop
- •14.6. Особые условия
- •Глава 15. Пример: логический вывод
- •15.1. Цель
- •15.2. Сопоставление
- •15.3. Отвечая на запросы
- •15.4. Анализ
- •Глава 16. Пример: генерация HTML
- •16.1. HTML
- •16.2. Утилиты HTML
- •16.3. Утилита для итерации
- •16.4. Генерация страниц
- •Глава 17. Пример: объекты
- •17.1. Наследование
- •17.2. Множественное наследование
- •17.3. Определение объектов
- •17.4. Функциональный синтаксис
- •17.5. Определение методов
- •17.6. Экземпляры
- •17.7. Новая реализация
- •17.8. Анализ
- •Комментарии
- •Алфавитный указатель
4.7. Пример: двоичные деревья поиска |
85 |
Кроме того, можно управлять такими вещами, как способ отображения структуры и префикс имен функций для доступа к полям. Вот более развитый вариант определения структуры point:
(defstruct (point (:conc-name p) (:print-function print-point))
(x 0) (y 0))
(defun print-point (p stream depth)
(format stream "#<~A, ~A>" (px p) (py p)))
Аргумент :conc-name задает префикс, с которого будут начинаться име на функций для доступа к полям структуры. По умолчанию он равен point-, а в новом определении это просто p. Отход от варианта по умолча нию делает код менее читаемым, поэтому использовать более короткий префикс стоит, только если вам предстоит постоянно пользоваться функциями доступа к полям.
Параметр :print-function – это имя функции, которая будет вызываться для печати объекта, когда его нужно будет отобразить (например, в top level) . Такая функция должна принимать три аргумента: сам объект; поток, куда он будет напечатан; третий аргумент обычно не требуется и может быть проигнорирован1. С потоками ввода-вывода мы познако мимся подробнее в разделе 7.1. Сейчас достаточно сказать, что второй аргумент, поток, может быть передан функции format.
Функция print-point будет отображать структуру в такой сокращенной форме:
> (make-point) #<0,0>
4.7. Пример: двоичные деревья поиска
Поскольку в Common Lisp имеется встроенная функция sort, вам, ско рее всего, не придется самостоятельно писать процедуры поиска. В этом разделе показано, как решить похожую задачу, для которой нет встро енной функции: поддержание набора объектов в отсортированном виде. Мы рассмотрим метод хранения объектов в двоичном дереве поиска (BST). Сбалансированное BST позволяет искать, добавлять или удалять элементы за время, пропорциональное log n, где n – количество объек тов в наборе.
BST – это бинарное дерево, в котором для каждого элемента и некото рой функции упорядочения (пусть это будет функция <) соблюдается правило: левый дочерний элемент < элемента-родителя, и сам элемент >
1В ANSI Common Lisp вы можете передавать в :print-object функцию двух па раметров. Кроме того, существует макрос print-unreadable-object, который может быть использован для отображения объекта в виде #<...>.
86 |
Глава 4. Специализированные структуры данных |
правого дочернего элемента. На рис. 4.4 показан пример BST, упорядо ченного с помощью функции <.
5 |
4 |
8 |
2 |
6 |
9 |
1 |
3 |
7 |
Рис. 4.4. Двоичное дерево поиска
Программа на рис. 4.5 содержит утилиты для вставки и поиска объек тов в BST. В качестве основной структуры данных используются узлы. Каждый узел имеет три поля: в одном хранится сам объект, в двух дру гих – левый и правый потомки. Можно рассматривать узел как consячейку с одним car и двумя cdr.
BST может быть либо nil, либо узлом, поддеревья которого (l и r) также являются BST. Продолжим дальнейшую аналогию со списками. Как список может быть создан последовательностью вызовов cons, так и би нарное дерево может быть построено с помощью вызовов bst-insert. Этой функции необходимо сообщить объект, дерево и функцию упоря дочения.
>(setf nums nil) NIL
>(dolist (x ’(5 8 4 2 1 9 6 7 3))
(setf nums (bst-insert x nums #’<)))
NIL
Теперь дерево nums соответствует рис. 4.4.
Функция bst-find, которая ищет объекты в дереве, принимает те же ар гументы, что и bst-insert. Аналогия со списками станет еще понятнее, если мы сравним определения bst-find и our-member (стр. 33).
Как и member, bst-find возвращает не сам элемент, а его поддерево:
>(bst-find 12 nums #’<) NIL
>(bst-find 4 nums #’<) #<4>
Такое представление позволяет нам различать случаи, в которых иско мый элемент не найден (nil) и в которых успешно найден элемент nil.
4.7. Пример: двоичные деревья поиска |
87 |
||
|
|
|
|
|
(defstruct (node (:print-function |
|
|
|
(lambda (n s d) |
|
|
|
|
(format s "#<~A>" (node-elt n))))) |
|
|
elt (l nil) (r nil)) |
|
|
|
(defun bst-insert (obj bst <) |
|
|
|
(if (null bst) |
|
|
|
(make-node :elt obj) |
|
|
|
(let ((elt (node-elt bst))) |
|
|
|
(if (eql obj elt) |
|
|
|
bst |
|
|
|
(if (funcall < obj elt) |
|
|
|
(make-node |
|
|
|
:elt elt |
|
|
|
:l |
(bst-insert obj (node-l bst) <) |
|
|
:r |
(node-r bst)) |
|
|
(make-node |
|
|
|
:elt elt |
|
|
|
:r |
(bst-insert obj (node-r bst) <) |
|
|
:l |
(node-l bst))))))) |
|
|
(defun bst-find (obj bst <) |
|
|
|
(if (null bst) |
|
|
|
nil |
|
|
|
(let ((elt (node-elt bst))) |
|
|
|
(if (eql obj elt) |
|
|
|
bst |
|
|
|
(if (funcall < obj elt) |
|
|
|
(bst-find obj (node-l bst) <) |
|
|
|
(bst-find obj (node-r bst) <)))))) |
|
|
|
(defun bst-min (bst) |
|
|
|
(and bst |
|
|
|
(or (bst-min (node-l bst)) bst))) |
|
|
|
(defun bst-max (bst) |
|
|
|
(and bst |
|
|
|
(or (bst-max (node-r bst)) bst))) |
|
|
|
|
|
|
Рис. 4.5. Двоичные деревья поиска: поиск и вставка
Нахождение наибольшего и наименьшего элементов BST также не со ставляет особого труда. Чтобы найти минимальный элемент, мы идем по дереву, всегда выбирая левую ветвь (bst-min). Аналогично, следуя правым поддеревьям, мы получим наибольший элемент (bst-max):
>(bst-min nums) #<1>
>(bst-max nums) #<9>
88 |
Глава 4. Специализированные структуры данных |
Удаление элемента из бинарного дерева выполняется так же быстро, но соответствующий код (рис. 4.6) выглядит сложнее.
(defun bst-remove (obj bst <) (if (null bst)
nil
(let ((elt (node-elt bst))) (if (eql obj elt)
(percolate bst)
(if (funcall < obj elt) (make-node
:elt elt
:l (bst-remove obj (node-l bst) <) :r (node-r bst))
(make-node :elt elt
:r (bst-remove obj (node-r bst) <) :l (node-l bst)))))))
(defun percolate (bst)
(let ((l (node-l bst)) (r (node-r bst)))
(cond ((null |
l) |
r) |
|
((null |
r) |
l) |
|
(t (if (zerop (random 2)) |
|||
|
(make-node :elt (node-elt (bst-max l)) |
||
|
|
|
:r r |
|
|
|
:l (bst-remove-max l)) |
|
(make-node :elt (node-elt (bst-min r)) |
||
|
|
|
:r (bst-remove-min r) |
|
|
|
:l l)))))) |
(defun bst-remove-min |
(bst) |
||
(if (null (node-l |
bst)) |
||
(node-r bst) |
|
|
|
(make-node :elt |
(node-elt bst) |
||
|
:l |
|
(bst-remove-min (node-l bst)) |
|
:r |
|
(node-r bst)))) |
(defun bst-remove-max |
(bst) |
||
(if (null (node-r |
bst)) |
||
(node-l bst) |
|
|
|
(make-node :elt |
(node-elt bst) |
||
|
:l |
|
(node-l bst) |
|
:r |
|
(bst-remove-max (node-r bst))))) |
Рис. 4.6. Двоичные деревья поиска: удаление
4.7. Пример: двоичные деревья поиска |
89 |
Функция bst-remove1 принимает объект, дерево и функцию упорядоче ния и возвращает это же дерево без заданного элемента. Как и remove, bst-remove не модифицирует исходное дерево:
>(setf nums (bst-remove 2 nums #’<)) #<5>
>(bst-find 2 nums #’<)
NIL
Теперь дерево nums соответствует рис. 4.7. (Другой возможный случай – подстановка элемента 1 на место 2.)
5 |
4 |
8 |
3 |
6 |
9 |
1 7
Рис. 4.7. Двоичное дерево поиска после удаления одного из элементов
Удаление – более затратная процедура, так как под удаляемым объек том появляется незанятое место, которое должно быть заполнено од ним из поддеревьев этого объекта. Этим занимается функция percolate. Она замещает элемент дерева одним из его поддеревьев, затем замещает это поддерево одним из его поддеревьев и т. д.
Чтобы сбалансировать дерево, percolate случайным образом выбирает одно из двух поддеревьев. Выражение (random 2) вернет либо 0, либо 1, в результате (zerop (random 2)) будет истинно в половине случаев.
Теперь, когда мы преобразовали набор объектов в бинарное дерево, по следовательный обход его элементов даст нам их в порядке возрастания.
1Версия bst-remove, приведенная в оригинале книги, содержит баг.
Крис Стовер сообщает: «Задания (left child) < node < (right child) для каждо го узла недостаточно. Необходимо более строгое ограничение: max(left sub tree) < node < min(right subtree). Без него функция bst-traverse, приведенная на рис. 4.8, не обязательно перечислит элементы в порядке возрастания. (Пример: дерево с основанием 1 имеет только правого потомка 2, а он имеет только правого потомка 0.) К счастью, функция bst-insert, представленная на рис. 4.5, корректна. С другой стороны, корректный порядок BST после применения bst-remove не гарантируется. Удаляемый внутренний узел необ ходимо заменять либо максимальным узлом левого поддерева, либо мини мальным узлом правого поддерева, а приведенная функция этого не делает.» В программе на рис. 4.6 данный баг уже исправлен. – Прим. перев.