Алгоритмы дерева на основе соединений
В информатике алгоритмы деревьев на основе соединений представляют собой класс алгоритмов самобалансирующихся двоичных деревьев поиска . Целью этой структуры является разработка алгоритмов с высокой степенью параллелизма для различных сбалансированных деревьев двоичного поиска. Алгоритмическая основа основана на одной операции join . [1] В рамках этой структуры операция соединения фиксирует все критерии балансировки различных схем балансировки, а все остальные функции объединения имеют общую реализацию в разных схемах балансировки. Алгоритмы на основе соединений могут применяться как минимум к четырем схемам балансировки: деревьям AVL , красно-черным деревьям , деревьям со сбалансированным весом и трепам .
Соединение операция принимает на вход два двоичных сбалансированных дерева и той же схемы балансировки и ключевого и выводит новое сбалансированное двоичное дерево которого упорядоченный обход является упорядоченным обходом , затем то обход по порядку . В частности, если деревья являются деревьями поиска , что означает, что порядок деревьев поддерживает полный порядок ключей, он должен удовлетворять условию, что все ключи в меньше, чем и все ключи вставлены больше, чем .
История
[ редактировать ]Операция соединения была впервые определена Тарьяном. [2] на красно-черных деревьях , который выполняется за логарифмическое время в худшем случае. Позже Слейтор и Тарьян [3] описал соединения алгоритм расширенных деревьев , который работает за амортизированное логарифмическое время. Позже Адамс [4] расширил соединение с деревьями со сбалансированным весом и использовал его для быстрых функций множества-множества, включая объединение , пересечение и разницу множеств . В 1998 году Блеллох и Рид-Миллер расширили соединение на Treaps и доказали, что граница функций множества равна для двух деревьев размером и , что является оптимальным в модели сравнения. Они также реализовали параллелизм в алгоритме Адамса, используя схему «разделяй и властвуй» . В 2016 году Блеллох и др. формально предложил алгоритмы на основе соединений и формализовал алгоритм соединения для четырех различных схем балансировки: деревьев AVL , красно-черных деревьев , деревьев со сбалансированным весом и трепов . В той же работе они доказали, что алгоритмы Адамса объединения, пересечения и разности оптимальны по работе для всех четырех схем балансировки.
Присоединяйтесь к алгоритмам
[ редактировать ]Функция присоединиться учитывает перебалансировку дерева и, таким образом, зависит от схемы балансировки входов. Если два дерева сбалансированы, соединение просто создает новый узел с левым поддеревом t 1 , корнем k и правым поддеревом t 2 . Предположим, что t 1 тяжелее (это «тяжелее» зависит от схемы балансировки), чем t 2 (другой случай симметричен). Соединение следует за правым позвоночником t 1 до узла c , который сбалансирован с t 2 . новый узел с левым дочерним элементом c , корнем k и правым дочерним элементом t2 На этом этапе для замены c создается . Новый узел может сделать недействительным балансирующий инвариант. Это можно исправить поворотами.
Ниже приведены алгоритмы объединения в различных схемах балансировки.
Алгоритм соединения для деревьев AVL :
function joinRightAVL(TL, k, TR) (l, k', c) := expose(TL) if h(c) ≤ h(TR) + 1 T' := Node(c, k, TR) if h(T') ≤ h(l) + 1 return Node(l, k', T') else return rotateLeft(Node(l, k', rotateRight(T'))) else T' := joinRightAVL(c, k, TR) T := Node(l, k', T') if h(T') ≤ h(l) + 1 return T else return rotateLeft(T) function joinLeftAVL(TL, k, TR) /* symmetric to joinRightAVL */ function join(TL, k, TR) if h(TL) > h(TR) + 1 return joinRightAVL(TL, k, TR) else if h(TR) > h(TL) + 1 return joinLeftAVL(TL, k, TR) else return Node(TL, k, TR)
Где:
- высота узла .
- извлекает левого дочернего элемента , ключ , и правый ребенок узла в кортеж .
- создает узел с левым дочерним элементом , ключ , и правый ребенок .
Алгоритм соединения красно-черных деревьев :
function joinRightRB(TL, k, TR) if TL.color = black and ĥ(TL) = ĥ(TR) return Node(TL, ⟨k, red⟩, TR) else (L', ⟨k', c'⟩, R') := expose(TL) T' := Node(L', ⟨k', c'⟩, joinRightRB(R', k, TR)) if c' = black and T'.right.color = T'.right.right.color = red T'.right.right.color := black return rotateLeft(T') else return T' function joinLeftRB(TL, k, TR) /* symmetric to joinRightRB */ function join(TL, k, TR) if ĥ(TL) > ĥ(TR) T' := joinRightRB(TL, k, TR) if (T'.color = red) and (T'.right.color = red) T'.color := black return T' else if ĥ(TR) > ĥ(TL) /* symmetric */ else if TL.color = black and TR = black return Node(TL, ⟨k, red⟩, TR) else return Node(TL, ⟨k, black⟩, TR)
Где:
- — черная высота узла .
- извлекает левого дочернего элемента , ключ , цвет , и правый ребенок узла в кортеж .
- создает узел с левым дочерним элементом , ключ , цвет , и правый ребенок .
Алгоритм соединения деревьев со сбалансированным весом :
function joinRightWB(TL, k, TR) (l, k', c) := expose(TL) if w(TL) =α w(TR) return Node(TL, k, TR) else T' := joinRightWB(c, k, TR) (l1, k1, r1) := expose(T') if w(l) =α w(T') return Node(l, k', T') else if w(l) =α w(l1) and w(l)+w(l1) =α w(r1) return rotateLeft(Node(l, k', T')) else return rotateLeft(Node(l, k', rotateRight(T')) function joinLeftWB(TL, k, TR) /* symmetric to joinRightWB */ function join(TL, k, TR) if w(TL) >α w(TR) return joinRightWB(TL, k, TR) else if w(TR) >α w(TL) return joinLeftWB(TL, k, TR) else return Node(TL, k, TR)
Где:
- это вес узла .
- означает веса и являются α-весовыми сбалансированными.
- означает вес тяжелее веса относительно α-весового баланса.
- извлекает левого дочернего элемента , ключ , и правый ребенок узла в кортеж .
- создает узел с левым дочерним элементом , ключ и правильный ребенок .
Алгоритмы на основе соединений
[ редактировать ]В дальнейшем извлекает левого дочернего элемента , ключ , и правый ребенок узла в кортеж . создает узел с левым дочерним элементом , ключ и правильный ребенок . " " означает, что два утверждения и может работать параллельно.
Расколоть
[ редактировать ]Чтобы разделить дерево на два дерева: меньшее, чем ключ x , и большее, чем ключ x , мы сначала рисуем путь от корня, вставляя x в дерево. После этой вставки все значения меньше x будут найдены слева от пути, а все значения больше x будут найдены справа. При применении Join все поддеревья с левой стороны объединяются снизу вверх с использованием ключей на пути в качестве промежуточных узлов снизу вверх, чтобы сформировать левое дерево, а правая часть становится асимметричной. Для некоторых приложений Split также возвращает логическое значение, указывающее, присутствует ли x в дереве. Стоимость Сплита , порядок высоты дерева.
Алгоритм разделения следующий:
function split(T, k) if (T = nil) return (nil, false, nil) else (L, m, R) := expose(T) if k < m (L', b, R') := split(L, k) return (L', b, join(R', m, R)) else if k > m (L', b, R') := split(R, k) return (join(L, m, L'), b, R')) else return (L, true, R)
Присоединяйтесь2
[ редактировать ]Эта функция определяется аналогично join , но без средней клавиши. Сначала он разделяет последний ключ левого дерева, а затем соедините остальную часть левого дерева с правым деревом с помощью . Алгоритм следующий:
function splitLast(T) (L, k, R) := expose(T) if R = nil return (L, k) else (T', k') := splitLast(R) return (join(L, k, T'), k') function join2(L, R) if L = nil return R else (L', k) := splitLast(L) return join(L', k, R)
Стоимость для дерева размером .
Вставить и удалить
[ редактировать ]Алгоритмы вставки и удаления при использовании соединения могут быть независимыми от схем балансировки. При вставке алгоритм сравнивает вставляемый ключ с ключом в корне, вставляет его в левое/правое поддерево, если ключ меньше/больше, чем ключ в корне, и соединяет два поддерева обратно с корнем. . При удалении сравнивается удаляемый ключ с ключом в корне. Если они равны, верните join2 для двух поддеревьев. В противном случае удалите ключ из соответствующего поддерева и соедините два поддерева обратно с корнем. Алгоритмы следующие:
function insert(T, k) if T = nil return Node(nil, k, nil) else (L, k', R) := expose(T) if k < k' return join(insert(L,k), k', R) else if k > k' return join(L, k', insert(R, k)) else return T function delete(T, k) if T = nil return nil else (L, k', R) := expose(T) if k < k' return join(delete(L, k), k', R) else if k > k' return join(L, k', delete(R, k)) else return join2(L, R)
И вставка, и удаление требуют время, если .
Установить-установить функции
[ редактировать ]Для деревьев со сбалансированным весом было определено несколько операций над множествами: объединение , пересечение и разность множеств . Объединение двух сбалансированных по весу деревьев t 1 и t 2, представляющих множества A и B , представляет собой дерево t , которое представляет A ∪ B . Следующая рекурсивная функция вычисляет это объединение:
function union(t1, t2) if t1 = nil return t2 else if t2 = nil return t1 else (l1, k1, r1) := expose(t1) (t<, b, t>) := split(t2, k1) l' := union(l1, t<) || r' := union(r1, t>) return join(l', k1, r')
Аналогично алгоритмы пересечения и множества-разности следующие:
function intersection(t1, t2) if t1 = nil or t2 = nil return nil else (l1, k1, r1) := expose(t1) (t<, b, t>) = split(t2, k1) l' := intersection(l1, t<) || r' := intersection(r1, t>) if b return join(l', k1, r') else return join2(l', r') function difference(t1, t2) if t1 = nil return nil else if t2 = nil return t1 else (l1, k1, r1) := expose(t1) (t<, b, t>) := split(t2, k1) l' = difference(l1, t<) || r' = difference(r1, t>) if b return join2(l', r') else return join(l', k1, r')
Сложность каждого объединения, пересечения и различия равна для двух сбалансированных по весу деревьев размером и . Эта сложность оптимальна по количеству сравнений. Что еще более важно, поскольку рекурсивные вызовы объединения, пересечения или различия независимы друг от друга, они могут выполняться параллельно с параллельной глубиной. . [1] Когда , реализация на основе соединения применяет те же вычисления, что и при вставке или удалении одного элемента, если корень большего дерева используется для разделения меньшего дерева.
Строить
[ редактировать ]Алгоритм построения дерева может использовать алгоритм объединения и схему «разделяй и властвуй»:
function build(A[], n) if n = 0 return nil else if n = 1 return Node(nil, A[0], nil) else l' := build(A, n/2) || r' := (A+n/2, n-n/2) return union(L, R)
Этот алгоритм стоит работать и имеет глубина. Более эффективный алгоритм использует алгоритм параллельной сортировки.
function buildSorted(A[], n) if n = 0 return nil else if n = 1 return Node(nil, A[0], nil) else l' := build(A, n/2) || r' := (A+n/2+1, n-n/2-1) return join(l', A[n/2], r') function build(A[], n) A' := sort(A, n) return buildSorted(A, n)
Этот алгоритм стоит работать и имеет глубина при условии, что алгоритм сортировки имеет работа и глубина.
Фильтр
[ редактировать ]Эта функция выбирает все записи в дереве, удовлетворяющие предикату и верните дерево, содержащее все выбранные записи. Он рекурсивно фильтрует два поддерева и соединяет их с корнем, если корень удовлетворяет условиям , в противном случае соедините2 два поддерева.
function filter(T, p) if T = nil return nil else (l, k, r) := expose(T) l' := filter(l, p) || r' := filter(r, p) if p(k) return join(l', k, r') else return join2(l', R)
Этот алгоритм стоит работы и глубина на дереве размером , предполагая имеет постоянную стоимость.
Используется в библиотеках
[ редактировать ]Алгоритмы на основе объединения применяются для поддержки интерфейса для наборов , карт и дополненных карт. [5] в таких библиотеках, как Hackage , SML/NJ и PAM . [5]
Примечания
[ редактировать ]Ссылки
[ редактировать ]- ^ Перейти обратно: а б Блеллок, Гай Э.; Феризович, Дэниел; Сунь, Ихан (2016), «Просто присоединяйтесь к параллельным упорядоченным наборам», Симпозиум по параллельным алгоритмам и архитектурам, Proc. 28-го симпозиума ACM. Параллельные алгоритмы и архитектуры (SPAA 2016) , ACM, стр. 253–264, arXiv : 1602.02120 , doi : 10.1145/2935764.2935768 , ISBN 978-1-4503-4210-0
- ^ Тарьян, Роберт Эндре (1983), «Структуры данных и сетевые алгоритмы», Структуры данных и сетевые алгоритмы , Сиам, стр. 45–56.
- ^ Слитор, Дэниел Доминик; Тарьян, Роберт Эндре (1985), «Самонастраивающиеся двоичные деревья поиска», Журнал ACM , Сиам
- ^ Адамс, Стивен (1992), «Эффективная реализация множеств на функциональном языке», Эффективная реализация множеств на функциональном языке , Citeseer, CiteSeerX 10.1.1.501.8427 .
- ^ Перейти обратно: а б Блеллок, Гай Э.; Феризович, Дэниел; Сунь, Ихан (2018), «PAM: параллельные дополненные карты», Материалы 23-го симпозиума ACM SIGPLAN по принципам и практике параллельного программирования , ACM, стр. 290–304