【问题标题】:Simple search tree in Haskell: why stack overflow?Haskell 中的简单搜索树:为什么堆栈溢出?
【发布时间】:2015-02-16 01:18:01
【问题描述】:

我对 Haskell 完全陌生,正在努力学习。我决定编写一个简短的(不平衡的)二叉搜索树代码来开始。它将文本分解为单词,将单词添加到二叉树(丢弃重复项),然后遍历树以打印出文本中单词的排序列表。

data BinTree t = ExternalNode
               | InternalNode (BinTree t) t (BinTree t)

treeInsert :: Ord t => BinTree t -> t -> BinTree t
treeInsert ExternalNode                     w = InternalNode ExternalNode w ExternalNode
treeInsert tree@(InternalNode left v right) w
  | w == v    = tree
  | w < v     = InternalNode (treeInsert left w) v right
  | otherwise = InternalNode left v (treeInsert right w)

treeFromList :: Ord t => [t] -> BinTree t
treeFromList l = go ExternalNode l
  where
    go acc []       = acc
    go acc (x : xs) = acc `seq` go (treeInsert acc x) xs

inOrderList :: BinTree t -> [t]
inOrderList ExternalNode                = []
inOrderList (InternalNode left v right) = (inOrderList left) ++ [ v ] ++ (inOrderList right)

main :: IO ()
main = do
  tmp <- readFile "words.txt"
  printList . inOrderList . treeFromList $ words tmp

  where
    printList []       = return ()
    printList (x : xs) = do
      putStrLn x
      printList xs

该程序适用于小文本。然后我给它喂了詹姆士国王圣经。它崩溃抱怨堆栈大小太小。我必须将堆栈大小增加到 200M 才能使其工作!

我的错误在哪里?我想这可能与懒惰的评估搞砸了一些事情有关。无论如何,问题不在于二叉搜索树的深度,对于圣经示例,二叉搜索树的深度仅为 163。

【问题讨论】:

  • “仅”163?如果树是平衡的,那就是 2^164-1 个元素。
  • 您能否提供您使用的文件words.txt 的链接? this version of the KJV 是否与您的相媲美
  • 使用 ghc 7.8.3 我能够运行只有 27M 堆栈的程序:bintree +RTS -K27M(使用 ghc -O2 bintree.hs -rtsopts 编译后)
  • 好吧,String 每个字符使用大约十几个字节。如果你的树的任何分支由于懒惰而未被评估,那可能是六个字节左右......
  • 确实,如果您关心性能,String 通常不是您想要的——使用Text。如果你绝对只需要 ASCII,你甚至可以使用 ByteString,但这是不受欢迎的。

标签: haskell tree lazy-evaluation evaluation


【解决方案1】:

问题在于您构建的 thunk 嵌套太深。

此版本在treeInsert 中添加了seq 调用,以强制在树的每个级别进行评估 并且可以在很少的堆栈中运行:

import System.Environment
import Control.Monad

data BinTree t = ExternalNode
               | InternalNode (BinTree t) !t (BinTree t)

treeInsert :: Ord t => BinTree t -> t -> BinTree t
treeInsert ExternalNode                     w = InternalNode ExternalNode w ExternalNode
treeInsert tree@(InternalNode left v right) w
  | w == v    = tree
  | w < v     = let t = treeInsert left w  in t `seq` InternalNode t v right
  | otherwise = let t = treeInsert right w in t `seq` InternalNode left v t

treeFromList :: Ord t => [t] -> BinTree t
treeFromList l = go ExternalNode l
  where
    go acc []       = acc
    go acc (x : xs) = let t = treeInsert acc x in t `seq` go t xs

inOrderList :: BinTree t -> [t]
inOrderList ExternalNode                = []
inOrderList (InternalNode left v right) = (inOrderList left) ++ [ v ] ++ (inOrderList right)

main1 = do
  (arg0:_) <- getArgs
  tmp <- readFile arg0
  let t = treeFromList $ words tmp
  forM_ (inOrderList t) putStrLn

main = main1

也可以在BinTree的定义中使用严格注解:

data BinTree t = ExternalNode | InternalNode !(BinTree t) !t !(BinTree t)

代替seq 调用treeInsert - 这是Data.Set 所做的。

看来treeFromList 中的seq 调用效果不大。

【讨论】:

  • BangPatterns 是一个 GHC 扩展,用于在模式中使用这些东西。在 data 声明中使用它们是 Haskell 98。
  • 也就是说,据我所知,data 声明中的刘海,除了明确解包的字段之外,主要是为了方便。您可以改为使用强制适当参数的“智能构造函数”,从而在某些特定情况下在这些字段中安装 thunk 的可能性(我正在考虑在 Data.IntMap 中这样做以支持 fromFunction 函数)。
猜你喜欢
  • 2011-08-22
  • 2012-05-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-08
  • 2015-08-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多