二、复杂度优化

上节内容回顾

上节说到了复杂度,简单介绍了 复杂度时间复杂度空间复杂度,同时举了小例子来说明这几个概念,同时也举例说明了一下优化空间复杂度的一个简单概念。

  • 复杂度: 复杂度是衡量代码运行效率的重要度量因素。换句通俗易懂的话说:其实复杂度就是代码运行时占用的内存的多少,和代码运行时所使用的时间的多少,这个总称为复杂度。复杂度是一个关于输入数据量 n 的函数。 假设你的代码复杂度是 f(n),那么就用个大写字母 O 和括号把 f(n) 括起来就可以了,即 O(f(n))。即O(n)表示复杂度。

  • 空间复杂度: 运行代码时所使用的的计算机内的内存的多少,被称为空间复杂度,用O(n)表示。

  • 时间复杂度: 运行代码时所消耗的时长被称为空间复杂度,也用O(n)表示。

  • 复杂度计算规则:

    • 它与具体的常系数无关,O(n) 和 O(2n) 表示的是同样的复杂度。
    • 复杂度相加的时候,选择高者作为结果,也就是说 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。
    • O(1) 也是表示一个特殊复杂度,即任务与算例个数 n 无关。

复杂度优化

复杂度的优化即代码效率的优化,换句话说就是要将可行解提高到更优解,最终目标是:要采用尽可能低的时间复杂度和空间复杂度,去完成一段代码的开发

如果没有复杂度概念的情况下开发代码,最直接的开发方式就是:假定在不限制时间、也不限制空间的情况下,可以完成某个任务的代码的开发。这就是通常我们所说的暴力解法,更是程序优化的起点。说到优化的起点就不得不举一个例(栗)子来说一下:

在100以内的正整数中,获取能够同时满足下列两个条件的数字:

  • 能够被3整除
  • 除5余2

我能猜到大家的解法就是将这100个数字进行循环一遍,然后判断同时满足以上两个条件的时候则返回数据。这时的时间内复杂度肯定就是O(n)了。你有没有更好的方法呢?(当然我是没有想到更好的办法了啦,哈哈哈哈!!!我懒我笨我不想,但我尽量不妨碍你去想。)

代码如下:

public function test()
{
    $res = [];
    for ($i = 0; $i < 100; $i++) {
        if ((($i%3) == 0) && (($i%5) == 2)) {
            array_push($res, $i);
        }
    }
    return $res;
}

这是一种不计较任何时间复杂度或空间复杂度的、最直观的暴力解法。如果暴力解法复杂度够低,可以接受,那我无话可说。继续使用暴力解法就好,毕竟也不费脑筋,死不了多少脑细胞,保留点黑发对程序员来说还是比较重要的。

言归正传:为了降低复杂度,我们的一个直观的思路是 "梳理程序,看其流程中是否有无效的计算或者无效的存储"

  • 降低时间复杂度常用的方法有递归、二分法、排序算法、动态规划等。

  • 降低空间复杂度的核心思路就是,能用低复杂度的数据结构能解决问题,就千万不要用高复杂度的数据结构。

好了,(敲黑板,重点来了,记一下,记一下哈!!),以上我们大概了解了基本的问题解决思路,那么复杂度优化的思路是什么呢?这也是本节的核心:

  1. 暴力解法: 在没有任何时间、空间约束下,完成代码任务的开发(随心所欲,为所欲为)。

  2. 无效操作剔除处理: 将代码中的无效计算、无效存储剔除,降低时间或空间复杂度(把没用的废物通通排出体外 【这是一句有味道的话🤮】)。

  3. 时空转换: 设计合理数据结构,完成时间复杂度向空间复杂度的转移(这。。这。。这该怎么解释呢?拿内存换时间:增加内存的使用来缩短执行消耗的时间)。

无效操作剔除处理

暴力解法 已经在上面举例说明了,不管三七二十一,第一印象很重中,想到什么什么,接到需求的第一个想法就是你的暴力解法。下面来举例说明一下剔除无效操作的处理,请听题:

假设有任意多张面额为 2 元、3 元、7 元的货币,现要用它们凑出 100 元,求总共有多少种可能性。我的想法是下面的代码:

public function test4()
{
    $count = 0;
    for ($i = 0; $i <= (100 / 7); $i++) {
        for ($j = 0; $j <= (100 / 3); $j++) {
            for ($k = 0; $k <= (100 / 2); $k++) {
                if ((($i * 7) + ($j * 3) + ($k * 2)) == 100) {
                    $count += 1;
                }
            }
        }
    }
    return $count;
}

大家可以想一下这个代码是否还有可优化的可能性呢?(我可能是在废话,没有的话我写出来干啥嘛,脑袋有泡了!😔😔😔!)

上面的代码可以很明显的看到是三层for循环,那么由之前说到的时间复杂度算法可以得出,改代码的时间复杂度为 O(n) * O(n) * O(n) = O(n³)

分析: 下面针对这段代码进行分析,仔细看这段代码,就会发现一个问题,三种面值循环了三次,如果是前两种面值的确定了,那么第三种面值岂不是就相当于确定了呢?(你品,你再品,你细品😁😁😁!!!)

所以进行改良代码如下:

public function test5()
{
    $count = 0;
    for ($i = 0; $i <= (100 / 7); $i++) {
        for ($j = 0; $j <= (100 / 3); $j++) {
            if ((100-$i*7-$j*3 >= 0) && ((100-$i*7-$j*3) % 2 == 0)) {
                $count += 1;
            }
        }
    }
    return $count;
}

是不是很神奇?没错,就是这么神奇(我在自言自语,可能对你来说没有那么奇怪,也许你早就想到了,没关系,没想到的还大有人在),这里我们会发现,原来有三层嵌套循环,现在编程了两层嵌套循环,复杂度变成了O(n²),这里就是经过分析后发现最内侧的for循环是无用的,所以把最内侧的for循环给剔除掉了,从而达到了降低复杂度的目的。

时空转换

说完了 无效操作剔除处理后我们该讨论什么是 时空转换了,所谓的 时空转换,我理解的字面意思就是 时间复杂度空间复杂度转换,那么谁转换谁呢?

举个例子:

  • 处理数据需要几十G的内存,或者上百上千G的内存来处理数据,那么这个时候很明显,一台机器是无法满足的,怎么办(这还用问么,憨憨才不知道怎么办😃)?花钱买啊,花钱买不就有了?对,没错,答案就是花钱买。计算完再释放掉就好了嘛。
  • 处理数据,需要花费1天甚至1星期,或者1个月的时间去处理该数据,内存和CPU其实用不了太多,这个时候能做的是什么?只能等着,如果期间出现了差错,那结果只能是重新开始处理数据了,没有其他办法。

综上所述: 有些问题能解决(空间可以花钱解决),但是时间却是花钱买不来的。(花钱能办的事儿都不叫事儿),只能说时间是无价的,空间是有价的。

空口无凭那不行,(肯定有人在等着举🌰了),废话不多说,翠花上🌰:

输入数组 [1,2,3,4,5,5,6 ] ,在数组中查找出现次数最多的数值,一眼就能看到答案:5.(不错,很不错,答案就是5),下面上代码。

public function test6()
{
    $a = [1, 2, 3, 4, 5, 5, 6];
    $max_value = -1;
    $temp_max = 0;

    foreach ($a as $sKey => $aItem) {
        $temp_count = 0;
        foreach ($a as $key => $item) {
            if ($aItem == $item) {
                $temp_count += 1;
            }
        }

        if ($temp_count > $temp_max) {
            $temp_max = $temp_count;
            $max_value = $aItem;
        }
    }
    return $max_value;
}

上面代码嵌套了两层foreach来解决该问题,最终计算出来的值是5,嵌套两层for循环可以得出时间复杂度为O(n²),当我们去查看空间复杂度的时候会发现,我们之定义了三个变量,变量的空间复杂度是O(1),三个变量的空间复杂度就是 O(1) + O(1) + O(1) = 3O(1),按照复杂度的计算规则,常数不计算在内,故复杂度是 3O(1) = O(1),所以该代码的空间复杂度与是 O(1)时间复杂度是O(n²)

我们这是我们需要做的就是降低时间复杂度,那么我们需要做的就是,在有限的空间内来降低时间复杂度,降低时间复杂度无疑就是将 O(n²)降低到 O(n)或者降低到 O(1)

不卖关子了,直接上代码:

public function test7()
{
    $a = [1, 2, 3, 4, 5, 5, 6];
    $b = [];
    foreach ($a as $key => $item) {
        if (!isset($b[$item])) {
            $b[$item] = 1;
        } elseif (isset($b[$item])) {
            $b[$item] += 1;
        }
    }

    $max_value = -1;
    $temp_max = 0;
    foreach ($b as $key => $item) {
        if ($item > $temp_max) {
            $temp_max = $item;
            $max_value = $key;
        }
    }
    return $max_value;
}

这里可以看到,我们定义的不在是三个变量,二十两个变量和一个数组,并且我们把两个嵌套的for循环单独拆开了成两个平级的for循环了。经过第一个for循环后我们可以得到定义的一个数组为 k-v 结构的字典了(注意:牵扯到数据结构了)。那么该字段的复杂度是跟随者输入量的变化而变化的,故空间复杂度为 O(n),前面根据复杂度的计算规则来算,空间复杂度中的两个变量为 O(1)不计算在其中,所以该段代码的空间复杂度为 O(n)

分析时间复杂度,两个for循环的时间复杂度分别是 O(n),那么合起来该段代码的时间复杂度为 O(n) + O(n) = 2O(n),根据计算规则,常数不算,那么最终时间复杂度为 O(n)

综上所述:

  • 优化前 时间复杂度:O(n²) 空间复杂度O(1)
  • 优化后 时间复杂度:O(n) 空间复杂度O(n)

可以发现,我们成功的将时间复杂度降低了,与此同时我们也提高了空间复杂度。但是我们做到了,利用有价的空间换回来了无价的时间,所以这一步是非常可行的。

总结一下吧

各单位注意!各部门注意:(这里句句是精华),无论是什么难题,其实降低复杂度的方法就是三个步骤:

  1. 暴力解法: 在没有任何时间、空间约束下,完成代码任务的开发(随心所欲,为所欲为)。
  2. 分析暴力解法后,无效操作剔除处理: 将代码中的无效计算、无效存储剔除,降低时间或空间复杂度(把没用的废物通通排出体外 【真的有味道了🤮】)。
  3. 无效操作剔除处理后再分析,时空转换: 设计合理数据结构,完成时间复杂度向空间复杂度的转移(把无价变成有价)。

上面我着重的讲了分析分析再分析 ,我所注重的还是要去理解,要去分析,要去拆解,会到最初的初衷上,就是这段代码要完成的是什么功能,或者说是什么业务,从源头出发,分析拆解业务,然后转换思路。

咚咚咚!!! 小彩蛋来喽: 上面的优化方式我们可以看到,时空转换的时候,我们用到了k-v 结构的字典,这里就是用到的数据结构。也就是说,我们如果涉及到操作空间复杂度的时候,我们就会涉及到数据结构,(有些小伙伴可能会说,数据结构我不会呀,没关系,我也不会,哈哈哈哈!!!😝😝😝),接下来我也会一步一步的去学习数据结构的知识,好东西就是要分享的。 ———— 那些年,我们挂过的科,迟早要还的!

最后:哪里理解的不对,哪里写的不好,需要改正或者优化的地方,欢迎指正,有兴趣的小伙伴也可以一起交流,相互学习。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值