您需要的前端面試演算法(上)

哈利呦發表於2018-07-10

閱前說明

文章將會分成上中下三部分,包含一些常見面試演算法題,大部分演算法題來自於《劍指offer》,在此對此書的作者表示感謝,還有一部分來自於本人的收集。題目解法有多種,望大蝦多多評論探討或指正

1、陣列遍歷

題述: 一個陣列中,每一行都按照從左至右遞增的順序排序,每一列按照從上到下遞增的順序排序。完成:輸入一個這樣的二維陣列和一個整數,判斷陣列是否含有這個整數

思路:陣列是有序的,可以根據規律減少遍歷次數。從右上角開始遍歷,如果這個數不等,那麼可以根據目標數與這個數的大小可以去掉一些行/列的遍歷

function findNumInSortedArray(arr, num) {
  if (!Array.isArray(arr) || typeof num != 'number' || isNaN(num)) {
    return;
  }
  let rows = arr.length;
  let columns = arr[0].length;
  let row = 0;
  let column = columns -1;

  while(row < rows && column >=0 ){
    if (arr[row][column] == num) {
      return true;
    } else if (arr[row][column] > num) {
      column --;
    } else {
      row ++ ;
    }
  }
  return false;
}
複製程式碼
2、字串替換

題述:實現一個函式,將字串中的每個空格替換成%20。如輸入'we arr happy', 則輸出'we%20are%20happy'

思路:可以使用正則替換與遍歷替換兩種方式

  //使用正則
  function replaceStr(str){
    if (typeof str !== 'string') {
      console.log('str is not string');
      return;
    }
    return str.replace(/\s/g, '%20')
  }

  //使用遍歷替換,需要遍歷str,識別空格然後替換字串
  function replaceStr2(str) {
    if (typeof str !== 'string') {
      console.log('str is not string');
      return;
    }
    let strArr = [];
    let len = str.length;
    let i = 0;
    while(i < len) {
      if (str[i] === ' ' ) {
        strArr[i] = '%20';
      } else {
        strArr[i] = str[i];
      }
    }
    return strArr.join('');
  }

複製程式碼
3、連結串列逆序列印

題述:輸入一個連結串列的頭結點,從尾到頭列印每個節點的值

思路:可以將連結串列翻轉,再列印,但會破壞連結串列的結構。還可以用棧儲存節點,然後列印

function displayLinkList(head) {
  let stack = [];
  let node = head;
  while(node) {
    stack.push(node);
    node = node.next;
  }
  for (let len = stack.length - 1; len >=0 ; len--) {
    console.log(stack[i].ele);
  }
}
複製程式碼
4、重建二叉樹

題述:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。

思路:在二叉樹的前序遍歷中,第一個數字總是樹的根節點的值,在中序遍歷中,根結點的值在序列的中間。找根節點,確定左右子樹,然後遞迴迴圈,關鍵是依次掛載'根'節點(確定其在左還是右)。前序確定根節點,中序確定左右節點

//節點定義
 function TreeNode(ele) {
   this.ele = ele;
   this.right = null;
   this.left = null;
 }
 
 function constructBinaryTree(preOrders, inOrders) {
  if (!inOrders.length) {
    return null;
  }
  let rootIndex = 0;
  let l_preOrders = [];
  let l_inOrders = [];
  let r_preOrders = [];
  let r_inOrders = [];
  //確定根節點
  let head = new TreeNode(preOrders[0]);
  for (let i = 0; i < inOrders.length; i++ ) {
    if (preOrders[0] === inOrders[i]) {
      rootIndex = i;
    }
  }
  //確定左右子節點樹
  for (let i = 0; i < rootIndex; i++) {
    l_preOrders.push(preOrders[ i + 1]);
    l_inOrders.push(inOrders[i]);
  }

  for (let i = rootIndex + 1; i < inOrders.length; i ++ ) {
    r_preOrders.push(preOrders[i]);
    r_inOrders.push(inOrders[i]);
  }

  head.left = constructBinaryTree(l_preOrders, l_inOrders);
  head.right = constructBinaryTree(r_preOrders, r_inOrders);

  return head;
 }

 function getTreeFromPreInOrders(preOrders, inOrders) {
  if (Array.isArray(preOrders) && Array.isArray(inOrders)) {
    return constructBinaryTree(preOrders, inOrders);
  }
  console.error('preOrders or inOrders is no Array');
 }
複製程式碼
5、棧與佇列的互相實現

棧:先進後出, 佇列:先進先出

  • 題述:用兩個棧實現佇列

    思路:棧a的資料全部依次放到棧b,那麼原先早進入棧a的資料會出現在棧b棧頂的位置, 那麼佇列的出隊,相當於棧b的出棧,佇列的入隊,相當於棧a的入棧。當棧b為空時,將棧a的資料全部出棧到棧b

 let stack_a = [];
 let stack_b = [];

 function push (node ) {
   stack_a.push(node);
 }

 function pop () {
   if (stack_b.length === 0 ) {
     for (let i = 0, len = stack_a.length; i < len; i ++ ) {
       stack_b.push(stack_a.pop());
     }
   } 
   return stack_b.pop();
 }

複製程式碼
  • 題述:使用兩個佇列實現棧

    思路:兩個佇列,拿一個佇列做儲存區,有資料的佇列依次出隊資料到快取佇列,那麼當有資料的佇列出到最後一個資料時,即是需要出棧的資料。入棧的資料入隊到有資料的佇列,如果兩個為空,任取一個入隊

 let queue_a = [];
 let queue_b = [];

 function push(node) {
  if (queue_a.length && queue_b.length) {
    return console.log('wrong !');
  }
  if (queue_a.length) {
    queue_a.push(node);
  } else if (queue_b.length) {
    queue_b.push(node);
  } else {
    queue_a.push(node);
  }
 }

 function pop() {
  if (queue_a.length && !queue_b.length) {
    for (let i = 0, len = queue_a.length; i < len; i++) {
      if (i == len -1) {
        return queue_a.shift();
      } else {
        queue_b.push(queue_a.shift());
      }
    }
  } else if (!queue_a.length && queue_b.length) {
    for (let i = 0, len = queue_b.length; i < len; i++) {
      if (i == len -1) {
        return queue_b.shift();
      } else {
        queue_a.push(queue_b.shift());
      }
    }
  } else if (queue_a.length && queue_b.length) {
    console.log('wrong!');
  } else {
    return null;
  }
  return null;
 }
複製程式碼
6、旋轉陣列的最小數字

題述:把一個數字最開始的若干個元素搬到陣列的末尾,稱之為陣列的旋轉。輸入一個遞增排序的陣列的一個旋轉,輸出旋轉陣列的最小元素。例如陣列{3,4,5,1,2}為{1,2,3,4,5}的一個旋轉,該陣列的最小值為1

思路:遞增有序找最值,可以嘗試二分法。陣列第一個元素肯定會比最後一個元素大,選擇中間元素,與末尾元素比較,如果大於末尾元素則表示最小元素在右區間,否則在左區間

function findMinFromRotateArr(arr) {
  if (!Array.isArray(arr)) {
    return console.error('wrong!')
  }
  let start = 0;
  let end = arr.length - 1;
  while((end - start) > 1) {
    let mid = Math.floor(((end + start)) / 2) ;
    if (arr[mid] >= arr[end]) {
      start = mid;
    } else {
      end = mid;
    }
  } 
  return arr[end];
}
複製程式碼
7、斐波那契數列

題述:當n = 0,f(n) = 0;當n = 1, f(n) = 1;當n > 1, f(n) = f(n-1) + f(n-2)。現在要求輸入一個整數n,請你輸出斐波那契數列的第n項

思路:斐波那契數列是一個經典數學題。可以採用遞迴與迴圈方式解決,注意遞迴下,如果n比較大時,會產生很大記憶體消耗

//遞迴解法
function fibonacci(n) {
  if (n <= 0) {
    return 0;
  }
  if(n == 1) {
    return 1;
  }
  return fibonacci(n - 2) + fibonacci(n-1);
}

//迴圈解法
function fabonacci(n) {
  if (n <= 0) {
    return 0;
  }
  if(n == 1) {
    return 1;
  };
  let fn_2 = 0;
  let fn_1 = 1;
  let fn = 0;
  for (let i = 2; i <= n; i++) {
    fn = fn_1 + fn_2;
    fn_2 = fn_1;
    fn_1 = fn;
  }
  return fn;
}
複製程式碼
  • 斐波那契變題1 題述:一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

    思路:n個臺階跳法-> f(n), 假如其第一次跳一級,那麼接下來是跳法是f(n-1),假如第一次跳2級,那麼跳法是f(n-2)。那麼f(n) = f(n-1) + f(n-2),就是一個斐波那契數列

  • 斐波那契變題2 題述:題述:[] 這是 2x1的矩形,可以橫著或者豎著擺放,那麼其覆蓋 8*2x1這樣的小矩形有多少種擺法

    //大矩形:[][][][][][][][]
    //       [][][][][][][][]                                                        
    複製程式碼

    思路:如果豎著擺,那麼會佔去1列,如果橫著擺,一種擺法會佔去2列,那麼從8列的矩形,第一次擺放的時候,要麼豎著擺,接著覆蓋7列矩形,要麼橫著擺,接著覆蓋6列矩形。從而可以抽象成 f(8) = f(7) + f(6)。還是一個斐波那契問題

8、位運算

js中的位運算:&(與), |(或) , ~(非) ,^(異或), <<(左移), >>(右移), >>>(無符號右移)

  • 題述:輸入一個整數,輸出該數二進位制表示中1的個數。其中負數用補碼錶示。

    思路:可以使用右移與位與運算。判斷整數的二進位制數的最右側數是不是1(和1與),然後右移,直至為0

//缺陷版:
//缺陷在於不能針對負數情況。因為帶符號的數字,其二進位制最高位有一個數字為符號標誌,負數為1
function numOf1(n) {
  if(n.toString().indexOf('.') != -1) {
    return console.error('n is not a int');
  }
  let num = 0;
  while(n) {
    if (n & 1) {
      num ++ ;
    }
    n = n >> 1;
  }
  return num;
}

//改進:將1進行左移與i比較,這樣來判斷i二進位制各個位是不是1
//如果是32位儲存,那麼會迴圈32次
function numOf1(n){
  if(n.toString().indexOf('.') != -1) {
    return console.error('n is not a int');
  }
  let nums = 0;
  let flag = 1;
  while(flag) {
    if(flg & n) {
      nums ++;
    }
    flag = flag << 1;
  }
  return nums;
}

//究極版:這個的原理是 一個二進位制與其減去1的二進位制進行位與運算後,產生的數與原先的二進位制數相比,
//從右邊看會少去一個1。問題可以簡化到二進位制數有多少個1,就會進行以上多少次的迴圈,這個是效率最高的
function numsOf1(n) {
  if(n.toString().indexOf('.') != -1) {
    return console.error('n is not a int');
  }
  let nums = 0;
  while(n) {
    nums ++ ;
    n = (n - 1) & n;
  }
  return nums;
}
複製程式碼
9、數值的整數次方

題述:給定一個double型別的浮點數base和int型別的整數exponent。求base的exponent次方。不使用庫函式

思路:解題的第一反應是用for迴圈累加乘積,但可能忽略一些情況:輸入的0值與負整數次冪。還有如何減少遍歷次數

function power(base, exponent) {
  if (base == 0 && exponent < 0) {
    return console.error('base should not be 0');
  }
  let absExponent = exponent < 0 ? -exponent : exponent;
  let result = 1;
  for (let i = 1; i <= absExponent; i++) {
    result *= base;
  }
  if (exponent < 0) {
    result = 1 / result;
  }
  return result;
}

//使用遞迴減少乘積次數
//使用位與運算可判斷奇偶, 整數右移一位可取數除2的整數
//可以通過互乘減少運算次數,如 數的8次方是數的4次冪的2次冪,數的4次冪是數的2次冪的2次冪 ...
function power (base, exponent) {
  if (exponent == 0) {
    return 1;
  }
  if (exponent == 1) {
    return base;
  }
  let result = power(base, exponent >> 1);
  result *= result;
  //為奇數
  if (exponent & 1 == 1) {
    result *= base;
  }
  return result;
}

複製程式碼
10、刪除連結串列節點

題述:定義一個刪除節點的函式,傳參為頭結點與待刪除節點,要求時間複雜度為O(1)。

思路:常規連結串列刪除,會迴圈遍歷到待刪除節點,然後將其前一個節點指向其後一個節點。但是每次刪除需要遍歷,時間複雜度為O(n)。如果直接將待刪除節點的下一個節點的值賦予給待刪除節點,然後刪除這個下一個節點,不是就相當於刪除了麼。

function deleteNode(headNode, deleteNode) {
  if (!headNode || !deleteNode) {
    return ;
  }
  //刪除的節點是頭結點
  if (headNode == deleteNode) {
    headNode = null;
  }
  //刪除的節點是尾節點
  else if (deleteNode.next == null) {
    let node = headNode;
    while(node.next != deleteNode) {
      node = node.next;
    }
    node.next = null;
    deleteNode = null;
  }
  //刪除的節點是中間節點
  else {
    let nextNode = delete.next;
    deleteNode.ele = nextNode.ele;
    deleteNode.next = nextNode.next;
    nextNode = null;
  }
}
//整體時間:[(n-1)O(1) + O(n)]/n -> O(1)
複製程式碼
11、調整陣列順序

題述:輸入一個整數陣列,實現一個函式來調整該陣列中數字的順序,使得所有的奇數位於陣列的前半部分,所有的偶數位於位於陣列的後半部分。

思路:常規下可以遍歷陣列,如果數是偶數,可以將數拿出放到陣列最後面,其後面的數字前移一位。同時也可以使用兩個指標,一個指向陣列頭p1,一個指向陣列尾p2,如果p1指向偶數,p2指向奇數,則雙方對調,這樣會出現4種情況,依次處理即可。

function reOrderArray(arr)
{
    // write code here
    if (!Array.isArray(arr)) {
    return ;
  };
  let start = 0;
  let end = arr.length - 1;
  while(start <= end) {
    let isOddS = arr[start] & 1;
    let isEvenE = !(arr[end] & 1);
   
    if (isOddS && !isEvenE) {
      start ++;
    } else if (isOddS && isEvenE) {
      start ++;
      end --;
    } else if(!isOddS && isEvenE) {
      end --;
    } else {
      let temp = arr[start];
      arr[start] = arr[end];
      arr[end] = temp;
      start ++ ;
      end --;
    }
  }
  return arr;
}
複製程式碼
12、連結串列中導數第k個結點

題述:輸入一個連結串列,輸出該連結串列中倒數第k個結點。

思路:一般想法可以第一次遍歷連結串列得到其長度,然後倒數第k個節點,那麼則是第n+1-k個節點,然後第二次遍歷連結串列即可得出,這樣的缺點是需要遍歷連結串列兩次。遍歷一次連結串列的做法:取兩個指標,一個指標指向頭節點,另外一個指標指向第k-1個節點,然後兩個指標同時遍歷,當第二個指標指向連結串列尾的時候,那麼第一個指標會指向導數第k個節點

//注意邊界情況:頭結點為空,節點數小於k個,k不大於0

function findKthToTial (head, k) {
  if (!head || k <= 0) {
    return null;
  }
  let startNode = head;
  let endNode = head;
  for (let i = 0; i < k - 1; i++) {
    if (!endNode.next) {
      return null;
    }
    endNode = endNode.next;
  }
  while(endNode.next) {
    startNode = startNode.next;
    endNode = endNode.next;
  }
  return startNode;
}
複製程式碼
13、反轉連結串列

題述:輸入一個連結串列,反轉連結串列後,輸出新連結串列的表頭。

思路:遍歷連結串列,將下一個節點指向前一個節點

function resverseList(head) {
  if (!head) {
    return null;
  }
  if (head.next == null) {
    return head;
  }
  let node = head;
  let nextNode = null;
  let reservedNode = null;
  let newHead = head;
  while (node.next) {
    nextNode = node.next;
    reservedNode = nextNode.next;
    nextNode.next = newHead;
    node.next = reservedNode;
    newHead = nextNode;
  }
  return newHead;
}
複製程式碼
14、合併兩個排序的連結串列

題述:輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則

思路:依次去取兩個連結串列的節點進行比較

function mergeLinkList(head1, head2) {
  if (head1 == null) {
    return head2;
  }
  if (head2 == null) {
    return head1;
  }
  let mergeHead = null;
  if (head1.ele < head2.ele ) {
    mergeHead = head1;
    mergeHead.next = mergeLinkList(haed1.next, head2);
  } else {
    mergeHead = head2;
    mergeHead.next = mergeLinkList(head1, head2.next);
  }
  return mergeHead;
}
複製程式碼
15、二叉樹的包含

輸入兩顆二叉樹A和B,判斷B是不是A的子結構。

思路:先找A包含B的根節點,然後根據該節點比較左右子樹

//樹節點定義
function Node(ele) {
  this.ele = ele;
  this.left = null;
  this.right = null;
}

//判斷樹A有樹B
function hasSubTree(pRootA, pRootB) {
  if(pRootA == null || pRootB == null) {
    return false;
  }
  let result = false;
  if (pRootA.ele === pRootB.ele) {
    result = doesTreeAHaveTreeB(pRootA, pRootB);
  }
  if (!result) {
    result = hasSubTree(pRootA.left, pRootB);
  }
  if (!result) {
    result = hasSubTree(pRootA.right, pRootB)
  }
  return result;
}

function doesTreeAHaveTreeB(pRootA, pRootB) {
   //先要判斷 pRootB
  if (pRootB == null) {
    return false;
  }

  if(pRootA == null) {
    return true;
  }
 
  if (pRootA.ele != pRootB.ele) {
    return false;
  }

  return doesTreeAHaveTreeB(pRootA.left, pRootB.left) && doesTreeAHaveTreeB(pRootA.right, pRootB.right)
}

複製程式碼
16、二叉樹的映象

題述:完成一個函式,輸入一個二叉樹,該函式輸出它的映象

思路:進行前序遍歷,對於非葉子節點,有兩個節點,則將其對換


function mirror(root) {
  if (root == null) {
    return ;
  }

  let temp = root.left;
  root.left = root.right;
  root.right = temp;

  if (root.left) {
    mirror(root.left);
  }
  if (root.right) {
    mirror(root.right);
  }
}
複製程式碼
17、順時針列印矩陣

題述:/輸入一個矩陣,按照從外向裡以順時針的順序依次列印出每一個數字,例如,如果輸入如下矩陣: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 則依次列印出數字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10。

思路:關鍵在於迴圈列印的條件在於 列數 > 開始列印的列數x2 ,而且 行數 > 開始列印的行數x2

function printMatrix (arr) {
  if (!Array.isArray(arr)) {
    return;
  }
  let rows = arr.length;
  let columns = arr[0].length;
  let start = 0;
  while( columns > start * 2 && rows > start * 2) {
    printMatrixInCicle(arr, columns, rows, start);
    start ++ ;
  }
}

function printMatrixInCicle (arr, columns, rows, start) {
  let endX = columns - 1 - start;
  let endY = rows -1 - start;
  //從左到右列印一行
  for (let i = start; i <= endX; ++i) {
    console.log(arr[start][i]);
  }
  //從上到下列印一列
  if (start < endY) {
    for (let i = start + 1; i <= endY; ++ i) {
      console.log(arr[endY][i]);
    }
  }
  //從右向左列印一行
  if (start < endX && start < endY) {
    for (let i = endX -1 ; i >= start; --i) {
      console.log(arr[endY][i]);
    }
  }

  //從下到上列印一行
  if (start < endX && start < endY - 1) {
    for (let i = endY -1 ; i >= start + 1; --i) {
      console.log(arr[i][start]);
    }
  }
}
複製程式碼

各位觀眾老爺,如果覺得可以的話,biaozhiwang.github.io star下

相關文章