递归算法,好处就是逻辑简单,坏处就是消耗内存,容易造成栈溢出!一笔带过!
最近学习二叉树,发现递归用的很多,而且发现其中的递归的用法很接近栈层面的理解了!不同于在前端层面那种简单的数据嵌套的时候的处理了。
当初,学二叉树没有想出如何插入,遍历的方法。根源是卡在对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);}本来,这个演绎的思路其实很像二叉树的,当然不是搜索树:12 11 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 1n=2 :1,1; 2。 2n =3 :1,1,1; 1,2; 2,1。 3n= 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 += last1i++}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<=minmax100 =+( max+ '00')add = 100}else{test = i => i>1000 && i+1000<=minmax100 = +( 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 += max100i += add} else {res += max;i += 1;}return y(i, res);};//这里有过测试,每增加一次判断,消耗增加,我为了适应后期大数量级,觉得应该填一个。return y(1, max);};


