java數據結構與算法之順序表與鏈表深入分析

一、線性表的順序存儲設計與實現(順序表)

1.1?? 順序存儲結構的設計原理概要

順序存儲結構底層是利用數組來實現的,而數組可以存儲具有相同數據類型的元素集合,,當我們創建一個數組時,計算機操作系統會為該數組分配一塊連續的內存塊,這也就意味著數組中的每個存儲單元的地址都是連續的,因此只要知道了數組的起始內存地址就可以通過簡單的乘法和加法計算出數組中第n-1個存儲單元的內存地址,就如下圖所示:


??通過上圖可以發現為了訪問一個數組元素,該元素的內存地址需要計算其距離數組基地址的偏移量,即用一個乘法計算偏移量然后加上基地址,就可以獲得數組中某個元素的內存地址。其中c代表的是元素數據類型的存儲空間大小,而序號則為數組的下標索引。

整個過程需要一次乘法和一次加法運算,因為這兩個操作的執行時間是常數時間,所以我們可以認為數組訪問操作能再常數時間內完成,即時間復雜度為O(1),這種存取任何一個元素的時間復雜度為O(1)的數據結構稱之為隨機存取結構。而順序表的存儲原理正如上圖所示,因此順序表的定義如下(引用):

線性表的順序存儲結構稱之為順序表(Sequential List),它使用一維數組依次存放從a0到an-1的數據元素(a0,a1,…,an-1),將ai(0< i <> n-1)存放在數組的第i個元素,使得ai與其前驅ai-1及后繼ai+1的存儲位置相鄰,因此數據元素在內存的物理存儲次序反映了線性表數據元素之間的邏輯次序。


1.2 順序存儲結構的實現分析

接著我們來分析一下順序表的實現,先聲明一個順序表接口類SeqList,然后實現該接口并實現接口方法的代碼,SeqList接口代碼如下:

我們創建ArraySeqList一個順序表實現這個接口。

private final Object[]EMPTY_ELEMENTDATA = {};

private final Object[]DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final int DEFAULT_CAPACITY =10;

/*** * 元素的個數 */

private int size;

ArraySeqList的構造方法

無參構造方法,我們默認是空數組,有參構造構造方法中,我們新建一個長度為size的空數組。


get(int index) 實現分析?

從順序表中獲取值是一種相當簡單的操作并且效率很高,這是由于順序表內部采用了數組作為存儲數據的容器。因此只要根據傳遞的索引值,然后直接獲取數組中相對應下標的值即可,代碼實現如下:

set(int index, T data) 實現分析

在順序表中替換值也是非常高效和簡單的,只要根據傳遞的索引值index找到需要替換的元素,然后把對應元素值替換成傳遞的data值即可,代碼如下:


add(int index, T data)和add(T data)實現分析

在順序表中執行插入操作時,如果其內部數組的容量尚未達到最大值時,可以歸結為兩種情況:一種是在頭部插入或者中間插入,這種情況下需要移動數組中的數據元素,效率較低;另一種是在尾部插入,無需移動數組中的元素,效率高。

但是當順序表內部數組的容量已達到最大值無法插入時,則需要申請另一個更大容量的數組并復制全部數組元素到新的數組,這樣的時間和空間開銷是比較大的,也就導致了效率更為糟糕了。

因此在插入頻繁的場景下,順序表的插入操作并不是理想的選擇。下面是順序表在數組容量充足下頭部或中間插入操作示意圖(尾部插入比較簡單就不演示了):


順序表在數組容量充足下頭部或中間插入操作示意圖


順序表在數組容量不充足的情況下頭部或中間插入操作示意圖


理解了以上幾種順序表的插入操作后,我們通過代碼來實現這個插入操作如下。

每次插入元素之前,我們都應該判斷數組是否夠用,如果數組長度夠用,就將index以后 的元素右移一位,然后將新元素添加到下標為index的位置。元素個數size++

數組長度不夠的時候,元素的個數大于當前數組的長度時,我們就應該擴容數組。代碼如下:



remove(int index) 實現分析

順序表的刪除操作和前的插入操作情況是類似的,如果是在中間或者頭部刪除順序表中的元素,那么在刪除位置之后的元素都必須依次往前移動,效率較低,如果是在順序表的尾部直接刪除的話,則無需移動元素,此情況下刪除效率高。如下圖所示在順序表中刪除元素ai時,ai之后的元素都依次往前移動:

刪除操作的代碼實現如下:


remove(E e) 實現分析

在順序表中根據數據data找到需要刪除的數據元素和前面分析的根據index刪除順序表中的數據元素是一樣的道理,因此我們只要通過比較找到與data相等的數據元素并獲取其下標,然后調用前面實現的remove(int index)方法來移除即可。代碼實現如下:



源碼實現:源碼

1.3 順序存儲結構的效率分析

數組的訪問操作能在常數時間內完成,即順序表的訪問操作(獲取和修改元素值)的時間復雜為O(1)。

對于在順序表中插入或者刪除元素,從效率上則顯得不太理想了,由于插入或者刪除操作是基于位置的,需要移動數組中的其他元素,所以順序表的插入或刪除操作,算法所花費的時間主要是用于移動元素,如在順序表頭部插入或刪除時,效率就顯得相當糟糕了。若在最前插入或刪除,則需要移動n(這里假設長度為n)個元素;若在最后插入或刪除,則需要移動的元素為0。這里我們假設插入或刪除值為第i(0)。


也就是說,在等概率的情況下,插入或者刪除一個順序表的元素平均需要移動順序表元素總量的一半,其時間復雜度是O(n)。當然如果在插入時,內部數組容量不足時,也會造成其他開銷,如復制元素的時間開銷和新建數組的空間開銷。

因此總得來說順序表有以下優缺點:

優點

使用數組作為內部容器簡單且易用;

在訪問元素方面效率高;

數組具有內存空間局部性的特點,由于本身定義為連續的內存塊,所以任何元素與其相鄰的元素在物理地址上也是相鄰的。

缺點

內部數組大小是靜態的,在使用前必須指定大小,如果遇到容量不足時,需動態拓展內部數組的大小,會造成額外的時間和空間開銷

在內部創建數組時提供的是一塊連續的空間塊,當規模較大時可能會無法分配數組所需要的內存空間

順序表的插入和刪除是基于位置的操作,如果需要在數組中的指定位置插入或者刪除元素,可能需要移動內部數組中的其他元素,這樣會造成較大的時間開銷,時間復雜度為O(n)


二、線性表的鏈式存儲設計與實現(鏈表)

2.1 鏈表的鏈式存儲結構設計原理概要

通過前面對線性順序表的分析,我們知道當創建順序表時必須分配一塊連續的內存存儲空間,而當順序表內部數組的容量不足時,則必須創建一個新的數組,然后把原數組的的元素復制到新的數組中,這將浪費大量的時間。而在插入或刪除元素時,可能需要移動數組中的元素,這也將消耗一定的時間。

鑒于這種種原因,于是鏈表就出場了,鏈表在初始化時僅需要分配一個元素的存儲空間,并且插入和刪除新的元素也相當便捷,同時鏈表在內存分配上可以是不連續的內存,也不需要做任何內存復制和重新分配的操作,由此看來順序表的缺點在鏈表中都變成了優勢,實際上也是如此,當然鏈表也有缺點,主要是在訪問單個元素的時間開銷上,這個問題留著后面分析,我們先通過一張圖來初步認識一下鏈表的存儲結構,如下:


從圖可以看出線性鏈表的存儲結構是用若干個地址分散的存儲單元存放數據元素的,邏輯上相鄰的數據元素在物理位置上不一定相鄰,因此每個存儲單元中都會有一個地址指向域,這個地址指向域指明其后繼元素的位置。在鏈表中存儲數據的單元稱為結點(Node),從圖中可以看出一個結點至少包含了數據域和地址域,其中數據域用于存儲數據,而地址域用于存儲前驅或后繼元素的地址。

前面我們說過鏈表的插入和刪除都相當便捷,這是由于鏈表中的結點的存儲空間是在插入或者刪除過程中動態申請和釋放的,不需要預先給單鏈表分配存儲空間的,從而避免了順序表因存儲空間不足需要擴充空間和復制元素的過程,提高了運行效率和存儲空間的利用率。

2.2 單鏈表的儲結構實現分析

同樣地,先來定義一個頂級的鏈表接口:ILinkedList和存儲數據的結點類Node,該類是代表一個最基本的存儲單元,Node代碼如下:

接著頂級的鏈表接口ILinkedList,該接口聲明了我們所有需要實現的方法。

boolean isEmpty()實現分析

需要判斷鏈表是否為空的依據是頭結點head是否為null,當head=null時鏈表即為空鏈表,因此我們只需判斷頭結點是否為空即可,isEmpty方法實現如下:

int length()實現分析

獲取鏈表的長度,我們提供2種方法,第一種就是增加一個變量size,用它倆記錄鏈表的長度。

第二種方法,因此我們只要遍歷整個鏈表并獲取結點的數量即可獲取到鏈表的長度。遍歷鏈表需要從頭結點HeadNode開始,為了不改變頭結點的存儲單元,聲明變量p指向當前頭結點和局部變量length,然后p從頭結點開始訪問,沿著next地址鏈到達后繼結點,逐個訪問,直到最后一個結點,每經過一個結點length就加一,最后length的大小就是鏈表的大小。實現代碼如下:


E get(int index)實現分析

在單鏈表中獲取某個元素的值是一種比較費時間的操作,需要從頭結點開始遍歷直至傳入值index指向的位置,其查詢獲取值的過程如下圖所示:


代碼如下:

T set(int index, T data)實現分析?

根據傳遞的index查找某個值并替換其值為data,其實現過程的原理跟get(int index)是基本一樣的,先找到對應值所在的位置然后刪除即可,不清晰可以看看前面的get方法的圖解,這里直接給出代碼實現:

add(int index, T data)實現分析?

單鏈表的插入操作分四種情況:?

a.空表插入一個新結點,插語句如下:

head=new Node(x,null);

b.在鏈表的表頭插入一個新結點(即鏈表的開始處),此時表頭head!=null,因此head后繼指針next應該指向新插入結點p,而p的后繼指針應該指向head原來的結點,代碼如下:

以上代碼可以合并為如下代碼:

執行過程如下圖:


c.在鏈表的中間插入一個新結點p,需要先找到給定插入位置的前一個結點,假設該結點為front,然后改變front的后繼指向為新結點p,同時更新新結點p的后繼指向為front原來的后繼結點,即front.next,其執行過程如下圖所示:

代碼實現如下:

d.在鏈表的表尾插入一個新結點(鏈表的結尾)在尾部插入時,同樣需要查找到插入結點P的前一個位置的結點front(假設為front),該結點front為尾部結點,更改尾部結點的next指針指向新結點P,新結點P的后繼指針設置為null,執行過程如下:

具體代碼如下:


T remove(int index) 刪除結點實現分析

在單向鏈表中,根據傳遞index位置刪除結點的操作分3種情況,并且刪除后返回被刪除結點的數據:

a.刪除鏈表頭部(第一個)結點,此時需要刪除頭部head指向的結點,并更新head的結點指向,執行圖示如下:

代碼如下:

b.刪除鏈表的中間結點,與添加是同樣的道理,需要先找到要刪除結點r(假設要刪除的結點為r)位置的前一個結點front(假設為front),然后把front.next指向r.next即要刪除結點的下一個結點,執行過程如下:

代碼如下:

void clear() 實現分析

清空鏈表是一件非常簡單的事,直接將所有的節點置空。代碼如下:

ok~,到此單鏈表主要的添加、刪除、獲取值、設置替換值、獲取長度等方法已分析完畢,其他未分析的方法都比較簡單這里就不一一分析了,單鏈表的整體代碼最后會分享到github給大家。


2.3 帶頭結點的單鏈表以及循環單鏈表的實現

帶頭結點的單鏈表

前面分析的單鏈表是不帶特殊頭結點的,所謂的特殊頭結點就是一個沒有值的結點即:

此時空鏈表的情況如下:


那么多了頭結點的單向鏈表有什么好處呢?通過對沒有帶頭結點的單鏈表的分析,我們可以知道,在鏈表插入和刪除時都需要區分操作位,比如插入操作就分頭部插入和中間或尾部插入兩種情況(中間或尾部插入視為一種情況對待即可),如果現在有不帶數據的頭結點,那么對于單鏈表的插入和刪除不再區分操作的位置,也就是說頭部、中間、尾部插入都可以視為一種情況處理了,這是因為此時頭部插入和頭部刪除無需改變head的指向了,頭部插入如下所示:


代碼如下所示:

接著再看看在頭部刪除的情況:

代碼如下:

帶頭結點遍歷從head.next開始:


因此無論是插入還是刪除,在有了不帶數據的頭結點后,在插入或者刪除時都無需區分操作位了,好~,到此我們來小結一下帶頭結點的單鏈表特點:

a.空單鏈表只有一個結點,head.next=null。

b.遍歷的起點為p=head.next。

c.頭部插入和頭部刪除無需改變head的指向。

??同時為了使鏈表在尾部插入時達到更加高效,我們可在鏈表內增加一個尾部指向的結點rear(代碼中使用的是last,上面代碼中已經使用),如果我們是在尾部添加結點,那么此時只要通過尾部結點rear進行直接操作即可,無需從表頭遍歷到表尾,帶尾部結點的單鏈表如下所示:

從尾部直接插入的代碼實現如下:


下面看看根據index插入的代碼實現,由于有了頭結點,頭部、中間、尾部插入無需區分操作位都視為一種情況處理。

代碼如下:

??最后在看看刪除的代碼實現,由于刪除和插入的邏輯和之前不帶頭結點的單鏈表分析過的原理的是一樣的,因此我們這里不重復了,主要注意遍歷的起始結點變化就行。

ok~,關于帶頭結點的單向鏈表就分析到這,這里貼出實現源碼,同樣地,稍后在github上也會提供。。文章末尾提供下載地址。


循環單鏈表

有上述的分析基礎,循環單鏈表(Circular Single Linked List)相對來說就比較簡單了,所謂的循環單鏈表是指鏈表中的最后一個結點的next域指向了頭結點head,形成環形的結構,我們通過圖示來理解:?


此時的循環單鏈表有如下特點:?

a.當循環鏈表為空鏈表時,head指向頭結點,head.next=head。?

b.尾部指向rear代表最后一個結點,則有rear.next=head。?

在處理循環單鏈表時,我們只需要注意在遍歷循環鏈表時,避免進入死循環即可,也就是在判斷循環鏈表是否到達結尾時,由之前的如下判斷:


//從head.next開始遍歷

Node item=head.next;

while (item!=null){

? ? item=item.next;

}

在循環單鏈表中改為如下判斷:

Node p=head;

while (p!=head){

p=p.next;

}


具體代碼實現,詳見github地址,文章末尾給出。

查找CircleHeadILinkedList.java文件。


2.3 單鏈表的效率分析

由于單鏈表并不是隨機存取結構,即使單鏈表在訪問第一個結點時花費的時間為常數時間,但是如果需要訪問第i(0),也就是說get(i)和set(i,x)的時間復雜度都為O(n)。


由于鏈表在插入和刪除結點方面十分高效的,因此鏈表比較適合那些插入刪除頻繁的場景使用。如果是單鏈表的話,插入和刪除的時間復雜度都是O(n)。

問題是大部分情況下查找所需時間比移動短多了,還有就是鏈表不需要連續空間也不需要擴容操作,因此即使時間復雜度都是O(n),所以相對來說鏈表更適合插入刪除操作。


GitHup源碼地址



參考文章:順序表和鏈表的深入分析。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容