PHP 巧用数组降低程序的时间复杂度


Posted in PHP onJanuary 01, 2010

关于作者
王丹丹 , IBM 中国系统与技术中心软件工程师,自从 2006 年加入 IBM,一直从事 Web 系统设计和开发工作,有五年 PHP 应用程序设计开发经验。

通常开发人员在写程序的时候,往往是把已经设计好或者构思好的运算逻辑,直接用编程语言翻译出来。程序能顺利编译通过,那是很令人高兴的事情。如果此时程序的运行时间还能接受,就会沉浸在写代码的成就感当中,常常在这个过程中忽略代码的优化。只有当程序运行速度受到影响时,才回过头去考虑优化的事情。本文主要是介绍在 PHP的编程中,如何巧用数组来降低因多层循环而引起的时间复杂度的问题。特别是当程序需要多次与数据库交互时,用此方法来优化你的代码,将会带给意想不到的效果。
什么是算法的时间复杂度
时间复杂度是开发人员用来衡量应用程序算法优劣的主要因素。客观地说,算法的优劣除了和时间复杂度有关,还与空间复杂度密切相关。而随着设备硬件配置的不断提升,对中小型应用程序来说,对算法的空间复杂度的要求也宽松了不少。不过,在当今 Web2.0 时代,对应用程序的时间复杂度却有了更高的要求。
什么是算法的时间复杂度呢?概要来说,是指从算法中选取一个能代表算法的原操作,以原操作重复执行的次数作为算法的时间量度。影响时间复杂度的因素有两个:一是原操作的执行时间,二是原操作因控制结构引起的执行次数。要把算法的时间复杂度降下来,降低原操作的执行次数是较为容易的方法,也是主要方法。本文所讲述的方法,是通过巧用 PHP 的数组,降低原操作的执行次数,从而达到降低算法时间复杂度的需求,和大家分享。
算法的时间量度记作 T(n)=O(f(n)),它表示算法中基本操作重复执行的次数是问题规模 n 的某个函数 f(n),也就是说随着问题规模 n的增大,算法执行时间的增长率和 f(n)的增长率相同。多数情况下,我们把最深层循环内的语句作为原操作来讨论算法的时间复杂度,因为它的执行次数和包含它的语句的频度相同。一般情况下,对一个问题只需选择一种基本操作来讨论算法的时间复杂度即可。有时也需要同时考虑多种基本操作。
在 Web开发中,通常一个功能的执行时间或响应时间,不仅仅跟服务器的响应能力、处理能力有关,还涉及第三方工具的交互时间,如对数据库的链接时间和对数据进行存取的时间。因而在选定原操作是,需要综合考虑应用程序各方面的因素,以最大影响程序执行时间的操作为原操作,来衡量算法的时间复杂度。也就是说,需要程序员在编写代码的时候,对重要操作的执行时间能有基本的认识。

常见程序中的时间复杂度分析
我们先看一个例子,假设 Web 程序的开发语言是 PHP,后台采用 DB2 数据库,PHP 通过 PEAR::DB 数据抽象层来实现对数据库的访问。
实例
数据库中有学生表 STUDENTS(见表 1),班级表 CLASSES(见表 2),学生成绩表 SCORES(见表 3),需要在 Web 页面中显示出本次考试数学成绩超过 90 分的同学姓名和所在班级。
表 1. STUDENTS Table
列名
描述
SID
学号
STUNAME
姓名
GENDER
性别
AGE
年龄
CLASSID
班级号

表 2. CLASSES Table
列名
描述
CLASSID
班级号
CLASSNAME
班级名

表 3. SCORES Table
列名
描述
SID
学生学号
COURSE
学科
SCORE
成绩

根据个人编程习惯的不同,要解决这个问题,通常有两种做法(访问数据库的操作用 PEAR::DB 的方式表示),参看方法 1、2。
[ 方法 1 ]对 STUDENTS, CLASSES, SCORES 三个表做联合查询,一次获取满足条件的学生信息和班级信息。PHP 算法描述如下:

清单 1. 方法 1

$querystr = "select distinct S.STUNAME as STUNAME,C.CLASSNAME as CLASSNAME ". 
"from STUDENTS as S,CLASSES as C,SCORES as R ". 
"where S.SID=R.SID and S.CLASSID=C.CLASSID and R.COURSE='Math' ". 
"and R.SCORE>=90"; 
$result = $db2handle->query( $querystr ); //从数据库中获取数据 
while( $row=$result->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//读取并显示数据 
echo "StudentName=".$row['STUNAME']."\t ClassName=".$row['CLASSNAME']."\n"; 
}//Done

[ 方法 2 ]从 SCORES 表中找出满足条件的学生学号,然后从 STUDENTS 表中查找学生的姓名和班级编码,最后在 CLASSES 表中获取班级的名称。PHP 算法描述如下:

清单 2. 方法 2

$scorestr = "select distinct SID from SCORES where COURSE='Math' and SCORE>=90"; 
$scoredata = $db2handle->query( $scorestr ); 
//从数据库中获取满足条件的学生学号 
while( $score=$scoredata->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//读取学生的学号,并在STUDENTS表中查找学生的姓名和班级编号 
$studentstr = "select STUNAME,CLASSID from STUDENTS where SID='".$score['SID']."'"; 
$studata =$db2handle->query( $studentstr); 
$stu=$studata->fetchRow(DB_FETCHMODE_ASSOC); 
//显示学生的姓名 
echo "StudentName=".$stu['STUNAME']."\t "; 
//读去学生的班级编号,并在CLASSES表中查找该学生所在班级名称 
$classstr = "select CLASSNAME from CLASSES where CLASSID='".$stu['CLASSID']."'"; 
$classdata = $db2handle->query( $classstr); 
$class=$classdata ->fetchRow(DB_FETCHMODE_ASSOC); 
//显示学生的班级 
echo "CLASSNAME=".$class['CLASSNAME']."\n"; 
}//end while for getting each student's ID. Done

对于这样的算法描述,相信大家会有似曾相识的感觉。这也是大多程序员广泛使用的算法。因为已经习惯了将思维中的算法逻辑直接译成代码,而往往没有时间和心思来斟酌算法的优劣。这里来分析一下这两种算法的时间复杂度。
因Web 服务器读取并显示数据的时间相对较小,一般在 10ms 的数量级,而从 DB2 数据库里查询并获取数据的时间数量级会是 100ms的数量级,并且随查询数据量的增加而增加。所以查询数据库的操作可作为量度时间复杂度的原操作,以 STUDENTS 表和 SCORES表中的数据量作为问题规模 n( 通常情况下,CLASSES 表的数据量较小且相对稳定 )。
对于方法 1,随着问题规模n 的增大,访问数据库的次数为常量 1。因而,时间复杂度为 T(n)=O(1)。对于方法 2,假设 SCORES 表中满足条件的记录有 m个,则原操作的执行次数为 m+1。也就是说随着数据规模 n 的增大,原操作的执行次数成线性增长。可见时间复杂度为T(n)=O(n)。可见,方法 1 的时间复杂度低。
那么方法 1 的问题在哪里?主要因为方法 1会增大数据库负载,也就是原操作的执行时间受问题规模 n 的影响比较大。假设 STUDENTS,CLASSES,SCORES 的记录数分别为X, Y, Z。那么在执行联合查询操作时,在数据库中会形成一个记录数为 X*Y*Z的矩阵,然后在这个矩阵中查找满足条件的记录数,最后获取记录的 STUNAME 信息和CLASSNAME。这样,任何一个表中的数据增加,都会造成矩阵表中记录的成倍增加。

用数组来优化算法
主要思路 :在所需数据中存在相对简单且数据量稳定的情况下,利用 PHP 数组 (Array) 的下标 (Index) 可以为字符串 (String)的特点,巧妙的将数据临时存放到数组中。这样可以通过下标 (Index) 快速获取所需值,从而降低对数据库的查询次数,进而降低算法的时间复杂度。
[ 方法 3 ]从CLASSES 表中获取 CLASSID 和 CLASSNAME 的对应关系存放到 ClassArray 一维数组中,从 STUDENTS表中获取 SID 和 STUNAME 以及 CLASSID 的对应关系存放到 StuArray 二维数组中。之后从 SCORES表中找出满足条件的学生学号,从 StuArray 数组中读取学生的姓名和班级编号,从 ClassArray 中读取班级的名称。PHP算法描述如下:

清单 3. 方法 3

$ClassArray = Array(); 
$StuArray = Array(); 
$classstr = "select CLASSID,CLASSNAME from CLASSES"; 
$classdata = $db2handle->query( $classstr); 
while( $class=$classdata ->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//生成ClassArray数组,下标Index以CLASSID命名,对应的值为CLASSNAME 
$ClassArray[$class['CLASSID']] = $class['CLASSNAME']; 
}//end while $ClassArray 
$stustr="select SID,STUNAME,CLASSID from STUDENTS"; 
$studata = $db2handle->query( $stustr); 
while( $stu=$studata ->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//生成StuArray数组,下标Index以SID命名,对应的值为STUNAME和CLASSID 
$StuArray[$stu ['SID']]['STUNAME'] = $stu['STUNAME']; 
$StuArray[$stu ['SID']]['CLASSID'] = $stu['CLASSID']; 
}//end while $StuArray 
$scorestr = "select distinct SID from SCORES where COURSE='Math' and SCORE>=90"; 
$scoredata = $db2handle->query( $scorestr ); 
//从数据库中获取满足条件的学生学号 
while( $score=$scoredata->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//读取学生的学号,并从StuArray中读取学生的姓名,从ClassArray中读取班级名称 
echo "StudentName=".$StuArray[ $score['SID'] ]['STUNAME']."\t "; 
echo "CLASSNAME=".$ClassArray[ $StuArray[ $score['SID'] ]['CLASSID'] ]."\n"; 
}//end while for getting each student's ID. Done

改进后方法的时间复杂度仍为 T(n)=O(1)。和方法 1 相比,方法 3 不必担心因某一个表中的记录增加而引起的数据库查询代价的成倍增加。和方法 2 相比,时间复杂度降低的同时,也没有影响算法空间复杂度。可谓一举两得。
虽然此优化方法简单易用,但并不是说它是万能的。使用时需要考虑“度”的问题。假设 STUDENTS 表的数据量很大,那么生成 StuArray的时候对系统内存的消耗就增加,这样算法的空间复杂度就会受到影响。另外,当数据量足够大时,影响算法执行时间的主要因素就发生了变化,需要重新选择原操作。针对 STUDENTS 表记录数大,CLASSES表记录少且稳定的情景,可以考虑用嵌套查询和数组相结合的方式,对算法进行优化。这里给出方法 4,以供参考。
[ 方法 4 ]从CLASSES 表中获取 CLASSID 和 CLASSNAME 的对应关系存放到 ClassArray 一维数组中。从 SCORES表中查询满足条件的学生学号,作为查询 STUDENTS 表的查询条件,获取学生的 STUNAME 和 CLASSID。之后从ClassArray 中读取班级的名称。PHP 算法描述如下:

清单 4. 方法 4

$ClassArray = Array(); 
$classstr = "select CLASSID,CLASSNAME from CLASSES"; 
$classdata = $db2handle->query( $classstr); 
while( $class=$classdata ->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//生成ClassArray数组,下标Index以CLASSID命名,对应的值为CLASSNAME 
$ClassArray[$class['CLASSID']] = $class['CLASSNAME']; 
}//end while $ClassArray 
$stustr = "select STUNAME,CLASSID from STUDENTS where SID in ". 
"(select distinct SID from SCORES where COURSE='M' and SCORE>=90)"; 
$studata = $db2handle->query( $stustr); 
//从数据库中获取满足条件的学生姓名和班级编号 
while( $stu=$studata ->fetchRow(DB_FETCHMODE_ASSOC) ){ 
//读取学生的姓名,并从ClassArray中读取班级名称 
echo "StudentName=".$stu ['STUNAME']."\t "; 
echo "CLASSNAME=".$ClassArray[ $stu ['CLASSID'] ]."\n"; 
}//end while for getting each student's Info. Done

总结
方法 3 和方法 4中引用了数组这个小技巧,巧妙地降低了算法的时间复杂度。在实际应用程序中,算法逻辑要复杂得多,对算法的优化需要综合考虑多方面的因素。需要提出的是,本文所述的方法不仅适用于 PHP应用程序。如果编程语言的数组支持以字符串作为下标,就可以考虑采用本文提出的方法:巧用数组的下标来降低算法的时间复杂度。对于不支持字符串做数组下标的编程语言,可以考虑使用建立哈希表来达到同样的效果。

PHP 相关文章推荐
Windows下的PHP5.0详解
Nov 18 PHP
PHP中的extract的作用分析
Apr 09 PHP
比较时间段一与时间段二是否有交集的php函数
May 31 PHP
PHP URL参数获取方式的四种例子
Feb 28 PHP
PHP防范SQL注入的具体方法详解(测试通过)
May 09 PHP
PHP 使用redis简单示例分享
Mar 05 PHP
php获取错误信息的方法
Jul 17 PHP
php session 写入数据库
Feb 13 PHP
PHP命令行执行整合pathinfo模拟定时任务实例
Aug 12 PHP
thinkPHP模板中for循环与switch语句用法示例
Nov 30 PHP
Yii2.0多文件上传实例说明
Jul 24 PHP
php利用ZipArchive类操作文件的实例
Jan 21 PHP
使用PHP获取网络文件的实现代码
Jan 01 #PHP
php中计算时间差的几种方法
Dec 31 #PHP
php 操作excel文件的方法小结
Dec 31 #PHP
PHP编程过程中需要了解的this,self,parent的区别
Dec 30 #PHP
用php实现让页面只能被百度gogole蜘蛛访问的方法
Dec 29 #PHP
PHP类的使用 实例代码讲解
Dec 28 #PHP
php 多线程上下文中安全写文件实现代码
Dec 28 #PHP
You might like
Zerg兵种介绍
2020/03/14 星际争霸
模拟OICQ的实现思路和核心程序(三)
2006/10/09 PHP
PHP 执行系统外部命令 system() exec() passthru()
2009/08/11 PHP
发款php蜘蛛统计插件只要有mysql就可用
2010/10/12 PHP
PHP中使用hidef扩展代替define提高性能
2015/04/09 PHP
PHP实现的简单排列组合算法应用示例
2017/06/20 PHP
PHP单例模式实例分析【防继承,防克隆操作】
2019/05/22 PHP
通用于ie和firefox的函数 GetCurrentStyle (obj, prop)
2006/12/27 Javascript
js获取键盘按键响应事件(兼容各浏览器)
2013/05/16 Javascript
javascript删除数组元素并且数组长度减小的简单实例
2014/02/14 Javascript
JavaScript中window.showModalDialog()用法详解
2014/12/18 Javascript
jQuery源码解读之removeAttr()方法分析
2015/02/20 Javascript
jquery实现键盘左右翻页特效
2015/04/30 Javascript
window.location.hash知识汇总
2015/11/09 Javascript
jQuery处理XML文件的几种方法
2016/06/14 Javascript
js 递归和定时器的实例解析
2017/02/03 Javascript
node.js利用mongoose获取mongodb数据的格式化问题详解
2017/10/06 Javascript
浅谈ElementUI中switch回调函数change的参数问题
2018/08/24 Javascript
详解Java中String JSONObject JSONArray List转换
2020/11/13 Javascript
[01:34]2016国际邀请赛中国区预选赛IG战队教练采访
2016/06/27 DOTA
Python中的Classes和Metaclasses详解
2015/04/02 Python
python中redis的安装和使用
2016/12/04 Python
Python实现批量压缩图片
2018/01/25 Python
python解析json串与正则匹配对比方法
2018/12/20 Python
基于Python下载网络图片方法汇总代码实例
2020/06/24 Python
Python绘图之柱形图绘制详解
2020/07/28 Python
Python3爬虫关于识别检验滑动验证码的实例
2020/07/30 Python
Pycharm2020最新激活码|永久激活(附最新激活码和插件的详细教程)
2020/09/29 Python
Python爬取梨视频的示例
2021/01/29 Python
美国鲍勃商店:Bob’s Stores
2018/07/22 全球购物
Tessabit日本:集世界奢侈品和设计师品牌的意大利精品买手店
2020/01/07 全球购物
税务干部个人整改措施思想汇报
2014/10/10 职场文书
论文评审意见
2015/06/05 职场文书
2015年教师国培感言
2015/08/01 职场文书
golang判断key是否在map中的代码
2021/04/24 Golang
Jupyter notebook 更改文件打开的默认路径操作
2021/05/21 Python