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

我将尽我所能尽最大的努力来解释我所做的一切,以期最终得出一些棘手的解决方案,希望某些Clojurers能够“得到”我没有得到的。

例如,问题34要求编写一个以两个整数作为参数的函数(不使用范围),并创建一个范围(不使用范围)。只需简单地做(... 1 7),您就会得到(1 2 3 4 5 6)。

现在,这个问题与解决这个特定问题无关。

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

我的思考过程如下:


我需要编写一个带有两个参数的函数,我以(fn [x y])开头
我需要递归,并且需要跟踪列表,我将使用累加器,因此我在第一个函数内编写了第二个函数,并附加了一个参数:

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


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

在这里,我不确定自己是否正确:第一个函数必须正好使用两个整数参数(而不是我的调用),而且我不确定:如果我要使用累加器,是否可以不创建累加器就使用累加器?嵌套函数?

然后我想让我参加,但我不能做:

(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进行递归


但是我最后还是上面的怪兽。

有很多方法可以解决此问题,再次,这不是我所追求的。

我要做的是,在决定进行cons / conj,使用累加器并递归之后,我可以得到以下结果(不是我写的):

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


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

最佳答案

我认为这里有几件事要学习。

首先,一种通用规则-递归函数通常具有自然顺序,而添加累加器则相反。您会看到,因为当运行“正常”(无累加器)递归函数时,它会做一些工作来计算值,然后递归以生成列表的尾部,最后以空列表结尾。相比之下,使用累加器时,您将从空列表开始,然后将内容添加到最前面-它朝着另一个方向增长。

因此通常在添加累加器时会得到相反的顺序。

现在通常这无关紧要。例如,如果您生成的不是序列,而是一个交换运算符(如加法或乘法)的重复应用的值。那么无论哪种方式您都会得到相同的答案。

但就您而言,这很重要。您将把列表倒退:

(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


[注意-下面有另一种方法可以逆转;我的论点不够好]

第二,正如您在my-range-1my-range-2中所看到的那样,使用累加器编写函数的一种好方法是将函数与两个不同的参数集一起使用。无需嵌套函数,即可实现非常干净的(imho)实现。



您还会对序列,conj等有一些更一般的问题。 Clojure在这里有点杂乱,但也很有用。以上,我一直在使用基于缺点的列表给出非常传统的观点。但是clojure会鼓励您使用其他序列。与cons列表不同,向量在右侧而不是左侧增长。因此,反转结果的另一种方法是使用向量:

(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,添加的东西作为第二个arg,而cons相反(添加的东西排在第一位)。



以上所有代码可从https://github.com/andrewcooke/clojure-lab获得



更新:我重写了测试,以便在代码生成列表的情况下,预期结果是带引号的列表。 =将比较列表和向量,如果内容相同,则返回true,但显式显示将更清楚地显示每种情况下的实际结果。请注意,前面带有'(0 1 2)'就像(list 0 1 2)-'阻止对列表进行求值(如果没有该列表,则将0视为命令)。

10-08 12:45