【问题标题】:BST: how to define `insert` in terms of catamorphic fold?BST:如何根据变形折叠定义“插入”?
【发布时间】:2021-04-15 21:36:12
【问题描述】:

我有一个典型的二叉搜索树数据类型:

data Tree a
  = Empty
  | Branch a (Tree a) (Tree a) deriving Show

还有变态

foldt :: b -> (a -> b -> b -> b) -> Tree a -> b
foldt empty _ Empty = empty
foldt empty branch (Branch a l r) = branch a (foldt empty branch l) (foldt empty branch r)

我尝试使用foldt 定义插入函数并得到了一些有趣的结果:

insert :: (Ord a) => a -> Tree a -> Tree a
insert x = foldt (single x) insertb
  where insertb a left right
          | x == a = Branch x left right
          | x < a = Branch a (insert x left) right
          | x > a = Branch a left (insert x right)
ghci> mytree = insert 2 (Branch 3 Empty Empty)  
ghci> mytree
Branch 3 (Branch 2 (Branch 2 Empty Empty) (Branch 2 Empty Empty)) (Branch 2 Empty Empty)
ghci> 

当然,传统的插入方法的行为符合预期:

insert' :: (Ord a) => a -> Tree a -> Tree a
insert' x Empty = single x
insert' x (Branch a left right)
  | x == a = Branch x left right
  | x < a = Branch a (insert' x left) right
  | x > a = Branch a left (insert' x right)
ghci> mytree2 = insert' 2 (Branch 3 Empty Empty)
ghci> mytree2
Branch 3 (Branch 2 Empty Empty) Empty
ghci>

有没有办法用foldt 来定义insert,还是我在这里找错了树(ha)?

【问题讨论】:

  • 也许cata 有可能,我不确定。但是使用para 很容易,这对于这个用例来说似乎是完美的。
  • 提示:foldt empty branch 应用程序不应直接生成树。它可以产生一个函数 Maybe a -&gt; Tree a,我怀疑也可以产生一对树,其中一个执行了插入,而另一个没有。从效率的角度来看,多态性肯定会更好。
  • 请注意,当使用 foldt 之类的东西时,您通常会尝试在其他地方避免显式递归。所以insertb 中的那些insert 调用有点危险。
  • @amalloy catapara 具有相同的功率。但是,是的,它们绝对没有同等的便利性。
  • @Carl,也没有同等性能。 para 为您赢得了很多分享。

标签: haskell functional-programming binary-search-tree catamorphism


【解决方案1】:

让我们定义一个函数

insertMaybe :: Ord a => Tree a -> Maybe a -> Tree a

这个函数需要一棵树,也可能是一个元素。在Just 的情况下,元素被插入。在Nothing 的情况下,树原样返回。那么我们可以定义

insert a t = insertMaybe t (Just a)

现在:

insertMaybe :: Ord a => Tree a -> Maybe a -> Tree a
insertMaybe = foldt leaf branch
  where
    leaf (Just new) = ?
    leaf Nothing = ?

    branch a l r Nothing = ?
    branch a l r (Just new)
      | ... = ?
      ...

或者:

data Ins a = Ins
  { inserted :: Tree a
  , notInserted :: Tree a }

insert a t = inserted (insertAndNot a t)

-- Return the tree with the 
-- element inserted, and also unchanged.
insertAndNot :: Ord a => a -> Tree a -> Ins a
insertAndNot new = foldt leaf branch
  where
    leaf = Ins ? ?
    branch a ~(Ins li lni) ~(Ins ri rni)
      | ... = Ins ? ?
      ...

变形

上述解决方案有一个主要的效率问题:它们完全重建树结构只是为了插入一个元素。正如 amalloy 建议的那样,我们可以通过将 foldt(变质)替换为 parat(变形)来解决这个问题。 parat 使 branch 函数可以访问递归修改的子树和未修改的子树。

parat :: b -> (a -> (Tree a, b) -> (Tree a, b) -> b) -> Tree a -> b
parat leaf _branch Empty = leaf
parat leaf branch (Branch a l r) =
  branch a
         (l, parat leaf branch l)
         (r, parat leaf branch r)

方便地,使用parat 定义insert 也稍微更容易。你能看出怎么做吗?这最终成为我建议使用 foldt 的“替代”方式的有效版本。

【讨论】:

  • 啊我想我现在明白了!我会更新我的帖子。如果有更好的写法,请告诉我。
  • 我还注意到parat 中的递归调用似乎比foldt 中的更自然一些。是偏态更适合这种递归还是它仍然是一种奇怪的模式?
【解决方案2】:

感谢 dfeuer 和 amalloy 提供关于超晶的提示,TIL!

给定 Tree 数据类型的变形:

parat :: b -> (a -> (Tree a, b) -> (Tree a, b) -> b) -> Tree a -> b
parat empty _ Empty = empty
parat empty branch (Branch a l r) =
  branch a
         (l, parat leaf branch l)
         (r, parat leaf branch r)

我们可以写一个插入函数:

insert :: Ord a => a -> Tree a -> Tree a
insert x = parat (single x) branch
  where branch a (l, l') (r, r')
          | x == a = Branch x l r
          | x < a = Branch a l' r
          | x > a = Branch a l r'
ghci> mytree = insert 2 (Branch 3 Empty Empty)
ghci> mytree
Branch 3 (Branch 2 Empty Empty) Empty
ghci>

测试一棵更大的树...

import Data.Function

mytree :: Tree Integer
mytree = (Branch 3 Empty Empty) & insert 2 & insert 4 & insert 6 & insert 5 & insert 10

inorder :: Tree a -> [a]
inorder = foldt [] (\a l r -> l ++ [a] ++ r)
ghci> mytree
Branch 3 (Branch 2 Empty Empty) (Branch 4 Empty (Branch 6 (Branch 5 Empty Empty) (Branch 10 Empty Empty)))
ghci> inorder mytree
[2,3,4,5,6,10]
ghci>

【讨论】:

  • 不,你真的不应该在branch 的定义中使用insert。记住 parat 传入的内容! parat 正在为你做递归;你不再需要递归了。
  • @dfeuer 啊,分别是l' rl r' 对吧?在那之后我的大脑很痛。
  • 是的,现在您已经掌握了窍门!只是为了好玩,我建议你尝试我的前两种方法(使用foldt)。完成后,也为了好玩,使用我的 second 方法使用foldt 实现parat 的低效版本。
  • @j_maes54 您也可以尝试使用“apomorphism”,它是paramorphism 的展开版本。它从一个种子开始(我认为它包含在原始树加上要插入的值)并生成一棵新树。如果您知道不打算修改该分支,apo 将让您立即返回一个子树。 hackage.haskell.org/package/recursion-schemes-5.2.2.1/docs/… 签名类似于apo :: (b -&gt; Maybe (a, Either (Tree a) b, Either (Tree a) b)) -&gt; b -&gt; Tree aNothing 表示产生 Empty
  • @danidiaz,是的,展开通常是一种选择。在变形前了解变形可能会很好。
猜你喜欢
  • 1970-01-01
  • 2020-09-09
  • 1970-01-01
  • 1970-01-01
  • 2019-06-28
  • 1970-01-01
  • 1970-01-01
  • 2018-08-07
  • 1970-01-01
相关资源
最近更新 更多