JavaScript中的算法(附10道面試常見算法題解決方法和思路)

2021-03-02 前端開發博客
https://juejin.im/post/6844903811505455118Introduction

面試過程通常從最初的電話面試開始,然後是現場面試,檢查編程技能和文化契合度。幾乎毫無例外,最終的決定因素是還是編碼能力。通常上,不僅僅要求能得到正確的答案,更重要的是要有清晰的思維過程。寫代碼中就像在生活中一樣,正確的答案並不總是清晰的,但是好的推理通常就足夠了。有效推理的能力預示著學習、適應和進化的潛力。好的工程師一直是在成長的,好的公司總是在創新的。

算法挑戰是有用的,因為解決它們的方法不止一種。這為決策的制定和決策的計算提供了可能性。在解決算法問題時,我們應該挑戰自己從多個角度來看待問題的定義,然後權衡各種方法的優缺點。通過足夠的嘗試後,我們甚至可能看到一個普遍的真理:不存在「完美」的解決方案。

要真正掌握算法,就必須了解它們與數據結構的關係。數據結構和算法就像陰陽、水杯和水一樣密不可分。沒有杯子,水就不能被容納。沒有數據結構,我們就沒有對象來應用邏輯。沒有水,杯子是空的,沒有營養。沒有算法,對象就不能被轉換或「消費」。

要了解和分析JavaScript中的數據結構,請看JavaScript中的數據結構

Primer

在JavaScript中,算法只是一個函數,它將某個確定的數據結構輸入轉換為某個確定的數據結構輸出。函數內部的邏輯決定了怎麼轉換。首先,輸入和輸出應該清楚地提前定義。這需要我們充分理解手上的問題,因為對問題的全面分析可以很自然地提出解決方案,而不需要編寫任何代碼。

一旦完全理解了問題,就可以開始對解決方案進行思考,需要那些變量? 有幾種循環? 有那些JavaScript內置方法可以提供幫助?需要考慮那些邊緣情況?複雜或者重複的邏輯會導致代碼十分的難以閱讀和理解,可以考慮能否提出抽象成多個函數?一個算法通常上需要可擴展的。隨著輸入size的增加,函數將如何執行? 是否應該有某種緩存機制嗎? 通常上,需要犧牲內存優化(空間)來換取性能提升(時間)。

為了使問題更加具體,畫圖表!

當解決方案的具體結構開始出現時,偽代碼就可以開始了。為了給面試官留下深刻印象,請提前尋找重構和重用代碼的機會。有時,行為相似的函數可以組合成一個更通用的函數,該函數接受一個額外的參數。其他時候,函數柯裡的效果更好。保證函數功能的純粹方便測試和維護也是非常重要的。換句話說,在做出解決問題的決策時需要考慮到架構和設計模式。

Big O(複雜度)

為了計算出算法運行時的複雜性,我們需要將算法的輸入大小外推到無窮大,從而近似得出算法的複雜度。最優算法有一個恆定的時間複雜度和空間複雜度。這意味著它不關心輸入的數量增長多少,其次是對數時間複雜度或空間複雜度,然後是線性、二次和指數。最糟糕的是階乘時間複雜度或空間複雜度。算法複雜度可用以下符號表示:

Constabt: O(1)

Logarithmic: O(log n)

Linear: O(n)

Linearithmic: O(n log n)

Quadratic: O(n^2)

Expontential: O(2^n)

Factorial: O(n!)

在設計算法的結構和邏輯時,時間複雜度和空間複雜度的優化和權衡是一個重要的步驟。

Arrays

一個最優的算法通常上會利用語言裡固有的標準對象實現。可以說,在計算機科學中最重要的是數組。在JavaScript中,沒有其他對象比數組擁有更多的實用方法。值得記住的數組方法有:sort、reverse、slice和splice。數組元素從第0個索引開始插入,所以最後一個元素的索引是 array.length-1。數組在push元素有很好的性能,但是在數組中間插入,刪除和查找元素上性能卻不是很優,JavaScript中的數組的大小是可以動態增長的。

數組的各種操作複雜度

Map 和 Set是和數組相似的數據結構。set中的元素都是不重複的,在map中,每個Item由鍵和值組成。當然,對象也可以用來存儲鍵值對,但是鍵必須是字符串。

Iterations

與數組密切相關的是使用循環遍歷它們。在JavaScript中,有5種最常用的遍歷方法,使用最多的是for循環,for循環可以用任何順序遍歷數組的索引。如果無法確定迭代的次數,我們可以使用while和do while循環,直到滿足某個條件。對於任何Object, 我們可以使用 for in 和 for of循環遍歷它的keys 和values。為了同時獲取key和value我們可以使用 entries()。我們也可以在任何時候使用break語句終止循環,或者使用continue語句跳出本次循環進入下一次循環。

原生數組提供了如下迭代方法:indexOf,lastIndexOf,includes,fill,join。 另外我們可以提供一個回調函數在如下方法中:findIndex,find,filter,forEach,map,some,every,reduce。

Recursions

在一篇開創性的論文中,Church-Turing論文證明了任何迭代函數都可以用遞歸函數來複製,反之亦然。有時候,遞歸方法更簡潔、更清晰、更優雅。以這個迭代階乘函數為例:

const factorial = number => {
let product = 1
for (let i = 2; i <= number; i++) {
product *= i
}
return product
}

如果使用遞歸,僅僅需要一行代碼

const _factorial = number => {
return number < 2 ? 1 : number * _factorial(number - 1)
}

所有的遞歸函數都有相同的模式。它們由創建一個調用自身的遞歸部分和一個不調用自身的基本部分組成。任何時候一個函數調用它自身都會創建一個新的執行上下文並推入執行棧頂直。這種情況會一直持續到直到滿足了基本情況為止。然後執行棧會一個接一個的將棧頂元素推出。因此,對遞歸的濫用可能導致堆棧溢出的錯誤。

最後,我們一起來思考一些常見算法題!1. 字符串反轉

一個函數接受一個字符串作為參數,返回反轉後的字符串

describe("String Reversal", () => {
it("Should reverse string", () => {
assert.equal(reverse("Hello World!"), "!dlroW olleH");
})
})

思考

這道題的關鍵點是我們可以使用數組自帶的reverse方法。首先我們使用 split方法將字符串轉為數組,然後使用reverse反轉字符串,最後使用join方法轉為字符串。另外也可以使用數組的reduce方法

給定一個字符串,每個字符需要訪問一次。雖然這種情況發生了很多次,但是時間複雜度會正常化為線性。由於沒有單獨的內部數據結構,空間複雜度是恆定的。

const reverse = string => string.split('').reverse().join('')

const _reverse = string => string.split('').reduce((res,char) => char + res)

2. 回文

回文是一個單詞或短語,它的讀法是前後一致的。寫一個函數來檢查。

describe("Palindrome", () => {
it("Should return true", () => {
assert.equal(isPalindrome("Cigar? Toss it in a can. It is so tragic"), true);
})
it("Should return false", () => {
assert.equal(isPalindrome("sit ad est love"), false);
})
})

思考

函數只需要簡單地判斷輸入的單詞或短語反轉之後是否和原輸入相同,完全可以參考第一題的解決方案。我們可以使用數組的 every 方法檢查第i個字符和第array.length-i個字符是否匹配。但是這個方法會使每個字符檢查2次,這是沒必要的。那麼,我們可以使用reduce方法。和第1題一樣,時間複雜度和空間複雜度是相同的。

const isPalindrome = string => {
const validCharacters = "abcdefghijklmnopqrstuvwxyz".split("")
const stringCharacters = string // 過濾掉特殊符號
.toLowerCase()
.split("")
.reduce(
(characters, character) =>
validCharacters.indexOf(character) > -1
? characters.concat(character)
: characters,
[]
);
return stringCharacters.join("") === stringCharacters.reverse().join("")

3. 整數反轉

給定一個整數,反轉數字的順序。

describe("Integer Reversal", () => {
it("Should reverse integer", () => {
assert.equal(reverse(1234), 4321);
assert.equal(reverse(-1200), -21);
})
})

思考

把number類型使用toString方法換成字符串,然後就可以按照字符串反轉的步驟來做。反轉完成之後,使用parseInt方法轉回number類型,然後使用Math.sign加入符號,只需一行代碼便可完成。

由於我們重用了字符串反轉的邏輯,因此該算法在空間和時間上也具有相同的複雜度。

const revserInteger = integer => parseInt(number
.toString()
.split('')
.reverse()
.join('')) * Math.sign(integer)

4. 出現次數最多的字符

給定一個字符串,返回出現次數最多的字符

describe("Max Character", () => {
it("Should return max character", () => {
assert.equal(max("Hello World!"), "l");
})
})

思考

可以創建一個對象,然後遍歷字符串,字符串的每個字符作為對象的key,value是對應該字符出現的次數。然後我們可以遍歷這個對象,找出value最大的key。

雖然我們使用兩個單獨的循環來迭代兩個不同的輸入(字符串和字符映射),但是時間複雜度仍然是線性的。它可能來自字符串,但最終,字符映射的大小將達到一個極限,因為在任何語言中只有有限數量的字符。空間複雜度是恆定的。

const maxCharacter = (str) => {
const obj = {}
let max = 0
let character = ''
for (let index in str) {
obj[str[index]] = obj[str[index]] + 1 || 1
}
for (let i in obj) {
if (obj[i] > max) {
max = obj[i]
character = i
}
}
return character
}

5.找出string中元音字母出現的個數

給定一個單詞或者短語,統計出元音字母出現的次數

describe("Vowels", () => {
it("Should count vowels", () => {
assert.equal(vowels("hello world"), 3);
})
})

思考

最簡單的解決辦法是利用正則表達式提取所有的元音,然後統計。如果不允許使用正則表達式,我們可以簡單的迭代每個字符並檢查是否屬於元音字母,首先應該把輸入的參數轉為小寫。

這兩種方法都具有線性的時間複雜度和恆定的空間複雜度,因為每個字符都需要檢查,臨時基元可以忽略不計。

  const vowels = str => {
const choices = ['a', 'e', 'i', 'o', 'u']
let count = 0
for (let character in str) {
if (choices.includes(str[character])) {
count ++
}
}
return count
}

const vowelsRegs = str => {
const match = str.match(/[aeiou]/gi)
return match ? match.length : 0
}

6.數組分隔

給定數組和大小,將數組項拆分為具有給定大小的數組列表。

describe("Array Chunking", () => {
it("Should implement array chunking", () => {
assert.deepEqual(chunk([1, 2, 3, 4], 2), [[1, 2], [3, 4]]);
assert.deepEqual(chunk([1, 2, 3, 4], 3), [[1, 2, 3], [4]]);
assert.deepEqual(chunk([1, 2, 3, 4], 5), [[1, 2, 3, 4]]);
})
})

一個好的解決方案是使用內置的slice方法。這樣就能生成更乾淨的代碼。可通過while循環或for循環來實現,它們按給定大小的步驟遞增。

這些算法都具有線性時間複雜度,因為每個數組項都需要訪問一次。它們還具有線性空間複雜度,因為保留了一個內部的「塊」數組,它與輸入數組成比例地增長。

const chunk = (array, size) => {
const chunks = []
let index = 0
while(index < array.length) {
chunks.push(array.slice(index, index + size))
index += size
}
return chunks
}

7.words反轉

給定一個短語,按照順序反轉每一個單詞

describe("Reverse Words", () => {
it("Should reverse words", () => {
assert.equal(reverseWords("I love JavaScript!"), "I evol !tpircSavaJ");
})
})

思考

可以使用split方法創建單個單詞數組。然後對於每一個單詞,可以復用之前反轉string的邏輯。

因為每一個字符都需要被訪問,而且所需的臨時變量與輸入的短語成比例增長,所以時間複雜度和空間複雜度是線性的。

const reverseWords = string => string
.split(' ')
.map(word => word
.split('')
.reverse()
.join('')
).join(' ')

8.首字母大寫

給定一個短語,每個首字母變為大寫。

describe("Capitalization", () => {
it("Should capitalize phrase", () => {
assert.equal(capitalize("hello world"), "Hello World");
})
})

思考

一種簡潔的方法是將輸入字符串拆分為單詞數組。然後,我們可以循環遍歷這個數組並將第一個字符大寫,然後再將這些單詞重新連接在一起。出於不變的相同原因,我們需要在內存中保存一個包含適當大寫字母的臨時數組。

因為每一個字符都需要被訪問,而且所需的臨時變量與輸入的短語成比例增長,所以時間複雜度和空間複雜度是線性的。

const capitalize = str => {
return str.split(' ').map(word => word[0].toUpperCase() + word.slice(1)).join(' ')
}

9.凱撒密碼

給定一個短語,通過在字母表中上下移動一個給定的整數來替換每個字符。如果有必要,這種轉換應該回到字母表的開頭或結尾。

describe("Caesar Cipher", () => {
it("Should shift to the right", () => {
assert.equal(caesarCipher("I love JavaScript!", 100), "E hkra FwrwOynelp!")
})
it("Should shift to the left", () => {
assert.equal(caesarCipher("I love JavaScript!", -100), "M pszi NezeWgvmtx!");
})
})

思考

首先我們需要一個包含所有字母的數組,這意味著我們需要把給定的字符串轉為小寫,然後遍歷整個字符串,給每個字符增加或減少給定的整數位置,最後判斷大小寫即可。

由於需要訪問輸入字符串中的每個字符,並且需要從中創建一個新的字符串,因此該算法具有線性的時間和空間複雜度。

const caesarCipher = (str, number) => {
const alphabet = "abcdefghijklmnopqrstuvwxyz".split("")
const string = str.toLowerCase()
const remainder = number % 26
let outPut = ''
for (let i = 0; i < string.length; i++) {
const letter = string[i]
if (!alphabet.includes(letter)) {
outPut += letter
} else {
let index = alphabet.indexOf(letter) + remainder
if (index > 25) {
index -= 26
}
if (index < 0) {
index += 26
}
outPut += str[i] === str[i].toUpperCase() ? alphabet[index].toUpperCase() : alphabet[index]
}
}
return outPut
}

10.找出從0開始到給定整數的所有質數
describe("Sieve of Eratosthenes", () => {
it("Should return all prime numbers", () => {
assert.deepEqual(primes(10), [2, 3, 5, 7])
})
})

思考

最簡單的方法是我們循環從0開始到給定整數的每個整數,並創建一個方法檢查它是否是質數。

const isPrime = n => {
if (n > 1 && n <= 3) {
return true
} else {
for(let i = 2;i <= Math.sqrt(n);i++){
if (n % i == 0) {
return false
}
}
return true
}
}

const prime = number => {
const primes = []
for (let i = 2; i < number; i++) {
if (isPrime(i)) {
primes.push(i)
}
}
return primes
}

自己動手實現一個高效的斐波那契隊列(歡迎評論區留下代碼)
describe("Fibonacci", () => {
it("Should implement fibonacci", () => {
assert.equal(fibonacci(1), 1);
assert.equal(fibonacci(2), 1);
assert.equal(fibonacci(3), 2);
assert.equal(fibonacci(6), 8);
assert.equal(fibonacci(10), 55);
})
})


最後

看完點個讚,分享一下吧,讓更多的朋友能夠看到。如果你喜歡前端開發博客的分享,就給公號標個星吧,這樣就不會錯過我的文章了。

相關焦點

  • 乾貨 | 算法和編程面試題精選TOP50!(附代碼+解題思路+答案)
    這次營長表示要翻 Java 的牌子啦~ 應大家的強烈反饋,我們找了一套 Java 語言的算法和編程的面試題。這份面試資源主要包含五部分內容:數組、鍊表、字符串、二叉樹和重要算法(如排序算法)的編程面試題,其中每部分內容我們都列出了一些最常被問到的熱門問題,並且在每個題目後給出了可以參考的解決思路和代碼,因為題目較多,我們沒有羅列所有的方法和代碼,只給出了訪問地址。相信大家在掌握了這些內容後,一定可以提升實力、信心大增。
  • 【算法總結】五道常見的算法-二叉樹
    前言前段時間,寫了面試必備的一系列文章,反應還不錯。有一些讀者反饋說,能不能整理一些面試常見的算法。前段時間,我恰好總結了 LeetCode 常見的面試算法題目。https://github.com/gdutxiaoxu/Android_interview剛開始準備刷算法題目的時候,感覺真的是好難,十道題目有九道是不會的。
  • 求職FLAG必須掌握哪些算法和面試技巧?附「FB最新算法面試題」
    本文整理了近期FB 技術類職位的高頻面試題,以及FLAG大廠常見的個人背景問題、文化問題、經驗問題、技術問題、編程問題等常規性問題。同時給予了準備和回答的tips。某一天,有人提交了錯誤版本的代碼,因此造成自身及之後版本的代碼在單元測試中均出錯。請找出第一個錯誤的版本號。你可以通過 isBadVersion 的接口來判斷版本號 version 是否在單元測試中出錯,具體接口詳情和調用方法請見代碼的注釋部分。
  • 10年+技術總監面FB,面試官竟然讓我寫算法題?
    FB面試要求coding,連10年+經驗的技術總監也不例外!前幾天,一位學員在群裡分享面經時說到。還好有《九章算法班》,這門金牌算法課,高度針對FLAG級別大廠面試,緊跟最新面試動態每年升級課程和題庫。
  • TOP 48 算法和編程面試題,牛逼啊!
    你在申請這些工作時,肯定很想知道面試官會問到哪些問題。在本文中,作者會分享一些常見的編程面試問題,這些問題來自於針對不同經驗層次的程式設計師的面試——從應屆畢業生到具有一兩年經驗的程式設計師。編程面試題通常包含數據結構和基於算法的問題,以及一些邏輯問題,例如:如何在不使用臨時變量的情況下交換兩個整數?為了清晰,編程面試題需要劃分為不同主題。
  • 《Python程式設計師面試算法寶典》PDF超清版開源了文末附下載方式
    簡介精選Facebook、Google、Microsoft和BAT等大型企業的Python算法面試題,並進行詳細的剖析全面介紹Python程式設計師面試筆試技巧和方法,教你如何以「不變應萬變」。√ 兩萬多行代碼,100多個知識點,全面覆蓋Python程式設計師各類面試題型。√ 15年開發經驗、實戰技巧總結,站在「巨人」的肩膀上,讓學習走捷徑。
  • 面試經驗分享之數據結構、算法題
    在正式介紹題目和準備方法之前,有兩點需要說明,Google 和 Facebook 這類對算法有很高要求的公司的在線測試我沒有參加過(不過在牛人內推幫助下有過面試體驗……),這超出了我目前的編碼能力範圍,網上有不少拿到 Google、Facebook offer 的經驗總結文章,可以移步觀賞;前段時間在微博上又看到有人說自己把 leetcode
  • 手把手解決三道括號相關的算法題
    假設字符串中只有圓括號,如果想讓括號字符串合法,那麼必須做到:每個右括號)的左邊必須有一個左括號(和它匹配。比如說字符串()))((中,中間的兩個右括號左邊就沒有左括號匹配,所以這個括號組合是不合法的。
  • 獨家 | 在Python編程面試前需要學會的10個算法(附代碼)
    在我看來,我認為花一天的時間解決算法問題有點太傻了,而且在實際工作環境中很不適用,而且長期來看這也不會給我帶來多大的收益。來源:https://leetcode.com/problemset/all/那裡有上百道不同的算法問題,這意味著,要做到能夠識別出常見的模式並在10分鐘以內得出有效的解決方法需要大量的時間和投入。
  • javascript經典算法之最小硬幣找零問題實戰
    在前端的職業生涯中我們會遇到很多選擇,走向不同的方向,但是唯一不變的,就是技術思維。而算法,正是技術思維應用的結晶。對於初中級工程師,實現業務是我們的首要職責,沒有太多的時間和精力關注代碼性能和執行效率,往往能用即可。
  • 網際網路公司最常見的面試算法題大集合!
    來源:Github編輯:元子【導讀】LeetCode是一個美國的在線編程網站,收集了各個大廠的筆試面試題,對找工作的畢業生和開發者來說,非常有價值。很多求職者都會在LeetCode刷上一遍,面試官也喜歡在上面挑選各類題目。LeetCode是一個美國的在線編程網站,收集了各個大廠的筆試面試題,對找工作的畢業生和開發者來說,非常有價值。不過LeetCode上面的題目很多都是考察應聘者對基礎知識的應用,適合進行練習編程基礎或者準備面試。很多求職者都會在LeetCode刷上一遍,面試官也喜歡在上面挑選各類題目。
  • 幾道和「廣度優先搜索」有關的算法面試題
    前言廣度優先遍歷(BFS)是搜索圖的算法,它的基本思想和操作方法就是:1、從圖中某個頂點 V0 出發,
  • 百度、阿里、騰訊、京東等面試算法題
    這是無量測試之道的第180篇原創今天給大家分享的是字符串相關的算法面試題。
  • JavaScript中的簡單排序算法
    -57d512ceaf5d排序是程式設計師處理數據處理時最常見的問題之一。我還建議將TopTal的排序算法動畫或Visualgo的排序部分加入書籤,以便在閱讀本文時可視化這些算法並在整個編程過程中提供幫助。記住,程式設計師最好的朋友是網際網路!交換助手方法所有這些算法都涉及交換數組中的元素。為了更好地理解算法的工作原理,我們將抽象出一個稱為「交換」的可重用函數。「交換」接收一個數組,並交換該數組的兩個索引。
  • BAT七年經驗,卻抵不過外企面試的兩道算法題?
    打開APP BAT七年經驗,卻抵不過外企面試的兩道算法題? 發表於 2019-01-06 10:24:32 又遇年底跳槽季,如果你曾在 BAT 等網際網路大廠有過較為豐富的工作經驗,想要換份工作,面試時會主要考慮哪些因素? 面試外企,卻被兩道算法題難住?
  • 使用JavaScript 遞歸算法,匯總累加題目的分數值
    所謂遞歸算法,是將問題轉化為規模縮小的同類問題的子問題,每一個子問題都用一個同樣的算法去解決。一般來說,一個遞歸算法就是函數調用自身去解決它的子問題。他的需求是:題目的分數值 score 是創建題目時預估定義的一個分數值,如20分;實際情況是在某個大題的下面還可以創建小題,小題也會定義一個分數值 score,這樣一來,某個大題原先預估定義的分數值 score 可能與它下面所有小題的分數值 score 加起來的和並不相等,這樣大題的分數值 score 就顯得不準確,希望我在前端能幫忙處理一下,使得大題的分數值等於匯總了其下所有小題的分數值之和
  • 網際網路公司最常見的面試算法題大集合
    ,對找工作的畢業生和開發者來說,非常有價值。很多求職者都會在LeetCode刷上一遍,面試官也喜歡在上面挑選各類題目。LeetCode是一個美國的在線編程網站,收集了各個大廠的筆試面試題,對找工作的畢業生和開發者來說,非常有價值。不過LeetCode上面的題目很多都是考察應聘者對基礎知識的應用,適合進行練習編程基礎或者準備面試。很多求職者都會在LeetCode刷上一遍,面試官也喜歡在上面挑選各類題目。
  • 14種模式解決面試算法編程題(PART II)
    作者:高開遠學校:上海交通大學研究方向:自然語言處理寫在前面繼續,14種模式解決面試算法編程題使用這種方法可以有效地解決涉及以逐級順序遍歷樹的任何問題。Tree BFS模式的基本思想是將根節點push到隊列然後不斷迭代直到隊列為空。對於每次迭代,刪除隊列頭部的節點並「訪問」該節點。從隊列中刪除每個節點後,我們還將其所有子節點push進隊列。
  • 經典算法題:懂二進位(小米筆試題)
    輸入例子:1999 2299輸出例子:7經典算法題:《算法題 :貪心算法(大眾點評筆試題)》《算法題 :所有String2的字母在String1裡是否存在(大眾點評筆試題)》《算法題 :順序表插入新元素(迅雷筆試題)》
  • JavaScript 面試中常見算法問題詳解
    JavaScript 面試中常見算法問題詳解,翻譯自 https://github.com/kennymkchan/interview-questions-in-javascript