Using recursion

from ANSI Common lisp p-114

Recursion plays a greater role in Lisp than in most other languages. There seem to be three main reasons why:

  1. Functional programming. Recursive algorithms are less likely to involve side-effects.

  2. Recursive data structures. Lisp's implicit use of pointers makes it easy to have recursively defined data structures. The most common is the list: a list is either nil , or a cons whose cdr is a list.

  3. Elegance. Lisp programmers care a great deal about the beauty of their programs, and recursive algorithms are often more elegant than their iterative counterparts.

Students sometimes find recursion difficult to understand at first. But as Section 3.9 pointed out, you don't have to think about all the invocations of a recursive function if you want to judge whether or not is correct.

  The same is true if you want to write a recursive function. If you can describe a recursive solution to a problem, it's usually straightforward to translate your solution into code. To solve a problem using recursion, you have to do two things:

  1. You have to show how to solve the problem in the general case by breaking it down into a finite number of similar, but smaller, problems.

  2. You have to show how to solve the smallest version of the problem—the base case—by some finite number of operations.

If you can do this, you're done. You know that a finite problem will get solved eventually, because each recursion makes it smaller, and the smallest problem takes a finite number of steps.

  For example, in the following recursive algorithm for finding the length of a proper list, we find the length of a smaller list on each recursion:

  1. In the general case, the length of a proper list is the length of its cdr plus 1.

  2. The length of an empty list is 0.

When this description is translated into code, the base case has to come first; but when formulating recursive algorithms, one usually begins with the general case.

  The preceding algorithm is explicitly described as a way of finding the length of a proper list. When you define a recursive function, you have to be sure that the way you break up the problem does in fact lead to smaller subproblems. Taking the cdr of a proper list yields a smaller subproblem for length, but-taking the cdr of a circular list would not.

  Here are two more examples of recursive algorithms. Again, both assume finite arguments. Notice in the second that we break the problem into two smaller problems on each recursion:

member Something is a member of a list if it is the first element, or a member of the cdr. Nothing is a member of the empty list.

copy-tree The copy-tree of a cons is a cons made of the copy-tree of its car, and the copy-tree of its cdr. The copy-tree of an atom is itself.

Once you can describe an algorithm this way, it is a short step to writing a

recursive definition.

  Some algorithms are most naturally expressed in such terms and some are not. You would have to bend over backwards to define our-copy-tree (page 41) without using recursion. On the other hand, the iterative version of show-squares on page 23 is probably easier to understand than the recursive version on page 24. Sometimes it may not be obvious which form will be more natural until you try to write the code.

  If you're concerned with efficiency, there are two more issues to consider.One, tail-recursion, will be discussed in Section 13.2. With a good compiler there should be little or no difference in speed between a tail-recursive function and a loop. However, if you would have to go out of your way to make a function tail-recursive, it may be better just to use iteration.

  The other issue to bear in mind is that the obvious recursive algorithm is not always the most efficient. The classic example is the Fibonacci function. It is defined recursively,

1. Fib(0) = Fib(l)=l.

2. Fib(n) = Fib(n-1) + Fib(rc-2).

but the literal translation of this definition,

CL-USER> (defun fib (n)
	   (if (<= n 1)
	       1
	       (+ (fib (- n 1))
		  ( f ib (- n 2 ) ) ) ))
is appallingly inefficient. The same computations are done over and over. If you ask for ( f ib 10), the function computes ( f ib 9) and ( f ib 8). But to compute ( f ib 9), it has to compute ( f ib 8) again, and so on. Here is an iterative function that computes the same result:

CL-USER> (defun fib (n)
	   (do ((in (- i 1))
		 (fl 1 (+ fl f2))
		 (f2 1 fl))
	       (<= i 1) f1 ) ))

The iterative version is not as clear, but it is far more efficient. How often does this kind of thing happen in practice? Very rarely—that's why all textbooks use the same example—but it is something one should be aware of.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值