链表list(链式存储结构实现)_数据结构知否知否系列之 — 线性表的顺序与链式存储篇(8000 多字长文)...
線性表是由 n 個數據元素組成的有限序列,也是最基本、最簡單、最常用的一種數據結構。
作者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 后青年,公眾號「Nodejs技術棧」,Github 開源項目 https://www.nodejs.red
前言
本篇文章歷時一周多,差不多花費了兩個周末的時間,在書寫的過程中更多的還是在思考每一種線性表的算法實現,鏈表的指針域部分對于不理解指針或者對象引用的童鞋,在閱讀代碼的時候可能會蒙蒙的,本篇文章代碼部分采用的 JavaScript 編程語言,但是實現思想是相通的,如果你用 Java、Python 等也可做參考,如文章有理解錯誤之處歡迎在下方評論區指正。
認識線性表
根據線性表的定義,可得出幾個關鍵詞:n 個數據元素、有限序列,也就是說它是有長度限制的且元素之間是有序的,在多個元素之間,第一個元素無前驅,最后一個元素無后繼,中間元素有且只有一個前驅和后繼。
舉一個與大家都息息相關的十二生肖例子,以“子(鼠)” 開頭,“亥(豬)”結尾,其中間的每個生肖也都有其前驅和后繼,圖例如下所示:
下面再介紹一個復雜的線性表,其一個元素由多個數據項構成,例如,我們的班級名單,含學生的學號、姓名、年齡、性別等信息,圖例如下所示:
線性表兩種存儲結構
線性表有兩種存儲結構,一種為順序結構存儲,稱為順序表;另一種為鏈式形式存儲,稱為鏈表,鏈表根據指針域的不同,鏈表分為單向鏈表、雙向鏈表、循環鏈表等。詳細的內容會在后面展開講解。
順序表
順序表是在計算機內存中以數組的形式保存的線性表,是指用一組地址連續的存儲單元依次存儲數據元素的線性結構。
在線性表里順序表相對更容易些,因此也先從順序表講起,通過實現編碼的方式帶著大家從零開始實現一個順序表,網上很多教程大多都是以 C 語言為例子,其實現思想都是相通的,這里采用 JavaScript 編碼實現。
實現步驟
初始化順序表空間
在構造函數的 constructor 里進行聲明,傳入 capacity 初始化順序表空間同時初始化順序表的元素長度(length)為 0。
/**順序表是否為空檢查
定義 isEmpty() 方法返回順序表是否為空,根據 length 順序表元素進行判斷。
isEmpty順序表是否溢出檢查
定義 isOverflow() 方法返回順序表空間是否溢出,根據順序表元素長度和初始化的空間容量進行判斷。
isOverflow查找指定位置元素
返回順序表中第 i 個數據元素的值
getElement查找元素的第一個位置索引
返回順序表中第 1 個與 e 滿足關系的元素,存在則返回其索引值;不存在,則返回值為 -1
locateElement在順序表中返回指定元素的前驅
這里就用到了上面定義的 locateElement 函數,先找到元素對應的索引位置,如果前驅就取前一個位置,后繼就取后一個位置,在這之前先校驗當前元素的索引位置是否存在合法。
priorElement在順序表中返回指定元素的后繼
nextElement插入元素
在順序表中第 i 個位置之前插入新的數據元素 e,在插入之前先進行元素位置后移,插入之后順序表元素的長度要加 1。
舉個例子,我們去火車站取票,恰逢人多大家都在排隊,突然來一個美女或者帥哥對你說我的車次馬上要開車了,你可能同意了,此時你的位置及你后面的童鞋就要后移一位了,也許你會聽到一些聲音,怎么回事呀?怎么插隊了呀,其實后面的人有的也不清楚什么原因 “233”,看一個圖
算法實現如下:
listInsert刪除元素
刪除順序表的第 i 個數據元素,并返回其值,與插入相反,需要將刪除位置之后的元素進行前移,最后將順序表元素長度減 1。
同樣以火車站取票的例子說明,如果大家都正在排隊取票,突然你前面一個妹子有急事臨時走了,那么你及你后面的童鞋就要前進一步,圖例如下所示:
算法實現如下:
listDelete清除順序表元素
這里有幾種實現,你也可以把順序表的空間進行初始化,或者把 length 棧位置設為 0 也可。
clear順序表銷毀
在一些高級語言中都會有垃圾回收機制,例如 JS 中只要當前對象不再持有引用,下次垃圾回收來臨時將會被回收。不清楚的可以看看我之前寫的 Node.js 內存管理和 V8 垃圾回收機制
destroy順序表元素遍歷
定義 traversing() 方法對順序表的元素進行遍歷輸出。
traversing做一些測試
做下測試分別看下插入、刪除、遍歷等操作,其它的功能大家在練習的過程中可自行實踐。
const順序表的運行機制源碼地址如下:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/sequence-table.js順序表優缺點總結
插入、刪除元素如果是在最后一個位置時間復雜度為 O(1),如果是在第一個(或其它非最后一個)位置,此時時間復雜度為 O(1),就要移動所有的元素向后或向前,時間復雜度為 O(n),當順序表的長度越大,插入和刪除操作可能就需要大量的移動操作。
對于存取操作,可以快速存取順序表中任意位置元素,時間復雜度為 O(1)。
鏈表
鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,但是并不會按線性的順序存儲數據,而是在每一個節點里存到下一個節點的指針(Pointer)。由于不必須按順序存儲,鏈表在插入的時候可以達到O(1)的復雜度,比另一種線性表順序表快得多,但是鏈表查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間復雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由于增加了節的指針域,空間開銷比較大。
單向鏈表
鏈表中最簡單的一種是單向鏈表,它包含兩個域,一個信息域和一個指針域。這個鏈接指向列表中的下一個節點,而最后一個節點則指向一個空值,圖例如下:
除了單向鏈表之外還有雙向鏈表、循環鏈表,在學習這些之前先從單向鏈表開始,因此,這里會完整講解單向鏈表的實現,其它的幾種后續都會在這個基礎之上進行改造。
單向鏈表實現步驟
初始化鏈表
在構造函數的 constructor 里進行聲明,無需傳入參數,分別對以下幾個屬性和方法做了聲明:
- node: 定義 node 方法,它包含一個 element 屬性,即添加到列表的值,及另一個 next 屬性,指向列表中下一個節點項的指針
- length: 鏈表元素長度
- head: 在 head 變量中存儲第一個節點的引用
當我們實例化一個 SingleList 對象時 head 指向為 null 及 length 默認等于 0,代碼示例如下:
class鏈表是否為空檢查
定義 isEmpty() 方法返回鏈表是否為空,根據鏈表的 length 進行判斷。
isEmpty返回鏈表長度
同樣使用鏈表的 length 即可
length鏈表尾部插入元素
鏈表 SingleList 尾部增加元素,需要考慮兩種情況:一種是鏈表(head)為空,直接賦值添加第一個元素,另一種情況就是鏈表不為空,找到鏈表最后一個節點在其尾部增加新的節點(node)即可。
第一種情況,假設我們插入一個元素 1,此時由于鏈表為空,就會走到(行 {2})代碼處,示意圖如下:
第二種情況,假設我們再插入一個元素 2,此時鏈表頭部 head 指向不為空,走到(行 {3})代碼處,通過 while 循環直到找到最后一個節點,也就是當 current.next = null 時說明已經達到鏈表尾部了,接下來我們要做的就是將 current.next 指向想要添加到鏈表的節點,示意圖如下:
算法實現如下:
insertTail鏈表指定位置插入元素
實現鏈表的 insert 方法,在任意位置插入數據,同樣分為兩種情況,以下一一進行介紹。
如果是鏈表的第一個位置,很簡單看代碼塊(行 {1})處,將 node.next 設置為 current(鏈表中的第一個元素),此時的 node 就是我們想要的值,接下來將 node 的引用改為 head(node、head 這兩個變量此時在堆內存中的地址是相同的),示意圖如下所示:
如果要插入的元素不是鏈表第一個位置,通過 for 循環,從鏈表的第一個位置開始循環,定位到要插入的目標位置,for 循環中的變量 previous(行 {3})是對想要插入新元素位置之前的一個對象引用,current(行 {4})是對想要插入新元素位置之后的一個對象引用,清楚這個關系之后開始鏈接,我們本次要插入的節點 node.next 與 current(行 {5})進行鏈接,之后 previous.next 指向 node(行 {6})。
算法實現如下:
/**移除指定位置的元素
定義 delete(i) 方法實現移除任意位置的元素,同樣也有兩種情況,第一種就是移除第一個元素(行 {1})處,第二種就是移除第一個元素以外的任一元素,通過 for 循環,從鏈表的第一個位置開始循環,定位到要刪除的目標位置,for 循環中的變量 previous(行 {2})是對想要刪除元素位置之前的一個對象引用,current(行 {3})是對想要刪除元素位置之后的一個對象引用,要從列表中移除元素,需要做的就是將 previous.next 與 current.next 進行鏈接,那么當前元素會被丟棄于計算機內存中,等待垃圾回收器回收處理。
關于內存管理和垃圾回收機制的知識可參考文章 Node.js 內存管理和 V8 垃圾回收機制通過一張圖,來看下刪除一個元素的過程:
算法實現如下:
delete獲取指定位置元素
定義 getElement(i) 方法獲取指定位置元素,類似于 delete 方法可做參考,在鎖定位置目標后,返回當前的元素即可 previous.element。
getElement查找元素的第一個位置索引
返回鏈表中第 1 個與 e 滿足關系的元素,存在則返回其索引值;不存在,則返回值為 -1
locateElement在鏈表中返回指定元素的前驅
如果是第一個元素,是沒有前驅的直接返回 false,否則的話,需要遍歷鏈表,定位到目標元素返回其前驅即當前元素的上一個元素,如果在鏈表中沒有找到,則返回 false。
priorElement在鏈表中返回指定元素的后繼
nextElement鏈表元素遍歷
定義 traversing() 方法對鏈表的元素進行遍歷輸出,主要是將 elment 轉為字符串拼接輸出。
traversing單向鏈表與順序表優缺點比較 查找:單向鏈表時間復雜度為 O(n);順序表時間復雜度為 O(1) 插入與刪除:單向鏈表時間復雜度為 O(1);順序表需要移動元素時間復雜度為 O(n) * 空間性能:單向鏈表無需預先分配存儲空間;順序表需要預先分配內存空間,大了浪費,小了易溢出
單向鏈表源碼地址如下:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/single-list.js雙向鏈表
雙向鏈表也叫雙鏈表。與單向鏈表的區別是雙向鏈表中不僅有指向后一個節點的指針,還有指向前一個節點的指針。這樣可以從任何一個節點訪問前一個節點,當然也可以訪問后一個節點,以至整個鏈表。
雙向鏈表是基于單向鏈表的擴展,很多操作與單向鏈表還是相同的,在構造函數中我們要增加 prev 指向前一個元素的指針和 tail 用來保存最后一個元素的引用,可以從尾到頭反向查找,重點修改插入、刪除方法。
修改初始化鏈表
constructor修改鏈表指定位置插入元素
在雙向鏈表中我們需要控制 prev 和 next 兩個指針,比單向鏈表要復雜些,這里可能會出現三種情況:
情況一:鏈表頭部添加
如果是在鏈表的第一個位置插入元素,當 head 頭部指針為 null 時,將 head 和 tail 都指向 node 節點即可,如果 head 頭部節點不為空,將 node.next 的下一個元素為 current,那么同樣 current 的上個元素就為 node(current.prev = node),node 就為第一個元素且 prev(node.prev = null)為空,最后我們將 head 指向 node。
假設我們當前鏈表僅有一個元素 b,我們要在第一個位置插入元素 a,圖例如下:
情況二:鏈表尾部添加
這又是一種特殊的情況鏈表尾部添加,這時候我們要改變 current 的指向為 tail(引用最后一個元素),開始鏈接把 current 的 next 指向我們要添加的節點 node,同樣 node 的上個節點 prev 就為 current,最后我們將 tail 指向 node。
繼續上面的例子,我們在鏈表尾部在增加一個元素 d
情況三:非鏈表頭部、尾部的任意位置添加
這個和單向鏈表插入那塊是一樣的思路,不清楚的,在回頭去看下,只不過增加了節點的向前一個元素的引用,current.prev 指向 node,node.prev 指向 previous。
繼續上面的例子,在元素 d 的位置插入元素 c,那么 d 就會變成 c 的下一個元素,圖例如下:
算法實現如下:
insert移除鏈表元素
雙向鏈表中移除元素同插入一樣,需要考慮三種情況,下面分別看下各自實現:
情況一:鏈表頭部移除
current 是鏈表中第一個元素的引用,對于移除第一個元素,我們讓 head = current 的下一個元素,即 current.next,這在單向鏈表中就已經完成了,但是雙向鏈表我們還要修改節點的上一個指針域,再次判斷當前鏈表長度是否等于 1,如果僅有一個元素,刪除之后鏈表就為空了,那么 tail 也要置為 null,如果不是一個元素,將 head 的 prev 設置為 null,圖例如下所示:
情況二:鏈表尾部移除
改變 current 的指向為 tail(引用最后一個元素),在這是 tail 的引用為 current 的上個元素,即最后一個元素的前一個元素,最后再將 tail 的下一個元素 next 設置為 null,圖例如下所示:
情況三:鏈表尾部移除
這個和單向鏈表刪除那塊是一樣的思路,不清楚的,在回頭去看下,只增加了 current.next.prev = previous 當前節點的下一個節點的 prev 指針域等于當前節點的上一個節點 previous,圖例如下所示:
算法實現如下:
delete雙向鏈表源碼地址如下:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/doubly-linked-list.js循環鏈表
在單向鏈表和雙向鏈表中,如果一個節點沒有前驅或后繼該節點的指針域就指向為 null,循環鏈表中最后一個節點 tail.next 不會指向 null 而是指向第一個節點 head,同樣雙向引用中 head.prev 也會指向 tail 元素,如下圖所示:
可以看出循環鏈表可以將整個鏈表形成一個環,既可以向單向鏈表那樣只有單向引用,也可以向雙向鏈表那樣擁有雙向引用。
以下基于單向鏈表一節的代碼進行改造
尾部插入元素
對于環形鏈表的節點插入與單向鏈表的方式不同,如果當前節點為空,當前節點的 next 值不指向為 null,指向 head。如果頭部節點不為空,遍歷到尾部節點,注意這里不能在用 current.next 為空進行判斷了,否則會進入死循環,我們需要判斷當前節點的下個節點是否等于頭部節點,算法實現如下所示:
insertTail鏈表任意位置插入元素
實現同鏈表尾部插入相似,注意:將新節點插入在原鏈表頭部之前,首先,要將新節點的指針指向原鏈表頭節點,并遍歷整個鏈表找到鏈表尾部,將鏈表尾部指針指向新增節點,圖例如下:
算法實現如下所示:
insert移除指定位置元素
與之前不同的是,如果刪除第一個節點,先判斷鏈表在僅有一個節點的情況下直接將 head 置為 null,否則不僅僅只有一個節點的情況下,首先將鏈表頭指針移動到下一個節點,同時將最后一個節點的指針指向新的鏈表頭部
算法實現如下所示:
delete最后在遍歷的時候也要注意,不能在根據 current.next 是否為空來判斷鏈表是否結束,可以根據鏈表元素長度或者 current.next 是否等于頭節點來判斷,本節源碼實現鏈接如下所示:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/circular-linked-list.js總結
本節主要講解的是線性表,從順序表->單向鏈表->雙向鏈表->循環鏈表,這個過程也是循序漸進的,前兩個講的很詳細,雙向鏈表與循環鏈表通過與前兩個不同的地方進行比較針對性的進行了講解,另外學習線性表也是學習其它數據結構的基礎,數據結構特別是涉及到一些實現算法的時候,有時候并不是看一遍就能理解的,總之多實踐、多思考。
Reference
- https://zh.wikipedia.org/wiki/線性表
- 學習JavaScript數據結構與算法(第2版)
- 大話數據結構
總結
以上是生活随笔為你收集整理的链表list(链式存储结构实现)_数据结构知否知否系列之 — 线性表的顺序与链式存储篇(8000 多字长文)...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gitee合并分支_使用Gitee进行协
- 下一篇: double类型怎么取余_数据类型和运算