一些概念
數據結構就是研究數據的邏輯結構和物理結構以及它們之間相互關系,并對這種結構定義相應的運算,而且確保經過這些運算后所得到的新結構仍然是原來的結構類型。
數據:所有能被輸入到計算機中,且能被計算機處理的符號的集合。是計算機操作的對象的總稱。
數據元素:數據(集合)中的一個“個體”,數據及結構中討論的基本單位
數據項:數據的不可分割的最小單位。一個數據元素可由若干個數據項組成。
數據類型:在一種程序設計語言中,變量所具有的數據種類。整型、浮點型、字符型等等
-
邏輯結構:數據之間的相互關系。
- 集合 結構中的數據元素除了同屬于一種類型外,別無其它關系。
- 線性結構 數據元素之間一對一的關系
- 樹形結構 數據元素之間一對多的關系
- 圖狀結構或網狀結構 結構中的數據元素之間存在多對多的關系
物理結構/存儲結構:數據在計算機中的表示。物理結構是描述數據具體在內存中的存儲(如:順序結構、鏈式結構、索引結構、哈希結構)等
在數據結構中,從邏輯上可以將其分為線性結構和非線性結構
數據結構的基本操作的設置的最重要的準則是,實現應用程序與存儲結構的獨立。實現應用程序是“邏輯結構”,存儲的是“物理結構”。邏輯結構主要是對該結構操作的設定,物理結構是描述數據具體在內存中的存儲(如:順序結構、鏈式結構、索引結構、希哈結構)等。
順序存儲結構中,線性表的邏輯順序和物理順序總是一致的。但在鏈式存儲結構中,線性表的邏輯順序和物理順序一般是不同的。
算法五個特性: 有窮性、確定性、可行性、輸入、輸出
算法設計要求:正確性、可讀性、健壯性、高效率與低存儲量需求。(好的算法)
算法的描述有偽程序、流程圖、N-S結構圖等。E-R圖是實體聯系模型,不是程序的描述方式。
設計算法在執行時間時需要考慮:算法選用的規模、問題的規模
時間復雜度:算法的執行時間與原操作執行次數之和成正比。時間復雜度有小到大:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)、O(n3)。冪次時間復雜度有小到大O(2n)、O(n!)、O(nn)
空間復雜度:若輸入數據所占空間只取決于問題本身,和算法無關,則只需要分析除輸入和程序之外的輔助變量所占額外空間。
線性表
線性表是一種典型的線性結構。頭結點無前驅有一個后繼,尾節點無后繼有一個前驅。鏈表只能順序查找,定位一個元素的時間為O(N),刪除一個元素的時間為O(1)
-
線性表的順序存儲結構:把線性表的結點按邏輯順序依次存放在一組地址連續的存儲單元里。用這種方法存儲的線性表簡稱順序表。是一種隨機存取的存儲結構。順序存儲指內存地址是一塊的,隨機存取指訪問時可以按下標隨機訪問,存儲和存取是不一樣的。如果是存儲,則是指按順序的,如果是存取,則是可以隨機的,可以利用元素下標進行。數組比線性表速度更快的是:原地逆序、返回中間節點、選擇隨機節點。
- 便于線性表的構造和任意元素的訪問
- 插入:插入新結點,之后結點后移。平均時間復雜度:O(n)
- 刪除:刪除節點,之后結點前移。平均時間復雜度:O(n)
-
線性鏈表:用一組任意的存儲單元來依次存放線性表的結點,這組存儲單元即可以是連續的,也可以是不連續的,甚至是零散分布在內存中的任意位置上的。因此,鏈表中結點的邏輯次序和物理次序不一定相同。為了能正確表示結點間的邏輯關系,在存儲每個結點值的同時,還必須存儲指示其后繼結點的地址。data域是數據域,用來存放結點的值。next是指針域(亦稱鏈域),用來存放結點的直接后繼的地址(或位置)。不需要事先估計存儲空間大小。
-
單鏈表中每個結點的存儲地址是存放在其前趨結點next域中,而開始結點無前趨,故應設頭指針head指向開始結點。同時,由于最后一個結點無后繼,故結點的指針域為空,即NULL。頭插法建表(逆序)、尾插法建表(順序)。增加頭結點的目的是算法實現上的方便,但增大了內存開銷。
- 查找:只能從鏈表的頭指針出發,順鏈域next逐個結點往下搜索,直到搜索到第i個結點為止。因此,鏈表不是隨機存取結構。
- 插入:先找到表的第i-1的存儲位置,然后插入。新結點先連后繼,再連前驅。
- 刪除:首先找到ai-1的存儲位置p。然后令p–>next指向ai的直接后繼結點,即把ai從鏈上摘下。最后釋放結點ai的空間.r=p->next;p->next=r->next;delete r。
- 判斷一個單向鏈表中是否存在環的最佳方法是快慢指針。
靜態鏈表:用一維數組來實現線性鏈表,這種用一維數組表示的線性鏈表,稱為靜態鏈表。靜態:體現在表的容量是一定的。(數組的大小);鏈表:插入與刪除同前面所述的動態鏈表方法相同。靜態鏈表中指針表示的是下一元素在數組中的位置。
靜態鏈表是用數組實現的,是順序的存儲結構,在物理地址上是連續的,而且需要預先分配大小。動態鏈表是用申請內存函數(C是malloc,C++是new)動態申請內存的,所以在鏈表的長度上沒有限制。動態鏈表因為是動態申請內存的,所以每個節點的物理地址不連續,要通過指針來順序訪問。靜態鏈表在插入、刪除時也是通過修改指針域來實現的,與動態鏈表沒有什么分別
-
循環鏈表:是一種頭尾相接的鏈表。其特點是無須增加存儲量,僅對表的鏈接方式稍作改變,即可使得表處理更加方便靈活。
- 在單鏈表中,將終端結點的指針域NULL改為指向表頭結點的或開始結點,就得到了單鏈形式的循環鏈表,并簡單稱為單循環鏈表。由于循環鏈表中沒有NULL指針,故涉及遍歷操作時,其終止條件就不再像非循環鏈表那樣判斷p或p—>next是否為空,而是判斷它們是否等于某一指定指針,如頭指針或尾指針等。
-
雙向鏈表:在單鏈表的每個結點里再增加一個指向其直接前趨的指針域prior。這樣就形成的鏈表中有兩個方向不同的鏈。雙鏈表一般由頭指針唯一確定的,將頭結點和尾結點鏈接起來構成循環鏈表,并稱之為雙向鏈表。設指針p指向某一結點,則雙向鏈表結構的對稱性可用下式描述:p—>prior—>next=p=p—>next—>prior。從兩個方向搜索雙鏈表,比從一個方向搜索雙鏈表的方差要小。
- 插入:先搞定插入節點的前驅和后繼,再搞定后結點的前驅,最后搞定前結點的后繼。
- 在有序雙向鏈表中定位刪除一個元素的平均時間復雜度為O(n)
- 可以直接刪除當前指針所指向的節點。而不需要像單向鏈表中,刪除一個元素必須找到其前驅。因此在插入數據時,單向鏈表和雙向鏈表操作復雜度相同,而刪除數據時,雙向鏈表的性能優于單向鏈表
-
棧和隊列
棧
棧(Stack)是限制在表的一端進行插入和刪除運算的線性表,通常稱插入、刪除的這一端為棧頂(Top),另一端為棧底(Bottom)。先進后出。top= -1時為空棧,top=0只能說明棧中只有一個元素,并且元素進棧時top應該自增
- 順序存儲棧:順序存儲結構
- 鏈棧:鏈式存儲結構。插入和刪除操作僅限制在鏈頭位置上進行。棧頂指針就是鏈表的頭指針。通常不會出現棧滿的情況。 不需要判斷棧滿但需要判斷棧空。
- 兩個棧共用靜態存儲空間,對頭使用也存在空間溢出問題。棧1的底在v[1],棧2的底在V[m],則棧滿的條件是top[1]+1=top[2]。
- 基本操作:刪除棧頂元素、判斷棧是否為空以及將棧置為空棧等
- 對于n各元素的入棧問題,可能的出棧順序有C(2n,n)/(n+1)個。
- 堆棧溢出一般是循環的遞歸調用、大數據結構的局部變量導致的
應用,代碼:
進制轉換
括號匹配的檢驗
行編輯程序
迷宮求解:若當前位置“可通”,則納入路徑,繼續前進;若當前位置“不可通”,則后退,換方向繼續探索;若四周“均無通路”,則將當前位置從路徑中刪除出去。
-
表達式求解:前綴、中綴、后綴。
- 操作數之間的相對次序不變;
- 運算符的相對次序不同;
- 中綴式丟失了括弧信息,致使運算的次序不確定
- 前綴式的運算規則為:連續出現的兩個操作數和在它們之前且緊靠它們的運算符構成一個最小表達式
- 后綴式的運算規則為:運算符在式中出現的順序恰為表達式的運算順序;每個運算符和在它之前出現且緊靠它的兩個操作數構成一個最小表達式。
實現遞歸:多個函數嵌套調用的規則是:后調用先返回。
瀏覽器歷史紀錄,Android中的最近任務,Activity的啟動模式,CPU中棧的實現,Word自動保存,解析計算式,解析xml/json。解析XML時,需要校驗節點是否閉合,節點閉合的話,有頭尾符號相對應,遇到頭符號將其放入棧中,遇到尾符號時,彈出棧的內容,看是否有與之對應的頭符號,棧的特性剛好符合符號匹配的就近原則。
不是所有的遞歸程序都需要棧來保護現場,比方說求階乘的,是單向遞歸,直接用循環去替代從1乘到n就是結果了,另外一些需要棧保存的也可以用隊列等來替代。不是所有的遞歸轉化為非遞歸都要用到棧。轉化為非遞歸主要有兩種方法:對于尾遞歸或單向遞歸,可以用循環結構算法代替
隊列
隊列(Queue)也是一種運算受限的線性表。它只允許在表的一端進行插入,而在另一端進行刪除。允許刪除的一端稱為隊頭(front),允許插入的一端稱為隊尾(rear)。先進先出。
順序隊列:順序存儲結構。當頭尾指針相等時隊列為空。在非空隊列里,頭指針始終指向隊頭前一個位置,而尾指針始終指向隊尾元素的實際位置
循環隊列。在循環隊列中進行出隊、入隊操作時,頭尾指針仍要加1,朝前移動。只不過當頭尾指針指向向量上界(MaxSize-1)時,其加1操作的結果是指向向量的下界0。除非向量空間真的被隊列元素全部占用,否則不會上溢。因此,除一些簡單的應用外,真正實用的順序隊列是循環隊列。故隊空和隊滿時頭尾指針均相等。因此,我們無法通過front=rear來判斷隊列“空”還是“滿”
鏈隊列:鏈式存儲結構。限制僅在表頭刪除和表尾插入的單鏈表。顯然僅有單鏈表的頭指針不便于在表尾做插入操作,為此再增加一個尾指針,指向鏈表的最后一個結點。
設尾指針的循環鏈表表示隊列,則入隊和出隊算法的時間復雜度均為O(1)。用循環鏈表表示隊列,必定有鏈表的頭結點,入隊操作在鏈表尾插入,直接插入在尾指針指向的節點后面,時間復雜度是常數級的;出隊操作在鏈表表頭進行,也就是刪除表頭指向的節點,時間復雜度也是常數級的。
隊空條件:rear==front,但是一般需要引入新的標記來說明棧滿還是棧空,比如每個位置布爾值
隊滿條件:(rear+1) % QueueSize==front,其中QueueSize為循環隊列的最大長度
計算隊列長度:(rear-front+QueueSize)% QueueSize
入隊:(rear+1)% QueueSize
出隊:(front+1)% QueueSize
假設以數組A[N]為容量存放循環隊列的元素,其頭指針是front,當前隊列有X個元素,則隊列的尾指針值為(front+X mod N)
串
串(String)是零個或多個字符組成的有限序列。長度為零的串稱為空串(Empty String),它不包含任何字符。通常將僅由一個或多個空格組成的串稱為空白串(Blank String) 注意:空串和空白串的不同,例如“ ”和“”分別表示長度為1的空白串和長度為0的空串。
串的表示和實現:
- 定長順序存儲表示。靜態存儲分配的順序表。
- 堆分配存儲表示。存儲空間是在程序執行過程中動態分配而得。所以也稱為動態存儲分配的順序表
- 串的鏈式存儲結構。
串匹配:將主串稱為目標串,子串稱之為模式串。蠻力法匹配。KMP算法匹配。Boyer-Moore算法匹配。
數組和廣義表
數組和廣義表可看成是一種特殊的線性表,其特殊在于: 表中的元素本身也是一種線性表。內存連續。根據下標在O(1)時間讀/寫任何元素。
二維數組,多維數組,廣義表、樹、圖都屬于非線性結構
數組
數組的順序存儲:行優先順序;列優先順序。數組中的任一元素可以在相同的時間內存取,即順序存儲的數組是一個隨機存取結構。
關聯數組(Associative Array),又稱映射(Map)、字典( Dictionary)是一個抽象的數據結構,它包含著類似于(鍵,值)的有序對。 不是線性表。
矩陣的壓縮:
- 對稱矩陣、三角矩陣:直接存儲矩陣的上三角或者下三角元素。注意區分i>=j和i
廣義表
廣義表(Lists,又稱列表)是線性表的推廣。廣義表是n(n≥0)個元素a1,a2,a3,…,an的有限序列,其中ai或者是原子項,或者是一個廣義表。若廣義表LS(n>=1)非空,則a1是LS的表頭,其余元素組成的表(a2,…an)稱為LS的表尾。廣義表的元素可以是廣義表,也可以是原子,廣義表的元素也可以為空。表尾是指除去表頭后剩下的元素組成的表,表頭可以為表或單元素值。所以表尾不可以是單個元素值。
例子:
- A=()——A是一個空表,其長度為零。
- B=(e)——表B只有一個原子e,B的長度為1。
- C=(a,(b,c,d))——表C的長度為2,兩個元素分別為原子a和子表(b,c,d)。
- D=(A,B,C)——表D的長度為3,三個元素都是廣義 表。顯然,將子表的值代入后,則有D=(( ),(e),(a,(b,c,d)))。
- E=(a,E)——這是一個遞歸的表,它的長度為2,E相當于一個無限的廣義表E=(a,(a,(a,(a,…)))).
三個結論:
- 廣義表的元素可以是子表,而子表的元素還可以是子表。由此,廣義表是一個多層次的結構,可以用圖形象地表示
- 廣義表可為其它表所共享。例如在上述例4中,廣義表A,B,C為D的子表,則在D中可以不必列出子表的值,而是通過子表的名稱來引用。
- 廣義表的遞歸性
考點:
- 廣義表是0個或多個單因素或子表組成的有限序列,廣義表可以是自身的子表,廣義表的長度n>=0,所以可以為空表。廣義表的同級元素(直屬于同一個表中的各元素)具有線性關系
- 廣義表的表頭為空,并不代表該廣義表為空表。廣義表()和(())不同。前者是長度為0的空表,對其不能做求表頭和表尾的運算;而后者是長度為l的非空表(只不過該表中惟一的一個元素是空表),對其可進行分解,得到的表頭和表尾均是空表()
- 已知廣義表LS=((a,b,c),(d,e,f)),運用head和tail函數取出LS中原子e的運算是head(tail(head(tail(LS)))。根據表頭、表尾的定義可知:任何一個非空廣義表的表頭是表中第一個元素,它可以是原子,也可以是子表,而其表尾必定是子表。也就是說,廣義表的head操作,取出的元素是什么,那么結果就是什么。但是tail操作取出的元素外必須加一個表——“()“。tail(LS)=((d,e,f));head(tail(LS))=(d,e,f);tail(head(tail(LS)))=(e,f);head(tail(head(tail(LS))))=e。
- 二維以上的數組其實是一種特殊的廣義表
- 在(非空)廣義表中:1、表頭head可以是原子或者一個表 2、表尾tail一定是一個表 3.廣義表難以用順序存儲結構 4.廣義表可以是一個多層次的結構
樹和二叉樹
一種非線性結構。樹是遞歸結構,在樹的定義中又用到了樹的概念。
基本術語:
- 樹結點:包含一個數據元素及若干指向子樹的分支;
- 孩子結點:結點的子樹的根稱為該結點的孩子;
- 雙親結點:B結點是A結點的孩子,則A結點是B結點的雙親;
- 兄弟結點:同一雙親的孩子結點;
- 堂兄結點:同一層上結點;
- 結點層次:根結點的層定義為1;根的孩子為第二層結點,依此類推;
- 樹的高(深)度:樹中最大的結點層
- 結點的度:結點子樹的個數
- 樹的度: 樹中最大的結點度。
- 葉子結點:也叫終端結點,是度為0的結點;
- 分枝結點:度不為0的結點(非終端結點);
- 森林:互不相交的樹集合;
- 有序樹:子樹有序的樹,如:家族樹;
- 無序樹:不考慮子樹的順序;
二叉樹
二叉樹可以為空。二叉樹結點的子樹要區分左子樹和右子樹,即使只有一棵子樹也要進行區分,說明它是左子樹,還是右子樹。這是二叉樹與樹的最主要的差別。注意區分:二叉樹、二叉查找樹/二叉排序樹/二叉搜索樹、二叉平衡(查找)樹
二叉平衡樹肯定是一顆二叉排序樹。堆不是一顆二叉平衡樹。
二叉樹與樹是不同的,二叉樹不等價于分支樹最多為二的有序樹。當一個結點只包含一個子節點時,對于有序樹并無左右孩子之分,而對于二叉樹來說依然有左右孩子之分,所以二叉樹與樹是兩種不同的結構。
性質:
在二叉樹的第 i 層上至多有2i-1個結點。
深度為 k 的二叉樹上至多含 2k-1 個結點(k≥1)
對任何一棵二叉樹,若它含有n0個葉子結點、n2個度為 2 的結點,則必存在關系式:n0= n2+1。
具有 n 個結點的完全二叉樹的深度為?log2 n?+1 。
n個結點的二叉樹中,完全二叉樹具有最小的路徑長度。
-
如果對一棵有n個結點的完全二叉樹的結點按層序編號,則對任一結點i(1<=i<=n),有:
- 如果i=1,則結點i無雙親,是二叉樹的根;如果i>1,則其雙親的編號是 i/2(整除)。
- 如果2i>n,無左孩子;否則,其左孩子是結點2i。
- 如果2i+1>n,則結點i無右孩子;否則,其右孩子是結點2i+1。
二叉樹的存儲結構
- 順序存儲結構:僅僅適用于滿或完全二叉樹,結點之間的層次關系由性質5確定。
- 二叉鏈表法:每個節點存儲左子樹和右子樹。三叉鏈表:左子樹、右子樹、父節點,總的指針是n+2
- 在有n個結點的二叉鏈表中,值為非空的鏈域的個數為n-1。在有N個結點的二叉鏈表中必定有2N個鏈域。除根結點外,其余N-1個結點都有一個父結點。所以,一共有N-1個非空鏈域,其余2N-(N-1)=N+1個為空鏈域。
- 二叉鏈存儲法也叫孩子兄弟法,左指針指向左孩子,右指針指向右兄弟。而中序遍歷的順序是左孩子,根,右孩子。這種遍歷順序與存儲結構不同,因此需要堆棧保存中間結果。而中序遍歷檢索二叉樹時,由于其存儲結構跟遍歷順序相符,因此不需要用堆棧。
遍歷二叉樹和線索二叉樹
遍歷二叉樹:使得每一個結點均被訪問一次,而且僅被訪問一次。非遞歸的遍歷實現要利用棧。
- 先序遍歷DLR:根節點->左子樹->右子樹
- 中序遍歷LDR:左子樹->根節點->右子樹。必須要有中序遍歷才能得到一棵二叉樹的正確順序
- 后續遍歷LRD:左子樹->右子樹->根節點。需要棧的支持。
- 層次遍歷:用一維數組存儲二叉樹時,總是以層次遍歷的順序存儲結點。層次遍歷應該借助隊列。
線索二叉樹:對二叉樹所有結點做某種處理可在遍歷過程中實現;檢索(查找)二叉樹某個結點,可通過遍歷實現;如果能將二叉樹線索化,就可以簡化遍歷算法,提高遍歷速度,目的是加快查找結點的前驅或后繼的速度。
如何線索化?以中序遍歷為例,若能將中序序列中每個結點前趨、后繼信息保存起來,以后再遍歷二叉樹時就可以根據所保存的結點前趨、后繼信息對二叉樹進行遍歷。對于二叉樹的線索化,實質上就是遍歷一次二叉樹,只是在遍歷的過程中,檢查當前結點左,右指針域是否為空,若為空,將它們改為指向前驅結點或后繼結點的線索。前驅就是在這一點之前走過的點,不是下一將要去往的點。
加上結點前趨后繼信息(結索)的二叉樹稱為線索二叉樹。n個結點的線索二叉樹上每個結點有2個指針域(指向左孩子和右孩子),總共有2n個指針域;一個n個結點的樹有n-1條邊,那么空指針域= 2n - (n-1) = n + 1,即線索數為n+1。指針域tag為0,存放孩子指針,為1,存放前驅/后繼節點指針。
線索樹下結點x的前驅與后繼查找:設結點x相應的左(右)標志是線索標志,則lchild(rchild)就是前驅(后繼),否則:
LDR–前驅:左子樹中最靠右邊的結點;后繼:右子樹中最靠左邊的結點
LRD–前驅:右子樹的根,若無右子樹,為左子樹跟。后繼:x是根,后繼是空;x是雙親的右孩子、x是雙親的左孩子,但雙親無右孩子,雙親是后繼;x是雙親的左孩子,雙親有右孩子,雙親右子樹中最左的葉子是后繼
DLR–對稱于LRD線索樹—將LRD中所有左右互換,前驅與后繼互換,得到DLR的方法。
-
為簡化線索鏈表的遍歷算法,仿照線性鏈表,為線索鏈表加上一頭結點,約定:
- 頭結點的lchild域:存放線索鏈表的根結點指針;
- 頭結點的rchild域: 中序序列最后一個結點的指針;
- 中序序列第一結點lchild域指向頭結點;
- 中序序列最后一個結點的rchild域指向頭結點;
中序遍歷的線索二叉樹以及線索二叉樹鏈表示意圖
一棵左右子樹均不空的二叉樹在前序線索化后,其中空的鏈域的個數是1。前序和后續線索化后空鏈域個數都是1,中序是2。二叉樹在線索化后,仍不能有效求解的問題是前序求前序先驅,后序求后序后繼。
中序遍歷的順序為:左、根、右,所以對于每一非空的線索,左子樹結點的后繼為根結點,右子樹結點的前驅為根結點,再遞歸的執行上面的過程,可得非空線索均指向其祖先結點。在中序線索二叉樹中,每一非空的線索均指向其祖先結點。
在二叉樹上加上結點前趨、后繼線索后,可利用線索對二叉樹進行遍歷,此時,不需棧,也不需遞歸。基本步驟:
p=T->lchild; p指向線索鏈表的根結點;
-
若線索鏈表非空,循環:
- 循環,順著p左孩子指針找到最左下結點;訪問之;
- 若p所指結點的右孩子域為線索,p的右孩子結點即為后繼結點循環: p=p->rchild; 并訪問p所指結點;(在此循環中,順著后繼線索訪問二叉樹中的結點)
- 一旦線索“中斷”,p所指結點的右孩子域為右孩子指針,p=p->rchild,使 p指向右孩子結點;
樹和森林
樹的存儲結構:
- 雙親表示法
- 孩子表示法
- 利用圖表示樹
- 孩子兄弟表示法(二叉樹表示法):鏈表中每個結點的兩指針域分別指向其第一個孩子結點和下一個兄弟結點
將樹轉化成二叉樹:右子樹一定為空
- 加線:在兄弟之間加一連線
- 抹線:對每個結點,除了其左孩子外,去除其與其余孩子之間的關系
- 旋轉:以樹的根結點為軸心,將整樹順時針轉45°
森林轉換成二叉樹:
- 將各棵樹分別轉換成二叉樹
- 將每棵樹的根結點用線相連
- 以第一棵樹根結點為二叉樹的根
樹與轉換后的二叉樹的關系:轉換后的二叉樹的先序對應樹的先序遍歷;轉換后的二叉樹的中序對應樹的后序遍歷
哈弗曼樹/霍夫曼樹
一些概念
- 路徑:從一個祖先結點到子孫結點之間的分支構成這兩個結點間的路徑;
- 路徑長度:路徑上的分支數目稱為路徑長度;
- 樹的路徑長度:從根到每個結點的路徑長度之和。
- 結點的權:根據應用的需要可以給樹的結點賦權值;
- 結點的帶權路徑長度:從根到該結點的路徑長度與該結點權的乘積;
- 樹的帶權路徑長度=樹中所有葉子結點的帶權路徑之和;通常記作 WPL=∑wi×li
- 哈夫曼樹:假設有n個權值(w1, w2, … , wn),構造有n個葉子結點的二叉樹,每個葉子結點有一個 wi作為它的權值。則帶權路徑長度最小的二叉樹稱為哈夫曼樹。最優二叉樹。
前綴碼的定義:在一個字符集中,任何一個字符的編碼都不是另一個字符編碼的前綴。霍夫曼編碼就是前綴碼,可用于快速判斷霍夫曼編碼是否正確。霍夫曼樹是滿二叉樹,若有n個節點,則共有(n+1)/2個碼子
給定n個權值作為n的葉子結點,構造一棵二叉樹,若帶權路徑長度達到最小,稱這樣的二叉樹為最優二叉樹,也稱為霍夫曼樹(Huffman Tree)。霍夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
假設哈夫曼樹是二叉的話,則度為0的結點個數為N,度為2的結點個數為N-1,則結點總數為2N-1。哈夫曼樹的結點個數必為奇數。
哈夫曼樹不一定是完全二叉樹,但一定是最優二叉樹。
若度為m的哈夫曼樹中,其葉結點個數為n,則非葉結點的個數為[(n-1)/(m-1)]。邊的數目等于度。
圖遍歷與回溯
圖搜索->形成搜索樹
- 窮舉法。
- 貪心法。多步決策,每步選擇使得構成一個問題的可能解,同時滿足目標函數。
- 回溯法。根據題意,選取度量標準,然后將可能的選擇方法按度量標準所要求順序排好,每次處理一個量,得到該意義下的最優解的分解處理。
圖
無向圖
- 回路或環:第一個頂點和最后一個頂點相同的路徑。
- 簡單回路或簡單環:除第一個頂點和最后一個頂點之外,其余頂點不重復出現的回路
- 連通:頂點v至v’ 之間有路徑存在
- 連通圖:無向圖圖 G 的任意兩點之間都是連通的,則稱G是連通圖。
- 連通分量:極大連通子圖,子圖中包含的頂點個數極大
- 所有頂點度的和必須為偶數
有向圖:
回路或環:第一個頂點和最后一個頂點相同的路徑。
簡單回路或簡單環:除第一個頂點和最后一個頂點之外,其余頂點不重復出現的回路。
連通:頂點v至v’之間有路徑存在
強連通圖:有向圖G的任意兩點之間都是連通的,則稱G是強連通圖。各個頂點間均可達。
強連通分量:極大連通子圖
有向圖頂點的度是頂點的入度與出度之和。鄰接矩陣中第V行中的1的個數是V的出度
生成樹:極小連通子圖。包含圖的所有n個結點,但只含圖的n-1條邊。在生成樹中添加一條邊之后,必定會形成回路或環。
完全圖:有 n(n-1)/2 條邊的無向圖。其中n是結點個數。必定是連通圖。
有向完全圖:有n(n-1)條邊的有向圖。其中n是結點個數。每兩個頂點之間都有兩條方向相反的邊連接的圖。
一個無向圖 G=(V,E) 是連通的,那么邊的數目大于等于頂點的數目減一:|E|>=|V|-1,而反之不成立。如果 G=(V,E) 是有向圖,那么它是強連通圖的必要條件是邊的數目大于等于頂點的數目:|E|>=|V|,而反之不成立。沒有回路的無向圖是連通的當且僅當它是樹,即等價于:|E|=|V|-1。
圖的存儲形式
-
鄰接矩陣和加權鄰接矩陣
- 無權有向圖:出度: i行之和;入度: j列之和。
- 無權無向圖:i結點的度: i行或i列之和。
- 加權鄰接矩陣:相連為w,不相連為∞
-
鄰接表
- 用頂點數組表、邊(弧)表表示該有向圖或無向圖
- 頂點數組表:用數組存放所有的頂點。數組大小為圖頂點數n
- 邊表(邊結點表):每條邊用一個結點進行表示。同一個結點的所有的邊形成它的邊結點單鏈表。
- n個頂點的無向圖的鄰接表最多有n(n-1)個邊表結點。有n個頂點的無向圖最多有n(n-1)/2條邊,此時為完全無向圖,而在鄰接表中每條邊存儲兩次,所以有n(n-1)個結點
圖的遍歷
深度優先搜索利用棧,廣度優先搜索利用隊列
求一條從頂點i到頂點s的簡單路徑–深搜。求兩個頂點之間的一條長度最短的路徑–廣搜。當各邊上的權值均相等時,BFS算法可用來解決單源最短路徑問題。
生成樹和最小生成樹
每次遍歷一個連通圖將圖的邊分成遍歷所經過的邊和沒有經過的邊兩部分,將遍歷經過的邊同圖的頂點構成一個子圖,該子圖稱為生成樹。因此有DFS生成樹和BFS生成樹。
生成樹是連通圖的極小子圖,有n個頂點的連通圖的生成樹必定有n-1條邊,在生成樹中任意增加一條邊,必定產生回路。若砍去它的一條邊,就會把生成樹變成非連通子圖
最小生成樹:生成樹中邊的權值(代價)之和最小的樹。最小生成樹問題是構造連通網的最小代價生成樹。
Kruskal算法:令最小生成樹集合T初始狀態為空,在有n個頂點的圖中選取代價最小的邊并從圖中刪去。若該邊加到T中有回路則丟棄,否則留在T中;依此類推,直至T中有n-1條邊為止。
Prim算法、Kruskal算法和Dijkstra算法均屬于貪心算法。
- Dijkstra算法解決的是帶權重的有向圖上單源最短路徑問題,該算法要求所有邊的權重都為非負值。
- Dijkstra算法解決了從某個原點到其余各頂點的最短路徑問題,由循環嵌套可知該算法的時間復雜度為O(NN)。若要求任一頂點到其余所有頂點的最短路徑,一個比較簡單的方法是對每個頂點當做源點運行一次該算法,等于在原有算法的基礎上,再來一次循環,此時整個算法的復雜度就變成了O(NN*N)。
- Bellman-Ford算法解決的是一般情況下的單源最短路徑問題,在這里,邊的權重可以為負值。該算法返回一個布爾值,以表明是否存在一個從源節點可以到達的權重為負值的環路。如果存在這樣一個環路,算法將告訴我們不存在解決方案。如果沒有這種環路存在,算法將給出最短路徑和它們的權重。
雙連通圖和關節點
若從一個連通圖中刪去任何一個頂點及其相關聯的邊,它仍為一個連通圖的話,則該連通圖被稱為重(雙)連通圖。
若連通圖中的某個頂點和其相關聯的邊被刪去之后,該連通圖被分割成兩個或兩個以上的連通分量,則稱此頂點為關節點。
沒有關節點的連通圖為雙連通圖
- 若生成樹的根結點,有兩個或兩個以上的分支,則此頂點(生成樹的根)必為關節點;
- 對生成樹上的任意一個非葉“頂點”,若其某棵子樹中的所有“頂點”沒有和其祖先相通的回邊,則該“頂點”必為關節點。
有向無環圖及其應用
拓撲排序。在用鄰接表表示圖時,對有n個頂點和e條弧的有向圖而言時間復雜度為O(n+e)。一個有向圖能被拓撲排序的充要條件就是它是一個有向無環圖。拓撲序列唯一不能唯一確定有向圖。
AOV網(Activity On Vertex):用頂點表示活動,邊表示活動的優先關系的有向圖稱為AOV網。AOV網中不允許有回路,這意味著某項活動以自己為先決條件。
拓撲有序序列:把AOV網絡中各頂點按照它們相互之間的優先關系排列一個線性序列的過程。若vi是vj前驅,則vi一定在vj之前;對于沒有優先關系的點,順序任意。
拓撲排序:對AOV網絡中頂點構造拓撲有序序列的過程。方法:
- 在有向圖中選一個沒有前驅的頂點且輸出之
- 從圖中刪除該頂點和所有以它為尾的弧
- 重復上述兩步,直至全部頂點均已輸出;或者當圖中不存在無前驅的頂點為止(此時說明圖中有環)
采用深度優先搜索或拓撲排序算法可以判斷出一個有向圖中是否有環(回路).深度優先搜索只要在其中記錄下搜索的節點數n,當n大于圖中節點數時退出,并可以得出有回路。若有回路,則拓撲排序訪問不到圖中所有的節點,所以也可以得出回路。廣度優先搜索過程中如果訪問到一個已經訪問過的節點,可能是多個節點指向這個節點,不一定是存在環。
算法描述:
把鄰接表中入度為0的頂點依此進棧
-
若棧不空,則
- 棧頂元素vj退棧并輸出;
- 在鄰接表中查找vj的直接后繼vk,把vk的入度減1;若vk的入度為0則進棧
若棧空時輸出的頂點個數不是n,則有向圖有環;否則,拓撲排序完畢。
AOE網:帶權的有向無環圖,其中頂點表示事件,弧表示活動,權表示活動持續時間。在工程上常用來表示工程進度計劃。
一些定義:
- 事件的最早發生時間(ve(j)):從源點到j結點的最長的路徑。意味著事件最早能夠發生的時間。
- 事件的最遲發生時間(vl(j)):不影響工程的如期完工,事件j必須發生的時間。
- 活動ai由弧
查找
順序查找、折半查找、索引查找、分塊查找是靜態查找,動態查找有二叉排序樹查找,最優二叉樹查找,鍵樹查找,哈希表查找
靜態查找表
順序表的順序查找:應用范圍:順序表或線性鏈表表示的表,表內元素之間無序。查找過程:從表的一端開始逐個進行記錄的關鍵字和給定值的比較。
順序有序表的二分查找。平均查找時間(n+1)/n log2(n+1)
分塊查找:將表分成幾塊,塊內無序,塊間有序,即前一塊中的最大值小于后一塊中的最小值。并且有一張索引表,每一項存放每一塊的最大值和指向該塊第一個元素的指針。索引表有序,塊內無序。所以,塊間查找用二分查找,塊內用順序查找,效率介于順序和二分之間;先確定待查記錄所在塊,再在塊內查找。因此跟表中元素個數和塊中元素個數都有關。
- 用數組存放待查記錄,
- 建立索引表,由每塊中最大(小)的關鍵字及所屬塊位置的信息組成。
- 當索引表較大時,可以采用二分查找
- 在數據量極大時,索引可能很多,可考慮建立索引表的索引,即二級索引,原則上索引不超過三級
分塊查找平均查找長度:ASLbs = Lb + Lw。其中,Lb是查找索引表確定所在塊的平均查找長度, Lw是在塊中查找元素的平均查找長度。在n一定時,可以通過選擇s使ASL盡可能小。當s=sqrt(n)時,ASL最小。
- 時間:順序查找最差,二分最好,分塊介于兩者之間
- 空間:分塊最大,需要增加索引數據的空間
- 順序查找對表沒有特殊要求
- 分塊時數據塊之間在物理上可不連續。所以可以達到插入、刪除數據只涉及對應的塊;另外,增加了索引的維護。
- 二分查找要求表有序,所以若表的元素的插入與刪除很頻繁,維持表有序的工作量極大。
- 在表不大時,一般直接使用順序查找。
動態查找
二叉排序樹的結點刪除:
- x為葉子結點,則直接刪除
- x只有左子樹xL或只有右子樹xR ,則令xL或xR直接成為雙親結點f的子樹;
- x即有左子樹xL也有右子樹xR,在xL中選值最大的代替x,該數據按二叉排序樹的性質應在最右邊。
平衡二叉樹:每個結點的平衡因子都為 1、-1、0 的二叉排序樹。或者說每個結點的左右子樹的高度最多差1的二叉排序樹。
平衡二叉樹的平衡:
-
左調整(新結點插入在左子樹上的調整):
- LL(插入在結點左子樹的左子樹上):旋轉前后高度都為h+1
- LR(新插入結點在左子樹的右子樹上):旋轉前后高度仍為h+1
-
右調整(新結點插入在右子樹上進行的調整):
- RR(插入在的右子樹的右子樹上):處理方法和 LL對稱
- RL(插入在的右子樹的左子樹上):處理方法和 LR對稱
平衡樹建立方法:
- 按二叉排序樹插入結點
- 如引起結點平衡因子變為|2|,則確定旋轉點,該點是離根最遠(或最接近于葉子的點)
- 確定平衡類型后進行平衡處理,平衡后以平衡點為根的子樹高不變
- 最小二叉平衡樹的節點的公式如下 F(n)=F(n-1)+F(n-2)+1 這個類似于一個遞歸的數列,可以參考Fibonacci數列,1是根節點,F(n-1)是左子樹的節點數量,F(n-2)是右子樹的節點數量。
常見的平衡二叉樹:
-
紅黑樹是平衡二叉樹,也就是左右子樹是平衡的,高度大概相等。這種情況等價于一塊完全二叉樹的高度,查找的時間復雜度是樹的高度,為logn,插入操作的平均時間復雜度為O(logn),最壞時間復雜度為O(logn)
紅黑樹- 節點是紅色或黑色。
- 根是黑色。
- 所有葉子都是黑色(葉子是NIL節點)。
- 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
- 從任一節點到其每個葉子的所有簡單路徑 都包含相同數目的黑色節點。
avl樹也是自平衡二叉樹;紅黑樹和AVL樹查找、插入、刪除的時間復雜度相同;包含n個內部結點的紅黑樹的高度是o(logn); TreeMap 是一個紅黑樹的實現,能保證插入的值保證排序
-
STL和linux多使用紅黑樹作為平衡樹的實現:
- 如果插入一個node引起了樹的不平衡,AVL和RB-Tree都是最多只需要2次旋轉操作,即兩者都是O(1);但是在刪除node引起樹的不平衡時,最壞情況下,AVL需要維護從被刪node到root這條路徑上所有node的平衡性,因此需要旋轉的量級O(logN),而RB-Tree最多只需3次旋轉,只需要O(1)的復雜度。
- 其次,AVL的結構相較RB-Tree來說更為平衡,在插入和刪除node更容易引起Tree的unbalance,因此在大量數據需要插入或者刪除時,AVL需要rebalance的頻率會更高。因此,RB-Tree在需要大量插入和刪除node的場景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
- map的實現只是折衷了兩者在search、insert以及delete下的效率。總體來說,RB-tree的統計性能是高于AVL的。
查找總結
- 既希望較快的查找又便于線性表動態變化的查找方法是哈希法查找。二叉排序樹查找,最優二叉樹查找,鍵樹查找,哈希法查找是動態查找。分塊、順序、折半、索引順序查找均為靜態。分塊法應該是將整個線性表分成若干塊進行保存,若動態變化則可以添加在表的尾部(非順序結構),時間復雜度是O(1),查找復雜度為O(n);若每個表內部為順序結構,則可用二分法將查找時間復雜度降至O(logn),但同時動態變化復雜度則變成O(n);順序法是挨個查找,這種方法最容易實現,不過查找時間復雜度都是O(n),動態變化時可將保存值放入線性表尾部,則時間復雜度為O(1);二分法是基于順序表的一種查找方式,時間復雜度為O(logn);通過哈希函數將值轉化成存放該值的目標地址,O(1)
- 二叉樹的平均查找長度為O(log2n)——O(n).二叉排序樹的查找效率與二叉樹的高度有關,高度越低,查找效率越高。二叉樹的查找成功的平均查找長度ASL不超過二叉樹的高度。二叉樹的高度與二叉樹的形態有關,n個節點的完全二叉樹高度最小,高度為[log2n]+1,n個節點的單只二叉樹的高度最大,高度為n,此時查找成功的ASL為最大(n+1)/2,因此二叉樹的高度范圍為[log2n]+1——n.
- 鏈式存儲不能隨機訪問,必須是順序存儲
B_樹的B+樹
B_樹
B-樹就是B樹。m階B_樹滿足或空,或為滿足下列性質的m叉樹:
- 樹中每個結點最多有m棵子樹
- 根結點在不是葉子時,至少有兩棵子樹
- 除根外,所有非終端結點至少有?m/2?棵子樹
- 有s個子樹的非葉結點具有 n = s-1個關鍵字,結點的信息組織為:(n,A0,K1,A1,K2,A2 … Kn,An)。這里:n為關鍵字的個數,ki(i=1,2,…,n)為關鍵字,且滿足Ki小于Ki+1,,Ai(i=0,1,..n)為指向子樹的指針。
- 所有的葉子結點都出現在同一層上,不帶信息(可認為外部結點或失敗結點)。
- 關鍵字集合分布在整顆樹中
- 任何一個關鍵字出現且只出現在一個結點中
- 搜索有可能在非葉子結點結束
- 其搜索性能等價于在關鍵字全集內做一次二分查找
- 只適用于隨機檢索,不適用于順序檢索。
- 有結點的平衡因子都為零
- M階B-樹中含有N個關鍵字,最大深度為log?m/2?(n+1)/2+2
B_樹中結點的插入
- m代表B_樹的階,插入總發生在最低層
- 插入后關鍵字個數小于等于 m-1,完成。
- 插入后關鍵字個數等于m,結點分裂,以中點數據為界一分為二,中點數據放到雙親結點中。這樣就有可能使得雙親結點的數據個數為m,引起雙親結點的分裂,最壞情況下一直波及到根,引起根的分裂——B_樹長高。
3階B_
樹的插入。每個結點最多3棵子樹,2個數據;最少2棵子樹,1個數據。所以3階B_樹也稱為2-3樹。
B_樹中結點的刪除
-
刪除發生在最底層
- 被刪關鍵字所在結點中的關鍵字數目大于等于 m/2 ,直接刪除。
- 刪除后結點中數據為?m/2?-2,而相鄰的左(右)兄弟中數據大于?m/2?-1,此時左(右兄弟)中最大(小)的數據上移到雙親中,雙親中接(靠)在它后(前)面的數據移到被刪數據的結點中
- 其左右兄弟結點中數據都是?m/2?-1,此時和左(右)兄弟合并,合并時連同雙親中相關的關鍵字。此時,雙親中少了一項,因此又可能引起雙親的合并,最壞一直到根,使B-樹降低一層。
-
刪除不在最底層
- 在大于被刪數據中選最小的代替被刪數據,問題轉換成在最底層的刪除
B+樹
在實際的文件系統中,用的是B+樹或其變形。有關性質與操作類似與B_樹。
[圖片上傳失敗...(image-3fd42c-1528633294716)]
差異:
- 有n棵子樹的結點中有n個關鍵字,每個關鍵字不保存數據,只用來索引,所有數據都保存在葉子節點。
- 所有葉子結點中包含全部關鍵字信息,及對應記錄位置信息及指向含有這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大的順序鏈接。(而B樹的葉子節點并沒有包括全部需要查找的信息)
- 所有非葉子為索引,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (而B樹的非終節點也包含需要查找的有效信息)
- 非葉最底層順序聯結,這樣可以進行順序查找
B+特性
- 所有關鍵字都出現在葉子結點的鏈表中(稠密索引),且鏈表中的關鍵字恰好是有序的;
- 不可能在非葉子結點命中
- 非葉子結點相當于是葉子結點的索引(稀疏索引),葉子結點相當于是存儲(關鍵字)數據的數據層
- 更適合文件索引系統
- B+樹插入操作的平均時間復雜度為O(logn),最壞時間復雜度為O(logn)
查找過程
- 在 B+ 樹上,既可以進行縮小范圍的查找,也可以進行順序查找;
- 在進行縮小范圍的查找時,不管成功與否,都必須查到葉子結點才能結束;
- 若在結點內查找時,給定值≤Ki, 則應繼續在 Ai 所指子樹中進行查找
插入和刪除的操作:類似于B_樹進行,即必要時,也需要進行結點的“分裂”或“合并”。
為什么說B+tree比B樹更適合實際應用中操作系統的文件索引和數據庫索引?
-
B+tree的磁盤讀寫代價更低
- B+tree的內部結點并沒有指向關鍵字具體信息的指針。因此其內部結點相對B 樹更小。如果把所有同一內部結點的關鍵字存放在同一盤塊中,那么盤塊所能容納的關鍵字數量也越多。一次性讀入內存中的需要查找的關鍵字也就越多。相對來說IO讀寫次數也就降低了。
- 舉個例子,假設磁盤中的一個盤塊容納16bytes,而一個關鍵字2bytes,一個關鍵字具體信息指針2bytes。一棵9階B-tree(一個結點最多8個關鍵字)的內部結點需要2個盤快。而B+樹內部結點只需要1個盤快。當需要把內部結點讀入內存中的時候,B樹就比B+樹多一次盤塊查找時間(在磁盤中就是盤片旋轉的時間)。
-
B+tree的查詢效率更加穩定
- 由于非終結點并不是最終指向文件內容的結點,而只是葉子結點中關鍵字的索引。所以任何關鍵字的查找必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,導致每一個數據的查詢效率相當。
B樹和B+樹都是平衡的多叉樹。B樹和B+樹都可用于文件的索引結構。B樹和B+樹都能有效的支持隨機檢索。B+樹既能索引查找也能順序查找.
哈希表
- 在記錄的存儲地址和它的關鍵字之間建立一個確定的對應關系;這樣不經過比較,一次存取就能得到元素。
- 哈希函數——在記錄的關鍵字與記錄的存儲位置之間建立的一種對應關系。是從關鍵字空間到存儲位置空間的一種映象。
- 哈希表——應用哈希函數,由記錄的關鍵字確定記錄在表中的位置信息,并將記錄根據此信息放入表中,這樣構成的表叫哈希表。
- Hash查找適合于關鍵字可能出現的值的集合遠遠大于實際關鍵字集合的情形。
- 更適合查找,不適合頻繁更新
- Hash表等查找復雜依賴于Hash值算法的有效性,在最好的情況下,hash表查找復雜度為O(1)。只有無沖突的hash_table復雜度才是O(1)。一般是O(c),c為哈希關鍵字沖突時查找的平均長度。插入,刪除,查找都是O(1)。平均查找長度不隨表中結點數目的增加而增加,而是隨負載因子的增大而增大
- 由于沖突的產生,使得哈希表的查找過程仍然是一個給定值與關鍵字比較的過程。
根據抽屜原理,沖突是不可能完全避免的,所以,選擇好的散列函數和沖突處理方法:
- 構造一個性能好,沖突少的Hash函數
- 如何解決沖突
常用的哈希函數
- 直接定址法。僅適合于:地址集合的大小 == 關鍵字集合的大小
- 數字分析法。對關鍵字進行分析,取關鍵字的若干位或其組合作哈希地址。僅適合于:能預先估計出全體關鍵字的每一位上各種數字出現的頻度。
- 平方取中法。以關鍵字的平方值的中間幾位作為存儲地址。
- 折疊法。將關鍵字分割成位數相同的幾部分,然后取這幾部分的疊加和(舍去進位)做哈希地址。移位疊加/間界疊加。適合于: 關鍵字的數字位數特別多,且每一位上數字分布大致均勻情況。
- 除留余數法。取關鍵字被某個不大于哈希表表長m的數p除后所得余數作哈希地址,即H(key)=key%p,p<=m。
- 隨機數法。取關鍵字的偽隨機函數值作哈希地址,即H(key)=random(key),適于關鍵字長度不等的情況。
沖突解決
-
開放定址法。當沖突發生時,形成一個探查序列;沿此序列逐個地址探查,直到找到一個空位置(開放的地址),將發生沖突的記錄放到該地址中。即Hi=(H(key)+di) % m,i=1,2,……k(k<=m-1),H(key)哈希函數,m哈希表長,di增量序列。缺點:刪除:只能作標記,不能真正刪除;溢出;載因子過大、解決沖突的算法選擇不好會發生聚集問題。要求裝填因子α較小,故當結點規模較大時會浪費很多空間。
- 線性探測再散列:di=1,2,3,…,m-1
- 二次探測再散列:di=12,-12,22,-22,…,±k2(k<=m/2)
- 偽隨機探測再散列: di為偽隨機數序列
鏈地址法:將所有關鍵字為同義詞的記錄存儲在一個單鏈表中,并用一維數組存放頭指針。拉鏈法中可取α≥1,且結點較大時,拉鏈法中增加的指針域可忽略不計,因此節省空間。一旦發生沖突,在當前位置給單鏈表增加結點就行。
其他方法:再哈希法、建立公共溢出區
在用拉鏈法構造的散列表中,刪除結點的操作易于實現。拉鏈法的缺點是:指針需要額外的空間,故當結點規模較小時,開放定址法較為節省空間。由于拉鏈法中各鏈表上的結點空間是動態申請的,故它更適合于造表前無法確定表長的情況。拉鏈法解決沖突時,需要使用指針,指示下一個元素的存儲位置
開哈希表–鏈式地址法;閉哈希表–開放地址法.開哈希和閉哈希主要的區別在于,隨著哈希表的密集度提高,使用閉哈希時,不僅會與相同哈希值的元素發生沖突,還容易與不同哈希值的元素發生沖突;而開哈希則不受哈希表疏密與否的影響,始終只會與相同哈希值的元素沖突而已。所以在密集度變大的哈希表中查找時,顯然開哈希的平均搜索長度不會增長。
設有n個關鍵字具有相同的Hash函數值,則用線性探測法把這n個關鍵字映射到Hash表中需要做n(n-1)/2次線性探測。如果使用二次探測再散列法將這n個關鍵字存入哈希表,至少要進行n(n+1)/2次探測
Hash查找效率:裝填因子=表中記錄數/表容量
有B+Tree/Hash_Map/STL Map三種數據結構。對于內存中數據,查找性能較好的數據結構是Hash_Map,對于磁盤中數據,查找性能較好的數據結構是B+Tree。Hash操作能根據散列值直接定位數據的存儲地址,設計良好的hash表能在常數級時間下找到需要的數據,但是更適合于內存中的查找。B+樹是一種是一種樹狀的數據結構,適合做索引,對磁盤數據來說,索引查找是比較高效的。STL_Map的內部實現是一顆紅黑樹,但是只是一顆在內存中建立二叉樹樹,不能用于磁盤操作,而其內存查找性能也比不上Hash查找。
內部排序
- 內部排序:全部數據可同時放入內存進行的排序。
- 外部排序:文件中數據太多,無法全部調入內存進行的排序。
插入類:
- 直接插入排序。最壞情況是數據遞減序,數據比較和移動量最大,達到O(n2),最好是數據是遞增序,比較和移動最少為O(n)。趟數是固定的n-1,即使有序,也要依次從第二個元素開始。排序趟數不等于時間復雜度。
- 折半插入排序 。由于插入第i個元素到r[1]到r[i-1]之間時,前i個數據是有序的,所以可以用折半查找確定插入位置,然后插入。
- 希爾排序。縮小增量排序。5-3-1。在實際應用中,步長的選取可簡化為開始為表長n的一半(n/2),以后每次減半,最后為1。插入的改進,最后一趟已基本有序,比較次數和移動次數相比直接插入最后一趟更少
交換類:
-
冒泡排序。O(n2)通常認為冒泡是比較差的,可以加些改進,比如在一趟中無數據的交換,則結束等措施。
- 在數據已基本有序時,冒泡是一個較好的方法
- 在數據量較少時(15個左右)可以用冒泡
-
快速排序。
- 時間復雜度。最好情況:每次支點總在中間,O(nlog2n),平均O(nlog2n)。最壞,數據已是遞增或遞減,O(n2)。pivotkey的選擇越靠近中央,即左右兩個子序列長度越接近,排序速度越快。越無序越快。
- 空間復雜度。需棧空間以實現遞歸,最壞情況:S(n)=O(n);一般情況:S(n)=O(log2n)
- 在序列已是有序的情況下,時間復雜度最高。原因:支點選擇不當。改進:隨機選取支點或最左、最右、中間三個元素中的值處于中間的作為支點,通常可以避免最壞情況。所以,快速排序在表已基本有序的情況下不合適。
- 在序列長度已較短時,采用直接插入排序、起泡排序等排序方法。序列的個數通常取10左右。
選擇類排序:
- 簡單選擇排序。O(n2)。總比較次數n(n-1)/2。
- 堆排序。建堆 O(n),篩選排序O(nlogn)。找出若干個數中最大/最小的前K個數,用堆排序是最好。小根堆中最大的數一定是放在葉子節點上,堆本身是個完全二叉樹,完全二叉樹的葉子節點的位置大于[n/2]。時間復雜度不會因為待排序序列的有序程度而改變,但是待排序序列的有序程度會影響比較次數。
- 歸并排序。時間:與表長成正比,若一個表表長是m,另一個是n,則時間是O(m+n)。單獨一個數組歸并,時間:O(nlogn),空間:O(n),比較次數介于(nlogn)/2和(nlogn)-n+1,賦值操作的次數是(2nlogn)。歸并排序算法比較占用內存,但卻是效率高且穩定的排序算法。在外排序中使用。歸并的趟數是logn。
- 基數排序。在一般情況下,每個結點有 d 位關鍵字,必須執行 t = d次分配和收集操作。分配的代價:O(n);收集的代價:O(rd) (rd是基數);總的代價為:O( d ×(n + rd))。適用于以數字和字符串為關鍵字的情況。
- 枚舉排序,通常也被叫做秩排序,比較計數排序。對每一個要排序的元素,統計小于它的所有元素的個數,從而得到該元素在整個序列中的位置,時間復雜度為O(n2)
比較法分類的下界:O(nlogn)
排序算法的一些特點:
- 堆排序、冒泡排序、快速排序在每趟排序過程中,都會有一個元素被放置在其最終的位置上。
- 有字符序列 {Q,H,C,Y,P,A,M,S,R,D,F,X} ,新序列{F,H,C,D,P,A,M,Q,R,S,Y,X},是快速排序算法一趟掃描的結果。(拿Q作為分割點,快速排序一輪。二路歸并,第一趟排序,得到 n / 2 個長度為 2 的各自有序的子序列,第二趟排序,得到 n / 4 個長度為 4 的各自有序的子序列H Q C Y A P M S D R F X。如果是快速排序的話,第一個元素t將會被放到一個最準確的位置,t前的數均小于t,后面的數均大于t。希爾排序每個小分組內將會是有序的。堆排序,把它構成一顆二叉樹的時候,該堆要么就是大根堆,要么就是小根堆,第一趟Y排在最后;冒泡,那么肯定會有數據下沉的動作,第一趟有A在第一位。)
- 在文件”局部有序”或文件長度較小的情況下,最佳內部排序的方法是直接插入排序。(歸并排序要求待排序列已經部分有序,而部分有序的含義是待排序列由若干有序的子序列組成,即每個子序列必須有序,并且其時間復雜度為O(nlog2n);直接插入排序在待排序列基本有序時,每趟的比較次數大為降低,即n-1趟比較的時間復雜度由O(n^2)降至O(n)。在待排序的元素序列基本有序或者每個元素距其最終位置不遠也可用插入排序,效率最高的排序方法是插入排序)
- 排序趟數與序列的原始狀態有關的排序方法是優化冒泡和快速排序法。(插入排序和選擇排序不管序列的原始狀態是什么都要執行n-1趟,優化冒泡和快排不一定。仔細理解
排序的次數
和比較次數
的區別) - 不穩定的排序方法:快排,堆排,希爾,選擇
- 要與關鍵字的初始排列次序無關,那么就是最好、最壞、一般的情況下排序時間復雜度不變, 總共有堆排序,歸并排序,選擇排序,基數排序
- 快速排序、Shell 排序、歸并排序、直接插入排序的關鍵碼比較次數與記錄的初始排列有關。折半插入排序、選擇排序無關。(直接插入排序在完全有序的情況下每個元素只需要與他左邊的元素比較一次就可以確定他最終的位置;折半插入排序,比較次數是固定的,與初始排序無關;快速排序,初始排序不影響每次劃分時的比較次數,都要比較n次,但是初始排序會影響劃分次數,所以會影響總的比較次數,但快排平均比較次數最小;歸并排序在歸并的時候,如果右路最小值比左路最大值還大,那么只需要比較n次,如果右路每個元素分別比左路對應位置的元素大,那么需要比較2*n-1次,所以與初始排序有關)
- 精儉排序,即一對數字不進行兩次和兩次以上的比較,插入和歸并是“精儉排序”。插入排序,前面是有序的,后面的每一個元素與前面有序的元素比較,比較過的就是有序的了,不會再比較一次。歸并每次合并后,內部都是有序的,內部的元素之間不用再比較。選擇排序,每次在后面的元素中找到最小的,找最小元素的過程是在沒有排好序的那部分進行,所有肯定會比較多次。堆排序也需比較多次。
外部排序
生成合并段(run):讀入文件的部分記錄到內存->在內存中進行內部排序->將排好序的這些記錄寫入外存,形成合并段->再讀入該文件的下面的記錄,往復進行,直至文件中的記錄全部形成合并段為止。
外部合并:將上一階段生成的合并段調入內存,進行合并,直至最后形成一個有序的文件。
外部排序指的是大文件的排序,即待排序的記錄存儲在外存儲器上,待排序的文件無法一次裝入內存,需要在內存和外部存儲器之間進行多次數據交換,以達到排序整個文件的目的。外部排序最常用的算法是多路歸并排序,即將原文件分解成多個能夠一次性裝入內存的部分,分別把每一部分調入內存完成排序。然后,對已經排序的子文件進行多路歸并排序
不管初始序列是否有序, 冒泡、選擇排序時間復雜度是O(n^2),歸并、堆排序時間復雜度是O(nlogn)
外部排序的總時間 = 內部排序(產出初始歸并段)所需時間 + 外存信息讀取時間 + 內部歸并所需的時間
外排中使用置換選擇排序的目的,是為了增加初始歸并段的長度。減少外存讀寫次數需要減小歸并趟數
根據內存容量設若干個輸入緩沖區和一個輸出緩沖區。若采用二路歸并,用兩個輸入緩沖。
歸并的方法類似于歸并排序的歸并算法。增加的是對緩沖的監視,對于輸入,一旦緩沖空,要到相應文件讀后續數據,對于輸出緩沖,一旦緩沖滿,要將緩沖內容寫到文件中去。
外排序和內排序不只是考慮內外排序算法的性能,還要考慮IO數據交換效率的問題,內存存取速度遠遠高于外存。影響外排序的時間因素主要是內存與外設交換信息的總次數
有效的算法設計
- 貪心法。Dijkstra的最短路徑(時間復雜度O(n2));Prim求最小生成樹鄰接表存儲時是O(n+e),圖O(n2);關鍵路徑及關鍵活動的求法。
- 回溯法
- 分支限界法
- 分治法。分割、求解、合并。二分查找、歸并排序、快速排序。
- 動態規劃。Floyd-Warshall算法求解圖中所有點對之間最短路徑時間復雜度為O(n3)
動態規劃解題的方法是一種高效率的方法,其時間復雜度通常為O(n2),O(n3)等,可以解決相當大的信息量。(數塔在n<=100層時,可以在很短的時間內得到問題解)
- 適用的原則:原則為優化原則,即整體優化可以分解為若干個局部優化。
- 動態規劃比窮舉法具有較少的計算次數
- 遞歸算法需要很大的棧空間,而動態規劃不需要棧空間
貪心和動態規劃的差別:
- 所謂貪心選擇性質是指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。
- 在動態規劃算法中,每步所作的選擇往往依賴于相關子問題的解。因而只有在解出相關子問題后,才能作出選擇。而在貪心算法中,僅在當前狀態下作出最好選擇,即局部最優選擇。然后再去解作出這個選擇后產生的相應的子問題。
- 貪心算法所作的貪心選擇可以依賴于以往所作過的選擇,但決不依賴于將來所作的選擇,也不依賴于子問題的解。正是由于這種差別,動態規劃算法通常以自底向上的方式解各子問題,而貪心算法則通常以自頂向下的方式進行,以迭代的方式作出相繼的貪心選擇,每作一次貪心選擇就將所求問題簡化為一個規模更小的子問題。
P問題
- P問題,如果它可以通過運行多項式次(即運行時間至多是輸入量大小的多項式函數的一種算法獲得解決),可以找到一個能在多項式的時間里解決它的算法。—-確定性問題
- NP問題,雖然可以用計算機求解,但是對于任意常數k,它們不能在O(nk)時間內得到解答,可以在多項式的時間里驗證一個解的問題。所有的P類問題都是NP問題。
- NP完全問題,知道有效的非確定性算法,但是不知道是否存在有效的確定性算法,同時,不能證明這些問題中的任何一個不存在有效的確定性算法。這類問題稱為NP完全問題。