上节内容回顾
上节说到了复杂度,简单介绍了 复杂度
、时间复杂度
、空间复杂度
,同时举了小例子来说明这几个概念,同时也举例说明了一下优化空间复杂度的一个简单概念。
-
复杂度: 复杂度是衡量代码运行效率的重要度量因素。换句通俗易懂的话说:其实复杂度就是代码运行时占用的内存的多少,和代码运行时所使用的时间的多少,这个总称为复杂度。复杂度是一个关于输入数据量 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;
}
这是一种不计较任何时间复杂度或空间复杂度的、最直观的暴力解法。如果暴力解法复杂度够低,可以接受,那我无话可说。继续使用暴力解法就好,毕竟也不费脑筋,死不了多少脑细胞,保留点黑发对程序员来说还是比较重要的。
言归正传:为了降低复杂度,我们的一个直观的思路是 "梳理程序,看其流程中是否有无效的计算或者无效的存储"
-
降低时间复杂度常用的方法有递归、二分法、排序算法、动态规划等。
-
降低空间复杂度的核心思路就是,能用低复杂度的数据结构能解决问题,就千万不要用高复杂度的数据结构。
好了,(敲黑板,重点来了,记一下,记一下哈!!),以上我们大概了解了基本的问题解决思路,那么复杂度优化的思路是什么呢?这也是本节的核心:
-
暴力解法: 在没有任何时间、空间约束下,完成代码任务的开发(随心所欲,为所欲为)。
-
无效操作剔除处理: 将代码中的无效计算、无效存储剔除,降低时间或空间复杂度(把没用的废物通通排出体外 【这是一句有味道的话🤮】)。
-
时空转换: 设计合理数据结构,完成时间复杂度向空间复杂度的转移(这。。这。。这该怎么解释呢?拿内存换时间:增加内存的使用来缩短执行消耗的时间)。
无效操作剔除处理
暴力解法 已经在上面举例说明了,不管三七二十一,第一印象很重中,想到什么什么,接到需求的第一个想法就是你的暴力解法。下面来举例说明一下剔除无效操作的处理,请听题:
假设有任意多张面额为 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)
可以发现,我们成功的将时间复杂度降低了,与此同时我们也提高了空间复杂度。但是我们做到了,利用有价的空间换回来了无价的时间,所以这一步是非常可行的。
总结一下吧
各单位注意!各部门注意:(这里句句是精华),无论是什么难题,其实降低复杂度的方法就是三个步骤:
- 暴力解法: 在没有任何时间、空间约束下,完成代码任务的开发(随心所欲,为所欲为)。
- 分析暴力解法后,无效操作剔除处理: 将代码中的无效计算、无效存储剔除,降低时间或空间复杂度(把没用的废物通通排出体外 【真的有味道了🤮】)。
- 无效操作剔除处理后再分析,时空转换: 设计合理数据结构,完成时间复杂度向空间复杂度的转移(把无价变成有价)。
上面我着重的讲了分析 ,分析 ,再分析 ,我所注重的还是要去理解,要去分析,要去拆解,会到最初的初衷上,就是这段代码要完成的是什么功能,或者说是什么业务,从源头出发,分析拆解业务,然后转换思路。
咚咚咚!!! 小彩蛋来喽: 上面的优化方式我们可以看到,时空转换的时候,我们用到了k-v 结构的字典,这里就是用到的数据结构。也就是说,我们如果涉及到操作空间复杂度的时候,我们就会涉及到数据结构,(有些小伙伴可能会说,数据结构我不会呀,没关系,我也不会,哈哈哈哈!!!😝😝😝),接下来我也会一步一步的去学习数据结构的知识,好东西就是要分享的。 ———— 那些年,我们挂过的科,迟早要还的!
最后:哪里理解的不对,哪里写的不好,需要改正或者优化的地方,欢迎指正,有兴趣的小伙伴也可以一起交流,相互学习。