鏈表是什麼?
上幾篇文章中,分別介紹了動態數組,棧和隊列,其中都是通過resize的方法進行動態擴容,從而實現了動態的數據結構,不過這種方法稱爲僞動態。真正的動態數據結構還要從鏈表說起,鏈表是真正的動態數據結構,一個動態數據結構不需要處理固定容量的問題,而且是最簡單的動態數據結構,學好鏈表很重要,對後面複雜的數據結構是一種鋪墊。鏈表見名知意,元素和元素之間就像鏈子一樣拴住,元素也可以成爲節點(Node),叫啥都行,反正能理解就好。每一個元素有兩部分組成,一部分是元素本身,另一部分是下一個元素的引用。下文將元素本身就叫e,他的下一個引用就叫做next。
class Node<E>
{
E e; //當前元素本身
Node next; //下一個元素的引用
}
上圖表示,第一個元素的e是1,他的next是2。第二個元素的e是2,他的next是3。最後一個元素的e是3,但next爲null,因爲3就是最後一個元素,可以說如果一個元素的next爲null,就代表他是最後一個元素。如果我想找到2,那就通過1去找,如果我想找到3,那不好意思,你也得通過1去找,因爲1和3之間沒有直接的關聯,只能先通過1先找到2,2裏面有存儲3的地址。這也代表鏈表沒有隨機訪問的能力,比如數組可以通過索引去找,效率很高。鏈表想找一個元素只能從頭到尾遍歷去找,查詢效率低。
鏈表的實現
首先創建了一個Node內部類,目的是隻給LinkedList外部類用,外面的類是不可以操作Node對象的,這樣做是比較安全的。成員變量分別是E e和Node next,e用於存儲元素本身,next存儲下一個元素的引用。LinkedList裏面維護了兩個變量,Node head和size,head用於存儲目前鏈表的頭元素,size用於維護鏈表裏元素的個數。
public class LinkedList<E> {
//內部維護一個Node對象,給外部類(LinkedList)使用
private class Node<E> {
public E e; //當前Node對象的元素值
public Node next; //當前Node對象下一個對象的引用
public Node(E e,Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this.e = e;
this.next = null;
}
public Node() {
this.e = null;
this.next = null;
}
}
//頭元素節點
private Node head;
//當前鏈表節點個數
private int size;
public LinkedList() {
head = null;
size = 0;
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
添加一個頭節點
添加頭節點就是要創建一個新Node節點,新節點的e爲666,新節點的next就指向了原來的頭節點,改變指向後,要維護一下head,讓新創建的這個節點變成頭節點。最後維護一下size的個數。
public void addFirst(E e)
{
Node node = new Node(e); //創建一個節點
node.next = head; //節點的next爲當前頭節點
head = node; //改變head指向,現在頭的位置變成了最新的node
//head = new Node(e,head); 以上三步可簡化成這種寫法
size++;
}
按照索引位置添加元素
現在要把新元素添加到指定位置,比如索引爲2的位置插入一個新元素。那就要找到索引2之前的那個元素,因爲新元素插入後,要維護一下指向關係,原來是1的next爲2,現在666的next變成了2,1的next爲666。找到了1之後,先把1的next給666,讓666的next先指向2,然後1的next在指向666就好啦。看下面圖或者自己畫一下就很清楚了。可是怎麼能找到索引爲1的元素呢?遍歷!我們可以維護一個變量,用於保存每次next的值,可以看到從頭元素到1的位置只需要一次next。
public void addByIndex(E e,int index) //index = 2
{
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Because index is illegal.");
if(index == 0)
addFirst(e);
//1.要想得到某個位置的元素,需要從第一個往後找,因爲第一個存儲的是第二個的"聯繫方式",第二個存儲的是第三個人的"聯繫方式"。
//這裏循環只是遍歷多少次才能拿到索引前的那個元素,裏面的x變量沒有任何使用意義。你也可以寫成x = 1; x < index; 只要是循環次數算對了就行。
//0 1 2 3 , 如果index = 2,那麼待插入元素就是這樣 0 1 _ 2 3 ,那麼從頭元素開始遍歷,拿到索引爲1的節點,只需要經過一次next
Node prevNode = head;
for(int x = 0; x < index - 1; x++) {
head = prevNode.next;
}
//2.拿到currentNode後,維護指向關係
Node node = new Node(e);
node.next = prevNode.next;
prevNode.next = node;
//currentNode.next = new Node(node,currentNode.next);//簡略寫法
size++;
}
虛擬節點的引入
實現上面的addByIndex方法時,需要對index爲0時,做特殊判斷處理。那能不能不做特殊判斷,改變一種思想,讓addFirst方法也能複用呢?辦法是使用虛擬節點。設計一個dummyHead,每次在第0個位置添加元素時候,都可以通過dummyHead找到原來的頭元素位置。然後按索引添加的邏輯就可以複用了。
private Node dummyHead;
private int size;
public LinkedList() {
dummyHead = new Node(null, null); //創建虛擬節點
size = 0;
}
//鏈表頭添加元素
public void addFirst(E e) {
addByIndex(e, 0);
}
//鏈表尾添加元素
public void addLast(E e) {
addByIndex(e, size);
}
public void addByIndex(E e, int index) //3
{
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Because index is illegal.");
//如果index爲3,那麼從dummyHead到索引2的位置,一共要三次next。循環從0開始,然後<3,也就是0 1 2一共三次
Node currentNode = dummyHead; //從頭元素開始遍歷 dummy 0 1 2 -- 3
for (int x = 0; x < index; x++) {
dummyHead = currentNode.next;
}
Node node = new Node(e);
node.next = currentNode.next;
currentNode.next = node;
//currentNode.next = new Node(node,currentNode.next);//簡略寫法
size++;
}
在提供一些基礎的方法,比如查詢,刪除,是否包含某個元素,修改某個元素。只要掌握了遍歷鏈表的思想,其實萬變不離其宗。要注意的就是控制好索引的邊界。
public E get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get filed.Index is not true!");
Node currentNode = dummyHead;
for (int x = 0; x < index; x++) {
currentNode = currentNode.next;
}
return (E) currentNode.next.e;
}
public E getFirst()
{
return get(0);
}
public E getLast()
{
return get(size - 1);
}
public void set(E e,int index)
{
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get filed.Index is not true!");
Node currentNode = dummyHead;
for(int x = 0; x < index; x++) {
currentNode = currentNode.next;
}
currentNode.next.e = e;
}
public boolean contains(E e)
{
Node currentNode = dummyHead;
for(int x = 0; x < size; x++)
{
currentNode = currentNode.next;
if(currentNode.e.equals(e))
{
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("linkedHead[");
Node currentNode = dummyHead;
for(int x = 1; x <= size; x++)
{
currentNode = currentNode.next;
sb.append(currentNode.e + "....");
}
sb.append("]linkedFooter");
return sb.toString();
}
刪除操作
//刪除鏈表中的某個元素
public E delete(int index)
{
Node currentNode = dummyHead;
for(int x = 0; x < index; x++) {
currentNode = currentNode.next;
}
Node delNode = currentNode.next;
currentNode.next = delNode.next;
delNode.next = null;
size--;
return (E) delNode.e;
}