我试图在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
我不明白我做错了什么。任何帮助将不胜感激。
答案 0 :(得分:12)
我认为你需要更加谨慎地思考这个问题。您的数据结构
data BinTree a =
Empty
| Node (BinTree a) a (BinTree a)
deriving (Eq,Show)
本质上是递归的,因为它是根据自身定义的,所以我们应该利用它。 bheklilr关于线条的评论是非常明智的,但我们可以更进一步。以下是如何打印树的总体计划:
您正在尝试通过分析是否存在Node
或Empty
子树的所有情况来处理来自一个层的详细信息。别。让递归做到这一点。以下是我们处理空树的方法:
请注意,我们仍然可以继续执行总体计划,因为如果你没有缩进,你仍然无法获得任何结果
大。现在我们已经对它进行了排序,我们可以编写一些代码。首先让我们对缩进事项进行排序:
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
我们只需要编写两个函数layoutTree
和unlines
。 (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
的特定表示的特殊知识构造的某些树一起使用;它必须仅使用empty
和node
构建,就像使用ADT一样。