Как вставить O (log (n)) в данные.Набор?

при просмотре документов Data.Set, Я увидел, что вставка элемента в дерево упоминается как O (log (n)). Однако я бы интуитивно ожидал, что это будет O(n*log(n)) (или, может быть, O(n)?), поскольку ссылочная прозрачность требует создания полной копии предыдущего дерева в O (n).

Я понимаю, что например (:) можно сделать O (1) вместо O (n), так как здесь не нужно копировать полный список; новый список может быть оптимизирован компилятором быть первым элементом плюс указатель на старый список (обратите внимание, что это компилятор - не языковой уровень - оптимизация). Однако вставка значения в Data.Set включает в себя перебалансировку, которая выглядит довольно сложной для меня, до такой степени, что я сомневаюсь, что есть что-то похожее на оптимизацию списка. Я пробовал читать документ, на который ссылается Set docs, но не смог ответить на мой вопрос.

Итак: как вставить элемент в двоичное дерево O (log (n)) на (чисто) функциональном языке?

2 ответов


нет необходимости делать полную копию Set для того чтобы вставить в него элемент. Внутренне элементы хранятся в дереве, что означает, что вам нужно только создать новые узлы вдоль пути вставки. Нетронутые узлы могут совместно использоваться между версией Set. И как Дитрих Эпп указал, в сбалансированном дереве O(log(n)) - длина пути введения. (Извините, что опустил это важное факт.)

скажи свой Tree тип выглядит так:

data Tree a = Node a (Tree a) (Tree a)
            | Leaf

... и скажите, что у вас есть Tree вот такой

let t = Node 10 tl (Node 15 Leaf tr')

... где tl и tr' некоторые названные поддеревья. Теперь скажите, что вы хотите вставить 12 в этом дереве. Ну, это будет выглядеть примерно так:

let t' = Node 10 tl (Node 15 (Node 12 Leaf Leaf) tr')

поддеревьев tl и tr' общие t и t', и вам нужно было только построить 3 новых Nodes сделать это, хотя размер t может быть намного больше, чем 3.


EDIT: перебалансировка

другое элемент. Ну, там нечетное число, так что ты ничего не сможешь сделать.

вот сложная часть. Скажи ... вставить другое элемент. Это может идти двумя путями: влево или вправо; сбалансированный или неуравновешенный. В случае, если он неуравновешен, вы можете четко выполнить вращение дерева, чтобы сбалансировать его. В том случае, если она сбалансирована, уже сбалансирована!

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

теперь скажите, что вы продолжаете вставлять элементы. Дерево буду неуравновешенные, но не намного. И когда это произойдет, во-первых, вы немедленно исправляете это, а во-вторых, коррекция происходит по пути вставки, который O(log(n)) в сбалансированном дереве. Вращения в документе, с которым вы связаны, касаются не более трех узлов дерева для выполнения вращения. так ты делаешь O(3 * log(n)) при перебалансировке. Это еще O(log(n)).


чтобы добавить дополнительный акцент на то, что dave4420 сказал в комментарии, в создании (:) запуск в постоянное время. Вы можете реализовать свой собственный тип данных списка и запустить его в простом не оптимизирующем интерпретаторе Haskell, и он все равно будет O(1).

список определена в начальный элемент плюс список (или он пуст в базовом случае). Вот определение, эквивалентное собственным спискам:

data List a = Nil | Cons a (List a)

так что если у вас есть элемент и список, и вы хотите создать новый список из них с Cons, это просто создание новой структуры данных непосредственно из аргументов конструктора требует. Больше нет необходимости даже изучить список хвостом (не говоря уже скопировать его), чем изучить или скопировать строку, когда вы делаете что-то вроде Person "Fred".

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

аналогично, для дерева, определенного как элемент плюс два дерева (или пустое дерево), при вставке элемента в непустое дерево он должен либо идти в левом или правом поддереве. Вам нужно будет создать новую версию этого дерева, содержащую элемент, что означает, что вам нужно будет создать новый родительский узел, содержащий новое поддерево. Но ... --7-->другое поддерево не нужно пересекать вообще; его можно поместить в новое родительское дерево как есть. В сбалансированном дереве это полный пол дерева, которое можно разделить.

применение этого рассуждения рекурсивно должно показать вам, что на самом деле нет никакого копирования элементов данных, необходимых вообще; есть только новые родительские узлы, необходимые на пути к конечной позиции вставленного элемента. Каждый новый узел хранит 3 вещи: элемент (совместно используемый непосредственно со ссылкой на элемент в исходном дереве), неизмененное поддерево (совместно с исходным деревом) и вновь созданным поддеревом (которое разделяет почти всю свою структуру с исходным деревом). В сбалансированном дереве будет O(log(n)).