前言
前面兩節內容我們詳細介紹了ArrayList,一是手寫實現ArrayList數據結構,而是通過分析ArrayList源碼看看內置實現,關於集合內容一如既往,本節課我們繼續學習集合LinkedList,我們首先入門LinkedList數據結構,然后再去看看LinkedList源碼是如何實現的,我們開始吧。
LinkedList入門
LinkedList內置是通過雙鏈表數據結構來存儲數據,和ArrayList不同的是,ArrayList屬於真正意義物理意義上的線性結構,而LinkedList也屬於線性鏈表,只不過需要通過我們手動來關聯前后節點數據,同時呢,雙鏈表和單鏈表只是在結構上有所不同而已,只是雙鏈表多了一個前驅節點,其他無差異,那么到底何為雙鏈表呢?在我們日常生活中到處都是這樣的例子,比如我們音樂播放器應該算比較形象了,如下:
單鏈表自定義實現
接下來我們來實現單鏈表,然后對單鏈表進行改造成雙鏈表,我們看到如上播放器,單鏈表只是少了前驅節點,但是有后繼節點(如上寫錯了),所以我們需要定義一個節點,然后在此節點上有連接下一節點的引用(在C或C++中為指針),和當前節點所存儲的數據,所以我們定義如下泛型節點類:
public class Node<T> { //當前節點值 public T data; //后繼節點 public Node next; public Node(T data) { this.data = data; } }
接下來則是定義鏈表來操作上述節點類並存儲數據了, 這里我們稍微做的簡單點,在鏈表中會存在頭節點和尾節點,這里呢我們通過來頭節點來操作,等我們升級到雙鏈表時再來定義尾節點,所以在單鏈表中有頭節點和鏈表長度兩個變量,如下:
public class MyLinkedList<T> { //頭節點 private Node head; //鏈表元素長度 private int length; }
溫馨提示:這里我就不給大家畫圖演示了,自行腦補,實在感覺繞的話自己在畫板或紙上畫一下就明白了,我也是在紙上畫了一番才動手寫代碼的。首先我們需要考慮頭節點和尾節點即播放器中第一首歌和最后一首歌,然后針對指定位置添加歌曲通過next串聯就形成了歌曲列表,更為形象的例子當屬我們吃過的串串了。那么接下來我們完成往播放器列表中添加第一個首歌,此時我們應該想,頭節點是否添加了第一首歌,若不存在則直接實例化頭節點即可,若已存在第一首歌,我們則將重新實例化一首歌,然后將其已添加的第一首歌的引用賦值給新添加的歌曲的next,所以就有了如下方法:
//添加至頭結點 public void addToHead(T data) { if (head == null) { head = new Node(data); } else { Node temp = head; head = new Node(data); head.next = temp; } length++; }
好了,將新添加的歌曲放在第一首我們已經完全搞定了,然后我們再來往歌曲列表中最后添加一首歌曲,這個時候我們腫么知道是最后一首呢,只要next為空,說明就是最后一首歌曲,這就是判斷依據,這點就不用我再過多解釋了,那么就有了如下方法:
//添加至尾節點 public void addToTail(T data) { Node temp = head; while (temp.next != null) { temp = temp.next; } temp.next = new Node(data); length++; }
單鏈表的確定就在這里,我們只能循環遍歷才能找到最后一首,然后添加對應歌曲,所以當數據量足夠大時,可想其性能。接下來則是最重要的一塊了,我們想要在指定歌曲下添加歌曲,這個時候就涉及到找到對應歌曲索引然后添加數據,
//添加到指定索引元素 public void add(int index, T data) { if (index < 0) { throw new RuntimeException("非法索引"); } if (index > length) { throw new RuntimeException("超出索引邊界"); } if (head == null || index == 0) { addToHead(data); return; } //頭節點 Node temp = head; //指定索引下一節點 Node holder; for (int i = 0; i < index - 1 && temp.next != null; i++) { temp = temp.next; } //未插入節點時指定索引下一節點 holder = temp.next; //指定索引節點下一節點即待插入的節點 temp.next = new Node(data); //將列表中指定索引節點下一節點引用指向指定待插入節點(此時指定索引下節點即為待插入節點,然后再下一節點即為待插入節點) temp.next.next = holder; length++; }
接下來則是根據指定索引查找元素,我就不解釋了,直接上代碼,如下
//根據索引查找元素 public T find(int index) { if (index < 0) { throw new RuntimeException("非法索引"); } if (length == 0 || index > length) { throw new RuntimeException("超出索引邊界"); } Node temp = head; for (int i = 0; i < index; i++) { temp = temp.next; } return (T) temp.data; }
最后老規矩重寫toString方法,打印鏈表數據,如下:
//鏈表元素大小 public int size() { return length; } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node temp = head; while (temp != null) { sb.append(temp.data); sb.append(","); temp = temp.next; } if (sb.charAt(sb.length() - 1) == ',') { sb.delete(sb.length() - 1, sb.length()); } return sb.toString(); }
最后我們來往播放器列表中添加歌曲做個測試吧,走你,如下:
public class Main { public static void main(String[] args) { MyLinkedList<Integer> list = new MyLinkedList<>(); //添加元素11到頭節點 list.addToHead(11); System.out.println(list); //添加元素15到尾節點 list.addToTail(15); System.out.println(list); //添加元素12到頭節點 list.addToHead(12); System.out.println(list); //添加元素13到頭節點 list.addToHead(13); System.out.println(list); //添加元素8到尾節點 list.addToTail(8); //添加元素7到尾節點 list.addToTail(7); list.add(2, 9); System.out.println(list); //在索引2位置添加元素9 list.add(2, 9); System.out.println(list); //刪除索引為4的元素 list.delete(4); System.out.println(list); } }
雙鏈表自定義實現
有了如上單鏈表的鋪墊,接下來我們再來實現雙鏈表則是輕而易舉了,只不過添加了前驅節點和鏈表中的尾結點而已,走你,我們往節點類中添加前驅節點,如下:
public class Node<T> { //當前節點值 public T data; //前驅節點 public Node previous; //后繼節點 public Node next; public Node(T data) { this.data = data; } }
同理,我們在鏈表類中添加尾節點字段,如下:
public class MyLinkedList<T> { //頭節點 private Node head; //尾節點 private Node tail; //鏈表元素長度 private int length; }
同樣,當添加歌曲至首位時,此時我們也需初始化頭節點,只不過這時多了個尾節點,沒關系,這個時候頭節點就是尾節點,我們封裝一個初始化頭節點和尾節點的方法,如下:
//初始化頭接點和尾節點 void initHead(T data) { //初始化頭節點 head = new Node(data); //此時尾節點即頭節點 tail = head; }
然后添加歌曲至頭節點時,只不過多了個前驅節點,也就相應多了一行代碼而已,就是將已添加首位歌曲的前驅節點賦給待添加的首位歌曲,如下:
//添加元素至頭結點 public void addToHead(T data) { if (head == null) { initHead(data); } else { Node temp = head; head = new Node(data); head.next = temp; temp.previous = head; } length++; }
而添加歌曲至末位時就和上述單鏈表就有些不同了,單鏈表中是直接循環遍歷,這里我們定義了尾節點,所以直接操作尾節點即可,如下:
//添加至尾節點 public void addToTail(T data) { if (size() == 0) { initHead(data); } else { Node temp = tail; tail = new Node(data); temp.next = tail; tail.previous = temp; } length++; }
接下來又是添加指定索引元素的核心方法了,其實也非常簡單,我都將注釋給你寫好了,還是看不懂,建議到紙上畫畫哈。
//添加指定索引元素 public void add(int index, T data) { if (index < 0) { throw new RuntimeException("非法索引"); } if (index > length) { throw new RuntimeException("超出索引邊界"); } if (head == null || index == 0) { initHead(data); return; } //頭節點 Node temp = head; //定義獲取指定索引節點下一節點 Node holder; for (int i = 0; i < index - 1 && temp.next != null; i++) { temp = temp.next; } //當前節點的下一節點 holder = temp.next; //要添加的下一節點 temp.next = new Node(data); //插入節點的后繼節點為當前節點下一節點 temp.next.next = holder; //當前節點下一前驅節點為插入節點 temp.next.next.previous = temp.next; length++; }
無論是添加還是刪除最重要的是我們需要想清楚,添加時和刪除后前驅節點和后繼節點分別指向誰,把這個問題想明白了,那也就沒什么了,走你,刪除方法:
//刪除指定索引元素 public void delete(int index) { if (index < 0) { throw new RuntimeException("非法索引"); } if (length == 0 || index > length) { throw new RuntimeException("超出索引邊界"); } Node temp = head; for (int i = 0; i < index - 1 && temp.next != null; i++) { temp = temp.next; } temp.next.next.previous = temp; temp.next = temp.next.next; length--; }
為了驗證我們所寫代碼,我們打印出對應節點的前驅和后繼節點,如下:
public int size() { return length; } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node temp = head; while (temp != null) { sb.append(temp.data); sb.append(","); if (temp.previous != null && temp.next != null) { System.out.println(temp.previous.data + "<-(" + temp.data + ")->" + temp.next.data); } temp = temp.next; } if (sb.charAt(sb.length() - 1) == ',') { sb.delete(sb.length() - 1, sb.length()); } return sb.toString(); }
控制台測試數據和單鏈表中一樣,結果數據如下(當然我們可以分開打印對應節點前驅和后繼節點去驗證也是闊以的,這里我也驗證過來,么有任何問題):
總結
本節我們通過手寫代碼實現了單鏈表和雙鏈表,還是非常簡單,下一節我們詳細分析LinkedList源碼,感謝您的閱讀,我們下節見