算法的定義是這樣的:解題方案的準確而完善的描述,是一系列解決問題的清晰指令,就是解決一個問題的完整性描述。javascript
如何衡量一個算法的好壞,能夠經過空間複雜度和時間複雜度兩個方面來進行衡量。java
空間複雜度 評估執行程序所需的存儲空間。能夠估算出程序對計算機內存的使用程度。算法
時間複雜度 評估執行程序所需的時間。能夠估算出程序對處理器的使用程度。數組
設計算法時,時間複雜度要比空間複雜度更容易出問題,因此通常狀況下咱們只對時間複雜度進行研究。bash
若是一個算法所花費的時間與算法中代碼語句執行次數成正比,那麼那個算法執行語句越多,它的花費時間也就越多。咱們把一個算法中的語句執行次數稱爲時間頻度。一般用T(n)表示。n用來表示問題的規模。dom
通常狀況下,算法中基本操做重複執行的次數是n的某個函數,用T(n)
表示,f(n)
用來描述T(n) 函數
中增加最快的部分,。記做T(n)=O(f(n))
,稱O(f(n))
爲算法的漸進時間複雜度,簡稱時間複雜度。函數
該表示方法被成爲大O表示法。工具
時間複雜度經常使用大O符號——O(f(n))
表述,不包括這個函數的低階項和首項係數。oop
推導大O階有一下三種規則:ui
大O表示法O(f(n))中的f(n)的值能夠爲一、n、logn、n^2 等,因此咱們將O(1)、O(n)、O(logn)、O( n^2 )分別稱爲常數階、線性階、對數階和平方階。下面咱們來看看推導大O階的方法:
例:段代碼的大O是多少?
let sum = 0, n = 100;
複製代碼
第一條就說明了全部加法常數給他個O(1)便可
通常含有非嵌套循環涉及線性階,線性階就是隨着問題規模n的擴大,對應計算次數呈直線增加。
let i , n = 100, sum = 0;
for( i=0; i < n; i++ )
{
sum = sum + i;
}
複製代碼
上面這段代碼,它的循環的時間複雜度爲O(n),由於循環體中的代碼須要執行n次。
let i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=0; j < n; j++ )
{
console.log('hi')
}
}
複製代碼
n等於100,也就是說外層循環每執行一次,內層循環就執行100次,那總共程序想要從這兩個循環出來,須要執行100*100次,也就是n的平方。因此這段代碼的時間複雜度爲O(n^2)。
總結:若是有三個這樣的嵌套循環就是n^3。因此總結得出,循環的時間複雜度等於循環體的複雜度乘以該循環運行的次數。
let i = 1, n = 100;
while( i < n )
{
i = i * 2;
}
複製代碼
因爲每次i*2以後,就距離n更近一步,假設有x個2相乘後大於或等於n,則會退出循環。 因而由2^x = n
獲得x = log(2)n
,因此這個循環的時間複雜度爲O(logn)
。
假設冒泡排序存在三種狀況,1.數組所有正序排列;2.數組所有反向排列;3.數組混亂排序;
第一種狀況下,數組只需遍歷一遍就能夠完成排序,假設數組長度爲n,須要遍歷n-1次,此時:
T(n) = n-1 = T(O(n));
複製代碼
第二種狀況,數組所有反向排列,那麼就須要一次次遍歷數組直到順序正確,第一次排序須要執行n-1步,第二次排序能夠排除排序正確的數,只須要執行(n-1)-1)次,以此類推,直到最後執行1次完成排序,該過程次數爲:
T(n)=n(n-1)/2 = T(O(n^2);
複製代碼
第三種狀況,數組混亂排序,可能不須要執行到最後便可完成排序,假設到第i次便可完成排序,該過程執行次數:
T(n)=n(n-i)/2 = T(O(n^2);
複製代碼
綜上,冒泡排序的平均時間複雜度爲
O(n^2)
複製代碼
推導大O階有一下三種規則:
當n增大到必定程度時,f(n)中最高階的部分佔據了主導地位,低階變量和常數對結果對影響幾乎能夠忽略不計。
工具地址:函數生成器
圖中的兩個函數分別爲
y=3x^2
複製代碼
y=3x^2+2x
複製代碼
隨着X的變大,兩個函數曲線幾乎重合,常數10000的影響微乎其微,可忽略不計。
圖中的兩個函數分別爲
y=2x^2
複製代碼
y=2x^2+10000
複製代碼
隨着X的變大,兩個函數曲線幾乎重合,常數10000的影響微乎其微,可忽略不計。
同理,高階變量前的常量也可忽略不計
綜上,低階變量和常量對最終結果影響不大
O(1)<O(logn)<O(n)<O(nlogn)<O(n²)<O(n³)<O(2ⁿ)<O(n!)
方法1:
var arr = [1,2,2,4,3,4,1,3,2,7,5,6,1]
var newArr = new Set(arr); //n
複製代碼
T(n)=n=T(O(n));
複製代碼
該算法時間複雜度爲
O(n);
複製代碼
方法2:
function fn(arr){
let newArr = [] // 1
arr.sort((a,b)=>{
return a-b
}) // O(n)=n^2
arr.forEach((val,index)=>{ // n
if(val != arr[index+1]){ // 1
newArr.push(val) // 1
}
})
return newArr //1
}
複製代碼
T(n) =T(O(n^2)) +n+2=T(O(n^2));
複製代碼
該算法時間複雜度爲
O(n^2);
複製代碼
方法3:
for(var i=0;i<arr.length;i++){ //n
for(var j=i+1;j<arr.length;j++){ // n*n
if(arr[i]==arr[j]){ // n*n
arr.splice(j,1) // n*n*n
}
}
}
複製代碼
T(n)=n^3+2n^2+n=T(O(n^3));
複製代碼
該算法時間複雜度爲
O(n^3);
複製代碼
O(n)表示了算法的複雜程度,可是並不表明複雜程度越大的算法,消耗時間越長,具體須要根據n的值來判斷。以上面的3種數組去重算法爲例,分析n不一樣的狀況下,不一樣算法的消耗時間。
for(var i = 0,arr =[];i<n;i++){
arr[arr.length]=parseInt(Math.random()*n);
}
function fn1(arr){
let newArr = [] // 1
arr.sort((a,b)=>{
return a-b
}) // O(n)=n^2
arr.forEach((val,index)=>{ // n
if(val != arr[index+1]){ // 1
newArr.push(val) // 1
}
})
return newArr //1
}
function fn2(arr){
for(var i=0;i<arr.length;i++){ //n
for(var j=i+1;j<arr.length;j++){ // n*n
if(arr[i]==arr[j]){ // n*n
arr.splice(j,1) // n*n*n
}
}
}
}
var newArr = new Set(arr)
console.time("newArr");
new Set(arr);
console.timeEnd("newArr");
console.time("fn1");
fn1(arr);
console.timeEnd("fn1");
console.time("fn2");
fn2(arr);
console.timeEnd("fn2");
複製代碼
n=500
n=1000
n=2000
n=5000
n=10000
綜合以上的對比結果能夠得知,當n<1000時,算法3的時間小於算法2的時間,儘管算法2的時間複雜度小於算法3。
當一個算法當T(n)能夠獲知時,能夠經過對比T(n)來決定使用哪一種算法。
綜合上文大O階表示法的推導規則,當n的數值絕對大時,低階表達式和常數的影響微乎其微,而上述的應用也驗證了這一觀點,當n超過1000時,O(n^3)的算法耗時大大超過O(n^2)的算法。
所以,當算法應用的狀況較爲複雜時,利用時間複雜度——O(n)來判斷是行之有效的方法。時間複雜度能夠是評價一個算法的相對條件,但不是絕對條件。