赞
踩
参考文档
思想 | 描述 |
---|---|
递归 | 当需要重复地多次计算相同的问题,通常可以采用递归或循环。递归是在一个函数内部调用这个函数自身。 递归的本质是把一个问题分解成两个或多个小问题。(注:当多个小问题存在相互重叠的部分,就存在重复的计算) |
分治 | 将大问题拆分为子问题,递归求出子问题的解后进行合并,就可以得到原问题的解 |
回溯 | 主要是在搜索尝试过程中寻找问题的解,当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择, 尝试别的路径,这种走不通就退回再走的技术称为回溯法。回溯法可理解为使用了递归思想的一种算法 |
什么样的情况下可以用递归?
- 一个问题的解可以分解为几个子问题的解:子问题
- 这个问题与分解之后的子问题,求解思路完全一样
- 一定有一个最后确定的答案,即递归的终止条件
注:使用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)
斐波那契递归问题:
- 每次执行递归方法会创建栈帧,jvm中栈帧的创建很消耗资源,上次递归依赖下次递归,导致上次资源不会释放
- 时间复杂度大,O(2^n) 可优化到O(n)或者O(nlogn)
优化方式
当编译器检测到尾递归时,覆盖当前栈帧,而不是去建立一个新的栈帧,这样只需要占用一个函数栈帧空间,防止了内存的大量浪费。
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
可以看到减少了重复的计算
使用循环模拟递归中的归过程,此时计算过程需要自己实现
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);
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。