问题一:斐波那契数列
斐波那契数列,其最开始的几项是1、1、2、3、5、8、13、21、34…… ,后面的每一项是前两项之和,事实上,斐波那契在数学上有自己的严格递归定义。
f0 = 1 f1 = 1 f(n) = f(n-1) + f(n-2)
方法一:递归求解
function fib(n) {
if (n == 1 || n == 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
console.log(fib(10))
这个方式很容易想到,只要我们用代码实现数学上的递推公式就可以了。
产生问题
运行如下结果:
function fib(n) {
if (n == 1 || n == 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
console.time()
console.log(fib(10)) // 55 耗时:9.102ms
console.timeEnd()
console.time()
console.log(fib(44)) // 701408733 耗时:6.494s
console.timeEnd()
我们发现在计算第44项的时候,耗时竟然惊人的到达了6.5s中,那我们计算第100项,估计宇宙毁灭了都算不出来,那怎么办呢?
哪里影响的耗时
通过树形分解,我们发现我们进行了重复计算,计算了2次fib(3),3次fib(2)。 哦豁,原来如此,是重复计算导致的耗时,我们知道哪里引起的问题,我们就可以进行优化。
方法二:增加缓存
// 性能差
function fib1(n) {
const cache = [];
var res = helper(cache, n);
return res;
}
function helper(cache, n) {
if (n == 1 || n == 2) {
cache[n] = 1;
return 1;
}
if (cache[n]) return cache[n];
var res = helper(cache, n - 1) + helper(cache, n - 2);
cache[n] = res;
return res;
}
我们使用cache数组进行缓存,减少重复计算。
运行结果:
console.time()
console.log(fib1(10)) // 55 耗时:9.102ms
console.timeEnd()
console.time()
console.log(fib1(44)) // 701408733 耗时:9.599ms
console.timeEnd()
时间复杂度降下来了,完美。
继续测试
console.time()
console.log(fib1(144)) //5.5556540422429276e+29 耗时:9.229ms
console.timeEnd()
console.time()
console.log(fib1(14400)) // ????
console.timeEnd()
当计算第14400项是出现了如下情况 这是什么情况?我没有写死循环,竟然出现了死循环的报错???? 分析原因,发现是因为js开辟函数是有限制的,不允许创建过多的函数,函数达到一定的量级,便会认为是死循环。
那没办法,只能继续优化
方法三:使用循环,放弃递归
function fib2(n){
let dp = []
dp[1] = 1;
dp[2] = 2;
for(var i=3;i<=n;i++){
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n];
}
改成如上的代码,我们算数列的第一百万,都是轻轻松松。
总结; 在做量级很大的计算时,需要放弃递归,使用for或者while循环进行,不然会出现死循环的警告。
问题二:找零问题
对于现实生活中的找零问题,假设有数目不限,面值为20,10,5,1的硬币。 求出找零方案, 要求:使用数目最少的硬币。 对于此类问题,贪心算法采取的方式是找钱时,总是选取可供找钱的硬币的最大值。比如,需要找钱数为25时,找钱方式为20+5,而不是10+10+5。
这个思路也比较好想:我们拿 整钱 去面值中,从最大的开始找,如果不够找了,就找下一个面值,以此思路就可以找到解决方法了,这种思考在算法中叫贪心算法。
class Change1 {
constructor(changeType) {
// 倒序,大的数字在前面
this.changeType = changeType.sort((r1, r2) => r2 - r1);
this.cache = {};
}
mkChange(amount) {
const arr = [];
for (let i = 0; i < this.changeType.length; i++) {
let tem = this.changeType[i];
while (amount - tem >= 0) {
arr.push(tem);
amount -= tem;
}
}
return arr;
}
}
var c = new Change1([20, 10, 5, 1])
console.log(c.mkChange(126))
// 答案
[
20, 20, 20, 20,
20, 20, 5, 1
]
嗯,我们得出结果,找5个20的,一个5块,一个1一块,完成。
产生问题
当我们面值和找零发生变化是结果不一定对 比如说:如果提供找零的面值是11,5,1,找零15。
var c = new Change1([11,5,1])
console.log(c.mkChange(15)) [ 11, 1, 1, 1, 1 ]
但是我们通过正常分析,我们知道应该找3个五块才是正确答案。所以在这种情况下,贪心算法就没办法找到最优解了。
如果我们想要找最优解,需要使用动态规划。
分析
凑0块钱:因为0块钱根本不需要硬币,因此结果是0:f(0) = 0; 凑1块钱:因为有1块钱面值的硬币可以使用,所以可以先用一个1块钱硬币,然后再凑够0块钱即可,而f(0)的值是我们已经算出来了的,所以:f(1) = 1 + f(0) = 1 + 0 = 1,这里f(1) = 1 + f(0) 中的1表示用一枚1块钱的硬币;
凑2块钱:此时四种面额的硬币中只有1块钱比2块钱小,所以只能先用一个1块钱硬币,然后再凑够1块钱即可,而f(1)的值我们也已经算出来了,所以:f(2) = 1 + f(1) = 1 + 1 = 2,这里f(2) = 1 + f(1) 中的1表示用一枚1块钱的硬币;
凑3块钱:和上一步同样的道理,f(3) = 1 + f(2) = 1 + 2 = 3;
凑4块钱:和上一步同样的道理,f(4) = 1 + f(3) = 1 + 3 = 4;
凑5块钱:这时就出现了不止一种选择了,因为有5块钱面值的硬币。方案一:使用一个5块钱的硬币,再凑够0块钱即可,这时:f(5) = 1 + f(0) = 1 + 0 = 1,这里f(5) = 1 + f(0) 中的1表示用一枚5块钱的硬币;方案二:使用1个1块钱的硬币,然后再凑够4块钱,此时:f(5) = 1 + f(4) = 1 + 4 = 5。综合方案一和方案二,可得:f(5) = min{1 + f(0),1 + f(4)} = 1;
凑6块钱:此时也有两种方案可选,方案一:先用一个1块钱,然后再凑够5块钱即可,即:f(6) = 1 + f(5) = 1 + 1 = 2;方案二:先用一个5块钱,然后再凑够1块钱即可,即:f(6) = 1 + f(1) = 1 + 1 = 2。综合两种方案,有:f(6) = min{1 + f(5), 1 + f(1)} = 2;
…(省略)
从上面的分析过程可以看出,要凑够i块钱,就要考虑如下各种方案的最小值: 1 + f(i - value[j]),其中value[j]表示第j种(j从0开始,0 <= j < 4)面值且value[j] <= i 那么现在就可以写出状态转移方程了:
f(i) = 0, i = 0
f(i) = 1, i = 1
f(i) = min{1 + f(i - value[j])}, i > 1,value[j] <= i
代码
// 动态规划
class Change {
constructor(changeType) {
this.changeType = changeType;
this.cache = {};
this.typeCount = {};
this.fn = {};
}
// 找的 amount 块钱的最优解,并发他放在 cache 缓存住
mkChange(amount) {
let min = []; // 最后方案
if (!amount) {
return [];
}
if (this.cache[amount]) {
return JSON.parse(this.cache[amount]);
}
for (let i = 0; i < this.changeType.length; i++) {
// 先找一张试试
const hasAmount = amount - this.changeType[i];
let newMin; // 临时方案
if (hasAmount >= 0) {
//继续找钱
newMin = this.mkChange(hasAmount);
}
if (hasAmount >= 0) {
if (newMin.length < min.length) {
min = [this.changeType[i]].concat(newMin);
}
if (min.length === 0) {
// 如果最优解不存在,那么他本上就是最优解
min = [this.changeType[i]].concat(newMin);
}
}
}
this.cache[amount] = JSON.stringify(min);
return min;
}
mkChangefn(amount, limit = 100) {
let calc = 0;
while (amount > calc) {
calc += 1;
this.mkChange(calc);
}
return this.mkChange(amount).join(",");
}
mk(amount) { // 使用自底向上计算
// let max = amount + 1;
let dp = new Array(amount + 1).fill(null);
dp[0] = []
for (let i = 1; i <= amount; i++) {
for (let j = 0; j < this.coins.length; j++) {
if (this.coins[j] <= i && dp[i - this.coins[j]]) {
if (dp[i]) {
var tem = [this.coins[j], ...(dp[i - this.coins[j]])]
if (dp[i].length > tem.length) {
dp[i] = tem
}
} else {
dp[i] = [this.coins[j], ...dp[i - this.coins[j]]]
}
}
}
}
// console.log(dp[amount])
return dp[amount] ? dp[amount].length : -1;
}
}
var c = new Change([11, 5, 1])
console.log(c.mkChangefn(15)) // 5,5,5