数据结构&算法模块总结
- (1)复杂度分析原理与方法
- (2)数组与链表原理和使用场景讲解
- (3)栈原理与应用场景讲解
- (4)队列原理与应用场景讲解
- (5)递归原理与虚拟机栈场景应用
- (6)二分查找及其应用场景
- (7) Redis有序集合跳表实现原理
- (8) 散列表(Hash Table)原理和业界应用场景
1.复杂度分析原理
-
事后统计:直接把代码跑⼀遍,通过统计、监控,就能得到算法执⾏的时间和占⽤的内存⼤⼩。但这种方式有很大的弊端:
-
测试结果⾮常依赖测试环境:例如Intel Core i9处理器和Intel Core i3处处理同样的代码可能效率不同
-
测试结果受数据规模的影响很⼤: 例如 对于⼩规模的数据排序,插⼊排序可能反倒会⽐快速排序要 快 !
-
-
事前统计:比如根据一次IO时间= 磁盘轴旋转时间(旋转延迟)+磁盘臂移动时间(寻道时间)+数据传输时间,大约0.0125s(1/80s)。所以我们可以利用这个值估算算法可能执行的IO次数,进而估算时间。
![](https://img-blog.csdnimg.cn/20210905153820932.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAQnJv5aSn6KGo5ZOl,size_13,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20210905153820894.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAQnJv5aSn6KGo5ZOl,size_20,color_FFFFFF,t_70,g_se,x_16)
//T(n) = O(2n+2)
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
int cal(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
}
2.时间复杂度分析方法
(1)只关注循环执⾏次数最多的⼀段代码
如T(n) = O(2n+2),只需要估算为O(n)
(2)加法法则:总复杂度等于量级最⼤的那段代码的复杂度
如main函数中只两个循环,第一个为O(f(n)),第二个为O(g(n)),则
那么T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).
(3)乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
【⼏种常⻅时间复杂度实例分析】
①O(1)
②O(m+n)、O(m*n)
③O(logn)、O(nlogn):代码如下:
int i=1;
while (i <= n) {
i = i * 2;
}
从代码中可以看出,变量i的值从1开始取,每循环⼀次就乘以2。x=logn,所以,这段代码的时间复杂度就是O(log n)。
3. 最好、最坏、平均、均摊时间复杂度
(1)最好、最坏时间复杂度
// n表示数组array的⻓度
// 任务:查找数组中是否存在目标元素,若存在找出位置
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;
}
- 顾名思义,最好情况时间复杂度就是,在最理想的情况下,执⾏这段代码的时间复杂度。如:要查找的变量x正好是数组的第⼀个元素。
- 最坏情况时间复杂度就是,在最糟糕的情况下,执⾏这段代码的时间复杂度。如:数组中没有要查找的变量x,我们需要把整个数组都遍历⼀遍才⾏。
(2)平均复杂度
根据上面的代码:要查找的变量x在数组中的位置,有n+1种情况:在数组的0~n-1位置中和不在数组中。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数的平均值,即:
但是真实情况下,每种情况出现的概率并不是⼀样的。j可能要查找的变量x,要么在数组⾥,要么就不在数组⾥。
我们假设在数组中与不在数组中的概率都为1/2。另外,要查找的数据出现在0~n-1这n个位置的概率也是⼀样的,为1/n。所以,根据概率乘法法则,要查找的数据出现在0~n-1中任意位置的概率就是1/(2n)。
这个值就是概率论中的加权平均值,也叫作期望值,所以平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。
引⼊概率之后,前⾯那段代码的加权平均值为(3n+1)/4。⽤⼤O表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂度仍然是O(n)。
(3)均摊复杂度
// array表示⼀个⻓度为n的数组
// 代码中的array.length就等于n
int[] array = new int[n];
int count = 0;
/*这段代码实现了⼀个往数组中插⼊数据的功能。当数组满了之后,也就是代码中的
count ==array.length时,我们⽤for循环遍历数组求和,并清空数组,
将求和之后的sum值放到数组的第⼀个位置,然后再将新的数据插⼊。
但如果数组⼀开始就有空闲空间,则直接将数据插⼊数组。*/
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;
}
最理想的情况下,数组中有空闲空间,我们只需要将数据插⼊到数组下标为count的位置就可以了,所以最好情况时间复杂度为O(1)。最坏的情况下,数组中没有空闲空间了,我们需要先做⼀次数组的遍历求和,然后再将数据插⼊,所以最坏情况时间复杂度为O(n)。
假设数组的⻓度是n,根据数据插⼊的位置的不同,我们可以分为n种情况,每种情况的时间复杂度是O(1)。除此之外,还有⼀种“额外”的情况,就是在数组没有空闲空间时插⼊⼀个数据,这个时候的时间复杂度是O(n)。⽽且,这n+1种情况发⽣的概率⼀样,都是1/(n+1)。所以,根据加权平均的计算⽅法,我们求得的平均时间复杂度就是:
均摊与平均区别:
之前的find()函数在极端情况下,复杂度才为O(1)。但insert()在⼤部分情况下,时间复杂度都为O(1)。只有个别情况下,复杂度才⽐较⾼,为O(n)。这是insert()第⼀个区别于find()的地⽅。
对于insert()函数来说,O(1)时间复杂度的插⼊和O(n)时间复杂度的插⼊,出现的频率是⾮常
有规律的,⽽且有⼀定的前后时序关系,⼀般都是⼀个O(n)插⼊之后,紧跟着n-1个O(1)的插⼊操作,循环往复。
所以,针对这样⼀种特殊场景的复杂度分析,我们并不需要像之前讲平均复杂度分析⽅法那样,找出所有的输⼊情况及相应的发⽣概率,然后再计算加权平均值。
每⼀次O(n)的插⼊操作,都会跟着n-1次O(1)的插⼊操作,所以把耗时多的那次操作均摊到接下来的n-1次耗时少的操作上,均摊下来,这⼀组连续的操作的均摊时间复杂度就是O(1)。这就是均摊分析的⼤致思路。你都理解了吗?
其实我个⼈认为,均摊时间复杂度就是⼀种特殊的平均时间复杂度,我们没必要花太多精⼒去区分它们。你最应该掌握的是它的分析⽅法,摊还分析。⾄于分析出来的结果是叫平均还是叫均摊,这只是个说法,并不重要。