鏈表,和數組一樣,也是一種線性的數據結構。但鏈表在存儲數據的時候,卻不像數組把所有的數據都存儲在一片連續的內存空間中,而是數據分散在內存中,數據和數據之間相互鏈接。數據和數據怎么才能相互鏈接?比如,5和10怎么才能進行鏈接?很顯然,僅僅依靠數據本身是無法鏈接起來的,還需要地址。存儲數據的時候,同時存儲一個地址,當再存儲一個數據的時候,把該數據的地址賦值前一個數據中的地址,只要知道地址,就能找到元素,也就相當於,數據和數據鏈接起來了,所以鏈表在存儲數據的時候,不僅要存數據本身,還要存地址。也正因為如此,鏈表存儲的是一個對象。對象的一個屬性是數據,一個屬性是地址。鏈表是對象與對象之間的鏈接。但地址怎么獲取呢?只要鏈表存儲的是對象,地址就很好獲取了。在Java 和JavaScript中,當創建一個對象,並把這個對象賦給一個變量的時候,這個變量保存的是就這個對象的地址。當把這個變量復制給另外一個變量的時候,復制的是對象的地址,也就是說,兩個變量同時指向了一個對象。也就是說,鏈表中的每一個對象中,它的地址屬性只要聲明成對象類型的變量就可以了。鏈表是對象與對象的鏈接,這也引出了另外一個問題,鏈表是存儲在堆內存中的(因為對象都是存儲在堆內存中的),怎么才能操作它呢?只能把鏈表中的第一個對象賦值給一個變量,操作鏈表的時候,通過這個變量去找到鏈表中的第一個對象,進而找到整個鏈表進行操作。也就是說,鏈表中始終都要存在一個指向鏈表第一個對象的變量,用來找到鏈表,進而操作鏈表。鏈表中的每一個對象也稱為節點, 指向鏈表第一個對象的變量稱為鏈表頭。一個簡單的鏈表如下:
鏈表的分類有三種,單向鏈表(單鏈表),雙向鏈表,循環鏈表。
單向鏈表:鏈接方向是單向的,一個節點只能鏈接它的后一個節點,而不能鏈接它的前一個節點,鏈表的最后一個節點鏈向null。對鏈表的訪問只能從頭部開始,依次向后順序訪問,像上圖一樣。
雙向鏈表,鏈接方向是雙向的,就是它的每一個節點中都有兩個地址(假設是prev和next),prev指向它的前一個節點, next指向后一個節點,第一個節點的prev提向null,最后一個節點next指向null
循環鏈表就是,鏈表的最后一個元素不是指向null,而是指向第一個結點。不管是單向鏈表還是雙向鏈表,它都能變成循環列表
鏈表的實現方式也有兩種方式,一種是不帶頭結點,鏈表頭指向的第一個元素就是真實的數據節點
一種是帶一個頭節點,頭節點並不保存任何有效數據,鏈表頭指向頭節點,頭節點指向鏈表中真正有意義的第一個節點。
先看單向鏈表,由於鏈表中的每一個節點都是對象,所以要聲明一個類來表示這個對象,由於這個類僅在單向鏈表中使用,可以聲明為私有內部類,類的屬性分為數據和地址,為了簡單起見,數據使用整數,地址就是節點類的變量。為了能夠操作鏈表,還需要一個變量,讓它指向鏈表中的第一個節點,所以一個單向鏈表要有兩個屬性,一個是內部節點類,一個是指向鏈表的第一個元素。有時還要計算鏈表中節點的個數,再加上一個屬性size
public class SingleLinkedList { private class Node { // 鏈表的節點類型 int data; // 數據 Node next; // 地址 Node(int data){ this.data = data; } } private Node head; // 指向鏈表的第一個元素,用來找到鏈表,操作鏈表 private int size; // 記錄節點的個數 }
可以想到,這樣的實現方式是不帶頭節點的,因為head默認初始化為null,它並沒有指向一個節點。
插入節點,可以從鏈表的頭部插入,可以從鏈表的尾部插入,還可以從鏈表的中間插入,還可以在某個節點后面插入,相應的,你可以單獨提供方法,也可以提供通用的方法。頭部插入最為簡單,就是創建新的節點,讓新的節點next指向head(把head賦值給新節點的next屬性),然后再讓head指向新的接點(把新節點賦值給head),最后不要忘記size++;
public void insertFirst(int data){ // 創建新的節點 Node newNode = new Node(data);// 新的節點的next指向head newNode.next = head; // 讓head指向新的節點 head = newNode;
size++; }
從尾部插入節點,就有點麻煩了,因為要先找到尾節點,然后再讓尾節點的next 指向新的節點。怎樣才能找到尾節點?尾節點的next 是null,只要找到一個節點的next 屬性是null, 它就是尾節點。如果head.next 是null,則head是尾節點,如果head.next.next 是null,那head.next 就是尾節點。找一個臨時變量temp, 把head賦值給它, 那么temp 也就是指向了第一個節點,判斷temp.next 是不是null, 如果是,它就是尾節點,如果不是,那就把temp.next 再賦值給temp, 這時temp就指向第二個節點。如果temp.next 還不是null, 那就再把temp.next 賦值給temp, 這時temp指向第三個節點。 可以發現,就是不停地把temp.next 賦值給temp,讓temp 指向下一個節點,直到temp.next 是null,那temp 就是最后一個節點,這時把新的節點賦值給temp.next。賦值成功后,不要忘記size++。尾部插入節點要注意的是,先判斷head是否為null,如果head是null,head賦值給temp,temp.next就會報錯。head為null,鏈表為空,也沒有必要循環鏈表。
public void insertLast(int data) { // 新node的next默認是null Node newNode = new Node(data); if (head == null){ head = newNode; } else{ Node temp = head; // 只有node的next的節點是null, 才是最后一個節點。 while (temp.next != null) { temp = temp.next; } // temp是最后一個節點 temp.next = newNode; }
size++; }
在鏈表的中間位置或任意位置插入節點,那就要先定位到這個位置,找到這個位置的前一個節點,因為單鏈表只能鏈接到下一個節點,要在某個位置插入節點,肯定要讓這個位置之前的那個節點的next指向新的節點,同時讓新的節點的next指向原來前一個節點指向的后一個節點。定位位置,找到前一個節點,肯定是要循環遍歷了,遍歷到指定的位置。插入到第n個位置,遍歷n-1次,得到的節點,就是前一個節點。
理論上是這樣,但實現上還要復雜,因為有好多情況要判斷,鏈表是否為空,如果為空,那就只能插入第一個節點。插入的位置是否符合要求,這里要確定一下,鏈表的第 一個節點是位置0還是位置1,如果按照數組的規則,鏈表中的第一個節點就是位置0。那么要插入的位置范圍就是[0, size]. 如果鏈表中只有一個節點,是可以插入到位置1的,位置0的節點next指向這個節點就可以了。
public void insert(int position, int data) { // 創建新節點 Node newNode = new Node(data); // 判斷鏈表是否為空 if(head == null) { // 如果為空,那只允許插入到第一個節點,也就是位置0處,否則報錯 if (position != 0){ throw new RuntimeException("鏈表為空"); } else { head = newNode; } } else { // 至少有一個節點 // 判斷位置是否符合要求 if(position < 0 || position > size) { throw new RuntimeException("超出范圍"); } else { // 插入頭節點 if(position == 0) { insertFirst(data); } else if (position == size) { // 插入尾節點 insertLast(data); } else { Node prevNode = head; // 注意是position -1,如果插入到1位置,那head就是前一個節點,無需循環,position-1 -> 0, 不循環 for (int i = 0; i < position -1; i++) { prevNode = prevNode.next; } // prevNode就是前一個節點。 // nextNode是后一 個節點 Node nextNode = prevNode.next; // 新節點的鏈接:前一個節點的next指向新節點,新節點的next指向后一個節點 prevNode.next = newNode; newNode.next = nextNode; } } }
size++; }
插入功能完成后,就要驗證實現的對不對,簡單的辦法就是把鏈表中的數據打印出來,遍歷整個鏈表。找一個臨時變量curNode, 把head賦值給它, 那么curNode也就指向了第一個節點,此時判斷curNode是不是null, 如果是,鏈表為空,什么都不用做了。如果不是,把curNode的data打印出來,再把curNode.next 賦值給curNode, 這時curNode就指向第二個節點。再判斷curNode是不是null, 如果不是,繼續打印它的data,把curNode.next 賦值給curNode, 這時curNode指向第三個節點。 可以發現,就是不停地判斷curNode是否為null, 如果不是,那就打印data,並curNode.next 賦值給curNode,讓curNode指向下一個節點,直到curNode 為null。這里要注意它和插入尾節點遍歷的不同,插入尾節點,是判斷.next是否為null,這里只判斷節點是否為null
public void display() { Node curNode = head; while (curNode != null) { System.out.print(curNode.data); curNode = curNode.next; } }
刪除節點,刪除節點也有很多情況,刪除頭節點,刪除某個位置節點,刪除某個指定的節點。刪除頭節點比較簡單,直接讓head指向head.next 就可以了,刪除方法通常會返回要刪除的節點
public Node deleteFirst() { if (head == null) { throw new RuntimeException("鏈表為空"); } Node firstDeleted = head; head = head.next; return firstDeleted; }
刪除某個指定的節點,分為三種情況,這個節點正好是頭節點,這個節點在中間或尾節點,這個節點沒有找到。當刪除某個節點時,不僅要找到這個節點,還要找到這個節點的上一個節點,還是因為鏈表是單向的,刪除某個節點,就要讓某個節點的上一個節點的next指向這個節點的next指向的下一個節點,那遍歷查找某個節點時,還要記錄前一個節點。
public Node deleteKey(int key){ if (head == null) { throw new RuntimeException("鏈表為空"); } Node curNode = head; Node prevNode = null; // 刪除的某個key正好位於頭節點中 if(curNode.data == key){ head = curNode.next; return curNode; } // 刪除的某個key位於中間節點或尾節點 while (curNode != null && curNode.data != key) { prevNode = curNode; //記錄前一個節點 curNode = curNode.next; } // 如果找到了 if(curNode != null){ prevNode.next = curNode.next; return curNode; } // 如果沒有找 curNode == null System.out.println("沒有找到"); return null; }
刪除某個位置的節點,要先找到這個節點,同時記錄上一個節點。怎么才能找到這個節點呢?遍歷到該位置。
public Node deleteByPostion(int postion) { if (head == null) { throw new RuntimeException("鏈表為空"); } if(postion < 0 || postion >= size) { throw new RuntimeException("位置不對"); } if(postion == 0){ Node first = head; head = head.next; return first; } Node curNode = head; Node prevNode = null; for (int i = 0; i < postion; i++) { prevNode = curNode; curNode = curNode.next; } prevNode.next = curNode.next; return; curNode; }
查找
public int find(int data){ Node curNode = head; int index = 0; boolean isFind = false; while (curNode != null){ if(curNode.data == data){ isFind = true; break; } else { index++; curNode = curNode.next; } } if(isFind) { return index; } else { return -1; } }