在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。若调用自身,称之为直接递归。若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。
任何间接递归都可以等价地转换为直接递归。
如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。
例1.1 设计求n!(n为正整数)的递归算法。
解:对应的递归函数如下:
int fun(int n) {
if (n == 1) {
return (1);
} else {
return (fun(n - 1) * n);
}
}
在该函数fun(n)求解过程中,直接调用fun(n-1)自身,所以它是一个直接递归函数。又由于递归调用是最后一条语句,所以它又属于尾递归。
一般来说,能够用递归解决的问题应该满足以下三个条件:
1.定义是递归的
有许多数学公式、数列等的定义是递归的。例如,求n!和Fibonacci数列等。这些问题的求解过程可以将其递归定义直接转化为对应的递归算法。
2.数据结构是递归的
有些数据结构是递归的。例如单链表就是一种递归数据结构,其结点类型声明如下:
public class ListNode {
int val;
ListNode next;
}
对于递归数据结构,采用递归的方法编写算法既方便又有效。例如,求一个不带头结点的单链表L的所有value域(假设为int型)之和的递归算法如下:
int sum(ListNode node) {
if (node == null) {
return 0;
} else {
return node.val + sum(node.next);
}
}
例1.2 分析二叉树的二叉链存储结构的递归性,设计求非空二叉链bt中所有结点值之和的递归算法,假设二叉链的value域为int型。
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
int sumTree(TreeNode node) {
if (node == null) {
return 0;
} else {
return node.val + sumTree(node.left)
+ sumTree(node.right);
}
}
(非递归使用队列实现)
3.问题的求解方法是递归的
有些问题的解法是递归的,典型的有Hanoi问题求解。
一般地,一个递归模型是由递归出口和递归体两部分组成,前者确定递归到何时结束,后者确定递归求解时的递推关系。
例1.3 Fibonacci数列定义为:
Fib(n)=1 n=1
Fib(n)=1 n=2
Fib(n)=Fib(n-1)+Fib(n-2) n>2
对应的递归算法如下:
int Fib(int n)
{ if (n==1 || n==2)
return 1;
else
return Fib(n-1)+Fib(n-2);
}
第一数学归纳法原理:若{P(1),P(2),P(3),P(4),…}是命题序列且满足以下两个性质,则所有命题均为真:
(1)P(1)为真。
(2)任何命题均可以从它的前一个命题推导得出。
例如,采用第一数学归纳法证明下式:
1+2+…+n=n*(n+1)/2
证明:当n=1时,左式=1,右式=1*2/2=1,左右两式相等,等式成立。
假设当n=k-1时等式成立,有1+2+…+(k-1)= k*(k-1)/2
当n=k时,左式=1+2+…+k=1+2+…+(k-1)+k=k*(k-1)/2+k=k*(k+1)/2
等式成立。即证。
第二数学归纳法原理:若{P(1),P(2),P(3),P(4),…}是满足以下两个性质的命题序列,则对于其他自然数,该命题序列均为真:
(1)P(1)为真。
(2)任何命题均可以从它的前面所有命题推导得出。
归纳步骤(条件2)的意思是P(n)可以从前面所有命题假设{P(1),P(2),P(3),…,P(n-1)}推导得出。
例如,采用第二数学归纳法证明,任何含有n(n≥0)个不同结点的二又树,都可由它的中序序列和先序序列唯一地确定。
数学归纳法是一种论证方法,而递归是算法和程序设计的一种实现技术,数学归纳法是递归的基础。
递归算法设计先要给出递归模型,再转换成对应的C/C++/Java语言函数。
获取递归模型的步骤如下:
(1)对原问题f(sn)进行分析,抽象出合理的“小问题”f(sn-1)(与数学归纳法中假设n=k-1时等式成立相似);
(2)假设f(sn-1)是可解的,在此基础上确定f(sn)的解,即给出f(sn)与f(sn-1)之间的关系(与数学归纳法中求证n=k时等式成立的过程相似);
(3)确定一个特定情况(如f(1)或f(0))的解,由此作为递归出口(与数学归纳法中求证n=1或n=0时等式成立相似)。
例 用递归法求一个整数数组a的最大元素。
解:设f(a,i)求解数组a中前i个元素即a[0…i-1]中的最大元素,则f(a,i-1)求解数组a中前i-1个元素即a[0…i-2]中的最大元素,前者为“大问题”,后者为“小问题”。
假设f(a,i-1)已求出,则有f(a,i)=MAX{f(a,i-1),a[i-1]}。递推方向是朝a中元素减少的方向推进,当a中只有一个元素时,该元素就是最大元素,所以f(a,1)=a[0]。
递归模型
f(a,i)=a[0] 当i=1时
f(a,i)=MAX{f(a,i-1),a[i-1]} 当i>1时
递归算法
int fmax(int a[],int i)
{ if (i==1)
return a[0];
else
return(fmax(a,i-1),a[i-1]);
}
1.递归数据结构的定义
二叉树、链表…
2.基于递归数据结构的递归算法设计
例 有一个不带头结点的单链表L,设计一个算法释放其中所有结点。
解:设L={a1,a2,…,an},f(L)的功能是释放a1~an的所有结点,则f(L->next)的功能是释放a2~an的所有结点,前者是“大问题”,后者是“小问题”。
假设f(L->next)是已实现,则f(L)就可以采用先调用f(L->next),然后释放L所指结点来求解。
//c++
void DestroyList(LinkNode *&L)
//释放单链表L中所有结点
{
if (L!=NULL)
{
DestroyList(L->next);
free(L);
}
}
例 对于含n(n>0)个结点的二叉树,所有结点值为int类型,设计一个算法由其先序序列a和中序序列b创建对应的二叉链存储结构。
剑指 Offer 07. 重建二叉树 - 力扣(LeetCode)
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder,inorder,0,preorder.length - 1,0,inorder.length - 1);
}
TreeNode build(int[] preorder, int[] inorder,int preStart,int preEnd,int inoStart,int inoEnd){
//base case
if(preStart > preEnd || inoStart > inoEnd){
return null;
}
//树的根节点
int val = preorder[preStart];
TreeNode root = new TreeNode(val);
//查找根节点在中序遍历的位置
int index = 0;
for(int i = inoStart;i <= inoEnd;i++){
if(inorder[i] == val){
index = i;
break;
}
}
//计算左子树在数组中的长度
int leftSize = index - inoStart;
//递归构建。
root.left = build(preorder,inorder,preStart + 1,preStart + leftSize,inoStart , index - 1);
root.right = build(preorder,inorder,preStart + leftSize + 1,preEnd,index + 1 , inoEnd);
return root;
}
}
例 假设二叉树采用二叉链存储结构,设计一个递归算法由二叉树bt复制产生另一棵二叉树bt1。
//c++
void CopyBTree(BTNode *bt,BTNode *&bt1)
//由二叉树bt复制产生bt1
{ if (bt==NULL)
bt1=NULL;
else
{
bt1=(BTNode *)malloc(sizeof(BTNode));
bt1->data=bt->data;
CopyBTree(bt->lchild,bt1->lchild);
CopyBTree(bt->rchild,bt1->rchild);
}
}
例 假设二叉树采用二叉链存储结构,设计一个递归算法输出从根结点到值为x的结点的路径,假设二叉树中所有结点值不同。
//c++
bool Findxpath2(BTNode *bt,int x,vector tmppath,
vector &path) //求根结点到x结点的(正向)路径
{ if (bt==NULL) //空树返回false
return false;
tmppath.push_back(bt->data); //当前结点加入path
if (bt->data==x) //当前结点值为x,返回true
{ path=tmppath; //路径复制
return true;
}
bool find=Findxpath2(bt->lchild,x,tmppath,path); //在左子树中查找
if (find) //左子树中成功找到
return true;
else //左子树中没有找到,在右子树中查找
return Findxpath2(bt->rchild,x,tmppath,path);
}
例 设计一个递归算法,输出一个大于零的十进制数n的各数字位,如n=123,输出各数字位为123。
void digits(int n)
{
if (n!=0)
{
digits(n/10);
printf("%d",n%10);
}
}
【问题描述】对于给定的含有n个元素的数组a,分别采用简单选择排序和冒泡排序方法对其按元素值递增排序。
/*
设f(a,n,i)用于对a[i..n-1]元素序列(共n-i个元素)进行简单选择排序,是“大问题”.
f(a,n,i+1)用于对a[i+1..n-1]元素序列(共n-i-1个元素)进行简单选择排序,是“小问题”。
当i=n-1时所有元素有序,算法结束。
*/
void selectSort(int[] nums, int n, int i) {
if (i == n - 1) {
return;
}
int k=i;
for (int j = i+1; j < n; j++) {
if (nums[j]<nums[k]){
k=j;
}
}
int temp=nums[k];
nums[k]=nums[i];
nums[i]=temp;
selectSort(nums,n,i+1);
}
/*
设f(a,n,i)用于对a[i..n-1]元素序列(共n-i个元素)进行冒泡排序,是“大问题”,则f(a,n,i+1)用于 对a[i+1..n-1]元素序列(共n-i-1个元素)进行冒泡排序,是“小问题”。当i=n-1时所有元素有序,算法结束。
*/
void bubbleSort(int[] nums, int n, int i) {
if (i == n - 1) {
return;
}
boolean exchange=false;
for (int j = n - 1; j > i; j--) {
if (nums[j]<nums[j-1]){
int temp=nums[j];
nums[j]=nums[j-1];
nums[j-1]=temp;
exchange=true;
}
}
if (!exchange){
return;
}
bubbleSort(nums,n,i+1);
}
【问题描述】在n×n的方格棋盘上,放置n个皇后,要求每个皇后不同行、不同列、不同左右对角线。
求解N皇后问题-基础递归版_松东路的博客-CSDN博客
51. N 皇后 - 力扣(LeetCode)
class Solution {
int[] q;
List<List<String>> result;
public List<List<String>> solveNQueens(int n) {
q = new int[n + 1];
result=new ArrayList<>();
queen(1,n);
return result;
}
//测试(i,j)位置能否放皇后
boolean place(int i, int j) {
//第一个位置OK
if (i == 1) {
return true;
}
int k = 1;
while (k < i) {
if (j == q[k] || Math.abs(i - k) == Math.abs(j - q[k])) {
return false;
}
k++;
}
return true;
}
void queen(int i, int n) {
if (i > n) {
dispasolution(n); //得到一个解
return;
}
for (int j = 1; j <= n; j++) {
if (place(i, j)) {
q[i]=j;
queen(i + 1, n);
}
}
}
private void dispasolution(int n) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= n; i++) {
StringBuilder builder = new StringBuilder();
for (int j = 1; j <= n; j++) {
if (j == q[i]) {
builder.append("Q");
} else {
builder.append(".");
}
}
list.add(builder.toString());
}
result.add(list);
}
}
把递归算法转化为非递归算法有如下两种基本方法:
(1)直接用循环结构的算法替代递归算法。
(2)用栈模拟系统的运行过程,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。
第(1)种是直接转化法,不需要使用栈。第(2)种是间接转化法,需要使用栈。
直接转化法特别适合于尾递归。尾递归只有一个递归调用语句,而且是处于算法的最后。
例 采用循环结构求n!的非递归算法fun1(n)如下:
int fun1(int n)
{ int f=1,i;
for (i=2;i<=n;i++)
f=f*i;
return(f);
}
除尾递归外,直接转化法也适合于单向递归。
单向递归是指递归函数中虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调用函数有关,相互之间参数无关,并且这些递归调用语句也和尾递归一样处于算法的最后。
例 采用循环结构求解Fibonacci数列的非递归算法如下:
int Fib1(int n)
{ int i,f1,f2,f3;
if (n==1 || n==2)
return(1);
f1=1;f2=1;
for (i=3;i<=n;i++)
{ f3=f1+f2;
f1=f2;
f2=f3;
}
return(f3);
}
…
【例】分析以下递归方程的时间复杂度:
T(n)=1 当n=1
T(n)=4T(n/2)+n 当n>1
a=4,b=2,f(n)=n 满足情况(1)所以T(n)=O(n^2)。