作者归档: Hessian

服务器变量:$_SERVER 详解

服务器变量:$_SERVER

其实这些东西手册里都有的,,,,,,,,,,,但我还是没事找事的放上来。。。我找抽吗?也许

注: 在 PHP 4.1.0 及以后版本使用。之前的版本,使用 $HTTP_SERVER_VARS。

$_SERVER 是一个包含诸如头部(headers)、路径(paths)和脚本位置(script locations)的数组。数组的实体由 web 服务器创建。不能保证所有的服务器都能产生所有的信息;服务器可能忽略了一些信息,或者产生了一些未在下面列出的新的信息。这意味着,大量的这些变量在 CGI 1.1 specification 中说明,所以您应该仔细研究它。

这是一个“superglobal”,或者可以描述为自动全局变量。这只不过意味这它在所有的脚本中都有效。在函数或方法中您不需要使用 global $_SERVER; 访问它,就如同使用 $HTTP_SERVER_VARS 一样。

$HTTP_SERVER_VARS 包含着同样的信息,但是不是一个自动全局变量。(注意: $HTTP_SERVER_VARS 和 $_SERVER 是不同的变量,PHP 处理它们的方式不同。)

如 果设置了 register_globals 指令,这些变量也在所有脚本中可用;也就是,分离了 $_SERVER 和 $HTTP_SERVER_VARS 数组。相关信息,请参阅安全的相关章节 使用 Register Globals。这些单独的全局变量不是自动全局变量。

您或许会发现下面列出的某些 $_SERVER 元素并不可用。注意,如果以命令行方式运行 PHP,下面列出的元素几乎没有有效的(或是没有任何实际意义的)。

“PHP_SELF”
当前正在执行脚本的文件名,与 document root相关。举例来说,在URL地址为
http://example.com/test.php/foo.bar 的脚本中使用 $_SERVER[‘PHP_SELF’] 将会得到 /test.php/foo.bar 这个结果。

如果 PHP 以命令行方式运行,该变量无效。

“argv”
传递给该脚本的参数。当脚本运行在命令行方式时,argv 变量传递给程序 C 语言样式的命令行参数。当调用 GET 方法时,该变量包含请求的数据。

“argc”
包含传递给程序的命令行参数的个数(如果运行在命令行模式)。

“GATEWAY_INTERFACE”
服务器使用的 CGI 规范的版本。例如,“CGI/1.1”。

‘SERVER_NAME’
当前运行脚本所在服务器主机的名称。如果该脚本运行在一个虚拟主机上,该名称是由那个虚拟主机所设置的值决定。

‘SERVER_SOFTWARE’
服务器标识的字串,在响应请求时的头部中给出。

“SERVER_PROTOCOL”
请求页面时通信协议的名称和版本。例如,“HTTP/1.0”。

“REQUEST_METHOD”
访问页面时的请求方法。例如:“GET”、“HEAD”,“POST”,“PUT”。

“QUERY_STRING”
查询(query)的字符串。

“DOCUMENT_ROOT”
当前运行脚本所在的文档根目录。在服务器配置文件中定义。

“HTTP_ACCEPT”
当前请求的 Accept: 头部的内容。

“HTTP_ACCEPT_CHARSET”
当前请求的 Accept-Charset: 头部的内容。例如:“iso-8859-1,*,utf-8”。

“HTTP_ACCEPT_ENCODING”
当前请求的 Accept-Encoding: 头部的内容。例如:“gzip”。

“HTTP_ACCEPT_LANGUAGE”
当前请求的 Accept-Language: 头部的内容。例如:“en”。

“HTTP_CONNECTION”
当前请求的 Connection: 头部的内容。例如:“Keep-Alive”。

“HTTP_HOST”
当前请求的 Host: 头部的内容。

“HTTP_REFERER”
链接到当前页面的前一页面的 URL 地址。不是所有的用户代理(浏览器)都会设置这个变量,而且有的还可以手工修改 HTTP_REFERER。因此,这个变量不总是正确真实的。

“HTTP_USER_AGENT”
当前请求的 User_Agent: 头部的内容。该字符串表明了访问该页面的用户代理的信息。一个典型的例子是:Mozilla/4.5 [en] (X11; U; Linux 2.2.9 i586)。您也可以使用 get_browser() 得到这个信息。

“REMOTE_ADDR”
正在浏览当前页面用户的 IP 地址。

‘REMOTE_HOST’
正在浏览当前页面用户的主机名。反向域名解析基于该用户的 REMOTE_ADDR。

注: 必须配置 Web 服务器来建立此变量。例如 Apache 需要在 httpd.conf 中有 HostnameLookups On。参见 gethostbyaddr()。

“REMOTE_PORT”
用户连接到服务器时所使用的端口。

“SCRIPT_FILENAME”
当前执行脚本的绝对路径名。

“SERVER_ADMIN”
该值指明了 Apache 服务器配置文件中的 SERVER_ADMIN 参数。如果脚本运行在一个虚拟主机上,则该值是那个虚拟主机的值。

“SERVER_PORT”
服务器所使用的端口。默认为“80”。如果你使用 SSL 安全连接,则这个值为您所设置的 HTTP 端口。

“SERVER_SIGNATURE”
包含服务器版本和虚拟主机名的字符串。

“PATH_TRANSLATED”
当前脚本所在文件系统(不是文档根目录)的基本路径。这是在服务器进行虚拟到真实路径的映像后的结果。

“SCRIPT_NAME”
包含当前脚本的路径。这在页面需要指向自己时非常有用。

“REQUEST_URI”
访问此页面所需的 URI。例如,“/index.html”。

“PHP_AUTH_USER”
当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是用户输入的用户名。

“PHP_AUTH_PW”
当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是用户输入的密码。

“AUTH_TYPE”
当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是认证的类型。

Read: 823

北京二手手机怎么买? 有内容!

北京的二手手机市场比较多,但是其中比较著名的应该是公主坟周边的飞新马二手商城,还有木樨园和西直门等三家二手手机市场

西直门地区二手手机市场

适合卖手机

理由:卖价较高,但比较混乱。

西直门附近的二手手机的收售价格都比较高,因此建议想要卖掉旧手机的朋友,可到这里来处理掉自己手中的旧机器。但是西直门附近的二手手机市场比较混乱,在 路边就有很多收购二手手机的商贩到处游走互相拉抢客人。如果是女孩最好带几个朋友一同前往,最好不要和路边收售手机的流动商贩交易。

在商户检验手机的时候,千万不要让商户拆开你的机器检查,否则你的手机就卖不上好价格了。

公主坟飞新马二手手机市场

适合买手机

理由:质量有保障,但价格高。

在公主坟的飞新马二手市场中购买二手手机还是比较有保障的,因为飞新马二手商城中的商户与商城有协约,商户所销售的二手商品如果在三保期内出现产品质量问 题,必须按照协约为顾客进行产品退换,因此,即使顾客在这里购买二手手机,也可享受到购买新手机的待遇。建议想要购买二手手机的朋友可到这里看一看。只是 这里的价格可能会稍微高一点。

木樨园的二手手机市场

适合买便宜手机

理由:价钱便宜,但质量差。

针对前两者而言,木樨园附近的二手手机市场的收售价格则比较低,但是质量也比较差,还有许多用旧手机翻新的机器。想买便宜二手手机的朋友可以到这里来看一 看,此外这里的新机价格也非常低,但大部分都是港行货品,产品质量还可以,但是如果一旦损坏,却无法到正规的维修站获得保修。
西直门二手手机市场

规模:有十几个小规模的营业厅,规模稍大点儿的营业厅里约
有二十几个柜台,规模小的营业厅里只有四五个柜台,在柜台上收二手手机要比路边上收手机的价格高一些,其中有部分在路边收手机的商贩会在收到手机 后再转卖给柜台上的商户,这样即可在中间获取几十元或者近百元的利润。在二手营业厅里也卖新手机,但大部分是那种被商户称为港行的手机。

地址:西直门桥向东第一个路口向南转

乘车路线:乘环线地铁、105路、719路、816路、800路、44路公交车均可到达。

公主坟飞新马二手手机市场

规模:飞新马二手商城约有一百多个柜台经营二手手机,这里的二手手机品牌、型号很全面。商城对商户管理严格,在这里购买二手手机,质量可获保障。

地址:公主坟环岛南侧100米三环西侧乘车路线:乘300路、374路、728路、817路、1路、4路、373路、52路公交车均可到达。

木樨园方仕通科技广场

规模:广场分上下两层,一楼有二手手机和新手机的销售柜台,并批发兼零售。同时,在一楼还有许多销售手机配件的柜台,如:手机挂绳、手机套、手机外壳、手机存储卡和手机耳机等配件,二楼主要以批发新手机为主。

地址:丰台区木樨园环岛东南角乘车路线:乘300路、730路、830路公交车均可到达。

清河鹏达通讯城

规模:约有二十个柜台,其中还有一两个空柜台,商城里的二手手机的型号比较少,收售手机的价格也都不高。

地址:清河桥向西过第一个路口后约30米路南。

乘车路线:乘719路、816路、305路公交车均可到达。

Read: 754

【转】推荐的C++书籍以及阅读顺序

当读者有一定c/c++基础
推荐的阅读顺序:
level 1
从<<essential c++>>开始,短小精悍,可以对c++能进一步了解其特性
以<<c++ primer>>作字典和课外读物,因为太厚不可能一口气看完

level 2
然后从<<effective c++>>开始转职,这是圣经,请遵守10诫,要经常看,没事就拿来翻翻
接着是<<exceptional c++>>,个人认为Herb Sutter主席大人的语言表达能力不及Scott Meyers总是在教育第一线的好
顺下来就是<<more effective c++>>和<<more exceptional c++>>,请熟读并牢记各条款
当你读到这里,应该会有一股升级的冲动了

level 3
<<insied the c++ object model>>看过后如一缕清风扫去一直以来你对语言的疑惑,你终于能明白compiler到底都背着你做了些什么了,这本书要细细回味,比较难啃,最好反复看几遍,加深印象
看完上一本之后,这本<<The design and evolution of c++>>会重演一次当年C++他爹在设计整个语言过程中的历程

level 4
<<the c++ standard library>>是stl的字典,要什么都可以查得到
学c++不能不学stl,那么首先是<<effective stl>>,它和圣经一样是你日常行为的规范
<<generic programming and the stl>>让你从oo向gp转变
光用不行,我们还有必要了解stl的工作原理,那么<<stl源码剖析>>会解决你所有的困惑

level 5
对于c++无非是oo和gp,想进一步提升oo,<<exeptional c++ style>>是一本主席这么多年的经验之谈,是很长esp的
一位stl高手是不能不去了解template的,<<c++ template>>是一本百科全书,足够你看完后对于gp游刃有余
<<modern c++ design>>是太过聪明的人写给明眼人看的

好书有很多,不能一一列举
以上我的读书经历,供各位参考。接下来的无非就是打怪练级,多听多写多看;boost、stl、loki这些都是利器,斩妖除魔,奉劝各位别再土法练钢了。

at last,无他,唯手熟尔。

忘了一本《thinking in C++》
也是经典系列之一

< <effective c++>>这本圣经的作者Scott Meyesr在给<<modern c++ design>>序言的时候高度的赞赏了Andrei同志的工作:C++社群对template的理解即将经历一次巨大的变化,我对它所说 的任何事情,也许很快就会被认为是陈旧的、肤浅的、甚至是完全错的。
就我所知,template的世界还在变化,速度之快就像我1995年回避写 它的时候一样。从发展的速度来看,我可能永远不会写有关template的技术书籍。幸运的是一些人比我勇敢,Andrei就是这样一位先锋。我想你会从 此书得到很多收获。我自己就得到了很多——Scott Meyers September2000。

并且,Scott Meyers 在最 近的Top5系列文章中,评价C++历史里面最重要5本书中、把Modern C++ Design列入其中,另外四本是它自己的effective c ++、以及C++ Programming Language、甚至包括《设计模式》和《C++标准文档》。
显然,Scott Meyers已经作为一个顶尖大师的角度承认了<<modern c++ design>>的价值。

并且调侃地说,可以把是否使用其中模板方法定义为,现代C++使用者和非现代C++使用者,并且检讨了自己在早期版本Effective对模板的忽视,最后重申在新版本Effective第七章节加入大量对模板程序设计的段落,作为对这次失误的补偿。

并 且,在这里要明确的是<<modern c++ design>>并不是一本泛型编成的书,也不是一本模板手册。其中提出了基于 策略的设计方法,有计划和目的的使用了模板、面向对象和设计模式。虽然Andrei本人对模板的研究世界无人能敌,但对其他领域的作为也令人赞叹。

任何做游戏的人都不能忽视OpenAL把,你在开发者的名单里能看到Loki的名字:)

Read: 903

一篇奇怪的文章…哈哈..电视?电脑?….显示器!

**1我有一篇文章寫過:「你看完一齣片長一小時的電影,其實你只看了30分鐘,因為影片在放映時,要捲片,捲片時快門是關閉的,快門在關閉的時間你什麼 也看不到,而這時間至少佔1/2……為什麼有30分鐘看不見任何東西,你卻絲毫不會察覺呢?很多人都說是因為視覺暫留,但很少人想到視覺暫留是雙向的,不 但看見的會延續到看不見之時,看不見的也會延續到看得見之時,因此30分鐘看成60分鐘的結果,就是亮度減半……」
       x       x      x      x      x
**2 而在另一篇文章我也寫過:「電影雖然每秒連拍24格,但在放映時,為了減輕閃爍,每一格都會投映二次,然後捲片,再投映二次,由於捲片的動作是下拉,因此 每投映二次下拉一格便稱為2-2PullDown……」這些視覺現象正說明了電視系統採用間插掃瞄的經典與必要。
       x       x      x      x      x
**3 如果看一場60分鐘電影事實只看了30分鐘,那麼不禁我又要說:一具42吋的電漿顯示幕,事實上我們大概也只能看到38吋,甚至還不到,因為它有一部分畫面被黑網給罩住了,這事絕對比看電影只看一半更嘔,因為那黑網你實在難以裝作沒看見。
       x       x      x      x      x
**4 而與電影相反的,是液晶顯示幕,它總是一幕未去又疊一幕,每一格都是溶進又溶出,Dissolve的結果,使我們在一小時內,看了二小時的重疊畫面,這檔事其實也不見得好受,只是大多數人都見新且喜,以亮麗掩蓋真實。
       x       x      x      x      x
**5 如果先有電腦再有電視,則電視一定不會是現在的電視,因為電視系統發明的初衷,便是將平面圖形轉成一條線,因為只有變成一條線才能用電來傳(現在的名詞叫 串流),而電視掃瞄成像的關鍵在”即時同步”,也就是說,除了電的傳送延遲外,基本上在家裡看到的電視畫面和電視台是完全”同時”的,但數位化後,這個” 即時同步”的特性就被摧毀了。無需”即時同步”固也有一些好處,卻也產生了一些新的問題。
       x       x      x      x      x
**6 典型的問題之一,是將場同步變成頁同步後,必須併左腳右腳為一步,結果就失去了應有的平衡。許多人都說:電視的畫面是由60個圖場捉雙成對地組成30格畫 面顯示,事實上這個說法只是算數式的想當然爾。因為兩條腿走路,由左腳到右腳算一步,由右腳到左腳同樣算一步,左右交互而行的最大的好處就是平衡。換言 之,電視的60個圖場即使雙雙組成也是60格畫面,只不過影像如果是動的時候,電視將出現60格”半畫面(場)”,但絕非30格鋸齒狀的”全畫面”。
       x       x      x      x      x
**6a CRT螢幕最妙的是景物呈現緩慢移動時(如湖上波光、噴泉流水及片尾徐徐上升的字幕….)最美,當影像一被凍結時,感覺就生硬了;相反的,LCD螢幕是在影像完全靜止時最美…..
       x       x      x      x      x
**7 60 場並成30格的問題是在數位化後才產生的,第一個碰到這問題的是DVD,因為MPG壓縮必須以格進行,說來有點荒謬,因為它有點像強迫馬兒的四條腿雙雙綁 起,變成兩條腿走路一樣。許多人在人模人樣走路的時候,從來沒想過馬走路的方式,其實比人走路高明100倍。儘管馬沒有手,但教馬用只兩條腿走路,多出來 的腿也不可能變成手啊!
       x       x      x      x      x
**8電視訊號原本是一條串一條串成一長條 的,每一短條間,都有一次同步,可稱為水平小同步,而在262.5個小同步後,更會有一次垂直大同步,同步的作用就是”中斷後從頭再來”。所以只要電視還 是電視,掃瞄線永遠是奇數的,而馬若仍然是馬,就一定用四條腿走路。525 / 625 / 1125條掃瞄線除2後,一定出現0.5條,在262.5條 處被垂直同步中斷後回到上方,自然也由半條處開始,這就形成左右交互而行的間插效果。除非你的雙腳變成輪子,千萬別說兩隻腳走路會搖擺……。
       x       x      x      x      x
**9 電視訊號進到掃瞄型顯示器上,只要有同步訊號就可將所有的事情搞定,一切都是那麼自然,可是在數位型顯示器上,事情就不單純了。它當然也可以來一行掃(更 新)一行,這是早期大多數480×640液晶電視(通常用於車上)的作法,毋庸置疑的,其畫面品質實在相去NTSC電視應有的品質太遠。原因有兩個:一是 液晶的遲頓反應比起人類的視覺暫留時間要大上許多,因而形成粗糙的齒狀拖尾;另一是480×640的成像矩陣實在太粗糙了。
       x       x      x      x      x
**10 理論上525條掃瞄線扣除垂直遮沒時間,大約只能看到480條,在A/D時當然就只需取樣480點,但取樣後的每一個點既非方又非圓,它只是一個數據,而 此數據再度成像時,究竟要以圓或方呈現就是個大問題,說得比較簡單一點,CRT的電子束在螢光幕上畫線除了濃淡之外還有粗細變化,可是用馬賽克拼圖,每一 個圖素都只能是一樣大小、一樣的形狀及一樣的間隔,這使得480點實際並拼不出480線。這問題十分有趣,因為總要有一行黑一行白才能形成一條線,但在掃 瞄型顯示器上,經由檢驗圖實測,480條的掃瞄線,居然可分辨出300~350條橫線條(垂直解析度),而在數位型顯示器上,若以相同的檢驗圖進行檢視 時,不只最多只能辨認240線,而且會出現難看的鋸齒。問題就出在馬賽克拼圖時,圖素與圖素間是驟然的變化的,一就是一,二就是二。用以解決鋸齒的方法是 邊緣模糊(True Type字型技術),但為補償邊緣模糊所造成的清晰度損失,必須將數位解析度提升,經實驗結果,將480點提升1.5倍也就是720 點,此時經由模糊後形成的視覺解析度大約和480線掃瞄相當。這也就是為什麼現在大多數數位顯示器的垂直像素多定為720的原因。而明白這點,也就會明白 720等級的顯示器,其成像解析度其實是與525線的CRT相當的。
       x       x      x      x      x
**10a 相同的原理,也出現在音響上,有許多人不解CD原始取樣率就只有44.1K,播放時超取樣為88.2K甚至176.4K有何意義?事實上超取樣的用意在便於濾波,而濾波在視覺上的功能就是模糊化。
**10b 還有300dpi的熱昇華也遠超過2400dpi甚至4800dpi的噴墨……不過說來也悲哀,因為熱昇華已經慢慢不見了。
       x       x      x      x      x
**11 繼DVD之後,在液晶顯示器所碰到的併場為格問題更是棘手,因為DVD在編碼時,可以人工設定是電影(24F/S)或電視(60f/S)、要不要去格?甚 至上場或下場優先,而這些資訊也會以旗標方式寫進數位化檔案,使得在播放時,得以順利還原為NTSC格式訊號。可是一架液晶顯示器通常會接上多個訊源,例 如DVD Player、有或無線電視,甚至同一訊源還參雜許多不同類型節目,尤其是電台節目總是電視、電影、廣告、插播胡接一通,再好的邏輯電路都難以 在瞬間辨認節目類型以進行不同的處理,即便是播放DVD也不只是電影,此時Y,Pb,Pr循序輸出,若不逐片設定,便會有錯綁馬腳學人走的尷尬場面出現。
如果有人說:我的電漿只用來看電影……那是他家的事(不要來這裡吵架)。
       x       x      x      x      x
**12 最近又出現一個說法:《只有點對點的解析度才算真實解析度……》。這個簡單、想當然爾的說法,主要在提醒720等級顯示器的用戶們,如果你不升格到 1080等級,就不能看到HD的真實面貌。儘管現在市上1080等級的顯示器不多,但我不得不更進一步提醒您:480×640的液晶畫面相對於525線 4:3的畫面不也是點對點嗎?既然事實已經證明其畫面品質相去NTSC應有品質甚遠,當你準備升格為1920×1080之前,即使我不明說,你也該知道我 會說什麼啦!

Read: 658

在数据库中存储层次数据

作者:Gijs Van Tulder
翻译:ShiningRay @ NirvanaStudio

无论你要构建自己的论坛,在你的网站上发布消息还是书写自己的cms [1]程序,你都会遇到要在数据库中存储层次数据的情况。同时,除非你使用一种像XML [2]的数据库,否则关系数据库中的表都不是层次结构的,他们只是一个平坦的列表。所以你必须找到一种把层次数据库转化的方法。

存储树形结构是一个很常见的问题,他有好几种解决方案。主要有两种方法:邻接列表模型和改进前序遍历树算法

在本文中,我们将探讨这两种保存层次数据的方法。我将举一个在线食品店树形图的例子。这个食品店通过类别、颜色和品种来组织食品。树形图如下:

1105_tree

本文包含了一些代码的例子来演示如何保存和获取数据。我选择PHP [3]来写例子,因为我常用这个语言,而且很多人也都使用或者知道这个语言。你可以很方便地把它们翻译成你自己用的语言。

邻接列表模型(The Adjacency List Model)

我们要尝试的第一个——也是最优美的——方法称为“邻接列表模型”或称为“递归方法”。它是一个很优雅的方法因为你只需要一个简单的方法来在你的树中进行迭代。在我们的食品店中,邻接列表的表格如下:

1105_table1

如你所见,对每个节点保存一个“父”节点。我们可以看到“Pear [4]”是“Green”的一个子节点,而后者又是“Fruit”的子节点,如此类推。根节点,“Food”,则他的父节点没有值。为了简单,我只用了“title”值来标识每个节点。当然,在实际的数据库中,你要使用数字的ID。

显示树

现在我们已经把树放入数据库中了,得写一个显示函数了。这个函数将从根节点开始——没有父节点的节点——同时要显示这个节点所有的子节点。对于这些子节点,函数也要获取并显示这个子节点的子节点。然后,对于他们的子节点,函数还要再显示所有的子节点,然后依次类推。

也许你已经注意到了,这种函数的描述,有一种普遍的模式。我们可以简单地只写一个函数,用来获得特定节点的子节点。这个函数然后要对每个子节点调用自身来再次显示他们的子节点。这就是“递归”机制,因此称这种方法叫“递归方法”。

<?php
// $parent 是我们要查看的子节点的父节点
// $level 会随着我们深入树的结构而不断增加,
// 用来显示一个清晰的缩进格式
function display_children($parent, $level) {
// 获取$parent的全部子节点
$result = mysql_query('SELECT title FROM tree '.
'WHERE parent="'.$parent.'";');

// 显示每个节点
while ($row = mysql_fetch_array($result)) {
// 缩进并显示他的子节点的标题
echo str_repeat(' ',$level).$row['title']."n";

// 再次调用这个函数来显着这个子节点的子节点
display_children($row['title'], $level+1);
}
}
?>

要实现整个树,我们只要调用函数时用一个空字符串作为$parent$level = 0: display_children('',0); 函数返回了我们的食品店的树状图如下:

Food
Fruit
Red
Cherry
Yellow
Banana
Meat
Beef
Pork

注意如果你只想看一个子树,你可以告诉函数从另一个节点开始。例如,要显示“Fruit”子树,你只要display_children('Fruit',0);

The Path to a Node节点的路径

利用差不多的函数,我们也可以查询某个节点的路径如果你只知道这个节点的名字或者ID。例如,“Cherry”的路径是“Food”> “Fruit”>“Red”。要获得这个路径,我们的函数要获得这个路径,这个函数必须从最深的层次开始:“Cheery”。但后查找这个节点的父 节点,并添加到路径中。在我们的例子中,这个父节点是“Red”。如果我们知道“Red”是“Cherry”的父节点。

<?php
// $node 是我们要查找路径的那个节点的名字
function get_path($node) {
// 查找这个节点的父节点
$result = mysql_query('SELECT parent FROM tree '.
'WHERE title="'.$node.'";');
$row = mysql_fetch_array($result);

// 在这个array [5] 中保存数组
$path = array();

// 如果 $node 不是根节点,那么继续
if ($row['parent']!='') {
// $node 的路径的最后一部分是$node父节点的名称
$path[] = $row['parent'];

// 我们要添加这个节点的父节点的路径到现在这个路径
$path = array_merge(get_path($row['parent']), $path);
}

// 返回路径
return $path;
}
?>

这个函数现在返回了指定节点的路径。他把路径作为数组返回,这样我们可以使用print_r(get_path('Cherry')); 来显示,其结果是:

Array
(
[0] => Food
[1] => Fruit
[2] => Red
)

不足

正如我们所见,这确实是一个很好的方法。他很容易理解,同时代码也很简单。但是邻接列表模型的缺点在哪里呢?在大多数编程语言中,他运行很慢,效率很差。这主要是“递归”造成的。我们每次查询节点都要访问数据库。

每次数据库查询都要花费一些时间,这让函数处理庞大的树时会十分慢。

造成这个函数不是太快的第二个原因可能是你使用的语言。不像Lisp这类语言,大多数语言不是针对递归函数设计的。对于每个节点,函数都要调用他自 己,产生新的实例。这样,对于一个4层的树,你可能同时要运行4个函数副本。对于每个函数都要占用一块内存并且需要一定的时间初始化,这样处理大树时递归 就很慢了。

改进前序遍历树

现在,让我们看另一种存储树的方法。递归可能会很慢,所以我们就尽量不使用递归函数。我们也想尽量减少数据库查询的次数。最好是每次只需要查询一次。

我们先把树按照水平方式摆开。从根节点开始(“Food”),然后他的左边写上1。然后按照树的顺序(从上到下)给“Fruit”的左边写上2。这 样,你沿着树的边界走啊走(这就是“遍历”),然后同时在每个节点的左边和右边写上数字。最后,我们回到了根节点“Food”在右边写上18。下面是标上 了数字的树,同时把遍历的顺序用箭头标出来了。

1105_numbering

我们称这些数字为左值和右值(如,“Food”的左值是1,右值是18)。正如你所见,这些数字按时了每个节点之间的关系。因为“Red”有3和6 两个值,所以,它是有拥有1-18值的“Food”节点的后续。同样的,我们可以推断所有左值大于2并且右值小于11的节点,都是有2-11的 “Food”节点的后续。这样,树的结构就通过左值和右值储存下来了。这种数遍整棵树算节点的方法叫做“改进前序遍历树”算法。

在继续前,我们先看看我们的表格里的这些值:

1105_table2

注意单词“left”和“right”在SQL中有特殊的含义。因此,我们只能用“lft”和“rgt”来表示这两个列。(译注——其实Mysql 中可以用“`”来表示,如“`left`”,MSSQL中可以用“[]”括出,如“[left]”,这样就不会和关键词冲突了。)同样注意这里我们已经不 需要“parent”列了。我们只需要使用lft和rgt就可以存储树的结构。

获取树

如果你要通过左值和右值来显示这个树的话,你要首先标识出你要获取的那些节点。例如,如果你想获得“Fruit”子树,你要选择那些左值在2到11的节点。用SQL语句表达:

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11;

这个会返回:

1105_table3

好吧,现在整个树都在一个查询中了。现在就要像前面的递归函数那样显示这个树,我们要加入一个ORDER BY子句在这个查询中。如果你从表中添加和删除行,你的表可能就顺序不对了,我们因此需要按照他们的左值来进行排序。

SELECT * FROM tree WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;

就只剩下缩进的问题了。

要显示树状结构,子节点应该比他们的父节点稍微缩进一些。我们可以通过保存一个右值的一个栈。每次你从一个节点的子节点开始时,你把这个节点的右值 添加到栈中。你也知道子节点的右值都比父节点的右值小,这样通过比较当前节点和栈中的前一个节点的右值,你可以判断你是不是在显示这个父节点的子节点。当 你显示完这个节点,你就要把他的右值从栈中删除。要获得当前节点的层数,只要数一下栈中的元素。

<?php
function display_tree($root) {
// 获得$root节点的左边和右边的值
$result = mysql_query('SELECT lft, rgt FROM tree '.
'WHERE title="'.$root.'";');
$row = mysql_fetch_array($result);

// 以一个空的$right栈开始
$right = array();

// 现在,获得$root节点的所有后序
$result = mysql_query('SELECT title, lft, rgt FROM tree '.
'WHERE lft BETWEEN '.$row['lft'].' AND '.
$row['rgt'].' ORDER BY lft ASC;');

// 显示每一行
   while ($row = mysql_fetch_array($result)) {
     // 检查栈里面有没有元素
     if (count($right)>0) {
       // 检查我们是否需要从栈中删除一个节点
       while ($right[count($right)-1]<$row['rgt']) {
         array_pop($right);
       }
     }

     // 显示缩进的节点标题
     echo str_repeat(' ',count($right)).$row['title']."n";

     // 把这个节点添加到栈中
     $right[] = $row['rgt'];
   }
}
?>

如果运行这段代码,你可以获得和上一部分讨论的递归函数一样的结果。而这个函数可能会更快一点:他不采用递归而且只是用了两个查询

节点的路径

有了新的算法,我们还要另找一种新的方法来获得指定节点的路径。这样,我们就需要这个节点的祖先的一个列表。

由于新的表结构,这不需要花太多功夫。你可以看一下,例如,4-5的“Cherry”节点,你会发现祖先的左值都小于4,同时右值都大于5。这样,我们就可以使用下面这个查询:

SELECT title FROM tree WHERE lft < 4 AND rgt > 5 ORDER BY lft ASC;

注意,就像前面的查询一样,我们必须使用一个ORDER BY子句来对节点排序。这个查询将返回:

+-------+
| title |
+-------+
| Food |
| Fruit |
| Red   |
+-------+

我们现在只要把各行连起来,就可以得到“Cherry”的路径了。

有多少个后续节点?How Many Descendants

如果你给我一个节点的左值和右值,我就可以告诉你他有多少个后续节点,只要利用一点点数学知识。

因为每个后续节点依次会对这个节点的右值增加2,所以后续节点的数量可以这样计算:

descendants = (right – left - 1) / 2

利用这个简单的公式,我可以立刻告诉你2-11的“Fruit”节点有4个后续节点,8-9的“Banana”节点只是1个子节点,而不是父节点。

自动化树遍历

现在你对这个表做一些事情,我们应该学习如何自动的建立表了。这是一个不错的练习,首先用一个小的树,我们也需要一个脚本来帮我们完成对节点的计数。

让我们先写一个脚本用来把一个邻接列表转换成前序遍历树表格。

<?php
function rebuild_tree($parent, $left) {
// 这个节点的右值是左值加1
$right = $left+1;

// 获得这个节点的所有子节点
$result = mysql_query('SELECT title FROM tree '.
'WHERE parent="'.$parent.'";');
while ($row = mysql_fetch_array($result)) {
// 对当前节点的每个子节点递归执行这个函数
// $right 是当前的右值,它会被rebuild_tree函数增加
$right = rebuild_tree($row['title'], $right);
}

// 我们得到了左值,同时现在我们已经处理这个节点我们知道右值的子节点
mysql_query('UPDATE tree SET lft='.$left.', rgt='.
$right.' WHERE title="'.$parent.'";');

// 返回该节点的右值+1
return $right+1;
}
?>

这是一个递归函数。你要从rebuild_tree('Food',1); 开始,这个函数就会获取所有的“Food”节点的子节点。

如果没有子节点,他就直接设置它的左值和右值。左值已经给出了,1,右值则是左值加1。如果有子节点,函数重复并且返回最后一个右值。这个右值用来作为“Food”的右值。

递归让这个函数有点复杂难于理解。然而,这个函数确实得到了同样的结果。他沿着树走,添加每一个他看见的节点。你运行了这个函数之后,你会发现左值和右值和预期的是一样的(一个快速检验的方法:根节点的右值应该是节点数量的两倍)。

添加一个节点

我们如何给这棵树添加一个节点?有两种方式:在表中保留“parent”列并且重新运行rebuild_tree() 函数——一个很简单但却不是很优雅的函数;或者你可以更新所有新节点右边的节点的左值和右值。

第一个想法比较简单。你使用邻接列表方法来更新,同时使用改进前序遍历树来查询。如果你想添加一个新的节点,你只需要把节点插入表格,并且设置好parent列。然后,你只需要重新运行rebuild_tree() 函数。这做起来很简单,但是对大的树效率不高。

第二种添加和删除节点的方法是更新新节点右边的所有节点。让我们看一下例子。我们要添加一种新的水果——“Strawberry”,作为“Red” 的最后一个子节点。首先,我们要腾出一个空间。“Red”的右值要从6变成8,7-10的“Yellow”节点要变成9-12,如此类推。更新“Red” 节点意味着我们要把所有左值和右值大于5的节点加上2。

我们用一下查询:

UPDATE tree SET rgt=rgt+2 WHERE rgt>5;
UPDATE tree SET lft=lft+2 WHERE lft>5;

现在我们可以添加一个新的节点“Strawberry”来填补这个新的空间。这个节点左值为6右值为7。

INSERT INTO tree SET lft=6, rgt=7, title='Strawberry';

如果我们运行display_tree() 函数,我们将发现我们新的“Strawberry”节点已经成功地插入了树中:

Food
Fruit
Red
Cherry
Strawberry
Yellow
Banana
Meat
Beef
Pork

缺点

首先,改进前序遍历树算法看上去很难理解。它当然没有邻接列表方法简单。然而,一旦你习惯了左值和右值这两个属性,他就会变得清晰起来,你可以用这 个技术来完成临街列表能完成的所有事情,同时改进前序遍历树算法更快。当然,更新树需要很多查询,要慢一点,但是取得节点却可以只用一个查询。

总结

你现在已经对两种在数据库存储树方式熟悉了吧。虽然在我这儿改进前序遍历树算法性能更好,但是也许在你特殊的情况下邻接列表方法可能表现更好一些。这个就留给你自己决定了

最后一点:就像我已经说得我部推荐你使用节点的标题来引用这个节点。你应该遵循数据库标准化的基本规则。我没有使用数字标识是因为用了之后例子就比较难读。

进一步阅读

数据库指导 Joe Celko写的更多关于SQL数据库中的树的问题:
http://searchdatabase.techtarget.com/tip/1,289483,sid13_gci537290,00.html [6]

另外两种处理层次数据的方法:
http://www.evolt.org/article/Four_ways_to_work_with_hierarchical_data/17/4047/index.html [7]

Xindice, “本地XML数据库”:
http://xml.apache.org/xindice/ [8]

递归的一个解释:
http://www.strath.ac.uk/IT/Docs/Ccourse/subsection3_9_5.html [9]

Read: 1285