1. 程式人生 > 其它 >連結串列之LinkedList

連結串列之LinkedList

寫在前面

在日常開發中,一般在對於List的場景,基本上都是通過ArrayList去封裝資料的,而對於連結串列LinkedList相對來說用的比較少。對我而言,好像ArrayList熟練度高一些,所以基本上也很少用LinkedList,也就是在面試的時候去背過八股文。

連結串列:資料分散的儲存在物理空間中,通過一根線儲存著它們之間的邏輯關係,這種儲存結構稱為鏈式儲存結構,簡稱連結串列。

連結串列的前驅和後繼

資料結構中,一組資料中的每個個體被稱為“資料元素”(簡稱“元素”)。
另外,對於具有“一對一”邏輯關係的資料,我們一直在用“某一元素的左側(前邊)或右側(後邊)”這樣不專業的詞,其實線性表中有更準確的術語:

  • 某一元素的左側相鄰元素稱為“直接前驅”,位於此元素左側的所有元素都統稱為“前驅元素”;
  • 某一元素的右側相鄰元素稱為“直接後繼”,位於此元素右側的所有元素都統稱為“後繼元素”;

單鏈表

概念:單鏈表,用於儲存邏輯關係為 "一對一" 的資料。與順序表不同,連結串列不限制資料的物理儲存狀態,換句話說,使用連結串列儲存的資料元素,其物理儲存位置是隨機的。HashMap在1.7就是通過單鏈表來解決hash衝突的。

節點

在上圖中無法提現出元素之間的邏輯關係,對此,連結串列的解決方案是,每個資料元素在儲存時都配備一個指標,用於指向自己的直接後繼元素。表示一個節點如下:

//節點資訊
class Node {
    E data;
    Node next;

    public Node(E element, Node next) {
        this.data = element;
        this.next = next;
    }

    public Node(E data) {
        this.data = data;
    }
}


因此,在連結串列中,每個資料的儲存有以下2部分組成

  • 資料本身,其所在的區域叫做資料域

  • 指向後繼元素的指標,叫指標域

上圖所示的結構在連結串列中稱為節點。也就是說,連結串列實際儲存的是一個一個的節點,真正的資料元素包含在這些節點中

單鏈表---增

 /**
  * 頭部新增節點
  *
  * @param e
  */
 public void add(E e) {
     //頭結點
     Node cur = new Node(e, list);
     list = cur;
     size++;
 }

 /**
   * 指定位置新增節點
   *
   * @param index
   * @param e     0 1 2 3 4
   */
 public void add(int index, E e) {
 	  //越界檢查
     checkElementIndex(index);
     Node preNode = list;
     for (int i = 0; i < index - 1; i++) {
         //找到插入位置的前一個節點
         preNode = preNode.next;
     }
     Node node = new Node(e);
     node.next = preNode.next;
     preNode.next = node;
     size++;
 }

單鏈表---刪

/**
 * 刪除頭部節點
 */
public void remove() {
    if (list != null) {
        Node node = list;
        list = node.next;
        //GC
        node.next = null;
        size--;
    }
}

/**
 * 刪除指定位置節點
 * 1 2 3 4 5
 * 0 1 2 3 4
 *
 * @param index
 */
public void remove(int index) {
    checkElementIndex(index);
    Node preNode = list;
    for (int i = 0; i < index - 1; i++) {
        //找到指定位置元素的前一個節點
        preNode = preNode.next;
    }
    //指定位置的節點
    Node next = preNode.next;
    preNode.next = next.next;
    //GC
    next = null;
    size--;
}

/**
 * 刪除最後一個節點
 */
public void removeLast() {
    if (list != null) {
        //當前節點
        Node cur = list;
        //最後一個節點的前一個節點
        Node preNode = list;
        while (cur.next != null) {
            preNode = cur;
            cur = cur.next;
        }
        preNode = null;//此時cur已經為null
        size--;
    }
}

單鏈表---改

/**
 * 修改指定索引的元素
 *
 * @param index
 * @param e
 */
public void set(int index, E e) {
    checkElementIndex(index);
    Node cur = list;
    for (int i = 0; i < index; i++) {
        cur = cur.next;
    }
    cur.data = e;
}

單鏈表---查

 /**
  * 獲取頭部節點
  */
 public E get() {
     if (list != null) {
         return list.data;
     } else {
         return null;
     }
 }

  /**
   * 獲取指定位置的元素
   *
   * @param index
   * @return
   */
  public E get(int index) {
      checkElementIndex(index);
      Node cur = list;
      for (int i = 0; i < index; i++) {
          cur = cur.next;
      }
      return cur.data;
  }

完整程式碼

/**
 * 單鏈表
 *
 */
public class SingleLinkedList<E> {

    int size = 0;

    /**
     * 指向第一個節點的指標
     */
    Node list;

    /**
     * 頭部新增節點
     *
     * @param e
     */
    public void add(E e) {
        //頭結點
        Node cur = new Node(e, list);
        list = cur;
        size++;
    }

    /**
     * 指定位置新增節點
     *
     * @param index
     * @param e     0 1 2 3 4
     */
    public void add(int index, E e) {
        checkElementIndex(index);
        Node preNode = list;
        for (int i = 0; i < index - 1; i++) {
            //找到插入位置的前一個節點
            preNode = preNode.next;
        }
        Node node = new Node(e);
        node.next = preNode.next;
        preNode.next = node;
        size++;
    }


    /**
     * 刪除頭部節點
     */
    public void remove() {
        if (list != null) {
            Node node = list;
            list = node.next;
            //GC
            node.next = null;
            size--;
        }
    }

    /**
     * 刪除指定位置節點
     * 1 2 3 4 5
     * 0 1 2 3 4
     *
     * @param index
     */
    public void remove(int index) {
        checkElementIndex(index);
        Node preNode = list;
        for (int i = 0; i < index - 1; i++) {
            //找到指定位置元素的前一個節點
            preNode = preNode.next;
        }
        //指定位置的節點
        Node next = preNode.next;
        preNode.next = next.next;
        //GC
        next = null;
        size--;
    }

    /**
     * 刪除最後一個節點
     */
    public void removeLast() {
        if (list != null) {
            //當前節點
            Node cur = list;
            //最後一個節點的前一個節點
            Node preNode = list;
            while (cur.next != null) {
                preNode = cur;
                cur = cur.next;
            }
            preNode = null;//此時cur已經為null
            size--;
        }
    }

    /**
     * 修改指定索引的元素
     *
     * @param index
     * @param e
     */
    public void set(int index, E e) {
        checkElementIndex(index);
        Node cur = list;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        cur.data = e;
    }

    /**
     * 獲取頭部節點
     */
    public E get() {
        if (list != null) {
            return list.data;
        } else {
            return null;
        }
    }

    /**
     * 獲取指定位置的元素
     *
     * @param index
     * @return
     */
    public E get(int index) {
        checkElementIndex(index);
        Node cur = list;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur.data;
    }


    class Node {
        E data;
        Node next;

        public Node(E element, Node next) {
            this.data = element;
            this.next = next;
        }

        public Node(E data) {
            this.data = data;
        }

        public Node() {
        }

        @Override
        public String toString() {
            return "Node{" +
                    "data=" + data +
                    ", next=" + next +
                    '}';
        }
    }

    /**
     * 判斷引數是否為現有元素的索引  即邊界
     *
     * @param index
     */
    private void checkElementIndex(int index) {
        if (!(index >= 0 && index < size))
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    }

    @Override
    public String toString() {
        Node node = list;
        for (int i = 0; i < size; i++) {
            System.out.print(node.data + " ");
            node = node.next;
        }
        return super.toString();
    }

    public static void main(String[] args) {
        SingleLinkedList<Integer> list = new SingleLinkedList<>();
        list.add(5);
        list.add(4);
        list.add(3);
        list.add(2);
        list.add(1);
        list.toString();
        System.out.println();
        list.add(2, 5);
        list.toString();
        list.removeLast();
        System.out.println();
        list.toString();
        list.set(1, 1);
        System.out.println();
        list.toString();
        System.out.println();
        System.out.println(list.get());
    }
}

雙鏈表LinkedList原始碼分析

雙鏈表,指各節點之間的邏輯關係是雙向的。

節點


因此,在雙向連結串列中各節點包含以下 3 部分資訊

  • 指標域:用於指向當前節點的直接前驅節點;
  • 資料域:用於儲存資料元素。
  • 指標域:用於指向當前節點的直接後繼節點;
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;
    }
}

如果理解了單鏈表,雙項鍊表其實也沒有太多的差別,主要在於限制條件。不僅僅是雙向連結串列,還有很多分類,比如靜態連結串列,動態連結串列,迴圈連結串列等等。這裡可以就增刪給出對應的過程,原始碼可以自己去研究研究。

類的繼承關係

雙鏈表---增

public void add(int index, E element) {
    checkPositionIndex(index);
	//如果索引和size相等直接尾部插入
    if (index == size)
    	linkLast(element);
    else
    	linkBefore(element, node(index));
}


/**
 * first 指向第一個節點的指標
 * @param e 要插入的元素
 * @param succ index位置的節點
 *  a1        a3
 * 在a3索引出插入新的元素  a2
 *  a1    a2    a3
 */
void linkBefore(E e, Node<E> succ) {
    // succ:原a3的前置節點a1
    final Node<E> pred = succ.prev;
    //pred(a1) e(a2) succ(a3)形成新的節點
    //即把e(a2)prev指向pred(a1)節點,把e(a2)next指向succ(a3)節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    //把succ(a3)的prev指向新的節點newNode
    succ.prev = newNode;
    //pred為空代表newNode為首節點
    if (pred == null)
    	first = newNode;
    else
    	//a1的next節點由a3變為a2
    	pred.next = newNode;
    size++;
    modCount++;
}

雙鏈表---刪

public E remove(int index) {    checkElementIndex(index);    return unlink(node(index));    }/** * Unlinks non-null node x. */ E unlink(Node<E> x) {     // assert x != null;     //獲取該節點的值     final E element = x.item;     //獲取該節點的next節點     final Node<E> next = x.next;     //獲取該節點的prev節點     final Node<E> prev = x.prev;     //把該節點的前節點的next指向該節點的next節點,並清除該節點的prev指向     if (prev == null) {     	first = next;     } else {     	prev.next = next;     	x.prev = null;     }     //把該節點的next節點的prev指向該節點的prev節點,並清除該節點的next指向     if (next == null) {     	last = prev;     } else {     	next.prev = prev;     	x.next = null;     }     x.item = null;//gc清除     size--;     modCount++;     return element; }

總結

前面講過ArrayList原始碼分析及擴容機制,如果你看了應該知道不管是用ArrayList還是LinkedList主要是看場景,LinkedList增刪快,因為只用調整指向即可,對於ArrayList而言卻要移動整個陣列,但是如果說是在尾部插入的話,使用兩者都可以。而查詢和修改卻要ArrayList只需要知道下標即可,而對於LinkedList卻要通過迴圈查詢。

對於LinkendList,其中還有很多方法,例如addFirst,addLast,remove等,如果你學會了單鏈表,其實雙鏈表也是一樣的,主要在於思維。

參考

拓展延伸

  • ArrayList和LinkedList有什麼區別?
  • 什麼時候用ArrayList,什麼時候用LinkedList?