【问题标题】:Haskell - Recursively adding node to binary tree and keeping track of its visit countHaskell - 递归地将节点添加到二叉树并跟踪其访问次数
【发布时间】:2022-12-12 09:43:48
【问题描述】:

Haskell 的新手,正在处理一个小问题。

我正在使用一棵二叉树,并希望树中的每个节点都能计算出它被访问的次数。为此,我创建了以下数据类型:

我还有代表树中当前节点的拉链:

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

将导致当前节点值为 1 的以下树。根节点将被访问两次。

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

【问题讨论】:

标签: haskell


【解决方案1】:

你可能不想让addNode带上拉链。如果节点以随机顺序插入,您希望始终从树的顶部调用 addNode,并且没有拉链的简单递归解决方案更好。

基于拉链的addNode 仅在按每个添加的节点“接近”前一个节点的顺序添加节点时才有意义。但是,如果是这种情况,不平衡二叉树可能是一种糟糕的数据结构选择。我很难想象一个用例,其中节点插入的顺序使得基于拉链的 addNode 是有效的不平衡的二叉树保持近似平衡。

如果你决定继续使用基于拉链的addNode,我认为您需要重新考虑“访问”节点的含义。在您的示例中,您大概有:

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

意味着新创建的节点已经被访问过一次。当你 addNode 4 到这里时,你会期望得到:

(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)])

这些访问次数与您在问题中给出的不同。

不管怎样,这里有一些想法——再一次,只有当你真的决定你想要一个基于拉链的addNode。我认为您应该将问题分为两个递归步骤。第一次递归应该向上移动树以找到合适的起点。第二次递归应该向下移动子树以执行实际的插入。

第一个递归是微妙的。考虑这棵树:

    4
   / 
  3   7
     / 
    6   9

并想象我们正在查看“6”并试图插入“2”。为了决定我们是否需要向上移动,我们需要将新节点“2”与当前节点“6”进行比较。我们还需要查看第一个碎屑的方向,我们可能需要看看那块面包屑的价值。

例如,由于 2<6 并且留下了第一个碎屑,我们知道我们必须无条件地向上移动,因为总是有可能有较小的数字在树中检查更高的位置。

将此与在节点“9”处插入“2”进行对比。我们仍然有 2<9,但是由于第一个 crumb 是正确的,所以只有当父节点的值也大于(或等于)2 时,我们才向上移动。在这里,由于 2<=7,我们向上移动,但是如果“7”改为“1”,我们将结束第一次递归并将“2”插入到节点“9”的子树中。

基本上,只要 (1) 我们小于当前节点,并且第一个碎屑剩下或者我们小于或等于第一个碎屑,我们就向上移动;或 (2) 我们大于当前节点并且第一个碎屑是正确的或者我们大于或等于第一个碎屑。

您需要决定“访问”一个节点是否意味着向上移动到它和/或是否在不向上移动的情况下检查碎屑方向和碎屑的值应该算在内。

一旦我们根据这些条件完成向上移动,我们就可以继续在当前子树中插入新节点,以通常的方式向下移动。

如果您想尝试一下,您可以从以下模板开始:

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)])

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-02
    • 2018-10-30
    相关资源
    最近更新 更多