我的筆記:
遞歸是一種很常見的計算編程方法,現在通過階乘案例來學習遞歸
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);
// 如果改為尾遞歸,只需要保留一個調用記錄,復雜度為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()。
計算方法
-
一般情況下,算法中的基本操作重復執行的次數是問題規模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)越小,算法的時間復雜度越低,算法的效率越高。
-
在計算時間復雜度的時候,先找出算法的基礎操作,然后根據相應的各語句確定它的執行次數,再找出T(n)的同數量級(他的同數量級有以下:1,,,,,,,),找出f(n)=該數量級,若T(n)/f(n)求極限可以得到一常數c,則時間復雜度T(n)=O(f(n))。
-
時間復雜度比較簡單的計算方法是:看看有幾重for循環,只有一重則時間復雜度為O(n) ,二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查找,如果一個for循環套一個二分,那么時間復雜度則為O(nlogn)。
-
O(1)的算法是一些運算為常數的算法。例如:
temp = a; a = b; b = temp; // 上面語句共三條操作,單條操作的頻度為1,即使他有成千上萬條操作,也只是個較大的書而已,這一類的時間復雜度為O(1)
let sum = 0; // 頻度為1 for(let i = 0; i < n; i++) { // 頻度為n sum++; // 頻度為n } // 三行的頻度加起來為f(n)= i + n + n = 2n + 1,所以時間復雜度為O(n),這一類算法中操作次數和n成正比線性增長
int i = 1; while(i <= n) { i = i * 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(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>=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