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

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

通常开发人员在写程序的时候,往往是把已经设计好或者构思好的运算逻辑,直接用编程语言翻译出来。程序能顺利编译通过,那是很令人高兴的事情。如果此时程序的运行时间还能接受,就会沉浸在写代码的成就感当中,常常在这个过程中忽略代码的优化。只有当程序运行速度受到影响时,才回过头去考虑优化的事情。 本文主要是介绍在 PHP 的编程中,如何巧用数组来降低因多层循环而引起的时间复杂度的问题。特别是当程序需要多次与数据库交互时,用此方法来优化你的代码,将会带给意想不到的效果。

原文:http://www.ibm.com/developerworks/cn/opensource/os-cn-php-time/

王 丹丹, 高级软件工程师, IBM

2009 年 11 月 26 日

  • 内容

什么是算法的时间复杂度

时间复杂度是开发人员用来衡量应用程序算法优劣的主要因素。客观地说,算法的优劣除了和时间复杂度有关,还与空间复杂度密切相关。而随着设备硬件配置的不断提升,对中小型应用程序来说,对算法的空间复杂度的要求也宽松了不少。不过,在当今 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 中巧用数组降低程序的时间复杂度

时间: 2024-10-24 15:15:14

PHP 中巧用数组降低程序的时间复杂度的相关文章

如何在c程序中动态使用数组

C语言不允许对数组的大小作动态定义,不能在程序中临时输入数组大小,这就给编程带来一定的困难. 下面通过一个求平均数和标准差的小程序来展示如何在程序中动态定义数组大小: 1 /*如何在c程序中动态使用数组(calloc函数)*/ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <math.h> 5 int main(){ 6 int num; //数据个数 7 double sum,ave,s2; //数据和.平

题目:返回一个整数数组中最大子数组的和。(要求程序必须能处理1000 个元素)

1.题目:返回一个整数数组中最大子数组的和. 2.要求: 要求程序必须能处理1000 个元素: 每个元素是int32 类型的: 输入一个整形数组,数组里有正数也有负数. 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和. 求所有子数组的和的最大值.要求时间复杂度为O(n). 3.设计思路: 将数组大小定义为1000,对于每个元素定义为int32类型,我们取数的时候对其进行了乘4294967296,使数组内的元素可以越界. 4.程序代码: 1 #include <iostream>

返回一个整数数组中最大子数组的值(程序能处理1000个元素)

课堂练习: 题目:要求返回一个整数数组中最大子数组的值 要求:程序必须能处理1000个元素 每个元素是int32类型的 设计思路: 将数组的大小定义为1000,每个元素定义为int32类型,取数值时对数成2的32次方,这样数值可以越界. 程序: #include <iostream>  #include<stdlib.h>  #include<time.h>  using namespace std;    int main()  {     int i;     in

返回一个二维整数数组中最大子数组的和的小程序

要求: 1.输入一个二维整形数组主,数组里有正数也有负数. 2.二维数组中连续的一个子矩阵组成一个子矩阵组成一个子数组,每个子数组都有一个和. 思路:根据上次写的找一维数组中最大子数组的经验,不妨把二维数组转化为一维数组的形式先进行求和,再比较,选出最大的子数组.首先以第一行为上界,依次向下确定数组的最大范围,把最大子数组按照一列有几个数,分成几种不同的行,对于产生数组的,进行一维数组求和计算并找出最大值.然后将上界下调一行,从第二行开始,依次向下确定数组最大范围,重复以上步骤. 代码如下: #

返回数组中最大子数组的和

一.题目: 在原有的一位数组上进行扩展,求首尾相连的最大子数组的和: 二.要求: 1)输入一个一维整形数组,数组里有正数也有负数. 2)数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和. 3) 求所有子数组的和的最大值. 4) 结对编程要求: 两人结对完成编程任务. 一人主要负责程序分析,一人负责代码编程代码复审和代码测试计划.发表一篇博客文章讲述两人合作中的过程.体会以及如何解决冲突(附结对开发的工作照). 三.组员:孔维春 崔鹏勃 四.思路分析: 这次的编程开发是基于上次的一维

C/C++中,空数组、空类、类中空数组的解析及其作用

转自:http://blog.sina.com.cn/s/blog_93b45b0f01015s95.html 我们经常会遇到这些问题: (1)C++中定义一个空类,他们它的大小(sizeof) 为多少? (2)只有一个char数据成员的类的大小? (3)能否定义一个空数组? (4)空数组名做标示的指针指向什么地方? (5)空类有什么用? (6)空数组有什么用? 等等...... 这些问题,笔者在这篇文章统统做一个比较详细的解析和认识.   1. sizeof是什么? 首先我们要理解sizeof

返回一个整数数组中最大子数组的和。(二人结对编程)

 题目:返回一个整数数组中最大子数组的和. 要求: 1.要求程序必须能处理1000 个元素: 2.每个元素是int32 类型的,出现子数组之和大于整型表示的最大范围会出现什么情况: 3.输入一个整型数组,数组里有正数也有负数. 4.数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和. 5.求所有子数组的和的最大值.要求时间复杂度为O(n).  设计思想:(1)将数组大小定义为n,用户输入p的值以确定该数组的长度,对于这n个元素用随机函数进行生成,每个元素定义为int32类型.(2)为

软件工程课程作业(四)--返回一个整数数组中最大子数组的和

伙伴链接:http://www.cnblogs.com/haoying1994/ 一.设计思想 本实验要求输入一个正负数混合的整型数组,长度不限,在此数组的所有子数组中找到和最大的数组,并求出相应数组的和,且时间复杂度为O(n).我们在课堂上共同讨论了多种解决方案,这些将在下面可能的解决方案中展示,在听了同学的思路和老师的讲解之后, 我们最终选取了老师课堂上描述的比较简便的思路.如下: 在输入数组的环节,采用for无限循环加if判断截止,直到触发回车键为止,将数组记录到Array中,数组长度记录

课后实验4--返回一个整数数组中最大子数组的和

伙伴链接:http://www.cnblogs.com/chengqiqin07/ 一.设计思想 本实验要求输入一个正负数混合的整型数组,长度不限,在此数组的所有子数组中找到和最大的数组,并求出相应数组的和,且时间复杂度为O(n).我们在课堂上共同讨论了多种解决方案,这些将在下面可能的解决方案中展示,在听了同学的思路和老师的讲解之后, 我们最终选取了老师课堂上描述的比较简便的思路.如下: 在输入数组的环节,采用for无限循环加if判断截止,直到触发回车键为止,将数组记录到Array中,数组长度记