1. 什么是時間復雜度
「時間復雜度是一個函數,它定性描述該算法的運行時間」。
我們在軟件開發中,時間復雜度就是用來方便開發者估算出程序運行的答題時間。
通常,我們會估算算法的操作單元數量來代表程序消耗的時間,這里默認CPU的每個單元運行消耗的時間都是相同的。
比如說:
//這個程序中,我們估算 doSomething() 這個操作的數量來代表程序消耗的時間
for(int i = 0;i < x; i++){
for(int j = 0; j < y;j++){
doSomething(); //這就是算法的操作單元
}
}
用O(f(n))來記作時間復雜度,其中:
- n是算法的問題規模 => 對應上面程序的 doSomething()
- f(n)是操作單元數量 => 對應上面程序的 doSomething() 操作的次數
所以說,O(f(n)) 就是對 f(n) 消耗的時間的估算(這里的“估算”指的是”一般情況“)(對於大O是什么,下一節有詳細講解)
隨着數據規模n的增大,算法執行時間的增長率和f(n)的增長率相同,這稱作為算法的漸近時間復雜度,簡稱時間復雜度,記為 O(f(n))。
2. 什么是大O
結論:算法的時間復雜度是多少指的都是一般情況。
=>
算法導論說:大O用來表示上界的,也就是算法最壞運行情況的時間上界。
但是:
插入排序的最壞情況是O(n2),快速排序也是O(n2),但是,
眾所周知,插入排序的時間復雜度為O(n^2),快速排序的時間復雜度是O(nlogn),
「但是我們依然說快速排序是O(nlogn)的時間復雜度,這個就是業內的一個默認規定,這里說的O代表的就是一般情況,而不是嚴格的上界」。
我們主要關心的還是一般情況下的數據形式。
「面試中說道算法的時間復雜度是多少指的都是一般情況」。但是如果面試官和我們深入探討一個算法的實現以及性能的時候,就要時刻想着數據用例的不一樣,時間復雜度也是不同的,這一點是一定要注意的。
3. 不同數據規模的差異
如下圖中可以看出不同算法的時間復雜度在不同數據輸入規模下的差異。
-
數據規模很大的時候,下式滿足:
O(1)常數階 < O(logn)對數階 < O(n)線性階 < O(n^2)平方階 < O(n^3)(立方階) < O(2^n) (指數階)
-
為什么在計算時間復雜度的時候要忽略常數項系數:
因為大O就是數據量級突破一個點且數據量級非常大的情況下所表現出的時間復雜度,這個數據量也就是常數項系數已經不起決定性作用的數據量
例如上圖中20就是那個點,n只要大於20 常數項系數已經不起決定性作用了。
-
但是規模小時,可能不滿足。
所以說在決定使用哪些算法的時候,不是時間復雜越低的越好(因為簡化后的時間復雜度忽略了常數項等等)。
-
還有一點要注意:要注意大常數,如果這個常數非常大,例如10^7 ,10^9 ,那么常數就是不得不考慮的因素了。
4. 復雜表達式的化簡
有時候我們去計算時間復雜度的時候發現不是一個簡單的O(n) 或者O(n^2), 而是一個復雜的表達式,例如:
O(2*n^2 + 10*n + 1000)
那這里如何描述這個算法的時間復雜度呢?
-
一種方法就是簡化法。
-
去掉運行時間中的加法常數項 (因為常數項並不會因為n的增大而增加計算機的操作次數)。
O(2*n^2 + 10*n)
-
去掉常數系數(上文中已經詳細講過為什么可以去掉常數項的原因)。
O(n^2 + n)
-
只保留保留最高項,去掉數量級小一級的n (因為n^2 的數據規模遠大於n),最終簡化為:
O(n^2)
-
-
如果這一步理解有困難,那也可以做提取n的操作,變成O(n(n+1)) ,省略加法常數項后也就別變成了:
O(n^2)
所以最后我們說:這個算法的算法時間復雜度是O(n^2) 。
-
也可以用另一種簡化的思路,其實當n大於40的時候, 這個復雜度會恆小於O(3 * n^2), O(2 * n^2 + 10 * n + 1000) < O(3 * n2),所以說最后省略掉常數項系數最終時間復雜度也是O(n2)。
5. O(logn)中的log是以什么為底?
平時說這個算法的時間復雜度是logn的,不一定是log 以2為底n的對數,可以是以10為底n的對數,也可以是以20為底n的對數。
「統一說 logn,也就是忽略底數的描述」
為什么可以這么做呢?如下圖所示:
假如有兩個算法的時間復雜度,分別是log以2為底n的對數和log以10為底n的對數,那么這里如果還記得高中數學的話,應該不能理解以2為底n的對數 = 以2為底10的對數 * 以10為底n的對數
。
而以2為底10的對數是一個常數,在上文已經講述了我們計算時間復雜度是忽略常數項系數的。
抽象一下就是在時間復雜度的計算過程中,log以i為底n的對數等於log 以j為底n的對數,所以忽略了i,直接說是logn。
這樣就應該不難理解為什么忽略底數了。
6. 關於O(n) 、O(n^2) 和 O(nlogn) 具體的例子
// O(n)
void function1(long long n) {
long long k = 0;
for (long long i = 0; i < n; i++) {
k++;
}
}
// O(n^2)
void function2(long long n) {
long long k = 0;
for (long long i = 0; i < n; i++) {
for (long j = 0; j < n; j++) {
k++;
}
}
}
// O(nlogn)
void function3(long long n) {
long long k = 0;
for (long long i = 0; i < n; i++) {
for (long long j = 1; j < n; j = j * 2) { // 注意這里j=1
k++;
}
}
}
關於測試三種算法的耗時,完整的測試代碼如下:
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
using namespace chrono;
// O(n)
void function1(long long n) {
long long k = 0;
for (long long i = 0; i < n; i++) {
k++;
}
}
// O(n^2)
void function2(long long n) {
long long k = 0;
for (long long i = 0; i < n; i++) {
for (long j = 0; j < n; j++) {
k++;
}
}
}
// O(nlogn)
void function3(long long n) {
long long k = 0;
for (long long i = 0; i < n; i++) {
for (long long j = 1; j < n; j = j * 2) { // 注意這里j=1
k++;
}
}
}
int main() {
long long n; // 數據規模
while (1) {
cout << "輸入n:";
cin >> n;
milliseconds start_time = duration_cast<milliseconds >(
system_clock::now().time_since_epoch()
);
function1(n);
// function2(n);
// function3(n);
milliseconds end_time = duration_cast<milliseconds >(
system_clock::now().time_since_epoch()
);
cout << "耗時:" << milliseconds(end_time).count() - milliseconds(start_time).count()
<<" ms"<< endl;
}
}
結果是:O(n) 的速度很快,而 O(n2) 的速度比前者慢了好多好多,但是,出於算法的復雜度,O(log(n) 的復雜度只比 O(n) 慢一個數量級,所以說,將 O(n2) 的算法的時間復雜度改為 O(log(n) 就是一種典型的、優秀的優化方式。
7. 一個分析的例子
通過這道面試題目,來分析一下時間復雜度。題目描述:找出n個字符串中相同的兩個字符串(假設這里只有兩個相同的字符串)。
如果是暴力枚舉的話,時間復雜度是多少呢,是O(n^2)么?
這里一些同學會忽略了字符串比較的時間消耗,這里並不像int 型數字做比較那么簡單,除了n^2 次的遍歷次數外,字符串比較依然要消耗m次操作(m也就是字母串的長度),所以時間復雜度是O(m * n * n)。
接下來再想一下其他解題思路。
先排對n個字符串按字典序來排序,排序后n個字符串就是有序的,意味着兩個相同的字符串就是挨在一起,然后在遍歷一遍n個字符串,這樣就找到兩個相同的字符串了。
那看看這種算法的時間復雜度,快速排序時間復雜度為O(nlogn),依然要考慮字符串的長度是m,那么快速排序每次的比較都要有m次的字符比較的操作,就是O(m * n * logn) 。
之后還要遍歷一遍這n個字符串找出兩個相同的字符串,別忘了遍歷的時候依然要比較字符串,所以總共的時間復雜度是 O(m * n * logn + n * m)。
我們對O(m * n * logn + n * m) 進行簡化操作,把m * n提取出來變成 O(m * n * (logn + 1)),再省略常數項最后的時間復雜度是 O(m * n * logn)。
最后很明顯O(m * n * logn) 要優於O(m * n * n)!
所以先把字符串集合排序再遍歷一遍找到兩個相同字符串的方法要比直接暴力枚舉的方式更快。
這就是我們通過分析兩種算法的時間復雜度得來的。
「當然這不是這道題目的最優解,我僅僅是用這道題目來講解一下時間復雜度」。
8. 總結
這篇文章講了什么是時間復雜度,怎樣表達時間復雜度,數據規模和時間復雜度的關系,最后用一個例子講解一下時間復雜度。