算法基礎(1)之遞歸、時間空間復雜度


參考目錄:

遞歸和棧幀的調用原理

時間復雜度

時間復雜度和空間復雜度

什么是時間復雜度

空間復雜度

斐波那契時間復雜度和空間復雜度分析

我的筆記:

JavaScript之遞歸

ES6 之 函數的擴展 尾調用以及尾遞歸

遞歸(recursion)

​ 遞歸是一種很常見的計算編程方法,現在通過階乘案例來學習遞歸

demo1:

function factorial(num) {
  if(num === 1) return num;
  return num * factorial(num - 1); // 遞歸求n的階乘,會遞歸n次,每次遞歸內部計算時間是常數,需要保存n個調用記錄,復雜度 O(n)
}

const view = factorial(100);
console.time(1);
console.log(view); // 1: 3.568ms
console.timeEnd(1);

遞歸可能會造成棧溢出,在程序執行中,函數通過棧(stack——后進先出)這種數據實現的,每當進入一個函數調用,棧就會增加一層棧幀,每次函數返回,棧就會減少一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,就會導致棧溢出(stack overflow) 。

demo2:尾遞歸

// 如果改為尾遞歸,只需要保留一個調用記錄,復雜度為O(1)
function factorial01(n, tntal) {
    if(n === 1) return tntal
    return factorial(n - 1, n * tntal) // 把每一步的乘積傳入到遞歸函數中,每次僅返回遞歸函數本身,total在函數調用前就會被計算,不會影響函數調用
}
console.time(2)
console.log(factorial01(5, 1)) // 120
console.timeEnd(2) // 2: 0.14404296875ms

棧幀

​ 每一個棧幀對應着一個未運行完的函數,棧幀中保存了該函數的返回地址和局部變量。

​ 棧幀也叫過程活動記錄,是編譯器用來實現過程/函數調用的一種數據結構。從邏輯上講,棧幀就是一個函數執行的環境:函數參數、函數的局部變量、函數執行完后返回到哪里等。

棧是從高地址向低地址延伸的。每一個函數的每次調用,都有他自己獨立的一個棧幀,這個棧幀中維持着所需要的各種信息。寄存器ebp指向當前棧幀的底部(高地址),寄存器esp指向當前的棧幀的頂部(低地址)

注意:

  • EBP指向當前位於系統棧最上邊的一個棧幀的底部,而不是系統棧的底部。嚴格說來,“棧幀底部”和“棧底”是不通的概念

  • ESP所指的是棧幀頂部和系統的頂部是同一個位置

引入復雜度

遞歸算法的時間復雜度:遞歸的總次數*每次遞歸的數量。

遞歸算法的空間復雜度:遞歸的深度*每次遞歸創建變量的個數。

時間復雜度

​ 算法的時間復雜度是一個函數,它定量描述了該算法的運行時間。這是一個代表算法輸入的字符串的長度的函數。時間復雜度通常用大O符號表示,不包括這個函數的低階項和首項系數。

​ 使用這種方式時,時間復雜度可被成為是漸進的,也考察輸入值大小趨近無窮時的情況。

名詞解釋:

  • n:問題的規模,n是不斷變化的。

  • T(n):語句頻度或稱時間頻度——算法解決問題所執行語句的次數。

  • f(n):輔助函數,使得T(n)/f(n)的極限為不等於零的常數,那么稱f(n)T(n)的同數量級函數。

  • O:大O符號,一種符號,表示漸進於無窮的行為——大O表示只是說有上界但並不是上確界。

 

​ 例如,如果一個算法對於任何大小為n(必須比 大)的輸入,它至少需要 的時間運行完畢,那么它的漸進時間復雜度是 O()。

 

為了計算時間復雜度,通常會估計算法的操作單元數量,每個單元運行的時間都是相同的。因此,總運行時間和算法的操作單元數量最多相差一個常量系數。

計算方法

  1. 一般情況下,算法中的基本操作重復執行的次數是問題規模n的摸個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限為不等於零的常數,則趁f(n)T(n)的同數量級函數。記作T(n)=O(f(n)),稱O(f(n))為算法的漸進時間復雜度,簡稱時間復雜度。

    分析:隨着模塊n的增大,算法執行時間的增長率和f(n)的增長率成正比,所以f(n)越小,算法的時間復雜度越低,算法的效率越高。

  2. 在計算時間復雜度的時候,先找出算法的基礎操作,然后根據相應的各語句確定它的執行次數,再找出T(n)的同數量級(他的同數量級有以下:1,),找出f(n)=該數量級,若T(n)/f(n)求極限可以得到一常數c,則時間復雜度T(n)=O(f(n))

  3. 時間復雜度比較簡單的計算方法是:看看有幾重for循環,只有一重則時間復雜度為O(n) ,二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查找,如果一個for循環套一個二分,那么時間復雜度則為O(nlogn)

簡單算法的時間復雜度舉例

  • ​O(1)的算法是一些運算為常數的算法。例如:

temp = a; a = b; b = temp;
// 上面語句共三條操作,單條操作的頻度為1,即使他有成千上萬條操作,也只是個較大的書而已,這一類的時間復雜度為O(1)
  • O(n)的算法是一些線性算法。例如:
let sum = 0; // 頻度為1
for(let i = 0; i < n; i++) { // 頻度為n
    sum++; // 頻度為n
}
// 三行的頻度加起來為f(n)= i + n + n = 2n + 1,所以時間復雜度為O(n),這一類算法中操作次數和n成正比線性增長
  • ​log2為底n的對數 ,一個算法如果能在每一個步驟去掉一半數據元素,如二分檢索,舉個例子:
int i = 1;
while(i <= n) {
    i = i * 2;
}

上面代碼設第三行的頻度是f(n),則:2的f(n)次方<=n;f(n)<=​log_2 n,取最大值為f(n)=​log_2 n,即T(n)=O(log_2 n)

  • O(n^2)(n的k次方的情況)最常見的就是平時的對數組進行排序的各種簡單算法都是O(n^2) ,例如直接插在排序的算法。
let sum = 0; // 頻度為1
for(let i = 0; i < n; i++) { // 頻度為n
    for(let j = 0; j < n; j++) { // 頻度為n的平方
        sum++; // 頻度為n的平方
    }
}

​ 時間復雜度按n越大算法越復雜來排的話:

  常數階O(1)、對數階O(logn)、線性階O(n)、線性對數階O(nlogn)、平方階O(n²)、立方階O(n³)、……k次方階O(n的k次方)、指數階O(2的n次方)。

​ 在快速排序中,最壞的情況運行時間是,但是期望值為,所以必須通過一些手段,就可以一期望時間運行。

​ 實際情況下如果不是迫不得已不要用時間復雜度為指數的算法,除非n特別小。

空間復雜度

空間復雜度是一個算法在運行過程中臨時占用存儲空間的大小(問題規模n的函數),記作​S(n)=O(f(n))

比如直接插入排序時間復雜度是O(n^2),空間復雜度是O(1) 。

有的算法需要占用的臨時工作單元數與解決問題的規模n有關,它隨着n的增大而增大,當n較大時,將占用較多的存儲單元,例如快速排序和歸並排序算法就屬於這種情況。

算法的空間復雜度是指算法所需要消耗的空間資源,其計算和表示方法與時間復雜度類似。一般都用復雜度的漸進性來表示。同時間復雜度相比,空間復雜度的分析要簡單的多。

插入排序

概念:有一個已經有序的數據序列,要求在這個已經排好的數據序列中插入一個數,但要求插入后此數據序列仍然有序。

可以分為直接插入排序折半插入排序(二分插入排序)

  • 直接插入排序——雙重for循環

  • 二分插入排序

    • 二分插入排序的基本思想和插入排序一致;都是將某個元素插入到已經有序的序列的正確的位置

    • 和直接插入排序的最大區別是,元素A[i]的位置的方法不一樣;直接插入排序是從A[i-1]往前一個個比較,從而找到正確的位置;而二分插入排序,利用前i-1個元素已經是有序的特點結合二分查找的特點,找到正確的位置,從而將A[i]插入,並保持新的序列依舊有序

    • 減少元素之間比較次數,最壞情況o(n^2),最好情況o(nlogn):剛好插入位置為二分位置

斐波那契時間復雜度和空間復雜度分析

function Fibonacci(n) {
    if(n <= 1) return 1;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
};
console.time(1)
console.log(Fibonacci(20)) // 10946
console.timeEnd(1) // 1: 4.115966796875ms

// console.time(2)
// console.log(Fibonacci(100))
// console.timeEnd(2) // stack overflow 堆棧溢出

當n=5時,在遞歸調用過程中,Fibonacci(3)被計算了2次,Fibonacci(2)被計算了3次,Fibonacci(1)被計算了5次,Fibonacci(0)被計算了3次。

可見遞歸出口在n>=2是,就會一直在現有函數棧上開辟新的空間,所以容易出現棧溢出。

二叉樹的高度為n-1,一個高度為k的二叉樹最多可以由​(2^n-1)個葉子節點,也就是遞歸過程函數調用的次數,所以時間復雜度為​O(2^n),而空間復雜度為​S(n)

尾遞歸優化

  • 空間復雜度可達到o(1),但時間復雜度是o(n);

function Fibonacci01(n, ac1 = 1, ac2 = 1) {
    if(n <= 1) return ac2
    return Fibonacci01(n - 1, ac2, ac1 + ac2)
}

console.time(3)
console.log(Fibonacci01(100)) // 573147844013817200000
console.timeEnd(3) // 3: 0.52197265625ms

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM