clojure 新手指南(16):基本迭代&递归

迭代和递归是两种不同的概念,但是它们彼此之间又有点相似。迭代是遍历一组元素,并在遍历的过程中对每一个元素做相应的操作,递归则是执行一个自己调用自己的操作。

从递归和迭代的概念上来看,这完全是两种完全不同的东西,那么它们的相似性又体现在什么地方呢?首先递归也可以作为一种遍历集合元素的方法,Clojure就有递归方式的迭代器。本章就是揭示clojure中迭代和递归的工作方式和使用它们的好处。

使用doseq进行迭代

首先让我们看一个示例问题,我们最终需要使用迭代来解决这个问题。我们的这个示例问题被称为FizzBuzz难题:

写一个程序打印1到100这些数字。但是遇到数字为3的倍数的时候,打印“Fizz”替代数字,5的倍数用“Buzz”代替,既是3的倍数又是5的倍数打印“FizzBuzz

让我们开始解决吧!

从题目中分析知道,首先我们要确定1到100这些数字中可以整除3、整除5、既能整除3又能整除5的数字,所以至少需要一个判断整除的函数,我们不妨称之为multiple?(以问号结尾的函数名一般都返回布尔值)。我们可以利用clojure的内置取余函数mod来创建我们的multiple?函数。

=>(defn multiple? [n div]
    ;; n 除以 div的余数是否等于0 
   (= 0 (mod n div)))
#'user/multiple

;; 3能被3整除,返回true
=>(multiple? 3 3)
true

;;判断4能被3整除,返回false
=>(multiple? 4 3)
false

;;判断5能被3整除,返回false
=>(multiple? 5 3)
false

;;判断6能被3整除,返回true
=>(multiple? 6 3)
true

现在我们已经有了一个判断整除的函数multiple?,可以开始着手处理FizzBuzz问题具体的处理了。在Clojure世界中,有多种方式可以遍历元素。下面,我们将会使用'doseq'( 宏标签) 来做迭代操作。它会遍历序列中的元素,并在遍历过程中做相应的处理。我们给doseq的第一个参数应该是一个向量(vector),这个向量里包含一个绑定当前元素的变量名(我们下面使用字母 i)和被遍历的序列。doseq的第二个参数是一个操作表达式(s表达式),遍历过程中将对每一个元素做处理。

先看一下一个简单的例子:

;;打印0到9的数字

user=> (doseq [i (range 0 10)] 
              (println i))
0
1
2
3
4
5
6
7
8
9
nil


再来看看嵌套迭代(类似嵌套for循环)

user=> (doseq [ x [1 2 3]
                y [1 2 3]]
              (println (* x y)))

1
2
3
2
4
6
3
6
9
nil

上面代码和下面java代码基本等价(所有的clojure表达式都是有返回值的,上面代码中最后的nil就是返回值):

int [] array = {1, 2, 3};
for(int i : array){
  for(int j : array){
     System.out.println( i * j );
  }
}
doseq介绍到此结束,我们来看如何使用doseq来解决我们的FizzBuzz问题:
=>(doseq [i (range 1 101)]  ;;遍历1到100
     ;;首先判断是否能同时被5和3整除
     (cond (and (multiple? i 3)(multiple? i 5))
             (println "FizzBuzz")
           ;;如果上面不满足则判断是否能被3整除
           (multiple? i 3)
             (println "Fizz")
            ;;如果上面不满足则判断能否被5整除
           (multiple? i 5)
             (println "Buzz")
        ;;否则直接打印数字值
       :else (println i)))
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
省略.......
强调一点:cond 的使用和if else非常像,cond按照从上到下的顺序依次判断表达式的真值,如果条件表达式真值为true,返回该条件表达式对应的执行表达式的值,然后此次判断结束,否则会执行下一条判断语句,直至最终执行到else语句。

;;cond 形式如下

(cond (条件表达式1) (执行表达式1)
      (条件表达式2) (执行表达式2)
               ......
       :else (执行表达式n)
) ;;cond 结束

使用for进行迭代

for循环是另一种迭代的方式,但是接下来你会发现使用for循环不适合解决FizzBuzz问题。for循环的语法和doseq是一样的,只不过for 返回lazy seq(类似python 中的yield)而doseq是side effect。这么说有点抽象,还是用例子来说明吧:

;; 我们原想返回0-10中所有的偶数,但是得到的结果是nil
user=> (doseq [x (range 0 11) :when (even? x)] x)
nil 

;; 使用doseq只能返回nil,不够我们可以在遍历期间做其他事情。比如 打印
user=> (doseq [x (range 0 10) :when (even? x)] (print x ","))
0 ,2 ,4 ,6 ,8 ,nil  ;; (nil 是整个式子的返回值,不要搞混了)

;;我们使用for来获取0-10中所有的偶数
user=> (for [x (range 0 10) :when (even? x)] x)
(0 2 4 6 8) 

 可以这么说,使用doseq就向java中的for循环,只能在循环过程中做些什么事情,而clojure中的for循环可以在每次的遍历中向外输出值,最终由这些值组成一个序列。

再用个例子体会一下

user=> (for [x [0 1 2 3 4 5]
             :let [y (* x 3)]
             :when (even? y)]
         y)
(0 6 12)  ;;我们得到的结果

 for循环不适合解决FizzBuzz问题的原因就在于,FizzBuzz只是在遍历过程中需要打印出对应的值,而不需要每次都返回结果。有兴趣你可以把解决FizzBuzz代码中的doseq换成for来看看输出效果就明白了。


使用loop进行递归

loop 在许多语言中都有这个关键字,基本上都是为了更好的使用迭代器而存在。但是在Clojure中,loop实际上是递归的,所以使用它需要更多一点的相关知识和代码。

先看一下如何使用loop 来解决 FizzBuzz问题,体会一下

(loop [data (range 1 101)]
  (if (not (empty? data))
      (let [n (first data)]
        (cond (and (multiple? n 3)(multiple? n 5))
                     (println "FizzBuzz")
                   (multiple? n 3)
                     (println "Fizz")
                   (multiple? n 5)
                     (println "Buzz")
                 :else (println n))
             (recur (rest data)))))
首先cond里面的逻辑和之前doseq的一模一样,这个是不变的。我们知道递归必须有一个结束条件,所以我们在这里在递归开始加入了一个判断语句(if (not (empty? data)) ,就是判断data是否为空列表,如果为空递归结束,否则继续进行。每次递归,我们都从列表中取出一个值,然后把它传递给cond那部分逻辑进行判断。cond逻辑结束后,为了能递归调用上面逻辑,我们使用recur来达到目的。上例中,我们每次都将使用本次递归中的列表除第一个元素以外的剩下列表进行下一次递归。(递归必须是一个收敛的过程,否则递归将永远无法结束)

我们使用loop来打印0-11的偶数,对比之前的例子。主要体会如何使用递归思想来解决问题

user=> (loop [x 0]
(when (<= x 10)   ;;判断递归是否结束的语句
 (if (even? x) (println x))
 (recur (+ x 1))))  ;;使用recur 向判断结束方向收敛
(建议大家可以看看《the little schemer》,看完肯定能更好的掌握递归思想,并且对学习clojure大有好处)

现在我们再来个稍微难点的例子,我们会递归迭代一组数字,然后搜集遍历过程中得到的前十个偶数。注意这个例子和前面不同的是,我们每次递归(recur)传入的参数是多个,而不是一个。recur后面参数其实是和loop的第一个向量参数中的绑定参数(data、n、n-count、result)是一一对应的,大家仔细观察一下。

(loop [data   (range 1 101)
       n      (first data)
       n-count   0
       result  nil]  ;; result 初始为空列表
   (if (and n (< n-count 10))   ;;递归结束条件
       (if (even? n)
           (recur (rest data) (first data) (inc n-count) (cons n result))
           (recur (rest data) (first data) n-count result))
       (reverse result))) ;;递归结束后,反转结果列表

我们可以做的更好一点,就是把上面定义成一个递归函数:

=>(defn take-evens                       ;;我们定义的递归函数
    ([x nums](take-evens x nums 0 nil))  ;;参数模式一
    ([x nums n-count result]             ;;参数模式二
      (if (empty? nums)                  ;;递归结束条件一
          (reverse result)     
          (if (< n-count x)              ;;递归结束条件二 
              (let [n (first nums)]
                (if (even? n)
                    (recur x (rest nums) (inc n-count) (cons n result))
                    (recur x (rest nums) n-count result)))
              (reverse result)))))
#'user/take-evens

;;取出1到100中前十个偶数
=>(take-evens 10 (range 1 101))
(2 4 6 8 10 12 14 16 18 20)
;;取出1到100宗前5个偶数
=>(take-evens 5 (range 1 101))
(2 4 6 8 10)





你可能感兴趣的:(递归,迭代,lisp,clojure)