少年留步!!!!!
我看你骨骼精奇,这本武林秘籍就便宜卖你了。
《前言》 欲练此功 必先自宫
。
。
。
。
。
。
。
。
。
。
。
如不自宫,也能成功
《第一章 - 时间和空间复杂度》
内功心法的基础,熟练运算复杂度才能更好的施展算法
可能有很多人还不知道怎么算复杂度,只是依稀听过这个词,或者死记硬背那些排序算法的时间复杂度 但是没有任何作用只是为了应付面试,接下来跟随这本秘籍打通任督二脉。
复杂度
    数据结构和算法解决是 “如何让计算机更快时间、更省空间的解决问题”,因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能,分别用时间复杂度和空间复杂度两个概念来描述性能问题,二者统称为复杂度。复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系。
    算法的执行时间与每行代码的执行次数成正比, 用 T(n) = O(f(n)) 表示,其中 T(n) 表示算法执行总时间,f(n) 表示每行代码执行总次数,而 n 往往表示数据的规模。这就是大 O 时间复杂度表示法。
时间复杂度
T(n) -> O(n) 的推导
我们先简单的举一个最简单的🌰
function helloWorld() {
console.log("Hello, World!"); // 执行 1 次
}
T(n) = 1
一层循环的🌰
function traverse (arr) {
var length = arr.length;
for (var i = 0; i < length; i++) {
console.log(arr[i]);
}
}
    一个简单的打印数组中每项的函数,在这个函数中
var length = arr.length;只执行了一次 计数为 1console.log(arr[i]);循环执行了n次,循环体毫无疑问执行了n次,计数为 nfor (var i = 0; i < length; i++)拆分来看var i = 0;执行了1次,i++执行了n次,i < length执行了n + 1次 ,总的来看 执行了 n + 1 次
所以T(n) = 1 + n + (n + 1) = 2n + 2
同样的思路,下一个二维数组的🌰
function traverse (arr) {
var length = arr.length;
for (var i = 0; i < length; i++) {
var insideLen = arr[i].length;
for (var j = 0; j < insideLen; j++) {
console.log(arr[i][j]);
}
}
}
    相信骨骼精奇的孩子到这里应该会举一反三了,不会的证明你没有慧根换门武功吧(哈哈哈哈 逗你的)。 T(n) = 1 + (n + 1) + n (1 + (n + 1) + n ) = 2n2 + 3n + 2
    由于 时间复杂度 描述的是算法执行时间与数据规模的 增长变化趋势,所以 常量、低阶、系数 实际上对这种增长趋势不产生决定性影响,所以在做时间复杂度分析时 忽略 这些项。
    所以,上面
- 🌰1 的时间复杂度为 T(n) = O(1)
- 🌰2 的时间复杂度为 T(n) = O(n)
- 🌰3 的时间复杂度为 T(n) = O(n2)。
这就是 T(n) -> O(n) 的推导过程 。
    当然还有这种O(logn)的这种,是因为 i 在以 i=i*2的规则递增了 x 次之后,i<n 开始不成立 比如
while (i <= n) {
i = i * 2;
}
    那么此时我们要计算的其实就是这样一个数学方程:
涉及到对数的时间复杂度,底数和系数都是要被简化掉的。
所以 O(n) = logn
    常见的时间复杂度按照从小到大的顺序排列,有以下几种:
时间复杂度分类
时间复杂度可以分为:
- 最好情况时间复杂度(best case time complexity):在最理想的情况下,执行这段代码的时间复杂度。
- 最坏情况时间复杂度(worst case time complexity):在最糟糕的情况下,执行这段代码的时间复杂度。
- 平均情况时间复杂度(average case time complexity),用代码在所有情况下执行的次数的加权平均值表示。也叫 加权平均时间复杂度 或者 期望时间复杂度。
- 均摊时间复杂度(amortized time complexity): 在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。
🌰来了
简单的find
function find(array, num) {
var index = -1;
var len = array.length;
for (var i = 0; i < len; i++) {
if (array[i] == num) {
index = i;
break;
}
}
return index;
}
    find 函数实现的功能是在一个数组中找到值等于 num 的项,并返回索引值,如果没找到就返回 -1 。
最好情况时间复杂度,最坏情况时间复杂度
    如果数组中第一个值就等于 num,那么时间复杂度为 O(1),如果数组中不存在变量 num,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。所以,不同的情况下,这段代码的时间复杂度是不一样的。
    所以上面代码的 最好情况时间复杂度为 O(1),最坏情况时间复杂度为 O(n)。
平均情况时间复杂度
    要查找的变量 num 在数组中的位置,有 n+1 种情况:在数组的 0~n-1 位置中和不在数组中。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以 n+1,就可以得到需要遍历的元素个数的平均值,
省略掉系数、低阶、常量,所以,这个公式简化之后,得到的平均时间复杂度就是 O(n)。
    有的人可能会说,你没有算 num 有么有在数组的概率,你加上概率化简之后 平均时间复杂度还是 O(n)。
均摊时间复杂度
    均摊时间复杂度就是一种特殊的平均时间复杂度 (应用场景非常特殊)。
空间复杂度
    时间复杂度的全称是 渐进时间复杂度,表示 算法的执行时间与数据规模之间的增长关系 。 类比一下,空间复杂度全称就是 渐进空间复杂度(asymptotic space complexity),表示 **算法的存储空间与数据规模之间的增长关系 **。
🌰
function traverse(arr) {
const len = arr.length;
for(let i=0; i<len; i++) {
console.log(arr[i]);
}
}
    在 traverse 中,占用空间的有以下变量:arr len i 。
后面尽做了很多次循环,但是这些都是时间上的开销。循环体在执行时,并没有开辟新的内存空间。因此,整个 traverse 函数对内存的占用量是恒定的,它对应的空间复杂度就是 O(1)。
另一个🌰
    此时我想要初始化一个规模为 n 的数组,并且要求这个数组的每个元素的值与其索引始终是相等关系。
function init(n) {
let arr = [];
for(let i=0;i<n;i++) {
arr[i] = i;
}
return arr
}
    在这个 init 中,涉及到的占用内存的变量有以下几个:arr n i 。
注意这里这个 arr,它并不是一个一成不变的数组。arr最终的大小是由输入的 n 的大小决定的,它会随着 n 的增大而增大、呈一个线性关系。因此这个算法的空间复杂度就是 O(n)。
    我们常见的空间复杂度就是 O(1)、O(n)、O(n2),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。