freecodecamp 高级算法地址戳这里。
freecodecamp的初级和中级算法,基本给个思路就能完成,而高级算法稍微麻烦了一点,所以我会把自己的解答思路写清楚,如果有错误或者更好的解法,欢迎留言。
Validate US Telephone Numbers
如果传入字符串是一个有效的美国电话号码,则返回 true
.
简单来说,美国号码的规则就是,国家代码(必须为1),然后就是3,3,4的数字组合,前三个数字可以用括号包起来。另外就是间隔使用空格或者“-”。
因为输入值肯定是字符串,规则也较多,所以考虑用正则做。
先贴代码:
function telephoneCheck(str) { // Good luck!
var reg=/^(1\s?)?\(?\d{3}\)?(\s|-)?\d{3}(\s|-)?\d{4}/; //正则规则
var index1=str.indexOf("("); var index2=str.indexOf(")"); //查询到两个括号
if( (index1!=-1 && index2!=-1) || (index1==-1 && index2==-1) ){ //存在双括号或者没有括号
if( index2!=index1 && index2-index1!=4 ){ //如果存在双括号,且序号间的字符有3个
return false; } var str2=str.replace(/[\(\)\s-]/g,""); //将括号和空格和“-”全局替换成空,便于统计数字长度
if( str2.length==11 && str2.substr(0,1)!=1 ){ return false; } }else{ return false; } return reg.test(str); } telephoneCheck("27576227382");
当首次尝试直接匹配号码的时候我们发现不行,因为我们没办法同时匹配到双括号,正则规则存在一些盲点,这些盲点首先就是双括号的问题,再有就是长度问题,对于超出长度的字符我们没有匹配验证的能力,这就需要我们用js进行一些弥补。
我的做法,首先验证是否有双括号,同时有或者同时没有皆可;如果只有一个,返回false。接着在同时有或者同时没有双括号里面追加两个判断,如果有双括号,那么两个括号之间的字符一定是三个,否则返回false,如果确实返回3个,那我们也不用进行过多的判断,因为正则里已经写好了。接着就是通过replace将一切干扰元素去掉,验证一下字符串的长度有没有超出11;当长度为11时,第一个数字是不是1。完成了这些用来完善的判断,最后进行一下正则的匹配就可以了。
Symmetric Difference
创建一个函数,接受两个或多个数组,返回所给数组的 对等差分(symmetric difference) (△
or ⊕
)数组
输入的数组可能会是多个,而题目的要求是按顺序两两处理。也就是说,我们把前两个数组各自独有的元素组成新数组后,再和第三个数组进行处理,以此类推,最终会返回一个数组。这种模式让我们想到了数组的reduce方法,前两个处理出一个结果,处理出的结果再和下一个进行处理,直到最后得到一个结果。
所以主体函数的最后只要使用reduce就可以了,那么目前的问题就是解决两个数组之间如何消去所有的相同元素,然后返回一个排好序的新数组。因为一个数组当中都可能存在重复的元素,如果只是两个数组都删除相同的,可能还会删不干净。
我的思路是这样的,既然我们的目标只有值,而不在乎数量,所以一个开始就可以对两个数组分别进行一次去重,然后就是两个数组删除一个相同元素然后拼接排序。我这里呢,偷了个懒,用的还是去重的函数,等于省了一个函数。
function sym(args) { var arrs=[]; for(var a of arguments){ arrs.push(a); } var res=arrs.reduce(function(a,b){ a=del(a); b=del(b); //数组分别处理
var arr=a.concat(b); return del(arr,true); //拼接成一个大数组后,再进行一次处理
}); return res; } function del(arr,flag){ //排序and去重 flag为true表示删干净,否则留一个
var start,end; arr.sort(function(a,b){ //数组由小到大排序
return a-b; }); for(var i=0;i<arr.length;i++){ if(arr[i]==arr[i+1]){ //发现重复
start=(start===undefined)?i:start; //start为重复的起始位置
end=i+1; //end为重复的结束位置
}else{ if( end && end==i ){ //如果存在重复,即end有值,按照flag对数组进行处理。
if( flag ){ arr.splice(start,end-start+1); i=i-(end-start+1); }else{ arr.splice(start,end-start); i=i-(end-start); } start=undefined; //没有重复了,start要还原
} } } return arr; } sym([1, 1, 2, 5], [2, 2, 3, 5], [3, 4, 5, 5]);
Exact Change
设计一个收银程序 checkCashRegister()
,其把购买价格(price
)作为第一个参数 , 付款金额 (cash
)作为第二个参数, 和收银机中零钱 (cid
) 作为第三个参数.
输入为实际付款,商品价格,和零钱的余额。然后返回值有三种,如果找不开返回"Insufficient Funds";如果正好找开,余额空了,返回"Closed";其余则返回找零的数组。我的思路可能偏繁琐一点,它给的余额是每种面值的总价值,比如20元它会显示60,那么实际上是20元的有3张。所以如果要找5块,20元的这个60其实没有办法找开。于是我建了一个对象,用来管理余额,存储每种货币的面额和数量。之后就是比对需要找零的钱是否大于等于面值,如果大于等于,就看该面值的数量是否足够,足够则找零,更新找零的数额。重复这个步骤,直到找开,或者找不开。
代码如下:
function checkCashRegister(price, cash, cid) { var change=[]; //储存结果
var cid_obj={ //存储值和数量
"ONE HUNDRED":{val:100}, "TWENTY":{val:20}, "TEN":{val:10}, "FIVE":{val:5}, "ONE":{val:1}, "QUARTER":{val:0.25}, "DIME":{val:0.1}, "NICKEL":{val:0.05}, "PENNY":{val:0.01} }; for(var a of cid){ cid_obj[a[0]].num=Math.ceil(a[1]/cid_obj[a[0]].val); //更新不同货币的数量
} if( price==cash ){ return "Closed"; }else{ var cha=cash-price; //需要找零的钱 for(let k of Object.keys(cid_obj)){ var count=0; while( cha>=cid_obj[k].val && cid_obj[k].num!==0 ){ //没有完成找零且当前零钱可以找零
cha=(cha-cid_obj[k].val).toFixed(2); //这里需要四舍五入成2位小数,不然会有计算误差
cid_obj[k].num--; count++; if( cid_obj[k].num===0 || cha<cid_obj[k].val ){ //如果没零钱了
change.push([k,cid_obj[k].val*count]); break; } } } if( cha==0 ){ if( cid_obj["PENNY"].num==0 ){ //偷懒的做法
return "Closed"; } return change; }else{ return "Insufficient Funds"; } } } checkCashRegister(19.50, 20.00, [["PENNY", 0.50], ["NICKEL", 0], ["DIME", 0], ["QUARTER", 0], ["ONE", 0], ["FIVE", 0], ["TEN", 0], ["TWENTY", 0], ["ONE HUNDRED", 0]]);
Inventory Update
依照一个存着新进货物的二维数组,更新存着现有库存(在arr1
中)的二维数组. 如果货物已存在则更新数量 . 如果没有对应货物则把其加入到数组中,更新最新的数量. 返回当前的库存数组,且按货物名称的字母顺序排列。
这个题目比较简单,如果没有就添加一个数组元素,如果有就更新一下对应的数量。稍微麻烦点的是按字母顺序排序,我是使用了sort方法,内部用了循环的方式,逐个比对。
代码如下:
function updateInventory(arr1, arr2) { // All inventory must be accounted for or you're fired!
var arr=[]; outer:for(let x of arr2){ //更新数组
for(let y of arr1){ if(x[1]==y[1]){ y[0]+=x[0]; continue outer; } } arr.push(x); //arr2独有的放进arr
} return arr.concat(arr1).sort(function(a,b){ //排序
var index=0; var char_a,char_b; do{ char_a=a[1].charCodeAt(index); char_b=b[1].charCodeAt(index); index++; }while( char_a==char_b ); return char_a-char_b; }); } // Example inventory lists
var curInv = [ [21, "Bowling Ball"], [2, "Dirty Sock"], [1, "Hair Pin"], [5, "Microphone"] ]; var newInv = [ [2, "Hair Pin"], [3, "Half-Eaten Apple"], [67, "Bowling Ball"], [7, "Toothpaste"] ]; updateInventory(curInv, newInv);
No repeats please
例如, aab
应该返回 2 因为它总共有6中排列 (aab
, aab
, aba
,aba
, baa
, baa
), 但是只有两个 (aba
and aba
)没有连续重复的字符 (在本例中是 a
)。
这个题目是我觉得最有意思的一个题目,我算法比较烂,所以一开始很懵逼,全排列算法,不会啊!于是就是百度了一下,找到了下面的两种方法,这两种方法也是最后实现算法的基础。
两种都是递归,但思路不一样,第一种是交换法,先看代码:
function swap(arr,i,j) { if(i!=j) { var temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } } var count=0; function perm(arr) { (function fn(n) { //为第n个位置选择元素
for(var i=n;i<arr.length;i++) { swap(arr,i,n); if(n+1<arr.length-1) //判断数组中剩余的待全排列的元素是否大于1个
fn(n+1); //从第n+1个下标进行全排列
else console.log(++count+" "+arr); //显示一组结果
swap(arr,i,n); } })(0); } perm(["01","02","03","04"]);
这里明确一下各部分的职能,swap函数,用于交换数组中两个序号的值,单纯的交换函数;count变量,计数器;perm函数,是全排列的入口函数,这里的话是调用递归函数fn。如果把fn函数单独拿到外面定义,然后perm函数内部写fn(0),其实也是一样的。
那么最后的重点就是fn函数。它的思路其实不算太难理解,你可以把fn后面接收的参数n当做一个箭头,它标记了一个数组序号。因为是递归,其实每一步所做的事情都是一样的,所以我们只要考虑它这一步做了什么就可以了。
我们从fn(0)开始看,它从n开始遍历,然后进行了交换,也就说这一步其实是在为n这个位置选一个值,而且只在n序号之后选,这样不会影响前面已经确定的值。选好之后,递归结束了么?没有,我们只选了一个值,所以它进行了一个判断,如果当前标记的序号不是倒数第二个,就为下一个序号选一个值。之所以是倒二,是因为倒一不需要进行任何判断,它只可能有一个值,所以确定了倒二,倒一也是确定的,整个排列也就确定了,所以在确定了一种排列之后,显示结果。
那么问题来了,为什么输出结果之后要再次用swap函数交换一次。这是因为arr是唯一的数组,我们的每次交换都是直接对它进行操作,我们需要保证我们通过循环给位置n交换别的值时,arr还是我们认为的arr,n的原始值应该不变,这样每次的交换才有意义,如果我们不回滚,arr数组里的元素就会变得乱七八糟。
以三个元素排列说一下过程,a,b,c三个元素,首先fn(0),然后通过循环交换了一个值(循环的第一个值是自己,也就是不交换);接着fn(1),也交换了一个值;发现序号1已经是倒二,输出一条结果,然后回滚,再次给序号1的位置交换一个值,再次输出一个值。继续回滚,然后发现序号1的位置已经循环完了。也就是说fn(1)已经执行完毕,而fn(1)是在fn(0)里的,那么继续执行fn(0)后续的代码,序号为0的位置回滚复原,然后给序号为0的位置通过循环交换一个新值,再次fn(1)。不断的重复,直到fn(0)循环完毕,结束。
交换法理解的难点就是n的意义,还有swap的作用,理解了这两点其实后面就顺畅了。
下面看另一种方法,这种方法就好理解多了,暂时叫它抓取法。
代码如下:
var count=0; function perm(arr) { (function fn(source, result) { if (source.length == 0) console.log(++count+" "+result); else
for (var i = 0; i < source.length; i++) fn(source.slice(0, i).concat(source.slice(i + 1)), result.concat(source[i])); })(arr, []); } perm(["01", "02", "03", "04"]);
count是计数器;fn是递归函数,它接收两个参数,一个是source(抓取池),另一个是result(排列结果)。
输出条件很简单,当抓取池没有可以取的元素时,说明已经排列完成,输出一个结果。否则呢,就通过循环抓取池,抓取一个值放进result数组。不断重复这个步骤,直到所有循环结束。
fn(source.slice(0, i).concat(source.slice(i + 1)), result.concat(source[i]));
之所以用上面的方式是因为,slice方法会生成新的数组,不会对原数组造成影响;而result使用concat则是因为concat也会生成一个新的数组,而我们需要的参数就是两个数组。我们常用的push方法,也可以在末尾添加元素,不过它的返回值是数组长度。
全排列算法已经搞定,那么回过头来讲这个题目,这个题目有两种做法,一种比较朴素一点,每获得一个结果,我们判断一次是否符合题目要求,符合则计数器++,最后返回计数器的值。这种做法相当于列出所有的可能性,然后对每个结果字符串进行遍历,比对相邻序号的字符是否一样。想想就是个大工程,代码如下:
var permAlone=(function() { var count; //计数器
function judge(arr) { //判断是否符合要求
for(let i=0,l=arr.length;i<l-1;i++){ if( arr[i]==arr[i+1] ){ return; } } count++; } function fn(source, result) { if (source.length == 0){ judge(result); }else{ for (var i = 0; i < source.length; i++){ fn(source.slice(0, i).concat(source.slice(i + 1)), result.concat(source[i])); } } } return function(str){ var start=new Date(); var arr=str.split(""); count=0; fn(arr, []); console.log(new Date()-start+"ms"); return count; }; })(); permAlone('abcdefa');
第二种方法呢,实在安排每个位置的时候,就进行判断,看这个结果是否符合要求,如果不符合就跳过,我在代码里加了验证运算速度的代码,可以比对一下两种方法在面对较长字符串时候的运行效率。
这里我用交换法,抓取法的话,应该比交换法还要简单一点。
代码如下:
var permAlone=(function(){ var count; //计数器
function swap(arr,i,j) { //交换
if(i!=j) { var temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } } function fn(n,arr) { //为第n个位置选择元素
for(var i=n;i<arr.length;i++) { swap(arr,i,n); if( arr[n]==arr[n-1] ){ //和前一个元素比对,是否相等,只有前面的元素是固定不变的
swap(arr,i,n); //跳过前先复原
continue; } if(n<arr.length-1){ //判断条件这里需要改一下,只有当n为最后一个时才输出
fn(n+1,arr); //为序号n+1的位置选取值
}else{ if( arr[n]!=arr[n-1] ){ count++; //计数
} } swap(arr,i,n); } } return function(str){ var start=new Date(); var arr=str.split(""); count=0; //计数器归零
fn(0,arr); console.log(new Date()-start+"ms"); return count; }; })(); permAlone('abcdefa');
Friendly Date Ranges
把常见的日期格式如:YYYY-MM-DD
转换成一种更易读的格式。
易读格式应该是用月份名称代替月份数字,用序数词代替数字来表示天 (1st
代替 1
).
记住不要显示那些可以被推测出来的信息: 如果一个日期区间里结束日期与开始日期相差小于一年,则结束日期就不用写年份了。月份开始和结束日期如果在同一个月,则结束日期月份就不用写了。
另外, 如果开始日期年份是当前年份,且结束日期与开始日期小于一年,则开始日期的年份也不用写。
这个题目只要细心点就可以了,我的思路就是把月份数组通过闭包缓存起来,然后通过三元判断,将值确定好,最终的结果用字符串拼接的方式呈现把值拼起来就好。
var makeFriendlyDates=(function() { var mounth=["January","February","March","April","May","June","July","August","September","October","November","December"]; var nth=["st","nd","rd","th"]; var now_year=new Date().getFullYear(); //以上皆为缓存
function num(x,max){ //处理数字
x=(x<max)?x:max; return --x; } function judge(str1,str2){ //判断两个时间戳是否小于一年
var cha=new Date(str2)-new Date(str1); if( cha/1000/3600/24<365 ){
return true; }else{ return false; } } return function(arr){ var res=[]; var time_start=arr[0].split("-"); var time_end=arr[1].split("-"); var end_year=( judge(arr[0],arr[1]) )?"":", "+time_end[0]; var end_mounth=(time_start[0]==time_end[0] && time_end[1]==time_start[1])?"":mounth[time_end[1]-1]+" "; var end_day=parseInt(time_end[2]); if( arr[0]==arr[1] ){ //结束时间和开始时间一样的话
return [mounth[time_end[1]-1]+" "+end_day+nth[num(end_day,4)]+", "+time_end[0] ]; } var start_year=( judge(arr[0],arr[1]) && time_start[0]==now_year )?"":", "+time_start[0]; var start_mounth=mounth[time_start[1]-1]+" "; var start_day=parseInt(time_start[2]);
var res_start=start_mounth+start_day+nth[num(start_day,4)]+start_year; res.push(res_start); var res_end=end_mounth+end_day+nth[num(end_day,4)]+end_year; res.push(res_end); return res; }; })(); makeFriendlyDates(["2022-09-05", "2023-09-05"]);
Make a Person
用下面给定的方法构造一个对象.
方法有 getFirstName(), getLastName(), getFullName(), setFirstName(first), setLastName(last), and setFullName(firstAndLast).
所有有参数的方法只接受一个字符串参数。
这个题目挺好玩,我一开始直接用prototype做,然后挂了,它有个验证是:
Object.keys(bob).length 应该返回 6
所以最后我必须用上闭包去满足它这个要求。
代码如下:
var Person = (function() { var name; //name闭包了
return function(firstAndLast){ name=firstAndLast; this.getFullName=function(){ return name; }; this.getLastName=function(){ var arr=name.split(" "); return arr[1]; }; this.getFirstName=function(){ var arr=name.split(" "); return arr[0]; }; this.setFirstName=function(first){ var arr=name.split(" "); arr[0]=first; return name=arr.join(" "); }; this.setLastName=function(last){ var arr=name.split(" "); arr[1]=last; return name=arr.join(" "); }; this.setFullName=function(firstAndLast){ return name=firstAndLast; }; }; })(); var bob = new Person('Bob Ross'); bob.getFullName();
Map the Debris
返回一个数组,其内容是把原数组中对应元素的平均海拔转换成其对应的轨道周期。
地球半径是 6367.4447 kilometers, 地球的GM值是 398600.4418, 圆周率为Math.PI。
题目倒是不难,只要找到公式,然后注意一下单位就可以,长度单位都是km,周期单位为s
var orbitalPeriod=(function() { // r^3=G*m2*T^2/(4*pi^2) m2是地球质量 G为6.67×10-11 r为轨道半径,到球心的距离
var GM = 398600.4418; //地球质量和G的乘积
var earthRadius = 6367.4447; //km
var calculate=function(r){ //计算函数 var top=4*Math.pow(Math.PI,2)*Math.pow((r+earthRadius),3); var res=Math.pow( (top/GM),0.5);
return Math.round(res); }; return function(arr){ var res=[]; for(let a of arr){ let obj={}; obj["name"]=a["name"]; obj["orbitalPeriod"]=calculate(a["avgAlt"]); res.push(obj); } return res; }; })(); orbitalPeriod([{name : "sputnik", avgAlt : 35873.5553}]);
Pairwise
找到你的另一半
举个例子:有一个能力数组[7,9,11,13,15]
,按照最佳组合值为20来计算,只有7+13和9+11两种组合。而7在数组的索引为0,13在数组的索引为3,9在数组的索引为1,11在数组的索引为2。
所以我们说函数:pairwise([7,9,11,13,15],20)
的返回值应该是0+3+1+2的和,即6。
也许是受全排列那道题目的影响,我第一反应就是递归,因为每一个步骤都是相同的。每个元素都要在自己后面的元素中寻找匹配的。唯一需要注意的是,找到的序号需要缓存起来,如果这个序号已经在缓存中,就跳过,不需要进行匹配。
代码如下:
var pairwise=(function() { var res=[]; //放序号的缓存 function judge(arr,val){ for(let a of arr){ if( a==val ){ return true; } } return false; } function fn(n,arr,arg){ //递归函数
for(let i=n,l=arr.length;i<l;i++){ if( judge(res,i) || judge(res,n) ){ continue; } if( n!=i && arr[n]+arr[i]==arg ){ res=res.concat(n,i); break; } } if( n!=arr.length-1 ){ fn(n+1,arr,arg); } } return function(arr, arg){ res=[]; if( arr.length==0 ){ return 0; } fn(0,arr,arg); return res.reduce(function(a,b){ return a+b; }); }; })(); pairwise([1, 1, 1], 2);
最后
以上就是高级算法所有题目,如果有错误或者更好的做法,欢迎讨论。对代码有不理解的地方也欢迎提问。