什么是递归
递归是一种应用非常广泛的算法,或者说是编程技巧。如DFS深度优先搜索,前中后序二叉树遍历等,都要用到递归。
递归的求解过程:一层一层向下求解,直至到第一层,这个过程叫做
递
; 然后一层一层向上返回值,这个过程叫做归
。
什么类型的问题适合递归求解
- 一个问题可以分解为几个子问题;
- 子问题与父问题求解方法一样;
- 存在递归终止条件;
如何编码递归问题
- 写出递推公式;
- 写出终止条件;
- 翻译成代码;
递归代码弊端及解决方法
- 堆栈溢出:通过控制递归深度;
- 重复计算:通过散列表存储计算值;
- 函数调用耗时多:通过控制递归深度;
- 空间复杂度高:转成非递归;
经典习题
-
爬楼梯:一次可以爬1层或者2层,问爬到n层有多少种方法; LeetCode 70
解题思路- 爬到最后一层楼梯只有两种可能,从倒数第二层走一步,或者从倒数第三层走两步;因此得出公式
f(n) = f(n-1) + f(n-2)
; - 一直往前倒推,只需知道爬到第一层和爬到第二层的方法,即可推出后面所有楼层方法;因此得出终止条件
f(1) = 1, f(2) = 2
;
// 原始递归版 function f(n) { if (n === 1) return 1 if (n === 2) return 2 return f(n-1) + f(n-2) } // 递归优化:散列表存储计算的值 function f(n) { let map = new Map() return g(n) function g(n) { if (map.has(n)) return map.get(n) if (n === 1) return 1 if (n === 2) return 2 let temp = g(n-1) + g(n-2) map.set(n, temp) return temp } } // 动态规划 function f(n) { let dp = [1, 2] for (let i = 2; i < n; i++) { dp[i] = dp[i-1] + dp[i-2] } return dp[n-1] }
- 爬到最后一层楼梯只有两种可能,从倒数第二层走一步,或者从倒数第三层走两步;因此得出公式
-
求二叉搜索树,任意两节点节点之间的和;LeetCode 938
解题思路- 二叉搜索树,说明左节点一定小于右节点;求任意两节点之间的和,只需要遍历所有节点,满足条件的值累加即可;遍历方式,深度优先搜索;
- node 为 null,返回即可。
// 递归 function fn1(root, L, R) { let sum = 0 dfs(root) return sum // 前序遍历 function dfs(root) { if (!root) return if (root.val <= R && root.val >= L) { sum += root.val } if (root.val > L) { dfs(root.left) } if (root.val < R) { dfs(root.right) } } }
-
重建二叉树,已知前序arr1,中序arr2;剑指offer07
解题思路
- 已知前序,中序两个数组,那么前序的第一个节点,一定是顶点;根据该顶点的值,在中序数组中,就可以分出左右子树的节点个数,根据该个数又可以求出左右子树的前序数组;同理,递归方式求解左右子树;
- 当前序数组或者后续数组为空时,直接返回,这便是递归终止条件;
// 已知前序,中序 function fn(arr1, arr2) { let root = new TreeNode() // 终止条件 if (!arr1 || !arr2) { return null } // 根节点 let rootValue = arr1[0] root.value = rootValue let index = arr2.indexOf(rootValue) root.left = fn(arr1.slice(1, index + 1), arr2.slice(0, index)) root.right = fn(arr1.slice(index + 1), arr2.slice(index + 1)) return root } // 已知后序,中序 function fn(arr1, arr2) { if (!arr1 || !arr2) { return null } let root = new TreeNode() let n = arr1.length let rootValue = arr1[n-1] let index = arr2.indexOf(rootValue) root.value = rootValue root.left = fn(arr1.slice(0, index), arr2.slice(0, index)) root.right = fn(arr1.slice(index, n-1), arr2.slice(index+1, n)) return root }
-
第一行为0,接下来每一行都将0替换为01,1替换为10;求第N行,第K列字符;LeetCode779
解题思路
- 先按照规则先几行,然后找找第n行和第n-1行的规律;不难发现,第n行数量时第n-1行的2倍,且前半部分相同,后半部分是前半部分的相反数(即0的相反数为1,1的相反数为0);因此,递推公式为 k在前半部分,f(n, k) = f (n-1, k) ; k在后半部分, f(n,k) = f(n-1, k-2^(n-2)) === 0 ? 1 : 0;
- 一直往前求解,得出终止条件为 f (1,1) = 0
function fn(n,k) { if (n === 1) { return 0 } if (k <= Math.pow(2, n-2)) { return fn(n-1, k) } else { return fn(n-1, k - Math.pow(2, n-2)) === 0 ? 1 : 0 } }
-
求二叉树中相同数字的最长路径;
解题思路
- 画出一个简单二叉树,通过查找发现,从下往上计算每个节点的路径;如果该节点值等于左节点,则该节点长度=左节点长度 + 1;否则为0; 同理右节点,最终该节点值为左节点路径+右节点路径;
- 不妨后序遍历每个节点,过程中记录最长路径;
- 遍历到顶点,即为终止条件;
function fn(root) { let max = 0 dfs(root) return max // 后序遍历 function dfs(root) { if (!root) { return 0 } let left = dfs(root.left) let right = dfs(root.right) let value = root.val if (root.left && value === root.left.value) { left += 1 } else { left = 0 } if (root.right && value === root.right.value) { right += 1 } else { right = 0 } // 最长路径可以经过该节点 max = Math.max(max, left + right) // 该节点向上返回的,只能是最长的那一条路径 return Math.max(left, right) } }
-
两个正整数的递归乘法实现;
算法思路
- 乘法换成累加,第n项与第n-1项的关系时, f(n, num) = f(n-1) + num;
- 当n===1时,返回num,即为终止条件;
function fn(A, B) { if (A === 1) { return B } return fn(A-1, B) + B }
-
给定一个二叉搜索树的根节点,返回任意两节点的差的最小值;
算法思路
- 二叉搜索树,第一时间应该联想到中序遍历,就是排序后的数组;容易发现,任意两节点的差的最小值,即为中序遍历后的数组中相邻两节点的最小值;
- 当节点为null时,返回;
function fn(root) { let min = Infinity let last = null dfs(root) return min // 中序遍历 function dfs(root) { if (!root) return dfs(root.left) let value = root.val if (last === null) { last = value } else { last = value min = Math.min(min, value - last) } dfs(root.right) } }