目錄java
今天咱們來聊聊「鏈表(Linked list)」這個數據結構。node
在咱們上一章中【從今天開始好好學數據結構02】棧與隊列棧與隊列底層都是採用順序存儲的這種方式的,而今天要聊的鏈表則是採用鏈式存儲,鏈表能夠說是繼數組以後第二種使用得最普遍的通用數據結構了,可見其重要性!算法
相比數組,鏈表是一種稍微複雜一點的數據結構。對於初學者來講,掌握起來也要比數組稍難一些。這兩個很是基礎、很是經常使用的數據結構,咱們經常將會放到一起來比較。因此咱們先來看,這二者有什麼區別。數組須要一塊連續的內存空間來存儲,對內存的要求比較高。而鏈表偏偏相反,它並不須要一塊連續的內存空間,它經過「指針」將一組零散的內存塊串聯起來使用,鏈表結構五花八門,今天我重點給你介紹三種最多見的鏈表結構,它們分別是:單鏈表、雙向鏈表和循環鏈表。數組
鏈表經過指針將一組零散的內存塊串聯在一塊兒。其中,咱們把內存塊稱爲鏈表的「結點」。爲了將全部的結點串起來,每一個鏈表的結點除了存儲數據以外,還須要記錄鏈上的下一個結點的地址。而尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址NULL,表示這是鏈表上最後一個結點。
@數據結構
package demo2; //一個節點 public class Node { //節點內容 int data; //下一個節點 Node next; public Node(int data) { this.data=data; } //爲節點追回節點 public Node append(Node node) { //當前節點 Node currentNode = this; //循環向後找 while(true) { //取出下一個節點 Node nextNode = currentNode.next; //若是下一個節點爲null,當前節點已是最後一個節點 if(nextNode==null) { break; } //賦給當前節點 currentNode = nextNode; } //把須要追回的節點追加爲找到的當前節點的下一個節點 currentNode.next=node; return this; } //插入一個節點作爲當前節點的下一個節點 public void after(Node node) { //取出下一個節點,做爲下下一個節點 Node nextNext = next; //把新節點做爲當前節點的下一個節點 this.next=node; //把下下一個節點設置爲新節點的下一個節點 node.next=nextNext; } //顯示全部節點信息 public void show() { Node currentNode = this; while(true) { System.out.print(currentNode.data+" "); //取出下一個節點 currentNode=currentNode.next; //若是是最後一個節點 if(currentNode==null) { break; } } System.out.println(); } //刪除下一個節點 public void removeNext() { //取出下下一個節點 Node newNext = next.next; //把下下一個節點設置爲當前節點的下一個節點。 this.next=newNext; } //獲取下一個節點 public Node next() { return this.next; } //獲取節點中的數據 public int getData() { return this.data; } //當前節點是不是最後一個節點 public boolean isLast() { return next==null; } }
package demo2.test; import demo2.Node; public class TestNode { public static void main(String[] args) { //建立節點 Node n1 = new Node(1); Node n2 = new Node(2); Node n3 = new Node(3); //追加節點 n1.append(n2).append(n3).append(new Node(4)); //取出下一個節點的數據 // System.out.println(n1.next().next().next().getData()); //判斷節點是否爲最後一個節點 // System.out.println(n1.isLast()); // System.out.println(n1.next().next().next().isLast()); //顯示全部節點內容 n1.show(); //刪除一個節點 // n1.next().removeNext(); //顯示全部節點內容 // n1.show(); //插入一個新節點 Node node = new Node(5); n1.next().after(node); n1.show(); } }
鏈表要想隨機訪問第k個元素,就沒有數組那麼高效了。由於鏈表中的數據並不是連續存儲的,因此沒法像數組那樣,根據首地址和下標,經過尋址公式就能直接計算出對應的內存地址,而是須要根據指針一個結點一個結點地依次遍歷,直到找到相應的結點。app
你能夠把鏈表想象成一個隊伍,隊伍中的每一個人都只知道本身後面的人是誰,因此當咱們但願知道排在第k位的人是誰的時候,咱們就須要從第一我的開始,一個一個地往下數。因此,鏈表隨機訪問的性能沒有數組好,須要O(n)的時間複雜度。oop
接下來咱們再來看一個稍微複雜的,在實際的軟件開發中,也更加經常使用的鏈表結構:雙向鏈表。單向鏈表只有一個方向,結點只有一個後繼指針next指向後面的結點。而雙向鏈表,顧名思義,它支持兩個方向,每一個結點不止有一個後繼指針next指向後面的結點,還有一個前驅指針prev指向前面的結點。性能
public class DoubleNode { //上一個節點 DoubleNode pre=this; //下一個節點 DoubleNode next=this; //節點數據 int data; public DoubleNode(int data) { this.data=data; } //增節點 public void after(DoubleNode node) { //原來的下一個節點 DoubleNode nextNext = next; //把新節點作爲當前節點的下一個節點 this.next=node; //把當前節點作新節點的前一個節點 node.pre=this; //讓原來的下一個節點做新節點的下一個節點 node.next=nextNext; //讓原來的下一個節點的上一個節點爲新節點 nextNext.pre=node; } //下一個節點 public DoubleNode next() { return this.next; } //上一個節點 public DoubleNode pre() { return this.pre; } //獲取數據 public int getData() { return this.data; } }
import demo2.DoubleNode; public class TestDoubleNode { public static void main(String[] args) { //建立節點 DoubleNode n1 = new DoubleNode(1); DoubleNode n2 = new DoubleNode(2); DoubleNode n3 = new DoubleNode(3); //追加節點 n1.after(n2); n2.after(n3); //查看上一個,本身,下一個節點的內容 System.out.println(n2.pre().getData()); System.out.println(n2.getData()); System.out.println(n2.next().getData()); System.out.println(n3.next().getData()); System.out.println(n1.pre().getData()); } }
若是咱們但願在鏈表的某個指定結點前面插入一個結點或者刪除操做,雙向鏈表比單鏈表有很大的優點。雙向鏈表能夠在O(1)時間複雜度搞定,而單向鏈表須要O(n)的時間複雜度,除了插入、刪除操做有優點以外,對於一個有序鏈表,雙向鏈表的按值查詢的效率也要比單鏈表高一些。由於,咱們能夠記錄上次查找的位置p,每次查詢時,根據要查找的值與p的大小關係,決定是往前仍是日後查找,因此平均只須要查找一半的數據。測試
如今,你有沒有以爲雙向鏈表要比單鏈表更加高效呢?這就是爲何在實際的軟件開發中,雙向鏈表儘管比較費內存,但仍是比單鏈表的應用更加普遍的緣由。若是你熟悉Java語言,你確定用過LinkedHashMap這個容器。若是你深刻研究LinkedHashMap的實現原理,就會發現其中就用到了雙向鏈表這種數據結構。實際上,這裏有一個更加劇要的知識點須要你掌握,那就是用空間換時間的設計思想。當內存空間充足的時候,若是咱們更加追求代碼的執行速度,咱們就能夠選擇空間複雜度相對較高、但時間複雜度相對很低的算法或者數據結構。相反,若是內存比較緊缺,好比代碼跑在手機或者單片機上,這個時候,就要反過來用時間換空間的設計思路。this
循環鏈表是一種特殊的單鏈表。實際上,循環鏈表也很簡單。它跟單鏈表惟一的區別就在尾結點。咱們知道,單鏈表的尾結點指針指向空地址,表示這就是最後的結點了。而循環鏈表的尾結點指針是指向鏈表的頭結點。和單鏈表相比,循環鏈表的優勢是從鏈尾到鏈頭比較方便。當要處理的數據具備環型結構特色時,就特別適合採用循環鏈表。好比著名的約瑟夫問題。儘管用單鏈表也能夠實現,可是用循環鏈表實現的話,代碼就會簡潔不少。
package demo2; //一個節點 public class LoopNode { //節點內容 int data; //下一個節點 LoopNode next=this; public LoopNode(int data) { this.data=data; } //插入一個節點作爲當前節點的下一個節點 public void after(LoopNode node) { //取出下一個節點,做爲下下一個節點 LoopNode nextNext = next; //把新節點做爲當前節點的下一個節點 this.next=node; //把下下一個節點設置爲新節點的下一個節點 node.next=nextNext; } //刪除下一個節點 public void removeNext() { //取出下下一個節點 LoopNode newNext = next.next; //把下下一個節點設置爲當前節點的下一個節點。 this.next=newNext; } //獲取下一個節點 public LoopNode next() { return this.next; } //獲取節點中的數據 public int getData() { return this.data; } }
package demo2.test; import demo2.LoopNode; public class TestLoopNode { public static void main(String[] args) { LoopNode n1 = new LoopNode(1); LoopNode n2 = new LoopNode(2); LoopNode n3 = new LoopNode(3); LoopNode n4 = new LoopNode(4); //增長節點 n1.after(n2); n2.after(n3); n3.after(n4); System.out.println(n1.next().getData()); System.out.println(n2.next().getData()); System.out.println(n3.next().getData()); System.out.println(n4.next().getData()); } }
最後,咱們再對比一下數組,數組的缺點是大小固定,一經聲明就要佔用整塊連續內存空間。若是聲明的數組過大,系統可能沒有足夠的連續內存空間分配給它,致使「內存不足(out of memory)」。若是聲明的數組太小,則可能出現不夠用的狀況。這時只能再申請一個更大的內存空間,把原數組拷貝進去,很是費時。鏈表自己沒有大小的限制,自然地支持動態擴容,我以爲這也是它與數組最大的區別。
你可能會說,咱們Java中的ArrayList容器,也能夠支持動態擴容啊?事實上當咱們往支持動態擴容的數組中插入一個數據時,若是數組中沒有空閒空間了,就會申請一個更大的空間,將數據拷貝過去,而數據拷貝的操做是很是耗時的。
我舉一個稍微極端的例子。若是咱們用ArrayList存儲了了1GB大小的數據,這個時候已經沒有空閒空間了,當咱們再插入數據的時候,ArrayList會申請一個1.5GB大小的存儲空間,而且把原來那1GB的數據拷貝到新申請的空間上。聽起來是否是就很耗時?
除此以外,若是你的代碼對內存的使用很是苛刻,那數組就更適合你。由於鏈表中的每一個結點都須要消耗額外的存儲空間去存儲一份指向下一個結點的指針,因此內存消耗會翻倍。並且,對鏈表進行頻繁的插入、刪除操做,還會致使頻繁的內存申請和釋放,容易形成內存碎片,若是是Java語言,就有可能會致使頻繁的GC(Garbage Collection,垃圾回收)。
因此,在咱們實際的開發中,針對不一樣類型的項目,要根據具體狀況,權衡到底是選擇數組仍是鏈表!
若是本文對你有一點點幫助,那麼請點個讚唄,謝謝~
最後,如有不足或者不正之處,歡迎指正批評,感激涕零!若是有疑問歡迎留言,絕對第一時間回覆!
歡迎各位關注個人公衆號,一塊兒探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...