Как посчитать количество непустых узлов в двоичном дереве в F # - PullRequest
0 голосов
/ 06 сентября 2018

Рассмотрим двоичное дерево, алгебраический тип данных

type btree = Empty | Node of btree * int * btree

и новый тип данных, определенный следующим образом:

 type finding = NotFound | Found of int

Вот мой код:

let s = Node (Node(Empty, 5, Node(Empty, 2, Empty)), 3, Node (Empty, 6, Empty))
(*
     (3)
    /    \
   (5)   (6)
   / \   |  \
  () (2) () ()
 / \
() ()
*)

(* size: btree -> int *)
let rec size t =
    match t with
    Empty -> false
  | Node (t1, m, t2) -> if (m != Empty) then sum+1 || (size t1) || (size t2)

let num = occurs s
printfn "There are %i nodes in the tree" num

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

Я очень новичок в использовании F # и буду признателен за любую помощь. Я пытаюсь сосчитать все непустые узлы в дереве. Например, дерево, которое я использую, должно напечатать значение 4.

Ответы [ 2 ]

0 голосов
/ 06 сентября 2018

Фридрих уже опубликовал простую версию функции size, которая будет работать для большинства деревьев. Однако решение не является «хвостово-рекурсивным», поэтому оно может вызвать переполнение стека для больших деревьев. В функциональных языках программирования, таких как F #, рекурсия часто является предпочтительным методом для таких вещей, как подсчет и другие агрегатные функции. Однако рекурсивные функции обычно используют кадр стека для каждого рекурсивного вызова. Это означает, что для больших структур стек вызовов может быть исчерпан до завершения функции. Чтобы избежать этой проблемы, компиляторы могут оптимизировать функции, которые считаются «хвостово-рекурсивными», так что они используют только один кадр стека независимо от того, сколько раз они рекурсируют. К сожалению, эта оптимизация не может быть просто реализована для любого рекурсивного алгоритма. Это требует, чтобы рекурсивный вызов был последним, что делает функция, тем самым гарантируя, что компилятору не придется беспокоиться о возврате в функцию после вызова, что позволяет ему перезаписывать кадр стека вместо добавления другого.

Чтобы изменить функцию size на хвостовую рекурсию, нам нужен какой-то способ, чтобы не вызывать ее дважды в случае непустого узла, чтобы этот вызов мог быть последним шагом функция, вместо сложения между двумя вызовами в решении Фридриха. Это может быть достигнуто с использованием нескольких различных методов, обычно с использованием аккумулятора или с использованием стиля продолжения продолжения. Более простое решение часто заключается в использовании аккумулятора для отслеживания общего размера, а не в качестве возвращаемого значения, в то время как стиль передачи продолжения является более общим решением, которое может обрабатывать более сложные рекурсивные алгоритмы.

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

let size t =
    let rec size acc ts = function
    | Empty -> 
        match ts with
        | [] -> acc
        | head :: tail -> head |> size acc tail
    | Node (t1, _, t2) -> 
        t1 |> size (acc + 1) (t2 :: ts)

    t |> size 0 []

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

0 голосов
/ 06 сентября 2018

Я не запускал компилятор в вашем коде, но я верю, что он даже компилируется. Однако ваша идея использовать сопоставление с образцом в рекурсивной функции хороша. Как прокомментировал rmunn, вы хотите определить количество узлов в каждом случае: Пустое дерево не имеет узлов, поэтому результат равен нулю. Непустое дерево, имеющее как минимум корневой узел плюс количество его левого и правого поддеревьев.

Так что-то вроде следующего должно работать

let rec size t =
    match t with
    | Empty -> 0
    | Node (t1, _, t2) -> 1 + (size t1) + (size t2)

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

Как замечание, ваше дерево в комментарии должно выглядеть так, я полагаю.

(*
     (3)
    /    \
   (5)   (6)
   / \   |  \
  () (2) () ()
     / \
    () ()
*)

Редактировать: Я неверно истолковал смещенное () как листья пустого дерева, где на самом деле они являются листьями поддерева (2). Так что это был просто вопрос искусства ASCII: -)

...