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

206

Глава 12. Структура

> whole (A B E)

Разу­ме­ет­ся,­ то же самое­ будет­ проис­хо­дить­ и для двух списков,­ имею­ щих общий­ хвост.

Изме­не­ние­ двух объек­тов­ одно­вре­мен­но­ не всегда­ явля­ет­ся­ ошибкой­. Иногда­ это именно­ то, что нужно­. Одна­ко­ если­ такое­ изме­не­ние­ проис­­ ходит­ непред­на­ме­рен­но,­ оно может­ привес­ти­ к некор­рект­ной­ рабо­те­ програм­мы­. Опытные­ програм­ми­сты­ умеют­ избе­гать­ подоб­ных­ ошибок­ и немед­лен­­но распо­зна­вать­ такие­ ситуа­ции­. Если­ список­ без види­мой­ причи­ны­ меня­ет­ свое содер­жи­мое,­ веро­ят­но,­ он имеет­ разде­ляе­мую­ структу­ру­.

Опасна­ не сама­ разде­ляе­мая­ структу­ра,­ а возмож­ность­ ее изме­не­ния­. Чтобы­ гаран­ти­ро­вать­ отсут­ст­вие­ подоб­ных­ ошибок,­ просто­ избе­гай­те­ исполь­зо­ва­ния­ setf (а также­ анало­гич­ных­ опера­то­ров­ типа­ pop, rplaca и других)­ для списков­. Если­ изме­няе­мость­ списков­ все же требу­ет­ся,­ то необ­хо­ди­мо­ выяс­нить,­ отку­да­ взялся­ изме­няе­мый­ список,­ чтобы­ убе­ диться,­ что он не делит­ структу­ру­ с чем-то, что нельзя­ менять­. Если­ же это не так или вам неизвест­но­ проис­хо­ж­де­ние­ списка,­ то моди­фи­ци­ро­­ вать необ­хо­ди­мо­ не сам список,­ а его копию­.

Нуж­но быть вдвой­не осто­рож­ным­ при исполь­зо­ва­нии­ функций,­ напи­­ санных­ кем-то другим­. Пока­ не уста­нов­ле­но­ обрат­ное,­ имейте­ в виду,­ что все, что пере­да­ет­ся­ функции:­

1.Может­ быть пере­да­но­ дест­рук­тив­ным­ опера­то­рам­.

2.Может­ быть сохра­не­но­ где-либо,­ и изме­не­ние­ этого­ объек­та­ приве­дет­ к изме­не­нию­ в других­ частях­ кода,­ исполь­зую­ще­го­ данный­ объект­.1

Вобоих­ случа­ях­ правиль­ным­ реше­ни­ем­ явля­ет­ся­ копи­ро­ва­ние­ аргу­­ ментов­.

ВCommon Lisp вызов­ любой­ функции,­ выпол­няю­щей­ся­ во время­ про­ хода­ по структу­ре­ (напри­мер,­ функции­-аргу­мен­та­ mapcar или remove-if), не должен­ менять­ эту структу­ру­. В против­ном­ случае­ послед­ст­вия­ вы­ полне­ния­ тако­го­ кода­ не опре­де­ле­ны­.

12.3.Пример: очереди

Разде­ляе­мые­ структу­ры ­– это не только­ повод­ для беспо­кой­ст­ва­. Ино­ гда они могут­ быть полез­ны­. В этом разде­ле­ пока­за­но,­ как с помо­щью­ разде­ляе­мых­ структур­ предста­вить­ очере­ди­. Очередь ­– это храни­ли­ще­ объек­тов,­ из кото­ро­го­ они могут­ быть извле­че­ны­ по одно­му­ в том же

1Напри­мер,­ в Common Lisp попыт­ка­ изме­не­ния­ строки,­ исполь­зуе­мой­ как имя симво­ла,­ счита­ет­ся­ ошибкой­. И посколь­ку­ нам неиз­вест­но,­ копи­ру­ет­ ли intern свой аргу­мент­-строку­ перед­ созда­ни­ем­ симво­ла,­ мы должны­ пред­ поло­жить,­ что моди­фи­ка­ция­ любой­ строки,­ кото­рая­ пере­да­ва­лась­ в intern, приве­дет­ к ошибке­.

12.3. Пример: очереди

207

поряд­ке,­ в кото­ром­ они были­ туда­ запи­са­ны­. Такую­ модель­ приня­то­ имено­вать­ FIFO – сокра­ще­ние­ от «первым­ пришел,­ первым­ ушел» («first in, first out»).

С помо­щью­ списков­ легко­ предста­вить­ стопку,­ так как добав­ле­ние­ и по­ луче­ние­ элемен­тов­ проис­хо­дит­ с одно­го­ конца­. Зада­ча­ представ­ле­ния­ очере­ди­ более­ сложна,­ посколь­ку­ добав­ле­ние­ и извле­че­ние­ объек­тов­ про­ исхо­дит­ с разных­ концов­. Для эффек­тив­ной­ ее реали­за­ции­ необ­хо­ди­мо­ каким­-то обра­зом­ обеспе­чить­ управле­ние­ обои­ми­ конца­ми­ списка­.

Одна­ из возмож­ных­ страте­гий­ приво­дит­ся­ на рис. 12.6, где пока­за­на­ очередь­ из трех элемен­тов:­ a, b и c. Очере­дью­ счита­ем­ точеч­ную­ пару,­ состоя­щую­ из списка­ и послед­ней­ ячейки­ этого­ же списка­. Будем­ назы­­ вать их нача­ло­ и конец­. Чтобы­ полу­чить­ элемент­ из очере­ди,­ необ­хо­ди­­ мо просто­ извлечь­ нача­ло­. Чтобы­ доба­вить­ новый­ элемент,­ необ­хо­ди­мо­ создать­ новую­ ячейку,­ сделать­ ее cdr конца­ очере­ди­ и затем­ сделать­ ее же концом­.

q1 =

nil

a

b

с

Рис. 12.6. Структу­ра­ очере­ди­

Такую­ страте­гию­ реали­зу­ет­ код на рис. 12.7.

(defun make-queue () (cons nil nil))

(defun enqueue (obj q) (if (null (car q))

(setf (cdr q) (setf (car q) (list obj))) (setf (cdr (cdr q)) (list obj)

(cdr q) (cdr (cdr q))))

(car q))

(defun dequeue (q) (pop (car q)))

Рис. 12.7. Реали­за­ция­ очере­дей­

Она исполь­зу­ет­ся­ следую­щим­ обра­зом:­

> (setf q1 (make-queue)) (NIL)

208

Глава 12. Структура

> (progn (enqueue ’a q1) (enqueue ’b q1) (enqueue ’c q1))

(A B C)

Теперь­ q1 представ­ля­ет­ очередь,­ изобра­жен­ную­ на рис. 12.6:

> q1

((A B C) C)

Попро­бу­ем­ забрать­ из очере­ди­ несколь­ко­ элемен­тов:­

>(dequeue q1)

A

>(dequeue q1)

B

>(enqueue ’d q1) (C D)

12.4.Деструктивные функции

Common Lisp включа­ет­ в себя­ несколь­ко­ функций,­ кото­рые­ могут­ изме­­ нять структу­ру­ списков­ и за счет этого­ рабо­тать­ быст­рее­. Они назы­­ва­ ются­ дест­рук­тив­ны­ми­. И хотя­ эти функции­ могут­ изме­нять­ ячейки,­ пере­дан­ные­ им в каче­ст­ве­ аргу­мен­тов,­ они дела­ют­ это не ради­ побоч­­ ных эффек­тов­.

Напри­мер,­ delete явля­ет­ся­ дест­рук­тив­ным­ анало­гом­ remove. Хотя­ ей и позво­ле­но­ портить­ пере­дан­ный­ список,­ она не дает­ ника­ких­ обеща­­ ний, что так и будет­ делать­. Посмот­рим,­ что проис­хо­дит­ в большин­ст­ве­ реали­за­ций:­

>(setf lst ’(a r a b i a)) (A R A B I A)

>(delete ’a lst)

(R B I) > lst

(A R B I)

Как и в случае­ с remove, чтобы­ зафик­си­ро­вать­ побоч­ный­ эффект,­ необ­хо­­ димо­ исполь­зо­вать­ setf:

(setf lst (delete ’a lst))

Приме­ром­ того,­ как дест­рук­тив­ные­ функции­ моди­фи­ци­ру­ют­ списки,­ яв­ ляет­ся­ nconc, дест­рук­тив­ная­ версия­ append.1 Приве­дем­ ее версию­ для двух аргу­мен­тов,­ демон­ст­ри­рую­щую,­ каким­ обра­зом­ сшива­ют­ся­ списки:­

(defun nconc2 (x y) (if (consp x)

1Бук­ва­ n в имени­ nconc соот­вет­ст­ву­ет­ «non-consing». Имена­ многих­ дест­рук­­ тивных­ функций­ начи­на­ют­ся­ с n.

12.5. Пример: двоичные деревья поиска

209

(progn

(setf (cdr (last x)) y) x)

y))

cdr послед­ней­ ячей­ки перво­го­ списка­ стано­вит­ся­ указа­те­лем­ на второй­ список­. Полно­цен­ная­ версия­ для произ­воль­но­го­ числа­ аргу­мен­тов­ при­ водит­ся­ в приложении B.

Функция mapcan похо­жа­ на mapcar, но соеди­ня­ет­ в один список­ возвра­­ щаемые­ значе­ния­ (кото­рые­ долж­ны быть списка­ми)­ с помо­щью­ nconc:

> (mapcan #’list ’(a b c)

’(1 2 3 4)) (A 1 B 2 C 3)

Эта функция­ может­ быть опре­де­ле­на­ следую­щим­ обра­зом:­

(defun our-mapcan (fn &rest lsts)

(apply #’nconc (apply #’mapcar fn lsts)))

Исполь­зуй­те­ mapcan с осто­рож­но­стью,­ учиты­вая­ ее дест­рук­тив­ный­ ха­ рактер­. Она соеди­ня­ет­ возвра­щае­мые­ списки­ с помо­щью­ nconc, поэто­му­ их лучше­ больше­ нигде­ не задей­ст­во­вать­.

Функция­ mapcan полез­на,­ в част­но­сти,­ в зада­чах,­ интер­пре­ти­руе­мых­ как сбор всех узлов­ одно­го­ уровня­ некое­го­ дере­ва­. Напри­мер,­ если­ children­ возвра­ща­ет­ список­ чьих-то детей,­ тогда­ мы сможем­ опре­де­лить­ функ­ цию для полу­че­ния­ списка­ внуков­ так:

(defun grandchildren (x) (mapcan #’(lambda (c)

(copy-list (children c))) (children x)))

Данная­ функция­ приме­ня­ет­ copy-list к резуль­та­ту­ вызо­ва­ children, так как он может­ возвра­щать­ уже суще­ст­вую­щий­ объект,­ а не произ­во­дить­ новый­.

Также­ можно­ опре­де­лить­ неде­ст­рук­тив­ный­ вари­ант­ mapcan:

(defun mappend (fn &rest lsts)

(apply #’append (apply #’mapcar fn lsts)))

Исполь­зуя­ mappend, мы можем­ обойтись­ без вызо­вов­ copy-list в опре­де­­ лении­ grandchildren:

(defun grandchildren (x)

(mappend #’children (children x)))

12.5.Пример: двоичные деревья поиска

Внеко­то­рых­ ситуа­ци­ях­ умест­нее­ исполь­зо­вать­ дест­рук­тив­ные­ опера­­ ции, неже­ли­ неде­ст­рук­тив­ные­. В разделе 4.7 было­ пока­за­но,­ как управ­

210

Глава 12. Структура

лять двоич­ны­ми­ деревь­я­ми­ поис­ка­ (BST). Все исполь­зо­ван­ные­ там функ­ ции были­ неде­ст­рук­тив­ны­ми,­ но если­ нужно­ приме­нить­ BST на практи­­ ке, такая­ осто­рож­ность­ излиш­ня­.

На рис. 12.8 при­веде­на­ дест­рук­тив­ная­ версия­ bst-insert (стр. 86). Она прини­ма­ет­ точно­ такие­ же аргу­мен­ты­ и возвра­ща­ет­ точно­ такое­ же зна­ чение,­ как и исход­ная­ версия­. Единст­вен­­ным отли­чи­ем­ явля­ет­ся­ то, что она может­ изме­нять­ дере­во,­ пере­да­вае­мое­ вторым­ аргу­мен­том­.

(defun bst-insert! (obj bst <) (if (null bst)

(make-node :elt obj) (progn (bsti obj bst <)

bst)))

(defun bsti (obj bst <)

(let ((elt (node-elt bst))) (if (eql obj elt)

bst

(if (funcall < obj elt) (let ((l (node–l bst)))

(if l

(bsti obj l <) (setf (node–l bst)

(make-node :elt obj)))) (let ((r (node-r bst)))

(if r

(bsti obj r <) (setf (node-r bst)

(make-node :elt obj))))))))

Рис. 12.8. Двоич­ные­ дере­вья­ поис­ка:­ дест­рук­тив­ная­ вставка­

В разделе 2.12 было­ преду­пре­ж­де­ние­ о том, что дест­рук­тив­ные­ функ­ ции вызы­­вают­ся­ не ради­ побоч­ных­ эффек­тов­. Поэто­му­ если­ вы хоти­те­ постро­ить­ дере­во­ с помо­щью­ bst-insert!, вам нуж­но вызы­­вать ее так же, как если­ бы вы вызы­­вали­ настоя­щую­ bst-insert:

>(setf *bst* nil) NIL

>(dolist (x ’(7 2 9 8 4 1 5 12))

(setf *bst* (bst-insert! x *bst* #’<)))

NIL

Вы могли­ бы также­ опре­де­лить­ аналог­ push для BST, но это доволь­но­ сложно­ и выхо­дит­ за рам­ки данной­ книги­. (Для любо­пыт­ных­ такой­ макрос­ опре­де­ля­ет­ся­ на стр. 429.°)

12.5. Пример: двоичные деревья поиска

211

На рис. 12.91 представ­лен­ дест­рук­тив­ный­ вари­ант­ функции­ bst-delete, кото­рая­ связа­на­ с bst-remove (стр. 89) так же, как delete связа­на­ с remove. Как и delete, она не подра­зу­ме­ва­ет­ вызо­в­ ради­ побоч­ных­ эффек­тов­. Ис­ пользо­вать­ bst-delete необ­хо­ди­мо­ так же, как и bst-remove:

>(setf *bst* (bst-delete 2 *bst* #’<)) #<7>

>(bst-find 2 *bst* #’<)

NIL

(defun bst-delete (obj bst <) (if (null bst)

nil

(if (eql obj (node-elt bst)) (del-root bst)

(progn

(if (funcall < obj (node-elt bst))

(setf (node-l bst) (bst-delete obj (node-l bst) <)) (setf (node-r bst) (bst-delete obj (node-r bst) <)))

bst))))

(defun del-root (bst)

(let ((l (node-l bst)) (r (node-r bst)))

(cond ((null l) r)

((null r) l)

(t

(if (zerop (random 2))

 

(cutnext r bst nil)

(cutprev l bst nil))))))

(defun cutnext (bst root prev) (if (node-l bst)

(cutnext (node-l bst) root bst)

(if prev

 

(progn

 

(setf (node-elt root)

(node-elt bst)

(node-l prev)

(node-r bst))

root)

 

(progn

 

(setf (node-l bst)

(node-l root))

bst))))

 

(defun cutprev (bst root prev) (if (node-r bst)

(cutprev (node-r bst) root bst) (if prev

(progn

(setf (node-elt root) (node-elt bst)

1Данный­ листинг­ содер­жит­ исправ­лен­ную­ версию­ bst-delete. За подроб­но­­ стями­ обра­щай­тесь­ к сноске­ на стр. 89. – Прим. перев­.

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