而隨著設備硬件配置的不斷提升,對中小型應用程序來說,對算法的空間復雜度的要求也寬松了不少。不過,在當今 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 算法描述如下:
$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 算法描述如下:
$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 算法描述如下:
$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 算法描述如下:
$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 應用程序。如果編程語言的數組支持以字符串作為下標,就可以考慮采用本文提出的方法:巧用數組的下標來降低算法的時間復雜度。對於不支持字符串做數組下標的編程語言,可以考慮使用建立哈希表來達到同樣的效果。