寫在前面:\(log_2n\)均記作\(logn\),實際上在計算機中,\(lnx,lgx\)和\(log_2x\)數值一致,因為:
所以:
一、時間復雜度
(一)概念
如果一個問題的規模是\(n\),解這一問題的某一算法所需要的時間為\(T(n)\),它是\(n\)的某一函數,\(T(n)\)稱為這一算法的“時間復雜性”。
當輸入量\(n\)逐漸加大時,時間復雜性的極限情形稱為算法的“漸近時間復雜性”,也可以表示為時間復雜度。時間復雜度是總運算次數表達式中受\(n\)的變化影響最大的那一項(不含系數)。
“大\(O\)記法”:在這種描述中使用的基本參數是\(n\),即問題實例的規模,把復雜性或運行時間表達為\(n\)的函數。這里的“\(O\)”表示量級 \((order)\),比如說“二分檢索是\(O(nlogn)\)的”,也就是說它需要“通過量級\(logn\)的步驟去檢索一個規模為\(n\)的數組”記法,表示當\(n\)增大時,\(O(f(n))\)運行時間至多將以正比於\(f(n)\)的速度增長。
這種漸進估計對算法的理論分析和大致比較是非常有價值的,但在實踐中細節也可能造成差異。例如,一個低附加代價的\(O(n^2)\)算法在\(n\)較小的情況下可能比一個高附加代價的\(O(nlogn)\)算法運行得更快。當然,隨着\(n\)足夠大以后,具有較慢上升函數的算法必然工作得更快。
計算時間復雜度的關鍵因素:循環次數和遞歸次數
(二)針對幾個程序的時間復雜度
\(O(1)\)
temp=i;i=j;j=temp;
我們經常會聽到大佬們說:“使用\(O(1)\)的查詢...”,實際上就是與\(n\)無關,只是在進行語句。
\(O(n)\)
例子1
1 sum=0;
2 for(i=1;i<=n;i++) {
3 for(j=1;j<=n;j++) {
4 sum++; }
5 }
第1行:\(1\)次
第2行:\(n\)次
第3行:\(n^2\)次
第4行:\(n^2\)次
解:\(T(n)=2n^2+n+1 =O(n^2)\)
以上是求解程序正常的時間復雜度的方法,即忽略\(T(n)\)中\(n\)的最高次項的系數和其他項(宗旨是只考慮最大項)
(此處想到了物理老師,當\(m<<M\)的時候,表達式\(a=\dfrac{M}{M+m}g\)忽略\(m\)的取值 即\(m\)微小到不對結果起到作用)
例子2
for (i=1;i<n;i++) {
y=y+1; ①
for(j=0;j<=(2*n);j++) {
x++; } ②
}
解:
語句1的頻度是\(n-1\)
語句2的頻度是\((n-1)*(2n+1)=2n^2-n-1\)
\(f(n)=2n^2-n-1+(n-1)=2n^2-2\)
該程序的時間復雜度\(T(n)=O(n^2)\)
\(O(logn)\)
i=1; ①
while (i<=n)
i=i*2; ②
解: 語句1的頻度是1
設語句2的頻度是\(f(n)\), 則:\(2^{f(n)}\leq n;f(n)\leq logn\)
取最大值\(f(n)=logn, T(n)=O(logn)\)
\(O(n^3)\)
for(i=0;i<n;i++)
{
for(j=0;j<i;j++)
{
for(k=0;k<j;k++)
x=x+2;
}
}
解:當\(i=m,j=k\)的時候,內層循環的次數為\(k\)
當\(i=m\)時,\(j\)可以取 \(0,1,…,m-1\) ,
所以這里最內循環共進行了\(0+1+…+m-1=\dfrac{(m-1)m}{2}\)次
所以,\(i\)從\(0\)取到\(n\), 則循環共進行了:\(0+\dfrac{(1-1)\times 1}{2}+…+\dfrac{(n-1)n}{2}=\dfrac{n(n+1)(n-1)}{6}\)
所以時間復雜度為\(O(n^3)\)
(三)一些特殊的時間復雜度
- 時間復雜度的不穩定性
我們還應該區分算法的最壞情況的行為和期望行為。如快速排序的最壞情況運行時間是\(O(n^2)\),但期望時間是\(O(nlogn)\)。通過每次都仔細地選擇基准值,我們有可能把平方情況 (即\(O(n^2)\)情況)的概率減小到幾乎等於\(0\)。在實際中,精心實現的快速排序一般都能以 \(O(nlogn)\)時間運行。
- 一些常用的記法:
(1) 訪問數組中的元素是常數時間操作,或說\(O(1)\)操作。
(2) 一個算法如果能在每個步驟去掉一半數據元素,如二分檢索,通常它就取 \(O(logn)\)時間。
(3) 用\(strcmp\)比較兩個具有\(n\)個字符的串需要\(O(n)\)時間。
(4) 常規的矩陣乘算法是\(O(n^3)\),因為算出每個元素都需要將\(n\)對元素相乘並加到一起,所有元素的個數是\(n^2\)。
(5) 指數時間算法通常來源於需要求出所有可能結果。例如,\(n\)個元素的集合共有\(2n\)個子集,所以要求出所有子集的算法將是\(O(2^n)\)的。
指數算法一般說來是太復雜了,除非\(n\)的值非常小,因為,在這個問題中增加一個元素就導致運行時間加倍。不幸的是,確實有許多問題 \((\)如著名的“巡回售貨員問題” \()\),到目前為止找到的算法都是指數的。如果我們真的遇到這種情況,通常應該用尋找近似最佳結果的算法替代之。
上一段來自網絡
計算方法
一般情況下,算法的基本操作重復執行的次數是模塊\(n\)的某一個函數\(f(n)\),因此,算法的時間復雜度記做:\(T(n)=O(f(n))\)。隨着模塊\(n\)的增大,算法執行的時間的增長率和\(f(n)\)的增長率成正比,所以\(f(n)\)越小,算法的時間復雜度越低,算法的效率越高。
在計算時間復雜度的時候,先找出算法的基本操作,然后根據相應的各語句確定它的執行次數,再找出\(T(n)\)的同數量級(它的同數量級有以下:\(logn,n,nlogn,n^2,n^3,2^n,n!\)),找出后,\(f(n)=\)該數量級,若\(\dfrac{T(n)}{f(n)}\)求極限可得到一常數\(c\),則時間復雜度\(T(n)=O(f(n))\)。
- 常見的時間復雜度
按數量級遞增排列,常見的時間復雜度有:常數階\(O(1)\), 對數階\(O(logn)\), 線性階\(O(n)\), 線性對數階\(O(nlogn)\), 平方階\(O(n^2)\), 立方階\(O(n^3)\),…, \(k\)次方階\(O(n^k)\), 指數階\(O(2^n)\) 。
其中,
1.\(O(n)\),\(O(n^2)\), 立方階\(O(n^3)\),…,\(k\) 次方階\(O(n^k)\) 為多項式階時間復雜度,分別稱為一階時間復雜度,二階時間復雜度……
2.\(O(2^n)\),指數階時間復雜度,該種不實用
3.對數階\(O(logn)\), 線性對數階\(O(nlogn)\),除了常數階以外,該種效率最高
例:算法:
for(i=1;i<=n;++i)
{
for(j=1;j<=n;++j)
{
c[i][j]=0; //1
for(k=1;k<=n;++k)
c[i][j]+=a[i][k]*b[k][j]; //2
}
}
1:該步驟執行次數:\(n^2\)
2:該步驟執行次數:\(n^3\)
則有\(T(n)= n^2+n^3\),根據上面括號里的同數量級,我們可以確定$ n^3\(為\)T(n)$的同數量級
則有\(f(n)= n^3\),然后根據\((\lim\limits_{n\to+\infty})\dfrac{T(n)}{f(n)}\)求極限可得到常數$c $
則該算法的 時間復雜度:\(T(n)=O(n^3)\)
重要:
\(O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<...<O(n^k)<O(2^n)<O(n!)\)
(以下方法來自網絡,僅供參考)
可以試着在程序的頭尾兩端記下時間,然后通過相減的方法,計算出時間!
#include <time.h>
好像是這個頭文件(一定要寫上時間的頭文件)
float start_time = clock();
這個放在你要計時間的程序的開始
float end_time = clock();
這個放在你要計時間的程序的末尾
#include<ctime>
printf("Time used=%.2lf\n",(double)clock()/CLOCKS_PER_SEC);
練習:請計算出以下程序的時間復雜度\(O(k)\)
#include <cstdio>
using namespace std;
int a[100],b[100];
int k;
int main(){
for(int i=0;i<10;i++){//1
a[i]=i+1;
}
for(int i=0;i<10;i++){//2
for(int j=0;j<a[i];j++){//3
b[k]=a[i];
k++;
}
}
for(int i=0;i<k;i++){//4
if(i==0){
printf("%d",b[i]);
}
else{
printf(",%d",b[i]);
}
}
return 0;
}
二、空間復雜度
空間復雜度是對一個算法在運行過程中臨時占用存儲空間大小的一個量度,同樣反映的是一個趨勢,用\(S(n)\) 來定義。
空間復雜度是對一個算法在運行過程中臨時占用存儲空間大小的量度,記做\(S(n)=O(f(n))\)。比如直接插入排序的時間復雜度是\(O(n^2)\),空間復雜度是\(O(1)\) 。而一般的遞歸算法就要有\(O(n)\)的空間復雜度了,因為每次遞歸都要存儲返回信息。一個算法的優劣主要從算法的執行時間和所需要占用的存儲空間兩個方面衡量。
空間復雜度比較常用的有:\(O(1)\)、\(O(n)\)、\(O(n^2)\):
- 空間復雜度\(O(1)\)
如果算法執行所需要的臨時空間不隨着某個變量n的大小而變化,即此算法空間復雜度為一個常量,可表示為 \(O(1)\)
舉例:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
代碼中的 \(i,j,m\)所分配的空間都不隨着處理數據量變化,因此它的空間復雜度 \(S(n) = O(1)\)
- 空間復雜度\(O(n)\)
我們先看一個代碼:
int n=1000;
int new[n];
for(i=1; i=n; ++i)
{
j = i;
j++;
}
這段代碼中,第一行\(new\)了一個數組出來,這個數據占用的大小為\(n\),這段代碼的\(2-6\)行,雖然有循環,但沒有再分配新的空間,因此,這段代碼的空間復雜度主要看第一行即可,即 \(S(n) = O(n)\)
\(125MB=131072000B\approx10^8B\)
類型 | 占字節 |
---|---|
\(int\) | 2 |
\(long\) \(long\) | 4 |
\(unsigned\) \(long\) \(long\) | 8 |
\(float\) | 4 |
\(double\) | 8 |
\(long\) \(double\) | 16 |
\(bool\) | 1 |
計算方法:
如果開了\(n\)個不同類型的數組,每個類型開了\(d_i\)個,則一共有\(n_t=\sum\limits_{i=1}^{n}d_i\),每個類型所占字節為\(p_i\),每個類型的每個數組大小為\(y_{i_j}(j\in[1,d_i],i\in[1,n])\)
程序最大空間為\(P\) \(MB\) \(=1048576P\) \(B=C\) \(B\)
對於\(S=\sum\limits_{i=1}^{n}\lbrack \sum\limits_{j=1}^{d_i}(p_i*y_{i_j})\rbrack\leq C\)滿足條件,即符合
舉例:
三個數組,\(bool[1000],int[20000],double[300000]\)
則\(S=1\times1000+2\times20000+8\times3000000=24041000>13107200\)
所以空間復雜度不符合
2019.11.28