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 相关文章推荐
apache+codeigniter 通过.htcaccess做动态二级域名解析
Jul 01 PHP
用php实现选择排序的解决方法
May 04 PHP
PHP递归删除目录几个代码实例
Apr 21 PHP
ThinkPHP令牌验证实例
Jun 18 PHP
浅谈PHP正则表达式中修饰符/i, /is, /s, /isU
Oct 21 PHP
Yii的CDbCriteria查询条件用法实例
Dec 04 PHP
PHP判断是否连接上网络的方法
Jul 01 PHP
Yii中实现处理前后台登录的新方法
Dec 28 PHP
PHP简单获取多个checkbox值的方法
Jun 13 PHP
php简单统计中文个数的方法
Sep 30 PHP
php中各种定义变量的方法小结
Oct 18 PHP
PHP观察者模式示例【Laravel框架中有用到】
Jun 15 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
改德生G88 - 加装等响度低音提升电路
2021/03/02 无线电
小文件php+SQLite存储方案
2010/09/04 PHP
fleaphp crud操作之find函数的使用方法
2011/04/23 PHP
PHP中使用json数据格式定义字面量对象的方法
2014/08/20 PHP
PHP基于单例模式编写PDO类的方法
2016/09/13 PHP
javascript之可拖动的iframe效果代码
2008/08/01 Javascript
jQuery 表单验证插件formValidation实现个性化错误提示
2009/06/23 Javascript
js异步加载的三种解决方案
2013/03/04 Javascript
ExtJS[Desktop]实现图标换行示例代码
2013/11/17 Javascript
js中array的sort()方法使用介绍
2014/02/20 Javascript
JavaScript调用ajax获取文本文件内容实现代码
2014/03/28 Javascript
jQuery获取节点和子节点文本的方法
2014/07/22 Javascript
JavaScript定时显示广告代码分享
2015/03/02 Javascript
JavaScript实现网页对象拖放功能的方法
2015/04/15 Javascript
Bootstrap作品展示站点实战项目2
2016/10/14 Javascript
vue实现的上传图片到数据库并显示到页面功能示例
2018/03/17 Javascript
如何在JavaScript中优雅的提取循环内数据详解
2019/03/04 Javascript
vue + el-form 实现的多层循环表单验证
2020/11/25 Vue.js
[05:17]DOTA2誓师:今天我们在这里 明天TI4等我!
2014/03/26 DOTA
[00:12]DAC2018 天才少年转战三号位,他的SOLO是否仍如昔日般强大?
2018/04/06 DOTA
python中实现数组和列表读取一列的方法
2018/04/03 Python
python MNIST手写识别数据调用API的方法
2018/08/08 Python
python标记语句块使用方法总结
2019/08/05 Python
通过实例学习Python Excel操作
2020/01/06 Python
Nginx+Uwsgi+Django 项目部署到服务器的思路详解
2020/05/08 Python
小 200 行 Python 代码制作一个换脸程序
2020/05/12 Python
美国领先的水果篮送货公司和新鲜水果供应商:The Fruit Company
2018/02/13 全球购物
英国可持续奢侈品包包品牌:Elvis & Kresse
2018/08/05 全球购物
顶撞老师检讨书
2014/02/07 职场文书
学校领导班子四风对照检查材料
2014/09/27 职场文书
2014年学生会部门工作总结
2014/11/07 职场文书
终止劳动合同证明书样本
2014/11/19 职场文书
[有人@你]你有一封绿色倡议书,请查收!
2019/07/18 职场文书
nginx安装以及配置的详细过程记录
2021/09/15 Servers
Redis官方可视化工具RedisInsight安装使用教程
2022/04/19 Redis
Win11怎么解除儿童账号限制?Win11解除微软儿童账号限制方法
2022/07/07 数码科技