Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Fp_6.doc
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
477.18 Кб
Скачать

6.2.2.2 Упорядоченные множества

Другой способ ускорить операции над множествами - изменить представление так, чтобы элементы множества были упорядочены.

Для этого потребуется отношение некоторого порядка между объектами. Рассмотрим только случай, когда элементы множества суть числа, упорядоченные по возрастающей, поэтому можно сравнивать элементы, используя привычные операции отношения: =, > и <.

Первое преимущество упорядочивания обнаруживается в предикате in?.

Теперь, проверяя присутствие элемента, не надо просматривать список полностью. Если мы достигли элемента, большего чем данный, то разумеется, что искомого элемента в множестве нет:

> (define ordset '(3 5 7 10 13 27 35))

> (define (in? x s)

(and (not (null? s))

(or (= x (car s))

(and (> x (car s))

(in? x (cdr s))))))

> (in? 13 ordset)

#t

> (in? 11 ordset)

#f

Такое  представление экономит в среднем половину операций по сравнению с предыдущим.

Процедура добавления несколько усложняется, но опять же, дает в среднем ускорение в два раза.

> (define (join s x)

(cond ((null? s) (list x))

((= x (car s)) s)

((< x (car s)) (cons x s))

((> x (car s)) (cons (car s) (join (cdr s) x)))))

> (join ordset 14)

(3 5 7 10 13 14 27 35)

> (join ordset 27)

(3 5 7 10 13 27 35)

Более внушительное ускорение мы получаем для объединения и пересечения множеств.

Прежде, выполняя эти операции, мы должны были полностью просматривать список s1 для каждого элемента s2. Но с новым представлением, мы можем использовать более быстрые методы.

Пусть x1 и x2 - начальные элементы  двух списков s1 и s2 .

Если x1=x2, то объединение получается присоединением x1 к объединению хвостов списков.

Пусть теперь x1<x2. Так как x2 - самый маленький элемент в s2, x1 не содержится в s2. Следовательно, мы можем построить объединение s1 и s2, присоединяя x1 к объединению (cdr s1) и s2.

Рассуждения для случая x1 > x2 аналогичны.

Если списки полностью совпадают, то в ответе получается первый список.

> (define (union s1 s2)

(cond

((null? s1) s2)

((null? s2) s1)

(else

(let ((x1 (car s1)) (x2 (car s2)))

(cond ((= x1 x2) (cons x1 (union (cdr s1) (cdr s2))))

((< x1 x2) (cons x1 (union (cdr s1) s2)))

((> x1 x2) (cons x2 (union s1 (cdr s2) ))))))))

> (union '(36 40 48) ordset)

(3 5 7 10 13 27 35 36 40 48)

> (union ordset '(12 23 45 97) )

(3 5 7 10 12 13 23 27 35 45 97)

> (union '(-63 -45 0) ordset)

(-63 -45 0 3 5 7 10 13 27 35)

Для этой процедуры требуется шагов не более чем сумма размеров s1 и s2. Но ещё раз заметим, что создание исходного множества должно обязательно сопровождаться его упорядочением.

6.2.3 Представление множеств двоичными деревьями

Мы можем добиться ещё большей эффективности, упорядочивая элементы множества в форме двоичного  дерева.

Каждый узел такого дерева содержит один элемент множества (значение или по-другому - «метку» узла), и два поддерева (возможно пустые). «Левое» поддерево содержит элементы меньшие, а «правое» - большие чем значение узла.

Для представления деревьев, естественно использовать списки. Каждый узел удобно считать списком из трех элементов: значение узла, левое поддерево, и правое поддерево. Пустое дерево – это пустой список.

Итак, представление описывается следующим процедурами.

(define (node val left right) (list val left right))

(define value car)

(define left cadr)

(define right caddr)

Для наглядности (в распечатке дерева) положим nil равным полюбившемуся всеми нами символу $, т.е.

(define nil '$)

(define $ '$)

Далее – пустое дерево.

(define empty-tree nil)

Проверки на пустоту будут выглядеть по-другому

(define (end_of_tree? tr) (if (eq? tr nil) #t #f))

(define empty-tree? end_of_tree?)

Мы представляем множества в терминах деревьев, а деревья в терминах списков, т.е. формируем абстракцию данных, используя другую абстракцию данных.

Поэтому пустое множество представляется пустым деревом, а одноэлементное множество - деревом, содержащим добавляемый элемент и две пустые ветви.

(define (empty_s ) (empty-tree))

(define (singleton x) (node x empty-tree empty-tree))

Для проверки, содержится ли элемент в множестве, надо сравнивнить его со значением вершины. Если элемент  меньше этого значения, тогда он может находиться только в левом поддереве, если больше - в правом.

Если дерево «сбалансировано», размер каждого из поддеревьев будет составлять примерно половину размера дерева.

С точки зрения эффективности, следует ожидать, что число шагов, необходимых для поиска в дереве размера n растет как O(log n). Для больших множеств, это будет существенное ускорение по сравнению с предыдущим представлениям в виде списка.

Используя описанную стратегию, можем записать процедуру:

> (define (in? x trs)

(and (not (empty-tree? trs))

(or (= x (value trs))

(and (< x (value trs)) (in? x (left trs)))

(and (> x (value trs)) (in? x (right trs))))))

Для примера, зададим дерево и попробуем работу нашего подхода

> (define tree '(9 (4 (3 $ $)

(7 (5 $ $)

(8 $ $)))

(13 (11 $ (12 $ $))

(19 (17 $ $)

(20 $ $)))))

> (in? 11 tree)

#t

> (in? 14 tree)

#f

Добавление элемента к множеству

также требует O(log n) шагов.

> (define (join trs x)

(cond ((empty-tree? trs) (singleton x))

((= x (value trs)) trs)

((< x (value trs))

(node (value trs) (join (left trs) x) (right trs)))

((> x (value trs))

(node (value trs) (left trs) (join (right trs) x)))))

Заметим, что мы всегда доходим до правого или левого конца дерева, образуем singleton, и, возвращаясь, восстанавливаем связи, включая связку на этот элемент.

> (join tree 14)

(9

(4 (3 $ $) (7 (5 $ $) (8 $ $)))

(13 (11 $ (12 $ $)) (19 (17 (14 $ $) $) (20 $ $))))

Рассматривая различные способы представления множеств, мы видели, как выбор представления объектов данных может оказать значительное воздействие на эффективность программ.

Н

14 Х Х

аше первое представление чрезвычайно неэффективное, но очень простое. Это позволяет использовать его (или другое простое представление, такое как неупорядоченные  списки) для первоначальной реализации. Скорее всего, оно окажется

неподходящим для реальной системы, но оно позволит проверить

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

Если рассмотреть на месте множеств какую-нибудь сложную базу данных, важность абстракции становится очевидной.

Многие современные языки содержат явные средства поддержки абстракции данных, но, как мы могли видеть, использование этой методологии возможно и в Лиспе.

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