- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
212 |
|
|
|
Глава 12. Структура |
|
|
|
|
|
|
|
(node-r prev) |
(node-l bst)) |
|
|
|
root) |
|
|
|
|
(progn |
|
|
|
|
(setf (node-r bst) |
(node-r root)) |
|
|
|
bst)))) |
|
|
|
(defun replace-node (old new) |
|
||
|
(setf |
(node-elt |
old) (node-elt new) |
|
|
|
(node-l |
old) (node-l |
new) |
|
|
(node-r |
old) (node-r |
new))) |
|
(defun cutmin (bst par dir) |
|
||
|
(if (node-l bst) |
|
|
|
|
(cutmin (node-l bst) bst :l) |
|||
|
(progn |
|
|
|
|
|
(set-par par dir (node-r bst)) |
||
|
|
(node-elt bst)))) |
|
|
|
(defun cutmax (bst par dir) |
|
||
|
(if (node-r bst) |
|
|
|
|
(cutmax (node-r bst) bst :r) |
|||
|
(progn |
|
|
|
|
|
(set-par par dir (node-l bst)) |
||
|
|
(node-elt bst)))) |
|
|
|
(defun set-par (par dir val) |
|
||
|
(case |
dir |
|
|
|
(:l |
(setf (node-l par) val)) |
|
|
|
(:r |
(setf (node-r par) val)))) |
||
|
|
|
|
|
Рис. 12.9. Двоичные деревья поиска: деструктивное удаление
12.6. Пример: двусвязные списки
Обычные списки в Лиспе являются односвязными. Это означает, что движение по указателям происходит только в одном направлении: вы можете перейти к следующему элементу, но не можете вернуться к пре дыдущему. Двусвязные списки имеют также и обратный указатель, по этому можно перемещаться в обе стороны. В этом разделе показано, как создавать и использовать двусвязные списки.
На рис. 12.10 показана их возможная реализация. cons-ячейки имеют два поля: car, указывающий на данные, и cdr, указывающий на следую щий элемент. Элемент двусвязного списка должен иметь еще одно поле, указывающее на предыдущий элемент. Вызов defstruct (рис. 12.10) соз дает объект из трех частей, названный dl (от «doubly linked»), который мы будем использовать для создания двусвязных списков. Поле data в dl соответствует car в cons-ячейке, а поле next соответствует cdr. Поле
12.6. Пример: двусвязные списки |
213 |
prev похоже на cdr, но указывает в обратном направлении. (Пример та кой структуры приведен на рис. 12.11.) Пустому двусвязному списку, как и обычному, соответствует nil.
Вызов defstruct также определяет функции для двусвязных списков, аналогичные car, cdr и consp: dl-data, dl-next и dl-p. Функция печати dl->list возвращает обычный список с теми же значениями, что и дву связный.
Функция dl-insert похожа на cons. По крайней мере, она, как и cons, яв ляется основной функцией-конструктором. В отличие от cons, она изме няет двусвязный список, переданный вторым аргументом. В данной си туации это совершенно нормально. Чтобы поместить новый объект в на чало обычного списка, вам не требуется его изменять, однако чтобы по местить объект в начало двусвязного списка, необходимо присвоить полю prev указатель на новый объект.
(defstruct (dl (:print-function print-dl)) prev data next)
(defun print-dl (dl stream depth) (declare (ignore depth))
(format stream "#<DL ~A>" (dl->list dl)))
(defun dl->list (lst) (if (dl-p lst)
(cons (dl-data lst) (dl->list (dl-next lst))) lst))
(defun dl-insert (x lst)
(let ((elt (make-dl :data x :next lst))) (when (dl-p lst)
(if (dl-prev lst)
(setf (dl-next (dl-prev lst)) elt (dl-prev elt) (dl-prev lst)))
(setf (dl-prev lst) elt)) elt))
(defun dl-list (&rest args) (reduce #’dl-insert args
:from-end t :initial-value nil))
(defun dl-remove (lst) (if (dl-prev lst)
(setf (dl-next (dl-prev lst)) (dl-next lst))) (if (dl-next lst)
(setf (dl-prev (dl-next lst)) (dl-prev lst))) (dl-next lst))
Рис. 12.10. Построение двусвязных списков
214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Глава 12. Структура |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nil |
|
|
|
|
|
|
|
|
||||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
a |
b |
|
|
|
c |
||||||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Рис. 12.11. Двусвязный список
Другими словами, несколько обычных списков могут иметь общий хвост. Но для пары двусвязных списков это невозможно, так как хвост каждого из них имеет разные указатели на голову. Если бы функция dl-insert не была деструктивна, ей бы приходилось всегда копировать свой второй аргумент.
Другое интересное различие между одно- и двусвязными списками за ключается в способе доступа к их элементам. Работая с односвязным списком, вы храните указатель на его начало. При работе с двусвязным списком, поскольку в нем элементы соединены с обоих концов, вы мо жете использовать указатель на любой из элементов. Поэтому dl-insert, в отличие от cons, может добавлять новый элемент в любое место дву связного списка, а не только в начало.
Функция dl-list является dl-аналогом list. Она получает произвольное количество аргументов и возвращает состоящий из них dl:
> (dl-list ’a ’b ’c) #<DL (A B C)>
В ней используется reduce с параметрами: :from-end, установленным в t, и :initial-value, установленным в nil, что делает приведенный выше вызов эквивалентным следующей последовательности:
(dl-insert ’a (dl-insert ’b (dl-insert ’c nil)))
Заменив #’dl-insert на #’cons в определении dl-list, эта функция будет вести себя аналогично list:
>(setf dl (dl-list ’a ’b)) #<DL (A B)>
>(setf dl (dl-insert ’c dl)) #<DL (C A B)>
>(dl-insert ’r (dl-next dl)) #<DL (R A B)>
>dl
#<DL (C R A B)>
Наконец, для удаления элемента из двусвязного списка определена dl-remove. Как и dl-insert, она сделана деструктивной.
12.7. Циклическая структура |
215 |
12.7. Циклическая структура
Изменяя структуру списков, можно создавать циклические списки. Они бывают двух видов. Наиболее полезными являются те, которые имеют замкнутую структуру верхнего уровня. Такие списки называются цик лическими по хвосту (cdr-circular), так как цикл создается cdr-частями ячеек.
Чтобы создать такой список, содержащий один элемент, необходимо установить указатель cdr на самого себя:
>(setf x (list ’a))
(A)
>(progn (setf (cdr x) x) nil) NIL
Теперь x – циклический список. Его структура изображена на рис. 12.12.
x = |
y = |
nil |
a
Рис. 12.12. Циклические списки
При попытке напечатать такой список символ a будет выводиться до бесконечности. Этого можно избежать, установив значение *print-circ le* в t:
>(setf *print-circle* t)
T
>x
#1=(A . #1#)
При необходимости можно использовать макросы чтения #n= и #n# для представления такой структуры.
Списки с циклическим хвостом могут быть полезны для представле ния, например, буферов или ограниченных наборов каких-то объектов (пулов1). Следующая функция превратит произвольный нецикличе ский непустой список в циклический с теми же элементами:
(defun circular (lst)
(setf (cdr (last lst)) lst))
1Пул – это набор инициализированных ресурсов, которые поддерживаются в готовом к использованию состоянии, а не выделяются по требованию. –
Прим. ред.
216 |
Глава 12. Структура |
Другой тип циклических списков – циклические по голове (car-circular). Список такого типа можно понимать как дерево, являющееся поддере вом самого себя. Его название обусловлено тем, что в нем содержится цикл, замкнутый на car ячейки. Ниже мы создадим циклический по голове список, второй элемент которого является им самим:
> (let ((y (list ’a))) (setf (car y) y) y)
#1=(#1#)
Результат изображен на рис. 12.12. Несмотря на цикличность, этот цик лический по голове список (car-circular) по-прежнему является пра вильным списком, в отличие от циклических по хвосту (cdr-circular), которые правильными быть не могут.
Список может быть циклическим по голове и хвосту одновременно. car и cdr такой ячейки будут указывать на нее саму:
> (let ((c (cons 1 1))) (setf (car c) c
(cdr c) c)
c)
#1=(#1# . #1#)
Сложно представить, для чего могут использоваться подобные объек ты. На самом деле, главное, что нужно вынести из этого, – необходимо избегать непреднамеренного создания циклических списков, так как большинство функций, работающих со списками, будут уходить в бес конечный цикл, если получат в качестве аргумента список, цикличе ский по тому направлению, по которому они совершают проход.
Циклическая структура может быть проблемой не только для списков, но и для других типов объектов, например для массивов:
>(setf *print-array* t)
T
>(let ((a (make-array 1))) (setf (aref a 0) a)
a)
#1=#(#1#)
И действительно, практически любой объект, состоящий из элементов, может включать себя в качестве одного из них.
Разумеется, структуры, создаваемые defstruct, также могут быть цик лическими. Например, структура c, представляющая элемент дерева, может иметь поле parent, содержащее другую структуру p, чье поле child ссылается обратно на c:
> (progn (defstruct elt
(parent nil) (child nil)) (let ((c (make-elt))
(p (make-elt)))