「面試指南」JS數組Array經常使用算法,Array算法的通常解答思路

先看一道面試題

在 LeetCode 中有這麼一道簡單的數組算法題:javascript

// 給定一個整數數組 nums 和一個目標值 target,
// 請你在該數組中找出和爲目標值的那兩個整數,並返回他們的數組下標。
// 你能夠假設每種輸入只會對應一個答案。
// 可是,你不能重複利用這個數組中一樣的元素。
// 示例:
//    給定 nums = [2, 7, 11, 15], target = 9;
//    由於 nums[0] + nums[1] = 2 + 7 = 9,
//    因此返回 [0, 1]。

對於上述的面試題,對於咱們前端開發,不一樣的解法,有着不一樣的技術水準。前端

那麼到底有幾種經常使用解法?實踐並彙總瞭如下幾種方法:java

  • 暴力雙 for 循環解法;
  • 單循環 indexOf 優化;
  • 單循環 obj 優化;
  • 單循環 map 優化;
  • 單循環尾遞歸優化;

暴力雙 for 循環破解

// 兩層循環判斷,找出當前元素cur與target-cur的,知足放入result結果中
function twoSum(nums, target) {
  for (let i = 0; i < nums.length; i++) {
    const cur = nums[i];

    for (let j = 0; j < nums.length; j++) {
      const others = nums[j];
      // 由於是不能夠重複利用一樣的元素,因此i!==j;
      if (others == target - cur && i !== j) {
        // 由於是咱們只找出一個結果,因此咱們找到後,直接返回結果
        return [i, j];
      }
    }
  }
  // 若是未找到,返回[]
  return [];
}

// 測試結果
let result = twoSum([2, 7, 11, 15], 9);
console.log(result); // [0,1]  2,7 知足結果,因此返回其下標[0,1]

時間複雜度:O(n^2),可能看似感受還不錯,可是執行時間長,內存佔用也不小,當 nums 數組足夠大時,它的性能瓶頸就會體現出來。面試

leetCood 測試結果:算法

csjg

單循環 indexOf 優化;

// 單循環判斷,找出當前元素cur,與target-cur是否相等,知足放入result結果中
function twoSum(nums, target) {
  for (let i = 0; i < nums.length; i++) {
    let cur = nums[i],
      others = target - cur, // 指望目標值
      others_index = nums.indexOf(others);

    // 判斷指望目標值是否在nums中,由於不能是它自己,要校驗兩個下標不能相等
    if (others_index > -1 && i !== others_index) {
      // 由於是咱們只找出一個結果,因此咱們找到後,直接返回結果
      return [i, others_index];
    }
  }
  // 若是未找到,返回[]
  return [];
}

// 測試結果
let result = twoSum([2, 7, 11, 15], 9);
console.log(result); // [0,1]  2,7 知足結果,因此返回其下標[0,1]

時間複雜度:O(n^2),由於 indexOf()方法的時間複雜度爲 O(n),因此和上述暴力破解只是寫法上區別了。執行時間,內存佔用依然存在可優化的空間。數組

leetCood 測試結果:微信

測試結果

單循環 obj 優化:

使用 obj,邊存邊比較目標差值是否在 obj 中。若是存在,直接返回下標,不存在繼續邊存邊比,直到結束循環;數據結構

function twoSum(nums, target) {
  let obj = {};

  for (let i = 0; i < nums.length; i++) {
    if (obj[target - nums[i]] >= 0) {
      return [obj[target - nums[i]], i];
    }
    obj[nums[i]] = i;
  }
  return [];
}
// 測試結果
let result = twoSum([2, 7, 11, 15], 9);
console.log(result); // [0,1]  2,7 知足結果,因此返回其下標[0,1]

時間複雜度:O(n),因爲對象鍵值對 key-value 的優越性,對於做爲查找類的算法頗有優點。時間複雜度降爲原有的一倍,性能會好一些。函數

leetCood 測試結果(較上優化了 90ms 左右):性能

測試結果

單循環 map 優化:

上述咱們使用了一個對象做爲查找的依據,一樣的咱們能夠根據 map 替換,來破解。

function twoSum(nums, target) {
  let map = new Map();

  // 遍歷nums 放入 map中
  for (let i = 0; i < nums.length; i++) {
    let value = nums[i];

    map.set(value, i);
  }

  for (let j = 0; j < nums.length - 1; j++) {
    if (map.has(target - nums[j]) && map.get(target - nums[j]) != j) {
      return [j, map.get(target - nums[j])];
    }
  }
  // 不符合,返回空數組
  return [];
}
// 測試結果
let result = twoSum([2, 7, 11, 15], 9);
console.log(result); // [0,1]  2,7 知足結果,因此返回其下標[0,1]

時間複雜度:O(2n),第一次循環時間度 n,第二次爲 n*1,故爲 O(2n), 因爲 map 的特殊數據結構,故做爲查找類的算法,相比 obj 具備絕對優點。

leetCood 測試結果(較上再次優化了 近 30ms):

測試結果

obj 尾遞歸優化;

咱們對於上面單循環 obj 作下改造,利用尾遞歸的方式破解:

var twoSum = function(nums, target, i = 0, objs = {}) {
  const obj = objs; //存在指望數字;
  // 判斷obj中是否
  if (obj[target - nums[i]] >= 0) {
    // 存在直接返回兩值的下標;
    return [obj[target - nums[i]], i];
  } else {
    // 不存在,存入obj
    obj[nums[i]] = i;
    // 遞歸繼續查找
    if (i < nums.length - 1) {
      // i 自增
      i++;
      return twoSum(nums, target, i, obj);
    } else {
      // 遞歸結束,未查詢到結果
      return [];
    }
  }
};

時間複雜度:O(n),假設咱們查找到,則遞歸的次數應該是最多的爲 n,因此時間複雜度 O(n);
遞歸相比於 for 循環是一種更近層次的查找,在樹結構數據、多維數組中咱們經常使用遞歸思想來處理數據。

leetCood 測試結果(結果爲 52ms),屢次執行測試大都在 60ms 上下,說明了遞歸思想的優點:

測試結果

map 尾遞歸優化破解;

咱們同時對單循環 map 的也是用遞歸,看看會發生什麼結果?

var twoSum = function(nums, target, i = 0, maps = new Map()) {
  const map = maps;

  // 判斷obj中是否
  if (map.has(target - nums[i])) {
    // 存在直接返回兩值的下標;
    return [map.has(target - nums[i]), i];
  } else {
    // 不存在,存入obj
    map.set([nums[i]], i);
    // 遞歸繼續查找
    if (i < nums.length - 1) {
      // i 自增
      i++;
      return twoSum(nums, target, i, map);
    } else {
      // 遞歸結束,未查詢到結果
      return [];
    }
  }
};

時間複雜度:O(n),假設咱們查找到,則遞歸的次數爲 n,因此時間複雜度也爲 O(n);

leetCood 測試結果(最快結果爲 44ms),屢次執行測試大都在 60ms 上下,與上一個性能類似:

測試結果

固然,測試結果只是一個參考可能不太準確,不過經過屢次測試也是能夠看出他們之間的差距的。

總結:

以上咱們使用了暴力破解、單循環 obj、單循環 map、obj 尾遞歸、map 尾遞歸作了對比。

通常對於數組的算法,幾乎均可以使用上次思路來解決,固然咱們要知道衡量算法指標時間複雜度 O()、空間複雜度 S()。

空間複雜度:算法的空間複雜度經過計算算法所需的存儲空間實現,算法的空間複雜度的計算公式記做:S(n)=O(f(n)),其中,n 爲問題的規模,f(n)爲語句關於 n 所佔存儲空間的函數。

一般,咱們都是用「時間複雜度」來指運行時間的需求,是用「空間複雜度」指空間需求。

當直接要讓咱們求「複雜度」時,一般指的是時間複雜度。不過,在必定程度上咱們也要考慮算法所需存儲空間。

在面試中與實際工做中,簡單數組算法的幾點經驗之談:

數組去重:使用單循環,結合 obj 或 map 作中間輔助判斷;
數組扁平化:使用遞歸;

樹結構的查找與處理:單循環使用 obj/map 作中間輔助判斷,同時結合遞歸思想;

數組的特定重組:除了上述思想外,可能要結合數組經常使用方法:indexOf(),map(),forEach()或數組高階函數 filter(),reduce(),sort(),every(),some()等。本文只是拋出一個算法的思路,再也不作長篇大論的演示。

// 遞歸思路
// 最簡遞歸:for循環形式
function recursive_simple(array) {
  for (let i = 0; i < array.length; i++) {
    const item = array[i];

    // 進入遞歸ifEntry:遞歸條件,subArray:遞歸參數
    if (ifEntry) {
      // do something
      recursive_simple(subArray);
    } else {
      // 跳出遞歸
      // do something
    }
  }
}

// 尾遞歸
function recursive_tail(array, i = array.length - 1, others) {
  const other = others;
  //do something

  // 進入遞歸,others:其餘參數,能夠obj、map等一些中間臨時變量
  if (i > 0) {
    // do something
    console.log(i, array[i]);

    i--;
    // 遞歸調用
    return recursive_tail(array, i, others);
  }
}

涉及方法:

indexOf():檢測 searchString 在 string、array 是否存在,不過期間複雜度 O(n);

map:數組的遍歷,返回新的數組,須要手動 return 當前 item;對於數組中對象的 key-value 改寫比較適合,時間複雜度 O(n);

forEach:改寫當前數組,不須要 return,對於直接改寫某個數組比較合適;

filter:過濾函數,對於過濾數組中符合某個條件的子項比較合適;

reduce:接收一個函數做爲累加器(accumulator),返回具體數值,對於須要對數組某些子項操做的比較合適,好比求和,斐波那契數列等的處理,
reduce(function(total, currentValue, currentIndex, arr), initialValue);

sort:適合數組中,複雜比較關係的,通常用於排序用途;

every:數組迭代方法,對數組中每一項運行給定函數,若是該函數對每一項返回 true,則返回 true;

some:數組迭代方法,對數組中每一項運行給定函數,若是該函數對任一項返回 true,則返回 true,與 every 有區別,如其名:every:每一項,some:任一項;

微信公衆號:前端開發那些事兒,歡迎關注!

前端開發那些事兒

相關文章
相關標籤/搜索