1 序
2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學(xué)在校門(mén)口照了最后一張合照,搬離寢室打車(chē)去了提前租好的房子,算來(lái)工作剛滿一年。在過(guò)去的一年里,很慶幸剛邁出校門(mén)的我遇見(jiàn)了現(xiàn)在的這一群同事,這一幫朋友,雖然工作之初經(jīng)歷波折,還是很開(kāi)心有現(xiàn)在的工作環(huán)境。其實(shí)走在這條路上在同學(xué)眼中我拋棄了很多,但是因?yàn)闊釔?ài),所以我相信我能在這條路上走的更遠(yuǎn)。可能是由于剛到一個(gè)節(jié)點(diǎn)吧,回顧過(guò)去的工作和學(xué)習(xí)方式,認(rèn)為還是應(yīng)該把所學(xué)到的知識(shí)記錄下來(lái),留待和人交流以及將來(lái)自己回顧。
??可能是作為我在簡(jiǎn)書(shū)上寫(xiě)的第一篇文章,總想留下點(diǎn)什么有紀(jì)念意義的東西。作為一名移動(dòng)端開(kāi)發(fā)人員,盡管在工作中對(duì)于數(shù)據(jù)結(jié)構(gòu)和算法的要求被無(wú)限弱化,但其作為計(jì)算機(jī)科學(xué)基礎(chǔ),很大程度決定了在開(kāi)發(fā)技能上所能到達(dá)的高度。最近決定從頭看C++數(shù)據(jù)結(jié)構(gòu)與算法一書(shū),這篇文章便是在看這本書(shū)時(shí)記下的筆記,由于目前還是主要以移動(dòng)端開(kāi)發(fā)為主,因此只記下基本的知識(shí)。
2 算法復(fù)雜度
根據(jù)算法中每個(gè)操作之間的關(guān)系,算法分為以下兩類(lèi):
- 決定性算法:對(duì)于給定的輸入只有一種方式能確定下一步采取的操作,如求一個(gè)集合的合只需要逐個(gè)相加并不需要進(jìn)行猜測(cè)。
- 非決定性算法:非決定性算法將問(wèn)題分解成猜測(cè)和驗(yàn)證兩個(gè)階段。算法的猜測(cè)階段是非確定性的,算法的驗(yàn)證階段是確定性的,它驗(yàn)證猜測(cè)階段給出解的正確性。如查找算法會(huì)先猜測(cè)數(shù)組中某個(gè)數(shù),再驗(yàn)證其是否是需要查找的那個(gè)數(shù)。
另外可以將需要解決的判定問(wèn)題問(wèn)題分為三類(lèi):
- P問(wèn)題:能夠用決定性算法在多項(xiàng)式時(shí)間內(nèi)解決的問(wèn)題。
- NP問(wèn)題:能夠用非決定性算法在多項(xiàng)式時(shí)間內(nèi)解決的問(wèn)題。
- NPC問(wèn)題:這里P類(lèi)問(wèn)題一定屬于NP類(lèi)問(wèn)題。如果任何一個(gè)NP問(wèn)題都能通過(guò)一個(gè)多項(xiàng)式時(shí)間算法轉(zhuǎn)換為某個(gè)NP問(wèn)題,那么這個(gè)NP問(wèn)題就稱(chēng)為NP完全問(wèn)題,該問(wèn)題則成為NP完整性,也成為NPC問(wèn)題。
所有的完全多項(xiàng)式非確定性問(wèn)題,都可以轉(zhuǎn)換為一類(lèi)叫做滿足性問(wèn)題的輯運(yùn)算問(wèn)題。既然這類(lèi)問(wèn)題的所有可能答案,都可以在多項(xiàng)式時(shí)間內(nèi)計(jì)算,于是就猜想,是否這類(lèi)問(wèn)題存在一個(gè)確定性算法,可以在多項(xiàng)式時(shí)間內(nèi)直接算出或是搜尋出正確的答案呢?這就是著名的NP=P?的猜想。
??算法效率指的是評(píng)估描述文件或數(shù)組尺度n同所需邏輯計(jì)算時(shí)間的關(guān)系,表示算法復(fù)雜度的方法有三種。
- O表示法?表最小上界
- Ω表示法?表最大下界
- Θ表示法?當(dāng)最小上界和最大下界相等時(shí)
表示算法可以通過(guò)以下兩種方式,每一類(lèi)都可用上述三種表示方式。
- 平均復(fù)雜度:將處理每個(gè)輸入所執(zhí)行的步驟數(shù)乘以改輸入數(shù)的概率數(shù)。
- 攤銷(xiāo)復(fù)雜度:當(dāng)某個(gè)操作執(zhí)行時(shí)會(huì)影響下一步操作所執(zhí)行的時(shí)間時(shí),考慮這種相互影響關(guān)系的復(fù)雜度表示方法。
例如向一個(gè)向量中連續(xù)插入單個(gè)元素,當(dāng)向量容積滿是便分配雙倍空間并復(fù)制原數(shù)據(jù)。攤銷(xiāo)成本可以用函數(shù)amCost(opi)=cost(opi)+potential(dsi)-potential(dsi-1)表示,其中ds為數(shù)據(jù)結(jié)構(gòu)的容量。
3 鏈表
- 單向鏈表:刪除和查找某個(gè)節(jié)點(diǎn)的最好情況復(fù)雜度為O(1),最壞情況為O(n),平均為O(n)。
- 雙向鏈表:鏈表中的每個(gè)節(jié)點(diǎn)同時(shí)包含前置節(jié)點(diǎn)及其后繼節(jié)點(diǎn)的指針變量。
- 循環(huán)鏈表:分為循環(huán)單向鏈表和循環(huán)雙向鏈表,鏈表中每個(gè)節(jié)點(diǎn)都有后置節(jié)點(diǎn),對(duì)于鏈表current節(jié)點(diǎn)標(biāo)識(shí)當(dāng)前節(jié)點(diǎn)。
- 跳躍鏈表:根據(jù)節(jié)點(diǎn)數(shù)量將鏈表分為多級(jí),每個(gè)節(jié)點(diǎn)包含自身級(jí)數(shù)的指針數(shù)組,其中的元素分別指向同級(jí)的下一個(gè)節(jié)點(diǎn)。從最低的0級(jí)到最高級(jí)分別可以形成一個(gè)獨(dú)立的子鏈。
跳躍鏈表一定是有序的,通常使用Root數(shù)組保存每一級(jí)的根節(jié)點(diǎn),每一級(jí)子鏈中的元素都存在于其下一級(jí)的子鏈中,其中0級(jí)節(jié)點(diǎn)包含所有的元素。鏈表的級(jí)數(shù)maxLevel于鏈表節(jié)點(diǎn)數(shù)n之間的關(guān)系為maxLevel = [lg 2 n] + 1
。為了避免在插入和刪除節(jié)點(diǎn)的時(shí)候重新夠著鏈表,放棄對(duì)不同級(jí)上節(jié)點(diǎn)的位置要求,僅保留不同級(jí)上的節(jié)點(diǎn)數(shù)目要求,這樣的鏈表又稱(chēng)為隨機(jī)跳躍鏈表。通過(guò)choosePowers()
函數(shù)生成powers數(shù)組,然后通過(guò)chooseLeves()
函數(shù)確定當(dāng)前插入節(jié)點(diǎn)的級(jí)數(shù)。
void choosePowers() {
powers[maxLevel-1] = (2 << (maxLevel - 1)) - 1;
for (int i = maxLevel - 2, j = 0; i >= 0; i--, j++) {
powers[i] = powers[i+1] - (2 << j);
}
}
//
int chooseLevel() {
int i, r = rand() % powers[maxLevel - 1] + 1;
for (i = 1; i < maxLevel; i++) {
if (r < powers[i]) {
return i - 1;
}
}
return i - 1;
}
當(dāng)對(duì)跳躍鏈表進(jìn)行插入操作的時(shí)候,需要將被插入節(jié)點(diǎn)的前置preNodes數(shù)組的各個(gè)元素對(duì)應(yīng)級(jí)的指針指向該節(jié)點(diǎn),同時(shí)將該節(jié)點(diǎn)的對(duì)應(yīng)各級(jí)指針指向?qū)?yīng)級(jí)的currentNodes數(shù)組。該類(lèi)鏈表的刪除操作類(lèi)似。查詢操作需要從最高級(jí)子鏈開(kāi)始查詢,如找到則返回,當(dāng)當(dāng)前節(jié)點(diǎn)的值小于時(shí)繼續(xù)查找,當(dāng)當(dāng)前節(jié)點(diǎn)的值大于被查值時(shí)從前置節(jié)點(diǎn)的第一級(jí)子鏈繼續(xù)查詢,直至查到0級(jí)子鏈。
??跳躍鏈表的平均時(shí)間復(fù)雜度為O(log 2 n),與更高級(jí)的數(shù)據(jù)結(jié)構(gòu)如自適應(yīng)樹(shù)或者AVL樹(shù)相比效率相當(dāng)不錯(cuò),因此可以用來(lái)代替這些數(shù)據(jù)結(jié)構(gòu)。
- 自組織鏈表:當(dāng)查找某個(gè)節(jié)點(diǎn)后動(dòng)態(tài)的重組織鏈表的結(jié)構(gòu),其重新組織鏈表的方法有以下四種。
- 前移法:在找到節(jié)點(diǎn)后將其放到鏈表頭。
- 換位法:在找到節(jié)點(diǎn)后將其和前置節(jié)點(diǎn)位置互換。
- 計(jì)數(shù)法:根據(jù)節(jié)點(diǎn)的訪問(wèn)次數(shù)由高到低進(jìn)行排序。
- 排序法:根據(jù)節(jié)點(diǎn)的自身屬性進(jìn)行排序。
前移法的查找某個(gè)節(jié)點(diǎn)x的攤銷(xiāo)復(fù)雜度根據(jù)公式amCost(x) = cost(x) + (inversionsBeforeAccess(x) - inversionsAfterAccess(x));
計(jì)算出amCost(x) <= 2posOL(x) - 1
,posOL(x)表示節(jié)點(diǎn)x在排序法鏈表中的位置。可以看出當(dāng)被查找節(jié)點(diǎn)x在前移法鏈表(MTF)的位置大于在排序法鏈表中的位置時(shí)候,其所需的訪問(wèn)數(shù)將大量增加。
- 稀疏表
當(dāng)一個(gè)表只有一小部分空間被使用的時(shí)候成為稀疏表。其中很多稀疏表都可以使用鏈表的數(shù)據(jù)結(jié)構(gòu)方式解決。例如當(dāng)儲(chǔ)存一個(gè)學(xué)校所有學(xué)生成績(jī)時(shí)。如果用二維數(shù)組,課程作為行,學(xué)生作為列,這時(shí)很多學(xué)生并不會(huì)選修所有的課,這會(huì)造成大量的空間浪費(fèi)。此時(shí),使用Class和Student兩個(gè)數(shù)組,其中class數(shù)組每個(gè)元素記錄選修這門(mén)課程的鏈表,Student中每個(gè)元素記錄這個(gè)學(xué)生所修課程的鏈表,這樣會(huì)大量節(jié)約所需的內(nèi)存空間。
??數(shù)組的優(yōu)點(diǎn)是隨機(jī)訪問(wèn),因此需要直接訪問(wèn)某個(gè)元素,數(shù)組是更好的選中,如二分查找法和大多數(shù)排序算法。當(dāng)只需要固定的訪問(wèn)某些元素(如第一個(gè)和最后一個(gè)),并且結(jié)構(gòu)的改變是算法的核心則鏈表是更好的選則,如隊(duì)列。另外數(shù)組的另一個(gè)優(yōu)點(diǎn)是空間,鏈表本身還會(huì)花空間存儲(chǔ)指向節(jié)點(diǎn)的指針。
4. 棧與隊(duì)列
- 棧:先進(jìn)后出只能從棧頂訪問(wèn)和刪除的數(shù)據(jù)結(jié)構(gòu),棧數(shù)據(jù)結(jié)構(gòu)可以用于匹配分隔符以及大數(shù)相加的操作,可以用向量(數(shù)組)和鏈表的方式實(shí)現(xiàn),其中鏈表的方式與抽象棧更匹配。在向量和鏈表形式的棧中,出棧操作的復(fù)雜度為O(1),在向量棧中最壞的入棧復(fù)雜度為O(n),而在鏈表?xiàng)V腥詾镺(1)。
- 隊(duì)列: 一端用于新加元素,一端用于刪除元素的數(shù)據(jù)結(jié)構(gòu)。同樣隊(duì)列也可以使用數(shù)組和鏈表的方式實(shí)現(xiàn)。在雙向鏈表的實(shí)現(xiàn)中,入隊(duì)和出隊(duì)的復(fù)雜度為O(1),單向鏈表的出隊(duì)操作復(fù)雜度為O(n)。
-
優(yōu)先隊(duì)列: 當(dāng)隊(duì)列中的某些操作需要優(yōu)先被執(zhí)行的時(shí)候采用的一直特殊隊(duì)列。有以下三種實(shí)現(xiàn)方式。
(1)單鏈表實(shí)現(xiàn):入隊(duì)和出隊(duì)的時(shí)間復(fù)雜度為O(n),適合10個(gè)以下的元素。
(2)一個(gè)數(shù)目不固定的短有序鏈表和一個(gè)無(wú)序鏈表,有序鏈表中元素?cái)?shù)目取決于閥值優(yōu)先級(jí),加入元素后會(huì)動(dòng)態(tài)變化,當(dāng)元素?cái)?shù)目眾多時(shí)效率和第一類(lèi)相近。
(3)一個(gè)具有√n數(shù)量的有序鏈表和一個(gè)無(wú)序鏈表,入隊(duì)平均操作時(shí)間是O(√n),出隊(duì)立即執(zhí)行,適合任意尺寸的隊(duì)列。 - STL中的雙端隊(duì)列:靠指針數(shù)組實(shí)現(xiàn)。雙端隊(duì)列Deque對(duì)象包含head、tail、headBlock、tailBlock和blocks五個(gè)字段。其中blocks字段保存了所有的數(shù)據(jù)數(shù)據(jù)組。
迷宮問(wèn)題通常可以使用棧數(shù)據(jù)結(jié)構(gòu)解決,將迷宮中的位置墻看做1,通道看做0,整個(gè)迷宮看做一個(gè)二維數(shù)組,從初始點(diǎn)開(kāi)始,將上下左右可以通過(guò)的點(diǎn)坐標(biāo)依次存入棧Stack,從棧頂取出一個(gè)位置作為當(dāng)前位置,并按上述順序繼續(xù)搜索可以通過(guò)的點(diǎn)并存入棧中,當(dāng)棧為空時(shí)則沒(méi)有路徑可以走出迷宮,當(dāng)棧頂為出口時(shí)則找到正確路徑。
5.遞歸
函數(shù)內(nèi)部對(duì)自己的調(diào)用,通常遞歸的定義通過(guò)運(yùn)行時(shí)棧實(shí)現(xiàn),實(shí)現(xiàn)遞歸的所有工作由操作系統(tǒng)完成。
unsigned int factorial (unsigned int n) {
if (n == 0) {
return 1;
} else {
return n * factorial (n -1);
}
}
每個(gè)函數(shù)被調(diào)用時(shí),系統(tǒng)會(huì)保存函數(shù)活動(dòng)記錄棧,從主函數(shù)開(kāi)始,當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)該函數(shù)的活動(dòng)記錄入棧,當(dāng)函數(shù)調(diào)用結(jié)束時(shí)函數(shù)的活動(dòng)記錄出站,系統(tǒng)也是基于此實(shí)現(xiàn)函數(shù)的遞歸調(diào)用。
int main {
/* 第136行 */ y = power (5.6,2);
}
double power (double x, unsigned int n) { /*102*/
if (n == 0) { /*103*/
return 1.0; /*104*/
} else {
return x * power(x,n-1); /*105*/
}
}
- 尾遞歸:在每個(gè)函數(shù)實(shí)現(xiàn)的末尾只使用一個(gè)遞歸調(diào)用。尾遞歸都可以轉(zhuǎn)化為迭代形式。
void tail (int i) {
if (i > 0) {
cout << i << '';
tail(i-1);
}
}
- 非尾遞歸:除尾遞歸以外的遞歸。將非尾遞歸轉(zhuǎn)化為迭代形式的時(shí)候都需要顯示的使用棧。
void nonTail (int i) {
if (i > 0) {
nonTail(i-1);
cout << i << '';
nonTail(i-1);
}
}
例如雪花圖案的繪制過(guò)程對(duì)遞歸函數(shù)的調(diào)用。
void drawSigleLine(sideLength, level) {
if (level = 0) {
畫(huà)一條線
} else {
drawSigleLine(sideLength/3, level - 1);
向左轉(zhuǎn)60°;
drawSigleLine(sideLength/3, level - 1);
向右轉(zhuǎn)120°;
drawSigleLine(sideLength/3, level - 1);
向左轉(zhuǎn)60°;
drawSigleLine(sideLength/3, level - 1);
}
}
- 間接遞歸:當(dāng)函數(shù)f()調(diào)用函數(shù)g(),函數(shù)g()調(diào)用函數(shù)f()時(shí);或者當(dāng)形成下述調(diào)用關(guān)系f() -> g1() -> g2() -> ... -> gm() -> f()時(shí)形成間接調(diào)用關(guān)系。如信息編碼函數(shù)receive() -> decode() -> store() -> receive() -> decode() -> ...
- 嵌套遞歸:函數(shù)不僅根據(jù)自身進(jìn)行定義,還作為該函數(shù)的一個(gè)參數(shù)進(jìn)行傳遞。
int regression(int n) {
if (n = 0) {
return 0;
} else if (n > 4) {
return n;
} else if (n <= 4) {
return regression(2+regression(2*n));
}
}
-
不合理遞歸:對(duì)于同一個(gè)輸入進(jìn)行多次計(jì)算,或者超大規(guī)模的遞歸調(diào)用通常是不合理的遞歸調(diào)用。
??盡管遞歸在邏輯上更簡(jiǎn)單和易于閱讀,但其降低了運(yùn)行速度,過(guò)多次數(shù)遞歸使得函數(shù)被多次調(diào)用導(dǎo)致運(yùn)行時(shí)棧空間有用盡崩潰的危險(xiǎn)。例如如下函數(shù):
int fib(int n) {
if (n < 2) {
return n;
} else {
return fib(n-2) + fib(n - 1);
}
}
上述函數(shù)fib當(dāng)n=6時(shí)可以看出對(duì)于同一個(gè)n進(jìn)行了多次調(diào)用,隨著n的增加,重復(fù)的計(jì)算次數(shù)極大提升。通常這類(lèi)問(wèn)題可以轉(zhuǎn)化為迭代方式進(jìn)行,當(dāng)n=30時(shí),遞歸方法調(diào)用次數(shù)為2 692 537次,而迭代方式只計(jì)算87次。
int iterativeFib(int n) {
if (n < 2) {
return n;
} else {
int i = 2, tmp, current = 1, last = 0;
for (; i <= n, ++i) {
tmp = current;
current += last;
last = tmp;
}
return current;
}
}
- 回溯:在解決某問(wèn)題是,從給定位置出發(fā)有許多不同路徑,嘗試一條路徑不成功后返回出發(fā)的十字路口嘗試另外一條路徑的方法。
在國(guó)際象棋棋盤(pán)中,根據(jù)規(guī)則,當(dāng)a中皇后確定位置后,其虛線上不能再放置皇后,求出能在每一行成功放置一名皇后的所有棋盤(pán)的解。使用二維數(shù)組標(biāo)識(shí)棋盤(pán)上所有的點(diǎn),假設(shè)棋盤(pán)邊長(zhǎng)為n,判斷一個(gè)皇后在當(dāng)前位置是否可以成功放置需先判斷該行,該列,正斜率斜線,負(fù)斜率斜線是否可用,用數(shù)組column[n-1]標(biāo)識(shí)所有的列,leftDiagonal[2n-1]標(biāo)識(shí)所有的正斜率斜線,該斜線的row+column為0~2*n-1用于標(biāo)識(shí)斜線在數(shù)組中的序號(hào),rightDiagonal[2n-1]標(biāo)識(shí)所有的負(fù)斜率斜線,該斜線的row-column為-n+1 ~ n-1,為其加上(n-1)用于標(biāo)識(shí)斜線在數(shù)組中的序號(hào)。
/*
棋盤(pán)邊長(zhǎng)為n column[n-1]所有列 leftDiagonal[2*n-1]所有正斜率斜線
rightDiagonal[2*n-1]所有負(fù)斜率斜線 positionInRow[n]每行皇后的列索引數(shù)組
*/
void putQueen (int row) {
for (int col = 0; col < n; col++) {
if (column[col] == true && leftDiagonal[row+col] == true
&& rightDiagonal[row-col+n-1] == true) {
positionInRow[row] = col;
column[col] = false;
leftDiagonal[row+col] = false;
rightDiagonal[row-col+n-1] = false;
if (row < n - 1) {
putQueen(row+1);
} else {
printBoard(cout);
}
column[col] = true;
leftDiagonal[row+col] = true;
rightDiagonal[row-col+n-1] = true;
}
}
}
遞歸的效率常常比等價(jià)迭代形式更低,但其具有清晰性、可讀性和簡(jiǎn)單性的特點(diǎn)。每個(gè)遞歸都可以轉(zhuǎn)化為迭代形式,但轉(zhuǎn)化過(guò)程并不總是很容易。以下兩種場(chǎng)合下非遞歸的實(shí)現(xiàn)方式更可取:第一種是在實(shí)時(shí)系統(tǒng)中,如軍事環(huán)境、航天器和科學(xué)實(shí)驗(yàn)中;第二種情況是需要避免使用遞歸的情況,如編譯器。但是有時(shí)遞歸操作比非遞歸實(shí)現(xiàn)更快,如硬件帶有內(nèi)置棧操作。在計(jì)算結(jié)果時(shí)候,當(dāng)某個(gè)部分毫無(wú)必要的重復(fù)就應(yīng)該避免遞歸的使用。通常繪制一個(gè)調(diào)用樹(shù)很有必要,如果樹(shù)太深運(yùn)行時(shí)棧就有溢出的風(fēng)險(xiǎn),如果樹(shù)淺且茂密遞歸就是一個(gè)很好的方法。
6 二叉樹(shù)
6.1 查找
二叉樹(shù)查找算法復(fù)雜性由查找過(guò)程中比較次數(shù)來(lái)度量。復(fù)雜度為到達(dá)某個(gè)節(jié)點(diǎn)的路徑長(zhǎng)度加1。內(nèi)部路徑長(zhǎng)度(IPL)是所有節(jié)點(diǎn)的所有路徑長(zhǎng)度總和,平均路徑深度可以代表查找一個(gè)節(jié)點(diǎn)的平均復(fù)雜度。在最壞情況下,即當(dāng)樹(shù)退化為鏈表時(shí)候,path = O(n)。最好情況下,即當(dāng)樹(shù)完全平衡時(shí),path = h - 2。平均情況下path = O(lg n)。
6.2 遍歷
二叉樹(shù)的遍歷方法根據(jù)樹(shù)的訪問(wèn)策略分很多,重要的是廣度優(yōu)先遍歷和深度優(yōu)先遍歷。
6.2.1 廣度優(yōu)先遍歷
廣度優(yōu)先遍歷先從樹(shù)的最高層開(kāi)始,從左向右逐層向下訪問(wèn)樹(shù)中的每一個(gè)元素。
template<class T>
void BST<T>::breadthFirst() {
Queue<BSTNode<T>*> queue;
BSTNode<T> *p = root;
if (p != 0) {
queue.enqueue(p);
while(!queue.empty()) {
p = queue.dequeue();
visit(p);
if (p->left != 0) {
queue.enqueue(p->left);
}
if (p->right != 0) {
queue.enqueue(p->right);
}
}
}
}
6.2.1 深度優(yōu)先遍歷
深度優(yōu)先遍歷會(huì)盡量從根節(jié)點(diǎn)訪問(wèn)到葉節(jié)點(diǎn),再回溯至最近一次有未訪問(wèn)子節(jié)點(diǎn)的節(jié)點(diǎn),再訪問(wèn)到其葉節(jié)點(diǎn)。根據(jù)其訪問(wèn)節(jié)點(diǎn)的先后順序可以有多種訪問(wèn)的方式,但常用的主要是前序樹(shù)遍歷、中序樹(shù)遍歷和后序樹(shù)遍歷。
- 前序樹(shù)遍歷
template<class T>
void BST<T>::preorder(BSTNode<T> *p) {
if (p != 0) {
visit(p);
preorder (p->left);
preorder (p->right);
}
}
- 中序樹(shù)遍歷
template<class T>
void BST<T>::inorder(BSTNode<T> *p) {
if (p != 0) {
inorder (p->left);
visti(p);
inorder (p->right);
}
}
- 后序樹(shù)遍歷
template<class T>
void BST<T>::postorder(BSTNode<T> *p) {
if (p != 0) {
postorder (p->left);
postorder (p->right);
visti(p);
}
}
但是使用遞歸函數(shù)會(huì)給系統(tǒng)帶來(lái)很大的負(fù)擔(dān),有運(yùn)行時(shí)棧溢出的危險(xiǎn),因此可以顯示的使用棧來(lái)遍歷二叉樹(shù)。
??另外對(duì)于特定形狀的樹(shù),可能需要將所有節(jié)點(diǎn)放入棧中,這樣會(huì)使用大量的空間。可以通過(guò)線索樹(shù)和樹(shù)的轉(zhuǎn)換的方式遍歷樹(shù)。
- 線索樹(shù)遍歷
普通的二叉樹(shù)的每個(gè)節(jié)點(diǎn)最多可以有兩個(gè)指針,分別指向其左右子節(jié)點(diǎn),為每個(gè)節(jié)點(diǎn)擴(kuò)充兩個(gè)分別指向前驅(qū)和后繼節(jié)點(diǎn)的指針,這樣包含線索的樹(shù)稱(chēng)為線索樹(shù)。但是通常我們可以通過(guò)重載已有指針的方式使用兩個(gè)指針變量,和一個(gè)標(biāo)識(shí)變量來(lái)同時(shí)維護(hù)后繼、左、右子節(jié)點(diǎn)信息。其中標(biāo)識(shí)變量標(biāo)識(shí)當(dāng)前節(jié)點(diǎn)的right指針代表的是右子節(jié)點(diǎn)還是后繼節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)最多擁有上述三個(gè)節(jié)點(diǎn)信息中的兩個(gè)。線索樹(shù)的插入操作在后面討論,這里只討論遍歷操作。擁有后繼、左和右子節(jié)點(diǎn)的線索樹(shù)可以進(jìn)行前序、中序和后序遍歷,這里只討論中序遍歷。
template<class T>
void ThreadedTree<T>::inorder() {
ThreadedNode<T> *prev, *p = root;
if (p != 0) {
while (p->left != 0) {
p = p->left;
}
while (p != 0) {
visit(p);
prev = p;
p = p->right;
if (p != 0 && prev->successor == 0) {
while (p->left != 0) {
p = p->left;
}
}
}
}
}
- Morris遍歷法
Morris開(kāi)發(fā)了一個(gè)精致的算法不使用棧和線索樹(shù),僅通過(guò)臨時(shí)將樹(shù)退化為鏈表的方式也能完成樹(shù)的遍歷,當(dāng)然在遍歷結(jié)束時(shí)需要恢復(fù)樹(shù)的結(jié)構(gòu)。
template<class T>
void BST<T>::MorrisInorder() {
BSTNode<T> *p = root, *tmp;
while (p != 0) {
if (p->left == 0) {
visit(p);
p = p->right;
} else {
tmp = p->left;
while (tmp->right != 0 && tmp->right != p) {
tmp = tmp->right;
}
if (tmp->right == 0) {
//改變樹(shù)結(jié)構(gòu)
tmp->right = p;
p = p->left;
} else {
visit(p);
//恢復(fù)樹(shù)結(jié)構(gòu)
tmp->right = 0;
p = p->right;
}
}
}
}
6.3 插入
6.3.1 一般樹(shù)的插入操作
template<class T>
void BST<T>::insert(const T &el) {
BSTNode<T> *p = root, *prev = 0;
while (p != 0) {
prev = p;
if (el < p->el) {
p = p->left;
} else {
p = p->right;
}
}
if (root == 0) {
root = new BSTNode<T>(el);
} else if (el < prev->el) {
prev->left = new BSTNode<T>(el);
} else {
prev-right = new BSTNode<T>(el);
}
}
6.3.1 線索樹(shù)的插入操作
這里只討論由左子節(jié)點(diǎn)、右子節(jié)點(diǎn)和后繼節(jié)點(diǎn)組成的線索樹(shù),除根節(jié)點(diǎn)外每個(gè)節(jié)點(diǎn)必有其右子節(jié)點(diǎn)后者后繼節(jié)點(diǎn)中的一個(gè)。
template<class T>
void ThreadedTree<T>::insert(const T &el) {
ThreadedNode<T> *p, *prev = 0, *newNode;
NewNode = new ThreadedNode<T>(el);
if (root == 0) {
Root = newNode;
return;
}
p = root;
while (p != 0) {
prev = p;
if (p->key > el) {
p = p->left;
} else if (p->successor == 0) {
p = p->right;
} else {
break;
}
if (prev->key > el) {
prev->left = newNode;
newNode->successor = 1;
newNode->right = prev;
} else if (prev->successor == 1) {
newNode->successor = 1;
prev->successor = 0;
newNode->right = prev->right;
prev->right = newNode;
} else {
prev->right = newNode;
}
}
}
6.4 刪除
刪除一個(gè)樹(shù)中的節(jié)點(diǎn)分為1)刪除一個(gè)子節(jié)點(diǎn),2)刪除的節(jié)點(diǎn)只有一個(gè)子節(jié)點(diǎn),3)刪除的節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn)。在面對(duì)前兩種情況時(shí)很容易刪除一個(gè)節(jié)點(diǎn),在處理第三種情況時(shí)根據(jù)對(duì)樹(shù)進(jìn)一步處理分為合并刪除和復(fù)制刪除。
6.4.1 合并刪除
合并刪除在處理有兩個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)P時(shí)通過(guò)找到左子樹(shù)的最大節(jié)點(diǎn)LMax,并使P的右子樹(shù)RTree成為節(jié)點(diǎn)LMax的右子樹(shù)。或者找到右子樹(shù)的最下節(jié)點(diǎn)RMin,并使節(jié)點(diǎn)P的左子樹(shù)LTree成為節(jié)點(diǎn)RMin的左子樹(shù)。合并刪除的缺點(diǎn)是隨著刪除節(jié)點(diǎn)的進(jìn)行可能導(dǎo)致樹(shù)高度增加,生成高度不平衡的樹(shù)。
template<class T>
//使用指針的引用變量做形參可以在函數(shù)內(nèi)部更改調(diào)用處的參數(shù)值
void BST<T>::deleteByMerging(BSTNode<T> *& node) {
BSTNode<T> *tmp = node;
if (node != 0) {
if (node->right == 0) {
node = node->left;
} else if (node->left == 0) {
node = node->right;
} else {
tmp = node->left;
while (tmp->right != 0) {
tmp = tmp->right;
}
//這里使用上文中的第一種1方案
tmp->right = node->right;
tmp = node;
node = node->left;
}
delete tmp;
}
}
如果再樹(shù)中刪除一個(gè)節(jié)點(diǎn)將查找和刪除操作分開(kāi)非常不合理,因?yàn)楹喜h除方法在調(diào)用的時(shí)候希望直接將需要?jiǎng)h除節(jié)點(diǎn)的指針存入父節(jié)點(diǎn)中,這樣組合后將它們的引用變量作為實(shí)參傳入函數(shù)才能在函數(shù)嫩不改變這個(gè)實(shí)參的值。
template<class T>
void BST<T>::findAndDeleteByMerging(const T &el) {
BSTNode<T> *node = root, *prev = 0;
While (node != 0) {
if (node->key == el) {
break;
}
prev = node;
if (node->key < el) {
node = node->right;
} else {
node = node->left;
}
}
if (node != 0 && node->key == el) {
if (node == root) {
deleteByMerging(root);
} else if (prev->left == node) {
deleteByMerging(prev->left);
} else {
deleteByMerging(prev->right);
}
} else if (root != 0) {
cout << "key" << el << "is not in the tree\n"
} else {
cout << "the tree is empty\n";
}
}
6.4.1 復(fù)制刪除
復(fù)制刪除可以在很大程度上解決合并刪除樹(shù)高度不斷增加的問(wèn)題,同樣它也可以通過(guò)將其前驅(qū)即左子樹(shù)中的最大節(jié)點(diǎn)和其后繼即右子樹(shù)中的最小節(jié)點(diǎn)復(fù)制到需要?jiǎng)h除的節(jié)點(diǎn)處。但是多次刪除后樹(shù)也會(huì)出現(xiàn)不平衡現(xiàn)象。可以通過(guò)交替的替代前驅(qū)和后繼節(jié)點(diǎn)來(lái)盡量使樹(shù)保持平衡。實(shí)驗(yàn)表明對(duì)于n個(gè)節(jié)點(diǎn)的樹(shù)多次插入和非對(duì)稱(chēng)復(fù)制刪除后IPL期望值為Θ(n lg3 n)。當(dāng)使用對(duì)稱(chēng)Θ(n lg n)。
template<class T>
void BST<T>::deleteByCopying(BSTNode<T>* & node) {
BSTNode<T> *previous, *tmp = node;
if (node->right == 0) {
node = node->left;
} else if (node->left == 0) {
node = node->right;
} else {
tmp = node->left;
previous = node;
while (tmp->right != 0) {
previous = tmp;
tmp = tmp->right;
}
node->el = tmp->el;
if (previous == node) {
previous->left = tmp->left;
} else {
previous->right = tmp->left;
}
}
delete tmp;
}
6.5 樹(shù)的平衡
盡管前面很小心的對(duì)樹(shù)進(jìn)行刪除操作,但是仍不能完全避免樹(shù)的不平衡現(xiàn)象,因此我們?cè)诒匾臅r(shí)候需要進(jìn)行平衡樹(shù)操作。效率最低的辦法是將所有數(shù)據(jù)放入一個(gè)數(shù)組中,通過(guò)排序算法將數(shù)組排序,通過(guò)特定的方式重建樹(shù)。此方法的改進(jìn)是通過(guò)中序樹(shù)遍歷得到遞增數(shù)組,重建樹(shù),但是其效率仍然低下。下面討論更高效的平衡樹(shù)算法。
6.5.1 DSW算法
DSW算法的核心操作是將一個(gè)節(jié)點(diǎn)ch圍繞其父節(jié)點(diǎn)pa進(jìn)行左旋轉(zhuǎn)或者右旋轉(zhuǎn)。第一階段該算法將樹(shù)旋轉(zhuǎn)退化為類(lèi)似鏈結(jié)構(gòu),并從根到子節(jié)點(diǎn)遞增,第二階段創(chuàng)建完全平衡樹(shù)。該算法創(chuàng)建主鏈最多需要O(n)次旋轉(zhuǎn),創(chuàng)建完全平衡樹(shù)也只需要O(n)次旋轉(zhuǎn)。
6.5.2 AVL樹(shù)
通常我們?cè)趯?duì)樹(shù)進(jìn)行操作時(shí)只需要對(duì)樹(shù)的部分進(jìn)行平衡操作。AVL樹(shù)也被稱(chēng)作可容許樹(shù),要求每個(gè)節(jié)點(diǎn)的左右子樹(shù)高度差為1。AVL樹(shù)的高度h受限于:lg(n+1) <= h < 1.44lg(n+2)-0.328。對(duì)于比較大的n,平均查找次數(shù)為lgn+0.25次。AVL樹(shù)在進(jìn)行插入和刪除操作時(shí)都必須實(shí)時(shí)更新平衡因子,當(dāng)平衡因子大于±1時(shí)則通過(guò)旋轉(zhuǎn)的方式對(duì)樹(shù)的部分進(jìn)行平衡,并繼續(xù)向父節(jié)點(diǎn)更新平衡因子,直至當(dāng)某個(gè)節(jié)點(diǎn)的平衡因子不發(fā)生變化或者到根節(jié)點(diǎn)時(shí)停止操作。
??另外AVL樹(shù)可以擴(kuò)展,平衡因子閾值可以調(diào)高,閾值越高,其平均查找效率越低,平均平衡效率越高。
6.6 自適應(yīng)樹(shù)
雖然平衡樹(shù)能使樹(shù)的平均路徑深度得到有效降低,但是頻繁的對(duì)樹(shù)進(jìn)行平衡操作會(huì)造成很大的性能浪費(fèi),因?yàn)橥ǔN覀兏P(guān)心執(zhí)行插入、刪除和查找操作的效率而不是樹(shù)的形狀。因?yàn)槲覀儗?duì)不同元素的訪問(wèn)有偏好性,因此根據(jù)訪問(wèn)頻率沿著樹(shù)向上移動(dòng)元素從而形成一種優(yōu)先樹(shù)即自適應(yīng)樹(shù)是一個(gè)很好的解決方案。
??自適應(yīng)樹(shù)的構(gòu)造策略分為:1)單一旋轉(zhuǎn):如果訪問(wèn)子節(jié)點(diǎn),則將子節(jié)點(diǎn)圍繞它的父節(jié)點(diǎn)進(jìn)行旋轉(zhuǎn)。2)移動(dòng)到根部:重復(fù)子節(jié)點(diǎn)-父節(jié)點(diǎn)的旋轉(zhuǎn),直至將被訪問(wèn)元素移到根部。
6.6.1 張開(kāi)策略
張開(kāi)策略是移動(dòng)到根部的一個(gè)修改版本。其根據(jù)子節(jié)點(diǎn)、父節(jié)點(diǎn)和祖父節(jié)點(diǎn)之間的鏈接關(guān)系的順序,成對(duì)的使用單一旋轉(zhuǎn)。主要有兩種鏈接關(guān)系:1)同構(gòu)配置:節(jié)點(diǎn)RQ同為左子節(jié)點(diǎn)或同為右子節(jié)點(diǎn);2)異構(gòu)配置:節(jié)點(diǎn)RQ分別為左右子節(jié)點(diǎn)中的一個(gè)。
splaying (P, Q, R) {
//R、Q、P分別為被訪問(wèn)節(jié)點(diǎn)、其父節(jié)點(diǎn)和祖父節(jié)點(diǎn)
while R不是根節(jié)點(diǎn)
if (R的父節(jié)點(diǎn)是根節(jié)點(diǎn)) {
進(jìn)行單一張開(kāi)操作,使R圍繞其父節(jié)點(diǎn)進(jìn)行旋轉(zhuǎn)
} else if (R與其前驅(qū)同構(gòu)) {
首先圍繞P旋轉(zhuǎn)Q,再圍繞Q旋轉(zhuǎn)R
} else {
首先圍繞Q旋轉(zhuǎn)R,再圍繞P旋轉(zhuǎn)R
}
}
由于每次訪問(wèn)自適應(yīng)樹(shù)后,其樹(shù)的結(jié)構(gòu)都會(huì)發(fā)生變化,因此因使用攤銷(xiāo)復(fù)雜度來(lái)計(jì)算訪問(wèn)節(jié)點(diǎn)的復(fù)雜度。其單詞訪問(wèn)節(jié)點(diǎn)的攤銷(xiāo)復(fù)雜度為O(lgn),對(duì)于一系列的m次訪問(wèn),其效率為O(m*lgn)。
6.6.2 半張開(kāi)策略
由于使用張開(kāi)策略經(jīng)常訪問(wèn)靠近根部的元素會(huì)使樹(shù)不平衡,因此考慮其優(yōu)化方法。半張開(kāi)是張開(kāi)策略的一個(gè)修改版本,它可以更加平衡,對(duì)于同構(gòu)情況,該策略只需進(jìn)行一次選擇,然后繼續(xù)張開(kāi)被訪問(wèn)節(jié)點(diǎn)的父節(jié)點(diǎn)。
6.7 堆
堆是一種特殊類(lèi)型的二叉樹(shù),通常可以分為最大堆和最小堆,最大堆具有以下性質(zhì)1)每個(gè)節(jié)點(diǎn)的值大于等于其每個(gè)子節(jié)點(diǎn)的值;2)該樹(shù)完全平衡,最后一層的葉子位于最左側(cè)的位置。當(dāng)?shù)谝粋€(gè)條件的大于變?yōu)樾∮跁r(shí)候則是最小堆。如果將一個(gè)堆通過(guò)廣度優(yōu)先算法遍歷得打一個(gè)數(shù)組,則數(shù)組元素中節(jié)點(diǎn)和其頁(yè)節(jié)點(diǎn)的序號(hào)對(duì)應(yīng)從前往后分別為(x :2x+1,2x+2)(其中x從1遞增至n/2)。
??將元素加入堆需要將元素加到堆末尾再逐層向上恢復(fù)堆的特性。在堆中刪除元素需要?jiǎng)h除根元素,因?yàn)槠鋬?yōu)先級(jí)最高,再將最后一個(gè)葉節(jié)點(diǎn)放在根上,再恢復(fù)堆屬性。
6.7.1 用堆實(shí)現(xiàn)優(yōu)先隊(duì)列
堆很適合用于實(shí)現(xiàn)優(yōu)先隊(duì)列,通過(guò)鏈表實(shí)現(xiàn)的優(yōu)先隊(duì)列的結(jié)構(gòu)復(fù)雜度是O(√n),而在堆中到達(dá)葉節(jié)點(diǎn)只需要O(lg n)次查找。
6.7.2 用數(shù)組實(shí)現(xiàn)堆
可以通過(guò)廣度優(yōu)先法遍歷堆得到的數(shù)組來(lái)表示一個(gè)堆。將數(shù)組轉(zhuǎn)化為堆的方法主要分從空堆創(chuàng)建和合并小堆兩種方式。
- 從空堆開(kāi)始創(chuàng)建:將數(shù)組中的元素挨個(gè)取出,創(chuàng)建一個(gè)堆。這個(gè)方法在最壞情況下的交換次數(shù)和比較次數(shù)為O(n lg n)。
- 合并小堆: 為了增加算法的效率可以采用合并小堆的方式,該算法從最后一個(gè)非葉節(jié)點(diǎn)data[n/2 - 1]開(kāi)始創(chuàng)建一個(gè)子樹(shù),并恢復(fù)堆屬性同事交換數(shù)組中的元素,直至處理完第一個(gè)根節(jié)點(diǎn)。最壞情況下改算法的移動(dòng)次數(shù)和比較次數(shù)都是O(n)。
最壞情況下合并小堆的方法效率更高,但是評(píng)價(jià)情況下兩個(gè)算法的效率處于同一水平。
6.8 treap樹(shù)
二叉查找樹(shù)的操作非常高效,但多次操作時(shí)會(huì)發(fā)生樹(shù)的不平衡現(xiàn)象,堆是完全平衡樹(shù),可以快速訪問(wèn)最大或者最小元素,但是不能立即訪問(wèn)其他元素。如果有一個(gè)樹(shù)同時(shí)滿足堆的部分性質(zhì)和二叉查找樹(shù)的部分性質(zhì)的樹(shù)稱(chēng)為他treap樹(shù)。它有多種實(shí)現(xiàn)方式。
6.8.1 顯示優(yōu)先級(jí)實(shí)現(xiàn)-笛卡爾樹(shù)
對(duì)于它的每個(gè)節(jié)點(diǎn)包含一個(gè)鍵值對(duì),其中鍵滿足二叉樹(shù)性質(zhì),值滿足最大堆性質(zhì)。
??在其中插入元素時(shí),首先生成隨機(jī)的優(yōu)先級(jí),用鍵根據(jù)二叉查找樹(shù)性質(zhì)在樹(shù)中找到合適的位置插入,再通過(guò)值通過(guò)旋轉(zhuǎn)二叉樹(shù)方式來(lái)恢復(fù)堆屬性。
??刪除其中元素時(shí)將其優(yōu)先級(jí)較高的節(jié)點(diǎn)圍繞它進(jìn)行旋轉(zhuǎn)直至被刪除的節(jié)點(diǎn)只有一個(gè)子節(jié)點(diǎn)后者沒(méi)有子節(jié)點(diǎn),此時(shí)直接將改元素刪除。
6.8.2 隱式優(yōu)先級(jí)實(shí)現(xiàn)
treap樹(shù)并不總是需要在每個(gè)節(jié)點(diǎn)儲(chǔ)存其優(yōu)先級(jí),第一種方法是使用一個(gè)散列函數(shù)h,將具有鍵值k的某項(xiàng)優(yōu)先級(jí)設(shè)置為h(k),但這種方案暫不討論。另外一種是通過(guò)數(shù)組分方式實(shí)現(xiàn)treap樹(shù),其中數(shù)組的序號(hào)代表其優(yōu)先級(jí),這種方式類(lèi)似于最小堆。但是節(jié)點(diǎn)和子節(jié)點(diǎn)在數(shù)組中序號(hào)的對(duì)應(yīng)方式不能套用堆中的公式。
??這種方案中插入一個(gè)節(jié)點(diǎn),需要隨機(jī)生成小于等于n的優(yōu)先級(jí)i,如果i=n,則直接將節(jié)點(diǎn)放在數(shù)組末尾,否則需要將數(shù)組中占據(jù)i位置的項(xiàng)通過(guò)一系列的旋轉(zhuǎn)操作變?yōu)槿~節(jié)點(diǎn),再將需要插入的節(jié)點(diǎn)根據(jù)二叉樹(shù)的性質(zhì)放在合適的位置,再根據(jù)其優(yōu)先級(jí)恢復(fù)整個(gè)的堆性質(zhì)就能得到新的treap樹(shù)。在數(shù)組中直接插入對(duì)應(yīng)索引即得到新的數(shù)組。
??刪除一個(gè)節(jié)點(diǎn)時(shí),首先從treap樹(shù)中刪除節(jié)點(diǎn),先通過(guò)二叉樹(shù)刪除節(jié)點(diǎn)規(guī)則將節(jié)點(diǎn)刪除,然后在數(shù)組中將最后一個(gè)元素填到當(dāng)前位置確定新的游優(yōu)先級(jí),再根據(jù)新的優(yōu)先級(jí)恢復(fù)堆屬性。
6.9 k-d樹(shù)
通常二叉查找樹(shù)的每個(gè)節(jié)點(diǎn)只有一個(gè)鍵值,當(dāng)每個(gè)節(jié)點(diǎn)擁有多個(gè)鍵值時(shí)成為k-d樹(shù),k代表每個(gè)節(jié)點(diǎn)擁有的鍵值數(shù)。k-d樹(shù)將各個(gè)維度在從根到子節(jié)點(diǎn)的每一層中有順序的交替使用。通過(guò)這種方式可以在空間中劃分很多不同的區(qū)域。
6.9.1 插入節(jié)點(diǎn)
void insert (el) {
i = 0;
p = root;
prev = 0;
while p != 0 {
prev = p;
if (el.keys[i] < p->el.keys[i]) {
p = p->left;
} else {
p = p->right;
}
i = (i+1) mod k;
}
if (root == 0) {
root = new BSTNode(el);
} else if (el.keys[(i-1) mod k] < p-el.keys[(i-1) mod k]) {
prev->left = new BSTNode(el);
} else {
prev->right = new BSTNode(el);
}
}
6.9.2 查找節(jié)點(diǎn)
最壞情況下,具有n個(gè)節(jié)點(diǎn)的完全k-d樹(shù)中執(zhí)行查找操作的復(fù)雜度為O(k*n^(1-1/k))。
search(ranges[][]) {
if (root != 0) {
search(root,0,ranges)
}
}
search(p, i, ranges[][]) {
found = true;
for (j = 0 ~ k-1) {
if !(ranges[j][0] <= p->el.keys[j] <= ranges[j][1]) {
found = false;
break;
}
}
if (found) {
輸出p->el;
}
if (p->left != 0 并且 ranges[i][0] <= p->el.keys[i]) {
search(p->left, (i+1) mod k, ranges);
}
if (p->right != 0 并且 p->el.keys[i] <= ranges[i][1]) {
search(p->right, (i+1) mod k, ranges);
}
}
6.9.3 刪除節(jié)點(diǎn)
k-d樹(shù)中刪除節(jié)點(diǎn)不能完全套用二叉樹(shù)的方法,因?yàn)樾枰獎(jiǎng)h除的節(jié)點(diǎn)N的標(biāo)識(shí)位i,其下一層的節(jié)點(diǎn)標(biāo)識(shí)位變?yōu)閖,因此其前驅(qū)節(jié)點(diǎn)即在i標(biāo)識(shí)位上取得最低值的節(jié)點(diǎn)可能在N的左子節(jié)點(diǎn)NL的下一層節(jié)點(diǎn)的左右子樹(shù)中,這和二叉樹(shù)不同。當(dāng)刪除一個(gè)節(jié)點(diǎn)只如果該節(jié)點(diǎn)是葉節(jié)點(diǎn),直接將其刪除;如果含有右子節(jié)點(diǎn)則在右子樹(shù)中找到和N在相同的標(biāo)識(shí)位上最小值的節(jié)點(diǎn),采用復(fù)制刪除法的方式刪除節(jié)點(diǎn);如果沒(méi)有右子節(jié)點(diǎn),則在左子樹(shù)中找到符合上述要求的節(jié)點(diǎn),但是在刪除后將其其左子樹(shù)變?yōu)橛摇W訕?shù)同樣的后繼節(jié)點(diǎn)也和二叉樹(shù)中不同。刪除隨機(jī)選擇的節(jié)點(diǎn)的復(fù)雜度為O(lg n)。
delete(el) {
p = 包含el的節(jié)點(diǎn);
delete(p, p的識(shí)別字索引i);
}
delete(p) {
if (p是葉節(jié)點(diǎn)) {
刪除p;
} else if (p->right != 0) {
q = smallest(p->right, i, (i+1) mod k);
} else {
q = smallest(p->left, i, (i+1) mod k);
p->right = p->left;
p->left = 0;
}
p->el = q->el;
delete(q, i);
}
smallest(q, i, j) {
qq = q;
if (i == j) {
if (q->left != 0) {
qq = q = q->left;
} else {
return q;
}
}
if (q->left != 0) {
lt = smallest(q->left, i, (j+1) mod k);
if (qq->el.keys[i] >= lt->el.keys[i]) {
qq = lt;
}
}
if (q->right != 0) {
rt = smallest(q->right, i, (j+1) mod k);
if (qq->el.keys[i] >= rt->el.keys[i]) {
qq = rt;
}
}
return qq;
}
6.10 表達(dá)式樹(shù)和波蘭表達(dá)式法
波蘭表達(dá)式法是不使用括號(hào)來(lái)無(wú)歧義的表示一個(gè)代數(shù)、關(guān)系或邏輯表達(dá)式的方法。而通過(guò)遍歷二叉樹(shù)得到這種表達(dá)式的樹(shù)成為表達(dá)式樹(shù)。根據(jù)遍歷表達(dá)式樹(shù)的方式將不同的轉(zhuǎn)換方法分為前綴表示法、中綴表示法和后綴表示法。優(yōu)勢(shì)中綴表示法并不能得到無(wú)歧義的波蘭表達(dá)式。
??表達(dá)式樹(shù)的結(jié)構(gòu)很適合在編譯器中生成中間代碼,另外表達(dá)式樹(shù)葉可以很好的執(zhí)行微分操作。
6.10.1 創(chuàng)建表達(dá)式樹(shù)
可以通過(guò)將表達(dá)式拆解為加減法連接的項(xiàng)term,乘除法表示的因子factor,和括號(hào)表示的表達(dá)式expr來(lái)構(gòu)建波蘭表達(dá)式樹(shù)。
term () {
ExprTreeNode *p1, *p2;
p1 = factor();
while (token 是 *或/) {
oper = token;
p2 = factor();
p1 = new ExprTreeNode(oper, p1, p2);
}
return p1;
}
factor() {
if (token是一個(gè)數(shù),id或者操作符) {
return new ExprTreeNode(token);
} else if (token 是 "(") {
ExprTreeNode *p = expr ();
if (token 是 ")") {
return p;
} else {
錯(cuò)誤
}
}
}
7 多叉樹(shù)
二叉樹(shù)中每個(gè)節(jié)點(diǎn)只有兩個(gè)子節(jié)點(diǎn),當(dāng)每個(gè)節(jié)點(diǎn)最多包含m個(gè)節(jié)點(diǎn)時(shí)就是m階多叉樹(shù)。但是我們通常只關(guān)注多差查找樹(shù)。對(duì)于m階的多差查找樹(shù)有以下四個(gè)特性。
- 1)每個(gè)節(jié)點(diǎn)都可以包含m個(gè)子節(jié)點(diǎn)和m-1個(gè)鍵值。
- 2)所有節(jié)點(diǎn)的鍵值都按升序排列。
- 3)前i個(gè)子節(jié)點(diǎn)中的鍵值都小于第i個(gè)鍵值。
- 4)后m-i個(gè)子節(jié)點(diǎn)中的鍵值都大于第i個(gè)鍵值。
但是如果不對(duì)多叉樹(shù)的結(jié)構(gòu)進(jìn)行有效的限制時(shí),當(dāng)多叉樹(shù)變得極不平衡時(shí),對(duì)其操作的成本將會(huì)變得很大,通常我們常用的是B樹(shù)。
7.1 B樹(shù)家族
7.1.1 B樹(shù)
對(duì)于在硬盤(pán)存儲(chǔ)大塊數(shù)據(jù),如果使用二叉樹(shù)的方式則每個(gè)節(jié)點(diǎn)可能放在磁盤(pán)的不同塊上,這樣查找、刪除和插入節(jié)點(diǎn)時(shí)需要不斷的在磁盤(pán)的不同軌中來(lái)回讀取數(shù)據(jù)。因此我們應(yīng)該盡量減少節(jié)點(diǎn)的訪問(wèn)次數(shù),同時(shí)應(yīng)盡量使單個(gè)節(jié)點(diǎn)的容量和單個(gè)磁盤(pán)塊大小相等。B樹(shù)就是按這個(gè)目的設(shè)計(jì)的一種數(shù)據(jù)結(jié)構(gòu)。
??m階的B樹(shù)具有以下性質(zhì)
- 1)除葉節(jié)點(diǎn)外,根節(jié)點(diǎn)至少有兩個(gè)子樹(shù)
- 2)每個(gè)非根非葉節(jié)點(diǎn)都有k-1個(gè)鍵值和k個(gè)指向子樹(shù)的指針 (其中m/2的上界整數(shù)<= k <= m)
- 3)每個(gè)葉節(jié)點(diǎn)都有k-1個(gè)鍵值(其中m/2的上界整數(shù) <= k <= m)
- 4)所有的葉節(jié)點(diǎn)都在同一層
B樹(shù)的每一個(gè)節(jié)點(diǎn)包含的鍵值可以直接表示為要存儲(chǔ)的數(shù)據(jù)數(shù)組,或者是一個(gè)對(duì)象數(shù)組,對(duì)象數(shù)組中每個(gè)對(duì)象都由一個(gè)標(biāo)識(shí)符和一個(gè)指向輔存的地址組成。從長(zhǎng)遠(yuǎn)來(lái)看第二種實(shí)現(xiàn)方式更優(yōu),因?yàn)檫@樣節(jié)點(diǎn)中可以盡量少存儲(chǔ)數(shù)據(jù)。
7.1.1.1 插入元素
除了當(dāng)前樹(shù)的元素總數(shù)只能支撐一個(gè)根節(jié)點(diǎn)存在外,B樹(shù)中插入元素第一步都會(huì)放到某個(gè)葉節(jié)點(diǎn)中。保證每個(gè)節(jié)點(diǎn)插入后B樹(shù)的性質(zhì)不會(huì)發(fā)生改變是一件很復(fù)雜的事情。
??插入節(jié)點(diǎn)時(shí)會(huì)遇到以下情況。1)鍵值被放入尚有空間的葉節(jié)點(diǎn)中:此時(shí)只需要插入在合適的位置即可。2)要插入的葉節(jié)點(diǎn)鍵值已滿,此時(shí)需要分解葉節(jié)點(diǎn),同時(shí)新建節(jié)點(diǎn),并在原葉節(jié)點(diǎn)、新葉節(jié)點(diǎn)和父節(jié)點(diǎn)中重新分配順序,并更新父節(jié)點(diǎn)指針。3)當(dāng)根節(jié)點(diǎn)是滿的時(shí)候,此時(shí)必須創(chuàng)建一個(gè)新的根節(jié)點(diǎn)以及與原節(jié)點(diǎn)同級(jí)的節(jié)點(diǎn)。
??插入操作時(shí)可以進(jìn)行預(yù)分解策略防止溢出不斷向上蔓延。具體實(shí)施方法是,當(dāng)未要插入的節(jié)點(diǎn)查找合適位置的過(guò)程中,遇到已滿的節(jié)點(diǎn)就預(yù)先分解。
??節(jié)點(diǎn)的容量和其分解的概率成正相關(guān)關(guān)系,當(dāng)m=10時(shí),概率為0.25;當(dāng)m=100時(shí),概率為0.02,m=1000時(shí),概率為0.002。
7.1.1.2 刪除元素
刪除元素很大程度上是插入元素的逆過(guò)程。1)葉節(jié)點(diǎn)刪除元素后滿足B樹(shù)性質(zhì)則不做多余操作。2)葉節(jié)點(diǎn)刪除元素后葉節(jié)點(diǎn)下溢,并左右節(jié)點(diǎn)數(shù)目超過(guò)節(jié)點(diǎn)鍵值下限,則在兩個(gè)節(jié)點(diǎn)和父節(jié)點(diǎn)中重新分配元素。3)葉節(jié)點(diǎn)刪除元素后葉節(jié)點(diǎn)下溢,并左右節(jié)點(diǎn)中沒(méi)有超過(guò)節(jié)點(diǎn)鍵值下限的節(jié)點(diǎn),則合并其中一個(gè)節(jié)點(diǎn)和他們父節(jié)點(diǎn)中他們之間的元素。4)從非葉節(jié)點(diǎn)中刪除鍵值,找到其前驅(qū)子節(jié)點(diǎn),將其中最大元素復(fù)制到該處,并刪除原來(lái)位置的鍵值。
7.1.2 B*樹(shù)
因?yàn)锽樹(shù)中每個(gè)節(jié)點(diǎn)都代表輔存中一個(gè)塊,因此每個(gè)節(jié)點(diǎn)的鍵值越飽和,創(chuàng)建的節(jié)點(diǎn)會(huì)更少,這樣效率會(huì)更高。B樹(shù)與B樹(shù)不同的地方在于,B樹(shù)要求節(jié)點(diǎn)是半滿的,而B(niǎo)樹(shù)要求節(jié)點(diǎn)是2/3滿,即k滿足 (2m-1)/3的下界整數(shù) <= k <= m-1,另外B樹(shù)在分解節(jié)點(diǎn)時(shí)講解的分解為3個(gè),B樹(shù)的平均使用率高達(dá)81%。
??另外在B樹(shù)中版本的要求標(biāo)識(shí)其充填因子為0.5,B*樹(shù)充填因子則為2/3,B^n樹(shù)允許自定義充填因子,其充填率為(n+1)/(n+2)。
7.1.3 B+樹(shù)
當(dāng)需要升序輸出B樹(shù)中的元素時(shí),盡管可以采用中序數(shù)遍歷方式,但是對(duì)于非終端節(jié)點(diǎn)需要多次訪問(wèn)該節(jié)點(diǎn)才能完成其所有鍵值的訪問(wèn),由于B樹(shù)存儲(chǔ)的不同節(jié)點(diǎn)是在輔存的不同塊中,這樣頻繁的在不同塊中移動(dòng)性能很低,因此引出B+樹(shù)。
??B+樹(shù)只有葉節(jié)點(diǎn)引用率數(shù)據(jù)。內(nèi)部節(jié)點(diǎn)構(gòu)成的集合稱(chēng)為索引集,子節(jié)點(diǎn)構(gòu)成的集合稱(chēng)為序列集。每個(gè)葉節(jié)點(diǎn)相教于B樹(shù)都多了一個(gè)指向下一個(gè)節(jié)點(diǎn)的指針。對(duì)于葉節(jié)點(diǎn)中的鍵值可以在它的父節(jié)點(diǎn)的中出現(xiàn)。
??B+樹(shù)的插入刪除操作類(lèi)似于B樹(shù),不同的是子節(jié)點(diǎn)合并時(shí),分界的鍵值是復(fù)制到父節(jié)點(diǎn)中而不是移動(dòng)。刪除葉節(jié)點(diǎn)引起下溢時(shí),合并葉節(jié)點(diǎn)并刪除父節(jié)點(diǎn)中的分界值。如果鍵值不在子節(jié)點(diǎn)中則刪除失敗,如果鍵值同時(shí)在子節(jié)點(diǎn)和非子節(jié)點(diǎn)中,只刪除子節(jié)點(diǎn)中的鍵值。
7.1.4 前綴B+樹(shù)
非終端節(jié)點(diǎn)中的鍵值主要是為查找子節(jié)點(diǎn),但是通常同一層次并屬于同一個(gè)父節(jié)點(diǎn)的非終端節(jié)點(diǎn)鍵值有很大的重復(fù)部分,如果我們?nèi)∷麄冩I值的最短前綴,并使之不會(huì)產(chǎn)生歧義。這樣的樹(shù)稱(chēng)為簡(jiǎn)單前綴B+樹(shù)。如果我們?cè)俸雎运麄児灿械那熬Y,僅保留能區(qū)分各個(gè)鍵值的部分,這樣的樹(shù)稱(chēng)為簡(jiǎn)化版本的前綴B+樹(shù),但這樣的樹(shù)僅停留在理論層面。
7.1.5 k-d B樹(shù)
k-d B樹(shù)是k-d樹(shù)的B樹(shù)版本,但是每個(gè)節(jié)點(diǎn)的鍵值數(shù)和指針數(shù)是相同的。其中葉節(jié)點(diǎn)保存K維空間的點(diǎn),非葉節(jié)點(diǎn)保存區(qū)域信息,有一個(gè)2*k維數(shù)組組成,每一列分別代表一個(gè)維度,第一行代表最小值,第二行代表最大值。葉節(jié)點(diǎn)保存的鍵值數(shù)和非葉節(jié)點(diǎn)保存的鍵值數(shù)并不一定相同。
??在k-d B樹(shù)中插入節(jié)點(diǎn)導(dǎo)致的重新分區(qū)問(wèn)題非常復(fù)雜。通常插入一個(gè)元素后會(huì)導(dǎo)致葉節(jié)點(diǎn)發(fā)生分裂,從而導(dǎo)致其父節(jié)點(diǎn)分裂,最后甚至導(dǎo)致其根節(jié)點(diǎn)分裂,因此會(huì)從下向上檢查溢出狀態(tài),直至不再溢出,再?gòu)南裣缕史止?jié)點(diǎn),同一個(gè)非終端節(jié)點(diǎn)中使用各個(gè)維度交替分區(qū),這與普通k-d樹(shù)中每一層有固定的分區(qū)標(biāo)識(shí)不同。
??k-d B樹(shù)刪除葉節(jié)點(diǎn)時(shí)如果發(fā)生下溢,可以合并節(jié)點(diǎn),但是節(jié)點(diǎn)合并僅能合并兩個(gè)相連仍是矩形區(qū)域的空間。
??由于普通k-d B樹(shù)區(qū)域只能是矩形,為了提高空間利用率,k-d B樹(shù)有多種改良版本,可以通過(guò)范圍節(jié)點(diǎn)實(shí)現(xiàn)k-b樹(shù),成為hB樹(shù),hB樹(shù)的字節(jié)點(diǎn)可多次被引用,從而劃分非矩形區(qū)域。
7.1.6 位樹(shù)
位數(shù)是前綴B+樹(shù)發(fā)揮到極致的狀態(tài),除了葉節(jié)點(diǎn)層上不再保留鍵值來(lái)區(qū)分不同的數(shù)據(jù),只用鍵值的二進(jìn)制差異位來(lái)區(qū)分,其余和前綴B+樹(shù)相同。位數(shù)在查詢到差異位后一定要將該數(shù)據(jù)的鍵值和所希望的鍵值比較,如果不同則查找失敗。
7.1.7 R樹(shù)
R樹(shù)是用來(lái)處理空間數(shù)據(jù)的一類(lèi)樹(shù),他對(duì)節(jié)點(diǎn)的飽和度沒(méi)有下限要求。其每個(gè)節(jié)點(diǎn)的指針數(shù)和數(shù)據(jù)項(xiàng)數(shù)是相同的,類(lèi)似于k-d B樹(shù),非終端子節(jié)點(diǎn)只負(fù)責(zé)分區(qū)僅包含分區(qū)信息和該分區(qū)下的子節(jié)點(diǎn)這種數(shù)據(jù)數(shù)組(rect,child),其中rect為k*2維數(shù)組,每一行代表一個(gè)維度,第一列代表下限,第二列代表上限。每個(gè)子節(jié)點(diǎn)的區(qū)域都被包含在父節(jié)點(diǎn)中,其插入和刪除操作類(lèi)似于k-d B樹(shù)。由于每個(gè)節(jié)點(diǎn)都表示了一個(gè)區(qū)域數(shù)組,因此單個(gè)節(jié)點(diǎn)的區(qū)域數(shù)組中的每個(gè)區(qū)域經(jīng)常發(fā)生重疊。
??為了消除R樹(shù)中的重疊現(xiàn)象,引入R+樹(shù),R+樹(shù)允許元素在葉節(jié)點(diǎn)中重復(fù)出現(xiàn),但同時(shí)禁止非葉節(jié)點(diǎn)區(qū)域相互重疊。
7.1.8 2-4樹(shù)
2-4樹(shù)指通過(guò)類(lèi)似于二叉樹(shù)的形式實(shí)現(xiàn)每個(gè)節(jié)點(diǎn)具有3個(gè)鍵值和4個(gè)指針的B樹(shù)。將B樹(shù)中的每個(gè)鍵值都轉(zhuǎn)換為一個(gè)節(jié)點(diǎn),并且B樹(shù)中一個(gè)節(jié)點(diǎn)的中間鍵值和其左右兩邊鍵值用水平(紅)指針相連,B樹(shù)中的每個(gè)節(jié)點(diǎn)的鍵值和其左右兩邊子節(jié)點(diǎn)的中間鍵值用垂直(黑)指針相連,這樣的2-4樹(shù)也可以稱(chēng)為垂直-水平樹(shù)(紅黑樹(shù))。
??2-4樹(shù)的插入操作采用了預(yù)分解策略。當(dāng)插入一個(gè)新元素時(shí)在查找的過(guò)程中需要根據(jù)需要對(duì)水平和垂直標(biāo)志做出更改。該樹(shù)的刪除操作可以通過(guò)復(fù)制刪除算法完成。
??另外AVL樹(shù)也可以轉(zhuǎn)化為垂直水平樹(shù),具體策略是將其中具有偶數(shù)高度根和該根具有偶數(shù)高度的子樹(shù)的子節(jié)點(diǎn)相連,并標(biāo)記為水平鏈接。
7.2 trie
trie是一種特殊的多叉樹(shù),其中葉節(jié)點(diǎn)為一個(gè)字符串,非葉節(jié)點(diǎn)由一個(gè)標(biāo)識(shí)從根到當(dāng)前葉節(jié)點(diǎn)路徑的字符串是否存在的變量和一系列鍵值組成,每個(gè)鍵值都為一個(gè)字符,并對(duì)應(yīng)一個(gè)指向子節(jié)點(diǎn)的指針,并且這個(gè)子節(jié)點(diǎn)下的所有葉節(jié)點(diǎn)都具有從根節(jié)點(diǎn)到當(dāng)前節(jié)點(diǎn)經(jīng)過(guò)的字符串形成的前綴。
??trie面臨的問(wèn)題是空間上巨大的浪費(fèi),某些節(jié)點(diǎn)可能會(huì)閑置大量空間。1)一種簡(jiǎn)單的辦法是只存儲(chǔ)需要的指針,但實(shí)現(xiàn)比較復(fù)雜,可以將同級(jí)節(jié)點(diǎn)放在一個(gè)鏈表中以2-4樹(shù)的方式實(shí)現(xiàn)。2)改變單詞的插入順序。3)通過(guò)將各個(gè)節(jié)點(diǎn)按一定的規(guī)律放在一個(gè)數(shù)組中來(lái)達(dá)到壓縮節(jié)點(diǎn)的目的。4)通過(guò)按一定規(guī)則創(chuàng)建二進(jìn)制版本的鍵值來(lái)達(dá)到壓縮的目的。
8 圖
通常圖有兩種常用的表示方法,1)鄰接表示法:通過(guò)鄰接表列出圖中所有相鄰的頂點(diǎn)。鄰接表也可以實(shí)現(xiàn)為鏈表。2)用矩陣表示:表示頂點(diǎn)間關(guān)系的鄰接矩陣表示法和表示頂點(diǎn)和邊關(guān)系的關(guān)聯(lián)矩陣表示法。
8.1 圖的遍歷
深度遍歷法。該算法可以保證生成至少一顆以上的生成樹(shù),因?yàn)閺哪硞€(gè)節(jié)點(diǎn)出發(fā)常常無(wú)法遍歷完整個(gè)圖,或者有不相連的子圖存在,因此不止一棵樹(shù)。生成樹(shù)中出現(xiàn)的邊成為正向邊,圖中有生成樹(shù)中沒(méi)有的邊稱(chēng)為負(fù)向邊。對(duì)于深度遍歷法其復(fù)雜度為O(|V|+|E|)。
DFS(v) {
num (v) = i++;
for (頂點(diǎn)v的所有鄰接頂點(diǎn)u) {
if (num (u) = 0) {
把變edge(uv)加入邊集edges;
DFS(u);
}
}
}
depthFirstSearch () {
for (所有的頂點(diǎn)v) {
num(v) = 0;
}
edges = null;
i = 1;
while (存在一個(gè)頂點(diǎn)v使num(v) = 0) {
DFS(v);
}
輸出邊集edges;
}
然而對(duì)于圖的遍歷,廣度優(yōu)先算法具有更高的效率。
breadFirstSearch () {
for (所有的頂點(diǎn)u) {
num(u) = 0;
}
edges = null;
i = 1;
while (存在一個(gè)頂點(diǎn)v使num(v)為0) {
num(v) = i++;
enqueue(v);
while (隊(duì)列非空) {
v = dequeue();
for (頂點(diǎn)v的所有鄰接頂點(diǎn)u) {
if (num(u) == 0) {
num(u) = i++;
enqueue(u);
把邊edges(vu)加入到edges中;
}
}
}
}
輸出edges;
}
8.2 最短路徑
如果圖的每一條邊有一個(gè)權(quán)重值weight,就可以尋找一個(gè)頂點(diǎn)到圖中其他頂點(diǎn)的最短路徑。其實(shí)現(xiàn)方式主要是通過(guò)標(biāo)記每個(gè)頂點(diǎn)距離初始頂點(diǎn)的距離,根據(jù)標(biāo)記更新的策略分為標(biāo)記設(shè)置法和標(biāo)記校正法。
8.2.1 標(biāo)記設(shè)置法
標(biāo)記設(shè)置法一旦給一個(gè)頂點(diǎn)標(biāo)記后就不會(huì)更改,此方法只能處理權(quán)值為正的圖,下述算法的復(fù)雜度為O(|V|^2)。
DijstraAlgorithm (帶權(quán)的簡(jiǎn)單有向圖diagraph, 頂點(diǎn)first) {
for (所有頂點(diǎn)v) {
currDist(v) = ∞;
}
currDist(first) = 0;
toBeChecked = 所有節(jié)點(diǎn);
while (toBeChecked非空) {
v = toBeChecked 中 currDist(v)最小的頂點(diǎn);
從toBeChecked中刪除v;
for (toBeChecked 中 v的所有鄰接頂點(diǎn)u) {
if (currDist(u) > currDist(v) + weight(edge(vu))) {
currDist(u) = currDist(v) + weight(edge(vu));
predecessor(u) = v;
}
}
}
}
8.2.1 標(biāo)記校正法
標(biāo)記校正法給每個(gè)頂點(diǎn)設(shè)置的標(biāo)記都可能會(huì)在后面的計(jì)算過(guò)程中被更新。它可以用來(lái)處理帶負(fù)權(quán)值但是不含反向循環(huán)的圖(反向循環(huán)指構(gòu)成環(huán)的邊的權(quán)相加為負(fù)值)。該算法的效率部分取決于toBechecked的數(shù)據(jù)結(jié)構(gòu),通常使用雙端隊(duì)列,首次包含在其中則加到隊(duì)列末尾,否則加在前端。
labelCorrectingAlgorithm (帶權(quán)的簡(jiǎn)單有向圖digraph,頂點(diǎn)first) {
for (所有的頂點(diǎn)v) {
CurrDist(v) = ∞;
}
CurrDist(first) = 0;
toBechecked = {first};
while (toBechecked非空) {
v = toBeChecked中的頂點(diǎn);
從toBechecked中刪除頂點(diǎn)v;
for (v的所有鄰接頂點(diǎn)u) {
if (currDist(u) > currDist(v) + weight(edge(vu))) {
currDist(u) = currDist(v) + weight(edge(vu));
predecessor(u) = v;
如果頂點(diǎn)u不在toBeChecked中,將其加入;
}
}
}
}
8.2.3 多源多目標(biāo)最短路徑
前面討論的是從單個(gè)節(jié)點(diǎn)到其他節(jié)點(diǎn)的最短路徑,如果要知道任意節(jié)點(diǎn)到其他任意節(jié)點(diǎn)的最短路徑。在稀疏圖中我們可以通過(guò)對(duì)每個(gè)節(jié)點(diǎn)執(zhí)行前面的操作即可,但是對(duì)于過(guò)于密集和完全圖我們一個(gè)鄰接矩陣完成。矩陣的橫縱坐標(biāo)都是每個(gè)節(jié)點(diǎn),初始化矩陣時(shí),矩陣中每一個(gè)值表示其橫縱坐標(biāo)代表的頂點(diǎn)間距離,左下三角全部賦值為∞,不相鄰的頂點(diǎn)也為∞。通過(guò)一定的計(jì)算就可以得到多源多目標(biāo)的最短路徑矩陣。
WLIalgorithm {
for (i = 1 to |V|) {
for (j = 1 to |V|) {
for (k = 1 to |V|) {
if (weight[j][k] > weight[j][i] + weight[i][k]) {
weight[j][k] > weight[j][i] + weight[i][k];
}
}
}
}
}
8.3 環(huán)的檢測(cè)
在無(wú)向圖中檢測(cè)環(huán)可以通過(guò)深度優(yōu)先遍歷改寫(xiě)完成,在有向圖中檢測(cè)環(huán)通過(guò)判斷如果一個(gè)反向邊的兩個(gè)頂點(diǎn)包含在同一個(gè)生成樹(shù)中則表示有環(huán)的存在。
??對(duì)于修改后的深度優(yōu)先遍歷法檢車(chē)環(huán)的存在在稠密圖中的復(fù)雜度可達(dá)O(|v|^4),因此需要更好的方法。判斷兩個(gè)頂點(diǎn)是否在一個(gè)集合中,首先需要找到v的集合,再找到w的集合。如果分別屬于不同的集合就將他們合并,這樣成為聯(lián)合查找。
8.4 生成樹(shù)
深度優(yōu)先遍歷法可以得到至少一顆生成樹(shù)。有時(shí)我們只關(guān)心最小生成樹(shù),即所有前向邊的權(quán)值和最小。我們可以將一個(gè)無(wú)向圖的所有邊一次加入集合中,如果形成環(huán),則將環(huán)中權(quán)值最大的邊刪除,直到處理完所有的邊。
8.5 連通性
8.5.1 無(wú)向圖中的連通性
如果任意兩個(gè)頂點(diǎn)至少有n條不同的路徑,并且這些路徑不會(huì)包含共同的節(jié)點(diǎn),稱(chēng)該圖為n聯(lián)通。如果一個(gè)頂點(diǎn)被刪除導(dǎo)致圖被分割為獨(dú)立的子圖,這樣的頂點(diǎn)稱(chēng)之為分割點(diǎn)或者關(guān)節(jié)點(diǎn)。如果刪除一條邊導(dǎo)致圖分為兩個(gè)子圖,這樣的邊稱(chēng)之為分割邊。
8.5.2 有向圖中的連通性
對(duì)于有向圖,根據(jù)是否將方向考慮在內(nèi),連通性有兩種定義方式。如果具有相同頂點(diǎn)的邊的無(wú)向圖是聯(lián)通的,有向圖就是弱連通的,如果每對(duì)頂點(diǎn)間存在雙向路徑,則有向圖是強(qiáng)連通的。通常有向圖不是強(qiáng)聯(lián)通的,但是其可以含有強(qiáng)連通分量。
8.6 拓?fù)渑判?/h4>
通常任務(wù)之間都會(huì)存在依賴關(guān)系,將一個(gè)任務(wù)看成一個(gè)節(jié)點(diǎn),將節(jié)點(diǎn)間的先后順序用邊表示,遍歷這個(gè)有向圖,找到一個(gè)沒(méi)有輸出邊的頂點(diǎn)v,再刪除所有到v的邊,將v放入序列,這樣最好得到的隊(duì)列就是拓?fù)渑判虻慕Y(jié)果。
8.7 網(wǎng)絡(luò)
8.7.1 最大流
有向圖可以形成一個(gè)網(wǎng)絡(luò),這個(gè)網(wǎng)絡(luò)有一個(gè)起點(diǎn)和一個(gè)匯點(diǎn),每條邊表示其最大的流通量。對(duì)于網(wǎng)絡(luò)我們通常關(guān)心如何配置每條邊的流量使得匯點(diǎn)能夠收到更多的流量。
??流增大路徑表示從原點(diǎn)到匯點(diǎn)的一系列邊,他們可以由前向邊和后向邊組成,其中前向邊負(fù)責(zé)向后送流,后向邊負(fù)責(zé)往回推流,我們可以找到所有的流增大路徑并分別對(duì)它們進(jìn)行最優(yōu)化操作,最后就可以得到最大流。
8.7.2 成本最低最大流
如果對(duì)于單一的一條邊,我們不僅考慮其最大容量,還要考慮通過(guò)這條邊的成本,這個(gè)問(wèn)題就轉(zhuǎn)化為成本最低最大流問(wèn)題。通過(guò)找到所有的最大流再來(lái)考慮其成本并不是一個(gè)可行的辦法。我們可以先找到傳送1單位流量的最便宜路徑,然后在這個(gè)路徑上最大可能傳送更多的流,然后在找到一條新的傳送1單位流量的最便宜路徑,然后再最大化使用該路徑,直到原點(diǎn)不能流出更多,或者匯點(diǎn)不能收到更多。
8.8 匹配
對(duì)于兩個(gè)集合A和B,其中A的每個(gè)元素分別只能和部分B中的元素匹配,此時(shí)我們要找出一種匹配方式能夠得到最多的匹配對(duì)。這樣的問(wèn)題可以轉(zhuǎn)化為無(wú)向圖來(lái)分析,將兩個(gè)集合中的元素作為頂點(diǎn),能匹配的連個(gè)頂點(diǎn)相連。要找最大匹配問(wèn)題就變成了再圖中找到一條最長(zhǎng)路徑問(wèn)題。
8.8.1 匹配
對(duì)于匹配問(wèn)題,通常每個(gè)元素對(duì)于能與其匹配的多個(gè)元素有不同的偏好。如果一個(gè)匹配結(jié)果中不會(huì)出現(xiàn)兩個(gè)子匹配對(duì)交換匹配單元能夠得到更高的匹配度時(shí),改匹配結(jié)果成為穩(wěn)定的匹配。
8.8.2 分配
對(duì)于加權(quán)圖的分配問(wèn)題,我們通常希望得到一個(gè)總權(quán)值最大的匹配結(jié)果,這樣轉(zhuǎn)變?yōu)榱朔峙鋯?wèn)題。
??完全二分圖是有兩個(gè)相同大小頂點(diǎn)集,并且每個(gè)集合中的元素都能在另一個(gè)集合中找到能匹配的元素,這樣的分配問(wèn)題也稱(chēng)為最優(yōu)分配問(wèn)題。
??非完全二分圖中可能會(huì)存在基數(shù)邊的環(huán),因此不能和完全二分圖使用相同的算法。
8.9 歐拉圖和漢密爾頓環(huán)
8.9.1 歐拉圖
如果一個(gè)圖中的每個(gè)頂點(diǎn)都與偶數(shù)條邊相關(guān)聯(lián),或者圖中剛好有兩個(gè)頂點(diǎn)有基數(shù)條邊,這個(gè)圖就是歐拉圖,這個(gè)圖包含歐拉軌跡,歐拉軌跡指一條能包含所有邊的不重復(fù)路徑。
8.9.2 漢密爾頓圖
漢密爾頓環(huán)是一個(gè)通過(guò)圖中所有頂點(diǎn)的環(huán),如果一個(gè)圖至少包含一個(gè)漢密爾頓環(huán),該圖稱(chēng)為漢密爾頓圖。通常我們將一個(gè)圖中找到一個(gè)度最高的頂點(diǎn)和另一個(gè)非鄰接頂點(diǎn),如果他們度的和大于節(jié)點(diǎn)總數(shù)就鏈接它們,并將這條邊標(biāo)記為k(k從1遞增),直到所有頂點(diǎn)鏈接得到一個(gè)新圖,在這個(gè)圖中很容易找到一個(gè)漢密爾頓環(huán)。下面進(jìn)入算法第二階段通過(guò)從標(biāo)記最大的邊逐漸斷開(kāi),并用不帶標(biāo)記的邊替代,直到最后所有的邊都不帶標(biāo)記時(shí)則找到一個(gè)漢密爾頓環(huán)。
8.10 圖的上色問(wèn)題
當(dāng)有很多任務(wù)需要處理,但有些任務(wù)不能同時(shí)由一個(gè)人處理時(shí),需要找到最小的人手,此時(shí)我們可以將這類(lèi)問(wèn)題轉(zhuǎn)換為圖來(lái)解決。每個(gè)任務(wù)都是圖中的一個(gè)節(jié)點(diǎn),不能由一個(gè)人處理的任務(wù)就用邊鏈接起來(lái)。此時(shí)我們考慮每個(gè)頂點(diǎn)都需要一種顏色,但是鄰接頂點(diǎn)的顏色不能相同,我們要用最少的顏色將所有頂點(diǎn)都涂上色。
8.11 圖中的NP完整性問(wèn)題
在知道三滿意問(wèn)題是NP完整性問(wèn)題前提下。對(duì)于1)派系問(wèn)題,派系是G的一個(gè)完全子圖,派系問(wèn)題可以轉(zhuǎn)換為三滿意問(wèn)題。2)三色問(wèn)題,確定一個(gè)圖是否能用三種顏色正確上色,三色問(wèn)題也能轉(zhuǎn)化為三滿意問(wèn)題。3)頂點(diǎn)覆蓋問(wèn)題,無(wú)向圖G=(V,E)的頂點(diǎn)覆蓋集合指的是這樣一個(gè)頂點(diǎn)集合W屬于V,圖G中每條邊都至少與W中的一個(gè)頂點(diǎn)相連。這個(gè)問(wèn)題可以轉(zhuǎn)化為派系問(wèn)題。4)漢密爾頓環(huán)問(wèn)題,能夠查找到一個(gè)漢密爾頓環(huán)可以轉(zhuǎn)化為頂點(diǎn)覆蓋問(wèn)題。因此以上4個(gè)問(wèn)題都是NP完整性問(wèn)題。
9 排序
9.1 基本排序算法
9.1.1 插入排序
插入排序單詞查詢從某個(gè)元素E開(kāi)始,依次像上查找如果遇見(jiàn)比當(dāng)前比較元素更大的值則向下移位,直至查找的第0個(gè)元素停止,再將E放在合適的位置。查詢起點(diǎn)E從i=1遞增到i=n-1,則實(shí)現(xiàn)數(shù)組排序。
template<class T> {
void insertionsort(T data[], int n) {
T tmp = data[i];
for (j = i; j > 0 && tmp < data[j-1]; j--) {
data[j] = data[j-1];
}
data[j] = tmp;
}
}
插入排序的優(yōu)點(diǎn)是只有需要時(shí)才會(huì)對(duì)數(shù)組排序,缺點(diǎn)是1)在某個(gè)元素在某次循環(huán)中實(shí)際已經(jīng)達(dá)到合適位置,但是后續(xù)操作可能會(huì)反復(fù)改變其位置;2)插入操作直接移動(dòng)數(shù)據(jù)項(xiàng),并且經(jīng)常移動(dòng)大量的項(xiàng)。
??插入排序最好的情況下比較次數(shù)為n-1,移動(dòng)次數(shù)為2(n-1)。最壞情況下比較次數(shù)是n(n-1)/2,移動(dòng)次數(shù)為(n+2)*(n-1)。平均情況下移動(dòng)和比較的次數(shù)都是O(n^2)。
9.1.2 選擇排序
選擇排序每次在數(shù)組中選出最小元素,將其移到當(dāng)前子數(shù)組的第一個(gè)位置,然后取不包含第一個(gè)元素的子數(shù)組繼續(xù)重復(fù)上述操作,直至數(shù)組中只有一個(gè)元素。
template<class T>
void selection(T data[], int n) {
for (int i = 0,j,least; i<n-1; i++) {
for (j = i+1,least = i; j<n; j++) {
if (data[j] < data[least]) {
least = j;
}
}
swap(data[least], data[i]);
}
}
}
選擇排序的優(yōu)點(diǎn)是賦值次數(shù)更少。但是其缺點(diǎn)是在任何情況下都有2*(n-1)次的交換次數(shù),因此在swap中判斷,如果需要才交換元素。選擇排序的比較次數(shù)是O(n^2),交換次數(shù)為O(n)。
9.1.3 冒泡排序
冒泡排序每次迭代將起始位置元素E和其后面所有元素比較,如果E更大則交換元素。起始元素從i=0的元素一直迭代到i=n-2的元素。
template<class T>
void bubblesort(T data[], int n) {
for (int i = 0; i < n-1; i++) {
for (int j = n-1; j>i; --j) {
if (data[j] < data[j-1]) {
swap(data[j],data[j-1]);
}
}
}
}
冒泡排序的比較次數(shù)在最壞、最好和平均情況下都是O(n2),移動(dòng)次數(shù)最好情況為0,最好和最壞情況下都是O(n2)。冒泡排序的主要缺點(diǎn)是,元素需要一步一步向上冒泡到頂部,這樣非常費(fèi)時(shí)。
9.1.4 梳排序
梳排序分為兩個(gè)階段,第一階段通過(guò)初始化步長(zhǎng)為n,步長(zhǎng)衰減因子為1.3,每次迭代時(shí)步長(zhǎng)都會(huì)發(fā)生衰減,在單次迭代中比較距離當(dāng)前步長(zhǎng)的兩個(gè)元素確定是否需要交換。當(dāng)步長(zhǎng)衰減到小于或等于1時(shí)進(jìn)入第二階段,這個(gè)階段使用冒泡排序的方式對(duì)數(shù)組進(jìn)行排序。梳排序的良好性能可以與快速排序媲美。
template<class T>
void combsort(T data[], const int n) {
int step = n, j, k;
while ((step = int(step/1.3)) > 1) {
for (j = n-1; j >= step; j--) {
k = j-step;
if (data[i] < data[k]) {
swap(data[j], data[k]);
}
}
}
bool again = true;
for (int i = 0; i < n-1 && again; i++) {
for (j = n -1, again = false; j>1; --j) {
if (data[j] < data[j-1]) {
swap(data[j],data[j-1]);
again = true;
}
}
}
}
9.2 決策樹(shù)
排序過(guò)程可以簡(jiǎn)單的歸納為多次表較兩個(gè)元素大小,最后得到一個(gè)正確順序的過(guò)程。如果將每次比較看做是二叉樹(shù)中的一個(gè)非葉節(jié)點(diǎn),是否滿足條件作為一個(gè)節(jié)點(diǎn)連接其子節(jié)點(diǎn)的兩條邊,所有的可能出現(xiàn)的結(jié)果和錯(cuò)誤的結(jié)果為葉節(jié)點(diǎn)。這樣對(duì)于一個(gè)排序操作就可以得到一個(gè)唯一的決策樹(shù)。為了得到一個(gè)排序結(jié)果即到達(dá)一個(gè)葉節(jié)點(diǎn),其中比較的次數(shù)為路徑深度+1,從二叉樹(shù)的性質(zhì)我們可以得到理論上的最優(yōu)平均路徑深度為O(n*lgn)。當(dāng)n很大時(shí),前面的插入、選擇和冒泡排序的比較量都非常巨大,因此我們可以盡量找到一個(gè)比較次數(shù)接近與這個(gè)值的排序算法。
9.3 高效排序算法
9.3.1 希爾排序
由于簡(jiǎn)單的排序算法比較次數(shù)是隨著n的增加而呈指數(shù)的增長(zhǎng),因此將大數(shù)組巧妙分成多個(gè)小數(shù)組,分別排序是一個(gè)很好的方法,這也是希爾排序的核心思想,其中希爾排序中子數(shù)組的排序方法用的是插入排序。希爾排序每一次采用增量k在原數(shù)組中邏輯上提取子數(shù)組,并將其用簡(jiǎn)單排序法排序,當(dāng)完成i次迭代直至增量衰減為1時(shí),數(shù)組成為有序數(shù)組。當(dāng)希爾排序采用的增量序列滿足h(1) = 1;h(i+1) = 3h(i)+1時(shí),其平均比較的效率為O(n1.25)。
template<class T>
void ShellSort(T data[], int arrSize) {
register int i,j,hCnt,h;
int increments[20],k;
for (h = 1,i=0; h < arrSize; i++) {
increments[i] = h;
h = 3*h +1;
}
for (i--;i>=0;i--) {
h = increment[i];
for (hCnt = h; hCnt < 2*h; hCnt++) {
for (j = hCnt; j < arrSize;) {
T tmp = data[j];
k = j;
while (k-h > 0 && tmp < data[k-h]) {
data[k] = data[k-h];
k -= h;
}
data[k] = tmp;
j += h;
}
}
}
}
9.3.2 堆排序
堆排序分為兩個(gè)階段,第一階段將數(shù)組轉(zhuǎn)化堆,第二階段的每一次迭代將最大值和數(shù)組末尾值交換,堆中將最后一個(gè)節(jié)點(diǎn)刪除,恢復(fù)堆屬性,重復(fù)該操作直到堆中只剩一個(gè)節(jié)點(diǎn),得到的新數(shù)組就是一個(gè)有序的數(shù)組。堆排序的平均情況下第一階段比較和移動(dòng)的時(shí)間復(fù)雜度為O(n),第二階段的移動(dòng)和比較的復(fù)雜度是O(nlgn),交換的次數(shù)是n-1,整個(gè)排序算法的復(fù)雜度是O(nlgn)。最好情況下第一階段比較次數(shù)為n,不需要移動(dòng)操作,第二階段比較次數(shù)為2(n-1),移動(dòng)次數(shù)為(n-1),整個(gè)算法的復(fù)雜度為O(n)。