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