本文涉及更多的是概念,代碼部分請參考之前寫過的 2 篇博客
本文主要是基礎的數據結構和算法概念,可能部分地方會涉及更高級的算法和算法,具體內容以后會單獨寫的。此外一些性質還會不斷補充,也希望可以得到您的指點,謝謝。
數據結構
程序 = 數據結構 + 算法
數據結構基本概念
- 數據的邏輯結構:反映數據元素之間的關系的數據元素集合的表示。數據的邏輯結構包括集合、線形結構、樹形結構和圖形結構四種。
- 數據的存儲結構:數據的邏輯結構在計算機存儲空間種的存放形式稱為數據的存儲結構。常用的存儲結構有順序、鏈接、索引等存儲結構。
在數據結構中,沒有前件的結點稱為根結點,沒有后件的結點成為終端結點
數據結構的基本操作
插入和刪除是對數據結構的兩種基本操作。此外還有查找、分類、合并、分解、復制和修改等。
線性結構和非線性結構
根據數據結構中各數據元素之間前后件關系的復雜程度,一般將數據結構分為兩大類型:線性結構和非線性結構。
- 線性結構:有且只有一個根結點;每個結點最多有一個前件,最多只有一個后件。
- 非線性結構: 如果一個數據結構不是線性結構,稱之為非線性結構。
本文涉及一下內容:
- 四種線性結構的存儲結構:順序表、鏈表、索引、散列
- 兩種常見的線性邏輯結構:隊列、棧
- 非線性邏輯結構:循環隊列、雙向隊列、雙向循環隊列、樹、圖
存儲結構
順序表
順序表是線性表的順序存儲結構,指的是用一組地址連續的存儲單元依次存儲線性表的數據元素。
順序表具備如下兩個基本特征:
- 順序表中的所有元素所占的存儲空間是連續的;
- 順序表中各數據元素在存儲空間中是按邏輯順序依次存放的。
假設順序表的每個元素需占用 K 個存儲單元,并以所占的第一個單元的存儲地址作為數據元素的存儲位置。則順序表中第 i+1 個數據元素的存儲位置 LOC(a_i+1) 和第 i 個數據元素的存儲位置 LOC(a_i) 之間滿足下列關系為:
LOC(a_(i+1)) = LOC(a_i)+K
LOC(a_i) = LOC(a_1)+(i-1)*K
其中,LOC(a_1)是順序表的第一個數據元素 a_1 的存儲位置,通常稱做順序表的起始位置或基地址。順序存儲結構也稱隨機存取結構。
順序表常見操作(括號中為算法平均時間復雜度,沒有寫明的具體復雜度依賴不同算法和運算規則):
插入(O(n))、刪除(O(n))、查找、排序、分解、合并、復制(O(n))、逆轉(O(n))
鏈表
鏈表指線性表的鏈式存儲結構。一組任意的存儲單元存儲線性表的數據元素,因此,為了表示每個數據元素 a_i 與其直接后繼數據元素 a_(i+1) 之間的邏輯關系,對數據元素 a_i 來說,除了存儲其本身的信息(數據域)之外,還需存儲一個變量指示其直接后繼的信息(指針域)。這兩部分信息組成數據元素 a_i 的存儲映象,稱為結點。N 個結點鏈結成一個鏈表。該鏈表就是傳統的單向鏈表。
有時,我們在單鏈表的第一個結點之前附設一個結點,稱之為頭結點,它指向表中第一個結點。頭結點的數據域可 以不存儲任何信息,也可存儲如線性表的長度等類的附加信息,頭結點的指針域存儲指向第一個結點的指針。在單鏈表中,取得第 I 個數據元素必須從頭指針出發尋找,因此,鏈表是非隨機存取的存儲結構。
以上提到的鏈表指針域只包括一個指針,指向下一個數據的地址,如果我們將鏈表最后一個結點指針域的指針指向鏈表的頭結點地址,就構成了一個環狀的存儲結構,我們稱作循環鏈表。
當然我們可以給每個結點的指針域再添加一個指針,使其指向前一個數據結點的地址,這樣就構成了雙向鏈表,而將頭結點的前一個結點指向尾結點,同時將尾結點的下一個結點指向頭結點就構成了雙向循環鏈表。
如果鏈表的尾結點的指針域指向了該鏈表之前的任意一個結點,我們稱該鏈表為有環鏈表。環形鏈表就是其中一個特例
順序表常見操作(括號中為算法平均時間復雜度,沒有寫明的具體復雜度依賴不同算法和運算規則):
插入(O(n))、刪除(O(n))、查找、排序、分解、合并、復制(O(n))、逆轉(O(n))
索引
索引存儲除建立存儲結點信息外,還建立附加的索引表來標識結點的地址。索引表由若干索引項組成。
對于索引的理解最好的例子就是《新華字典》,它建立的2套索引表(拼音、部首)。字典的正文就是從“啊”到“做”的每個字的解釋,有上千頁,就是是數據。而前面的拼音/部首就是索引表,索引表告訴你某個讀音/部首在第幾頁,這就好比是指向數據地址的指針。而索引表可以有一級的也可以是多級的,比如字典中的部首索引就是兩級的。
索引存儲結構是用結點的索引號來確定結點存儲地址,其優點是檢索速度快,缺點是增加了附加的索引表,會占用較多的存儲空間。
散列
散列存儲,又稱哈希(hash)存儲,是一種力圖將數據元素的存儲位置(預留連續存儲區域)與關鍵碼之間建立確定對應關系的查找技術。散列法存儲的基本思想是由結點的關鍵碼值決定結點的存儲地址。散列技術除了可以用于存儲外,還可以用于查找。
散列以數據中每個元素的關鍵字 K 為自變量,通過散列函數 H(k) 計算出函數值,以該函數值作為一塊連續存儲空間的的單元地址,將該元素存儲到函數值對應的單元中。由于該函數值唯一,所以查找時間復雜度為 O(1)
線性邏輯結構
線性表
線性表滿足以下特征:
- 有且只有一個根結點 a_1,它無前件;
- 有且只有一個終端結點 a_n,它無后件;
- 除根結點與終端結點外,其他所有結點有且只有一個前件,也有且只有一個后件。線性表中結點的個數 n 稱 為線性表的長度。當 n=0 時稱為空表。
棧
棧實際上也是一個線性表,只不過是一種特殊的線性表。棧是只能在表的一端進行插入和刪除運算的線性表,通常稱插入、刪除這一端為棧頂(TOP),另一端為棧底(BOTTOM)。當表中沒有元素時稱為棧空。 棧頂元素總是后被插入(入棧)的元素,從而也是最先被移除(出棧)的元素;棧底元素總是最先被插入的元素,從而也是最后才能被移除的元素。所以棧是個 后進先出(LIFO) 的數據結構
棧的基本運算有三種:入棧、出棧與讀棧頂,時間復雜度都是O(1)
隊列
隊列是只允許在一端刪除,在另一端插入的順序表,允許刪除的一端叫做隊頭,用對頭指針 front 指向對頭元素的下一個元素,允許插入的一端叫做隊尾,用隊尾指針 rear 指向隊列中的隊尾元素,因此,從排頭指針 front 指向的下一個位置直到隊尾指針 rear 指向的位置之間所有的元素均為隊列中的元素。
隊列的修改是 先進先出(FIFO) 。往隊尾插入一個元素稱為入隊運算。從對頭刪除一個元素稱為退隊運算。
隊列主要有兩種基本運算:入隊運算和退隊運算,復雜度都是O(1)
循環隊列
在實際應用中,隊列的順序存儲結構一般采用循環隊列的形式。所謂循環隊列,就是將隊列存儲空間的最后一個 位置繞到第一個位置,形成邏輯上的環狀空間。在實際使用循環隊列時,為了能區分隊滿還是隊列空,通常需要增加一個標志 S。
循環隊列主要有兩種基本運算:入隊運算和退隊運算,復雜度都是O(1)
- 入隊運算
指在循環隊列的隊尾加入一個新元素,首先 rear=rear+1, 當 rear=m+1 時,置 rear=1,然后將新元素插入到隊尾指針 指向的位置。當 S=1, rear=front,說明隊列已滿,不能進行入隊運算,稱為“上溢”。 - 退隊運算
指在循環隊列的排頭位置退出一個元素并賦給指定的變量。首先 front=front+1, 并當 front=m+1 時,置 front=1, 然后 將排頭指針指向的元素賦給指定的變量。當循環隊列為空 S=0,不能進行退隊運算,這種情況成為“下溢”。
非線性邏輯結構
樹
樹是一種簡單的非線性結構。樹型結構具有以下特點:
- 每個結點只有一個前件,稱為父結點,沒有前件的結點只有一個,稱為樹的根結點。
- 每一個結點可以有多個后件結點,稱為該結點的子結點。沒有后件的結點稱為葉子結點
- 一個結點所擁有的后件個數稱為結點的度
- 樹的最大層次稱為樹的深度。
二叉樹
二叉樹是一種樹型結構,通常采用鏈式存儲結構,滿足以下特性:
- 它的特點是每個結點至多只有二棵子樹(即二叉樹中不存在度大于 2 的結點);
- 二叉樹的子樹有左右之分,其次序不能任意顛倒。
二叉樹的基本性質
- 在二叉樹的第 i 層上至多有 2i-1 個結點
- 深度為 k 的二叉樹至多有 2k-1 個結點(k <= 1)
- 在任意一個二叉樹中,度為 0 的結點總是比度為 2 的結點多一個
- 具有 N 個結點的二叉樹,其深度至少為 floor(log N) + 1
- 霍夫曼樹的帶權路徑長度 len = 2n+1; n 為所以葉子權重和。
二叉樹的遍歷
就是遵從某種次序,訪問二叉樹中的所有結點,使得每個結點僅被訪問一次。分為以下幾種:
- 前序遍歷(DLR): 首先訪問根結點,然后遍歷左子樹,最后遍歷右子樹。
- 中序遍歷(LDR): 首先遍歷左子樹,然后根結點,最后右子樹
- 后序遍歷(LRD): 首先遍歷左子樹,然后遍歷右子樹,最后訪問根結點。
此外圖的遍歷也可以用在樹上,包括:
- 廣度優先遍歷(層序遍歷): 從根結點開開始逐層向下,從左到右遍歷。
- 深度優先遍歷: 從根結點出發沿左子樹遍歷到葉子結點再逐層向上向遍歷右子樹。
除此之外還有很多有特點的特殊二叉樹:
- 滿二叉樹:除最后一層以外,每一層上的所有結點都有兩個子結點。
- 在滿二叉樹的第 K 層上有 2^(K-1) 個結點,且深度為 M 的滿二叉樹有 2M-1 個結點
- 完全二叉樹:除最后一層以外,每一層上的結點數均達到最大值;在最后一層上只缺少右邊的若干結點。
- 具有 N 個結點的完全二叉樹的深度為 floor(log_2 N) + 1
- 完全二叉樹總結點數為 N,則葉子結點數為 ceil(N/2)
堆
最常見的完全二叉樹就是 堆 了。堆滿足以下條件
- 堆中某個結點的值總是不大于或不小于其父結點的值
- 堆總是一棵完全二叉樹
將根結點最大的堆叫做 最大堆 或 大根堆 ,根結點最小的堆叫做 最小堆 或 小根堆 。
堆具有以下基本操作:
- 插入: 向堆中插入一個新元素
- 獲取: 獲取當前堆頂元素的值
- 刪除: 刪除堆頂元素
- 包含: 判斷堆中是否存在某個元素
哈希表
常用的哈希函數
- 直接尋址法: H(k) 是一個線性函數,如果該位置已經有值就向下尋找到第一個空的地方作為散列地址
- 平方取中法: 取 k 平方以后值的中間幾位作為散列地址
- 數字分析法: 對于比較規律的 k 值,找出其差異較大的部分作為散列地址
- 折疊法: 將 k 分成很多部分,然后做模二和作為散列地址
- 留余數法: H(k)=k % p, p <= m,其中 p 為素數,m 為表的長度
- 隨機數法: 取鍵字的隨機值作為散列地址,關鍵字長度時使用
實現映射的函數是哈希函數,簡單的 hash 可能會發生碰撞(不同輸入得到相同輸出),為了防止碰撞,考慮以下方法:
- 鏈地址法(拉鏈法): 當發生碰撞時,將發生碰撞的數據元素連接到同一個單鏈表中,而新元素插入到鏈表的前端
- 線性探針法: 線性探針法地址增量 d \∈ D_1 = {1,2,3, ... , m-1}, i 為探測次數,m 為表的長度,該方法遇到沖突地址會依次探測下一個地址(d = d_i + 1)直到有空的地址,若找不到空地址,則溢出。
平均查找長度
- 線性探針法平均長度推算(其中 m 為表中數據長度,n 為表長度):
ASL_s = d_1 + d_2 + ... + d_m)/m,查找成功
ASL_u = (d_1 + d_2 + ... + d_n)/n,查找不成功
注:線性探針法查找成功時 d_i 為每次放入元素時的地址增量,不成功時 d_i 為在表長度內依次查找每個元素到下一個空地址的地址增量(索引在表長度內循環)
- 鏈地址法平均長度推算(其中 k 為最長鏈長度,其中 m 為表中數據長度,n 為表長度):
ASL_s = (∑(1~k)(當前級指針數量 * 當前級數))/m,查找成功
ASL_u = (∑(1~n)(當前個位置鏈長度)) / n,查找不成功
哈希表相關特性
- 線性探針法容易“聚集”,影響查找效率,而鏈地址法不會
- 鏈地址法適應表長不確定情況
- 裝填因子(α) = 哈希表中的記錄數 / 哈希表的長度
圖
圖有兩種定義:
- 二元組的定義:圖 G 是一個有序二元組 (V,E),其中 V 稱為頂集(Vertices Set),E 稱為邊集(Edges set),E 與 V 不相交。它們亦可寫成 V(G) 和 E(G) 。E 的元素都是二元組,用 (x,y) 表示,其中 x,y &is∈; V
- 三元組的定義: 圖 G 是指一個三元組(V,E,I),其中 V 稱為頂集,E 稱為邊集,E 與 V 不相交;I 稱為關聯函數,I 將 E 中的每一個元素映射到V * V。如果 e 被映射到 (u,v),那么稱邊 e 連接頂點 u,v,而 u,v 則稱作 e 的端點,u,v 此時關于 e 相鄰。同時,若兩條邊 i,j 有一個公共頂點 u,則稱 i,j 關于 u 相鄰。
圖的分類
圖有不同的分類規則,具體如下:
分類1
- 有向圖: 如果圖中頂點之間關系不僅僅是連通與不連通,而且區分兩邊的頂點的出入(存在出邊和入邊),則為有向圖。
- 無向圖: 如果圖中頂點之間關系僅僅是連通與不連通,而不區分兩邊頂點的出入(不存在出邊和入邊),則為無向圖。
單圖
分類2
- 有環圖: 單向遍歷回可以到已遍歷的點,比如有環鏈表
- 無環圖: 單向遍歷不能回到已遍歷的點,比如樹
分類3
- 帶權圖: 圖的具有邊帶有關于該邊信息的權值,比如地圖中兩點間距離
- 無權圖: 圖的每個邊都不具有有關于該邊信息的權值,其僅表示是否連通
其他
- 單圖: 一個圖如果任意兩頂點之間只有一條邊且邊集中不含環,則稱為單圖
圖的表示采用鄰接矩陣和類似樹的形式(頂點指針域是個指針數組)的形式,其具有以下特點:
- 無向圖的鄰接矩陣是對稱矩陣
- 帶權圖的矩陣中元素為全職,無權圖中用0/1分別表示不連通/連通
- 主對角線有不為零元素,該圖一定有環
圖的遍歷
- 廣度優先遍歷: 廣度優先遍歷是連通圖的一種遍歷策略。因為它的思想是從一個頂點 V_0 開始,輻射狀地優先遍歷其周圍較廣的區域。
- 深度優先遍歷:
圖的相關性質:
- N 個頂點的連通圖中邊的條數至少為 N-1
- N 個頂點的強連通圖的邊數至少有 N
- 廣度優先遍歷用來找無權圖最短路徑(有權圖其實也行,多點東西唄)
簡單數據結構的增刪改查
操作 | 添加 | 刪除 | 查找 | 使用條件 |
---|---|---|---|---|
數組 | O(n) | O(n) | O(n) | 數定下標 |
鏈表 | O(1) | O(n) | O(n) | 兩端修改 |
變長數組 | O(1) | O(n) | O(n) | 數不定下標 |
棧 | O(1) | O(1) | - | LIFO |
隊列 | O(1) | O(1) | - | FIFO |
哈希表 | O(1) | O(1) | O(1) | key操作,無序 |
樹字典 | O(log n) | O(log n) | O(log n) | key操作,有序 |
哈希集合 | O(1) | O(1) | O(1) | 唯一值,無序 |
樹集合 | O(log n) | O(log n) | O(log n) | 唯一值,有序 |
算法
算法基本概念
- 算法的基本特征:可行性,確定性,有窮性
- 算法的基本要素:算法中對數據的運算和操作、算法的控制結構。
- 算法設計的基本方法:窮舉法、動態規劃、貪心法、回溯法、遞推法、遞歸法、分治法、散列法,分支限界法。
- 算法設計的要求:正確性、可讀性、健壯性、效率與低存儲量需求
- 算法的基本結構:順序、循環、選擇
算法復雜度
- 算法的時間復雜度:指執行算法所需要的計算工作量(不代表算法實際需要時間)
- 算法的空間復雜度:執行這個算法所需要的額外內存空間(代表算法實際需要的空間)
復雜度表示方法: 使用大寫 O 表示:O(n)表示時間復雜度時指 n 個數據處理完成使用 n 個單位的時間;表示空間復雜度時指 n 個數據處理完成使用了 n 個單位的輔助空間。
字符串算法
字符串算法除了增刪改查以外,還有很多匹配算法,比如最耳熟能詳的 KMP 算法(不屬于基礎部分),這里整理一些相關算法的性質:
- 一個長為 n 的字符串有 n(n+1)/2+1 個子串
- n的字符串,其中的字符各不相同,則S中的互異的非平凡子串有 \frac{1}{2}n^2+\frac{1}{2}n-1 個
排序算法
排序算法實際上可以分為內排序和外排序:
- 內排序:在排序過程中,所有元素調到內存中進行的排序,稱為內排序。內排序是排序的基礎。內排序效率用比較次數來衡量。按所用策略不同,內排序又可分為插入排序、選擇排序、堆排序、歸并排序、冒泡排序、快速排序、希爾排序及基數排序等等。
- 外排序:在數據量大的情況下,只能分塊排序,但塊與塊間不能保證有序。外排序用讀/寫外存的次數來衡量其效率。
排序算法時間復雜度
排序算法分為以下幾類:
- 插入類排序:插入排序、希爾排序
- 交換類排序:冒泡排序、快速排序
- 選擇類排序:選擇排序、堆排序
- 歸并排序
- 基數排序
算法 | 時間復雜度(最好) | 時間復雜度(最好) | 時間復雜度(最壞) | 空間復雜度 | 穩定性 |
---|---|---|---|---|---|
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
希爾排序 | O(n^{1.3}) | O(n) | O(n^2) | O(1) | 不穩定 |
選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不穩定 |
堆排序 | O(nlog n) | O(nlog n) | O(nlog n) | O(1) | 不穩定 |
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
快速排序 | O(nlog n) | O(nlog n) | O(n^2) | O(nlog n) | 不穩定 |
歸并排序 | O(nlog n) | O(nlog n) | O(nlog n) | O(n) | 穩定 |
基數排序 | O(d(r+n)) | O(d(n+rd)) | O(d(r+n)) | O(n+rd) | 穩定 |
注:
- 基數排序的復雜度中,r 代表關鍵字基數,d 代表長度,n 代表關鍵字個數
- 排序算法的穩定性指在原序列中,r_i=r_j,且 r_i 在 r_j 之前,而在排序后的序列中,r_i 仍在 r_j 之前,則稱這種排序算法是穩定的;否則稱為不穩定的。
查找算法
查找算法時間復雜度
算法 | 查找(最壞) | 插入(最壞) | 刪除(最壞) | 查找(最好) | 插入(最好) | 刪除(最好) | 是否要求有序 |
---|---|---|---|---|---|---|---|
順序結構 | N | N | N | N/2 | N | N/2 | No |
二分算法 | logN | N | N | logN | N/2 | N/2 | Yes |
二叉查找樹(BST) | N | N | N | 1.39logN | 1.39logN | \sqrt{N} | Yes |
2-3樹 | clogN | clogN | clogN | clogN | clogN | clogN | Yes |
紅黑樹 | 2logN | 2logN | 2logN | logN | logN | logN | Yes |
哈希散列查找 | logN | logN | logN | 3~5 | 3~5 | 3~5 | No |
哈希探針查找 | logN | logN | logN | 3~5 | 3~5 | 3~5 | No |
平均查找長度(ASL) = 查找表中第 i 個元素概率(P_i) * 找到第 i 個元素時已經比較的次數(C_i)