一、递归
定义:程序调用本身的编程技巧称为递归( recursion)。
特点:一个过程或函数在其定义或说明中又间接或间接调用本身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题类似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。用递归思想写出的程序往往十分简洁易懂。
一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
注意: 1) 递归就是在过程或函数里调用本身; 2) 在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
规则:1.基准情形。必须总要有某些基准情形,它无需递归就能得出;
2.不断推进。对于需要递归求解的情形,每一次递归调用都必须要使状况朝向一张基准情形推进;
3.设计法则。假设所有的递归调用都能运行。
4.合成效益法则。求解一个问题的同一实例时切勿在不同的递归中做重复性工作。
结构:递归是一个树结构,从字面可以其理解为重复“递推”和“回归”的过程,当“递推”到达底部时就会开始“回归”,其过程相当于树的深度优先遍历。如下图
eg:计算n!示意图
二、迭代(iteration)
定义:重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。(A重复调用B)
特点:迭代算法是用计算机处理问题的一种基本方法。它利用计算机运算速度快、适合做重复性操做的特点,让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,都从变量的原值推出它的一个新值。
利用迭代算法处理问题,需要做好以下三个方面的工作:
1、确定迭代变量。在能够用迭代算法处理的问题中,至少具有一个间接或间接地不断由旧值递推出新值的变量,这个变量就是迭代变量。
2、建立迭代关系式。所谓迭代关系式,指如何从变量的前一个值推出其下一个值的公式(或关系)。迭代关系式的建立是处理迭代问题的关键,通常能够使用递推或倒推的方法来完成。
3、对迭代过程进行控制。在什么时候结束迭代过程?这是编写迭代程序必须考虑的问题。不能让迭代过程无休止地重复执行下去。迭代过程的控制通常可分为两种情况:一种是所需的迭代次数是个确定的值,能够计算出来;另一种是所需的迭代次数无法确定。对于前一种情况,能够建立一个固定次数的循环来实现对迭代过程的控制;对于后一种情况,需要进一步分析出用来结束迭代过程的条件。
结构:迭代是一个环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。
三、递归转迭代
理论上递归和迭代时间复杂度方面是一样的,但实际应用中(函数调用和函数调用堆栈的开销)递归比迭代效率要低,而且占用空间大,容易出现堆栈溢出(了解JVM的运行原理)
将递归算法转换为非递归算法有两种方法,一种是直接求值(迭代),不需要回溯;另一种是不能直接求值,需要回溯。前者使用一些变量保存中间结果,称为直接转换法,后者使用栈保存中间结果,称为间接转换法。
直接转换法
直接转换法通常用来消除尾递归(tail recursion)和单向递归,将递归结构用迭代结构来替代。(单向递归 → 尾递归 → 迭代)
间接转换法
递归实际上利用了系统堆栈实现自身调用,我们通过使用栈保存中间结果模拟递归过程,将其转为非递归形式。
例子:
第一个例子用求阶乘,顺便加了迭代方法。
import java.util.Stack; public class Factorial{ public static void main(String[] args){ Factorial f = new Factorial(); System.out.println(f.recursion(5)); System.out.println(f.loop(5)); System.out.println(f.iteration(5)); } /** * 递归求阶乘 */ public int recursion(int n){ if (n == 1) return 1; return recursion(n-1) * n; } /** * 循环求阶乘 */ public int loop(int n){ Stackstack = new Stack (); int result = 1; stack.push(n); while(!stack.isEmpty()){ n = stack.pop(); result *= n; if (n > 1) stack.push(n-1); } return result; } /** * 迭代求阶乘 */ public int iteration(int n){ int result = 1; for(int i = 1; i <= n; i++){ result *= i; } return result; } }
第二个例子是快速排序。递归快排大量数量时,容易爆栈。这时可以改成循环。
import java.util.Random; public class Sorts{ private static Random rand = new Random(); public static void main(String[] args){ int[] a = {49,38,65,97,76,13,27,49,78,34,12,64,5,4,62 ,99,98,54,56,17,18,23,34,15,35,25,53,51}; quickSort(a); System.out.println(Arrays.toString(a)); int[] a = {49,38,65,97,76,13,27,49,78,34,12,64,5,4,62 ,99,98,54,56,17,18,23,34,15,35,25,53,51}; quickSortByLoop(a); System.out.println(Arrays.toString(a)); } /** * 快速排序 * 递归实现 */ private static void quickSort(int[] arr, int start, int end){ if (start >= end) return; int base = arr[start + rand.nextInt(end-start+1)]; //中轴值 int left = start, right = end; //指示左右两端未排序的边界索引 int i = start; // 用于排序 while(i <= right){ if (arr[i] < base){ //小于中轴值则移到左侧 swap(arr, left++, i++); }else if (arr[i] > base){ //大于中轴值则移到右侧 swap(arr, right--, i); //当前i位置的值未排序,故i不自增 }else{ i++; } } //排完后left左侧均为小于base的数,right右侧均为大于base的数 quickSort(arr, start, left-1); quickSort(arr, right+1, end); } /** * 快速排序 * 循环实现 */ private static quickSortByLoop(int[] arr){ Stackstack = new Stack (); int start = 0; int end = arr.length - 1; stack.push(start); stack.push(end); while(!stack.isEmpty()){ end = stack.pop(); // 顺序很重要 start = stack.pop(); if (start < end){ // 开始排序 int i = start; int base = arr[start + rand.nextInt(end-start+1)] int left = start; int right = end; while(i <= right){ if (arr[i] < base){ swap(arr, left++, i++); }else if (arr[i] > base){ swap(arr, right--, i); }else { i++; } } // ----右半边---- stack.push(right + 1); stack.push(end); // ----左半边---- stack.push(start); stack.push(left - 1); } } } }
四、递归与迭代的区别
|
定义 |
优点 |
缺点 |
递归 |
重复调用函数自身实现循环 |
a.用有限的循环语句实现无限集合; b.代码易读; c.大问题转化成小问题,减少了代码量。 |
a.递归不断调用函数,浪费空间 b.容易造成堆栈溢出 |
迭代 |
利用变量的原值推出新值;函数内某段代码实现循环。 |
a.效率高,运行时间只随循环的增加而增加; b.无额外开销。 |
a.代码难理解; b.代码不如递归代码简洁; c.编写复杂问题时,代码逻辑不易想出 |