《大話資料結構》——學習筆記(棧&串&樹)
棧
棧的定義
棧(stack)是限定僅在表尾進行插入和刪除操作的線性表
棧是一種後進先出(Last In First Out)的線性表,簡稱LIFO結構
棧的順序儲存結構與鏈式儲存結構
棧的順序儲存結構如下圖
棧的鏈式儲存結構如下圖
比較:
- 順序棧與鏈棧在時間複雜度上是一樣的,均為O(1)
- 對於空間效能,順序棧需要事先確定一個固定的長度,可能會存在記憶體空間浪費的問題,但它的優勢是存取時定位很方便,而鏈棧則要求每個元素都有指標域,這同時也增加了一些記憶體開銷,但對於棧的長度無限制
- 如果棧的使用過程中元素變化不可預料,有時很小,有時非常大,那麼最好是用鏈棧,反之,如果它的變化在可控範圍內,建議使用順序棧
棧的應用——四則運算表示式求值
程式中解決四則運算是比較麻煩的,因為計算有優先順序,波蘭邏輯學家發明了一種不需要括號的字尾表達法,稱為逆波蘭表示
如
9 + (3 - 1) x 3 + 10 ÷ 2
轉換成字尾表示式為
9 3 1 - 3 * + 10 2 / +
轉換規則:
從左到右遍歷表示式的每個數字和符號,若是數字就輸出,即成為字尾表示式的一部分,若是符號,則判斷其與棧頂符號的優先順序,是右括號或優先順序低於棧頂符號(乘除優先加減)則棧頂元素依次出棧並輸出,並將當前符號進棧,一直到最終輸出字尾表示式為止
計算規則:
從左到右遍歷表示式的每個數字和符號,遇到是數字就進棧,遇到是符號,就將處於棧頂兩個數字出棧,進行運算,運算結果進棧,一直到最終獲得結果
佇列
佇列的定義
佇列(queue)是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表
佇列是一種先進先出(First In First Out)的線性表,簡稱FIFO
允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭
佇列的順序儲存結構——迴圈佇列
佇列的頭尾相接的順序儲存結構稱為迴圈佇列
佇列的鏈式儲存結構
佇列的鏈式儲存結構,就是線性表的單鏈表,只不過它只能尾進頭出
比較
- 迴圈佇列與鏈佇列的時間複雜度都為O(1)
- 迴圈佇列需要事先申請好空間,使用期間不釋放,而對於鏈佇列,每次申請和釋放結點也會存在一些時間開銷
- 對於空間上來說,迴圈佇列必須有一個固定的長度,所以就有了儲存元素個數和空間浪費的問題,而鏈佇列不存在這個問題,儘管它需要一個指標域,會產生一些空間上的開銷,但也可以接受
- 在可以確定佇列長度最大值的情況下,建議用迴圈佇列,如果無法預估佇列的長度時,則用鏈佇列
串
串(string)是由零個或多個字元組成的有限序列,又名叫字串
串的比較
串的比較是通過組成串的字元之間的編碼來進行的,而字元的編碼指的是字元在對應字符集中的序號(如ASCII值)
串的儲存結構
串的儲存結構與線性表相同,分為兩種:串的順序儲存結構和串的鏈式儲存結構
串的順序儲存結構
串的順序儲存結構是用一組地址連續的儲存單元來儲存串中的字元序列,按照預定義的大小,為每個定義的串變數分配一個固定長度的儲存區,一般是用定長陣列來定義
串的鏈式儲存結構
串結構中的每個元素資料是一個字元,如果一個結點對應一個字元,就會存在很大的空間浪費,因此可以考慮一個結點存放多個字元,最後一個結點若是未被佔滿時,可以用”#”或其他非串值字元補全,如下圖所示
每個結點存多少個字元會直接影響串處理的效率,需要根據實際情況做出選擇
串的鏈式儲存結構除了在連線串與串操作時有一定方便之外,總的來說不如順序儲存靈活,效能也不如順序儲存結構好
樸素的模式匹配演算法
子串的定位操作通常稱做串的模式匹配,如從主串S=”goodgoogle”中,找到子串T=”google”這個子串的位置,通常需要下面的步驟
- 主串S第一位開始匹配,匹配失敗
- 主串S第二位開始匹配,匹配失敗
- 主串S第三位開始匹配,匹配失敗
- 主串S第四位開始匹配,匹配失敗
- 主串S第五位開始匹配,S與T,6個字母全匹配,匹配成功
時間複雜度為O(n+m),其中n為主串長度,m為要匹配的子串長度
極端情況下,主串為S=”00000000000000000000000001”,子串為T=”0001”,在匹配時,每次都得將T中字元迴圈到最後一位才發現不匹配,此時的時間複雜度為O((n-m+1)*m)
樹
樹的定義
樹(Tree)是n(n≥0)個結點的有限集合。n=0時稱為空樹,在任意一棵非空樹中:
- 有且僅有一個特定的稱為根(Root)的結點
- 當n>1時,其餘結點可分為m(m>0)個互不相交的有限集T1、T2…Tm,其中每一個集合本身又是一棵樹,並且稱為根的子樹(SubTree)
結點分類
樹的結點包含一個數據元素及若干指向其子樹的分支,結點擁有的子樹數稱為結點的度(Degree),度為0的結點稱為葉結點(Leaf)或終端結點;度不為0的結點稱為非終端結點或分支結點
樹的度是樹內各結點的度的最大值
結點間關係
結點的子樹的根稱為該結點的孩子(Child),相應地,該結點稱為孩子的雙親(Parent)
同一個雙親的孩子之間互稱兄弟(Sibling)
樹的其他相關概念
結點層次(Level)從根開始定義起,根為第一層,根的孩子為第二層。若某結點在第i層,則其子樹的根就在第i+1層
在同一層的結點互為兄弟
如果將樹中結點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹
森林(Forest)是m(m≥0)棵互不相交的樹的集合
線性結構與樹結構對比
線性結構
- 第一個資料元素:無前驅
- 最後一個數據元素:無後繼
- 中間元素:一個前驅一個後繼
樹結構
- 根結點:無雙親,唯一
- 葉結點:無孩子,可以多個
- 中間結點:一個雙親多個孩子
樹的儲存結構
雙親表示法
在每個結點中,附設一個指示器指示其雙親結點到連結串列中的位置
該儲存方式根據結點的parent指標很容易找到它的雙親結點,時間複雜度為O(1)
缺點: 如果需要知道某個結點的所有孩子,需要遍歷整棵樹
孩子表示法
把每個結點的孩子結點排列起來,以單鏈表作儲存結構,則n個結點有n個孩子連結串列,如果是葉子結點則此單鏈表為空,然後n個頭指標又組成一個線性表,採用順序儲存結構,存放進一個一維陣列中,如下圖所示
缺點: 如果需要知道某個結點的雙親,需要遍歷整棵樹
改進: 雙親孩子表示法
孩子兄弟表示法
任意一棵樹,它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此,可以設定兩個指標,分別指向該結點的第一個孩子和此結點的右兄弟
這個表示法的最大好處是它把一棵複雜的樹變成了一棵二叉樹
二叉樹
二叉樹的定義
二叉樹(Binary Tree)是n(n≥0)個結點的有限集合,該集合或者為空集(稱為空二叉樹),或者由一個根結點和兩棵互不相交的、分別稱為根結點的左子樹和右子樹的二叉樹組成
二叉樹特點
- 每個結點最多有兩棵子樹,所以二叉樹中不存在度大於2的結點
- 左子樹和右子樹是有順序的,次序不能任意顛倒
- 即使樹中某結點只有一棵子樹,也要區分它是左子樹還是右子樹
特殊的二叉樹
- 斜樹(左斜樹、右斜樹)
- 滿二叉樹
完全二叉樹
- 對一棵具有n個結點的二叉樹按層序編號,如果編號為i(1≤i≤n)的結點與同樣深度的滿二叉樹中編號為i的結點在二叉樹位置完全相同,則這棵二叉樹稱為完全二叉樹
滿二叉樹一定是一棵完全二叉樹,但完全二叉樹不一定是滿二叉樹
二叉樹的性質
- 在二叉樹的第i層上至多有
2(i−1) 個結點(i≥1) - 深度為k的二叉樹至多有
2k−1 個結點(k≥1) - 對任何一棵二叉樹T,其葉子結點數=度為2的結點數+1
- 具有n個結點的完全二叉樹的深度不大於
log2n +1的最大整數 如果對一棵有n個結點的完全二叉樹的結點按層序編號(每層從左到右),對任一結點i(1≤i≤n)有:
- 如果i=1,則結點i是二叉樹的根,無雙親;如果i>1,則其雙親是結點i/2
- 如果2i>n,則結點i無左孩子(結點i為葉子結點);否則其左孩子是結點2i
- 如果2i+1>n,則結點i無右孩子;否則其右孩子是結點2i+1
二叉樹的儲存結構
二叉樹順序儲存結構
二叉樹的順序儲存結構就是用一維陣列儲存二叉樹中的結點,並且結點的儲存位置,也就是陣列的下標要能體現結點之間的邏輯關係,比如雙親與孩子的關係,左右兄弟的關係等
上圖淺色代表不存在的結點,不存在的結點用^表示,會造成對儲存空間的浪費,所以順序儲存結構一般只用於完全二叉樹
二叉連結串列
二叉樹每個結點最多有兩個孩子,所以為它設計一個數據域和兩個指標域
遍歷二叉樹
二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹中所有結點,使得每個結點被訪問一次且僅被訪問一次
前序遍歷
先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹
中序遍歷
從根結點開始(並不是先訪問根結點),中序遍歷根結點的左子樹,然後是訪問根結點,最後中序遍歷右子樹
後序遍歷
從左到右先葉子後結點的方式遍歷訪問左右子樹,最後是訪問根結點
層序遍歷
從樹的第一層,也就是根結點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問
線索二叉樹
在二叉連結串列上,只能知道每個結點指向其左右孩子結點的地址,而不知道某個結點的前驅是誰,後繼是誰,可以利用如下結構,存放指向結點在某種遍歷次序下的前驅和後繼結點的地址
這種指向前驅和後繼的指標稱為線索,加上線索的二叉連結串列稱為線索連結串列,相應的二叉樹稱為線索二叉樹
- ltag為0時指向該結點的左孩子,為1時指向該結點的前驅
- rtag為0時指向該結點的右孩子,為1時指向該結點的後繼
如果所用的二叉樹需經常遍歷或查詢結點時需要某種遍歷序列中的前驅和後繼,就比較適合用線索二叉連結串列的儲存結構
樹、森林與二叉樹的轉換
樹轉換為二叉樹
將樹轉換為二叉樹的步驟如下
- 加線,在所有兄弟結點之間加一條連線
- 去線,對樹中每個結點,只保留它與第一個孩子結點的連線,刪除它與其他孩子結點之間的連線
- 層次調整,以樹的根結點為軸心,將整棵樹順時針旋轉一定的角度,使之結構層次分明,注意第一個孩子是二叉樹結點的左孩子,兄弟轉換過來的孩子是結點的右孩子
森林轉換為二叉樹
森林是由若干棵樹組成的,所以完全可以理解為,森林中的每一棵樹都是兄弟,可以按照兄弟的處理辦法來操作
步驟如下:
- 1.把每個樹轉換為二叉樹
- 2.第一棵二叉樹不動,從第二棵二叉樹開始,依次把後一棵二叉樹的根結點作為前一棵二叉樹的根結點的右孩子,用線連線起來,當所有的二叉樹連線起來後就得到了由森林轉換來的二叉樹
二叉樹轉換為樹
二叉樹轉換為樹是樹轉換為二叉樹的逆過程
步驟如下:
- 1.加線,若某結點的左孩子結點存在,則將這個左孩子的右孩子結點、右孩子的右孩子結點…都作為此結點的孩子,將該結點與這些右孩子結點用線連線起來
- 2.去線,刪除原二叉樹中所有結點與其右孩子結點的連線
- 層次調整,使之結構層次分明
二叉樹轉換為森林
判斷一棵二叉樹能夠轉換成一棵樹還是森林,標準很簡單,只要看這棵二叉樹的根結點有沒有右孩子,有就是森林,沒有就是一棵樹,轉換成森林的步驟如下:
- 1.從根結點開始,若右孩子存在,則把與右孩子結點的連線刪除,再檢視分離後的二叉樹,若右孩子存在,則連線刪除…,直到所有右孩子連線都刪除為止,得到分離的二叉樹
- 2.再將每棵分離後的二叉樹轉換為樹即可
樹與森林的遍歷
樹的遍歷分為兩種方式
- 一種是先根遍歷樹,即先訪問樹的根結點,然後依次先根遍歷根的每棵子樹,如下圖遍歷結果為ABEFCDG
- 另一種是後根遍歷,即先依次後根遍歷每棵子樹,然後再訪問根結點,如下圖遍歷結果為EFBCGDA
森林的遍歷也分為兩種方式
- 前序遍歷:先訪問森林中第一棵樹的根結點,然後再依次先根遍歷根的每棵子樹,再依次用同樣方式遍歷除去第一棵樹的剩餘樹構成的森林,如下圖遍歷結果為ABCDEFGHJI
- 後序遍歷:是先訪問森林中第一棵樹,後根遍歷的方式遍歷每棵子樹,然後再訪問根結點,再依次同樣方式遍歷除去第一棵樹的剩餘樹構成的森林,如下圖遍歷結果為BCDAFEJHIG
森林的前序遍歷和二叉樹的前序遍歷結果相同,森林的後序遍歷和二叉樹的中序遍歷結果相同
赫夫曼樹及其應用
赫夫曼樹定義與原理
從樹中一個結點到另一個結點之間的分支構成兩個結點之間的路徑,路徑上的分支數目稱做路徑長度
樹的路徑長度就是從樹根到每一結點的路徑長度之和
如果考慮到帶權的結點,結點的帶權的路徑長度為從該結點到樹根之間的路徑長度與結點上權的乘積,樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和
帶權路徑長度WPL最小的二叉樹稱做赫夫曼樹
二叉樹a的 WPL = 5x1+15x2+40x3+30x4+10x4 = 315
二叉樹b的 WPL = 5x3+15x3+40x2+30x2+10x2 = 220
構造赫夫曼樹的步驟:
- 1.先把有權值的葉子結點按照從小到大的順序排列成一個有序序列,即:A5,E10,B15,D30,C40
- 2.取頭兩個最小權值的結點作為一個新結點
N1 的兩個子結點,相對較小的是左孩子 - 3.將
N1 替換A與E,插入有序序列中,保持從小到大排列,即N115 ,B15,D30,C40 - 4.重複步驟2,3,直到只含一棵樹為止
此時構造出來的赫夫曼樹的 WPL = 40x1+30x2+15x3+10x4+5x4 = 205