《我的第一本演算法書》根據 iOS 和 Android 平臺上的應用程式“演算法動畫圖解”編寫而成,為配合圖書出版,對內容進行了補充和修正,專門新增了基礎理論方面的內容。
決定了資料的順序和位置關係
資料儲存於計算機的記憶體中。記憶體如右圖所示,形似排成 1 列的箱子,1 個箱子裡儲存 1 個資料。
資料儲存於記憶體時,決定了資料順序和位置關係的便是“資料結構”。
電話簿的資料結構
▶ 例① 從上往下順序新增
舉個簡單的例子。假設我們有 1 個電話簿——雖說現在很多人都把電話號碼存在手機裡,但是這裡我們考慮使用紙質電話簿的情況——每當我們得到了新的電話號碼,就按從上往下的順序把它們記在電話簿上。
假設此時我們想給“張偉”打電話,但是因為資料都是按獲取順序排列的,所以我們並不知道張偉的號碼具體在哪裡,只能從頭一個個往下找(雖說也可以“從後往前找”或者“隨機查詢”,但是效率並不會比“從上往下找”高)。如果電話簿上號碼不多的話很快就能找到,但如果存了 500 個號碼,找起來就不那麼容易了。
▶ 例② 按姓名的拼音順序排列
接下來,試試以聯絡人姓名的拼音順序排列吧。因為資料都是以字典順序排列的,所以它們是有“結構”的。
使用這種方式給聯絡人排序的話,想要找到目標人物就輕鬆多了。通過姓名的拼音首字母就能推測出該資料的大致位置。
那麼,如何往這個按拼音順序排列的電話簿裡新增資料呢?假設我們認識了新朋友“柯津博”並拿到了他的電話號碼,打算把號碼記到電話簿中。由於資料按姓名的拼音順序排列,所以柯津博必須寫在韓巨集宇和李希之間,但是上面的這張表裡已經沒有空位可供填寫,所以需要把李希及其以下的資料往下移 1 行。
此時我們需要從下往上執行“將本行的內容寫進下一行,然後清除本行內容”的操作。如果一共有 500 個資料,一次操作需要 10 秒,那麼 1 個小時也完成不了這項工作。
▶ 兩種方法的優缺點
總的來說,資料按獲取順序排列的話,雖然新增資料非常簡單,只需要把資料加在最後就可以了,但是在查詢時較為麻煩;以拼音順序來排列的話,雖然在查詢上較為簡單,但是新增資料時又會比較麻煩。
雖說這兩種方法各有各的優缺點,但具體選擇哪種還是要取決於這個電話簿的用法。如果電話簿做好之後就不再新增新號碼,那麼選擇後者更為合適;如果需要經常新增新號碼,但不怎麼需要再查詢,就應該選擇前者。
▶ 將獲取順序與拼音順序結合起來怎麼樣
我們還可以考慮一種新的排列方法,將二者的優點結合起來。那就是分別使用不同的表儲存不同的拼音首字母,比如表 L、表 M、表 N 等,然後將同一張表中的資料按獲取順序進行排列。
這樣一來,在新增新資料時,直接將資料加入到相應表中的末尾就可以了,而查詢資料時,也只需要到其對應的表中去查詢即可。
因為各個表中儲存的資料依舊是沒有規律的,所以查詢時仍需從表頭開始找起,但比查詢整個電話簿來說還是要輕鬆多了。
選擇合適的資料結構以提高記憶體的利用率
資料結構方面的思路也和製作電話簿時的一樣。將資料儲存於記憶體時,根據使用目的選擇合適的資料結構,可以提高記憶體的利用率。
本章將會講解 7 種資料結構。如本節開頭所述,資料在記憶體中是呈線性排列的,但是我們也可以使用指標等道具,構造出類似“樹形”的複雜結構(樹形結構將在 4-2 節詳細說明)。
參考:4-2 廣度優先搜尋
1-2 連結串列
連結串列是資料結構之一,其中的資料呈線性排列。在連結串列中,資料的新增和刪除都較為方便,就是訪問比較耗費時間。
解說
對連結串列的操作所需的執行時間到底是多少呢?在這裡,我們把連結串列中的資料量記成n。訪問資料時,我們需要從連結串列頭部開始查詢(線性查詢),如果目標資料在連結串列最後的話,需要的時間就是 O(n)。
另外,新增資料只需要更改兩個指標的指向,所以耗費的時間與 n 無關。如果已經到達了新增資料的位置,那麼新增操作只需花費 O(1) 的時間。刪除資料同樣也只需O(1) 的時間。
參考:3-1 線性查詢
補充說明
上文中講述的連結串列是最基本的一種連結串列。除此之外,還存在幾種擴充套件方便的連結串列。
雖然上文中提到的連結串列在尾部沒有指標,但我們也可以在連結串列尾部使用指標,並且讓它指向連結串列頭部的資料,將連結串列變成環形。這便是“迴圈連結串列”,也叫“環形連結串列”。迴圈連結串列沒有頭和尾的概念。想要儲存數量固定的最新資料時通常會使用這種連結串列。
另外,上文連結串列裡的每個資料都只有一個指標,但我們可以把指標設定為兩個,並且讓它們分別指向前後資料,這就是“雙向連結串列”。使用這種連結串列,不僅可以從前往後,還可以從後往前遍歷資料,十分方便。
但是,雙向連結串列存在兩個缺點:一是指標數的增加會導致儲存空間需求增加;二是新增和刪除資料時需要改變更多指標的指向。
1-3 陣列
陣列也是資料呈線性排列的一種資料結構。與前一節中的連結串列不同,在陣列中,訪問資料十分簡單,而新增和刪除資料比較耗工夫。這和 1-1 節中講到的姓名按拼音順序排列的電話簿類似。
參考:1-1 什麼是資料結構
解說
這裡講解一下對陣列操作所花費的執行時間。假設陣列中有 n 個資料,由於訪問資料時使用的是隨機訪問(通過下標可計算出記憶體地址),所以需要的執行時間僅為恆定的O(1)。
但另一方面,想要向陣列中新增新資料時,必須把目標位置後面的資料一個個移開。所以,如果在陣列頭部新增資料,就需要 O(n) 的時間。刪除操作同理。
補充說明
在連結串列和陣列中,資料都是線性地排成一列。在連結串列中訪問資料較為複雜,新增和刪除資料較為簡單;而在陣列中訪問資料比較簡單,新增和刪除資料卻比較複雜。
我們可以根據哪種操作較為頻繁來決定使用哪種資料結構。
1-4棧
棧也是一種資料呈線性排列的資料結構,不過在這種結構中,我們只能訪問最新新增的資料。棧就像是一摞書,拿到新書時我們會把它放在書堆的最上面,取書時也只能從最上面的新書開始取。
解說
像棧這種最後新增的資料最先被取出,即“後進先出”的結構,我們稱為 Last In First Out,簡稱 LIFO。
與連結串列和陣列一樣,棧的資料也是線性排列,但在棧中,新增和刪除資料的操作只能在一端進行,訪問資料也只能訪問到頂端的資料。想要訪問中間的資料時,就必須通過出棧操作將目標資料移到棧頂才行。
應用示例
棧只能在一端操作這一點看起來似乎十分不便,但在只需要訪問最新資料時,使用它就比較方便了。
比如,規定(AB(C(DE)F)(G((H)I J)K))這一串字元中括號的處理方式如下:首先從左邊開始讀取字元,讀到左括號就將其入棧,讀到右括號就將棧頂的左括號出棧。此時,出棧的左括號便與當前讀取的右括號相匹配。通過這種處理方式,我們就能得知配對括號的具體位置。
另外,我們將要在 4-3 節中學習的深度優先搜尋演算法,通常會選擇最新的資料作為候補頂點。在候補頂點的管理上就可以使用棧。
參考:4-3 深度優先搜尋