算法的定义是这样的:解题方案的准确而完善的描述,是一系列解决问题的清晰指令,就是解决一个问题的完整性描述。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)来判断是行之有效的方法。时间复杂度能够是评价一个算法的相对条件,但不是绝对条件。