1. 程式人生 > 實用技巧 >演算法學習記錄-持續更新

演算法學習記錄-持續更新

1.求n項累加和

function sum(n){
  return (n/2)*(n-1)
}

時間複雜度:O(1) 常數時間複雜度

2.判斷一個數是不是質數

​ 質數的特點:

​ 1.除了1以外的自然數如果只能被1和它本身整除這個數就是質數

​ 2.質數還有一個特點,就是它總是等於 6x-1(等同於6x+5) 或者 6x+1,其中 x 是大於等於1的自然數

​ 3.合數 能被除1和本身以外的數整除, 這個數一定小於等於合數的開平方

需求分析:

- 1 既不是質數也不是合數
- 小於等於3的數,是否大於1可判斷是不是質數
- 不是6的倍數兩側的一定不是質數  number % 6 != 1 && number % 6 !=5
- i = 2;i<=Math.sqrt(number);i++;  number % i === 0 false 否則 true
function isPrime(number){
  if(number<=3){
    return number>1
  }
  // 不在6的倍數兩側的一定不是質數
  if (number % 6 != 1 && number % 6 != 5) {
    return false;
  }
  for(let i =2;i<= Math.sqrt(number);i++){
  	if(number%i === 0){
      return false
    }
  }
  return true
}

時間複雜度:O(開平方n)

2-1 擴充套件

示例 1:

輸入:n = 10
輸出:4
解釋:小於 10 的質數一共有 4 個, 它們是 2, 3, 5, 7 。
示例 2:

輸入:n = 0
輸出:0
示例 3:

輸入:n = 1
輸出:0

提示:

0 <= n <= 5 * 106

第一種方法 列舉 + 用上面的isPrime方法

var countPrimes = function(n){
  let count = 0
  n--
  for(let i = 2;i<n;i++){
    if(isPrime(n)){
      count ++
    }
  }
  return count
}
function isPrime(n){
  if(n<=3){
    return n>1
  }
  if(n % 6 != 1 && n % 6 !=5){
    return false
  }
  for(let i = 2;i<=Math.sqrt(n);i++){
    if(n%i ===0) return false
  }
  return true
}
	var countPrimes = function(n) {
    for(var i = 2, r = 0, isPrime = Array(n).fill(true); i < n; i++) 
        if(isPrime[i]) {
            r++
            for(var j = 2 * i; j < n; j += i) isPrime[j] = false
        }
    return r
};

知識補充:Array(n).fill(true) 建立一個長度為n的陣列並且填充值為true

埃氏篩 · 優化

上圖中黃色數字,被重複標記。例如10,2時標記1次,5時又1次。如何減少重複呢?
舉例5。16以內,從2起,5的倍數,5 * 2,5 * 3。在2和3時倍數時,被標記過
n以內,從2起,任意數i的倍數,i * 2,i * 3 ... i * i ... i * x(x > i) < n
在i之前的倍數,一定在遍歷到i前,被標記過

程式碼

var countPrimes = function(n) {
for(var i = 2, r = 0, isPrime = new Array(n).fill(true); i < n; i++)
if(isPrime[i]) {
r++
for(var j = i * i; j < n; j += i) isPrime[j] = false
}
return r
};

奇數篩

思路:

質數一定是奇數,偶數一定不是質數(2除外)。只要在奇數範圍標記合數,未標記的是質數

奇數乘以偶數一定是偶數,只用奇數乘以奇數,確保在奇數範圍內標記

用上面兩條優化埃氏篩解法,過程如圖

var countPrimes = function(n){
  var isCom = new Array(n),
      b=Math.sqrt(n),
      r = n>2?1:0,
      t = 0
  for(var i = 3;i<n;i+=2){
    if(!isCom[i]){
      r++
      if(i <= b){
        for(var j = i;(t=i*j) < n;j+=2){
          isCom[t] = 1
        }
      }
    }
  }
  return r
}

個人總結:奇數+2 得到的還是奇數

作者:mantoufan
連結:https://leetcode-cn.com/problems/count-primes/solution/mei-ju-ai-shi-shai-xian-xing-shai-qi-shu-shai-5xin/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

4.計算N的階乘 n! (遞迴方法)

如果n = 5 5! = 5 * 4 * 3 * 2 * 1

function factorial(number){
	if(number <= 1){
    return 1
  }
  return number*factorial(number-1)
}

時間複雜度:O(n)

5.是不是2的整數次冪

需求分析

小於1的肯定不是2的整數次冪

如果一個數對2取餘數不等於0肯定不是2的整數次冪

一個數除以2得到商和餘數,商不等於1 & 取餘數不等於0,直到最後商等於1,餘數等於0返回true,否則是false

8%2 =0 8 / 2 = 4

4%2 =0 4/2 = 2

2%2 = 0 2/2 = 1

得到商1 餘0 得出結論8 是2的整數次冪

function isPowerTwo(number){
 if(number<1){
   return false
 }
 let divideNumber = number
 while(divideNumber !== 1){
   if(divideNumber % 2 !== 0){
			return false
   }
   divideNumber = divideNumber / 2
 }
  return true
}

時間複雜度:O(logn) 對數時間複雜度

2.求斐波拉(那)契數列指定位置的值 (從底層構建)

function fibNum(number){
  let fibArray = [1,1]
  for(let i =2;i<number+1;i++){
    fibArray.push(fibArray[i-2]+fibArray[i-1])
  }
  return fibArray[number]
}

時間複雜度:O(n) 線性時間複雜度

什麼是動態規劃

動態規劃是解決多階段決策過程中最優化的一種有效的數學方法

遞迴 Recursion + 儲存資料 Memoization

非常適合重複計算 通過儲存資料避免不必要的遞迴步驟

可能導致重複的工作 中間結果被儲存並重復使用

動態規劃

遞迴 + 斐波那契數列 演算法

從底層構建 也就是自下而上方法 找到的是下標

需求分析:

建立中間值變數result

進到函式體執行,先驗證傳進來的中間值memo是否存在,存在則直接返回

因為斐波那契數列前兩項為1 ,所以當n 值為0 或者 1的時候,直接返回1

否則 中間值result 賦值回撥函式 fib(n-1,memo) + fib(n-2,memo)

條件外賦值 memo[n] = result

返回result

function fib(n,memo={}){
  let result // 儲存中間值 
  if(memo[n]) return memo[n] // 驗證這個中間值是否存在,若存在就返回結果,
  if(n === 0 || n === 1){
    result = 1
  }else{
    result = fib(n-1,memo) + fib(n-2,memo)
  }
  memo[n] = result
  return result
}
[1,1,2,3,5,8,13]
fib(5,{}) // 傳入空物件目的:在遞迴過程中,重複中間值才會被儲存到物件中
fib(6,{})

時間複雜度 O(n) 線性時間複雜度

搜尋演算法

線性搜尋

可以用於有序和無序的列表,它會驗證所有item項知道它找到你需要搜尋的元素為止

[2,17,-1,41,5,8,10,32]

查詢數字 5

function findElement(arr,element,comparatorFn){
  let index = 0
  for(const item of arr){
    //判斷是否為物件
    if(typeof element === 'object' &&
      element !== null &&
      comparatorFn(element,item)){
      return index;
    }
    //數字或物件
    if(item === index){
      return index
    }
    index++
  }
}
const arr = [2,17,-1,41,5,8,10,32]
const object = [
  {name:'Summer',age:26},
  {name:'Henry',age:18}
]
findElement(arr,5,(element,item)=>{
  return element.name === item.name
})
arr.findIndex(el=>el === 5)


知識補充

ES6為Array增加了find(),findIndex函式。

find 函式用來查詢目標元素,找到就返回該元素,找不到返回undefined

findIndex 函式也是查詢目標元素,找到就返回元素位置,找不到就返回-1

const arr = [1,2,3,4,5]
arr.find(item=>item===5)  5
arr.find(item=>item === 6) undefined
arr.findIndex(item=>item === 5) 4
arr.findIndex(item=>item === 6) -1

二分查詢法

前提 : 陣列必須是有序的

迴圈 查詢數組裡是否有指定的數字,並返回下標

function findElement(sortedArr,element){
  let startIndex = 0
  let endIndex = sortedArr.length - 1
  while(startIndex <= endIndex){
    //中間元素的下標
    const middleIndex = startIndex + Math.floor((endIndex - startIndex)/2)
    //判斷中間元素和查詢元素是否相等
    if(element === sortedArr[middleIndex]){
      return middleIndex
    }
    //比較中間元素和查詢元素的大小
    if(sortedArr[middleIndex] < element){
      startIndex = middleIndex + 1
    }else{
      endIndex = middleIndex - 1
    }
  }
}
const arr = [1,5,9,13,99,100]
fineElement(arr,99)

遞迴二分查詢

中間值offset 記錄下標,每次重置陣列傳進遞迴函式裡面,返回值 middleIndex + offset

startIndex 和 endIndex 必須是大於等於0的數,否則跳出遞迴

function findElement(sortedArr,element,offset = 0){
  //offset 記憶原始陣列下標
  let startIndex = 0
  let endIndex = sortedArr.length - 1
  //這裡的中間元素下標與迴圈二分查詢相比 不比加startIndex了 
  const middleIndex = Math.floor((endIndex - startIndex ) /2)
  //判斷中間元素和查詢元素是否相等 basecase 跳出遞迴的關鍵程式碼
  if(element === sortedArr[middleIndex]){
    return middleIndex + offset
  }
  if(sortedArr[middleIndex] < element){
		startIndex = middleIndex + 1 //重置陣列起點下標    
    offset = middleIndex + 1
  }else{
    endIndex = middleIndex - 1 // 重置陣列終點下標
  }
 	if(startIndex >= 0&&endIndex >= 0){
  	return findElement(sortedArr.slice(startIndex,endIndex+1),element,offset)  
  }else{
    return -1
  }
  
}
const arr = [1,5,9,13,99,100]
console.log(findElement(arr,99,0)) 

時間複雜度 : O(logn) [這種拆分成小部分的演算法時間複雜度 都為O(logn)]

知識補充:

slice 返回一個新的陣列,包含從 start 到 end (不包括該元素)

arr.slice(start,end) 取值 區間:[ start, end )

排序演算法

  • 什麼是排序演算法

    排序演算法是對一些無序資料進行排序

    • 氣泡排序
    • 歸併排序
    • ...
  • 不同型別的排序演算法

氣泡排序 Bubble Sort

重複掃描待排序序列,並比較每一對相鄰的元素,當該元素順序不正確時進行交換。一直重複這個過程,知道沒有任何兩個相鄰元素可以交換,就表明完成了排序

每一遍迴圈都是拿外層這個位置的元素跟內層的元素進行比較,與數無關與位置有關

升序
function sort(arr){
  const resultArr = [...arr] //得到一個新陣列
  // 外層迴圈
  for(let outer = 0;outer<resultArr.length;outer++){
		// 裡層迴圈
    let outerEl = resultArr[outer]
    for(let inner = outer + 1;inner<resultArra.length;inner++ ){
      let innerEl = resultArr[inner]
      //對比
      if(outerEl > innerEl){
        //交換位置
        resultArr[outer] = innerEl
        resultArr[inner] = outerEl
        // 交換完位置重新賦值
        outerEl = resultArr[outer]
        innerEl = resultArr[inner] // 這行感覺可以不用寫
      }
    }
  }
  return resultArr
}
const sortedArr = [4,12,-3.-8,1,72,36]
console.log(sort(sortedArr))

氣泡排序的時間複雜度:

最好情況 : 元素項已經排序 O(n)

平均情況:元素隨機排序,不知道元素的位置 趨向於 O(n^2)

最差情況:元素項按照相反排序 O(n^2)

快速排序 Quicksort

1.選擇一個分界元素將陣列拆分為較小的快

2.小於分界元素(左邊),等於分界元素(中間),大於分界元素(右邊);

3.左右兩側的元素又獨立排序,執行重複步驟;

4.最後左右兩部分都排序完成後,整個陣列就實現排序

function sort(arr) {
    //   複製陣列
    const copyArr = [...arr]
    // base case 當分割陣列只有一項時就返回陣列,跳出遞迴
    if(copyArr.length <= 1){
        return copyArr
    }
    //初始化不同型別的陣列容器
    const smallerElementsArr = []; //放置比分界值小的元素
    const biggerElementArr = []; //放置比分界值大的元素
    const pivotElement = copyArr.shift();//找到每個陣列的分界值,(用陣列第一項作為分界值去進行比較)
    const centerElementArr = [pivotElement]; //存放分界值

    // 遞迴步驟
    while(copyArr.length){
        const currentElement = copyArr.shift();
        // 和分界值對比
        if(currentElement === pivotElement){
            centerElementArr.push(currentElement)
        }else if(currentElement < pivotElement){
            smallerElementsArr.push(currentElement)
        }else{
            biggerElementArr.push(currentElement)
        }
    }
    // 遞迴
    const smallerElementsSortedArr = sort(smallerElementsArr)
    const biggerElementsSortedArr =  sort(biggerElementArr)

    return smallerElementsSortedArr.concat(centerElementArr,biggerElementsSortedArr)
}
const sortedArray = sort([-2, 9, 3, 41,-2, -6, 35, 18])
console.log(sortedArray)
/**
 * 快速排序,找到一個分界值,數組裡所有元素與它比較,比它小的放左邊,比它大的放右邊,不斷的去找這個值,最後合併到一起
 */

快速排序的時間複雜度

最好情況 :元素隨機排序(不是升序和降序) 時間複雜度 O(n*logn)

平均情況 : 元素是隨機排序(不是升序和降序) 時間複雜度趨向於 O(n*logn)

最差情況 : 元素已經排序(升序或降序) 時間複雜度O(n^2)

歸併排序 Merge Sort

歸併排序是建立在歸併操作上的一種有效穩定的排序方法

1) 多次拆分陣列,直到只剩下2個元素的陣列

2) 對這些陣列進行排序,然後將它們合併在一起

![1841606291477_.pic_hd](/Users/zhangfanglan/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/d52199f38055a59ff7217d3e8ca0cada/Message/MessageTemp/9e20f478899dc29eb19741386f9343c8/Image/1841606291477_.pic_hd.jpg)

function sort(arr){

  //遞迴  base case 跳出遞迴語句
  // 驗證分割陣列長度小於2的時候
  if(arr.length < 2){
    return arr
  }
  // 驗證分割陣列長度為2的時候,進行排序
  if(arr.length === 2){
    return arr[0] > arr[1] ? [arr[1],arr[0]] : arr 
  }
  // 對陣列進行分割,建立中間值,Math.floor() 向下取整,Array.slice(start,end) [start,end) 
  const middle = Math.floor(arr.length / 2)
  const leftArray = arr.slice(0,middle)
  const rightArray = arr.slice(middle,arr.length)
  
  // 遞迴步驟
  const leftSortedArray = sort(leftArray)
  const rightSortedArray = sort(rightArray)
  
  // 實現歸併和排序
  let mergedArr = []
  let leftArrIndex = 0
  let rightArrIndex = 0
  // 兩個下標值同時大於等於兩個陣列的長度時跳出迴圈
  while(leftArrIndex < leftSortedArray.length || rightArrayIndex < rightSortedArray.length){
    // 比較陣列的元素大小
    if(leftArrIndex >= leftSortedArray.length || leftSortedArray[leftArrIndex] > rightSortedArray[rightArrIndex]){
      mergedArr.push(rightSortedArray[rightArrIndex])
      rightArrIndex++
    }else {
      mergedArr.push(leftSortedArray[leftArrIndex])
      leftArrIndex++
    }
  }
  return mergedArr
}
sort([43,-12,5,12,8,2,-20,300])

小結:

  • 氣泡排序 是 雙層迴圈巢狀

  • 快速排序,找到一個分界值(預設是陣列第一項),數組裡所有元素與它比較,比它小的放左邊,比它大的放右邊,不斷的去找這個值(遞迴),最後合併到一起

  • 歸併排序,陣列從中間分隔,倆陣列如果長度大於2就不斷分割(遞迴),直到長度為1或者2 就排序並返回,然後進行歸併比較陣列大小, 歸併就是一個空陣列,然後比較得到的left和right陣列對比大小放進新的空陣列並返回,兩個下標值同時大於等於兩個陣列的長度時跳出迴圈

空間複雜度 Space Algorithms

什麼是空間複雜度

是對一個演算法在執行過程中臨時佔用儲存空間大小的量度(算法佔用多少記憶體空間)

一個演算法在計算機儲存器上所佔用的儲存空間,包括儲存演算法本身所佔用的儲存空間,演算法的輸入輸出資料所佔用的儲存空間和演算法在執行過程中臨時佔用的儲存空間

JavaScript中所有的值都儲存在記憶體中,陣列和物件會佔用更多空間

通常,在JS中,我們不必擔心空間複雜性和記憶體過多的情況

如何推導演算法的空間複雜度

查詢演算法中 “永久”儲存的資料(值)【 有沒有開闢新的記憶體空間】

​ ||

計算這種永久儲存的值被建立的頻次【保持存在】

​ ||

判斷這些值的數量是如何取決於輸入的“n‘”【找和輸入n的關係】

得出對應的空間複雜度,例如O(n) O (1) 等

案例分析

數學演算法和搜尋演算法

演算法 空間複雜度 理由
階乘(迴圈) O(1) 我們運算的是相同或同一個數值,每次迭代都不會建立新的(永久)的值
階乘(遞迴) O(n) 每個巢狀函式被呼叫的時候就建立一個新的值(函式接收到的引數)
線性搜尋 O(1) 函式在迭代過程中並沒有建立新的“永久”的值
二分查詢 O(1) 函式在迭代過程中並沒有建立新的“永久”的值

階乘迴圈

function fact(number){
  let result = 1 
  // 這裡 number 、 result 被儲存在記憶體空間中
  for(let i = 2;i<=number;i++){
     // i 隨著變化,result 和 i 都自增,空間複雜度並不受輸入number的輸入而影響(輸入number與result 和 i 變化無關)
    result += result * i
  }
 
  return result
}
// 空間複雜度 O(1) => 常數空間複雜度

階乘遞迴

function fact(number){
  if(number === 1){
    	return 1
  }
  return number*fact(number-1)
}

線性搜尋

function findElement(arr,element,comparatorFn){
  let index = 0
  for(const item of arr){
    if(typeof element === 'object' &&element != null && comparatorFn(element,item)){
      return index
    }
    if(item === element){
      return index
    }
    index++
  }
}

二分查詢法

  • 迴圈
function findElement(sortedArr,element){
  let startIndex = 0
  let endIndex = sortedArr.length - 1
  while(startIndex <= endIndex){
    const middleIndex = startIndex + Math.floor((endIndex-startIndex)/2)
    const currentEle = sortedArr[middleIndex]
    if(element === currentEle){
      return middleIndex
    }
    if(element > currentEle){
      startIndex = middleIndex + 1
    }else{
      endIndex  = middleIndex - 1
    }
  }
}
  • 遞迴

    function findElement(sortedArr,element,offset =  0){
      let startIndex = 0,
          endIndex = sortedArr.length - 1,
          middleIndex = Math.floort((endIndex-startIndex)/2),
          currentEle = sortedArr[middleIndex]
      if(element === currentEle){
        return middleIndex + offset
      }
      if(element > current){
        startIndex = middleIndex + 1
        offset = middleIndex + 1
      }else{
        endIndex = middleIndex - 1
      }
      if(startIndex >= 0 && endIndex >=0){
        findeElement(sortedArr(startIndex,endIndex+1),element,offset)
      }else{
        return -1
      }
      
    }
    

    排序演算法

    演算法 空間複雜度 理由
    氣泡排序 O(1) 函式在迭代過程中,並沒有建立新的“永久”值
    快速排序

    氣泡排序

function sort(arr){
  let result = [...arr]
  for(let outer = 0;outer<arr.length;outer++){
    let outerEl = result[outer]
    for(let inner = outer+1;inner<arr.length;inner++){
      let innerEL = result[inner]
      if(outerEl < inner){
        result[outer] = innerEL
        result[inner] = outerEl
        
        outerEl = result[outer]
      }
    }
  }
  return result
}

快速排序

function sort(arr){
  const copyArr = [...arr]
  if(copyArr.length <=1){
    return copyArr
  }
  const smallerElementArr = []
  const biggerElementArr = []
  const privotElemenet = copyArr.shift()
  const centerElmentArr = [privotElement]
  while(copyArr.length){
    const currentElement = copyArr.shift()
    if(currentElement  === privotElement){
      centerElmentArr.push(currentElement)
    }else if(currentElement < privotElement){
      smallerElementArr.push(currentElement)
    }else{
      biggerElementArr.push(currentElement)
    }
  }
  const smallerElementSortedArr = sort(smallerElementArr)
  const biggerElementSortedArr = sort(biggerElementArr)
  return smallerElementSortedArr.concat(centerElmentArr,biggerElementSortedArr)
}