棧
棧的定義
棧(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