累加器,联合和递归

时间:2012-05-19 14:54:50

标签: recursion clojure accumulator cons

我已经从4clojure.com解决了45个问题,我注意到我尝试使用递归和累加器解决一些问题的方式中反复出现问题。

我会尽力解释我能做的最好的事情,最终会找到一些非常好的解决方案,希望有些Clojurers会“得到”我不会得到的东西。

例如,问题34要求编写一个函数(不使用 range ),将两个整数作为参数并创建一个范围(不使用范围)。简单地说你做(... 1 7)然后你得到(1 2 3 4 5 6)。

现在这个问题不是解决这个特殊问题。

如果我希望使用递归和累加器来解决这个问题怎么办?

我的思维过程是这样的:

  • 我需要编写一个带有两个参数的函数,我从(fn [x y])开始

  • 我需要递归,我需要跟踪一个列表,我会使用一个累加器,所以我在第一个函数中写了第二个函数,并附加了一个参数:

    (FN  [x y]
     ((fn g [x y acc] ...)   X   ÿ   “())

(显然我无法在SO上正确格式化Clojure代码!?)

在这里我已经不确定我是否正确地做了:第一个函数必须正好接受两个整数参数(不是我的调用)而且我不确定:如果我想使用一个累加器,我可以使用累加器而不创建嵌套函数吗?

然后我想 conj ,但我不能这样做:

(conj 0 1)

所以我做了一些奇怪的事情,以确保我先得到一个序列,然后我最终得到这个:

(fn
   [x y]
   ((fn g [x y acc] (if (= x y) y (conj (conj acc (g (inc x) y acc)) x)))
    x
    y
    '()))

然后这产生了这个:

(1 (2 (3 4)))

而不是:

(1 2 3 4)

所以我最终做了一个额外的展平并且它有效,但它完全是丑陋的。

我开始理解一些事情,在某些情况下,我甚至开始以更多的方式“思考”,但我在编写解决方案时遇到了问题。

例如我决定:

  • 使用累加器
  • 通过递增 x 进行递归,直到达到 y

但我最终得到了上面的怪物。

有一个很多解决这个问题的方法,而且,这不是我想要的。

我所追求的是,​​在我决定使用累积器后,如何使用累加器并递归,我最终会得到这个(不是由我写的):

#(loop [i %1
        acc nil]
  (if (<= %2 i)
    (reverse acc)
    (recur (inc i) (cons i acc))))

而不是:

((fn
  f
  [x y]
  (flatten
   ((fn
     g
     [x y acc]
     (if (= x y) acc (conj (conj acc (g (inc x) y acc)) x)))
    x
    y
    '())))
 1
 4)

我认为这是一个能够解决一些问题的开始,但我对我倾向于产生的丑陋解决方案感到有点失望......

5 个答案:

答案 0 :(得分:9)

我认为这里有几点需要学习。

第一个,一种通用规则 - 递归函数通常具有自然顺序,并且添加累加器会反转它。你可以看到,因为当“正常”(没有累加器)递归函数运行时,它会做一些计算值的工作,然后递归生成列表的尾部,最后以空列表结束。相反,使用累加器,你从空列表开始,然后在前面添加东西 - 它正朝着另一个方向增长

通常情况下,当您添加累加器时,您会得到相反的顺序。

现在经常这没关系。例如,如果您生成的不是序列,而是生成重复应用可交换运算符的值(如加法或乘法)。然后你会得到同样的答案。

但在你的情况下,这将是重要的。你会向后推倒列表:

(defn my-range-0 [lo hi] ; normal recursive solution
  (if (= lo hi)
    nil
    (cons lo (my-range-0 (inc lo) hi))))

(deftest test-my-range-1
  (is (= '(0 1 2) (my-range-0 0 3))))

(defn my-range-1 ; with an accumulator
  ([lo hi] (my-range-1 lo hi nil))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (recur (inc lo) hi (cons lo acc)))))

(deftest test-my-range-1
  (is (= '(2 1 0) (my-range-1 0 3)))) ; oops!  backwards!

并且通常你能做的最好的事情就是在结束时反对该列表。

但是这里有另一种选择 - 我们实际上可以向后工作。而不是递增下限,你可以递减上限:

(defn my-range-2
  ([lo hi] (my-range-2 lo hi nil))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (let [hi (dec hi)]
        (recur lo hi (cons hi acc))))))

(deftest test-my-range-2
  (is (= '(0 1 2) (my-range-2 0 3)))) ; back to the original order

[注意 - 还有另一种方法可以扭转下面的事情;我没有很好地构建我的论点]

second ,正如您在my-range-1my-range-2中看到的,使用累加器编写函数的一种好方法是使用两组不同的参数。它为您提供了一个非常干净(imho)的实现,而无需嵌套函数。


你还有一些关于序列的更一般的问题,conj等。这里的clojure有点凌乱,但也很有用。上面我一直在给出一个非常传统的观点与利弊列表。但是,clojure鼓励你使用其他序列。与cons列表不同,向量增长到右边,而不是左边。所以另一种反转该结果的方法是使用vector:

(defn my-range-3 ; this looks like my-range-1
  ([lo hi] (my-range-3 lo hi []))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (recur (inc lo) hi (conj acc lo)))))

(deftest test-my-range-3 ; except that it works right!
  (is (= [0 1 2] (my-range-3 0 3))))

此处conj正在添加到右侧。我没有在conj中使用my-range-1,所以在这里重写它会更清楚:

(defn my-range-4 ; my-range-1 written using conj instead of cons
  ([lo hi] (my-range-4 lo hi nil))
  ([lo hi acc]
    (if (= lo hi)
      acc
      (recur (inc lo) hi (conj acc lo)))))

(deftest test-my-range-4
  (is (= '(2 1 0) (my-range-4 0 3))))

请注意,此代码看起来非常类似于my-range-3,但结果是向后的,因为我们从空列表开始,而不是空向量。在这两种情况下,conj都会在“自然”位置添加新元素。对于向右的向量,但对于列表,它在左侧。

我刚想到你可能不太了解列表是什么。基本上cons创建一个包含两个东西(它的参数)的框。第一个是内容,第二个是列表的其余部分。所以列表(1 2 3)基本上是(cons 1 (cons 2 (cons 3 nil)))。相反,向量[1 2 3]更像是一个数组(虽然我认为它是用树实现的)。

所以conj有点令人困惑,因为它的工作方式取决于第一个参数。对于列表,它会调用cons,因此会向左侧添加内容。但是对于一个向量,它将数组(类似的东西)扩展到右边。另外,请注意conj将现有序列作为第一个arg,将第二个作为第二个添加,而cons则相反(添加的内容首先出现)。


https://github.com/andrewcooke/clojure-lab

上提供的所有上述代码

更新:我重写了测试,以便在代码生成列表的情况下,预期结果是引用列表。 =将比较列表和向量,如果内容相同则返回true,但明确表示您在每种情况下实际获得的内容。请注意,前面有'(0 1 2)的{​​{1}}就像'一样 - (list 0 1 2)会阻止评估列表(如果没有它,'将被视为一个命令)。

答案 1 :(得分:4)

阅读完所有内容后,我仍然不确定你为什么需要累加器。

((fn r [a b]
    (if (<= a b) 
       (cons a (r (inc a) b)))) 
  2 4)
=> (2 3 4)

似乎是一个非常直观的递归解决方案。我在“真实”代码中唯一改变的是使用lazy-seq,这样你就不会在大范围内用完堆栈。

我是如何达到这个解决方案的:

当你考虑使用递归时,我发现尝试用你能想到的最少可能的术语来解决问题是有帮助的,并尝试将递归本身的“工作”移交给它。

特别是,如果您怀疑可以删除一个或多个参数/变量,那通常就是这样 - 至少如果您希望代码易于理解和调试;有时你最终会牺牲简单性来支持执行速度或减少内存使用。

在这种情况下,我开始写作时的想法是:“函数的第一个参数也是范围的起始元素,最后一个参数是最后一个元素”。递归思维是你需要训练自己做的事情,但是一个相当明显的解决方案就是说:范围 [a, b]是一个以元素a开头的序列< em>后跟 范围 [a + 1, b]。因此,确实可以递归地描述范围。我写的代码几乎就是这个想法的直接实现。

<强>附录

我发现在编写功能代码时,最好避免使用累加器(和索引)。有些问题需要它们,但是如果你能找到摆脱它们的方法,那么你通常会更好。

附录2:

关于递归函数和列表/序列, 在编写那种代码时最有用的思考方式是用“列表的第一项(头)”来表示你的问题, “列表的其余部分(尾部)”。

答案 2 :(得分:3)

如果我使用累加器解决了这个问题,我会做类似的事情:

user=> (defn my-range [lb up c]
         (if (= lb up)
           c
           (recur (inc lb) up (conj c lb))))
#'user/my-range

然后用

调用它
#(my-range % %2 [])

当然,我会使用letfn或其他东西来解决没有defn的问题。

所以是的,你确实需要一个内部函数来使用累加器方法。

我的思维过程是,一旦我完成了我想要返回的答案将在累加器中。 (这与你的解决方案形成鲜明对比,在那里你做了大量的工作来找到结束条件。)所以我寻找我的结束条件,如果我已经达到它,我会返回累加器。否则,我将下一个项目添加到累加器并重复一个较小的情况。因此,只有两件事要弄清楚,最终条件是什么,以及我想把它放在累加器中。

使用向量会有很大帮助,因为conj会附加到它,而且不需要使用reverse

I'm on 4clojure too,顺便说一下。我一直很忙,所以我最近落后了。

答案 3 :(得分:3)

我无法添加你收到的已经很好的答案,但我会回答一般情况。当您完成Clojure学习过程时,您可能会发现许多但不是所有解决方案都可以使用Clojure内置函数来解决,例如map和思考序列方面的问题。这并不意味着你不应该递归地解决问题,但你会听到 - 而且我认为这是明智的建议 - Clojure递归是为了解决你无法用另一种方式解决的非常低级别的问题。

我碰巧做了很多.csv文件处理,并且最近收到了一条关于nth创建依赖关系的评论。它确实如此,使用地图可以让我得到元素,通过名称而不是位置进行比较。

我不打算在已经在生产中的两个小应用程序中抛弃使用nth和clojure-csv解析数据的代码。但是下次我会以更顺序的方式思考问题。

很难从书上谈论矢量和nth,循环...复发等等,然后实现学习Clojure从那里成长你。

我发现的其中一件事就是学习Clojure,社区是尊重和乐于助人的。毕竟,他们帮助第一个学习语言是Fortland IV的人在CDC网络上使用穿孔卡,并且他的第一个商业编程语言是PL / I.

答案 4 :(得分:1)

看起来你的问题更多的是关于“如何学习”然后是技术/代码问题。你最终编写了这样的代码,因为你从一般的学习编程或特定的Clojure以任何方式或来源创建了一个大脑中的“神经高速公路”,这使你以这种特殊的方式思考解决方案并最终编写代码像这样。基本上每当你遇到任何问题时(在这种特殊情况下递归和/或积累),你最终都会使用那个“神经高速公路”并且总是想出那种代码。

摆脱这种“神经高速公路”的解决方案是暂时停止编写代码,远离键盘并开始阅读大量现有的clojure代码(从4clojure问题的现有解决方案到github上的开源项目)并深入思考(甚至阅读一个功能2-3次,真正让它在你的大脑中安定下来)。通过这种方式,您最终会破坏现有的“神经高速公路”(产生您现在编写的代码)并创建一个新的“神经高速公路”,从而产生美丽而惯用的Clojure代码。另外,一旦看到问题,尽量不要跳到键入代码,而是给自己一些时间来清楚深入地思考问题和解决方案。