Haskell二叉树快速实现

时间:2011-07-22 16:43:49

标签: c algorithm haskell benchmarking

我在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,也许我可以让我的二叉树实现更有效?

谢谢。

2 个答案:

答案 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秒。