Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Pol_Grem_-_ANSI_Common_Lisp_High_tech_-_2012.pdf
Скачиваний:
27
Добавлен:
12.03.2016
Размер:
4.85 Mб
Скачать

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 данный­ баг уже исправ­лен­. – Прим. перев­.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]