數據結構和算法 - 線性表
線性表 是一種基礎的數據結構,顧名思義,線性表具有類似 線 一樣的性質。先給出一個定義:線性表是零個或者多個多個數據元素的 有限
序列
線性表的抽象數據類型
由線性表的定義,我們可以知道,對於一個線性表而言,長度是給定的,而且可以對線性表中的數據進行 插入和刪除 操作
線性表的抽象數據類型定義:
ADT 線性表
Data
Operation
InitList(*L): 初始化線性表
ListEmpty(L): 線性表是否爲空,空返回 true,否則返回 false
ClearLits(*L): 清空線性表
GetElem(L,i,*e): 將線性表的第 i 個元素返回給 e
LocateElem(L,e): 在線性表中查找給定值 e 相等的元素,查找成功返回該元素在線性表的序號,否則返回 0 表示失敗
ListInsert(*L,i,e): 在線性表第 i 個位置插入新元素 e
ListDelete(*L,i,*e): 刪除線性表中的第 i 個元素,返回其值
ListLenght(L): 返回線性表元素個數
end ADT
以上只是線性表的 基礎
操作,隨着不同線性表的具體實現可能有跟多更復雜的操作
線性表的存儲結構
線性表的存儲結構分爲基本的兩種:順序存儲
和 鏈式存儲
順序存儲
鏈式存儲的基本定義爲:線性表的順序存儲結構是指,用一段 連續的
存儲單元存放線性表中 相同數據類型
數據元素
線性表的順序存儲結構中,首先要求存儲單元是連續的,其次是存放相同類型的數據。正是因爲存儲單元是連續的,所以,數組
這種線性表實現的基本數據結構就具有 隨機訪問
的特性,時間複雜度爲 O(1)
數組是如何實現隨機訪問的?
當我們在向系統申請一塊 數組
類型的結構空間的時候,系統會分配一塊連續的內存給我們,存儲器中每一塊內存單元都有自己的 地址編號
,我們把第一塊地址稱爲 首地址
,首地址的地址單元我們標記爲 base_address
由於數據類型是相同的,例如一個整型的線性表,每個元素所需要的 type_size
空間爲 4 個字節,我們就可以通過 尋址公式
直接訪問到 數組下標
爲 i 的數據存儲的地址:base_address + i * type_size
這樣就實現了根據數組下標隨機訪問的特性
同時,數組爲了維護空間的連續性,在 刪除和插入
操作的時候,就需要 額外的搬移數據
操作了。比如,在數據未滿的情況下,向第 i 個位置插入一個元素,此時需要從第 i 個元素開始,將其後面的元素一次往後搬移,直至騰出空間,將元素放入 i 的位置。這樣操作的時間複雜度爲 O(n)
刪除操作同理,在具體的實現過程中,我們可以不用每次刪除元素的時候都去挪動元素,而是將已刪除的數據標記爲 已刪除
,等到數組滿了的時候再進行一次統一的刪除操作。這也是 JVM 垃圾回收中 標記清除算法
的思想
鏈式存儲
單鏈表的定義: 爲了表示元素與元素之間的邏輯關係,我們將順序表中每個數據元素拆分爲 數據域
和 指針域
。指針域中存儲下一個節點的地址。我們把由這兩部分組成的數據元素成爲 節點 ( Node )
。由 n 個節點組成的順序表,每個節點中包含一個指針域,稱之爲 單鏈表
順序存儲的線性表 插入和刪除
操作十分低效,但是在鏈表中,插入和刪除就會變得十分高效(某些特定的插入和刪除,需要藉助 循環鏈表或雙向循環鏈表
實現)
單鏈表如何實現快速的插入和刪除操作?
我們知道,在單鏈表中,鏈表順序的維護是用指針,所以在插入和刪除的時候,我們只需要維護指針的指向而不需要去挪動元素就可以實現
刪除 node 節點後的節點:
$node->next = $node->next->next;
在 node 節點後插入新節點:
$newNode->next = $node->next;
$node->next = $newNode;
我們可以看到,在某些特定的插入和刪除操作下,單鏈表的時間複雜度爲 O(1),但是,如果要在鏈表中查詢某個節點,據需要遍歷整個鏈表,時間複雜度爲 O(n)
鏈表和數組的對比
鏈表適合插入和刪除操作,數組適合查找。在數組中,根據 下標隨機訪問
的時間複雜度爲 O(1)
時間複雜度 | 數組 | 鏈表 |
---|---|---|
插入、刪除 | O(n) | O(1) |
查找 | O(1) | O(n) |