递归集合并集:它是如何工作的?
Posted
技术标签:
【中文标题】递归集合并集:它是如何工作的?【英文标题】:Recursive set union: how does it work really? 【发布时间】:2013-04-19 11:57:38 【问题描述】:我目前正在下班后的空闲时间参加 Coursera 上的 Scala 课程,以尝试最终尝试函数式编程。我目前正在处理一项任务,我们应该“计算”包含某个对象的两个集合的并集。我故意省略了细节,因为这对我在这里要问的内容并不重要。然而,相关的是集合被定义为二叉树,每个节点包含一个元素和两个子树。
既然如此;讲座中union
的例子如下:
def union(other:BTSet) :BTSet = ((left union right) union other) incl element
问题1:坦率地说,即使在阅读了相关的常见问题解答和其他论坛帖子之后,我仍然不明白这个功能是如何以及为什么起作用的。除了在头节点添加(incl
调用)元素之外,在联合实现中绝对没有执行任何“动作”,它只是一遍又一遍地调用自己。我会非常感谢一些解释...
问题 2: 课程论坛包含许多帖子,指出此解决方案根本没有效率,而且还不够好。看到我不明白它是如何工作的,我真的不明白为什么它不够好。
请注意,我不会以任何方式要求对分配解决方案进行剧透。我非常愿意“为成绩做作业”,但我根本不明白我应该在这里做什么。我不相信课程中提供的说明和指导足以让您了解函数式编程的怪癖,因此我欢迎任何解决如何正确思考而不是的 cmets/answers >如何正确编码。
【问题讨论】:
也许您缺少的空集有不同的实现? 所以试着一步一步画出发生了什么!假装别人里面有一个东西,而你调用的那个东西只有一个左、右和元素,但左右本身没有孩子。这应该足以得到图片。 当你递归下去时,你最终会得到一个空节点作为子节点,这将调用以不同方式实现的联合。 基本上“this”集合正在被拆除,其元素被一一添加到“other”集合中。 2016 coursera 的讨论也很有启发性:coursera.org/learn/progfun1/discussions/weeks/3/threads/… 【参考方案1】: A
/ \ union D
B C
((B union C) union D) incl A
^^^^^^^^^......................................assume it works
( B )
( \ union D ) incl A
( C )
(((0 union C) union D) incl B) incl A
^^^^^^^^^.....................................just C
(((C union D) incl B) incl A
^^^^^^^^^.....................................expand
((((0 union 0) union D) incl C) incl B) incl A
^^^^^^^^^....................................just 0
(((0 union D) incl C) incl B) incl A
^^^^^^^^^.....................................just D
((D incl C) incl B) incl A
^^^^^^^^^^^^^^^^^^^^^^^^^^.......................all incl now
一步一步写出来。现在您看到 union 简化为一组应用于右侧参数的 incl 语句。
【讨论】:
我刚想在这里发表评论,所以在纸上或多或少做了同样的事情之后;我有点明白你的意思。虽然它显示为递归弹出左孩子,并将其添加到右手。所以我想第二步应该是相反的(C 有 B 作为左孩子),我弄错了吗? @posdef - 这并不重要,这就是我没有问的原因,但是是的,如果B union C
产生C
作为根而不是B
,你应该翻转那些。取决于C incl B
做了什么,这就是归结为。 (看看C union D
变成D incl C
的地方。)【参考方案2】:
我认为incl
将一个元素插入到现有集合中?如果是这样,那就是所有实际工作发生的地方。
并集的定义是包含任一输入集中所有内容的集合。给定存储为二叉树的两个集合,如果将第一个集合的并集与第二个集合的分支相结合,则结果中唯一可能丢失的元素是第二个树的根节点处的元素,所以如果你插入那个元素你有两个输入集的联合。
这只是将两个集合中的每个元素插入到一个开始为空的新集合中的一种非常低效的方法。推测重复被incl
丢弃,所以结果是两个输入的并集。
也许暂时忽略树状结构会有所帮助;它对基本算法并不重要。假设我们有抽象的数学集合。给定一个包含未知元素的输入集,我们可以做两件事:
向其中添加一个元素(如果该元素已经存在,则不执行任何操作) 检查集合是否非空,如果是,则将其分解为单个元素和两个不相交的子集。为了取两个集合 1,2 和 2,3 的并集,我们首先将第一个集合分解为元素 1 和子集 和 2。我们使用相同的过程递归地取 、2 和 2,3 的并集,然后将 1 插入结果中。
在每一步,问题从一个联合运算减少到两个较小输入的联合运算;一个标准的分治算法。当达到单例集 x 和空集 的联合时,联合是平凡的 x,然后返回到链上。
树结构仅用于将案例分析/分解为更小的集合,并使插入更有效。使用其他数据结构也可以做到这一点,例如将列表分成两半以进行分解,并通过详尽的唯一性检查完成插入。要有效地进行联合,需要一种更聪明的算法,并利用用于存储元素的结构。
【讨论】:
听起来不屑一顾或消极,但我完全了解两组联合的含义。我只是不明白第一组的元素是如何神奇地与第二组的元素(加上根)组合在一起的 @posdef:因为在每一点,它都可以将输入转换为多个较小的树和单个元素。较小的树的并集是递归完成的,然后插入单个元素。当较小的树为空时,递归的“较小的树”过程停止。所有真正的逻辑都在这里插入。 为了记录,我根据时间接受了 Rex Kerr 的回答。我同样感谢编辑中的解释。【参考方案3】:因此,基于以上所有回复,我认为真正的主力是incl
,而调用union
的递归方式仅用于遍历集合中的所有元素。
我想出了以下联合的实现,这样更好吗?
def union(other:BTSet) :BTSet = right union (left union (other incl element))
【讨论】:
这个确实有效,上面的括号设置不同,它实际上不会终止“大”集。我不太明白为什么 我相信最初的实现,即((left union right) union other) incl element
在指数时间内运行。想想每次调用union
时(left union right)
做了多少冗余工作。【参考方案4】:
2
/ \ union 4
1 3
((1 union 3) union 4) incl 2
^^^^^^^^^......................................assume it works
(((E union E) union 3 incl 1) union 4) incl 2
^^^^^^^^^.....................................still E
(E union E) union 3 incl 1 = E union 3 incl 1 = 3 incl 1
下面的子树应该是3 包括 1
( 3 )
( \ union D ) incl 2
( 1 )
(((1 union E) union 4) incl 3) incl 2
^^^^^^^^^.......................................expand
(((( (E union E) union E) incl 1) union 4) incl 3) incl 2
^^^^^^^^^^^^^^^^^^^^^^^^^^..................still 1
((1 union 4) incl 3) incl 2
^^^^^^^^......................................continue
((((E union E) union 4) incl 1) incl 3) incl 2
^^^^^^^^^^^^^^^^^^^^^^^^^^^^..........expand 1 union 4
((4 incl 1) incl 3) incl 2
^^^^^^^^^^^^^^^^^^^^^^^^^............Final union result
感谢@Rex Kerr 列出了步骤。我将第二步替换为实际的运行时步骤,这样可以更清楚地描述 Scala union
函数。
【讨论】:
【参考方案5】:除非您查看基本情况,否则您无法理解递归算法。事实上,很多时候,理解的关键在于首先理解基本情况。由于未显示基本情况(可能是因为您一开始没有注意到有一个),因此无法理解。
【讨论】:
【参考方案6】:我在做同样的课程,上面的union
实现确实效率极低。
我想出了以下功能不太强大的解决方案来创建二叉树集合的联合,这样效率更高:
def union(that: BTSet): BTSet =
var result:BTSet = this
that.foreach(element => result = result.incl(element))
result
【讨论】:
迭代上的突变不仅仅是功能不全。以上是关于递归集合并集:它是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章