最簡單的動態數據結構 - 鏈表

鏈表是什麼?

上幾篇文章中,分別介紹了動態數組,棧和隊列,其中都是通過resize的方法進行動態擴容,從而實現了動態的數據結構,不過這種方法稱爲僞動態。真正的動態數據結構還要從鏈表說起,鏈表是真正的動態數據結構,一個動態數據結構不需要處理固定容量的問題,而且是最簡單的動態數據結構,學好鏈表很重要,對後面複雜的數據結構是一種鋪墊。鏈表見名知意,元素和元素之間就像鏈子一樣拴住,元素也可以成爲節點(Node),叫啥都行,反正能理解就好。每一個元素有兩部分組成,一部分是元素本身,另一部分是下一個元素的引用。下文將元素本身就叫e,他的下一個引用就叫做next。

class Node<E>
{
    E e; //當前元素本身
    Node next; //下一個元素的引用
}

image.png
上圖表示,第一個元素的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的個數。
image.png

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。

image.png

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找到原來的頭元素位置。然後按索引添加的邏輯就可以複用了。
image.png

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();
}

刪除操作

image.png

//刪除鏈表中的某個元素
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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章