1. 程式人生 > >算法面試中的時間復雜度分析

算法面試中的時間復雜度分析

數量 簡單 void 需要 對數 排序 ron size 最長

例子: 有一個字符串數組,首先將數組中每一個字符串按照字母序排序,之後再將整個字符串按照字典序排序。整個操作的時間復雜度?

答: 假設最長的字符串長度是s,數組中有n個字符串。
對每個字符串進行排序: slogs, 共有n個,所以 nslog(s)
所有的字符串進行排序:O(s*nlog(n)) //對字符串進行排序,每一次比較最多為s

==> O(n * slogs) + O(s * nlogn) = O(sn(logn + logs))

算法復雜度有些情況下是和用例相關的

對數據規模有一個概念 -- 封底估算

用下面的一個程序進行測試:

for(int x = 1; x <= 9; x++){
    int
n = pow(10, x); clock_t startTime = clock(); int sum = 0; for(int i = 0; i < n; i++) sum += i; clock_t endTime = clock(); cout << "10^" << x << " : "<< double(endTime - startTime)/CLOCKS_PER_SEC << " s"<< endl; }

這是一個O(n)的算法在本機4核i7的機器跑出來結果如下:

10^1 : 0 s
10^2 : 0 s
10^3 : 0 s
10^4 : 0 s
10^5 : 0 s
10^6 : 0 s
10^7 : 0.03125 s
10^8 : 0.25 s
10^9 : 2.4375 s

也就是說,如果程序要求在1s內跑出結果,數據規模最好不要超過10^8,不要達到10^9,也就是說:
O(n^2): 大約可處理10^4級別的數據
O(n^logn): 大約可處理10^7級別數據
O(n): 大約可處理10^8級別數據

常見的復雜度分析

\(O(1)\)

void swap(int &a, int& b){
    int tmp = a; a = b; b = tmp;
}

\(O(n)\)
--- 常系數很可能不為1

int sum(int n) {
    int sum = 0;
    for(int i = 1; i <= n; i++) {
        sum += n;
    }
    return sum;
}

\(O(n^{2})\)

// 選擇排序
for(int i = 0; i < n; i++){
    int minIndex = i;
    for(int j = i + 1; j < n; j++){
        if(arr[j] < arr[minIndex]) {
            minIndex = j;
        }
    }
}
swap(arr[i], arr[minIndex]);

\(O(logn)\)

// lower_bound
int binSearch(const vector<int>& nums, int lo, int hi, int key) {
    while(lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if(nums[mid] < key) {
            lo = mid;
        }
        else{
            hi = mid;
        }
    } 
    return lo;
}

考慮下面這個例子:n經過幾次除以10的操作後,等於0? 答案是log_{10}n

// Warnning!! this code is buggy
// 需要考慮各種情況
string intToString (int num) {
    string s = "";
    while(num) {
        s += ‘0‘ + num % 10;
        num /= 10
    }
    reverse(s);
    return s;
}

以2為底和以10為底的對數,在數量級上沒有區別。(只是一個線性關系)

\(O(nlogn)\)

雖然下面的代碼也是兩重循環,但是復雜度卻是\(O(nlogn)\)的,因為外層循環是指數級增加的(每次乘以2)

void hello(int n) {
    for(int sz = 1; sz < n; sz += sz) {
        for(int i = 1; i < n; i++){
            cout << "Hello" << endl;
        }
    }
}

復雜度實驗

我明明寫的是\(O(nlogn)\)的算法,面試官卻說我是\(O(n^{2})\)的?

可以自己進行驗證,看能夠處理什麽級別的數據規模,參考封底估算。
實驗,觀察趨勢。比如每次將數據規模提高兩倍,觀察時間變化.

遞歸算法的復雜度分析

對於單次遞歸調用,復雜度一般為\(O(T*depth)\), 寫遞推表達式進行推導。

如下面的代碼:

double pow(double x, int n) {
    assert(n >= 0);
    if(n == 0) return 1;
    
    double t = pow(x, n/ 2);
    if(n %2) 
        return x*t*t;
    else
        return t*t;
}

遞歸深度\(depth = logn, T = 1\),因此復雜度為\(O(logn)\)

int f() {
    //遞歸基 here
    return f(n - 1) + f(n - 1);
}

多次遞歸調用,遞歸深度\(depth = n, 每次操作2\),可以推導出算法復雜度為指數級\(O(2^n)\)
\[\begin{equation}\begin{split}\\f(n) &= 2f(n-1)\\&= 4(n-2)\\&= 8f(n-3)\\&\cdots\\&= 2^{n}f(1)\\&= O(2^{n})\\\end{split}\end{equation} \tag{1}\]

簡單不嚴謹地分析快速排序的復雜度:

快速排序的每一次partition操作可構造出這麽一個位置,即左邊的所有值都比軸點小,右邊的都比軸點大,因此\(f(n) = 2*f(n/2) + f(partition)\) 而partition操作只需要做一次循環,所以是一個O(n),所以
\[\begin{split}\\f(n) &= 2f(n/2) + O(n)\\&= 4f(n/4) + O(n) + 2*O(n/2)\\&= 8f(n/8) + O(n) + 2*(n/2) + 4*(n/4)\\&\cdots\\&= 2^{\log_{2}n} * f(1) + \underbrace{ O(n) + O(n) +\cdots+ O(n) }_{k = \log_{2}n個}\\&= n + O(n*\log_{2}n)\\&= O(n * \log_{2}n)\\\end{split} \tag{2}\]
當然,嚴謹的分析還需要引入概率(隨機分布)。這裏只是簡單不嚴謹的推導。

均攤復雜度分析 Amoritzed Time

典型例子:動態數組(vector)
每一次動態擴容(resize()),需要開辟一個新的空間,然後進行一一賦值,這樣一個操作的復雜度是\(O(n)\)那麽問題來了:vector push_back的平均復雜度是多少?

假設當前數組容量為n,從空到滿,每一次操作的消耗是\(O(1)?\). 如果此時再來一個元素,就需要resize, 那麽最後這一次操作耗費為\(O(n)?\), 那麽平均來看,過去n+1次操作的總花費為
\[ \underbrace{ O(1) + O(1) +\cdots+ O(1) }_{n個} + O(n) = O(2n)\]
那麽分攤來看每一次push_back的操作花費為\(O(\frac{2n}{n+1}) = O(2) = O(1)\)

那麽問題又來了,如果pop_back的時候,發現size為當前capacity的1/2就resize,那麽時間復雜度是多少?
假設當前數組容量為2n,從滿到一半,每一次操作的消耗是\(O(1)\),如果此時再pop_back,需要再消耗的時間為\(O(2*n)\), 那麽這個視角下的均攤分析還是為級別O(1)
可是換一種奇怪的情況:數組滿後,resize為兩倍,需要O(n);此時又要刪除,那麽又到達臨界點了,此時要resize縮容,又需要\(O(n)\),那麽在這種退化情況下,單次操作復雜度退化到了\(O(n)\), 這種情況也被稱之為復雜度的振蕩

正確的做法是什麽呢?pop_back要等到size為capacity的1/4再resize縮容。

面試被問過的復雜度分析

假設現在的動態數組/哈希表有n個元素,vector的初始大小為1,問從開始到現在,共有多少次復制操作?

最後一次復制:n/2參與

倒數第二次復制:n/4參與

倒數第三次復制:n/8參與

...

第二次:2個參與

第一次:1個參與

\[\therefore S(n) = \frac{n}{2} + \frac{n}{4} + \frac{n}{8} +\cdots+4+2+1 \tag{3}\]

\[\Rightarrow 2S(n) = n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} +\cdots+4+2 \tag{4}\]

很容易就可以得出

\[S(n) = n - 1 \tag{*}\]

也就是說,每一次插入新元素的復雜度能夠分攤為\(O(1)\)的級別

算法面試中的時間復雜度分析