算法复杂度分析
之前写过一篇文章“不要陷入技术的魔咒”,然而最近又迷上了写拓展,我TM!!!管不住自己,之前我哥推荐了很好的git管理工具,很多东西都用那个托管了,真的很少在写博客了,直话直说我c++这还是学的非常差的,当然后面也没有好好学过,自从开始写一些脚本开始,我更加意识到了自己的软肋,虽然我浑身都是软肋,陈sir,每次气的每次都说我该回炉重造。的确我非常有必要回炉重造了,客观来看我底子的确是弱得很,以后尽量希望明确学习方向,不在对一些应用场景少
、难以产生效益
、无法应用到实际项目
、瞎折腾
的技术进行钻研。重最简单的开始学习吧,之前,这个博客是我建的第一个博客,之前也想换换托管方式,打算在阿里云上托管的,但转念一想。又觉得不是很有必要,之前也试过阿里云oss。不过我感觉git+github pages+hexo这个已经完全够用了。也懒得再折腾。
其次c++已经鸽了有大半年了,所以有必要重hello world,重新开始学的必要,我感觉,
算法复杂度以及渐进复杂度
一个问题往往可以以效率不同的算法来解决,当然在数据较少的时候,各种算法之间的差异也许微不足道,但当数据量成倍增加时,你就会发现不同算法之间的差异非常非常明显,如果你关注LeetCode你会发现不同的算法在占用和时间上会有非常明显的差别,比如一道题,你用双指针可能只需要4ms,其他的算法可能是他的一百多倍。为了比较算法的效率,两个名字很复杂的老外,引入了一种称为计算复杂度的标准来衡量算法。计算复杂度表示一种算法需要付出多大的成本和努力,当然成本在很多不同的地方有自己的衡量标准,我们常说的衡量效率就是时间和空间,当然远远不止这两种,其他,其他我也不知道啊!!!当然时间因数往往比空间因素重要的多,我们往往关注的是数据处理的时间,而不是空间,毕竟现在在技术层面,数据储存完全不是人们担心的地方,而时间和效率往往决定这一个程序的好坏。印度是全世界最大的代码输出国,微软ceo也是印度人,印度人写代码哪管那么多啊,所以是现在硬件的发展完全可以弥补算法层面的不住,当然这只是在某些领域,在一些非常需要算法效率的领域,你,如果,不会写算法还是删库趁早跑路吧,我觉得你进公司面试可能面试官就直接吧你pass了
评估算法效率
算法效率的评估并不能以时间来评估,而是采用某种逻辑关系,用文件和数组的尺度n同时间t之间的关系,我们该如何表示不同的t和n之间的关系呢,
假如t和n之间是线性关系
n 2 = 5 n 1 则必有 t 2 = 5 t 1 n2=5n1
则必有t2=5t1
n 2 = 5 n 1 则 必 有 t 2 = 5 t 1
同样
t1=log2(n),t2=log2(2n)则t1=t2+1
$$ {log2}
当然通常情况下。,这些n与t之间的关系复杂的多的多。当数据量非常巨大的时候,计算这些函数才变得比较重要,不会实质上改变函数数量级的应该从函数中剔除,emmmmm就相当于数学的极限思想。比如说f(n)=n+10000,在n足够大时我们就没有必要考虑常数项。
## 大O表示法
这大概是最长用来表示渐进复杂度(评估函数的增长率)的表示方法了,是Paul Bachmann引入的
具体我就不说了详情请看百科
[请点我!!!!](https://baike.baidu.com/item/大O表示法/1851162?fr=aladdin)
## 常见的时间复杂度量级
我们先从常见的时间复杂度量级进行大O的理解:
- 常数阶O(1)
- 线性阶O(n)
- 平方阶O(n²)
- 对数阶O(logn)
- 线性对数阶O(nlogn)
![](C:\Users\Administrator.USER-20181115IF\Desktop\b&bo=bwNwAm8DcAIRCT4!&rf=viewer_4)
### O(1)
![](http://m.qpic.cn/psc?/V13PUOHK44wDVX/4pNOqgOvBLvj4yTC9qc55aKetXJx4AS93ajzRKFTHJhI3psuRi3P2h0bUOiDmn2n*47302gYOXxReWb7HbXD0ZHP7hGQ2mWcuMH1UcSxSLw!/b&bo=tgMTArYDEwICCS0!&rf=viewer_4)
[^]: 图片转载至掘金
无论代码执行了多少行,其他区域不会影响到操作,这个代码的时间复杂度都是O(1)
1 2 3 4 5 6 void swapTwoInts (int &a, int &b) { int temp = a; a = b; b = temp; }
### O(n)
![](http://m.qpic.cn/psc?/V13PUOHK44wDVX/4pNOqgOvBLvj4yTC9qc55cUDXjmkugsoYy7opoXUiCvLNRMvYGKmzn6CAQkKpXsRsHKsHMDKh9xhB.2TynbNmTOKZTW.FgHzh7gugVjXLh8!/b&bo=tgMTArYDEwICCS0!&rf=viewer_4)
在下面这段代码,for循环里面的代码会执行 n 遍,因此它消耗的时间是随着 n 的变化而变化的,因此可以用O(n)来表示它的时间复杂度。
1 2 3 4 5 6 7 8 int sum ( int n ) { int ret = 0 ; for ( int i = 0 ; i <= n ; i ++){ ret += i; } return ret; }
特别一提的是 c * O(n) 中的 c 可能小于 1 ,比如下面这段代码:
1 2 3 4 5 6 7 void reverse ( string &s ) { int n = s.size (); for (int i = 0 ; i < n/2 ; i++){ swap ( s[i] , s[n-1 -i]); } }
### O(n²)
![](https://user-gold-cdn.xitu.io/2018/12/13/167a509fc3d8fd52?imageslim)
当存在双重循环的时候,即把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。
1 2 3 4 5 6 7 8 9 10 11 void selectionSort (int arr[],int n) { for (int i = 0 ; i < n ; i++){ int minIndex = i; for (int j = i + 1 ; j < n ; j++ ) if (arr[j] < arr[minIndex]) minIndex = j; swap ( arr[i], arr[minIndex]); } }
这里简单的推导一下
- 当 i = 0 时,第二重循环需要运行 (n - 1) 次
- 当 i = 1 时,第二重循环需要运行 (n - 2) 次
- 。。。。。。
不难得到公式:
1 2 3 4 5 (n - 1 ) + (n - 2 ) + (n - 3 ) + ... + 0 = (0 + n - 1 ) * n / 2 = O (n ^2 )
当然并不是所有的双重循环都是 O(n²),比如下面这段输出 30n 次 `youmingsama:)`的代码。
1 2 3 4 5 6 void printInformation (int n ){ for (int i = 1 ; i <= n ; i++) for (int j = 1 ; j <= 30 ; j ++) cout<< "youmingsama:)"<< endl; }
### O(logn)
![](http://m.qpic.cn/psc?/V13PUOHK44wDVX/4pNOqgOvBLvj4yTC9qc55SAtAYyoKoFH9LThDmv5KBqEilX7kZS5*YENt9eb53UUj1tsZjOiqyATMn2d7fz6GmBNZqXgsYxamz8WIaLU6og!/b&bo=vQMcAr0DHAICGT0!&rf=viewer_4)
1 2 3 4 5 6 7 8 9 10 11 int binarySearch ( int arr[], int n , int target) { int l = 0 , r = n - 1 ; while ( l <= r) { int mid = l + (r - l) / 2 ; if (arr[mid] == target) return mid; if (arr[mid] > target ) r = mid - 1 ; else l = mid + 1 ; } return -1 ; }
在二分查找法的代码中,通过while循环,成 2 倍数的缩减搜索范围,也就是说需要经过 log2^n 次即可跳出循环。
同样的还有下面两段代码也是 O(logn) 级别的时间复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 string intToString ( int num ) { string s = "" ; while (num ){ s += '0' + num%10 ; num /= 10 ; } reverse(s) return s; } void hello (int n ) { for ( int sz = 1 ; sz < n ; sz += sz) for (int i = 1 ; i < n; i++) cout << "youmingsama:)" << endl ; }
### O(nlogn)
将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logn),也就是了O(nlogn)。
1 2 3 4 5 6 7 8 void hello () { for ( m = 1 ; m < n ; m++){ i = 1 ; while ( i < n ){ i = i * 2 ; } } }
## 最好,平均和最坏情况
### 先举个栗子
分析以下这段代码的时间复杂度
1 2 3 4 5 6 7 8 9 int find (int [] array , int n, int x) { int i = 0 ; int pos = -1 ; for (; i < n; ++i) { if (array [i] == x) pos = i; } return pos; }
#### 分析
- 功能:在一个无序数组array中,查找变量x出现的位置
- 时间复杂度:`O(n)`, n表示数组的长度
更为优化的方式
1 2 3 4 5 6 7 8 9 10 11 12 int find (int [] array , int n, int x) { int i = 0 ; int pos = -1 ; for (; i < n; ++i) { if (array [i] == x) { pos = i; break ; } } return pos; }
> 时间复杂度: 不一定是O(n)了,不同情况下这段代码的时间复杂度是不同的。
### 引入概念
- 最好情况时间复杂度:在最理想情况下,执行代码的时间复杂度
- 最坏情况时间复杂度:在最糟糕情况下,执行代码的时间复杂度
- 平均情况时间复杂度:最好情况和最坏情况都是属于极端情况,发生的概率并不大。需要表示平均情况下的复杂度
### 如何分析平均情况时间复杂度
以上面的例子为例:查找x在数组中的位置,有`n+1`种情况,在数组`0~n-1`的位置上和不在数组中。
将每种情况下,查找需要遍历的元素个数相加,再除以n+1种情况,就可得到需要遍历的元素个数的平均值
`(1+2+3+...+n+n)/n+1 = n (n+3) /2(n+1)`
省略掉系数、低阶、常量后得到平均时间复杂度为`O(n)`
#### 存在问题
在以上的n+1种情况中,未考虑x在每种情况下出现的概率。
现在假设,x在数组中与x不在数组中的概率各为`1/2`;
要查找的x出现在`0~n-1`这n个位置的可能性是相同的,即`1/n`;
那么,要查找的x出现在`0~n-1`中任意位置的概率为 `1/2*1/n = 1/(2n)`
将每种情况发生的概率考虑进去后,平均时间复杂度计算过程变成:
1 (1+2+3+...+n+n)*(1/(2n))
作期望值。因而平均时间复杂度的全称为**加权平均时间复杂度**或**期望时间复杂度**。
> 注:很多时候,我们只使用一个复杂度就可以满足要求。只有同一块代码在不同情况下,时间复杂度有量级的差距,才会使用以上3种复杂度的表示法来区分。
### 均摊时间复杂度、摊还分析(摊销分析)
举栗子说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int [] array = new int [n];int count = 0 ;void insert (int val) { if (count == array.length) { int sum = 0 ; for (int i = 0 ; i < array.length; ++i) { sum = sum + array[i]; } array[0 ] = sum; count = 1 ; } array[count] = val; ++count; }
#### 分析
- 功能:实现了往数组中插入数据的功能。
- 具体:
1 . 当数组满了`count == array.length`,就用for循环遍历求和,将求得的和放在数组的第一个位置,并清空数组其余元素;然后再插入新的元素。
2 . 当数组一开始就有空闲,则直接将数据插入数组。
- 复杂度分析:
1 . 最好情况:数组中有空闲,直接将数据插入到`count`的位置,为`O(1)`
2 . 最坏情况:数组没有空闲空间,需要先做一个遍历求和,再作插入。所以复杂度为`O(n)`
3 . 平均时间复杂度:`O(1)`
##### 平均时间复杂度如何得到
总共`n+1`种情况,前`n`中情况每种时间复杂度都为`O(1)`,后一种情况时间复杂度为`O(n)`;`n+1`种情况发生的概率一样,为`1/(n+1)`;根据加权平均计算方法:
> 1 1 *1 /(n+1 ) + 1 *1 /(n+1 ) + ... + n*1 /(n+1 ) = O (1 )
#### 摊销时间复杂度(amortized time complexity)
分析发现:
1 . `insert()`函数在**大部分情况**下,时间复杂度都为`O(1)`;**极个别情况**下,复杂度才高,为`O(n)`
2 . `insert()`函数中,`O(1)`时间复杂度的插入和`O(n)`时间复杂度的插入,**出现频率很有规律**。存在前后时序关系,一般一个`O(n)`插入之后,紧跟着`n-1`个`O(1)`的插入操作,循环往复。
##### 针对这种场景,引入均摊分析法
大致思路:每一次 `O(n)` 的插入操作,都会跟着 `n-1` 次 `O(1)` 的插入操作,所以把耗时多的那次操作均摊到接下来的 `n-1` 次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是 `O(1)`。
#### 总结
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。
而且,**在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度**。
> 均摊时间复杂度就是一种特殊的平均时间复杂度,没必要花太多精力去区分它们。
## NP完整性
决定性算法,,,,,,emmmmmmm这个好难,看不太懂,暂时就鸽了吧。就酱
![](http://m.qpic.cn/psc?/V13PUOHK0NDZXM/4pNOqgOvBLvj4yTC9qc55fBIxyAZSXpIVMg.7WexfLdGNmW.mlaAcPX7syv*IA5cDPQiD.SX5XbpJ4wbz98KbX5FY5Kp7p0ZtX0VTT5LE1E!/b&bo=LAHUACwB1AACCS0!&rf=viewer_4)