第一章 緒論
什么是數據結構?
數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。
第二章 算法
算法的特性:有窮性、確定性、可行性、輸入、輸出。
什么是好的算法? ----正確性、可讀性、健壯性、時間效率高、存儲量低
函數的漸近增長:給定兩個函數f(n)和g(n),如果存在一個整數N,使得對于所有的n>N,f(n)總是比g(n)大,那么,我們說f(n)的增長漸近快于g(n)。于是我們可以得出一個結論,判斷一個算法好不好,我們只通過少量的數據是不能做出準確判斷的,如果我們可以對比算法的關鍵執行次數函數的漸近增長性,基本就可以分析出:某個算法,隨著n的變大,它會越來越優于另一算法,或者越來越差于另一算法。
時間復雜度(大O階)的計算方法——如果級數展開學得好的話就很好理解
- 用常數1取代運行時間中的所有加法常數。
- 在修改后的運行次數函數中,只保留最高階項。
- 如果最高階項存在且不是1,則去除與這個項相乘的常數。
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)< O(n!) < O(n^n)
第三章 線性表
線性表是零個或多個具有相同類型的數據元素的有限序列。
線性表的兩種存儲結構:順序存儲結構和鏈式存儲結構
鏈式存儲結構包括四種:
單鏈表:指針的指向只是從表頭到表尾
靜態鏈表:數組的元素都是由兩個數據域組成,data和cur。也就是說,數組的每個下標都對應一個data和一個cur。數據域data,用來存放數據元素,也就是通常我們要處理的數據;而cur相當于單鏈表中的next指針,存放該元素的后繼在數組中的下標,我們把cur叫做游標。
循環鏈表:表尾的指針指向表頭
雙向鏈表:不僅有指向后面的指針,還有指向前面的指針
第四章 棧和隊列
棧stack是限定在表尾進行插入和刪除操作的線性表
隊列queue是只允許在一端進行插入操作,而在另一端進行刪除操作的線性表。
棧和隊列用線性表的順序存儲結構的缺點:浪費空間 操作復雜
解決方法:對于棧來說,如果是兩個相同數據類型的棧,則可以用數組的兩端作棧底的方法來讓兩個棧共享數據,這就可以最大化地利用數組的空間;對于隊列來說,為了避免數組插入和刪除時需要移動數據,于是就引入了循環隊列,使得隊頭和隊尾可以在數組中循環變化。解決了移動數據的時間損耗,使得本來插入和刪除是O(n)的時間復雜度變成了O(1)。
逆波蘭表達式是什么?——又叫做后綴表達式,處理四則運算,計算方法采用棧的先進后出的方式。
第五章 字符串
KMP算法是什么?
介紹next[j]值的計算方法?
第六章 樹
樹(Tree)是n(n≥0)個結點的有限集。
線性結構和樹結構的比較:
樹的度是什么?各個結點度的最大值
樹的存儲結構?雙親表示法、孩子表示法、孩子兄弟表示法(其實所有的表示法都是在指針域上做手腳)
二叉樹的分類?——斜樹、滿二叉樹、完全二叉樹(若按層序編號后其編號與同樣深度的滿二叉樹中編號結點在二叉樹中位置完全相同,那它就是完全二叉樹。)
二叉樹的性質?——
性質1:在二叉樹的第i層至多有2^(i-1)個結點(i>=1)
性質2 :深度為k的二叉樹至多有2^k-1個結點(k>=1)
性質3:對任何一棵二叉樹T,如果其終端結點數為n0,度為2的結點數為n2,則n0=n2+1。
性質4:具有n個結點的完全二叉樹的深度為|log2(n)+1|(|x|表示不大于x的最大整數)。
性質5:如果對一棵有n個結點的完全二叉樹(其深度為k)的結點按層序編號(從第1層到第層,每層從左到右),對任一結點i(1≤i≤n)有: 1.如果i=1,則結點i是二叉樹的根,無雙親;如果i>1,則其雙親是結點。 2.如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子是結點2i。 3.如果2i+1>n,則結點i無右孩子;否則其右孩子是結點2i+1。
二叉樹的順序存儲結構?完全二叉樹使用順序存儲是最好的,不會浪費存儲空間;
對于一般的二叉樹用二叉鏈表,結點的結構是 lchild data rchild(中間是數據域,兩邊是指針域)
二叉樹遍歷原理(限制從左到右):
1.前序遍歷:若二叉樹為空,則空操作返回,否則先訪問根結點,然后前序遍歷左子樹,再前序遍歷右子樹。
2.中序遍歷:若樹為空,則空操作返回,否則從最左下結點開始(注意并不是先訪問根結點),中序遍歷根結點的左子樹,然后是訪問根結點,最后中序遍歷右子樹。
3.后序遍歷:若樹為空,則空操作返回,否則從左到右先葉子后結點的方式遍歷訪問左右子樹,最后是訪問根結點。
4.層序遍歷:若樹為空,則空操作返回,否則從樹的第一層,也就是根結點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問。
二叉樹遍歷的性質:
? 已知前序遍歷序列和中序遍歷序列,可以唯一確定一棵二叉樹。
? 已知后序遍歷序列和中序遍歷序列,可以唯一確定一棵二叉樹。
? 注意,已知前序和后序遍歷,是不能確定一棵二叉樹的。
什么是線索二叉樹?——指向前驅和后繼的指針稱為線索,加上線索的二叉鏈表稱為線索鏈表,相應的二叉樹就稱為線索二叉樹。其實線索二叉樹,等于是把一棵二叉樹轉變成了一個雙向鏈表,這樣對我們的插入刪除結點、查找某個結點都帶來了方便。所以我們對二叉樹以某種次序遍歷使其變為線索二叉樹的過程稱做是線索化。
將樹轉換為二叉樹的步驟如下 1.加線。在所有兄弟結點之間加一條連線。 2.去線。對樹中每個結點,只保留它與第一個孩子結點的連線,刪除它與其他孩子結點之間的連線。 3.層次調整。以樹的根結點為軸心,將整棵樹順時針旋轉一定的角度,使之結構層次分明。注意第一個孩子是二叉樹結點的左孩子,兄弟轉換過來的孩子是結點的右孩子。(兄弟變兒子哈哈!)
森林轉化為二叉樹:
森林是由若干棵樹組成的,所以完全可以理解為,森林中的每一棵樹都是兄弟,可以按照兄弟的處理辦法來操作。步驟如下: 1.把每個樹轉換為二叉樹。 2.第一棵二叉樹不動,從第二棵二叉樹開始,依次把后一棵二叉樹的根結點作為前一棵二叉樹的根結點的右孩子,用線連接起來。當所有的二叉樹連接起來后就得到了由森林轉換來的二叉樹。
二叉樹轉換為樹
二叉樹轉換為樹是樹轉換為二叉樹的逆過程。步驟如下: 1.加線。若某結點的左孩子結點存在,則將這個左孩子的右孩子結點、右孩子的右孩子結點、右孩子的右孩子的右孩子結點……哈,反正就是左孩子的n個右孩子結點都作為此結點的孩子。將該結點與這些右孩子結點用線連接起來。 2.去線。刪除原二叉樹中所有結點與其右孩子結點的連線。 3.層次調整。使之結構層次分明。
判斷一棵二叉樹能夠轉換成一棵樹還是森林,標準很簡單,那就是只要看這棵二叉樹的根結點有沒有右孩子,有就是森林,沒有就是一棵樹。
二叉樹轉換成森林,步驟如下: 1.從根結點開始,若右孩子存在,則把與右孩子結點的連線刪除,再查看分離后的二叉樹,若右孩子存在,則連線刪除……,直到所有右孩子連線都刪除為止,得到分離的二叉樹。 2.再將每棵分離后的二叉樹轉換為樹即可。
樹的遍歷分為兩種方式。 1.一種是先根遍歷樹,即先訪問樹的根結點,然后依次先根遍歷根的每棵子樹。 2.另一種是后根遍歷,即先依次后根遍歷每棵子樹,然后再訪問根結點。
森林的遍歷也分為兩種方式: 1.前序遍歷:先訪問森林中第一棵樹的根結點,然后再依次先根遍歷根的每棵子樹,再依次用同樣方式遍歷除去第一棵樹的剩余樹構成的森林。2.后序遍歷:是先訪問森林中第一棵樹,后根遍歷的方式遍歷每棵子樹,然后再訪問根結點,再依次同樣方式遍歷除去第一棵樹的剩余樹構成的森林。
當以二叉鏈表作樹的存儲結構時,樹的先根遍歷和后根遍歷完全可以借用二叉樹的前序遍歷和中序遍歷的算法來實現。
Huffman樹
樹的路徑長度就是從樹根到每一結點的路徑長度之和。
如果考慮到帶權的結點,結點的帶權的路徑長度為從該結點到樹根之間的路徑長度與結點上權的乘積。樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。
假設有n個權值{w1,w2,...,wn},構造一棵有n個葉子結點的二叉樹,每個葉子結點帶權wk,每個葉子的路徑長度為lk,我們通常記作,則其中帶權路徑長度WPL最小的二叉樹稱做赫夫曼樹。
構造赫夫曼樹的赫夫曼算法描述:
1.根據給定的n個權值{w1,w2,...,wn}構成n棵二叉樹的集合F={T1,T2,...,Tn},其中每棵二叉樹Ti中只有一個帶權為wi根結點,其左右子樹均為空。
2.在F中選取兩棵根結點的權值最小的樹作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的根結點的權值為其左右子樹上根結點的權值之和。
3.在F中刪除這兩棵樹,同時將新得到的二叉樹加入F中。
4.重復2和3步驟,直到F只含一棵樹為止。這棵樹便是赫夫曼樹。
赫夫曼編碼是什么?——設需要編碼的字符集為{d1,d2,...,dn},各個字符在電文中出現的次數或頻率集合為{w1,w2,...,wn},以d1,d2,...,dn作為葉子結點,以w1,w2,...,wn作為相應葉子結點的權值來構造一棵赫夫曼樹。規定赫夫曼樹的左分支代表0,右分支代表1,則從根結點到葉子結點所經過的路徑分支組成的0和1的序列便為該結點對應字符的編碼,這就是赫夫曼編碼。
第七章 圖
圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。
無向邊:若頂點vi到vj之間的邊沒有方向,則稱這條邊為無向邊(Edge),用無序偶對(vi,vj)來表示。如果圖中任意兩個頂點之間的邊都是無向邊,則稱該圖為無向圖(Undirected graphs)。
有向邊:若從頂點vi到vj的邊有方向,則稱這條邊為有向邊,也稱為弧(Arc)。用有序偶<vi,vj>來表示,vi稱為弧尾(Tail),vj稱為弧頭(Head)。如果圖中任意兩個頂點之間的邊都是有向邊,則稱該圖為有向圖(Directed graphs)。
簡單圖——在圖中,若不存在頂點到其自身的邊,且同一條邊不重復出現,則稱這樣的圖為簡單圖。
在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖為無向完全圖。含有n個頂點的無向完全圖有n(n-1)/2條邊。
在有向圖中,如果任意兩個頂點之間都存在方向互為相反的兩條弧,則稱該圖為有向完全圖。含有n個頂點的有向完全圖有n×(n-1)條邊。
對于具有n個頂點和e條邊數的圖,無向圖0≤e≤n(n-1)/2,有向圖0≤e≤n(n-1)。
有很少條邊或弧的圖稱為稀疏圖,反之稱為稠密圖。
有些圖的邊或弧具有與它相關的數字,這種與圖的邊或弧相關的數叫做權(Weight)。這些權可以表示從一個頂點到另一個頂點的距離或耗費。這種帶權的圖通常稱為網(Network)。
假設有兩個圖G=(V,{E})和G'=(V',{E'}),如果V' ?V且E' ?E,則稱G'為G的子圖(Sub-graph)。
對于無向圖G=(V,{E}),如果邊(v,v')∈E,則稱頂點v和v'互為鄰接點(Adjacent),即v和v'相鄰接。邊(v,v')依附(incident)于頂點v和v',或者說(v,v')與頂點v和v'相關聯。頂點v的度(Degree)是和v相關聯的邊的數目,記為TD(v)。
對于有向圖G=(V,{E}),如果弧<v,v'>∈E,則稱頂點v鄰接到頂點v',頂點v'鄰接自頂點v。弧<v,v'>和頂點v,v'相關聯。以頂點v為頭的弧的數目稱為v的入度(InDegree),記為ID(v);以v為尾的弧的數目稱為v的出度(OutDegree),記為OD(v);頂點v的度為D(v)=ID(v)+OD(v)。
路徑的長度是路徑上的邊或弧的數目。
第一個頂點和最后一個頂點相同的路徑稱為回路或環(Cycle)。序列中頂點不重復出現的路徑稱為簡單路徑。除了第一個頂點和最后一個頂點之外,其余頂點不重復出現的回路,稱為簡單回路或簡單環。
在無向圖G中,如果從頂點v到頂點v'有路徑,則稱v和v'是連通的。如果對于圖中任意兩個頂點vi、vj∈V,vi和vj都是連通的,則稱G是連通圖(Connected Graph)。
無向圖中的極大連通子圖稱為連通分量。注意連通分量的概念,它強調: 要是子圖; 子圖要是連通的; 連通子圖含有極大頂點數; 具有極大頂點數的連通子圖包含依附于這些頂點的所有邊。
在有向圖G中,如果對于每一對vi、vj∈V、vi≠vj,從vi到vj和從vj到vi都存在路徑,則稱G是強連通圖。有向圖中的極大強連通子圖稱做有向圖的強連通分量。
所謂的一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的n個頂點,但只有足以構成一棵樹的n-1條邊。如果一個圖有n個頂點和小于n-1條邊,則是非連通圖,如果它多于n-1邊條,必定構成一個環,因為這條邊使得它依附的那兩個頂點之間有了第二條路徑。
如果一個有向圖恰有一個頂點的入度為0,其余頂點的入度均為1,則是一個有向樹。一個有向圖的生成森林由若干棵有向樹組成,含有圖中全部頂點,但只有足以構成若干棵不相交的有向樹的弧。
(概念太多總結一下:圖按照有無方向分為無向圖和有向圖。無向圖由頂點和邊構成,有向圖由頂點和弧構成。弧有弧尾和弧頭之分。
圖按照邊或弧的多少分稀疏圖和稠密圖。如果任意兩個頂點之間都存在邊叫完全圖,有向的叫有向完全圖。若無重復的邊或頂點到自身的邊則叫簡單圖。
圖中頂點之間有鄰接點、依附的概念。無向圖頂點的邊數叫做度,有向圖頂點分為入度和出度。
圖上的邊或弧上帶權則稱為網。
圖中頂點間存在路徑,兩頂點存在路徑則說明是連通的,如果路徑最終回到起始點則稱為環,當中不重復叫簡單路徑。若任意兩頂點都是連通的,則圖就是連通圖,有向則稱強連通圖。圖中有子圖,若子圖極大連通則就是連通分量,有向的則稱強連通分量。
無向圖中連通且n個頂點n-1條邊叫生成樹。有向圖中一頂點入度為0其余頂點入度為1的叫有向樹。一個有向圖由若干棵有向樹構成生成森林。)
5種圖的多重鏈表存儲:
一、鄰接矩陣:圖的鄰接矩陣(Adjacency Matrix)存儲方式是用兩個數組來表示圖。一個一維數組vertex[m]存儲圖中頂點信息,一個二維數組arc[i][j](稱為鄰接矩陣)存儲圖中的邊或弧的信息。
設圖G有n個頂點,則鄰接矩陣是一個n×n的方陣,定義為:
這是一個對稱矩陣,有了這個矩陣,我們就可以很容易地知道圖中的信息。
1.我們要判定任意兩頂點是否有邊無邊就非常容易了。
2.我們要知道某個頂點的度,其實就是這個頂點vi在鄰接矩陣中第i行(或第i列)的元素之和。
3.求頂點vi的所有鄰接點就是將矩陣中第i行元素掃描一遍,arc[i][j]為1就是鄰接點。
二、鄰接表
鄰接矩陣在處理稀疏圖時會浪費存儲空間,鄰接表是其改進。
鄰接表的處理辦法:
1.圖中頂點用一個一維數組存儲,當然,頂點也可以用單鏈表來存儲,不過數組可以較容易地讀取頂點信息,更加方便。另外,對于頂點數組中,每個數據元素還需要存儲指向第一個鄰接點的指針,以便于查找該頂點的邊信息。
2.圖中每個頂點vi的所有鄰接點構成一個線性表,由于鄰接點的個數不定,所以用單鏈表存儲,無向圖稱為頂點vi的邊表,有向圖則稱為頂點vi作為弧尾的出邊表。
三、十字鏈表
對于有向圖,鄰接表是有缺陷的。關心了出度問題,想了解入度就必須要遍歷整個圖才能知道,反之,逆鄰接表解決了入度卻不了解出度的情況。可以把鄰接表和逆鄰接表做在一起,成為十字鏈表。
重新定義頂點表結點結構為data firstin firstout
重新定義的邊表結點結構為 tailvex headvex headlink taillink
其中tailvex是指弧起點在頂點表的下標,headvex是指弧終點在頂點表中的下標,headlink是指入邊表指針域,指向終點相同的下一條邊,taillink是指邊表指針域,指向起點相同的下一條邊。如果是網,還可以再增加一個weight域來存儲權值。
四、鄰接多重表
重新定義邊表結點結構為:ivex ilink jvex jlink
其中ivex和jvex是與某條邊依附的兩個頂點在頂點表中的下標。ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊。這就是鄰接多重表結構。
鄰接多重表與鄰接表的差別,僅僅是在于同一條邊在鄰接表中用兩個結點表示,而在鄰接多重表中只有一個結點。
五、邊集數組
邊集數組是由兩個一維數組構成。一個是存儲頂點的信息;另一個是存儲邊的信息,這個邊數組每個數據元素由一條邊的起點下標(begin)、終點下標(end)和權(weight)組成。
邊集數組關注的是邊的集合,在邊集數組中要查找一個頂點的度需要掃描整個邊數組,效率并不高。因此它更適合對邊依次進行處理的操作,而不適合對頂點相關的操作。
圖的遍歷:
深度優先遍歷 廣度優先遍歷
深度優先遍歷類似樹的前序遍歷,圖的廣度優先遍歷就類似于樹的層序遍歷。
找連通網的最小生成樹,經典的有兩種算法,普里姆算法和克魯斯卡爾算法。
普里姆Prim算法:
假設N=(V,{E})是連通網,TE是N上最小生成樹中邊的集合。算法從U={u0}(u0∈V),TE={}開始。重復執行下述操作:在所有u∈U,v∈V-U的邊(u,v)∈E中找一條代價最小的邊(u0,v0)并入集合TE,同時v0并入U,直至U=V為止。此時TE中必有n-1條邊,則T=(V,{TE})為N的最小生成樹。
此算法的時間復雜度為O(n2)。
(說白了,普里姆算法是以某頂點為起點,逐步找各頂點上最小權值的邊來構建最小生成樹的。)
克魯斯卡爾(Kruskal)算法:
假設N=(V,{E})是連通網,則令最小生成樹的初始狀態為只有n個頂點而無邊的非連通圖T={V,{}},圖中每個頂點自成一個連通分量。在E中選擇代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中,否則舍去此邊而選擇下一條代價最小的邊。依次類推,直至T中所有頂點都在同一連通分量上為止。
此算法的Find函數由邊數e決定,時間復雜度為O(loge),而外面有一個for循環e次。所以克魯斯卡爾算法的時間復雜度為O(eloge)。
(說白了,把邊的權值小的先占下來當做連通分量,再試圖把他們連起來。)
對比兩個算法,克魯斯卡爾算法主要是針對邊來展開,邊數少時效率會非常高,所以對于稀疏圖有很大的優勢;而普里姆算法對于稠密圖,即邊數非常多的情況會更好一些。
計算最短路徑:
迪杰斯特拉(Dijkstra)算法——并不是一下子就求出了源點到終點的最短路徑,而是一步步求出它們之間頂點的最短路徑,過程中都是基于已經求出的最短路徑的基礎上,求得更遠頂點的最短路徑,最終得到你要的結果。
弗洛伊德(Floyd)算法——從圖的帶權鄰接矩陣A=[a(i,j)] n×n開始,遞歸地進行n次更新,即由矩陣D(0)=A,按一個公式,構造出矩陣D(1);又用同樣地公式由D(1)構造出D(2);……;最后又用同樣的公式由D(n-1)構造出矩陣D(n)。矩陣D(n)的i行j列元素便是i號頂點到j號頂點的最短路徑長度,稱D(n)為圖的距離矩陣。
時間復雜度是O(n3)。
在一個表示工程的有向圖中,用頂點表示活動,用弧表示活動之間的優先關系,這樣的有向圖為頂點表示活動的網,我們稱為AOV網(ActivityOn Vertex Network)。
拓撲序列:設G=(V,E)是一個具有n個頂點的有向圖,V中的頂點序列v1,v2,……,vn,滿足若從頂點vi到vj有一條路徑,則在頂點序列中頂點vi必在頂點vj之前。則我們稱這樣的頂點序列為一個拓撲序列。
所謂拓撲排序,其實就是對一個有向圖構造拓撲序列的過程。構造時會有兩個結果,如果此網的全部頂點都被輸出,則說明它是不存在環(回路)的AOV網;如果輸出頂點數少了,哪怕是少了一個,也說明這個網存在環(回路),不是AOV網。
對AOV網進行拓撲排序的基本思路是:從AOV網中選擇一個入度為0的頂點輸出,然后刪去此頂點,并刪除以此頂點為尾的弧,繼續重復此步驟,直到輸出全部頂點或者AOV網中不存在入度為0的頂點為止。
分析整個算法,對一個具有n個頂點e條弧的AOV網來說,掃描頂點表,將入度為0的頂點入棧的時間復雜為O(n),而之后的while循環中,每個頂點進一次棧,出一次棧,入度減1的操作共執行了e次,所以整個算法的時間復雜度為O(n+e)。
在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,我們稱之為AOE網(Activity On Edge Net-work)。
盡管AOE網與AOV網都是用來對工程建模的,但它們還是有很大的不同,主要體現在AOV網是頂點表示活動的網,它只描述活動之間的制約關系,而AOE網是用邊表示活動的網,邊上的權值表示活動持續的時間。因此,AOE網是要建立在活動之間制約關系沒有矛盾的基礎之上,再來分析完成整個工程至少需要多少時間,或者為縮短完成工程所需時間,應當加快哪些活動等問題。
我們把路徑上各個活動所持續的時間之和稱為路徑長度,從源點到匯點具有最大長度的路徑叫關鍵路徑,在關鍵路徑上的活動叫關鍵活動。
計算關鍵路徑:
1.事件的最早發生時間etv(earliest time ofvertex):即頂點vk的最早發生時間。
2.事件的最晚發生時間ltv(latest time ofvertex):即頂點vk的最晚發生時間,也就是每個頂點對應的事件最晚需要開始的時間,超出此時間將會延誤整個工期。
3.活動的最早開工時間ete(earliest time ofedge):即弧ak的最早發生時間。
4.活動的最晚開工時間lte(latest time ofedge):即弧ak的最晚發生時間,也就是不推遲工期的最晚開工時間。
第八章 查找
查找表(Search Table)是由同一類型的數據元素(或記錄)構成的集合。
靜態查找表(Static Search Table):只作查找操作的查找表。它的主要操作有:(1)查詢某個“特定的”數據元素是否在查找表中。(2)檢索某個“特定的”數據元素和各種屬性。
動態查找表(Dynamic Search Table):在查找過程中同時插入查找表中不存在的數據元素,或者從查找表中刪除已經存在的某個數據元素。顯然動態查找表的操作就是兩個:(1)查找時插入數據元素。(2)查找時刪除數據元素。
為了提高查找的效率,我們需要專門為查找操作設置數據結構,這種面向查找操作的數據結構稱為查找結構。
順序查找(Sequential Search)又叫線性查找,是最基本的查找技術,它的查找過程是:從表中第一個(或最后一個)記錄開始,逐個進行記錄的關鍵字和給定值比較,若某個記錄的關鍵字和給定值相等,則查找成功,找到所查的記錄;如果直到最后一個(或第一個)記錄,其關鍵字和給定值比較都不等時,則表中沒有所查的記錄,查找不成功。
時間復雜度為O(n)。
有序表查找:對目標實現進行有序化
折半查找:折半查找(Binary Search)技術,又稱為二分查找。它的前提是線性表中的記錄必須是關鍵碼有序(通常從小到大有序),線性表必須采用順序存儲。折半查找的基本思想是:在有序表中,取中間記錄作為比較對象,若給定值與中間記錄的關鍵字相等,則查找成功;若給定值小于中間記錄的關鍵字,則在中間記錄的左半區繼續查找;若給定值大于中間記錄的關鍵字,則在中間記錄的右半區繼續查找。不斷重復上述過程,直到查找成功,或所有查找區域無記錄,查找失敗為止。時間復雜度來是O(logn)。
插值查找(Interpolation Search)是根據要查找的關鍵字key與查找表中最大最小記錄的關鍵字比較后的查找方法,其核心就在于插值的計算公式(key-a[low])/(a[high]-a[low])。時間復雜度來也是O(logn)。
斐波那契查找算法的核心在于: 1)當key=a[mid]時,查找就成功; 2)當key<a[mid]時,新范圍是第low個到第mid-1個,此時范圍個數為F[k-1]-1個; 3)當key>a[mid]時,新范圍是第m+1個到第high個,此時范圍個數為F[k-2]-1個。
索引按照結構可以分為線性索引、樹形索引和多級索引。我們重點介紹三種線性索引:稠密索引、分塊索引和倒排索引。
稠密索引:是指在線性索引中,將數據集中的每個記錄對應一個索引項。
分塊索引:對于分塊有序的數據集,將每塊對應一個索引項,這種索引方法叫做分塊索引。
倒排索引 :記錄號表存儲具有相同次關鍵字的所有記錄的記錄號(可以是指向記錄的指針或者是該記錄的主關鍵字)。這樣的索引方法就是倒排索引(in-verted index)。
二叉排序樹(Binary Sort Tree),又稱為二叉查找樹。當我們對它進行中序遍歷時,就可以得到一個有序的序列。它或者是一棵空樹,或者是具有下列性質的二叉樹。
? 若它的左子樹不空,則左子樹上所有結點的值均小于它的根結構的值;
? 若它的右子樹不空,則右子樹上所有結點的值均大于它的根結點的值;
? 它的左、右子樹也分別為二叉排序樹。
總之,二叉排序樹是以鏈接的方式存儲,保持了鏈接存儲結構在執行插入或刪除操作時不用移動元素的優點,只要找到合適的插入和刪除位置后,僅需修改鏈接指針即可。插入刪除的時間性能比較好。而對于二叉排序樹的查找,走的就是從根結點到要查找的結點的路徑,其比較次數等于給定值的結點在二叉排序樹的層數。極端情況,最少為1次,即根結點就是要找的結點,最多也不會超過樹的深度。也就是說,二叉排序樹的查找性能取決于二叉排序樹的形狀。可問題就在于,二叉排序樹的形狀是不確定的。
所以,進行優化的方法是讓二叉樹的左右兩邊最好平衡一下,這樣二叉樹的深度最淺,查找最節省時間。
平衡二叉樹(Self-Balancing Binary SearchTree或Height-Balanced Binary Search Tree),是一種二叉排序樹,其中每一個節點的左子樹和右子樹的高度差至多等于1。
我們將二叉樹上結點的左子樹深度減去右子樹深度的值稱為平衡因子BF(Balance Factor),那么平衡二叉樹上所有結點的平衡因子只可能是-1、0和1。
距離插入結點最近的,且平衡因子的絕對值大于1的結點為根的子樹,我們稱為最小不平衡子樹。
平衡二叉樹構建的基本思想就是在構建二叉排序樹的過程中,每當插入一個結點時,先檢查是否因插入而破壞了樹的平衡性,若是,則找出最小不平衡子樹。在保持二叉排序樹特性的前提下,調整最小不平衡子樹中各結點之間的鏈接關系,進行相應的旋轉,使之成為新的平衡子樹。
多路查找樹(muitl-way search tree),其每一個結點的孩子數可以多于兩個,且每一個結點處可以存儲多個元素。由于它是查找樹,所有元素之間存在某種特定的排序關系。
2-3樹是這樣的一棵多路查找樹:其中的每一個結點都具有兩個孩子(我們稱它為2結點)或三個孩子(我們稱它為3結點)。
一個2結點包含一個元素和兩個孩子(或沒有孩子),且與二叉排序樹類似,左子樹包含的元素小于該元素,右子樹包含的元素大于該元素。不過,與二叉排序樹不同的是,這個2結點要么沒有孩子,要有就有兩個,不能只有一個孩子。
一個3結點包含一小一大兩個元素和三個孩子(或沒有孩子),一個3結點要么沒有孩子,要么具有3個孩子。如果某個3結點有孩子的話,左子樹包含小于較小元素的元素,右子樹包含大于較大元素的元素,中間子樹包含介于兩元素之間的元素。
并且2-3樹中所有的葉子都在同一層次上。
2-3-4樹:它其實就是2-3樹的概念擴展,包括了4結點的使用。一個4結點包含小中大三個元素和四個孩子(或沒有孩子),一個4結點要么沒有孩子,要么具有4個孩子。如果某個4結點有孩子的話,左子樹包含小于最小元素的元素;第二子樹包含大于最小元素,小于第二元素的元素;第三子樹包含大于第二元素,小于最大元素的元素;右子樹包含大于最大元素的元素。
B樹:,2-3樹是3階B樹,2-3-4樹是4階B樹。
一個m階的B樹具有如下屬性:
? 如果根結點不是葉結點,則其至少有兩棵子樹。
? 每一個非根的分支結點都有k-1個元素和k個孩子,其中。每一個葉子結點n都有k-1個元素,其中。
? 所有葉子結點都位于同一層次。
? 所有分支結點包含下列信息數據
B+樹是應文件系統所需而出的一種B樹的變形樹,注意嚴格意義上講,它其實已經不是第六章定義的樹了。在B樹中,每一個元素在該樹中只出現一次,有可能在葉子結點上,也有可能在分支結點上。而在B+樹中,出現在分支結點中的元素會被當作它們在該分支結點位置的中序后繼者(葉子結點)中再次列出。另外,每一個葉子結點都會保存一個指向后一葉子結點的指針。下圖是B+樹。
一棵m階的B+樹和m階的B樹的差異在于:
有n棵子樹的結點中包含有n個關鍵字;
所有的葉子結點包含全部關鍵字的信息,及指向含這些關鍵字記錄的指針,葉子結點本身依關鍵字的大小自小而大順序鏈接;
所有分支結點可以看成是索引,結點中僅含有其子樹中的最大(或最小)關鍵字。
散列表查找(哈希表)
散列技術是在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關系f,使得每個關鍵字key對應一個存儲位置f(key)。查找時,根據這個確定的對應關系找到給定值key的映射f(key),若查找集合中存在這個記錄,則必定在f(key)的位置上。我們把這種對應關系f稱為散列函數,又稱為哈希(Hash)函數。
采用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱為散列表或哈希表(Hash table)。
設計好的散列函數:1計算簡單 2散列地址分布均勻。方法有:
直接地址法:取關鍵字的某個線性函數值為散列地址
數字分析法:使用關鍵字的一部分來計算散列存儲位置的方法
平方取中法:
折疊法:折疊法是將關鍵字從左到右分割成位數相等的幾部分(注意最后一部分位數不夠時可以短些),然后將這幾部分疊加求和,并按散列表表長,取后幾位作為散列地址。
除留余數法:
隨機數法:選擇一個隨機數,取關鍵字的隨機函數值為它的散列地址。也就是f(key)=random(key)。這里random是隨機函數。當關鍵字的長度不等時,采用這個方法構造散列函數是比較合適的。
處理三列沖突的方法:
開放定址法:所謂的開放定址法就是一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,并將記錄存入。
再散列函數法:使用多個散列函數,如果發生沖突,則換一個散列函數。
鏈地址法:將所有關鍵字為同義詞的結點鏈接在同一個單鏈表中。若選定的散列表長度為m,則可將散列表定義為一個由m個頭指針組成的指針數組T[0..m-1]。
公共溢出區法:為所有沖突的單列出一個區域
散列查找的平均查找長度取決于哪些因素?1.散列函數是否均勻 2.處理沖突的方法 3.散列表的裝填因子
第九章 排序
假設ki=kj(1≤i≤n,1≤j≤n,i≠j),且在排序前的序列中ri領先于rj(即i<j)。如果排序后ri仍領先于rj,則稱所用的排序方法是穩定的;反之,若可能使得排序后的序列中rj領先ri,則稱所用的排序方法是不穩定的。
內排序是在排序整個過程中,待排序的所有記錄全部被放置在內存中。外排序是由于排序的記錄個數太多,不能同時放置在內存,整個排序過程需要在內外存之間多次交換數據才能進行。
排序算法的性能主要受3個方面的影響:時間性能、輔助空間、算法的復雜性。按照算法的復雜度分為兩大類,冒泡排序、簡單選擇排序和直接插入排序屬于簡單算法,而希爾排序、堆排序、歸并排序、快速排序屬于改進算法。
冒泡排序(Bubble Sort)一種交換排序,它的基本思想是:兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序的記錄為止。
簡單選擇排序法(Simple Selection Sort)就是通過n-i次關鍵字間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,并和第i(1≤i≤n)個記錄交換之。
直接插入排序:基本操作是將一個記錄插入到已經排好序的有序表中,從而得到一個新的、記錄數增1的有序表。
希爾排序(相當于直接插入法的升級)::將相距某個“增量”的記錄組成一個子序列,這樣才能保證在子序列內分別進行直接插入排序后得到的結果是基本有序而不是局部有序。逐漸縮小這個“增量”。
堆排序(相當于簡單選擇排序的升級):
堆是具有下列性質的完全二叉樹:每個結點的值都大于或等于其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小于或等于其左右孩子結點的值,稱為小頂堆。
堆排序(Heap Sort)就是利用堆(假設利用大頂堆)進行排序的方法。它的基本思想是,將待排序的序列構造成一個大頂堆。此時,整個序列的最大值就是堆頂的根結點。將它移走(其實就是將其與堆數組的末尾元素交換,此時末尾元素就是最大值),然后將剩余的n-1個序列重新構造成一個堆,這樣就會得到n個元素中的次大值。如此反復執行,便能得到一個有序序列了。
歸并排序:歸并排序(Merging Sort)就是利用歸并的思想實現的排序方法。它的原理是假設初始序列含有n個記錄,則可以看成是n個有序的子序列,每個子序列的長度為1,然后兩兩歸并,得到|n/2|(|x|表示不小于x的最小整數)個長度為2或1的有序子序列;再兩兩歸并,……,如此重復,直至得到一個長度為n的有序序列為止,這種排序方法稱為2路歸并排序。
快速排序(排序算法王者,20世紀十大算法之一!卻是前面最慢的冒泡排序的升級):基本思想是通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。
總結:
歸類
時間復雜度比較:
從最好情況看,反而冒泡和直接插入排序要更勝一籌,也就是說,如果你的待排序序列總是基本有序,反而不應該考慮4種復雜的改進算法。 從最壞情況看,堆排序與歸并排序又強過快速排序以及其他簡單排序。
從這三組時間復雜度的數據對比中,我們可以得出這樣一個認識。堆排序和歸并排序就像兩個參加奧數考試的優等生,心理素質強,發揮穩定。而快速排序像是很情緒化的天才,心情好時表現極佳,碰到較糟糕環境會變得差強人意。但是他們如果都來比賽計算個位數的加減法,它們反而算不過成績極普通的冒泡和直接插入。 從空間復雜度來說,歸并排序強調要馬跑得快,就得給馬吃個飽。快速排序也有相應的空間要求,反而堆排序等卻都是少量索取,大量付出,對空間要求是O(1)。如果執行算法的軟件所處的環境非常在乎內存使用量的多少時,選擇歸并排序和快速排序就不是一個較好的決策了。
從穩定性來看,歸并排序獨占鰲頭,我們前面也說過,對于非常在乎排序穩定性的應用中,歸并排序是個好算法。 從待排序記錄的個數上來說,待排序的個數n越小,采用簡單排序方法越合適。反之,n越大,采用改進排序方法越合適。這也就是我們為什么對快速排序優化時,增加了一個閥值,低于閥值時換作直接插入排序的原因。
因此對于數據量不是很大而記錄的關鍵字信息量較大的排序要求,簡單排序算法是占優的。