FreeCodeCamp 高级算法(个人向)
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);
最后
以上就是高级算法所有题目,如果有错误或者更好的做法,欢迎讨论。对代码有不理解的地方也欢迎提问。