三界之内没有解决不了的bug 宣君.|

宣君

园龄:7年7个月粉丝:18关注:2

📂算法
🔖算法
2022-07-19 22:36阅读: 122评论: 0推荐: 1

时间复杂度和空间复杂度

时间复杂度和空间复杂度

评价一个算法的优劣,往往会从时间复杂度和空间复杂度两个维度来评价。执行时间越短,占用空间越小的算法,被视为最优算法。
——《数据结构和算法之美》

概念

● 时间复杂度全称渐进式时间复杂度
● 空间复杂度全称渐进式空间复杂度

为什么需要对算法进行评价呢?一个算法直接执行一遍,它的消耗时间和占用空间就一目了然了。这个策略也可以,它有个专业名称——事后统计法,但是它有着很大局限性:

● 依赖运行环境和机器性能
● 依赖测试数据的规模

因此,需要在算法设计之初,就要对它进行一个大概的评估。当算法开发完成后,可以执行算法,来验证评估。

大 O 复杂度表示法

算法的执行效率,可以简单的描述为执行时间。算法没有运行,如何估计它的运行时间呢?来分析下面一段代码:

var dic = new Dictionary<int, int>(); // 1行
var n = nums.Length; // 2行
for (int i = 0; i < n; i++) // 3行
{ // 4行
res = nums[i]; // 5行
dic.Add(nums[i], i); // 6行
} // 7行

这段代码一看,消耗时间的地方肯定是这个for循环。当 n 很大时,第1,2行代码消耗的时间可以忽略不计了,因此在算法分析过程里,只考虑消耗最大的那段代码。假设第4、5行代码执行所需时间都是一个单位时间(unit_time),那4、5行代码共需 2unitTime,整个for循环执行完的时间 T(n) 大概是 n2unitTime,可以看出,T(n)与n成正比,用一个公式记作 T(n)=O(f(n))。这个公式中 T(n) 表示代码执行时间总和,n表示数据规模,f(n) 表示每行代码执行时间,O 表示 T(n)f(n) 的一个正比函数。

时间复杂度

分析方法

1. 只关注执行次数最多的代码段

例如上面示例中的代码,代码执行次数最多的是for循环这一段。第4、5行代码共执行n次,执行时间T(n)=O(f(n)),因此时间复杂度记作O(n)

2. 加法法则

有下面一段代码

//第一段

for (int i = 0; i < n; i++)
{
res = nums[i];
dic.Add(nums[i], i);
}

//第二段

for (int k = 0; i < m; i++)
{
res = nums[k];
dic.Add(nums[k], k);
}

这段代码有2小段,先看第一段,假设单行代码执行所需时间为 unit_time,那么第一段代码所需时间就是 n2unitTime,记作T1(n)=O(f(n))。同理第二段代码消耗时间为T2(n)=O(g(n)),整段代码所需全部时间为T(n)=T1(n)+T2(n)

根据第一条规则,取时间数量级最大的作为总的时间复杂度,那么T(n)=Max((O(f(n))),(O(g(n))))=O(n)

3. 乘法规则

看下面代码:

for (int i = 0; i < n; i++)
{
for (int k = 0; i < n; i++)
{
res = nums[k];
dic.Add(nums[k], k);
}
}

外层循环的消耗时间为T1(n),内层循环消耗时间为T2(n),总消耗时间为 T(n)=T1(n)T2(n)=O(nn)=O(n2)


常见时间复杂度

● 常量阶 O(1)
● 线性阶 O(n)
● 对数阶 O(logn)
● 线性对数阶 O(nlogn)
● 指数阶 O(2n)
● 阶乘阶 O(n!)
● 平方阶 O(n2) 立方阶 O(n3) ……m次方阶 O(nm)

O(1)

是一种时间复杂度表示方式,并非指执行了一行代码,例如下面的代码,及时它执行了100次,1千次,时间复杂度仍是O(1)

int i=0;
int j=i+10;

O(n)

这种时间复杂度非常常见,例如for循环

for(int i=0; i<n;i++){
int k=i;
}

O(logn)

int i=1;
while(i<n){
i*=2;
}

这段代码的终止条件是 2i>=n ,那么 i=log2n=logn ,因此时间复杂度为O(logn)

最好、最坏、均摊、平均时间复杂度

● 最好情况时间复杂度
● 最坏情况时间复杂度
● 均摊时间复杂度
● 平均情况时间复杂度

最好、最好情况时间复杂度,很好理解,也很常见。例如使用循环在一个数组中找到某个元素,最好的情况是,这个元素是第一个元素,那么就是最好情况复杂度 O(1),如果这个元素是最后一个,那么就需要遍历整个数组,才能找到,其时间复杂度就是 O(n)

空间复杂度

空间复杂度分析比时间复杂度分析要简单很多,例如以下代码:

int i=0;
int[] arr=new int[n];
for (int i = 0; i < n; i++)
{
res = nums[k];
}

这段代码中,第1行声明一个变量,它是常量阶 O(1) ,和数据规模无关。第2行声明一个长度为 n 的数组,它占用空间取决于数据规模n,因此它的空间复杂度为 O(n)。for循环中只是进行副职,并没有开辟新的空间。因此这段代码空间复杂度为 O(n)

在看一段代码:

for (int i = 0; i < n; i++)
{
for (int k = 0; i < n; i++)
{
int a=i;
}
}

这段代码 int a=i 是常量阶,但是它处于2层循环内,相当于声明了nn=n2 次变量,因此它的空间复杂度为 O(n2).

最好、最坏时间复杂度是个极端情况,出现的概率不大。为了更好的表示大多数情况下的时间复杂度,引入平均情况时间复杂度概念。

还是假设在一个数组中查找某个元素,这个元素可能在数组中或不在数组中、在与不在概率都是12 ,这个元素可能出现在索引0~n-1任意位置的概率为 1n ,根据概率乘法得到综合概率为 12n 。那么目标元素出现在索引0位置的时间复杂度为 112n ,出现在索引1位置的时间复杂度为 212n,以此类推得到如下公式:

112n+212n+312n++n12n=3n+14 .

这个值就是概率论中的平均加权值,也称作期望值。在去掉系数、常量、低阶量之后,得到的平均情况时间复杂度为 O(n)

均摊时间复杂度。这个概念跟平均时间复杂度容易混淆。有这样一个场景,集合(数组)中的 insert() 方法,待插入的数据可能会插入到任意一个索引位置,且每个索引位置概率都是 1n+1 。根据平均时间复杂度可得:

11n+1+21n+1+31n+1++n1n+1=O(1) .

由此可看出数组的 insert() 操作时间复杂度为 O(1),而 find() 操作时间复杂度为 O(n)。这也是这两个函数的一个不同点。当然也存在特殊情况,当数组满了之后,此时插入数据,数组需要进行扩容,此时时间复杂度为 O(n) (注意,这里的数组是指可变长数组,如果是定长数组,满了之后无法插入) 。当数组扩容后,再次插入数据时,时间复杂度又是O(1),每到数组满了之后插入数据,时间复杂度又变成O(n),如此循环往复。有n-1个O(1),1个O(n),然后接着是O(1)O(n)……那么这一个O(n)就可以均摊到n-1个O(1)上去,最终时间复杂度还是O(1)。这块概念比较难理解,可以将均摊时间复杂度理解为特殊的平均情况时间复杂度。不用过度纠结。

posted @   宣君  阅读(122)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起