参考文档

递归思想

思想 描述
递归 当需要重复地多次计算相同的问题,通常可以采用递归或循环。递归是在一个函数内部调用这个函数自身。
递归的本质是把一个问题分解成两个或多个小问题。(注:当多个小问题存在相互重叠的部分,就存在重复的计算)
分治 将大问题拆分为子问题,递归求出子问题的解后进行合并,就可以得到原问题的解
回溯 主要是在搜索尝试过程中寻找问题的解,当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择,
尝试别的路径,这种走不通就退回再走的技术称为回溯法。回溯法可理解为使用了递归思想的一种算法

什么样的情况下可以用递归?

  1. 一个问题的解可以分解为几个子问题的解:子问题
  2. 这个问题与分解之后的子问题,求解思路完全一样
  3. 一定有一个最后确定的答案,即递归的终止条件

Master公式(计算递归复杂度)

注:使用Master公式分析递归问题复杂度时,各子问题的规模应该是一致的,否则不能使用Master公式

参数含义

a表示:递归的次数(生成的子问题数)
N表示:母问题的规模
b表示:子过程的样本量(当前母问题的子问题数量)
N/b表示:子问题的规模
O(N^d)表示:除了递归操作以外其余操作的复杂度

时间复杂度

举例分析master公式
1、递归求数组最大值

public static int maxNum(int[] arr, int L, int R){if(L == R) {return arr[L];}int mid = L + ((R - L) >> 1);int lMax = maxNum(arr, L, mid);int rMax = maxNum(arr, mid + 1, R);return Math.max(lMax, rMax); // O(N^d)=1
}

子问题数b=2,额外复杂度O(N^d)=O(1),每次方法递归次数a=2
T(N)=2*T(N/2)+O(1)

2、斐波那契数列

    public static int fab(int n) {if (n <= 2) return 1; return fab(n - 1) + fab(n - 2); // 递归次数 a=2}

T(N)=T(N-1)+T(N-1) +O(1)不符合master公式
斐波那契时间复杂度,通过画图能很好分辨为O(2^n)

递归的优化

斐波那契递归问题:

  1. 每次执行递归方法会创建栈帧,jvm中栈帧的创建很消耗资源,上次递归依赖下次递归,导致上次资源不会释放
  2. 时间复杂度大,O(2^n) 可优化到O(n)或者O(nlogn)

优化方式

  1. 加入缓存:把我们中间的运算结果保存起来,这样就可以把递归降至为o(n)
  2. 使用非递归(循环)。所有的递归代码理论上是一定可以转换成非递归的
  3. 尾递归 :函数最后一步调用另外一个函数

当编译器检测到尾递归时,覆盖当前栈帧,而不是去建立一个新的栈帧,这样只需要占用一个函数栈帧空间,防止了内存的大量浪费。

斐波那契优化

缓存优化

    private static int data[]; // 初始换全部是0/*** 缓存优化递归,用数组来做缓存* 时间复杂度:O(n) 空间复杂度:O(n)*/public static int cacheFab(int n) {if (n <= 2) return 1; // 递归的终止条件if (data[n] > 0) {System.out.println(String.format("当前递查询缓存:f(%d)=f(%d)+f(%d)=%d", n, n - 1, n - 2, data[n]));return data[n];}System.out.println(String.format("当前递:f(%d)=f(%d)+f(%d)", n, n - 1, n - 2));int retult = cacheFab(n - 1) + cacheFab(n - 2);data[n] = retult; // 缓存记录计算结果System.out.println(String.format("当前归记录缓存:f(%d)=f(%d)+f(%d)=%d,缓存data[%d]=%d",n, n - 1, n - 2, retult,n,data[n]));return retult;}

int retult = cacheFab(n - 1) + cacheFab(n - 2);归过程进行计算
测试用例

 public static void main(String[] args) {cacheFab(6);}

运行结果

当前递:f(6)=f(5)+f(4)
当前递:f(5)=f(4)+f(3)
当前递:f(4)=f(3)+f(2)
当前递:f(3)=f(2)+f(1)
当前归记录缓存:f(3)=f(2)+f(1)=2,缓存data[3]=2
当前归记录缓存:f(4)=f(3)+f(2)=3,缓存data[4]=3
当前递查询缓存:f(3)=f(2)+f(1)=2
当前归记录缓存:f(5)=f(4)+f(3)=5,缓存data[5]=5
当前递查询缓存:f(4)=f(3)+f(2)=3
当前归记录缓存:f(6)=f(5)+f(4)=8,缓存data[6]=8

可以看到减少了重复的计算

循环优化

使用循环模拟递归中的归过程,此时计算过程需要自己实现

  1. 循环:是从初始值,往后计算每一个步骤的值,直到计算出当前步骤的值
  2. 递归:从当前计算,往下调用自己【递过程】,找到知道的值,在往上返回计算【归过程】
public static int fab(int n) {// 递前置处理int res = fab(n-1) + f(n-2)// 归后置处理return res;
}

两种方式在于思维逻辑的区别

递归:从未知值,找已知,然后从已知到未知开始计算
循环:从已知开始计算,计算出未知值

// 时间复杂度:O(n)
public static int noFab(int n) {int a = 1; // f(1)  已知int b = 1; // f(2)  已知int c = 0; // f(n) = f(n-1)+f(n-2) 下次结果未知for (int i = 3; i <= n; i++) { // i=3,从第一个未知开始c = a + b; // 计算当前步骤结果a = b;b = c; // 作为下次的输入System.out.println(String.format("f(%d)=f(%d)+f(%d)=%d", i, i - 1, i - 2, c));}return c;}

画图演示
每一轮计算后 a = b、b = c

noFab(5)运行结果

-----f(2)、f(1)一开始就知道值了--------------
f(3)=f(2)+f(1)=2
f(4)=f(3)+f(2)=3
f(5)=f(4)+f(3)=5

尾递归优化

普通递归:一个大问题可以拆分为多个子问题,递归处理,复杂度指数级

int retult = fab(n - 1) + fab(n - 2);
利用递归函数进行计算,调用多次递归函数

尾递归:发现

int tailfab = tailfab(res, pre + res, n - 1);
计算放入递归函数中,只调用一次递归函数

/*** @param pre 上一次运算出来结果* @param res 上上一次运算出来的结果* @param n   是肯定有的*/
public static int tailfab(int pre, int res, int n) {if (n <= 2) return res; // 递归的终止条件return tailfab(res, pre + res, n - 1); //倒着算
}//--------用于测试分析改造的代码,尾递归要将调用放在最后-----------------------
public static int tailfab2(int pre, int res, int n,int m) {if (n <= 2) return res; // 递归的终止条件String s = "递:f(%d)=f(%d)+f(%d) ";System.out.println(String.format(s, n, n - 1, n - 2));String s2 = "递计算:pre+res=%d+%d=%d";System.out.println(String.format(s2, pre , res, pre + res ));int tailfab = tailfab2(res, pre + res, n - 1,m);String s3 = "归:f(%d)=f(%d)+f(%d)=%d";System.out.println(String.format(s3, n, n - 1, n - 2, pre + res));return tailfab;
}

tailfab2(1,1,5,5)运行结果

递:f(5)=f(4)+f(3)
递计算:pre+res=1+1=2
递:f(4)=f(3)+f(2)
递计算:pre+res=1+2=3
递:f(3)=f(2)+f(1)
递计算:pre+res=2+3=5
归:f(3)=f(2)+f(1)=5
归:f(4)=f(3)+f(2)=3
归:f(5)=f(4)+f(3)=2

单向链表翻转

MyLinkedListFlip.java
MyLinkedList.java
MyNode.java

递归方式

    public MyNode reverse(MyNode curr) {if (curr == null || curr.next == null) {return curr;}MyNode next = curr.next;System.out.println(String.format("当前递,curr=%d,next=%d", curr.value, next.value));MyNode newHead = reverse(next); // 此处返回的是最后一个元素next.next = curr; // 指向前一个curr.next = null; // 前一个本身指向后一个,打断环形System.out.println(String.format("当前归,next=%d,curr=%d  ", next.value, curr.value));print(newHead);return newHead;}

测试用例

    public static void main(String[] args) {MyLinkedListFlip<Integer> linkedList = new MyLinkedListFlip<>();for (int i = 0; i < 5; i++) {linkedList.add(i);}linkedList.print();linkedList.reverse(linkedList.head);linkedList.print();}

运行结果

size=5  [0,1,2,3,4]
当前递,curr=0,next=1
当前递,curr=1,next=2
当前递,curr=2,next=3
当前递,curr=3,next=4
当前归,next=4,curr=3
size=5  [4,3]当前归,next=3,curr=2
size=5  [4,3,2]当前归,next=2,curr=1
size=5  [4,3,2,1]当前归,next=1,curr=0
size=5  [4,3,2,1,0]

非递归优化

    public MyNode noRecursionReverse(MyNode curr) {MyNode pre = null; // 当前节点curr的上一个节点// 每次遍历将当前节点指针指向上一个while (curr != null) {MyNode next = curr.next;curr.next = pre;pre = curr;curr = next;}return pre;}

尾递归优化

  /*** 尾递归* @param pre 以前一个节点 开始为null* @param curr 当前节点  开始为头结点* @return*/private MyNode tailReverse2(MyNode<E> pre, MyNode<E> curr) {if (curr == null) {return pre;}MyNode next = curr.next;curr.next = pre;return tailReverse2(curr, next);}

算法思想之递归分治回溯相关推荐

  1. Algorithms_算法思想_递归分治

    文章目录 引导案例 递归的定义 什么样的问题可以用递归算法来解决 递归如何实现以及包含的算法思 递归的公式 斐波那契数列代码实现 递归的时间复杂度和空间复杂度 递 与 归 递归的优化 优化方式一:不使 ...

  2. 五大算法思想(三)回溯法及常见例子

    文章目录 一.理论基础 1.1 基本策略 1.2 使用步骤 1.3 经典例子 二.常见例子 2.1 八皇后问题 2.2 装载问题 2.3 批量作业调度问题 2.4 背包问题 一.理论基础   回溯法作 ...

  3. 算法与数据结构——算法基础——暴力递归(回溯)(java)(左程云b站课程总结)

    暴力递归 暴力递归就是尝试 类似于回溯:可以理解为回溯就是在暴力递归的基础上在每个操作步骤加上标记和取消标记的处理 把问题转化为规模缩小了的同类问题的子问题 有明确的不需要继续进行递归的条件(base ...

  4. 数据结构与算法(二):排序(递归、回溯、数论、插入、希尔、归并、选择、冒泡、快排、贪心、动态规划)

    算法排序:递归.回溯.数论.插入.希尔.归并.选择.冒泡.快排.贪心.动态规划 数论思想:利用数学公式或者定理或者规律求解问题: 算法思想中最难的点:递归+动态规划:树论:二叉树,红黑树 思考题: 微 ...

  5. 基于C++的递归和回溯国际象棋女王安全算法

    资源下载地址:https://download.csdn.net/download/sheziqiong/85722216 递归和回溯 目标与要求 熟悉递归的基本思想 能够设计自己的递归函数. 关键是 ...

  6. 分治限界算法思想和应用

    目录 一.分支限界算法思想 1. 分支限界法类似于回溯算法,是在问题的解空间树上搜索问题的算法,主要体现在两点不同: 2. 分治限界算法基本思想: 二.分支限界法的应用 1. 集装箱装载问题 2. 0 ...

  7. 常见的算法思想(整理)

    1.算法特征 算法的英文名称是Algorithm,这个词在1957年之前在Webster's New World Dictionary(<韦氏新世界词典>)中还未出现,只能找到带有它的古代 ...

  8. 数据结构(一)递归和回溯

    本文目录 0 前言 1 递归 1.1 什么是递归 1.2 为什么要用递归 1.3  递归函数的格式 1.4 递归和迭代 1.4.1 递归 1.4.2 迭代 1.5 递归算法的经典用例 2 回溯 2.1 ...

  9. 数据结构与算法--再谈递归与循环(斐波那契数列)

    再谈递归与循环 在某些算法中,可能需要重复计算相同的问题,通常我们可以选择用递归或者循环两种方法.递归是一个函数内部的调用这个函数自身.循环则是通过设置计算的初始值以及终止条件,在一个范围内重复运算. ...

最新文章

  1. 深度学习中的注意力机制(三)
  2. [转]MySQL innodb buffer pool
  3. 利淘优选——青龙羊毛
  4. 计网链路层mac地址和ip地址缺一不可
  5. dict过滤 python_关于python:过滤dict以只包含某些键?
  6. Linux下生成动态链接库是否必须使用 -fPIC 的问题
  7. RabbitMQ集群原理介绍
  8. OJ1158: 又是排序(指针专题)(C语言)
  9. python 解小学数学题_孩子尝试python解数学题,怎么实现呢?
  10. 指定精确度(*号的使用)
  11. Android技能树 — 网络小结(6)之 OkHttp超超超超超超超详细解析
  12. ffmpeg的安装和使用教程
  13. 摄影毁一生单反穷三代顺口溜_哪款便宜的单反相机好
  14. c语言如何文件指针指向开头,fseek设置好文件指针 在C语言中fseek()的功能
  15. 制造企业年终仓库盘点有哪些好的方法
  16. 数字后端基本概念介绍Size Blockage
  17. 团队组成五个基本要素_团队建设的五个要素是什么?
  18. [渝粤教育] 西南科技大学 建筑CAD 在线考试复习资料
  19. Away3d材质实战——旋转的地球
  20. 程序人生:hello程序的P2P

热门文章

  1. linux中audit服务,linux下的audit服务
  2. EJB是个什么东东?
  3. Wap Push 源码
  4. JPA之EntityManager踩坑笔记:更改PersistenceContext
  5. 华北地区博友链接集合(陆续增加中)
  6. Linux操作系统 - 信号
  7. 使用openCV进行视频人脸识别
  8. 【干货】java课程实战培训
  9. 饭后吃一种水果就能化痰止咳(附赠10个化痰小偏方)
  10. WuThreat身份安全云-TVD每日漏洞情报-2023-02-07