haskell漂亮的打印二进制树没有正确显示

时间:2013-09-29 19:52:04

标签: haskell binary-tree pretty-print

我试图在Haskell中打印一个二叉树,这样如果你把头转向左边,它应该看起来像一棵树。树中的每个级别应该比前一级别缩进2个空格。

这是预期的输出:

--        18
--      17
--        16
--    15
--          14
--        13
--      12
--        11
--  10
--        9
--          8
--      7
--        6
--    5
--        4
--      3
--          2
--        1

对于这棵树:

treeB = (Node (Node (Node (Node Empty 1 (Node Empty 2 Empty)) 3 (Node Empty 4 Empty)) 5 (Node (Node Empty 6 Empty) 7 (Node (Node Empty 8 Empty) 9 Empty))) 10 (Node (Node (Node Empty 11 Empty) 12 (Node Empty 13 (Node Empty 14 Empty))) 15 (Node (Node Empty 16 Empty) 17 (Node Empty 18 Empty))))

这是树的定义方式:

data BinTree a =
    Empty
  | Node (BinTree a) a (BinTree a)
  deriving (Eq,Show)

但是,我的结果看起来并不像那样。这是我的结果:

      18
  17
    16
  15
        14
  13
  12
    11
10
      9  
  8
  7
    6
  5
      4
  3
      2
  1

这是我的代码:

prettyTree :: (Show a) => BinTree a -> String
prettyTree Empty = "\n"
prettyTree (Node Empty x Empty) = "  " ++ show x ++ "\n"
prettyTree (Node Empty x r) = prettyTree' r ++ "  " ++ show x ++ "\n"
prettyTree (Node l x Empty) = show x ++ "\n" ++ "  " ++ prettyTree' l
prettyTree (Node l x r) = prettyTree' r ++ show x ++ "\n" ++ prettyTree' l

prettyTree' :: (Show a) => BinTree a -> String
prettyTree' Empty = "\n"
prettyTree' (Node Empty x Empty) = "  " ++ show x ++ "\n"
prettyTree' (Node Empty x r) = "  " ++ prettyTree' r ++ "  " ++ show x ++ "\n"
prettyTree' (Node l x Empty) = "  " ++ show x ++ "  " ++ "\n" ++ prettyTree' l
prettyTree' (Node l x r) = "  " ++ prettyTree' r ++ "  " ++ show x ++ "\n" ++ "  " ++ prettyTree' l

我不明白我做错了什么。任何帮助将不胜感激。

2 个答案:

答案 0 :(得分:12)

如何思考这个问题

我认为你需要更加谨慎地思考这个问题。您的数据结构

data BinTree a =
    Empty
  | Node (BinTree a) a (BinTree a)
  deriving (Eq,Show)

本质上是递归的,因为它是根据自身定义的,所以我们应该利用它。 bheklilr关于线条的评论是非常明智的,但我们可以更进一步。以下是如何打印树的总体计划:

  1. 打印正确的子树,从我们的位置缩进一点,
  2. 打印当前节点
  3. 打印左侧子树,从我们所在的位置缩进一点。
  4. 您正在尝试通过分析是否存在NodeEmpty子树的所有情况来处理来自一个层的详细信息。别。让递归做到这一点。以下是我们处理空树的方法:

    1. 什么都不输。
    2. 请注意,我们仍然可以继续执行总体计划,因为如果你没有缩进,你仍然无法获得任何结果

      编写函数

      大。现在我们已经对它进行了排序,我们可以编写一些代码。首先让我们对缩进事项进行排序:

      indent :: [String] -> [String]
      indent = map ("  "++)
      

      所以无论字符串是什么,前面都会附加" "。好。 (请注意,它适用于空列表并且不管它。)

      layoutTree :: Show a => BinTree a -> [String]
      layoutTree Empty = []  -- wow, that was easy
      layoutTree (Node left here right) 
               = indent (layoutTree right) ++ [show here] ++ indent (layoutTree left)
      

      那不是很好吗?我们只做左边,然后是当前,然后右边。递归不是很好!

      以下是您的示例树:

      treeB = (Node (Node (Node (Node Empty 1 (Node Empty 2 Empty)) 3 (Node Empty 4 Empty)) 5 (Node (Node Empty 6 Empty) 7 (Node (Node Empty 8 Empty) 9 Empty))) 10 (Node (Node (Node Empty 11 Empty) 12 (Node Empty 13 (Node Empty 14 Empty))) 15 (Node (Node Empty 16 Empty) 17 (Node Empty 18 Empty))))
      
      > layoutTree treeB
      ["      1","        2","    3","      4","  5","      6","    7","        8","      9","10","      11","    12","      13","        14","  15","      16","    17","      18"]
      

      你可以看到我们刚刚为每个元素创建了一个String表示行,但每行都缩进了多次,因为它被包含在另一个Node中。

      现在我们只需要将它们放在一起,但这并不难。请注意,之前的功能很简单,因为此步骤一直持续到最后。

      prettyTree :: Show a => BinTree a -> String
      prettyTree = unlines.layoutTree
      

      我们只需要编写两个函数layoutTreeunlines。 (unlines将所有字​​符串与之间的换行连接起来。)

      成品:

      > putStrLn (prettyTree treeB)
            18
          17
            16
        15
              14
            13
          12
            11
      10
            9
              8
          7
            6
        5
            4
          3
              2
            1
      

答案 1 :(得分:3)

我只想提供一些读者可能会觉得有趣的替代方法;我支持user2727321的答案更适合您的目的。

我要演示的内容称为“最终编码”(与“初始编码”相反,这是您的ADT表示),这样称呼它是因为它是一种数据类型的编码语义(它的解释)而不是语法(它的结构)。假设我们没有数据类型,而只想使用函数而不是构造函数。这意味着我们可以将逻辑直接编码到我们的“构造函数”中,而不是创建一个单独的函数来解释数据。

代表树作为自己的漂亮打印机

观察数据结构的每个解释,包括漂亮的打印,在数据上放置一些含义。在这种特殊情况下,树的含义是与深度有关的字符串。也就是说,可以在不同深度渲染相同的子树。例如,这是一个在深度为0的树:

  3
2
  1

这是在深度4处呈现的相同树:

          3
        2
          1

对于这种情况,我们可以进一步假设深度仅用于生成空格的前缀,所以让我们改为说树是依赖于某个给定前缀的字符串,这只是另一个字符串。我们可以说我们的树有以下表示:

type BinTree a = String -> String

有趣的是,类型参数a在这里实际上从未使用过,但为了保留与原始问题的一些不必要的相似性,我会把它留在那里。

构造函数

现在我们可以定义每个“构造函数”。回想一下,您的原始Empty构造函数具有以下类型:

Empty :: BinTree a

因此,我们希望我们自己的empty值具有相同的类型,只是根据我们的最终编码而不是您的初始编码:

empty :: BinTree a

如果我们扩展类型同义词,我们有:

empty :: String -> String

所有empty都是空字符串,完全忽略前缀:

empty _prefix = ""

现在我们转到内部节点。回想一下原始Node构造函数的类型:

Node :: BinTree a -> a -> BinTree a -> BinTree a

所以我们想写一个大致相同类型的node函数。但是,我们将使用show,因此Show约束将在此处显示出来:

node :: Show a => BinTree a -> a -> BinTree a -> BinTree a

扩展类型同义词非常混乱,但在学习这种技术时可能有助于参考:

node :: Show a =>
        (String -> String) -> a -> (String -> String) ->
        (String -> String)

要以给定前缀呈现内部节点,我们首先渲染右分支,前缀略长,然后使用我们的前缀渲染当前值,添加换行符,然后使用更长的前缀渲染左分支:

node l x r prefix =
  let prefix' = "  " ++ prefix
  in r prefix' ++ prefix ++ show x ++ "\n" ++ l prefix'

结束

我们编写一个函数来方便地打印一个没有前缀的树:

prettyTree :: BinTree a -> String
prettyTree tree = tree ""

可能有趣的是,由于我们在show而不是node中使用prettyTree,因此我们实际上不必在此处添加Show约束。我们只需要Show实际使用a参数的唯一函数。

在GHCi中测试:

> let treeB = (node (node (node (node empty 1 (node empty 2 empty)) 3 (node empty 4 empty)) 5 (node (node empty 6 empty) 7 (node (node empty 8 empty) 9 empty))) 10 (node (node (node empty 11 empty) 12 (node empty 13 (node empty 14 empty))) 15 (node (node empty 16 empty) 17 (node empty 18 empty))))
> putStr $ prettyTree treeB
      18
    17
      16
  15
        14
      13
    12
      11
10
      9
        8
    7
      6
  5
      4
    3
        2
      1

多重解释怎么办?

有人可能会合理地反对所有这些,你并不总是只想 打印一棵树。我完全同意。幸运的是,类型类有我们的支持。我们所要做的就是使用类型类重载类似构造函数的函数:

class BinaryTree f where
  empty :: f a
  node  :: Show a => f a -> a -> f a -> f a

我们之前的实现只是该类的一个实例(使用适当的newtype包装而不是类型同义词,因为这是使其成为类型类的实例所必需的)。其他解释可以有其他表示。您甚至可以构造一次树,并使用多态来以多种方式解释它。

这是一个带有类型类的完整实现,使用-XConstraintKinds-XTypeFamilies将恼人的Show约束从类型类移动到此特定实例:

class BinaryTree f where
  type Elem f a
  empty :: Elem f a => f a
  node  :: Elem f a => f a -> a -> f a -> f a

newtype BinTree a = BinTree { prettyTree' :: String -> String }

instance BinaryTree BinTree where
  type Elem BinTree a = Show a
  empty      = BinTree $ const ""
  node l x r =
    BinTree $ \prefix ->
    let prefix' = "  " ++ prefix
    in prettyTree' r prefix' ++ prefix ++ show x ++ "\n" ++ prettyTree' l prefix'

prettyTree :: (forall f. BinaryTree f => f a) -> String
prettyTree tree = prettyTree' tree ""

我做了一件我没有解释过的事情,就是强制prettyTree的二叉树参数的实际类型是多态的。这可以防止您将prettyTree与使用BinTree的特定表示的特殊知识构造的某些树一起使用;它必须仅使用emptynode构建,就像使用ADT一样。