單向連結串列的實現(不設立虛擬頭節點)
(希望我所描述的,給你帶來收穫!)——關於閱讀筆者資料結構系列,建議先將程式碼粘至IDE,然後對照文字解釋進行理解
開始丟擲——什麼是連結串列?
答:連結串列(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序表快得多,但是查詢一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)——————(摘自維基百科)
我的補充:連結串列具有真正意義上的動態
我們首要思考的是,我們應該用什麼樣的“節點”去儲存資料,在這裡我採用的是私有內部類的設計——內部類相關知識傳送門(待補充!):
節點程式碼如下:
1 public class LinkedList<E> { 2 private class Node { 3 public E e; //資料域 4 public Node next; //引用域(java中不存在指標說法,但也可以理解成指標)5 6 public Node() { 7 this(null,null); 8 } 9 10 public Node(E e) { 11 this.e = e; 12 this.next = null; 13 } 14 15 public Node(E e,Node next) { 16 this.e = e; 17 this.next = next; 18 } 1920 @Override 21 public String toString() { 22 return e.toString(); 23 } 24 }
}
整體程式碼的補充:
1 public class LinkedList<E> { 2 private class Node { 3 public E e; 4 public Node next; 5 6 public Node() { 7 this(null,null); 8 } 9 10 public Node(E e) { 11 this.e = e; 12 this.next = null; 13 } 14 15 public Node(E e,Node next) { 16 this.e = e; 17 this.next = next; 18 } 19 20 @Override 21 public String toString() { 22 return e.toString(); 23 } 24 } 25 private Node head; 26 private int size; 27 28 public int getSize() { 29 return size; 30 } 31 32 public boolean isEmpty() { 33 return size == 0; 34 } 35 36 public void addFirst(E e) { 37 head = new Node(e,head); 38 size ++; 39 } 40 41 public void add(int index, E e) { 42 if (index < 0 || index > size) 43 throw new IllegalArgumentException("index is illegal"); 44 if (index == 0) 45 addFirst(e); 46 else { 47 Node prev = head; 48 for (int i = 0; i < index - 1; i++) { 49 prev = prev.next; 50 } 51 prev.next = new Node(e,prev.next); 52 size ++; 53 } 54 } 55 56 public void addLast(E e) { 57 add(size,e); 58 } 59 60 //測試列印輸出 61 @Override 62 public String toString() { 63 StringBuilder stringBuilder = new StringBuilder(); 64 stringBuilder.append(String.format("LinkedList size = %d\n",size)); 65 stringBuilder.append("head ["); 66 Node cur = head; 67 for (int i = 0; cur != null; cur = cur.next) { 68 stringBuilder.append(cur); 69 if (cur.next != null) 70 stringBuilder.append("->"); 71 } 72 stringBuilder.append("] tail"); 73 return String.valueOf(stringBuilder); 74 } 75 76 public static void main(String[] args) { 77 LinkedList<Integer> linkedList = new LinkedList<>(); 78 for (int i = 0; i < 10; i++) { 79 linkedList.addLast(i); 80 } 81 System.out.println(linkedList.toString()); 82 } 83 }
關於addFirst方法的闡述:
這裡我們首先設立一個頭節點(head)的概念,在這裡!如果連結串列有資料,頭節點其實就是連結串列頭的那個節點(如果連結串列沒有資料呢?那麼null就是該連結串列的“表頭”)
1、addFirst也就是說,在連結串列的第一個位置插入一個數據元素!
2、想要在表頭插入一個元素e,必須先建立一個節點去承載 ---->>>Node node = new Node(e);
3、因為這個節點要當表頭了,所以它要放在head的前面 ---->>> node.next = head;
4、這個節點成為新的表頭了,所以head這個名字要交給這個新的節點 ---->>> head = node;
5、size是連結串列長度的變數,新增節點成功,則size ++
總結以上:三段程式碼可以直接濃縮為 head = new Node(e,head); ------->這和Node類的雙參建構函式有關
關於add(int index, E e)方法的闡述:
要強調的是,這裡的index是我們人為意識去強加給連結串列的(目的是我們在底層操作的時候,更便於存取資料)——陣列具有索引(index),可以通過索引值直接取資料,但連結串列不可以!
很自然的,add方法是說:我們要在index位置上儲存元素e
第一步:我們需要找到 index - 1的那個節點,因為每一個節點只有指向下一個節點的“引用域”(這是單向連結串列)
第二步:index - 1位置的節點我們稱為 prev,待插入的節點稱為 cur,我們需要操作的是----->>>cur.next = prev.next;
(prev是(index - 1)位置的節點,那 prev.next 就是index位置的節點了,cur 需要替換 prev.next 的位置得先像 prev 學習,cur.next 要成為index位置上的節點)
第三步:當cur.next在index位置上,要想明白的是prev.next也在index位置上 執行----->>> prev.next = cur;(prev.next 始終是index位置節點,我把cur放在index位置上,那自然的cur.next 就是index + 1上的位置了——插入了新節點cur!)
第四步:新增節點 我們要維護我們的size變數------->>>size ++
總結來說:三個步驟依然可以用-------->prev.next = new Node(e,prev.next);這樣的形式來表達!
在闡述add方法的時候,我用了index - 1位置這樣的描述,認真閱讀了程式碼的朋友會發現add方法中for迴圈的條件語句是這樣的:
for (int i = 0; i < index - 1; i++) { prev = prev.next; }
該迴圈表明的是,當 i == index - 2是for的最後一次迴圈;那index - 2才是待插入位置的前一個位置(prve)?
答:前面闡述的add方法中,筆者只是借用index、index - 1 來表明邏輯上的前後關係、因為實際上在不設立虛擬節點的前提下,我們預設表頭的第一個節點就是head節點! 迴圈體中 prev = prev.next; 語句, 假使 i == 0,第一次迴圈就已經使得prev在第二個位置上了;假設index可以取-1(-1是0的前一個節點,要在-1 和 0之間插入資料),如果i == 0,prev實則在index為1的位置,要使得插入成功,prev必須往回走兩步才能走到-1得位置,因此就是相差2,index - 2的邏輯來得自然(-1這種思考方式個人認為很奇特,讀者可以換種思路——比如在index == 3位置上插入節點;可以畫圖理解)