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

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

關注github每日一道面試題詳解git

Introduction

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

算法挑戰是有用的,由於解決它們的方法不止一種。這爲決策的制定和決策的計算提供了可能性。在解決算法問題時,咱們應該挑戰本身從多個角度來看待問題的定義,而後權衡各類方法的優缺點。經過足夠的嘗試後,咱們甚至可能看到一個廣泛的真理:不存在「完美」的解決方案。面試

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

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

Primer

JavaScript中,算法只是一個函數,它將某個肯定的數據結構輸入轉換爲某個肯定的數據結構輸出。函數內部的邏輯決定了怎麼轉換。首先,輸入和輸出應該清楚地提早定義。這須要咱們充分理解手上的問題,由於對問題的全面分析能夠很天然地提出解決方案,而不須要編寫任何代碼。編程

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

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

當解決方案的具體結構開始出現時,僞代碼就能夠開始了。爲了給面試官留下深入印象,請提早尋找重構和重用代碼的機會。有時,行爲類似的函數能夠組合成一個更通用的函數,該函數接受一個額外的參數。其餘時候,函數柯里的效果更好。保證函數功能的純粹方便測試和維護也是很是重要的。換句話說,在作出解決問題的決策時須要考慮到架構和設計模式。api

Big O(複雜度)

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

  1. Constabt: O(1)
  2. Logarithmic: O(log n)
  3. Linear: O(n)
  4. Linearithmic: O(n log n)
  5. Quadratic: O(n^2)
  6. Expontential: O(2^n)
  7. Factorial: O(n!)

''

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

Arrays

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

數組的各類操做複雜度

  • Push: O(1)
  • Insert: O(n)
  • Delet: O(n)
  • Searching: O(n)
  • Optimized Searching: O(log n)

MapSet是和數組類似的數據結構。set中的元素都是不重複的,在map中,每一個Item由鍵和值組成。固然,對象也能夠用來存儲鍵值對,可是鍵必須是字符串。

Iterations

與數組密切相關的是使用循環遍歷它們。在JavaScript中,有5種最經常使用的遍歷方法,使用最多的是for循環,for循環能夠用任何順序遍歷數組的索引。若是沒法肯定迭代的次數,咱們可使用whiledo while循環,直到知足某個條件。對於任何Object, 咱們可使用 for infor 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);
 })
})

查看原文

關注github每日一道面試題詳解

相關文章
相關標籤/搜索