软件设计师-3.数据结构与算法基础
結構:結構是指元素之間的關系。
邏輯結構:元素之間的相互關系稱為數據的邏輯結構,可劃分為線性結構和非線性結構。
-
常用的線性結構有:線性表,棧隊列、數組和串。
-
常見的非線性結構有:二維數組,多維數組,廣義表,樹(二叉樹),圖。
存儲結構:數據元素及元素之間的存儲形式稱為存儲結構,可分為順序存儲和鏈接存儲兩種基本方式。
- 順序存儲時,相鄰數據元素的存放地址相鄰(邏輯與物理統一);要求內存中可用存儲單元的地址必須是連續的。
- 鏈接存儲時,相鄰數據元素可隨意存放,但所占存儲空間分兩部分,一部分存放節點信息,另一部分存放表示節點間關系的指針。
3.1 線性結構
3.1.1線性表
3.1.1.1 順序表
存儲結構:順序存儲結構(順序表)
存儲方式:內存是連續分配的,并且是靜態分配的,在使用前需要分配固定大小的空間。
操作:
訪問第i個元素:根據下標,可以直接訪問對應的元素。
查找是否含有某值:依次比對每個元素的值,直到找到。
刪除第i個元素
例如刪除 a4,刪除后,需要將 a5、a6、a7依次向前移動一個位置。
含有n個元素的線性表采用順序存儲,等概率刪除其中任一個元素,平均需要移動(n-1)/2個元素。
第i元素前插入某值
例如在 a4 前插入元素,則a4、a5、a6、a7依次向后移動一個位置,然后再插入新元素。
含有n個元素的線性表采用順序存儲,等概率插入一個元素,平均需要移動n/2個元素。
3.1.1.2 鏈表
存儲結構:鏈式存儲結構(鏈表)。
鏈表(linked-list)存儲方式:鏈表的內存是不連續的,前一個元素存儲地址的下一個地址中存儲的不一定是下一個元素。訪問某結點應找上一結點提供的地址,每一節點有一指針變量存放下一結點的地址。數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
操作:
訪問第i個元素:通過head開始查找,根據地址指針找到下一個元素,依次找到第i個元素
查找是否含有某值:依次比對每個元素的值,直到找到。
刪除第i個元素:
例如刪除B結點,刪除后,A的下一個地址需要指向C結點,即1021
第i元素前插入某值:
例如在B結點前插入,插入元素后,A結點的下一個地址為新插入結點的地址;新插入結點的下一個地址為 B。
其他概念
-
尾結點:最后一個有效結點。
-
首結點:第一個有效結點。
-
頭結點:第一個有效結點之前的那個結點,存放鏈表首地址。
-
頭指針:指向頭結點的指針變量。
-
尾指針:指向尾結點的指針變量。
特點:
以上講解的是單鏈表,除了單鏈表外,鏈表該有循環鏈表、雙向鏈表,如下圖所示
數據結構中的邏輯結構是指數據對象中元素之間的相互關系。按邏輯結構可將數據結構分為()。
A.靜態結構和動態結構
B.線性結構和非線性結構
C.散列結構和索引結構
D.順序結構和鏈表結構
答案 B
若某線性表長度為n且采用順序存儲方式,則運算速度最快的操作是()。
A.查找與給定值相匹配的元素的位置
B.查找并返回第i個元素的值(1≤i≤n)
C.刪除第i個元素(1≤i≤n)
D.在第i個元素(1≤i≤n )之前插入一個新元素
答案 B
以下關于單鏈表存儲結構特征的敘述中,不正確的是()。
A. 表中結點所占用存儲空間的地址不必是連續的
B. 在表中任意位置進行插入和刪除操作都不用移動元素
C. 所需空間與結點個數成正比
D. 可隨機訪問表中的任一結點
答案 D
3.1.2棧和隊列
隊列:先進先出(FIFO——first in first out)
- 隊尾(rear)進行插入工作;
- 隊頭(front)進行讀取(刪除)操作。
棧:先進后出(FILO——first in last out);
- 棧頂(top):進行插入和讀取(刪除)操作的一端稱為棧頂;
- 棧底(bottom):固定不變,不可進行插入和刪除操作;
- 進棧: Push the stack
- 出棧:Pop the stack
題1:元素按照 a、b、c的次序入棧,請嘗試寫出其所有可能的出棧序列。
這類題的解法主要是先將abc三個字符的素有排列給寫出來,然后看看哪個是不可能的。
列出所有的排列: abc、acb、bac、bca、cab、cba
然后看看哪個不符合要求:cab
所以答案是 abc、acb、bac、bca、cba
3.1.3串
由字符(數組、字母、下劃線等)構成的一維數組。
概念:
- 空串:無任何字符的字符串。
- 空白串:由空白符號(空格、制表符等)構成的字符串。
- 子串:串中任一個連續的字符組成的子序列稱為該串的子串。
- 非平凡子串:非空且不等同意字符串本身。
- 串的模式匹配:模式串在主串中首次出現的位置。
- 字符串比較:從左至右按取ASCII碼值進行比較。
設S 是一個長度為n的非空字符串,其中的字符各不相同,則其互異的非平凡子串(非空且不同于S本身)個數為( )。
A.2n-1
B.n2
C.n(n+1)/2
D.(n+2) (n-1)/2
答案 選D
分析:我們可以使用帶入法來對本地進行分析,不需要真的推導公式。
比如,當長度為1時,它的非平凡子串的個數是0,看看哪個公式符合即可;若還是不行,則當長度為2時,它的非平凡子串的個數是2,再看看哪個公式符合。
3.2 數組、矩陣和廣義表
3.2.1 數組
數組是由n個相同的元素所組成的序列。
注意:
3.2.1.1 一維數組
A[i]的存儲地址為:a+i*len。 首地址為a,len表示單個元素所占用的存儲單元。
以存放首地址為100,每個元素占用3個存儲單元為例。
A[i] 地址 = 100 + i*3
3.2.1.2 二維數組
3*4的二維數組
假設有n*m的二維數組,下標從0開始,按行存儲,則
- A[i][j]的偏移量為 (i*n+j) * len
- A[i][j]的存儲地址為 (i*n+j) * len + 數組首地址
3.2.2 矩陣
矩陣是很多科學與工程計算領域研究的數學對象。在數據結構中,主要討論如何在節省存儲空間的情況下使矩陣的各種運算能高效的進行。
3.2.2.1 特殊矩陣
在一些矩陣中,存在很多相同的元素或者是0的元素。為了節省空間,可以對這類矩陣進行壓縮存儲,即多個值相同的元素只分配一個存儲單元,對0不分配存儲單元。假如值相同的元素或0元素在矩陣中的分布有一定的規律,則稱此類矩陣為特殊矩陣,否則為稀疏矩陣。
對稱矩陣:若每一對元素(Aij、Aji)僅占用一個存儲單元,則可將n2個元素壓縮存儲到能存放 n(n+1)/2 個元素的存儲空間中。假設一維數組 B[n(n+1)/2]作為n階對稱矩陣A中元素的存儲空間,則 B[k] 與 矩陣元素 Aij 之間存在一一對應的關系,如下所示 $$ k = i(i-1)/2 + j k = j(j-1)/2 + i $$ 對角矩陣:若以行為主序將n階三角矩陣的非0元素近占用一個存儲單元,則可將n2個元素壓縮存儲到能存放 3n-2 個元素的存儲空間中。假設一維數組B[3n-2]z作為n階對角矩陣A中元素的存儲空間,則B[k]與 矩陣元素 Aij 之間存在一一對應的關系,如下所示 $$ k=3×(i-1) + j-i+1+1 = 2i +j-2 $$
3.2.2.2 非特殊矩陣(稀疏矩陣)
稀疏矩陣:非0元素的個數遠遠少于0元素的個數,且非0元素的分布沒有規律。
對于稀疏矩陣,存儲非0元素時必須同時存儲器位置(即行號和列號),所以三元組(i,j,a??) 可唯一確定矩陣中的一個元素。
三元組表為 (1,2,12),(1,4,9),(2,4,7),(3,1,1),(4,1,2),(4,4,1)
3.2.2.3 矩陣的乘法
矩陣的乘法運算:設A為m*p的矩陣,B為p*n的矩陣,那么m*n的矩陣C為矩陣A與B的乘積,記作 C = AB,其中C中的第i行第j列元素可以表示為:
如下所示:
A的行與B的列依次相乘
f(1)=1,f(2)=1,n>2時f(n)=f(n-1)+f(n-2) 據此可以導出,n>1時,有向量的遞推關系式:
(f(n+1),f(n))=(f(n),f(n-1))A
其中A是2x2矩陣( )。從而,(f(n+1),f(n)=(f(2),f(1))*( )
解析:
首先我們知道f(n)=f(n-1)+f(n-2),即n位置的數是前兩個數字之和。 f(n+1)=f(n)+f(n-1) 是成立的, f(n-1)=f(n-2)+f(n-3)也是成立的。
選項1:
- 這是一個矩陣乘法,我們可以使用代入的方式來進行計算。
- (f(n),f(n-1))與A選項矩陣進行相乘
- 結果是 ( f(n)x0+f(n-1)x1, f(n)x1+f(n-1)x1 ) = ( f(n-1), f(n) + f(n-1) )
- 而 f(n+1)=f(n)+f(n-1) ,對上面的結果進行替換, ( f(n-1), f(n+1) )
- 算出的結果不等于 (f(n+1),f(n))
- (f(n),f(n-1))與B選項矩陣進行相乘
- 結果是 ( f(n)x1+f(n-1)x1, f(n)x0+f(n-1)x1 ) = ( f(n) + f(n-1), f(n-1) )
- 替換后 ( f(n+1), f(n-1) ) 結果也是錯誤的
- (f(n),f(n-1))與C選項矩陣進行相乘
- ( f(n)x1+f(n-1)x0, f(n)x1+f(n-1)x1 ) = ( f(n), f(n) + f(n-1) )
- 替換后 ( f(n), f(n+1) ) 結果也是錯誤的
- (f(n),f(n-1))與D選項矩陣進行相乘
- ( f(n)x1+f(n-1)x1, f(n)x1+f(n-1)x0 ) = ( f(n) + f(n-1), f(n) )
- 替換后 ( f(n+1), f(n) ),結果正確
- 所以選項1的答案是 D
選項2:
- 由選項1可知 ( f(n+1),f(n) )=( f(n),f(n-1) )A
- 對n進行降維,即令n=n-1后, ( f(n),f(n-1) )=( f(n-2),f(n-3) )A
- 繼續降維 ( f(n-1),f(n-2) )=( f(n-3),f(n-4) )A
- ...
- 最終 ( f(3),f(4) ) = ( f(2),f(1) )A
- 再次回到 ( f(n+1),f(n) )=( f(n),f(n-1) )A ,將 f(n),f(n-1)替換成 ( f(n-2),f(n-3) )A ,則 ( f(n+1),f(n) )= ( f(n+1),f(n) )=( f(n-2),f(n-3) ) A2
- 繼續替換 ( f(n-2),f(n-3) ),則 ( f(n+1),f(n) )=( f(n-3),f(n-4) ) A3
- 最終發現由 ( f(n+1),f(n) ) 替換到 ( f(2),f(1) ) A,一共經過了n-1次替換。(也可以發現規律,就是后一個參數與A的次方之后等于n,則 ( f(2),f(1) ) 情況下,應該是A的n-1次方)
- 所以答案選擇 A
3.2.3 廣義表
廣義表(Lists,又稱列表)是一種非連續性的數據結構,是線性表的一種推廣。即廣義表中放松對表元素的原子限制,容許它們具有其自身結構。
廣義表通常記作: $$ Ls=( a1,a2,…,an) $$ ai即可以是一個元素,也可以是一個廣義表,分別稱為原子和子表。
廣義表的長度是指廣義表中元素的個數。廣義表的深度是指廣義表展開后所含的括號的最大層數。
在此,只討論廣義表的兩個重要的基本運算:取表頭head(Ls)和取表尾tail(Ls)。
- 取表頭head(Ls):非空廣義表LS的第一個元素為表頭,它可以是一個單元素,也可以是一個子表。
- 取表尾tail(Ls):在非空廣義表中,出表頭元素外,由其余元素所構成的表稱為表尾。非空廣義表的表尾必定是一個表。
特點:
- 廣義表可以是多層次結構,因為廣義表的元素可以是子表,而子表的元素還可以是子表。
- 廣義表的元素可以是已經定義的廣義表的名字,所以一個廣義表可以被其他廣義表所共享。
- 廣義表可以是一個遞歸表,即廣義表的元素也可以是本廣義表的名字
存儲結構,采用鏈式存儲。
由于廣義表中可同時存儲原子和子表兩種形式的數據,因此鏈表節點的結構也有兩種,如圖所示:
表示原子的節點由兩部分構成,分別是 tag 標記位和原子的值,表示子表的節點由三部分構成,分別是 tag 標記位、hp 指針和 tp 指針。
tag 標記位用于區分此節點是原子還是子表,通常原子的 tag 值為 0,子表的 tag 值為 1。子表節點中的 hp 指針用于連接本子表中存儲的原子或子表,tp 指針用于連接廣義表中下一個原子或子表。
例如,廣義表 {a,{b,c,d}} 是由一個原子 a 和子表 {b,c,d} 構成,而子表 {b,c,d} 又是由原子 b、c 和 d 構成,用鏈表存儲該廣義表如圖 2 所示:
3.3 樹
3.3.1 樹與二叉樹的定義
?樹的基本概念
父結點:例如結點1是結點2的父節點
子結點:結點2是結點1的子節點
兄弟結點:擁有同一父結點的兩個子結點為兄弟結點。例如 結點2和結點3是兄弟節點
葉子結點:無子結點的結點。如 結點4是葉子節點。
結點度:一個結點擁有幾個子節點,它的度就是幾。例如結點2的度為2
樹的度:所有結點中,度最大的那個結點的度就是樹的度。如上圖,所有的結點中,每個節點的度最大不超過2,所以樹的度為2.
樹的度為2的樹就是二叉樹。s
層(深度、高度):一共有多少層。如上圖,層是4層。
結點1是整個樹的根節點。
3.3.2 二叉樹的性質與存儲結構
?二叉樹的劃分
滿二叉樹:每一層都不能再插入一個結點。
完全二叉樹:1~倒數第二層是滿的,最后一層葉子節點是從左到右依次排序(左全右空)。
非完全二叉樹:不是滿二叉樹、完全二叉樹的樹就是非完全二叉樹。
?二叉樹的特性
在二叉樹的第i層上最多有2 ? ?1個結點(i≥1)
深度為k的二叉樹最多有2?-1個結點(k≥1)
葉子節點數為n。度為2的節點數為m,則n=m+1。
如果對一顆有n個結點的完全二叉樹的結點按序編號(從第一層到log?n+1層,每層從左到右),則對任一結點i(1≤i≤n),有:
-
如果i=1,則結點i無父結點,是二叉樹的根;如果i>1,則父結點是 i/2。
-
如果 2i>n,則結點 i 為葉子結點,無左子結點;否則,其左子結點是結點2i。
例如下圖 9,2i=18 > 17,無左孩子結點
-
如果 2i+1>n,則結點i無右子結點,否則其右子結點是結點 2i+1。
例如下圖 9,2i+1=19 > 17,無右孩子結點
-
以上規律可以結合下圖來看
提問:一個二叉樹如果共有65個節點,問至少有多少層?最多有多少層?
最少有7層,可以套用公式深度為k的二叉樹最多有2?-1個結點,當有6層是最多有63個結點,放不下65,所以最少有7層。
最多有65層,每層放一個結點。
?二叉樹的存儲結構
顯然對于完全二叉樹和滿二叉樹采用順序存儲結構即簡單又節省空間,對于一般的二叉樹則不宜采用順序存儲結構。因為一般的二叉樹也必須按照完全二叉樹的形式存儲,也就是要添加上一些實際不存在的虛點。
如下圖所示
對于普通的二叉樹采用鏈式存儲結構
由于二叉樹包含數據元素、左子樹的根、右子樹的根及雙親信息,因此可以用三叉鏈表或二叉鏈表來存儲二叉樹,鏈表的頭指針指向二叉樹的根結點。
題1:對下圖所示的二叉樹進行順序存儲(根結點編號為1,對于編號為i的結點,其左孩子結點為2i,右孩子結點為2i+1)并用一維數組BT來表示。已知結點X、E和D在數組BT中的下標為分別為1、2、3,可推出結點G、K和H在數組BT中的下標分別為( )。
A.10、11、12
B.12、24、25
C.11、12、13
D.11、22、23
答案 D
分析:由題可知 E下標2,它的右子節點F的下標為 2E+1 = 5;
? G的坐標為 2F+1=11
? K的坐標為 2G = 22
? H的坐標為 2G+1=23
題2:完全二叉樹的特點是葉子結點分布在最后兩層,且除最后一層之外,其他層的結點數都達到最大值,那么25個結點的完全二叉樹的高度(即層數)為()。
A.3
B.4
C.5
D.6
答案 5
3.3.3二叉樹的遍歷
-
前序/先序遍歷:先遍歷根結點,然后遍歷左子樹,最后遍歷右子樹。
上圖前序遍歷的結果是: 1 2 4 5 7 8 3 6
-
中序遍歷:先遍歷左子樹,然后遍歷根結點,最后遍歷右子樹
上圖前序遍歷的結果是:4 2 7 8 5 1 3 6
-
后序遍歷:先遍歷左子樹,然后遍歷右子樹,最后遍歷根結點
上圖前序遍歷的結果是:4 8 7 5 2 6 3 1
-
層序遍歷:從上往下逐層遍歷
上圖前序遍歷的結果是:1 2 3 4 5 6 7 8
提問:由前序為 ABHFDECG;中序序列為 HBEDFAGC 構造的二叉樹。
由前序序列找到根結點,再有中序序列來確認哪個是左子結點,哪個是右子結點。
由前序序列,知道 A 為根結點。
由中序序列,知道左子樹為 HBEDF ,右子樹為 GC。
再回到前序序列,根據步驟2知道,BHFDE 是左子樹,CG為右子樹。
根據步驟3, C 是右子樹的根結點。
步驟2可知,中序遍歷是 GC 是右子樹,可知道 G 是 C 的左子節點。
由步驟3 知道左子樹的前序序列為 BHFDE,可知B是根結點
由步驟2 HBEDF ,可知H 為左節點,EDF 為右子樹
由前序 FDE 可知, F是根結點
由中序EDF,可知,ED為F的左子樹
由前序 DE 可知,D是根結點
由中序 ED,可知 E 是 D 的左子結點
3.3.4線索二叉樹
二叉樹的遍歷實質上是一個非線性結構進行線性化的過程,它使得每個節點(除第一個和最后一個)在這些線性序列中有且僅有一個直接前驅和直接后繼。但在二叉鏈表存儲結構中只能找打一個節點的左、右孩子,不能直接得到結點在任一遍歷序列中的前驅和后繼,這些信息只能在遍歷的動態過程中才能得到,因此引入線索二叉樹來保存這些動態過程得到的信息。
為了保持前驅和后繼信息,可考慮在每個節點中增加兩個指針域來存放遍歷時得到的前驅和后繼信息,這樣就可以為以后的訪問帶來方便。但增加指針信息會降低存儲空間的利用率,因此可考慮采用其他方法。
若n個節點的二叉樹采用二叉鏈表做存儲結構,則鏈表中必然有n+1個空指針域,可以使用這些空指針域來存放結點的前驅和后繼信息。為此,需要在節點中增加ltag和rtag,以區分孩子指針的指向,如下所示: $$ |ltag|lchild|data|rchild|rtag| $$ 其中:
ltag:0-lchild域指示結點的左孩子;1-lchild域指示結點的直接前驅。
rtag:0-rchild域指示結點的右孩子;1-rchild域指示結點的直接后繼。
若二叉樹的二叉鏈表采用以上所示的結構,則響應的鏈表稱為線索鏈表,其中指向節點前驅、后繼的指針稱為線索。加上線索的二叉樹稱為線索二叉樹。對二叉樹以某種次序遍歷使其稱為線索二叉樹的過程稱為線索化。中序線索二叉樹及其存儲結構如圖所示。
如何進行線索化呢?
實質上是在遍歷過程中用線索取代空指針。因此,設指針p執行正在訪問的結點,則遍歷時設立一個指針 pre,使其時鐘執行剛剛訪問過的節點(即p所示結點的前驅結點),這樣就記下了遍歷過程中結點被訪問的先后關系。
需要說明的是,用這種方法得到的線索二叉樹,其線索并不完整。也就是說部分節點的前驅或后繼信息還需要通過進一步運算來得到。
如何在線索二叉樹中查找結點的前驅和后繼呢?以中序線索二叉樹為例,令p執行樹種的某個結點,查找p所指結點的后繼節點的方法:
3.3.5特殊二叉樹
3.3.5.1 二叉查找樹
左子樹小于根,右子樹大于根
特點:
上圖插入結點:序列(89,48,56,48,20,112,51)
3.3.5.2 哈夫曼樹(最優二叉樹)
需要了解的基本概念:
-
樹的路徑長度:從樹根到樹中每一結點的路徑長度之和。
如下圖左邊的樹,結點2的路徑長度為2,結點4的長度為3,結點8的長度為3,結點1的長度為1
-
權:在一些應用中,賦予樹中結點的一個有某種意義的實數。
-
帶權路徑長度:結點到樹根之間的路徑長度與該結點上權的乘積。
如下圖左邊的樹,結點2的帶權路徑長度為2x2=4,結點4的長度為3x4=12,結點8的長度為3x8=24,結點1的長度為1*1=1
-
樹的帶權路徑長度(樹的代價):樹中所有葉結點的帶權路徑長度之和。
如下圖左邊的樹,樹的代價 = 4+12+24+1=41
哈夫曼樹就是樹的代價最小的那顆樹。
假設有一組權值 50,20,30,40,10,請嘗試構造哈夫曼樹。
最小的兩個構造一顆子樹,10和20 ,并讓他們的父節點為子節點之和即30
從上面剔除10,20,并將30加入即【50,30,30,40】,再找到最小的兩個值 30,30,再次按照步驟1構造樹
再次按照步驟2得到數組【50,60,40】,選擇最小的進行樹的構造
再次按照步驟2得到數組【90,60】,選擇最小的進行樹的構造
3.3.6樹和森林
3.3.6.1樹的存儲結構
樹的雙親表示法(雙親結點就是父結點)
該表示法用一組地址連續的單元存儲樹的結點,并在每個結點中附設一個指示器,指出其雙親在該存儲結構中的位置(即父結點所在元素的下標)。
?
雙親表示法求指定結點的雙親和父結點都十分方便,但是求一個結點的孩子結點的時候就比較麻煩。
樹的孩子表示法
孩子表示法就是把每個節點的孩子節點存到一個單鏈表中,這個鏈表成為“孩子鏈表”,每個節點都對應一個孩子鏈表,沒有孩子的結點,對應的孩子鏈表為空,結點的數據和孩子的頭指針,我們還是用一個順序表來存儲。
孩子表示法在找節點的孩子的時候十分方便,但是在找他的雙親的時候又有些麻煩,于是我們可以在這個順序表的每個結點加上一個雙親域形成帶雙親的孩子表示法。
孩子兄弟表示法
孩子兄弟表示法是三種存儲方式中最好操作的,它的本質是二叉樹,只不過,它的右指針從右孩子變成了兄弟,其他與二叉樹相同,如下圖。
如果想找到某個節點的第n個孩子,可以先通過他的指針找到第一個孩子,然后通過第一個孩子的兄弟結點遍歷n-1次,此時得到的結點就是它的第n個孩子。
3.3.6.2 樹和森林的遍歷
由于樹的每個結點可以有多個子樹,因此遍歷樹的方法有兩種,即先根遍歷和后根遍歷。
先根遍歷
先訪問樹的根結點,然后依次先根遍歷各課子樹。對樹的先根遍歷等同于對轉換的二叉樹進行先序遍歷。
后根遍歷
先依次后邊遍歷樹根的各課子樹,然后訪問樹的根結點。對樹的后根遍歷等同于對轉換的二叉樹進行中序遍歷。
森林的遍歷
先序遍歷
若森林非空,首先訪問森林中第一棵樹的根結點,然后先序遍歷第一課樹根結點的子樹森林,最后先序遍歷出第一課樹之外剩余的樹所構成的森林。
中序遍歷
若森林非空,首先訪問森林中第一棵樹的子樹森林,然后先序遍歷第一課樹的根結點,最后中序遍歷出第一課樹之外剩余的樹所構成的森林。
3.3.6.3 樹、森林和二叉樹之間的轉化
二叉樹與樹是一 一對應的關系,給定一棵樹有其對應的唯一的二叉樹,同理,給定一個二叉樹,也有唯一對應的樹(或森林)與之對應。
二叉樹與樹轉化的實質就是,拿右指針為其兄弟,左指針為其孩子的二叉樹解釋成為一棵樹,本質是與二叉樹差不多的,只是他的右指針不再是他的右孩子,而是他的兄弟了,所以我們在解釋的時候一定要注意他的指向含義。
根據我們對二叉樹的定義,我們知道,任何一棵樹對應的二叉鏈表的根節點的是沒有兄弟的,那么如果我們遇到了,根節點有兄弟的我們應該如何理解呢?
其實,這個時候,我們可以將森林中的各個樹的根節點,視為兄弟,這樣子,這個樹我們就可以解釋了,其實他是一個森林對應的二叉樹。他的根節點和他的第一個孩子視為是這個森林中的第一個樹,右節點則是森林中的其他的樹,這樣子我們對根節點有兄弟的二叉樹也可以做解釋。
樹轉二叉樹
森林轉二叉樹
二叉樹轉森林
相當于2的逆過程,對于每一個右節點已經不再是右孩子,而是該結點對應的兄弟,除此之外其余正常
3.4 圖
3.4.1圖的定義和存儲
3.4.1.1 圖的定義
完全圖:
- 在無向圖中,若每對頂點之間都有一條邊相連,則稱該圖為完全圖。
- 在有向圖中,若每對頂點之間都有2條有向邊相連,則稱該圖為完全圖。
問題:n個頂點的無向圖和有向圖的完全圖的邊的個數為多少?
答案:無向圖的完全圖有n(n-1)/2個邊,有向圖的完全圖有n(n-1)個邊。
連通圖:指圖中任意兩個頂點之間都有一個路徑相連
注意:
針對有向圖而言,有強連通(帶上方向是連通圖)和弱連通(帶上方向不是連通圖,去掉方向后是連通圖)。
3.4.1.2 圖的轉換-無向圖轉鄰接矩陣
用一個n階方陣R來存放圖中各結點的關聯信息,其矩陣元素R??定義為:
無向圖轉換的鄰接矩陣為對稱矩陣。
矩陣列和行分別表示頂點的序號,R??
-
i對應的行序號,j對應的列序號。例如下圖,圈出的表示 R??=1,R??=0
-
針對行出發,標記與其他列頂點的關系。例如,從第2行,標注的是頂點2與其他頂點的關系。
-
針對列出發,標記與其他行頂點的關系。例如,從第2列,標注的是頂點2與其他頂點的關系。
3.4.1.3 圖的轉換-有向圖轉鄰接矩陣
與無向圖的轉換方式相同,當路徑上有權值的時候,在矩陣中就填入權值,否則就填入0、1。
-
該頂點為起點的有向邊個數,叫做出度。其實就是從改行的頂點出發的所有路徑。
-
該頂點為終點的有向邊個數,,叫做入度。其實就是所有指向達該頂點的路徑。
-
出入與入度之和叫做度。
3.4.1.4 圖的轉換-有向圖轉鄰接鏈表
首先把每個頂點的鄰接頂點用鏈表表示,然后用一個一維數組來順序存儲上每個鏈表的表頭指針。
3.5 算法特性與復雜度
3.5.1 算法特性
基本特性
-
有窮性:執行有窮步之后結束。
-
確定性:算法中每一條指令都必須有確切的含義,不能含糊不清。
-
輸入輸出數目約定:輸入(>=0)。輸出(>=1)。
-
有效性(可行性):算法的每個步驟都有效執行并能得到確定的結果。
例如 a=0,b/a就是無效的。
算法評價指標:
- 正確性:正確實現算法功能,最重要的指標。
- 友好性:具有良好的使用性。
- 可讀性:可讀、可以理解的,方便分析、修改和移植。
- 健壯性:對不合理的數據或非法的操作能進行檢查、糾正。
- 效率:對計算機資源的消耗,包括計算機內存和運行時間的消耗。
3.5.2 算法復雜度
時間復雜度
程序運行從開始到結束所需要的時間。通常分析時間復雜度的方法是從算法中選取一種對于所研究的問題來說是基本運算的操作,以該操作重復執行的次數為算法的時間度量。一般來說,算法中原操作重復執行的次數是規模n的某個函數T(n)。由于許多情況下要精確計算T(n)是困難的,因此引入了漸進時間復雜度在數量上估計一個算法的執行時間。其定義如下:
如果存在兩個常數c和m,對于所有的n,當n>=m時由 f(n) <= g(n),則有f(n)=O(g(n))。也就是說,隨著n的增大, f(n) 逐漸也不大于 g(n) 。例如,一個程序的實際執行時間常見的對算法執行所需時間的度量: $$ O(1)<O(log?(n))<O(n)<O(nlog?(n))<O(n2)<O(n3)<O(2?) $$
O(1) :一次可以執行完,沒有循環、遞歸
O(log?(n)):對半拆分、對半拆分。二分查找
O(n):依次循環,例如輸出數組元素
O(nlog?(n)):歸并排序、快速排序
O(n2):插入排序、選擇排序
考試一般會考,對應的查找、排序算法,它的時間復雜度。
空間復雜度
是指對一個算法在運行過程中臨時占用存儲空間大小的度量。一個算法的空間復雜度值考慮在運行過程中為局部變量分配的存儲空間的大小。
3.6 查找
3.6.1順序查找
順序查找:將待查找的關鍵字跟表中的數據從頭至尾按順序進行比較。
平均查找長度(等概率情況):
3.6.2二分查找
二分法查找(折半查找)的基本思想是:(設R[low,...,high]是當前的查找區)
- 若 R[mid] > k,則由表的有序性可知,R[mid,...,high]均大于k,因此若表中存在關鍵字等于k的結點,則該結點必定是在 mid 左邊的子表R[low,...,mid-1]中。因此,新的查找區間是左子表R[low,...,high],其中high=mid-1。
- 若 R[mid] < k,則要查找的k必定在 mid 右邊的子表R[mid+1,...,high]中。因此,新的查找區間是左子表R[low,...,high],其中low=mid+1。
- 若 R[mid] = k,則查找成功,算法結束。
例子,請給出在含有12個元素的有序表{1,4,10,16,17,18,23,29,33,40,50,51} 中二分查找關鍵字17的過程。
二分查找在查找成功時關鍵字的比較次數最多為 log?(n) + 1 次
二分查找的時間復雜度為 O(log?(n)) 次
二分查找僅適用于元素有序的順序表
二分查找(循環法)
int bigSearch(int r[], int low, int high, int key){//r[low,...,high]中的元素按非遞減順序,用二分查找法在數組r中查找與key相同的元素,若找到則返回該元素在數組的下標,否則返回-1int mid;while (low<=high) {mid = (low+high)/2;if(key == r[mid]) return mid;else if(key < r[mid]) high = mid-1;else low = mid+1;}return -1; }非遞減即遞增的!
二分查找(遞歸法)
int bigSearch(int r[], int low, int high, int key){//r[low,...,high]中的元素按非遞減順序,用二分查找法在數組r中查找與key相同的元素,若找到則返回該元素在數組的下標,否則返回-1int mid;if(low<=high) {mid = (low+high)/2;if(key == r[mid]) return mid;else if(key < r[mid]) bigSearch(r,low,mid-1,key) ;else bigSearch(r,mid+1,high,key);}return -1; }3.6.3散列表查找
3.6.3.1 散列表查找-線性探查法
散列表查找的基本思想是:已知關鍵字集合U,最大關鍵字為m,設計一個函數Hash,它以關鍵字為自變量,關鍵字的存儲地址為因變量,將關鍵字映射到一個有限的、地址連續的區間 T[0,...,n-1] (n<<m) 中,這個區間就稱為散列表,散列表查找中使用的轉換函數稱為散列函數。
關鍵碼為(3,18,26,29,17),存儲空間為10,散列函數 h = key % 7
3.6.3.2 散列表查找-拉鏈法
關鍵碼為(3,18,26,29,17),散列函數 h = key % 7,用鏈地址法。
3.7 排序
穩定排序與不穩定排序
穩定排序通俗地講就是能保證排序前2個相等的數其在序列的前后位置順序和排序后它們兩個的前后位置順序相同。在簡單形式化一下,如果Ai = Aj,Ai原來在位置前,排序后Ai還是要在Aj位置前。與之相反就是不穩定排序。
內排序與外排序
內排序是被排序的數據元素全部存放在計算機內存中的排序算法。若待排序記錄的數量龐大,在排序的過程中需要使用到外部存儲介質如磁盤等,這種涉及內外存儲器數據交換的排序過程稱為外部排序,又稱為外排序。
排序方法分類:
-
插入類排序
直接插入排序 、希爾排序
-
交換類排序
冒泡排序、快速排序
-
選擇類排序
簡單選擇排序、堆排序
-
歸并排序
-
基數排序
3.7.1 插入類排序-直接插入排序
即當插入第i個記錄時,R?,R?,...,R??? 均已排好序,因此,將第i個記錄 R? 依次與 R???,...,R?,R? 進行比較,找到合適的位置插入。它簡單明了,但速度很慢。
元 素 :16 3 25 11 17
第一趟:16 3 25 11 17
第二趟:3 16 25 11 17
第三趟:3 16 25 11 17
第四趟:3 11 16 25 17
結 果 :3 11 16 17 25
void insertSort(int data[], int n) {/**用直接插入排序法將data[0]~data[n-1]中的n個整數進行升序排列*/int i,j; int tmp;for(i=1; i<n; i++) {if(data[i] < data[i-1]) { // 將data[i]插入有序子序列data[0]~data[i-1]tmp = data[i]; // 備份待插入的元素data[i] = data[i-1];for(j=i-2; j>=0&&data[j]>tmp; j--)//查找插入位置并將元素后移data[j+1]=data[j];data[j+1]=tmp;//插入正確位置}} }時間復雜度 O(n2)
穩定性穩定的
空間復雜度 O(1)
使用的元素基本有序且與算法的排序方向一致,它會非常快速。
3.7.2 插入類排序-希爾排序
先取一個小于n的整數 d? 作為第一個增量,把文件的全部記錄分成 d? 個組。所有距離 d? 的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然后,取第二個增量 d? < d? 重復上述的分組和排序,直至所取的增量 d? = 1(d? <d??? <O<d? < d?),即所有記錄放在同一組中進行直接插入排序為止。該方法實質行是一種分組插入方法。
在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2,(n/2)/2...1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但其實這個增量序列不是最優的。此處我們做示例使用希爾增量。
void hearSort(int data[], int len) {int i,j,k,tmp,gap;//gap為希爾增量for(gap=len/2; gap>0; gap/=2) {for(i=0;i<gap;i++) {// 變量 i 為每次分組的第一個元素下標for(j=i+gap; j<len; j+=gap) { //對步長為gap的元素進行直插排序,當gap為1時,就是直插排序tmp = data[j];// 備份 data[j] 的值for(k=j-gap; k>=0&&data[k]>tmp;k-=gap) {data[k+gap]=data[k];}data[k+gap]=tmp;//將其插入正確的位置}}} }時間復雜度:O(n2)
穩定性:不穩定
空間復雜度:O(1)
3.7.3 選擇類排序-直接選擇排序
過程:首先在所有記錄中選出排序碼最小的記錄,把它與第1個記錄交換,然后再其余的記錄內選出排序碼最小的記錄,與第二個記錄交換......依次類推,直到所有記錄排完為止。
void selectSort(int data[], int n) {int i,j,k;int temp;for(i=0; i<n-1; i++) {for(k=i,j=i+1; j<n;j++) { //k表示data[i]~data[n-1]中最小元素的下標if(data[j]<data[k]) k=j;}if(k!=i) { //將本趟找出的最小元素與data[i]交換temp = data[i];data[i] = data[k];data[k] = temp;}} }時間復雜度:O(n2)
穩定性:不穩定
空間復雜度:O(1)
3.7.4 選擇類排序-堆排序
堆是具有以下性質的完全二叉樹:每個結點的值都大于或等于其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小于或等于其左右孩子結點的值,稱為小頂堆。如下圖:
同時,我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子
該數組從邏輯上講就是一個堆結構,我們用簡單的公式來描述一下堆的定義就是:
大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反復執行,便能得到一個有序序列了。
步驟:
構造初始堆。將給定無序序列構造程一個大頂堆(一般升序采用大頂堆,降序采用小頂堆)。
假設給定無序序列結構如下
此時我們從最后一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
找到第二個非葉節點4,由于[4,9,8]中9元素最大,4和9交換。
這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。
此時,我們就將一個無需序列構造成了一個大頂堆
將堆頂元素與末尾元素進行交換,使末尾元素最大。然后繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反復進行交換、重建、交換。
將堆頂元素9和末尾元素4進行交換
重新調整結構,使其繼續滿足堆定義
再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.
后續過程,繼續進行調整,交換,如此反復進行,最終使得整個序列有序
堆排序的時間復雜度為:O(nlog?(n))
穩定性:不穩定
空間復雜度:O(1)
3.7.5 交換類排序-冒泡排序
冒泡排序的基本思想是,通過相鄰元素之間的比較和交換,將排序較小的元素逐漸從底部移向頂部。由于整個排序的過程就像水底下的氣泡一樣逐漸向上冒,因此稱為冒泡算法。
void bubbleSort(int arr[], int n) { //冒泡排序int temp;int i,j;for(i=0;i<n-1;i++) { //外循環排序為排序趟數,n個數進行n-1趟for(j=0;j<n-1-i;j++) { //內循環為每趟比較的次數,第趟比較 n-i 次if(arr[j] > arr[j+1]) { //相鄰元素比較,若逆序則交換temp = arr[j];arr[j]=arr[j+1];arr[j+1]=temp;}}} }時間復雜度:O(n2)
穩定性:穩定
空間復雜度:O(1)
3.7.6 交換類排序-快速排序
快速排序采用的是分治法,其基本思想是將原問題分解成若干個規模更小但結構與其原問題相似的子問題。通過遞歸解決這些子問題,然后再將這些子問題的解 組合成原問題的解。
一般情況下它的排序速度很快,只有當數據基本有序的時候速度是最慢的。
快速排序包括兩個步驟:
以下例子是模擬第一遍分組的過程:
初始數組 57 68 59 52 72 28 96 33 24 19
首先找到基準數 57,此時l=0,h=9
從右側開始, 19小于57,l(0)與h(9)處數據進行交換(數組為 19 68 59 52 72 28 96 33 24 57) 。
從左側開始(l=l+1=1),68大于57停止移動,l(1)與h(9)處數據進行交換(數組為 19 57 59 52 72 28 96 33 24 68)
從右側開始(h=h-1=8),24小于57停止移動,l(1)與h(8)處數據進行交換(數組為19 24 59 52 72 28 96 33 57 68)
從左側開始(l=l+1=2),59大于57停止移動,l(2)與h(8)處數據進行交換(數組為19 24 57 52 72 28 96 33 59 68)
從右側開始(h=h-1=7),33小于57停止移動,l(2)與h(7)處數據進行交換(數組為19 24 33 52 72 28 96 57 59 68)
從左側開始(l=l+1=3),52小于57繼續移動(l=l+1=4),72大于57停止移動,l(4)與h(7)處數據進行交換
(數組為19 24 33 52 57 28 96 72 59 68)
從右側開始(h=h-1=6),96大于57繼續移動(h=h-1=5),28小于57停止移動,l(4)與h(5)處數據進行交換
(數組為19 24 33 52 28 57 96 72 59 68)
從左側開始(l=l+1=5),此時發現l=h=5,第一次排序過程結束,數組為 19 24 33 52 28 57 96 72 59 68,57的左邊部分小于它,右邊部分大于它。接下來講左邊部分【19 24 33 52 28】和右邊部分【96 72 59 68】繼續進行排序,直到最后排序結束為止。
時間復雜度:O(n2)
穩定性:不穩定
空間復雜度:O(nlog?(n))
3.7.7歸并排序
歸并排序(MERGE-SORT)是利用歸并的思想實現的排序方法,該算法采用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然后遞歸求解,而**治(conquer)**的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。
分而治之
可以看到這種結構很像一棵完全二叉樹,本文的歸并排序我們采用遞歸去實現(也可采用迭代的方式去實現)。分階段可以理解為就是遞歸拆分子序列的過程,遞歸深度為log2n。
再來看看治階段,我們需要將兩個已經有序的子序列合并成一個有序序列,比如上圖中的最后一次合并,要將[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合并為最終序列[1,2,3,4,5,6,7,8],來看下實現步驟。
/*** 歸并排序* @param arr 數組* @param left 開始下標* @param right 結束下標* @param temp 臨時數組*/void mergeSort(int[] arr,int left,int right,int[] temp) {if (left<right) {//當還能繼續分解數組時遞歸int mid=(left+right)/2;//向左進行分解-遞歸mergeSort(arr,left,mid,temp);//向右進行分解-遞歸mergeSort(arr,mid+1,right,temp);//每次分解都進行合并merge(arr,left,mid,right,temp);}}/*** 歸并排序的合并過程* @param left 左側下標* @param mid 中間下標* @param right 右側下標* @param temp 臨時數組*/void merge(int[] arr,int left,int mid,int right,int[] temp) {int i=left; //左邊數組的初始索引值int j=mid+1; //右邊數組的初始索引值int t=0; //臨時數組的索引值int tempLeft;//1、通過比較左右兩邊的數組將數值有序放入臨時數組while (i<=mid && j<=right) {if (arr[i]<=arr[j]) {temp[t++]=arr[i++];} else {temp[t++]=arr[j++];}}//2、將左邊數組和右邊數組剩余的元素遷移到while (i<=mid) { temp[t++]=arr[i++]; }while (j<=right) { temp[t++]=arr[j++]; }//3、最后將臨時數組遷移到arr中t=0; tempLeft=left;while (tempLeft<=right) {arr[tempLeft++]=temp[t++];}}3.7.8基數排序
基數排序介紹
- 基數排序屬于"分配式排序",又稱"桶子發",顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配到某些桶中,達到排序的作用
- 基數排序法是屬于穩定性的排序,基數排序法是效率高的穩定性 排序法
- 基數排序的實現方式是:將整數按位數切割成不同的數字,然后按照每個位數分別比較
基數排序的思想
將所有待比較數值統一為同樣的數位長度,數位較短的數前面補零.然后,從最低位開始,依次進行一次排序.這樣從最低位排序一直到最高位排序完成以后,數列就變成一個有序序列.
基數排序圖文說明
將數組{53,3,524,748,14,214} 使用基數排序,進行升序排序
void radixSort(int[] array, int length){int i,j,l,tmp;int maxNumLength=1;//記錄數據的最大位數為幾位,比如25 2位,130 3位int n=1;//代表位數對應的數:1,10,100...int k=0;//保存每一位排序后的結果用于下一位的排序輸入int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的結果,這一位上排序結果相同的數字放在同一個桶里int[] order=new int[10];//用于保存每個桶里有多少個數字int max;//最大值//獲取到待排序數中的最大值的位數max = array[0];for (i = 0; i < length; i++) {if (array[i] > max) { max = array[i]; }}while(max/10>0) {maxNumLength++;max = max/10;}//基數排序n=1;for(l=0;l<maxNumLength;l++) {//裝桶for(i=0;i<length;i++) {tmp=(array[i]/n)%10;bucket[tmp][order[tmp]]=array[i];order[tmp]++;}n*=10;//從桶中取出數據for(i=0;i<10;i++) {if(order[i]>0) {for(j=0;j<order[i];j++) {array[k++]=bucket[i][j];}}order[i]=0;//將桶里計數器置0,用于下一次位排序}k=0;//將k置0,用于下一輪保存位排序結果}}3.7.9 排序總結
3.8 錯誤習題集
題2
給定一個有n個元素的有序線性表。若采用順序存儲結構,則在等概率前提下,刪除其中的一個元素平均需要移動()個元素。
A.(n+1)/2
B.n/2
C.(n-1)/2
D.1
答案 C
題目要求計算進行刪除時平均移動元素個數,如 a、b、c、d、e、f ,若要刪除f,則無需一定任何元素,直接刪除即可;若要刪除e,則需移動一個元素,即把f移至e位置;若要刪除d,則需移動2個元素,把把e移至d位置,f移至e位置;依次類推,要刪除第一個元素,則要移動n-1個元素。
y偶遇每個元素被刪除的概率是相等的,所以平均需要移動的元素個數為 ( 0+(n-1) )/2 = (n-1)/2。
(最小數+最大數)/2為平均數。
題3
下來敘述中,不正確的是()。
A.線性表在鏈式存儲時,查找第i個元素的時間與i的值成正比。
B.線性表在鏈式存儲時,查找第i個元素的時間與i的值有關。
C.線性表在順序存儲時,查找第i個元素的時間與i的值成正比。
D.線性表在順序存儲時,查找第i個元素的時間與i的值無關。
答案 C
順序存儲結構的特點是“順序存儲,隨機存取”,也就是說線性表在順序存儲時,查找第i個元素的時間與i的值無關。
鏈式存儲結構的特點是“隨機存儲,順序存取”,也就是說鏈式存儲結構的數據元素可以隨機地存儲在內存單元,但訪問其任意一個數據元素時,都必須從其頭指針開始逐個進行訪問。
題8
在一顆度為4的樹T中,若有20個度為4的結點,10個度為3的結點,1個度為2的結點,10個度為1的結點,則樹T的葉子節點的個數是()。
A.41
B.82
C.113
D.122
答案B
在樹中,除根節點外,其余所有結點都是由其雙親節點引出的。一個度為n的節點表示由該結點引出n個孩子節點,因此樹T的結點個為20*4+10*3+1*2+10*1+1= 123 ,其中最后的1位根節點,則葉子結點的個數為 123-(20-10-1-10)=82個。
題11
在查找算法中,可用平均查找長度(記為ASL)來衡量一個查找算法的優劣,其定義為:
此處P?為查找表中第i個記錄的概率,C?為查找第i個記錄時同關鍵字比較次數,n為表中記錄數。
以下敘述中均假定每個記錄被查找的概率相等,即 p?=1/n (i=1,2,...,n)。當表中的記錄連續有序存儲在一個一維數組中時,采用順序查找與折半查找方法查找的ASL值分別是()。
A.O(n),O(n)
B.O(n),O(lbn)
C.O(lbn),O(n)
D.O(lbn),O(lbn)
答案 B
順序查找的基本思想是從表的一端開始,順序掃描線性表,依次將掃描到的節點關鍵字和給定值k相比較。若當前掃描到的節點關鍵字與k相等,則查找成功;若掃描結束后,仍未找到關鍵字等于k的結點,則查找失敗。順序查找方法既適用于線性表的順序存儲結構,也適用于線性表的鏈式存儲結構。
成功的順序查找的平均查找長度如下: $$ ASL=np? + (n-1)p? + ... + 2p??? + p? $$ 在等概率情況下,P?=1/n(1≤i≤n),故成功的平均查找長度為(n+...+2+1)= (n+1)/2,即查找成功時的平均比較次數為表長的一半。若k值不在表中,則需要進行n+1次比較之后才能確定查找失敗。查找的時間復雜度的為 O(n)。
若事先知道表中個結點的查找概率不相等,以及它們的分布情況,則應將表中結點查找概率由小到大的順序存放,以便提高順序查找的效率。
順序查找的優點是算法簡單,且對表的結構無任何要求,無論是用向量還是用鏈表來存放節點,也無論節點之間是否按關鍵字有序,它都同樣使用。其缺點是查找效率低,因此當n較大時不宜采用順序查找。
二分查找又稱這版查找,是一種效率較高的查找方法。二分查找要求線性表是有序表,即表中結點按關鍵字有序,并且要求用向量作為表的存儲結構。
二分查找的基本思想是(設R[low,...,high])是當前的查找區間:
- 若R[mid].key > k,則表的有序性可知 R[mid,...,high]均大于k,因此若表中存在關鍵字等于k的節點,則該在R[low,...,mid-1]中。因此,新的查找區間是在左子表R[low,...,high],其中,high=mid-1
- 若R[mid].key < k,則k在R[mid+1,...,high]中,即新的查找區間是在左子表R[low,...,high],其中,low=mid+1
- 若R[mid].key = k,則查找成功,算法結束。
因此,從初始的查找區間R[1,...,n]開始,每經過一次與當前查找區間中點位置上結點關鍵字的比較,就可確定是否成功,不成功則當前的區間就縮小一半。重復這一過程,知道找到關鍵字為k的節點,或直至當前的查找區間為空時為止。查找的時間復雜度為:O(log?n).
因此,答案是 B
題12★
根據使用頻率,為5個字符設計哈夫曼編碼不可能是()。
A.111,110,10,01,00
B.000,001,010,011,1
C.001,000,10,01,11
D.110,100,101,11,1
答案 D
哈夫曼編碼屬于前綴編碼,根據前綴編碼的定義,任一字符的編碼都不是另一字符編碼的前綴。而在選項D中,1是前面4個字符的前綴,明顯違反了這一原則,所以不屬于哈夫曼編碼。
題13
二叉樹在線索化后,仍不能有效解決的問題是()。
A.先序線索二叉樹中求先序后繼
B.中序線索二叉樹中求中序后繼
C.中序線索二叉樹中求中序前驅
D.后序線索二叉樹中求后序后繼
答案 D
在中序線索二叉樹中,查找結點P的中序后繼分為以下兩種情況。
在中序線索二叉樹中,查找結點p的前驅結點也有兩種情況
因此,在中序線索二叉樹中,查找中序前驅和中序后繼都可以有效的解決。
在先序線索二叉樹中,查找結點先序后繼很簡單,僅從P出發就可以找到,但是找其先序前驅必須要知道 P 的雙親結點。
在后序線索二叉樹中,查找結點后序前驅很簡單,僅從P出發就可以找到,但是找其后序后繼必須要知道 P 的雙親結點。
題14
由元素序列(27,16,75,38,51)構造平衡二叉樹,則首次出現的最小不平衡子樹的根(即離插入結點最近且平衡因子的絕對值為2的結點)為()。
A.27
B.38
C.51
D.75
答案 D
平衡二叉樹的構造過程如圖:
根據題目要求,首次出現最小不平衡子樹的根是 75.
題15
若 G 是一個具有36條邊的非連通無向圖(不含自回路和多重邊),則圖G至少有()個頂點。
A.11
B.10
C.9
D.8
答案 B
因G為非連通圖,所以G中至少含有兩個連通子圖,而且該圖不含有回路和多重邊。題目問的是至少有多少個頂點,因此一個連通圖可以看成是只有1個頂點,另一個連通圖可以看成是一個完全圖(因為完全圖在最小頂點的情況下能得到的邊數最多),這樣該題就轉化為“36條邊的完全圖有多少個頂點”,因為具有n個頂點的無向完全圖的邊條數為 n*(n-1)/2,可以算出 n=9 滿足條件。在加上一個連通圖(只有一個頂點),則圖G至少有10個頂點。
題16
有向圖 的所有拓撲排序序列有 () 個。
A.2
B.4
C.6
D.7
答案 A
拓撲排序是將AOV網中所有頂點排成一個線性序列的過程,并且該列滿足:若在AOV網中從頂點v? 到v?有一條路徑,則在該線性序列中,頂點v?必在頂點v?之前。
對AOV網進行拓撲排序的方法如下:
本題中 A必須是第一個元素,E必須是最后一個元素,D必須是倒數第二個元素,即序列 A**DE,其中*為B或C,所以共兩種拓撲排序序列。
2中情況的拓撲排序過程如下:
題19
用插入排序和歸并排序算法對數組<3,1,4,1,5,9,6,5>進行從小到大排序,則分別需要進行()次數組元素之間的比較。
A.12,14
B.10,14
C.12,16
D.10,16
答案 A
插入排序是逐個將待排序元素插入到已排序的有序表中。用插入排序算法對數組<3,1,4,1,5,9,6,5>進行排序的過程:
- 原元素序列:監視哨(3),1,4,1,5,9,6,5
- 第一趟排序:3 (1,3),4,1,5,9,6,5 3插入時與1比較1次
- 第二趟排序:4 (1,3,4),1,5,9,6,5 4插入時與3比較1次
- 第三趟排序:1 (1,1,3,4),5,9,6,5 1插入時比較3次
- 第四趟排序:5 (1,1,3,4,5),9,6,5 5插入時與4比較1次
- 第五趟排序:9 (1,1,3,4,5,9),6,5 9插入時與5比較1次
- 第六趟排序:6 (1,1,3,4,5,6,9),5 6插入時比較2次
- 第七趟排序:5 (1,1,3,4,5,5,6,9) 5插入時比較3次
整個排序的比較次數 1+1+3+1+1+2+3 = 12
歸并排序的思想是將兩個相鄰的有序子序列歸并為一個序列,然后再將新產生的相鄰序列進行歸并,當只剩下一個有序序列時算法結束。那么用歸并排序算法對數組<3,1,4,1,5,9,6,5>進行排序的過程:
- 原元素序列:3,1,4,1,5,9,6,5
- 第一趟排序:[1,3] [1,4] [5,9] [5,6] 比較4次
- 第二趟排序:[1,1,3,4] [5,5,6,9] 前半部分比較3次,后半部分比較3次
- 第三趟排序:[1,1,3,4,5,5,6,9] 5分別與 1、2、3、4比較一次
整個排序過程需要比較的次數為 4+3+3+4 = 14
題20
遞歸算法的執行過程,一般來說,可先后分成()兩個階段。
A.試探和回歸
B.遞推和回歸
C.試探和返回
D.遞推和返回
答案 B
遞歸算法的執行過程分為遞推和回歸兩個階段。在遞推階段,把較復雜的問題(規模為n)的求解推到比原問題簡單一些的問題(規模小于n)的求解。
在回歸階段,當獲得最簡單的情況后,逐級返回,依次得到稍復雜問題的解。
下面列舉一個經典的遞歸算法的例子——菲波那切數列問題來說明這一過程。
菲波那切數列為:0,1,1,2,3,...,即
fib(0)=0;
fib(1)=1;
fib(n)=fib(n-1) + fib(n-2) (當n>1時)
寫成遞歸函數有:
int fib(int n) {if(n==0) return 0;if(n==1) return 1;if(n>1) return fib(n-1) + fib(n-2); }這個例子的遞推過程為:求解 fib(n) ,把它分解到 fib(n-1) + fib(n-2) 。也就是說,為計算 f(n),必須先計算 fib(n-1) 和 fib(n-2) ,而計算 fib(n-1) 和 fib(n-2) 又必須先計算 fib(n-3) 和 fib(n-4)。依次類推,直至計算 fib(1) 和 fib(0),分別能立即得到結果 1 和 0。在遞推階段,必須要有終止遞歸的條件。例如在 fib(n) 中,當 n 為 1和0的情況。
回歸過程:得到 fib(1) 和 fib(0) 后,返回得到 fib(2) 的結果......在得到了 fib(n-1) 和 fib(n-2) 的結果后,返回得到 fib(n) 的結果。
題23
若循環隊列以數組 Q[0,...,m-1]作為其存儲結構,變量rear表示循環隊列中隊尾元素的實際位置,其移動按 read = (rear+1) mod m 進行,變量 length 表示當前循環隊列中元素的個數,則循環隊列的隊首元素的實際位置是()。
A. rear - length
B.(rear - length + m) mod m
C.(1 + rear + m - length) mod m
D.m - length
答案 C
其實這種題目在考場上最好的解題方法是找一個實際的例子,往里面一套便知道了。下面理解以下原理。因為 rear 表示的是隊尾元素的實際位置(注意,不是隊尾指針)。而且題中有“移動按 read = (rear+1) mod m 進行”,這說明:隊列存放元素的順序為 Q[1],Q[2],...,Q[0]。所以在理想情況下 rear - length +1 能算出隊首元素的位置,即當 m=8, rear=5, length=2 時, rear - length +1 = 4,4就是正確的隊首元素實際位置。但 rear - length +1 有一種情況無法處理,即當 m=8, read=1, length=5 時無法計算出。
所以在 rear + 1 -length 的基礎上加上 m 再與 m 求模,以此方法來計算。
題25
若廣義表 L=( (a,b,c),e ),則L的長度和深度分別為()。
A.2和1
B.2和2
C.4和2
D.4和1
答案 B
廣義表記作 LS=(a1,a2,...,an)其中LS是廣義表名,n是它的長度,所以本表的長度為2.而廣義表中嵌套括號的層數為其深度,所以L的深度為2.
題27
已知一個線性表(38,25,74,63,52,48),假定采用散列函數 h(key) = key % 7 計算散列地址,并散列存儲在散列表 A[0,...,6] 中,若采用線性探測方法來解決沖突,則在該散列表上進行等概率成功查找的平均查找長度為()。
A.1.5
B.1.7
C.2.0
D.2.3
答案 C
要計算散列表上的平均查找長度,首先必須知道再建立散列表時,每個數據存儲時進行了幾次散列。這樣就知道哪一個元素的查找長度是多少。散列表的填表過程如下:
首先存入第一個元素 38,由于 h(38) = 38 % 7 = 3,又因為 3號單元現在沒有數據,所以把38存入3號單元
| 38 |
接著存入25,由于 h(25) = 25 % 7 = 4,又因為 4號單元現在沒有數據,所以把25存入4號單元
| 38 | 25 |
接著存入 74,由于 h(74) = 74 % 7 = 4,此時4號單元已被25占據,所以進行線性再散列,線性再散列公式為 H? = (h(key)+d? ) % m,其中 d? 為 1,2,3,4,...,所以 H? = (4 + 1) % 7 = 5,此時 單元5沒有數據,所以把74存入到5號單元
| 38 | 25 | 74 |
接著存入 63,由于 h(63) = 63 % 7 = 0,又因為 0號單元現在沒有數據,所以把63存入0號單元
| 63 | 38 | 25 | 74 |
接著存入 52,由于 h(52) = 52 % 7 = 3,此時3號單元已被38占據,所以進行線性散列 H? = (3+1)%7 = 4,但4號單元也被占據了,所以再次散列 H? = (3+2)%7 = 5,但5號單元也被占據了,所以再次散列 H? = (3+3)%7 = 6,6號單元為空,所以把52存入6號單元
| 63 | 38 | 25 | 74 | 52 |
接著存入 48,由于 h(48) = 48 % 7 = 6,此時6號單元已被占據,所以進行線性再散列 H? = (6+1)%7 = 0,但0號單元也被占據了,所以再次散列 H? = (6+2)%7 = 1,1號單元為空,所以把48存入1號單元
| 63 | 48 | 38 | 25 | 74 | 52 |
如果一個元素進行了N次散列,相應的查找次數也是N,所以 38,25,63 這三個元素查找長度為1,74查找長度為2,48查找長度為3,52查找長度為4,平均查找長度為 (1+1+1+2+3+4)/6 = 2。
題28
設某算法的計算時間可用遞推關系式 T(n) = 2T(n/2) + n 表示,則該算法的時間復雜度為()。
A. O(lgn)
B. O(nlgn)
C. O(n)
D. O(n2)
答案 B
遞推關系式 T(n) = 2T(n/2) + n 其實是在給n個元素進行快速排序時最好情況(每次分割都恰好將記錄分為兩個長度相等的子序列)下的時間遞推關系式,其中T(n/2) 是一個子表需要的處理時間,n為當次分割需要的時間。注意,這里實際上是用比較次數來度量時間。可以對此表達式進行變形得
? T(n)/n - (2/n)*T(n/2) = T(n)/n - T(n/2)/(n/2) = 1
用 n/2 代替上式中的n可得
? T(n/2)/(n/2) - T(n/4)/(n/4) = 1
繼續用 n/2 代替上式中的n可得
? T(n/4)/(n/4) - T(n/8)/(n/8) = 1
? ...
? T(2)/2 - T(1)/1 = 1
算法共需要進行 log?n 次分割,將上述 log?n 個式子相加,刪除互相抵消的部分,得
? T(n)/n - T(1)/1 = log?n,而T(1) = 1
那么上式可轉化為
? T(n)/n = log?n + 1 => T(n) = nlog?n + n
而在求時間復雜度時關注“大頭”,那么
? T(n) = O(n log?n) = O(n lgn)
題29
()算法策略與遞歸技術的聯系最弱。
A.分治
B.動態規劃
C.貪心
D.回溯
答案 C
- 分治法:對于一個規模為n的問題,若該問題可以容易地解決(如說規模n較小)則直接解決;否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解這些子問題,然后將各個子問題的解藕餅到原問題的解。
- 動態規劃法:這種算法也用到了分治思想,它的做法是將問題實例分解為更小、相似的子問題,并存儲子問題的解而避免計算重復的子問題。
- 貪心算法:它是一種不追求最優解,只希望得到較為滿意解的方法。貪心算法一般可以快速得到滿意的解,因為它省去了為找到最優解而窮盡所有可能所必須耗費的大量時間。貪心算法常以當前情況為基礎做最優選擇,而不考慮各種可能的整體情況,所以貪心法不要回溯。
- 回溯算法(試探法):它是一種系統地搜索問題的解的方法。回溯算法的基本思想是:從一條路往前走,能進則進,不能進則退回來,換一條路再試。其實現一般要用到遞歸和堆棧。
以上算法中的分治法和動態規劃法通常要用到回溯算法,而回溯算法又一般要用到遞歸,所以只有貪心算法與遞歸技術聯系最弱。
題30★
對于具有 n 個元素的一個數據序列,若只需要得到其中第k個元素之前的部分排序,最好采用()。
A.直接插入排序
B.希爾排序
C.快速排序
D.堆排序
答案 D
此題考察的是場景的內部排序算法。
- 直接插入排序的基本思想:每步將一個待排序的記錄按其排序碼值的大小,插入到前面已經拍好的文件中的適當位置,直到全部插入為止。
- 希爾排序的基本思想:先取一個小n的整數d1作為第一個增量,把文件的全部記錄分成 d1 個組,所有距離為 d1 的倍數記錄放在同一個組中。先在各組內進行直接插入排序;然后取第二個增量d2 < d1,重復上述的分組和排序,直至所有的增量 dt=1(dt < dt-1 < O < d2 < d1),即所有記錄放在同一組中進行直接插入排序為止。該方法實質上是一種分組插入方法。
- 直接選擇排序:首先在所有記錄中選出排序碼最小的記錄,把它與第一個記錄交換,然后在其余的記錄內選出排序碼最小的記錄,與第2個記錄交換......依次類推,直到所有記錄排完為止。
- 堆排序:堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。它通過建立初始堆和不斷地重建堆,逐個將排序關鍵字按順序輸出,從而達到排序的目的。(從小到大排序:大頂推,每次取出根元素放在數組最后)
- 冒泡排序:被排序的記錄數組R[1,...,n]垂直排列,每個記錄R[i]看做是重量為 ki 的氣泡。根據輕氣泡不能在重氣泡之下的原則,從下往上掃碼數組 R ,凡掃描到違反本原則的輕氣泡,就使其向上“漂浮”。如此反復進行,知道最后任何兩個氣泡都是在輕者在上,重者在下為止。
- 快速排序:采用了一種分治的策略,將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞歸地解這些子問題,然后將這些子問題的解組合為原問題的解。
- 歸并排序:將兩個或兩個以上的有序子表合并程一個新的有序表,初始時,把含有n個結點的待排序序列看作由n個長度都為1的有序子表所組成,將他們依次兩兩歸并得到長度為2的若干有序子表,再對他們兩兩合并,知道得到長度為n的有序表為止,排序結束。
- 基數排序:從低位到高位依次對待排序的關鍵碼進行分配和收集,經過d趟分配和收集,就可以得到一個有序序列。
了解這些算法思想后,解題就容易了。現在看題目具體要求,題目中“若只需得到其中第k個元素之前的部分排序”有歧義。例如,現在待排序列(15,8,9,2,23,69,5)。現在要求得到其中第三個元素之前的部分排序。
第一種理解:得到 (15,8,9)的排序
第二種理解:得到排序之后的序列(2,5,8,9,15,23,69)的(2,5,8,9);得到排序后第三個元素之前的部分排序,即(2,5,8)。
但綜合題義,第一種理解可以排除。對于第二種理解,只有堆排序合適,因為希爾排序、直接插入排序和快速排序都不能實現部分排序。若要達到題目要求,只能把所有元素排序完成,在從結果集中把需要的數據列截取出來,這樣效率遠遠不及堆排序。所以本體選擇D.
總結
以上是生活随笔為你收集整理的软件设计师-3.数据结构与算法基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式架构之思考
- 下一篇: 【多媒体基础知识】 --- 什么是流媒体