2019.12.03 add Manchester algorithm
2019.12.03 add Graph
2019.12.03 add 有向图的最长路径问题
2019.12.24 add 二分法
2019.12.27 add 牛顿迭代法
2019.12.30 add 二分查找数组旋转点
2019.12.31 add 排序算法的应用
2020.02.02 add 堆或链表的应用-设计推特javascript
经常使用计算公式:html
首先要说的是,时间复杂度的计算并非计算程序具体运行的时间,而是算法执行语句的次数。执行算法所须要的计算工做量。
当咱们面前有多个算法时,咱们能够经过计算时间复杂度,判断出哪个算法在具体执行时花费时间最多和最少。java
解释:时间频度–>时间复杂度:一个算法花费的时间与算法中语句的执行次数成正比例。T(n),n表示问题的规模node
计算方式:一般咱们计算时间复杂度都是计算最坏状况git
①选取相对增加最高的项:计算基本语句(执行次数最多的语句)执行次数的数量级。(通常就是找循环体)
②最高项系数是都化为1
③如果常数的话用O(1)表示es6
空间复杂度是对一个算法在运行过程当中临时占用存储空间大小的量度。web
①忽略常数,用O(1)表示
②递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
③对于单线程来讲,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,由于递归最深的那一次所耗费的空间足以容纳它全部递归过程。正则表达式
避免浮躁/循序渐进,不投机取巧,按部就班算法
难度大的算法题目如何解?segmentfault
算法的本质就是寻找规律并实现
如何找到规律?
发现输入和输出的关系,寻找突破点
复杂的实现怎么办?
实现是程序和数据结构的结合体。
步骤:
找规律===>伪代码===>源代码
在给的测试用例输入不多的状况,本身把输入变长来测试。
难点:
//大整数相加 var bigNum=function(a,b){ var aa=a.split('').reverse(); var bb=b.split('').reverse(); var temp=0; var res=[]; var len=aa.length>bb.length?aa.length:bb.length; for(var i=0;i<len;i++){ aa[i]=(aa[i]?aa[i]:0); bb[i]=(bb[i]?bb[i]:0); var res0=parseInt(aa[i])+parseInt(bb[i])+temp; temp=res0>=10?1:0; res.unshift(res0%10); } if (temp) {res.unshift(1);} return res.join(''); }
代码过于冗长
占用内存较大
思路基本正确
处理点:
改进版代码:
var addBigNum = function (a, b) { var res = '', c = 0; a = a.split(''); b = b.split(''); while (a.length || b.length || c) { c += ~~a.pop()+~~b.pop(); //c +=(a.pop()|0)+(b.pop()|0); res = c % 10 + res; c = c > 9; //c=c>9 } return res }
知识点整理:
"~"的做用是将数字转化为有符号32位整数并做位取反, 位取反即把数字转换为2进制补码而后把0和1反转.
那么:双波浪线的做用就是把一个小数舍弃小数点转换为整数,在数字较小转换为32位整数时不会溢出的状况下能够看成Math.floor的偷懒写法。可是位或0的方式写起来更通常。
总结:做用:
//链表类 function ListNode(val) { this.val = val; this.next = null; } //数组转链表 var changeToLink=function(arr){ var link=new ListNode(arr[0]); var cur=link; arr.splice(0,1); while(arr.length){ cur.next=new ListNode(arr[0]); cur=cur.next; arr.splice(0,1); } return link; } //改进版:既然每一次都使用上一次的next那么必须用reduce岂不是更加方便: var changeToLink0=function(arr){ var link0=new ListNode(arr.pop()); return arr.reduceRight((res,cur)=>{ var node=new ListNode(cur); node.next=res; return node; },link0) } //为何时reduceRight而不是reduce呢?思考一下这个采坑点:用reduce时从左到右缩减,返回只能是res.next,可是最后缩减完毕就是只剩下最后一个数了,因此用reduceRight以后能从右向左挂上去,最后返回一条完整的链。 //链表转数组 var changeToArray=function(link){ var res=[]; var cur=link; res.push(cur.val); cur=link.next; while(cur){ res.push(cur.val); cur=cur.next; } return res; } //核心思想也是不停push,让cur不停后移
**采坑点 ** :
关键点:使用正则去首位的0,Math.pow(2,31)判断溢出
建议:正则很差用,建议用数组的 shift+while 检验
对每一个可能的分支路径深刻到不能再深刻为止,并且每一个节点只能访问一次。
深度优先搜索算法:
概念 | 特色 | |
---|---|---|
深度优先 | 对每一个可能的分支路径深刻到不能再深刻为止,并且每一个节点只能访问一次 | 不所有保留节点,占用空间小;有回溯操做,及入栈出栈,运行速度慢 |
广度优先 | 从上向下对每一层依次访问,每一层从左往右访问节点,访问完一层就进入下一层,直到没有节点能够访问为止。 | 保留所有节点,占用空间少,无回溯操做,运行速度快 |
const reConstructBinaryTree0=(vin,post)=>{ if(vin.length==0||post.length==0){ return null; } const index=vin.indexOf(post[post.length-1]); const left=vin.slice(0,index); const right=vin.slice(index+1); const newPost0=post.slice(0,index); const newPost1=post.slice(index,post.length-1); const node=new TreeNode(post[post.length-1]); node.left=reConstructBinaryTree0(left,newPost0); node.right=reConstructBinaryTree0(right,newPost1); return node; }
如何更好的覆盖全部的边界状况,减少代码的冗余度?
/* * 在处理这个问题上出现了较大的误差,利用栈的方式没有错 * 可是首先对字符串进行了一系列操做后致使边界状况没法很好的覆盖 * 其实只要考虑好全部的状况就能够了,不该该遍历的时候只去处理两个点的问题,尤为在/..abc这种状况时 * 彻底能够经过利用/去分割字符串的方式解决 */
辅助栈和原栈同步的方式实现追踪最小值
/** * 首先这个题目看似简单,但其实也有一些奥妙在里面,首先要求在常数时间内找到最小的元素 * 这就意味着时间复杂度为O(1) * 所以咱们应该在push数据的时候就已经作好最小数据的检测,所以利用辅助栈来实现获取最小值,同时辅助栈和原栈同步, * 只是在push数据的时候须要额外判断push哪个数。同时由于pop操做的存在,不能使用this.min来定义最小值,故而辅助栈是最好的选择 */ var MinStack = function() { this.data=[]; this.temp=[]; }; /** * @param {number} x * @return {void} */ MinStack.prototype.push = function(x) { if(this.temp.length<1||this.temp[this.temp.length-1]>x){ this.temp.push(x); }else{ this.temp.push(this.temp[this.temp.length-1]); } this.data.push(x); }; /** * @return {void} */ MinStack.prototype.pop = function() { this.data.pop(); this.temp.pop(); }; /** * @return {number} */ MinStack.prototype.top = function() { return this.data[this.data.length-1]; }; /** * @return {number} */ MinStack.prototype.getMin = function() { return this.temp[this.temp.length-1]; };
接下来的题都不一样程度的和链表有关系
链表的题中一个重要的特色就是移动指针,以及增断链。
const flatten1=root=>{ let temp=[],pre=null; root&&temp.push(root); while(temp.length){ let tempTree=temp.pop(); // root.right=tempTree; // root.left=null; if(pre!==null){ pre.right=tempTree; pre.left=null; } tempTree.right&&temp.push(tempTree.right); tempTree.left&&temp.push(tempTree.left); pre=tempTree; } console.info(root); }; const flatten2=root=>{ let pre=null; const recur=root=>{ if(!root) return; recur(root.right); recur(root.left); root.right=pre; root.left=null; pre=root; }; recur(root); console.info(root); }; const flatten3 = root=>{ if(!root) return null; const stack = []; while(root.left || root.right || stack.length>0){ if(root.right) stack.push(root.right); if(root.left){ root.right = root.left; root.left = null; }else{ root.right = stack.pop() } root = root.right } };
/** * 如此就写出来了一种非递归的写法 * 主要经过以其中一个链表为标准依次遍历向其中添加另外一个链表中数据的方式 * 时间复杂度O(m+n),空间复杂度O(1) * @param l1 * @param l2 * @returns {null|*} */ const mergeTwoLists1=(l1,l2)=>{ // if(!l1&&!l2){ // return l1; // }else if(!l1){ // return l2; // }else if(!l2){ // return l1; // } // 对上面的代码进行简化 if(!l1) return l2; if(!l2) return l1; let res = new ListNode(),res0=res; while(l1){ let temp1=l1.next; while(l2){ let temp2=l2.next; if (l2.val>=l1.val){ l1.next=l2; res0.next=l1; res0=res0.next; break; }else{ res0.next=l2; res0=res0.next; l2=temp2; } } if(!l2){ res0.next=l1; res0=res0.next; } l1=temp1; } console.info(res.next); return res.next; }; mergeTwoLists1(list1,list2); /** * 下面是对上面这段代码的优化,实际上咱们老是在给res0向后追加 * 改进后的代码更加清晰简明扼要 * 思路的本质是迭代 */ const mergeTwoLists2=(l1,l2)=>{ let res=new ListNode(),res0=res; while(l1&&l2){ if(l1.val>=l2.val){ res0.next=l2; l2=l2.next; }else{ res0.next=l1; l1=l1.next; } res0=res0.next; } res0.next=!l1===true?l2:l1; return res.next; }; /** * 递归实现上面的内容,递归的关键是输入,输出还有边界条件 * 可是这个想法过于巧妙 * @param l1 * @param l2 */ const mergeTwoLists3=(l1,l2)=>{ if(!l1) return l2; if(!l2) return l1; if(l1.val>=l2.val){ // 以l2为标准进行融合 l2.next=mergeTwoLists3(l1,l2.next); return l2; }else{ // 以l1为标准进行融合 l1.next=mergeTwoLists3(l1.next,l2); return l1; } };
/** * leetcode-148解决方案 * 巧妙的利用两个指针,一个遍历一个负责移动位置 * @param head * @returns {*} */ var sortList = function(head) { var swap=(n,m)=>{ var val=n.val; n.val=m.val; m.val=val; }; var sort=(start,end)=>{ let val=start.val; let p=start.next; let q=start; while(p!==end){ if(p.val<val){ q=q.next; swap(p,q); } p=p.next; } swap(start,q); return q; }; var sort0=(start,end)=>{ if(start!==end){ let temp=sort(start,end); sort0(start,temp); sort0(temp.next,end); } }; sort0(head,null); return head; };
一样是用双指针来解决一些方法的定义,经过对比首尾指针来肯定数据是否满或空,须要领会这个思想。
相似于选择排序中定义好最小值而后进行寻找比它小的值的算法
可是他们只接受可变长度的参数列表,不接受直接传递数组的形式,能够用apply的方法改变传参的方式:
Math.min.apply(null,arr); //或者 Math.min.apply(Math,arr);
即:call 和 apply的做用是什么?除了改变函数的this指向外,还有什么?—apply能够改变传递给函数参数的形式(其实我认为也是改变this指向的一种应用)
关键点:
$1, …, $9 属性是静态的, 他不是独立的的正则表达式属性. 因此, 咱们老是像这样子使用他们RegExp.$1
, …, RegExp.$9
.
可是在str.replace中也能够直接用$1, … ,9等
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/n
str.repeat使用:
"abc".repeat(2) // "abcabc"
针对leetcode上出现的正则表达式过长的现象
var countBinarySubstrings = function(str) { let r = [] // 给定任意子输入都返回第一个符合条件的子串 let match = (str) => { let j = str.match(/^(0+|1+)/)[0] let o = (j[0] ^ 1).toString().repeat(j.length) if(str.substr(j.length,j.length)===o) return true; } // 经过for循环控制程序运行的流程 for (let i = 0, len = str.length - 1; i < len; i++) { let sub = match(str.slice(i)) if (sub) { r.push(sub) } } return r.length };
抛出问题:若是我输入2,3,4呢,不是写三个for循环这么垃圾的算法的,要先算出2,3再跟4组合,显然最后的方案是:递归
本题:我暴露出来的缺点是映射没有作好,第一个想到的是object.keys object.value显然这是不对的,把算法作复杂的…
关键点:
var letterCombinations = function(digits) { var map=['','','abc','def','ghi','jkl','mno','pqrs','tuv','wxyz']; if(digits.length===0) return []; if(digits.length===1){ // 本身写的冗余 // return arr0[0].toString().split(''); return map[digits].split(''); }; // 递归能够不递归最外层的主函数,而是内部的函数 var arr=digits.split(''); var arr0=arr.map((item)=>{ return map[item]; }) var combine=function(arr0){ // 结果数组 var res=[]; for(var i=0;i<arr0[0].length;i++){ for(var j=0;j<arr0[1].length;j++){ res.push(`${arr0[0][i]}${arr0[1][j]}`); } } if (arr0.length>2){ arr0.splice(0,2,res); res=combine(arr0); } return res; } return combine(arr0); };
关键点:
找最大公约数的一个规律:
老是在寻找b和d(其中a%b=d)
注意寻找规律:
d=0:最大公约数就是b,f=0:最大公约数就是d
于是仍是用递归的方式来进行;
var GCD=(a,b)=>{ if(b===0){ return a; }else { return GCD(b,a%b); } }
找重复的数字:
str.match(/(\d)\1+|(\d)/g);//匹配全局中aaa或者a(单个字符)
知识点: 正则中\1的使用:匹配的是第一个()分组匹配的引用,即第一个分组()重复
let temp={}; arr.forEach(item=>{ temp[item]=temp[item]?temp[item]+1:1; });
出现的问题:复杂度过高,用了递归来实现,分别判断了数组长度为1,数组的第一位,数组的最后一位。
var canPlaceFlowers = function(flowerbed, n) { if (n<0||flowerbed.length<=0) return null; if (n==0) return true; if (flowerbed.length==1){ if (flowerbed[0]==0){ flowerbed.splice(0,1,1); return canPlaceFlowers(flowerbed,n-1); }else return false; } if (flowerbed[0]==0&&flowerbed[1]==0){ flowerbed.splice(0,1,1); return canPlaceFlowers(flowerbed,n-1); } if (flowerbed[flowerbed.length-1]==0&&flowerbed[flowerbed.length-2]==0){ flowerbed.splice(flowerbed.length-1,1,1); return canPlaceFlowers(flowerbed,n-1); } for (var i=1;i<flowerbed.length-1;i++){ if (flowerbed[i]==0&&flowerbed[i-1]==0&&flowerbed[i+1]==0){ flowerbed.splice(i,1,1); return canPlaceFlowers(flowerbed,n-1); } } return false; };
最佳方案:
var canPlaceFlowers = function(arr, n) { // 计数器 let max = 0 // 右边界补充[0,0,0],最后一块地能不能种只取决于前面的是否是1,因此默认最后一块地的右侧是0(无须考虑右侧边界有阻碍)(LeetCode测试用例) arr.push(0) for (let i = 0, len = arr.length - 1; i < len; i++) { if (arr[i] === 0) { if (i === 0 && arr[1] === 0) { max++ i++ } else if (arr[i - 1] === 0 && arr[i + 1] === 0) { max++ i++ } } } return max >= n }
此时只去判断可以插入的个数和想要插入的个数大小就能够了,复杂度直接将为n;
关键点:对于我考虑的已经加入新节点的位置不用再审查的问题,并不须要必定去操做数组,能够经过在for循环中再次经过i++越过这一位。
这个题暴露的问题仍是规律没有找好:
本身在找规律这方面实在是特别的欠缺。
这个题暴露出一个问题:思路基本正确,可是仍是想的过于崎岖,虽然本身写的时间复杂度也是n,可是空间复杂度太大,过多的操做数组
// 想起了快排的思路 var sortArrayByParityII=function(arr){ if (arr.length<=0||arr.length%2!=0) return null; if (arr.length==1) return A; var single=[],double=[],res=[]; arr.forEach((item)=>{ if (item%2!=0){ single.push(item); }else double.push(item); }) console.info(single,double); for (var i=0;i<arr.length;i+=2){ res[i]=double.pop(); } for (var j=1;j<arr.length;j+=2){ res[j]=single.pop(); } return res; }
改进版:
var sortArrayByParityII=function(arr){ if(arr.length<=0||arr.length%2!=0) return null; if(arr.length==1) return arr; var res=[]; // arr.sort((a,b)=>a-b); var even=0; var odd=1; arr.forEach((item)=>{ if(item%2==0){ res[even]=item; even+=2; }else{ res[odd]=item; odd+=2; } }) return res; }
优化方案:
使用sort其实把全部的数都遍历一次:而它内部的原理又是什么样的呢?
老是比较相邻的两个元素,相比a-b>0则交换位置,不然不交换位置。(有点相似于冒泡排序,而且在查找资料的时候发如今java中Array.sort原理会按数组的长度相应的使用对应的排序算法,而在MDN的解释上:js中,算法叫in-place algorithm就地算法)
就地算法(在快排的优化中也会用到)
优化方案:
区别于普通思路中的:
先利用sort函数进行排序,而后对排序后的数组进行一次遍历筛选,
在优化方案中利用冒泡排序中每一轮都能排出最大的值这一特色,边排序边比较;
另外
console.info(undefined-10);//NaN console.info(undefined-undefined);//NaN console.info(typeof null);// object
针对冒泡排序的写法(外层i的数量)有相应额解决方案:
const maximumGap = (nums)=>{ if (nums.length<2){ return 0; } let min=0; // i是从nums.length开始的 for (let i=nums.length;i>0;i--){ for (let j=0;j<i;j++){ if (nums[j]>nums[j+1]){ let temp=nums[j]; nums[j]=nums[j+1]; nums[j+1]=temp; } } if (nums[i]-nums[i-1]>min){ min=nums[i]-nums[i-1]; } } return min; }
const maximumGap = (nums)=>{ if (nums.length<2){ return 0; } let min=0; // i是从nums.length-1开始的 for (let i=nums.length-1;i>0;i--){ for (let j=0;j<i;j++){ if (nums[j]>nums[j+1]){ let temp=nums[j]; nums[j]=nums[j+1]; nums[j+1]=temp; } } // console.info(nums,i); if (nums[i+1]-nums[i]>min){ min=nums[i+1]-nums[i]; // console.info('testing==>',min); } } return Math.max(min,nums[1]-nums[0]); }
const firstMissingPositive = (nums)=>{ nums=nums.filter((item)=>item>0); if(nums.length<1) return 1; for (let i=0;i<nums.length;i++){ let min=i; for (let j=i+1;j<nums.length;j++){ if (nums[j]<nums[min]){ min=j; } } let temp=nums[min]; nums[min]=nums[i]; nums[i]=temp; // console.info('==>',nums); if (nums[0]>1) return 1; // console.info(nums[i]-nums[i-1]); if (nums[i]-nums[i-1]>1){ return nums[i-1]+1; } } // 若是是排好顺序的 return nums[nums.length-1]+1; }
/** * 查看是否有重复的子字符串组成,若是是返回true * 若是没有返回false * 我最初写的这个代码时间复杂度太高, * 下面列出来用正则和一种很牛逼的方法写的答案 */ const repeatedSubstringPattern=str=>{ for (let i=1;i<str.length;i++){ let temp=str.slice(0,i); let num=Math.ceil(str.length/i); if (num!==1&&temp.repeat(num)===str){ // console.info(temp,i,j); return true; } } return false; }; /** /w匹配一个单字字符(字母、数字或者下划线)。等价于 [A-Za-z0-9_]。 /W匹配一个非单字字符。等价于 [^A-Za-z0-9_]。 */ const repeatedSubstringPattern0=str=>{ return (/^(\w)\1+$/).test(str); }; /** * str+str 截取第一位日后和倒数第一位往前必定会包含以前的字符串 * 真是神操做啊 * @param str * @returns {boolean} */ const repeatedSubstringPattern1=str=>{ return (str+str).slice(1,str.length*2-1).indexOf(str)!==-1; }; let str='aba'; console.info(repeatedSubstringPattern0(str));
leetcode-10
分不一样状况来解决这个问题:
首先其实整体思路仍然是应用递归:
边界条件:s 和 p是否长度为0,由于第一种状况的存在,应该在p长度为0的时候检测s的长度.
输入: s和p
输出:裁剪后s和p
规则:
首先检测s和p的首字母是否同样,若是同样则置标志位为true.
关于标志位为什么必定要存在:
由于咱们分两种状况讨论:
所谓有模式:即存在✳️,存不存在点 只须要一句话检查一下便可,而✳️确定不能在第一位,只能在第二位。因此s p第一位要先核对,把它单独拿出来。
有模式状况下还分两种状况:
直接裁剪,而后递归重复整个流程
const isMatch=(s,p)=>{ const match=(s,p)=>{ if (p.length===0){ return s.length === 0; } let flag=false; if (s.length>0&&(p[0]===s[0]||p[0]==='.')){ flag=true; } if (p.length>1&&p[1]==='*'){ // p *后没法匹配s // 则继续检查是否是*前的内容是否出现了屡次 return match(s,p.slice(2))||(flag&&match(s.slice(1),p)); }else{ return flag&& match(s.slice(1),p.slice(1)); } }; return match(s,p); };
const quickSortH=(arr)=>{ const swap=(arr0,i,j)=>{ // 交换两个数的位置 let temp=arr0[i]; arr0[i]=arr0[j]; arr0[j]=temp; console.info('swap',arr0); } const findPivot=(arr,left,right)=>{ // 寻找基准,left和right指的是本次递归的过程当中的左下标和右下标 let flag=arr[left]; let idx=left+1;// 交换的下角标 for (let i=idx;i<=right;i++){ if (arr[i]<flag){ swap(arr,i,idx); idx++;// 交换结束后,交换的下角标应该日后移动一位 } } console.info('idx',idx); // 最后要交换基准和标尺的第一位 swap(arr,idx-1,left); console.info('find',arr); return idx; } const sort=(arr,left,right)=>{ if(left<right) { let pivot=findPivot(arr,left,right); console.info(arr); sort(arr,left,pivot-1); sort(arr,pivot,arr.length-1); } } sort(arr,0,arr.length-1); return arr; } const my_arr=[7,1,3,2,8,0,5]; console.info('res===>',quickSortH(my_arr));
原理分析 |
---|
1.pivot:也就是快排的基准,以后的全部元素都是跟它进行对比 |
2.left:本次遍历的起始指针,通常pivot选为arr[left] |
3.right:本次遍历的结束指针 |
4.idx:交换的角标,每次交换都是在left的后一位,并且每次交换结束后这个角标应该日后加一个 |
5.一轮结束后就根据基准分出了左右,而基准在哪里了呢,因此还要进行一次交换就是left和idx-1的交换,将基准放在交换角标的前面一个位置 |
6.最后一步进行递归,也就是左侧left,idx-1,右侧idx,arr.length-1 |
更多关于堆的总结请参看个人整理:堆
和冒泡排序和选择排序同样,构建最大堆:每次老是从最后一个根节点开始比较,每次排序一遍以后总能找出最大的值,而后将这个最大值和堆的最后一个数交换,并把最大值剔除,此时又破坏了堆的结构,而后进行从新排序。最后只剩下一个节点的时候就排序完成。
超级丑数是指全部质因数都是长度为k的质数列表primes中的正整数
解题顺序:求质因数(找约数–>是否为质数)–>是否在列表primes中–>是否达到指定个数n
难点:
这个题能够用堆排序来实现,使用堆排序。
学到了两点:
map[Symbol.iterator]===map.entries
Iterator 接口的目的,就是为全部数据结构,提供了一种统一的访问机制,即for…of循环(详见下文)。当使用for…of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具备Symbol.iterator属性,就能够认为是“可遍历的”(iterable)。Symbol.iterator属性自己是一个函数,就是当前数据结构默认的遍历器生成函数。更多内容能够访问阮一峰es6
本题是一个很是有挑战的设计题也是一个算法题;关于算法部分具体解题思路参看个人题解
思路讲解:
能够先看第一圈,每次永远在重复的查找一圈,化难为简。
学习总结:
这个题主要思路仍是观察找规律,实际上是一个矩阵的转置加一些交换操做。时间复杂度为O(n^2)
关于图的各类基本算法:涉及BFS,DFS,拓扑排序,Hierholzer算法等。
参考个人另外一篇Blog:
/** * 这个题剥去题意自己,考虑各类除法,能够认为是一个无向图的问题 * 首先创建邻接表,对queries里面的数组定义为start和end * 对邻接表从起点开始进行dfs,同时注意:dfs时没法返回,只能在合适的条件收集结果 * 必须用回溯的缘由是起点和终点固定住了,而不是须要遍历全部元素,所以,须要回溯 */ const calcEquation = (equations, values, queries)=>{ // initialize adj let adj={},res0=1,ans=[],res; for(let i=0;i<equations.length;i++){ if(!adj[equations[i][0]]){ adj[equations[i][0]]=[[equations[i][1],values[i]]]; }else{ adj[equations[i][0]].push([equations[i][1],values[i]]); } if(!adj[equations[i][1]]){ adj[equations[i][1]]=[[equations[i][0],Number((1/values[i]))]]; }else{ adj[equations[i][1]].push([equations[i][0],Number((1/values[i]))]); } } const dfsGetVal=(st,ed,adj,map)=>{ if(st===ed){ res=res0; return; } map.set(st,true); for(let i=0;i<adj[st].length;i++){ if(!map.has(adj[st][i][0])) { res0*=adj[st][i][1]; dfsGetVal(adj[st][i][0],ed,adj,map); // 回溯 res0/=adj[st][i][1]; } } }; // 而后进行深度优先探索找到值 for(let i=0;i<queries.length;i++){ res0=1.0; if(!adj[queries[i][0]]||!adj[queries[i][1]]){ res=-1.0; }else{ dfsGetVal(queries[i][0],queries[i][1],adj,new Map()); } // console.info(res); if(res===undefined){ ans[i]=-1.0; }else{ ans[i]=res; } } console.info(ans); return ans; };
=====> 无向图的最长路径:
/** * 寻找无向图的最长路径,由于是无向图因此通过检测没法使用拓扑排序这种依赖顺序的排序 * @param n * @param edges */ const longestPathUDG=(n,edges)=>{ let adj=[],leaves=[],maxLen=0,path=[]; for(let i=0;i<edges.length;i++){ if(!adj[edges[i][0]]){ adj[edges[i][0]]=[edges[i][1]]; }else{ adj[edges[i][0]].push(edges[i][1]); } if(!adj[edges[i][1]]){ adj[edges[i][1]]=[edges[i][0]]; }else{ adj[edges[i][1]].push(edges[i][0]); } } for(let i=0;i<n;i++){ if(adj[i].length===1){ leaves.push(i); } } const search=(start,adj,map,res)=>{ res.push(start); // 判断最后一个访问的顶点是叶子节点而且它的邻接点已经被访问过了 if(adj[start].length===1&&map.has(adj[start][0])){ if(res.length>maxLen){ path=JSON.parse(JSON.stringify(res)); } // console.info(res); } map.set(start,true); for(let i=0;i<adj[start].length;i++){ if(!map.has(adj[start][i])){ search(adj[start][i],adj,map,res); // 回溯 res.pop(); } } }; for(let i=0;i<leaves.length;i++){ search(leaves[i],adj,new Map(),[]); } return path; }; console.info(longestPathUDG(7,[[0,1],[1,2],[1,3],[2,4],[3,5],[4,6]])); // [6, 4, 2, 1, 3, 5]
=====> 有向图的最长路径:
本题结论:
/* * 每一个单元格能够看做图 G 中的一个定点。 * 若两相邻细胞的值知足 a < b,则存在有向边 (a, b)。问题转化成: * 求有向图的最长路径长度问题 首先这道题是归类到拓扑排序条目下,可是目前想不到和拓扑排序相关的内容。 首先递增的路径能够是多个的,至关于求一组解,所以能够考虑回溯法 */ const longestIncreasingPath = (matrix)=>{ if(matrix.length<1) return matrix.length; let res=[],ans=0; const check=(i,j)=>{ return i <= matrix.length - 1 && i >= 0 && j <= matrix[0].length - 1 && j >= 0; }; const search=(matrix,i,j,flag)=>{ res.push(matrix[i][j]); if(res.length>=1){ if(res.length>ans){ ans=res.length; } // console.info(res); } // 应该有四个方向可取 // code review // 下方向 if (check(i+1,j)){ let ck=(matrix[i+1][j]>matrix[i][j]&&flag)||(matrix[i+1][j]<matrix[i][j]&&!flag); if(ck){ search(matrix,i+1,j,flag); // 回溯 res.pop(); } } // 上方向 if(check(i-1,j)){ let ck=(matrix[i-1][j]>matrix[i][j]&&flag)||(matrix[i-1][j]<matrix[i][j]&&!flag); if(ck){ search(matrix,i-1,j,flag); res.pop(); } } // 右方向 if(check(i,j+1)){ let ck=(matrix[i][j+1]>matrix[i][j]&&flag)||(matrix[i][j+1]<matrix[i][j]&&!flag); if(ck){ search(matrix,i,j+1,flag); res.pop(); } } // 左方向 if(check(i,j-1)){ let ck=(matrix[i][j-1]>matrix[i][j]&&flag)||(matrix[i][j-1]<matrix[i][j]&&!flag); if(ck){ search(matrix,i,j-1,flag); res.pop(); } } }; for (let i=0;i<matrix.length;i++){ for (let j=0;j<matrix[0].length;j++){ res=[]; search(matrix,i,j,true); res=[]; search(matrix,i,j,false); } } console.info(ans); return ans; }; /** * 优化1:只寻找递增的路径 * @param matrix * @returns {number|*} */ const longestIncreasingPath1 = (matrix)=>{ if(matrix.length<1) return matrix.length; let ans=0; const check=(i,j)=>{ return i <= matrix.length - 1 && i >= 0 && j <= matrix[0].length - 1 && j >= 0; }; const search=(matrix,i,j)=>{ res.push(matrix[i][j]); if(res.length>=1){ if(res.length>ans){ ans=res.length; } // console.info(res); } // 应该有四个方向可取 // code review // 下方向 if (check(i+1,j)){ if(matrix[i+1][j]>matrix[i][j]){ search(matrix,i+1,j); res.pop(); } } // 上方向 if(check(i-1,j)){ if(matrix[i-1][j]>matrix[i][j]){ search(matrix,i-1,j); res.pop(); } } // 右方向 if(check(i,j+1)){ if(matrix[i][j+1]>matrix[i][j]){ search(matrix,i,j+1); res.pop(); } } // 左方向 if(check(i,j-1)){ if(matrix[i][j-1]>matrix[i][j]){ search(matrix,i,j-1); res.pop(); } } }; for (let i=0;i<matrix.length;i++){ for (let j=0;j<matrix[0].length;j++){ res=[]; search(matrix,i,j); } } console.info(ans); return ans; }; /** * 优化1:只寻找递增的路径 * 优化2:四个方向用数组表示使代码简洁 * dfs返回每次选取路径的长度,这样就没必要占用太多空间构建数组了 * 此方法仍属于暴力深度优先搜索 * 时间复杂度:O(2^(m+n))。对每一个有效递增路径均进行搜索。在最坏状况下,会有O(2^(m+n))次调用。 * 空间复杂度:O(mn)。对于每次深度优先搜索,系统栈须要 O(h) 空间,其中 h 为递归的最深深度。最坏状况下为mn * @param matrix * @returns {number|*} */ const longestIncreasingPath2 = (matrix)=>{ if(matrix.length<1) return matrix.length; let ans=0,dir=[[-1,0],[0,1],[1,0],[0,-1]]; const check=(i,j)=>{ return i <= matrix.length - 1 && i >= 0 && j <= matrix[0].length - 1 && j >= 0; }; const search=(matrix,i,j)=>{ let res=0; // 应该有四个方向可取 for(let k=0;k<dir.length;k++){ if(check(i+dir[k][0],j+dir[k][1])&&matrix[i+dir[k][0]][j+dir[k][1]]>matrix[i][j]){ res=Math.max(search(matrix,i+dir[k][0],j+dir[k][1]),res); } } // 返回时应该加上起点 return res+1; }; for (let i=0;i<matrix.length;i++){ for (let j=0;j<matrix[0].length;j++){ ans=Math.max(ans,search(matrix,i,j)); } } console.info(ans); return ans; }; /** * 优化1:只寻找递增的路径 * 优化2:四个方向用数组表示使代码简洁 * 优化3:利用数组缓存每一个点的路径长度 * 时间复杂度:O(mn).每一个顶点均计算且只计算一次,每条边也有且只计算一次,总时间复杂度是 O(V+E)。 * V 是顶点总数,E 是边总数。本问题中,O(V) = O(mn),O(E) = O(4V) = O(mn)。 * @param matrix * @returns {number|*} */ const longestIncreasingPath3 = (matrix)=>{ if(matrix.length<1) return matrix.length; let ans=0,dir=[[-1,0],[0,1],[1,0],[0,-1]],temp=[]; for (let i=0;i<matrix.length;i++){ temp[i]=new Array(matrix[0].length).fill(0); } const check=(i,j)=>{ return i <= matrix.length - 1 && i >= 0 && j <= matrix[0].length - 1 && j >= 0; }; const search=(matrix,i,j,temp)=>{ if(temp[i][j]!==0) return temp[i][j]; // 应该有四个方向可取 for(let k=0;k<dir.length;k++){ if(check(i+dir[k][0],j+dir[k][1])&&matrix[i+dir[k][0]][j+dir[k][1]]>matrix[i][j]){ temp[i][j]=Math.max(search(matrix,i+dir[k][0],j+dir[k][1],temp),temp[i][j]); } } // 返回时应该加上起点 temp[i][j]+=1; return temp[i][j]; }; for (let i=0;i<matrix.length;i++){ for (let j=0;j<matrix[0].length;j++){ ans=Math.max(ans,search(matrix,i,j,temp)); } } console.info(ans); return ans; }; /** * 咱们注意到某个点的最长递增路径老是跟相邻的点有关系,子问题重叠,所以天然而然的就会想到动态规划 * 所以最优子结构 L(i,j)=1+Math.max(L(i-1,j),L(i,j+1),L(i+1,j),L(i,j-1)) * 而且须要判断相邻节点是不是增长关系,可是有一个问题是没法肯定边界点的L长度, * 而这种依赖其余顶点的现象又称为拓扑排序,将此题转化成拓扑排序 * 时间复杂度 : O(mn)。 * 拓扑排序的时间复杂度为 O(V+E) = O(mn) O(V+E)=O(mn)。 * V 是顶点总数,E 是边总数。 * 本问题中,O(V) = O(mn),O(E) = O(4V) = O(mn)。 * 空间复杂度 : O(mn) 咱们须要存储出度和每层的叶子 * @param matrix */ const longestIncreasingPath4 =matrix=>{ if(matrix.length<1) return matrix.length; let leaves=[],m=matrix.length,n=matrix[0].length, dir=[[-1,0],[0,1],[1,0],[0,-1]],ans=0; const check=(i,j)=>{ return i <= matrix.length - 1 && i >= 0 && j <= matrix[0].length - 1 && j >= 0; }; // initialize degree let degree=[]; for(let i=0;i<m;i++){ degree[i]=(new Array(n)).fill(0); } for(let i=0;i<m;i++){ for(let j=0;j<n;j++){ for(let k=0;k<dir.length;k++){ if(check(i+dir[k][0],j+dir[k][1])&&matrix[i][j]<matrix[i+dir[k][0]][j+dir[k][1]]){ degree[i][j]++; } } } } // console.info(degree); // collect which degree is 0,寻找叶子顶点,即不依赖于其余顶点的顶点 for(let i=0;i<m;i++){ for(let j=0;j<n;j++){ if (degree[i][j]===0){ leaves.push([i,j]); } } } console.info(leaves.map(item=>matrix[item[0]][item[1]])); while(leaves.length>0){ ans++; let newLeaves=[]; for(let i=0;i<leaves.length;i++) { for(let d=0;d<dir.length;d++){ let x=leaves[i][0]+dir[d][0],y=leaves[i][1]+dir[d][1]; // 此时check的倒是比当前点小的点即寻找新的叶子的过程 if(check(x,y)&&matrix[leaves[i][0]][leaves[i][1]]>matrix[x][y]){ if(--degree[x][y]===0){ newLeaves.push([x,y]); } } } } console.info(newLeaves.map(item=>matrix[item[0]][[item[1]]])); leaves=newLeaves; } // console.info(ans); return ans; };
贪心算法解题步骤:
const maxProfit=arr=>{ let res=0; let len=arr.length; // Infinity为全局变量,表示无穷大 let min=Infinity; for(let i=0;i<len;i++){ min=Math.min(min,arr[i]); res=Math.max(res,arr[i]-min); } return res; }
//todo
有两个策略:
这个题暴露出来的问题太多了
for(){ if(change-hand[i]>0){ change-=hand[i]; hand.splice(i,1); i-- }else if(change-hand[i]===0){ change-=hand[i]; hand.splice(i,1); hand.push(customer); } } if(change!==0) return false;
降冗余===>(上面的代码有一部分产生了重复,这是不该该的)
for(){ if(change-hand[i]>=0){ change-=hand[i]; hand.splice(i,1); i--; } if(change===0) break; } if(change!==0){ return false }else{ hand.push(customer); }
/** * leetcode-62 不一样路径 * typical dp: F(m,n)=F(m-1,n)+F(m,n-1); * 运行超时,时间复杂度 * @param m * @param n */ const uniquePaths0 = (m, n)=>{ let res=0; if(m===1||n===1){ return 1; } return uniquePaths0(m-1,n)+uniquePaths0(m,n-1); }; /* * 下面是dp优化,暂存那些重复计算的部分 * 时间复杂度O(m*n) 空间复杂度O(m*n) */ const uniquePaths1=(m,n)=>{ let res=new Array(m); for (let i=0;i<m;i++){ res[i]=new Array(n); for(let j=0;j<n;j++){ if(i===0||j===0){ res[i][j]=1; }else{ res[i][j]=res[i-1][j]+res[i][j-1]; } } } // console.info(res); return res[m-1][n-1]; }; /** * 优化1:不须要二维数组进行暂存全部状况,只须要两个数组进行转存,就是杨辉三角 * 空间复杂度O(n) */ const uniquePaths2=(m,n)=>{ let temp0=new Array(n), temp1=new Array(n); temp0.fill(1); temp1.fill(1); for (let i=1;i<m;i++){ for(let j=1;j<n;j++){ temp1[j]=temp1[j-1]+temp0[j]; } temp1.forEach((item,idx)=>{ temp0[idx]=item; }); console.info(temp0); } return temp0[n-1]; }; console.info(uniquePaths2(7,3)); /** * 优化2:当前状态只须要上一行最后的状态和同行左边的状态 * 空间复杂度为O(n) */ const uniquePaths3=(m,n)=>{ let temp=new Array(n); temp.fill(1); for(let i=1;i<m;i++){ for(let j=1;j<n;j++){ temp[j]=temp[j]+temp[j-1]; } } return temp[n-1]; };
/** * 固然这个题的解法不少,能够考虑一种正规的暴力解法: * 时间复杂度为O(n^2) ,正是每一个元素都会遍历一遍,而且进行向左向右查找最大值,所以时间复杂度较高 * 空间复杂度O(1) */ const trap1=height=>{ let ans=0,len=height.length; for (let i=1;i<len-1;i++){ let left=0,right=0; for(let j=i;j>=0;j--){ left=Math.max(left,height[j]); } for(let k=i;k<len;k++){ right=Math.max(right,height[k]); } // console.info(left,right); ans+=Math.min(left,right)-height[i]; } return ans; }; /** * 经过对暴力法的总结以后能够发现一个问题:老是在重复的计算左边最大,右边最大===>重复子问题 * 状态:每一个item可接雨水的量 Math.min(left_max,right_max)-arr[i] 状态转移方程 * 最优子结构 left_max right_max * 而dp在处理重复子问题上能够经过暂存的方式下降时间复杂度 * 由于只遍历了一次 T(n)=3n O(n)=n * 空间复杂度 S(n)=2n O(n)=n * @param height */ const trap2=height=>{ let res=0,left=[],right=[],len=height.length,left_max=0,right_max=0; for(let i=0;i<len;i++){ left_max=Math.max(left_max,height[i]); left.push(left_max); } for(let i=len-1;i>=0;i--){ right_max=Math.max(right_max,height[i]); right.unshift(right_max); } for(let i=1;i<len-1;i++){ res+=Math.min(left[i],right[i])-height[i]; } console.info(left,right); return res; }; /** * 最后来一个终极版的代码,时间复杂度O(n)空间复杂度O(1) * 咱们已经将时间复杂度降到了O(n),空间复杂度仍有优化的空间 * 其实经过观察咱们分析的表格发现0-6咱们老是在用max_left来计算res,剩下的则用max_right来计算 * 所以能够只进行一次遍历便可完成对res的收集,利用双指针的方式,若是left比right小则选left, * 但仍要维护的是max_left和max_right * 分析图以下图2 */ const trap3=height=>{ let res=0,left=0,right=height.length-1,max_left=0,max_right=0; while(left<right){ if(height[left]<height[right]){ //todo 利用左边进行累加 if(height[left]>max_left){ max_left=height[left]; } res+=max_left-height[left]; left++; }else{ //todo 利用右边进行累加 if(height[right]>max_right){ max_right=height[right]; } res+=max_right-height[right]; right--; } } return res; };
固然这道题并不限于以上几种方法,个人解法是经过对暴力法的简单优化,可是显然dp作起来更优雅方便。所以也能得出一条结论,作题的思路对是一方面,怎么优化又是另外一方面,而从前者到后者实际上是一个很大的跨度,将问题解剖图置于以下:
显然经过图示解剖问题,可以获得极好的思路提示和优化提示。
references:
最近在作313超级丑数的问题时从新思考了一下为何用dp怎么用dp的问题。
在处理313问题时,咱们能够先求约数–>是否为质数–>质因数是否为primes中的数字来解决,无奈在处理这个问题时用到了大量的递归来解决求质因数的问题,尽管继续用构建最大堆的方式查找是否在primes中的方式进行优化后仍然复杂度过于高。下面咱们从新从动态规划的角度考虑一下这个问题。
用动态规划解题时,将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。
定义出什么是“状态”,以及在该 “状态”下的“值”后,就要找出不一样的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另外一个“状态”的“值”。状态的迁移能够用递推公式表示,此递推公式也可被称做“状态转移方程”。
这是一道经典问题,有暴力法,dp, 中心扩展,Manchester算法
/** * 时间复杂度为O(N*N*N),空间复杂度为O(1) * 时间复杂度太高,没法经过leetcode测试 */ const longestPalindrome = s=>{ /** * 检查是否为回文子串 * @param s * @returns {boolean} */ let res=''; const testStr=s=>{ let pivot=Math.floor(s.length/2); let idx=s.length%2!==0?pivot+1:pivot; return s.slice(0,pivot)===s.slice(idx).split('').reverse().join('') }; for(let i=0;i<s.length;i++){ for(let j=i+1;j<s.length+1;j++){ if (testStr(s.slice(i,j))){ if(j-i>res.length){ res=s.slice(i,j); } } } } return res; };
"babad"
为测试用例/** * dp * 由于暴力求解的方式没法经过全部测试用例 * 利用动态规划用空间换取时间的减小,去暂存那些已是回文串的字符串 * P[s][e]=P[s+1][e-1]&&s.charAt(s)===s.chatAt(e) * 显然最终时间复杂度变成了O(n2) 空间复杂度也是O(n2) */ const longestPalindrome1=s=>{ let length=s.length; let P=new Array(length),res=""; /** * js 语法须要预设数组的内容 */ for(let prev=0;prev<length;prev++){ P[prev]=new Array(length); } for(let len=1;len<=length;len++){ for(let start=0;start<length;start++){ let end=start+len-1; if(end>=length) break; // 长度为 1 和 2 的单独判断下 P[start][end] = (len === 1 || len === 2 || P[start + 1][end - 1]) && s.charAt(start) === s.charAt(end); if (P[start][end] && len > res.length) { res = s.slice(start, end + 1); } } } return res; };
/** * 第三种方法是采用从中间向两边进行扩散的方式来肯定字符串是否为回文串 * 关键点在于奇数个数的子串和偶数个数的子串都要去验证避免出现遗漏的现象 * 时间复杂度O(n2) 空间复杂度O(1) * @param str */ const longestPalindrome2=str=>{ const expandAroundCenter=(s,left,right)=>{ let L=left,R=right; while(L>=0&&R<s.length&&s.charAt(L)===s.charAt(R)){ L--; R++; } // 注意此处的R L已是加过,减过的数了 return R-L-1; }; if(!str||str.length<1) return ""; let start=0,end=0; for(let i=0;i<str.length;i++){ // 判断奇数子串问题 let len1=expandAroundCenter(str,i,i); // 判断偶数子串问题 let len2=expandAroundCenter(str,i,i+1); let len=Math.max(len1,len2); if(len>end-start){ start = i - Math.floor((len - 1) / 2); end = i + Math.floor(len / 2); } } return str.substring(start,end+1); };
references:
/** * Manchester算法 * @param str */ /** * 解决最长回文子串问题,采用Manchester算法 * key1:将原字符串进行变形,插入#,此时字符串变成了绝对奇数个 * key2:定义辅助数组P[]保存以当前字符为中心的回文串的半径长度 * key3:定义中心C,最右边界R,P[i]=R-i,P[i]取决于R-i和与i对称的点的位置的P[x]大小,为了防止i_mirror不在咱们取最小值 * 2*P[i]+1 是新串中以nStr[i]为中心的回文子串的长度L,同时(L-1)/2是原串中回文子串的长度即为P[i] * 思考时间复杂度:虽然有while循环,可是事实上每一个节点都只访问了一次 O(n) */ const process=str=>{ let res='^'; if(str.length===0) return '^$'; for(let i=0;i<str.length;i++){ res+=`#${str[i]}`; } return res+'#$'; }; const longestPalindrome3=str=>{ let nStr=process(str); let C=0,R=0,P=new Array(nStr.length); for(let i=0;i<nStr.length;i++){ let i_mirror=2*C-i; if(R>i){ // 防止超过R P[i]=Math.min(R-i,P[i_mirror]); }else{ P[i]=0; } //console.info(nStr[i+1+P[i]],nStr[i-1-P[i]]); while(nStr[i+1+P[i]]===nStr[i-1-P[i]]){ P[i]++; } /** * update R C */ if(i+P[i]>R){ C=i; R=i+P[i]; } } let maxLen=0,id=0; for(let i=0;i<P.length;i++){ if (P[i]>maxLen){ maxLen=P[i]; id=i; } } // console.info(P); let start=Math.floor((id-maxLen)/2); return str.slice(start,start+maxLen); };
本质:每一个处理过程都是相同的,输入和输出是相同的,处理次数未知
一上来这个思路就错了,不该该是分割字符串的思路,由于255是最大的确定就三个占满,
递归处理的三个关键:
参考上面递归的特色,可是在这道题上没有解出来的缘由是
const hasPathSum = (root, sum) => { let res0=false,res1=false; if (!root) return false; // 边界条件处理的过于混乱 // if (sum===0&&!root.left&&!root.right) return false; // if(root.val>=sum) return true; if (!root.left && !root.right) { return sum === root.val; } if (root.left) { res0=hasPathSum(root.left, sum - root.val); } if (root.right) { res1=hasPathSum(root.right, sum - root.val); } return res0||res1; };
判断一个二叉树是否为对称二叉树。一开始个人思路是利用中序遍历后的值来查看,发如今leetcode上跑不通.缘由不明,遍历的时候会将为null的值过滤掉,可是在本地跑却没有任何问题。而后思考能够用递归的方式实现。
理由以下:老是在重复同一个过程,有输入 有输出 有边界条件,所以可经过递归来解决这个问题:为null的状况,有值进行对比的状况。
读题时出现了一个比较严重的错误:对题意出现了误解,致使没法经过测试用例。题目能够经过递归来实现,也能够经过变种为二叉树的遍从来实现。
下面介绍一种比较厉害的递归方式:
// 从个人角度来看这段代码并不可以一看到题目就能领会到这种解题方法,由于没有校验左树比根节点小的操做。 const isValidBST = (root)=>{ let lst=-Number.MAX_SAFE_INTEGER; let isValidBST0=root=>{ if(root===null) return true; if (isValidBST0(root.left)){ if(lst<root.val){ lst=root.val; return isValidBST0(root.right); } } return false; }; return isValidBST0(root); };
首先分析一下这个题的思路:
references:
特征:
将原问题划分红若干个规模较小而结构与原问题类似的子问题,递归的解决这些子问题,而后再合其结果,就获得原问题的解。
解决什么样的问题:
求解步骤:
关于分治算法的总结参考个人另外一篇博客:
特征:
与分治法类似,也是经过组合子问题的解而解决整个问题。区别是,动态规划适用于分解获得的子问题每每不是相互独立的。在这种状况下若是采用分治法,有些子问题会被重复计算屡次,动态规划经过记录已解决的子问题,能够避免重复计算。(在必要的状况下记录重复的子问题的值。)
解决什么样的问题:
求解步骤:
dp问题的解决一般经过递归来实现。
/** * 前段时间已经写过关于一个铺地砖问题的答案,实际上就是斐波那契数列 * 而dp的关键是什么:有状态 有状态转移方程 有重叠的子问题 有边界 * 下面就写一下fib数列,同时经过一个中间数组保存每个子问题的值,避免重复计算 */ const fib = n => { let res = [0, 1, 1]; if (n === 1 || n === 2) return res[n]; for (let i = 3; i <= n; i++) { res.push(res[i - 1] + res[i - 2]); } console.info('fib===>',res); return res[n]; }; console.info(fib(4)); console.info(fib(5));
特征:
经过作一系列的选择来给出某一问题的最优解,对算法中的每个决策点,作一个当时(看起来)是最优的选择。这种启发式的策略并非总能产生出最优解。
解决什么样的问题:
要素:贪心选择性质、最优子结构性质
贪心选择性质:一个全局最优解能够经过局部最(贪心)选择来达到。
求解步骤:
求二维01矩阵中最大矩形的问题能够转换为求柱状图中最大图形的问题。那么咱们主要从第84题的角度来看看该类题适合用什么方法来作。
首先确定咱们把柱状图的规模缩小以后就会发现问题很容易解决了,经过求出可能的矩形,不停的找比当前值大的便可。
那么思考一下:子问题是否有重叠?是的,就拿下图来看:
只要56存在于子问题中,那么它就是最大的矩形,所以这是重叠的,每一个子问题都从新求解一遍时会存在重复。
references:
/** *问题描述,给定一个数组,表示的是出售长度为i的钢条的价格。如p = [1, 5, 8, 9, 10, 17, 17, 20, 24, 30] * 表示的是长度为1的钢条为1美圆,长度为2的钢条为5美圆,以此类推。 * 如今有一个钢条长度为n,那么如何切割钢条可以使得收益最高,切割的时候不消耗费用。来源于算法导论15.1。 */
对于这个问题其实能够归结为一个 dp问题:
知足最优子结构
子问题重叠
无后效性
const biggestProfit=(len,p)=>{ let res=0; if (len<1) return res; if (len===1) return p[0]; if (len<=p.length){ res=p[len-1]; } for (let i=1;i<=Math.floor(len/2);i++){ if (biggestProfit(len-i,p)+biggestProfit(i,p)>res){ res=biggestProfit(len-i,p)+biggestProfit(i,p); // console.info(biggestProfit(len-i,p),biggestProfit(i,p)); } } return res; };
而朴素的递归方法正式在求解该问题的子问题时老是有重复计算的地方。而使用dp是经过用一个备忘把计算过的值存储下来。
以下图所示为朴素递归的方式:
const big=(len,p)=>{ let res=[0],s=[0],sRes=[]; let temp; for (let i=1;i<=len;i++){ temp=-(Infinity); for (let j=1;j<=i;j++){ // 思考这里:将问题分解成了p[j-1]+res[i-j] // 那么实际上是将钢条分红两端 一段再也不切割 一段切割后求最大值 // 那么会想一段不切割的话这种方式并不必定获得最优解啊,可是由于for循环遍历了全部的可能,(所以双重for循环是必要的一层检测) // 因此老是能找到最优解的 //temp=Math.max(temp,p[j-1]+res[i-j]); if (p[j-1]+res[i-j]>temp) { temp = p[j - 1] + res[i - j]; s[i]=j; } } res[i]=temp; } let len0=len; while(len0>0){ sRes.push(s[len0]); len0=len0-sRes[sRes.length-1]; } return `max:${res[len]},组合为:${sRes}`; };
关于二分查找法的详解能够参考个人另外一篇文章:详解二分查找法
是一个比较难的题,值得推敲,以及怎么去二分
left_part | right_part A[0], A[1], ..., A[i-1] | A[i], A[i+1], ..., A[m-1] B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[n-1]
此时咱们只须要知道两个数组的长度之和是奇数仍是偶数便可:
能够总结出规律:
若是m+n为奇数呢?
i+j=Math.floor((m+n-1)/2)
i+j=Math.ceil((m+n-1)/2)
同时 i和j之间的关系是怎样的呢:
i+j=m-i+n-j( or m-i+n-j+1)======>j=(m+n+1)/2-i,i=(0,...逐渐移动) // 同时确保 A[i-1]<=B[j] B[j-1]<=A[i]
鉴定条件:
if (i < ed && B[j-1] > A[i]){ iMin = i + 1; // i is too small } else if (i > st && A[i-1] > B[j]) { iMax = i - 1; // i is too big }
边界条件处理:
// 处理AB左侧无值或者右侧无值的状况下如何计算中位数
/** * 先考虑若是不去计较时间复杂度的大小,如何肯定中位数呢 * 经过以其中最长的数组为基准向数组中插入短数组数据的方式肯定排序好的数组,最终肯定中位数 * 时间复杂度为O(m+n)最差的状况下两个数组都须要遍历一遍 * @param A * @param B */ const findMedianSortedArrays=(A,B)=>{ let sum=A.length+B.length; if (A.length===B.length&&sum===2){ return (A[0]+B[0])/2 } const sortAB=(a,b)=>{ let j=0; for(let i=0;i<b.length;i++){ for(let k=j;k<a.length-1;k++){ if (k===0&&a[k]>=b[i]){ a.unshift(b[i]); j=k; break; }else if(a[k]<=b[i]&&a[k+1]>=b[i]){ // console.info('k',k); a.splice(k+1,0,b[i]); j=k+1; break; }else{ j=k; } } if(a.length<sum&&j+1===a.length-1&&a[j+1]<=b[i]){ a=a.concat(b.slice(i)); break; } } return a; }; if(A.length>=B.length){ A=sortAB(A,B); }else{ A=sortAB(B,A); } console.info(A); let len=A.length; return len%2===0?(A[len/2]+A[len/2-1])/2:A[Math.floor(len/2)]; }; /** * 其实上述的代码仍然具备冗余,首先不必定以长数组为基准进行插入,能够用任意一个数组做为基准 * 假如以B为基准向B中插入数据 */ const findMedianSortedArrays0=(A,B)=>{ let j=0; for(let i=0;i<A.length;i++){ while(j<B.length){ if (B[j]>=A[i]){ B.splice(j,0,A[i]); j++; break; }else if(j===B.length-1&&B[j]<A[i]){ B.push(A[i]); j++; break; }else{ j++; } } } if(B.length===0){ B=A; } let len=B.length; return len%2===0?(B[len/2]+B[len/2-1])/2:B[Math.floor(len/2)]; }; /** * 以上的问题解法显然不符合题目要求的时间复杂度,所以有必要经过二分法来解决这个问题 * @param A * @param B */ const findMedianSortedArrays1=(A,B)=>{ // make sure A.length<B.length if(A.length>B.length){ let temp=A; A=B; B=temp; } let m=A.length,n=B.length; // start binary-search,build start and end let st = 0,ed=m; while(st<=ed){ let i=Math.floor((st+ed)/2); // to avoid m+n is odd let j=Math.floor((m+n+1)/2)-i; // case 1' 只是鉴于st能够等于ed的缘故,判断一下是否i就是st,若是不判断亦可 if(i>st&&A[i-1]>B[j]){ // binary search directly // i is too big ed=i-1; // case 2' }else if(i<ed&&B[j-1]>A[i]){ // i is too small st=i+1; }else{ let maxLeft=0; if(i===0){ maxLeft=B[j-1]; }else if(j===0){ maxLeft=A[i-1]; }else{ maxLeft=Math.max(A[i-1],B[j-1]); } let minRight = 0; if (i === m){ minRight = B[j]; }else if (j === n) { minRight = A[i]; }else { minRight = Math.min(B[j], A[i]); } return (m+n)%2===0?(maxLeft+minRight)/2:maxLeft; } } };
总结:
const divide = (dividend, divisor)=>{ if(dividend===0) return 0; if(divisor===1) return dividend; if(divisor===-1) { if(-dividend>Math.pow(2,31)-1||-dividend<-Math.pow(2,31)){ return Math.pow(2,31)-1; } return -dividend; } let sign=1; // check +- if((dividend<0&&divisor>0)||(dividend>0&&divisor<0)){ sign=-1; } dividend=dividend>0?dividend:-dividend; divisor=divisor>0?divisor:-divisor; const div=(a,b)=>{ if(a<b) return 0; let count=1,tb=b; while((tb+b)<a){ count+=1; tb+=b; } // key: 若是不递归,每每求的值是在count和count+1之间浮动, // 至关于须要把头部代码从新写一遍,所以递归便可 return count+div(a-tb,b); }; let res=div(dividend,divisor); if(sign>0){ return res>Math.pow(2,31)?Math.pow(2,31)-1:res; }else{ return -res; } }; /** * optimize1:加快步伐 * @param dividend * @param divisor * @returns {number|*} */ const divide1 = (dividend, divisor)=>{ if(dividend===0) return 0; if(divisor===1) return dividend; if(divisor===-1) { if(-dividend>Math.pow(2,31)-1||-dividend<-Math.pow(2,31)){ return Math.pow(2,31)-1; } return -dividend; } let sign=1; // check +- if((dividend<0&&divisor>0)||(dividend>0&&divisor<0)){ sign=-1; } dividend=dividend>0?dividend:-dividend; divisor=divisor>0?divisor:-divisor; /** * 加快收缩的进程 * @param a * @param b * @returns {number|*} */ const div=(a,b)=>{ if(a<b) return 0; let count=1,tb=b; while((tb+tb)<a){ count+=count; tb+=tb; } return count+div(a-tb,b); }; let res=div(dividend,divisor); if(sign>0){ return res>Math.pow(2,31)?Math.pow(2,31)-1:res; }else{ return -res; } };
接下来两道题很是的有意思,有重复元素的数组相对寻找起来更加困难,但究其根本也不过是移动指针。那么在logN的时间复杂度内寻找到旋转点的关键是什么呢?
arr[mid]>arr[mid+1]
是判断该点是否是旋转点的根本
arr[mid]>arr[left]
是一种正常的状况,应该移动leftarr[mid]<arr[left]
应该移动rightarr[mid]===arr[left]
??此时应该是移动left:为何呢?由于咱们取mid时用的floor,若是移动right根据代码流程会跳过旋转点const find=arr=>{ let left=0,right=arr.length-1; if (arr[right]>arr[left]){ // 此时没有旋转,也就是说旋转索引为0 return 0; } while(left<=right){ let mid=Math.floor((left+right)/2); // 依靠判断后一位是否比当前位的值大来决断是否位旋转点 // mid+1必定存在,由于咱们mid取的是floor if (arr[mid]>arr[mid+1]){ return mid; }else { // 难点+:和开头的判断同样 if(arr[mid]<arr[left]){ right=mid-1; }else{ left=mid+1; } } } }; console.info(find([3,4,5,6,7,0,1,2])); // 4
总结:
nums[mid]>nums[right]
来判断呢?固然能够,可是限于这个问题咱们经过Math.floor来取中间值,因此右边界不能每次都让mid-1
须要保留mid
以免丢失判断项。可是咱们通常不采用这种方式的缘由是:前面判断mid与mid+1已经默认右边暂时符合条件/** * 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 搜索一个给定的目标值,若是数组中存在这个目标值,则返回它的索引,不然返回 -1 。 你能够假设数组中不存在重复的元素。 你的算法时间复杂度必须是 O(log n) 级别。 输入: nums = [4,5,6,7,0,1,2], target = 0 输出: 4 首先咱们知道若是利用二分查找,那么时间复杂度符合要求,可是该数组不是严格有序 那么为了符合条件,其实有一个很巧妙的方法,就是利用二分查找法找到旋转点 * @param nums * @param target */ const search = (nums, target)=>{ const find=arr=>{ let left=0,right=arr.length-1; if (arr[right]>arr[left]){ // 此时没有旋转,也就是说旋转索引为0 return 0; } while(left<=right){ let mid=Math.floor((left+right)/2); // 依靠判断后一位是否比当前位的值大来决断是否位旋转点 // mid+1必定存在,由于咱们mid取的是floor if (arr[mid]>arr[mid+1]){ return mid; }else { // 和开头的判断同样 if(arr[mid]<arr[left]){ right=mid-1; }else{ left=mid+1; } } } }; const search0=(left,right)=>{ while(left<=right){ let mid=Math.floor((left+right)/2); if(nums[mid]===target){ return mid; }else if(nums[mid]>target){ right=mid-1; }else if(nums[mid]<target){ left=mid+1; } } return -1; }; if(nums.length===0){ return -1; }else if(nums.length===1){ return nums[0]===target?0:-1; } let rotate=find(nums); // console.info(find(nums)); if(nums[rotate]===target){ return rotate; }else{ let a=search0(0,rotate); let b=search0(rotate+1,nums.length-1); if(a===-1&&b===-1){ return -1; }else if(a===-1){ return b; }else{ return a; } } };
和33题的区别是数组中有了重复的元素,所以相对于原来的代码也就有了移动left right的操做。
总结:
由于javascript是动态变量类型的语言,即运行时肯定数据类型,所以必须用Math.floor或者Math.ceil控制mid大小
const search = (nums, target)=>{ if(nums.length===0) return false; if(nums.length===1) return nums[0]===target; const find=arr=>{ let left=0,right=arr.length-1; while(left<=right){ let mid=Math.floor((left+right)/2); // 由于mid取的是floor if(arr[mid]===arr[left]&&mid!==left){ // key1: 不能让mid和left同样的时候移动left,否则此时会发生像[3,1]这种没法经过的状况。究其缘由是mid取floor的缘故 left+=1; continue; } if(arr[mid]===arr[right]){ right-=1; continue; } if(arr[mid]>arr[mid+1]){ return mid; }else { if (arr[mid] >= arr[left]) { // key2: arr[mid]=arr[left]的状况要移动left而不是移动right。究其缘由是mid取floor的缘故 left=mid+1; } else { right = mid - 1; } } } return left; }; const search0=(left,right)=>{ while(left<=right){ let mid=Math.floor((left+right)/2); if(nums[mid]===target){ return true; }else if(nums[mid]>target){ right=mid-1; }else{ left=mid+1; } } return false; }; let idx=find(nums); console.info('idx',idx); return (search0(0,idx)||search0(idx+1,nums.length-1)); };
总结:
牛顿迭代法在求解平方根和最优化问题上获得普遍应用。
主要迭代关系式以下:
推导过程以下:
/** * 首先利用二分法来寻找问题的答案, * 结合之前几道作过的题目进行过的总结能够知道: * 若是存在target则返回正好合适的mid值,若是不存在 * left就是待插入位置的索引,right比left小1 * 固然:以上mid的选取都是取的floor,在极少数的状况下我也用过ceil,不管哪种都要仔细分析一下边界以避免发生错误 * 应用到这个题中就是返回right值便可 * 时间复杂度:O(logN) * 空间复杂度:O(1) * @param x */ const mySqrt = (x)=>{ if(x===1||x===0) return x; let left=0,right=Math.floor(x/2); while(left<=right){ let mid=Math.floor((left+right)/2); if(mid*mid===x){ return mid; }else if(mid*mid>x){ // mid is too big right=mid-1; }else if(mid*mid<x){ left=mid+1; } } // console.info('left===>',left,right); return right; }; /** * 牛顿迭代法:Newton's method * https://baike.baidu.com/item/%E7%89%9B%E9%A1%BF%E8%BF%AD%E4%BB%A3%E6%B3%95/10887580?fr=aladdin * 牛顿迭代法是求方程根的重要方法之一,其最大优势是在方程 的单根附近具备平方收敛, * 并且该法还能够用来求方程的重根、复根,此时线性收敛,可是可经过一些方法变成超线性收敛。 * 首先知道一阶泰勒展开公式f(x)=f(x0)+f'(x0)(x-x0) * 将f(x)=x^2-a代入便可获得x=(x0+a/x0)/2 即获得了迭代关系式即:x1=(x0+a/x0)/2,x2=(x1+a/x1)/2 * 迭代变量即为x * 迭代跳出为是否x*x为a * @param x * @returns {number} */ const mySqrt0=x=>{ let a=x; while(x*x-a>1-Number.EPSILON){ // x=Math.floor((x+a/x)/2); // 如下若是while判断为x*x>a会陷入死循环,好比取值5,那么x老是无限逼近√5且一直比a大而不跳出循环 // 所以为了不进入死循环,将判断条件改成x*x-a>1-Number.EPSILON // Number.EPSILON 属性表示 1 与Number可表示的大于 1 的最小的浮点数之间的差值。2^(-52) x=(x+a/x)/2; // console.info(x); } // console.info(x); return (Math.floor(x)); };
references:
排序算法的应用-leetcode
关于数学问题的转换也是在算法题中常见的一种巧妙的解决问题的方法,好比说如何不使用中间变量交换两个数字能够经过以下数学方法解决
a=a+b; b=a-b; a=a-b;
a=a*b; b=a/b; a=a/b;
由于题目只须要求得最短期,所以将题目转化为数学问题最为简单,复杂度也低。
如下是部分核心代码
str.padEnd(len,str0),在str后面用str0补全使其长度为len
相应的还有str.padStart()
const leastInterval = (tasks, n)=>{ let obj={}; let res=''; if (tasks.length<1) return res; tasks.forEach(item=>{ if(!obj[item]){ obj[item]=1; }else{ obj[item]++; } }); /* 我所没法决定的点是如何排序这个obj对象,而后如何把待命给加进去,如何知道ab完以后继续进行a呢? * 三个问题的解决方式是经过遍历的方式找到出现频率最大的,利用字符串的补齐API,利用参数n进行分组每组长度为n+1 */ while(JSON.stringify(obj)!=="{}") { let temp=[]; let tempObj=JSON.parse(JSON.stringify(obj)); for(let i=0;i<n+1;i++){ if (JSON.stringify(tempObj)==="{}") break; let keys=Object.keys(tempObj); let maxKey=keys[0]; let maxVal=tempObj[maxKey]; for (let i=1;i<keys.length;i++){ if (obj[keys[i]]>maxVal){ maxVal=obj[keys[i]]; maxKey=keys[i]; } } temp.push(maxKey); if (obj[maxKey]===1){ delete(obj[maxKey]); }else{ obj[maxKey]--; } delete(tempObj[maxKey]); } res+=temp.join('').padEnd(n+1,'0'); } res=res.replace(/0+$/,''); return res.length };
/** * 回文数,正着反着都同样 * 首先最简单的就是转成字符串来判断是否为回文字符串 */ const isPalindrome = (x)=>{ let s=x.toString(); let pivot=Math.floor(s.length/2); let idx=s.length%2!==0?pivot+1:pivot; return s.slice(0,pivot)===s.slice(idx).split('').reverse().join('') }; /** * 思考能不能不转为字符串进行判断是否为回文数 * 参考官方题解: * 负数首先排除 * 同时考虑到大整数溢出的问题,咱们只去翻转一半的原数字 * 如何判断咱们已经翻转了一半呢,好比122/10=12<=12*10即此时提早到达了中间数 * 同时记得奇数个数的回文数,最后判断的时候跳过中间位便可 */ const isPalindrome1=x=>{ if(x<0||x%10===0&&x!==0){ return false; } let reverse=0; while(x>reverse){ reverse=reverse*10+x%10; x=Math.floor(x/10); } return x===reverse||x===Math.floor(reverse/10); }
从二叉树的前序遍历和中序遍历还原出二叉树这个问题总结分治法计算复杂度的一种方式
在计算机科学中,分治法是建基于多项分支递归的一种重要的算法范式。
references:
Master theorem (analysis of algorithms)
n
is the size of an input problem
a
is the number of subproblems in the recursion
b
is the factor by which the subproblem size is reduced in each recursive call
const buildTree = (preorder, inorder) => { if (preorder.length <= 0 || inorder.length <= 0) { return null; } let root = preorder[0]; let node = new TreeNode(root); let idx = inorder.indexOf(root); node.left = buildTree(preorder.slice(1, idx + 1), inorder.slice(0, idx)); node.right = buildTree(preorder.slice(idx + 1), inorder.slice(idx + 1)); // console.info(node); return node; };
从以上这段代码来看
T(n)=2T(n/2)+n^0
即a=2,b=2,c=0
此时 Ccrit=1>C=0
知足第一种case:所以时间复杂度O(n)=n^Ccrit=n
.
空间复杂度:O(n)
,存储整棵树的开销.