我在Haskell中实现了二叉树数据结构。
我的代码:
module Data.BTree where
data Tree a = EmptyTree
| Node a (Tree a) (Tree a)
deriving (Eq, Ord, Read, Show)
emptyTree :: a -> Tree a
emptyTree a = Node a EmptyTree EmptyTree
treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = emptyTree x
treeInsert x (Node a left right)
| x == a = (Node x left right)
| x < a = (Node a (treeInsert x left) right)
| x > a = (Node a left (treeInsert x right))
fillTree :: Int -> Tree Int -> Tree Int
fillTree 10000 tree = tree
fillTree x tree = let a = treeInsert x tree
in fillTree (x + 1) a
这段代码很慢。我跑:
fillTree 1 EmptyTree
我得到:50.24秒
我尝试用C语言实现此代码,并尝试使用此测试的结果:0m0.438s
为什么这么大的区别? Haskell代码依赖这么慢还是我的二进制树在haskell中坏了?我想问一下haskell guru,也许我可以让我的二叉树实现更有效?
谢谢。
答案 0 :(得分:14)
首先,另一个数据点:Set
模块中的Data.Set
数据结构恰好是二叉树。我已将您的fillTree
函数翻译为使用它,而不是:
import qualified Data.Set as Set
import Data.Set (Set)
fillSet :: Int -> Set Int -> Set Int
fillSet 10000 set = set
fillSet x set = let a = Set.insert x set
in fillSet (x + 1) a
在GHCi中运行fillSet 1 Set.empty
,包括一些额外的计算,以确保评估整个结果,运行时没有明显的延迟。所以,这似乎表明问题在于您的实施。
首先,我怀疑使用Data.Set.Set
与您的实现之间的最大区别在于,如果我正确地读取您的代码,那么您实际上并未测试二叉树。您正在测试一个过于复杂的链表 - 即最大不平衡树 - 因为按顺序插入元素。 Data.Set.Set
使用平衡二叉树,在这种情况下可以更好地处理病态输入。
我们还可以查看Set
:
data Set a = Tip
| Bin {-# UNPACK #-} !Size a !(Set a) !(Set a)
没有详细说明,这说明了跟踪树的大小,并避免了一些不太有用的间接层,否则这些层将存在于数据类型中。
可以找到Data.Set
模块的完整来源here;你可能会觉得很有启发性。
进一步观察,以展示不同运行方式之间的差异。我在您的代码中添加了以下内容:
toList EmptyTree = []
toList (Node x l r) = toList l ++ [x] ++ toList r
main = print . sum . toList $ fillTree 1 EmptyTree
这遍历树,对元素求和,并打印总数,这应该确保一切都是强制的。我的系统可能有点不寻常,所以你可能会自己尝试不同的结果,但相对差异应该足够准确。一些结果:
使用runhaskell
,大致相当于在GHCi中运行它:
real 1m36.055s
user 0m0.093s
sys 0m0.062s
使用ghc --make -O0
构建:
real 0m3.904s
user 0m0.030s
sys 0m0.031s
使用ghc --make -O2
构建:
real 0m1.765s
user 0m0.015s
sys 0m0.030s
使用基于Data.Set
的等效函数代替:
使用runhaskell
:
real 0m0.521s
user 0m0.031s
sys 0m0.015s
使用ghc --make -O2
:
real 0m0.183s
user 0m0.015s
sys 0m0.031s
今天的故事的道德是:评估GHCi中的表达式并使用秒表计时,这是测试代码性能的一种非常非常糟糕的方式。
答案 1 :(得分:6)
我怀疑你在C中实现了相同的代码。您可能使用了非持久性树结构。 这意味着你将Haskell中的O(n ^ 2)算法与C中的O(n)算法进行比较。 Nevermind,你正在使用的具体情况是O(n ^ 2)是否具有持久性结构。持久性结构只有更多的分配,因此它不是一个基本的算法差异。
此外,看起来你是从ghci运行的。 “我”在“ghci”中的意思是“翻译”。是的,解释器可能比编译代码慢几十倍或几百倍。尝试使用优化进行编译并运行它。 我怀疑由于基本的算法差异,它仍然会变慢,但它不会接近50秒。