一说到数据结构与算法,大家都会避之不及。这本来是一门专业基础课,但是大部分人都并没有学好,更不用说我这种半路出家的码农了。说实话,还是很羡慕科班出身的程序员,因为你们在日常工作或者面试中,只需要复习一下就好了,而我则是完全的从头开始学。不过,还好一切都不晚,在这里,我们就用 PHP 作为示例代码,来和大家一起真正的从头学一遍恐怖的数据结构与算法。
什么是数据结构呢?
数据结构是带“结构”的数据元素的集合,“结构”就是指数据元素之间存在的关系
这是严蔚敏老师在《数据结构》第二版中对数据结构的定义。其实,就是关于数据的一种组合形式。就像你去一家书店,或者是图书馆,或者是你家的书柜。这些书应该怎么摆放呢?关于书的摆放形式,就是数据结构。你可以乱七八糟的摆放,也可以分门别类的摆放,也可以根据自己的兴趣爱好摆放,也可以将最常用的书放在手边,将不常看的书放在柜子的深处。这些都是数据结构。
在程序世界中,数据结构包含两种形式,一种是逻辑结构,一种是物理结构。
即使你完全没有接触过数据结构,但只要你学习过编程,一定会多多少少地听说过这样一些名词:集合、线性表、树、图,它们指的就是数据结构中的逻辑结构。也就是从逻辑关系上描述数据,是具体问题抽象出来的数学模型。比如我们将书进行分类,每个分类下放着相对应的书籍,这就是一种树形结构。或者我们将书按书名拼音建立索引,然后在书架上贴上索引标签,这就是一种哈希结构。
逻辑结构将是我们整个学习中的一个重点,因为各种算法都是针对这些结构的操作实现。这个我们在下面的算法解释中再进行详细的说明。
物理结构主要是数据的物理存储方式,也叫做存储结构。这个非常好记,它只有两种形式,顺序存储结构和链式存储结构。通常,顺序存储结构我们使用数组来表示,而链式存储结构在 C语言 中使用结构体的指针来表示,但在 PHP 中,链式结构我们将使用类来表述。
上面说的那些逻辑结构,都可以用顺序或者链式的方式来实现,不管使用哪种方式,都可以完成对应逻辑结构的算法操作,但不同的形式或者算法又有不同的效率。而效率,正是整个数据结构和算法学习核心中的核心。
接下来我们看看什么是算法。
算法是一个有限指令集,它接受一些输入(有些情况下不需要输入),产生输出,并一定在有限步骤之后终止
这是陈越老师在浙大版《数据结构》第二版中对于算法的定义。其实我们简单点理解的话,针对上面的数据结构的一系列操作,就是算法。比如说我们定义了一个树,如何遍历这颗树呢?这就是一个算法,遍历一颗树有先序、中序、后序,也可以进行层序遍历,有这么多种方法,哪种好?哪种不好?用哪种物理结构?线性还是链式?这些结论都以算法的执行效率为基础。可以说,算法种类繁多,效率也千差万别,但是,不能一棒子打死说某个算法一定不好,每种算法也有它不同的应用场景。这就是我们要研究算法的原因。
关于算法,我们最关心的是它的效率,这个效率在这里我们使用的是时间复杂度和空间复杂度来定义的。
时间复杂度一般使用 O(n) 来表示,它关心的是问题规模和语句频度,一般会以 n 表示问题规模 (注意,这个 n 是未知的,如果这个 n 是已知的,那么它就是常数阶)。这个 n 有可能是一个常数(n 明确等于多少),记成 O(1) 。这是最好的情况,也可以线性增长,如 O(n) 。当然也可能对数或指数级增长,O(logN) 、 O(N^2),当然最要不得的是 O(2^N) 级的增长,这种情况下可能有生之年你都看不到运算的结果了。
我们可以看看简单的一段代码来分析它的时间复杂度:
echo $a++, PHP_EOL; // O(1)
$n = 10; // 假设一个数量用于测试,实际这个 n 是未知的,如果面试题代码中真的出现了这种已知 n 的情况,那么这个算法就是 O(1)
for($i = 0;$i<$n;$i++){
echo $i, PHP_EOL;
}
// O(n)
for($i = 1;$i<$n;$i*=2){
echo $i, PHP_EOL;
}
// O(logN)
for($i = 0;$i<$n;$i++){
for($j = 0;$j<$n;$j++){
echo $i, $j, PHP_EOL;
}
}
// O(N^2)
从上面代码中可以看出,循环嵌套次数与 n 的增长有很大的关系,另外就是看在循环内部的操作也会影响到增长的情况。比如如果我们是这样一段代码:
$n = 10; // 假设一个数量,实际这个 n 是未知的
$m = 3; // 假设一个数量,实际这个 m 是未知的
for($i = 0;$i<$n;$i++){
for($j = 0;$j<$m;$j++){
echo $i, $j, PHP_EOL;
}
}
那么它的时间复杂度就不是 O(N^2) 而是,O(NM) ,因为我们这两层循环并不是都是对最大的 N 的操作,而是 N * M 的操作。但是,如果当 M 很大甚至等于 N 的时候,那么这个算法也就成为了 N^2 。
对于时间复杂度的分析其实是需要一些数学功底的,但是对于我这种半吊子出身的码农来说,把握住循环层次以及循环内的操作情况就可以大致地分析出一段算法的时间复杂度了。当然,对于某些大厂比较刁钻的面试题来说,还是需要用数学方法进行分解求得正确的时间复杂度。
另外,在一段算法或者说一个函数中,时间复杂度以最大的那个为准,同时,也要考虑最好和最差时间复杂度,因为基于数据规模有可能在数据量大的时候时间复杂度会越来越惨。这个时候我们就会以最差时间复杂度来作为这一段算法的最终时间复杂度。
关于时间复杂度的问题,可以参考各类算法书籍,当然最好是以大学教材及练习题为主,多做题就能掌握得更深入。
相对时间复杂度来,空间复杂度在数据结构和算法中要关心的少一些,因为大部分情况下我们只借助一个第三方变量的话,这个空间复杂度就是 O(1) 。而如果需要借助一个数组或者链表来实现算法的话,这个算法的空间复杂度就是 O(n) 。
一般情况下我们不太会去过于的关注空间复杂度,因为大部分算法基本都会维持在 O(1) 或 O(n) 这个级别。当然,也就一些算法会出现占用非常大的空间复杂度的情况,这样分分钟就能撑爆你的内存。当在遇到这类算法的时候,我们会单独来说明空间复杂度的情况。
第一篇文章都是以理论方面的东西为基础的,这也是学习数据结构与算法的实际情况,必须是理论与实际相结合的学习。让我们从此开始,迈向这个深不见底的超级大坑吧!!
参考资料:
《数据结构》第二版,严蔚敏
《数据结构》第二版,陈越
《数据结构高分笔记》2020版,天勤考研