Построение бинарного дерева из списка (предзаказ)

Интересный вопрос, с которым я столкнулся несколько дней назад: существует ли элегантное решение функционального программирования для построения (помеченного узлами) двоичного дерева из списка?

Результирующее дерево должно быть сбалансировано по левому краю, т.е. каждый уровень узлов в дереве должен быть либо полностью заполнен, либо, в случае самого нижнего, заполняться слева направо. Кроме того, обход дерева в порядке уровней (т.е. сверху вниз, слева направо) должен дать исходный список.

Пример: список 1,2,3,4,5 должен привести к

          1
    2         3
 4    5

Очевидное решение - преобразовать список в массив, присвоить корню нулевое значение, а затем рекурсивно для узла n передать дочерним элементам значения 2n+1 и 2n+2.

Но мне было интересно: есть ли более «функциональный» способ, не требующий вспомогательного массива?


person Manuel Eberl    schedule 23.12.2015    source источник
comment
Когда в примере дерева просматривается предварительный заказ, я получаю (1 2 4 5 3), дерево предварительного заказа будет выглядеть как (1 (2 (3 () ()) (4 () ()) (5 ​​() ()) ), возможно, вы имели в виду порядок уровней? Или я что-то не так понимаю?   -  person WorBlux    schedule 24.12.2015


Ответы (1)


Мысли, выраженные в схеме rsr5

Мои первоначальные мысли связаны с бесконечным деревом, узлами которого являются функции, которые принимают значение и дерево и вставляют это значение в это дерево в том же месте, где находится функция. Затем вы можете выполнить рекурсию fold2 по списку и пересечение дерева функций на уровне порядка, применяя последовательную функцию к последовательным элементам списка для накопления двоичного дерева. (Хотя работает только с ленивым вычислением)

Это показалось немного громоздким, поэтому я пересмотрел идею дерева данных, которое можно было бы передать функции вроде ('l' r 'l) или чему-то, что могло бы сказать, куда вставлять и индексировать.

(define (insert-new ls-and-rs tree)
  (cond ((or (empty-tree?) (null? ls-and-rs))
         (if (and (empty-tree?) (null? ls-and-rs))
             (make-tree x emptyTree emptyTree)
             (error "insert-new can only insert at emptyree")))
        ((sybol=? (car ls-and-rs) 'l)
         (make-tree (node tree) 
                    (insert-new (cdr ls-and-rs) 
                                (left-tree tree)) 
                    (right-tree tree)))
        ((sybol=? (car ls-and-rs) 'r)
         (make-tree (node tree) 
                    (left-tree tree) 
                    (insert-new (cdr ls-and-rs) 
                                (right-tree tree))))
        (else (error "insert-new expected 'l or 'r, recieved " 
                     (car ls-and-rs)))))) 

Затем я увидел, что это можно построить из самого индекса. Индекс, если 1 - это голова дерева. В противном случае, если это нечетно, это правая ветвь узла. Если хоть ветка левая. Индекс его родителя всегда равен полу дочернего элемента, разделенному на два. На основе этих знаний вы можете создать средство вставки или средства доступа просто с помощью индекса.

(define (branch-order i)
  (let loop ((i i) (accum '()))
    (cond ((i = 1) accum)
          ((odd? i) (loop (quotient i 2) (cons 'r accum)))
          (else     (loop (quotient i 2) (cons 'l accum))))))

Отсюда простая рекурсия

 (define (list->tree list)
   (let loop ((list list) (i 1) (tree emptyTree))
     (cond ((null? list) tree)
           (else (loop (cdr list)
                       (+ i 1)
                       (insert-new (branch-order i) tree))))))

Конечно, самый простой способ - принять двоичное дерево минимальной глубины. В дереве глубины помимо узлов и ветвей указывается минимальная глубина. Затем вставка будет вставлена ​​в левое поддерево, если минимальная глубина правого поддерева не будет меньше, чем у левого.

(define (list->d-tree list)
  (fold-left (flip balanced-insert) emptyTree list))

(define (balanced-insert x d-tree)
  (cond ((= 0 (d-d-tree d-tree)) 
        (mk-d-tree x emptyTree emptyTree)
       ((= 1 (- (d-d-tree d-tree) (d-d-tree (l-d-tree d-tree))))
        (mk-d-tree (n-d-tree d-tree)
                   (balanced-insert x (l-d-tree d-tree))
                   (r-d-tree d-dtree)))
      (else 
       (mk-d-tree (n-d-tree d-tree)
                  (l-d-tree d-tree)
                  (balanced-insert x (r-d-tree d-dtree))))))

(define (mk-d-tree n l r)
 (let ((d-l (d-d-tree l))
       (d-r (d-d-tree r)))
 (list n l r (+ 1 (min d-l d-r)))))
(define (n-d-tree d-tree)
 (car d-tree))
(define (l-d-tree d-tree)
 (cadr d-tree))
(define (r-d-tree d-tree)
 (caddr d-tree))
(define (d-d-tree d-tree)
 (if emptyTree? d-tree)
     0
     (cadddr d-tree)))
(define (emptyTree? tree)
       (null? tree))
(define emptyTree '())

(define (flip f)
 (lambda (a b)
   (f b a)))

TLdr; используйте дерево минимальной глубины и вставьте до крайнего левого края минимальной глубины.

person WorBlux    schedule 24.12.2015
comment
Я не совсем понимаю все это (я действительно плохо читаю Lisp-подобные программы), но я просто приму ответ и подумаю над ним еще немного позже. Спасибо! - person Manuel Eberl; 28.12.2015
comment
Возвращаясь к этому сейчас, я тоже не стал самым ясным. Первый пример не выглядит полным и правильным. Основная идея заключалась в том, чтобы отслеживать индекс при перемещении вниз по списку и использовать индекс для создания списка поворотов, которые необходимо выполнить в обход дерева перед вставкой. Второй использует дополнительный элемент данных в дереве для принятия решений о вставке. - person WorBlux; 07.03.2020