Haskell -递归地向二叉树添加节点并跟踪其访问计数

5ssjco0h  于 2022-12-04  发布在  其他
关注(0)|答案(1)|浏览(194)

刚到 haskell ,正在解决一个小问题。
我正在使用一个二叉树,希望树中的每个节点都有一个它被访问过的次数的计数。为此,我创建了以下数据类型:

data BTree a = Leaf | Node (BTree a) a Int (BTree a)

我还有一个zipper,它表示树中的当前节点:

data Direction a = Lt a Int (BTree a) | Rt a Int (BTree a)
type Breadcrumbs a = [Direction a]
type Zipper a = (BTree a, Breadcrumbs a)

使用这种zipper类型,我试图将集合表示为二叉搜索树。为此,我将实现下面的函数,该函数接受一个值和zipper,并将具有给定值的节点插入树中。它通过从当前节点导航到树中的适当区域来完成此操作。

addNode:: Ord a => a -> Zipper a -> Zipper a

例如,使用空的zipper(Leaf,[])调用此函数:

addNode 2 $ addNode 4 $ addNode 3 (Leaf,[])

将产生以下树,其中当前节点的值为2。根节点将被访问两次。

Node (Node Leaf 2 1 Leaf) 3 2 (Node Leaf 4 1 Leaf)

然而,我不知道如何实现addNode函数,以便我可以递归地将给定的节点添加到二叉树中,同时跟踪该节点被访问的次数。有人能帮忙吗?

5uzkadbs

5uzkadbs1#

你可能不想让addNode取拉链,如果节点是随机插入的,你想总是从树的顶部调用addNode,没有拉链的简单递归解决方案会更好。
基于zipper的addNode只有在节点的添加顺序是“靠近”前一个节点的情况下才有意义。不平衡的二叉树可能是一个糟糕的数据结构选择。我很难想象这样一个用例,其中节点插入的顺序是这样的,基于拉链的addNode是有效的 * 和 * 不平衡二叉树保持近似平衡。
如果您决定使用基于zipper的addNode,我认为您需要重新思考“访问”节点的含义。

addNode 3 (Leaf, []) = (Node Leaf 3 1 Leaf, [])

这意味着新创建的节点已经被访问过一次。

(Node Leaf 4 1 Leaf, [Rt 3 2 Leaf])

因为你再次访问节点“3”并创建一个已经访问过一次的新节点“4”。但是,当你进一步addNode 2时,你必须重新访问节点“4”(以确定“2”小于“4”),重新访问节点“3”(以确定“2”也小于“3”),最后插入新的“2”节点,给出:

(Node Leaf 2 1 Leaf, [Lt 3 3 (Node Leaf 4 2 Leaf)])

这些访问次数与您在问题中给出的访问次数不同。
无论如何,这里有一些想法--同样,只有当你 * 真的 * 决定你想要一个基于zipper的addNode时。我认为你应该把问题分成两个递归步骤。第一个递归应该在树中向上移动,找到合适的起点。第二个递归应该在子树中向下移动,执行实际的插入。
第一个递归是微妙的,考虑下面这个树:

4
   / \
  3   7
     / \
    6   9

并且想象我们正在查看“6”并且试图插入“2”。为了决定我们是否需要向上移动,我们需要将新节点“2”与当前节点“6”进行比较。我们还需要查看第一个面包屑的方向,并且我们 * 可能 * 需要查看该面包屑的值。
例如,由于2〈6,并且第一个面包屑被留下,我们知道我们必须无条件地向上移动,因为在树的更高位置总是可能有更小的数字需要检查。
与此相反,在节点“9”处插入“2”。我们仍然有2〈9,但由于第一个面包屑是正确的,我们只有在父节点的值也大于(或等于)2。这里,由于2〈=7,我们上移,但如果“7”被“1”代替,我们将结束第一次递归并将“2”插入到节点“9”处的子树中。
基本上,只要(1)我们小于当前节点并且第一面包屑被留下或者我们小于或等于第一面包屑,我们就向上移动;或者(2)We大于当前节点并且第一面包屑是右的或者We大于或等于第一面包屑。
您需要决定“访问”一个节点是否意味着向上移动到它,和/或检查一个面包屑的方向和值而不向上移动到它是否应该计数。
一旦我们根据这些条件完成了向上移动,我们就可以继续在当前子树中插入新节点,以通常的方式向下移动。
如果您想给予一下,可以从以下模板开始:

addNode:: Ord a => a -> Zipper a -> Zipper a
addNode x = insertDown . upToSubtree
  where
    upToSubtree z@(_, []) = z        -- at root, so we're done moving up
    upToSubtree z@(Leaf, _) = ...    -- unconditionally move up
    upToSubtree z@(Node l y n r, (crumb:crumbs))
      =  case compare x y of
          LT -> case crumb of
            Lt _ _ _ -> ...              -- move up, regardless of crumb value
            Rt u _ _ | x<=u -> ...       -- move up only if x <= crumb
                     | otherwise -> ...  -- don't move up, but count visit to crumb?
          GT -> case crumb of
            Rt _ _ _ -> ...              -- move up, regardless of crumb value
            Lt u _ _ | x>=u -> ...       -- move up only if x >= crumb
                     | otherwise -> ...  -- don't move up, but count visit to crumb?
          EQ -> ...                      -- don't move up

    insertDown (Leaf, crumbs) = ...      -- insert new node
    insertDown (Node l y n r, crumbs)
      = case compare x y of
          LT -> ... go left and recurse ...
          GT -> ... go right and recruse ...
          -- don't insert duplicates, but consider as visited
          EQ -> ... note visit ...

扰流板

下面是我提出的完整的解决方案。我还没有对它进行广泛的测试。我的目的是将每次与节点的比较都计为一次访问,将每次与节点的比较都计为一次访问,但要避免重复计算对upToSubtree返回的节点的访问。即使下面的代码在大多数情况下实际上对该节点做了两次比较。我怀疑逻辑是否100%正确。


data BTree a = Leaf | Node (BTree a) a Int (BTree a) deriving (Show)
data Direction a = Lt a Int (BTree a) | Rt a Int (BTree a) deriving (Show)
type Breadcrumbs a = [Direction a]
type Zipper a = (BTree a, Breadcrumbs a)

addNode:: Ord a => a -> Zipper a -> Zipper a
addNode x = insertDown . upToSubtree
  where
    upToSubtree z@(_, []) = z
    upToSubtree z@(Leaf, _) = up z
    upToSubtree z@(Node l y n r, (crumb:crumbs))
      =  let z' = visit z  -- count visit for this comparison
      in case compare x y of
          LT -> case crumb of
            Lt _ _ _ -> up z'
            Rt u _ _ | x <= u -> up z'
                     -- we note comparison with parent, visit of this node counted by insertDown
                     | otherwise -> visitParent z
          GT -> case crumb of
            Rt _ _ _ -> up z'
            Lt u _ _ | x >= u -> up z'
                     -- we note comparison with parent, visit of this node counted by insertDown
                     | otherwise -> visitParent z
          EQ -> z -- visit will be counted by insertDown

    up (t, Lt y n r : crumbs) = upToSubtree (Node t y n r, crumbs)
    up (t, Rt y n l : crumbs) = upToSubtree (Node l y n t, crumbs)

    insertDown (Leaf, crumbs) = (Node Leaf x 1 Leaf, crumbs)
    insertDown (Node l y n r, crumbs)
      = case compare x y of
          LT -> insertDown (l, Lt y (n+1) r : crumbs)
          GT -> insertDown (r, Rt y (n+1) l : crumbs)
          -- don't insert duplicates, but consider as visited
          EQ -> (Node l y (n+1) r, crumbs)

    visit (Node l x n r, crumbs) = (Node l x (n+1) r, crumbs)
    visitParent (t, Lt x n r : crumbs) = (t, Lt x (n+1) r : crumbs)
    visitParent (t, Rt x n l : crumbs) = (t, Rt x (n+1) l : crumbs)

main = do
  print $ addNode 2 $ addNode 4 $ addNode 3 (Leaf,[])
  -- adding "2" to my ASCII tree example at node "9"
  print $ addNode 2 $ (Node Leaf 9 0 Leaf,
                       [ Rt 7 0 (Node Leaf 6 0 Leaf)
                       , Rt 4 0 (Node Leaf 3 0 Leaf)])
  -- adding "2" to my ASCII tree example at node "6"
  print $ addNode 2 $ (Node Leaf 6 0 Leaf,
                       [ Lt 7 0 (Node Leaf 9 0 Leaf)
                       , Rt 4 0 (Node Leaf 3 0 Leaf)])

相关问题