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);

 

最后

以上就是高級算法所有題目,如果有錯誤或者更好的做法,歡迎討論。對代碼有不理解的地方也歡迎提問。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM