# 複雜度 ***統整自AP325*** 為了估計程式的執行效率,~~並且為了二分搜的優勢鋪路~~,在這裡做複雜度的介紹。 ## 估算方式 複雜度$O(f(n))$,念作Big-O,**一定要大寫**,小寫有不同意思,習慣上以n表示資料量,複雜度所代表的意思為「**當資料量為n時,這個程式(演算法)的執行時間(執行指令數)不超過$f(n)$的某個常數倍**」 其中資料量可以是字串長度、輸入數量等,常見的複雜度有$O(log(n))、O(n)$、$O(nlog(n))、O(n^2)等$,計算中有以下幾個原則: :::success * $O(2n)=O(3n)=O(n)$,因為根據定義,我們不在乎常數倍。 * 兩個函數相加的Big-O等於比較大的函數。也就是說,當n落在足夠大時,當$f(n)>=g(n)$時,$O(f(n)+g(n))=O(f(n))$,總的來說,找最差的那個當作複雜度即可。 * 若某段程式的複雜度$O(f(n))$執行了$g(n)$次,複雜度為$O(f(n)*g(n))$ * 複雜度計算時找最大的n來計算(worst-case) ::: ## 例子 ```cpp= for(int i=0;i<n;i++) total+=a[i]*i; ``` 以這個程式來說,需要做 +=,\*,i++,i<n 四個運算,所以複雜度是O(4n),根據定義,我們會描述為O(n)。 **PS.sort的時間複雜度為O(nlogn)** ## 資料量對應複雜度 以1秒為例,現在電腦大約可以處理$10^8$~$10^9$的資料量,所以可以整理出下表 | n(大約) | 10^18^ | 10^9^ | 10^6^ | 10^4^ | 10 | | ------- | ----------- | ------ | ---------- | -------- | ------ | | 複雜度 | $O(log(n))$ | $O(n)$ | $O(nlogn)$ | $O(n^2)$ | $O(n!)$ | # 線性搜尋 在學習迴圈的時候,我們曾經用for迴圈作為搜尋的方法 ```cpp= for(int i=0;i<n;i++) { if(a[i]==2) { cout<<"Found 2 in "<<i<<endl; } } ``` 以剛剛的估算法中,時間複雜度為O(n),優勢為簡單好寫,但缺點是在資料超過$10^9$時會TLE # 二分搜 ## 原理 在尋找一群已經排序好的資料中,每次都將現在的資料分為一半,因為已經排序的關係,只須所以只需看中間那個數字大於或小於欲尋找的數字,選擇適合的區間直至搜尋到為止。 ## 時間複雜度 (直接記住結論即可) 已知每次折半的時間複雜度為O(1),因為是在做數學運算。 因為每次都將資料折半,假設有n筆資料。 :::success 第一次剩下n/2 第二次剩下n/4 . . . 第m次剩下n/2^m^ ::: 假設在最差的情況下,第m次搜尋到,也就是$n/2^m=1$,解得$2^m=n$等於$lg(n)=m$,所以時間複雜度為 $O(m)=O(lg(n))=O(log(n)/log(2))=O(log(n))$ 相較於線性搜尋的$O(n)$,二分搜的$O(log(n))$相對較佳,但對於無序的情況下,排序的時間複雜度為$O(nlog(n))$,所以在正常情況下還是會選用線性搜尋。 ## 實作 ### 維護左右端點 這是相對較為傳統的一種二分搜方法,選擇將維護的區間(常用左閉右開,也就是\[a,b))作為迴圈變數來運算。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { int wannafind=836,right=1000,left=1; while(left<=right) { int mid=(left+right)/2; cout<<left<<' '<<right<<endl; if(wannafind==mid) { cout<<wannafind<<" Find "<<mid<<endl; break; } if(wannafind>mid) left=mid+1; if(wannafind<mid) right=mid-1; } } ``` ### 用跳的! 將一開始放在0,第一次跳總距離的一半,接下來每次跳的距離都砍半,能跳就往前跳,直到找到為止。這樣寫的好處是相較於傳統的更不容易寫錯,因為不須注意範圍。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { int wannafind=835,n=0,sizes=1000; for(int jump=sizes/2;jump>0;jump>>=1)//jump>>=1等價於jump/=2 { if(n+jump==wannafind) { cout<<"Find"<<endl; break; } while(n+jump<wannafind) { n+=jump; cout<<n<<endl; } } } ``` ### lower/upper bound 這裡套用C++既有的函式庫,作為簡單方便的搜尋用,但相對制式,所以還是要會實作。 * lower_bound:尋找最小且**不小於**所需的數 * upper_bound:尋找最小且**大於**所需的數 通式 :::success lower_bound(起始指標,終止指標,欲尋找之數); upper_bound(起始指標,終止指標,欲尋找之數); ::: 兩者皆回傳欲尋找之數的指標。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { int a[10]={1,15,23,45,67,89,234,453,675,897}; cout<<*lower_bound(a,a+10,234)<<endl; cout<<*upper_bound(a,a+10,234)<<endl; } ```