一、 算法
算法的定義是這樣的:解題方案的准確而完善的描述,是一系列解決問題的清晰指令。巴拉巴拉的,雖然是一小句但還是不想看(題外話:有時候吧專業名詞記下來面試的時候還是挺有用的),其實就是解決一個問題的完整性描述。只不過這個描述就可能是用不同的方式或者說是“語言”了。
- 算法的效率
既然算法是解決問題的描述,那么就像一千個人眼中有一千個阿姆雷特他大姨夫一樣,解決同一個問題的辦法也是多種多樣的,只是在這過程中我們所使用/消耗的時間或者時間以外的代價(計算機消耗的則為內存了)不一樣。為了更快、更好、更強的發揚奧利奧..哦不,提高算法的效率。所以很多時候一個優秀的算法就在於它與其他實現同一個問題的算法相比,在時間或空間(內存)或者時間和空間(內存)上都得到明顯的降低。
所以呢,算法的效率主要由以下兩個復雜度來評估:
時間復雜度:評估執行程序所需的時間。可以估算出程序對處理器的使用程度。
空間復雜度:評估執行程序所需的存儲空間。可以估算出程序對計算機內存的使用程度。
設計算法時,時間復雜度要比空間復雜度更容易出問題,所以一般情況一下我們只對時間復雜度進行研究。一般面試或者工作的時候沒有特別說明的話,復雜度就是指時間復雜度。
二、 時間復雜度
接下來我們還需要知道另一個概念:時間頻度。這個時候你可能會說:“不是說好一起學算法嗎,這些東東是什么?贈品嗎?”。非也非也,這是非賣品。
因為一個算法執行所消耗的時間理論上是不能算出來的,沒錯正是理論上,so我們任然可以在程序中測試獲得。但是我們不可能又沒必要對每個算法進行測試,只需要知道大概的哪個算法執行所花費的時間多,哪個花費的時間少就行了。如果一個算法所花費的時間與算法中代碼語句執行次數成正比,那么那個算法執行語句越多,它的花費時間也就越多。我們把一個算法中的語句執行次數稱為時間頻度。通常(ps:很想知道通常是誰)用T(n)表示。
在時間頻度T(n)中,n又代表着問題的規模,當n不斷變化時,T(n)也會不斷地隨之變化。為了了解這個變化的規律,時間復雜度這一概念就被引入了。一般情況下算法基礎本操作的重復執行次數為問題規模n的某個函數,用也就是時間頻度T(n)。如果有某個輔助函數f(n),當趨於無窮大的時候,T(n)/f(n)的極限值是不為零的某個常數,那么f(n)是T(n)的同數量級函數,記作T(n)=O(f(n)),被稱為算法的漸進時間復雜度,又簡稱為時間復雜度。
1- 大O表示法及時間復雜度舉例
用O(n)來體現算法時間復雜度的記法被稱作大O表示法
一般我們我們評估一個算法都是直接評估它的最壞的復雜度。
大O表示法O(f(n))中的f(n)的值可以為1、n、logn、n^2 等,所以我們將O(1)、O(n)、O(logn)、O( n^2 )分別稱為常數階、線性階、對數階和平方階。下面我們來看看推導大O階的方法:
推導大O階
推導大O階有一下三種規則:
- 用常數1取代運行時間中的所有加法常數
- 只保留最高階項
- 去除最高階的常數
舉好多栗子
- 常數階
-
let sum = 0, n = 10; // 語句執行一次
-
let sum = (1+n)*n/2; // 語句執行一次
-
console.log(`The sum is : ${sum}`) //語句執行一次
這樣的一段代碼它的執行次數為 3 ,然后我們套用規則1,則這個算法的時間復雜度為O(1),也就是常數階。
- 線性階
-
let i =0; // 語句執行一次
-
while (i < n) { // 語句執行n次
-
console.log(`Current i is ${i}`); //語句執行n次
-
i++; // 語句執行n次
-
}
這個算法中代碼總共執行了 3n + 1次,根據規則 2->3,因此該算法的時間復雜度是O(n)。
- 對數階
-
let number = 1; // 語句執行一次
-
while (number < n) { // 語句執行logn次
-
number *= 2; // 語句執行logn次
-
}
上面的算法中,number每次都放大兩倍,我們假設這個循環體執行了m次,那么2^m = n即m = logn,所以整段代碼執行次數為1 + 2*logn,則f(n) = logn,時間復雜度為O(logn)。
- 平方階
-
for (let i = 0; i < n; i++) { // 語句執行n次
-
for (let j = 0; j < n; j++) { // 語句執行n^2次
-
console.log('I am here!'); // 語句執行n^2
-
}
-
}
上面的嵌套循環中,代碼共執行 2*n^2 + n,則f(n) = n^2。所以該算法的時間復雜度為O(n^2 )
2、常見時間復雜度的比較
常見的時間復雜度函數相信大家在大學中都已經見過了,這里也不多做解釋了:
O(1)<O(logn)<O(n)<O(nlogn)<O(n²)<O(n³)<O(2ⁿ)<O(n!)
三、空間復雜度
2. 算法的空間復雜度
我們在寫代碼時,完全可以用空間來換去時間。
舉個例子說,要判斷某年是不是閏年,你可能會花一點心思來寫一個算法,每給一個年份,就可以通過這個算法計算得到是否閏年的結果。
另外一種方法是,事先建立一個有2050個元素的數組,然后把所有的年份按下標的數字對應,如果是閏年,則此數組元素的值是1,如果不是元素的值則為0。這樣,所謂的判斷某一年是否為閏年就變成了查找這個數組某一個元素的值的問題。
第一種方法相比起第二種來說很明顯非常節省空間,但每一次查詢都需要經過一系列的計算才能知道是否為閏年。第二種方法雖然需要在內存里存儲2050個元素的數組,但是每次查詢只需要一次索引判斷即可。
這就是通過一筆空間上的開銷來換取計算時間開銷的小技巧。到底哪一種方法好?其實還是要看你用在什么地方。
2.1 算法的空間復雜度定義
算法的空間復雜度通過計算算法所需的存儲空間實現,算法的空間復雜度的計算公式記作:S(n)=O(f(n)),其中,n為問題的規模,f(n)為語句關於n所占存儲空間的函數,也是一種“漸進表示法”,這些所需要的內存空間通常分為“固定空間內存”(包括基本程序代碼、常數、變量等)和“變動空間內存”(隨程序運行時而改變大小的使用空間)
通常,我們都是用“時間復雜度”來指運行時間的需求,是用“空間復雜度”指空間需求。
當直接要讓我們求“復雜度”時,通常指的是時間復雜度。
2.2 計算方法
忽略常數,用O(1)表示
遞歸算法的空間復雜度=遞歸深度N*每次遞歸所要的輔助空間
對於單線程來說,遞歸有運行時堆棧,求的是遞歸最深的那一次壓棧所耗費的空間的個數,因為遞歸最深的那一次所耗費的空間足以容納它所有遞歸過程。
-
a = 0
-
b = 0
-
print(a,b)
它的空間復雜度O(n)=O(1);
-
def fun(n):
-
k = 10
-
if n == k:
-
return n
-
else:
-
return fun(++n)
遞歸實現,調用fun函數,每次都創建1個變量k。調用n次,空間復雜度O(n*1)=O(n)。
-
-
for(i=0;i<n;++):
-
-
temp = i
變量的內存分配發生在定義的時候,因為temp的定義是循環里邊,所以是n*O(1)
-
-
temp= 0;
-
-
for(i=0;i<n;i++):
-
-
temp = i
temp定義在循環外邊,所以是1*O(1)
四、時間復雜度與空間復雜度比較
1、常用算法的時間復雜度和空間復雜度

2、時間復雜度為主,空間復雜度為輔
考慮復雜度的話,“程序本身所占的空間”你可以忽略,因為這是“算法的描述”部分,這部分就是個常量(你的程序代碼段寫完就固定大小了),算上它也只是加一個常數
“輸入數據所占的存儲空間”這個其實可以算“需要多少空間”,但不一定是“訪問到多少空間”,如果算上這塊,那么所有算法的空間復雜度至少是O(N),就像如果你計算讀入輸入的時間的話,所有算法的時間復雜度至少也是O(N)一樣,比如說二分查找,雖然你需要O(N)的空間存儲所有數據,但是每次查找只需要O(lgN)次操作和訪問,也就是說訪問到的空間撐死也就是O(lgN),一個算法的所有操作涉及到的空間的復雜度不會超出其時間復雜度,時間復雜度已經給出了一個上界了,這也是平時不怎么關注空間復雜度的一個原因
第三個“算法執行過程中所需的存儲空間”可以認為是算法的額外需要的空間,這個可能有也可能沒有,具體算法具體分析了,但就像上面說的,它也不會超出時間復雜度的上界
