Построение бинарного дерева (не BST) в Haskell в ширину - PullRequest
7 голосов
/ 04 марта 2020

Я недавно начал использовать Haskell, и, вероятно, это будет short while. Просто попросили его использовать, чтобы лучше понять функциональное программирование для класса, который я посещаю в Uni.

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

Так что, по сути, если я дам ему [“A1-Gate”, “North-Region”, “South-Region”, “Convention Center”, “Rectorate”, “Academic Building1”, “Academic Building2”] и [0.0, 0.5, 0.7, 0.3, 0.6, 1.2, 1.4, 1.2], мое дерево должно выйти как

Tree Layout I am trying to achieve

Но результаты моего тестового прогона не такие, как я ожидал. Так что сверхъестественный эксперт по Haskell может помочь мне определить, что я делаю неправильно. Вывод:

*Main> l1 = ["A1-Gate", "North-Region", "South-Region", "Convention Center", 
             "Rectorate", "Academic Building1", "Academic Building2"]
*Main> l3 = [0.0, 0.5, 0.7, 0.3, 0.6, 1.2, 1.4, 1.2]
*Main> parkingtree = createBinaryParkingTree l1 l3
*Main> parkingtree
Node "North-Region" 0.5 
   (Node "A1-Gate" 0.0 EmptyTree EmptyTree) 
   (Node "Convention Center" 0.3 
     (Node "South-Region" 0.7 EmptyTree EmptyTree) 
     (Node "Academic Building2" 1.4 
       (Node "Academic Building1" 1.2 EmptyTree EmptyTree) 
       (Node "Rectorate" 0.6 EmptyTree EmptyTree)))

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

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

data Tree = EmptyTree | Node [Char] Float Tree Tree deriving (Show,Eq,Ord)

insertElement location cost EmptyTree = 
   Node location cost EmptyTree EmptyTree
insertElement newlocation newcost (Node location cost left right) = 
   if (left == EmptyTree && right == EmptyTree)
   then Node location cost (insertElement newlocation newcost EmptyTree) 
                           right
   else if (left == EmptyTree && right /= EmptyTree)
        then Node location cost (insertElement newlocation newcost EmptyTree) 
                                right
        else if (left /= EmptyTree && right == EmptyTree)
             then Node location cost left 
                                (insertElement newlocation newcost EmptyTree)
             else Node newlocation newcost EmptyTree
                                (Node location cost left right)

buildBPT [] = EmptyTree
--buildBPT (xs:[]) = insertElement (fst xs) (snd xs) (buildBPT [])
buildBPT (x:xs) = insertElement (fst x) (snd x) (buildBPT xs)

createBinaryParkingTree a b = buildBPT (zip a b)

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

Ответы [ 3 ]

7 голосов
/ 06 марта 2020

Вот решение corecursive .

{-#  bft(Xs,T) :- bft(Xs, [T|R], R).   % if you don't read Prolog, see (*) 

     bft(     [],    Ns ,      []) :-  maplist( =(empty), Ns).
     bft( [X|Xs], [N|Ns], [L,R|Z]) :-  N = node(X,L,R), 
        bft( Xs,     Ns,       Z).
#-}

data Tree a = Empty | Node a (Tree a) (Tree a) deriving Show

bft :: [a] -> Tree a
bft xs = head nodes    -- Breadth First Tree
  where
  nodes = zipWith g (map Just xs ++ repeat Nothing)
                                 -- true length of Empty leaves: |xs| + 1
                    (pairs $ tail nodes)
  g (Just x) (lt,rt) = Node x lt rt
  g Nothing  _       = Empty
  pairs ~(a: ~(b:c)) = (a,b) : pairs c

nodes - это перечисление в ширину всех поддеревьев дерева результатов. Само дерево является верхним поддеревом, т. Е. Первым в этом списке. Мы создаем Node с из каждого x на входе xs, а когда вход исчерпан, мы создаем Empty с.

И нам не нужно было вообще считать.

Тестирование:

> bft [1..4]
Node 1 (Node 2 (Node 4 Empty Empty) Empty) (Node 3 Empty Empty)

> bft [1..10]
Node 1 
   (Node 2 
      (Node 4 
         (Node 8  Empty Empty) 
         (Node 9  Empty Empty)) 
      (Node 5 
         (Node 10 Empty Empty) 
         Empty)) 
   (Node 3 
      (Node 6 Empty Empty) 
      (Node 7 Empty Empty))

Как это работает: ключ g лень, что он не заставляет lt или rt значение, в то время как структура кортежа легко обслуживается - очень ленивая сама по себе - pairs. Таким образом, оба эти параметра аналогичны еще не установленным переменным в этом псевдокоде Prolog (*), когда используются как 2-й и 3-й аргументы g. Но затем, для следующих x в xs, lt становится g результатом .

А затем очередь rt и др. c. И когда xs заканчивается и мы нажимаем Nothing s, g прекращает извлекать значения из вывода pairs в целом. Так что pairs останавливается также на nodes.


(*) Переменные Пролога явно set-once : им разрешено быть в еще не назначенном состоянии. Haskell (x:xs) - это пролог [X | Xs].

Псевдокод: поддерживает очередь; поставить в очередь «неназначенный указатель»; для каждого x в xs: {установите указатель в текущем заголовке очереди на Node(x, lt, rt), где lt, rt - неназначенные указатели; поставить в очередь lt; поставить в очередь rt; поп-очередь}; установить все указатели, оставшиеся в очереди, на Empty; найти результирующее дерево в исходном заголовке очереди, то есть в исходном первом «неназначенном указателе» (или «пустой ящик» вместо «неназначенного указателя» - другой вариант).

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


Обновление: @ dfeuer придумали гораздо упрощенную версия этого, которая намного ближе к оригиналу Пролога (тот, что в комментарии вверху поста), который может быть намного яснее . Ищите более эффективный код, обсуждения и прочее в его посте . Если вместо очереди dfeuer использовать более эффективный тип бесконечного потока data IS a = a :+ IS a для очереди поддеревьев, вместо простого

bftree :: [a] -> Tree a
bftree xs = t
    where
    t : q  =  go xs q
    go []       _              =  repeat Empty
    go (x:ys) ~(l : ~(r : q))  =  Node x l r : go ys q

      ---READ-- ----READ----      ---WRITE---

для сравнения будет выполнена обратная операция перечисления в ширину дерево -

bflist :: Tree a -> [a]
bflist t = [x | Node x _ _ <- q]
    where
    q  =  t : go 1 q
    go 0  _                =          []
    go i (Empty      : q)  =          go (i-1) q
    go i (Node _ l r : q)  =  l : r : go (i+1) q

         -----READ------     --WRITE--

Как работает bftree: t : q - список поддеревьев дерева в первом порядке. В конкретном вызове go (x:ys) используются l и r до , они определяются последующими вызовами go, либо с другим x дальше вниз по ys, либо go [] который всегда возвращает Empty. Результат t - самый первый в этом списке, самый верхний узел дерева, то есть само дерево.

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

Эти дополнительные узлы, таким образом, также должны быть определены, как Empty. Нам все равно, сколько нужно, и просто создаем их бесконечный список, чтобы удовлетворить любую потребность, хотя фактическое количество пустых листов будет на один больше, чем было xs.

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

6 голосов
/ 04 марта 2020

Обновление: приведенное ниже решение является оптимальным и, как мне кажется, довольно простым для понимания, поэтому я оставляю его здесь на всякий случай. Однако решение Уилла Несса гораздо красивее, и, особенно при оптимизации немного , можно ожидать, что оно будет работать лучше на практике. Это гораздо более достойно изучения!


Я собираюсь пока игнорировать фальшивые метки краев и сосредоточиться только на сути происходящего.

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

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

-- Built a tree from a breadth-first list
bft :: [a] -> Tree a
bft xs = case dff 1 xs of
  [] -> Empty
  [t] -> t
  _ -> error "something went wrong"

-- Build a forest of nonempty trees.
-- The given number indicates the (maximum)
-- number of trees to build.
bff :: Int -> [a] -> [Tree a]
bff _ [] = []
bff n xs = case splitAt n xs of
  (front, rear) -> combine front (bff (2 * n) rear)
  where
    combine :: [a] -> [Tree a] -> [Tree a]
    -- you write this

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

bft' :: [a] -> Tree a
bft' xs = case bff 1 xs of
  [] -> Empty
  [t] -> t
  _ -> error "whoops"

bff' :: Int -> [a] -> [Tree a]
bff' !_ [] = []
bff' n xs = combine n xs (bff (2 * n) (drop n xs))
  where
    -- The "take" portion of the splitAt in the original
    -- bff is integrated into this version of combine. That
    -- lets us avoid allocating an intermediate list we don't
    -- really need.
    combine :: Int -> [a] -> [Tree a] -> [Tree a]
    combine 0 !_ ~[] = [] -- These two lazy patterns are just documentation
    combine _k [] ~[] = []
    combine k (y : ys) ts = Node y l r : combine (k - 1) ys dropped
      where
        (l, ~(r, dropped)) = case ts of  -- This lazy pattern matters.
          [] -> (Empty, (Empty, []))
          t1 : ts' -> (t1, case ts' of
            [] -> (Empty, [])
            t2 : ts'' -> (t2, ts''))

Для менее ленивого варианта замените (!l, ~(!r, dropped)) на (!l, !r, dropped) и соответственно отрегулируйте RHS.

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

data SL a = Cons !a (SL a) | Nil

И пары в приведенном выше (l, ~(r, dropped)) должны быть представлены с использованием типа, подобного

data LSP a b = LSP !a b

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

3 голосов
/ 04 марта 2020

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

Добавление 8 узлов в дерево потребует следующих шагов (смотрите, как вставляются узлы). от последнего к первому):

   .              4                                                                                                                                                                                          
                6   6                                                                                                                                                                                        
   8           7 8 . .                                                                                                                                                                                       
  . .                                                                                                                                                                                                           
                  3                                                                                                                                                                                          
   7            4   5                                                                                                                                                                                        
  8 .          6 7 8 .                                                                                                                                                                                       

   6              2                                                                                                                                                                                          
  7 8           3   4                                                                                                                                                                                        
               5 6 7 8                                                                                                                                                                                       
   5                                                                                                                                                                                                         
 6   7            1                                                                                                                                                                                      
8 . . .       2       3                                                                                                                                                                                  
            4   5   6   7                                                                                                                                                                                
           8 . . . . . . .

Если вместо этого вы вставите узлы слева направо, сверху вниз, вы получите гораздо более простое решение, не требующее поворота, а вместо этого некоторая древовидная структура. Смотрите порядок вставки; всегда существующие значения остаются там, где они были:

   .              1                                                                                                                                                                                               
                2   3                                                                                                                                                                                             
   1           4 5 . .                                                                                                                                                                                            
  . .                                                                                                                                                                                                             
                  1                                                                                                                                                                                               
   1            2   3                                                                                                                                                                                             
  2 .          4 5 6 .                                                                                                                                                                                            

   1              1                                                                                                                                                                                               
  2 3           2   3                                                                                                                                                                                             
               4 5 6 7                                                                                                                                                                                            
   1                                                                                                                                                                                                              
 2   3            1                                                                                                                                                                                           
4 . . .       2       3                                                                                                                                                                                       
            4   5   6   7                                                                                                                                                                                     
           8 . . . . . . .

Шаг вставки имеет асимптотику c сложность времени порядка O(n^2), где n - количество узлов для вставки , поскольку вы вставляете узлы один за другим, а затем перебираете узлы, уже присутствующие в дереве.

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

  • , если это так, и правое поддерево не завершено, затем вернитесь вправо.
  • , если оно есть, и правое поддерево также завершите, затем повторите влево (начиная с новой строки).
  • , если это не так, затем выполните возврат влево.

Вот мой (более обобщенный c) решение:

data Tree a = Leaf | Node a (Tree a) (Tree a)
            deriving (Eq, Show)

main = do
    let l1 = ["A1-Gate", "North-Region", "South-Region", "Convention Center", 
              "Rectorate", "Academic Building1", "Academic Building2"]
    let l2 = [0.0, 0.5, 0.7, 0.3, 0.6, 1.2, 1.4, 1.2]
    print $ treeFromList $ zip l1 l2

mkNode :: a -> Tree a
mkNode x = Node x Leaf Leaf

insertValue :: Tree a -> a -> Tree a
insertValue Leaf y = mkNode y
insertValue (Node x left right) y
    | isComplete left && nodeCount left /= nodeCount right = Node x left (insertValue right y)
    | otherwise = Node x (insertValue left y) right
    where nodeCount Leaf = 0
          nodeCount (Node _ left right) = 1 + nodeCount left + nodeCount right
          depth Leaf = 0
          depth (Node _ left right) = 1 + max (depth left) (depth right)
          isComplete n = nodeCount n == 2 ^ (depth n) - 1

treeFromList :: (Show a) => [a] -> Tree a
treeFromList = foldl insertValue Leaf

РЕДАКТИРОВАТЬ: более подробное объяснение:

Идея состоит в том, чтобы запомнить, в каком порядке вы вставляете узлы: слева направо * Сначала 1033 *, затем сверху вниз . Я сжал различные случаи в фактической функции, но вы можете расширить их до трех:

  1. Левая сторона завершена? Если нет, то вставьте в левую сторону.
  2. Является ли правая сторона такой же полной, как левая, которая завершена? Если нет, то вставьте в правую часть.
  3. Обе стороны заполнены, поэтому мы начинаем новый уровень, вставив в левую сторону.

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

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

Теперь, как мы можем определить, является ли дерево «полным»? Что ж, оно завершено, если оно идеально сбалансировано или если - визуально - его значения образуют треугольник. Поскольку мы работаем с бинарными деревьями, то основание треугольника (при заполнении) должно иметь число значений, равное степени два. Более конкретно, он должен иметь значения 2^(depth-1). Подсчитайте сами в примерах:

  • depth = 1 -> base = 1: 2^(1-1) = 1
  • depth = 2 -> base = 2: 2^(2-1) = 2
  • depth = 3 -> base = 4: 2^(3-1) = 4
  • depth = 4 -> base = 8: 2^(4-1) = 8

Общее количество узлов над основанием на единицу меньше ширины основания: 2^(n-1) - 1. Таким образом, общее количество узлов в полном дереве - это количество узлов над базой плюс число узлов в базе, поэтому:

num nodes in complete tree = 2^(depth-1) - 1 + 2^(depth-1)
                           = 2 × 2^(depth-1) - 1
                           = 2^depth - 1

Итак, теперь мы можем сказать, что дерево завершено, если оно имеет точно 2^depth - 1 непустые узлы в нем.

Поскольку мы go слева направо, сверху вниз, когда левая сторона завершена, мы двигаемся вправо, а когда вправо сторона так же полна, как и левая сторона (это означает, что она имеет такое же количество узлов, что означает, что она также завершена из-за инварианта), тогда мы знаем, что все дерево завершено, и, следовательно, должна быть добавлена ​​новая строка.

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

  • Если обе стороны пусты, то countNodes left == countNodes right, поэтому мы добавляем еще одну строку (слева).
  • Если левая сторона пуста, то обе стороны пусты (см. Предыдущую точку).
  • Если правая сторона пуста, то левая сторона должна иметь глубину 1 и количество узлов 1, то есть завершено, и 1 /= 0, поэтому мы добавляем к правой стороне.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...