折半查找所需要的,有序的、可以隨機(jī)存取的、順序結(jié)構(gòu)的限制,導(dǎo)致了排序的額外負(fù)擔(dān)(如果是逐個(gè)添加,主要的負(fù)擔(dān)是移動(dòng)數(shù)據(jù),此時(shí)是折半插入排序)。通過(guò)觀察折半查找的過(guò)程,發(fā)現(xiàn)實(shí)際上mid是從判定樹(shù)的根走到了葉子節(jié)點(diǎn),而這棵判定樹(shù)和有相同節(jié)點(diǎn)的完全二叉樹(shù)的高度是相同的。鏈?zhǔn)浇Y(jié)構(gòu)的好處就是不用大量移動(dòng)數(shù)據(jù),自然的用鏈樹(shù)來(lái)做查找結(jié)構(gòu)應(yīng)該是個(gè)好選擇。 在前面我們?cè)?jīng)寫(xiě)過(guò)一個(gè)BSTree類(lèi),這個(gè)類(lèi)大致上能滿(mǎn)足我們的要求。而為了在不同的輸入情況下,使得樹(shù)的高度盡可能的小,平衡樹(shù)的概念被提出來(lái)了,例如前面的AVLTree類(lèi)。所謂的平衡,現(xiàn)在更多意義上是指和完全m叉樹(shù)高度的比值是小于某個(gè)常數(shù)的樹(shù),也就是高度≤klogmn的樹(shù),k為一個(gè)定常數(shù)。注意到AVL樹(shù)的定義實(shí)際上太苛刻,就很容易理解為什么要放寬要求。而實(shí)際能應(yīng)用的平衡樹(shù),都是為了滿(mǎn)足這個(gè)要求而自己定一套規(guī)則,比如B-樹(shù),這個(gè)后面會(huì)講到。 索引有些字典會(huì)提供一個(gè)目錄,大部分情況是這樣的A……xx;B……xx;……。這樣就能迅速翻到開(kāi)頭字母對(duì)應(yīng)的頁(yè)數(shù)(實(shí)際上也知道了開(kāi)頭字母結(jié)束的地方),并且每個(gè)頁(yè)的左右上角的單詞也說(shuō)明了本頁(yè)的單詞范圍(可以判斷所查單詞在不在此頁(yè))。這些就是索引。 使用索引能夠快速的定位查找范圍,從我們查字典的經(jīng)驗(yàn)看來(lái),這也應(yīng)該是能提高查找效率的方法。注意到目錄的作用,它使得我們所需的字典空間分布,在一頁(yè)(或者幾頁(yè))的空間上迅速的被我們得到——類(lèi)比來(lái)看,如果數(shù)據(jù)太多以至內(nèi)存裝不下,我們也可以弄個(gè)“目錄”,使得可以將數(shù)據(jù)分塊的讀入內(nèi)存查找。 B-樹(shù)當(dāng)數(shù)據(jù)膨脹到一個(gè)可怕的程度時(shí),連索引都不能被全部裝入內(nèi)存——見(jiàn)過(guò)印刷版的EI檢索都有這個(gè)感覺(jué),光一個(gè)檢索目錄都比我們用的字典厚。我們的辦法就是索引再索引,顯而易見(jiàn)的,每個(gè)索引塊都應(yīng)該盡可能的大,以幫助我們獲得盡可能多的信息,而避免再次的查索引(此時(shí)一般都會(huì)涉及外存的存取)。AVL樹(shù)此時(shí)便力不從心了,我們需要一種新的結(jié)構(gòu)。 相信每個(gè)學(xué)到這里的人都對(duì)B-樹(shù)的定義深?lèi)和唇^,這個(gè)的責(zé)任應(yīng)該由寫(xiě)書(shū)的人來(lái)負(fù),雖然定義、概念是人認(rèn)知的重要工具和途徑,但在這里是適得其反的。原因就是B-樹(shù)根本不存在概念意義上的“概念”,它只是一個(gè)描述型的“概念”,是B-樹(shù)能夠運(yùn)行從而表現(xiàn)出來(lái)的一種現(xiàn)象。不管怎么說(shuō),先看一下現(xiàn)有的書(shū)上的定義(省略了“或者為空樹(shù)”這句話(huà)): 嚴(yán)蔚敏、吳偉民《數(shù)據(jù)結(jié)構(gòu)(C語(yǔ)言版)》 1. 每個(gè)節(jié)點(diǎn)至多有m棵子樹(shù) 2. 若根不是葉子節(jié)點(diǎn),至少有兩棵子樹(shù) 3. 除根以外的所有非終端節(jié)點(diǎn)至少有ém/2ù棵子樹(shù) 4. 所有非終端節(jié)點(diǎn)包含下列信息數(shù)據(jù)(n,A0,K1,A1,K2,A2……,Kn,An),其中,Ki為關(guān)鍵字,且Ki<Ki+1(i=1,2,……,n-1);Ai(i=0,……,n)為指向子樹(shù)根節(jié)點(diǎn)的指針,且指針Ai-1所指子樹(shù)中所有節(jié)點(diǎn)的關(guān)鍵字均小于Ki(i=1,……,n),An所指子樹(shù)中所有的節(jié)點(diǎn)的關(guān)鍵字均大于Kn,n(ém/2ù-1£ n £ m-1)為關(guān)鍵字的個(gè)數(shù)(或n+1為子樹(shù)個(gè)數(shù))。 5. 所有的葉子節(jié)點(diǎn)都出現(xiàn)在同一層次上,并不帶信息(可以看作是外部節(jié)點(diǎn)或查找失敗的節(jié)點(diǎn),實(shí)際上這些節(jié)點(diǎn)不存在,指向這些節(jié)點(diǎn)的指針為空)。 殷人昆等《數(shù)據(jù)結(jié)構(gòu)(用面向?qū)ο蠓椒ㄅcC++描述)》 1. m路搜索樹(shù)(實(shí)際上是上面第1、4條的內(nèi)容) 2. 根節(jié)點(diǎn)至少有兩個(gè)子女 3. 除根節(jié)點(diǎn)以外的所有節(jié)點(diǎn)(不包括失敗節(jié)點(diǎn))至少有ém/2ù個(gè)子女 4. 所有的失敗節(jié)點(diǎn)都位于同一層。事實(shí)上,這些節(jié)點(diǎn)都是作為外部節(jié)點(diǎn)存在,不是樹(shù)上的節(jié)點(diǎn)。 非常莫名其妙的定義——憑什么根至少有兩個(gè)子女,而其他的至少ém/2ù個(gè)子女?跟AVL樹(shù)的定義比較一下就能看出這個(gè)定義的不合理處——AVL樹(shù)只是給出了平衡的定義“左右子樹(shù)高度差不超過(guò)1”,至于什么平衡因子、旋轉(zhuǎn),根本就沒(méi)有提及,那只是為了保證平衡的手段。顯然,現(xiàn)在的B-樹(shù)的定義,把為了保證平衡而采取措施的結(jié)果包括在定義之中了,而這必將導(dǎo)致人的認(rèn)知上的迷惑,因此這樣的定義是不合格的。或許,一個(gè)不和現(xiàn)在定義矛盾的合理的定義應(yīng)該是這樣:采用如下辦法達(dá)到“平衡”的m路搜索樹(shù)……,當(dāng)然,我們首先要解決什么叫“平衡”,而這個(gè)直覺(jué)上的定義前面已經(jīng)提到了。 接下來(lái),讓我們看看保持平衡的“如下辦法”是什么。回憶一下AVL樹(shù),那時(shí)的旋轉(zhuǎn)確實(shí)是很精妙的方法——保持中序有序的情況下改變子樹(shù)高度差,我也耗費(fèi)了不少腦細(xì)胞把AVL樹(shù)的講解從書(shū)上的莫名其妙變成了按部就班,這是本人到目前為止最為得意的一件事情,到大家能看到這篇文字的時(shí)候,應(yīng)該也能看見(jiàn)我寫(xiě)的AVL樹(shù)的講解了。 兩個(gè)叉的AVL樹(shù)能旋轉(zhuǎn),m個(gè)叉的樹(shù)怎么轉(zhuǎn)?看來(lái)得換個(gè)思路,前人已經(jīng)為我們做到了,就是節(jié)點(diǎn)的分裂和合并。 Ø 分裂 節(jié)點(diǎn)裝不下的時(shí)候就分裂,也就是說(shuō)對(duì)于一個(gè)節(jié)點(diǎn),當(dāng)?shù)趍個(gè)元素進(jìn)來(lái)的時(shí)候,就要分裂。怎么分裂?以中間元素為軸,左右兩部分各形成一個(gè)節(jié)點(diǎn),作為中間元素的左右孩子——這里還是借用了二叉樹(shù)的概念,而事實(shí)上,m叉搜索樹(shù)就是多個(gè)二叉搜索樹(shù)拼合的產(chǎn)物——這樣還是中序有序的(或許不該說(shuō)“中序”,大家結(jié)合二叉樹(shù)自己理解吧),中間元素帶著這兩個(gè)孩子插入到原來(lái)節(jié)點(diǎn)的父節(jié)點(diǎn),當(dāng)然,父節(jié)點(diǎn)滿(mǎn)的時(shí)候同樣要分裂,這樣一層層直到根節(jié)點(diǎn)(或者不用分裂)。 縱觀分裂的過(guò)程,我們才能明白前面的定義是怎么回事。如果采用如上的分裂方法,對(duì)于根節(jié)點(diǎn)來(lái)說(shuō),要么沒(méi)有子女,要么一分裂就兩個(gè)子女。而對(duì)于非根節(jié)點(diǎn),只有當(dāng)節(jié)點(diǎn)滿(mǎn)的時(shí)候才會(huì)分裂,那自然最少有ém/2ù個(gè)子女。 而“所有的失敗節(jié)點(diǎn)都位于同一層”是B-樹(shù)的“平衡準(zhǔn)則”,很容易就能看出這樣的樹(shù)是“平衡”的。 所以,關(guān)于B-樹(shù)的定義實(shí)際上是B-樹(shù)維持平衡的外在表現(xiàn),它不應(yīng)該作為B-樹(shù)的定義,而只能是一種描述。 Ø 合并 和二叉搜索樹(shù)一樣,刪除都?xì)w結(jié)成在葉子節(jié)點(diǎn)的刪除,如果不在葉子節(jié)點(diǎn),就拿葉子節(jié)點(diǎn)中的覆蓋掉,轉(zhuǎn)化成在葉子節(jié)點(diǎn)的刪除。同樣的,當(dāng)不平衡出現(xiàn)時(shí)(即不滿(mǎn)足上面B-樹(shù)的描述),需要平衡化。 我們看到,父節(jié)點(diǎn)中的元素和左右兩個(gè)孩子原來(lái)可能是一個(gè)節(jié)點(diǎn)的,或者可以說(shuō)他們可以合并成一個(gè)節(jié)點(diǎn)。這樣一來(lái),如果他們的元素總和大于m-1,那么就從多的向少的撥一個(gè)元素——想想電視里老和尚手里的念珠,元素的移動(dòng)過(guò)程是這樣的:元素多的節(jié)點(diǎn)——父節(jié)點(diǎn)(手里的珠子)——元素少的節(jié)點(diǎn)。如果他們的元素總和小于等于m-1,那么就把他們合并成一個(gè)節(jié)點(diǎn),這樣一來(lái),父節(jié)點(diǎn)就會(huì)少一個(gè)元素,如果也出現(xiàn)了不平衡,也同樣處理。 觀看分裂合并的過(guò)程,就會(huì)發(fā)現(xiàn)插入刪除的起點(diǎn)都在葉子節(jié)點(diǎn),出現(xiàn)不平衡后,無(wú)論分裂還是合并,都會(huì)把多了(或者少了)一個(gè)元素的變化傳遞到父節(jié)點(diǎn),從而導(dǎo)致對(duì)父節(jié)點(diǎn)的再度平衡化。和AVL樹(shù)的調(diào)整簡(jiǎn)直是如出一轍,不同的是,AVL樹(shù)不可能分裂合并,因此采取的是旋轉(zhuǎn)的辦法;或者可以說(shuō)B-樹(shù)不能旋轉(zhuǎn),因此采取了分裂合并。而兩種調(diào)整方法都是在保持有序的情況下,使樹(shù)高(差)發(fā)生了變化。 當(dāng)樹(shù)的叉多起來(lái)的時(shí)候,操作起來(lái)都會(huì)覺(jué)得不便,因此B-樹(shù)更應(yīng)該是為了減少內(nèi)外存交換開(kāi)銷(xiāo)而提出來(lái)的,和外排一樣,對(duì)一般的應(yīng)用意義不大,不做系統(tǒng)級(jí)別的應(yīng)用(或者是為了考試)很少會(huì)用到,就不再具體講解了。 而如果僅是在內(nèi)存中,叉的數(shù)目少一點(diǎn)會(huì)比較好,比如3叉(因?yàn)檫@樣的樹(shù)每個(gè)節(jié)點(diǎn)有2或3個(gè)叉,又叫2-3樹(shù)),而實(shí)際上,AVL樹(shù)(或者紅黑樹(shù))已經(jīng)做得很好了,沒(méi)必要費(fèi)心在多叉搜索樹(shù)上。 B+樹(shù)想想人們還真煩,B-樹(shù)都有人認(rèn)為是Binary樹(shù)了,又來(lái)一個(gè)B+樹(shù),看來(lái)老外還真是該打,Binary和Balance縮寫(xiě)也不多保留幾個(gè)字母,都是B。 B+樹(shù)更是為了外存而提出來(lái)的,同樣的,一般用途不大,一般人不必費(fèi)心了(考試的例外)。注意的是和B-樹(shù)的幾點(diǎn)差別:B+樹(shù)的非葉子節(jié)點(diǎn)都是索引,因此當(dāng)刪除關(guān)鍵碼的時(shí)候,有些情況不必改動(dòng)索引,查到了關(guān)鍵字也要一直走到葉子節(jié)點(diǎn);另外所有的葉子節(jié)點(diǎn)都是鏈接在一起的,從頭到尾可以順序遍歷——和線(xiàn)索二叉樹(shù)很像不是嗎? 嚴(yán)版的和殷版的在B+樹(shù)的定義(更準(zhǔn)確的說(shuō)是描述)上有一個(gè)很大的分歧(節(jié)點(diǎn)中幾個(gè)元素幾個(gè)子樹(shù)),從教學(xué)的目的上,我同意殷版的描述,因?yàn)檫@樣的描述能更直接的表達(dá)B+樹(shù)是如何從B-樹(shù)演化來(lái)的,嚴(yán)版的引入了額外的區(qū)分,分散了讀者的注意力。 散列(哈希表)我更覺(jué)得哈希表是因?yàn)槟壳暗膬?chǔ)存結(jié)構(gòu)而誕生的。想想我們的儲(chǔ)存器(RAM),每個(gè)單元對(duì)應(yīng)一個(gè)地址,對(duì)于數(shù)組來(lái)說(shuō),當(dāng)知道首地址后,可以馬上計(jì)算出第N個(gè)元素的地址,因此可以馬上讀取。反過(guò)來(lái)說(shuō),卻不能由內(nèi)容來(lái)確定元素的地址,想知道含某個(gè)內(nèi)容的單元的地址,就必須挨個(gè)查看,或者在有序的情況下折半查找。 而當(dāng)我們的儲(chǔ)存器能夠支持按內(nèi)容存取的話(huà),那我們前面介紹的一切查找方法都會(huì)變得黯淡無(wú)光——想找15,馬上就能定位到15的位置,這樣O(1)的方法簡(jiǎn)直是人們夢(mèng)寐以求的。不知是幸運(yùn)(前面查找的方法還是大有用武之地)還是不幸(直接的O(1)查找不可能容易的實(shí)現(xiàn)),我們想要的那種類(lèi)型的儲(chǔ)存器(相聯(lián)存儲(chǔ)器)貴的要命(也可能容量根本做不大)。 既然硬件不支持,就從軟件下手吧,前面費(fèi)了點(diǎn)口舌,是為了使大家明白現(xiàn)有的方法是建立在現(xiàn)有的結(jié)構(gòu)上的,而當(dāng)現(xiàn)有體系發(fā)生改變的時(shí)候,方法也會(huì)跟著改變。 顯然,只要我們能在內(nèi)容和序號(hào)之間建立一種函數(shù)關(guān)系,在現(xiàn)有儲(chǔ)存結(jié)構(gòu)上就能實(shí)現(xiàn)按內(nèi)容存取了——內(nèi)容→函數(shù)變換→序號(hào)。 實(shí)際上,任何可列的內(nèi)容都是可以和整數(shù)一一對(duì)應(yīng)的(我們總能找到這樣的法則),但是,一一對(duì)應(yīng)的代價(jià)很可能是整數(shù)的范圍很廣,結(jié)果使得空間的浪費(fèi)很大。打個(gè)比方,一張1901~2000年事件索引表,只需要一個(gè)100大小的數(shù)組,0元素代表1901,99元素代表2000——這里也回答了很多人疑惑的問(wèn)題,2000年究竟是不是21世紀(jì),這么一對(duì)照就會(huì)發(fā)現(xiàn)這是個(gè)歷史遺留問(wèn)題,公元元年是公元1年而不是公元0年才會(huì)導(dǎo)致這樣的問(wèn)題,歷法幾千年來(lái)修訂了好幾回,我們也不要對(duì)這個(gè)問(wèn)題太較真了,一句話(huà),隨大流。然而我們?nèi)绻鲆粋(gè)人類(lèi)歷史的索引表,上下五千年(把古代人算上就是多少萬(wàn)年了),但卻不是每年都有事發(fā)生(應(yīng)該說(shuō)是值得我們注意的事),更有甚者是確切的年份都不知道,顯然前面的做法就不太合理了,絕大多數(shù)空間都是空閑的。 因此,實(shí)際上采用的函數(shù)變換都是帶有壓縮性質(zhì)的,即輸出的值域都是有限的一個(gè)范圍,典型的就是0~表長(zhǎng)。就像我們的日常經(jīng)驗(yàn)一樣,一壓縮,就會(huì)有些元素被擠到一起,“你我不分”了。因此,還必須有合理處理“沖突”的辦法。函數(shù)變換加上“沖突”處理方法,就構(gòu)成了散列這種查找方法。 具體的散列函數(shù)和沖突處理,請(qǐng)閱讀教科書(shū),我不再詳細(xì)說(shuō)明了。 鍵樹(shù)(Trie樹(shù))與基數(shù)排序這其實(shí)都是建立在散列的思想上的,在他們身上,很容易就能發(fā)現(xiàn)鏈散列的影子,而Trie樹(shù)更像是靜態(tài)的基數(shù)排序。Trie樹(shù)用來(lái)組織內(nèi)存中的數(shù)據(jù)也是很好用的,并非是只適用于外存。那么一本厚書(shū)也僅僅是一兩頁(yè),我這幾十頁(yè)的有這么一段也差不多了,^_^。具體請(qǐng)閱讀教科書(shū),一般讀者可跳過(guò)。
|