Freecodecamp 高级算法(个人向)

Posted 出世Sunny

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);

 

最后

以上就是高级算法所有题目,如果有错误或者更好的做法,欢迎讨论。对代码有不理解的地方也欢迎提问。

 

以上是关于Freecodecamp 高级算法(个人向)的主要内容,如果未能解决你的问题,请参考以下文章

No repeats please(freecodecamp高级算法6)

Map the Debris(freecodecamp高级算法8)

Falsy Bouncer-freecodecamp算法题目

Roman Numeral Converter-freecodecamp算法题目

Diff Two Arrays-freecodecamp算法题目

Sum All Primes-freecodecamp算法题目