相親交友原始碼開發,演算法的定義及複雜度分析

雲豹科技程式設計師 發表於 2021-11-25
演算法

演算法的評估

演算法(Algorithm)是指用來運算元據、解決相親交友原始碼中問題的一系列方法。
在相親交友原始碼中對於同一個問題,使用不同的演算法,也許最終得到的結果是一樣的,但在過程中消耗的資源和時間卻會有很大的區別。就比如擰一個螺母,扳手和鉗 子都可以勝任,但使用鉗 子擰螺母肯定沒有扳手的效率高。

斐波那契數引入複雜度分析

/**
 * 求第n個斐波那契數
 * 斐波那契數列:這個數列從第3項開始,每一項都等於前兩項之和。
 * 下標 0 1 2 3 4 5 6 7
 * 數列 0 1 1 2 3 5 8 13
 *///遞迴的方式:function fun1(n) {
  if (n <= 1) return n;
  return fun1(n - 1) + fun1(n - 2)}// 迴圈function fun2(n) {
  if (n <= 1) return n;
  let first = 0;
  let second = 1;
  for (let i = 0; i < n - 1; i++) {
    let sum = first + second;
    first = second;
    second = sum  }
  return second}

計時工具

function check(title, task, num) {
  console.log(title);
  let start = new Date().getTime()
  console.log('開始時間', start);
  task(num)
  let end = new Date().getTime()
  console.log('結束時間', end);
  console.log('耗時', (end - start) / 1000);}check('遞迴', fun1, 45)check('迭代', fun2, 1111111145)遞迴
開始時間 1637648114480結束時間 1637648126090耗時 11.61迭代
開始時間 1637648126091結束時間 1637648130718耗時 4.627

遞迴和迭代的差距竟然如此之大。

那麼我們應該如何去衡量相親交友原始碼中不同演算法之間的優劣呢?

1.事後統計法

通過統計、監控,利用計算機計時器對相親交友原始碼中不同演算法的執行時間進行比較,從而確定演算法效率的高低,但有非常大的侷限性:

  • 測試結果非常依賴測試環境
  • 測試結果受資料規模的影響很大

2.事前分析估算

在計算機程式編制前,依據統計方法對演算法進行估算。

大家想一下,當我們要實現相親交友原始碼中的一個功能時,更多的希望快速知道幾種解法中的最優解然後去實現,而不是花大力氣去把每種解法都做出來再測試得到結果,因為太低效。 所以我們需要在程式碼執行前對影響程式碼效率的因素(如時間、空間複雜度等)做一個評估。因此我們需要通過複雜度分析來決策,下面我們主要講解面試中最高頻的時間複雜度。

  • 時間維度:是指執行相親交友原始碼當前演算法所消耗的時間,我們通常用「時間複雜度」來描述。
  • 空間維度:是指執行相親交友原始碼當前演算法需要佔用多少記憶體空間,我們通常用「空間複雜度」來描述。

推導大O階方法

相親交友原始碼中演算法的執行效率,粗略地講,就是演算法程式碼執行的時間。但是,如何在不執行程式碼的情況下,用“肉眼”得到一段程式碼的執行時間呢?這裡有段非常簡單的程式碼,現在,我就帶你一塊來估算一下這段程式碼的執行時間。

function cal01(age) {
  // 1 * unit-time
  if (age > 58) {
    console.log(1);
  } else if (age > 28) {
    console.log(2);
  } else {
    console.log(3);
  }}function cal02(n) {
  // (3+3n)*unit-time
  let sum = 0; // 執行一次
  let i = 1; // 執行一次
  //       n次    n次
  for (; i <= n; ++i) {
    sum += i // 執行n次
  }
  return sum // 執行一次}function cal03(n) {
  // (1+3n)*unit-time
  //        一次     n次          n次
  for (let i = 0; i < n.length; i++) {
    console.log(n[i]); // n次
  }}function cal04(n) {
  // 1+2n+n(1+3n) = (3n^2+3n+1)*unit-time
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      console.log('666');
    }
  }}function cal05(n) {
  // 1+2n+n*(1+3*20) = (1+63n)*unit-time
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < 20; j++) {
      console.log('666');
    }
  }}function cal06(n) {
  // (1+log(2)n)*unit-time
  let i = 1; // 一次
  while (i < n) {
    i + i * 2 // 2*x = n x = log(2)n
  }}function cal07(n) {
  // i+=i 表示i=i+i=>i=2i,每次都乘以二,執行log2(n)次
  // 所以外層迴圈執行1+2*log2(n)次,內層執行log2(n)*(1+3n)次
  // (1+(3+2n)log2(n))*unit-time
  for (let i = 0; i < n; i += i) {
    for (j = 0; j < n; j++) {
      console.log('666');
    }
  }}

儘管我們不知道unit_time的具體值,但是通過這幾段程式碼執行時間的推導過程,我們可以得到一個非常重要的規律,那就是,相親交友原始碼中所有程式碼的執行時間T(n)與每行程式碼的執行次數n成正比。 我們可以把這個規律總結成一個公式。
在這裡插入圖片描述

  • T(n)表示程式碼執行的時間;
  • n表示資料規模的大小;
  • f(n)表示每行程式碼執行的次數總和。

因為這是一個公式,所以用f(n)來表示。公式中的O,表示程式碼的執行時間T(n)與f(n)表示式成正比。

1、公式中的低階、常量、係數三部分並不左右增長趨勢,所以都可以忽略。用大O表示法表示剛講的那兩段程式碼的時間複雜度,就可以記為:T(n) = O(n); T(n) = O(n^2)。
2、大O時間複雜度實際上並不具體表示程式碼真正的執行時間,而是表示程式碼執行時間隨資料規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。一般隨著n的增大,T(n)增長最慢的演算法為最優演算法。

總結

  • 用常數1取代執行時間中的所有加法常數
  • 在修改後的執行次數函式中,只保留最高階項
  • 如果最高階項存在且係數不是1,則去除去除與這個項相乘的係數

常見的時間複雜度量級

常數階O(1)

首先介紹相親交友原始碼順序結構的時間複雜度
無論程式碼執行了多少行,只要是沒有迴圈等複雜結構,那這個程式碼的時間複雜度就都是O(1),如:

int i = 1;int j = 2;++i;j++;int m = i + j;

上述程式碼在執行的時候,它消耗的時候並不隨著某個變數的增長而增長,那麼無論相親交友原始碼中這類程式碼有多長,即使有幾萬幾十萬行,都可以用O(1)來表示它的時間複雜度。
注意:不管這個常數是多少,都記作O(1),而不能記作O(3),O(12)等其他任何數字。
對於分支結構無論判斷條件是真還是假,執行的次數都是恆定的,不會隨著n的變大而發生變化,所以單純的分支結構(不包含在迴圈結構中),其時間複雜度都是O(1)。

線性階O(n)

線性階的迴圈結構會複雜很多,要確定相親交友原始碼中某個演算法的階次,需要確定某個特定語句執行的次數,因此分析演算法的複雜度,關鍵就是要分析迴圈結構的執行情況。
「一個迴圈」,演算法需要執行的運算次數用輸入大小n的函式表示,即 T(n) 。

for(int i=1;i<=n;i++){
	console.log(i)}

「一個迴圈」,演算法需要執行的運算次數用輸入大小n的函式表示,即 T(n) 。

for(int i=1;i<=n;i++){
  console.log(i)}for(int i=1;i<=n;i++){
  console.log(i)}

如果是for迴圈並列關係那麼n會執行2n次,忽略常數也是O(n)

對數階O(logn)

let i = 1;while(i<n){
    i = i * 2;}

從上面程式碼可以看到,在while迴圈裡面,每次都將 i 乘以 2,乘完之後,i 距離 n 就越來越近了。我們試著求解一下,假設迴圈x次之後,i 就大於 n 了,此時這個迴圈就退出了,也就是說 2 的 x 次方等於 n,那麼 x = log_{2}n
也就是說當迴圈log_{2}n以後,這個程式碼就結束了。一般忽略省略底數,因此這個程式碼的時間複雜度為:O(logn).

平方階O(n^2)

舉例:

for(i=1; i<=n; i++){
   for(j=1; j<=n; j++){
       j = i;
       j++;
    }}

這段程式碼其實就是巢狀了2層n迴圈,它的時間複雜度就是 O(n*n),即 O(n²) 如果將其中一層迴圈的n改成m,即:

for(i=1; i<=m; i++){
   for(j=1; j<=n; j++){
       j = i;
       j++;
    }}

那它的時間複雜度就變成了 O(m*n),所以相親交友原始碼總結迴圈的時間複雜度等於迴圈體的複雜度乘以該迴圈執行的次數。下面這個迴圈巢狀它的時間複雜度又是多少呢?

for(i=0; i<n; i++){
   for(j=i; j<n; i++){
      console.log(1)
    }}

由於i=0時,內迴圈執行了n次,當i=1時,執行了n-1次…當i=n-1時,執行了1次,所以總共執行了:n+(n-1)+(n-2)+…+1=n(n+1)/2=n^2/2+n/2 使用推導大O階的方法:最終為n^2

線性對數階O(nlogn)

線性對數階O(nlogN) 其實非常容易理解,將相親交友原始碼的時間複雜度為O(logn)的程式碼迴圈N遍的話,那麼它的時間複雜度就是 n * O(logN),也就是了O(nlogN)。
就拿上面的程式碼加一點修改來舉例:

for(m=1; m<n; m++){
    i = 1;
    while(i<n){
        i = i * 2;
    }}

立方階O(n³)、K次方階O(n^k) 參考上面的O(n²) 去理解就好了,O(n³)相當於三層n迴圈,其它的類似。
但是O(n³)過大的n都會使得結果變得不現實,同樣O(n²) 和O(n!)等除非是很小的n值,否則哪怕n只是100都是噩夢般的執行時間,所以這種不切實際的演算法時間複雜度一般我們不討論。

分析斐波那契數的時間複雜度

//時間複雜度:其實就是看fun1方法被呼叫了多少次,呼叫了多少次就是執行了多少次,如果傳入的是5呼叫fun1(4)和fun1(3)依次推導共呼叫O(2^n)function fun1(n){
    if(n<=1) return n;
    return fun1(n-1)+fun1(n-2);}//時間複雜度:O(n)function fun2(int n){
    if(n<=1) return n;
    let first=0;
    let second=1;
    for (let i = 0; i <n-1 ; i++) {
        //每次加都是前兩個
        let sum=first+second;
        first=second;
        second=sum;
    }
    return second;}

Leetcode(斐波那契數)

斐波那契數,通常用 F(n) 表示,形成的序列稱為 斐波那契數列 。該數列由 0 和 1 開始,後面的每一項數字都是前面兩項數字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
給你 n ,請計算 F(n) 。
示例 1:
輸入:2
輸出:1
解釋:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
輸入:3
輸出:2
解釋:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
輸入:4
輸出:3
解釋:F(4) = F(3) + F(2) = 2 + 1 = 3

本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理
原文連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2844153/,如需轉載,請註明出處,否則將追究法律責任。