數組回爐重造+6道前端算法面試高頻題解

溫故而知新

數組是一種線性表數據結構,它用一組連續的內存空間,來存儲一組具備相同類型的數據。

數組能夠根據索引下標隨機訪問(時間複雜度爲 O(1)),這個索引一般來講是數字,用來計算元素之間的存儲位置的偏移量。javascript

與其餘編程語言不一樣,JavaScript 中的數組長度能夠隨時改變,數組中的每一個槽位能夠儲存任意類型的數據,而且其數據在內存中也能夠不連續。前端

上文提到,這個索引一般是數字,也就是說在 JavaScript 中,經過字符串也能夠訪問對應的元素:java

const arr = [0, 1, 2]
arr['1'] // 1

其實,JavaScript 中的數組是一種比較特殊的對象,由於在 JavaScript 中,對象的屬性名必須是字符串,這些數字索引就被轉化成了字符串類型。git

建立數組

// 1. 使用 Array 構造函數
let webCanteen = new Array()
// 初始爲 20 的數組
let webCanteen = new Array(20)
// 傳入要保存的元素
let webCanteen = new Array('食堂老闆', '店小二', '大廚')
// 若是傳入了非數值,則會建立一個只包含該特定值的數組
let webCanteen = new Array('前端食堂')
// 省略 new 操做符
let webCanteen = Array(20)

// 2. 使用數組字面量

let webCanteen = ['食堂老闆', '店小二', '大廚']
let webCanteen = []

ES6 新增了 2 個用於建立數組的靜態方法:Array.ofArray.fromgithub

Array.of 用於將一組參數轉換爲數組實例(不考慮參數數量和類型),而 Array.from 用於將類數組結構和可遍歷對象轉換爲數組實例(淺拷貝)。web

// Array.of 和 Array 構造函數之間的區別在於處理整數參數
Array(5)  // [, , , , ,]
Array(1, 2, 3) // [1, 2, 3]
// Array.from 擁有 3 個參數
// 1. 類數組對象,必選
// 2. 加工函數,新數組中的每一個元素會執行該回調,可選
// 3. this,表示加工函數執行時的 this,可選

const obj = {0: 10, 1: 20, 2: 30, length: 3}
Array.from(obj, function(item) {
    return item * 2
}, obj)
// [20, 40, 60]

// 不指定 this 的話,加工函數能夠是一個箭頭函數
Array.from(obj, (item) => item * 2)

檢測數組的六種方法

let arr = []
// 1. instanceof
arr instanceof Array 
// 2. constructor
arr.constructor === Array
// 3. Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr)
// 4. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype
// 5. Object.prototype.toString
Object.prototype.toString.call(arr) === '[object Array]'
// 6. Array.isArray ES6 新增
Array.isArray(arr)

前 4 種方法比較渣,絲絕不負責任,好比咱們將 arr 的 __proto__ 指向了 Array.prototype 後:面試

let arr = {
    __proto__: Array.prototype
}
// 1. instanceof
arr instanceof Array // true
// 2. constructor
arr.constructor === Array // true
// 3. Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr) // true
// 4. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype // true

截至 ES10 規範,數組共包含 35 個標準的 API 方法和一個非標準的 API 方法

建立數組:Array.of、Array.from算法

改變自身(9 種):poppushshiftunshiftreversesortsplicecopyWithinfill編程

不改變自身(12 種):concatjoinslicetoStringtoLocaleStringvalueOfindexOflastIndexOf、未造成標準的 toSource,以及 ES7 新增的方法 includes,以及 ES10 新增的方法 flatflatMap數組

不會改變自身的遍歷方法一共有(12 種): forEacheverysomefiltermapreducereduceRight,以及 ES6 新增的方法 findfindIndexkeysvaluesentries

本文就不給你們一一去介紹這些 API 的用法了,目的是爲你們起個頭,若是你對數組的 API 尚未爛熟於心的話,能夠找個整塊的時間統一的進行系統學習。

由於這些 API 正是咱們平常開發中的武器庫,完全搞清楚它們會大大提升開發效率,避免在開發中頻繁的查閱文檔。

我按照如上規律整理了一張表格,方便你總結。

改變自身 不改變自身 遍歷方法(不改變自身)
ES5及之前 pop、push、shift、unshift、reverse、sort、splice concat、join、slice、toString、toLocaleString、valueOf、indexOf、lastIndexOf forEach、every、some、filter、map、reduce、reduceRight
ES6+ copyWithin、fill includes、flat、flatMap、toSource find、findIndex、keys、values、entries

共同點

其實數組中的方法有一些共同之處,咱們能夠將其整理出來,更加方便咱們理解和記憶。

  • 插入元素的方法,好比 push、unshift 都返回數組新的長度。
  • 刪除元素的方法,好比 pop、shift、splice 都返回刪除的元素,或者返回刪除的多個元素組成的數組。
  • 部分遍歷方法,好比 forEach、every、some、filter、map、find、findIndex,它們都包含 function(value, index, array) {} 和 thisArg 這樣兩個形參。

定型數組

定型數組(typed array)是 ECMAScript 新增的結構,目的是提高向原生庫傳輸數據的效率。這部分的內容本文再也不展開,有興趣的同窗們能夠自行學習。

開啓刷題

回顧了 JavaScript 中數組的基本知識後,立刻開啓咱們愉快的刷題之旅,我整理了 6 道高頻的 LeetCode 數組題的題解以下。

01 兩數之和

🔗原題連接

1.暴力法

符合第一直覺的暴力法,潛意識裏要學會將兩數之和轉換爲兩數之差

而後問題就變得像切菜同樣簡單了,雙層循環找出當前數組中符合條件的兩個元素,並將它們的索引下標組合成數組返回即所求。

const twoSum = function(nums, target) {
    for (let i = 0; i < nums.length; i++) {
        for (let j = i + 1; j < nums.length; j++) {
            if (nums[i] === target - nums[j]) {
                return [i, j]
            }
        }
    }
}
  • 時間複雜度:O(n^2)
  • 空間複雜度:O(1)

寫出這種方法是不會讓面試官滿意的,因此接下來咱們要想辦法進行優化。

2.借用 Map

算法優化的核心方針基本上都是用空間換時間。

咱們能夠借用 Map 存儲遍歷過的元素及其索引,每遍歷一個元素時,去 Map 中查看是否存在知足要求的元素。

若是存在的話將其對應的索引從 Map 中取出和當前索引值組合爲數組返回即爲所求,若是不存在則將當前值做爲鍵,當前索引做爲值存入。

題目要求返回的是數組下標,因此 Map 中的鍵名是數組元素,鍵值是索引。

const twoSum = function(nums, target) {
    const map = new Map()
    for (let i = 0; i < nums.length; i++) {
        const diff = target - nums[i]
        if (map.has(diff)) {
            return [map.get(diff), i]
        }
        map.set(nums[i], i)
    }
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(n)

02 盛水最多的容器

🔗原題連接

雖然是中等難度,但這道題解起來仍是比較簡單的,老規矩,咱們看下符合第一直覺的暴力法:

暴力法

幼兒園數學題:矩形面積 = 長 * 寬

放到咱們這道題中,矩形的長和寬就分別對應:

  • 長:兩條垂直線的距離
  • 寬:兩條垂直線中較短的一條的長度

雙重 for 循環遍歷全部可能,記錄最大值。

const maxArea = function(height) {
    let max = 0 // 最大容納水量
    for (let i = 0; i < height.length; i++) {
        for (let j = i + 1; j < height.length; j++) {
            // 當前容納水量
            let cur = (j - i) * Math.min(height[i], height[j])
            if (cur > max) {
                max = cur
            }
        }
    }
    return max
}
  • 時間複雜度:O(n^2)
  • 空間複雜度:O(1)

暴力法時間複雜度 O(n^2) 過高了,咱們仍是要想辦法進行優化。

雙指針

咱們能夠借用雙指針來減小搜索空間,轉換爲雙指針的視角後,回顧矩形的面積對應關係以下:

(矩形面積)容納的水量 = (兩條垂直線的距離)指針之間的距離 * (兩個指針指向的數字中的較小值)兩條垂直線中較短的一條的長度

設置兩個指針,分別指向頭和尾(i指向頭,j指向尾),不斷向中間逼近,在逼近的過程當中爲了找到更長的垂直線:

  • 若是左邊低,將i右移
  • 若是右邊低,將j左移

有點貪心思想那味兒了,由於更長的垂直線能組成更大的面積,因此咱們放棄了較短的那一條的可能性。

可是這麼作,咱們有沒有更能漏掉一個更大的面積的可能性呢?

先告訴你答案是不會漏掉。

關於該算法的正確性證實已經有不少同窗們給出了答案,感興趣的請戳下面連接。

const maxArea = function(height) {
    let max = 0 // 最大容納水量
    let left = 0 // 左指針
    let right = height.length - 1 // 右指針
    
    while (left < right) {
        // 當前容納水量
        let cur = (right - left) * Math.min(height[left], height[right]);
        max = Math.max(cur, max)
        height[left] < height[right] ? left ++ : right --
    }

    return max
};
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

03 三數之和

🔗原題連接

先明確,題目不只是要求 a + b + c = 0,並且須要三個元素都不重複。

因此咱們能夠先將數組從小到大排序,排序後,去除重複項會更加簡單。

  1. 層循環指針 i 遍歷數組,題目要求三數之和爲 0,考慮這次循環中的數若大於 0,另外兩個數確定也大於 0,則當前位置下無解。
  2. 重,若是當前元素和前一個元素相同時,直接跳過便可。
  3. 層循環藉助雙指針夾逼求解,兩個指針收縮時也要去除重複的位置。
  4. 數之和爲 0 時,將當前三個指針位置的數字推入數組即所求。若當前和小於 0 則將左指針向右移動,若當前和大於 0 則將右指針向左移動。
const threeSum = function(nums) {
    const result = []
    const len = nums.length
    if (len < 3) {
        return result
    }
    nums.sort((a, b) => a - b)
    for (let i = 0; i < len - 2; i++) {
        if (nums[i] > 0) {
            break
        }
        if (i > 0 && nums[i] === nums[i - 1]) {
            continue
        }
        let L = i + 1
        let R = len - 1
        while (L < R) {
            let sum = nums[i] + nums[L] + nums[R]
            if (sum === 0) {
                result.push([nums[i], nums[L], nums[R]])
                while(nums[L] === nums[L + 1]){
                    L++
                }
                while(nums[R] === nums[R - 1]){
                    R--
                }
                L++
                R--
            } else if (sum < 0) {
                L++
            } else {
                R--
            }
        }
    }
    return result
}
  • 時間複雜度:O(n^2)
  • 空間複雜度:O(1)

04 刪除排序數組中的重複項

🔗原題連接

雙指針

題目要求原地刪除重複出現的元素,不要使用額外的數組空間,返回移除後數組的新長度。

先明確,這道題給咱們提供的是排好序的數組,因此重複的元素必然相鄰。

因此實際上咱們只須要將不重複的元素移到數組的左側,並返回其對應的長度便可。

  1. 藉助雙指針,i 從索引 0 開始,j 從索引 1 開始。
  2. 當前項 nums[j] 與前一位置 nums[j - 1] 相等時,j++ 跳太重複項。
  3. 當兩者不相等時,意味着不是重複項,此時須要將 i 指針右移, 並將 nums[j] 複製到此時的 nums[i] 位置上,而後將指針 j 右移。
  4. 重複上述過程,直到循環完成,最終返回 i + 1,由於題目要求返回長度,i 是索引位置,加 1 即所求。
const removeDuplicates = function(nums) {
    if (nums.length === 0) return 0
    let i = 0
    const n = nums.length
    for (let j = 1; j < n; j++) {
        if (nums[j] != nums[j - 1]) {
            i++
            nums[i] = nums[j]
        }
    }
    return i + 1
};
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

05 加一

🔗原題連接

加一,其實就是小學數學題,很簡單,咱們逐步來分析。

數字 9 加 1 會進位,其餘的數字不會。

因此,狀況無非下面這幾種:

  1. 1 + 1 = 2 末位無進位,則末位加 1 便可。
  2. 299 + 1 = 300 末位有進位,首位加 1。
  3. 999 + 1 = 1000 末位有進位,最終致使前方多出一位,循環以外單獨處理。
const plusOne = function(digits) {
    for (let i = digits.length - 1; i >= 0; i--) {
        if (digits[i] === 9) {
            digits[i] = 0
        } else {
            digits[i]++
            return digits
        }
    }
    digits.unshift(1)
    return digits
};
  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

06 移動零

🔗原題連接

雙指針

題目要求將全部 0 移動到數組的末尾,同時還要保持非零元素的相對順序。

在此基礎上附加了兩個條件:

  1. 必須在原數組上操做,不能拷貝額外的數組。
  2. 儘可能減小操做次數。

咱們能夠藉助雙指針來進行求解,求解過程以下:

  1. 初始化雙指針 i 、j 指向數組頭部索引 0。
  2. 將 i 指針不斷向右移動,j 指針負責提供交換的位置,當遇到非 0 數字時,將兩個指針位置的數字交換,同時繼續向右邊移動兩個指針。這樣交換能夠保證題目要求的非 0 數字相對順序不變。
  3. 當遇到 0 時,向右移動 i 指針,j 指針不動。
  4. 循環完成時便可將全部的 0 移動到數組的末尾。
const moveZeroes = function (nums) {
    let i = 0, j = 0;
    while (i < nums.length) {
        if (nums[i] != 0) {
            [nums[i], nums[j]] = [nums[j], nums[I]];
            i++;
            j++;
        } else {
            i++;
        }
    }
}
  • 時間複雜度: O(n)
  • 空間複雜度: O(1)

2021 組團刷題計劃

年初立了一個 flag,上面這個倉庫在 2021 年寫滿 100 道前端面試高頻題解,目前進度已經完成了 50%

若是你也準備刷或者正在刷 LeetCode,不妨加入前端食堂,一塊兒並肩做戰,刷個痛快。

image

相關文章
相關標籤/搜索