递归
为何用递归呢?更快吗?递归并不比普通版本更快,反倒更慢。但要知道,递归更容易理解,
并且它所需的代码量更少。
通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
(ECMAScript 6中,因为尾调用优化的缘故,递归并不会更慢。但是在其他语言中,递归通常更慢)
递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题。通常涉及函数调用自身。
每个递归函数都必须要有边界条件,即一个不再递归调用的条件(停止点),以防止无限递归。
如果忘记加上用以停止函数递归调用的边界条件,递归并不会无限地执行下
去;浏览器会抛出错误,也就是所谓的栈溢出错误(stack overflow error)。
测试浏览器上限
var i = 0;
function recursiveFn () {
i++;
recursiveFn(); // 尾调用
}
try {
recursiveFn();
} catch (ex) {
alert('i = ' + i + ' error: ' + ex);
}
ECMAScript 6有尾调用优化(tail call optimization)。
如果函数内最后一个操作是调用函数,会通过“跳转指令”(jump)
而不是“子程序调用”(subroutine call)来控制。
也就是说,在ECMAScript 6中,这里的代码可以一直执行下去。
所以,具有停止递归的边界条件非常重要。
斐波那契问题。
斐波那契数列的定义如下:
1和2的斐波那契数是 1;
n(n>2)的斐波那契数是(n1)的斐波那契数加上(n2)的斐波那契数
function fibonacci(num){
if (num === 1 || num === 2){
return 1;
}
return fibonacci(num - 1) + fibonacci(num - 2);
}
动态规划(Dynamic Programming,DP)
是一种将复杂问题分解成更小的子问题来解决的优化技术。
区别:
要注意动态规划和分而治之(归并排序和快速排序算法中用到的那种)是不
同的方法。分而治之方法是把问题分解成相互独立的子问题,然后组合它们的答
案,而动态规划则是将问题分解成相互依赖的子问题。
用动态规划解决问题时,要遵循三个重要步骤:
(1) 定义子问题;
(2) 实现要反复执行而解决子问题的部分(递归的步骤);
(3) 识别并求解出边界条件。
/*
1和2的斐波那契数是 1;
n(n>2)的斐波那契数是(n-1)的斐波那契数加上(n-2)的斐波那契数。
*/
let x = 0
function fib(num) {
if(num === 1 || num === 2) {
return 1
}
x++
return fib(num-1) + fib(num-2)
}
function fib2(num){
var n1 = 1,
n2 = 1,
n = 1;
let j = 0
for (var i = 3; i<=num; i++){
j++
n = n1 + n2;
n1 = n2;
n2 = n;
}
console.log(`j`, j)
return n;
}
console.time('1')
console.log(fib(6))
console.timeEnd('1')
console.log(`x`, x)
console.time('2')
console.log(fib2(6))
console.timeEnd('2')
console.info('=====')
// 动态规划更循环次数更多.时间复杂度为O(2^n)
// 而简单的循环方式次数少。时间复杂度为O(n)。
// 递归看情况而言,有时候是比较消耗内存,有时候是优解。
// 最少硬币找零问题
/* 例如,美国有以下面额(硬币):d1=1,d2=5,d3=10,d4=25。
如果要找36美分的零钱,我们可以用1个25美分、1个10美分和1个便士(1美分)。
如何将这个解答转化成算法? */
// 判断条件是最难确定的。
// 这里的思想是,计算每种金额组成的可能组合,并将结果进行缓存。
// 比如1的可能组合,2的可能组合,...,以此类推。
// 到最后也就是我们需要的那个金额的时候,
// 循环里面是每次都去取得之前的可能组合回来比较并存入结果集。
function MinCoinChange(coins) {
let cache = {} // 缓存计算结果
this.makeChange = function(amount) {
let me = this
if(!amount) {
return []
}
if(cache[amount]) {
return cache[amount]
}
let min = []
let newMin
let newAmount
for (let i = 0; i < coins.length; i++) {
let coin = coins[i]
newAmount = amount - coin // 按照顺序减去一种,试验每一种装满是需要多少个
if(newAmount >= 0) { // 直到最后为负数
newMin = me.makeChange(newAmount) // 不断计算剩余的钱的金额的可能值,缓存起来,最后返回最小的数组值。
}
/*
1. 还有剩余的钱
2. 最小的值的长度大于目前最小值的数组的长度
或者最小值数组第一次为空的情况
3. 最小值的长度并且没有剩余
*/
/*
我们判断 newAmount 是否有效,
minValue (最少硬币数)是否是最优解,
// 1. newMin.length < min.length - 1,新的数组必须是长度小于旧的数组的,才会替换。
// 2. newAmount == 0 的时候,刚好最小, 或者有最小的新数组
// 3. newAmount 不为负数
与此同时 minValue 和 newAmount 是否是合理的值
*/
if(newAmount>=0 && (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount) ) {
// 有一个更优的解
min = [coin].concat(newMin) // 当前的值加上最新的值
console.log('new min' + min + 'for' + amount)
}
}
return (cache[amount] = min)
}
}
// let coins = [1, 5, 10, 25]
// console.time('3')
// let minCoinChange = new MinCoinChange(coins)
// console.log(`MinCoinChange()`, minCoinChange.makeChange(36))
// console.timeEnd('3')
let coins2 = [1, 3, 4]
console.time('4')
let minCoinChange2 = new MinCoinChange(coins2)
console.log(`MinCoinChange()`, minCoinChange2.makeChange(6))
console.timeEnd('4')
// 贪心算法
// 循环所有金额可能值,
// 内部再循环计算取得的当前金额总数得到多少次才大于或者等于总金额
function TxMinCoinChange(coins) {
this.makeChange = function(amount) {
let change = []
let total = 0
// 从大到小装填
for (let i = coins.length; i >= 0; i--) {
const coin = coins[i];
while(total + coin <= amount) {
change.push(coin)
total += coin
}
}
return change
}
}
console.time('5')
let txMinCoinChange = new TxMinCoinChange(coins2)
console.log(`txMinCoinChange.makeChange(6) `, txMinCoinChange.makeChange(6) )
console.timeEnd('5')
本文探讨了递归的原理,强调其简洁性与边界条件的重要性,以斐波那契数列为例展示了递归和动态规划的区别。同时讲解了尾调用优化在ECMAScript6中的角色。最后,通过硬币找零问题对比了递归和循环的效率,并涉及了动态规划的应用。

被折叠的 条评论
为什么被折叠?



