數據結構和算法-基礎數據結構

基礎數據結構,最基礎的就只有兩種,一個是數組,一個是鏈表。其餘的數據結構都是在它們之上抽象出來的,好比,棧,隊列,散列表,樹,圖等。javascript

數組

數組在內存中是申請的一組連續的內存空間,在一些強類型語言中,初始化數組時是要事先指定數組大小和類型的,一旦完成,則數組大小和類型不可改變。咱們常說的對數組的動態擴容等,其實也是申請更大的數組,而後把舊數組裏的數據搬移到新數組裏。在 Javascript 中,數組卻沒有這樣的限制,能夠隨意增長內容,隨意改變數組的大小,且數組裏元素類型能夠不同。JavaScript 底層中實現數組時,若是元素是同類型的,則使用的是連續的內存空間,若是是不一樣類型的元素,則應該使用的是相似哈希結構。Javascript 中的TypeArray就是使用的連續的內存空間。html

對數組中任意位置元素的訪問,是十分高效的。咱們能夠根據下標值,很快就能夠在 O(1)時間內完成訪問,而鏈表則須要移動屢次 head 指針才能完成。因爲它是一組連續的內存空間,計算機能夠一次性把它所有讀入內存緩衝區中,下次訪問其餘位置元素,只須要計算偏移量就能夠從內存緩衝區中讀取,速度是很是快的。可是對於頻繁的插入和刪除操做,可能就涉及動態擴容或者維護數據的有序性,那麼就會存在額外的數據搬移工做,額外花費的時間多是 O(n)。java

鏈表

鏈表是由一個一個節點連接起來的,每一個節點會存儲當前節點的值,還會有一個 next 指針,指向下一個節點。對於第一個節點,會有一個 head 指針指向它,最後一個節點的 next 指向 null。它天生就支持動態擴容或者縮容,因爲對內存空間不要求連續,對內存利用率更高。若是須要擴容,就增長節點,插入到鏈表的某一個節點後面。若是要縮容,就刪除釋放掉部分不用的節點。git

/* 鏈表節點 鏈表節點用於組成單向鏈表,雙向鏈表,循環鏈表等。 */
class LinkedNode<Item> {
  public val: Item
  public next: LinkedNode<Item>
  public pre: LinkedNode<Item>
  constructor(val: Item, next: LinkedNode<Item>, pre: LinkedNode<Item> = null) {
    this.val = val
    this.next = next
    this.pre = pre
  }
}
複製代碼

因爲它的不連續性,咱們在訪問鏈表中某個位置的節點數據時,須要從頭開始遍歷 head 指針,直到 head 指向要訪問的節點,須要的時間複雜度爲 O(n)。對於刪除或者插入數據,只須要簡單的改變上一個節點和當前節點的 next 指針便可,不須要額外的搬移其餘節點,時間複雜度通常爲 O(1)。github

鏈表有多種變體,好比雙向鏈表,循環鏈表,雙向循環鏈表等。雙向鏈表,就是節點不只有一個 next 指針,還有一個 pre 指針,指向前一個節點。因爲單向鏈表只有一個 next 指針,因此只能日後遍歷,而雙向鏈表,既能夠日後遍歷,也能夠根據 pre 指針往前遍歷,使用很是方便,而且只須要多存儲一個 pre 指針便可。在實際應用中,更多的是使用雙向鏈表。循環鏈表,就是最後一個節點的 next 不指向 null,而是指向第一個節點,從而造成了一個環。typescript

因爲鏈表中每一個節點不只存儲了值,還須要額外的空間存儲 next 指針(雙向鏈表還須要存儲 pre 指針),因此對於相同數據而言,鏈表花費的內存空間比數組要大。數組

對於鏈表的掌握,我作了以下一些練習,你能夠看看,瀏覽器

訪問任意位置元素 插入或者刪除某一個元素 內存空間 使用內存大小
數組 O(1) O(n) 連續 較小
鏈表 O(n) O(1) 不連續 較大

棧是一種抽象的 LIFO(last in , first out)數據結構。用數組實現的棧,通常稱爲順序棧,用鏈表實現的棧,通常稱爲鏈式棧;實際應用中,順序棧使用較多。棧通常暴露出來的操做,只有出棧和入棧,可能還會有清空,查找等其餘輔助操做。它遵循後進,先出的策略,只有經過不停的出棧操做才能遍歷或者訪問它最開始加入的數據。數據結構

函數調用棧,就是用的這種結構,在一個函數 A 中調用另一個函數 B,就會先把函數 B 壓入到執行棧裏,當函數 B 執行完畢以後,就會把函數 B 出棧,繼續執行棧頂函數 A。特別是對於遞歸調用,咱們要控制終止條件,否則就會出現遞歸次數過多,拋出maximum-call-stack-size-exceeded-error 的錯誤。解決辦法能夠將遞歸轉化爲迭代,或者使用尾遞歸優化。函數

對於棧的掌握,我作了以下一些練習,你能夠看看,

隊列

隊列是一種抽象的 FIFO(first in, first out)數據結構。同理,隊列也能夠用數組或者鏈表實現。實際應用中,順序隊列使用比較多。隊列通常暴露出來的操做,只有入隊列和出隊列,可能還有清空隊列,查找等其餘輔助操做。它遵循先進,先出的策略,後加入的元素放在隊尾,相似於咱們生活中排隊買票同樣。

JavaScript 中常說的event loop,就是隊列的應用之一。它會不斷的從可執行隊列中出隊列,取出一個可執行的函數,而後將它放入執行棧中執行。咱們在實現 IO 操做,事件監聽,或者setTimeout時就會入隊列操做,將執行函數放入隊列末尾。若是更加深刻,JavaScript 的 event loop 分爲兩種隊列,一個是 macrotask,一個是 microtask,這裏不作更加深刻的探討。

在使用廣度優先搜素(BFS)遍歷圖時,隊列也是經常使用的數據結構。初始時,隨機選擇一個節點入隊列,而後經過每次從隊列裏出隊列一個節點,訪問它,而後把它全部的關聯節點都入隊列。這樣當隊列爲空時,整個圖全部節點就都被訪問到了。

隊列也有變體,循環隊列,優先級隊列。循環隊列,跟循環鏈表相似,循環隊列只是咱們思惟抽象上的環。因爲隊列入隊列時,只能加到隊尾,當一個固定大小的隊列的尾部有元素時,咱們就沒法再執行入隊列了,即便隊列前面有空的位置,這將致使內存空間的浪費。解決辦法之一就是咱們每次執行出隊列時,都移動隊列中元素,填充第一個空的位置,這樣雖然能夠防止隊列空間的浪費,可是每次搬移隊列中數據,將致使性能急劇降低。解決辦法之二就是使用咱們的循環隊列,計算隊尾位置時,並非咱們固定的數組最後一個位置,而是結合隊首空的位置來計算。優先級隊列,出隊列邏輯並非先入隊列的元素出隊列,而是優先級高的元素先出隊列,若是優先級相同,則先入隊列的元素先出隊列。堆的應用之一就是優先級隊列。

對於隊列的掌握,我作了以下一些練習,你能夠看看,

散列表

散列表又叫哈希表,一般是經過鍵(key)來存儲一個值(value),也就是經常使用的 key-value 結構。散列表是基於數組抽象出來的,不過它是經過一個 key 來訪問一個 value 的,時間複雜度也是 O(1)。當咱們存儲一個 key-value 時,會先經過散列函數和 key 計算出一個非負整數 index,再把 value 存在下標爲 index 的位置。經過 key 查詢 value 時,過程也是相似的,也是先經過散列函數和 key 計算出下標 index,而後返回數組中下標爲 index 的位置值。

const index = hash(key)
複製代碼

一個好的散列函數,既要計算過程簡單,不能消耗太多時間,也要知足生成的下標 index 隨機且分佈均勻。若是散列函數計算過程很是複雜,每次插入或者查詢時都將花費更多的時間,影響性能。若是散列函數生成的 index 不夠隨機分佈,就會增長髮生散列衝突的機率,解決散列衝突也會花費額外的時間,也會影響性能。

若是 k1 經過散列函數獲得 i1,咱們把 v1 存在數組下標爲 i1 的位置;若是 k2 經過散列函數也到 i1,因爲 i1 的位置已經被 v1 使用了,v2 不能直接存在 i1 的位置,這個時候就發生了散列衝突。散列衝突的機率不只受散列函數的影響,也受當前裝載因子大小的影響。當裝載因子太高時,能夠啓動動態擴容,減小散列衝突,當裝載因子太低時,能夠啓動動態縮容,釋放沒有使用的內存空間。

裝載因子 = 數組已經使用的元素個數 / 數組的長度

解決散列衝突常見的方法一種是開放尋址法,一種是鏈表法。開放尋找法中有一種是線性探測,簡單點說就是若是當前位置 i1 已經被使用了,就繼續遍歷數組後面的位置,直到找到一個爲空的位置,而後將 v2 放入。鏈表法就是數組中存儲的是一個鏈表的地址,經過散列函數獲得的下標 index,而後將數組插入當前鏈表的尾部。這裏不作深刻說明了,想繼續深刻的,能夠看下面的資料,

相關文章
相關標籤/搜索