算法与数据结构-递归

文章目录

  • 递归算法设计技术
    • 1 什么是递归
      • 1.1 递归的定义
      • 1.2 何时使用递归
      • 1.3 递归模型
    • 2 递归算法设计
      • 2.1 递归与数学归纳法
      • 2.2 递归算法设计的一般步骤
      • 2.3 递归数据结构及其递归算法设计
      • 2.4 基于归纳思想的递归算法设计
    • 3 递归算法设计示例
      • 3.1 简单选择排序和冒泡排序
      • 3.2 求解n皇后问题
    • 4 递归算法转化非递归算法
      • 4.1 用循环结构替代递归过程
      • 4.2 用栈消除递归过程
    • 5 递推式的计算
      • 主方法

递归算法设计技术

1 什么是递归

1.1 递归的定义

在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。若调用自身,称之为直接递归。若过程或函数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.2 何时使用递归

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 递归模型

一般地,一个递归模型是由递归出口递归体两部分组成,前者确定递归到何时结束,后者确定递归求解时的递推关系。

例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);

}

2 递归算法设计

2.1 递归与数学归纳法

第一数学归纳法原理:若{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)个不同结点的二又树,都可由它的中序序列和先序序列唯一地确定。

数学归纳法是一种论证方法,而递归是算法和程序设计的一种实现技术,数学归纳法是递归的基础

2.2 递归算法设计的一般步骤

递归算法设计先要给出递归模型,再转换成对应的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]);
}

2.3 递归数据结构及其递归算法设计

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);
}

2.4 基于归纳思想的递归算法设计

设计一个递归算法,输出一个大于零的十进制数n的各数字位,如n=123,输出各数字位为123。

void digits(int n)
{  
   if (n!=0)
   {  
     digits(n/10);
     printf("%d",n%10);
   }
}

3 递归算法设计示例

3.1 简单选择排序和冒泡排序

【问题描述】对于给定的含有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);
  }

3.2 求解n皇后问题

【问题描述】在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);
  }
}

4 递归算法转化非递归算法

把递归算法转化为非递归算法有如下两种基本方法:

(1)直接用循环结构的算法替代递归算法。

(2)用模拟系统的运行过程,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。

第(1)种是直接转化法,不需要使用栈。第(2)种是间接转化法,需要使用栈。

4.1 用循环结构替代递归过程

直接转化法特别适合于尾递归。尾递归只有一个递归调用语句,而且是处于算法的最后。

采用循环结构求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);
}

4.2 用栈消除递归过程

5 递推式的计算

主方法

算法与数据结构-递归_第1张图片

算法与数据结构-递归_第2张图片

算法与数据结构-递归_第3张图片

】分析以下递归方程的时间复杂度:

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)。

你可能感兴趣的:(算法与数据结构,算法,数据结构)