資料結構基礎
程式碼效率優化
複雜度 -- 一個關於輸入資料量n的函式
-
時間複雜度 -- 昂貴
-
與程式碼的結構設計有著緊密關係
-
一個順序結構的程式碼,時間複雜度是O(1), 即任務與算例個數 n 無關
-
空間複雜度 -- 廉價
-
與資料結構設計有關
資料結構 -- 考慮如何去組織計算機中一定量的資料。
資料結構連線時空,用空間換取時間。
資料處理 -- 瞭解問題,明確資料操作方法,設計出更加高效的資料結構型別
-
找到需要處理的資料,計算結果,再把結果儲存下來
-
把結果存到新的記憶體空間中
-
把結果存到已使用的記憶體空間中
-
基本操作只有三個:增,刪,查
-
增和刪可以細分為資料結構的中間以及最後的增和刪
-
查詢可以細分為按照位置條件查詢和資料數值特徵查詢
-
所有資料處理都是這些基本操著的組合和疊加
-
只有字典型別資料結構能在 O(1) 的時間複雜度內完成查詢動作
-
迴歸問題本源,明確資料被處理的動作,來解決資料結構的問題
線性表
n 個具有相同特性的元素的有限序列,Linear List
資料元素之間的關係是一對一的關係
-
即除了頭尾元素外,其它資料元素都是首尾相接的
-
這句話只適用大部分線性表,而不是全部
-
比如,迴圈連結串列尾的指標指向首位結點
實現方式
-
最常用的是鏈式表達,也叫線性連結串列或連結串列
-
每個結點包括具體的資料值和指向下一個結點的指標
-
單向連結串列,迴圈連結串列,雙向連結串列,雙向迴圈連結串列
-
新增和刪除為 O(1) 時間複雜度,而查詢為 O(n)
-
適合資料元素個數不確定,且經常進行新增和刪除
-
連結串列的翻轉,快慢指標的方法,是必須掌握的內容
-
使用陣列實現,也叫順序儲存,順序表
類別
-
一般線性表,可以自由的刪除和新增結點
-
受限線性表,主要包含棧和佇列
棧和佇列是特殊的線性表,本質上他們都可以被看作是一類基本結構
線性表案例
-
連結串列的翻轉
-
快慢指標
-
查詢奇數個數的連結串列的中間位置結點的數值
-
判斷連結串列是否有環
棧
後進先出的(限制後的)線性表,Last In First Out, Stack.
新增和刪除操作只能在這個線性表的表尾進行,即線上性表基礎上加了限制
-
新增: 壓棧 push, which adds an element to the collection
-
刪除: 出棧 pop, which removes the most recently added element
功能上,陣列或者連結串列可以代替棧,但它們靈活性過高,資料量大時有風險
棧頂和棧底是用來表示這個棧的兩個指標
-
棧頂 (top) 是表尾,用來輸入資料
-
棧底 (bottom) 是表頭,用來鎖定棧內的某一單元的地址
棧有順序表示和鏈式表示,分別稱作順序棧和鏈棧
-
順序棧
-
陣列的首元素存在棧底,尾元素放在棧頂
-
定義指標 top 來指示棧頂元素在陣列的位置
-
可以藉助陣列來實現
-
棧中只有一個元素,則 top = 0
-
以 top 是否為 -1 來判定是否為空棧
-
棧頂 top 需小於棧的最大容量
-
出棧操作,只需要 top - 1 即可
-
鏈式棧
-
通常把棧頂放在單連結串列的頭部
-
top 指標替換了連結串列原來的尾指標,去掉了頭指標
-
用連結串列的方式實現
-
出棧操作,將 top 指標指向棧頂元素的 next 指標即可
-
對比棧和一般線性表
-
操作原理相似
-
時間複雜度一樣
-
都依賴當前位置指標進行資料物件的操作
-
相同點:
-
區別:棧只能新增和刪除棧頂的資料結點
棧的案例
-
判斷括號字串是否合法
-
瀏覽器頁面訪問的後退和前進
佇列
先進先出 (限制後的) 線性表, First In First Out, Queue
新增和刪除操作只能分別在隊尾和隊頭進行
-
先進 - 佇列的資料新增操作只能在末端進行, add
-
不允許在佇列的中間某個結點後新增資料
-
先出 - 佇列的資料刪除操作只能在始端進行, remove
-
不允許在佇列的中間某個結點後刪除資料
佇列適合面對資料處理順序非常敏感的問題
-
可以確定佇列長度最大值, 建議使用迴圈佇列
-
無法確定佇列長度時, 應考慮使用鏈式佇列
front 和 rear 兩個指標
-
隊頭 (front), 用來刪除資料
-
隊尾 (rear), 用來增加資料
佇列有兩種儲存方式, 即順序佇列和鏈式佇列
-
順序佇列
-
必須有一個固定的長度
-
實現刪除的時間複雜度為 O(1)
-
使用 flag 來判斷佇列空或滿
-
每次刪除都需要把整個陣列前移
-
時間複雜度為 O(n)
-
尾指標會向後移動
-
時間複雜度為 O(1)
-
資料在記憶體中也是順序儲存
-
依賴陣列來實現
-
進行新增插入操作時,
-
如果只刪除頭的第一個元素時
-
使用迴圈佇列
-
鏈式佇列
-
讓 front 指標指向頭結點
-
頭結點不儲存資料, 只是輔助標識
-
資料依賴每個結點的指標互聯
-
是離散儲存線性結構
-
實際上就是尾進頭出的單鏈線性表
-
在空間上更為靈活
-
依賴連結串列來實現
-
通常會增加一個頭結點
-
當進行資料刪除時, 實際刪除的是頭結點的後繼結點
-
佇列為空時, 頭尾指標都指向頭結點
-
對比佇列和一般線性表
-
佇列繼承了線性表的優點和不足
-
是加了限制的線性表
佇列案例
-
約瑟夫環 - Josephus problem
陣列
陣列可以看成是線性表的一種推廣,它屬於另外一種基本的資料結構
陣列是資料結構中的最基本結構
-
幾乎所有的程式設計語言都把陣列型別設定為固定的基礎變數型別。
-
可以把陣列理解為一種容器,它可以用來存放若干個相同型別的資料元素。
-
例如:
-
存放的資料是整數型的陣列,稱作整型陣列;
-
存放的資料是字元型的陣列,則稱作字元陣列;
-
另外還有一類陣列比較特殊,它是陣列的陣列,也可以叫作二維陣列。
-
可以把普通的陣列看成是一個向量,那麼二維陣列就是一個矩陣。
-
陣列在記憶體中是連續存放的,陣列內的資料,可以通過索引值直接取出得到。
陣列的索引就是對應陣列空間
-
在進行新增、刪除、查詢操作的時候,完全可以根據代表陣列空間位置的索引值進行。
-
只要記錄該陣列頭部的第一個資料位置,然後累加空間位置即可。
陣列的基本操作
具有增刪困難、查詢容易的特點,可以在任意位置增刪資料,所以陣列的增刪操作會更為多樣。
-
新增操作
-
若插入資料在最後,則時間複雜度為 O(1)
-
如果中間某處插入資料,則時間複雜度為 O(n)
-
刪除操作
-
在陣列的最後刪除一個資料元素,則時間複雜度是 O(1)
-
在這個陣列的中間某個位置刪除一條資料, 時間複雜度為 O(n)
-
查詢操作
-
如果只需根據索引值進行一次查詢,時間複雜度是 O(1)
-
要在陣列中查詢一個數值滿足指定條件的資料,則時間複雜度是 O(n)。
對比陣列和連結串列
-
連結串列的長度是可變的,陣列的長度是固定的,在申請陣列的長度時就已經在記憶體中開闢了若干個空間。如果沒有引用
ArrayList
時,陣列申請的空間永遠是我們在估計了資料的大小後才執行,所以在後期維護中也相當麻煩。 -
連結串列不會根據有序位置儲存,進行插入資料元素時,可以用指標來充分利用記憶體空間。陣列是有序儲存的,如果想充分利用記憶體的空間就只能選擇順序儲存,而且需要在不取資料、不刪除資料的情況下才能實現。
陣列的案例
-
基於陣列,計算平均值
字串
由 n 個字元組成的一個有序整體( n >= 0 )
對比字串和線性表
-
字串的邏輯結構和線性表極為相似,區別僅在於串的資料物件約束為字符集。
-
字串的基本操作和線性表有很大差別:
-
線上性表的基本操作中,大多以“單個元素”作為操作物件;
-
在字串的基本操作中,通常以“串的整體”作為操作物件;
-
字串的增刪操作和陣列很像,複雜度也與之一樣。但字串的查詢操作就複雜多了,它是參加面試、筆試常常被考察的內容。
特殊的字串
-
空串,指含有零個字元的串。例如,
s = ""
,書面中也可以直接用Ø
表示。 -
空格串,只包含空格的串。它和空串是不一樣的,空格串中是有內容的,只不過包含的是空格,且空格串中可以包含多個空格。例如,
s = " "
,就是包含了 3 個空格的字串。 -
子串,串中任意連續字元組成的字串叫作該串的子串。
-
原串通常也稱為主串。
字串的儲存結構與線性表相同,也有順序儲存和鏈式儲存兩種
-
字串的順序儲存結構,是用一組地址連續的儲存單元來儲存串中的字元序列,一般是用定長陣列來實現。有些語言會在串值後面加一個不計入串長度的結束標記符,比如 \0 來表示串值的終結。
-
字串的鏈式儲存結構,與線性表是相似的,但由於串結構的特殊性(結構中的每個元素資料都是一個字元),如果也簡單地將每個鏈結點儲存為一個字元,就會造成很大的空間浪費。因此,一個結點可以考慮存放多個字元,如果最後一個結點未被佔滿時,可以使用 "#" 或其他非串值字元補全。
-
每個結點設定字元數量的多少,與串的長度、可以佔用的儲存空間以及程式實現的功能相關。
-
除了在連線串與串操作時有一定的方便之外,不如順序儲存靈活,在效能方面也不如順序儲存結構好。
字串的基本操作
-
新增操作
-
和陣列非常相似,都牽涉對插入字串之後字元的挪移操作,所以時間複雜度是 O(n)。
-
對於特殊的插入操作時間複雜度也可以降低為 O(1)。例如,在 s1 的最後插入 s2,也叫作字串的連線。
-
刪除操作
-
和陣列同樣非常相似,也可能會牽涉刪除字串後字元的挪移操作,所以時間複雜度是 O(n)。
-
對於特殊的刪除操作時間複雜度也可以降低為 O(1)。例如,在 s1 的最後刪除若干個字元,不牽涉任何字元的挪移。
-
查詢操作
-
在字串 A 中查詢字串 B,則 A 就是主串,B 就是模式串。
-
主串的長度記為 n,模式串長度記為 m,則n>m。
-
字串匹配演算法的時間複雜度就是 n 和 m 的函式。
-
子串查詢(字串匹配)
字串匹配演算法的案例
-
查詢出兩個字串的最大公共字串
樹和二叉樹
樹 -- Tree
-
樹結構在存在“一對多”的資料關係中,可被高頻使用,這也是它區別於連結串列系列資料結構的關鍵點。
-
樹是由結點和邊組成的,不存在環的一種資料結構。
-
樹滿足遞迴定義的特性。如果一個資料結構是樹結構,那麼剔除掉根結點後,得到的若干個子結構也是樹,通常稱作子樹。
-
樹的結點的層次從根結點算起,根為第一層,根的“孩子”為第二層,根的“孩子”的“孩子”為第三層,依此類推。
-
樹中結點的最大層次數,就是這棵樹的樹深(稱為深度,也稱為高度)。
二叉樹 -- Binary Tree
二叉樹每個結點最多有兩個子結點,分別稱作左子結點和右子結點。
二叉樹中兩個特殊的型別
-
滿二叉樹,定義為除了葉子結點外,所有結點都有 2 個子結點。
-
完全二叉樹,定義為除了最後一層以外,其他層的結點個數都達到最大,並且最後一層的葉子結點都靠左排列。它方便了順序儲存法的儲存方式。
儲存二叉樹的兩種辦法
-
鏈式儲存法,也就是像連結串列一樣,每個結點有三個欄位,一個儲存資料,另外兩個分別存放指向左右子結點的指標。
-
順序儲存法,就是按照規律把結點存放在陣列裡。
樹的基本操作
遍歷
-
前序遍歷,對樹中的任意結點來說,先列印這個結點,然後前序遍歷它的左子樹,最後前序遍歷它的右子樹。
public static void preOrderTraverse(Node node) {
if (node == null)
return;
System.out.print(node.data + " ");
preOrderTraverse(node.left);
preOrderTraverse(node.right);
} -
中序遍歷,對樹中的任意結點來說,先中序遍歷它的左子樹,然後列印這個結點,最後中序遍歷它的右子樹。
public static void inOrderTraverse(Node node) {
if (node == null)
return;
inOrderTraverse(node.left);
System.out.print(node.data + " ");
inOrderTraverse(node.right);
} -
後序遍歷,對樹中的任意結點來說,先後序遍歷它的左子樹,然後後序遍歷它的右子樹,最後列印它本身。
public static void postOrderTraverse(Node node) {
if (node == null)
return;
postOrderTraverse(node.left);
postOrderTraverse(node.right);
System.out.print(node.data + " ");
}
二叉樹的增刪查操作很普通,時間複雜度與連結串列並沒有太多差別
二叉查詢樹 -- Binary Search Tree, BST
特性
-
在二叉查詢樹中的任意一個結點,其左子樹中的每個結點的值,都要小於這個結點的值。
-
在二叉查詢樹中的任意一個結點,其右子樹中每個結點的值,都要大於這個結點的值。
-
在二叉查詢樹中,會盡可能規避兩個結點數值相等的情況。
-
對二叉查詢樹進行中序遍歷,就可以輸出一個從小到大的有序資料佇列。
查詢操作 -- 利用了“二分查詢”,所消耗的時間複雜度為 O(logn)
。
-
首先判斷根結點是否等於要查詢的資料,如果是就返回。
-
如果根結點大於要查詢的資料,就在左子樹中遞迴執行查詢動作,直到葉子結點。
-
如果根結點小於要查詢的資料,就在右子樹中遞迴執行查詢動作,直到葉子結點。
插入操作
-
插入操作很簡單。從根結點開始,如果要插入的資料比根結點的資料大,且根結點的右子結點不為空,則在根結點的右子樹中繼續嘗試執行插入操作。直到找到為空的子結點執行插入動作。
-
二叉查詢樹插入資料的時間複雜度是
O(logn)
。這裡的時間複雜度更多是消耗在了遍歷資料去找到查詢位置上,真正執行插入動作的時間複雜度仍然是 O(1)。
刪除操作
-
情況一,如果要刪除的結點是某個葉子結點,則直接刪除,將其父結點指標指向 null 即可。
-
情況二,如果要刪除的結點只有一個子結點,只需要將其父結點指向的子結點的指標換成其子結點的指標即可。
-
情況三,如果要刪除的結點有兩個子結點,則有兩種可行的操作方式:
-
第一種,找到這個結點的左子樹中最大的結點,替換要刪除的結點。
-
第二種,找到這個結點的右子樹中最小的結點,替換要刪除的結點。
樹的案例
字典樹 -- Dictionary Tree
-
第一,根結點不包含字元;
-
第二,除根結點外每一個結點都只包含一個字元;
-
第三,從根結點到某一葉子結點,路徑上經過的字元連線起來,即為集合中的某個字串。
雜湊表
雜湊表 -- Hash Table, 也叫作雜湊表。
雜湊表是一種特殊的資料結構,它與陣列、連結串列以及樹等我們之前學過的資料結構相比,有很明顯的區別。
-
線性表中的棧和佇列對增刪有嚴格要求,它們會更關注資料的順序。
-
陣列和字串需要保持資料型別的統一,並且在基於索引的查詢上會更有優勢。
-
樹的優勢則體現在資料的層次結構上。
-
雜湊表優勢體現在,無論有多少資料,查詢、插入、刪除只需要接近常量的時間,即 O(1)的時間級。
核心思想
實現 “地址 = f (關鍵字)” 的對映關係,快速完成基於資料的數值的查詢。
雜湊函式的設計
直接定製法
雜湊函式為關鍵字到地址的線性函式。如,H (key) = a*key + b。這裡,a 和 b 是設定好的常數。
數字分析法
假設關鍵字集合中的每個關鍵字 key 都是由 s 位數字組成(k1,k2,…,Ks),並從中提取分佈均勻的若干位組成雜湊地址。
平方取中法
如果關鍵字的每一位都有某些數字重複出現,並且頻率很高,我們就可以先求關鍵字的平方值,通過平方擴大差異,然後取中間幾位作為最終儲存地址。
摺疊法
如果關鍵字的位數很多,可以將關鍵字分割為幾個等長的部分,取它們的疊加和的值(捨去進位)作為雜湊地址。
除留餘數法
預先設定一個數 p,然後對關鍵字進行取餘運算。即地址為 key mod p。
解決雜湊衝突
開放定址法
常用的探測方法是線性探測法。比如有一組關鍵字 {34,35,36,45},採用的雜湊函式為 key mod 11。當插入 34,35,36 時可以直接插入,地址分別為 1、2、3。而當插入 45 時,雜湊地址為 45 mod 11 = 1。然而,地址 1 已經被佔用,因此沿著地址 1 依次往下探測,直到探測到地址 4,發現為空,則將 45 插入其中。
鏈地址法
將雜湊地址相同的記錄儲存在一張線性連結串列中。如果出現衝突,就在對應的位置上加上鍊表的資料結構。
雜湊表的基本操作
雜湊表中的增加和刪除資料操作,不涉及增刪後對資料的挪移問題
-
如果是採用陣列實現就需要考慮資料的挪移問題
雜湊表查詢的細節過程是:對於給定的 key,通過雜湊函式計算雜湊地址 H (key)。
-
如果雜湊地址對應的值為空,則查詢不成功。
-
反之,則查詢成功。
雜湊表的案例
實時返回使用者的字串搜尋結果