Java資料結構和演算法(七)——連結串列
目錄
前面部落格我們在講解陣列中,知道陣列作為資料儲存結構有一定的缺陷。在無序陣列中,搜尋效能差,在有序陣列中,插入效率又很低,而且這兩種陣列的刪除效率都很低,並且陣列在建立後,其大小是固定了,設定的過大會造成記憶體的浪費,過小又不能滿足資料量的儲存。
本篇部落格我們將講解一種新型的資料結構——連結串列。我們知道陣列是一種通用的資料結構,能用來實現棧、佇列等很多資料結構。而連結串列也是一種使用廣泛的通用資料結構,它也可以用來作為實現棧、佇列等資料結構的基礎,基本上除非需要頻繁的通過下標來隨機訪問各個資料,否則很多使用陣列的地方都可以用連結串列來代替。
但是我們需要說明的是,連結串列是不能解決資料儲存的所有問題的,它也有它的優點和缺點。本篇部落格我們介紹幾種常見的連結串列,分別是單向連結串列、雙端連結串列、有序連結串列、雙向連結串列以及有迭代器的連結串列。並且會講解一下抽象資料型別(ADT)的思想,如何用 ADT 描述棧和佇列,如何用連結串列代替陣列來實現棧和佇列。
1、連結串列(Linked List)
連結串列通常由一連串節點組成,每個節點包含任意的例項資料(data fields)和一或兩個用來指向上一個/或下一個節點的位置的連結("links")
連結串列(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。
使用連結串列結構可以克服陣列連結串列需要預先知道資料大小的缺點,連結串列結構可以充分利用計算機記憶體空間,實現靈活的記憶體動態管理。但是連結串列失去了陣列隨機讀取的優點,同時連結串列由於增加了結點的指標域,空間開銷比較大。
2、單向連結串列(Single-Linked List)
單鏈表是連結串列中結構最簡單的。一個單鏈表的節點(Node)分為兩個部分,第一個部分(data)儲存或者顯示關於節點的資訊,另一個部分儲存下一個節點的地址。最後一個節點儲存地址的部分指向空值。
單向連結串列只可向一個方向遍歷,一般查詢一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。而插入一個節點,對於單向連結串列,我們只提供在連結串列頭插入,只需要將當前插入的節點設定為頭節點,next指向原頭節點即可。刪除一個節點,我們將該節點的上一個節點的next指向該節點的下一個節點。
在表頭增加節點:
刪除節點:
①、單向連結串列的具體實現
1 package com.ys.datastructure; 2 3 public class SingleLinkedList { 4 private int size;//連結串列節點的個數 5 private Node head;//頭節點 6 7 public SingleLinkedList(){ 8 size = 0; 9 head = null; 10 } 11 12 //連結串列的每個節點類 13 private class Node{ 14 private Object data;//每個節點的資料 15 private Node next;//每個節點指向下一個節點的連線 16 17 public Node(Object data){ 18 this.data = data; 19 } 20 } 21 22 //在連結串列頭新增元素 23 public Object addHead(Object obj){ 24 Node newHead = new Node(obj); 25 if(size == 0){ 26 head = newHead; 27 }else{ 28 newHead.next = head; 29 head = newHead; 30 } 31 size++; 32 return obj; 33 } 34 35 //在連結串列頭刪除元素 36 public Object deleteHead(){ 37 Object obj = head.data; 38 head = head.next; 39 size--; 40 return obj; 41 } 42 43 //查詢指定元素,找到了返回節點Node,找不到返回null 44 public Node find(Object obj){ 45 Node current = head; 46 int tempSize = size; 47 while(tempSize > 0){ 48 if(obj.equals(current.data)){ 49 return current; 50 }else{ 51 current = current.next; 52 } 53 tempSize--; 54 } 55 return null; 56 } 57 58 //刪除指定的元素,刪除成功返回true 59 public boolean delete(Object value){ 60 if(size == 0){ 61 return false; 62 } 63 Node current = head; 64 Node previous = head; 65 while(current.data != value){ 66 if(current.next == null){ 67 return false; 68 }else{ 69 previous = current; 70 current = current.next; 71 } 72 } 73 //如果刪除的節點是第一個節點 74 if(current == head){ 75 head = current.next; 76 size--; 77 }else{//刪除的節點不是第一個節點 78 previous.next = current.next; 79 size--; 80 } 81 return true; 82 } 83 84 //判斷連結串列是否為空 85 public boolean isEmpty(){ 86 return (size == 0); 87 } 88 89 //顯示節點資訊 90 public void display(){ 91 if(size >0){ 92 Node node = head; 93 int tempSize = size; 94 if(tempSize == 1){//當前連結串列只有一個節點 95 System.out.println("["+node.data+"]"); 96 return; 97 } 98 while(tempSize>0){ 99 if(node.equals(head)){ 100 System.out.print("["+node.data+"->"); 101 }else if(node.next == null){ 102 System.out.print(node.data+"]"); 103 }else{ 104 System.out.print(node.data+"->"); 105 } 106 node = node.next; 107 tempSize--; 108 } 109 System.out.println(); 110 }else{//如果連結串列一個節點都沒有,直接列印[] 111 System.out.println("[]"); 112 } 113 114 } 115 116 }
測試:
View Code
列印結果:
②、用單向連結串列實現棧
棧的pop()方法和push()方法,對應於連結串列的在頭部刪除元素deleteHead()以及在頭部增加元素addHead()。
View Code
4、雙端連結串列
對於單項鍊表,我們如果想在尾部新增一個節點,那麼必須從頭部一直遍歷到尾部,找到尾節點,然後在尾節點後面插入一個節點。這樣操作很麻煩,如果我們在設計連結串列的時候多個對尾節點的引用,那麼會簡單很多。
注意和後面將的雙向連結串列的區別!!!
①、雙端連結串列的具體實現
View Code
②、用雙端連結串列實現佇列
View Code
5、抽象資料型別(ADT)
在介紹抽象資料型別的時候,我們先看看什麼是資料型別,聽到這個詞,在Java中我們可能首先會想到像 int,double這樣的詞,這是Java中的基本資料型別,一個數據型別會涉及到兩件事:
①、擁有特定特徵的資料項
②、在資料上允許的操作
比如Java中的int資料型別,它表示整數,取值範圍為:-2147483648~2147483647,還能使用各種操作符,+、-、*、/ 等對其操作。資料型別允許的操作是它本身不可分離的部分,理解型別包括理解什麼樣的操作可以應用在該型別上。
那麼當年設計計算機語言的人,為什麼會考慮到資料型別?
我們先看這樣一個例子,比如,大家都需要住房子,也都希望房子越大越好。但顯然,沒有錢,考慮房子沒有意義。於是就出現了各種各樣的商品房,有別墅的、複式的、錯層的、單間的……甚至只有兩平米的膠囊房間。這樣做的意義是滿足不同人的需要。
同樣,在計算機中,也存在相同的問題。計算1+1這樣的表示式不需要開闢很大的儲存空間,不需要適合小數甚至字元運算的記憶體空間。於是計算機的研究者們就考慮,要對資料進行分類,分出來多種資料型別。比如int,比如float。
雖然不同的計算機有不同的硬體系統,但實際上高階語言編寫者才不管程式執行在什麼計算機上,他們的目的就是為了實現整形數字的運算,比如a+b等。他們才不關心整數在計算機內部是如何表示的,也不管CPU是如何計算的。於是我們就考慮,無論什麼計算機、什麼語言都會面臨類似的整數運算,我們可以考慮將其抽象出來。抽象是抽取出事物具有的普遍性本質,是對事物的一個概括,是一種思考問題的方式。
抽象資料型別(ADT)是指一個數學模型及定義在該模型上的一組操作。它僅取決於其邏輯特徵,而與計算機內部如何表示和實現無關。比如剛才說得整型,各個計算機,不管大型機、小型機、PC、平板電腦甚至智慧手機,都有“整型”型別,也需要整形運算,那麼整型其實就是一個抽象資料型別。
更廣泛一點的,比如我們剛講解的棧和佇列這兩種資料結構,我們分別使用了陣列和連結串列來實現,比如棧,對於使用者只需要知道pop()和push()方法或其它方法的存在以及如何使用即可,使用者不需要知道我們是使用的陣列或是連結串列來實現的。
ADT的思想可以作為我們設計工具的理念,比如我們需要儲存資料,那麼就從考慮需要在資料上實現的操作開始,需要存取最後一個數據項嗎?還是第一個?還是特定值的項?還是特定位置的項?回答這些問題會引出ADT的定義,只有完整的定義了ADT後,才應該考慮實現的細節。
這在我們Java語言中的介面設計理念是想通的。
6、有序連結串列
前面的連結串列實現插入資料都是無序的,在有些應用中需要連結串列中的資料有序,這稱為有序連結串列。
在有序連結串列中,資料是按照關鍵值有序排列的。一般在大多數需要使用有序陣列的場合也可以使用有序連結串列。有序連結串列優於有序陣列的地方是插入的速度(因為元素不需要移動),另外連結串列可以擴充套件到全部有效的使用記憶體,而陣列只能侷限於一個固定的大小中。
View Code
在有序連結串列中插入和刪除某一項最多需要O(N)次比較,平均需要O(N/2)次,因為必須沿著連結串列上一步一步走才能找到正確的插入位置,然而可以最快速度刪除最值,因為只需要刪除表頭即可,如果一個應用需要頻繁的存取最小值,且不需要快速的插入,那麼有序連結串列是一個比較好的選擇方案。比如優先順序佇列可以使用有序連結串列來實現。
7、有序連結串列和無序陣列組合排序
比如有一個無序陣列需要排序,前面我們在講解氣泡排序、選擇排序、插入排序這三種簡單的排序時,需要的時間級別都是O(N2)。
現在我們講解了有序連結串列之後,對於一個無序陣列,我們先將陣列元素取出,一個一個的插入到有序連結串列中,然後將他們從有序連結串列中一個一個刪除,重新放入陣列,那麼陣列就會排好序了。和插入排序一樣,如果插入了N個新資料,那麼進行大概N2/4次比較。但是相對於插入排序,每個元素只進行了兩次排序,一次從陣列到連結串列,一次從連結串列到陣列,大概需要2*N次移動,而插入排序則需要N2次移動,
效率肯定是比前面講的簡單排序要高,但是缺點就是需要開闢差不多兩倍的空間,而且陣列和連結串列必須在記憶體中同時存在,如果有現成的連結串列可以用,那麼這種方法還是挺好的。
8、雙向連結串列
我們知道單向連結串列只能從一個方向遍歷,那麼雙向連結串列它可以從兩個方向遍歷。
具體程式碼實現:
View Code
我們也可以用雙向連結串列來實現雙端佇列,這裡就不做具體程式碼演示了。
9、總結
上面我們講了各種連結串列,每個連結串列都包括一個LinikedList物件和許多Node物件,LinkedList物件通常包含頭和尾節點的引用,分別指向連結串列的第一個節點和最後一個節點。而每個節點物件通常包含資料部分data,以及對上一個節點的引用prev和下一個節點的引用next,只有下一個節點的引用稱為單向連結串列,兩個都有的稱為雙向連結串列。next值為null則說明是連結串列的結尾,如果想找到某個節點,我們必須從第一個節點開始遍歷,不斷通過next找到下一個節點,直到找到所需要的。棧和佇列都是ADT,可以用陣列來實現,也可以用連結串列實現。