Algorithm 使用高阶遍历函数查找顺序遍历的第k个元素后中断
我有以下代码来按顺序遍历二叉树:Algorithm 使用高阶遍历函数查找顺序遍历的第k个元素后中断,algorithm,haskell,binary-tree,Algorithm,Haskell,Binary Tree,我有以下代码来按顺序遍历二叉树: data BinaryTree a = Node a (BinaryTree a) (BinaryTree a) | Leaf deriving (Show) inorder :: (a -> b -> b) -> b -> BinaryTree a -> b inorder f acc tree = go tree acc where go Leaf z = z go (Node v l r) z
data BinaryTree a =
Node a (BinaryTree a) (BinaryTree a)
| Leaf
deriving (Show)
inorder :: (a -> b -> b) -> b -> BinaryTree a -> b
inorder f acc tree = go tree acc
where go Leaf z = z
go (Node v l r) z = (go r . f v . go l) z
使用上面的inoorder函数,我希望获得第k个元素,而不必遍历整个列表
遍历有点像一个折叠,只要你给它传递一个函数和一个起始值。我想我可以通过传递k
作为起始值来解决这个问题,并且一个函数将递减k
,直到它达到0,然后在该点返回当前节点内的值
我的问题是,除了修改整个函数外,我不太确定如何打破顺序遍历的递归,但我觉得必须修改高阶函数会破坏首先使用高阶函数的意义
有没有办法在k次迭代后中断?我观察到在左子树和右子树上递归调用go
的结果对f
不可用;因此,无论f
做什么,它都不能选择忽略递归调用的结果。因此,我相信,按照所写的顺序,将始终遍历整个树。(edit:回顾一下,这句话可能有点强;f
似乎有机会忽略左子树。但这一点基本上是正确的;没有理由用这种方式将左子树提升到右子树之上。)
更好的选择是对f
进行递归调用。例如:
anyOldOrder :: (a -> b -> b -> b) -> b -> BinaryTree a -> b
anyOldOrder f z = go where
go Leaf = z
go (Node v l r) = f v (go l) (go r)
现在当我们写作的时候
flatten = anyOldOrder (\v ls rs -> ls ++ [v] ++ rs) []
我们会发现flatten
足够懒惰:
> take 3 (flatten (Node 'c' (Node 'b' (Node 'a' Leaf Leaf) Leaf) undefined))
"abc"
(未定义的用于提供证据,证明在遍历过程中从未检查过树的这一部分。)因此,我们可以编写
findK k = take 1 . reverse . take k . flatten
这将正确短路。您可以使用标准技术使展平
稍微更有效:
flatten' t = anyOldOrder (\v l r -> l . (v:) . r) id t []
为了好玩,我还想展示如何在不使用累加器列表的情况下实现这个函数。相反,我们将生成一个有状态计算,它遍历树的“有趣”部分,在到达k
th元素时停止。有状态计算如下所示:
import Control.Applicative
import Control.Monad.State
import Control.Monad.Trans.Maybe
kthElem k v l r = l <|> do
i <- get
if i == k
then return v
else put (i+1) >> r
我们可以验证它是否仍然像预期的那样懒惰:
> findK' 3 $ Node 'c' (Node 'b' (Node 'a' Leaf Leaf) Leaf) undefined
Just 'c'
折叠列表的概念(至少?)有两个重要的概括。第一个更强大的概念是一个反同构。Daniel Wagner回答的anyOldOrder
遵循这种模式
但是对于你的特殊问题,类同构的概念比你需要的更强大。第二个较弱的概念是可折叠的容器Foldable
表达了一种容器的概念,该容器的元素可以通过任意Monoid
的操作混合在一起。这里有一个可爱的小把戏:
{-# LANGUAGE DeriveFoldable #-}
-- Note that for this trick only I've
-- switched the order of the Node fields.
data BinaryTree a =
Node (BinaryTree a) a (BinaryTree a)
| Leaf
deriving (Show, Foldable)
index :: [a] -> Int -> Maybe a
[] `index` _ = Nothing
(x : _) `index` 0 = Just x
(_ : xs) `index` i = xs `index` (i - 1)
(!?) :: Foldable f => Int -> f a -> Maybe a
xs !? i = toList xs `index` i
那么您可以直接使用代码>以索引到树中
这个技巧很可爱,事实上派生可折叠的非常方便,但它不会帮助您理解任何东西。首先,我将展示如何在不使用Foldable
的情况下,相当直接和高效地定义treetolost
treeToList :: BinaryTree a -> [a]
treeToList t = treeToListThen t []
神奇之处在于treeToListThen
函数treeToListThen t more
将t
转换为列表,并将列表more
附加到结果的末尾。事实证明,这种轻微的泛化是使列表转换高效所需的全部
treeToListThen :: BinaryTree a -> [a] -> [a]
treeToListThen Leaf more = more
treeToListThen (Node v l r) more =
treeToListThen l $ v : treeToListThen r more
我们不需要对左子树进行顺序遍历,然后追加其他所有内容,而是告诉左遍历完成后要在末尾粘贴什么!这避免了重复列表连接的潜在严重低效性,在不好的情况下,重复列表连接会使事情变成O(n^2)
回到可折叠的概念,将事物转换为列表是foldr
的一个特例:
toList = foldr (:) []
那么,我们如何为树实现foldr
?它最终与我们使用toList
所做的有些相似:
foldrTree :: (a -> b -> b) -> b -> BinaryTree a -> b
foldrTree _ n Leaf = n
foldrTree c n (Node v l r) = foldrTree c rest l
where
rest = v `c` foldrTree c n r
也就是说,当我们走到左边时,我们告诉它,当它完成时,它应该处理当前节点及其右子节点
现在foldr
并不是Foldable
最基本的操作;那实际上是
foldMap :: (Foldable f, Monoid m)
=> (a -> m) -> f a -> m
可以使用foldMap
来实现foldr
,使用一个特殊的Monoid
,这有点棘手。除非您要求,否则我现在不想给您带来太多的细节(但您应该查看Data.Foldable
中foldr
的默认定义)。相反,我将展示如何使用Daniel Wagner的anyOldOrder
定义foldMap
:
instance Foldable BinaryTree where
foldMap f = anyOldOrder bin mempty where
bin lres v rres = lres <> f v <> rres
实例可折叠二进制树,其中
foldMap f=任何旧订单仓位成员,其中
垃圾箱lres v rres=垃圾箱f v rres
以k(顺序为f[]树)
为例,其中f
是一个函数,它生成一个列表,其中按顺序遍历树。@chepner我认为编写的inoorder
将始终遍历整个树。显然,最好编写一个“更好的”in order
函数,当它的f
足够懒时可以避免这样做;在我看来,这里的问题是关于如何解决的。@chepner,但这并没有提前结束。我原以为这样也行,inorder会懒散地进行计算,但当我使用获取1(inorder(:)[]testTree)
@AR7 Withgo(Node v l r)z=go r(f v(go l z))
时,情况似乎并非如此,最外层的调用是对go
的递归调用,因此除了递归之外别无选择(因此检查子树,并通过归纳法检查右脊椎上的每个节点)。另一方面,对于go(node v l r)=fv(go l)(go r)
,最外层的调用是对f
的非递归调用。这意味着它是finstance Foldable BinaryTree where
foldMap f = anyOldOrder bin mempty where
bin lres v rres = lres <> f v <> rres