鏈表是很常見的數據結構,由一個個節點組成,每一個節點中儲存着數據和指針(地址引用),指針負責節點間的鏈接。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; } // ... }
鏈表的增刪改查操做。鏈表查找節點須要從頭或者尾部(單向鏈表只能從頭開始)開始查找,刪除或插入節點先查找到節點,而後改變相關節點的指針指向便可。
以雙向鏈表爲例:
僞代碼:
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++; // 記錄長度 }
僞代碼:
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++; }
僞代碼:
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++; }
僞代碼:
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; }
僞代碼:
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; }
僞代碼-按位置刪除:
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; }
僞代碼-按位置索引查找:
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指針算法來解決不少問題,例如:
因爲鏈表長度未知,首先循環鏈表獲得 length,而後再次循環鏈表到length-(N-1) 處獲得元素。可是利用快慢指針來保持固定位置間隔,只須要循環一次鏈表便可查找到元素。
僞代碼:
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
除此以外,還能夠判斷鏈表中是否有環等等問題,快慢指針在面試時可能會被問到,有興趣朋友能夠到網上找些鏈表的算法題。