筆記首發(fā)于:lengyueling.cn
PDF版本附在?lengyueling.cn?對應(yīng)文章結(jié)尾,歡迎下載訪問交流
緒論
數(shù)據(jù)結(jié)構(gòu)在學(xué)什么
-
如何用程序代碼把現(xiàn)實世界的問題信息化
-
如何用計算機高效地處理這些信息從而創(chuàng)造價值
數(shù)據(jù)結(jié)構(gòu)的基本概念
什么是數(shù)據(jù):
數(shù)據(jù)是信息的載體,是描述客觀事物屬性的數(shù)、字符及所有能輸入到計算機中并被計算機程序識別和處理的符號的集合。數(shù)據(jù)是計算機程序加工的原料。
現(xiàn)代計算機處理的數(shù)據(jù):
-
現(xiàn)代計算機——經(jīng)常處理非數(shù)值型問題
-
對于非數(shù)值型的問題:
-
我們關(guān)心每個個體的具體信息
-
我們還關(guān)心個體之間的關(guān)系
-
數(shù)據(jù)元素:
數(shù)據(jù)元素是數(shù)據(jù)的基本單位,通常作為一個整體進(jìn)行考慮和處理。
數(shù)據(jù)項:
一個數(shù)據(jù)元素可由若干數(shù)據(jù)項組成,數(shù)據(jù)項是構(gòu)成數(shù)據(jù)元素的不可分割的最小單位。
數(shù)據(jù)對象:
數(shù)據(jù)對象是具有相同性質(zhì)的數(shù)據(jù)元素的集合,是數(shù)據(jù)的一個子集
數(shù)據(jù)結(jié)構(gòu):
-
數(shù)據(jù)結(jié)構(gòu)是相互之間存在一種或多種特定關(guān)系的數(shù)據(jù)元素的集合。
-
數(shù)據(jù)結(jié)構(gòu)這門課著重關(guān)注的是數(shù)據(jù)元素之間的關(guān)系,和對這些數(shù)據(jù)元素的操作,而不關(guān)心具體的數(shù)據(jù)項內(nèi)容。
數(shù)據(jù)結(jié)構(gòu)的三要素
三要素:
-
邏輯結(jié)構(gòu)
-
集合結(jié)構(gòu),各個元素同屬一個集合,別無其他關(guān)系
-
線性結(jié)構(gòu),一對一,順序關(guān)系
-
樹狀結(jié)構(gòu),一對多
-
圖狀結(jié)構(gòu),多對多
-
-
數(shù)據(jù)的運算,針對某種邏輯結(jié)構(gòu),結(jié)合實際需求,定義基本運算(增刪改查)
-
物理結(jié)構(gòu)(儲存結(jié)構(gòu)),如何用計算機實現(xiàn)這種數(shù)據(jù)結(jié)構(gòu)
-
順序存儲:把邏輯上相鄰的元素存儲在物理位置上也相鄰的儲存單元中,元素之間的關(guān)系由儲存單元的鄰接關(guān)系來體現(xiàn)
-
鏈?zhǔn)酱鎯Γ喊堰壿嬌舷噜彽脑卮鎯υ谖锢砦恢蒙峡梢圆幌噜彛柚甘驹卮鎯Φ刂返闹羔榿肀硎驹刂g的邏輯關(guān)系。
-
索引存儲:在儲存元素信息的同時,還簡歷附加的索引表。索引表中的每項成為索引項,索引項的一般形式是(關(guān)鍵字,地址)
-
散列存儲:根據(jù)元素的關(guān)鍵字直接計算出該元素的存儲地址,又稱哈希存儲
-
總結(jié):
-
若采用順序存儲,則各個數(shù)據(jù)元素在物理上必須是連續(xù)的;若采用非順序存儲則各個數(shù)據(jù)元素在物理上可以是離散的
-
數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)會影響存儲空間分配的方便程度
-
數(shù)據(jù)的存儲結(jié)構(gòu)會影響對數(shù)據(jù)運算的速度
-
運算的定義是針對邏輯結(jié)構(gòu)的,指出運算的功能
-
運算的實現(xiàn)是針對存儲結(jié)構(gòu)的,指出運算的具體步驟
數(shù)據(jù)類型:
數(shù)據(jù)類型是一個值的集合和定義在此集合上的一組操作的總稱
-
原子類型:其值不可再分的數(shù)據(jù)類型(bool、int等)
-
結(jié)構(gòu)類型:其值可以再分解為若干分量的數(shù)據(jù)類型(struct等)
抽象數(shù)據(jù)類型(ADT):
是抽象數(shù)據(jù)組織及其相關(guān)的操作,定義了一個ADT就是在定義一種數(shù)據(jù)結(jié)構(gòu)
算法的基本概念
什么是算法:
-
程序=數(shù)據(jù)結(jié)構(gòu)+算法
-
算法(Algorithm)是對特定問題求解步驟的一種描述,它是指令的有限序列,其中的每條指令表示一個或多個操作
算法的特征:
-
算法必須擁有以下特性,否則不能被稱為算法:
-
有窮性:一個算法必須總在執(zhí)行有窮步之后結(jié)束,且每一步都可在有窮時間內(nèi)完成。
-
確定性:算法中每條指令必須有確切的含義,對于相同的輸入只能得出相同的輸出。
-
可行性:算法中描述的操作都可以通過已經(jīng)實現(xiàn)的基本運算執(zhí)行有限次來實現(xiàn)。
-
輸入:一個算法有零個或多個輸入,這些輸入取自于某個特定的對象的集合。
-
輸出:一個算法有一個或多個輸出,這些輸出是與輸入有著某種特定關(guān)系的量。
好算法的特質(zhì):
-
正確性:算法應(yīng)能夠正確地解決求解問題。
-
可讀性:算法應(yīng)具有良好的可讀性,以幫助人們理解。
-
健壯性:輸入非法數(shù)據(jù)時,算法能適當(dāng)?shù)刈龀龇磻?yīng)或進(jìn)行處理,而不會產(chǎn)生莫名其妙的輸出結(jié)果。
-
高效率:
-
花的時間少:時間復(fù)雜度低
-
低存儲量需求:費內(nèi)存,空間復(fù)雜度低
算法的時間復(fù)雜度
定義: 事前預(yù)估算法時間開銷T(n)與問題規(guī)模n的關(guān)系(T表示“time”)
規(guī)則:
-
加法規(guī)則:T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n), g(n))) (多項相加,只保留最高階的項,且系數(shù)變?yōu)?)
-
乘法規(guī)則:T(n) = T1(n)×T2(n) = O(f(n))×O(g(n)) = O(f(n)×g(n))(多項相乘,都保留 )
-
算法好壞:O(1) < O(log2n) < O(n) < O(nlog2n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)(口訣:常對冪指階)
-
數(shù)量級僅需考慮循環(huán)內(nèi),最深層嵌套的部分
-
最壞時間復(fù)雜度:最壞情況下算法的時間復(fù)雜度
-
平均時間復(fù)雜度:所有輸入示例等概率出現(xiàn)的情況下,算法的期望運行時間
-
最好時間復(fù)雜度:最好情況下算法的時間復(fù)雜度
-
一般只考慮最壞和平均復(fù)雜度
算法的空間復(fù)雜度
定義:空間開銷(內(nèi)存開銷,S(n))與問題規(guī)模n之間的關(guān)系
算法原地工作: 算法所需內(nèi)存空間為常量
規(guī)則:
-
只需關(guān)注存儲空間大小與問題規(guī)模相關(guān)的變量
-
加法規(guī)則、乘法規(guī)則、算法好壞判定與時間復(fù)雜度一樣
-
遞歸調(diào)用的大多數(shù)情況:空間復(fù)雜度=遞歸調(diào)用的深度
線性表
線性表的定義和基本操作
定義:
-
線性表(Linear List)是具有相同數(shù)據(jù)類型的n(n≥0)個數(shù)據(jù)元素的有限序列,其中n為表長,當(dāng)n = 0時線性表是一個空表
-
若用L命名線性表,則其一般表示為L = (a1, a2, … , ai, ai+1, … , an)
-
a1是表頭元素
-
an是表尾元素
-
除第一個元素外,每個元素有且僅有一個直接前驅(qū)
-
除最后一個元素外,每個元素有且僅有一個直接后繼
基本操作:
-
InitList(&L):初始化表。構(gòu)造一個空的線性表L,分配內(nèi)存空間。
-
DestroyList(&L):銷毀操作。銷毀線性表,并釋放線性表L所占用的內(nèi)存空間。
-
ListInsert(&L,i,e):插入操作。在表L中的第i個位置上插入指定元素e。
-
ListDelete(&L,i,&e):刪除操作。刪除表L中第i個位置的元素,并用e返回刪除元素的值。
-
LocateElem(L,e):按值查找操作。在表L中查找具有給定關(guān)鍵字值的元素。
-
GetElem(L,i):按位查找操作。獲取表L中第i個位置的元素的值。
-
Length(L):求表長。返回線性表L的長度,即L中數(shù)據(jù)元素的個數(shù)。
-
PrintList(L):輸出操作。按前后順序輸出線性表L的所有元素值。
-
Empty(L):判空操作。若L為空表,則返回true,否則返回false。
-
什么時候要傳入?yún)?shù)的引用“&”: 對參數(shù)的修改結(jié)果需要“帶回來”,是引用類型而不是值類型
順序表的定義
定義:
用順序存儲的方式實現(xiàn)線性表順序存儲,把邏輯上相鄰的元素存儲在物理位置上也相鄰的存儲單元中,元素之間的關(guān)系由存儲單元的鄰接關(guān)系來體現(xiàn)。
如何知道一個數(shù)據(jù)元素大?。?/strong>
C語言sizeof(ElemType)
順序表的實現(xiàn)-靜態(tài)分配:
如果“數(shù)組”存滿了怎么辦:
可以放棄治療,順序表的表長剛開始確定后就無法更改(存儲空間是靜態(tài)的),同時如果提前初始化太多的空間而不用,又會造成資源的浪費,因此動態(tài)分配應(yīng)運而生。
動態(tài)申請和釋放內(nèi)存空間:
-
C:malloc、free函數(shù)
-
L.data = (ElemType *) malloc (sizeof(ElemType) * InitSize);
-
malloc函數(shù)返回一個指針, 空間需要強制轉(zhuǎn)型為你定義的數(shù)據(jù)元素類型指針
-
malloc函數(shù)的參數(shù),指明要分配多大的連續(xù)內(nèi)存空間
-
-
C++: new、delete關(guān)鍵字
順序表的實現(xiàn)-動態(tài)分配:
順序表的實現(xiàn):
-
隨機訪問,即可以在O(1)時間內(nèi)找到第i個元素。
-
存儲密度高,每個結(jié)點只存儲數(shù)據(jù)元素
-
拓展容量不方便(即便采用動態(tài)分配的方式實現(xiàn),拓展長度的時間復(fù)雜度也比較高)
-
插入、刪除操作不方便,需要移動大量元素
順序表的插入、刪除
順序表的基本操作-插入:
增加i的合法性判斷:
順序表的基本操作-刪除:
插入和刪除的時間復(fù)雜度:
-
最好時間復(fù)雜度= O(1)
-
最壞時間復(fù)雜度= O(n)
-
平均時間復(fù)雜度= O(n)
順序表的查找
順序表的按位查找:
-
正是如此,在初始化順序表時候malloc需要強制轉(zhuǎn)換為與數(shù)據(jù)元素的數(shù)據(jù)類型相對應(yīng)的指針
-
時間復(fù)雜度= O(1)
-
隨機存取:由于順序表的各個數(shù)據(jù)元素在內(nèi)存中連續(xù)存放,因此可以根據(jù)起始地址和數(shù)據(jù)元素大小立即找到第i個元素,
順序表的按值查找:
-
結(jié)構(gòu)類型的數(shù)據(jù)元素也能用 == 比較嗎:不能!(C++可以用 == 的重載來實現(xiàn))
-
更好的辦法:定義一個函數(shù)
-
依次對比各個分量來判斷兩個結(jié)構(gòu)體是否相等
-
最好時間復(fù)雜度= O(1)
-
最壞時間復(fù)雜度= O(n)
-
平均時間復(fù)雜度= O(n)
單鏈表的定義
順序表:
-
每個結(jié)點中只存放數(shù)據(jù)元素
-
優(yōu)點:可隨機存取,存儲密度高
-
缺點:要求大片連續(xù)空間,改變?nèi)萘坎环奖?/p>
單鏈表:
-
每個結(jié)點除了存放數(shù)據(jù)元素外,還要存儲指向下一個結(jié)點的指針
-
優(yōu)點:不要求大片連續(xù)空間,改變?nèi)萘糠奖?/p>
-
缺點:不可隨機存取,要耗費一定空間存放指針
定義一個單鏈表:
-
typedef關(guān)鍵字:數(shù)據(jù)類型重命名
-
typedef <數(shù)據(jù)類型> <別名>
-
typedef struct LNode LNode;
-
之后可以用LNode代替struct LNode
-
不帶頭結(jié)點的單鏈表:
帶頭結(jié)點的單鏈表:
區(qū)別:
-
不帶頭結(jié)點,寫代碼更麻煩
-
對第一個數(shù)據(jù)結(jié)點和后續(xù)數(shù)據(jù)結(jié)點的處理需要用不同的代碼邏輯
-
對空表和非空表的處理需要用不同的代碼邏輯
-
我們一般使用的都是帶頭結(jié)點的單鏈表
單鏈表的插入、刪除
按位序插入(帶頭結(jié)點):
ListInsert(&L,i,e):
-
插入操作,在表L中的第i個位置上插入指定元素e
-
找到第i-1個結(jié)點,將新結(jié)點插入其后
-
若帶有頭結(jié)點,插入更加方便,頭結(jié)點可以看作“第0個”結(jié)點,直接做上面的操作即可
-
若i插在表中則與插在表頭一樣進(jìn)行操作,可以插入成功
-
若i插在表尾則s->next為NULL(在表的定義時規(guī)定的),可以插入成功
-
若i插在表外(i>Lengh)則p指針指向NULL(While循環(huán)一直指向p->next),不能插入成功
-
最好時間復(fù)雜度= O(1)
-
最壞時間復(fù)雜度= O(n)
-
平均時間復(fù)雜度= O(n)
按位序插入(不帶頭結(jié)點):
ListInsert(&L,i,e):
-
插入操作,在表L中的第i個位置上插入指定元素e
-
不存在“第0個”結(jié)點,因此i=1時需要特殊處理
-
找到第i-1個結(jié)點,將新結(jié)點插入其后
-
若i!=1則處理方法和帶頭結(jié)點一模一樣
-
值得注意的是int j =1而非帶頭結(jié)點的0(帶頭結(jié)點的頭結(jié)點為第0個結(jié)點)
-
結(jié)論:不帶頭結(jié)點寫代碼更不方便,推薦用帶頭結(jié)點
指定結(jié)點的后插操作:
-
這一段代碼是按位序插入中插入的那一部分代碼
-
也可以直接調(diào)用InsertNextNode來執(zhí)行
-
封裝代碼,以此提高代碼復(fù)用性,讓代碼更容易維護(hù)
指定結(jié)點的前插操作:
-
因為僅知道指定結(jié)點的信息和后繼結(jié)點的指向,因此無法直接獲取到前驅(qū)結(jié)點
-
方法1:獲取頭結(jié)點然后再一步步找到指定結(jié)點的前驅(qū)
-
方法2:將新結(jié)點先連上指定結(jié)點p的后繼,接著指定結(jié)點p連上新結(jié)點s,將p中元素復(fù)制到s中,將p中元素覆蓋為要插入的元素e
(方法1)
(方法2)
按位序刪除(帶頭結(jié)點):
ListDelete(&L,i,&e):
-
刪除操作,刪除表L中第i個位置的元素,并用e返回刪除元素的值。
-
找到第i-1個結(jié)點,將其指針指向第i+1個結(jié)點,并釋放第i個結(jié)點
指定結(jié)點的刪除:
-
刪除結(jié)點p,需要修改其前驅(qū)結(jié)點的next指針,和指定結(jié)點的前插操作一樣
-
方法1:傳入頭指針,循環(huán)尋找p的前驅(qū)結(jié)點
-
方法2:偷天換日,類似于結(jié)點前插的實現(xiàn)
(方法2)
如果要刪除的結(jié)點p是最后一個結(jié)點:
-
只能從表頭開始依次尋找p的前驅(qū),時間復(fù)雜度O(n)
-
這就體現(xiàn)了單鏈表的局限性:無法逆向檢索,有時候不太方便
單鏈表的查找
按位查找:
-
GetElem(L,i):按位查找操作。獲取表L中第i個位置的元素的值。
-
實際上單鏈表的插入中找到i-1部分就是按位查找i-1個結(jié)點,如下圖
-
因此查找第i個結(jié)點如下圖
-
如果i=0則直接不滿足j<i則指針p直接返回頭結(jié)點L
-
如果i超界則當(dāng)時p指向了NULL,指針p返回NULL
-
平均時間復(fù)雜度:O(n)
按值查找:
-
LocateElem(L,e):按值查找操作。在表L中查找具有給定關(guān)鍵字值的元素。
-
能找到的情況:p指向了e值對應(yīng)的元素,返回該元素
-
不能找到的情況:p指向了NULL,指針p返回NULL
-
平均時間復(fù)雜度:O(n)
求表的長度:
平均時間復(fù)雜度:O(n)
單鏈表的建立
尾插法:
-
每次插入元素都插入到單鏈表的表尾
-
方法1:套用之前學(xué)過的位序插入,每次都要從頭開始往后面遍歷,時間復(fù)雜度為O(n^2)
-
方法2:增加一個尾指針r,每次插入都讓r指向新的表尾結(jié)點,時間復(fù)雜度為O(n)
頭插法:
-
每次插入元素都插入到單鏈表的表頭
-
頭插法和之前學(xué)過的單鏈表后插操作是一樣的,可以直接套用
-
L->next=NULL;可以防止野指針
總結(jié):
-
頭插法、尾插法:核心就是初始化操作、指定結(jié)點的后插操作
-
注意設(shè)置一個指向表尾結(jié)點的指針
-
頭插法的重要應(yīng)用:鏈表的逆置
雙鏈表
為什么要要使用雙鏈表:
-
單鏈表:無法逆向檢索,有時候不太方便
-
雙鏈表:可進(jìn)可退,但是存儲密度更低一丟丟
雙鏈表的初始化(帶頭結(jié)點):
雙鏈表的插入:
-
小心如果p結(jié)點為最后一個結(jié)點產(chǎn)生的空指針問題,因此循環(huán)鏈表應(yīng)運而生(詳見后面的循環(huán)鏈表插入刪除)
-
注意指針的修改順序
雙鏈表的刪除:
雙鏈表的遍歷:
循環(huán)鏈表
循環(huán)單鏈表與單鏈表的區(qū)別:
單鏈表:
-
表尾結(jié)點的next指針指向NULL
-
從一個結(jié)點出發(fā)只能找到后續(xù)的各個結(jié)點
循環(huán)單鏈表:
-
表尾結(jié)點的next指針指向頭結(jié)點
-
從一個結(jié)點出發(fā)可以找到其他任何一個結(jié)點
循環(huán)單鏈表初始化:
-
從頭結(jié)點找到尾部,時間復(fù)雜度為O(n)
-
如果需要頻繁的訪問表頭、表尾,可以讓L指向表尾元素(插入、刪除時可能需要修改L)
-
從尾部找到頭部,時間復(fù)雜度為O(1)
循環(huán)雙鏈表與雙鏈表的區(qū)別:
雙鏈表:
-
表頭結(jié)點的prior指向NULL
-
表尾結(jié)點的next指向NULL
循環(huán)雙鏈表:
-
表頭結(jié)點的prior指向表尾結(jié)點
-
表尾結(jié)點的next指向頭結(jié)點
循環(huán)雙鏈表的初始化:
循環(huán)鏈表的插入:
-
對于雙鏈表來說如果p結(jié)點為最后一個結(jié)點,因為next結(jié)點為null,p->next->prior=s會產(chǎn)生的空指針問題
-
循環(huán)鏈表規(guī)避因為最后結(jié)點的next結(jié)點為頭結(jié)點因此不會發(fā)生問題
循環(huán)鏈表的刪除:
與循環(huán)鏈表的插入相同。
注意點:
寫代碼時候注意以下幾點,以此規(guī)避錯誤:
-
如何判空
-
如何判斷結(jié)點p是否是表尾/表頭元素(后向/前向遍歷的實現(xiàn)核心)
-
如何在表頭、表中、表尾插入/刪除一個結(jié)點
靜態(tài)鏈表
什么是靜態(tài)鏈表:
-
分配一整片連續(xù)的內(nèi)存空間,各個結(jié)點集中安置
-
每個結(jié)點由兩部分組成:data(數(shù)據(jù)元素)和next(游標(biāo))
-
0號結(jié)點充當(dāng)“頭結(jié)點”,不具體存放數(shù)據(jù)
-
游標(biāo)為-1表示已經(jīng)到達(dá)表尾
-
游標(biāo)充當(dāng)“指針”,表示下個結(jié)點的存放位置,下面舉一個例子:
-
每個數(shù)據(jù)元素4B,每個游標(biāo)4B(每個結(jié)點共8B),設(shè)起始地址為addr,e1的存放地址為addr + 8*2(游標(biāo)值)
定義靜態(tài)鏈表:
方法1:
方法2:
基本操作:
初始化:
-
把a[0]的next設(shè)為-1
-
把其他結(jié)點的next設(shè)為一個特殊值用來表示結(jié)點空閑,如-2
插入位序為i的結(jié)點:
-
找到一個空的結(jié)點,存入數(shù)據(jù)元素(設(shè)為一個特殊值用來表示結(jié)點空閑,如-2)
-
從頭結(jié)點出發(fā)找到位序為i-1的結(jié)點
-
修改新結(jié)點的next
-
修改i-1號結(jié)點的next
刪除某個結(jié)點:
-
從頭結(jié)點出發(fā)找到前驅(qū)結(jié)點
-
修改前驅(qū)結(jié)點的游標(biāo)
-
被刪除結(jié)點next設(shè)為-2
總結(jié):
-
靜態(tài)鏈表:用數(shù)組的方式實現(xiàn)的鏈表
-
優(yōu)點:增、刪操作不需要大量移動元素
-
缺點:不能隨機存取,只能從頭結(jié)點開始依次往后查找;容量固定不可變
-
適用場景:①不支持指針的低級語言;②數(shù)據(jù)元素數(shù)量固定不變的場景(如操作系統(tǒng)的文件分配表FAT)
順序表和鏈表的比較
邏輯結(jié)構(gòu):
都屬于線性表,都是線性結(jié)構(gòu)
存儲結(jié)構(gòu):
順序表:
-
優(yōu)點:支持隨機存取、存儲密度高
-
缺點:大片連續(xù)空間分配不方便,改變?nèi)萘坎环奖?/p>
鏈表:
-
優(yōu)點:離散的小空間分配方便,改變?nèi)萘糠奖?/p>
-
缺點:不可隨機存取,存儲密度低
基本操作:
順序表:
-
創(chuàng)建
-
需要預(yù)分配大片連續(xù)空間。
-
若分配空間過小,則之后不方便拓展容量;
-
若分配空間過大,則浪費內(nèi)存資源
-
靜態(tài)分配:靜態(tài)數(shù)組實現(xiàn),容量不可改變
-
動態(tài)分配:動態(tài)數(shù)組(malloc、free)實現(xiàn),容量可以改變但需要移動大量元素,時間代價高
-
-
銷毀
-
修改Length = 0
-
靜態(tài)分配:靜態(tài)數(shù)組,系統(tǒng)自動回收空間
-
動態(tài)分配:動態(tài)數(shù)組(malloc、free),需要手動free
-
-
增刪
-
插入/刪除元素要將后續(xù)元素都后移/前移
-
時間復(fù)雜度O(n),時間開銷主要來自移動元素
-
若數(shù)據(jù)元素很大,則移動的時間代價很高
-
-
查
-
按位查找:O(1)
-
按值查找:O(n)若表內(nèi)元素有序,可在O(log2n)時間內(nèi)找到
-
鏈表:
-
創(chuàng)建
-
只需分配一個頭結(jié)點(也可以不要頭結(jié)點,只聲明一個頭指針),之后方便拓展
-
-
銷毀
-
依次刪除各個結(jié)點(free)
-
-
增刪
-
插入/刪除元素只需修改指針即可
-
時間復(fù)雜度O(n),時間開銷主要來自查找目標(biāo)元素
-
查找元素的時間代價更低
-
-
查
-
按位查找:O(n)
-
按值查找:O(n)
-
用哪個:
-
表長難以預(yù)估、經(jīng)常要增加/刪除元素——鏈表
-
表長可預(yù)估、查詢(搜索)操作較多——順序表
開放式問題的解題思路:
問題: 請描述順序表和鏈表的bla bla bla…實現(xiàn)線性表時,用順序表還是鏈表好?
答案:
-
順序表和鏈表的邏輯結(jié)構(gòu)都是線性結(jié)構(gòu),都屬于線性表。
-
但是二者的存儲結(jié)構(gòu)不同,順序表采用順序存儲…(特點,帶來的優(yōu)點缺點);鏈表采用鏈?zhǔn)酱鎯Αㄌ攸c、導(dǎo)致的優(yōu)缺點)。
-
由于采用不同的存儲方式實現(xiàn),因此基本操作的實現(xiàn)效率也不同。
-
當(dāng)初始化時…;當(dāng)插入一個數(shù)據(jù)元素時…;當(dāng)刪除一個數(shù)據(jù)元素時…;當(dāng)查找一個數(shù)據(jù)元素時…
棧和隊列
棧的基本概念
棧的定義:
-
棧(Stack)是只允許在一端進(jìn)行插入或刪除操作的線性表
-
邏輯結(jié)構(gòu):與普通線性表相同
-
數(shù)據(jù)的運算:插入、刪除操作有區(qū)別
-
棧頂:允許插入和刪除的一端,對應(yīng)元素被稱為棧頂元素
-
棧底:不允許插入和刪除的一端,對應(yīng)元素被稱為棧底元素
-
特點:后進(jìn)先出Last In First Out(LIFO)
棧的基本操作:
-
InitStack(&S):初始化棧。構(gòu)造一個空棧S,分配內(nèi)存空間。
-
DestroyStack(&S):銷毀棧。銷毀并釋放棧S所占用的內(nèi)存空間。
-
Push(&S,x):進(jìn)棧,若棧S未滿,則將x加入使之成為新棧頂。
-
Pop(&S,&x):出棧,若棧S非空,則彈出棧頂元素,并用x返回。
-
GetTop(S, &x):讀棧頂元素。若棧S非空,則用x返回棧頂元素
-
StackEmpty(S):判斷一個棧S是否為空。若S為空,則返回true,否則返回false。
出棧順序數(shù)量:
-
n個不同元素進(jìn)棧,出棧元素不同排列的個數(shù)為
-
上述公式稱為卡特蘭(Catalan)數(shù),可采用數(shù)學(xué)歸納法證明
棧的順序存儲結(jié)構(gòu)
順序棧的定義和初始化:
進(jìn)棧操作:
出棧操作:
讀取棧頂元素:
注意:也可以讓棧頂指針top先指向0,每次進(jìn)棧S.top++,出棧--S.top
共享棧:
-
使用靜態(tài)數(shù)組要求提前規(guī)定好棧的大小,容易造成內(nèi)存資源的浪費因此共享棧應(yīng)運而生
-
兩個棧共享同一片空間,0、1號棧朝著同一方向進(jìn)棧
-
棧滿的條件:top0 + 1 == top1
棧的鏈?zhǔn)酱鎯Y(jié)構(gòu)
棧的鏈?zhǔn)酱鎯嵸|(zhì):
-
進(jìn)棧:頭插法建立單鏈表,也就是對頭結(jié)點的后插操作
-
出棧:單鏈表的刪除操作,對頭結(jié)點的“后刪”操作
-
推薦使用不帶頭結(jié)點的鏈棧
-
創(chuàng)銷增刪查的操作參考鏈表
鏈棧的定義:
隊列的基本概念
隊列的定義:
-
棧(Stack)是只允許在一端進(jìn)行插入或刪除操作的操作受限的線性表
-
隊列(Queue)是只允許在一端進(jìn)行插入,在另一端刪除的線性表
-
隊頭:允許刪除的一端,對應(yīng)的元素被稱為隊頭元素
-
隊尾:允許插入的一端,對應(yīng)的元素被稱為隊尾元素
-
隊列的特點:先進(jìn)先出First In First Out(FIFO)
隊列的基本操作:
-
InitQueue(&Q):初始化隊列,構(gòu)造一個空隊列Q。
-
DestroyQueue(&Q):銷毀隊列。銷毀并釋放隊列Q所占用的內(nèi)存空間。
-
EnQueue(&Q,x):入隊,若隊列Q未滿,將x加入,使之成為新的隊尾。
-
DeQueue(&Q,&x):出隊,若隊列Q非空,刪除隊頭元素,并用x返回。
-
GetHead(Q,&x):讀隊頭元素,若隊列Q非空,則將隊頭元素賦值給x。
-
QueueEmpty(Q):判隊列空,若隊列Q為空返回true,否則返回false。
隊列的順序存儲結(jié)構(gòu)
隊列的定義和初始化:
入隊操作:
-
通過取余操作,只要隊列不滿,就可以一直利用之前已經(jīng)出隊了的空間,邏輯上實現(xiàn)了循環(huán)隊列的操作
-
于是,隊列已滿的條件:隊尾指針的再下一個位置是隊頭,即(Q.rear+1)%MaxSize==Q.front
-
代價:犧牲了一個存儲單元,因為如果rear和front相同,與判空的條件相同了
出隊操作:
實際上獲取隊頭元素的值就是出隊操作去掉隊頭指針后移的代碼
判斷隊列已滿/已空:
方案1:
-
使用前面講的犧牲一個存儲空間的方式來解決
-
初始化時rear=front=0
-
隊列元素個數(shù):(rear+MaxSize-front)%MaxSize
-
隊列已滿的條件:隊尾指針的再下一個位置是隊頭,即(Q.rear+1)%MaxSize==Q.front
-
隊空條件:Q.rear==Q.front
方案2:
-
不犧牲一個存儲空間,在結(jié)構(gòu)體中多建立一個變量size
-
初始化時rear=front=0;size = 0;
-
隊列元素個數(shù)= size
-
插入成功size++;刪除成功size--;
-
此時隊滿條件:size==MaxSize
-
隊空條件:size == 0;
方案3:
-
不犧牲一個存儲空間,在結(jié)構(gòu)體中多建立一個變量tag
-
初始化時rear=front=0;tag = 0;
-
因為只有刪除操作,才可能導(dǎo)致隊空,只有插入操作,才可能導(dǎo)致隊滿因此
-
每次刪除操作成功時,都令tag=0;
-
每次插入操作成功時,都令tag=1;
-
隊滿條件:front==rear && tag == 1
-
隊空條件:front==rear && tag == 0
-
隊列元素個數(shù):(rear+MaxSize-front)%MaxSize
隊列的鏈?zhǔn)酱鎯Y(jié)構(gòu)
初始化(帶頭結(jié)點):
初始化(不帶頭結(jié)點):
入隊(帶頭結(jié)點):
入隊(不帶頭結(jié)點):
出隊(帶頭結(jié)點):
出隊(不帶頭結(jié)點):
隊列滿的條件:
-
順序存儲:預(yù)分配的空間耗盡時隊滿
-
鏈?zhǔn)酱鎯Γ阂话悴粫牆M,除非內(nèi)存不足
-
因此一般不用考慮隊滿
雙端隊列
定義:
-
雙端隊列:只允許從兩端插入、兩端刪除的線性表
-
輸入受限的雙端隊列:只允許從一端插入、兩端刪除的線性表
-
輸出受限的雙端隊列:只允許從兩端插入、一端刪除的線性表
-
不管是怎么樣的雙端隊列實際都是棧和隊列的變種
考點:
-
判斷輸出序列合法性
-
在棧中合法的輸出序列,在雙端隊列中必定合法
棧在括號匹配中的應(yīng)用
括號匹配問題:
-
若有括號無法被匹配則出現(xiàn)編譯錯誤
-
遇到左括號就入棧
-
遇到右括號,就“消耗”一個左括號
代碼實現(xiàn):
棧在表達(dá)式求值中的應(yīng)用
算數(shù)表達(dá)式:
-
由三個部分組成:操作數(shù)、運算符、界限符
-
我們平時寫的算術(shù)表達(dá)式都是中綴表達(dá)式
-
如何可以不用界限符也能無歧義地表達(dá)運算順序
-
Reverse Polish notation(逆波蘭表達(dá)式=后綴表達(dá)式)
-
Polish notation(波蘭表達(dá)式=前綴表達(dá)式)
中綴、后綴、前綴表達(dá)式:
中綴轉(zhuǎn)后綴的方法(手算):
-
確定中綴表達(dá)式中各個運算符的運算順序
-
選擇下一個運算符,按照「左操作數(shù)右操作數(shù)運算符」的方式組合成一個新的操作數(shù)
-
如果還有運算符沒被處理,就繼續(xù)第二步
-
注意:運算順序不唯一,因此對應(yīng)的后綴表達(dá)式也不唯一
-
“左優(yōu)先”原則:只要左邊的運算符能先計算,就優(yōu)先算左邊的,保證手算和機算是一致的
中綴表達(dá)式轉(zhuǎn)后綴表達(dá)式(機算,用棧實現(xiàn)):
-
初始化一個棧,用于保存暫時還不能確定運算順序的運算符。
-
從左到右處理各個元素,直到末尾??赡苡龅饺N情況:
-
遇到操作數(shù)。直接加入后綴表達(dá)式。
-
遇到界限符。遇到“(”直接入棧;遇到“)”則依次彈出棧內(nèi)運算符并加入后綴表達(dá)式,直到彈出“(”為止。注意:“(”不加入后綴表達(dá)式。
-
遇到運算符。依次彈出棧中優(yōu)先級高于或等于當(dāng)前運算符的所有運算符,并加入后綴表達(dá)式,若碰到“(”或??談t停止。之后再把當(dāng)前運算符入棧。
-
-
按上述方法處理完所有字符后,將棧中剩余運算符依次彈出,并加入后綴表達(dá)式。
后綴表達(dá)式的計算(手算):
-
從左往右掃描,每遇到一個運算符,就讓運算符前面最近的兩個操作數(shù)執(zhí)行對應(yīng)運算,合體為一個操作數(shù)
-
注意:兩個操作數(shù)的左右順序
-
特點:最后出現(xiàn)的操作數(shù)先被運算,LIFO(后進(jìn)先出),可以使用棧來完成這個步驟
-
“左優(yōu)先”原則:只要左邊的運算符能先計算,就優(yōu)先算左邊的
后綴表達(dá)式的計算(機算,用棧實現(xiàn)):
-
從左往右掃描下一個元素,直到處理完所有元素
-
若掃描到操作數(shù)則壓入棧,并回到第一步;否則執(zhí)行第三步
-
若掃描到運算符,則彈出兩個棧頂元素,執(zhí)行相應(yīng)運算,運算結(jié)果壓回棧頂,回到第一步
-
注意:先出棧的是“右操作數(shù)”
-
若表達(dá)式合法,則最后棧中只會留下一個元素,就是最終結(jié)果
-
后綴表達(dá)式適用于基于棧的編程語言(stack-orientedprogramming language),如:Forth、PostScript
中綴表達(dá)式轉(zhuǎn)前綴表達(dá)式(手算):
-
確定中綴表達(dá)式中各個運算符的運算順序
-
選擇下一個運算符,按照「運算符左操作數(shù)右操作數(shù)」的方式組合成一個新的操作數(shù)
-
如果還有運算符沒被處理,就繼續(xù)第二步
-
“右優(yōu)先”原則:只要右邊的運算符能先計算,就優(yōu)先算右邊的
中綴表達(dá)式的計算(機算,用棧實現(xiàn)):
-
中綴表達(dá)式的計算=中綴轉(zhuǎn)后綴+后綴表達(dá)式求值,兩個算法的結(jié)合
-
用棧實現(xiàn)中綴表達(dá)式的計算:
-
初始化兩個棧,操作數(shù)棧和運算符棧
-
若掃描到操作數(shù),壓入操作數(shù)棧
-
若掃描到運算符或界限符,則按照“中綴轉(zhuǎn)后綴”相同的邏輯壓入運算符棧(期間也會彈出運算符,每當(dāng)彈出一個運算符時,就需要再彈出兩個操作數(shù)棧的棧頂元素并執(zhí)行相應(yīng)運算,運算結(jié)果再壓回操作數(shù)棧)
-
棧在遞歸中的應(yīng)用
函數(shù)調(diào)用的特點:
-
最后被調(diào)用的函數(shù)最先執(zhí)行結(jié)束(LIFO)
-
函數(shù)調(diào)用時,需要用一個棧(函數(shù)調(diào)用棧)存儲,里面包含以下信息:
-
調(diào)用返回地址
-
實參
-
局部變量
-
-
適合用“遞歸”算法解決:可以把原始問題轉(zhuǎn)換為屬性相同,但規(guī)模較小的問題
棧在遞歸中的應(yīng)用:
-
計算正整數(shù)的階乘n!
-
求斐波那契數(shù)列
-
......
棧在遞歸中過程:
-
遞歸調(diào)用時,函數(shù)調(diào)用??煞Q為“遞歸工作棧”
-
每進(jìn)入一層遞歸,就將遞歸調(diào)用所需信息壓入棧頂
-
每退出一層遞歸,就從棧頂彈出相應(yīng)信息
-
缺點:
-
太多層遞歸可能會導(dǎo)致棧溢出
-
可能包含很多重復(fù)計算
-
隊列的應(yīng)用
-
樹的層次遍歷
-
圖的廣度優(yōu)先遍歷
-
操作系統(tǒng)中的應(yīng)用
-
多個進(jìn)程爭搶著使用有限的系統(tǒng)資源時,F(xiàn)CFS(First Come First Service,先來先服務(wù))是一種常用策略??梢杂藐犃袑崿F(xiàn)
-
CPU資源的分配
-
打印數(shù)據(jù)緩沖區(qū)
-
特殊矩陣的壓縮儲存
一維數(shù)組的存儲結(jié)構(gòu):
-
起始地址:LOC
-
各數(shù)組元素大小相同,且物理上連續(xù)存放。
-
數(shù)組元素a[i]的存放地址= LOC + i * sizeof(ElemType)
二維數(shù)組的存儲結(jié)構(gòu):
-
分為行優(yōu)先和列優(yōu)先,本質(zhì)就是把二維的邏輯視角轉(zhuǎn)換為內(nèi)存中的一維儲存
-
M行N列的二維數(shù)組b[M][N]中,若按行優(yōu)先存儲,則b[i][j]的存儲地址= LOC + (i*N + j) * sizeof(ElemType)
-
M行N列的二維數(shù)組b[M][N]中,若按列優(yōu)先存儲,則b[i][j]的存儲地址= LOC + ( j*M+ i ) * sizeof(ElemType)
-
二維數(shù)組也有隨機存儲的特性
普通矩陣的存儲:
-
可用二維數(shù)組存儲
-
注意:描述矩陣元素時,行、列號通常從1開始;而描述數(shù)組時通常下標(biāo)從0開始
-
某些特殊矩陣可以壓縮存儲空間(比如對稱矩陣)
對稱矩陣的壓縮存儲:
-
若n階方陣中任意一個元素ai,j都有ai,j = aj,i則該矩陣為對稱矩陣
-
普通存儲:n*n二維數(shù)組
-
壓縮存儲策略:只存儲主對角線+下三角區(qū)(或主對角線+上三角區(qū)),按行優(yōu)先原則將各元素存入一維數(shù)組中
-
數(shù)組大小應(yīng)為多少:(1+n)*n/2
-
站在程序員的角度,對稱矩陣壓縮存儲后怎樣才能方便使用:可以實現(xiàn)一個“映射”函數(shù)矩陣下標(biāo)->一維數(shù)組下標(biāo)
-
按行優(yōu)先的原則,ai,j是第幾個元素:
三角矩陣的壓縮存儲:
-
下三角矩陣:除了主對角線和下三角區(qū),其余的元素都相同
-
上三角矩陣:除了主對角線和上三角區(qū),其余的元素都相同
-
壓縮存儲策略:按行優(yōu)先原則將橙色區(qū)元素存入一維數(shù)組中,并在最后一個位置存儲常量c
-
下三角矩陣,按行優(yōu)先的原則,ai,j是第幾個元素:
-
上三角矩陣,按行優(yōu)先的原則,ai,j是第幾個元素:
三對角矩陣的壓縮存儲:
-
三對角矩陣,又稱帶狀矩陣:當(dāng)|i - j|>1時,有ai,j = 0 (1≤ i, j ≤n)
-
壓縮存儲策略:按行優(yōu)先(或列優(yōu)先)原則,只存儲帶狀部分
-
按行優(yōu)先的原則,ai,j是第幾個元素:
-
稀疏矩陣的壓縮存儲:
-
稀疏矩陣:非零元素遠(yuǎn)遠(yuǎn)少于矩陣元素的個數(shù)
-
壓縮存儲策略1:順序存儲——三元組<i(行),j(列),v(值)>,失去了數(shù)組隨機存儲的特性
-
壓縮存儲策略2:鏈?zhǔn)酱鎯?,十字鏈表?/p>
-
串
串的定義和基本操作
定義:
-
串,即字符串(String)是由零個或多個字符組成的有限序列。一般記為S = ‘a(chǎn)1a2······an' (n ≥0)
-
其中,S是串名,單引號括起來的字符序列是串的值;ai可以是字母、數(shù)字或其他字符;串中字符的個數(shù)n稱為串的長度。n = 0時的串稱為空串(用?表示)。
-
有的地方用雙引號(如Java、C),有的地方用單引號(如Python)
-
例如:S=”HelloWorld!”T=‘iPhone 11 Pro Max?’
-
子串:串中任意個連續(xù)的字符組成的子序列。Eg:’iPhone’,’Pro M’是串T的子串
-
主串:包含子串的串。Eg:T是子串’iPhone’的主串
-
字符在主串中的位置:字符在串中的序號。Eg:’1’在T中的位置是8(第一次出現(xiàn))
-
子串在主串中的位置:子串的第一個字符在主串中的位置。Eg:’11 Pro’在T中的位置為8
-
每個空格字符占1B,不是空串
-
串的位序從1開始而不是從0開始
-
串是一種特殊的線性表,數(shù)據(jù)元素之間呈線性關(guān)系
-
串的數(shù)據(jù)對象限定為字符集(如中文字符、英文字符、數(shù)字字符、標(biāo)點字符等)
-
串的基本操作,如增刪改查等通常以子串為操作對象,因為人類的語言通常要多個字符組成的序列才有現(xiàn)實意義
串的基本操作:
-
假設(shè)有串T=“”,S=”iPhone 11 Pro Max?”,W=“Pro”
-
StrAssign(&T,chars):賦值操作。把串T賦值為chars。
-
StrCopy(&T,S):復(fù)制操作。由串S復(fù)制得到串T。
-
StrEmpty(S):判空操作。若S為空串,則返回TRUE,否則返回FALSE。
-
StrLength(S):求串長。返回串S的元素個數(shù)。
-
ClearString(&S):清空操作。將S清為空串。
-
DestroyString(&S):銷毀串。將串S銷毀(回收存儲空間)。
-
Concat(&T,S1,S2):串聯(lián)接。用T返回由S1和S2聯(lián)接而成的新串
-
SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos個字符起長度為len的子串。
-
Index(S,T):定位操作。若主串S中存在與串T值相同的子串,則返回它在主串S中第一次出現(xiàn)的位置;否則函數(shù)值為0。
-
StrCompare(S,T):比較操作。若S>T,則返回值>0;若S=T,則返回值=0;若S<T,則返回值<0。
-
從第一個字符開始往后依次對比,先出現(xiàn)更大字符的串就更大
-
長串的前綴與短串相同時,長串更大
-
只有兩個串完全相同時,才相等
-
字符集編碼:
-
任何數(shù)據(jù)存到計算機中一定是二進(jìn)制數(shù)。
-
需要確定一個字符和二進(jìn)制數(shù)的對應(yīng)規(guī)則這就是“編碼”
-
“字符集”:英文字符,ASCII字符集,中英文,Unicode字符集
-
基于同一個字符集,可以有多種編碼方案,如:UTF-8,UTF-16
-
注:采用不同的編碼方式,每個字符所占空間不同,考研中只需默認(rèn)每個字符占1B即可
串的儲存結(jié)構(gòu)
順序存儲:
-
方案一:一個數(shù)組來儲存字符,一個int變量length儲存實際長度
-
方案二:數(shù)組的ch[0]來充當(dāng)length,優(yōu)點:字符的位序和數(shù)組下標(biāo)相同
-
方案三:沒有Length變量,以字符’\0’表示結(jié)尾(對應(yīng)ASCII碼的0),缺點:獲取字符串長度需要遍歷,時間復(fù)雜度高
-
方案四:數(shù)組的ch[0]廢棄不用,從看開始儲存字符,外加一個int變量length儲存實際長度
鏈?zhǔn)酱鎯Γ?/strong>
推薦使用第二種方式,存儲密度較高,ch數(shù)組未必一定是4個字符,也可以比4多
基本操作的實現(xiàn)(使用第四種方案):
樸素模式匹配算法
字符串模式匹配:
-
在主串中找到與模式串相同的子串,并返回其所在位置。
-
子串:主串的一部分,一定存在
-
模式串:不一定能在主串中找到
-
要掌握樸素模式匹配算法、KMP算法兩種方法
樸素模式匹配算法(兩種實現(xiàn)方法):
-
將主串中所有長度為m的子串依次與模式串對比,直到找到一個完全匹配的子串,或所有的子串都不匹配為止。
-
主串長度為n,模式串長度為 m,最多對比 n-m+1 個子串
-
上節(jié)講的index定位操作就是樸素模式匹配算法中其中一種實現(xiàn)方法
-
也可以使用兩個指針i和j來進(jìn)行匹配。若當(dāng)前子串匹配失敗,則主串指針 i 指向下一個子串的第一個位置,模式串指針 j 回到模式串的第一個位置,即i = i - j + 2; j = 1;
-
若 j > T.length,則當(dāng)前子串匹配成功,返回當(dāng)前子串第一個字符的位置即i - T.length
-
-
設(shè)主串長度為 n,模式串長度為 m,則最壞時間復(fù)雜度 = O(n*m),最好時間復(fù)雜度 = O(n)
KMP算法的概念
-
由D.E.Knuth,J.H.Morris和V.R.Pratt提出,因此稱為 KMP算法
-
是對樸素模式匹配算法的優(yōu)化
-
優(yōu)化的原理就是減少了i指針的回溯,通過已經(jīng)計算好的next指針,提高算法的整體運行效率
-
next數(shù)組記錄了當(dāng)?shù)趲讉€元素匹配失敗時候,j的取值例如:
-
對于模式串 T = ‘a(chǎn)baabc’
-
當(dāng)?shù)?個元素匹配失敗時,可令主串指針 i 不變,模式串指針 j=3
-
當(dāng)?shù)?個元素匹配失敗時,可令主串指針 i 不變,模式串指針 j=2
-
當(dāng)?shù)?個元素匹配失敗時,可令主串指針 i 不變,模式串指針 j=2
-
當(dāng)?shù)?個元素匹配失敗時,可令主串指針 i 不變,模式串指針 j=1
-
當(dāng)?shù)?個元素匹配失敗時,可令主串指針 i 不變,模式串指針 j=1
-
當(dāng)?shù)?個元素匹配失敗時,匹配下一個相鄰子串,令 j=0, i++, j++
-
-
next數(shù)組只和短短的模式串有關(guān),和長長的主串無關(guān)(重要)
-
之所以只和模式串有關(guān),是因為如果在哪里匹配失敗,同時說明在這之前的部分主串和模式串是相同的
-
KMP算法,最壞時間復(fù)雜度 O(m+n),其中,求 next 數(shù)組時間復(fù)雜度 O(m),模式匹配過程最壞時間復(fù)雜度 O(n)
-
KMP算法精髓:利用好已經(jīng)匹配過的模式串的信息
-
步驟:
-
根據(jù)模式串T,求出 next 數(shù)組
-
-
利用next數(shù)組進(jìn)行匹配(主串指針不回溯)
KMP算法之求next數(shù)組
next數(shù)組的作用:
當(dāng)模式串的第 j 個字符失配時,從模式串的第 next[j] 的繼續(xù)往后匹配
next數(shù)組的求法:
-
任何模式串都一樣,第1個字符不匹配時,只能匹配下一個子串,因此next[1]都無腦寫 0(if(j==0) {i++; j++;})
-
任何模式串都一樣,第2個字符不匹配時,應(yīng)嘗試匹配模式串的第1個字符,因此next[2]都無腦寫 1
-
每一個模式串不一樣,對于第3個字符及之后的字符串,在不匹配的位置前邊,劃一根美麗的分界線模式串一步一步往后退,直到分界線之前“能對上”,此時 j 指向哪兒,next數(shù)組值就是多少
KMP算法之求nextval數(shù)組
定義:
nextval數(shù)組是對next數(shù)組的優(yōu)化,用nextval替代next數(shù)組,減少了無意義的對比
nextval數(shù)組的求法:
-
先根據(jù)上面的方法算出next數(shù)組
-
先令nextval[1]=0
-
再根據(jù)下面代碼算出后面的nextval數(shù)組
-
for(int j = 2; j <= T.length; j++) { ? ?//讓第next值個元素的值和當(dāng)前元素比較 ? ?if(T.ch[next[j]] == T.ch[j]) ? { ? ? ? ?//若相等則讓第next值個元素的nextval值復(fù)制給當(dāng)前元素的nextval值 ? ? ? ?nextval[j] = nextval[next[j]]; ? } ? ?else ? { ? ? ? ?//若不等則讓當(dāng)前元素的next值賦值給當(dāng)前元素的nextval值 ? nextval[j] = next[j]; ? } }
樹
樹的定義和基本術(shù)語
空樹:
結(jié)點數(shù)為0的樹
非空樹的特性:
-
有且僅有一個根結(jié)點
-
沒有后繼的結(jié)點稱為“葉子結(jié)點”(或終端結(jié)點)
-
有后繼的結(jié)點稱為“分支結(jié)點”(或非終端結(jié)點)
-
除了根結(jié)點外,任何一個結(jié)點都有且僅有一個前驅(qū)
-
每個結(jié)點可以有0個或多個后繼
樹的定義:
-
樹是n(n≥0)個結(jié)點的有限集合,n = 0時,稱為空樹,這是一種特殊情況。
-
在任意一棵非空樹中應(yīng)滿足:
-
有且僅有一個特定的稱為根的結(jié)點。
-
當(dāng)n > 1時,其余結(jié)點可分為m(m > 0)個互不相交的有限集合T1, T2,…, Tm,其中每個集合本身又是一棵樹,并且稱為根結(jié)點的子樹。
-
結(jié)點之間的關(guān)系描述:
-
什么是祖先結(jié)點?比自己深度低的結(jié)點
-
什么是子孫結(jié)點?比自己深度高的結(jié)點
-
什么是雙親結(jié)點(父結(jié)點)?自己的根結(jié)點
-
什么是孩子結(jié)點?自己作為根結(jié)點的兒子
-
什么是兄弟結(jié)點?與自己擁有相同父結(jié)點結(jié)點
-
什么是堂兄弟結(jié)點?與自己同一層的結(jié)點
-
什么是兩個結(jié)點之間的路徑?能從上往下前往的兩個結(jié)點被稱為有路徑
-
什么是路徑長度?經(jīng)過幾條邊
結(jié)點、樹的屬性描述:
-
結(jié)點的層次(深度):從上往下數(shù),默認(rèn)從1開始
-
結(jié)點的高度:從下往上數(shù)
-
樹的高度(深度):總共多少層
-
結(jié)點的度:有幾個孩子(分支)
-
樹的度:各結(jié)點的度的最大值
有序數(shù)和無序樹:
-
有序樹:邏輯上看,樹中結(jié)點的各子樹從左至右是有次序的,不能互換
-
無序樹:邏輯上看,樹中結(jié)點的各子樹從左至右是無次序的,可以互換
-
是否是什么,具體看你用樹存什么,是否需要用結(jié)點的左右位置反映某些邏輯關(guān)系
樹和森林:
-
森林是m(m≥0)棵互不相交的樹的集合
-
考點:樹和森林的相互轉(zhuǎn)換
樹的性質(zhì)
-
樹的總結(jié)點數(shù)=樹的總度數(shù)+1
-
度數(shù)為m的樹和m叉樹的區(qū)別
-
度為m的樹第i 層至多有m^i-1 個結(jié)點(i≥1)
-
m叉樹第i 層至多有m^i-1 個結(jié)點(i≥1)
-
高度為h的m叉樹至多有((m^h)-1)/m-1個結(jié)點
-
高度為h的m叉樹至少有h 個結(jié)點
-
高度為h、度為m的樹至少有h+m-1 個結(jié)點
-
具有n個結(jié)點的m叉樹的最小高度為logm(n(m - 1) + 1)
-
高度最小的情況:所有結(jié)點都有m個孩子
二叉樹的定義和基本術(shù)語
定義:
-
二叉樹是n(n≥0)個結(jié)點的有限集合
-
或者為空二叉樹,即n = 0。
-
或者由一個根結(jié)點和兩個互不相交的被稱為根的左子樹和右子樹組成。左子樹和右子樹又分別是一棵二叉樹。
-
-
特點:
-
每個結(jié)點至多只有兩棵子樹
-
左右子樹不能顛倒(二叉樹是有序樹)
-
二叉樹的五種狀態(tài):
-
空二叉樹
-
只有左子樹
-
只有右子樹
-
只有根結(jié)點
-
左右子樹都有
幾個特殊的二叉樹:
-
滿二叉樹:一棵高度為h,且含有2^h - 1個結(jié)點的二叉樹
-
特點:
-
只有最后一層有葉子結(jié)點
-
不存在度為1 的結(jié)點
-
按層序從1 開始編號,結(jié)點i 的左孩子為2i,右孩子為2i+1;結(jié)點i 的父結(jié)點為??/2 (如果有的話)
-
-
-
完全二叉樹:當(dāng)且僅當(dāng)其每個結(jié)點都與高度為h的滿二叉樹中編號為1~n的結(jié)點一一對應(yīng)時,稱為完全二叉樹
-
特點:
-
只有最后兩層可能有葉子結(jié)點
-
最多只有一個度為1的結(jié)點
-
按層序從1 開始編號,結(jié)點i 的左孩子為2i,右孩子為2i+1;結(jié)點i 的父結(jié)點為??/2 (如果有的話)
-
i≤ n/2 為分支結(jié)點, i> n/2 為葉子結(jié)點
-
如果某結(jié)點只有一個孩子,那么一定是左孩子
-
-
-
是滿二叉樹一定是完全二叉樹,是完全二叉樹不一定是滿二叉樹
-
二叉排序樹:一棵二叉樹或者是空二叉樹,或者是具有如下性質(zhì)的二叉樹:
-
左子樹上所有結(jié)點的關(guān)鍵字均小于根結(jié)點的關(guān)鍵字
-
右子樹上所有結(jié)點的關(guān)鍵字均大于根結(jié)點的關(guān)鍵字
-
左子樹和右子樹又各是一棵二叉排序樹
-
-
二叉排序樹可用于元素的排序、搜索
-
平衡二叉樹:樹上任一結(jié)點的左子樹和右子樹的深度之差不超過1。
-
平衡二叉樹能有更高的搜索效率
-
又寬又胖的樹
-
各種二叉樹的性質(zhì)
二叉樹的性質(zhì):
-
設(shè)非空二叉樹中度為0、1和2的結(jié)點個數(shù)分別為n0、n1和n2,則n0 = n2 + 1(葉子結(jié)點比二分支結(jié)點多一個)
-
二叉樹第i 層至多有2^(i-1)個結(jié)點(i≥1)
-
高度為h的2叉樹至多有(2^h)-1個結(jié)點(滿二叉樹)
完全二叉樹的性質(zhì):
-
具有n個(n>0)結(jié)點的完全二叉樹的高度h為log2(n + 1) 或[log2n] + 1
-
對于完全二叉樹,可以由的結(jié)點數(shù)n 推出度為0、1和2的結(jié)點個數(shù)為n0、n1和n2(突破點:完全二叉樹最多只會有一個度為1的結(jié)點)
二叉樹的儲存結(jié)構(gòu)
順序存儲:
常用的基本操作:
-
i的左孩子:2i
-
i的右孩子:2i+1
-
i的父結(jié)點:?i/2?
-
i所在的層次:?log2(n+1)?或者?log2n?+1
若完全二叉樹中共有n個結(jié)點,則:
-
判斷i是否有左孩子:2i ≤ n ?
-
判斷i是否有右孩子:2i+1 ≤ n ?
-
判斷i是否是葉子/分支結(jié)點:i > ?n/2? ?
儲存非完全二叉樹:
二叉樹的鏈?zhǔn)酱鎯Γ?/strong>
定義和初始化:
-
這樣的方法可以很簡單的找到p結(jié)點的左右孩子,但是只能通過從根開始遍歷查找找到結(jié)點p的父結(jié)點
-
可以通過多定一個父結(jié)點的指針來方便的查找父結(jié)點(三叉鏈表)
-
n個結(jié)點的二叉鏈表共有n+1 個空鏈域(可以用來構(gòu)建線索二叉樹)
二叉樹的先中后序遍歷
遍歷:
-
遍歷:按照某種次序把所有結(jié)點都訪問一遍
-
層序遍歷:基于樹的層次特性確定的次序規(guī)則
-
先/中/后序遍歷:基于樹的遞歸特性確定的次序規(guī)則
二叉樹的遍歷:
-
二叉樹的遞歸特性:
-
要么是個空二叉樹
-
要么就是由“根結(jié)點+左子樹+右子樹”組成的二叉樹
-
-
先序遍歷:根左右(NLR)
-
中序遍歷:左根右(LNR)
-
后序遍歷:左右根(LRN)
先序遍歷(代碼):
中序遍歷(代碼):
后序遍歷(代碼):
二叉樹遍歷總結(jié):
-
空間復(fù)雜度為O(h),h為樹的高度
-
每個結(jié)點都會被路過3次
求樹的深度:
-
是后序遍歷的變種
-
先后訪問左右兒子,得出對應(yīng)深度返回左右兒子深度更高的那個就是樹的深度
二叉樹的層序遍歷
層序遍歷:
基于樹的層次特性確定的次序規(guī)則
算法思想:
-
初始化一個輔助隊列
-
根結(jié)點入隊
-
若隊列非空,則隊頭結(jié)點出隊,訪問該結(jié)點,并將其左、右孩子插入隊尾(如果有的話)
-
重復(fù)第三步直至隊列為空
代碼實現(xiàn):
由遍歷序列構(gòu)造二叉樹
結(jié)論:
-
一個前/中/后/層序遍歷序列可能對應(yīng)多種二叉樹形態(tài)
-
只有至少同時擁有兩種遍歷序列才能確定二叉樹的形態(tài)
-
結(jié)論:前序、后序、層序序列的兩兩組合無法唯一確定一科二叉樹
通過兩種遍歷序列確定二叉樹:
前序+中序:
中序+后序:
層序+中序:
線索二叉樹的概念
中序遍歷的問題:
-
如何找到指定結(jié)點p在q 中序遍歷序列中的前驅(qū)?
-
如何找到p的中序后繼?
-
能否從一個指定結(jié)點開始中序遍歷?
-
完成上述需求的思路:
-
從根結(jié)點出發(fā),重新進(jìn)行一次中序遍歷,指針q記錄當(dāng)前訪問的結(jié)點,指針pre記錄上一個被訪問的結(jié)點
-
當(dāng)q == p時,pre為前驅(qū)
-
當(dāng)pre == p時,q為后繼
-
-
缺點:找前驅(qū)、后繼很不方便; 操作必須從根開始
中序線索二叉樹:
線索二叉樹的存儲結(jié)構(gòu):
先/中/后序線索二叉樹同理
三種線索二叉樹的對比:
二叉樹的線索化
通過頭結(jié)點找到中序前驅(qū):
中序二叉樹線索化:
先序二叉樹線索化:
-
由于先序遍歷先遍歷根結(jié)點然后再遍歷左結(jié)點,若左孩子為空,通過線索化后會指回前驅(qū)結(jié)點(他的根結(jié)點)
-
這時在此訪問左孩子時候會又訪問回根結(jié)點,因此需要增加一個判斷來確定左孩子不是真正的左孩子而是線索化后的前驅(qū)結(jié)點
-
因此PreThread函數(shù)需要優(yōu)化為:
后序二叉樹線索化:
在線索二叉樹中找前驅(qū)后驅(qū)
中序線索二叉樹找中序前驅(qū)后繼:
-
在中序線索二叉樹中找到指定結(jié)點*p的中序后繼next
-
若p->rtag == 1,則next = p->rchild
-
若p->rtag == 0,說明p必定有右孩子,next = (p的右子樹中最左下的結(jié)點)
-
-
在中序線索二叉樹中找到指定結(jié)點*p的中序前驅(qū)pre
-
若p->ltag == 1,則pre = p->lchild
-
若p->ltag == 0,說明p必定有左孩子,pre = (p的左子樹中最右下的結(jié)點)
-
先序線索二叉樹找先序前驅(qū)后繼:
-
在先序線索二叉樹中找到指定結(jié)點*p的先序后繼next
-
若p->rtag == 1,則next = p->rchild
-
若p->rtag == 0,說明p必定有右孩子
-
若p有左孩子,則先序后繼為左孩子
-
若p沒有左孩子,則先序后繼為右孩子
-
-
-
在先序線索二叉樹中找到指定結(jié)點*p的先序前驅(qū)pre
-
若p->ltag == 1,則pre = p->lchild
-
若p->ltag == 0,說明p必定有左孩子
-
先序遍歷中,左右子樹中的結(jié)點只可能是根的后繼,不可能是前驅(qū)
-
方法1:用土辦法從頭開始先序遍歷
-
方法2:可以改用三叉鏈表以找到父結(jié)點
-
-
后序線索二叉樹找后序前驅(qū)后繼:
-
在后序線索二叉樹中找到指定結(jié)點*p的后序前驅(qū)pre
-
若p->ltag == 1,則pre = p->lchild
-
若p->ltag == 0,說明p必有左孩子
-
若p有右孩子,則后序前驅(qū)為右孩子
-
若p沒有右孩子,則后序前驅(qū)為左孩子
-
-
-
在后序線索二叉樹中找到指定結(jié)點*p的后序后繼next
-
若p->rtag == 1,則next = p->rchild
-
若p->rtag == 0,說明p必定有右孩子
-
后序遍歷中,左右子樹中的結(jié)點只可能是根的前驅(qū),不可能是后繼
-
方法1:用土辦法從頭開始先序遍歷
-
方法2:可以改用三叉鏈表以找到父結(jié)點
-
-
樹的儲存結(jié)構(gòu)
雙親表示法(順序存儲):
-
每個結(jié)點中保存指向雙親的“指針”,data,parrent
-
根結(jié)點固定存儲在0,-1表示沒有雙親
-
新增數(shù)據(jù)元素,無需按邏輯上的次序存儲,只需說明新增元素的data,parrent即可
-
刪除數(shù)據(jù)元素
-
方案1:把要刪除的數(shù)據(jù)元素data設(shè)為空,parent設(shè)為-1
-
方案2:將數(shù)組尾部的數(shù)據(jù)元素覆蓋要刪除的數(shù)據(jù)元素
-
-
查詢數(shù)據(jù)元素
-
優(yōu)點:查指定結(jié)點的雙親很方便
-
缺點:查指定結(jié)點的孩子只能從頭遍歷
-
孩子表示法(順序+鏈?zhǔn)酱鎯Γ?/strong>
孩子兄弟表示法(鏈?zhǔn)酱鎯Γ?/strong>
規(guī)則:
-
左指針指向第一個孩子
-
右指針指向自己的第一個兄弟
森林和二叉樹的轉(zhuǎn)換:
-
本質(zhì):用二叉鏈表存儲森林
-
規(guī)則:
-
各個樹的根結(jié)點視為兄弟關(guān)系
-
左指針指向第一個孩子
-
右指針指向自己的第一個兄弟
-
樹和森林的遍歷
樹的先根遍歷:
-
先根遍歷:若樹非空,先訪問根結(jié)點,再依次對每棵子樹進(jìn)行先根遍歷。
-
樹的先根遍歷序列與這棵樹相應(yīng)二叉樹的先序序列相同。
樹的后根遍歷:
-
后根遍歷:若樹非空,先依次對每棵子樹進(jìn)行后根遍歷,最后再訪問根結(jié)點
-
樹的后根遍歷序列與這棵樹相應(yīng)二叉樹的中序序列相同
-
也被稱為深度優(yōu)先遍歷
樹的層次遍歷:
-
用隊列實現(xiàn),又被稱為廣度優(yōu)先遍歷
-
步驟
-
若樹非空,則根結(jié)點入隊
-
若隊列非空,隊頭元素出隊并訪問,同時將該元素的孩子依次入隊
-
重復(fù)第二步直到隊列為空
-
森林的先序遍歷:
-
若森林為非空,則按如下規(guī)則進(jìn)行遍歷:
-
訪問森林中第一棵樹的根結(jié)點。
-
先序遍歷第一棵樹中根結(jié)點的子樹森林。
-
先序遍歷除去第一棵樹之后剩余的樹構(gòu)成的森林。
-
-
效果等同于依次對各個樹進(jìn)行先根遍歷
-
用孩子兄弟表示法轉(zhuǎn)換為二叉樹,效果等同于依次對二叉樹的先序遍歷
森林的中序遍歷:
-
若森林為非空,則按如下規(guī)則進(jìn)行遍歷:
-
中序遍歷森林中第一棵樹的根結(jié)點的子樹森林。
-
訪問第一棵樹的根結(jié)點。
-
中序遍歷除去第一棵樹之后剩余的樹構(gòu)成的森林。
-
-
效果等同于依次對各個樹進(jìn)行后根遍歷
-
用孩子兄弟表示法轉(zhuǎn)換為二叉樹,效果等同于依次對二叉樹的中序遍歷
二叉排序樹(BST)
定義:
-
二叉排序樹,又稱二叉查找樹(BST,Binary Search Tree)
-
一棵二叉樹或者是空二叉樹,或者是具有如下性質(zhì)的二叉樹:
-
左子樹上所有結(jié)點的關(guān)鍵字均小于根結(jié)點的關(guān)鍵字
-
右子樹上所有結(jié)點的關(guān)鍵字均大于根結(jié)點的關(guān)鍵字
-
左子樹和右子樹又各是一棵二叉排序樹
-
-
即左子樹結(jié)點值< 根結(jié)點值< 右子樹結(jié)點值
-
進(jìn)行中序遍歷,可以得到一個遞增的有序序列
-
二叉排序樹可用于元素的有序組織、搜索
BST的查找:
-
若樹非空,目標(biāo)值與根結(jié)點的值比較:
-
若相等,則查找成功
-
若小于根結(jié)點,則在左子樹上查找,否則在右子樹上查找。
-
查找成功,返回結(jié)點指針;
-
查找失敗返回NULL
-
-
遞歸實現(xiàn)的最壞空間復(fù)雜度為O(h),普通實現(xiàn)的最壞空間復(fù)雜度為O(1)
BST的插入:
-
若原二叉排序樹為空,則直接插入結(jié)點;
-
否則,若關(guān)鍵字k小于根結(jié)點值,則插入到左子樹,若關(guān)鍵字k大于根結(jié)點值,則插入到右子樹
-
遞歸實現(xiàn)的最壞空間復(fù)雜度為O(h)
二叉排序樹的構(gòu)造:
注意:不同的關(guān)鍵字序列可能得到同款二叉排序樹,也可能得到不同款二叉排序樹
二叉排序樹的刪除:
-
先搜索找到目標(biāo)結(jié)點:
-
若被刪除結(jié)點z是葉結(jié)點,則直接刪除,不會破壞二叉排序樹的性質(zhì)。
-
若結(jié)點z只有一棵左子樹或右子樹,則讓z的子樹成為z父結(jié)點的子樹,替代z的位置。
-
若結(jié)點z有左、右兩棵子樹,則令z的直接后繼(或直接前驅(qū))替代z,然后從二叉排序樹中刪去這個直接后繼(或直接前驅(qū)),這樣就轉(zhuǎn)換成了第一或第二種情況。
-
z的后繼:z的右子樹中最左下結(jié)點,是右子樹最小的結(jié)點(該結(jié)點一定沒有左子樹)
-
z的前驅(qū):z的左子樹中最右下結(jié)點,是左子樹中最大的結(jié)點(該結(jié)點一定沒有右子樹)
-
-
查找效率分析:
-
查找長度:在查找運算中,需要對比關(guān)鍵字的次數(shù)稱為查找長度,反映了查找操作時間復(fù)雜度
-
查找成功的平均查找長度ASL(Average Search Length):(各層(層數(shù)*層結(jié)點個數(shù))相加)/總結(jié)點個數(shù)
-
最壞情況:每個結(jié)點只有一個分支,樹高h(yuǎn)=結(jié)點數(shù)n。平均查找長度=O(n)
-
最好情況:n個結(jié)點的二叉樹最小高度為(log2n)+ 1。平均查找長度= O(log2n)
平衡二叉樹(AVL)
定義:
-
平衡二叉樹(Balanced Binary Tree),簡稱平衡樹(AVL樹)——樹上任一結(jié)點的左子樹和右子樹的高度之差不超過1
-
結(jié)點的平衡因子=左子樹高-右子樹高
-
平衡二叉樹結(jié)點的平衡因子的值只可能是?1、0或1
-
只要有任一結(jié)點的平衡因子絕對值大于1,就不是平衡二叉樹
平衡二叉樹的插入:
-
在二叉排序樹中插入新結(jié)點后,如何保持平衡?
-
查找路徑上的所有結(jié)點都有可能受到影響
-
從插入點往回找到第一個不平衡結(jié)點,調(diào)整以該結(jié)點為根的子樹
-
每次調(diào)整的對象都是“最小不平衡子樹”
-
在插入操作中,只要將最小不平衡子樹調(diào)整平衡,則其他祖先結(jié)點都會恢復(fù)平衡
-
插入操作導(dǎo)致“最小不平衡子樹”高度+1,經(jīng)過調(diào)整后高度恢復(fù)
-
-
調(diào)整最小不平衡子樹
調(diào)整最小不平衡子樹:
-
只有假設(shè)所有子樹的高度都是H才能保證出現(xiàn)最小不平衡子樹恒定
-
目標(biāo):
-
恢復(fù)平衡
-
保持二叉排序樹特性
-
-
二叉排序樹的特性:左子樹結(jié)點值< 根結(jié)點值< 右子樹結(jié)點值
-
LL平衡旋轉(zhuǎn)(右單旋轉(zhuǎn))
-
由于在結(jié)點A的左孩子(L)的左子樹(L)上插入了新結(jié)點,A的平衡因子由1增至2,導(dǎo)致以A為根的子樹失去平衡,需要一次向右的旋轉(zhuǎn)操作
-
將A的左孩子B向右上旋轉(zhuǎn)代替A成為根結(jié)點,將A結(jié)點向右下旋轉(zhuǎn)成為B的右子樹的根結(jié)點,而B的原右子樹則作為A結(jié)點的左子樹
-
-
RR平衡旋轉(zhuǎn)(左單旋轉(zhuǎn))
-
由于在結(jié)點A的右孩子(R)的右子樹(R)上插入了新結(jié)點,A的平衡因子由-1減至-2,導(dǎo)致以A為根的子樹失去平衡,需要一次向左的旋轉(zhuǎn)操作
-
將A的右孩子B向左上旋轉(zhuǎn)代替A成為根結(jié)點,將A結(jié)點向左下旋轉(zhuǎn)成為B的左子樹的根結(jié)點,而B的原左子樹則作為A結(jié)點的右子樹
-
-
右旋和左旋的代碼實現(xiàn)思路
-
LR平衡旋轉(zhuǎn)(先左后右雙旋轉(zhuǎn))
-
由于在A的左孩子(L)的右子樹(R)上插入新結(jié)點,A的平衡因子由1增至2,導(dǎo)致以A為根的子樹失去平衡,需要進(jìn)行兩次旋轉(zhuǎn)操作,先左旋轉(zhuǎn)后右旋轉(zhuǎn)
-
先將A結(jié)點的左孩子B的右子樹的根結(jié)點C向左上旋轉(zhuǎn)提升到B結(jié)點的位置,然后再把該C結(jié)點向右上旋轉(zhuǎn)提升到A結(jié)點的位置
-
-
RL平衡旋轉(zhuǎn)(先右后左雙旋轉(zhuǎn))
-
由于在A的右孩子(R)的左子樹(L)上插入新結(jié)點,A的平衡因子由-1減至-2,導(dǎo)致以A為根的子樹失去平衡,需要進(jìn)行兩次旋轉(zhuǎn)操作,先右旋轉(zhuǎn)后左旋轉(zhuǎn)
-
先將A結(jié)點的右孩子B的左子樹的根結(jié)點C向右上旋轉(zhuǎn)提升到B結(jié)點的位置,然后再把該C結(jié)點向左上旋轉(zhuǎn)提升到A結(jié)點的位置
-
查找效率分析:
-
若樹高為h,則最壞情況下,查找一個關(guān)鍵字最多需要對比h次,即查找操作的時間復(fù)雜度不可能超過O(h)
-
平衡二叉樹——樹上任一結(jié)點的左子樹和右子樹的高度之差不超過1
-
假設(shè)以nh表示深度為h的平衡樹中含有的最少結(jié)點數(shù)。
-
則有n0 = 0, n1 = 1, n2 = 2,并且有nh = n(h?1) + n(h?2) + 1
-
可以證明含有n個結(jié)點的平衡二叉樹的最大深度為O(log2n) ,平衡二叉樹的平均查找長度為O(log2n)
哈夫曼樹
帶權(quán)路徑長度:
-
結(jié)點的權(quán):有某種現(xiàn)實含義的數(shù)值(如:表示結(jié)點的重要性等)
-
結(jié)點的帶權(quán)路徑長度:從樹的根到該結(jié)點的路徑長度(經(jīng)過的邊數(shù))與該結(jié)點上權(quán)值的乘積
-
樹的帶權(quán)路徑長度:樹中所有葉結(jié)點的帶權(quán)路徑長度之和(WPL, Weighted Path Length)
哈夫曼樹的定義:
在含有n個帶權(quán)葉結(jié)點的二叉樹中,其中帶權(quán)路徑長度(WPL)最小的二叉樹稱為哈夫曼樹,也稱最優(yōu)二叉樹
哈夫曼樹的構(gòu)造:
-
給定n個權(quán)值分別為w1, w2,…, wn的結(jié)點,構(gòu)造哈夫曼樹的算法描述如下:
-
將這n個結(jié)點分別作為n棵僅含一個結(jié)點的二叉樹,構(gòu)成森林F。
-
構(gòu)造一個新結(jié)點,從F中選取兩棵根結(jié)點權(quán)值最小的樹作為新結(jié)點的左、右子樹,并且將新結(jié)點的權(quán)值置為左、右子樹上根結(jié)點的權(quán)值之和。
-
從F中刪除剛才選出的兩棵樹,同時將新得到的樹加入F中。
-
重復(fù)步驟2和3,直至F中只剩下一棵樹為止。
-
-
每個初始結(jié)點最終都成為葉結(jié)點,且權(quán)值越小的結(jié)點到根結(jié)點的路徑長度越大
-
哈夫曼樹的結(jié)點總數(shù)為2n ? 1
-
哈夫曼樹中不存在度為1的結(jié)點。
-
哈夫曼樹并不唯一,但WPL必然相同且為最優(yōu)
哈夫曼編碼:
-
固定長度編碼:每個字符用相等長度的二進(jìn)制位表示
-
可變長度編碼:允許對不同字符用不等長的二進(jìn)制位表示
-
若沒有一個編碼是另一個編碼的前綴,則稱這樣的編碼為前綴編碼,如果沒有前綴編碼容易出現(xiàn)歧義
-
由哈夫曼樹得到哈夫曼編碼:字符集中的每個字符作為一個葉子結(jié)點,各個字符出現(xiàn)的頻度作為結(jié)點的權(quán)值,根據(jù)之前介紹的方法構(gòu)造哈夫曼樹
并查集
定義:
并查集(Disjoint Set)是邏輯結(jié)構(gòu)集合的一種具體實現(xiàn),只進(jìn)行“并”和“查”兩種基本操作
并查集的基本操作:
-
Find:查操作,確定一個指定元素所屬集合
-
Union:并操作,將兩個不相交的集合合并為一個
初始化:
S[]實際上就是樹的雙親表示法,里面的值就是自己對應(yīng)根結(jié)點的下標(biāo)
并、查:
時間復(fù)雜度分析:
-
Find操作最壞時間復(fù)雜度O(n)
-
Union操作時間復(fù)雜度O(1)
Union操作的優(yōu)化:
-
在每次Union操作構(gòu)建樹的時候,盡量讓樹不長高
-
用根結(jié)點的絕對值表示樹的結(jié)點總數(shù)(根結(jié)點從-1改成-(樹的總結(jié)點))
-
Union操縱,讓小樹合并到大樹
-
該方法構(gòu)造的樹高不超過[log2n]+1
-
Find最壞時間復(fù)雜度變?yōu)镺(log2n)
并查集的壓縮路徑
-
核心想法就是讓樹越來越“矮”
-
Find操作先找到根結(jié)點,再將查找路徑上所有結(jié)點都掛到根結(jié)點下
-
這樣的操縱可以讓樹的告訴不超過O(α(n))
-
O(α(n))是一個增長很緩慢的函數(shù),對于常見的n值,O(α(n))通常<=4
-
因此優(yōu)化后的并查集Find、Union操作時間開銷都很低
圖
圖的基本概念
定義:
-
圖G由頂點集V和邊集E組成,記為G = (V, E),其中V(G)表示圖G中頂點的有限非空集;
-
E(G)表示圖G中頂點之間的關(guān)系(邊)集合。
-
若V = {v1, v2, … , vn},則用|V|表示圖G中頂點的個數(shù),也稱圖G的階,E = {(u, v) | u∈V, v∈V},用|E|表示圖G中邊的條數(shù)。
-
注意:線性表可以是空表,樹可以是空樹,但圖不可以是空,即V一定是非空集
無向圖、有向圖:
無向圖:
-
若E是無向邊(簡稱$)的有限集合時,則圖G為無向圖。
-
邊是頂點的無序?qū)Γ洖?v, w)或(w, v),因為(v, w) = (w, v),其中v、w是頂點??梢哉f頂點w和頂點v互為鄰接點。
-
邊(v, w)依附于頂點w和v,或者說邊(v, w)和頂點v、w相關(guān)聯(lián)。
有向圖:
-
若E是有向邊(也稱!)的有限集合時,則圖G為有向圖。
-
弧是頂點的有序?qū)?,記?lt;v, w>,其中v、w是頂點,v稱為弧尾,w稱為弧頭,<v, w>稱為從頂點v到頂點w的弧,也稱v鄰接到w,或w鄰接自v。
-
注意:<v, w> ≠ <w, v>
簡單圖、多重圖:
數(shù)據(jù)結(jié)構(gòu)課程只探討“簡單圖”
簡單圖:
-
不存在重復(fù)邊
-
不存在頂點到自身的邊
多重圖:
-
圖G中某兩個結(jié)點之間的邊數(shù)多于一條又允許頂點通過同一條邊和自己關(guān)聯(lián),則G為多重圖
頂點的度、入度、出度:
-
對于無向圖:
-
頂點v的度是指依附于該頂點的邊的條數(shù),記為TD(v)。
-
在具有n個頂點、e條邊的無向圖中,無向圖的全部頂點的度的和等于邊數(shù)的2倍
-
-
對于有向圖:
-
入度是以頂點v為終點的有向邊的數(shù)目,記為ID(v);
-
出度是以頂點v為起點的有向邊的數(shù)目,記為OD(v)。
-
頂點v的度等于其入度和出度之和,即TD(v) = ID(v) + OD(v)。
-
頂點-頂點的關(guān)系描述:
-
路徑:頂點vp到頂點vq之間的一條路徑是指頂點序列,
-
回路:第一個頂點和最后一個頂點相同的路徑稱為回路或環(huán)
-
簡單路徑:在路徑序列中,頂點不重復(fù)出現(xiàn)的路徑稱為簡單路徑。
-
簡單回路:除第一個頂點和最后一個頂點外,其余頂點不重復(fù)出現(xiàn)的回路稱為簡單回路。
-
路徑長度:路徑上邊的數(shù)目
-
點到點的距離:從頂點u出發(fā)到頂點v的最短路徑若存在,則此路徑的長度稱為從u到v的距離。若從u到v根本不存在路徑,則記該距離為無窮(∞)。
-
無向圖中,若從頂點v到頂點w有路徑存在,則稱v和w是連通的
-
有向圖中,若從頂點v到頂點w和從頂點w到頂點v之間都有路徑,則稱這兩個頂點是強連通的
連通圖、強連通圖:
-
若圖G中任意兩個頂點都是連通的,則稱圖G為連通圖,否則稱為非連通圖。
-
對于n個頂點的無向圖G,
-
若G是連通圖,則最少有n-1條邊
-
若G是非連通圖,則最多可能有
條邊
-
-
若圖中任何一對頂點都是強連通的,則稱此圖為強連通圖。
-
對于n個頂點的有向圖G,
-
若G是強連通圖,則最少有n條邊(形成回路)
研究圖的局部——子圖:
-
設(shè)有兩個圖G = (V, E)和G¢ = (V', E'),若V¢是V的子集,且E'是E的子集,則稱G'是G的子圖
-
若有滿足V(G') = V(G)的子圖G',則稱其為G的生成子圖
-
注意:并非任意挑幾個點、幾條邊都能構(gòu)成子圖
連通分量:
-
無向圖中的極大聯(lián)通子圖稱為連通分量
-
子圖必須連通,且包含盡可能多的頂點和邊
強連通分量:
-
有向圖中的極大強聯(lián)通子圖成為有向圖的強連通分量
-
子圖必須強連通,同時保留盡可能多的邊
生成樹:
-
連通圖的生成樹是包含圖中全部頂點的一個極小連通子圖
-
邊盡可能的少,但要保持連通
-
若圖中頂點數(shù)為n,則它的生成樹含有n-1條邊。對生成樹而言,若砍去它的一條邊,則會變成非連通圖,若加上一條邊則會形成一個回路。
生成森林:
在非連通圖中,連通分量的生成樹構(gòu)成了非連通圖的生成森林。
邊的權(quán)、帶權(quán)圖/網(wǎng):
-
邊的權(quán):在一個圖中,每條邊都可以標(biāo)上具有某種含義的數(shù)值,該數(shù)值稱為該邊的權(quán)值。
-
帶權(quán)圖/網(wǎng)——邊上帶有權(quán)值的圖稱為帶權(quán)圖,也稱網(wǎng)。
-
帶權(quán)路徑長度——當(dāng)圖是帶權(quán)圖時,一條路徑上所有邊的權(quán)值之和,稱為該路徑的帶權(quán)路徑長度
幾種特殊形態(tài)的圖:
-
無向完全圖:無向圖中任意兩個頂點之間都存在邊
-
有向完全圖:有向圖中任意兩個頂點之間都存在方向相反的兩條弧
-
邊數(shù)很少的圖稱為稀疏圖
-
反之稱為稠密圖
-
沒有絕對的界限,一般來說|E| < |V|log|V|時,可以將G視為稀疏圖
-
樹:不存在回路,且連通的無向圖
-
有向樹:一個頂點的入度為0、其余頂點的入度均為1的有向圖,稱為有向樹
-
n個頂點的樹,必有n-1條邊。
-
n個頂點的圖,若|E|>n-1,則一定有回路
-
鄰接矩陣法
定義:
-
無向圖:
-
第i個結(jié)點的度=第i行(或第i列)的非零元素個數(shù)
-
-
有向圖:
-
第i個結(jié)點的出度=第i行的非零元素個數(shù)
-
第i個結(jié)點的入度=第i列的非零元素個數(shù)
-
第i個結(jié)點的度=第i行、第i列的非零元素個數(shù)之和
-
-
鄰接矩陣法求頂點的度/出度/入度的時間復(fù)雜度為O(|V|)
鄰接矩陣法存儲帶權(quán)圖(網(wǎng)):
鄰接矩陣法的性能分析:
-
空間復(fù)雜度:O(|V|^2) ——只和頂點數(shù)相關(guān),和實際的邊數(shù)無關(guān)
-
適合用于存儲稠密圖
-
無向圖的鄰接矩陣是對稱矩陣,可以壓縮存儲(只存儲上三角區(qū)/下三角區(qū))
鄰接矩陣法的性質(zhì):
鄰接表法
為什么要使用鄰接表:
-
鄰接矩陣是使用數(shù)組實現(xiàn)的順序存儲,空間復(fù)雜度高,不適合存儲稀疏圖
-
鄰接表是順序+鏈?zhǔn)酱鎯?/p>
定義:
-
其實這和樹的孩子表示法是類似的,孩子表示法:順序存儲各個結(jié)點,每個結(jié)點中保存孩子鏈表頭指針
-
邊結(jié)點的數(shù)量是2|E|,整體空間復(fù)雜度為O(|V| + 2|E|)
-
邊結(jié)點的數(shù)量是|E|,整體空間復(fù)雜度為O(|V| + |E|)
-
只要確定了頂點編號,圖的鄰接矩陣表示方式唯一,圖的鄰接表表示方式并不唯一
鄰接矩陣和鄰接表的對比:
十字鏈表、鄰接多重表
關(guān)系:
-
十字鏈表儲存有向圖
-
鄰接多重表儲存無向圖
十字鏈表存儲有向圖:
-
空間復(fù)雜度:O(|V|+|E|)
-
如何找到指定頂點的所有出邊?順著綠色線路找
-
如何找到指定頂點的所有入邊?順著橙色線路找
鄰接矩陣、鄰接表存儲無向圖:
-
鄰接表每條邊對應(yīng)兩份冗余信息,刪除頂點、刪除邊等操作時間復(fù)雜度高
-
臨接矩陣空間復(fù)雜度高O(|V|2)
臨接多重表儲存無向圖:
-
空間復(fù)雜度:O(|V|+|E|)
-
刪除邊、刪除結(jié)點等操作很方便,只需要讓各指針指向下一個對應(yīng)的指向即可
總結(jié):
圖的基本操作
-
Adjacent(G,x,y):判斷圖G是否存在邊<x, y>或(x, y)。
-
Neighbors(G,x):列出圖G中與結(jié)點x鄰接的邊。
-
InsertVertex(G,x):在圖G中插入頂點x。
-
DeleteVertex(G,x):從圖G中刪除頂點x。
-
AddEdge(G,x,y):若無向邊(x, y)或有向邊<x, y>不存在,則向圖G中添加該邊。
-
RemoveEdge(G,x,y):若無向邊(x, y)或有向邊<x, y>存在,則從圖G中刪除該邊。
-
FirstNeighbor(G,x):求圖G中頂點x的第一個鄰接點,若有則返回頂點號。若x沒有鄰接點或圖中不存在x,則返回-1。
-
NextNeighbor(G,x,y):假設(shè)圖G中頂點y是頂點x的一個鄰接點,返回除y之外頂點x的下一個鄰接點的頂點號,若y是x的最后一個鄰接點,則返回-1。
-
Get_edge_value(G,x,y):獲取圖G中邊(x, y)或<x, y>對應(yīng)的權(quán)值。
-
Set_edge_value(G,x,y,v):設(shè)置圖G中邊(x, y)或<x, y>對應(yīng)的權(quán)值為v。
-
注意:上面的操作都只針對鄰接矩陣和鄰接表
圖的廣度優(yōu)先遍歷(BFS)
樹和圖的廣度優(yōu)先遍歷:
-
樹的廣度優(yōu)先遍歷:通過根結(jié)點,可以找到下一層的結(jié)點2,3,4.通過234又可以找到再下一層的結(jié)點5678
-
若樹非空,則根結(jié)點入隊
-
若隊列非空,隊頭元素出隊并訪問,同時將該元素的孩字依次入隊
-
重復(fù)第二步直到隊列為空
-
-
圖的廣度優(yōu)先遍歷類似于樹的廣度優(yōu)先遍歷(層序遍歷)
-
區(qū)別:
-
樹不存在“回路”,搜索相鄰的結(jié)點時,不可能搜到已經(jīng)訪問過的結(jié)點
-
圖搜索相鄰的頂點時,有可能搜到已經(jīng)訪問過的頂點
-
代碼實現(xiàn):
-
找到與一個頂點相鄰的所有頂點
-
FirstNeighbor(G,x):求圖G中頂點x的第一個鄰接點,若有則返回頂點號。若x沒有鄰接點或圖中不存在x,則返回-1。
-
NextNeighbor(G,x,y):假設(shè)圖G中頂點y是頂點x的一個鄰接點,返回除y之外頂點x的下一個鄰接點的頂點號,若y是x的最后一個鄰接點,則返回-1。
-
使用上面兩個基本操作
-
-
標(biāo)記哪些頂點被訪問過
-
-
都初始化為false
-
-
需要一個輔助隊列
遍歷序列的可變性:
-
同一個圖的鄰接矩陣表示方式唯一,因此廣度優(yōu)先遍歷序列唯一
-
同一個圖鄰接表表示方式不唯一,因此廣度優(yōu)先遍歷序列不唯一
算法存在的問題和解決方案:
如果是非連通圖,則無法遍歷完所有結(jié)點
-
空間復(fù)雜度:最壞情況,輔助隊列大小為 O(|V|)
-
鄰接矩陣存儲的圖:
-
訪問 |V| 個頂點需要O(|V|)的時間
-
查找每個頂點的鄰接點都需要O(|V|)的時間,而總共有|V|個頂點
-
時間復(fù)雜度= O(|V|^2)
-
-
鄰接表存儲的圖:
-
訪問 |V| 個頂點需要O(|V|)的時間
-
查找各個頂點的鄰接點共需要O(|E|)的時間
-
時間復(fù)雜度= O(|V|+|E|)
-
廣度優(yōu)先生成樹:
-
廣度優(yōu)先生成樹由廣度優(yōu)先遍歷過程確定。
-
由于鄰接表的表示方式不唯一,因此基于鄰接表的廣度優(yōu)先生成樹也不唯一
-
對非連通圖的廣度優(yōu)先遍歷,可得到廣度優(yōu)先生成森林
圖的深度優(yōu)先遍歷(DFS)
樹和圖的深度優(yōu)先遍歷:
-
樹的深度優(yōu)先遍歷(先根、后根):
-
從根結(jié)點出發(fā),能往更深處走就盡量往深處走。
-
每當(dāng)訪問一個結(jié)點的時候,要檢查是否還有與當(dāng)前結(jié)點相鄰的且沒有被訪問過的結(jié)點,如果有的話就往下一層鉆。
-
-
圖的深度優(yōu)先遍歷類似于樹的先根遍歷。
-
圖的深度優(yōu)先遍歷是遞歸實現(xiàn)的,廣度優(yōu)先辦理是隊列實現(xiàn)的
圖的深度優(yōu)先遍歷:
算法存在的問題和解決方案:
如果是非連通圖,則無法遍歷完所有結(jié)點
復(fù)雜度分析:
-
空間復(fù)雜度:
-
來自函數(shù)調(diào)用棧,最壞情況,遞歸深度為O(|V|)
-
最好情況,O(1)
-
-
時間復(fù)雜度:
-
時間復(fù)雜度=訪問各結(jié)點所需時間+探索各條邊所需時間
-
鄰接矩陣存儲的圖:
-
訪問 |V| 個頂點需要O(|V|)的時間
-
查找每個頂點的鄰接點都需要O(|V|)的時間,而總共有|V|個頂點
-
時間復(fù)雜度= O(|V|^2)
-
-
鄰接表存儲的圖:
-
訪問 |V| 個頂點需要O(|V|)的時間
-
查找各個頂點的鄰接點共需要O(|E|)的時間
-
時間復(fù)雜度= O(|V|+|E|)
-
-
深度優(yōu)先序列:
-
和圖的鄰接表是一個原理,樹中各個孩子結(jié)點在鄰接表中出現(xiàn)的順序是可變的
-
因此如果采用這種數(shù)據(jù)結(jié)構(gòu)存儲樹,那么可能會有不同的遍歷序列
深度優(yōu)先生成樹:
-
同一個圖的鄰接矩陣表示方式唯一,因此深度優(yōu)先遍歷序列唯一,深度優(yōu)先生成樹也唯一
-
同一個圖鄰接表表示方式不唯一,因此深度優(yōu)先遍歷序列不唯一,深度優(yōu)先生成樹也不唯一
-
對無向圖進(jìn)行BFS/DFS遍歷
圖的遍歷與圖的連通性:
-
調(diào)用BFS/DFS函數(shù)的次數(shù)=連通分量數(shù)
-
對于連通圖,只需調(diào)用1次 BFS/DFS
-
對有向圖進(jìn)行BFS/DFS遍歷
-
調(diào)用BFS/DFS函數(shù)的次數(shù)要具體問題具體分析,若起始頂點到其他各頂點都有路徑,則只需調(diào)用1次BFS/DFS 函數(shù)
-
對于強連通圖,從任一結(jié)點出發(fā)都只需調(diào)用1次 BFS/DFS
最小生成樹
生成樹:
-
連通圖的生成樹是包含圖中全部頂點的一個極小連通子圖。
-
若圖中頂點數(shù)為n,則它的生成樹含有n-1條邊。對生成樹而言,若砍去它的一條邊,則會變成非連通圖,若加上一條邊則會形成一個回路
最小生成樹(最小代價樹):
-
對于一個帶權(quán)連通無向圖G = (V, E),生成樹不同,每棵樹的權(quán)(即樹中所有邊上的權(quán)值之和)也可能不同
-
設(shè)R為G的所有生成樹的集合,若T為R中邊的權(quán)值之和最小的生成樹,則T稱為G的最小生成樹(Minimum-Spanning-Tree, MST)
-
最小生成樹可能有多個,但邊的權(quán)值之和總是唯一且最小的
-
最小生成樹的邊數(shù) = 頂點數(shù) - 1
-
砍掉一條則不連通,增加一條邊則會出現(xiàn)回路
-
如果一個連通圖本身就是一棵樹,則其最小生成樹就是它本身
-
只有連通圖才有生成樹,非連通圖只有生成森林
Prim 算法(普里姆):
算法思想:
-
從某一個頂點開始構(gòu)建生成樹;
-
每次將代價最小的新頂點納入生成樹,直到所有頂點都納入為止。
-
時間復(fù)雜度:O(|V|^2),適合用于邊稠密圖
數(shù)據(jù)結(jié)構(gòu):
實現(xiàn)步驟:
-
從V0開始,總共需要 n-1 輪處理
-
循環(huán)遍歷所有個結(jié)點,找到lowCost最低的,且還沒加入樹的頂點,isJoin對應(yīng)結(jié)點標(biāo)記為true
-
再次循環(huán)遍歷,更新還沒加入的各個頂點的lowCost值
-
重復(fù)上面步驟,直到所有結(jié)點都加入樹,生成的樹即為最小生成樹
Kruskal 算法(克魯斯卡爾)
算法思想:
-
每次選擇一條權(quán)值最小的邊,使這條邊的兩頭連通(原本已經(jīng)連通的就不選)
-
直到所有結(jié)點都連通
-
時間復(fù)雜度:O( |E|log2|E| ),適合用于邊稀疏圖
數(shù)據(jù)結(jié)構(gòu):
讓各條邊按照權(quán)值順序排序
實現(xiàn)步驟:
-
共執(zhí)行 e 輪,每輪判斷兩個頂點是否屬于同一集合
-
檢查第e條邊的兩個頂點是否連通(是否屬于同一個集合)
-
若不聯(lián)通則連起來
-
若聯(lián)通則不操作
-
重復(fù)上面的步驟直到所有邊都被遍歷過
最短路徑問題之BFS算法
介紹:
-
無權(quán)圖可以視為一種特殊的帶權(quán)圖,只是每條邊的權(quán)值都為1
-
BFS一般只適用于無權(quán)圖的最短路徑問題
代碼實現(xiàn):
-
d數(shù)組表示u到各個結(jié)點的最短路徑
-
path數(shù)組表示該結(jié)點回到u結(jié)點的最短前驅(qū)結(jié)點
-
由此生成的生成樹同時也反應(yīng)了起點到任意結(jié)點的距離
最短路徑問題之Dijkstra算法
BFS算法的局限性:
-
帶權(quán)路徑長度——當(dāng)圖是帶權(quán)圖時,一條路徑上所有邊的權(quán)值之和,稱為該路徑的帶權(quán)路徑長度
-
BFS算法求單源最短路徑只適用于無權(quán)圖,或所有邊的權(quán)值都相同的圖
算法思想:
-
初始:從V0開始,初始化三個數(shù)組信息
-
循環(huán)遍歷所有結(jié)點,找到還沒確定最短路徑,且dist 最小的頂點Vi,令final[i]=ture。
-
檢查所有鄰接自 Vi 的頂點,若其 final 值為false,則更新 dist 和 path 信息
-
重復(fù)上述步驟,知道所有結(jié)點的final都標(biāo)記為true
代碼實現(xiàn)思路:
-
初始:
-
若從V0開始,令 final[0]=ture; dist[0]=0; path[0]=-1。
-
-
n-1輪處理
-
循環(huán)遍歷所有頂點,找到還沒確定最短路徑,且dist 最小的頂點Vi,令final[i]=ture。
-
并檢查所有鄰接自Vi 的頂點,對于鄰接自Vi 的頂點 Vj ,若 final[j]==false 且 dist[i]+arcsi < dist[j],則令 dist[j]=dist[i]+arcsi; path[j]=i。
-
注:arcsi表示Vi 到Vj 的弧的權(quán)值
-
-
時間復(fù)雜度:O(n2)即O(|V|2)
用于負(fù)權(quán)值帶權(quán)圖:
Dijkstra 算法不適用于有負(fù)權(quán)值的帶權(quán)圖
最短路徑問題之Floyd算法
算法思想:
-
Floyd算法:求出每一對頂點之間的最短路徑
-
使用動態(tài)規(guī)劃思想,將問題的求解分為多個階段
-
對于n個頂點的圖G,求任意一對頂點 Vi —> Vj 之間的最短路徑可分為如下幾個階段:
-
初始:不允許在其他頂點中轉(zhuǎn),最短路徑是?
-
0:若允許在 V0 中轉(zhuǎn),最短路徑是?
-
1:若允許在 V0、V1 中轉(zhuǎn),最短路徑是?
-
2:若允許在 V0、V1、V2 中轉(zhuǎn),最短路徑是?
-
…
-
n-1:若允許在 V0、V1、V2 …… Vn-1 中轉(zhuǎn),最短路徑是?
-
實現(xiàn)步驟:
代碼實現(xiàn):
注意點:
-
Floyd算法可以用于負(fù)權(quán)圖
-
Floyd 算法不能解決帶有“負(fù)權(quán)回路”的圖(有負(fù)權(quán)值的邊組成回路),這種圖有可能沒有最短路徑(每走一圈路徑越小)
總結(jié):
有向無環(huán)圖(DAG圖)的描述表達(dá)式
定義:
若一個有向圖中不存在環(huán),則稱為有向無環(huán)圖,簡稱DAG圖(Directed Acyclic Graph)
例子:
轉(zhuǎn)換前:
轉(zhuǎn)換后:
解題方法:
-
把各個操作數(shù)不重復(fù)地排成一排
-
標(biāo)出各個運算符的生效順序(先后順序有點出入無所謂)
-
按順序加入運算符,注意“分層”
-
從底向上逐層檢查同層的運算符是否可以合體
拓?fù)渑判?/h4>
AOV網(wǎng):
-
AOV網(wǎng)(Activity On Vertex NetWork,用頂點表示活動的網(wǎng))
-
用DAG圖(有向無環(huán)圖)表示一個工程。頂點表示活動,有向邊<Vi, Vj>表示活動Vi必須先于活動Vj進(jìn)行
拓?fù)渑判颍?/strong>
-
拓?fù)渑判颍涸趫D論中,由一個有向無環(huán)圖的頂點組成的序列,當(dāng)且僅當(dāng)滿足下列條件時,稱為該圖的一個拓?fù)渑判颍?/p>
-
每個頂點出現(xiàn)且只出現(xiàn)一次。
-
若頂點A在序列中排在頂點B的前面,則在圖中不存在從頂點B到頂點A的路徑。
-
-
或定義為:
-
拓?fù)渑判蚴菍τ邢驘o環(huán)圖的頂點的一種排序,它使得若存在一條從頂點A到頂點B的路徑,則在排序中頂點B出現(xiàn)在頂點A的后面。
-
-
每個AOV網(wǎng)都有一個或多個拓?fù)渑判蛐蛄小?/p>
-
簡單來說拓?fù)渑判蚓褪钦业阶鍪碌南群箜樞?/p>
-
拓?fù)渑判虻膶崿F(xiàn):
-
從AOV網(wǎng)中選擇一個沒有前驅(qū)(入度為0)的頂點并輸出。
-
從網(wǎng)中刪除該頂點和所有以它為起點的有向邊。
-
重復(fù)前面的步驟直到當(dāng)前的AOV網(wǎng)為空或當(dāng)前網(wǎng)中不存在無前驅(qū)的頂點為止。
-
代碼實現(xiàn):
-
時間復(fù)雜度:O(|V|+|E|)
-
若采用鄰接矩陣,則需O(|V|^2)
逆拓?fù)渑判颍?/strong>
-
對一個AOV網(wǎng),如果采用下列步驟進(jìn)行排序,則稱之為逆拓?fù)渑判颍?/p>
-
從AOV網(wǎng)中選擇一個沒有后繼(出度為0)的頂點并輸出。
-
從網(wǎng)中刪除該頂點和所有以它為終點的有向邊。
-
重復(fù)上面步驟直到當(dāng)前的AOV網(wǎng)為空。
-
-
用鄰接表實現(xiàn)會更簡單一些
-
使用逆鄰接表:鄰接表的頂點對應(yīng)儲存的信息是指向該頂點的邊的信息
-
使用深度優(yōu)先算法實現(xiàn)逆拓?fù)渑判?,頂點輸出的序列就是逆拓?fù)渑判蛐蛄?/p>
-
DFS實現(xiàn)逆拓?fù)渑判颍涸陧旤c退棧前輸出
關(guān)鍵路徑
AOE網(wǎng):
-
在帶權(quán)有向圖中,以頂點表示事件,以有向邊表示活動,以邊上的權(quán)值表示完成該活動的開銷(如完成活動所需的時間)
-
稱之為用邊表示活動的網(wǎng)絡(luò),簡稱AOE網(wǎng) (Activity On Edge NetWork)
-
AOE網(wǎng)具有以下兩個性質(zhì):
-
只有在某頂點所代表的事件發(fā)生后,從該頂點出發(fā)的各有向邊所代表的活動才能開始;
-
只有在進(jìn)入某頂點的各有向邊所代表的活動都已結(jié)束時,該頂點所代表的事件才能發(fā)生。
-
另外,有些活動是可以并行進(jìn)行的
-
關(guān)鍵路徑:
-
從源點到匯點的有向路徑可能有多條,所有路徑中,具有最大路徑長度的路徑稱為關(guān)鍵路徑,而把關(guān)鍵路徑上的活動稱為關(guān)鍵活動
-
完成整個工程的最短時間就是關(guān)鍵路徑的長度,若關(guān)鍵活動不能按時完成,則整個工程的完成時間就會延長
-
事件vk的最早發(fā)生時間ve(k):決定了所有從vk開始的活動能夠開工的最早時間
-
活動ai的最早開始時間e(i):指該活動弧的起點所表示的事件的最早發(fā)生時間
-
事件vk的最遲發(fā)生時間vl(k):它是指在不推遲整個工程完成的前提下,該事件最遲必須發(fā)生的時間。
-
活動ai的最遲開始時間l(i):它是指該活動弧的終點所表示事件的最遲發(fā)生時間與該活動所需時間之差。
-
活動ai的時間余量d(i)=l(i)-e(i),表示在不增加完成整個工程所需總時間的情況下,活動ai可以拖延的時間
-
若一個活動的時間余量為零,則說明該活動必須要如期完成,d(i)=0即l(i) = e(i)的活動ai是關(guān)鍵活動,
-
由關(guān)鍵活動組成的路徑就是關(guān)鍵路徑
求關(guān)鍵路徑的步驟:
-
求所有事件的最早發(fā)生時間 ve( )
-
按拓?fù)渑判蛐蛄?,依次求各個頂點的 ve(k):
-
ve(源點) = 0
-
ve(k) = Max{ve(j) + Weight(vj, vk)}, vj為vk 的任意前驅(qū)
-
-
求所有事件的最遲發(fā)生時間 vl( )
-
按逆拓?fù)渑判蛐蛄校来吻蟾鱾€頂點的 vl(k):
-
vl(匯點) = ve(匯點)
-
vl(k) = Min{vl(j) - Weight(vk, vj)} , vj為vk的任意后繼
-
-
求所有活動的最早發(fā)生時間 e( )
-
若邊<vk, vj>表示活動ai,則有e(i) = ve(k)
-
-
求所有活動的最遲發(fā)生時間 l( )
-
若邊<vk, vj>表示活動ai,則有l(wèi)(i) = vl(j) - Weight(vk, vj)
-
-
求所有活動的時間余量 d( )
-
d(i) = l(i) - e(i)
-
-
d(i)=0的活動就是關(guān)鍵活動, 由關(guān)鍵活動可得關(guān)鍵路徑
關(guān)鍵活動、關(guān)鍵路徑的特性:
-
若關(guān)鍵活動耗時增加,則整個工程的工期將增長
-
縮短關(guān)鍵活動的時間,可以縮短整個工程的工期
-
當(dāng)縮短到一定程度時,關(guān)鍵活動可能會變成非關(guān)鍵活動
-
可能有多條關(guān)鍵路徑,只提高一條關(guān)鍵路徑上的關(guān)鍵活動速度并不能縮短整個工程的工期,只有加快那些包括在所有關(guān)鍵路徑上的關(guān)鍵活動才能達(dá)到縮短工期的目的。
查找
查找的基本概念
-
查找: 在數(shù)據(jù)集合中尋找滿足某種條件的數(shù)據(jù)元素的過程稱為查找
-
查找表(查找結(jié)構(gòu)):用于查找的數(shù)據(jù)集合稱為查找表,它由同一類型的數(shù)據(jù)元素(或記錄)組成
-
關(guān)鍵字: 數(shù)據(jù)元素中唯一標(biāo)識該元素的某個數(shù)據(jù)項的值,使用基于關(guān)鍵字的查找,查找結(jié)果應(yīng)該是唯一的。
對查找表的常見操作:
-
查找符合條件的數(shù)據(jù)元素
-
靜態(tài)查找表
-
僅關(guān)注查找速度即可
-
-
插入、刪除某個數(shù)據(jù)元素
-
動態(tài)查找表
-
除了查找速度,也要關(guān)注插/刪操作是否方便實現(xiàn)
-
查找算法的評價指標(biāo):
-
查找長度:在查找運算中,需要對比關(guān)鍵字的次數(shù)稱為查找長度
-
平均查找長度(ASL, Average Search Length ):所有查找過程中進(jìn)行關(guān)鍵字的比較次數(shù)的平均值
-
ASL的數(shù)量級反應(yīng)了查找算法的時間復(fù)雜度
-
評價一個查找算法的效率時,通??紤]查找成功、查找失敗兩種情況的ASL
順序查找
定義:
順序查找,又叫“線性查找”,通常用于線性表。
算法思想:從頭到尾依次往后找
順序查找的實現(xiàn):
順序查找的實現(xiàn)(哨兵):
優(yōu)點:無需判斷是否越界,效率更高
順序查找的優(yōu)化(對有序表):
用查找判定樹分析ASL:
-
一個成功結(jié)點的查找長度 = 自身所在層數(shù)
-
一個失敗結(jié)點的查找長度 = 其父結(jié)點所在層數(shù)
-
默認(rèn)情況下,各種失敗情況或成功情況都等概率發(fā)生
順序查找的優(yōu)化(被查概率不相等):
折半查找
算法思想:
折半查找,又稱“二分查找”,僅適用于有序的順序表。
查找成功:
注意:下面的mid向下取整
查找失?。?/p>
代碼實現(xiàn):
查找效率分析:
折半查找判定樹的構(gòu)造:
-
如果當(dāng)前l(fā)ow和high之間有偶數(shù)個元素,則 mid 分隔后,左半部分比右半部分少一個元素
-
如果當(dāng)前l(fā)ow和high之間有奇數(shù)個元素,則 mid 分隔后,左右兩部分元素個數(shù)相等
-
折半查找的判定樹中,mid = [(low + high)/2]則對于任何一個結(jié)點,必有:右樹結(jié)點數(shù)-左子樹結(jié)點數(shù)=0或1
-
折半查找的判定樹一定是平衡二叉樹
-
折半查找的判定樹中,只有最下面一層是不滿的因此,元素個數(shù)為n時樹高h(yuǎn) = [log2(n + 1)]
-
判定樹結(jié)點關(guān)鍵字:左<中<右,滿足二叉排序樹的定義
-
失敗結(jié)點:n+1個(等于成功結(jié)點的空鏈域數(shù)量)
-
折半查找的時間復(fù)雜度 = O(log2n)
分塊查找
算法思想:
-
特點:塊內(nèi)無序、塊間有序
-
分塊查找,又稱索引順序查找,算法過程如下:
-
在索引表中確定待查記錄所屬的分塊(可順序、可折半)
-
在塊內(nèi)順序查找
-
用折半查找查索引:
若查找目標(biāo)與索引表值相同:
若查找目標(biāo)與索引值不同:
-
若索引表中不包含目標(biāo)關(guān)鍵字,則折半查找索引表最終停在 low>high,要在low所指分塊中查找
-
原因:最終low左邊一定小于目標(biāo)關(guān)鍵字,high右邊一定大于目標(biāo)關(guān)鍵字。而分塊存儲的索引表中保存的是各個分塊的最大關(guān)鍵字
查找失敗的例子:
查找效率分析:
B樹
m叉查找樹:
-
實際上就是二叉查找樹的拓展,結(jié)點多少有多少個分叉就是多少叉查找樹
-
每個結(jié)點關(guān)鍵字的查找可以用順序查找也可以用折半查找
如何保證查找效率:
-
若每個結(jié)點內(nèi)關(guān)鍵字太少,導(dǎo)致樹變高,要查更多層結(jié)點,效率低
-
策略:m叉查找樹中,規(guī)定除了根結(jié)點外,任何結(jié)點至少有?m/2?個分叉,即至少含有?m/2? ? 1 個關(guān)鍵字
-
為什么不能保證根結(jié)點至少有?m/2?個分叉:如果整個樹只有1個元素,根結(jié)點只能有兩個分叉
-
-
不夠“平衡”,樹會很高,要查很多層結(jié)點
-
策略:m叉查找樹中,規(guī)定對于任何一個結(jié)點,其所有子樹的高度都要相同。
-
B樹的定義:
-
B樹,又稱多路平衡查找樹,B樹中所有結(jié)點的孩子個數(shù)的最大值稱為B樹的階,通常用m表示。一棵m階B樹或為空樹,或為滿足如下特性的m叉樹:
-
樹中每個結(jié)點至多有m棵子樹,即至多含有m-1個關(guān)鍵字。
-
若根結(jié)點不是終端結(jié)點,則至少有兩棵子樹。
-
除根結(jié)點外的所有非葉結(jié)點至少有?m/2?棵子樹,即至少含有 ?m/2?-1個關(guān)鍵字。
-
所有非葉結(jié)點的結(jié)構(gòu)如下:
-
-
其中,Ki(i = 1, 2,…, n)為結(jié)點的關(guān)鍵字,且滿足K1 < K2 <…< Kn;Pi(i = 0, 1,…, n)為指向子樹根結(jié)點的指針,且指針Pi-1所指子樹中所有結(jié)點的關(guān)鍵字均小于Ki,Pi所指子樹中所有結(jié)點的關(guān)鍵字均大于Ki,n(?m/2?- 1≤n≤m - 1)為結(jié)點中關(guān)鍵字的個數(shù)。
-
-
所有的葉結(jié)點都出現(xiàn)在同一層次上,并且不帶信息(可以視為外部結(jié)點或類似于折半查找判定樹的查找失敗結(jié)點,實際上這些結(jié)點不存在,指向這些結(jié)點的指針為空)。
-
B樹的高度:
最大高度的另一種推導(dǎo)方法:
B樹的插入刪除
B樹的插入:
-
新元素一定是插入到最底層“終端結(jié)點”,用“查找”來確定插入位置
-
在插入key后,若導(dǎo)致原結(jié)點關(guān)鍵字?jǐn)?shù)超過上限,則從中間位置(?m/2?)將其中的關(guān)鍵字分為兩部分
-
左部分包含的關(guān)鍵字放在原結(jié)點中,右部分包含的關(guān)鍵字放到新結(jié)點中,中間位置(?m/2?)的結(jié)點插入原結(jié)點的父結(jié)點
-
若此時導(dǎo)致其父結(jié)點的關(guān)鍵字個數(shù)也超過了上限,則繼續(xù)進(jìn)行這種分裂操作,直至這個過程傳到根結(jié)點為止,進(jìn)而導(dǎo)致B樹高度增1。
B樹的刪除:
-
若被刪除關(guān)鍵字在終端結(jié)點,則直接刪除該關(guān)鍵字(要注意結(jié)點關(guān)鍵字個數(shù)是否低于下限 ?m/2? ? 1)
-
若被刪除關(guān)鍵字在非終端結(jié)點,則用直接前驅(qū)或直接后繼來替代被刪除的關(guān)鍵字
-
直接前驅(qū):當(dāng)前關(guān)鍵字左側(cè)指針?biāo)缸訕渲小白钣蚁隆钡脑?/p>
-
直接后繼:當(dāng)前關(guān)鍵字右側(cè)指針?biāo)缸訕渲小白钭笙隆钡脑?/p>
-
-
若被刪除關(guān)鍵字所在結(jié)點刪除前的關(guān)鍵字個數(shù)低于下限,且與此結(jié)點右(或左)兄弟結(jié)點的關(guān)鍵字個數(shù)還很寬裕,則需要調(diào)整該結(jié)點、右(或左)兄弟結(jié)點及其雙親結(jié)點(父子換位法)
-
當(dāng)右兄弟很寬裕時,用當(dāng)前結(jié)點的后繼(比當(dāng)前結(jié)點大一位)、后繼的后繼(比當(dāng)前結(jié)點大兩位)來填補空缺
-
當(dāng)左兄弟很寬裕時,用當(dāng)前結(jié)點的前驅(qū)(比當(dāng)前結(jié)點小一位)、前驅(qū)的前驅(qū)(比當(dāng)前結(jié)點小兩位)來填補空缺
-
-
若被刪除關(guān)鍵字所在結(jié)點刪除前的關(guān)鍵字個數(shù)低于下限,且此時與該結(jié)點相鄰的左、右兄弟結(jié)點的關(guān)鍵字個數(shù)均=?m/2? ? 1,則將關(guān)鍵字刪除后與左(或右)兄弟結(jié)點及雙親結(jié)點中的關(guān)鍵字進(jìn)行合并
B+樹
定義和性質(zhì):
一棵m階的B+樹需滿足下列條件:
-
每個分支結(jié)點最多有m棵子樹(孩子結(jié)點)。
-
非葉根結(jié)點至少有兩棵子樹,其他每個分支結(jié)點至少有?m/2?棵子樹。
-
可以理解為:要追求“絕對平衡”,即所有子樹高度要相同
-
-
結(jié)點的子樹個數(shù)與關(guān)鍵字個數(shù)相等。
-
所有葉結(jié)點包含全部關(guān)鍵字及指向相應(yīng)記錄的指針,葉結(jié)點中將關(guān)鍵字按大小順序排列,并且相鄰葉結(jié)點按大小順序相互鏈接起來。
-
所有分支結(jié)點中僅包含它的各個子結(jié)點中關(guān)鍵字的最大值及指向其子結(jié)點的指針。
B+樹的查找:
B+樹中,無論查找成功與否,最終一定都要走到最下面一層結(jié)點,因為只有葉子結(jié)點才有關(guān)鍵字對應(yīng)的記錄
B+樹和B樹區(qū)別:
-
典型應(yīng)用:關(guān)系型數(shù)據(jù)庫的“索引”(如MySQL)
-
在B+樹中,非葉結(jié)點不含有該關(guān)鍵字對應(yīng)記錄的存儲地址。
-
可以使一個磁盤塊可以包含更多個關(guān)鍵字,使得B+樹的階更大,樹高更矮,讀磁盤次數(shù)更少,查找更快
散列(哈希)查找
定義:
-
散列表(Hash Table),又稱哈希表
-
是一種數(shù)據(jù)結(jié)構(gòu),特點是:數(shù)據(jù)元素的關(guān)鍵字與其存儲地址直接相關(guān)
-
通過哈希函數(shù)建立關(guān)鍵字和存儲地址的聯(lián)系
-
若不同的關(guān)鍵字通過散列函數(shù)映射到同一個值,則稱它們?yōu)椤巴x詞”
-
通過散列函數(shù)確定的位置已經(jīng)存放了其他元素,則稱這種情況為“沖突”
處理沖突的方法——拉鏈法:
-
用拉鏈法(又稱鏈接法、鏈地址法)處理“沖突”:把所有“同義詞”存儲在一個鏈表中
-
最理想情況:散列查找時間復(fù)雜度可到達(dá)O(1)
-
“沖突”越多,查找效率越低
-
實際上就是順序表和鏈表的結(jié)合結(jié)合兩者優(yōu)點,比如順序表的隨機存取,然后用鏈表的解決沖突問題,又規(guī)避了順序表的一系列缺點
-
在插入新元素時,保持關(guān)鍵字有序,可微微提高查找效率
常見的散列函數(shù):
-
散列函數(shù)的設(shè)計目的:
-
讓不同關(guān)鍵字的沖突盡可能的少
-
散列查找是典型的“用空間換時間”的算法,只要散列函數(shù)設(shè)計的合理,則散列表越長,沖突的概率越低。
-
-
除留余數(shù)法:H(key) = key % p
-
散列表表長為m,取一個不大于m但最接近或等于m的質(zhì)數(shù)p
-
質(zhì)數(shù)又稱素數(shù)。指除了1和此整數(shù)自身外,不能被其他自然數(shù)整除的數(shù)
-
用質(zhì)數(shù)取模,分布更均勻,沖突更少。參見《數(shù)論》
-
注意:散列函數(shù)的設(shè)計要結(jié)合實際的關(guān)鍵字分布特點來考慮,不要教條化
-
-
直接定址法 : H(key) = key 或 H(key) = a*key + b
-
其中,a和b是常數(shù)。這種方法計算最簡單,且不會產(chǎn)生沖突。它適合關(guān)鍵字的分布基本連續(xù)的情況,若關(guān)鍵字分布不連續(xù),空位較多,則會造成存儲空間的浪費。
-
例如:存儲同一個班級的學(xué)生信息
-
-
數(shù)字分析法:選取數(shù)碼分布較為均勻的若干位作為散列地址
-
設(shè)關(guān)鍵字是r進(jìn)制數(shù)(如十進(jìn)制數(shù)),而r個數(shù)碼在各位上出現(xiàn)的頻率不一定相同,可能在某些位上分布均勻一些,每種數(shù)碼出現(xiàn)的機會均等;
-
而在某些位上分布不均勻,只有某幾種數(shù)碼經(jīng)常出現(xiàn),此時可選取數(shù)碼分布較為均勻的若干位作為散列地址。這種方法適合于已知的關(guān)鍵字集合,若更換了關(guān)鍵字,則需要重新構(gòu)造新的散列函數(shù)。
-
例如:以“手機號碼”作為關(guān)鍵字設(shè)計散列函數(shù)
-
-
平方取中法:取關(guān)鍵字的平方值的中間幾位作為散列地址。
-
具體取多少位要視實際情況而定。
-
這種方法得到的散列地址與關(guān)鍵字的每位都有關(guān)系,因此使得散列地址分布比較均勻,適用于關(guān)鍵字的每位取值都不夠均勻或均小于散列地址所需的位數(shù)。
-
例如:要存儲整個學(xué)校的學(xué)生信息,以“身份證號”作為關(guān)鍵字設(shè)計散列函數(shù)
-
處理沖突的方法——開放定址法:
-
所謂開放定址法,是指可存放新表項的空閑地址既向它的同義詞表項開放,又向它的非同義詞表項開放。其數(shù)學(xué)遞推公式為:
-
Hi = (H(key) + di) % m
-
i = 0, 1, 2,…, k(k≤m - 1),m表示散列表表長;di為增量序列;
-
i 可理解為“第i次發(fā)生沖突”
-
-
線性探測法
-
di = 0, 1, 2, 3, …, m-1;即發(fā)生沖突時,每次往后探測相鄰的下一個單元是否為空,為空則可以插入數(shù)值
-
查找也是類似,先通過公式得到Hi再依次往后查找,若遇到空的位置則說明查找失敗,所以越早遇到空位置,就可以越早確定查找失敗
-
刪除結(jié)點不能簡單地將被刪結(jié)點的空間置為空,否則將截斷在它之后填入散列表的同義詞結(jié)點的查找路徑,可以做一個“刪除標(biāo)記”,進(jìn)行邏輯刪除
-
線性探測法由于沖突后再探測一定是放在某個連續(xù)的位置,很容易造成同義詞、非同義詞的“聚集(堆積)”現(xiàn)象,嚴(yán)重影響查找效率
-
-
平方探測法
-
當(dāng)di = 0^2, 1^2, -1^2, 2^2, -2^2, …, k^2, -k^2時,稱為平方探測法,又稱二次探測法,其中k≤m/2
-
比起線性探測法更不易產(chǎn)生“聚集(堆積)”問題
-
注意:負(fù)數(shù)的模運算,(-3)%27 = 24,而不是3
-
模運算的規(guī)則:a MOD m == (a+km) MOD m , 其中,k為任意整數(shù)
-
散列表長度m必須是一個可以表示成4j + 3的素數(shù),才能探測到所有位置
-
-
偽隨機序列法
-
di 是一個偽隨機序列,如 di= 0, 5, 24, 11, …
-
處理沖突的方法——再散列法:
-
除了原始的散列函數(shù) H(key) 之外,多準(zhǔn)備幾個散列函數(shù)
-
當(dāng)散列函數(shù)沖突時,用下一個散列函數(shù)計算一個新地址,直到不沖突為止:
-
Hi = RHi(Key)
-
i=1,2,3….,k
排序
排序的基本概念
定義:
-
排序(Sort),就是重新排列表中的元素,使表中的元素滿足按關(guān)鍵字有序的過程。
-
輸入:n個記錄R1, R2,…, Rn,對應(yīng)的關(guān)鍵字為k1, k2,…, kn。
-
輸出:輸入序列的一個重排R1?, R2?,…, Rn?,使得有k1?≤k2?≤…≤kn?(也可遞減)
排序算法的評價指標(biāo):
-
時間復(fù)雜度
-
空間復(fù)雜度
-
算法的穩(wěn)定性:
-
若待排序表中有兩個元素Ri和Rj,其對應(yīng)的關(guān)鍵字相同即keyi = keyj,且在排序前Ri在Rj的前面
-
若使用某一排序算法排序后,Ri仍然在Rj的前面
-
則稱這個排序算法是穩(wěn)定的,否則稱排序算法是不穩(wěn)定的。
-
排序算法的分類:
-
內(nèi)部排序:數(shù)據(jù)都在內(nèi)存中,關(guān)注如何使算法時、空復(fù)雜度更低
-
外部排序:數(shù)據(jù)太多,無法全部放入內(nèi)存,還要關(guān)注如何使讀/寫磁盤次數(shù)更少
插入排序
算法思想:
每次將一個待排序的記錄按其關(guān)鍵字大小插入到前面已排好序的子序列中,直到全部記錄插入完成。
代碼實現(xiàn):
算法效率分析:
-
空間復(fù)雜度:O(1)
-
時間復(fù)雜度:主要來自對比關(guān)鍵字、移動元素,若有 n 個元素,則需要 n-1 趟處理
-
最好時間復(fù)雜度:O(n) 共n-1趟處理,每一趟只需要對比關(guān)鍵字1次,不用移動元素
-
最壞時間復(fù)雜度:O(n^2) 共n-1趟處理,每一趟都需要從尾移到到頭(全部逆序)
-
-
算法穩(wěn)定性:穩(wěn)定
優(yōu)化——折半插入排序:
思路:
-
先用折半查找找到應(yīng)該插入的位置,再移動元素
-
當(dāng) low>high 時折半查找停止,應(yīng)將 [low, i-1] 內(nèi)的元素全部右移,并將 A[0] 復(fù)制到 low 所指位置
-
當(dāng) A[mid]==A[0] 時,為了保證算法的“穩(wěn)定性”,應(yīng)繼續(xù)在 mid 所指位置右邊尋找插入位置
代碼實現(xiàn):
比起“直接插入排序”,比較關(guān)鍵字的次數(shù)減少了,但是移動元素的次數(shù)沒變,整體來看時間復(fù)雜度依然是O(n^2)
對鏈表進(jìn)行插入排序:
-
使用鏈表不需要對序列進(jìn)行依次友誼,移動元素的次數(shù)變少了
-
但是關(guān)鍵字對比的次數(shù)依然是O(n^2) 數(shù)量級,整體來看時間復(fù)雜度依然是O(n^2)
希爾排序
算法思想:
-
先追求表中元素部分有序,再逐漸逼近全局有序
-
先將待排序表分割成若干形如 L[i, i + d, i + 2d,…, i + kd] 的“特殊”子表,對各個子表分別進(jìn)行直接插入排序??s小增量d,重復(fù)上述過程,直到d=1為止。
代碼實現(xiàn):
算法性能分析:
-
空間復(fù)雜度:O(1)
-
時間復(fù)雜度:
-
和增量序列 d1, d2, d3… 的選擇有關(guān),目前無法用數(shù)學(xué)手段證明確切的時間復(fù)雜度
-
最壞時間復(fù)雜度為 O(n^2),當(dāng)n在某個范圍內(nèi)時,可達(dá)O(n^1.3)
-
-
穩(wěn)定性:不穩(wěn)定
-
適用性:僅適用于順序表,不適用于鏈表
冒泡排序
交換排序分類:
-
冒泡排序
-
選擇排序
算法思想:
-
從后往前(或從前往后)兩兩比較相鄰元素的值,若為逆序(即A[i-1]>A[i]),則交換它們,直到序列比較完
-
這樣過程稱為“一趟”冒泡排序
-
第n趟結(jié)束后,最小的n個元素會“冒”到最前邊
-
若某一趟排序沒有發(fā)生“交換”,說明此時已經(jīng)整體有序。
-
可以從后往前冒泡,也可以沖前往后冒泡
代碼實現(xiàn):
算法性能分析:
-
空間復(fù)雜度:O(1)
-
時間復(fù)雜度:
-
最好情況(有序):O(n)
-
最壞情況(逆序):O(n^2)
-
平均情況:O(n^2)
-
-
穩(wěn)定性:穩(wěn)定
-
適用性:順序表、鏈表都可以
快速排序
算法思想:
-
在待排序表L[1…n]中任取一個元素pivot作為樞軸(或基準(zhǔn),通常取?元素)
-
通過一趟排序?qū)⒋判虮韯澐譃楠毩⒌膬刹糠諰[1…k-1]和L[k+1…n]
-
使得L[1…k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于等于pivot
-
則pivot放在了其最終位置L(k)上,這個過程稱為一次“劃分”
-
然后分別遞歸地對兩個子表重復(fù)上述過程,直至每部分內(nèi)只有一個元素或空為止,即所有元素放在了其最終位置上
代碼實現(xiàn):
算法性能分析:
-
n個結(jié)點的二叉樹
-
最小高度 = ?log2n? + 1
-
最大高度 = n
-
-
時間復(fù)雜度=O(n*遞歸層數(shù))
-
最好時間復(fù)雜度=O(nlog2n)
-
每次選的樞軸元素都能將序列劃分成均勻的兩部分
-
-
最壞時間復(fù)雜度=O(n^2)
-
若序列原本就有序或逆序,則時、空復(fù)雜度最高(可優(yōu)化,盡量選擇可以把數(shù)據(jù)中分的樞軸元素。)
-
-
-
空間復(fù)雜度=O(遞歸層數(shù))
-
最好空間復(fù)雜度=O(log2n)
-
最壞空間復(fù)雜度=O(n)
-
-
若每一次選中的“樞軸”將待排序序列劃分為很不均勻的兩個部分,則會導(dǎo)致遞歸深度增加,算法效率變低
-
若初始序列有序或逆序,則快速排序的性能最差(因為每次選擇的都是最靠邊的元素)
-
快速排序算法優(yōu)化思路:盡量選擇可以把數(shù)據(jù)中分的樞軸元素。
-
選頭、中、尾三個位置的元素,取中間值作為樞軸元素;
-
隨機選一個元素作為樞軸元素
-
-
快速排序是所有內(nèi)部排序算法中平均性能最優(yōu)的排序算法
-
穩(wěn)定性:不穩(wěn)定
簡單選擇排序
選擇排序分類:
-
簡單選擇排序
-
堆排序
算法思想:
每一趟在待排序元素中選取關(guān)鍵字最?。ɑ蜃畲螅┑脑兀恳惶舜判蛐蛄虚L度-1)加入有序子序列(每一趟有序序列長度+1)
代碼實現(xiàn):
算法性能分析:
-
無論有序、逆序、還是亂序,一定需要 n-1 趟處理
-
總共需要對比關(guān)鍵字 (n-1)+(n-2)+…+1=[n(n-1)]/2次
-
元素交換次數(shù) < n-1
-
空間復(fù)雜度:O(1)
-
時間復(fù)雜度=O(n^2)
-
穩(wěn)定性:不穩(wěn)定
-
適用性:既可以用于順序表,也可用于鏈表
堆排序
堆的定義:
-
若n個關(guān)鍵字序列L[1…n] 滿足下面某一條性質(zhì),則稱為堆(Heap):
-
若滿足:L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2 )則為大根堆(大頂堆)
-
即完全二叉樹中,任意根≥左、右
-
-
若滿足:L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i ≤n/2 )則為小根堆(小頂堆)
-
即完全二叉樹中,任意根≤左、右
-
-
建立大根堆:
-
把所有非終端結(jié)點都檢查一遍,是否滿足大根堆的要求,如果不滿足,則進(jìn)行調(diào)整
-
在順序存儲的完全二叉樹中,非終端結(jié)點編號 i≤?n/2?
-
檢查當(dāng)前結(jié)點是否滿足 根≥左、右,若不滿足,將當(dāng)前結(jié)點與更大的一個孩子互換
-
i的左孩子:2i
-
i的右孩子:2i+1
-
i的父結(jié)點:?i/2?
-
-
若元素互換破壞了下一級的堆,則采用相同的方法繼續(xù)往下調(diào)整(小元素不斷“下墜”)
-
建立大根堆(代碼):
基于大根堆進(jìn)行排序:
-
選擇排序:每一趟在待排序元素中選取關(guān)鍵字最大的元素加入有序子序列
-
堆排序每一趟完成以下工作:
-
將堆頂元素(就是最大的元素)加入有序子序列(與待排序序列中的最后一個元素交換)
-
并將待排序元素序列再次調(diào)整為大根堆(小元素不斷“下墜”)
-
-
注意:大根堆獲得的排序序列是遞增序列,小跟堆相反,獲得的是遞減序列
代碼實現(xiàn)(大根堆排序):
堆排序的效率分析:
-
建堆的過程,關(guān)鍵字對比次數(shù)不超過4n,建堆時間復(fù)雜度=O(n)
-
堆排序的時間復(fù)雜度 =O(n) + O(nlog2n) = O(nlog2n)
-
堆排序的空間復(fù)雜度 =O(1)
-
穩(wěn)定性:不穩(wěn)定
堆的插入刪除
基本操作:
-
i的左孩子:2i
-
i的右孩子:2i+1
-
i的父結(jié)點:?i/2?
在堆中插入新元素:
-
對于小根堆,新元素放到表尾,與父節(jié)點對比,若新元素比父節(jié)點更小,則將二者互換。
-
新元素就這樣一路“上升”,直到無法繼續(xù)上升為止
在堆中刪除元素:
-
被刪除的元素用堆底元素替代
-
然后讓該元素不斷“下墜”,直到無法下墜為止
關(guān)鍵字對比次數(shù):
-
每次“上升”調(diào)整只需對比關(guān)鍵字1次
-
每次“下墜”調(diào)整可能需要對比關(guān)鍵字2次,也可能只需對比1次
歸并排序
算法思想:
-
把兩個或多個已經(jīng)有序的序列合并成一個
-
對于兩個有序序列,將i、j指針指向序列的表頭,選擇更小的一個放入k所指的位置
-
k++,i/j指向更小元素的指針++
-
只剩一個子表未合并時,可以將該表的剩余元素全部加到總表
-
m路歸并:每選出一個小的元素,需要對比關(guān)鍵字m-1次
-
核心操作:把數(shù)組內(nèi)的兩個有序序列歸并為一個
代碼實現(xiàn):
-
low是數(shù)組中第一個有序序列的開始
-
mid是數(shù)組中第一個有序序列的結(jié)尾
-
high是數(shù)組中第二個有序序列的結(jié)尾
-
輔助數(shù)組B臨時存放這兩段有序序列
算法效率分析:
-
2路歸并的“歸并樹”形態(tài)上就是一棵倒立的二叉樹
-
二叉樹的第h層最多有2 ^ (h-1)個結(jié)點,若樹高為h,則應(yīng)滿足n <= 2 ^ (h-1),即h ? 1 = ?log2n?
-
n個元素進(jìn)行2路歸并排序,歸并趟數(shù)=?log2n?
-
每趟歸并時間復(fù)雜度為O(n),則算法時間復(fù)雜度O(nlog2n)
-
空間復(fù)雜度=O(n),來自于輔助數(shù)組B
-
穩(wěn)定性:穩(wěn)定
基數(shù)排序
算法思想:
基數(shù)排序得到遞減序列的過程如下:
-
初始化: 設(shè)置 r 個空隊列,Qr-1, Qr-2,…, Q0
-
按照各個 關(guān)鍵字位 權(quán)重遞增的次序(個、十、百),對 d 個關(guān)鍵字位分別做“分配”和“收集”
-
分配:順序掃描各個元素,若當(dāng)前處理的關(guān)鍵字位=x,則將元素插入 Qx 隊尾
-
收集:把 Qr-1, Qr-2,…, Q0 各個隊列中的結(jié)點依次出隊并鏈接
基數(shù)排序得到遞增序列的過程如下:
-
初始化: 設(shè)置 r 個空隊列,Q0, Q1,…, Qr?1
-
按照各個 關(guān)鍵字位 權(quán)重遞增的次序(個、十、百),對 d 個關(guān)鍵字位分別做“分配”和“收集”
-
分配:順序掃描各個元素,若當(dāng)前處理的關(guān)鍵字位=x,則將元素插入 Qx 隊尾
-
收集:把 Q0, Q1,…, Qr?1 各個隊列中的結(jié)點依次出隊并鏈接
算法效率分析:
-
需要r個輔助隊列,空間復(fù)雜度 = O(r)
-
一趟分配O(n),一趟收集O(r),總共 d 趟分配、收集,總的時間復(fù)雜度=O(d(n+r))
-
穩(wěn)定性:穩(wěn)定
基數(shù)排序的應(yīng)用:
-
例如:
-
某學(xué)校有10000名學(xué)生,將學(xué)生信息按照年齡遞減排序
-
給十億人的身份證號排序
-
-
基數(shù)排序擅長解決的問題:
-
數(shù)據(jù)元素的關(guān)鍵字可以方便地拆分為 d 組,且 d 較小
-
每組關(guān)鍵字的取值范圍不大,即 r 較小
-
數(shù)據(jù)元素個數(shù) n 較大
-
外部排序
外存、內(nèi)存的數(shù)據(jù)交換:
-
外存:
-
操作系統(tǒng)以“塊”為單位對磁盤存儲空間進(jìn)行管理
-
如:每塊大小 1KB各個磁盤塊內(nèi)存放著各種各樣的數(shù)據(jù)
-
-
內(nèi)存:
-
磁盤的讀/寫以“塊”為單位
-
數(shù)據(jù)讀入內(nèi)存后才能被修改
-
修改完了還要寫回磁盤
-
外部排序原理:
-
數(shù)據(jù)元素太多,無法一次全部讀入內(nèi)存進(jìn)行排序
-
使用“歸并排序”的方法,最少只需在內(nèi)存或只能怪分配3塊大小的緩沖區(qū)即可對任意一個大文件進(jìn)行排序
-
”歸并排序“要求各個子序列有序,每次讀入兩個塊的內(nèi)容,進(jìn)行內(nèi)部排序后寫回磁盤
構(gòu)造初始“歸并段”:
若有N個記錄,內(nèi)存工作區(qū)可以容納L個記錄,則初始?xì)w并段數(shù)量=r=N/L
第一趟歸并:
第二趟歸并:
第三趟歸并:
時間開銷分析:
外部排序時間開銷=讀寫外存的時間+內(nèi)部排序所需時間+內(nèi)部歸并所需時間
優(yōu)化思路:
-
多路歸并
-
采用多路歸并可以減少歸并趟數(shù),從而減少磁盤I/O(讀寫)次數(shù)
-
對 r 個初始?xì)w并段,做k路歸并,則歸并樹可用 k 叉樹表示,若樹高為h,則歸并趟數(shù) = h-1 = ?logkr?
-
推導(dǎo):k叉樹第h層最多有k^(h?1) 個結(jié)點,則r ≤ kh?1, (h-1)最小 = ?logkr?
-
k越大,r越小,歸并趟數(shù)越少,讀寫磁盤次數(shù)越少
-
多路歸并帶來的負(fù)面影響:
-
k路歸并時,需要開辟k個輸入緩沖區(qū),內(nèi)存開銷增加
-
每挑選一個關(guān)鍵字需要對比關(guān)鍵字(k-1)次,內(nèi)部歸并所需時間增加(可使用敗者樹優(yōu)化)
-
-
-
減少初始?xì)w并段數(shù)量
-
生成初始?xì)w并段的“內(nèi)存工作區(qū)”越大,初始?xì)w并段越長
-
敗者樹
定義:
-
可視為一棵完全二叉樹(多了一個頭頭)
-
k個葉結(jié)點分別是當(dāng)前參加比較的元素,非葉子結(jié)點用來記憶左右子樹中的“失敗者”,而讓勝者往上繼續(xù)進(jìn)行比較,一直到根結(jié)點。
-
即失敗者留在這一回合,勝利者進(jìn)入下一回合比拼
敗者樹在多路平衡歸并中的應(yīng)用:
敗者樹的存儲結(jié)構(gòu):
置換-選擇排序
-
使用置換-選擇排序,可以讓每個初始?xì)w并段的長度超過內(nèi)存工作區(qū)大小的限制
-
設(shè)初始待排文件為FI,初始?xì)w并段輸出文件為FO,內(nèi)存工作區(qū)為WA,F(xiàn)O和WA的初始狀態(tài)為空,WA可容納w個記錄。
-
置換-選擇算法的步驟如下:
-
從FI輸入w個記錄到工作區(qū)WA。
-
從WA中選出其中關(guān)鍵字取最小值的記錄,記為MINIMAX記錄。
-
將MINIMAX記錄輸出到FO中去。
-
若FI不空,則從FI輸入下一個記錄到WA中。
-
從WA中所有關(guān)鍵字比MINIMAX記錄的關(guān)鍵字大的記錄中選出最小關(guān)鍵字記錄,作為新的MINIMAX記錄。
-
重復(fù)3)~5),直至在WA中選不出新的MINIMAX記錄為止,由此得到一個初始?xì)w并段,輸出一個歸并段的結(jié)束標(biāo)志到FO中去。
-
重復(fù)2)~6),直至WA為空。由此得到全部初始?xì)w并段。
-
最佳歸并樹
引子:
-
歸并過程中磁盤I/O次數(shù)=歸并樹的WPL*2
-
因此要讓磁盤I/O次數(shù)最小,就要使歸并樹的WPL最小即構(gòu)建一個哈夫曼樹
m叉最佳歸并樹的構(gòu)造:
-
注意:對于k叉歸并,若初始?xì)w并段的數(shù)量無法構(gòu)成嚴(yán)格的k叉歸并樹
-
則需要補充幾個長度為0的“虛段”,再進(jìn)行k叉哈夫曼樹的構(gòu)造
增加虛段的數(shù)量:
-
若(初始?xì)w并段數(shù)量 -1)% (k-1)= 0,說明剛好可以構(gòu)成嚴(yán)格k叉樹,此時不需要添加虛段文章來源:http://www.zghlxwxcb.cn/news/detail-404702.html
-
若(初始?xì)w并段數(shù)量 -1)% (k-1)= u ≠ 0,則需要補充 (k-1) - u 個虛段文章來源地址http://www.zghlxwxcb.cn/news/detail-404702.html
到了這里,關(guān)于一篇學(xué)完:王道考研408數(shù)據(jù)結(jié)構(gòu)(全)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!