數據結構之鏈表(Linked list)

說明:若是仔細閱讀徹底文後,可能感受有些不統一,這裏先說明下緣由。
鏈表尾引用不統一:在介紹單鏈表時,只有一個鏈表首部的引用(head) 指向第一個節點。你看到後面關於雙鏈表及循環列表時,除了指向第一個節點的引用 還有指向最後一個節點(尾部)的引用。
這樣作主要是, 鏈表設計可能包含尾部的引用,也可能不包含,在最後關於時間複雜度的對比也作了區分。我的也傾向添加尾部引用,但爲了完整性 以及 更好的對比理解有無尾部引用的差別,因此在單鏈表 是沒有尾部引用設計 來實現。單鏈表 總結的比較詳細,每一個關鍵操做 都有代碼及示意圖。
命名不統一是單鏈表時 首部引用使用的head,而雙鏈表直接使用的JDK源碼中的代碼介紹(沒作任何改動) first和last。那時單鏈表已經弄好了,代碼及示意圖改起來比較麻煩,並且我的更喜歡head的命名。
這些徹底不會影響理解的。
 

鏈表概述

鏈表(Linked list)也是一種線性數據結構,經過每一個節點中的指針(引用)依次鏈接 造成的順序存儲。

單鏈表(Singly linked list)

單鏈表中每一個元素(鏈表中稱爲節點,node)包含兩部分:數據部分 和 一個指向下一個節點的引用。最後一個節點指向爲null。
鏈表的入口點稱爲head,它是第一個節點的引用。若是鏈表爲空 則head指向爲null。
示意圖:

雙鏈表(Doubly linked list)

雙鏈表中每一個節點包含3個部分:數據部分 和兩個引用,一個指向下個節點(next) 另外一個指向前一個節點(previous)。
示意圖:

循環鏈表(Circular linked list)

最後一個節點引用 指向 第一個節點(或者head)。
單鏈表和雙鏈表均可以造成循環鏈表,以下示意圖(上面一個是 單循環鏈表,後面一個是 雙循環鏈表):
 

鏈表操做

單鏈表操做

節點定義:定義節點中的數據項 和 下一個節點引用。
//E->anyType object
private static class Node<E> {
    //數據項
    private E data;
    //指向next節點的引用
    private Node<E> next;
 
    //構造
    public Node(E data, Node<E> next) {
        this.data = data;
        this.next = next;
    }
}
head定義:指向 第一個節點(即鏈表首部)的引用,初始化爲null
private Node<E> head = null;

插入

  • 插入鏈表首部

建立一個節點,節點的next指向head的引用,而後讓head指向新建的節點便可。
若head爲null,即當前鏈表爲空,即向鏈表中插入新建的節點, 節點的next指向null。
public void addFirst(E item) {
    head = new Node<E>(item, head);
}
變化以下示意圖
  • 插入目標節點的前面

目標節點數據爲key,在目標節點前插入新的節點(數據爲toInsert)。
具體過程看下面代碼及註釋。
public void insertBefore(E key, E toInsert) {
   //若head爲null,即鏈表爲空,不存在目標節點 直接返回
   if(head == null) return;
   //若head指向的鏈表第一個節點 就是目標節點,即要把新建節點插入鏈表首部
   if(head.data.equals(key)) {
      addFirst(toInsert);
      return;
   }
 
   Node<E> prev = null;
   Node<E> curr = head;
   //curr定義了 指向當前節點,prev指向當前節點的上個節點。一直順序查找,若找到目標節點(數據爲key),則curr指向目標節點,prev指向了目標節點的上個節點。
   while(curr != null && !curr.data.equals(key)) {
      prev = curr;
      curr = curr.next;
   }
   //若curr不爲空,即找到目標節點,在curr和prev之間插入了新節點(數據爲toInsert)。若curr爲null,即沒找到。
   if(curr != null) prev.next = new Node<E>(toInsert, curr);
}
示意圖以下(鏈表不爲空 且 目標節點不是第一個節點):
  • 插入某個節點後面

相比上面(插入某節點的前面)比較容易,只需一個指向當前節點的臨時變量便可。查找到目標節點後,即在以後插入新節點便可。
public void insertAfter(E key, E toInsert) {
   Node<E> curr = head;
   //查找目標節點,找到即curr指向目標節點
   while(curr != null && !curr.data.equals(key)) {
       curr = curr.next;
   }
 
   //curr不爲null,即找到目標節點。在以後插入便可。
   if(curr != null)
       curr.next = new Node<E>(toInsert, curr.next);
}
示意圖:
  • 插入鏈表尾部

若head爲null,則鏈表爲空。插入鏈表首部便可。
若不爲空,使用臨時變量tmp從head依次日後遍歷,當tmp的next爲null時即鏈表尾部,插入到後面便可。
public void addLast(E item) {
   if(head == null) {
       addFirst(item);
   } else {
      Node<E> tmp = head;
      while(tmp.next != null) tmp = tmp.next;
      tmp.next = new Node<E>(item, null);
   }
}

刪除

有點相似插入。
下面以刪除某指定數據的節點爲例。
public void remove(E key) {
   //head爲null,即鏈表爲空
   if(head == null) throw new RuntimeException("linkedlist is null, cannot delete");
 
   //鏈表第一個節點即目標節點,改變head指向下個節點就能夠了。
   if(head.data.equals(key)) {
      head = head.next;
      return;
   }
 
 
   Node<E> prev = null;
   Node<E> curr  = head;
   //查找,若找到即curr指向目標節點,prev指向目標節點的上個節點
   while(curr != null && !curr.data.equals(key)) {
      prev = curr;
      curr = curr.next;
   }
   //curr爲null,即沒找到
   if(curr == null) throw new RuntimeException("cannot find your node, cannot delete");
   //curr不爲null,刪除目標節點(即改變prev的next 指向curr的next便可)
   prev.next = curr.next;
}
示意圖:
注:上述插入或刪除操做中,都有查找或遍歷的過程。所以查找或遍歷不單獨列出。
 
 

雙鏈表操做

在Java中實現的LinkedList類是一個雙鏈表,所以直接使用了JDK源碼說明(只在代碼中添加了註釋,沒作任何修改)。
下面是節點及這兩個引用的代碼:類中定義了兩個引用,分別指向鏈表的首部和尾部,
transient int size = 0;
 
 
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
*            (first.prev == null && first.item != null)
*/
transient Node<E> first;//指向首部(第一個節點)
 
 
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
*            (last.next == null && last.item != null)
*/
transient Node<E> last;//指向尾部(最後一個節點)
 
 
private static class Node<E> {
    //數據項
    E item;
    //指向下個節點
    Node<E> next;
    //指向上個節點
    Node<E> prev;
 
 
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

插入到鏈表首部

具體看下面的代碼註釋和示意圖。
/**
* Links e as first element.
*/
private void linkFirst(E e) {
    //f指向first指向的節點。即f和first同時指向第一個節點。f在方法中標誌 鏈表插入前的第一個節點
    final Node<E> f = first;
    //建立新的節點,節點的prev指向null,next指向f(即鏈表插入前第一個節點)
    final Node<E> newNode = new Node<>(null, e, f);
    //first指向新節點(鏈表首部已改變)
    first = newNode;
    if (f == null)
        //若f爲null,即插入前鏈表是空的,插入到新節點既是開始節點也是結束節點,因此last也指向了它
        last = newNode;
    else
        //若是f不爲空,則讓f(插入前的第一個節點)的prev指向新節點就完成了。
        f.prev = newNode;
    size++;
    modCount++;
}
示意圖:
這個是f指向不爲空的狀況示意圖。
若f爲null,則沒有節點指向新建節點,同時新建節點的next指向的是null, 尾部引用last也會指向新建節點。
詳細點就是:
第一步---紅色:final Node<E> f = first;
第二步---橙色: final Node<E> newNode = new Node<>(null, e, f);
第三步---綠色:first = newNode;
第四步---深藍: f.prev = newNode;

插入到某個元素前面

這裏是目標節點已經找到了後的操做。查找目標節點能夠參考單鏈表中的代碼。
下面代碼:用e新建節點,插入到succ節點以前。
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //這裏把succ稱爲目標節點。succ在這裏是已經查找到並存在的,查找過程可參考單鏈表中相關代碼。
    //用pred指向 目標節點succ的上個節點。
    final Node<E> pred = succ.prev;
    //新建節點,prev指向pred 即新建節點的prev指向目標節點的上個節點,next指向目標節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    //目標節點的prev指向 新建節點
    succ.prev = newNode;
    if (pred == null)
        //若pred爲null 是目標節點上個節點爲null,即鏈表插入前只有一個節點,因此first會指向新建節點。
        first = newNode;
    else
        //若pred不爲null,目標節點的上個節點的next指向新節點
        pred.next = newNode;
    size++;
    modCount++;
}

插入到鏈表尾部

由於有兩個引用first、last分別指向鏈表首部和鏈表尾部。這樣尾部操做就變得和首部操做同樣容易,不須要找到鏈表尾部才能插入。看完上面這個應該很簡單,不作解釋了。
/**
* Links e as last element.
*/
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

刪除

源碼中,首先查找到數據與o相同的第一個節點,在經過unlink刪除該節點,並返回狀態。
查找那考慮了o是否爲null,很嚴謹。o==null?get(i)==null:o.equals(get(i))。jdk文檔不少地方說明均可以看到,若是null去執行equals,就會出現Null pointer access:的異常了,值得注意。
/**
* Removes the first occurrence of the specified element from this list,
* if it is present.  If this list does not contain the element, it is
* unchanged.  More formally, removes the element with the lowest index
* {@code i} such that
* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
* (if such an element exists).  Returns {@code true} if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return {@code true} if this list contained the specified element
*/
public boolean remove(Object o) {
    //這裏就是區分o是否爲null,找到第一個指定element的節點,經過unlink刪除。
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
 
 
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
    
    if (prev == null) {
        //prev爲null 即x是首部(第一個節點),first指向next便可
        first = next;
    } else {
        //prev不爲null,x與prev之間關聯即斷開。prev的next指向next了,x的prev爲null
        prev.next = next;
        x.prev = null;
    }
 
    //同prev處理相似,斷開x與next聯繫
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
 
 
    x.item = null;
    size--;
    modCount++;
    return element;
}
示意圖是prev和next都不爲null的狀況:
橙色--->prev.next = next; x.prev = null;
紅色---> next.prev = prev; x.next = null;
最後x.item E也被置爲null,size--。x被刪除。
 
 

循環鏈表

循環鏈表的最後一個節點有指向第一個節點的引用。
僅已單循環鏈表爲例。
下面是幾個操做示例,能夠看下代碼和註釋,不作詳細說明了。看過上面應該很容易理解。
private Node<E> head = null;
private Node<E> tail = null;
 
 
private class Node<E> {
    E value;
    Node<E> next;
    public Node(E value) {
        this.value = value;
    }
}
 
 
//往尾部(即tail節點以後)添加節點
public void addNode(E value) {
    Node<E> newNode = new Node<>(value);
    if (head == null) {
        //head爲null,即鏈表爲空,head指向新建節點。
        head = newNode;
    } else {
        tail.next = newNode;
    }
    //tail指向新節點,即新節點至關於鏈表的尾部
    tail = newNode;
    //tail.next即新節點next指向head,造成環
    tail.next = head;
}
 
 
//查找,鏈表中是否包含數據爲searchValue的節點
public boolean containsNode(E searchValue) {
    Node<E> currentNode = head;
    if (head == null) {
        return false;
    } else {
        //以head開始依次向後查找,直到碰到的還是head中止。找到返回
        do {
            if (currentNode.value == searchValue) {
                return true;
            }
            currentNode = currentNode.next;
        } while (currentNode != head);
        return false;
    }
}
 
 
//刪除查找到的第一個數據爲valueToDelete的節點
public void deleteNode(E valueToDelete) {
    Node<E> currentNode = head;
    if (head != null) {
        if (currentNode.value == valueToDelete) {
            //head節點且值爲valueToDelete
            head = head.next;
            tail.next = head;
        } else {
            //以head開始依次向後查找,直到碰到的還是head中止。找到刪除
            do {
                Node<E> next = currentNode.next;
                if (next.value == valueToDelete) {
                    currentNode.next = next.next;
                    break;
                }
                currentNode = currentNode.next;
            } while (currentNode != head);
        }
    }
}
 

鏈表與數組比較

簡單作了個表格node

鏈表
數組
動態分配:須要時才分配內存
固定分配:大小固定,new時即分配全部內存
分散存儲:內存中不連續存儲
連續存儲:內存中連續存儲
總量限制:因爲分散存儲,受內存總量限制
使用限制:因爲連續存儲,若無合適連續控件即沒法完成分配,且容易造成內存碎片
插入/刪除方便:改變節點中指向next的或者prev的引用便可
插入/刪除代價大:需建立新數組並移動元素
有內存浪費:節點中需額外存儲next或prev的信息
無內存浪費:數組元素只存放數據
順序訪問:在某個節點沿某一方向只能逐一訪問
隨機訪問:能夠直接計算獲得某一元素的地址
 
 

鏈表的一些操做複雜度

 
操做
鏈表
數組
訪問(訪問第N個元素)
O(n)
O(1)
插入到首部
O(1)
O(n)
插入到尾部
有尾部引用tail:O(1)
無尾部引用O(n)
O(n)
插入到中部
查找時間+O(1)~=O(n)
O(n)
查找
O(n)
O(n)
刪除
相似插入,看刪除位置或者鏈表的設計
O(n)
相關文章
相關標籤/搜索