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 相关文章推荐
mayfish 数据入库验证代码
Apr 30 PHP
PHP操作MongoDB GridFS 存储文件的详解
Jun 20 PHP
用PHP代替JS玩转DOM的思路及示例代码
Jun 15 PHP
什么情况下可以不写PHP的闭合标签“?>”
Aug 28 PHP
php实现的css文件背景图片下载器代码
Nov 11 PHP
php getcwd与dirname(__FILE__)区别详解
Sep 24 PHP
PHP获取redis里不存在的6位随机数应用示例【设置24小时过时】
Jun 07 PHP
利用PHP判断是否是连乘数字串的方法示例
Jul 03 PHP
phpstorm 正则匹配删除空行、注释行(替换注释行为空行)
Jan 21 PHP
PHP关于foreach复制知识点总结
Jan 28 PHP
PHP中rename()函数的妙用讲解
Feb 28 PHP
php实现QQ小程序发送模板消息功能
Sep 18 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
PHP项目开发中最常用的自定义函数整理
2010/12/02 PHP
php中使用Imagick实现图像直方图的实现代码
2011/08/30 PHP
PHP+Ajax异步通讯实现用户名邮箱验证是否已注册( 2种方法实现)
2011/12/28 PHP
ThinkPHP5与单元测试PHPUnit使用详解
2020/02/23 PHP
JavaScript URL参数读取改进版
2009/01/16 Javascript
javascript自启动函数的问题探讨
2013/10/05 Javascript
JavaScript设计模式之建造者模式介绍
2014/12/28 Javascript
jquery Validation表单验证使用详解
2020/09/12 Javascript
基于javascript实现彩票随机数生成(升级版)
2020/04/17 Javascript
js获取对象、数组的实际长度,元素实际个数的实现代码
2016/06/08 Javascript
jQuery简单实现页面元素置顶时悬浮效果示例
2016/08/01 Javascript
js鼠标跟随运动效果
2017/03/11 Javascript
jQury Ajax使用Token验证身份实例代码
2017/09/22 Javascript
微信小程序之页面跳转和参数传递的实现
2017/09/29 Javascript
浅谈React Event实现原理
2018/09/20 Javascript
Vue 指令实现按钮级别权限管理功能
2019/04/23 Javascript
JavaScript创建表格的方法
2020/04/13 Javascript
[00:58]他们到底在电话里听到了什么?
2017/11/21 DOTA
[51:30]OG vs LGD 2018国际邀请赛淘汰赛BO3 第二场 8.26
2018/08/30 DOTA
安装Python的web.py框架并从hello world开始编程
2015/04/25 Python
Python的Django框架中的Context使用
2015/07/15 Python
机器学习python实战之决策树
2017/11/01 Python
Python八大常见排序算法定义、实现及时间消耗效率分析
2018/04/27 Python
分享vim python缩进等一些配置
2018/07/02 Python
使用Numpy读取CSV文件,并进行行列删除的操作方法
2018/07/04 Python
Python从单元素字典中获取key和value的实例
2018/12/31 Python
动态设置django的model field的默认值操作步骤
2020/03/30 Python
终于搞懂了Keras中multiloss的对应关系介绍
2020/06/22 Python
AmazeUI 单选框和多选框的实现示例
2020/08/18 HTML / CSS
一份关于丢失公司财物的检讨书
2014/09/19 职场文书
镇党政领导班子民主生活会思想汇报
2014/10/11 职场文书
个人自查自纠材料
2014/10/14 职场文书
瘦西湖导游词
2015/02/03 职场文书
加薪申请报告范本
2015/05/15 职场文书
PHP实现考试倒计时功能代码
2021/04/16 PHP
vue使用localStorage持久性存储实现评论列表
2022/04/14 Vue.js