【问题标题】:Haskell binary tree traversalHaskell二叉树遍历
【发布时间】:2021-11-20 14:42:24
【问题描述】:

我正在努力解决如何在 Haskell 中递归遍历二叉树

当返回类型是列表时,它是如何完成的很清楚。但是如果返回类型是树本身,我不明白它是如何完成的?

例如,如果我们有树并希望排除零:

                 5
                / \
               0   2
              / \
             0   1

我们想返回一棵带有其他数字的树:

                 5
                / \
               1   2

然后我想你会这样做:

modTree :: Tree -> Tree
modTree Void = Void
modTree (Node l x r)
    | x == 0 = modTree l
    | otherwise = Node (modTree l) x (modTree r)

我的问题是我知道这只会遍历左侧。但我无法理解如何在 Haskell 中为整个树递归调用该函数。

任何建议都将受到高度赞赏。

【问题讨论】:

  • 假设在您的绘图中,根节点50,当前0s 是5。你希望看到哪棵树从modTree 中长出来?
  • @DanielWagner 然后我不明白描述。 OP,您能否针对0 子树不是叶子的情况发布示例输入/输出?
  • @EugeneSh。正确的。现在也许你可以明白我为什么问我做的这个问题了。 ^_^
  • 在您的更新中,您 a) 不显示正确的输出,b) 与您关于零只是叶子的评论相矛盾。
  • @AwhatLoop 我非常仔细地问了我的问题(关于交换 5 和 0)。请回答我提出的问题,而不是其他问题。在我的问题中,输入树有两个5 节点:0 在根,左树是(4 在根,5 在左边,1 在右边)和右树是5.

标签: haskell binary-tree transformation traversal


【解决方案1】:

这里的标准做法是:

  1. 如果根不为零,我们会递归地将算法应用于其每个子分支,然后我们就完成了(正如您在代码中已经指出的那样)
  2. 如果只有一个孩子的根为零,我们就返回那个孩子(当然是经过转换的)
  3. 如果根为零且有两个子分支,我们会递归地将算法应用于两个分支中的每一个,然后从新的左子分支中提取最右边的值并将其放入根中。

从树中提取最右边的值:

  1. 如果是叶子,只需移除叶子
  2. 如果它是一个节点,它没有右孩子,所以把它的左孩子放在它的位置上

整个事情是线性的,(*) 显然。

modTree :: Tree -> Tree
modTree Void = Void
modTree (Node l x r)
    | x == 0 = modZero (modTree l)   (modTree r)
    | otherwise = Node (modTree l) x (modTree r)

modZero Void r = r
modZero l Void = l
modZero l r = let (v, nl) = pullRightmost l in
  Node nl v r

实现pullRightmost 留作练习。


(*)没有脊椎被拉过不止一次,因为我们是从已经没有任何0s的树上拉的,“之后”递归调用。所以这些总共将是另一个 O(n) 操作。

【讨论】:

  • 不错!这甚至可以通过将右脊椎的每个元素提升一层而不是将右脊椎的最后一个元素提升许多层来变得非常懒惰。
  • 是的,我知道他们说顺序无关紧要,但尽可能保留它还是很好的,我想...旧习惯,所有这些 cra爵士乐。好主意。
  • 啊,非常懒惰的版本也有一个非常好的线性时间执行证明。与其考虑提升,不如继续考虑将零点向下冒泡。该算法等效于,对于每个零,向左冒泡一次,然后向右向下冒泡直到一片叶子。没有两个零在同一条边上冒泡。有线性数量的边,所以线性数量的右气泡;和线性数量的零,所以线性数量的左气泡。完成!
  • 所以,回顾一下:因为从来没有从两次拉出脊椎,它都是线性的。既适用于我的版本,也适用于您对它的改进。 (因为拉动发生在递归调用的结果上,其中不再有0s)。至于懒惰,你的“懒惰”方式在结果树的前序遍历下可能更懒惰,但我的方式实际上可能在有序遍历下更懒惰。我想。
  • 啊,关于遍历顺序的好点子!
【解决方案2】:

一个想法是将0s 向下和向左冒泡,直到他们没有左孩子,然后用他们的右孩子替换它们。例如:

    0          1          1          1
   / \        / \        / \        / \
  1   2  ->  0   2  ->  3   2  ->  3   2
 / \        / \        / \          \
3   4      3   4      0   4          4

您需要注意当从根到叶的路径上有多个0s 时会发生什么。

    0
   / \
  0   2
 / \
3   4

可能最简单(尽管可能不是最有效)的处理方法是在开始冒泡之前递归地从左孩子中删除0s。

    0          0          0          3          3
   / \        / \        / \        / \        / \
  0   2  ->  3   2  ->  3   2  ->  0   2  ->  4   2
 / \        / \          \          \
3   4      0   4          4          4

我鼓励您自己尝试实现这一点。如果您遇到问题,请描述您的尝试、您遇到的问题以及您认为您遇到的问题无法克服的原因,这将是一个很好的后续问题。

【讨论】:

  • 挑战题:这个算法的时间复杂度是多少?显然是 O(n^2);它也是O(n)吗?我试图创建一个病态输入,以几种不同的方式实现 n^2 工作,它们实际上都只需要线性工作——但这​​可能只是意味着我的想象力失败了!我看不出如何证明它总是线性的。实现 n^2 的问题似乎是你想要有很多 0s,但是如果你有很多 0s,那么你的递归调用会产生一棵非常小的树并且冒泡不会不需要太多的工作。
  • ...啊,明白了。具有交替 0s 和 1s 的仅左脊柱树需要 θ(n^2) 工作。
  • 我喜欢这种方法,但我仍然遇到如何递归遍历树的问题。在其他语言中,它将是:postorder(Node) { if (Node != null) { postorder(left_child) postorder(right_child) visit Node } } 但我不明白这在 Haskell 中是如何工作的
  • @AwhatLoop 在 Haskell 中,你写 postorder Void = ...; postorder (Node left_child node right_child) = visit node (postorder left_child) (postorder right_child)。实际上并没有太大的不同,尽管当评估顺序更加流畅时,“postorder”会变得有点陌生。 (它已经很奇怪了,因为它根本没有提到 visit 的意图;现在它更奇怪了。)
  • 哦,我刚刚拿了一个教科书的 postorder 示例。 "visit" 只是解释节点动作的一行。
猜你喜欢
  • 1970-01-01
  • 2021-05-04
  • 2012-01-01
  • 2022-11-11
  • 1970-01-01
  • 2021-03-08
  • 2020-01-15
  • 1970-01-01
相关资源
最近更新 更多