數據結構之鏈表-動圖演示

鏈表簡介

鏈表是很常見的數據結構,由一個個節點組成,每一個節點中儲存着數據和指針(地址引用),指針負責節點間的鏈接。java

它是一種線性表,線性表有兩種存儲方式:順序存儲和鏈式存儲。鏈表屬於鏈式存儲,順序由元素間的指針決定,元素在內存中非連續存放,且鏈表長度能夠改變。數組是順序存儲的線性表,元素在內存中連續存放的,且數組建立時大小已固定。git

鏈表能夠用來實現棧和隊列數據結構(棧和隊列可理解爲邏輯類數據結構,鏈表屬於存儲類數據結構),實現緩存LRU算法,Java類庫也使用了鏈表(如,LinkedList,LinkedHashMap)等。鏈表的形式有不少,經常使用的有單向鏈表、雙向鏈表、循環鏈表 ...github

單向鏈表

單鏈表中的節點分兩部分,分別是數據(data)和指向下一個節點的地址(next),尾節點(tail)的next指向null。單向鏈表只能從頭至尾一個方向遍歷,查找節點時須要從頭節點(head)開始向下查找。
插入節點首先遍歷查找到插入的位置,而後將當前插入節點的next指向下一節點,上一節點的next指向當前插入節點。刪除節點一樣從頭遍歷找到要刪除的節點,而後將當前刪除節點的上一個節點next指向當前刪除節點的下一個節點。面試

單向鏈表

節點的僞代碼:算法

class Node<E>{
    private E item;
    private Node<E> next; // 若是是尾節點,next指向null
    Node(E data, Node<E> next) {
        this.item = data;
        this.next = next;
    }
    // ...
}

單向循環鏈表

循環鏈表和非循環鏈表基本同樣,區別是首尾節點連在了一塊兒,最後一個節點的next指向頭節點,造成了一個閉環。數組

單向循環鏈表

節點的僞代碼:緩存

class Node<E>{
    private E item;
    // 若是是末尾節點,指向首節點的引用地址
    private Node<E> next;
    Node(E data, Node<E> next) {
        this.item = data;
        this.next = next;
    }
    // ...
}

雙向鏈表

顧名思義,與單向鏈表相比較,雙向鏈表能夠從頭至尾或從尾到頭兩個方向來遍歷數據。雙向鏈表中的節點分三個部分,分別是指向上一個節點的地址(prev)和數據(data)以及指向下一個節點的地址(next),尾節點(tail)節點的next指向null,頭節點(head)的prev指向null。
增長和刪除節點和單向鏈表同理,只是增長了修改prev地址的操做。數據結構

雙向鏈表

節點的僞代碼:this

class Node<E>{
    private E item;
    private Node<E> prev; // 頭節點prev指向null
    private Node<E> next; // 尾節點next指向null
    Node(Node<E> prev, E data, Node<E> next) {
        this.item = data;
        this.prev = prev;
        this.next = next;
    }
   // ... 
}

雙向循環鏈表

尾節點的next指向頭節點,頭結點的prev指向尾節點,首尾節點連在一塊兒造成閉環。spa

雙向循環鏈表

節點的僞代碼:

class Node<E> { 
    private E item;
    // 若是是第一個節點,其一用指向末尾節點
    private Node<E> prev;
    // 若是是末尾結點,指向第一個結點的引用地址,造成一個環形
    private Node<E> next;
    Node(Node<E> prev, E data, Node<E> next) {
        this.item = data;
        this.prev = prev;
       this.next = next;
   }
   // ...
}

鏈表操做

鏈表的增刪改查操做。鏈表查找節點須要從頭或者尾部(單向鏈表只能從頭開始)開始查找,刪除或插入節點先查找到節點,而後改變相關節點的指針指向便可。

以雙向鏈表爲例:

添加節點

  1. 頭部添加節點

添加節點

僞代碼:

Node<E> head;
Node<E> tail;
int size;

// 頭部添加節點
void addHead(E e) {
    Node<E> h = head;
    Node<E> newNode = new Node<>(null, e, h); // (Node<E> prev, E element, Node<E> next)
    head = newNode;
    if(h == null) { // 空鏈表
        tail = newNode;
     } else {
       h.prev = newNode;
    }   
   size++; // 記錄長度
}
  1. 尾部添加節點

添加節點

僞代碼:

void addTail(E e) {
    Node<E> t = tail;
    Node<E> newNode = new Node<>(t, e, null);
    tail = newNode;
    if(t == null) {
        head = newNode;
    } else {
       t.next = newNode;      
    }       
    size++;
}
  1. 按位置插入節點

添加節點

僞代碼:

void add(int index, E element) {
        if (index == size) {
            // 直接在尾部添加節點
        } else {
           // 查找的節點
           Node<E> temp = null;
           if (index < (size >> 1)) {//因爲雙向鏈表,選擇從離index位置最近端查找
                Node<E> x = head;
                for (int i = 0; i < index; i++) {
                    x = x.next;
                }
                temp = x;    
            } else {
                Node<E> x = tail;
                for (int i = size - 1; i > index; i--) {
                    x = x.prev;
                }
                temp = x;   
            }
            
            // 插入節點
            Node<E> pred = temp.prev;
            Node<E> newNode = new Node<>(pred, element, temp);
            temp.prev = newNode;
            if (pred == null) { // 查找到的節點爲Head節點
                head = newNode;
            } else {
                pred.next = newNode;
            }   
        } 
        size++;
    }

刪除節點

  1. 刪除頭部節點

刪除節點

僞代碼:

E removeHead() {
    Node<E> h = head;    
    if (h != null){
        E element = h.item;
        Node<E> next = h.next;
        head = next;
        if (next == null) {
            tail = null;
        } else {
            next.prev = null;
       }   
       size--; // 減小長度
       return element; // 返回刪除元素
    }
    return null;
}
  1. 刪除尾部節點

刪除節點

僞代碼:

E removeTail() {
    Node<E> t = tail;
    if (t != null) {
        E element = t.item;
        Node<E> prev = t.prev;
        tail = prev;
        if (prev == null) {
            head = null;
        } else {
           prev.next = null;
       }
       size--;
       return element;
    }
    return null; 
}
  1. 按節點位置或值刪除

刪除節點

僞代碼-按位置刪除:

E remove(int index) {
    // 根據index查找節點
    Node<E> temp = null;
    if (index < (size >> 1)) {
        Node<E> x = head;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
        temp = x;    
    } else {
        Node<E> x = tail;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        }
        temp = x;   
    }
    // 刪除節點
    E element = temp.item;
    Node<E> next = temp.next;
    Node<E> prev = temp.prev;
    if (prev == null) {
        head = next;
    } else {
        prev.next = next;
        temp.prev = null;
    }
    if (next == null) {
        tail= prev;
    } else {
        next.prev = prev;
        temp.next = null;
    }
    temp.item = null;
    size--; 
    return element;
}

查找節點

  1. 按位置或值查找節點

查找節點

僞代碼-按位置索引查找:

E get(int index) {
  Node<E> temp = null;
  if (index < (size >> 1)) { // 從近的一端開始查找
        Node<E> x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        } 
        temp = x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        } 
        temp = x;
    }
    return temp.item;
}

若是是單向鏈表只能從頭部開始向後查找。

更新節點

更新節點首先查找到節點,而後修改節點data的指針。

具體可參考LinkedList源碼

鏈表實現棧和隊列

棧和隊列是一種對數據存取有嚴格順序要求的線性數據結構,使用鏈表和數組都能實現。下面使用鏈表來實現棧和隊列。

棧只能從一端存取數據,遵循後進先出(LIFO)原則。進出棧的一端稱爲棧頂,另外一封閉端稱爲棧底,數據進入棧稱爲入棧或壓棧,取出數據稱爲出棧或彈棧。

棧

僞代碼 - 基於雙向鏈表實現簡單的「棧」:

class Stack<E> {
  
    // 返回棧頂元素值
    public E peek() {
        Node<E> h = head;
        return (h == null) ? null : h.item;
    }
    
    // 入棧
    public void push(E e) {
        addHead(e); // 在頭部添加節點
    }
    
    // 出棧
    public E pop() {
        // 移除頭部節點並返回值
        return removeHead(); 
    }
    
    // ...
    
    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;
        }
    }  
}

隊列

隊列是從兩端存取數據,而且從一端進,從另外一端出,遵循先進先出(FIFO)原則。隊列進數據一端稱爲隊尾,出數據端稱爲隊頭,數據進隊列稱爲入隊,取出隊列稱爲出隊。

隊列

僞代碼 - 基於鏈表實現「隊列」:

class Queue {
    // 入隊
    public boolean offer(E e) {
        return addTail(e); 
    }
    
    // 出隊
    public E poll() {
        return removeHead();
    }
    
    // 返回頭元素值
    public E peek() {
        Node<E> h = head;
        return (h == null) ? null : h.item;
    }
    
    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;
        }
    }   
}

快慢指針

快慢指針是解決鏈表某些問題的經常使用方法,利用兩個不一樣步頻的指針fast指針和slow指針算法來解決不少問題,例如:

查找未知長度的單向鏈表倒數第N個值

因爲鏈表長度未知,首先循環鏈表獲得 length,而後再次循環鏈表到length-(N-1) 處獲得元素。可是利用快慢指針來保持固定位置間隔,只須要循環一次鏈表便可查找到元素。

查找倒數第N個值

僞代碼:

public E getLastN(int n) {
    Node<E> h = head;
    if (h == null || n < 1) {
        return null;
    }
    Node<E> fast = h; // 快
    Node<E> slow = h; // 慢
    int count = 1;
    while ((fast = fast.next) != null) {
        // 倒數第k個節點與倒數第1個節點相隔 n-1 個位置,所以fast先走 n-1 個位置
        if (count++ > n - 1) {
            slow = slow.next;
        }
    }
    // 鏈表中的元素個數小於 n
    if (count < n) {
        return null;
    }
 
    return slow.item;
}

找到鏈表中間節點值

使快指針移動步頻是慢指針二倍,一次遍歷便可快速找到中間節點。

查找中間節點

僞代碼:

public E getMiddle() {
    Node<E> h = head;
    if (h == null) {
        return null;
    }
    Node<E> fast = h; // 快
    Node<E> slow = h; // 慢
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        // 鏈表長度爲偶數會兩個中間節點,返回第一個
        if (fast != null) {
            slow = slow.next;
        }
    }
    return slow.item;
}

源碼:https://github.com/newobjectcc/code-example/blob/master/basic/Linked.java

除此以外,還能夠判斷鏈表中是否有環等等問題,快慢指針在面試時可能會被問到,有興趣朋友能夠到網上找些鏈表的算法題。

相關文章
相關標籤/搜索