递归是一种解决问题的方法,通常涉及函数调用自身。我们使用递归,并不是因为它运行速度更快,而是因为它更利于理解,代码也少。
能够像下面这样直接调用自身的方法或函数,是递归函数:
var recursiveFunction = function(someParam){
recursiveFunction(someParam);
};
能够像下面这样间接调用自身的函数,也是递归函数:
var recursiveFunction1 = function(someParam){
recursiveFunction2(someParam);
};
var recursiveFunction2 = function(someParam){
recursiveFunction1(someParam);
};
栈溢出错误
递归并不会无限地执行下去;浏览器会抛出错误,也就是所谓的栈溢出错误(stack overflow error)
。
每个浏览器都有自己的上限,可用以下代码测试:
var i = 0;
function recursiveFn () {
i++;
recursiveFn();
}
try {
recursiveFn();
} catch (ex) {
console.log('i = ' + i + ' error: ' + ex);
}
n = (n-1) + (n-2)
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, …
function fibonacci(num){
if (num < 2) {
return num;
}
return fibonacci(num - 1) + fibonacci(num - 2);
}
上面这个函数的问题在于它的执行效率非常低,有太多的值在递归调用中被重新计算,如果编译器可以将已经计算的值记录下来,函数的执行效率就不会如此差。我们可以使用动态规划的技巧来设计一个效率更高的算法。
假如需要找出第六个位置的斐波那契数,如下所示,仅仅是第三个斐波那契数就计算了三次,求值越大,计算的次数会变得更多:
使用递归去解决问题虽然简洁,但效率不高,尽管写出来的程序简洁,但是执行效率低下。许多使用递归去解决的编程问题,可以重写为使用动态规划的技巧去解决。动态规划方案通常会使用一个数组来建立一张表,用于存放计算过程中的值,可以避免重新计算。
function dynFib(n) {
if (n < 2) {
return n;
}
else {
var val = [0,1]; // 初始化 作为保存中间结果的数组
for (var i = 2; i <= n; ++i) {
val[i] = val[i-1] + val[i-2];
}
return val[n];
}
}
比较一下时间:
var start = new Date().getTime();
console.log(fibonacci(40));
var stop = new Date().getTime();
console.log(" 递归计算耗时 - " + (stop-start) + " 毫秒");
start = new Date().getTime();
console.log(dynFib(40));
stop = new Date().getTime();
console.log(" 动态规划耗时 - " + (stop-start) + " 毫秒");
看到这里我们可以轻易地发现,上面的数组,只是存储值的一种方式,实际上我们也可以不使用数组来实现这个功能,如下所示:
function iterFib(n) {
// 声明前三个值
var last = 1;
var nextLast = 0;
var result = 1;
for (var i = 2; i <= n; ++i) {
result = last + nextLast;
nextLast = last;
last = result;
}
return result;
}
虽然教材上说这个版本的函数在计算斐波那契数列时和动态规划版本的效率一样。但是经过测试我们发现,这种方式比使用数组实际上还要更快一点。
要注意动态规划和分而治之(归并排序和快速排序算法中用到的那种)是不同的方法。分而治之方法是把问题分解成相互独立的子问题,然后组合它们的答案,而动态规划则是将问题分解成相互依赖的子问题。
例如,在单词“raven”和“havoc”中,最长的公共子串是“av”。
function lcs(word1, word2) {
var max = 0;
var index = 0;
// 声明一个二维数组,长宽分别为两个比较的字符串的长度,数组中所有内容为 0
var lcsarr = new Array(word1.length + 1);
for (var i = 0; i <= word1.length + 1; ++i) {
lcsarr[i] = new Array(word2.length + 1);
for (var j = 0; j <= word2.length + 1; ++j) {
lcsarr[i][j] = 0;
}
}
// 对角线匹配,如果遇到相等的,就对角线加一
for (var i = 0; i <= word1.length; ++i) {
for (var j = 0; j <= word2.length; ++j) {
if (i == 0 || j == 0) {
lcsarr[i][j] = 0;
} else {
if (word1[i-1] == word2[j-1]) {
// lcsarr[i][j] = word1[i-1]
lcsarr[i][j] = lcsarr[i - 1][j - 1] + 1;
} else {
lcsarr[i][j] = 0;
}
}
if (max < lcsarr[i][j]) {
// 存储匹配初始位置和匹配长度
max = lcsarr[i][j];
index = i;
}
}
}
console.log(lcsarr )
var str = "";
console.log(index,max)
if (max == 0) {
return "";
} else {
return word1.substr(index-max,max);
}
}
实现原理如下图所示:
为了方便理解,我们先实现前面两步,打印出连续的元素有哪些:
function lcs(word1, word2) {
var max = 0;
var index = 0;
// 声明一个二维数组,长宽分别为两个比较的字符串的长度,数组中所有内容为 0
var lcsarr = new Array(word1.length + 1);
for (var i = 0; i <= word1.length + 1; ++i) {
lcsarr[i] = new Array(word2.length + 1);
for (var j = 0; j <= word2.length + 1; ++j) {
lcsarr[i][j] = 0;
}
}
// 对角线匹配,如果遇到相等的,就对角线加一
for (var i = 0; i <= word1.length; ++i) {
for (var j = 0; j <= word2.length; ++j) {
if (i == 0 || j == 0) {
lcsarr[i][j] = 0;
} else {
if (word1[i-1] == word2[j-1]) {
// 这里直接输出 匹配到的字符
lcsarr[i][j] = word1[i-1]
// lcsarr[i][j] = lcsarr[i - 1][j - 1] + 1;
} else {
lcsarr[i][j] = 0;
}
}
if (max < lcsarr[i][j]) {
// 存储匹配初始位置和匹配长度
max = lcsarr[i][j];
index = i-1;
}
}
}
return lcsarr;
}
然后将打印出来的字符换为递增数字,并保留最后匹配到的位置,使用如下方法返回,即可打印出结果:
return {
'最长公共子串长度':max,
'最长公共子串内容':word1.substr(index-max,max)
};
背包问题是算法研究中的一个经典问题。试想你一些物品放入你的一个小背包中。物品的尺寸和价值不同。你希望自己的背包装进的宝贝总价值最大。
如果在我们例子中的保险箱中有5 件物品,它们的尺寸分别是3、4、7、8、9,而它们的价值分别是4、5、10、11、13,且背包的容积为16,那么恰当的解决方案是选取第三件物品和第五件物品,他们的总尺寸是16,总价值是23。
function max(a, b) {
return (a > b) ? a : b;
}
function knapsack(capacity, size, value, n) {
if (n == 0 || capacity == 0) {
// 如果背包容量或者物品总数为零,返回总价值 零
return 0;
}
if (size[n - 1] > capacity) {
// 从后往前,如果物品尺寸大于背包容量,就跳过
return knapsack(capacity, size, value, n - 1);
} else {
// 如果物品尺寸小于背包容量,就放进背包,计算物品价值
return max(value[n - 1] +
knapsack(capacity - size[n - 1], size, value, n - 1),
knapsack(capacity, size, value, n - 1));
}
}
函数 knapsack
接收四个参数,分别为 背包容量、物品尺寸、物品价值和物品数量,物品根据物品价值排序。
验证:
var value = [4, 5, 10, 11, 13];
var size = [3, 4, 7, 8, 9];
var capacity = 16;
var n = 5;
knapsack(capacity, size, value, n);
待补充