为了账号安全,请及时绑定邮箱和手机立即绑定

前端面试总结(算法篇)- 2

问题:如何计算不规则容器积水量?
这是一道 Twitter 算法面试题,题目很好理解,就是求蓝色格子的数量:

未积水容器
积水容器

我们先用最原始的方法来做,算每一列可蓄水量的和,而积水的充分必要条件是两边高中间低,那么每一列可蓄水的量是多少呢?

我们假定该侧的左挡板的高度为 L(i),自身为M(i),右侧为R(i),蓄水量为V(i);

//先写伪代码
IF Min(L(i),M(i),R(i)) === M(i)
V(i) = Min(L(i),R(i)) - M(i);
Total = ∑(n,i=0) V(i); //累加得出答案

举个例子:[4,2,8] 这个三列的容器里 Total = V(0) + V(1) + V(2) = 0 + 2 + 0 = 2;看起来没什么问题。但是如果是 [5,3,2,4,6] 呢?明显按照我们公式是错的,Total = 0 + 0 + 1 + 0 + 0 = 1;正确的应该是 Total = 0 + 2 +3 + 1 + 0 = 6;

也就说该列左右挡板的高度,并不是由相邻的列决定,而是一直向左右寻找到其左边列的最高项与右边列的最高项,由此我们可以得到:

function Sediment() {
    let total = 0;
    array.forEach((val, index, arr) => {
        let L = ~~Math.max(...arr.slice(0, index)), //...ES6的函数解构,因为Math.max(a,b,c)的传参结构
        R = ~~Math.max(...arr.slice(index + 1)), // ~~两次位预算,目的是把[]转成0,其他的整数不变
        tag = Math.min(L, R, val);
        if (tag === val) {
            total += Math.min(L, R) - val;
          }
    });
    console.log(total);
}

const array = [2, 5, 1, 2, 3, 4, 7, 2];
Sediment(array); //10

当你跟我一样做完满意得交给面试官时,他来一句:“你这个时间复杂度是多少?空间复杂度呢?最优解了吗?”是不是就懵了。不慌,现在我们就对这道题进行拓展。

拓展

衡量一个算法的优劣主要有两个标准,时间复制度 T(n) = O(f(n)),空间复杂度 S(n)=O(f(n)) ,O 表示当n无穷大时的极限,它们定性的描述了该算法的运行时间与运行时所占的内存空间。统称为算法复杂度

1. 如何计算时间复杂度

根据两点:
1. 执行的基本语句数量(K)
2. 重复执行的次数(n)

举个例子:

for (let index = 0; index < array.length; index++) {
        const element = array[index];
        console.log(element);
     };

在循环内,我们有两条基本语句,即 K = 2;循环了 array.length 次(假设为n),那么整个函数我们执行了:T(n) = K*n = 2n 次;

假如我们里面是执行了三条基本语句呢?那么 T(n) = 3n 次,系数是大了,次数是多了,就能说明执行两次的就比执行三次的算法高明吗?答案显然易见,都只是一个线性的循环而已,没有谁优谁劣。

那我们如何给它做定性的比较呢?对高等数学还有印象不,我记得一开始就是学的就是极限,当n无穷大时,多执行一次不并影响最后的结果,也就是说我们可以通过对其求极限来进行定性。这时候我们用 O() 来表示,它们两都是 T(n) = O(n) ;属于同等级的线性阶O(n)。

按数量级递增排列,常见的时间复杂度有:

等级 例子
1 常数阶O(1) ()=>{console.log('没有循环');}
2 对数阶O(log2N) 二分法 每次处理一半
3 线性阶O(n) 任何单循环
4 线性对数阶O(nlog2n) 单循环嵌套二分法
5 平方阶O(n^2) 循环套循环
6 k次方阶O(n^k) 循环套循环 K层
7 指数阶O(2^n) 递归

上述时间复杂度不断增大,算法的执行效率越低,整个算法的执行时间与基本操作重复执行的次数成正比。

看看下面图例,了解下各阶的差异,即在某个阶段算法的优劣。

各阶对比图
各阶对比二

2. 如何计算空间复杂度

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。一般的递归算法的空间复杂度为O(n),因为每次递归都要存储返回信息。

const array = [1,2,3,4]
for (let i = 0; i < array.length; i++) {
   const element = array[i];
   console.log(i)
}

整个循环都在这个数组中进行,也没有开辟临时变量,所以S(n) 等于数组的长度3,也就是说它为线性阶 S(n) = O(n);

Review

了解完算法复杂度的概念后,我们再看看刚刚我们的解法到底优劣如何,观察可知:

array.forEach((val, index, arr) => { //单循环执行了n次
    let L = ~~Math.max(...arr.slice(0, index)), 
      R = ~~Math.max(...arr.slice(index + 1)), 
      tag = Math.min(L, R, val);
    if (tag === val) {
       total += Math.min(L, R) - val;
    }
});

1. T(n) = 5n = O(n);
2. S(n) = 3*n +1 = O(n)

两个都同为线性阶,看起来不错,但这里是因为我们忽略了 Math.max() 与 Math.min() 两个的算法复杂度(交给js内部执行了),在 [2, 5, 1, 2, 3, 4, 7, 2] 的模型里,很明显我们没有必要在 [5,1,2,3,4,7] 这段中不停的向两边查找,这就是我们可以优化的一个点了。

至于如何优化,这个就交给大家自己去发挥了(或者下次我心血来潮补上吧)~

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JS工程师
手记
粉丝
15
获赞与收藏
113

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消