數據結構之——鏈表

課程《玩轉數據結構》學習java

  • 鏈表與數組
  • 使用虛擬頭節點
  • 鏈表的增刪改查
  • 鏈表的時間複雜度分析
  • 使用鏈表實現棧與隊列

鏈表與數組

鏈表是典型的線性動態數據結構,也是學習樹形數據結構的敲門磚。與數組不一樣,鏈表的意義在於動態二字。再回顧一下什麼是數組:在內存中開闢一段連續的存儲空間的相同數據類型元素存儲的集合 。數組並不具有動態的能力,爲了讓數組具備動態的特性,咱們能夠實現本身的數組,讓其具有自動擴容以及縮容(resize)的能力。動態數組
而對於,與隊列這兩種具有特殊功能的線性數據結構,可使用數組做爲底層原理來實現。對於棧的特性即LIFO,使用動態數組做爲底層實現知足了棧各個功能的時間複雜度爲O(1)。而隊列的特性爲:FIFO,若是使用數組做爲底層,在隊列的出隊操做時,這一項功能的時間複雜度就爲O(n)。使用循環隊列的思想,則能夠將出隊操做優化至O(1)。
鏈表則是一種真正的動態數據結構。由於數組在內存的空間是連續的,因此最大的優點 是支持「隨機訪問」,而鏈表最大的優勢則是「真正的動態」。鏈表不會浪費多餘的內存空間,不須要處理容量的問題,可是也喪失了數組的隨機訪問的能力。
node


上圖表示的就是一個鏈表,圖中的圓形表示爲鏈表中的一個節點,每一個節點都存儲着下一個節點的引用。鏈表的尾部,也就是鏈中最後一個節點,是沒有指向下一個節點的,因此天然指向了Null。要想實現 「一個節點存儲着下一個節點的引用」功能並不難,咱們只須要在鏈表類中,增長一個內部類Node便可:

public class LinkedList<E>{
    private class Node{
        public E e;// 存儲數據
        public Node next;// 指向下一個節點
        public Node(E e,Node next){
            this.e = e;
            this.next = next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }
        @Override
        public String toString(){
            return e.toString();
        }
    }
    // 指向鏈表頭
    private Node head;
    private int size;
    public LinkedList(){
        head = null;
        size = 0;
    }
    //  獲取鏈表中元素的個數
    public int getSize(){
        return size;
    }

    // 判斷鏈表是否爲空
    public boolean isEmpty(){
        return size==0;
    }
}
複製代碼

鏈表中每個節點都存儲着下一個節點的引用,那麼誰來存儲鏈表頭部的引用呢?因此,與數組不一樣,鏈表須要額外去維護一個變量,這個變量咱們稱做head,用於存儲鏈表頭的引用。git

使用虛擬頭節點

如今向鏈表添加元素。
咱們須要考慮兩種狀況,第一種狀況爲:向鏈表頭部添加元素。
github


在向鏈表頭部添加元素時,咱們須要:

1:newNode.next = head;// 將添加的節點的next指向head
2: head = newNode;// 將head再次指向頭部
複製代碼

實現代碼爲:數組

public void addFirst(E e){
    head = new Node(e,head);
    size++;
}
複製代碼

還有一種狀況是:在鏈表任意位置添加元素,這一點和在鏈表頭部添加元素略有不一樣。(廣泛來說,當你選擇了鏈表這種數據結構時,每每不會涉及向鏈表的中間添加元素,實現此功能是爲了更加深刻地學習鏈表)
bash


咱們考慮一下,在鏈表中間插入元素時,假設插入位置的"索引"稱做index。咱們首先須要將插入的節點指向index處的節點,本圖爲向index==2的位置插入元素99。而後再將index位置前的一個節點指向被插入的節點,那麼咱們如何獲取index-1處的節點呢?答案就是遍歷。咱們須要一個特殊的變量,讓它指向index-1處的節點,如今用prev表示這個變量,最初讓 prev=head,每次讓 prev=prev.next,遍歷index-1次,就能夠得到index-1處的,也就是待插入位置的前一個位置的索引處的節點。插入的過程爲:

1: newNode.next = prev.next
2: prev.next = newNode
複製代碼

代碼爲:數據結構

public void add(int index,E e){
    if(index<0 || index>size)
        throw new IllegalArgumentException("Index is Illegal");
    if(index==0){
        // 若是在鏈表頭部添加元素
        addFirst(e);
    }else{
        Node prev = head;
        for(int i=0;i<index-1;i++){
            prev = prev.next;
        }
        prev.next = new Node(e,prev.next);
        size++;
    }
}
複製代碼

若是使用head這個變量去維護鏈表頭天然是能夠的,可是咱們看到了,咱們的鏈表在頭部添加元素時,和在其餘位置添加元素的思路是不同的。有沒有辦法可以將鏈表進行優化,使得鏈表的頭部同鏈表的其餘位置在增刪改查的操做一致呢?使用虛擬頭節點就能夠優化鏈表,解決這樣的一個問題。
ide


如上圖所示,咱們在本來的head前使用一個dummyHead這樣的一個變量,讓它指向本來的head,這樣對於咱們來說,鏈表中全部的節點都知足了「有指向它的節點」這樣一個特性。

鏈表的增刪改查

有了dummyHead虛擬頭節點後,鏈表的增刪改查都會變的很是容易。post

向鏈表中添加元素

public void add(int index,E e){
    if(index<0 || index>e)
        throw new IllegalArgumentException("Index is Illegal");
    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    prev.next = new Node(e,prev.next);
    size++;
}
// 在鏈表頭添加新的元素e
public void addFirst(E e){
    add(0,e);
}
// 在鏈表尾添加新的元素e
public void addLast(E e){
    add(size,e);
}
複製代碼

向鏈表中刪除元素



public E remove(int index){
    if(index<0 || index>=size)
        throw new IllegalArgumentException("index is Illegal");
    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    E delNode = prev.next;
    prev.next = prev.next.next; // prev.next = delNode.next;
    delNode.next = null;
    return delNode.e;
}
// 從鏈表中刪除第一個元素,並返回
public E removeFirst(){
    return remove(0);
}
// 從鏈表中刪除最後一個元素,並返回
public E removeLast(){
    return remove(size-1);
}
複製代碼

向鏈表中查詢及修改元素

// 改
public void set(int index,E e){
    if(index<0 || index>=size)
        throw new IllegalArgumentException("Index is Illegal");

    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    prev.next.e = e;
}
// 查
public E get(int index){
    if(index<0 || index>=size)
        throw new IllegalArgumentException("Index is Illegal");
    Node prev = dummyHead;
    for(int i=0;i<index;i++){
        prev = prev.next;
    }
    return prev.next.e;
}
// 得到鏈表的第一個元素
public E getFirst(){
    return get(0);
}
// 得到鏈表的最後一個元素
public E getLast(){
    return get(size-1);
}
複製代碼

代碼連接學習

鏈表的時間複雜度分析

咱們如今來看一下鏈表的增刪改查各個操做的時間複雜度:

  • 向鏈表中添加元素
    與動態數組相反,在動態數組的末尾添加元素的時間複雜度爲O(1),在頭部添加元素則須要將數組總體向後挪動,須要O(n)的時間複雜度。鏈表的添加元素操做中,在鏈表頭添加元素的時間複雜度爲O(1),在鏈表尾部添加元素,須要將鏈表遍歷一遍,因此時間複雜度則爲O(n)。
  • 向鏈表中刪除元素
    在鏈表頭部刪除元素很是簡單,時間複雜度爲O(1)。刪除鏈表尾部仍是須要將鏈表總體遍歷,因此時間複雜度爲O(n)。
  • 鏈表中查詢元素
    鏈表不具有數組的下標索引這種快速查詢的機制,因此對於鏈表來講,查詢元素的時間複雜度爲O(n)。由於只有將鏈表進行遍歷,才能知道鏈表中是否有你想要查詢的元素,對於鏈表來講,查詢元素這個功能是不利的,事實上,也確實如此。選擇了鏈表這種數據結構主要的操做都是在增刪上,而數組這種數據結構則更加適合查詢操做,由於數組的索引特性使得查詢操做爲O(1)的時間複雜度。
  • 鏈表中修改元素
    對於鏈表的修改元素這一操做來講,在鏈表頭操做的時間複雜度爲O(1),在鏈表尾修改元素的時間複雜度則是O(n)。

使用鏈表實現棧與隊列

棧與隊列是兩種特殊的線性數據結構,它們都是基於某種線性數據結構做爲底層進行實現的。動態數組做爲底層能夠實現棧與隊列,而且咱們使得棧這種數據結構的各個操做均爲O(1)的時間複雜度,而隊列在使用數組做爲底層實現時,出隊操做的時間複雜度爲O(n),可是循環隊列則作出了改進,將隊列的各個操做優化至O(1)。咱們再回顧一下棧與隊列的接口方法:
Stack

public interface Stack<E> {
    // 入棧
    void push(E e);
    // 出棧
    E pop();
    // 查看棧頂元素
    E peek();
    int getSize();
    boolean isEmpty();
}
複製代碼

Queue

public interface Queue<E> {
    // 入隊
    void enqueue(E e);
    // 出隊
    E dequeue();
    // 查看隊首的元素
    E getFront();
    int getSize();
    boolean isEmpty();
}
複製代碼

若是將棧與隊列的底層變爲鏈表,那麼如何進行實現呢?

LinkedListStack

對於鏈表來講,在鏈表頭操做元素均爲O(1)的時間複雜度,而棧是一種僅在棧頂進行push與pop的特殊的數據結構。因此咱們的思路很是簡單,將鏈表頭做爲棧頂就可使得棧的相關操做爲O(1)的時間複雜度了,由於代碼比較簡單,因此直接給出連接,再也不敘述:連接

LinkedListQueue

隊列和棧不一樣,由於FIFO的這種特性,就須要在隊列的兩頭進行操做(從一端添加元素,從另外一端刪除元素)。對於數組和鏈表兩種數據結構來講,不管是哪種,在兩端進行操做的時間複雜度必是O(1)和O(n)。對於數組來講,咱們使用了循環隊列這種思想對出隊操做進行優化,對於鏈表也必然有優化的方法,試想一下,在鏈表頭部進行操做的時間複雜度爲O(1),若是在鏈表的尾部也添加一個變量進行維護,那麼每次在添加元素時,只須要讓尾部指向新添加的元素,而且再次讓維護鏈表尾部的這個變量指向最後一個元素不就能夠了嗎?假設維護鏈表尾部的這個變量叫作"tail",在每次向鏈表中添加元素時,咱們只須要tail.next = newNode;tail = newNode就能夠了,這樣在鏈表尾部添加元素就會變爲一個時間複雜度爲O(1)的操做。


而鏈表頭不管是刪除元素仍是添加元素都是O(1),咱們能夠將鏈表尾部變爲隊列尾,將鏈表頭看成隊列頭。 咱們只看入隊操做和出隊操做:

// 鏈表爲底層的隊列:入隊
@Override
public void enqueue(E e){
    Node node = new Node(e);
    if(isEmpty()){
        head = node;
        tail = node;
    }else{
        tail.next = node;
        tail = tail.next;
    }
    size++;
}
複製代碼
// 鏈表爲底層的隊列:出隊
@Override
public E dequeue(){
    if(isEmpty())
        throw new IllegalArgumentException("Queue is Empty");
    Node retNode = head;
    if(head==tail){
        head = null;
        tail = null;
    }else{
        head = head.next;
    }
    size--;
    retNode.next = null;
    return retNode.e;
}
複製代碼

代碼連接