递归算法,好处就是逻辑简单,坏处就是消耗内存,容易造成栈溢出!一笔带过!
最近学习二叉树,发现递归用的很多,而且发现其中的递归的用法很接近栈层面的理解了!不同于在前端层面那种简单的数据嵌套的时候的处理了。
当初,学二叉树没有想出如何插入,遍历的方法。根源是卡在对left节点的操作入栈到了下一层的函数了,但是当前的right节点也需要处理啊,当前的right还没处理了!想想真的是惭愧!说真的,对递归的理解一直都是模棱两可,也就是业务层面处理一下嵌套结构的数据了!
中午,吃饭的时候,看了一下极客的算法视频,专门看了一下递归的。
这位老师其实很牛!讲的都是痛点!所以,我初步总结并树立一个良好的递归观念吧!
递归的认知
我认为,因为有重复性的操作是产生递归的根本原因!
抽离出重复性操作才是重中之重!
暴力递归,重于现象,就是傻递归!不动脑子型的!偏向于演绎法!
抽象出符号递归,重于内部关系,就是真递归!偏向于归纳法!
要会归纳出内部的实质关系,找到抽象递归!递归的问题就会转化,从而效率很高了!
而我总是喜欢演绎法,暴力递归,弄得效率相当地低。需要改进!
题目
吃苹果问题
这个问题最初接触是前一个月左右吧!当初一个前端的朋友,出题:
32个苹果,一个人吃,每次只能吃1个或者2个,一共多少种吃法吃完??我没记错的话,就这个意思了!
**
当初我还记得我的暴力演绎法,,,,
当初我的思路:
let arr = [[1], [2]], sums = [1, 2];
const x = num => {
const copy = [];
sums.map(
(sum, index) => {
if (sum) {
const item = [...arr[index]];
if (sum + 1 <= num) {
arr[index].push(1);
sums[index] += 1;
if (sums[index] === num) {
sums[index] = null;
}
}
if (sum + 2 <= num) {
item.push(2);
copy.push(item);
sums.push(sum + 2 === num ? null : sum + 2);
}
}
}
);
// console.log(arr,sums,copy);
arr = arr.concat(copy);
};
const eat = num => {
while (sums.filter(val => val).length) {
x(num);
}
};
eat(30);
console.log(arr.length);
//这是我当初的基本思路,代码写的可能没现在这版如此的清晰。
//当初,我执着于求出组合的具体内容,根本就不限于题目的结果,一共有多少种!
// 回归到题目,我可以再优化的:
{
let sums = [1, 2];
const y = num => {
const copy = [];
sums.map(
(sum, i) => {
if (sum === null) return;
if (sum === num) return sums[i] = null;
if (sum + 1 <= num) {
sums[i] += 1;
}
if (sum + 2 <= num) {
copy.push(sum + 2);
}
}
);
sums = sums.concat(copy);
};
const eat1 = num => {
while (sums.filter(val => val).length) {
y(num);
}
return sums.length
};
console.log(eat1(30));
}
都是演绎法,说句实话,我从不觉得这个思路错过!
当初,这个前端朋友弄得我很苦恼,毕竟他弄得我以为我思路错了!当初我相当不理解了,甚至有些郁闷,毕竟那哥们有些很那个,他不看你代码,不思考,也不听你解释,就是说你这想法错了!他喜欢舔其他他认为牛皮的人,嗯,对我来说算是挺受打击的。
不过我从不觉得自己错了:毕竟这是硬解,暴力演绎的,如果这都错了,我是无话可说了!
爬梯子问题
然后有意思事来了,我今天正好,看了一下递归的两个视频,看到那个爬梯子的题目,当时我就联想到他出的那个吃苹果的题了,一个思路的!然后,我也不看老师的讲解,准备再如今的角度去考虑,如何爬梯子:
给定n个阶梯,人上阶梯,一步可以上1个,或者两个,问多少种走法??
**
这道题直接让我想起了吃苹果的题了!然后我就立马直接开始硬推演绎法了,当时我也是木有看老师如何做题的思路的。
于是,自己原本想用递归实现暴力演绎法,结果错了,,,想想挺尴尬的,还专门给那个前端朋友发了,当然,他还是直接给我说你这不对,我反正是不清楚他有木有看思路,还是直接知道结果了。不管他。
错误的暴力演绎法
//错误的思路:
{
const storage = {count:0}
const x = num =>{
if (num === 0) return ;
if (num - 1 >= 0 ){
storage.count += 1 //错在计数的这里的逻辑!要根据上面的吃苹果的增加
x(num - 1)
}
if (num - 2 >= 0 ){
storage.count += 1 //错在计数的这里的逻辑!
x(num - 2)
}
}
x(2)
console.log(storage.count);
}
本来,这个演绎的思路其实很像二叉树的,当然不是搜索树:
1
2 1
1 2 1 2
**************
但是还是有个限制,就是一个路径的节点的和小于num!跟路径挂钩,就可以明白上面的计数哪里错了吧!
真正的纯暴力演绎法,还是要看吃苹果的那个方法!
修改:
const storage = {count:1}
let counts = [1,2]
const x = (num,lastChange) =>{
if (num - 1 >= 0 ){
// 这里不加1,所以走第一步的时候是1的情况,少了,我把初始的count值设为1,为底数。
x(num - 1,1)
}
if (num - 2 >= 0 ){
storage.count += 1 //因为像极了二叉树的链路,其实每个节点都可以分出两条路径,
//但是其中总有一条已经计算到count了
x(num - 2,2)
}
}
x(10)
console.log(storage.count);
测试
效率太低,导致超时了!这就是暴力递归,而且是演绎法的问题所在!
同理,其实吃苹果的那个代码就不再跑了。单个用例小于30多的,抽测几个都是对的。
正确的抽象思路
一般的抽象套路:穷举法
n =1 :
1 1
n=2 :
1,1; 2。 2
n =3 :
1,1,1; 1,2; 2,1。 3
n= 4 :
1111; 121; 112; 22; 211; 5 //数学归纳法,就要猜了,,,会不会是 f(n)=f(n-1)+f(n-2)?
n=5:
11111;1211;1121;221;2111;1112;122;212; 8 //还真是!!
开始抽象:
人走到第n级时,总是要先走到第n-1级,或是n-2级。这就是根本的抽象原理!
人走到第n-1级的情况跟人走到第n-2级的情况有木有冲突?木有!因为最后那一步迈出去的对应为1/2。
互斥,互补!这是最根本的!
转化问题
求的是f(n) = f(n-1) + f(n-2) 如此的多项式关系了!
const x = n => {
if (n === 1) return 1;
if (n === 2) return 2;
if (n >= 3) {
let last1=x(1), last2=x(2), i = 3;
while (i <= n - 1) {
i%2 !== 0 ? last1 += last2 :
last2 += last1
i++
}
return last2 + last1;
}
};
内存少,但是时效太差了!我觉得好奇怪啊哈哈!因为我也看了一下其他的人的代码思路,觉得差不多啊!我就想哪里的问题??
我突然看到我用的const,let了,,,我心里有些激灵了,立马试了试:
啥都不说了!!
总结
只要有重复操作的步骤,就尽可能地去抽象重复步骤,找到重复步骤跟下一次重复步骤之间的关系!
实在找不出来,就先穷举法,然后用数学归纳法去归纳,由一般到n,就找到重复步骤的抽象了!
关键结论,就是抽象出来的重复步骤之间的关系!
因为递归性能差,所以最好用尾递归,相当于迭代的意思了!
练习题
有效括号问题
这道题,要很好地理解题目要做什么?如何做到?
3对括号,就是3个左括号,三个右括号!
先不复杂化,先做出,三个左括号和三个右括号的左右的排列情况,然后再考虑其中哪个不合格!
第二个点就是,如何筛选不合规矩的排列:右括号不能大于左括号数!
尾递归
//括号问题 https://leetcode-cn.com/problems/generate-parentheses/
{
let arr = [
{
val: "(", left: 1, right: 0, ok: false
}
], res = [];
const x = (n) => {
while (arr.length) {
const arr1 = [];
arr.map(
({val, left, right, ok}, i) => {
if (ok) return;
if (right < n) { //这里有坑!第一顺序不可颠倒,否则提前拷贝;2,如果left满了,就不需要复制添加了
if (left < n) {
if (left < right) {
return arr[i].ok = null;
};
const copy = {val, left, right, ok};
copy.val += ")";
copy.right += 1;
if (right + 1 === n && left === n) {
copy.ok = true;
res.push(copy.val);
} else {
arr1.push(copy);
}
} else {
arr[i].val += ")";
arr[i].right += 1;
if (right + 1 === n && left === n) {
arr[i].ok = true;
res.push(val + ")");
}
}
}
if (left < n) {
if (right > left) {
arr[i].ok = null;
} else {
arr[i].val += "(";
arr[i].left += 1;
}
}
}
);
arr = arr.concat(arr1).filter(val => val.ok !== null && !val.ok);
}
};
x(n);
return res;
}
时效性还是太差了!!我想了想应该跟强制性地递归有关系的!
纯控制性递归思路
{
var res = [];
var x = ( n, s,left,right) => {
if (left === n && right === n) return res.push(s);
right<left && x(n,s+')',left,right+1);
left < n &&left>= right && x(n,s+'(',left+1,right);
};
x(3,'',0,0)
console.log(res);
}
不知道为什么,我总是喜欢尾递归,,,还不如递归来得快!涨涨记性!
递归求乘
var multiply = function(A, B) {
const min = A > B ? B : A;
const max = A > B ? A : B;
let i = 1, res = max;
if (i === min) return res;
const y = (i, res) => {
if (i === min) return res;
if (i + i <= min) {
res += res;
i += i;
} else { //只有两种分法,会让数据大的时候吃亏的。所以我尝试了优化了一个中庸版的
res += max;
i += 1;
}
return y(i, res);
};
return y(1, max);
};
const x = (a, b,) => {
const min = a > b ? b : a;
const max = a > b ? a : b;
let i = 1, res = max,max100,test,add;
if (min === 0) return 0;
if (i === min) return res;
if(min>1000){
if(min<10000){
test = i => i> 100 && i+100<=min
max100 =+( max+ '00')
add = 100
}else{
test = i => i>1000 && i+1000<=min
max100 = +( max+ '000')
add = 1000
}
}
const y = (i, res) => {
if (i === min) return res;
if (i + i <= min) {
res += res;
i += i;
}
else if(min>1000 && test(i)){
res += max100
i += add
} else {
res += max;
i += 1;
}
return y(i, res);
};
//这里有过测试,每增加一次判断,消耗增加,我为了适应后期大数量级,觉得应该填一个。
return y(1, max);
};