2017年9月21日

[演算法] Reverse Array in Place:暫存變數的使用

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

在這次的練習中,我們要將輸入的陣列進行反轉,但有幾點需要注意的:
  1. 不能建立一個新的陣列,然後透過 push 的方式把新的元素內容推進去新陣列。
  2. 不能使用 Array.prototype.reverse() 這個方法。
function reverseArrayInPlace (arr) {...}

前置閱讀

演算法實做

第一個元素和倒數第一個元素對調
為了達到陣列反轉的效果,我們有一個很特別,而且實務上也經常用到的技巧,第一個元素和倒數第一個元素對調;第二個元素和倒數第二個元素對調,以此類推…。
這個作法的技巧在於,我們要建立一個 暫存變數 ,邏輯有點像這樣:
let tempVar = 第一個元素
第一個元素 = 倒數第一個元素
倒數第一個元素 = tempVar      // 因為這時候第一個元素的值已經改變了,所以不能再直接代入第一個元素
實做的方法會像這樣:
function reverseArrayInPlace (arr) {
  for (let i = 0; i < arr.length; i++) {
    let tempVar = arr[i]
    arr[i] = arr[arr.length - 1 - i]
    arr[arr.length - 1 - i] = tempVar
  }
  return arr
}
和上次演算法 Algorithm: reverse words 使用到相同的技巧 arr.length - 1 - i 來反轉陣列的元素;但是這麼做還有一個問題,就是一開始第一個元素會和最後一個元素交換位置,可是跑到最後一個元素的時候,又和第一個元素交換位置,最後使的陣列沒有達到預期反轉的效果,因此我們的迴圈只要跑前一半就好
for (let i = 0; i < (arr.length / 2); i++) {...}

完整程式碼

function reverseArrayInPlace (arr) {
  for (let i = 0; i < arr.length / 2; i++) {
    let tempVar = arr[i]
    arr[i] = arr[arr.length - 1 - i]
    arr[arr.length - 1 - i] = tempVar
  }
  return arr
}

let arr = ['a', 'b', 'c', 'd']
console.log(reverseArrayInPlace(arr))      // ['d', 'c', 'b', 'a']

資料來源

[演算法] Reverse Words: 把單字反過來寫

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

在這次的練習中,我們要實做一個能夠將單字反轉過來的函式,但有兩點要注意的:
  1. 反轉的是單字,而不是整個句子,例如 This is a cat,應該要變成 sihT si a tac,而不是 tac a si sihT
  2. 不能使用 Array.prototype.reverse() 這個方法。
function reverseWords (str) {...}

演算法實做

字串反轉

比較重要的是如何不用 Array.prototype.reverse() 的方式來實做將字串反過來,我們可以觀察假設一個字串原本是 abcd,它的 index 會是 0123,如果反過來變成 dbca 的話,它的 index 會是 3210,寫成條列是我們就可以看出些有趣的規律:
abcd
0123
3210
dcba
可以發現 a+d=3, b+c = 3, c+b=3, d+a=3;利用這樣的規則,我們就可以把我們的字串反轉過來,例如:
let str = 'abcd'
let strReverse = ''
for (let i = str.length - 1; i >= 0 ; i--){
  strReverse += str[i]
}
console.log(strReverse) // dbca

不要整句反轉

為了不要整句反轉,所以我們要根據空行把句子拆開成陣列:
function reverseWords (str) {
  strArr = str.split(' ')
  /* ... */
}
接著要把陣列 strArr 中的每個元素進行字串反轉,這裡我們使用 Array.prototype.map() 這個方法,最後在透過 Array.prototype.join() 這個方法把它組回字串:
function reverseWords (str) {
  strArr = str.split(' ')

  strArrReverse = strArr.map(str => {
    let newStr = ''
    for (let i = str.length - 1; i >= 0; i--) {
      newStr += str[i]
    }
    return newStr
  })
  return strArrReverse.join(' ')
}

完整程式碼

function reverseWords (str) {
  strArr = str.split(' ')

  strArrReverse = strArr.map(str => {
    let newStr = ''
    for (let i = str.length - 1; i >= 0; i--) {
      newStr += str[i]
    }
    return newStr
  })
  return strArrReverse.join(' ')
}

reverseWords('This is a cat')                // sihT si a tac
reverseWords('This is a string of words')    // sihT si a gnirts fo sdrow
reverseWords('Coding JavaScript')            // gnidoC tpircSavaJ

資料來源

[演算法] Caesar Cipher: 往後或往前推移英文字母

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

Caesar Cipher 要做的事,是把輸入的字串根據所給定的數值往前/後推幾位,例如輸入字串 a 和數字 2,則會把 a 往後推兩位,於是要回傳 c。
function caesarCipher (str, num) {...}

caesarCipher ('zoo keeper', 2)     // bqq mggrgt

前置知識

在實做這個演算法前,我們先來瞭解一下 ASCII Code。由於電腦實際上指認得數字,並不認得我們所輸入的字母,而 ASCII Code 簡單來說,就是英文字母和數字的轉換表。
舉例來說,英文字母 a 對應到的十進位代碼就是 97; A 對應到的十進位代碼則是 65;o 對應到的十進位代碼就是 111。
從下面的 ASCII Table 中我們可以看到大寫的英文字母分別對應到 65-90;小寫的英文字母分別對應到 97-122。
透過 JavaScript 函式,我們可以很容易地將字母與數值做轉換:
String.prototype.charCodeAt(index)
透過 String.prototype.charCodeAt(index) 這個函式,我們可以將英文字母轉為 ASCII Code:
'Aao'.charCodeAt(0)          // A 是 65
'Aao'.charCodeAt(1)          // a 是 97
'Aao'.charCodeAt(2)          // o 是 111
String.fromCharCode(num1[, …[, numN]])
透過 String.fromCharCode(num1[, ...[, numN]]) 我們則可以將數值轉換成回字串:
String.fromCharCode(65, 97, 111)      // Aao

演算法實做

這個演算法中比較麻煩的部分是函式後第二個數值沒有限制正負和數值大小,因此如果原本的字串是 a ,數值是 1 ,則回傳 b ;數值如果是 -1,則回傳 z。
因此我們必須先把輸入的數值限制在某一個範圍內,由於英文字母有 26 個,我們可以透過餘數的使用讓 num 的值限制在 -25 ~ 25 之間:
function caesarCipher (str, num) {
  num = num % 26        // num: -25 ~ 25
}
再來我們要分別去跑 str 裡面的每一個字母做轉換:
function caesarCipher (str, num) {
  /* ... */
  
  for (let i = 0; i < str.length; i++) {
    let currentCharCode = str.charCodeAt(i)
  }
}
大寫英文字母的 ASCII Code 65 ~ 90;小寫英文字母的 ASCII Code 97 ~ 122。
針對大寫英文字母,因為 num 會介於 -25 到 25 之間,而大寫英文字母會介於 65 到 90 之間,所以 newCharCode 將會介於 40 ~ 115 之間。
如果 newCharCode 小於 65 的話,那麼要加 26 讓它重新介於 65 以上;如果 newCharCode 大於 90 的話,那麼要減 26 讓它重新介於 90 以下:
if (currentCharCode >= 65 && currentCharCode <= 90) {
  // 大寫英文字母轉換
  newCharCode = currentCharCode + num  // newCharCode: 40 ~ 115
  if (newCharCode < 65) {
    newCharCode = newCharCode + 26
  } else if (newCharCode > 90) {
    newCharCode = newCharCode - 26
  }
}
同樣的道理,針對小寫英文字母的轉換:
else if (currentCharCode >= 97 && currentCharCode <= 122) {
  // 小寫英文字母轉換
  newCharCode = currentCharCode + num
  if (newCharCode < 97) {
    newCharCode = newCharCode + 26
  } else if (newCharCode > 122) {
    newCharCode = newCharCode - 26
  }
} 

完整程式碼

function caesarCipher (str, num) {
  let newString = []
  num = num % 26      // num: 0 ~ 25
  
  for (let i = 0; i < str.length; i++) {
    let currentCharCode = str.charCodeAt(i)
    let newCharCode
    
    /**
     * 大寫英文字母的 ASCII Code 65 ~ 90
     * 小寫英文字母的 ASCII Code 97 ~ 122
    **/
    
    if (currentCharCode >= 65 && currentCharCode <= 90) {
      // 大寫英文字母轉換
      newCharCode = currentCharCode + num
      if (newCharCode < 65) {
        newCharCode = newCharCode + 26
      } else if (newCharCode > 90) {
        newCharCode = newCharCode - 26
      }
    } else if (currentCharCode >= 97 && currentCharCode <= 122) {
      // 小寫英文字母轉換
      newCharCode = currentCharCode + num
      if (newCharCode < 97) {
        newCharCode = newCharCode + 26
      } else if (newCharCode > 122) {
        newCharCode = newCharCode - 26
      }
    } else {
      // 其餘保留原樣
       newCharCode = currentCharCode
    }
    
    newString.push(String.fromCharCode(newCharCode))
  }
  return newString.join('')
  
}

console.log(caesarCipher('Zoo Keeper', 2))    //  Bqq Mggrgt
console.log(caesarCipher('Big Car', -16))    //  Lsq Mkb
console.log(caesarCipher('JavaScript', -900))    //  TkfkCmbszd

資料來源

延伸閱讀

2017年9月19日

[演算法] Is Palindrome:判斷順寫逆寫是不是一樣

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

在忽略單字大小寫和標點符號的情況下,判斷字串是不是迴文(palindrome),也就是順著寫和逆著寫都是一樣的,例如,Madam, I'm Adam, race car
function isPalindrome (str) {
  // return true or false
}
isPalindrome("Madam, I'm Adam")   // true

演算法實做

步驟一:將所有的單字轉成小寫,拆成陣列,並且排除非英文單字

我們先透過 String.toLowerCase() 將字串的內容全部轉成小寫,接著透過 String.split() 將字串拆成陣列,最後透過 Array.filter() 搭配一些正規表達式 /[a-z]/ 只保留小寫的英文字母,其他都過濾掉:
function isPalindrome (str) {
  // 將所有的單字轉成小寫,拆成陣列,並且排除非英文單字
  str = str.toLowerCase()
  charactersArr = str.split('').filter(character => {
    return /[a-z]/.test(character)
  })
  
  /* ... */
}
步驟二:如果正著寫和逆著寫都一樣,則回傳 true,否則 false
透過 Array.join() 將原本的陣列重新合併為字串。利用 Array.reverse() 將陣列反轉([a, b, c] --> [c, b, a]):
function isPalindrome (str) {
  // 將所有的單字轉成小寫,拆成陣列,並且排除非英文單字
  /* ... */
  
  // 如果正著寫和反轉過來寫的內容都一樣,則回傳 true,否則 false
  return charactersArr.join('') === charactersArr.reverse().join('')
}

完整程式碼

function isPalindrome (str) {
  // 將所有的單字轉成小寫,拆成陣列,並且排除非英文單字
  str = str.toLowerCase()
  charactersArr = str.split('').filter(character => {
    return /[a-z]/.test(character)
  })
  
  // 如果正著寫和反轉過來寫的內容都一樣,則回傳 true,否則 false
  return charactersArr.join('') === charactersArr.reverse().join('')
}

console.log(isPalindrome("Madam, I'm Adam"))    // true
console.log(isPalindrome("Hello, I'm Adam"))    // false

資料來源

[演算法] Big O Notation & Time Complexity

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

Big O Notation & Time Complexity

同樣的問題可以用許多種不同的方式加以解決,因此,我們需要一些指標來評量各種方式的好壞。在演算法中,常會使用 Big O NotationTime Complextiy 來衡量一個演算法(函式)的好壞。通常,會根據這個函式隨著輸入的資料量增加時,執行時間會拉長多少來作為衡量的標準之一,下面會說明其中四種類型:
補充:
Big O Notation 代表演算法時間函式的上限(Upper bound),表示在最壞的狀況下,演算法的執行時間不會超過Big-Ο。
[資料結構]演算法評估與資料型別

Constant Run Time (O(1))

第一個類型是屬於 constant run time(O(1)),這個演算法(函式)的執行時間不會隨著輸入資料量的增加而增加
以下面的函式為例,不論我們代入的資料量有多大,它都只是輸出陣列中第一和第二個元素的值,因此執行時間不會隨著輸入資料量的增加而增加。
let arr1 = [1,2,3,4,5]
let arr2 = [1,2,3,4,5,6,7,8,9,10]

/**
 * Constant Run Time:不會隨著輸入的資料量越大而使得執行時間變長
 * Big O Notation: "O(1)"
 **/
function log (arr) {
  console.log(arr[0])
  console.log(arr[1])
}
log(arr1)    // 1, 2
log(arr2)    // 1, 2

Linear Run Time (O(n))

下面的函式,當我們輸入的資料越多的時候,它就會需要等比例輸出越多的內容給我們,因此會需要消耗等比例越多的時間:
/**
 * Linear Run Time: 隨著資料量的增加,執行時間會等比增加
 * Big O Notation: "O(n)"
 **/
function logAll(arr) {
  for (let item of arr) {
    console.log(item)
  }
}

logAll(arr1)  // 1, 2, 3, 4, 5
logAll(arr2)  // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Exponential Run Time (O(n^2))

隨著資料量的增加,執行時間會以指數成長。以下面的函式為例,當我們輸入的陣列包含 5 個元素的時候,它會輸出 25 (5^2) 筆資料;但是當我們數入的陣列包含 10 個元素的時候,它則會輸出 100 (10^2) 筆資料:
/**
 * Exponential Run Time:  隨著資料量的增加,執行時間會誇張的增長
 * Big O Notation: "O(n^2)"
 **/
function addAndLog (arr) {
  for (let item of arr) {
    for (let item2 of arr) {
      console.log ('First', item + item2)
    }
  }
}
addAndLog(arr1)  // 25 pairs logged out
addAndLog(arr2)  // 100 pairs logged out

Logarithmic Run Time (O(log n))

隨著資料量增加,執行時間雖然會增加,但增加率會趨緩。下面的程式碼類似 findIndex 的函式,當輸入的資料有 5 個元素時,它會先切對半後,再尋找,再切半再尋找,因此雖然隨著資料量增加,執行的時間會增加,但是當資料量越大時,執行速度增加的情況越不明顯:
/**
 * Logarithmic Run Time: 隨著資料量增加,執行時間雖然會增加,但增加率會趨緩
 * Big O Notation: "O (log n)"
 **/
function binarySearch (arr, key) {
  let low = 0
  let high = arr.length - 1
  let mid
  let element
  
  while (low <= high) {
    mid = Math.floor((low + high) / 2, 10)
    element = arr[mid]
    if (element < key) {
      low = mid + 1
    } else if (element > key) {
      high = mid - 1
    } else {
      return mid
    }
  }
  return -1
}

console.log(binarySearch(arr1, 3))
console.log(binarySearch(arr2, 3))

圖示

把上面這四種類型用圖線表示,縱軸是時間、橫軸是輸入資料量的多少,可以用來判斷這四種類型的演算法(函式)的好壞:

完整程式碼

/**
 * Demo Big O Notation
 **/

let arr1 = [1,2,3,4,5]
let arr2 = [1,2,3,4,5,6,7,8,9,10]


/**
 * Constant Run Time:不會隨著輸入的資料量越大而使得執行時間變長
 * Big O Notation: "O(1)"
 **/
function log (arr) {
  console.log(arr[0])
  console.log(arr[1])
}
log(arr1)    // 1, 2
log(arr2)    // 1, 2


/**
 * Linear Run Time: 隨著資料量的增加,執行時間會等比增加
 * Big O Notation: "O(n)"
 **/
function logAll (arr) {
  for (let item of arr) {
    console.log(item)
  }
}

logAll(arr1)  // 1, 2, 3, 4, 5
logAll(arr2)  // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

/**
 * Exponential Run Time:  隨著資料量的增加,執行時間會誇張的增加
 * Big O Notation: "O(n^2)"
 **/
function addAndLog (arr) {
  for (let item of arr) {
    for (let item2 of arr) {
      console.log (item + item2)
    }
  }
}
addAndLog(arr1)  // 25 pairs logged out
addAndLog(arr2)  // 100 pairs logged out

/**
 * Logarithmic Run Time: 隨著資料量增加,執行時間雖然會增加,但增加率會趨緩
 * Big O Notation: "O (log n)"
 **/
function binarySearch (arr, key) {
  let low = 0
  let high = arr.length - 1
  let mid
  let element
  
  while (low <= high) {
    mid = Math.floor((low + high) / 2, 10)
    element = arr[mid]
    console.log('ele', mid, element)
    if (element < key) {
      low = mid + 1
    } else if (element > key) {
      high = mid - 1
    } else {
      return mid
    }
  }
  return -1
}

console.log(binarySearch(arr1, 1))
console.log(binarySearch(arr2, 1))
    

資料來源

2017年9月18日

[演算法] Fizz Buzz


此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

透過一個 fizzBuzz 函式,裡面代入參數 num
  • 會輸出從 1 ~ num 的數值
  • 但若這個輸出的數值是 3 的倍數,則輸出 fizz
  • 但若這個輸出的數值是 5 的倍數,則輸出 buzz
  • 但若這個輸出的數值同時是 3 和 5 的倍數,則輸出 fizzBuzz
function fizzBuzz (num) {...}

fizzBuzz(20)    
期望結果:

所須知識

Modulus Operator(餘數運算子)

10 % 3    // 1
12 % 5    // 2

完整程式碼


資料來源


[筆記] 從 JavaScript 學起演算法與資料結構(Learning Algorithm and Data Structure in JavaScript)


記得以前剛轉行從事網頁工程的時候,需要從 database 撈資料到前端呈現,可是當時真的是不清楚要怎麼處理資料,主管便問我說:「你要用什麼演算法」。演・算・法,當時聽到這三個字我真的是滿臉黑人問號?演算法到底是什麼碗糕?
後來想想在學習的路上,總是接觸到很多陌生但卻又耳熟的詞彙,做為新手,常常聽到很多詞就先畏懼了,覺得自己好像很多東西都還不懂的感覺,但其實很多字詞並沒有想得這麼複雜或這麼難。
演算法簡單來說,就是「解決問題的方法」—當碰到一個問題時,要用什麼樣的方式來解決所碰到的這個問題。
這個在 TEDEd 短短 5 分鐘的影片(中文字幕),清楚說明了演算法(Algorithm)的概念:

接下來這一系列的筆記,則是最近剛好有機會在 Udemy 上觀看了由 Eric Traub 的 Learning Algorithm in JavaScript from Scratch 和 Learning Data Structures in JavaScript from Scratch 這兩堂課,讓我真正從 JavaScript 開始學起了演算法和資料結構,筆記當中主要是按照該課程的脈絡加以整理,但是很多程式碼的寫法是消化過後以自己能夠理解的方式撰寫,因此可能和原課程內容有些出入。希望對於沒有接觸過演算法/資料結構的朋友們,也可以一起從 JavaScript 學起演算法/資料結構:

演算法篇(Algorithm)

資料結構篇(Data Structure)

這裡一併列出其他和演算法/資料結構有關的學習資源: